feat(vue): init, app, component and lib generators (#19130)

Co-authored-by: Katerina Skroumpelou <sk.katherine@gmail.com>
This commit is contained in:
Jack Hsu 2023-09-13 15:05:10 -04:00 committed by GitHub
parent 6a847190af
commit 769974b45a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
98 changed files with 5389 additions and 406 deletions

View File

@ -139,7 +139,7 @@ Package manager to use
Type: `string`
Customizes the initial content of your workspace. Default presets include: ["apps", "empty", "core", "npm", "ts", "web-components", "angular-monorepo", "angular-standalone", "react-monorepo", "react-standalone", "next", "nextjs-standalone", "react-native", "expo", "nest", "express", "react", "angular", "node-standalone", "node-monorepo", "ts-standalone"]. To build your own see https://nx.dev/extending-nx/recipes/create-preset
Customizes the initial content of your workspace. Default presets include: ["apps", "empty", "core", "npm", "ts", "web-components", "angular-monorepo", "angular-standalone", "react-monorepo", "react-standalone", "vue-monorepo", "vue-standalone", "next", "nextjs-standalone", "react-native", "expo", "nest", "express", "react", "angular", "node-standalone", "node-monorepo", "ts-standalone"]. To build your own see https://nx.dev/extending-nx/recipes/create-preset
### routing

View File

@ -139,7 +139,7 @@ Package manager to use
Type: `string`
Customizes the initial content of your workspace. Default presets include: ["apps", "empty", "core", "npm", "ts", "web-components", "angular-monorepo", "angular-standalone", "react-monorepo", "react-standalone", "next", "nextjs-standalone", "react-native", "expo", "nest", "express", "react", "angular", "node-standalone", "node-monorepo", "ts-standalone"]. To build your own see https://nx.dev/extending-nx/recipes/create-preset
Customizes the initial content of your workspace. Default presets include: ["apps", "empty", "core", "npm", "ts", "web-components", "angular-monorepo", "angular-standalone", "react-monorepo", "react-standalone", "vue-monorepo", "vue-standalone", "next", "nextjs-standalone", "react-native", "expo", "nest", "express", "react", "angular", "node-standalone", "node-monorepo", "ts-standalone"]. To build your own see https://nx.dev/extending-nx/recipes/create-preset
### routing

View File

@ -2084,6 +2084,20 @@
}
]
},
{
"name": "vue",
"id": "vue",
"description": "Vue package.",
"itemList": [
{
"id": "overview",
"path": "/packages/vue",
"name": "Overview of the Nx Vue Plugin",
"description": "The Nx Plugin for Vue contains generators for managing Vue applications and libraries within an Nx workspace. This page also explains how to configure Vue on your Nx workspace.",
"file": "shared/packages/vue/vue-plugin"
}
]
},
{
"name": "webpack",
"id": "webpack",

View File

@ -361,6 +361,16 @@
"generators": ["init", "configuration", "vitest"]
}
},
{
"name": "vue",
"packageName": "vue",
"description": "The Vue plugin for Nx contains executors and generators for managing Vue applications and libraries within an Nx workspace. It provides:\n\n\n- Integration with libraries such as Jest, Cypress, and Storybook.\n\n- Generators for applications, libraries, components, hooks, and more.\n\n- Library build support for publishing packages to npm or other registries.\n\n- Utilities for automatic workspace refactoring.",
"path": "generated/packages/vite.json",
"schemas": {
"executors": [],
"generators": ["init", "library", "application", "component"]
}
},
{
"name": "web",
"packageName": "web",

View File

@ -0,0 +1,60 @@
---
title: Overview of the Nx Vue Plugin
description: The Nx Plugin for Vue contains generators for managing Vue applications and libraries within an Nx workspace. This page also explains how to configure Vue on your Nx workspace.
---
The Nx plugin for [Vue](https://vuejs.org/).
## Setting up a new Nx workspace with Vue
You can create a new workspace that uses Vue with one of the following commands:
- Generate a new monorepo with a Vue app set up with Vue
```shell
npx create-nx-workspace@latest --preset=vue
```
## Add Vue to an existing workspace
There are a number of ways to use Vue in your existing workspace.
### Install the `@nx/vue` plugin
{% tabs %}
{% tab label="npm" %}
```shell
npm install -D @nx/vue
```
{% /tab %}
{% tab label="yarn" %}
```shell
yarn add -D @nx/vue
```
{% /tab %}
{% tab label="pnpm" %}
```shell
pnpm install -D @nx/vue
```
{% /tab %}
{% /tabs %}
### Generate a new project using Vue
To generate a Vue application, run the following:
```bash
nx g @nx/vue:app my-app
```
To generate a Vue library, run the following:
```bash
nx g @nx/vue:lib my-lib
```

View File

@ -90,6 +90,7 @@ export function newProject({
`@nx/rollup`,
`@nx/react`,
`@nx/storybook`,
`@nx/vue`,
`@nx/vite`,
`@nx/web`,
`@nx/webpack`,

54
e2e/vue/src/vue.test.ts Normal file
View File

@ -0,0 +1,54 @@
import {
cleanupProject,
killPorts,
newProject,
runCLI,
runE2ETests,
uniq,
} from '@nx/e2e/utils';
describe('Vue Plugin', () => {
let proj: string;
beforeAll(() => {
proj = newProject({
unsetProjectNameAndRootFormat: false,
});
});
afterAll(() => cleanupProject());
it('should serve application in dev mode', async () => {
const app = uniq('app');
runCLI(
`generate @nx/vue:app ${app} --unitTestRunner=vitest --e2eTestRunner=playwright`
);
let result = runCLI(`test ${app}`);
expect(result).toContain(`Successfully ran target test for project ${app}`);
result = runCLI(`build ${app}`);
expect(result).toContain(
`Successfully ran target build for project ${app}`
);
if (runE2ETests()) {
const e2eResults = runCLI(`e2e ${app}-e2e --no-watch`);
expect(e2eResults).toContain('Successfully ran target e2e');
expect(await killPorts()).toBeTruthy();
}
}, 200_000);
it('should build library', async () => {
const lib = uniq('lib');
runCLI(
`generate @nx/vue:lib ${lib} --bundler=vite --unitTestRunner=vitest`
);
const result = runCLI(`build ${lib}`);
expect(result).toContain(
`Successfully ran target build for project ${lib}`
);
});
});

View File

@ -420,6 +420,38 @@ describe('create-nx-workspace', () => {
}, 90000);
}
});
it('should create a workspace with a single vue app at the root', () => {
const wsName = uniq('vue');
runCreateWorkspace(wsName, {
preset: 'vue-standalone',
appName: wsName,
style: 'css',
packageManager,
e2eTestRunner: 'none',
});
checkFilesExist('package.json');
checkFilesExist('project.json');
checkFilesExist('index.html');
checkFilesExist('src/main.ts');
checkFilesExist('src/App.vue');
expectCodeIsFormatted();
});
it('should be able to create an vue monorepo', () => {
const wsName = uniq('vue');
const appName = uniq('app');
runCreateWorkspace(wsName, {
preset: 'vue-monorepo',
appName,
style: 'css',
packageManager,
e2eTestRunner: 'none',
});
expectCodeIsFormatted();
});
});
describe('create-nx-workspace parent folder', () => {

View File

@ -62,6 +62,15 @@ interface AngularArguments extends BaseArguments {
e2eTestRunner: 'none' | 'cypress' | 'playwright';
}
interface VueArguments extends BaseArguments {
stack: 'vue';
workspaceType: 'standalone' | 'integrated';
appName: string;
// framework: 'none' | 'nuxt';
style: string;
e2eTestRunner: 'none' | 'cypress' | 'playwright';
}
interface NodeArguments extends BaseArguments {
stack: 'node';
workspaceType: 'standalone' | 'integrated';
@ -78,6 +87,7 @@ type Arguments =
| NoneArguments
| ReactArguments
| AngularArguments
| VueArguments
| NodeArguments
| UnknownStackArguments;
@ -347,7 +357,7 @@ async function determineFolder(
async function determineStack(
parsedArgs: yargs.Arguments<Arguments>
): Promise<'none' | 'react' | 'angular' | 'node' | 'unknown'> {
): Promise<'none' | 'react' | 'angular' | 'vue' | 'node' | 'unknown'> {
if (parsedArgs.preset) {
switch (parsedArgs.preset) {
case Preset.Angular:
@ -360,7 +370,9 @@ async function determineStack(
case Preset.NextJs:
case Preset.NextJsStandalone:
return 'react';
case Preset.VueStandalone:
case Preset.VueMonorepo:
return 'vue';
case Preset.Nest:
case Preset.NodeStandalone:
case Preset.Express:
@ -379,7 +391,7 @@ async function determineStack(
}
const { stack } = await enquirer.prompt<{
stack: 'none' | 'react' | 'angular' | 'node';
stack: 'none' | 'react' | 'angular' | 'node' | 'vue';
}>([
{
name: 'stack',
@ -394,6 +406,10 @@ async function determineStack(
name: `react`,
message: `React: Configures a React application with your framework of choice.`,
},
{
name: `vue`,
message: `Vue: Configures a Vue application with modern tooling.`,
},
{
name: `angular`,
message: `Angular: Configures a Angular application with modern tooling.`,
@ -419,6 +435,8 @@ async function determinePresetOptions(
return determineReactOptions(parsedArgs);
case 'angular':
return determineAngularOptions(parsedArgs);
case 'vue':
return determineVueOptions(parsedArgs);
case 'node':
return determineNodeOptions(parsedArgs);
default:
@ -589,6 +607,69 @@ async function determineReactOptions(
return { preset, style, appName, bundler, nextAppDir, e2eTestRunner };
}
async function determineVueOptions(
parsedArgs: yargs.Arguments<VueArguments>
): Promise<Partial<Arguments>> {
let preset: Preset;
let style: undefined | string = undefined;
let appName: string;
let e2eTestRunner: undefined | 'none' | 'cypress' | 'playwright' = undefined;
if (parsedArgs.preset) {
preset = parsedArgs.preset;
} else {
const workspaceType = await determineStandaloneOrMonorepo();
if (workspaceType === 'standalone') {
preset = Preset.VueStandalone;
} else {
preset = Preset.VueMonorepo;
}
}
if (preset === Preset.VueStandalone) {
appName = parsedArgs.appName ?? parsedArgs.name;
} else {
appName = await determineAppName(parsedArgs);
}
e2eTestRunner = await determineE2eTestRunner(parsedArgs);
if (parsedArgs.style) {
style = parsedArgs.style;
} else {
const reply = await enquirer.prompt<{ style: string }>([
{
name: 'style',
message: `Default stylesheet format`,
initial: 'css' as any,
type: 'autocomplete',
choices: [
{
name: 'css',
message: 'CSS',
},
{
name: 'scss',
message: 'SASS(.scss) [ http://sass-lang.com ]',
},
{
name: 'less',
message: 'LESS [ http://lesscss.org ]',
},
{
name: 'none',
message: 'None',
},
],
},
]);
style = reply.style;
}
return { preset, style, appName, e2eTestRunner };
}
async function determineAngularOptions(
parsedArgs: yargs.Arguments<AngularArguments>
): Promise<Partial<Arguments>> {
@ -847,7 +928,9 @@ async function determineStandaloneOrMonorepo(): Promise<
}
async function determineAppName(
parsedArgs: yargs.Arguments<ReactArguments | AngularArguments | NodeArguments>
parsedArgs: yargs.Arguments<
ReactArguments | AngularArguments | NodeArguments | VueArguments
>
): Promise<string> {
if (parsedArgs.appName) return parsedArgs.appName;

View File

@ -19,6 +19,10 @@ export const presetOptions: { name: Preset; message: string }[] = [
name: Preset.AngularMonorepo,
message: 'angular [a monorepo with a single Angular application]',
},
{
name: Preset.VueMonorepo,
message: 'vue [a monorepo with a single Vue application]',
},
{
name: Preset.NextJs,
message: 'next.js [a monorepo with a single Next.js application]',

View File

@ -9,6 +9,8 @@ export enum Preset {
AngularStandalone = 'angular-standalone',
ReactMonorepo = 'react-monorepo',
ReactStandalone = 'react-standalone',
VueMonorepo = 'vue-monorepo',
VueStandalone = 'vue-standalone',
NextJs = 'next',
NextJsStandalone = 'nextjs-standalone',
ReactNative = 'react-native',

View File

@ -63,6 +63,7 @@ describe('app', () => {
'vitest/importMeta',
'vite/client',
'node',
'vitest',
]);
});

View File

@ -115,9 +115,8 @@ export async function applicationGeneratorInternal(
addProject(host, options);
if (options.bundler === 'vite') {
const { viteConfigurationGenerator } = ensurePackage<
typeof import('@nx/vite')
>('@nx/vite', nxVersion);
const { createOrEditViteConfig, viteConfigurationGenerator } =
ensurePackage<typeof import('@nx/vite')>('@nx/vite', nxVersion);
// We recommend users use `import.meta.env.MODE` and other variables in their code to differentiate between production and development.
// See: https://vitejs.dev/guide/env-and-mode.html
if (
@ -138,6 +137,28 @@ export async function applicationGeneratorInternal(
skipFormat: true,
});
tasks.push(viteTask);
createOrEditViteConfig(
host,
{
project: options.projectName,
includeLib: false,
includeVitest: options.unitTestRunner === 'vitest',
inSourceTests: options.inSourceTests,
rollupOptionsExternal: [
`'react'`,
`'react-dom'`,
`'react/jsx-runtime'`,
],
rollupOptionsExternalString: `"'react', 'react-dom', 'react/jsx-runtime'"`,
imports: [
options.compiler === 'swc'
? `import react from '@vitejs/plugin-react-swc'`
: `import react from '@vitejs/plugin-react'`,
],
plugins: ['react()'],
},
false
);
} else if (options.bundler === 'webpack') {
const { webpackInitGenerator } = ensurePackage<
typeof import('@nx/webpack')
@ -167,10 +188,9 @@ export async function applicationGeneratorInternal(
}
if (options.bundler !== 'vite' && options.unitTestRunner === 'vitest') {
const { vitestGenerator } = ensurePackage<typeof import('@nx/vite')>(
'@nx/vite',
nxVersion
);
const { createOrEditViteConfig, vitestGenerator } = ensurePackage<
typeof import('@nx/vite')
>('@nx/vite', nxVersion);
const vitestTask = await vitestGenerator(host, {
uiFramework: 'react',
@ -180,6 +200,28 @@ export async function applicationGeneratorInternal(
skipFormat: true,
});
tasks.push(vitestTask);
createOrEditViteConfig(
host,
{
project: options.projectName,
includeLib: false,
includeVitest: true,
inSourceTests: options.inSourceTests,
rollupOptionsExternal: [
`'react'`,
`'react-dom'`,
`'react/jsx-runtime'`,
],
rollupOptionsExternalString: `"'react', 'react-dom', 'react/jsx-runtime'"`,
imports: [
options.compiler === 'swc'
? `import react from '@vitejs/plugin-react-swc'`
: `import react from '@vitejs/plugin-react'`,
],
plugins: ['react()'],
},
true
);
}
if (

View File

@ -80,6 +80,7 @@ describe('lib', () => {
'vitest/importMeta',
'vite/client',
'node',
'vitest',
]);
});

View File

@ -69,9 +69,8 @@ export async function libraryGeneratorInternal(host: Tree, schema: Schema) {
// Set up build target
if (options.buildable && options.bundler === 'vite') {
const { viteConfigurationGenerator } = ensurePackage<
typeof import('@nx/vite')
>('@nx/vite', nxVersion);
const { viteConfigurationGenerator, createOrEditViteConfig } =
ensurePackage<typeof import('@nx/vite')>('@nx/vite', nxVersion);
const viteTask = await viteConfigurationGenerator(host, {
uiFramework: 'react',
project: options.name,
@ -84,6 +83,28 @@ export async function libraryGeneratorInternal(host: Tree, schema: Schema) {
testEnvironment: 'jsdom',
});
tasks.push(viteTask);
createOrEditViteConfig(
host,
{
project: options.name,
includeLib: true,
includeVitest: options.unitTestRunner === 'vitest',
inSourceTests: options.inSourceTests,
rollupOptionsExternal: [
`'react'`,
`'react-dom'`,
`'react/jsx-runtime'`,
],
rollupOptionsExternalString: `"'react', 'react-dom', 'react/jsx-runtime'"`,
imports: [
options.compiler === 'swc'
? `import react from '@vitejs/plugin-react-swc'`
: `import react from '@vitejs/plugin-react'`,
],
plugins: ['react()'],
},
false
);
} else if (options.buildable && options.bundler === 'rollup') {
const rollupTask = await addRollupBuildTarget(host, options);
tasks.push(rollupTask);
@ -120,10 +141,9 @@ export async function libraryGeneratorInternal(host: Tree, schema: Schema) {
options.unitTestRunner === 'vitest' &&
options.bundler !== 'vite' // tests are already configured if bundler is vite
) {
const { vitestGenerator } = ensurePackage<typeof import('@nx/vite')>(
'@nx/vite',
nxVersion
);
const { vitestGenerator, createOrEditViteConfig } = ensurePackage<
typeof import('@nx/vite')
>('@nx/vite', nxVersion);
const vitestTask = await vitestGenerator(host, {
uiFramework: 'react',
project: options.name,
@ -133,6 +153,24 @@ export async function libraryGeneratorInternal(host: Tree, schema: Schema) {
testEnvironment: 'jsdom',
});
tasks.push(vitestTask);
createOrEditViteConfig(
host,
{
project: options.name,
includeLib: true,
includeVitest: true,
inSourceTests: options.inSourceTests,
rollupOptionsExternal: [
`'react'`,
`'react-dom'`,
`'react/jsx-runtime'`,
],
rollupOptionsExternalString: `"'react', 'react-dom', 'react/jsx-runtime'"`,
imports: [`import react from '@vitejs/plugin-react'`],
plugins: ['react()'],
},
true
);
}
if (options.component) {

View File

@ -1,24 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`@nx/vite:configuration library mode should add config for building library 1`] = `
"/// <reference types="vitest" />
"/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import dts from 'vite-plugin-dts';
import * as path from 'path';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
cacheDir: '../node_modules/.vite/my-lib',
plugins: [
react(),
nxViteTsPaths(),
dts({
entryRoot: 'src',
tsConfigFilePath: path.join(__dirname, 'tsconfig.lib.json'),
skipDiagnostics: true,
}),
react(),
nxViteTsPaths(),
],
// Uncomment this if you are using workers.
@ -48,24 +48,24 @@ export default defineConfig({
`;
exports[`@nx/vite:configuration library mode should set up non buildable library correctly 1`] = `
"/// <reference types="vitest" />
"/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import dts from 'vite-plugin-dts';
import * as path from 'path';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
cacheDir: '../../node_modules/.vite/react-lib-nonb-jest',
plugins: [
react(),
nxViteTsPaths(),
dts({
entryRoot: 'src',
tsConfigFilePath: path.join(__dirname, 'tsconfig.lib.json'),
skipDiagnostics: true,
}),
react(),
nxViteTsPaths(),
],
// Uncomment this if you are using workers.
@ -143,7 +143,7 @@ exports[`@nx/vite:configuration library mode should set up non buildable library
import * as path from 'path';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsConfigPaths from 'vite-tsconfig-paths';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
// Configuration for building your library.
@ -165,12 +165,8 @@ export default defineConfig({
},
},
plugins: [
...[
nxViteTsPaths(),
react(),
viteTsConfigPaths({
root: '../../',
}),
],
dts({
entryRoot: 'src',
tsConfigFilePath: path.join(__dirname, 'tsconfig.lib.json'),
@ -180,9 +176,7 @@ export default defineConfig({
test: {
globals: true,
cache: {
dir: '../../node_modules/.vitest',
},
cache: { dir: '../../node_modules/.vitest' },
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
},
@ -286,7 +280,7 @@ exports[`@nx/vite:configuration transform React app to use Vite by providing cus
`;
exports[`@nx/vite:configuration transform React app to use Vite should create vite.config file at the root of the app 1`] = `
"/// <reference types="vitest" />
"/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
@ -427,7 +421,7 @@ exports[`@nx/vite:configuration transform React app to use Vite should transform
`;
exports[`@nx/vite:configuration transform Web app to use Vite should create vite.config file at the root of the app 1`] = `
"/// <reference types="vitest" />
"/// <reference types='vitest' />
import { defineConfig } from 'vite';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
@ -555,7 +549,7 @@ exports[`@nx/vite:configuration transform Web app to use Vite should transform w
`;
exports[`@nx/vite:configuration vitest should create a vitest configuration if "includeVitest" is true 1`] = `
"/// <reference types="vitest" />
"/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';

View File

@ -350,7 +350,6 @@ describe('@nx/vite:configuration', () => {
const { Confirm } = require('enquirer');
const confirmSpy = jest.spyOn(Confirm.prototype, 'run');
confirmSpy.mockResolvedValue(true);
expect.assertions(2);
mockReactLibNonBuildableVitestRunnerGenerator(tree);

View File

@ -198,7 +198,32 @@ export async function viteConfigurationGenerator(
});
}
if (schema.uiFramework === 'react') {
createOrEditViteConfig(
tree,
{
project: schema.project,
includeLib: schema.includeLib,
includeVitest: schema.includeVitest,
inSourceTests: schema.inSourceTests,
rollupOptionsExternal: [
`'react'`,
`'react-dom'`,
`'react/jsx-runtime'`,
],
rollupOptionsExternalString: `"'react', 'react-dom', 'react/jsx-runtime'"`,
imports: [
schema.compiler === 'swc'
? `import react from '@vitejs/plugin-react-swc'`
: `import react from '@vitejs/plugin-react'`,
],
plugins: ['react()'],
},
false
);
} else {
createOrEditViteConfig(tree, schema, false, projectAlreadyHasViteTargets);
}
if (schema.includeVitest) {
const vitestTask = await vitestGenerator(tree, {

View File

@ -45,7 +45,7 @@ function checkDependenciesInstalled(host: Tree, schema: InitGeneratorSchema) {
devDependencies['happy-dom'] = happyDomVersion;
} else if (schema.testEnvironment === 'edge-runtime') {
devDependencies['@edge-runtime/vm'] = edgeRuntimeVmVersion;
} else if (schema.testEnvironment !== 'node') {
} else if (schema.testEnvironment !== 'node' && schema.testEnvironment) {
logger.info(
`A custom environment was provided: ${schema.testEnvironment}. You need to install it manually.`
);

View File

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`vitest generator insourceTests should add the insourceSource option in the vite config 1`] = `
"/// <reference types="vitest" />
"/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
@ -33,7 +33,7 @@ export default defineConfig({
`;
exports[`vitest generator vite.config should create correct vite.config.ts file for apps 1`] = `
"/// <reference types="vitest" />
"/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
@ -61,7 +61,7 @@ export default defineConfig({
`;
exports[`vitest generator vite.config should create correct vite.config.ts file for non buildable libs 1`] = `
"/// <reference types="vitest" />
"/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';

View File

@ -52,6 +52,26 @@ export async function vitestGenerator(
tasks.push(initTask);
if (!schema.skipViteConfig) {
if (schema.uiFramework === 'react') {
createOrEditViteConfig(
tree,
{
project: schema.project,
includeLib: projectType === 'library',
includeVitest: true,
inSourceTests: schema.inSourceTests,
rollupOptionsExternal: [
`'react'`,
`'react-dom'`,
`'react/jsx-runtime'`,
],
rollupOptionsExternalString: `"'react', 'react-dom', 'react/jsx-runtime'"`,
imports: [`import react from '@vitejs/plugin-react'`],
plugins: ['react()'],
},
true
);
} else {
createOrEditViteConfig(
tree,
{
@ -62,6 +82,7 @@ export async function vitestGenerator(
true
);
}
}
createFiles(tree, schema, root);
updateTsConfig(tree, schema, root);
@ -89,16 +110,11 @@ function updateTsConfig(
options: VitestGeneratorSchema,
projectRoot: string
) {
updateJson(tree, joinPathFragments(projectRoot, 'tsconfig.json'), (json) => {
if (
json.references &&
!json.references.some((r) => r.path === './tsconfig.spec.json')
) {
json.references.push({
path: './tsconfig.spec.json',
});
}
if (tree.exists(joinPathFragments(projectRoot, 'tsconfig.spec.json'))) {
updateJson(
tree,
joinPathFragments(projectRoot, 'tsconfig.spec.json'),
(json) => {
if (!json.compilerOptions?.types?.includes('vitest')) {
if (json.compilerOptions?.types) {
json.compilerOptions.types.push('vitest');
@ -108,7 +124,41 @@ function updateTsConfig(
}
}
return json;
}
);
updateJson(
tree,
joinPathFragments(projectRoot, 'tsconfig.json'),
(json) => {
if (
json.references &&
!json.references.some((r) => r.path === './tsconfig.spec.json')
) {
json.references.push({
path: './tsconfig.spec.json',
});
}
return json;
}
);
} else {
updateJson(
tree,
joinPathFragments(projectRoot, 'tsconfig.json'),
(json) => {
if (!json.compilerOptions?.types?.includes('vitest')) {
if (json.compilerOptions?.types) {
json.compilerOptions.types.push('vitest');
} else {
json.compilerOptions ??= {};
json.compilerOptions.types = ['vitest'];
}
}
return json;
}
);
}
if (options.inSourceTests) {
const tsconfigLibPath = joinPathFragments(projectRoot, 'tsconfig.lib.json');

View File

@ -96,6 +96,7 @@ describe('vitest generator', () => {
"vitest/importMeta",
"vite/client",
"node",
"vitest",
],
},
"extends": "./tsconfig.json",

View File

@ -1,19 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ensureViteConfigIsCorrect should add build and test options if defineConfig is empty 1`] = `
"import dts from 'vite-plugin-dts';
import { joinPathFragments } from '@nx/devkit';
"import dts from 'vite-plugin-dts';import { joinPathFragments } from '@nx/devkit'
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsConfigPaths from 'vite-tsconfig-paths';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
plugins: [react(),
nxViteTsPaths()],build: {
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
@ -27,18 +26,7 @@ import { joinPathFragments } from '@nx/devkit';
// External packages that should not be bundled into your library.
external: ["'react', 'react-dom', 'react/jsx-runtime'"]
}
},plugins: [
dts({
entryRoot: 'src',
tsConfigFilePath: joinPathFragments(__dirname, 'tsconfig.lib.json'),
skipDiagnostics: true,
}),
react(),
viteTsConfigPaths({
root: '../../',
}),
],
test: {
},test: {
globals: true,
cache: {
dir: '../node_modules/.vitest'
@ -50,11 +38,10 @@ import { joinPathFragments } from '@nx/devkit';
`;
exports[`ensureViteConfigIsCorrect should add build option but not update test option if test already setup 1`] = `
"import dts from 'vite-plugin-dts';
import { joinPathFragments } from '@nx/devkit';
"import dts from 'vite-plugin-dts';import { joinPathFragments } from '@nx/devkit'
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsConfigPaths from 'vite-tsconfig-paths';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
@ -74,18 +61,8 @@ import { defineConfig } from 'vite';
// External packages that should not be bundled into your library.
external: ["'react', 'react-dom', 'react/jsx-runtime'"]
}
},plugins: [
...[
react(),
viteTsConfigPaths({
root: '../../',
}),
],
dts({
entryRoot: 'src',
tsConfigFilePath: joinPathFragments(__dirname, 'tsconfig.lib.json'),
skipDiagnostics: true,
}),
},plugins: [react(),
nxViteTsPaths(),
],
test: {
@ -102,11 +79,10 @@ import { defineConfig } from 'vite';
`;
exports[`ensureViteConfigIsCorrect should add build options if build options don't exist 1`] = `
"import dts from 'vite-plugin-dts';
import { joinPathFragments } from '@nx/devkit';
"import dts from 'vite-plugin-dts';import { joinPathFragments } from '@nx/devkit'
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsConfigPaths from 'vite-tsconfig-paths';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
@ -126,18 +102,8 @@ import { defineConfig } from 'vite';
// External packages that should not be bundled into your library.
external: ["'react', 'react-dom', 'react/jsx-runtime'"]
}
},plugins: [
...[
react(),
viteTsConfigPaths({
root: '../../',
}),
],
dts({
entryRoot: 'src',
tsConfigFilePath: joinPathFragments(__dirname, 'tsconfig.lib.json'),
skipDiagnostics: true,
}),
},plugins: [react(),
nxViteTsPaths(),
],
test: {
@ -154,11 +120,10 @@ import { defineConfig } from 'vite';
`;
exports[`ensureViteConfigIsCorrect should add build options if defineConfig is not used 1`] = `
"import dts from 'vite-plugin-dts';
import { joinPathFragments } from '@nx/devkit';
"import dts from 'vite-plugin-dts';import { joinPathFragments } from '@nx/devkit'
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsConfigPaths from 'vite-tsconfig-paths';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default {
// Configuration for building your library.
@ -185,18 +150,8 @@ import { defineConfig } from 'vite';
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
},
plugins: [
...[
react(),
viteTsConfigPaths({
root: '../../',
}),
],
dts({
entryRoot: 'src',
tsConfigFilePath: joinPathFragments(__dirname, 'tsconfig.lib.json'),
skipDiagnostics: true,
}),
plugins: [react(),
nxViteTsPaths(),
],
};
"
@ -226,25 +181,14 @@ exports[`ensureViteConfigIsCorrect should add build options if it is using condi
`;
exports[`ensureViteConfigIsCorrect should add new build options if some build options already exist 1`] = `
"import dts from 'vite-plugin-dts';
import { joinPathFragments } from '@nx/devkit';
"import dts from 'vite-plugin-dts';import { joinPathFragments } from '@nx/devkit'
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsConfigPaths from 'vite-tsconfig-paths';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
plugins: [
...[
react(),
viteTsConfigPaths({
root: '../../',
}),
],
dts({
entryRoot: 'src',
tsConfigFilePath: joinPathFragments(__dirname, 'tsconfig.lib.json'),
skipDiagnostics: true,
}),
plugins: [react(),
nxViteTsPaths(),
],
test: {
@ -257,10 +201,10 @@ import { defineConfig } from 'vite';
},
build: {
...{
my: 'option',
},
...{"lib":{"entry":"src/index.ts","name":"my-app","fileName":"index","formats":["es","cjs"]},"rollupOptions":{"external":["'react', 'react-dom', 'react/jsx-runtime'"]}}
'my': 'option',
'lib': {"entry":"src/index.ts","name":"my-app","fileName":"index","formats":["es","cjs"]},
'rollupOptions': {"external":["'react', 'react-dom', 'react/jsx-runtime'"]},
}
});
@ -271,24 +215,16 @@ exports[`ensureViteConfigIsCorrect should not do anything if cannot understand s
exports[`ensureViteConfigIsCorrect should not do anything if project has everything setup already 1`] = `
"
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsConfigPaths from 'vite-tsconfig-paths';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import dts from 'vite-plugin-dts';
import { joinPathFragments } from '@nx/devkit';
export default defineConfig({
plugins: [
dts({
entryRoot: 'src',
tsConfigFilePath: joinPathFragments(__dirname, 'tsconfig.lib.json'),
skipDiagnostics: true,
}),
plugins: [dts({ entryRoot: 'src', tsConfigFilePath: joinPathFragments(__dirname, 'tsconfig.lib.json'), skipDiagnostics: true }),
react(),
viteTsConfigPaths({
root: '../../../',
}),
nxViteTsPaths(),
],
// Configuration for building your library.
@ -322,39 +258,30 @@ exports[`ensureViteConfigIsCorrect should not do anything if project has everyth
`;
exports[`ensureViteConfigIsCorrect should update both test and build options - keep existing settings 1`] = `
"import dts from 'vite-plugin-dts';
import { joinPathFragments } from '@nx/devkit';
"import dts from 'vite-plugin-dts';import { joinPathFragments } from '@nx/devkit'
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsConfigPaths from 'vite-tsconfig-paths';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
plugins: [
...[
react(),
viteTsConfigPaths({
root: '../../',
}),
],
dts({
entryRoot: 'src',
tsConfigFilePath: joinPathFragments(__dirname, 'tsconfig.lib.json'),
skipDiagnostics: true,
}),
plugins: [react(),
nxViteTsPaths(),
],
test: {
...{
my: 'option',
},
...{"globals":true,"cache":{"dir":"../node_modules/.vitest"},"environment":"jsdom","include":["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"]}
'my': 'option',
'globals': true,
'cache': {"dir":"../node_modules/.vitest"},
'environment': "jsdom",
'include': ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
},
build: {
...{
my: 'option',
},
...{"lib":{"entry":"src/index.ts","name":"my-app","fileName":"index","formats":["es","cjs"]},"rollupOptions":{"external":["'react', 'react-dom', 'react/jsx-runtime'"]}}
'my': 'option',
'lib': {"entry":"src/index.ts","name":"my-app","fileName":"index","formats":["es","cjs"]},
'rollupOptions': {"external":["'react', 'react-dom', 'react/jsx-runtime'"]},
}
});

View File

@ -441,14 +441,14 @@ export function moveAndEditIndexHtml(
const indexHtmlContent = tree.read(indexHtmlPath, 'utf8');
if (
!indexHtmlContent.includes(
`<script type="module" src="${mainPath}"></script>`
`<script type='module' src='${mainPath}'></script>`
)
) {
tree.write(
`${projectConfig.root}/index.html`,
indexHtmlContent.replace(
'</body>',
`<script type="module" src="${mainPath}"></script>
`<script type='module' src='${mainPath}'></script>
</body>`
)
);
@ -461,25 +461,37 @@ export function moveAndEditIndexHtml(
tree.write(
`${projectConfig.root}/index.html`,
`<!DOCTYPE html>
<html lang="en">
<html lang='en'>
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta charset='UTF-8' />
<link rel='icon' href='/favicon.ico' />
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<title>Vite</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="${mainPath}"></script>
<div id='root'></div>
<script type='module' src='${mainPath}'></script>
</body>
</html>`
);
}
}
export interface ViteConfigFileOptions {
project: string;
includeLib?: boolean;
includeVitest?: boolean;
inSourceTests?: boolean;
testEnvironment?: 'node' | 'jsdom' | 'happy-dom' | 'edge-runtime' | string;
rollupOptionsExternalString?: string;
rollupOptionsExternal?: string[];
imports?: string[];
plugins?: string[];
}
export function createOrEditViteConfig(
tree: Tree,
options: ViteConfigurationGeneratorSchema,
options: ViteConfigFileOptions,
onlyVitest: boolean,
projectAlreadyHasViteTargets?: TargetFlags
) {
@ -505,33 +517,32 @@ export function createOrEditViteConfig(
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: [${
options.uiFramework === 'react'
? "'react', 'react-dom', 'react/jsx-runtime'"
: ''
}]
external: [${options.rollupOptionsExternal ?? ''}]
}
},`
: ``;
const dtsPlugin = onlyVitest
? ''
: options.includeLib
? `dts({
entryRoot: 'src',
tsConfigFilePath: path.join(__dirname, 'tsconfig.lib.json'),
skipDiagnostics: true,
}),`
: '';
const imports: string[] = options.imports ? options.imports : [];
const dtsImportLine = onlyVitest
? ''
: options.includeLib
? `import dts from 'vite-plugin-dts';\nimport * as path from 'path';`
: '';
if (!onlyVitest && options.includeLib) {
imports.push(
`import dts from 'vite-plugin-dts'`,
`import * as path from 'path'`
);
}
let viteConfigContent = '';
const plugins = options.plugins
? [...options.plugins, `nxViteTsPaths()`]
: [`nxViteTsPaths()`];
if (!onlyVitest && options.includeLib) {
plugins.push(
`dts({ entryRoot: 'src', tsConfigFilePath: path.join(__dirname, 'tsconfig.lib.json'), skipDiagnostics: true })`
);
}
const testOption = options.includeVitest
? `test: {
globals: true,
@ -554,15 +565,6 @@ export function createOrEditViteConfig(
},`
: '';
const reactPluginImportLine =
options.uiFramework === 'react'
? options.compiler === 'swc'
? `import react from '@vitejs/plugin-react-swc';`
: `import react from '@vitejs/plugin-react';`
: '';
const reactPlugin = options.uiFramework === 'react' ? `react(),` : '';
const devServerOption = onlyVitest
? ''
: options.includeLib
@ -583,14 +585,6 @@ export function createOrEditViteConfig(
host: 'localhost',
},`;
const pluginOption = `
plugins: [
${dtsPlugin}
${reactPlugin}
nxViteTsPaths(),
],
`;
const workerOption = `
// Uncomment this if you are using workers.
// worker: {
@ -607,9 +601,8 @@ export function createOrEditViteConfig(
viteConfigPath,
options,
buildOption,
dtsPlugin,
dtsImportLine,
pluginOption,
imports,
plugins,
testOption,
cacheDir,
offsetFromRoot(projectConfig.root),
@ -619,17 +612,17 @@ export function createOrEditViteConfig(
}
viteConfigContent = `
/// <reference types="vitest" />
/// <reference types='vitest' />
import { defineConfig } from 'vite';
${reactPluginImportLine}
${imports.join(';\n')}${imports.length ? ';' : ''}
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
${dtsImportLine}
export default defineConfig({
${cacheDir}
${devServerOption}
${previewServerOption}
${pluginOption}
plugins: [${plugins.join(',\n')}],
${workerOption}
${buildOption}
${defineOption}
@ -774,21 +767,28 @@ export async function handleUnknownExecutors(projectName: string) {
function handleViteConfigFileExists(
tree: Tree,
viteConfigPath: string,
options: ViteConfigurationGeneratorSchema,
options: ViteConfigFileOptions,
buildOption: string,
dtsPlugin: string,
dtsImportLine: string,
pluginOption: string,
imports: string[],
plugins: string[],
testOption: string,
cacheDir: string,
offsetFromRoot: string,
projectAlreadyHasViteTargets?: TargetFlags
) {
if (projectAlreadyHasViteTargets.build && projectAlreadyHasViteTargets.test) {
if (
projectAlreadyHasViteTargets?.build &&
projectAlreadyHasViteTargets?.test
) {
return;
}
logger.info(`vite.config.ts already exists for project ${options.project}.`);
if (process.env.NX_VERBOSE_LOGGING === 'true') {
logger.info(
`vite.config.ts already exists for project ${options.project}.`
);
}
const buildOptionObject = {
lib: {
entry: 'src/index.ts',
@ -797,10 +797,7 @@ function handleViteConfigFileExists(
formats: ['es', 'cjs'],
},
rollupOptions: {
external:
options.uiFramework === 'react'
? ['react', 'react-dom', 'react/jsx-runtime']
: [],
external: options.rollupOptionsExternal ?? [],
},
};
@ -818,13 +815,12 @@ function handleViteConfigFileExists(
viteConfigPath,
buildOption,
buildOptionObject,
dtsPlugin,
dtsImportLine,
pluginOption,
imports,
plugins,
testOption,
testOptionObject,
cacheDir,
projectAlreadyHasViteTargets
projectAlreadyHasViteTargets ?? {}
);
if (!changed) {
@ -835,9 +831,5 @@ function handleViteConfigFileExists(
`
);
} else {
logger.info(`
Vite configuration file (${viteConfigPath}) has been updated with the required settings for the new target(s).
`);
}
}

View File

@ -2,14 +2,12 @@ export const noBuildOptions = `
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsConfigPaths from 'vite-tsconfig-paths';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
plugins: [
react(),
viteTsConfigPaths({
root: '../../',
}),
nxViteTsPaths(),
],
test: {
@ -28,14 +26,12 @@ export const someBuildOptions = `
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsConfigPaths from 'vite-tsconfig-paths';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
plugins: [
react(),
viteTsConfigPaths({
root: '../../',
}),
nxViteTsPaths(),
],
test: {
@ -58,7 +54,7 @@ export const noContentDefineConfig = `
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsConfigPaths from 'vite-tsconfig-paths';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({});
`;
@ -85,14 +81,12 @@ export const configNoDefineConfig = `
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsConfigPaths from 'vite-tsconfig-paths';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default {
plugins: [
react(),
viteTsConfigPaths({
root: '../../',
}),
nxViteTsPaths(),
],
};
`;
@ -101,14 +95,12 @@ export const noBuildOptionsHasTestOption = `
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsConfigPaths from 'vite-tsconfig-paths';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
plugins: [
react(),
viteTsConfigPaths({
root: '../../',
}),
nxViteTsPaths(),
],
test: {
@ -127,14 +119,12 @@ export const someBuildOptionsSomeTestOption = `
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsConfigPaths from 'vite-tsconfig-paths';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
plugins: [
react(),
viteTsConfigPaths({
root: '../../',
}),
nxViteTsPaths(),
],
test: {
@ -152,21 +142,15 @@ export const hasEverything = `
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsConfigPaths from 'vite-tsconfig-paths';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import dts from 'vite-plugin-dts';
import { joinPathFragments } from '@nx/devkit';
export default defineConfig({
plugins: [
dts({
entryRoot: 'src',
tsConfigFilePath: joinPathFragments(__dirname, 'tsconfig.lib.json'),
skipDiagnostics: true,
}),
dts({ entryRoot: 'src', tsConfigFilePath: joinPathFragments(__dirname, 'tsconfig.lib.json'), skipDiagnostics: true }),
react(),
viteTsConfigPaths({
root: '../../../',
}),
nxViteTsPaths(),
],
// Configuration for building your library.
@ -246,19 +230,9 @@ export const testOptionObject = {
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
};
export const dtsPlugin = `dts({
entryRoot: 'src',
tsConfigFilePath: joinPathFragments(__dirname, 'tsconfig.lib.json'),
skipDiagnostics: true,
}),`;
export const dtsImportLine = `import dts from 'vite-plugin-dts';\nimport { joinPathFragments } from '@nx/devkit';`;
export const imports = [
`import dts from 'vite-plugin-dts'`,
`import { joinPathFragments } from '@nx/devkit'`,
];
export const pluginOption = `
plugins: [
${dtsPlugin}
react(),
viteTsConfigPaths({
root: '../../',
}),
],
`;
export const plugins = [`react()`, `nxViteTsPaths()`];

View File

@ -537,15 +537,13 @@ export function mockReactLibNonBuildableVitestRunnerGenerator(
`/// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsConfigPaths from 'vite-tsconfig-paths';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
plugins: [
nxViteTsPaths(),
react(),
viteTsConfigPaths({
root: '../../',
}),
],
test: {

View File

@ -6,13 +6,12 @@ import {
buildOptionObject,
conditionalConfig,
configNoDefineConfig,
dtsImportLine,
dtsPlugin,
imports,
hasEverything,
noBuildOptions,
noBuildOptionsHasTestOption,
noContentDefineConfig,
pluginOption,
plugins,
someBuildOptions,
someBuildOptionsSomeTestOption,
testOption,
@ -34,9 +33,8 @@ describe('ensureViteConfigIsCorrect', () => {
'apps/my-app/vite.config.ts',
buildOption,
buildOptionObject,
dtsPlugin,
dtsImportLine,
pluginOption,
imports,
plugins,
testOption,
testOptionObject,
'',
@ -59,9 +57,8 @@ describe('ensureViteConfigIsCorrect', () => {
'apps/my-app/vite.config.ts',
buildOption,
buildOptionObject,
dtsPlugin,
dtsImportLine,
pluginOption,
imports,
plugins,
testOption,
testOptionObject,
'',
@ -84,9 +81,8 @@ describe('ensureViteConfigIsCorrect', () => {
'apps/my-app/vite.config.ts',
buildOption,
buildOptionObject,
dtsPlugin,
dtsImportLine,
pluginOption,
imports,
plugins,
testOption,
testOptionObject,
'',
@ -109,9 +105,8 @@ describe('ensureViteConfigIsCorrect', () => {
'apps/my-app/vite.config.ts',
buildOption,
buildOptionObject,
dtsPlugin,
dtsImportLine,
pluginOption,
imports,
plugins,
testOption,
testOptionObject,
'',
@ -134,9 +129,8 @@ describe('ensureViteConfigIsCorrect', () => {
'apps/my-app/vite.config.ts',
buildOption,
buildOptionObject,
dtsPlugin,
dtsImportLine,
pluginOption,
imports,
plugins,
testOption,
testOptionObject,
'',
@ -159,9 +153,8 @@ describe('ensureViteConfigIsCorrect', () => {
'apps/my-app/vite.config.ts',
buildOption,
buildOptionObject,
dtsPlugin,
dtsImportLine,
pluginOption,
imports,
plugins,
testOption,
testOptionObject,
'',
@ -178,9 +171,8 @@ describe('ensureViteConfigIsCorrect', () => {
'apps/my-app/vite.config.ts',
buildOption,
buildOptionObject,
dtsPlugin,
dtsImportLine,
pluginOption,
imports,
plugins,
testOption,
testOptionObject,
'',
@ -197,9 +189,8 @@ describe('ensureViteConfigIsCorrect', () => {
'apps/my-app/vite.config.ts',
buildOption,
buildOptionObject,
dtsPlugin,
dtsImportLine,
pluginOption,
imports,
plugins,
testOption,
testOptionObject,
'',
@ -216,9 +207,8 @@ describe('ensureViteConfigIsCorrect', () => {
'apps/my-app/vite.config.ts',
buildOption,
buildOptionObject,
dtsPlugin,
dtsImportLine,
pluginOption,
imports,
plugins,
testOption,
testOptionObject,
'',

View File

@ -1,16 +1,20 @@
import { applyChangesToString, ChangeType, Tree } from '@nx/devkit';
import { findNodes } from '@nx/js';
import { TargetFlags } from './generator-utils';
import type { Node, ReturnStatement } from 'typescript';
import type {
ArrayLiteralExpression,
Node,
PropertyAssignment,
ReturnStatement,
} from 'typescript';
export function ensureViteConfigIsCorrect(
tree: Tree,
path: string,
buildConfigString: string,
buildConfigObject: {},
dtsPlugin: string,
dtsImportLine: string,
pluginOption: string,
imports: string[],
plugins: string[],
testConfigString: string,
testConfigObject: {},
cacheDir: string,
@ -30,13 +34,6 @@ export function ensureViteConfigIsCorrect(
}
if (!projectAlreadyHasViteTargets?.build && buildConfigString?.length) {
updatedContent = handlePluginNode(
updatedContent ?? fileContent,
dtsPlugin,
dtsImportLine,
pluginOption
);
updatedContent = handleBuildOrTestNode(
updatedContent ?? fileContent,
buildConfigString,
@ -45,12 +42,17 @@ export function ensureViteConfigIsCorrect(
);
}
updatedContent =
handlePluginNode(updatedContent ?? fileContent, imports, plugins) ??
updatedContent;
if (cacheDir?.length) {
updatedContent = handleCacheDirNode(
updatedContent ?? fileContent,
cacheDir
);
}
if (updatedContent) {
tree.write(path, updatedContent);
return true;
@ -66,20 +68,36 @@ function handleBuildOrTestNode(
name: 'build' | 'test'
): string | undefined {
const { tsquery } = require('@phenomnomnominal/tsquery');
const buildNode = tsquery.query(
const buildOrTestNode = tsquery.query(
updatedFileContent,
`PropertyAssignment:has(Identifier[name="${name}"])`
);
if (buildNode.length) {
if (buildOrTestNode.length) {
return tsquery.replace(
updatedFileContent,
`PropertyAssignment:has(Identifier[name="${name}"])`,
(node: Node) => {
const found = tsquery.query(node, 'ObjectLiteralExpression');
(node: PropertyAssignment) => {
const existingProperties = tsquery.query(
node.initializer,
'PropertyAssignment'
) as PropertyAssignment[];
let updatedPropsString = '';
for (const prop of existingProperties) {
const propName = prop.name.getText();
if (!configContentObject[propName] && propName !== 'dir') {
updatedPropsString += `'${propName}': ${prop.initializer.getText()},\n`;
}
}
for (const [propName, propValue] of Object.entries(
configContentObject
)) {
updatedPropsString += `'${propName}': ${JSON.stringify(
propValue
)},\n`;
}
return `${name}: {
...${found?.[0].getText()},
...${JSON.stringify(configContentObject)}
${updatedPropsString}
}`;
}
);
@ -173,7 +191,6 @@ function transformCurrentBuildObject(
const currentBuildObjectStart = returnStatements[index].getStart();
const currentBuildObjectEnd = returnStatements[index].getEnd();
const newReturnObject = tsquery.replace(
returnStatements[index].getText(),
'ObjectLiteralExpression',
@ -209,7 +226,6 @@ function transformConditionalConfig(
const { tsquery } = require('@phenomnomnominal/tsquery');
const { SyntaxKind } = require('typescript');
const functionBlock = tsquery.query(conditionalConfig[0], 'Block');
const ifStatement = tsquery.query(functionBlock?.[0], 'IfStatement');
const binaryExpressions = tsquery.query(ifStatement?.[0], 'BinaryExpression');
@ -235,7 +251,6 @@ function transformConditionalConfig(
if (!buildExists) {
if (serveExists && elseKeywordExists) {
// build options live inside the else block
return (
transformCurrentBuildObject(
returnStatements?.length - 1,
@ -278,12 +293,10 @@ function transformConditionalConfig(
function handlePluginNode(
appFileContent: string,
dtsPlugin: string,
dtsImportLine: string,
pluginOption: string
imports: string[],
plugins: string[]
): string | undefined {
const { tsquery } = require('@phenomnomnominal/tsquery');
const file = tsquery.ast(appFileContent);
const pluginsNode = tsquery.query(
file,
@ -297,11 +310,29 @@ function handlePluginNode(
file.getText(),
'PropertyAssignment:has(Identifier[name="plugins"])',
(node: Node) => {
const found = tsquery.query(node, 'ArrayLiteralExpression');
return `plugins: [
...${found?.[0].getText()},
${dtsPlugin}
]`;
const found = tsquery.query(
node,
'ArrayLiteralExpression'
) as ArrayLiteralExpression[];
let updatedPluginsString = '';
const existingPluginNodes = found?.[0].elements ?? [];
for (const plugin of existingPluginNodes) {
updatedPluginsString += `${plugin.getText()},\n`;
}
for (const plugin of plugins) {
if (
!existingPluginNodes?.some((node) =>
node.getText().includes(plugin)
)
) {
updatedPluginsString += `${plugin},\n`;
}
}
return `plugins: [${updatedPluginsString}]`;
}
);
writeFile = true;
@ -335,7 +366,7 @@ function handlePluginNode(
{
type: ChangeType.Insert,
index: propertyAssignments[0].getStart(),
text: pluginOption,
text: `plugins: [${plugins.join(',\n')}],`,
},
]);
writeFile = true;
@ -344,7 +375,7 @@ function handlePluginNode(
{
type: ChangeType.Insert,
index: foundDefineConfig[0].getStart() + 14,
text: pluginOption,
text: `plugins: [${plugins.join(',\n')}],`,
},
]);
writeFile = true;
@ -364,7 +395,7 @@ function handlePluginNode(
{
type: ChangeType.Insert,
index: startOfObject + 1,
text: pluginOption,
text: `plugins: [${plugins.join(',\n')}],`,
},
]);
writeFile = true;
@ -373,14 +404,27 @@ function handlePluginNode(
}
}
}
if (writeFile) {
if (!appFileContent.includes(`import dts from 'vite-plugin-dts'`)) {
return dtsImportLine + '\n' + appFileContent;
const filteredImports = filterImport(appFileContent, imports);
return filteredImports.join(';') + '\n' + appFileContent;
}
return appFileContent;
}
return appFileContent;
function filterImport(appFileContent: string, imports: string[]): string[] {
const { tsquery } = require('@phenomnomnominal/tsquery');
const file = tsquery.ast(appFileContent);
const importNodes = tsquery.query(
file,
':matches(ImportDeclaration, VariableStatement)'
);
const importsArrayExisting = importNodes?.map((node) => {
return node.getText().slice(0, -1);
});
return imports.filter((importString) => {
return !importsArrayExisting?.includes(importString);
});
}
function handleCacheDirNode(appFileContent: string, cacheDir: string): string {

View File

@ -29,7 +29,12 @@
"error",
{
"buildTargets": ["build-base"],
"ignoredDependencies": ["nx", "typescript"]
"ignoredDependencies": [
"nx",
"typescript",
"@nx/cypress",
"@nx/playwright"
]
}
]
}

View File

@ -1,5 +1,33 @@
{
"name": "Nx Vue",
"version": "0.1",
"generators": {}
"generators": {
"init": {
"factory": "./src/generators/init/init",
"schema": "./src/generators/init/schema.json",
"description": "Initialize the `@nx/vue` plugin.",
"aliases": ["ng-add"],
"hidden": true
},
"application": {
"factory": "./src/generators/application/application",
"schema": "./src/generators/application/schema.json",
"aliases": ["app"],
"description": "Create a Vue application."
},
"library": {
"factory": "./src/generators/library/library",
"schema": "./src/generators/library/schema.json",
"aliases": ["lib"],
"x-type": "library",
"description": "Create a Vue library."
},
"component": {
"factory": "./src/generators/component/component",
"schema": "./src/generators/component/schema.json",
"aliases": ["c"],
"x-type": "component",
"description": "Create a Vue component."
}
}
}

View File

@ -0,0 +1,6 @@
export * from './src/utils/versions';
export { applicationGenerator } from './src/generators/application/application';
export { libraryGenerator } from './src/generators/library/library';
export { componentGenerator } from './src/generators/component/component';
export { type InitSchema } from './src/generators/init/schema';
export { vueInitGenerator } from './src/generators/init/init';

View File

@ -28,14 +28,19 @@
"migrations": "./migrations.json"
},
"dependencies": {
"tslib": "^2.3.0"
"tslib": "^2.3.0",
"@nx/devkit": "file:../devkit",
"@nx/jest": "file:../jest",
"@nx/js": "file:../js",
"@nx/linter": "file:../linter",
"@nx/vite": "file:../vite",
"@nx/web": "file:../web",
"@phenomnomnominal/tsquery": "~5.0.1"
},
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"nx": ">= 15 <= 17"
},
"peerDependencies": {},
"exports": {
".": "./index.js",
"./package.json": "./package.json",

View File

@ -0,0 +1,226 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`application generator should set up project correctly with given options 1`] = `
"{
"root": true,
"ignorePatterns": ["**/*"],
"plugins": ["@nx"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx", "*.vue"],
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{
"sourceTag": "*",
"onlyDependOnLibsWithTags": ["*"]
}
]
}
]
}
},
{
"files": ["*.ts", "*.tsx"],
"extends": ["plugin:@nx/typescript"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"extends": ["plugin:@nx/javascript"],
"rules": {}
}
]
}
"
`;
exports[`application generator should set up project correctly with given options 2`] = `
"import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
cacheDir: '../node_modules/.vite/test',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [nxViteTsPaths(), vue()],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
test: {
globals: true,
cache: { dir: '../node_modules/.vitest' },
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
},
});
"
`;
exports[`application generator should set up project correctly with given options 3`] = `
"{
"name": "test",
"$schema": "../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"sourceRoot": "test/src",
"targets": {
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["test/**/*.{ts,tsx,js,jsx,vue}"]
}
},
"build": {
"executor": "@nx/vite:build",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
"outputPath": "dist/test",
"skipTypeCheck": true
},
"configurations": {
"development": {
"mode": "development"
},
"production": {
"mode": "production"
}
}
},
"serve": {
"executor": "@nx/vite:dev-server",
"defaultConfiguration": "development",
"options": {
"buildTarget": "test:build"
},
"configurations": {
"development": {
"buildTarget": "test:build:development",
"hmr": true
},
"production": {
"buildTarget": "test:build:production",
"hmr": false
}
}
},
"preview": {
"executor": "@nx/vite:preview-server",
"defaultConfiguration": "development",
"options": {
"buildTarget": "test:build"
},
"configurations": {
"development": {
"buildTarget": "test:build:development"
},
"production": {
"buildTarget": "test:build:production"
}
}
},
"test": {
"executor": "@nx/vite:test",
"outputs": ["{options.reportsDirectory}"],
"options": {
"passWithNoTests": true,
"reportsDirectory": "../coverage/test"
}
},
"serve-static": {
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "test:build"
}
}
}
}
"
`;
exports[`application generator should set up project correctly with given options 4`] = `
"{
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/eslint-config-typescript",
"@vue/eslint-config-prettier/skip-formatting",
"../.eslintrc.json"
],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx", "*.vue"],
"rules": {}
}
]
}
"
`;
exports[`application generator should set up project correctly with given options 5`] = `
"import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import App from '../App.vue';
describe('App', () => {
it('renders properly', () => {
const wrapper = mount(App, {});
expect(wrapper.text()).toContain('Welcome test 👋');
});
});
"
`;
exports[`application generator should set up project correctly with given options 6`] = `
[
".eslintignore",
".eslintrc.json",
".prettierignore",
".prettierrc",
"nx.json",
"package.json",
"test-e2e/.eslintrc.json",
"test-e2e/cypress.config.ts",
"test-e2e/project.json",
"test-e2e/src/e2e/app.cy.ts",
"test-e2e/src/fixtures/example.json",
"test-e2e/src/support/app.po.ts",
"test-e2e/src/support/commands.ts",
"test-e2e/src/support/e2e.ts",
"test-e2e/tsconfig.json",
"test/.eslintrc.json",
"test/index.html",
"test/project.json",
"test/src/__tests__/App.spec.ts",
"test/src/App.vue",
"test/src/components/NxWelcome.vue",
"test/src/main.ts",
"test/src/styles.css",
"test/tsconfig.app.json",
"test/tsconfig.json",
"test/tsconfig.spec.json",
"test/vite.config.ts",
"tsconfig.base.json",
]
`;

View File

@ -0,0 +1,50 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Tree, readProjectConfiguration } from '@nx/devkit';
import { applicationGenerator } from './application';
import { Schema } from './schema';
describe('application generator', () => {
let tree: Tree;
const options: Schema = { name: 'test' } as Schema;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
it('should run successfully', async () => {
await applicationGenerator(tree, options);
const config = readProjectConfiguration(tree, 'test');
expect(config).toBeDefined();
});
it('should set up project correctly with given options', async () => {
await applicationGenerator(tree, { ...options, unitTestRunner: 'vitest' });
expect(tree.read('.eslintrc.json', 'utf-8')).toMatchSnapshot();
expect(tree.read('test/vite.config.ts', 'utf-8')).toMatchSnapshot();
expect(tree.read('test/project.json', 'utf-8')).toMatchSnapshot();
expect(tree.read('test/.eslintrc.json', 'utf-8')).toMatchSnapshot();
expect(
tree.read('test/src/__tests__/App.spec.ts', 'utf-8')
).toMatchSnapshot();
expect(listFiles(tree)).toMatchSnapshot();
});
it('should not use stylesheet if --style=none', async () => {
await applicationGenerator(tree, { ...options, style: 'none' });
expect(tree.exists('test/src/style.none')).toBeFalsy();
expect(tree.read('test/src/main.ts', 'utf-8')).not.toContain('styles.none');
});
});
function listFiles(tree: Tree): string[] {
const files = new Set<string>();
tree.listChanges().forEach((change) => {
if (change.type !== 'DELETE') {
files.add(change.path);
}
});
return Array.from(files).sort((a, b) => a.localeCompare(b));
}

View File

@ -0,0 +1,75 @@
import {
addProjectConfiguration,
formatFiles,
GeneratorCallback,
runTasksInSerial,
toJS,
Tree,
} from '@nx/devkit';
import { Linter } from '@nx/linter';
import { Schema } from './schema';
import { normalizeOptions } from './lib/normalize-options';
import { vueInitGenerator } from '../init/init';
import { addLinting } from '../../utils/add-linting';
import { addE2e } from './lib/add-e2e';
import { createApplicationFiles } from './lib/create-application-files';
import { addVite } from './lib/add-vite';
import { addJest } from './lib/add-jest';
import { extractTsConfigBase } from '../../utils/create-ts-config';
export async function applicationGenerator(
tree: Tree,
_options: Schema
): Promise<GeneratorCallback> {
const options = await normalizeOptions(tree, _options);
const tasks: GeneratorCallback[] = [];
addProjectConfiguration(tree, options.name, {
root: options.appProjectRoot,
projectType: 'application',
sourceRoot: `${options.appProjectRoot}/src`,
targets: {},
});
tasks.push(
await vueInitGenerator(tree, {
...options,
skipFormat: true,
})
);
extractTsConfigBase(tree);
createApplicationFiles(tree, options);
tasks.push(
await addLinting(
tree,
{
name: options.projectName,
projectRoot: options.appProjectRoot,
linter: options.linter ?? Linter.EsLint,
unitTestRunner: options.unitTestRunner,
skipPackageJson: options.skipPackageJson,
setParserOptionsProject: options.setParserOptionsProject,
rootProject: options.rootProject,
},
'app'
)
);
tasks.push(await addVite(tree, options));
if (options.unitTestRunner === 'jest')
tasks.push(await addJest(tree, options));
tasks.push(await addE2e(tree, options));
if (options.js) toJS(tree);
if (!options.skipFormat) await formatFiles(tree);
return runTasksInSerial(...tasks);
}
export default applicationGenerator;

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %></title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -0,0 +1,53 @@
<script setup lang="ts">
<% if (routing) { %>
import { RouterLink, RouterView } from 'vue-router';
<% } %>
import NxWelcome from './components/NxWelcome.vue';
</script>
<template>
<% if (routing) { %>
<header>
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
</nav>
</header>
<RouterView />
<% } else { %>
<NxWelcome title="<%= title %>" />
<% } %>
</template>
<% if (routing && style !== 'none') { %>
<style scoped lang="<%= style %>">
header {
line-height: 1.5;
max-width: 100vw;
}
nav > a {
padding-left: 1rem;
padding-right: 1rem;
}
@media (min-width: 768px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
margin-left: auto;
margin-right: auto;
max-width: 768px;
}
nav {
text-align: left;
font-size: 1rem;
padding: 1rem 0;
margin-top: 1rem;
}
}
</style>
<% } %>

View File

@ -0,0 +1,12 @@
<% if ( unitTestRunner === 'vitest' ) { %>
import { describe, it, expect } from 'vitest'
<% } %>
import { mount } from '@vue/test-utils'
import App from '../App.vue';
describe('App', () => {
it('renders properly', () => {
const wrapper = mount(App, {})
expect(wrapper.text()).toContain('Welcome <%= title %> 👋')
})
});

View File

@ -0,0 +1,793 @@
<script setup lang="ts">
defineProps<{
title: string
}>()
</script>
<template>
<div className="wrapper">
<div className="container">
<div id="welcome">
<h1>
<span> Hello there, </span>
Welcome {{ title }} 👋
</h1>
</div>
<div id="hero" className="rounded">
<div className="text-container">
<h2>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"
/>
</svg>
<span>You&apos;re up and running</span>
</h2>
<a href="#commands"> What&apos;s next? </a>
</div>
<div className="logo-container">
<svg
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M11.987 14.138l-3.132 4.923-5.193-8.427-.012 8.822H0V4.544h3.691l5.247 8.833.005-3.998 3.044 4.759zm.601-5.761c.024-.048 0-3.784.008-3.833h-3.65c.002.059-.005 3.776-.003 3.833h3.645zm5.634 4.134a2.061 2.061 0 0 0-1.969 1.336 1.963 1.963 0 0 1 2.343-.739c.396.161.917.422 1.33.283a2.1 2.1 0 0 0-1.704-.88zm3.39 1.061c-.375-.13-.8-.277-1.109-.681-.06-.08-.116-.17-.176-.265a2.143 2.143 0 0 0-.533-.642c-.294-.216-.68-.322-1.18-.322a2.482 2.482 0 0 0-2.294 1.536 2.325 2.325 0 0 1 4.002.388.75.75 0 0 0 .836.334c.493-.105.46.36 1.203.518v-.133c-.003-.446-.246-.55-.75-.733zm2.024 1.266a.723.723 0 0 0 .347-.638c-.01-2.957-2.41-5.487-5.37-5.487a5.364 5.364 0 0 0-4.487 2.418c-.01-.026-1.522-2.39-1.538-2.418H8.943l3.463 5.423-3.379 5.32h3.54l1.54-2.366 1.568 2.366h3.541l-3.21-5.052a.7.7 0 0 1-.084-.32 2.69 2.69 0 0 1 2.69-2.691h.001c1.488 0 1.736.89 2.057 1.308.634.826 1.9.464 1.9 1.541a.707.707 0 0 0 1.066.596zm.35.133c-.173.372-.56.338-.755.639-.176.271.114.412.114.412s.337.156.538-.311c.104-.231.14-.488.103-.74z" />
</svg>
</div>
</div>
<div id="middle-content">
<div id="learning-materials" className="rounded shadow">
<h2>Learning materials</h2>
<a
href="https://nx.dev/getting-started/intro?utm_source=nx-project"
target="_blank"
rel="noreferrer"
className="list-item-link"
>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
<span>
Documentation
<span> Everything is in there </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
<a
href="https://blog.nrwl.io/?utm_source=nx-project"
target="_blank"
rel="noreferrer"
className="list-item-link"
>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z"
/>
</svg>
<span>
Blog
<span> Changelog, features & events </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
<a
href="https://www.youtube.com/@NxDevtools/videos?utm_source=nx-project&sub_confirmation=1"
target="_blank"
rel="noreferrer"
className="list-item-link"
>
<svg
role="img"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<title>YouTube</title>
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
</svg>
<span>
YouTube channel
<span> Nx Show, talks & tutorials </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
<a
href="https://nx.dev/react-tutorial/1-code-generation?utm_source=nx-project"
target="_blank"
rel="noreferrer"
className="list-item-link"
>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"
/>
</svg>
<span>
Interactive tutorials
<span> Create an app, step-by-step </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
<a
href="https://nxplaybook.com/?utm_source=nx-project"
target="_blank"
rel="noreferrer"
className="list-item-link"
>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M12 14l9-5-9-5-9 5 9 5z" />
<path d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z" />
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 14l9-5-9-5-9 5 9 5zm0 0l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14zm-4 6v-7.5l4-2.222"
/>
</svg>
<span>
Video courses
<span> Nx custom courses </span>
</span>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
</div>
<div id="other-links">
<a
className="button-pill nx-console rounded shadow"
href="https://marketplace.visualstudio.com/items?itemName=nrwl.angular-console&utm_source=nx-project"
target="_blank"
rel="noreferrer"
>
<svg
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>Visual Studio Code</title>
<path d="M23.15 2.587L18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z" />
</svg>
<span>
Install Nx Console for VSCode
<span>The official VSCode plugin for Nx.</span>
</span>
</a>
<a
className="button-pill nx-console rounded shadow"
href="https://plugins.jetbrains.com/plugin/21060-nx-console"
target="_blank"
rel="noreferrer"
>
<svg
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>IntelliJ</title>
<path d="M0 0v24h24V0zm3.723 3.111h5v1.834h-1.39v6.277h1.39v1.834h-5v-1.834h1.444V4.945H3.723zm11.055 0H17v6.5c0 .612-.055 1.111-.222 1.556-.167.444-.39.777-.723 1.11-.277.279-.666.557-1.11.668a3.933 3.933 0 0 1-1.445.278c-.778 0-1.444-.167-1.944-.445a4.81 4.81 0 0 1-1.279-1.056l1.39-1.555c.277.334.555.555.833.722.277.167.611.278.945.278.389 0 .721-.111 1-.389.221-.278.333-.667.333-1.278zM2.222 19.5h9V21h-9z"></path>
</svg>
<span>
Install Nx Console for JetBrains
<span>
Available for WebStorm, Intellij IDEA Ultimate and more!
</span>
</span>
</a>
<div id="nx-cloud" className="rounded shadow">
<div>
<svg
id="nx-cloud-logo"
role="img"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
fill="transparent"
viewBox="0 0 24 24"
>
<path
strokeWidth="2"
d="M23 3.75V6.5c-3.036 0-5.5 2.464-5.5 5.5s-2.464 5.5-5.5 5.5-5.5 2.464-5.5 5.5H3.75C2.232 23 1 21.768 1 20.25V3.75C1 2.232 2.232 1 3.75 1h16.5C21.768 1 23 2.232 23 3.75Z"
/>
<path
strokeWidth="2"
d="M23 6v14.1667C23 21.7307 21.7307 23 20.1667 23H6c0-3.128 2.53867-5.6667 5.6667-5.6667 3.128 0 5.6666-2.5386 5.6666-5.6666C17.3333 8.53867 19.872 6 23 6Z"
/>
</svg>
<h2>
NxCloud
<span>Enable faster CI & better DX</span>
</h2>
</div>
<p>
You can activate distributed tasks executions and caching by
running:
</p>
<pre>nx connect-to-nx-cloud</pre>
<a
href="https://nx.app/?utm_source=nx-project"
target="_blank"
rel="noreferrer"
>
{' '}
What is Nx Cloud?{' '}
</a>
</div>
<a
id="nx-repo"
className="button-pill rounded shadow"
href="https://github.com/nrwl/nx?utm_source=nx-project"
target="_blank"
rel="noreferrer"
>
<svg
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
<span>
Nx is open source
<span> Love Nx? Give us a star! </span>
</span>
</a>
</div>
</div>
<div id="commands" className="rounded shadow">
<h2>Next steps</h2>
<p>Here are some things you can do with Nx:</p>
<details>
<summary>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
Add UI library
</summary>
<pre>
<span># Generate UI lib</span>
nx g @nx/react:lib ui
<span># Add a component</span>
nx g @nx/react:component button --project ui
</pre>
</details>
<details>
<summary>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
View interactive project graph
</summary>
<pre>nx graph</pre>
</details>
<details>
<summary>
<svg
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
Run affected commands
</summary>
<pre>
<span># see what&apos;s been affected by changes</span>
nx affected:graph
<span># run tests for current changes</span>
nx affected:test
<span># run e2e tests for current changes</span>
nx affected:e2e
</pre>
</details>
</div>
<p id="love">
Carefully crafted with
<svg
fill="currentColor"
stroke="none"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
/>
</svg>
</p>
</div>
</div>
</template>
<style scoped>
svg {
display: block;
vertical-align: middle;
shape-rendering: auto;
text-rendering: optimizeLegibility;
}
pre {
background-color: rgba(55, 65, 81, 1);
border-radius: 0.25rem;
color: rgba(229, 231, 235, 1);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
'Liberation Mono', 'Courier New', monospace;
overflow: scroll;
padding: 0.5rem 0.75rem;
}
.shadow {
box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.rounded {
border-radius: 1.5rem;
}
.wrapper {
width: 100%;
}
.container {
margin-left: auto;
margin-right: auto;
max-width: 768px;
padding-bottom: 3rem;
padding-left: 1rem;
padding-right: 1rem;
color: rgba(55, 65, 81, 1);
width: 100%;
}
#welcome {
margin-top: 2.5rem;
}
#welcome h1 {
font-size: 3rem;
font-weight: 500;
letter-spacing: -0.025em;
line-height: 1;
}
#welcome span {
display: block;
font-size: 1.875rem;
font-weight: 300;
line-height: 2.25rem;
margin-bottom: 0.5rem;
}
#hero {
align-items: center;
background-color: hsla(214, 62%, 21%, 1);
border: none;
box-sizing: border-box;
color: rgba(55, 65, 81, 1);
display: grid;
grid-template-columns: 1fr;
margin-top: 3.5rem;
}
#hero .text-container {
color: rgba(255, 255, 255, 1);
padding: 3rem 2rem;
}
#hero .text-container h2 {
font-size: 1.5rem;
line-height: 2rem;
position: relative;
}
#hero .text-container h2 svg {
color: hsla(162, 47%, 50%, 1);
height: 2rem;
left: -0.25rem;
position: absolute;
top: 0;
width: 2rem;
}
#hero .text-container h2 span {
margin-left: 2.5rem;
}
#hero .text-container a {
background-color: rgba(255, 255, 255, 1);
border-radius: 0.75rem;
color: rgba(55, 65, 81, 1);
display: inline-block;
margin-top: 1.5rem;
padding: 1rem 2rem;
text-decoration: inherit;
}
#hero .logo-container {
display: none;
justify-content: center;
padding-left: 2rem;
padding-right: 2rem;
}
#hero .logo-container svg {
color: rgba(255, 255, 255, 1);
width: 66.666667%;
}
#middle-content {
align-items: flex-start;
display: grid;
gap: 4rem;
grid-template-columns: 1fr;
margin-top: 3.5rem;
}
#learning-materials {
padding: 2.5rem 2rem;
}
#learning-materials h2 {
font-weight: 500;
font-size: 1.25rem;
letter-spacing: -0.025em;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
.list-item-link {
align-items: center;
border-radius: 0.75rem;
display: flex;
margin-top: 1rem;
padding: 1rem;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
width: 100%;
}
.list-item-link svg:first-child {
margin-right: 1rem;
height: 1.5rem;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
width: 1.5rem;
}
.list-item-link > span {
flex-grow: 1;
font-weight: 400;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.list-item-link > span > span {
color: rgba(107, 114, 128, 1);
display: block;
flex-grow: 1;
font-size: 0.75rem;
font-weight: 300;
line-height: 1rem;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.list-item-link svg:last-child {
height: 1rem;
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
width: 1rem;
}
.list-item-link:hover {
color: rgba(255, 255, 255, 1);
background-color: hsla(162, 47%, 50%, 1);
}
.list-item-link:hover > span {}
.list-item-link:hover > span > span {
color: rgba(243, 244, 246, 1);
}
.list-item-link:hover svg:last-child {
transform: translateX(0.25rem);
}
#other-links {}
.button-pill {
padding: 1.5rem 2rem;
margin-bottom: 2rem;
transition-duration: 300ms;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
align-items: center;
display: flex;
}
.button-pill svg {
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
flex-shrink: 0;
width: 3rem;
}
.button-pill > span {
letter-spacing: -0.025em;
font-weight: 400;
font-size: 1.125rem;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
.button-pill span span {
display: block;
font-size: 0.875rem;
font-weight: 300;
line-height: 1.25rem;
}
.button-pill:hover svg,
.button-pill:hover {
color: rgba(255, 255, 255, 1) !important;
}
.nx-console:hover {
background-color: rgba(0, 122, 204, 1);
}
.nx-console svg {
color: rgba(0, 122, 204, 1);
}
#nx-repo:hover {
background-color: rgba(24, 23, 23, 1);
}
#nx-repo svg {
color: rgba(24, 23, 23, 1);
}
#nx-cloud {
margin-bottom: 2rem;
margin-top: 2rem;
padding: 2.5rem 2rem;
}
#nx-cloud > div {
align-items: center;
display: flex;
}
#nx-cloud > div svg {
border-radius: 0.375rem;
flex-shrink: 0;
width: 3rem;
}
#nx-cloud > div h2 {
font-size: 1.125rem;
font-weight: 400;
letter-spacing: -0.025em;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
#nx-cloud > div h2 span {
display: block;
font-size: 0.875rem;
font-weight: 300;
line-height: 1.25rem;
}
#nx-cloud p {
font-size: 1rem;
line-height: 1.5rem;
margin-top: 1rem;
}
#nx-cloud pre {
margin-top: 1rem;
}
#nx-cloud a {
color: rgba(107, 114, 128, 1);
display: block;
font-size: 0.875rem;
line-height: 1.25rem;
margin-top: 1.5rem;
text-align: right;
}
#nx-cloud a:hover {
text-decoration: underline;
}
#commands {
padding: 2.5rem 2rem;
margin-top: 3.5rem;
}
#commands h2 {
font-size: 1.25rem;
font-weight: 400;
letter-spacing: -0.025em;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
#commands p {
font-size: 1rem;
font-weight: 300;
line-height: 1.5rem;
margin-top: 1rem;
padding-left: 1rem;
padding-right: 1rem;
}
details {
align-items: center;
display: flex;
margin-top: 1rem;
padding-left: 1rem;
padding-right: 1rem;
width: 100%;
}
details pre > span {
color: rgba(181, 181, 181, 1);
display: block;
}
summary {
border-radius: 0.5rem;
display: flex;
font-weight: 400;
padding: 0.5rem;
cursor: pointer;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
summary:hover {
background-color: rgba(243, 244, 246, 1);
}
summary svg {
height: 1.5rem;
margin-right: 1rem;
width: 1.5rem;
}
#love {
color: rgba(107, 114, 128, 1);
font-size: 0.875rem;
line-height: 1.25rem;
margin-top: 3.5rem;
opacity: 0.6;
text-align: center;
}
#love svg {
color: rgba(252, 165, 165, 1);
width: 1.25rem;
height: 1.25rem;
display: inline;
margin-top: -0.25rem;
}
@media screen and (min-width: 768px) {
#hero {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
#hero .logo-container {
display: flex;
}
#middle-content {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>

View File

@ -0,0 +1,15 @@
<% if (style !== 'none') { %>
import './styles.<%= style %>';
<% } %>
<% if (routing) { %>
import router from './router';
<% } %>
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
<% if (routing) { %>
app.use(router);
<% } %>
app.mount('#root');

View File

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "<%= offsetFromRoot %>dist/out-tsc"
},
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts", "src/**/*.spec.vue", "src/**/*.test.vue"],
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.vue"]
}

View File

@ -0,0 +1,23 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../views/AboutView.vue')
}
]
})
export default router

View File

@ -0,0 +1,16 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style>
@media (min-width: 768px) {
.about {
max-width: 768px;
margin-left: auto;
margin-right: auto;
padding: 0 1rem;
}
}
</style>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import NxWelcome from '../components/NxWelcome.vue'
</script>
<template>
<main>
<NxWelcome />
</main>
</template>

View File

@ -0,0 +1,42 @@
html {
-webkit-text-size-adjust: 100%;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
line-height: 1.5;
tab-size: 4;
scroll-behavior: smooth;
}
body {
font-family: inherit;
line-height: inherit;
margin: 0;
}
h1,
h2,
p,
pre {
margin: 0;
}
*,
::before,
::after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: currentColor;
}
h1,
h2 {
font-size: inherit;
font-weight: inherit;
}
a {
color: inherit;
text-decoration: inherit;
}
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
'Liberation Mono', 'Courier New', monospace;
}

View File

@ -0,0 +1,64 @@
import type { GeneratorCallback, Tree } from '@nx/devkit';
import {
addProjectConfiguration,
ensurePackage,
getPackageManagerCommand,
joinPathFragments,
} from '@nx/devkit';
import { webStaticServeGenerator } from '@nx/web';
import { nxVersion } from '../../../utils/versions';
import { NormalizedSchema } from '../schema';
export async function addE2e(
tree: Tree,
options: NormalizedSchema
): Promise<GeneratorCallback> {
switch (options.e2eTestRunner) {
case 'cypress':
webStaticServeGenerator(tree, {
buildTarget: `${options.projectName}:build`,
targetName: 'serve-static',
});
const { cypressProjectGenerator } = ensurePackage<
typeof import('@nx/cypress')
>('@nx/cypress', nxVersion);
return await cypressProjectGenerator(tree, {
...options,
name: options.e2eProjectName,
directory: options.e2eProjectRoot,
projectNameAndRootFormat: 'as-provided',
project: options.projectName,
bundler: 'vite',
skipFormat: true,
});
case 'playwright':
const { configurationGenerator } = ensurePackage<
typeof import('@nx/playwright')
>('@nx/playwright', nxVersion);
addProjectConfiguration(tree, options.e2eProjectName, {
root: options.e2eProjectRoot,
sourceRoot: joinPathFragments(options.e2eProjectRoot, 'src'),
targets: {},
implicitDependencies: [options.projectName],
});
return configurationGenerator(tree, {
project: options.e2eProjectName,
skipFormat: true,
skipPackageJson: options.skipPackageJson,
directory: 'src',
js: false,
linter: options.linter,
setParserOptionsProject: options.setParserOptionsProject,
webServerCommand: `${getPackageManagerCommand().exec} nx serve ${
options.name
}`,
webServerAddress: 'http://localhost:4200',
});
case 'none':
default:
return () => {};
}
}

View File

@ -0,0 +1,44 @@
import {
addDependenciesToPackageJson,
ensurePackage,
GeneratorCallback,
runTasksInSerial,
Tree,
} from '@nx/devkit';
import { NormalizedSchema } from '../schema';
import { nxVersion, vueJest3Version } from '../../../utils/versions';
import { setupJestProject } from '../../../utils/setup-jest';
export async function addJest(
tree: Tree,
options: NormalizedSchema
): Promise<GeneratorCallback> {
const tasks: GeneratorCallback[] = [];
const { configurationGenerator } = ensurePackage<typeof import('@nx/jest')>(
'@nx/jest',
nxVersion
);
tasks.push(
await configurationGenerator(tree, {
project: options.name,
skipFormat: true,
testEnvironment: 'jsdom',
compiler: 'babel',
})
);
setupJestProject(tree, options.appProjectRoot);
tasks.push(
addDependenciesToPackageJson(
tree,
{},
{
'@vue/vue3-jest': vueJest3Version,
}
)
);
return runTasksInSerial(...tasks);
}

View File

@ -0,0 +1,46 @@
import {
GeneratorCallback,
readProjectConfiguration,
Tree,
updateProjectConfiguration,
} from '@nx/devkit';
import { createOrEditViteConfig, viteConfigurationGenerator } from '@nx/vite';
import { NormalizedSchema } from '../schema';
export async function addVite(
tree: Tree,
options: NormalizedSchema
): Promise<GeneratorCallback> {
// Set up build target (and test target if using vitest)
const viteTask = await viteConfigurationGenerator(tree, {
uiFramework: 'none',
project: options.name,
newProject: true,
inSourceTests: options.inSourceTests,
includeVitest: options.unitTestRunner === 'vitest',
skipFormat: true,
testEnvironment: 'jsdom',
});
createOrEditViteConfig(
tree,
{
project: options.name,
includeLib: false,
includeVitest: options.unitTestRunner === 'vitest',
inSourceTests: options.inSourceTests,
imports: [`import vue from '@vitejs/plugin-vue'`],
plugins: ['vue()'],
},
false
);
// Update build to skip type checking since tsc won't work on .vue files.
// Need to use vue-tsc instead.
const projectConfig = readProjectConfiguration(tree, options.name);
projectConfig.targets.build.options.skipTypeCheck = true;
updateProjectConfiguration(tree, options.name, projectConfig);
return viteTask;
}

View File

@ -0,0 +1,53 @@
import * as path from 'path';
import { generateFiles, offsetFromRoot, Tree } from '@nx/devkit';
import { getRelativePathToRootTsConfig } from '@nx/js';
import { createTsConfig } from '../../../utils/create-ts-config';
import { NormalizedSchema } from '../schema';
export function createApplicationFiles(tree: Tree, options: NormalizedSchema) {
generateFiles(
tree,
path.join(__dirname, '../files/common'),
options.appProjectRoot,
{
...options,
offsetFromRoot: offsetFromRoot(options.appProjectRoot),
title: options.projectName,
}
);
if (options.style !== 'none') {
generateFiles(
tree,
path.join(__dirname, '../files/stylesheet'),
options.appProjectRoot,
{
...options,
offsetFromRoot: offsetFromRoot(options.appProjectRoot),
title: options.projectName,
}
);
}
if (options.routing) {
generateFiles(
tree,
path.join(__dirname, '../files/routing'),
options.appProjectRoot,
{
...options,
offsetFromRoot: offsetFromRoot(options.appProjectRoot),
title: options.projectName,
}
);
}
createTsConfig(
tree,
options.appProjectRoot,
'app',
options,
getRelativePathToRootTsConfig(tree, options.appProjectRoot)
);
}

View File

@ -0,0 +1,56 @@
import { Tree, extractLayoutDirectory, names } from '@nx/devkit';
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { NormalizedSchema, Schema } from '../schema';
export function normalizeDirectory(options: Schema) {
options.directory = options.directory?.replace(/\\{1,2}/g, '/');
const { projectDirectory } = extractLayoutDirectory(options.directory);
return projectDirectory
? `${names(projectDirectory).fileName}/${names(options.name).fileName}`
: names(options.name).fileName;
}
export async function normalizeOptions(
host: Tree,
options: Schema,
callingGenerator = '@nx/vue:application'
): Promise<NormalizedSchema> {
const {
projectName: appProjectName,
projectRoot: appProjectRoot,
projectNameAndRootFormat,
} = await determineProjectNameAndRootOptions(host, {
name: options.name,
projectType: 'application',
directory: options.directory,
projectNameAndRootFormat: options.projectNameAndRootFormat,
rootProject: options.rootProject,
callingGenerator,
});
options.rootProject = appProjectRoot === '.';
options.projectNameAndRootFormat = projectNameAndRootFormat;
const e2eProjectName = options.rootProject ? 'e2e' : `${appProjectName}-e2e`;
const e2eProjectRoot = options.rootProject ? 'e2e' : `${appProjectRoot}-e2e`;
const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim())
: [];
const normalized = {
...options,
name: names(options.name).fileName,
projectName: appProjectName,
appProjectRoot,
e2eProjectName,
e2eProjectRoot,
parsedTags,
} as NormalizedSchema;
normalized.style = options.style ?? 'css';
normalized.routing = normalized.routing ?? false;
normalized.unitTestRunner ??= 'vitest';
normalized.e2eTestRunner = normalized.e2eTestRunner ?? 'cypress';
return normalized;
}

View File

@ -0,0 +1,30 @@
import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils';
import type { Linter } from '@nx/linter';
export interface Schema {
name: string;
style: 'none' | 'css' | 'scss' | 'less';
skipFormat?: boolean;
directory?: string;
projectNameAndRootFormat?: ProjectNameAndRootFormat;
tags?: string;
unitTestRunner?: 'jest' | 'vitest' | 'none';
inSourceTests?: boolean;
e2eTestRunner: 'cypress' | 'playwright' | 'none';
linter: Linter;
routing?: boolean;
js?: boolean;
strict?: boolean;
setParserOptionsProject?: boolean;
skipPackageJson?: boolean;
rootProject?: boolean;
}
export interface NormalizedSchema extends Schema {
projectName: string;
appProjectRoot: string;
e2eProjectName: string;
e2eProjectRoot: string;
parsedTags: string[];
devServerPort?: number;
}

View File

@ -0,0 +1,140 @@
{
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"$id": "NxVueApp",
"title": "Create a Vue Application",
"description": "Create a Vue application for Nx.",
"examples": [
{
"command": "nx g app myapp --directory=myorg/myapp",
"description": "Generate `apps/myorg/myapp` and `apps/myorg/myapp-e2e`"
},
{
"command": "nx g app myapp --routing",
"description": "Set up Vue Router"
}
],
"type": "object",
"properties": {
"name": {
"description": "The name of the application.",
"type": "string",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use for the application?",
"pattern": "^[a-zA-Z][^:]*$"
},
"directory": {
"description": "The directory of the new application.",
"type": "string",
"alias": "dir",
"x-priority": "important"
},
"projectNameAndRootFormat": {
"description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"style": {
"description": "The file extension to be used for style files.",
"type": "string",
"default": "css",
"alias": "s",
"x-prompt": {
"message": "Which stylesheet format would you like to use?",
"type": "list",
"items": [
{
"value": "css",
"label": "CSS"
},
{
"value": "scss",
"label": "SASS(.scss) [ http://sass-lang.com ]"
},
{
"value": "less",
"label": "LESS [ http://lesscss.org ]"
},
{
"value": "none",
"label": "None"
}
]
}
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
},
"routing": {
"type": "boolean",
"description": "Generate application with routes.",
"x-prompt": "Would you like to add Vue Router to this application?",
"default": false
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false,
"x-priority": "internal"
},
"unitTestRunner": {
"type": "string",
"enum": ["jest", "vitest", "none"],
"description": "Test runner to use for unit tests.",
"x-prompt": "Which unit test runner would you like to use?",
"default": "none"
},
"inSourceTests": {
"type": "boolean",
"default": false,
"description": "When using Vitest, separate spec files will not be generated and instead will be included within the source files. Read more on the Vitest docs site: https://vitest.dev/guide/in-source.html"
},
"e2eTestRunner": {
"type": "string",
"enum": ["cypress", "playwright", "none"],
"description": "Test runner to use for end to end (E2E) tests.",
"x-prompt": "Which E2E test runner would you like to use?",
"default": "cypress"
},
"tags": {
"type": "string",
"description": "Add tags to the application (used for linting).",
"alias": "t"
},
"js": {
"type": "boolean",
"description": "Generate JavaScript files rather than TypeScript files.",
"default": false
},
"strict": {
"type": "boolean",
"description": "Whether to enable tsconfig strict mode or not.",
"default": true
},
"setParserOptionsProject": {
"type": "boolean",
"description": "Whether or not to configure the ESLint `parserOptions.project` option. We do not do this by default for lint performance reasons.",
"default": false
},
"skipPackageJson": {
"description": "Do not add dependencies to `package.json`.",
"type": "boolean",
"default": false,
"x-priority": "internal"
},
"rootProject": {
"description": "Create a application at the root of the workspace",
"type": "boolean",
"default": false,
"hidden": true
}
},
"required": ["name"],
"examplesFile": "../../../docs/application-examples.md"
}

View File

@ -0,0 +1,72 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`component --export should add to index.ts barrel 1`] = `
"export { default as Hello } from './components/hello/hello.vue';
"
`;
exports[`component should generate files with jest 1`] = `
"<script setup lang="ts">
defineProps<{}>();
</script>
<template>
<div>
<p>Welcome to Hello!</p>
</div>
</template>
<style scoped>
div {
color: pink;
}
</style>
"
`;
exports[`component should generate files with jest 2`] = `
"import { mount } from '@vue/test-utils';
import Hello from '../hello.vue';
describe('Hello', () => {
it('renders properly', () => {
const wrapper = mount(Hello, {});
expect(wrapper.text()).toContain('Welcome to Hello');
});
});
"
`;
exports[`component should generate files with vitest 1`] = `
"<script setup lang="ts">
defineProps<{}>();
</script>
<template>
<div>
<p>Welcome to Hello!</p>
</div>
</template>
<style scoped>
div {
color: pink;
}
</style>
"
`;
exports[`component should generate files with vitest 2`] = `
"import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import Hello from '../hello.vue';
describe('Hello', () => {
it('renders properly', () => {
const wrapper = mount(Hello, {});
expect(wrapper.text()).toContain('Welcome to Hello');
});
});
"
`;

View File

@ -0,0 +1,207 @@
import { logger, readJson, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { componentGenerator } from './component';
import { createLib } from '../../utils/test-utils';
describe('component', () => {
let appTree: Tree;
let projectName: string;
beforeEach(async () => {
projectName = 'my-lib';
appTree = createTreeWithEmptyWorkspace();
await createLib(appTree, projectName);
jest.spyOn(logger, 'warn').mockImplementation(() => {});
jest.spyOn(logger, 'debug').mockImplementation(() => {});
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should generate files with vitest', async () => {
await componentGenerator(appTree, {
name: 'hello',
project: projectName,
unitTestRunner: 'vitest',
});
expect(
appTree.exists('my-lib/src/components/hello/hello.vue')
).toBeTruthy();
expect(
appTree.exists('my-lib/src/components/hello/__tests__/hello.spec.ts')
).toBeTruthy();
expect(
appTree.read('my-lib/src/components/hello/hello.vue', 'utf-8')
).toMatchSnapshot();
expect(
appTree.read(
'my-lib/src/components/hello/__tests__/hello.spec.ts',
'utf-8'
)
).toMatchSnapshot();
});
it('should generate files with jest', async () => {
await componentGenerator(appTree, {
name: 'hello',
project: projectName,
unitTestRunner: 'jest',
});
expect(
appTree.read('my-lib/src/components/hello/hello.vue', 'utf-8')
).toMatchSnapshot();
expect(
appTree.read(
'my-lib/src/components/hello/__tests__/hello.spec.ts',
'utf-8'
)
).toMatchSnapshot();
});
// we don't have app generator yet
xit('should generate files for an app', async () => {
await componentGenerator(appTree, {
name: 'hello',
project: 'my-app',
unitTestRunner: 'vitest',
});
expect(
appTree.exists('my-app/src/components/hello/hello.tsx')
).toBeTruthy();
expect(
appTree.exists('my-app/src/components/hello/hello.spec.ts')
).toBeTruthy();
expect(
appTree.exists('my-app/src/components/hello/hello.module.css')
).toBeTruthy();
});
describe('--export', () => {
it('should add to index.ts barrel', async () => {
await componentGenerator(appTree, {
name: 'hello',
project: projectName,
export: true,
});
expect(appTree.read('my-lib/src/index.ts', 'utf-8')).toMatchSnapshot();
});
// no app generator yet
xit('should not export from an app', async () => {
await componentGenerator(appTree, {
name: 'hello',
project: 'my-app',
export: true,
});
expect(appTree.read('my-app/src/index.ts', 'utf-8')).toMatchSnapshot();
});
});
describe('--pascalCaseFiles', () => {
it('should generate component files with upper case names', async () => {
await componentGenerator(appTree, {
name: 'hello',
project: projectName,
pascalCaseFiles: true,
});
expect(
appTree.exists('my-lib/src/components/hello/Hello.vue')
).toBeTruthy();
expect(
appTree.exists('my-lib/src/components/hello/__tests__/Hello.spec.ts')
).toBeTruthy();
});
});
describe('--pascalCaseDirectory', () => {
it('should generate component files with pascal case directories', async () => {
await componentGenerator(appTree, {
name: 'hello-world',
project: projectName,
pascalCaseFiles: true,
pascalCaseDirectory: true,
});
expect(
appTree.exists('my-lib/src/components/HelloWorld/HelloWorld.vue')
).toBeTruthy();
expect(
appTree.exists(
'my-lib/src/components/HelloWorld/__tests__/HelloWorld.spec.ts'
)
).toBeTruthy();
});
});
// TODO: figure out routing
xdescribe('--routing', () => {
it('should add routes to the component', async () => {
await componentGenerator(appTree, {
name: 'hello',
project: projectName,
routing: true,
});
const content = appTree
.read('my-lib/src/components/hello/hello.tsx')
.toString();
expect(content).toContain('react-router-dom');
expect(content).toMatch(/<Route\s*path="\/"/);
expect(content).toMatch(/<Link\s*to="\/"/);
const packageJSON = readJson(appTree, 'package.json');
expect(packageJSON.dependencies['react-router-dom']).toBeDefined();
});
});
describe('--directory', () => {
it('should create component under the directory', async () => {
await componentGenerator(appTree, {
name: 'hello',
project: projectName,
directory: 'components',
});
expect(appTree.exists('/my-lib/src/components/hello/hello.vue'));
});
it('should create with nested directories', async () => {
await componentGenerator(appTree, {
name: 'helloWorld',
project: projectName,
directory: 'lib/foo',
});
expect(
appTree.exists('/my-lib/src/components/foo/hello-world/hello-world.vue')
);
});
});
describe('--flat', () => {
it('should create in project directory rather than in its own folder', async () => {
await componentGenerator(appTree, {
name: 'hello',
project: projectName,
flat: true,
});
expect(appTree.exists('/my-lib/src/components/hello.vue'));
});
it('should work with custom directory path', async () => {
await componentGenerator(appTree, {
name: 'hello',
project: projectName,
flat: true,
directory: 'components',
});
expect(appTree.exists('/my-lib/src/components/hello.vue'));
});
});
});

View File

@ -0,0 +1,161 @@
import {
applyChangesToString,
formatFiles,
generateFiles,
GeneratorCallback,
getProjects,
joinPathFragments,
logger,
names,
runTasksInSerial,
toJS,
Tree,
} from '@nx/devkit';
import { NormalizedSchema, Schema } from './schema';
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
import { join } from 'path';
import { addImport } from '../../utils/ast-utils';
export async function componentGenerator(host: Tree, schema: Schema) {
const options = await normalizeOptions(host, schema);
createComponentFiles(host, options);
const tasks: GeneratorCallback[] = [];
addExportsToBarrel(host, options);
if (!options.skipFormat) {
await formatFiles(host);
}
return runTasksInSerial(...tasks);
}
function createComponentFiles(host: Tree, options: NormalizedSchema) {
const componentDir = joinPathFragments(
options.projectSourceRoot,
options.directory
);
generateFiles(host, join(__dirname, './files'), componentDir, {
...options,
tmpl: '',
unitTestRunner: options.unitTestRunner,
});
for (const c of host.listChanges()) {
let deleteFile = false;
if (
(options.skipTests || options.inSourceTests) &&
/.*spec.ts/.test(c.path)
) {
deleteFile = true;
}
if (deleteFile) {
host.delete(c.path);
}
}
if (options.js) toJS(host);
}
let tsModule: typeof import('typescript');
function addExportsToBarrel(host: Tree, options: NormalizedSchema) {
if (!tsModule) {
tsModule = ensureTypescript();
}
const workspace = getProjects(host);
const isApp = workspace.get(options.project).projectType === 'application';
if (options.export && !isApp) {
const indexFilePath = joinPathFragments(
options.projectSourceRoot,
options.js ? 'index.js' : 'index.ts'
);
const indexSource = host.read(indexFilePath, 'utf-8');
if (indexSource !== null) {
const indexSourceFile = tsModule.createSourceFile(
indexFilePath,
indexSource,
tsModule.ScriptTarget.Latest,
true
);
const changes = applyChangesToString(
indexSource,
addImport(
indexSourceFile,
`export { default as ${options.className} } from './${options.directory}/${options.fileName}.vue';`
)
);
host.write(indexFilePath, changes);
}
}
}
async function normalizeOptions(
host: Tree,
options: Schema
): Promise<NormalizedSchema> {
assertValidOptions(options);
const { className, fileName } = names(options.name);
const componentFileName =
options.fileName ?? (options.pascalCaseFiles ? className : fileName);
const project = getProjects(host).get(options.project);
if (!project) {
throw new Error(
`Cannot find the ${options.project} project. Please double check the project name.`
);
}
const { sourceRoot: projectSourceRoot, projectType } = project;
const directory = await getDirectory(host, options);
if (options.export && projectType === 'application') {
logger.warn(
`The "--export" option should not be used with applications and will do nothing.`
);
}
options.routing = options.routing ?? false;
options.inSourceTests = options.inSourceTests ?? false;
return {
...options,
directory,
className,
fileName: componentFileName,
projectSourceRoot,
};
}
async function getDirectory(host: Tree, options: Schema) {
if (options.directory) return options.directory;
if (options.flat) return 'components';
const { className, fileName } = names(options.name);
const nestedDir = options.pascalCaseDirectory === true ? className : fileName;
return joinPathFragments('components', nestedDir);
}
function assertValidOptions(options: Schema) {
const slashes = ['/', '\\'];
slashes.forEach((s) => {
if (options.name.indexOf(s) !== -1) {
const [name, ...rest] = options.name.split(s).reverse();
let suggestion = rest.map((x) => x.toLowerCase()).join(s);
if (options.directory) {
suggestion = `${options.directory}${s}${suggestion}`;
}
throw new Error(
`Found "${s}" in the component name. Did you mean to use the --directory option (e.g. \`nx g c ${name} --directory ${suggestion}\`)?`
);
}
});
}
export default componentGenerator;

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
defineProps<{}>()
</script>
<template>
<div>
<p>Welcome to <%= className %>!</p>
</div>
</template>
<style scoped>
div {
color: pink;
}
</style>

View File

@ -0,0 +1,13 @@
<% if ( unitTestRunner === 'vitest' ) { %>
import { describe, it, expect } from 'vitest'
<% } %>
import { mount } from '@vue/test-utils'
import <%= className %> from '../<%= fileName %>.vue';
describe('<%= className %>', () => {
it('renders properly', () => {
const wrapper = mount(<%= className %>, {})
expect(wrapper.text()).toContain('Welcome to <%= className %>')
})
});

View File

@ -0,0 +1,22 @@
export interface Schema {
name: string;
project: string;
skipTests?: boolean;
directory?: string;
export?: boolean;
pascalCaseFiles?: boolean;
pascalCaseDirectory?: boolean;
routing?: boolean;
js?: boolean;
flat?: boolean;
fileName?: string;
inSourceTests?: boolean;
skipFormat?: boolean;
unitTestRunner?: 'jest' | 'vitest' | 'none';
}
export interface NormalizedSchema extends Schema {
projectSourceRoot: string;
fileName: string;
className: string;
}

View File

@ -0,0 +1,107 @@
{
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"$id": "NxVueComponent",
"title": "Create a Vue Component",
"description": "Create a Vue Component for Nx.",
"type": "object",
"examples": [
{
"command": "nx g component my-component --project=mylib",
"description": "Generate a component in the `mylib` library"
},
{
"command": "nx g component my-component --project=mylib --classComponent",
"description": "Generate a class component in the `mylib` library"
}
],
"properties": {
"project": {
"type": "string",
"description": "The name of the project.",
"alias": "p",
"$default": {
"$source": "projectName"
},
"x-prompt": "What is the name of the project for this component?",
"x-priority": "important"
},
"name": {
"type": "string",
"description": "The name of the component.",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use for the component?",
"x-priority": "important"
},
"js": {
"type": "boolean",
"description": "Generate JavaScript files rather than TypeScript files.",
"default": false
},
"skipTests": {
"type": "boolean",
"description": "When true, does not create `spec.ts` test files for the new component.",
"default": false,
"x-priority": "internal"
},
"directory": {
"type": "string",
"description": "Create the component under this directory (can be nested).",
"alias": "dir",
"x-priority": "important"
},
"flat": {
"type": "boolean",
"description": "Create component at the source root rather than its own directory.",
"default": false
},
"export": {
"type": "boolean",
"description": "When true, the component is exported from the project `index.ts` (if it exists).",
"alias": "e",
"default": false,
"x-prompt": "Should this component be exported in the project?"
},
"pascalCaseFiles": {
"type": "boolean",
"description": "Use pascal case component file name (e.g. `App.tsx`).",
"alias": "P",
"default": false
},
"pascalCaseDirectory": {
"type": "boolean",
"description": "Use pascal case directory name (e.g. `App/App.tsx`).",
"alias": "R",
"default": false
},
"routing": {
"type": "boolean",
"description": "Generate a library with routes."
},
"fileName": {
"type": "string",
"description": "Create a component with this file name."
},
"inSourceTests": {
"type": "boolean",
"default": false,
"description": "When using Vitest, separate spec files will not be generated and instead will be included within the source files. Read more on the Vitest docs site: https://vitest.dev/guide/in-source.html"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false,
"x-priority": "internal"
},
"unitTestRunner": {
"type": "string",
"enum": ["vitest", "jest", "none"],
"description": "Test runner to use for unit tests.",
"x-prompt": "What unit test runner should be used?"
}
},
"required": ["name", "project"]
}

View File

@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`init should add vue dependencies 1`] = `
{
"dependencies": {
"vue": "^3.3.4",
"vue-router": "^4.2.4",
"vue-tsc": "^1.8.8",
},
"devDependencies": {
"@nx/js": "0.0.1",
"@nx/vue": "0.0.1",
"@vitejs/plugin-vue": "^4.3.1",
"@vue/test-utils": "^2.4.1",
"@vue/tsconfig": "^0.4.0",
"prettier": "^2.6.2",
"typescript": "~5.1.3",
},
"name": "test-name",
}
`;

View File

@ -0,0 +1,20 @@
import { readJson, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { vueInitGenerator } from './init';
describe('init', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
it('should add vue dependencies', async () => {
await vueInitGenerator(tree, {
skipFormat: false,
routing: true,
});
const packageJson = readJson(tree, 'package.json');
expect(packageJson).toMatchSnapshot();
});
});

View File

@ -0,0 +1,67 @@
import {
addDependenciesToPackageJson,
GeneratorCallback,
removeDependenciesFromPackageJson,
runTasksInSerial,
Tree,
} from '@nx/devkit';
import { initGenerator as jsInitGenerator } from '@nx/js';
import {
lessVersion,
nxVersion,
sassVersion,
vitePluginVueVersion,
vueRouterVersion,
vueTestUtilsVersion,
vueTsconfigVersion,
vueTscVersion,
vueVersion,
} from '../../utils/versions';
import { InitSchema } from './schema';
function updateDependencies(host: Tree, schema: InitSchema) {
removeDependenciesFromPackageJson(host, ['@nx/vue'], []);
let dependencies: { [key: string]: string } = {
vue: vueVersion,
'vue-tsc': vueTscVersion,
};
let devDependencies: { [key: string]: string } = {
'@nx/vue': nxVersion,
'@vue/tsconfig': vueTsconfigVersion,
'@vue/test-utils': vueTestUtilsVersion,
'@vitejs/plugin-vue': vitePluginVueVersion,
};
if (schema.routing) {
dependencies['vue-router'] = vueRouterVersion;
}
if (schema.style === 'scss') {
devDependencies['sass'] = sassVersion;
} else if (schema.style === 'less') {
devDependencies['less'] = lessVersion;
}
return addDependenciesToPackageJson(host, dependencies, devDependencies);
}
export async function vueInitGenerator(host: Tree, schema: InitSchema) {
const tasks: GeneratorCallback[] = [];
tasks.push(
await jsInitGenerator(host, {
...schema,
tsConfigName: schema.rootProject ? 'tsconfig.json' : 'tsconfig.base.json',
skipFormat: true,
})
);
tasks.push(updateDependencies(host, schema));
return runTasksInSerial(...tasks);
}
export default vueInitGenerator;

View File

@ -0,0 +1,7 @@
export interface InitSchema {
skipFormat?: boolean;
js?: boolean;
rootProject?: boolean;
routing?: boolean;
style?: 'css' | 'scss' | 'less' | 'none';
}

View File

@ -0,0 +1,37 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "NxVueInit",
"title": "Init Vue Plugin",
"description": "Initialize a Vue Plugin.",
"cli": "nx",
"type": "object",
"properties": {
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false
},
"js": {
"type": "boolean",
"description": "Use JavaScript instead of TypeScript",
"default": false
},
"rootProject": {
"description": "Create a project at the root of the workspace",
"type": "boolean",
"default": false
},
"routing": {
"type": "boolean",
"description": "Generate application with routes.",
"x-prompt": "Would you like to add React Router to this application?",
"default": false
},
"style": {
"description": "The file extension to be used for style files.",
"type": "string",
"default": "css"
}
},
"required": []
}

View File

@ -0,0 +1,300 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`lib nested should create a local tsconfig.json 1`] = `
{
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": false,
"jsx": "preserve",
"jsxImportSource": "vue",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"strict": true,
"verbatimModuleSyntax": true,
},
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json",
},
{
"path": "./tsconfig.spec.json",
},
],
}
`;
exports[`lib should add correct jest.config.ts and dependencies to package.json 1`] = `
{
"dependencies": {
"tslib": "^2.3.0",
"vue": "^3.3.4",
"vue-tsc": "^1.8.8",
},
"devDependencies": {
"@nx/cypress": "0.0.1",
"@nx/eslint-plugin": "0.0.1",
"@nx/jest": "0.0.1",
"@nx/js": "0.0.1",
"@nx/linter": "0.0.1",
"@nx/rollup": "0.0.1",
"@nx/vite": "0.0.1",
"@nx/vue": "0.0.1",
"@types/jest": "^29.4.0",
"@types/node": "16.11.7",
"@typescript-eslint/eslint-plugin": "^5.60.1",
"@typescript-eslint/parser": "^5.60.1",
"@vitejs/plugin-vue": "^4.3.1",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/test-utils": "^2.4.1",
"@vue/tsconfig": "^0.4.0",
"@vue/vue3-jest": "^29.2.6",
"babel-jest": "^29.4.1",
"eslint": "~8.46.0",
"eslint-config-prettier": "8.1.0",
"eslint-plugin-vue": "^9.16.1",
"jest": "^29.4.1",
"jest-environment-jsdom": "^29.4.1",
"prettier": "^2.6.2",
"ts-jest": "^29.1.0",
"ts-node": "10.9.1",
"typescript": "~5.1.3",
},
"name": "test-name",
}
`;
exports[`lib should add correct jest.config.ts and dependencies to package.json 2`] = `
{
"ignorePatterns": [
"**/*",
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx",
"*.vue",
],
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"allow": [],
"depConstraints": [
{
"onlyDependOnLibsWithTags": [
"*",
],
"sourceTag": "*",
},
],
"enforceBuildableLibDependency": true,
},
],
},
},
{
"extends": [
"plugin:@nx/typescript",
],
"files": [
"*.ts",
"*.tsx",
],
"rules": {},
},
{
"extends": [
"plugin:@nx/javascript",
],
"files": [
"*.js",
"*.jsx",
],
"rules": {},
},
{
"env": {
"jest": true,
},
"files": [
"*.spec.ts",
"*.spec.tsx",
"*.spec.js",
"*.spec.jsx",
],
"rules": {},
},
],
"plugins": [
"@nx",
],
"root": true,
}
`;
exports[`lib should add correct jest.config.ts and dependencies to package.json 3`] = `
"/* eslint-disable */
export default {
displayName: 'my-lib',
preset: '../jest.preset.js',
coverageDirectory: '../coverage/my-lib',
moduleFileExtensions: ['js', 'ts', 'json', 'vue'],
transform: {
'^.+.[tj]sx?$': ['babel-jest'],
'^.+.vue$': [
'@vue/vue3-jest',
{
tsConfig: './tsconfig.spec.json',
},
],
},
testEnvironment: 'jsdom',
testMatch: ['**/__tests__/**/*.spec.ts?(x)', '**/__tests__/*.ts?(x)'],
};
"
`;
exports[`lib should add correct jest.config.ts and dependencies to package.json 4`] = `
"{
"presets": ["@nx/js/babel"]
}
"
`;
exports[`lib should add vite types to tsconfigs and generate correct vite.config.ts file 1`] = `
"import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import * as path from 'path';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
cacheDir: '../node_modules/.vite/my-lib',
plugins: [
nxViteTsPaths(),
dts({
entryRoot: 'src',
tsConfigFilePath: path.join(__dirname, 'tsconfig.lib.json'),
skipDiagnostics: true,
}),
vue(),
],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
entry: 'src/index.ts',
name: 'my-lib',
fileName: 'index',
formats: ['es', 'cjs'],
external: [],
lib: {
entry: 'src/index.ts',
name: 'my-lib',
fileName: 'index',
formats: ['es', 'cjs'],
},
rollupOptions: { external: [] },
},
test: {
globals: true,
cache: { dir: '../node_modules/.vitest' },
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
},
});
"
`;
exports[`lib should add vue, vite and vitest to package.json 1`] = `
{
"dependencies": {
"vue": "^3.3.4",
"vue-tsc": "^1.8.8",
},
"devDependencies": {
"@nx/cypress": "0.0.1",
"@nx/eslint-plugin": "0.0.1",
"@nx/js": "0.0.1",
"@nx/linter": "0.0.1",
"@nx/rollup": "0.0.1",
"@nx/vite": "0.0.1",
"@nx/vue": "0.0.1",
"@typescript-eslint/eslint-plugin": "^5.60.1",
"@typescript-eslint/parser": "^5.60.1",
"@vitejs/plugin-vue": "^4.3.1",
"@vitest/coverage-c8": "~0.32.0",
"@vitest/ui": "~0.32.0",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/test-utils": "^2.4.1",
"@vue/tsconfig": "^0.4.0",
"eslint": "~8.46.0",
"eslint-config-prettier": "8.1.0",
"eslint-plugin-vue": "^9.16.1",
"jsdom": "~22.1.0",
"prettier": "^2.6.2",
"typescript": "~5.1.3",
"vite": "~4.3.9",
"vitest": "~0.32.0",
},
"name": "test-name",
}
`;
exports[`lib should generate files 1`] = `
{
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/eslint-config-typescript",
"@vue/eslint-config-prettier/skip-formatting",
"../.eslintrc.json",
],
"ignorePatterns": [
"!**/*",
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx",
"*.vue",
],
"rules": {},
},
],
}
`;
exports[`lib should ignore test files in tsconfig.lib.json 1`] = `
[
"src/**/__tests__/*",
"src/**/*.spec.ts",
"src/**/*.test.ts",
"src/**/*.spec.tsx",
"src/**/*.test.tsx",
"src/**/*.spec.js",
"src/**/*.test.js",
"src/**/*.spec.jsx",
"src/**/*.test.jsx",
]
`;

View File

@ -0,0 +1,7 @@
# <%= name %>
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test <%= name %>` to execute the unit tests via [Vitest](https://vitest.dev/).

View File

@ -0,0 +1,12 @@
{
"name": "<%= name %>",
"version": "0.0.1",
"main": "./index.js",
"types": "./index.d.ts",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js"
}
}
}

View File

@ -0,0 +1,5 @@
declare module '*.vue' {
import { defineComponent } from 'vue';
const component: ReturnType<typeof defineComponent>;
export default component;
}

View File

@ -0,0 +1,34 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "<%= offsetFromRoot %>dist/out-tsc",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"types": [],
"lib": [
"ES2016",
"DOM",
"DOM.Iterable"
],
},
"exclude": [
"src/**/__tests__/*",
"src/**/*.spec.ts",
"src/**/*.test.ts",
"src/**/*.spec.tsx",
"src/**/*.test.tsx",
"src/**/*.spec.js",
"src/**/*.test.js",
"src/**/*.spec.jsx",
"src/**/*.test.jsx"
],
"include": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue"
]
}

View File

@ -0,0 +1,27 @@
{
"extends": "./tsconfig.json",
"exclude": [],
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"lib": [],
"types": [
"vitest",
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node"]
},
"include": [
"vite.config.ts",
"src/**/__tests__/*",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
]
}

View File

@ -0,0 +1,41 @@
import {
addDependenciesToPackageJson,
ensurePackage,
GeneratorCallback,
runTasksInSerial,
Tree,
} from '@nx/devkit';
import { nxVersion, vueJest3Version } from '../../../utils/versions';
import { setupJestProject } from '../../../utils/setup-jest';
import { NormalizedSchema } from '../schema';
export async function addJest(
tree: Tree,
options: NormalizedSchema
): Promise<GeneratorCallback> {
const tasks: GeneratorCallback[] = [];
const { configurationGenerator } = ensurePackage<typeof import('@nx/jest')>(
'@nx/jest',
nxVersion
);
const jestTask = await configurationGenerator(tree, {
project: options.name,
skipFormat: true,
testEnvironment: 'jsdom',
compiler: 'babel',
});
tasks.push(jestTask);
setupJestProject(tree, options.projectRoot);
tasks.push(
addDependenciesToPackageJson(
tree,
{},
{
'@vue/vue3-jest': vueJest3Version,
}
)
);
return runTasksInSerial(...tasks);
}

View File

@ -0,0 +1,78 @@
import {
GeneratorCallback,
Tree,
ensurePackage,
runTasksInSerial,
} from '@nx/devkit';
import { NormalizedSchema } from '../schema';
import { nxVersion } from '../../../utils/versions';
export async function addVite(
tree: Tree,
options: NormalizedSchema
): Promise<GeneratorCallback> {
const tasks: GeneratorCallback[] = [];
// Set up build target
if (options.bundler === 'vite') {
const { viteConfigurationGenerator, createOrEditViteConfig } =
ensurePackage<typeof import('@nx/vite')>('@nx/vite', nxVersion);
const viteTask = await viteConfigurationGenerator(tree, {
uiFramework: 'none',
project: options.name,
newProject: true,
includeLib: true,
inSourceTests: options.inSourceTests,
includeVitest: options.unitTestRunner === 'vitest',
skipFormat: true,
testEnvironment: 'jsdom',
});
tasks.push(viteTask);
createOrEditViteConfig(
tree,
{
project: options.name,
includeLib: true,
includeVitest: options.unitTestRunner === 'vitest',
inSourceTests: options.inSourceTests,
imports: [`import vue from '@vitejs/plugin-vue'`],
plugins: ['vue()'],
},
false
);
}
// Set up test target
if (
options.unitTestRunner === 'vitest' &&
options.bundler !== 'vite' // tests are already configured if bundler is vite
) {
const { vitestGenerator, createOrEditViteConfig } = ensurePackage<
typeof import('@nx/vite')
>('@nx/vite', nxVersion);
const vitestTask = await vitestGenerator(tree, {
uiFramework: 'none',
project: options.name,
coverageProvider: 'c8',
inSourceTests: options.inSourceTests,
skipFormat: true,
testEnvironment: 'jsdom',
});
tasks.push(vitestTask);
createOrEditViteConfig(
tree,
{
project: options.name,
includeLib: true,
includeVitest: true,
inSourceTests: options.inSourceTests,
imports: [`import vue from '@vitejs/plugin-vue'`],
plugins: ['vue()'],
},
true
);
}
return runTasksInSerial(...tasks);
}

View File

@ -0,0 +1,53 @@
import type { Tree } from '@nx/devkit';
import {
generateFiles,
joinPathFragments,
names,
offsetFromRoot,
toJS,
writeJson,
} from '@nx/devkit';
import { getRelativePathToRootTsConfig } from '@nx/js';
import { NormalizedSchema } from '../schema';
import { createTsConfig } from '../../../utils/create-ts-config';
export function createLibraryFiles(host: Tree, options: NormalizedSchema) {
const relativePathToRootTsConfig = getRelativePathToRootTsConfig(
host,
options.projectRoot
);
const substitutions = {
...options,
...names(options.name),
tmpl: '',
offsetFromRoot: offsetFromRoot(options.projectRoot),
fileName: options.fileName,
};
generateFiles(
host,
joinPathFragments(__dirname, '../files'),
options.projectRoot,
substitutions
);
if (!options.publishable && options.bundler === 'none') {
host.delete(`${options.projectRoot}/package.json`);
}
if (options.unitTestRunner !== 'vitest') {
host.delete(`${options.projectRoot}/tsconfig.spec.json`);
}
if (options.js) {
toJS(host);
}
createTsConfig(
host,
options.projectRoot,
'lib',
options,
relativePathToRootTsConfig
);
}

View File

@ -0,0 +1,53 @@
import type { Tree } from '@nx/devkit';
import { Linter } from '@nx/linter';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { normalizeOptions } from './normalize-options';
describe('normalizeOptions', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
});
it('should set unitTestRunner=jest and bundler=none by default', async () => {
const options = await normalizeOptions(tree, {
name: 'test',
linter: Linter.None,
unitTestRunner: 'vitest',
});
expect(options).toMatchObject({
bundler: 'none',
unitTestRunner: 'vitest',
});
});
it('should set unitTestRunner=vitest by default when bundler is vite', async () => {
const options = await normalizeOptions(tree, {
name: 'test',
linter: Linter.None,
bundler: 'vite',
unitTestRunner: 'vitest',
});
expect(options).toMatchObject({
bundler: 'vite',
unitTestRunner: 'vitest',
});
});
it('should set maintain unitTestRunner when bundler is vite', async () => {
const options = await normalizeOptions(tree, {
name: 'test',
linter: Linter.None,
bundler: 'vite',
unitTestRunner: 'vitest',
});
expect(options).toMatchObject({
bundler: 'vite',
unitTestRunner: 'vitest',
});
});
});

View File

@ -0,0 +1,77 @@
import { getProjects, logger, normalizePath, Tree } from '@nx/devkit';
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { NormalizedSchema, Schema } from '../schema';
export async function normalizeOptions(
host: Tree,
options: Schema
): Promise<NormalizedSchema> {
const {
projectName,
names: projectNames,
projectRoot,
importPath,
} = await determineProjectNameAndRootOptions(host, {
name: options.name,
projectType: 'library',
directory: options.directory,
importPath: options.importPath,
projectNameAndRootFormat: options.projectNameAndRootFormat,
callingGenerator: '@nx/vue:library',
});
const fileName = projectNames.projectFileName;
const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim())
: [];
let bundler = options.bundler ?? 'none';
if (bundler === 'none') {
if (options.publishable) {
logger.warn(
`Publishable libraries cannot be used with bundler: 'none'. Defaulting to 'vite'.`
);
bundler = 'vite';
}
}
const normalized = {
...options,
bundler,
fileName,
routePath: `/${projectNames.projectFileName}`,
name: projectName,
projectRoot,
parsedTags,
importPath,
} as NormalizedSchema;
// Libraries with a bundler or is publishable must also be buildable.
normalized.bundler =
normalized.bundler !== 'none' || options.publishable ? 'vite' : 'none';
normalized.inSourceTests === normalized.minimal || normalized.inSourceTests;
if (options.appProject) {
const appProjectConfig = getProjects(host).get(options.appProject);
if (appProjectConfig.projectType !== 'application') {
throw new Error(
`appProject expected type of "application" but got "${appProjectConfig.projectType}"`
);
}
try {
normalized.appMain = appProjectConfig.targets.build.options.main;
normalized.appSourceRoot = normalizePath(appProjectConfig.sourceRoot);
} catch (e) {
throw new Error(
`Could not locate project main for ${options.appProject}`
);
}
}
return normalized;
}

View File

@ -0,0 +1,409 @@
import {
getProjects,
readJson,
readProjectConfiguration,
Tree,
updateJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Linter } from '@nx/linter';
import { nxVersion } from '../../utils/versions';
import libraryGenerator from './library';
import { Schema } from './schema';
describe('lib', () => {
let tree: Tree;
let defaultSchema: Schema = {
name: 'myLib',
linter: Linter.EsLint,
skipFormat: false,
skipTsConfig: false,
unitTestRunner: 'vitest',
component: true,
strict: true,
};
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
updateJson(tree, '/package.json', (json) => {
json.devDependencies = {
'@nx/cypress': nxVersion,
'@nx/rollup': nxVersion,
'@nx/vite': nxVersion,
};
return json;
});
});
it('should update project configuration', async () => {
await libraryGenerator(tree, defaultSchema);
const project = readProjectConfiguration(tree, 'my-lib');
expect(project.root).toEqual('my-lib');
expect(project.targets.build).toBeUndefined();
expect(project.targets.lint).toEqual({
executor: '@nx/linter:eslint',
outputs: ['{options.outputFile}'],
options: {
lintFilePatterns: ['my-lib/**/*.{ts,tsx,js,jsx,vue}'],
},
});
});
it('should add vite types to tsconfigs and generate correct vite.config.ts file', async () => {
await libraryGenerator(tree, {
...defaultSchema,
bundler: 'vite',
unitTestRunner: 'vitest',
});
const tsconfigApp = readJson(tree, 'my-lib/tsconfig.lib.json');
expect(tsconfigApp.compilerOptions.types).toEqual(['vite/client']);
const tsconfigSpec = readJson(tree, 'my-lib/tsconfig.spec.json');
expect(tsconfigSpec.compilerOptions.types).toEqual([
'vitest/globals',
'vitest/importMeta',
'vite/client',
'node',
'vitest',
]);
expect(tree.read('my-lib/vite.config.ts', 'utf-8')).toMatchSnapshot();
});
it('should update tags', async () => {
await libraryGenerator(tree, { ...defaultSchema, tags: 'one,two' });
const project = readProjectConfiguration(tree, 'my-lib');
expect(project).toEqual(
expect.objectContaining({
tags: ['one', 'two'],
})
);
});
it('should add vue, vite and vitest to package.json', async () => {
await libraryGenerator(tree, defaultSchema);
expect(readJson(tree, '/package.json')).toMatchSnapshot();
});
it('should add correct jest.config.ts and dependencies to package.json', async () => {
await libraryGenerator(tree, { ...defaultSchema, unitTestRunner: 'jest' });
expect(readJson(tree, '/package.json')).toMatchSnapshot();
expect(readJson(tree, '.eslintrc.json')).toMatchSnapshot();
expect(tree.read('my-lib/jest.config.ts', 'utf-8')).toMatchSnapshot();
expect(tree.read('my-lib/.babelrc', 'utf-8')).toMatchSnapshot();
});
it('should update root tsconfig.base.json', async () => {
await libraryGenerator(tree, defaultSchema);
const tsconfigJson = readJson(tree, '/tsconfig.base.json');
expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([
'my-lib/src/index.ts',
]);
});
it('should create tsconfig.base.json out of tsconfig.json', async () => {
tree.rename('tsconfig.base.json', 'tsconfig.json');
await libraryGenerator(tree, defaultSchema);
expect(tree.exists('tsconfig.base.json')).toEqual(true);
const tsconfigJson = readJson(tree, 'tsconfig.base.json');
expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([
'my-lib/src/index.ts',
]);
});
it('should update root tsconfig.base.json (no existing path mappings)', async () => {
updateJson(tree, 'tsconfig.base.json', (json) => {
json.compilerOptions.paths = undefined;
return json;
});
await libraryGenerator(tree, defaultSchema);
const tsconfigJson = readJson(tree, '/tsconfig.base.json');
expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([
'my-lib/src/index.ts',
]);
});
it('should create a local tsconfig.json', async () => {
await libraryGenerator(tree, defaultSchema);
const tsconfigJson = readJson(tree, 'my-lib/tsconfig.json');
expect(tsconfigJson.extends).toBe('../tsconfig.base.json');
expect(tsconfigJson.references).toEqual([
{
path: './tsconfig.lib.json',
},
{
path: './tsconfig.spec.json',
},
]);
});
it('should extend the tsconfig.lib.json with tsconfig.spec.json', async () => {
await libraryGenerator(tree, defaultSchema);
const tsconfigJson = readJson(tree, 'my-lib/tsconfig.spec.json');
expect(tsconfigJson.extends).toEqual('./tsconfig.json');
});
it('should extend ./tsconfig.json with tsconfig.lib.json', async () => {
await libraryGenerator(tree, defaultSchema);
const tsconfigJson = readJson(tree, 'my-lib/tsconfig.lib.json');
expect(tsconfigJson.extends).toEqual('./tsconfig.json');
});
it('should ignore test files in tsconfig.lib.json', async () => {
await libraryGenerator(tree, defaultSchema);
const tsconfigJson = readJson(tree, 'my-lib/tsconfig.lib.json');
expect(tsconfigJson.exclude).toMatchSnapshot();
});
it('should generate files', async () => {
await libraryGenerator(tree, defaultSchema);
expect(tree.exists('my-lib/package.json')).toBeFalsy();
expect(tree.exists('my-lib/src/index.ts')).toBeTruthy();
expect(tree.exists('my-lib/src/components/my-lib.vue')).toBeTruthy();
expect(
tree.exists('my-lib/src/components/__tests__/my-lib.spec.ts')
).toBeTruthy();
const eslintJson = readJson(tree, 'my-lib/.eslintrc.json');
expect(eslintJson).toMatchSnapshot();
});
describe('nested', () => {
it('should update tags and implicitDependencies', async () => {
await libraryGenerator(tree, {
...defaultSchema,
directory: 'myDir',
tags: 'one',
});
const myLib = readProjectConfiguration(tree, 'my-dir-my-lib');
expect(myLib).toEqual(
expect.objectContaining({
tags: ['one'],
})
);
await libraryGenerator(tree, {
...defaultSchema,
name: 'myLib2',
directory: 'myDir',
tags: 'one,two',
});
const myLib2 = readProjectConfiguration(tree, 'my-dir-my-lib2');
expect(myLib2).toEqual(
expect.objectContaining({
tags: ['one', 'two'],
})
);
});
it('should generate files', async () => {
await libraryGenerator(tree, { ...defaultSchema, directory: 'myDir' });
expect(tree.exists('my-dir/my-lib/src/index.ts')).toBeTruthy();
expect(
tree.exists('my-dir/my-lib/src/components/my-dir-my-lib.vue')
).toBeTruthy();
expect(
tree.exists(
'my-dir/my-lib/src/components/__tests__/my-dir-my-lib.spec.ts'
)
).toBeTruthy();
});
it('should update project configurations', async () => {
await libraryGenerator(tree, { ...defaultSchema, directory: 'myDir' });
const config = readProjectConfiguration(tree, 'my-dir-my-lib');
expect(config.root).toEqual('my-dir/my-lib');
expect(config.targets.lint).toEqual({
executor: '@nx/linter:eslint',
outputs: ['{options.outputFile}'],
options: {
lintFilePatterns: ['my-dir/my-lib/**/*.{ts,tsx,js,jsx,vue}'],
},
});
});
it('should update root tsconfig.base.json', async () => {
await libraryGenerator(tree, { ...defaultSchema, directory: 'myDir' });
const tsconfigJson = readJson(tree, '/tsconfig.base.json');
expect(tsconfigJson.compilerOptions.paths['@proj/my-dir/my-lib']).toEqual(
['my-dir/my-lib/src/index.ts']
);
expect(
tsconfigJson.compilerOptions.paths['my-dir-my-lib/*']
).toBeUndefined();
});
it('should create a local tsconfig.json', async () => {
await libraryGenerator(tree, { ...defaultSchema, directory: 'myDir' });
const tsconfigJson = readJson(tree, 'my-dir/my-lib/tsconfig.json');
expect(tsconfigJson).toMatchSnapshot();
});
});
describe('--no-component', () => {
it('should not generate components or styles', async () => {
await libraryGenerator(tree, { ...defaultSchema, component: false });
expect(tree.exists('my-lib/src/lib')).toBeFalsy();
});
});
describe('--unit-test-runner none', () => {
it('should not generate test configuration', async () => {
await libraryGenerator(tree, {
...defaultSchema,
unitTestRunner: 'none',
});
expect(tree.exists('my-lib/tsconfig.spec.json')).toBeFalsy();
const config = readProjectConfiguration(tree, 'my-lib');
expect(config.targets.test).toBeUndefined();
expect(config.targets.lint).toMatchInlineSnapshot(`
{
"executor": "@nx/linter:eslint",
"options": {
"lintFilePatterns": [
"my-lib/**/*.{ts,tsx,js,jsx,vue}",
],
},
"outputs": [
"{options.outputFile}",
],
}
`);
});
});
describe('--publishable', () => {
it('should add build targets', async () => {
await libraryGenerator(tree, {
...defaultSchema,
publishable: true,
importPath: '@proj/my-lib',
});
const projectsConfigurations = getProjects(tree);
expect(projectsConfigurations.get('my-lib').targets.build).toMatchObject({
executor: '@nx/vite:build',
outputs: ['{options.outputPath}'],
options: {
outputPath: 'dist/my-lib',
},
});
});
it('should fail if no importPath is provided with publishable', async () => {
expect.assertions(1);
try {
await libraryGenerator(tree, {
...defaultSchema,
directory: 'myDir',
publishable: true,
});
} catch (e) {
expect(e.message).toContain(
'For publishable libs you have to provide a proper "--importPath" which needs to be a valid npm package name (e.g. my-awesome-lib or @myorg/my-lib)'
);
}
});
it('should add package.json and .babelrc', async () => {
await libraryGenerator(tree, {
...defaultSchema,
publishable: true,
importPath: '@proj/my-lib',
});
const packageJson = readJson(tree, '/my-lib/package.json');
expect(packageJson.name).toEqual('@proj/my-lib');
expect(tree.exists('/my-lib/.babelrc'));
});
});
describe('--js', () => {
it('should generate JS files', async () => {
await libraryGenerator(tree, {
...defaultSchema,
js: true,
});
expect(tree.exists('/my-lib/src/index.js')).toBe(true);
});
});
describe('--importPath', () => {
it('should update the package.json & tsconfig with the given import path', async () => {
await libraryGenerator(tree, {
...defaultSchema,
publishable: true,
directory: 'myDir',
importPath: '@myorg/lib',
});
const packageJson = readJson(tree, 'my-dir/my-lib/package.json');
const tsconfigJson = readJson(tree, '/tsconfig.base.json');
expect(packageJson.name).toBe('@myorg/lib');
expect(
tsconfigJson.compilerOptions.paths[packageJson.name]
).toBeDefined();
});
it('should fail if the same importPath has already been used', async () => {
await libraryGenerator(tree, {
...defaultSchema,
name: 'myLib1',
publishable: true,
importPath: '@myorg/lib',
});
try {
await libraryGenerator(tree, {
...defaultSchema,
name: 'myLib2',
publishable: true,
importPath: '@myorg/lib',
});
} catch (e) {
expect(e.message).toContain(
'You already have a library using the import path'
);
}
expect.assertions(1);
});
});
describe('--no-strict', () => {
it('should not add options for strict mode', async () => {
await libraryGenerator(tree, {
...defaultSchema,
strict: false,
});
const tsconfigJson = readJson(tree, '/my-lib/tsconfig.json');
expect(tsconfigJson.compilerOptions.strict).toEqual(false);
});
});
describe('--setParserOptionsProject', () => {
it('should set the parserOptions.project in the eslintrc.json file', async () => {
await libraryGenerator(tree, {
...defaultSchema,
setParserOptionsProject: true,
});
const eslintConfig = readJson(tree, 'my-lib/.eslintrc.json');
expect(eslintConfig.overrides[0].parserOptions.project).toEqual([
'my-lib/tsconfig.*?.json',
]);
expect(eslintConfig.overrides[0].files).toContain('*.vue');
});
});
});

View File

@ -0,0 +1,102 @@
import {
addProjectConfiguration,
formatFiles,
GeneratorCallback,
joinPathFragments,
runTasksInSerial,
toJS,
Tree,
updateJson,
} from '@nx/devkit';
import { addTsConfigPath } from '@nx/js';
import { vueInitGenerator } from '../init/init';
import { Schema } from './schema';
import { normalizeOptions } from './lib/normalize-options';
import { addLinting } from '../../utils/add-linting';
import { createLibraryFiles } from './lib/create-library-files';
import { extractTsConfigBase } from '../../utils/create-ts-config';
import componentGenerator from '../component/component';
import { addVite } from './lib/add-vite';
import { addJest } from './lib/add-jest';
export async function libraryGenerator(tree: Tree, schema: Schema) {
const tasks: GeneratorCallback[] = [];
const options = await normalizeOptions(tree, schema);
if (options.publishable === true && !schema.importPath) {
throw new Error(
`For publishable libs you have to provide a proper "--importPath" which needs to be a valid npm package name (e.g. my-awesome-lib or @myorg/my-lib)`
);
}
addProjectConfiguration(tree, options.name, {
root: options.projectRoot,
sourceRoot: joinPathFragments(options.projectRoot, 'src'),
projectType: 'library',
tags: options.parsedTags,
targets: {},
});
tasks.push(
await vueInitGenerator(tree, {
...options,
skipFormat: true,
})
);
extractTsConfigBase(tree);
tasks.push(await addLinting(tree, options, 'lib'));
createLibraryFiles(tree, options);
tasks.push(await addVite(tree, options));
if (options.unitTestRunner === 'jest')
tasks.push(await addJest(tree, options));
if (options.component) {
tasks.push(
await componentGenerator(tree, {
name: options.fileName,
project: options.name,
flat: true,
skipTests:
options.unitTestRunner === 'none' ||
(options.unitTestRunner === 'vitest' &&
options.inSourceTests == true),
export: true,
routing: options.routing,
js: options.js,
pascalCaseFiles: options.pascalCaseFiles,
inSourceTests: options.inSourceTests,
skipFormat: true,
})
);
}
if (options.publishable || options.bundler !== 'none') {
updateJson(tree, `${options.projectRoot}/package.json`, (json) => {
json.name = options.importPath;
return json;
});
}
if (!options.skipTsConfig) {
addTsConfigPath(tree, options.importPath, [
joinPathFragments(
options.projectRoot,
'./src',
'index.' + (options.js ? 'js' : 'ts')
),
]);
}
if (options.js) toJS(tree);
if (!options.skipFormat) await formatFiles(tree);
return runTasksInSerial(...tasks);
}
export default libraryGenerator;

View File

@ -0,0 +1,41 @@
import type { ProjectNameAndRootFormat } from '@nx/devkit/src/generators/project-name-and-root-utils';
import type { Linter } from '@nx/linter';
import type { SupportedStyles } from '../../../typings/style';
export interface Schema {
appProject?: string;
bundler?: 'none' | 'vite';
component?: boolean;
directory?: string;
projectNameAndRootFormat?: ProjectNameAndRootFormat;
importPath?: string;
inSourceTests?: boolean;
js?: boolean;
linter: Linter;
name: string;
pascalCaseFiles?: boolean;
publishable?: boolean;
routing?: boolean;
setParserOptionsProject?: boolean;
skipFormat?: boolean;
skipPackageJson?: boolean;
skipTsConfig?: boolean;
strict?: boolean;
tags?: string;
unitTestRunner?: 'jest' | 'vitest' | 'none';
minimal?: boolean;
e2eTestRunner?: 'cypress' | 'none';
}
export interface NormalizedSchema extends Schema {
js: boolean;
name: string;
linter: Linter;
fileName: string;
projectRoot: string;
routePath: string;
parsedTags: string[];
appMain?: string;
appSourceRoot?: string;
unitTestRunner?: 'jest' | 'vitest' | 'none';
}

View File

@ -0,0 +1,139 @@
{
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"$id": "NxVueLibrary",
"title": "Create a Vue Library",
"description": "Create a Vue Library for an Nx workspace.",
"type": "object",
"examples": [
{
"command": "nx g lib mylib --directory=libs/mylib",
"description": "Generate `libs/mylib`"
},
{
"command": "nx g lib mylib --appProject=myapp",
"description": "Generate a library with routes and add them to `myapp`"
}
],
"properties": {
"name": {
"type": "string",
"description": "Library name",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use for the library?",
"pattern": "(?:^@[a-zA-Z0-9-*~][a-zA-Z0-9-*._~]*\\/[a-zA-Z0-9-~][a-zA-Z0-9-._~]*|^[a-zA-Z][^:]*)$",
"x-priority": "important"
},
"directory": {
"type": "string",
"description": "A directory where the lib is placed.",
"alias": "dir",
"x-priority": "important"
},
"projectNameAndRootFormat": {
"description": "Whether to generate the project name and root directory as provided (`as-provided`) or generate them composing their values and taking the configured layout into account (`derived`).",
"type": "string",
"enum": ["as-provided", "derived"]
},
"linter": {
"description": "The tool to use for running lint checks.",
"type": "string",
"enum": ["eslint", "none"],
"default": "eslint"
},
"unitTestRunner": {
"type": "string",
"enum": ["vitest", "jest", "none"],
"description": "Test runner to use for unit tests.",
"x-prompt": "What unit test runner should be used?"
},
"inSourceTests": {
"type": "boolean",
"default": false,
"description": "When using Vitest, separate spec files will not be generated and instead will be included within the source files."
},
"tags": {
"type": "string",
"description": "Add tags to the library (used for linting).",
"alias": "t"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false,
"x-priority": "internal"
},
"skipTsConfig": {
"type": "boolean",
"default": false,
"description": "Do not update `tsconfig.json` for development experience.",
"x-priority": "internal"
},
"pascalCaseFiles": {
"type": "boolean",
"description": "Use pascal case component file name (e.g. `App.tsx`).",
"alias": "P",
"default": false
},
"routing": {
"type": "boolean",
"description": "Generate library with routes."
},
"appProject": {
"type": "string",
"description": "The application project to add the library route to.",
"alias": "a"
},
"publishable": {
"type": "boolean",
"description": "Create a publishable library."
},
"importPath": {
"type": "string",
"description": "The library name used to import it, like `@myorg/my-awesome-lib`."
},
"component": {
"type": "boolean",
"description": "Generate a default component.",
"default": true
},
"js": {
"type": "boolean",
"description": "Generate JavaScript files rather than TypeScript files.",
"default": false
},
"strict": {
"type": "boolean",
"description": "Whether to enable tsconfig strict mode or not.",
"default": true
},
"setParserOptionsProject": {
"type": "boolean",
"description": "Whether or not to configure the ESLint `parserOptions.project` option. We do not do this by default for lint performance reasons.",
"default": false
},
"bundler": {
"type": "string",
"description": "The bundler to use. Choosing 'none' means this library is not buildable.",
"enum": ["none", "vite"],
"default": "none",
"x-prompt": "Which bundler would you like to use to build the library? Choose 'none' to skip build setup.",
"x-priority": "important"
},
"skipPackageJson": {
"description": "Do not add dependencies to `package.json`.",
"type": "boolean",
"default": false,
"x-priority": "internal"
},
"minimal": {
"description": "Create a Vue library with a minimal setup, no separate test files.",
"type": "boolean",
"default": false
}
},
"required": ["name"]
}

View File

@ -0,0 +1,149 @@
import { Tree } from 'nx/src/generators/tree';
import { Linter, lintProjectGenerator } from '@nx/linter';
import { joinPathFragments } from 'nx/src/utils/path';
import {
addDependenciesToPackageJson,
runTasksInSerial,
updateJson,
} from '@nx/devkit';
import { extraEslintDependencies } from './lint';
import {
addExtendsToLintConfig,
isEslintConfigSupported,
} from '@nx/linter/src/generators/utils/eslint-file';
export async function addLinting(
host: Tree,
options: {
linter: Linter;
name: string;
projectRoot: string;
unitTestRunner?: 'jest' | 'vitest' | 'none';
setParserOptionsProject?: boolean;
skipPackageJson?: boolean;
rootProject?: boolean;
},
projectType: 'lib' | 'app'
) {
if (options.linter === Linter.EsLint) {
const lintTask = await lintProjectGenerator(host, {
linter: options.linter,
project: options.name,
tsConfigPaths: [
joinPathFragments(options.projectRoot, `tsconfig.${projectType}.json`),
],
unitTestRunner: options.unitTestRunner,
eslintFilePatterns: [`${options.projectRoot}/**/*.{ts,tsx,js,jsx,vue}`],
skipFormat: true,
setParserOptionsProject: options.setParserOptionsProject,
rootProject: options.rootProject,
});
if (isEslintConfigSupported(host)) {
addExtendsToLintConfig(host, options.projectRoot, [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting',
]);
}
editEslintConfigFiles(host, options.projectRoot, options.rootProject);
let installTask = () => {};
if (!options.skipPackageJson) {
installTask = addDependenciesToPackageJson(
host,
extraEslintDependencies.dependencies,
extraEslintDependencies.devDependencies
);
}
return runTasksInSerial(lintTask, installTask);
} else {
return () => {};
}
}
function editEslintConfigFiles(
tree: Tree,
projectRoot: string,
rootProject?: boolean
) {
if (tree.exists(joinPathFragments(projectRoot, 'eslint.config.js'))) {
const fileName = joinPathFragments(projectRoot, 'eslint.config.js');
updateJson(tree, fileName, (json) => {
let updated = false;
for (let override of json.overrides) {
if (override.parserOptions) {
if (!override.files.includes('*.vue')) {
override.files.push('*.vue');
}
updated = true;
}
}
if (!updated) {
json.overrides = [
{
files: ['*.ts', '*.tsx', '*.js', '*.jsx', '*.vue'],
rules: {},
},
];
}
return json;
});
} else {
const fileName = joinPathFragments(projectRoot, '.eslintrc.json');
updateJson(tree, fileName, (json) => {
let updated = false;
for (let override of json.overrides) {
if (override.parserOptions) {
if (!override.files.includes('*.vue')) {
override.files.push('*.vue');
}
updated = true;
}
}
if (!updated) {
json.overrides = [
{
files: ['*.ts', '*.tsx', '*.js', '*.jsx', '*.vue'],
rules: {},
},
];
}
return json;
});
}
// Edit root config too
if (tree.exists('.eslintrc.base.json')) {
updateJson(tree, '.eslintrc.base.json', (json) => {
for (let override of json.overrides) {
if (
override.rules &&
'@nx/enforce-module-boundaries' in override.rules
) {
if (!override.files.includes('*.vue')) {
override.files.push('*.vue');
}
}
}
return json;
});
} else if (tree.exists('.eslintrc.json') && !rootProject) {
updateJson(tree, '.eslintrc.json', (json) => {
for (let override of json.overrides) {
if (
override.rules &&
'@nx/enforce-module-boundaries' in override.rules
) {
if (!override.files.includes('*.vue')) {
override.files.push('*.vue');
}
}
}
return json;
});
}
}

View File

@ -0,0 +1,35 @@
import type * as ts from 'typescript';
import { findNodes } from '@nx/js';
import { ChangeType, StringChange } from '@nx/devkit';
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
let tsModule: typeof import('typescript');
export function addImport(
source: ts.SourceFile,
statement: string
): StringChange[] {
if (!tsModule) {
tsModule = ensureTypescript();
}
const allImports = findNodes(source, tsModule.SyntaxKind.ImportDeclaration);
if (allImports.length > 0) {
const lastImport = allImports[allImports.length - 1];
return [
{
type: ChangeType.Insert,
index: lastImport.end + 1,
text: `\n${statement}\n`,
},
];
} else {
return [
{
type: ChangeType.Insert,
index: 0,
text: `\n${statement}\n`,
},
];
}
}

View File

@ -0,0 +1,84 @@
import { Tree, updateJson, writeJson } from '@nx/devkit';
import * as shared from '@nx/js/src/utils/typescript/create-ts-config';
export function createTsConfig(
host: Tree,
projectRoot: string,
type: 'app' | 'lib',
options: {
strict?: boolean;
style?: string;
bundler?: string;
rootProject?: boolean;
unitTestRunner?: string;
},
relativePathToRootTsConfig: string
) {
const json = {
compilerOptions: {
allowJs: true,
esModuleInterop: false,
allowSyntheticDefaultImports: true,
strict: options.strict,
jsx: 'preserve',
jsxImportSource: 'vue',
moduleResolution: 'bundler',
resolveJsonModule: true,
verbatimModuleSyntax: options.unitTestRunner !== 'jest',
},
files: [],
include: [],
references: [
{
path: type === 'app' ? './tsconfig.app.json' : './tsconfig.lib.json',
},
],
} as any;
if (options.unitTestRunner === 'vitest') {
json.references.push({
path: './tsconfig.spec.json',
});
}
// inline tsconfig.base.json into the project
if (options.rootProject) {
json.compileOnSave = false;
json.compilerOptions = {
...shared.tsConfigBaseOptions,
...json.compilerOptions,
};
json.exclude = ['node_modules', 'tmp'];
} else {
json.extends = relativePathToRootTsConfig;
}
writeJson(host, `${projectRoot}/tsconfig.json`, json);
const tsconfigProjectPath = `${projectRoot}/tsconfig.${type}.json`;
if (options.bundler === 'vite' && host.exists(tsconfigProjectPath)) {
updateJson(host, tsconfigProjectPath, (json) => {
json.compilerOptions ??= {};
const types = new Set(json.compilerOptions.types ?? []);
types.add('vite/client');
json.compilerOptions.types = Array.from(types);
return json;
});
} else {
}
}
export function extractTsConfigBase(host: Tree) {
shared.extractTsConfigBase(host);
if (host.exists('vite.config.ts')) {
const vite = host.read('vite.config.ts').toString();
host.write(
'vite.config.ts',
vite.replace(`projects: []`, `projects: ['tsconfig.base.json']`)
);
}
}

View File

@ -0,0 +1,29 @@
import {
eslintPluginVueVersion,
vueEslintConfigPrettierVersion,
vueEslintConfigTypescriptVersion,
} from './versions';
export const extraEslintDependencies = {
dependencies: {},
devDependencies: {
'@vue/eslint-config-prettier': vueEslintConfigPrettierVersion,
'@vue/eslint-config-typescript': vueEslintConfigTypescriptVersion,
'eslint-plugin-vue': eslintPluginVueVersion,
},
};
export const extendVueEslintJson = (json: any) => {
const { extends: pluginExtends, ...config } = json;
return {
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting',
...(pluginExtends || []),
],
...config,
};
};

View File

@ -0,0 +1,124 @@
import {
applyChangesToString,
ChangeType,
joinPathFragments,
offsetFromRoot,
Tree,
writeJson,
} from '@nx/devkit';
import { ObjectLiteralExpression } from 'typescript';
export function setupJestProject(tree: Tree, projectRoot: string) {
updateJestConfigTsFile(tree, projectRoot);
writeBabelRcFile(tree, projectRoot);
}
export function writeBabelRcFile(tree: Tree, projectRoot: string) {
writeJson(tree, joinPathFragments(projectRoot, '.babelrc'), {
presets: ['@nx/js/babel'],
});
}
export function updateJestConfigTsFile(tree: Tree, projectRoot: string) {
const jestConfigTs = joinPathFragments(projectRoot, 'jest.config.ts');
if (tree.exists(jestConfigTs)) {
const { tsquery } = require('@phenomnomnominal/tsquery');
let fileContent = tree.read(jestConfigTs, 'utf-8');
const sourceFile = tsquery.ast(fileContent);
const settingsObject = tsquery.query(
sourceFile,
'ObjectLiteralExpression'
)?.[0] as ObjectLiteralExpression;
if (settingsObject) {
const moduleFileExtensions = tsquery.query(
sourceFile,
`PropertyAssignment:has(Identifier:has([name="moduleFileExtensions"]))`
)?.[0];
if (moduleFileExtensions) {
fileContent = applyChangesToString(fileContent, [
{
type: ChangeType.Delete,
start: moduleFileExtensions.getStart(),
length:
moduleFileExtensions.getEnd() -
moduleFileExtensions.getStart() +
1,
},
]);
}
const transformProperty = tsquery.query(
sourceFile,
`PropertyAssignment:has(Identifier:has([name="transform"]))`
)?.[0];
if (transformProperty) {
fileContent = applyChangesToString(fileContent, [
{
type: ChangeType.Delete,
start: transformProperty.getStart(),
length:
transformProperty.getEnd() - transformProperty.getStart() + 1,
},
]);
}
const settingsObjectUpdated = tsquery.query(
fileContent,
'ObjectLiteralExpression'
)?.[0] as ObjectLiteralExpression;
fileContent = applyChangesToString(fileContent, [
{
type: ChangeType.Insert,
index: settingsObjectUpdated.getEnd() - 1,
text: `,
moduleFileExtensions: ['js', 'ts', 'json', 'vue'],
transform: {
'^.+\\.[tj]sx?$': ['babel-jest'],
'^.+\\.vue$': [
'@vue/vue3-jest',
{
tsConfig: './tsconfig.spec.json',
},
],
},
testEnvironment: 'jsdom',
testMatch: ['**/__tests__/**/*.spec.ts?(x)', '**/__tests__/*.ts?(x)'],
`,
},
]);
tree.write(jestConfigTs, fileContent);
} else {
writeNewJestConfig(tree, projectRoot);
}
} else {
writeNewJestConfig(tree, projectRoot);
}
}
function writeNewJestConfig(tree: Tree, projectRoot: string) {
tree.write(
joinPathFragments(projectRoot, 'jest.config.js'),
`
module.exports = {
preset: '${offsetFromRoot}/jest.preset.js',
moduleFileExtensions: ['js', 'ts', 'json', 'vue'],
transform: {
'^.+\\.[tj]sx?$': ['babel-jest'],
'^.+\\.vue$': [
'@vue/vue3-jest',
{
tsConfig: './tsconfig.spec.json',
},
],
},
testEnvironment: 'jsdom',
testMatch: ['**/__tests__/**/*.spec.ts?(x)', '**/__tests__/*.ts?(x)'],
};
`
);
}

View File

@ -0,0 +1,29 @@
import { addProjectConfiguration, names, Tree } from '@nx/devkit';
import { Linter } from '@nx/linter';
import applicationGenerator from '../generators/application/application';
export async function createApp(tree: Tree, appName: string): Promise<any> {
await applicationGenerator(tree, {
e2eTestRunner: 'none',
linter: Linter.EsLint,
skipFormat: true,
style: 'css',
unitTestRunner: 'none',
name: appName,
projectNameAndRootFormat: 'as-provided',
});
}
export async function createLib(tree: Tree, libName: string): Promise<any> {
const { fileName } = names(libName);
tree.write(`/${fileName}/src/index.ts`, ``);
addProjectConfiguration(tree, fileName, {
tags: [],
root: `${fileName}`,
projectType: 'library',
sourceRoot: `${fileName}/src`,
targets: {},
});
}

View File

@ -0,0 +1,23 @@
export const nxVersion = require('../../package.json').version;
// vue core
export const vueVersion = '^3.3.4';
export const vueTscVersion = '^1.8.8';
export const vueRouterVersion = '^4.2.4';
// build deps
export const vueTsconfigVersion = '^0.4.0';
// test deps
export const vueTestUtilsVersion = '^2.4.1';
export const vitePluginVueVersion = '^4.3.1';
export const vueJest3Version = '^29.2.6';
// linting deps
export const vueEslintConfigPrettierVersion = '^8.0.0';
export const vueEslintConfigTypescriptVersion = '^11.0.3';
export const eslintPluginVueVersion = '^9.16.1';
// other deps
export const sassVersion = '1.62.1';
export const lessVersion = '3.12.2';

View File

@ -184,10 +184,7 @@ describe('app', () => {
path: './tsconfig.spec.json',
},
]);
expect(tsconfig.compilerOptions.types).toMatchObject([
'vite/client',
'vitest',
]);
expect(tsconfig.compilerOptions.types).toMatchObject(['vite/client']);
expect(tree.exists('my-app-e2e/cypress.config.ts')).toBeTruthy();
expect(tree.exists('my-app/index.html')).toBeTruthy();
@ -555,6 +552,7 @@ describe('app', () => {
"vitest/importMeta",
"vite/client",
"node",
"vitest",
]
`);
expect(
@ -660,10 +658,7 @@ describe('app', () => {
it('should create correct tsconfig compilerOptions', () => {
const tsconfigJson = readJson(viteAppTree, '/my-app/tsconfig.json');
expect(tsconfigJson.compilerOptions.types).toMatchObject([
'vite/client',
'vitest',
]);
expect(tsconfigJson.compilerOptions.types).toMatchObject(['vite/client']);
});
it('should create index.html and vite.config file at the root of the app', () => {

View File

@ -1134,6 +1134,136 @@ Nx comes with local caching already built-in (check your \`nx.json\`). On CI you
"
`;
exports[`@nx/workspace:generateWorkspaceFiles README.md should be created for VueMonorepo preset 1`] = `
"# Proj
<a alt="Nx logo" href="https://nx.dev" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-logo.png" width="45"></a>
✨ **This workspace has been generated by [Nx, a Smart, fast and extensible build system.](https://nx.dev)** ✨
## Start the app
To start the development server run \`nx serve app1\`. Open your browser and navigate to http://localhost:4200/. Happy coding!
## Generate code
If you happen to use Nx plugins, you can leverage code generators that might come with it.
Run \`nx list\` to get a list of available plugins and whether they have generators. Then run \`nx list <plugin-name>\` to see what generators are available.
Learn more about [Nx generators on the docs](https://nx.dev/plugin-features/use-code-generators).
## Running tasks
To execute tasks with Nx use the following syntax:
\`\`\`
nx <target> <project> <...options>
\`\`\`
You can also run multiple targets:
\`\`\`
nx run-many -t <target1> <target2>
\`\`\`
..or add \`-p\` to filter specific projects
\`\`\`
nx run-many -t <target1> <target2> -p <proj1> <proj2>
\`\`\`
Targets can be defined in the \`package.json\` or \`projects.json\`. Learn more [in the docs](https://nx.dev/core-features/run-tasks).
## Want better Editor Integration?
Have a look at the [Nx Console extensions](https://nx.dev/nx-console). It provides autocomplete support, a UI for exploring and running tasks & generators, and more! Available for VSCode, IntelliJ and comes with a LSP for Vim users.
## Ready to deploy?
Just run \`nx build demoapp\` to build the application. The build artifacts will be stored in the \`dist/\` directory, ready to be deployed.
## Set up CI!
Nx comes with local caching already built-in (check your \`nx.json\`). On CI you might want to go a step further.
- [Set up remote caching](https://nx.dev/core-features/share-your-cache)
- [Set up task distribution across multiple machines](https://nx.dev/core-features/distribute-task-execution)
- [Learn more how to setup CI](https://nx.dev/recipes/ci)
## Connect with us!
- [Join the community](https://nx.dev/community)
- [Subscribe to the Nx Youtube Channel](https://www.youtube.com/@nxdevtools)
- [Follow us on Twitter](https://twitter.com/nxdevtools)
"
`;
exports[`@nx/workspace:generateWorkspaceFiles README.md should be created for VueStandalone preset 1`] = `
"# Proj
<a alt="Nx logo" href="https://nx.dev" target="_blank" rel="noreferrer"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-logo.png" width="45"></a>
✨ **This workspace has been generated by [Nx, a Smart, fast and extensible build system.](https://nx.dev)** ✨
## Start the app
To start the development server run \`nx serve app1\`. Open your browser and navigate to http://localhost:4200/. Happy coding!
## Generate code
If you happen to use Nx plugins, you can leverage code generators that might come with it.
Run \`nx list\` to get a list of available plugins and whether they have generators. Then run \`nx list <plugin-name>\` to see what generators are available.
Learn more about [Nx generators on the docs](https://nx.dev/plugin-features/use-code-generators).
## Running tasks
To execute tasks with Nx use the following syntax:
\`\`\`
nx <target> <project> <...options>
\`\`\`
You can also run multiple targets:
\`\`\`
nx run-many -t <target1> <target2>
\`\`\`
..or add \`-p\` to filter specific projects
\`\`\`
nx run-many -t <target1> <target2> -p <proj1> <proj2>
\`\`\`
Targets can be defined in the \`package.json\` or \`projects.json\`. Learn more [in the docs](https://nx.dev/core-features/run-tasks).
## Want better Editor Integration?
Have a look at the [Nx Console extensions](https://nx.dev/nx-console). It provides autocomplete support, a UI for exploring and running tasks & generators, and more! Available for VSCode, IntelliJ and comes with a LSP for Vim users.
## Ready to deploy?
Just run \`nx build demoapp\` to build the application. The build artifacts will be stored in the \`dist/\` directory, ready to be deployed.
## Set up CI!
Nx comes with local caching already built-in (check your \`nx.json\`). On CI you might want to go a step further.
- [Set up remote caching](https://nx.dev/core-features/share-your-cache)
- [Set up task distribution across multiple machines](https://nx.dev/core-features/distribute-task-execution)
- [Learn more how to setup CI](https://nx.dev/recipes/ci)
## Connect with us!
- [Join the community](https://nx.dev/community)
- [Subscribe to the Nx Youtube Channel](https://www.youtube.com/@nxdevtools)
- [Follow us on Twitter](https://twitter.com/nxdevtools)
"
`;
exports[`@nx/workspace:generateWorkspaceFiles README.md should be created for WebComponents preset 1`] = `
"# Proj

View File

@ -119,6 +119,19 @@ function getPresetDependencies({
case Preset.NextJsStandalone:
return { dependencies: { '@nx/next': nxVersion }, dev: {} };
case Preset.VueMonorepo:
case Preset.VueStandalone:
return {
dependencies: {},
dev: {
'@nx/vue': nxVersion,
'@nx/cypress': e2eTestRunner === 'cypress' ? nxVersion : undefined,
'@nx/playwright':
e2eTestRunner === 'playwright' ? nxVersion : undefined,
'@nx/vite': nxVersion,
},
};
case Preset.ReactMonorepo:
case Preset.ReactStandalone:
return {

View File

@ -36,6 +36,8 @@ describe('@nx/workspace:generateWorkspaceFiles', () => {
[
Preset.ReactMonorepo,
Preset.ReactStandalone,
Preset.VueMonorepo,
Preset.VueStandalone,
Preset.AngularMonorepo,
Preset.AngularStandalone,
Preset.Nest,

View File

@ -67,6 +67,7 @@ function createAppsAndLibsFolders(tree: Tree, options: NormalizedSchema) {
} else if (
options.preset === Preset.AngularStandalone ||
options.preset === Preset.ReactStandalone ||
options.preset === Preset.VueStandalone ||
options.preset === Preset.NodeStandalone ||
options.preset === Preset.NextJsStandalone ||
options.preset === Preset.TsStandalone ||
@ -127,6 +128,7 @@ function createFiles(tree: Tree, options: NormalizedSchema) {
const filesDirName =
options.preset === Preset.AngularStandalone ||
options.preset === Preset.ReactStandalone ||
options.preset === Preset.VueStandalone ||
options.preset === Preset.NodeStandalone ||
options.preset === Preset.NextJsStandalone ||
options.preset === Preset.TsStandalone
@ -181,6 +183,7 @@ function addNpmScripts(tree: Tree, options: NormalizedSchema) {
if (
options.preset === Preset.AngularStandalone ||
options.preset === Preset.ReactStandalone ||
options.preset === Preset.VueStandalone ||
options.preset === Preset.NodeStandalone ||
options.preset === Preset.NextJsStandalone
) {

View File

@ -79,6 +79,26 @@ describe('new', () => {
});
});
it('should generate necessary npm dependencies for vue preset', async () => {
await newGenerator(tree, {
...defaultOptions,
name: 'my-workspace',
directory: 'my-workspace',
appName: 'app',
e2eTestRunner: 'cypress',
preset: Preset.VueMonorepo,
});
const { devDependencies } = readJson(tree, 'my-workspace/package.json');
expect(devDependencies).toStrictEqual({
'@nx/vue': nxVersion,
'@nx/cypress': nxVersion,
'@nx/vite': nxVersion,
'@nx/workspace': nxVersion,
nx: nxVersion,
});
});
it('should generate necessary npm dependencies for angular preset', async () => {
await newGenerator(tree, {
...defaultOptions,

View File

@ -75,3 +75,23 @@ exports[`preset should create files (preset = react-standalone bundler = webpack
},
}
`;
exports[`preset should create files (preset = vue-standalone) 1`] = `
{
"configurations": {
"development": {
"buildTarget": "proj:build:development",
"hmr": true,
},
"production": {
"buildTarget": "proj:build:production",
"hmr": false,
},
},
"defaultConfiguration": "development",
"executor": "@nx/vite:dev-server",
"options": {
"buildTarget": "proj:build",
},
}
`;

View File

@ -41,6 +41,17 @@ describe('preset', () => {
expect(readProjectConfiguration(tree, 'proj').targets.serve).toBeDefined();
});
it(`should create files (preset = ${Preset.VueMonorepo})`, async () => {
await presetGenerator(tree, {
name: 'proj',
preset: Preset.VueMonorepo,
style: 'css',
linter: 'eslint',
});
expect(tree.exists('apps/proj/src/main.ts')).toBe(true);
expect(readProjectConfiguration(tree, 'proj').targets.serve).toBeDefined();
});
it(`should create files (preset = ${Preset.NextJs})`, async () => {
await presetGenerator(tree, {
name: 'proj',
@ -99,4 +110,17 @@ describe('preset', () => {
readProjectConfiguration(tree, 'proj').targets.serve
).toMatchSnapshot();
});
it(`should create files (preset = ${Preset.VueStandalone})`, async () => {
await presetGenerator(tree, {
name: 'proj',
preset: Preset.VueStandalone,
style: 'css',
e2eTestRunner: 'cypress',
});
expect(tree.exists('vite.config.ts')).toBe(true);
expect(
readProjectConfiguration(tree, 'proj').targets.serve
).toMatchSnapshot();
});
});

View File

@ -76,6 +76,32 @@ async function createPreset(tree: Tree, options: Schema) {
e2eTestRunner: options.e2eTestRunner ?? 'cypress',
unitTestRunner: options.bundler === 'vite' ? 'vitest' : 'jest',
});
} else if (options.preset === Preset.VueMonorepo) {
const { applicationGenerator: vueApplicationGenerator } = require('@nx' +
'/vue');
return vueApplicationGenerator(tree, {
name: options.name,
directory: join('apps', options.name),
projectNameAndRootFormat: 'as-provided',
style: options.style,
linter: options.linter,
e2eTestRunner: options.e2eTestRunner ?? 'cypress',
});
} else if (options.preset === Preset.VueStandalone) {
const { applicationGenerator: vueApplicationGenerator } = require('@nx' +
'/vue');
return vueApplicationGenerator(tree, {
name: options.name,
directory: '.',
projectNameAndRootFormat: 'as-provided',
style: options.style,
linter: options.linter,
rootProject: true,
e2eTestRunner: options.e2eTestRunner ?? 'cypress',
unitTestRunner: 'vitest',
});
} else if (options.preset === Preset.NextJs) {
const { applicationGenerator: nextApplicationGenerator } = require('@nx' +
'/next');

View File

@ -16,6 +16,8 @@ export enum Preset {
ReactStandalone = 'react-standalone',
NextJsStandalone = 'nextjs-standalone',
ReactNative = 'react-native',
VueMonorepo = 'vue-monorepo',
VueStandalone = 'vue-standalone',
Expo = 'expo',
NextJs = 'next',
Nest = 'nest',

View File

@ -101,7 +101,7 @@
"@nx/typedoc-theme": ["typedoc-theme/src/index.ts"],
"@nx/vite": ["packages/vite"],
"@nx/vite/*": ["packages/vite/*"],
"@nx/vue": ["packages/vue/index.ts"],
"@nx/vue": ["packages/vue"],
"@nx/vue/*": ["packages/vue/*"],
"@nx/web": ["packages/web"],
"@nx/web/*": ["packages/web/*"],