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>/, `<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=<your_secret_key_goes_here> \ 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<string, Record<string, string>><%=/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 <LoginForm />; + case 'create-account': return <CreateAccountForm />; + case 'reset-password': return <ResetPasswordForm />; + case 'change-password': return <ChangePasswordForm />; + } +} + +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 <SingleCard title={title} description={description}> + <AuthForm name={type}/> + </SingleCard> +} 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<object><%=/isTypeScript%>) { + return ( + <html lang="en"> + <title>NextJs Dx App + + +
+ {children} +
+
+ + + ); +} diff --git a/packages/devextreme-cli/src/templates/nextjs/application/src/app/lib/session.ts b/packages/devextreme-cli/src/templates/nextjs/application/src/app/lib/session.ts new file mode 100644 index 000000000..2e3e5df69 --- /dev/null +++ b/packages/devextreme-cli/src/templates/nextjs/application/src/app/lib/session.ts @@ -0,0 +1,47 @@ +import 'server-only'; +import { SignJWT, jwtVerify } from 'jose'; +import { cookies } from 'next/headers'; +<%=#isTypeScript%>import type { SessionPayload } from '@/types'; +<%=/isTypeScript%> +const secretKey = process.env.SESSION_SECRET; +const encoder = new TextEncoder(); +const encodedKey = encoder.encode(secretKey); + +export async function encrypt(payload<%=#isTypeScript%>: SessionPayload<%=/isTypeScript%>) { + return new SignJWT(payload) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('7d') + .sign(encodedKey); +} + +export async function decrypt(session<%=#isTypeScript%>: string | undefined = ''<%=/isTypeScript%>) { + try { + const { payload } = await jwtVerify(session, encodedKey, { + algorithms: ['HS256'], + }); + + return payload; + } catch { + console.log('Failed to verify session'); + } +} + +export async function createSession(userId<%=#isTypeScript%>: string<%=/isTypeScript%>) { + const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); + const session = await encrypt({ userId, expiresAt }); + const cookieStore = await cookies(); + + cookieStore.set('session', session, { + httpOnly: true, + secure: true, + expires: expiresAt, + sameSite: 'lax', + path: '/', + }); +} + +export async function deleteSession() { + const cookieStore = await cookies(); + cookieStore.delete('session'); +} diff --git a/packages/devextreme-cli/src/templates/nextjs/application/src/app/pages/layout.tsx b/packages/devextreme-cli/src/templates/nextjs/application/src/app/pages/layout.tsx new file mode 100644 index 000000000..12cb05321 --- /dev/null +++ b/packages/devextreme-cli/src/templates/nextjs/application/src/app/pages/layout.tsx @@ -0,0 +1,18 @@ +<%=#isTypeScript%>import type { PropsWithChildren } from 'react'; +<%=/isTypeScript%>import appInfo from '@/app-info'; +import { Footer } from '@/components'; +import { <%=layout%> as SideNavBarLayout } from '@/layouts'; + +export default function Content({children}<%=#isTypeScript%>: PropsWithChildren<%=/isTypeScript%>) { + return ( + + {children} +
+ Copyright © 2011-{new Date().getFullYear()} {appInfo.title} Inc. +
+ All trademarks or registered trademarks are property of their + respective owners. +
+
+ ); +} diff --git a/packages/devextreme-cli/src/templates/nextjs/application/src/components/change-password-form/ChangePasswordForm.tsx b/packages/devextreme-cli/src/templates/nextjs/application/src/components/change-password-form/ChangePasswordForm.tsx new file mode 100644 index 000000000..6c779f650 --- /dev/null +++ b/packages/devextreme-cli/src/templates/nextjs/application/src/components/change-password-form/ChangePasswordForm.tsx @@ -0,0 +1,86 @@ +'use client' +import <%=#isTypeScript%>React, <%=/isTypeScript%>{ useState, useRef, useCallback } from 'react'; +import { useRouter } from 'next/navigation'; +import Form, { + Item, + Label, + ButtonItem, + ButtonOptions, + RequiredRule, + CustomRule, +} from 'devextreme-react/form'; +import LoadIndicator from 'devextreme-react/load-indicator'; +import notify from 'devextreme/ui/notify'; +<%=#isTypeScript%>import { ValidationCallbackData } from 'devextreme-react/common';<%=/isTypeScript%> +import { changePassword } from '@/app/actions/auth'; + +export default function ChangePasswordForm() { + const router = useRouter(); + const [loading, setLoading] = useState(false); + const formData = useRef({ password: '' }); + + const onSubmit = useCallback(async (e<%=#isTypeScript%>: React.FormEvent<%=/isTypeScript%>) => { + e.preventDefault(); + const { password } = formData.current; + setLoading(true); + + const result = await changePassword(password); + setLoading(false); + + if (result.isOk) { + router.push('/login'); + } else { + notify(result.message, 'error', 2000); + } +}, [router]); + +const confirmPassword = useCallback( + ({ value }<%=#isTypeScript%>: ValidationCallbackData<%=/isTypeScript%>) => value === formData.current.password, + [] +); + +return ( +
+ + + + + + + + + + + + { + loading + ? + : 'Continue' + } + + + +
+ +); +} + +const passwordEditorOptions = { stylingMode: 'filled', placeholder: 'Password', mode: 'password' }; +const confirmedPasswordEditorOptions = { stylingMode: 'filled', placeholder: 'Confirm Password', mode: 'password' }; diff --git a/packages/devextreme-cli/src/templates/nextjs/application/src/components/create-account-form/CreateAccountForm.scss b/packages/devextreme-cli/src/templates/nextjs/application/src/components/create-account-form/CreateAccountForm.scss new file mode 100644 index 000000000..830ab7065 --- /dev/null +++ b/packages/devextreme-cli/src/templates/nextjs/application/src/components/create-account-form/CreateAccountForm.scss @@ -0,0 +1,19 @@ +.create-account-form { + .policy-info { + color: var(--base-text-color-alpha-7); + font-size: 12px; + font-style: normal; + + a { + color: var(--base-text-color-alpha-7); + } + } + + .login-link { + color: var(--base-accent); + font-size: 12px; + text-align: center; + padding: 6px 0 32px 0; + border-bottom: 1px solid var(--border-color); + } +} diff --git a/packages/devextreme-cli/src/templates/nextjs/application/src/components/create-account-form/CreateAccountForm.tsx b/packages/devextreme-cli/src/templates/nextjs/application/src/components/create-account-form/CreateAccountForm.tsx new file mode 100644 index 000000000..1d7c8fe4f --- /dev/null +++ b/packages/devextreme-cli/src/templates/nextjs/application/src/components/create-account-form/CreateAccountForm.tsx @@ -0,0 +1,107 @@ +'use client' +import <%=#isTypeScript%>React, <%=/isTypeScript%>{ useState, useRef, useCallback } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import Form, { + Item, + Label, + ButtonItem, + ButtonOptions, + RequiredRule, + CustomRule, + EmailRule +} from 'devextreme-react/form'; +import notify from 'devextreme/ui/notify'; +import LoadIndicator from 'devextreme-react/load-indicator'; +import { signUp } from '@/app/actions/auth'; +<%=#isTypeScript%>import { ValidationCallbackData } from 'devextreme-react/common';<%=/isTypeScript%> +import './CreateAccountForm.scss'; + +export default function CreateAccountForm() { + const router = useRouter(); + const [loading, setLoading] = useState(false); + const formData = useRef({ email: '', password: '' }); + + const onSubmit = useCallback(async (e<%=#isTypeScript%>: React.FormEvent<%=/isTypeScript%>) => { + e.preventDefault(); + const { email, password } = formData.current; + setLoading(true); + + const result = await signUp(email, password); + setLoading(false); + + if (result.isOk) { + router.push('/login'); + } else { + notify(result.message, 'error', 2000); + } + }, [router]); + + const confirmPassword = useCallback( + ({ value }<%=#isTypeScript%>: ValidationCallbackData<%=/isTypeScript%>) => value === formData.current.password, + [] + ); + + return ( +
+ + + + + + + + + + + + + +
+ By creating an account, you agree to the Terms of Service and Privacy Policy +
+
+ + + + { + loading + ? + : 'Create a new account' + } + + + +
+
+ Have an account? Sign In +
+ + ); +} + +const emailEditorOptions = { stylingMode: 'filled', placeholder: 'Email', mode: 'email' }; +const passwordEditorOptions = { stylingMode: 'filled', placeholder: 'Password', mode: 'password' }; +const confirmedPasswordEditorOptions = { stylingMode: 'filled', placeholder: 'Confirm Password', mode: 'password' }; diff --git a/packages/devextreme-cli/src/templates/nextjs/application/src/components/footer/Footer.scss b/packages/devextreme-cli/src/templates/nextjs/application/src/components/footer/Footer.scss new file mode 100644 index 000000000..c1dc94dea --- /dev/null +++ b/packages/devextreme-cli/src/templates/nextjs/application/src/components/footer/Footer.scss @@ -0,0 +1,12 @@ +.footer { + display: block; + color: var(--base-text-color-alpha-7); + border-top: 1px solid var(--footer-border-color); + padding-top: 20px; + padding-bottom: 24px; + margin: 0 40px; + + @media (max-width: 599.99px) { + margin: 0 20px; + } +} diff --git a/packages/devextreme-cli/src/templates/nextjs/application/src/components/footer/Footer.tsx b/packages/devextreme-cli/src/templates/nextjs/application/src/components/footer/Footer.tsx new file mode 100644 index 000000000..bc82bfbd5 --- /dev/null +++ b/packages/devextreme-cli/src/templates/nextjs/application/src/components/footer/Footer.tsx @@ -0,0 +1,5 @@ +import './Footer.scss'; + +export default function Footer({ ...rest }) { + return