From f1c2628d3cb0b54459fa865da0a620b77464050b Mon Sep 17 00:00:00 2001 From: Zachary Williams Date: Sun, 12 Dec 2021 17:09:56 -0600 Subject: [PATCH] feat: add vite plugin --- apps/nuxt-e2e/tests/nuxt.test.ts | 9 +- apps/vite-e2e/jest.config.js | 14 + apps/vite-e2e/tests/vite.spec.ts | 58 ++++ apps/vite-e2e/tsconfig.json | 10 + apps/vite-e2e/tsconfig.spec.json | 9 + apps/vue-e2e/tests/vue-2.test.ts | 22 +- apps/vue-e2e/tests/vue-3.test.ts | 77 ++--- libs/vite/.babelrc | 3 + libs/vite/.eslintrc.json | 21 ++ libs/vite/README.md | 153 +++++++++ libs/vite/executors.json | 27 ++ libs/vite/generators.json | 21 ++ libs/vite/jest.config.js | 14 + libs/vite/package.json | 37 +++ libs/vite/patch-nx-dep-graph.js | 29 ++ libs/vite/src/app-root.ts | 24 ++ libs/vite/src/executors/build/compat.ts | 4 + libs/vite/src/executors/build/executor.ts | 29 ++ libs/vite/src/executors/build/schema.d.ts | 23 ++ libs/vite/src/executors/build/schema.json | 77 +++++ libs/vite/src/executors/server/compat.ts | 4 + libs/vite/src/executors/server/executor.ts | 30 ++ libs/vite/src/executors/server/schema.d.ts | 17 + libs/vite/src/executors/server/schema.json | 64 ++++ libs/vite/src/executors/utils.ts | 17 + .../application/files/index.html.template | 13 + .../files/public/favicon.ico.template | Bin 0 -> 4286 bytes .../application/files/src/App.vue.template | 27 ++ .../application/files/src/assets/logo.png | Bin 0 -> 6849 bytes .../src/components/HelloWorld.vue.template | 65 ++++ .../application/files/src/main.ts.template | 4 + .../files/src/shims-vue.d.ts.template | 6 + .../files/tests/unit/example.spec.ts.template | 10 + .../files/tsconfig.app.json.template | 17 + .../application/files/tsconfig.json.template | 10 + .../application/files/vite.config.ts.template | 24 ++ .../generators/application/generator.spec.ts | 98 ++++++ .../src/generators/application/generator.ts | 301 ++++++++++++++++++ .../src/generators/application/schema.d.ts | 8 + .../src/generators/application/schema.json | 46 +++ libs/vite/src/index.ts | 0 libs/vite/src/utils.ts | 80 +++++ libs/vite/tsconfig.json | 13 + libs/vite/tsconfig.lib.json | 11 + libs/vite/tsconfig.spec.json | 15 + libs/vue/nx-plus-vue.png | Bin 21403 -> 0 bytes package.json | 5 +- tsconfig.base.json | 3 +- workspace.json | 73 +++++ yarn.lock | 146 +++++++++ 50 files changed, 1706 insertions(+), 62 deletions(-) create mode 100644 apps/vite-e2e/jest.config.js create mode 100644 apps/vite-e2e/tests/vite.spec.ts create mode 100644 apps/vite-e2e/tsconfig.json create mode 100644 apps/vite-e2e/tsconfig.spec.json create mode 100644 libs/vite/.babelrc create mode 100644 libs/vite/.eslintrc.json create mode 100644 libs/vite/README.md create mode 100644 libs/vite/executors.json create mode 100644 libs/vite/generators.json create mode 100644 libs/vite/jest.config.js create mode 100644 libs/vite/package.json create mode 100644 libs/vite/patch-nx-dep-graph.js create mode 100644 libs/vite/src/app-root.ts create mode 100644 libs/vite/src/executors/build/compat.ts create mode 100644 libs/vite/src/executors/build/executor.ts create mode 100644 libs/vite/src/executors/build/schema.d.ts create mode 100644 libs/vite/src/executors/build/schema.json create mode 100644 libs/vite/src/executors/server/compat.ts create mode 100644 libs/vite/src/executors/server/executor.ts create mode 100644 libs/vite/src/executors/server/schema.d.ts create mode 100644 libs/vite/src/executors/server/schema.json create mode 100644 libs/vite/src/executors/utils.ts create mode 100644 libs/vite/src/generators/application/files/index.html.template create mode 100644 libs/vite/src/generators/application/files/public/favicon.ico.template create mode 100644 libs/vite/src/generators/application/files/src/App.vue.template create mode 100644 libs/vite/src/generators/application/files/src/assets/logo.png create mode 100644 libs/vite/src/generators/application/files/src/components/HelloWorld.vue.template create mode 100644 libs/vite/src/generators/application/files/src/main.ts.template create mode 100644 libs/vite/src/generators/application/files/src/shims-vue.d.ts.template create mode 100644 libs/vite/src/generators/application/files/tests/unit/example.spec.ts.template create mode 100644 libs/vite/src/generators/application/files/tsconfig.app.json.template create mode 100644 libs/vite/src/generators/application/files/tsconfig.json.template create mode 100644 libs/vite/src/generators/application/files/vite.config.ts.template create mode 100644 libs/vite/src/generators/application/generator.spec.ts create mode 100644 libs/vite/src/generators/application/generator.ts create mode 100644 libs/vite/src/generators/application/schema.d.ts create mode 100644 libs/vite/src/generators/application/schema.json create mode 100644 libs/vite/src/index.ts create mode 100644 libs/vite/src/utils.ts create mode 100644 libs/vite/tsconfig.json create mode 100644 libs/vite/tsconfig.lib.json create mode 100644 libs/vite/tsconfig.spec.json delete mode 100644 libs/vue/nx-plus-vue.png diff --git a/apps/nuxt-e2e/tests/nuxt.test.ts b/apps/nuxt-e2e/tests/nuxt.test.ts index 6a6b4911..8b325342 100644 --- a/apps/nuxt-e2e/tests/nuxt.test.ts +++ b/apps/nuxt-e2e/tests/nuxt.test.ts @@ -8,9 +8,12 @@ import { } from '@nrwl/nx-plugin/testing'; describe('nuxt e2e', () => { + beforeAll(() => { + ensureNxProject('@nx-plus/nuxt', 'dist/libs/nuxt'); + }); + it('should generate app', async () => { const appName = uniq('app'); - ensureNxProject('@nx-plus/nuxt', 'dist/libs/nuxt'); await runNxCommandAsync(`generate @nx-plus/nuxt:app ${appName}`); const lintResult = await runNxCommandAsync(`lint ${appName}`); @@ -57,7 +60,6 @@ describe('nuxt e2e', () => { it('should report lint error in index.vue', async () => { const appName = uniq('app'); - ensureNxProject('@nx-plus/nuxt', 'dist/libs/nuxt'); await runNxCommandAsync(`generate @nx-plus/nuxt:app ${appName}`); updateFile( @@ -73,7 +75,6 @@ describe('nuxt e2e', () => { it('should generate static app', async () => { const appName = uniq('app'); - ensureNxProject('@nx-plus/nuxt', 'dist/libs/nuxt'); await runNxCommandAsync(`generate @nx-plus/nuxt:app ${appName}`); await runNxCommandAsync(`static ${appName}`); @@ -92,7 +93,6 @@ describe('nuxt e2e', () => { describe('--directory subdir', () => { it('should generate app', async () => { const appName = uniq('app'); - ensureNxProject('@nx-plus/nuxt', 'dist/libs/nuxt'); await runNxCommandAsync( `generate @nx-plus/nuxt:app ${appName} --directory subdir` ); @@ -123,7 +123,6 @@ describe('nuxt e2e', () => { it('should generate static app', async () => { const appName = uniq('app'); - ensureNxProject('@nx-plus/nuxt', 'dist/libs/nuxt'); await runNxCommandAsync( `generate @nx-plus/nuxt:app ${appName} --directory subdir` ); diff --git a/apps/vite-e2e/jest.config.js b/apps/vite-e2e/jest.config.js new file mode 100644 index 00000000..9d574a50 --- /dev/null +++ b/apps/vite-e2e/jest.config.js @@ -0,0 +1,14 @@ +module.exports = { + displayName: 'vite-e2e', + preset: '../../jest.preset.js', + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + }, + }, + transform: { + '^.+\\.[tj]s$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/apps/vite-e2e', +}; diff --git a/apps/vite-e2e/tests/vite.spec.ts b/apps/vite-e2e/tests/vite.spec.ts new file mode 100644 index 00000000..9a5834d1 --- /dev/null +++ b/apps/vite-e2e/tests/vite.spec.ts @@ -0,0 +1,58 @@ +import { tags } from '@angular-devkit/core'; +import { + checkFilesExist, + ensureNxProject, + runNxCommandAsync, + uniq, + updateFile, +} from '@nrwl/nx-plugin/testing'; +import { join } from 'path'; + +describe('vite e2e', () => { + it('should create vite app', async () => { + const appName = uniq('vite'); + ensureNxProject('@nx-plus/vite', 'dist/libs/vite'); + await runNxCommandAsync(`generate @nx-plus/vite:app ${appName}`); + + const lintResult = await runNxCommandAsync(`lint ${appName}`); + expect(lintResult.stdout).toContain('All files pass linting.'); + + const testResult = await runNxCommandAsync(`test ${appName}`); + expect(testResult.stderr).toContain(tags.stripIndent` + Test Suites: 1 passed, 1 total + Tests: 1 passed, 1 total + Snapshots: 0 total + `); + + disableHashing(appName); + const buildResult = await runNxCommandAsync(`build ${appName}`); + checkFilesExist( + `dist/apps/${appName}/index.html`, + `dist/apps/${appName}/assets/index.css`, + `dist/apps/${appName}/assets/index.js`, + `dist/apps/${appName}/assets/vendor.js` + ); + // Cannot disable hashing for assets + // see: https://github.com/vitejs/vite/issues/2944 + expect(buildResult.stdout).toContain(`dist/apps/${appName}/assets/logo`); + + const e2eResult = await runNxCommandAsync(`e2e ${appName}-e2e --headless`); + expect(e2eResult.stdout).toContain('All specs passed!'); + }, 100000); +}); + +function disableHashing(app: string, directory: string = '') { + updateFile(join('apps', app, directory, 'vite.config.ts'), (content) => + content.replace( + 'emptyOutDir: true,', + `emptyOutDir: true, + rollupOptions: { + output: { + entryFileNames: \`assets/[name].js\`, + chunkFileNames: \`assets/[name].js\`, + assetFileNames: \`assets/[name].[ext]\` + } + }` + ) + ); +} diff --git a/apps/vite-e2e/tsconfig.json b/apps/vite-e2e/tsconfig.json new file mode 100644 index 00000000..b9c9d953 --- /dev/null +++ b/apps/vite-e2e/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/apps/vite-e2e/tsconfig.spec.json b/apps/vite-e2e/tsconfig.spec.json new file mode 100644 index 00000000..29efa430 --- /dev/null +++ b/apps/vite-e2e/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/apps/vue-e2e/tests/vue-2.test.ts b/apps/vue-e2e/tests/vue-2.test.ts index 89210af0..4f754f21 100644 --- a/apps/vue-e2e/tests/vue-2.test.ts +++ b/apps/vue-e2e/tests/vue-2.test.ts @@ -8,11 +8,14 @@ import { } from '@nrwl/nx-plugin/testing'; import { runNxProdCommandAsync, testGeneratedApp } from './utils'; -describe('vue 2 e2e', () => { +describe.skip('vue 2 e2e', () => { describe('app', () => { + beforeAll(() => { + ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); + }); + it('should generate app', async () => { const appName = uniq('app'); - ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); await runNxCommandAsync(`generate @nx-plus/vue:app ${appName}`); await testGeneratedApp(appName, { @@ -27,7 +30,6 @@ describe('vue 2 e2e', () => { describe('--routing', () => { it('should generate app with routing', async () => { const appName = uniq('app'); - ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); await runNxCommandAsync( `generate @nx-plus/vue:app ${appName} --routing` ); @@ -52,7 +54,6 @@ describe('vue 2 e2e', () => { describe('--style', () => { it('should generate app with scss', async () => { const appName = uniq('app'); - ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); await runNxCommandAsync( `generate @nx-plus/vue:app ${appName} --style scss` ); @@ -68,7 +69,6 @@ describe('vue 2 e2e', () => { it('should generate app with stylus', async () => { const appName = uniq('app'); - ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); await runNxCommandAsync( `generate @nx-plus/vue:app ${appName} --style stylus` ); @@ -84,7 +84,6 @@ describe('vue 2 e2e', () => { it('should generate app with less', async () => { const appName = uniq('app'); - ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); await runNxCommandAsync( `generate @nx-plus/vue:app ${appName} --style less` ); @@ -102,7 +101,6 @@ describe('vue 2 e2e', () => { describe('vuex', () => { it('should generate app and add vuex', async () => { const appName = uniq('app'); - ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); await runNxCommandAsync(`generate @nx-plus/vue:app ${appName}`); await runNxCommandAsync(`generate @nx-plus/vue:vuex ${appName}`); @@ -118,7 +116,6 @@ describe('vue 2 e2e', () => { it('should generate app with routing and add vuex', async () => { const appName = uniq('app'); - ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); await runNxCommandAsync(`generate @nx-plus/vue:app ${appName} --routing`); await runNxCommandAsync(`generate @nx-plus/vue:vuex ${appName}`); @@ -140,7 +137,6 @@ describe('vue 2 e2e', () => { it('should report lint error in App.vue', async () => { const appName = uniq('app'); - ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); await runNxCommandAsync(`generate @nx-plus/vue:app ${appName}`); updateFile( @@ -157,7 +153,6 @@ describe('vue 2 e2e', () => { describe('--directory subdir', () => { it('should generate app', async () => { const appName = uniq('app'); - ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); await runNxCommandAsync( `generate @nx-plus/vue:app ${appName} --directory subdir` ); @@ -177,9 +172,12 @@ describe('vue 2 e2e', () => { }); describe('library', () => { + beforeAll(() => { + ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); + }); + it('should generate lib', async () => { const lib = uniq('lib'); - ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); await runNxCommandAsync(`generate @nx-plus/vue:lib ${lib}`); const lintResult = await runNxCommandAsync(`lint ${lib}`); @@ -195,7 +193,6 @@ describe('vue 2 e2e', () => { it('should generate publishable lib', async () => { const lib = uniq('lib'); - ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); await runNxCommandAsync(`generate @nx-plus/vue:lib ${lib} --publishable`); let buildResult = await runNxProdCommandAsync(`build ${lib}`); @@ -248,7 +245,6 @@ describe('vue 2 e2e', () => { describe('--directory subdir', () => { it('should generate publishable lib', async () => { const lib = uniq('lib'); - ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); await runNxCommandAsync( `generate @nx-plus/vue:lib ${lib} --directory subdir --publishable` ); diff --git a/apps/vue-e2e/tests/vue-3.test.ts b/apps/vue-e2e/tests/vue-3.test.ts index 3a510677..157c07ed 100644 --- a/apps/vue-e2e/tests/vue-3.test.ts +++ b/apps/vue-e2e/tests/vue-3.test.ts @@ -10,9 +10,12 @@ import { runNxProdCommandAsync, testGeneratedApp } from './utils'; describe('vue 3 e2e', () => { describe('app', () => { + beforeAll(() => { + ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); + }); + it('should generate app', async () => { const appName = uniq('app'); - ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); await runNxCommandAsync( `generate @nx-plus/vue:app ${appName} --vueVersion 3` ); @@ -29,7 +32,6 @@ describe('vue 3 e2e', () => { describe('--routing', () => { it('should generate app with routing', async () => { const appName = uniq('app'); - ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); await runNxCommandAsync( `generate @nx-plus/vue:app ${appName} --vueVersion 3 --routing` ); @@ -54,7 +56,6 @@ describe('vue 3 e2e', () => { describe('vuex', () => { it('should generate app and add vuex', async () => { const appName = uniq('app'); - ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); await runNxCommandAsync( `generate @nx-plus/vue:app ${appName} --vueVersion 3` ); @@ -72,7 +73,6 @@ describe('vue 3 e2e', () => { it('should generate app with routing and add vuex', async () => { const appName = uniq('app'); - ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); await runNxCommandAsync( `generate @nx-plus/vue:app ${appName} --vueVersion 3 --routing` ); @@ -96,7 +96,6 @@ describe('vue 3 e2e', () => { it('should report lint error in App.vue', async () => { const appName = uniq('app'); - ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); await runNxCommandAsync( `generate @nx-plus/vue:app ${appName} --vueVersion 3` ); @@ -111,12 +110,30 @@ describe('vue 3 e2e', () => { }); expect(result.stderr).toContain('Lint errors found in the listed files.'); }, 300000); + + it('should generate component', async () => { + const appName = uniq('app'); + await runNxCommandAsync( + `generate @nx-plus/vue:app ${appName} --vueVersion 3` + ); + + await runNxCommandAsync( + `generate @nx-plus/vue:component my-component --project ${appName}` + ); + + expect(() => + checkFilesExist(`apps/${appName}/src/MyComponent.vue`) + ).not.toThrow(); + }, 300000); }); describe('library', () => { + beforeAll(() => { + ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); + }); + it('should generate lib', async () => { const lib = uniq('lib'); - ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); await runNxCommandAsync( `generate @nx-plus/vue:lib ${lib} --vueVersion 3` ); @@ -134,7 +151,6 @@ describe('vue 3 e2e', () => { it('should generate publishable lib', async () => { const lib = uniq('lib'); - ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); await runNxCommandAsync( `generate @nx-plus/vue:lib ${lib} --vueVersion 3 --publishable` ); @@ -185,43 +201,20 @@ describe('vue 3 e2e', () => { ) ).toThrow(); }, 300000); - }); - - describe('component', () => { - describe('inside an app', () => { - it('should generate component', async () => { - const appName = uniq('app'); - ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); - await runNxCommandAsync( - `generate @nx-plus/vue:app ${appName} --vueVersion 3` - ); - await runNxCommandAsync( - `generate @nx-plus/vue:component my-component --project ${appName}` - ); - - expect(() => - checkFilesExist(`apps/${appName}/src/MyComponent.vue`) - ).not.toThrow(); - }, 300000); - }); - - describe('inside a library', () => { - it('should generate component', async () => { - const libName = uniq('lib'); - ensureNxProject('@nx-plus/vue', 'dist/libs/vue'); - await runNxCommandAsync( - `generate @nx-plus/vue:lib ${libName} --vueVersion 3` - ); + it('should generate component', async () => { + const libName = uniq('lib'); + await runNxCommandAsync( + `generate @nx-plus/vue:lib ${libName} --vueVersion 3` + ); - await runNxCommandAsync( - `generate @nx-plus/vue:component my-component --project ${libName}` - ); + await runNxCommandAsync( + `generate @nx-plus/vue:component my-component --project ${libName}` + ); - expect(() => - checkFilesExist(`libs/${libName}/src/lib/MyComponent.vue`) - ).not.toThrow(); - }, 300000); - }); + expect(() => + checkFilesExist(`libs/${libName}/src/lib/MyComponent.vue`) + ).not.toThrow(); + }, 300000); }); }); diff --git a/libs/vite/.babelrc b/libs/vite/.babelrc new file mode 100644 index 00000000..cf7ddd99 --- /dev/null +++ b/libs/vite/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]] +} diff --git a/libs/vite/.eslintrc.json b/libs/vite/.eslintrc.json new file mode 100644 index 00000000..c3d6e153 --- /dev/null +++ b/libs/vite/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "parserOptions": { + "project": ["libs/vite/tsconfig.*?.json"] + }, + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/vite/README.md b/libs/vite/README.md new file mode 100644 index 00000000..6ebda71a --- /dev/null +++ b/libs/vite/README.md @@ -0,0 +1,153 @@ +# Nx Plus Vite + +> First class support for [Vite](https://vitejs.dev/) in your [Nx](https://nx.dev/) workspace. + +
+ +
+ +## Contents + +- [Prerequisite](#prerequisite) +- [Getting Started](#getting-started) +- [Schematics (i.e. code generation)](#schematics-ie-code-generation) +- [Builders (i.e. task runners)](#builders-ie-task-runners) +- [Configuring Vite](#configuring-vite) +- [Updating Nx Plus Vite](#updating-nx-plus-vite) + +## Prerequisite + +### Nx Workspace + +If you have not already, [create an Nx workspace](https://github.com/nrwl/nx#creating-an-nx-workspace) with the following: + +``` +npx create-nx-workspace@^12.0.0 +``` + +### Peer Dependencies + +If you have not already, install peer dependencies with the following: + +``` +# npm +npm install @nrwl/cypress@^12.0.0 @nrwl/jest@^12.0.0 @nrwl/linter@^12.0.0 --save-dev + +# yarn +yarn add @nrwl/cypress@^12.0.0 @nrwl/jest@^12.0.0 @nrwl/linter@^12.0.0 --dev +``` + +## Getting Started + +### Install Plugin + +``` +# npm +npm install @nx-plus/vite --save-dev + +# yarn +yarn add @nx-plus/vite --dev +``` + +### Generate Your App + +``` +nx g @nx-plus/vite:app my-app +``` + +### Serve Your App + +``` +nx serve my-app +``` + +## Schematics (i.e. code generation) + +### Application + +`nx g @nx-plus/vite:app [options]` + +| Arguments | Description | +| --------- | --------------------- | +| `` | The name of your app. | + +| Options | Default | Description | +| ------------------ | --------- | ---------------------------------------------- | +| `--tags` | - | Tags to use for linting (comma-delimited). | +| `--directory` | `apps` | A directory where the project is placed. | +| `--unitTestRunner` | `jest` | Test runner to use for unit tests. | +| `--e2eTestRunner` | `cypress` | Test runner to use for end to end (e2e) tests. | +| `--skipFormat` | `false` | Skip formatting files. | + +## Builders (i.e. task runners) + +### Server + +`nx serve [options]` + +| Arguments | Description | +| --------- | --------------------- | +| `` | The name of your app. | + +| Options | Default | Description | +| -------------- | ------- | ------------------------------------------------------ | +| `--config` | - | Use specified config file. | +| `--root` | - | Use specified root directory. | +| `--base` | '/' | Public base path. | +| `--host` | - | Specify hostname. | +| `--port` | - | Specify port. | +| `--https` | - | Use TLS + HTTP/2. | +| `--open` | - | Open browser on startup. | +| `--cors` | - | Enable cors. | +| `--strictPort` | - | Exit if specified port is already in use. | +| `--mode` | - | Set env mode. | +| `--force` | - | Force the optimizer to ignore the cache and re-bundle. | + +### Build + +`nx build [options]` + +| Arguments | Description | +| --------- | --------------------- | +| `` | The name of your app. | + +| Options | Default | Description | +| --------------------- | ---------- | -------------------------------------------------------- | +| `--config` | - | Use specified config file. | +| `--root` | - | Use specified root directory. | +| `--base` | '/' | Public base path. | +| `--target` | 'modules' | Transpile target. | +| `--outDir` | - | Output directory. | +| `--assetsDir` | '\_assets' | Directory under outDir to place assets in. | +| `--assetsInlineLimit` | `4096` | Static asset base64 inline threshold in bytes. | +| `--ssr` | - | Build specified entry for server-side rendering. | +| `--sourcemap` | `false` | Output source maps for build. | +| `--minify` | 'esbuild' | Enable/disable minification, or specify minifier to use. | +| `--manifest` | - | Emit build manifest json. | +| `--ssrManifest` | - | Emit ssr manifest json. | +| `--emptyOutDir` | - | Force empty outDir when it's outside of root. | +| `--mode` | - | Set env mode. | +| `--force` | - | Force the optimizer to ignore the cache and re-bundle. | +| `--watch` | - | Rebuilds when modules have changed on disk. | + +### Configuring Vite + +A `vite.config.js` can be found at the root of your project. See the [Vite documentation](https://vitejs.dev/config/) for more details. + +### Linting + +`nx lint [options]` + +We use `@nrwl/linter` for linting, so the options are as documented [here](https://github.com/nrwl/nx/blob/master/docs/angular/api-linter/builders/eslint.md#eslint). + +### Unit Testing + +`nx test [options]` + +We use `@nrwl/jest` for unit testing, so the options are as documented [here](https://github.com/nrwl/nx/blob/master/docs/angular/api-jest/builders/jest.md#jest). + +### E2E Testing + +`nx e2e [options]` + +We use `@nrwl/cypress` for e2e testing, so the options are as documented [here](https://github.com/nrwl/nx/blob/master/docs/angular/api-cypress/builders/cypress.md#cypress). diff --git a/libs/vite/executors.json b/libs/vite/executors.json new file mode 100644 index 00000000..9473e60f --- /dev/null +++ b/libs/vite/executors.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/schema", + "executors": { + "build": { + "implementation": "./src/executors/build/executor", + "schema": "./src/executors/build/schema.json", + "description": "build executor" + }, + "server": { + "implementation": "./src/executors/server/executor", + "schema": "./src/executors/server/schema.json", + "description": "serve executor" + } + }, + "builders": { + "build": { + "implementation": "./src/executors/browser/compat", + "schema": "./src/executors/browser/schema.json", + "description": "browser builder" + }, + "server": { + "implementation": "./src/executors/server/compat", + "schema": "./src/executors/server/schema.json", + "description": "server builder" + } + } +} diff --git a/libs/vite/generators.json b/libs/vite/generators.json new file mode 100644 index 00000000..ccd2193a --- /dev/null +++ b/libs/vite/generators.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/schema", + "name": "vite", + "version": "0.0.1", + "generators": { + "application": { + "factory": "./src/generators/application/generator#applicationGenerator", + "schema": "./src/generators/application/schema.json", + "description": "application generator", + "aliases": ["app"] + } + }, + "schematics": { + "application": { + "factory": "./src/generators/application/generator#applicationSchematic", + "schema": "./src/generators/application/schema.json", + "description": "application schematic", + "aliases": ["app"] + } + } +} diff --git a/libs/vite/jest.config.js b/libs/vite/jest.config.js new file mode 100644 index 00000000..f160989f --- /dev/null +++ b/libs/vite/jest.config.js @@ -0,0 +1,14 @@ +module.exports = { + displayName: 'vite', + preset: '../../jest.preset.js', + globals: { + 'ts-jest': { + tsconfig: '/tsconfig.spec.json', + }, + }, + transform: { + '^.+\\.[tj]sx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/libs/vite', +}; diff --git a/libs/vite/package.json b/libs/vite/package.json new file mode 100644 index 00000000..e41f9048 --- /dev/null +++ b/libs/vite/package.json @@ -0,0 +1,37 @@ +{ + "name": "@nx-plus/vite", + "version": "12.2.0", + "main": "src/index.js", + "generators": "./generators.json", + "executors": "./executors.json", + "description": "Vite plugin for Nx", + "repository": { + "type": "git", + "url": "git+https://github.com/ZachJW34/nx-plus.git" + }, + "keywords": [ + "nx", + "vite", + "documentation", + "website" + ], + "author": "Zachary Williams (https://github.com/ZachJW34/)", + "contributors": [ + "Bucky Maler (http://buckymaler.com/)" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/ZachJW34/nx-plus/issues" + }, + "homepage": "https://github.com/ZachJW34/nx-plus/tree/master/libs/vite#readme", + "peerDependencies": { + "@nrwl/cypress": "^12.0.0", + "@nrwl/jest": "^12.0.0", + "@nrwl/linter": "^12.0.0", + "@nrwl/workspace": "^12.0.0" + }, + "dependencies": { + "@nrwl/devkit": "^12.0.0", + "semver": "^7.3.2" + } +} diff --git a/libs/vite/patch-nx-dep-graph.js b/libs/vite/patch-nx-dep-graph.js new file mode 100644 index 00000000..c9d5d80c --- /dev/null +++ b/libs/vite/patch-nx-dep-graph.js @@ -0,0 +1,29 @@ +const fs = require('fs'); +const path = require('path'); + +/** + * Patch dep-graph builder function to support Vue files. + * @see https://github.com/nrwl/nx/issues/2960 + */ +function patchNxDepGraph() { + const filePath = path.join( + process.env.INIT_CWD || '', + 'node_modules/@nrwl/workspace/src/core/project-graph/build-dependencies/typescript-import-locator.js' + ); + try { + const fileContent = fs.readFileSync(filePath).toString('utf-8'); + const replacement = "extension !== '.ts' && extension !== '.vue'"; + if (fileContent.includes(replacement)) { + return; + } + fs.writeFileSync( + filePath, + fileContent.replace("extension !== '.ts'", replacement) + ); + console.log('Successfully patched Nx dep-graph for Vue support.'); + } catch (err) { + console.error('Failed to patch Nx dep-graph for Vue support.', err); + } +} + +patchNxDepGraph(); diff --git a/libs/vite/src/app-root.ts b/libs/vite/src/app-root.ts new file mode 100644 index 00000000..5f1f2ca5 --- /dev/null +++ b/libs/vite/src/app-root.ts @@ -0,0 +1,24 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; + +export const appRootPath = pathInner(__dirname); + +function pathInner(dir: string): string { + if (path.dirname(dir) === dir) return process.cwd(); + if ( + fileExists(path.join(dir, 'workspace.json')) || + fileExists(path.join(dir, 'angular.json')) + ) { + return dir; + } else { + return pathInner(path.dirname(dir)); + } +} + +function fileExists(filePath: string): boolean { + try { + return fs.statSync(filePath).isFile(); + } catch (err) { + return false; + } +} diff --git a/libs/vite/src/executors/build/compat.ts b/libs/vite/src/executors/build/compat.ts new file mode 100644 index 00000000..c78195e9 --- /dev/null +++ b/libs/vite/src/executors/build/compat.ts @@ -0,0 +1,4 @@ +import { convertNxExecutor } from '@nrwl/devkit'; +import { default as buildExecutor } from './executor'; + +export default convertNxExecutor(buildExecutor); diff --git a/libs/vite/src/executors/build/executor.ts b/libs/vite/src/executors/build/executor.ts new file mode 100644 index 00000000..4234d86a --- /dev/null +++ b/libs/vite/src/executors/build/executor.ts @@ -0,0 +1,29 @@ +import { build, BuildOptions } from 'vite'; +import { ViteBuildExecutorSchema } from './schema'; +import { cleanViteOptions } from '../utils'; + +export default async function* runExecutor(options: ViteBuildExecutorSchema) { + try { + await build({ + base: options.base, + mode: options.mode, + configFile: options.config, + logLevel: options.logLevel, + clearScreen: options.clearScreen, + build: cleanViteOptions(options) as BuildOptions, + }); + yield { + success: true, + }; + if (options.watch) { + // This Promise intentionally never resolves, leaving the process running + // eslint-disable-next-line @typescript-eslint/no-empty-function + await new Promise<{ success: boolean }>(() => {}); + } + } catch (err) { + return { + success: false, + error: err, + }; + } +} diff --git a/libs/vite/src/executors/build/schema.d.ts b/libs/vite/src/executors/build/schema.d.ts new file mode 100644 index 00000000..4c87a939 --- /dev/null +++ b/libs/vite/src/executors/build/schema.d.ts @@ -0,0 +1,23 @@ +import { LogLevel } from 'vite'; + +export interface ViteBuildExecutorSchema { + config: string; + root?: string; + base?: string; + logLevel?: LogLevel; + clearScreen?: boolean; + debug?: string | boolean; + filter?: string; + target?: string; + outDir?: string; + assetsDir?: string; + assetsInlineLimit?: number; + ssr?: string; + sourcemap?: boolean; + minify?: string | boolean; + manifest?: boolean; + ssrManifest?: boolean; + emptyOutDir?: boolean; + mode?: string; + watch?: boolean; +} diff --git a/libs/vite/src/executors/build/schema.json b/libs/vite/src/executors/build/schema.json new file mode 100644 index 00000000..b202bf2b --- /dev/null +++ b/libs/vite/src/executors/build/schema.json @@ -0,0 +1,77 @@ +{ + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "title": "Build executor", + "description": "", + "type": "object", + "properties": { + "config": { + "type": "string", + "description": "use specified config file" + }, + "root": { + "type": "string", + "description": "use specified root directory" + }, + "base": { "type": "string", "description": "public base path" }, + "logLevel": { + "type": "string", + "description": "silent | error | warn | all" + }, + "clearScreen": { + "type": "boolean", + "description": "allow/disable clear screen when logging" + }, + "debug": { + "type": ["string", "boolean"], + "description": "show debug logs" + }, + "filter": { "type": "string", "description": "filter debug log" }, + "target": { + "type": "string", + "description": "transpile target" + }, + "outDir": { + "type": "string", + "description": "output directory" + }, + "assetsDir": { + "type": "string", + "description": "directory under outDir to place assets in" + }, + "assetsInlineLimit": { + "type": "number", + "description": "static asset base64 inline threshold in bytes" + }, + "ssr": { + "type": "string", + "description": "build specified entry for server-side rendering" + }, + "sourcemap": { + "type": "boolean", + "description": "output source maps for build" + }, + "minify": { + "type": ["string", "boolean"], + "description": "enable/disable minification, or specify minifier to use" + }, + "manifest": { + "type": "boolean", + "description": "emit build manifest json" + }, + "ssrManifest": { + "type": "boolean", + "description": "emit ssr manifest json" + }, + "emptyOutDir": { + "type": "boolean", + "description": "force empty outDir when it's outside of root" + }, + "mode": { "type": "string", "description": "set env mode" }, + "watch": { + "type": "boolean", + "description": "rebuilds when modules have changed on disk" + } + }, + "required": ["config"] +} diff --git a/libs/vite/src/executors/server/compat.ts b/libs/vite/src/executors/server/compat.ts new file mode 100644 index 00000000..bbac558f --- /dev/null +++ b/libs/vite/src/executors/server/compat.ts @@ -0,0 +1,4 @@ +import { convertNxExecutor } from '@nrwl/devkit'; +import { default as serverExecutor } from './executor'; + +export default convertNxExecutor(serverExecutor); diff --git a/libs/vite/src/executors/server/executor.ts b/libs/vite/src/executors/server/executor.ts new file mode 100644 index 00000000..e1b7f5f3 --- /dev/null +++ b/libs/vite/src/executors/server/executor.ts @@ -0,0 +1,30 @@ +import { createServer, ServerOptions } from 'vite'; +import { ViteServerExecutorSchema } from './schema'; +import { cleanViteOptions } from '../utils'; + +export default async function* runExecutor(options: ViteServerExecutorSchema) { + const server = await createServer({ + base: options.base, + mode: options.mode, + configFile: options.config, + logLevel: options.logLevel, + clearScreen: options.clearScreen, + server: cleanViteOptions(options) as ServerOptions, + }); + + if (!server.httpServer) { + throw new Error('HTTP server not available'); + } + + await server.listen(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (server as any).printUrls(); + yield { + baseUrl: `http://localhost:${server.config.server.port}`, + success: true, + }; + // This Promise intentionally never resolves, leaving the process running + // eslint-disable-next-line @typescript-eslint/no-empty-function + await new Promise<{ success: boolean }>(() => {}); +} diff --git a/libs/vite/src/executors/server/schema.d.ts b/libs/vite/src/executors/server/schema.d.ts new file mode 100644 index 00000000..2359fad1 --- /dev/null +++ b/libs/vite/src/executors/server/schema.d.ts @@ -0,0 +1,17 @@ +export interface ViteServerExecutorSchema { + config: string; + root?: string; + base?: string; + logLevel?: LogLevel; + clearScreen?: boolean; + debug?: boolean; + filter?: string; + host?: string; + port?: number; + https?: boolean; + open?: string | boolean; + cors?: boolean; + strictPort?: boolean; + mode?: string; + force?: boolean; +} diff --git a/libs/vite/src/executors/server/schema.json b/libs/vite/src/executors/server/schema.json new file mode 100644 index 00000000..09e96f83 --- /dev/null +++ b/libs/vite/src/executors/server/schema.json @@ -0,0 +1,64 @@ +{ + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "title": "Server executor", + "description": "", + "type": "object", + "properties": { + "config": { + "type": "string", + "description": "use specified config file" + }, + "root": { + "type": "string", + "description": "use specified root directory" + }, + "base": { "type": "string", "description": "public base path" }, + "logLevel": { + "type": "string", + "description": "silent | error | warn | all" + }, + "clearScreen": { + "type": "boolean", + "description": "allow/disable clear screen when logging" + }, + "debug": { + "type": "boolean", + "description": "show debug logs" + }, + "filter": { "type": "string", "description": "filter debug log" }, + "host": { + "type": "string", + "description": "specify hostname" + }, + "port": { + "type": "number", + "description": "specify port" + }, + "https": { + "type": "boolean", + "description": "use TLS + HTTP/2" + }, + "open": { + "type": "boolean", + "description": "open browser on startup" + }, + "cors": { + "type": "boolean", + "description": "enable cors" + }, + "strictPort": { + "type": "boolean", + "description": "exit if specified port is already in use" + }, + "mode": { + "type": "string", + "description": "set env mode" + }, + "force": { + "type": "boolean", + "description": "force the optimizer to ignore the cache and re-bundle" + } + }, + "required": ["config"] +} diff --git a/libs/vite/src/executors/utils.ts b/libs/vite/src/executors/utils.ts new file mode 100644 index 00000000..c87b26e7 --- /dev/null +++ b/libs/vite/src/executors/utils.ts @@ -0,0 +1,17 @@ +import { ViteBuildExecutorSchema } from './build/schema'; +import { ViteServerExecutorSchema } from './server/schema'; + +export function cleanViteOptions( + options: ViteServerExecutorSchema | ViteBuildExecutorSchema +) { + const ret = { ...options }; + delete ret.debug; + delete ret.filter; + delete ret.config; + delete ret.root; + delete ret.base; + delete ret.mode; + delete ret.logLevel; + delete ret.clearScreen; + return ret; +} diff --git a/libs/vite/src/generators/application/files/index.html.template b/libs/vite/src/generators/application/files/index.html.template new file mode 100644 index 00000000..11603f87 --- /dev/null +++ b/libs/vite/src/generators/application/files/index.html.template @@ -0,0 +1,13 @@ + + + + + + + Vite App + + +
+ + + diff --git a/libs/vite/src/generators/application/files/public/favicon.ico.template b/libs/vite/src/generators/application/files/public/favicon.ico.template new file mode 100644 index 0000000000000000000000000000000000000000..df36fcfb72584e00488330b560ebcf34a41c64c2 GIT binary patch literal 4286 zcmds*O-Phc6o&64GDVCEQHxsW(p4>LW*W<827=Unuo8sGpRux(DN@jWP-e29Wl%wj zY84_aq9}^Am9-cWTD5GGEo#+5Fi2wX_P*bo+xO!)p*7B;iKlbFd(U~_d(U?#hLj56 zPhFkj-|A6~Qk#@g^#D^U0XT1cu=c-vu1+SElX9NR;kzAUV(q0|dl0|%h|dI$%VICy zJnu2^L*Te9JrJMGh%-P79CL0}dq92RGU6gI{v2~|)p}sG5x0U*z<8U;Ij*hB9z?ei z@g6Xq-pDoPl=MANPiR7%172VA%r)kevtV-_5H*QJKFmd;8yA$98zCxBZYXTNZ#QFk2(TX0;Y2dt&WitL#$96|gJY=3xX zpCoi|YNzgO3R`f@IiEeSmKrPSf#h#Qd<$%Ej^RIeeYfsxhPMOG`S`Pz8q``=511zm zAm)MX5AV^5xIWPyEu7u>qYs?pn$I4nL9J!=K=SGlKLXpE<5x+2cDTXq?brj?n6sp= zphe9;_JHf40^9~}9i08r{XM$7HB!`{Ys~TK0kx<}ZQng`UPvH*11|q7&l9?@FQz;8 zx!=3<4seY*%=OlbCbcae?5^V_}*K>Uo6ZWV8mTyE^B=DKy7-sdLYkR5Z?paTgK-zyIkKjIcpyO z{+uIt&YSa_$QnN_@t~L014dyK(fOOo+W*MIxbA6Ndgr=Y!f#Tokqv}n<7-9qfHkc3 z=>a|HWqcX8fzQCT=dqVbogRq!-S>H%yA{1w#2Pn;=e>JiEj7Hl;zdt-2f+j2%DeVD zsW0Ab)ZK@0cIW%W7z}H{&~yGhn~D;aiP4=;m-HCo`BEI+Kd6 z={Xwx{TKxD#iCLfl2vQGDitKtN>z|-AdCN|$jTFDg0m3O`WLD4_s#$S literal 0 HcmV?d00001 diff --git a/libs/vite/src/generators/application/files/src/App.vue.template b/libs/vite/src/generators/application/files/src/App.vue.template new file mode 100644 index 00000000..26d5731c --- /dev/null +++ b/libs/vite/src/generators/application/files/src/App.vue.template @@ -0,0 +1,27 @@ + + + + + \ No newline at end of file diff --git a/libs/vite/src/generators/application/files/src/assets/logo.png b/libs/vite/src/generators/application/files/src/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f3d2503fc2a44b5053b0837ebea6e87a2d339a43 GIT binary patch literal 6849 zcmaKRcUV(fvo}bjDT-7nLI_nlK}sT_69H+`qzVWDA|yaU?}j417wLi^B1KB1SLsC& zL0ag7$U(XW5YR7p&Ux?sP$d4lvMt8C^+TcQu4F zQqv!UF!I+kw)c0jhd6+g6oCr9P?7)?!qX1ui*iL{p}sKCAGuJ{{W)0z1pLF|=>h}& zt(2Lr0Z`2ig8<5i%Zk}cO5Fm=LByqGWaS`oqChZdEFmc`0hSb#gg|Aap^{+WKOYcj zHjINK)KDG%&s?Mt4CL(T=?;~U@bU2x_mLKN!#GJuK_CzbNw5SMEJorG!}_5;?R>@1 zSl)jns3WlU7^J%=(hUtfmuUCU&C3%8B5C^f5>W2Cy8jW3#{Od{lF1}|?c61##3dzA zsPlFG;l_FzBK}8>|H_Ru_H#!_7$UH4UKo3lKOA}g1(R&|e@}GINYVzX?q=_WLZCgh z)L|eJMce`D0EIwgRaNETDsr+?vQknSGAi=7H00r`QnI%oQnFxm`G2umXso9l+8*&Q z7WqF|$p49js$mdzo^BXpH#gURy=UO;=IMrYc5?@+sR4y_?d*~0^YP7d+y0{}0)zBM zIKVM(DBvICK#~7N0a+PY6)7;u=dutmNqK3AlsrUU9U`d;msiucB_|8|2kY=(7XA;G zwDA8AR)VCA#JOkxm#6oHNS^YVuOU;8p$N)2{`;oF|rQ?B~K$%rHDxXs+_G zF5|-uqHZvSzq}L;5Kcy_P+x0${33}Ofb6+TX&=y;;PkEOpz%+_bCw_{<&~ zeLV|!bP%l1qxywfVr9Z9JI+++EO^x>ZuCK);=$VIG1`kxK8F2M8AdC$iOe3cj1fo(ce4l-9 z7*zKy3={MixvUk=enQE;ED~7tv%qh&3lR<0m??@w{ILF|e#QOyPkFYK!&Up7xWNtL zOW%1QMC<3o;G9_S1;NkPB6bqbCOjeztEc6TsBM<(q9((JKiH{01+Ud=uw9B@{;(JJ z-DxI2*{pMq`q1RQc;V8@gYAY44Z!%#W~M9pRxI(R?SJ7sy7em=Z5DbuDlr@*q|25V)($-f}9c#?D%dU^RS<(wz?{P zFFHtCab*!rl(~j@0(Nadvwg8q|4!}L^>d?0al6}Rrv9$0M#^&@zjbfJy_n!%mVHK4 z6pLRIQ^Uq~dnyy$`ay51Us6WaP%&O;@49m&{G3z7xV3dLtt1VTOMYl3UW~Rm{Eq4m zF?Zl_v;?7EFx1_+#WFUXxcK78IV)FO>42@cm@}2I%pVbZqQ}3;p;sDIm&knay03a^ zn$5}Q$G!@fTwD$e(x-~aWP0h+4NRz$KlnO_H2c< z(XX#lPuW_%H#Q+c&(nRyX1-IadKR-%$4FYC0fsCmL9ky3 zKpxyjd^JFR+vg2!=HWf}2Z?@Td`0EG`kU?{8zKrvtsm)|7>pPk9nu@2^z96aU2<#` z2QhvH5w&V;wER?mopu+nqu*n8p~(%QkwSs&*0eJwa zMXR05`OSFpfyRb!Y_+H@O%Y z0=K^y6B8Gcbl?SA)qMP3Z+=C(?8zL@=74R=EVnE?vY!1BQy2@q*RUgRx4yJ$k}MnL zs!?74QciNb-LcG*&o<9=DSL>1n}ZNd)w1z3-0Pd^4ED1{qd=9|!!N?xnXjM!EuylY z5=!H>&hSofh8V?Jofyd!h`xDI1fYAuV(sZwwN~{$a}MX^=+0TH*SFp$vyxmUv7C*W zv^3Gl0+eTFgBi3FVD;$nhcp)ka*4gSskYIqQ&+M}xP9yLAkWzBI^I%zR^l1e?bW_6 zIn{mo{dD=)9@V?s^fa55jh78rP*Ze<3`tRCN4*mpO$@7a^*2B*7N_|A(Ve2VB|)_o z$=#_=aBkhe(ifX}MLT()@5?OV+~7cXC3r!%{QJxriXo9I%*3q4KT4Xxzyd{ z9;_%=W%q!Vw$Z7F3lUnY+1HZ*lO;4;VR2+i4+D(m#01OYq|L_fbnT;KN<^dkkCwtd zF7n+O7KvAw8c`JUh6LmeIrk4`F3o|AagKSMK3))_5Cv~y2Bb2!Ibg9BO7Vkz?pAYX zoI=B}+$R22&IL`NCYUYjrdhwjnMx_v=-Qcx-jmtN>!Zqf|n1^SWrHy zK|MwJ?Z#^>)rfT5YSY{qjZ&`Fjd;^vv&gF-Yj6$9-Dy$<6zeP4s+78gS2|t%Z309b z0^fp~ue_}i`U9j!<|qF92_3oB09NqgAoehQ`)<)dSfKoJl_A6Ec#*Mx9Cpd-p#$Ez z={AM*r-bQs6*z$!*VA4|QE7bf@-4vb?Q+pPKLkY2{yKsw{&udv_2v8{Dbd zm~8VAv!G~s)`O3|Q6vFUV%8%+?ZSVUa(;fhPNg#vab@J*9XE4#D%)$UU-T5`fwjz! z6&gA^`OGu6aUk{l*h9eB?opVdrHK>Q@U>&JQ_2pR%}TyOXGq_6s56_`U(WoOaAb+K zXQr#6H}>a-GYs9^bGP2Y&hSP5gEtW+GVC4=wy0wQk=~%CSXj=GH6q z-T#s!BV`xZVxm{~jr_ezYRpqqIcXC=Oq`b{lu`Rt(IYr4B91hhVC?yg{ol4WUr3v9 zOAk2LG>CIECZ-WIs0$N}F#eoIUEtZudc7DPYIjzGqDLWk_A4#(LgacooD z2K4IWs@N`Bddm-{%oy}!k0^i6Yh)uJ1S*90>|bm3TOZxcV|ywHUb(+CeX-o1|LTZM zwU>dY3R&U)T(}5#Neh?-CWT~@{6Ke@sI)uSuzoah8COy)w)B)aslJmp`WUcjdia-0 zl2Y}&L~XfA`uYQboAJ1;J{XLhYjH){cObH3FDva+^8ioOQy%Z=xyjGLmWMrzfFoH; zEi3AG`_v+%)&lDJE;iJWJDI@-X9K5O)LD~j*PBe(wu+|%ar~C+LK1+-+lK=t# z+Xc+J7qp~5q=B~rD!x78)?1+KUIbYr^5rcl&tB-cTtj+e%{gpZZ4G~6r15+d|J(ky zjg@@UzMW0k9@S#W(1H{u;Nq(7llJbq;;4t$awM;l&(2s+$l!Ay9^Ge|34CVhr7|BG z?dAR83smef^frq9V(OH+a+ki#q&-7TkWfFM=5bsGbU(8mC;>QTCWL5ydz9s6k@?+V zcjiH`VI=59P-(-DWXZ~5DH>B^_H~;4$)KUhnmGo*G!Tq8^LjfUDO)lASN*=#AY_yS zqW9UX(VOCO&p@kHdUUgsBO0KhXxn1sprK5h8}+>IhX(nSXZKwlNsjk^M|RAaqmCZB zHBolOHYBas@&{PT=R+?d8pZu zUHfyucQ`(umXSW7o?HQ3H21M`ZJal+%*)SH1B1j6rxTlG3hx1IGJN^M7{$j(9V;MZ zRKybgVuxKo#XVM+?*yTy{W+XHaU5Jbt-UG33x{u(N-2wmw;zzPH&4DE103HV@ER86 z|FZEmQb|&1s5#`$4!Cm}&`^{(4V}OP$bk`}v6q6rm;P!H)W|2i^e{7lTk2W@jo_9q z*aw|U7#+g59Fv(5qI`#O-qPj#@_P>PC#I(GSp3DLv7x-dmYK=C7lPF8a)bxb=@)B1 zUZ`EqpXV2dR}B&r`uM}N(TS99ZT0UB%IN|0H%DcVO#T%L_chrgn#m6%x4KE*IMfjX zJ%4veCEqbXZ`H`F_+fELMC@wuy_ch%t*+Z+1I}wN#C+dRrf2X{1C8=yZ_%Pt6wL_~ zZ2NN-hXOT4P4n$QFO7yYHS-4wF1Xfr-meG9Pn;uK51?hfel`d38k{W)F*|gJLT2#T z<~>spMu4(mul-8Q3*pf=N4DcI)zzjqAgbE2eOT7~&f1W3VsdD44Ffe;3mJp-V@8UC z)|qnPc12o~$X-+U@L_lWqv-RtvB~%hLF($%Ew5w>^NR82qC_0FB z)=hP1-OEx?lLi#jnLzH}a;Nvr@JDO-zQWd}#k^an$Kwml;MrD&)sC5b`s0ZkVyPkb zt}-jOq^%_9>YZe7Y}PhW{a)c39G`kg(P4@kxjcYfgB4XOOcmezdUI7j-!gs7oAo2o zx(Ph{G+YZ`a%~kzK!HTAA5NXE-7vOFRr5oqY$rH>WI6SFvWmahFav!CfRMM3%8J&c z*p+%|-fNS_@QrFr(at!JY9jCg9F-%5{nb5Bo~z@Y9m&SHYV`49GAJjA5h~h4(G!Se zZmK{Bo7ivCfvl}@A-ptkFGcWXAzj3xfl{evi-OG(TaCn1FAHxRc{}B|x+Ua1D=I6M z!C^ZIvK6aS_c&(=OQDZfm>O`Nxsw{ta&yiYPA~@e#c%N>>#rq)k6Aru-qD4(D^v)y z*>Rs;YUbD1S8^D(ps6Jbj0K3wJw>L4m)0e(6Pee3Y?gy9i0^bZO?$*sv+xKV?WBlh zAp*;v6w!a8;A7sLB*g-^<$Z4L7|5jXxxP1}hQZ<55f9<^KJ>^mKlWSGaLcO0=$jem zWyZkRwe~u{{tU63DlCaS9$Y4CP4f?+wwa(&1ou)b>72ydrFvm`Rj-0`kBJgK@nd(*Eh!(NC{F-@=FnF&Y!q`7){YsLLHf0_B6aHc# z>WIuHTyJwIH{BJ4)2RtEauC7Yq7Cytc|S)4^*t8Va3HR zg=~sN^tp9re@w=GTx$;zOWMjcg-7X3Wk^N$n;&Kf1RgVG2}2L-(0o)54C509C&77i zrjSi{X*WV=%C17((N^6R4Ya*4#6s_L99RtQ>m(%#nQ#wrRC8Y%yxkH;d!MdY+Tw@r zjpSnK`;C-U{ATcgaxoEpP0Gf+tx);buOMlK=01D|J+ROu37qc*rD(w`#O=3*O*w9?biwNoq3WN1`&Wp8TvKj3C z3HR9ssH7a&Vr<6waJrU zdLg!ieYz%U^bmpn%;(V%%ugMk92&?_XX1K@mwnVSE6!&%P%Wdi7_h`CpScvspMx?N zQUR>oadnG17#hNc$pkTp+9lW+MBKHRZ~74XWUryd)4yd zj98$%XmIL4(9OnoeO5Fnyn&fpQ9b0h4e6EHHw*l68j;>(ya`g^S&y2{O8U>1*>4zR zq*WSI_2o$CHQ?x0!wl9bpx|Cm2+kFMR)oMud1%n2=qn5nE&t@Fgr#=Zv2?}wtEz^T z9rrj=?IH*qI5{G@Rn&}^Z{+TW}mQeb9=8b<_a`&Cm#n%n~ zU47MvCBsdXFB1+adOO)03+nczfWa#vwk#r{o{dF)QWya9v2nv43Zp3%Ps}($lA02*_g25t;|T{A5snSY?3A zrRQ~(Ygh_ebltHo1VCbJb*eOAr;4cnlXLvI>*$-#AVsGg6B1r7@;g^L zFlJ_th0vxO7;-opU@WAFe;<}?!2q?RBrFK5U{*ai@NLKZ^};Ul}beukveh?TQn;$%9=R+DX07m82gP$=}Uo_%&ngV`}Hyv8g{u z3SWzTGV|cwQuFIs7ZDOqO_fGf8Q`8MwL}eUp>q?4eqCmOTcwQuXtQckPy|4F1on8l zP*h>d+cH#XQf|+6c|S{7SF(Lg>bR~l(0uY?O{OEVlaxa5@e%T&xju=o1`=OD#qc16 zSvyH*my(dcp6~VqR;o(#@m44Lug@~_qw+HA=mS#Z^4reBy8iV?H~I;{LQWk3aKK8$bLRyt$g?- +

{{ msg }}

+ +

+ Recommended IDE setup: + VSCode + + + Vetur + or + Volar + (if using + <script setup>) +

+ +

See README.md for more information.

+ +

+ Vite Docs | + Vue 3 Docs +

+ + +

+ Edit + components/HelloWorld.vue to test hot module replacement. +

+ + + + + diff --git a/libs/vite/src/generators/application/files/src/main.ts.template b/libs/vite/src/generators/application/files/src/main.ts.template new file mode 100644 index 00000000..684d0421 --- /dev/null +++ b/libs/vite/src/generators/application/files/src/main.ts.template @@ -0,0 +1,4 @@ +import { createApp } from 'vue'; +import App from './App.vue'; + +createApp(App).mount('#app'); diff --git a/libs/vite/src/generators/application/files/src/shims-vue.d.ts.template b/libs/vite/src/generators/application/files/src/shims-vue.d.ts.template new file mode 100644 index 00000000..e5f68219 --- /dev/null +++ b/libs/vite/src/generators/application/files/src/shims-vue.d.ts.template @@ -0,0 +1,6 @@ +declare module '*.vue' { + import { DefineComponent } from 'vue'; + // eslint-disable-next-line + const component: DefineComponent<{}, {}, any>; + export default component; +} diff --git a/libs/vite/src/generators/application/files/tests/unit/example.spec.ts.template b/libs/vite/src/generators/application/files/tests/unit/example.spec.ts.template new file mode 100644 index 00000000..116c8b75 --- /dev/null +++ b/libs/vite/src/generators/application/files/tests/unit/example.spec.ts.template @@ -0,0 +1,10 @@ +import { shallowMount } from '@vue/test-utils'; +import HelloWorld from '../../src/components/HelloWorld.vue'; + +describe('HelloWorld.vue', () => { + it('renders props.msg when passed', () => { + const msg = 'new message'; + const wrapper = shallowMount(HelloWorld, { props: { msg }}); + expect(wrapper.text()).toMatch(msg); + }); +}); diff --git a/libs/vite/src/generators/application/files/tsconfig.app.json.template b/libs/vite/src/generators/application/files/tsconfig.app.json.template new file mode 100644 index 00000000..d1e71c48 --- /dev/null +++ b/libs/vite/src/generators/application/files/tsconfig.app.json.template @@ -0,0 +1,17 @@ +{ + "extends": "<%= offsetFromRoot %>tsconfig.base.json", + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "node", + "strict": true, + "jsx": "preserve", + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "lib": ["esnext", "dom"], + "types": ["vite/client"] + },<% if (unitTestRunner === 'jest') { %> + "exclude": ["**/*.spec.ts", "**/*.spec.tsx"],<% } %> + "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/libs/vite/src/generators/application/files/tsconfig.json.template b/libs/vite/src/generators/application/files/tsconfig.json.template new file mode 100644 index 00000000..1ee22e07 --- /dev/null +++ b/libs/vite/src/generators/application/files/tsconfig.json.template @@ -0,0 +1,10 @@ +{ + "extends": "<%= offsetFromRoot %>tsconfig.base.json", + "include": [], + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/libs/vite/src/generators/application/files/vite.config.ts.template b/libs/vite/src/generators/application/files/vite.config.ts.template new file mode 100644 index 00000000..fa125b80 --- /dev/null +++ b/libs/vite/src/generators/application/files/vite.config.ts.template @@ -0,0 +1,24 @@ +import { appRootPath } from '@nrwl/tao/src/utils/app-root'; +import vue from '@vitejs/plugin-vue'; +import { defineConfig } from 'vite'; +import { join } from 'path'; +import baseTsConfig from '<%= offsetFromRoot %>tsconfig.base.json'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()], + root: __dirname, + build: { + outDir: '<%= offsetFromRoot %>dist/<%= projectRoot %>', + emptyOutDir: true, + }, + resolve: { + alias: Object.entries(baseTsConfig.compilerOptions.paths).reduce( + (acc, [key, paths]) => ({ + ...acc, + [key]: (paths as string[]).map((path) => join(appRootPath, path)), + }), + {} + ), + }, +}); diff --git a/libs/vite/src/generators/application/generator.spec.ts b/libs/vite/src/generators/application/generator.spec.ts new file mode 100644 index 00000000..81690d23 --- /dev/null +++ b/libs/vite/src/generators/application/generator.spec.ts @@ -0,0 +1,98 @@ +import { Tree, readProjectConfiguration, readJson } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { applicationGenerator } from './generator'; +import { ApplicationGeneratorSchema } from './schema'; + +describe('application generator', () => { + let appTree: Tree; + const options: ApplicationGeneratorSchema = { + name: 'my-app', + skipFormat: false, + unitTestRunner: 'jest', + e2eTestRunner: 'cypress', + }; + + beforeEach(() => { + appTree = createTreeWithEmptyWorkspace(); + }); + + it('should update workspace.json', async () => { + await applicationGenerator(appTree, options); + + const config = readProjectConfiguration(appTree, 'my-app'); + + expect(config.root).toBe('apps/my-app'); + expect(config.sourceRoot).toBe('apps/my-app/src'); + expect(config.targets.build.executor).toBe('@nx-plus/vite:build'); + expect(config.targets.build.options).toEqual({ + config: 'apps/my-app/vite.config.ts', + }); + expect(config.targets.serve.executor).toBe('@nx-plus/vite:server'); + expect(config.targets.serve.options).toEqual({ + config: 'apps/my-app/vite.config.ts', + }); + + expect(config.targets.lint.executor).toBe('@nrwl/linter:eslint'); + expect(config.targets.test.executor).toBe('@nrwl/jest:jest'); + + const e2eConfig = readProjectConfiguration(appTree, 'my-app-e2e'); + expect(e2eConfig).toBeDefined(); + }); + + it('should generate files', async () => { + await applicationGenerator(appTree, options); + + [ + 'apps/my-app/index.html', + 'apps/my-app/tsconfig.json', + 'apps/my-app/tsconfig.app.json', + 'apps/my-app/vite.config.ts', + 'apps/my-app/public/favicon.ico', + 'apps/my-app/src/assets/logo.png', + 'apps/my-app/src/components/HelloWorld.vue', + 'apps/my-app/src/App.vue', + 'apps/my-app/src/main.ts', + 'apps/my-app/src/shims-vue.d.ts', + 'apps/my-app/tests/unit/example.spec.ts', + ].forEach((path) => expect(appTree.exists(path)).toBeTruthy()); + }); + + it('should add postinstall script', async () => { + await applicationGenerator(appTree, options); + + expect(readJson(appTree, 'package.json').scripts.postinstall).toBe( + 'node node_modules/@nx-plus/vite/patch-nx-dep-graph.js' + ); + }); + + describe('--unitTestRunner none', () => { + it('should not generate test configuration', async () => { + await applicationGenerator(appTree, { + ...options, + unitTestRunner: 'none', + }); + const config = readProjectConfiguration(appTree, 'my-app'); + + expect(config.targets.test).toBeUndefined(); + + [ + 'apps/my-app/tsconfig.spec.json', + 'apps/my-app/jest.config.js', + 'apps/my-app/tests/unit/example.spec.ts', + ].forEach((path) => expect(appTree.exists(path)).toBeFalsy()); + + const tsconfigAppJson = readJson( + appTree, + 'apps/my-app/tsconfig.app.json' + ); + expect(tsconfigAppJson.exclude).toBeUndefined(); + + expect(appTree.read('apps/my-app/.eslintrc.js').toString()).not.toContain( + 'overrides:' + ); + + const tsConfigJson = readJson(appTree, 'apps/my-app/tsconfig.json'); + expect(tsConfigJson.references[1]).toBeUndefined(); + }); + }); +}); diff --git a/libs/vite/src/generators/application/generator.ts b/libs/vite/src/generators/application/generator.ts new file mode 100644 index 00000000..effbe3f1 --- /dev/null +++ b/libs/vite/src/generators/application/generator.ts @@ -0,0 +1,301 @@ +import { + addProjectConfiguration, + formatFiles, + generateFiles, + getWorkspaceLayout, + names, + offsetFromRoot, + Tree, + addDependenciesToPackageJson, + updateJson, + convertNxGenerator, + logger, +} from '@nrwl/devkit'; +import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial'; +import * as path from 'path'; +import { checkPeerDeps } from '../../utils'; +import { ApplicationGeneratorSchema } from './schema'; + +interface NormalizedSchema extends ApplicationGeneratorSchema { + projectName: string; + projectRoot: string; + projectDirectory: string; + parsedTags: string[]; +} + +function normalizeOptions( + host: Tree, + options: ApplicationGeneratorSchema +): NormalizedSchema { + const name = names(options.name).fileName; + const projectDirectory = options.directory + ? `${names(options.directory).fileName}/${name}` + : name; + const projectName = projectDirectory.replace(new RegExp('/', 'g'), '-'); + const projectRoot = `${getWorkspaceLayout(host).appsDir}/${projectDirectory}`; + const parsedTags = options.tags + ? options.tags.split(',').map((s) => s.trim()) + : []; + + return { + ...options, + name, + projectName, + projectRoot, + projectDirectory, + parsedTags, + }; +} + +function addFiles(host: Tree, options: NormalizedSchema) { + const templateOptions = { + ...options, + ...names(options.name), + offsetFromRoot: offsetFromRoot(options.projectRoot), + template: '', + }; + generateFiles( + host, + path.join(__dirname, 'files'), + options.projectRoot, + templateOptions + ); + if (options.unitTestRunner === 'none') { + const { path } = host + .listChanges() + .find(({ path }) => path.includes('example.spec.ts')); + host.delete(path); + } +} + +function getEslintConfig(options: NormalizedSchema) { + const eslintConfig = { + extends: [ + `${offsetFromRoot(options.projectRoot)}.eslintrc.json`, + `plugin:vue/vue3-essential`, + '@vue/typescript/recommended', + 'prettier', + ], + rules: {}, + ignorePatterns: ['!**/*'], + env: { + node: true, + }, + } as any; + + if (options.unitTestRunner === 'jest') { + eslintConfig.overrides = [ + { + files: ['**/*.spec.{j,t}s?(x)'], + env: { + jest: true, + }, + }, + ]; + } + + return eslintConfig; +} + +async function addEsLint(tree: Tree, options: NormalizedSchema) { + const { lintProjectGenerator, Linter } = await import('@nrwl/linter'); + const lintTask = await lintProjectGenerator(tree, { + linter: Linter.EsLint, + project: options.projectName, + eslintFilePatterns: [`${options.projectRoot}/**/*.{ts,tsx,vue}`], + skipFormat: true, + }); + + updateJson(tree, `${options.projectRoot}/.eslintrc.json`, (json) => { + json.extends.unshift(json.extends.pop()); + return json; + }); + + const content = JSON.stringify(getEslintConfig(options)); + const configPath = `${options.projectRoot}/.eslintrc.json`; + const newConfigPath = configPath.slice(0, -2); + tree.rename(configPath, newConfigPath); + tree.write(newConfigPath, `module.exports = ${content};`); + + const installTask = addDependenciesToPackageJson( + tree, + {}, + { + '@vue/eslint-config-prettier': '6.0.0', + '@vue/eslint-config-typescript': '^5.0.2', + 'eslint-plugin-prettier': '^3.1.3', + 'eslint-plugin-vue': '^7.0.0-0', + } + ); + + return [lintTask, installTask]; +} + +async function addCypress(tree: Tree, options: NormalizedSchema) { + const { cypressInitGenerator, cypressProjectGenerator } = await import( + '@nrwl/cypress' + ); + const { Linter } = await import('@nrwl/linter'); + const cypressInitTask = await cypressInitGenerator(tree); + const cypressTask = await cypressProjectGenerator(tree, { + project: options.projectName, + name: options.name + '-e2e', + directory: options.directory, + linter: Linter.EsLint, + js: false, + }); + + const appSpecPath = options.projectRoot + '-e2e/src/integration/app.spec.ts'; + tree.write( + appSpecPath, + tree + .read(appSpecPath) + .toString('utf-8') + .replace( + `Welcome to ${options.projectName}!`, + 'Hello Vue 3 + TypeScript + Vite' + ) + ); + + return [cypressInitTask, cypressTask]; +} + +async function addJest(tree: Tree, options: NormalizedSchema) { + const { jestProjectGenerator, jestInitGenerator } = await import( + '@nrwl/jest' + ); + const jestInitTask = await jestInitGenerator(tree, { babelJest: false }); + const jestTask = await jestProjectGenerator(tree, { + project: options.projectName, + setupFile: 'none', + skipSerializers: true, + supportTsx: true, + testEnvironment: 'jsdom', + babelJest: false, + }); + updateJson(tree, `${options.projectRoot}/tsconfig.spec.json`, (json) => { + json.include = json.include.filter((pattern) => !/\.jsx?$/.test(pattern)); + json.compilerOptions = { + ...json.compilerOptions, + jsx: 'preserve', + esModuleInterop: true, + allowSyntheticDefaultImports: true, + }; + return json; + }); + const content = `module.exports = { + displayName: '${options.projectName}', + preset: '${offsetFromRoot(options.projectRoot)}jest.preset.js', + transform: { + '^.+\\.vue$': 'vue3-jest', + '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': + 'jest-transform-stub', + '^.+\\.tsx?$': 'ts-jest', + }, + moduleFileExtensions: ["ts", "tsx", "vue", "js", "json"], + coverageDirectory: '${offsetFromRoot(options.projectRoot)}coverage/${ + options.projectRoot + }', + snapshotSerializers: ['jest-serializer-vue'], + globals: { + 'ts-jest': { + tsconfig: '${options.projectRoot}/tsconfig.spec.json', + }, + 'vue-jest': { + tsConfig: '${options.projectRoot}/tsconfig.spec.json', + } + }, +}; +`; + tree.write(`${options.projectRoot}/jest.config.js`, content); + + const installTask = addDependenciesToPackageJson( + tree, + {}, + { + '@vue/test-utils': '^2.0.0-0', + 'jest-serializer-vue': '^2.0.2', + 'jest-transform-stub': '^2.0.0', + 'vue3-jest': '^27.0.0-alpha.1', + } + ); + return [jestInitTask, jestTask, installTask]; +} + +function addPostInstall(host: Tree) { + return updateJson(host, 'package.json', (json) => { + const vuePostInstall = + 'node node_modules/@nx-plus/vite/patch-nx-dep-graph.js'; + const { postinstall } = json.scripts || {}; + if (postinstall) { + if (postinstall !== vuePostInstall) { + logger.warn( + "We couldn't add our postinstall script. Without it Nx's dependency graph won't support Vue files. For more information see https://github.com/ZachJW34/nx-plus/tree/master/libs/vite#nx-dependency-graph-support" + ); + } + return json; + } + json.scripts = { ...json.scripts, postinstall: vuePostInstall }; + return json; + }); +} + +export async function applicationGenerator( + host: Tree, + options: ApplicationGeneratorSchema +) { + checkPeerDeps(host, options); + const normalizedOptions = normalizeOptions(host, options); + addProjectConfiguration(host, normalizedOptions.projectName, { + root: normalizedOptions.projectRoot, + projectType: 'application', + sourceRoot: `${normalizedOptions.projectRoot}/src`, + targets: { + build: { + executor: '@nx-plus/vite:build', + options: { + config: `${normalizedOptions.projectRoot}/vite.config.ts`, + }, + }, + serve: { + executor: '@nx-plus/vite:server', + options: { + config: `${normalizedOptions.projectRoot}/vite.config.ts`, + }, + }, + }, + tags: normalizedOptions.parsedTags, + }); + addFiles(host, normalizedOptions); + const lintTasks = await addEsLint(host, normalizedOptions); + const cypressTasks = + options.e2eTestRunner === 'cypress' + ? await addCypress(host, normalizedOptions) + : []; + const jestTasks = + options.unitTestRunner === 'jest' + ? await addJest(host, normalizedOptions) + : []; + const installTask = addDependenciesToPackageJson( + host, + { vue: '^3.0.5' }, + { + '@vitejs/plugin-vue': '^2.0.0', + typescript: '^4.4.4', + vite: '^2.7.1', + } + ); + addPostInstall(host); + if (!normalizedOptions.skipFormat) { + await formatFiles(host); + } + + return runTasksInSerial( + ...lintTasks, + ...cypressTasks, + ...jestTasks, + installTask + ); +} + +export const applicationSchematic = convertNxGenerator(applicationGenerator); diff --git a/libs/vite/src/generators/application/schema.d.ts b/libs/vite/src/generators/application/schema.d.ts new file mode 100644 index 00000000..7e108a8e --- /dev/null +++ b/libs/vite/src/generators/application/schema.d.ts @@ -0,0 +1,8 @@ +export interface ApplicationGeneratorSchema { + name: string; + tags?: string; + directory?: string; + skipFormat: boolean; + unitTestRunner: 'jest' | 'none'; + e2eTestRunner: 'cypress' | 'none'; +} diff --git a/libs/vite/src/generators/application/schema.json b/libs/vite/src/generators/application/schema.json new file mode 100644 index 00000000..2c6f13b4 --- /dev/null +++ b/libs/vite/src/generators/application/schema.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://json-schema.org/schema", + "cli": "nx", + "id": "Application", + "title": "", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-prompt": "What name would you like to use?" + }, + "tags": { + "type": "string", + "description": "Add tags to the project (used for linting)", + "alias": "t" + }, + "directory": { + "type": "string", + "description": "A directory where the project is placed", + "alias": "d" + }, + "skipFormat": { + "type": "boolean", + "description": "Skip formatting files", + "default": false + }, + "unitTestRunner": { + "type": "string", + "enum": ["jest", "none"], + "description": "Test runner to use for unit tests", + "default": "jest" + }, + "e2eTestRunner": { + "type": "string", + "enum": ["cypress", "none"], + "description": "Test runner to use for end to end (e2e) tests", + "default": "cypress" + } + }, + "required": ["name"] +} diff --git a/libs/vite/src/index.ts b/libs/vite/src/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/libs/vite/src/utils.ts b/libs/vite/src/utils.ts new file mode 100644 index 00000000..0b30eaa8 --- /dev/null +++ b/libs/vite/src/utils.ts @@ -0,0 +1,80 @@ +import { logger, Tree } from '@nrwl/devkit'; +import * as path from 'path'; +import * as semver from 'semver'; +import { appRootPath } from './app-root'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const Module = require('module'); + +export function loadModule(request, context, force = false) { + try { + return createRequire(path.resolve(context, 'package.json'))(request); + } catch (e) { + const resolvedPath = require.resolve(request, { paths: [context] }); + if (resolvedPath) { + if (force) { + clearRequireCache(resolvedPath); + } + return require(resolvedPath); + } + } +} + +// https://github.com/benmosher/eslint-plugin-import/pull/1591 +// https://github.com/benmosher/eslint-plugin-import/pull/1602 +// Polyfill Node's `Module.createRequireFromPath` if not present (added in Node v10.12.0) +// Use `Module.createRequire` if available (added in Node v12.2.0) +const createRequire = + Module.createRequire || + Module.createRequireFromPath || + function (filename) { + const mod = new Module(filename, null); + mod.filename = filename; + mod.paths = Module._nodeModulePaths(path.dirname(filename)); + + mod._compile(`module.exports = require;`, filename); + + return mod.exports; + }; + +function clearRequireCache(id, map = new Map()) { + const module = require.cache[id]; + if (module) { + map.set(id, true); + // Clear children modules + module.children.forEach((child) => { + if (!map.get(child.id)) clearRequireCache(child.id, map); + }); + delete require.cache[id]; + } +} + +export function checkPeerDeps(tree: Tree, options): void { + const expectedVersion = '^12.0.0'; + const unmetPeerDeps = [ + ...(options.e2eTestRunner === 'cypress' ? ['@nrwl/cypress'] : []), + ...(options.unitTestRunner === 'jest' ? ['@nrwl/jest'] : []), + '@nrwl/linter', + '@nrwl/workspace', + ].filter((dep) => { + try { + const { version } = loadModule(`${dep}/package.json`, appRootPath, true); + return !semver.satisfies(version, expectedVersion); + } catch (err) { + return true; + } + }); + + if (unmetPeerDeps.length) { + logger.warn(` +You have the following unmet peer dependencies: + +${unmetPeerDeps + .map((dep) => `${dep}@${expectedVersion}\n`) + .join() + .split(',') + .join('')} +@nx-plus/vite may not work as expected. + `); + } +} diff --git a/libs/vite/tsconfig.json b/libs/vite/tsconfig.json new file mode 100644 index 00000000..62ebbd94 --- /dev/null +++ b/libs/vite/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/vite/tsconfig.lib.json b/libs/vite/tsconfig.lib.json new file mode 100644 index 00000000..037d796f --- /dev/null +++ b/libs/vite/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "exclude": ["**/*.spec.ts"], + "include": ["**/*.ts"] +} diff --git a/libs/vite/tsconfig.spec.json b/libs/vite/tsconfig.spec.json new file mode 100644 index 00000000..559410b9 --- /dev/null +++ b/libs/vite/tsconfig.spec.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.spec.js", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/libs/vue/nx-plus-vue.png b/libs/vue/nx-plus-vue.png deleted file mode 100644 index 1350f1a5757945c18af4a8a01cc7ab1e13277b7f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21403 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