diff --git a/.github/workflows/applications.yml b/.github/workflows/applications.yml
index a78dbcd51..aab90cad8 100644
--- a/.github/workflows/applications.yml
+++ b/.github/workflows/applications.yml
@@ -20,14 +20,15 @@ jobs:
- angular
- vue-v3
- react
- - react-swc
- react-ts
+ - react-swc
- react-swc-ts
+ - nextjs
+ - nextjs-ts
NODE:
- 18
OS:
- ubuntu-latest
- - windows-latest
runs-on: ${{ matrix.OS }}
env:
diff --git a/.github/workflows/check-nextjs.yml b/.github/workflows/check-nextjs.yml
new file mode 100644
index 000000000..f3c5071ee
--- /dev/null
+++ b/.github/workflows/check-nextjs.yml
@@ -0,0 +1,94 @@
+name: Check "add devextreme-react" for NextJS app
+
+on:
+ push:
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ nextjs-devextreme-test:
+ strategy:
+ fail-fast: false
+ matrix:
+ TYPESCRIPT: [true, false]
+ SRC_DIR: [true, false]
+ APP_ROUTER: [true, false]
+ NODE:
+ - 18
+ OS:
+ - ubuntu-latest
+
+ runs-on: ${{ matrix.OS }}
+ name: Next.js + DevExtreme (TS:${{ matrix.TYPESCRIPT }}, src:${{ matrix.SRC_DIR }}, app-router:${{ matrix.APP_ROUTER }}), node ${{ matrix.NODE }}, ${{ matrix.OS }}
+
+ steps:
+ - name: Get sources
+ uses: actions/checkout@v4
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.NODE }}
+ cache: 'npm'
+
+ - name: Extract create-next-app version
+ run: |
+ NEXT_APP_VERSION=$(node -e "const versions = require('./packages/devextreme-cli/src/utility/latest-versions.js'); console.log(versions['create-next-app'])")
+ echo "Using create-next-app version: $NEXT_APP_VERSION"
+ echo "NEXT_APP_VERSION=$NEXT_APP_VERSION" >> $GITHUB_ENV
+ shell: bash
+
+ - name: Create Next.js application
+ run: |
+ npx create-next-app@${{ env.NEXT_APP_VERSION }} test-nextjs-app \
+ --typescript=${{ matrix.TYPESCRIPT }} \
+ --src-dir=${{ matrix.SRC_DIR }} \
+ --app=${{ matrix.APP_ROUTER }} \
+ --eslint \
+ --no-tailwind \
+ --import-alias="@/*" \
+ --no-git \
+ --use-npm
+ shell: bash
+
+ - name: Add actual devExtreme-cli
+ run: |
+ cd test-nextjs-app
+ npm add devextreme-cli
+ rm -r ./node_modules/devextreme-cli/src/
+ cp -r ../packages/devextreme-cli/src/ ./node_modules/devextreme-cli/
+ ls ./node_modules/devextreme-cli
+ ls ./node_modules/devextreme-cli/src
+ shell: bash
+ timeout-minutes: 15
+
+ - name: Add DevExtreme to Next.js application
+ run: |
+ cd test-nextjs-app
+ npx devextreme-cli add devextreme-react
+ shell: bash
+ timeout-minutes: 15
+
+ - name: Verify DevExtreme dependencies in package.json
+ run: |
+ cd test-nextjs-app
+
+ if ! grep -q '"devextreme":' package.json; then
+ echo "Error: devextreme dependency not found in package.json"
+ exit 1
+ fi
+
+ if ! grep -q '"devextreme-react":' package.json; then
+ echo "Error: devextreme-react dependency not found in package.json"
+ exit 1
+ fi
+
+ echo "DevExtreme dependencies successfully installed"
+ shell: bash
+
+ - name: Build Next.js application
+ run: |
+ cd test-nextjs-app
+ npm run build
+ shell: bash
+ timeout-minutes: 15
diff --git a/packages/devextreme-cli/package-lock.json b/packages/devextreme-cli/package-lock.json
index 604e9d2b5..f4043a38f 100644
--- a/packages/devextreme-cli/package-lock.json
+++ b/packages/devextreme-cli/package-lock.json
@@ -46,7 +46,8 @@
"tree-kill": "^1.2.2",
"tree-kill-promise": "^1.0.12",
"typescript": "^4.0.2",
- "typescript-eslint-parser": "^22.0.0"
+ "typescript-eslint-parser": "^22.0.0",
+ "wait-on": "8.0.0"
},
"engines": {
"node": ">12.6.0",
@@ -642,6 +643,23 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@hapi/hoek": {
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
+ "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@hapi/topo": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
+ "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^9.0.0"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
@@ -1703,6 +1721,30 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/@sideway/address": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
+ "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^9.0.0"
+ }
+ },
+ "node_modules/@sideway/formula": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
+ "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@sideway/pinpoint": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
+ "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -2265,6 +2307,13 @@
"node": ">=8"
}
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -2281,6 +2330,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/axios": {
+ "version": "1.8.4",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
+ "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/babel-eslint": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz",
@@ -2764,6 +2825,19 @@
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -3061,6 +3135,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
@@ -3312,15 +3396,16 @@
}
},
"node_modules/es-set-tostringtag": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
- "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "get-intrinsic": "^1.2.4",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
- "hasown": "^2.0.1"
+ "hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -4355,6 +4440,27 @@
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
"dev": true
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
"node_modules/for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -4365,6 +4471,22 @@
"is-callable": "^1.1.3"
}
},
+ "node_modules/form-data": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
+ "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -7073,6 +7195,20 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
+ "node_modules/joi": {
+ "version": "17.13.3",
+ "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
+ "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@hapi/hoek": "^9.3.0",
+ "@hapi/topo": "^5.1.0",
+ "@sideway/address": "^4.1.5",
+ "@sideway/formula": "^3.0.1",
+ "@sideway/pinpoint": "^2.0.0"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -7369,6 +7505,29 @@
"node": ">=4"
}
},
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
@@ -7939,6 +8098,13 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
@@ -8182,6 +8348,23 @@
"queue-microtask": "^1.2.2"
}
},
+ "node_modules/rxjs": {
+ "version": "7.8.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/rxjs/node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD"
+ },
"node_modules/safe-array-concat": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
@@ -9165,6 +9348,26 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/wait-on": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.0.tgz",
+ "integrity": "sha512-fNE5SXinLr2Bt7cJvjvLg2PcXfqznlqRvtE3f8AqYdRZ9BhE+XpsCp1mwQbRoO7s1q7uhAuCw0Ro3mG/KdZjEw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "axios": "^1.7.4",
+ "joi": "^17.13.3",
+ "lodash": "^4.17.21",
+ "minimist": "^1.2.8",
+ "rxjs": "^7.8.1"
+ },
+ "bin": {
+ "wait-on": "bin/wait-on"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/walker": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
@@ -9848,6 +10051,21 @@
}
}
},
+ "@hapi/hoek": {
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
+ "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==",
+ "dev": true
+ },
+ "@hapi/topo": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
+ "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
+ "dev": true,
+ "requires": {
+ "@hapi/hoek": "^9.0.0"
+ }
+ },
"@humanwhocodes/config-array": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
@@ -10535,6 +10753,27 @@
"integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==",
"optional": true
},
+ "@sideway/address": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
+ "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==",
+ "dev": true,
+ "requires": {
+ "@hapi/hoek": "^9.0.0"
+ }
+ },
+ "@sideway/formula": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
+ "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
+ "dev": true
+ },
+ "@sideway/pinpoint": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
+ "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
+ "dev": true
+ },
"@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -10939,6 +11178,12 @@
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
"dev": true
},
+ "asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true
+ },
"available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -10948,6 +11193,17 @@
"possible-typed-array-names": "^1.0.0"
}
},
+ "axios": {
+ "version": "1.8.4",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
+ "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
+ "dev": true,
+ "requires": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"babel-eslint": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz",
@@ -11289,6 +11545,15 @@
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"dev": true
},
+ "combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "requires": {
+ "delayed-stream": "~1.0.0"
+ }
+ },
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -11485,6 +11750,12 @@
"object-keys": "^1.1.1"
}
},
+ "delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true
+ },
"detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
@@ -11678,14 +11949,15 @@
}
},
"es-set-tostringtag": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
- "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"requires": {
- "get-intrinsic": "^1.2.4",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
- "hasown": "^2.0.1"
+ "hasown": "^2.0.2"
}
},
"es-shim-unscopables": {
@@ -12425,6 +12697,12 @@
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
"dev": true
},
+ "follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "dev": true
+ },
"for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -12434,6 +12712,18 @@
"is-callable": "^1.1.3"
}
},
+ "form-data": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
+ "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
+ "dev": true,
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "mime-types": "^2.1.12"
+ }
+ },
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -14339,6 +14629,19 @@
}
}
},
+ "joi": {
+ "version": "17.13.3",
+ "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
+ "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
+ "dev": true,
+ "requires": {
+ "@hapi/hoek": "^9.3.0",
+ "@hapi/topo": "^5.1.0",
+ "@sideway/address": "^4.1.5",
+ "@sideway/formula": "^3.0.1",
+ "@sideway/pinpoint": "^2.0.0"
+ }
+ },
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -14564,6 +14867,21 @@
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"optional": true
},
+ "mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true
+ },
+ "mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "requires": {
+ "mime-db": "1.52.0"
+ }
+ },
"mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
@@ -14975,6 +15293,12 @@
}
}
},
+ "proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "dev": true
+ },
"prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
@@ -15120,6 +15444,23 @@
"queue-microtask": "^1.2.2"
}
},
+ "rxjs": {
+ "version": "7.8.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+ "dev": true,
+ "requires": {
+ "tslib": "^2.1.0"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true
+ }
+ }
+ },
"safe-array-concat": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
@@ -15813,6 +16154,19 @@
}
}
},
+ "wait-on": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.0.tgz",
+ "integrity": "sha512-fNE5SXinLr2Bt7cJvjvLg2PcXfqznlqRvtE3f8AqYdRZ9BhE+XpsCp1mwQbRoO7s1q7uhAuCw0Ro3mG/KdZjEw==",
+ "dev": true,
+ "requires": {
+ "axios": "^1.7.4",
+ "joi": "^17.13.3",
+ "lodash": "^4.17.21",
+ "minimist": "^1.2.8",
+ "rxjs": "^7.8.1"
+ }
+ },
"walker": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
diff --git a/packages/devextreme-cli/package.json b/packages/devextreme-cli/package.json
index 5a82a2df7..2433b20bf 100644
--- a/packages/devextreme-cli/package.json
+++ b/packages/devextreme-cli/package.json
@@ -71,6 +71,7 @@
"tree-kill": "^1.2.2",
"tree-kill-promise": "^1.0.12",
"typescript": "^4.0.2",
- "typescript-eslint-parser": "^22.0.0"
+ "typescript-eslint-parser": "^22.0.0",
+ "wait-on": "8.0.0"
}
}
diff --git a/packages/devextreme-cli/src/application.js b/packages/devextreme-cli/src/application.js
index 35a64a81d..22186055a 100644
--- a/packages/devextreme-cli/src/application.js
+++ b/packages/devextreme-cli/src/application.js
@@ -1,5 +1,6 @@
const angularApplication = require('./applications/application.angular');
const reactApplication = require('./applications/application.react');
+const nextjsApplication = require('./applications/application.nextjs');
const vueApplication = require('./applications/application.vue');
const printHelp = require('./help').printHelp;
@@ -25,6 +26,9 @@ const run = async(commands, options, devextremeConfig) => {
case 'react-app':
await reactApplication.create(appName, options);
return;
+ case 'nextjs-app':
+ await nextjsApplication.create(appName, options);
+ return;
case 'vue-app':
await vueApplication.create(appName, options);
return;
@@ -40,7 +44,12 @@ const run = async(commands, options, devextremeConfig) => {
}
if(commands[1] === 'devextreme-react') {
- reactApplication.install(options);
+ if(nextjsApplication.isNextJsApp()) {
+ nextjsApplication.install(options);
+ } else {
+ reactApplication.install(options);
+ }
+
return;
}
@@ -54,23 +63,16 @@ const run = async(commands, options, devextremeConfig) => {
return;
}
- if(devextremeConfig.applicationEngine === 'angular') {
- if(commands[1] === 'view') {
- angularApplication.addView(commands[2], options);
- } else {
- console.error('Invalid command');
- printHelp(commands[0]);
- }
- } else if(devextremeConfig.applicationEngine === 'react') {
- if(commands[1] === 'view') {
- reactApplication.addView(commands[2], options);
- } else {
- console.error('Invalid command');
- printHelp(commands[0]);
- }
- } else if(devextremeConfig.applicationEngine === 'vue') {
+ const app = {
+ 'angular': angularApplication,
+ 'react': reactApplication,
+ 'nextjs': nextjsApplication,
+ 'vue': vueApplication,
+ }[devextremeConfig.applicationEngine];
+
+ if(app) {
if(commands[1] === 'view') {
- vueApplication.addView(commands[2], options);
+ app.addView(commands[2], options);
} else {
console.error('Invalid command');
printHelp(commands[0]);
diff --git a/packages/devextreme-cli/src/applications/application.nextjs.js b/packages/devextreme-cli/src/applications/application.nextjs.js
new file mode 100644
index 000000000..de7b9fd63
--- /dev/null
+++ b/packages/devextreme-cli/src/applications/application.nextjs.js
@@ -0,0 +1,231 @@
+const runCommand = require('../utility/run-command');
+const path = require('path');
+const fs = require('fs');
+const getLayoutInfo = require('../utility/prompts/layout');
+const getTemplateTypeInfo = require('../utility/prompts/typescript');
+const templateCreator = require('../utility/template-creator');
+const packageManager = require('../utility/package-manager');
+const packageJsonUtils = require('../utility/package-json-utils');
+const insertItemToArray = require('../utility/file-content').insertItemToArray;
+const stringUtils = require('../utility/string');
+const typescriptUtils = require('../utility/typescript-extension');
+const removeFile = require('../utility/file-operations').remove;
+const latestVersions = require('../utility/latest-versions');
+const { extractDepsVersionTag } = require('../utility/extract-deps-version-tag');
+const {
+ updateJsonPropName,
+ bumpReact,
+ getCorrectPath,
+ addStylesToApp,
+ getComponentPageName,
+} = require('./application.react');
+
+const defaultStyles = [
+ 'devextreme/dist/css/dx.light.css'
+];
+
+const isNextJsApp = () => {
+ const appPath = process.cwd();
+
+ return fs.existsSync(path.join(appPath, 'next.config.ts')) || fs.existsSync(path.join(appPath, 'next.config.mjs'));
+};
+
+const isTsApp = (appPath) => {
+ return fs.existsSync(path.join(appPath, 'next.config.ts'));
+};
+
+const getExtension = (appPath) => {
+ return fs.existsSync(path.join(appPath, 'src/app', 'layout.tsx')) ? '.tsx' : '.jsx';
+};
+
+const pathToPagesIndex = () => {
+ const extension = getExtension(process.cwd());
+ return path.join(process.cwd(), 'src', 'views', `index${extension}`);
+};
+
+const preparePackageJsonForTemplate = (appPath, appName) => {
+ const dependencies = [
+ { name: 'devextreme-cli', version: latestVersions['devextreme-cli'], dev: true },
+ { name: 'jose', version: latestVersions['jose'] },
+ ];
+ const scripts = [
+ { name: 'build-themes', value: 'devextreme build' },
+ { name: 'postinstall', value: 'npm run build-themes' }
+ ];
+
+ packageJsonUtils.addDependencies(appPath, dependencies);
+ packageJsonUtils.updateScripts(appPath, scripts);
+ packageJsonUtils.updateName(appPath, appName);
+};
+
+const create = async(appName, options) => {
+ const templateType = await getTemplateTypeInfo(options.template);
+ const layoutType = await getLayoutInfo(options.layout);
+
+ const templateOptions = Object.assign({}, options, {
+ project: stringUtils.humanize(appName),
+ layout: stringUtils.classify(layoutType),
+ isTypeScript: typescriptUtils.isTypeScript(templateType)
+ });
+ const depsVersionTag = extractDepsVersionTag(options);
+
+ let commandArguments = [`-p=create-next-app@${depsVersionTag || latestVersions['create-next-app']}`, 'create-next-app', appName];
+
+ commandArguments = [
+ ...commandArguments,
+ `${templateOptions.isTypeScript ? '--typescript' : '--javascript'}`,
+ '--eslint',
+ '--no-tailwind',
+ '--src-dir',
+ '--app',
+ '--no-turbopack',
+ '--import-alias "@/*"',
+ ];
+
+ await runCommand('npx', commandArguments);
+
+ const appPath = path.join(process.cwd(), appName);
+
+ if(depsVersionTag) {
+ bumpReact(appPath, depsVersionTag, templateOptions.isTypeScript);
+ }
+
+ addTemplate(appPath, appName, templateOptions);
+ modifyAppFiles(appPath, templateOptions);
+};
+
+const modifyAppFiles = (appPath, { project, isTypeScript }) => {
+ const entryFilePath = path.join(appPath, `src/app/layout.${isTypeScript ? 'tsx' : 'jsx'}`);
+
+ let content = fs.readFileSync(entryFilePath).toString();
+ content = content.replace(/
[^<]+<\/title>/, `${project}<\/title>`);
+
+ fs.writeFileSync(entryFilePath, content);
+};
+
+const addTemplate = (appPath, appName, templateOptions) => {
+ const applicationTemplatePath = path.join(
+ templateCreator.getTempaltePath('nextjs'),
+ 'application'
+ );
+
+ const manifestPath = path.join(appPath, 'public', 'manifest.json');
+
+ const styles = [
+ '../dx-styles.scss',
+ '../themes/generated/theme.additional.css',
+ '../themes/generated/theme.additional.dark.css',
+ '../themes/generated/theme.base.css',
+ '../themes/generated/theme.base.dark.css',
+ 'devextreme/dist/css/dx.common.css'
+ ];
+
+ templateCreator.moveTemplateFilesToProject(applicationTemplatePath, appPath, templateOptions, getCorrectPath);
+
+ !templateOptions.isTypeScript && removeFile(path.join(appPath, 'src', 'types.jsx'));
+ removeFile(path.join(appPath, 'src/app', 'page.js'));
+ removeFile(path.join(appPath, 'src/app', 'layout.js'));
+ removeFile(path.join(appPath, 'src/app', 'globals.scss'));
+
+ if(!templateOptions.empty) {
+ addSamplePages(appPath, templateOptions);
+ }
+
+ preparePackageJsonForTemplate(appPath, appName, templateOptions.isTypeScript);
+ updateJsonPropName(manifestPath, appName);
+ install({ isTypeScript: templateOptions.isTypeScript }, appPath, styles);
+};
+
+const getEntryFilePath = (options, appPath) => {
+ const extension = options.isTypeScript || isTsApp(appPath) ? 'ts' : 'js';
+ const srcFolder = fs.existsSync(path.join(appPath, 'src')) ? 'src' : '';
+ const isAppRouterApp = fs.existsSync(path.join(appPath, srcFolder, 'app')) && fs.lstatSync(appPath).isDirectory();
+
+ const entryFilePath = isAppRouterApp
+ ? path.join('app', `layout.${extension}`)
+ : path.join('pages', `_app.${extension}`);
+
+ const jsx = fs.existsSync(path.join(appPath, srcFolder, entryFilePath + 'x')) ? 'x' : '';
+
+ return path.join(srcFolder, entryFilePath + jsx);
+};
+
+const install = (options, appPath, styles) => {
+ appPath = appPath ? appPath : process.cwd();
+
+ const pathToMainComponent = path.join(appPath, getEntryFilePath(options, appPath));
+
+ addStylesToApp(pathToMainComponent, styles || defaultStyles);
+ packageJsonUtils.addDevextreme(appPath, options.dxversion, 'react');
+
+ packageManager.runInstall({ cwd: appPath });
+};
+
+const getNavigationData = (viewName, componentName, icon) => {
+ const pagePath = stringUtils.dasherize(viewName);
+ return {
+ navigation: `\n {\n text: \'${stringUtils.humanize(viewName)}\',\n path: \'/pages/${pagePath}\',\n icon: \'${icon}\'\n }`
+ };
+};
+
+const createPathToPage = (pageName) => {
+ const pagesPath = path.join(process.cwd(), 'src', 'app/pages');
+ const newPageFolderPath = path.join(pagesPath, pageName);
+
+ if(!fs.existsSync(pagesPath)) {
+ fs.mkdirSync(pagesPath);
+ fs.writeFileSync(pathToPagesIndex(), '');
+ }
+
+ if(!fs.existsSync(newPageFolderPath)) {
+ fs.mkdirSync(newPageFolderPath);
+ }
+
+ return newPageFolderPath;
+};
+
+const addSamplePages = (appPath, templateOptions) => {
+ const samplePageTemplatePath = path.join(
+ templateCreator.getTempaltePath('nextjs'),
+ 'sample-pages'
+ );
+
+ const pagesPath = path.join(appPath, 'src', 'app/pages');
+
+ templateCreator.moveTemplateFilesToProject(samplePageTemplatePath, pagesPath, {
+ isTypeScript: templateOptions.isTypeScript
+ }, getCorrectPath);
+};
+
+const addView = (pageName, options) => {
+ const pageTemplatePath = path.join(
+ templateCreator.getTempaltePath('nextjs'),
+ 'page'
+ );
+ const extension = getExtension(process.cwd());
+
+ const componentName = getComponentPageName(pageName);
+ const pathToPage = createPathToPage(pageName);
+ const navigationModulePath = path.join(process.cwd(), 'src', `app-navigation${extension}`);
+ const navigationData = getNavigationData(pageName, componentName, options && options.icon || 'folder');
+
+ const getCorrectExtension = (fileExtension) => {
+ return fileExtension === '.tsx' ? extension : fileExtension;
+ };
+
+ const getPageFileName = (pageName, pageItem) => {
+ return pageItem === 'page.tsx' ? 'page' : pageName;
+ };
+
+ templateCreator.addPageToApp(pageName, pathToPage, pageTemplatePath, getCorrectExtension, { getPageFileName });
+
+ insertItemToArray(navigationModulePath, navigationData.navigation);
+};
+
+module.exports = {
+ isNextJsApp,
+ install,
+ create,
+ addTemplate,
+ addView
+};
diff --git a/packages/devextreme-cli/src/applications/application.react.js b/packages/devextreme-cli/src/applications/application.react.js
index 43330a1c1..1b3f8c36c 100644
--- a/packages/devextreme-cli/src/applications/application.react.js
+++ b/packages/devextreme-cli/src/applications/application.react.js
@@ -52,14 +52,19 @@ const updateJsonPropName = (path, name) => {
});
};
-const bumpReact = (appPath, versionTag) => {
+const bumpReact = (appPath, versionTag, isTypeScript) => {
const dependencies = [
{ name: 'react', version: versionTag },
{ name: 'react-dom', version: versionTag },
- { name: '@types/react', version: versionTag, dev: true },
- { name: '@types/react-dom', version: versionTag, dev: true },
];
+ if(isTypeScript) {
+ dependencies.push(
+ { name: '@types/react', version: versionTag, dev: true },
+ { name: '@types/react-dom', version: versionTag, dev: true },
+ );
+ }
+
packageJsonUtils.addDependencies(appPath, dependencies);
};
@@ -86,7 +91,7 @@ const create = async(appName, options) => {
modifyIndexHtml(appPath, templateOptions.project);
if(depsVersionTag) {
- bumpReact(appPath, depsVersionTag);
+ bumpReact(appPath, depsVersionTag, templateOptions.isTypeScript);
}
addTemplate(appPath, appName, templateOptions);
@@ -219,5 +224,10 @@ module.exports = {
install,
create,
addTemplate,
- addView
+ addView,
+ updateJsonPropName,
+ bumpReact,
+ getCorrectPath,
+ addStylesToApp,
+ getComponentPageName,
};
diff --git a/packages/devextreme-cli/src/templates/nextjs/application/.env b/packages/devextreme-cli/src/templates/nextjs/application/.env
new file mode 100644
index 000000000..1d70c522d
--- /dev/null
+++ b/packages/devextreme-cli/src/templates/nextjs/application/.env
@@ -0,0 +1 @@
+SESSION_SECRET=
\ No newline at end of file
diff --git a/packages/devextreme-cli/src/templates/nextjs/application/devextreme.json b/packages/devextreme-cli/src/templates/nextjs/application/devextreme.json
new file mode 100644
index 000000000..b6bf64dac
--- /dev/null
+++ b/packages/devextreme-cli/src/templates/nextjs/application/devextreme.json
@@ -0,0 +1,63 @@
+{
+ "applicationEngine": "nextjs",
+ "build": {
+ "commands": [
+ {
+ "command": "build-theme",
+ "options": {
+ "inputFile": "src/themes/metadata.base.json",
+ "outputFile": "src/themes/generated/theme.base.css"
+ }
+ },
+ {
+ "command": "build-theme",
+ "options": {
+ "inputFile": "src/themes/metadata.base.dark.json",
+ "outputFile": "src/themes/generated/theme.base.dark.css"
+ }
+ },
+ {
+ "command": "build-theme",
+ "options": {
+ "inputFile": "src/themes/metadata.additional.json",
+ "outputFile": "src/themes/generated/theme.additional.css"
+ }
+ },
+ {
+ "command": "build-theme",
+ "options": {
+ "inputFile": "src/themes/metadata.additional.dark.json",
+ "outputFile": "src/themes/generated/theme.additional.dark.css"
+ }
+ },
+ {
+ "command": "export-theme-vars",
+ "options": {
+ "inputFile": "src/themes/metadata.base.json",
+ "outputFile": "src/themes/generated/variables.base.scss"
+ }
+ },
+ {
+ "command": "export-theme-vars",
+ "options": {
+ "inputFile": "src/themes/metadata.base.dark.json",
+ "outputFile": "src/themes/generated/variables.base.dark.scss"
+ }
+ },
+ {
+ "command": "export-theme-vars",
+ "options": {
+ "inputFile": "src/themes/metadata.additional.json",
+ "outputFile": "src/themes/generated/variables.additional.scss"
+ }
+ },
+ {
+ "command": "export-theme-vars",
+ "options": {
+ "inputFile": "src/themes/metadata.additional.dark.json",
+ "outputFile": "src/themes/generated/variables.additional.dark.scss"
+ }
+ }
+ ]
+ }
+}
diff --git a/packages/devextreme-cli/src/templates/nextjs/application/next.config.mjs b/packages/devextreme-cli/src/templates/nextjs/application/next.config.mjs
new file mode 100644
index 000000000..377a4d1eb
--- /dev/null
+++ b/packages/devextreme-cli/src/templates/nextjs/application/next.config.mjs
@@ -0,0 +1,32 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ async redirects() {
+ return [
+ {
+ source: '/',
+ destination: '/pages/home',
+ permanent: true,
+ },
+ {
+ source: '/login',
+ destination: '/auth/login',
+ permanent: true,
+ },
+ {
+ source: '/reset-password',
+ destination: '/auth/reset-password',
+ permanent: true,
+ },
+ {
+ source: '/create-account',
+ destination: '/auth/create-account',
+ permanent: true,
+ },
+ ]
+ },
+ images: {
+ remotePatterns: [new URL('https://js.devexpress.com/**')]
+ },
+}
+
+export default nextConfig;
diff --git a/packages/devextreme-cli/src/templates/nextjs/application/public/logo192.png b/packages/devextreme-cli/src/templates/nextjs/application/public/logo192.png
new file mode 100644
index 000000000..fc44b0a37
Binary files /dev/null and b/packages/devextreme-cli/src/templates/nextjs/application/public/logo192.png differ
diff --git a/packages/devextreme-cli/src/templates/nextjs/application/public/logo512.png b/packages/devextreme-cli/src/templates/nextjs/application/public/logo512.png
new file mode 100644
index 000000000..a4e47a654
Binary files /dev/null and b/packages/devextreme-cli/src/templates/nextjs/application/public/logo512.png differ
diff --git a/packages/devextreme-cli/src/templates/nextjs/application/public/manifest.json b/packages/devextreme-cli/src/templates/nextjs/application/public/manifest.json
new file mode 100644
index 000000000..080d6c77a
--- /dev/null
+++ b/packages/devextreme-cli/src/templates/nextjs/application/public/manifest.json
@@ -0,0 +1,25 @@
+{
+ "short_name": "React App",
+ "name": "Create React App Sample",
+ "icons": [
+ {
+ "src": "favicon.ico",
+ "sizes": "64x64 32x32 24x24 16x16",
+ "type": "image/x-icon"
+ },
+ {
+ "src": "logo192.png",
+ "type": "image/png",
+ "sizes": "192x192"
+ },
+ {
+ "src": "logo512.png",
+ "type": "image/png",
+ "sizes": "512x512"
+ }
+ ],
+ "start_url": ".",
+ "display": "standalone",
+ "theme_color": "#000000",
+ "background_color": "#ffffff"
+}
diff --git a/packages/devextreme-cli/src/templates/nextjs/application/public/robots.txt b/packages/devextreme-cli/src/templates/nextjs/application/public/robots.txt
new file mode 100644
index 000000000..e9e57dc4d
--- /dev/null
+++ b/packages/devextreme-cli/src/templates/nextjs/application/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/packages/devextreme-cli/src/templates/nextjs/application/src/app-info.tsx b/packages/devextreme-cli/src/templates/nextjs/application/src/app-info.tsx
new file mode 100644
index 000000000..9e15c1476
--- /dev/null
+++ b/packages/devextreme-cli/src/templates/nextjs/application/src/app-info.tsx
@@ -0,0 +1,5 @@
+const appInfo = {
+ title: '<%=project%>'
+};
+export default appInfo;
+
diff --git a/packages/devextreme-cli/src/templates/nextjs/application/src/app-navigation.tsx b/packages/devextreme-cli/src/templates/nextjs/application/src/app-navigation.tsx
new file mode 100644
index 000000000..8e39088af
--- /dev/null
+++ b/packages/devextreme-cli/src/templates/nextjs/application/src/app-navigation.tsx
@@ -0,0 +1,21 @@
+export const navigation = [<%=^empty%>
+ {
+ text: 'Home',
+ path: '/pages/home',
+ icon: 'home'
+ },
+ {
+ text: 'Examples',
+ icon: 'folder',
+ items: [
+ {
+ text: 'Profile',
+ path: '/pages/profile'
+ },
+ {
+ text: 'Tasks',
+ path: '/pages/tasks'
+ }
+ ]
+ }
+ <%=/empty%>];
diff --git a/packages/devextreme-cli/src/templates/nextjs/application/src/app/actions/auth.ts b/packages/devextreme-cli/src/templates/nextjs/application/src/app/actions/auth.ts
new file mode 100644
index 000000000..b21fa21de
--- /dev/null
+++ b/packages/devextreme-cli/src/templates/nextjs/application/src/app/actions/auth.ts
@@ -0,0 +1,77 @@
+'use server'
+import { redirect } from 'next/navigation'
+import defaultUser from '@/utils/default-user';
+import { createSession, deleteSession } from '@/app/lib/session'
+
+export async function signUp(email<%=#isTypeScript%>: string<%=/isTypeScript%>, password<%=#isTypeScript%>: string<%=/isTypeScript%>) {
+ try {
+ // Create a user in the database
+ console.log(email, password);
+
+ await signIn(email, password);
+
+ return {
+ isOk: true,
+ }
+ } catch {
+ return {
+ isOk: false,
+ message: 'Unable to create an account',
+ }
+ }
+}
+
+export async function signIn(email<%=#isTypeScript%>: string<%=/isTypeScript%>, password<%=#isTypeScript%>: string<%=/isTypeScript%>) {
+ try {
+ // Verify that a user exists
+ console.log(email, password);
+
+ await createSession(defaultUser.id);
+
+ return {
+ isOk: true,
+ }
+ } catch {
+ return {
+ isOk: false,
+ message: 'Unable to sign in',
+ }
+ }
+}
+
+export async function signOut() {
+ await deleteSession();
+ redirect('/login');
+}
+
+export async function changePassword(email<%=#isTypeScript%>: string<%=/isTypeScript%>, recoveryCode<%=#isTypeScript%>?: string<%=/isTypeScript%>) {
+ try {
+ // Verify the recovery code
+ console.log(email, recoveryCode);
+
+ return {
+ isOk: true,
+ }
+ } catch {
+ return {
+ isOk: false,
+ message: 'Unable to change the password',
+ }
+ }
+}
+
+export async function resetPassword(email<%=#isTypeScript%>: string<%=/isTypeScript%>) {
+ try {
+ // Reset password
+ console.log(email);
+
+ return {
+ isOk: true,
+ }
+ } catch {
+ return {
+ isOk: false,
+ message: 'Unable to reset password',
+ }
+ }
+}
diff --git a/packages/devextreme-cli/src/templates/nextjs/application/src/app/auth/[type]/page.tsx b/packages/devextreme-cli/src/templates/nextjs/application/src/app/auth/[type]/page.tsx
new file mode 100644
index 000000000..a208cc522
--- /dev/null
+++ b/packages/devextreme-cli/src/templates/nextjs/application/src/app/auth/[type]/page.tsx
@@ -0,0 +1,49 @@
+'use client'
+import { use } from 'react';
+import { notFound } from 'next/navigation';
+import { SingleCard } from '@/layouts';
+import {
+ LoginForm,
+ CreateAccountForm,
+ ResetPasswordForm,
+ ChangePasswordForm,
+} from '@/components';
+
+const formText<%=#isTypeScript%>: Record><%=/isTypeScript%> = {
+ 'login': {
+ title: 'Sign In'
+ },
+ 'create-account': {
+ title: 'Sign Up'
+ },
+ 'reset-password': {
+ title: 'Reset Password',
+ description: 'Please enter the email address that you used to register, and we will send you a link to reset your password via Email.'
+ },
+ 'change-password': {
+ title: 'Change Password',
+ }
+}
+
+function AuthForm({name}<%=#isTypeScript%>: {name: string}<%=/isTypeScript%>) {
+ switch (name) {
+ case 'login': return ;
+ case 'create-account': return ;
+ case 'reset-password': return ;
+ case 'change-password': return ;
+ }
+}
+
+export default function AuthPage({ params }<%=#isTypeScript%>: {params: Promise<{type: string}>}<%=/isTypeScript%>) {
+ const { type } = use(params)
+
+ if (!formText[type]) {
+ notFound();
+ }
+
+ const { title, description } = formText[type];
+
+ return
+
+
+}
diff --git a/packages/devextreme-cli/src/templates/nextjs/application/src/app/layout.tsx b/packages/devextreme-cli/src/templates/nextjs/application/src/app/layout.tsx
new file mode 100644
index 000000000..daaaf4484
--- /dev/null
+++ b/packages/devextreme-cli/src/templates/nextjs/application/src/app/layout.tsx
@@ -0,0 +1,17 @@
+<%=#isTypeScript%>import type { PropsWithChildren } from 'react';
+<%=/isTypeScript%>import { ThemeProvider } from "@/theme";
+
+export default function RootLayout({ children }<%=#isTypeScript%>: PropsWithChildren