feat(testing): e2e-ci should use serve-static or vite preview for playwright and cypress (#27240)

- fix(vite): preview should dependOn build
- fix(react): playwright should use vite preview
- fix(vue): playwright should use vite preview
- fix(web): playwright should use vite preview
- chore(testing): add e2e test

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->
Currently, `playwright` uses the `vite serve` command when setting up
the web server to run the e2e tests against.

The `vite preview` command/target should also depend on `vite build`.

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
`playwright` should use the `vite preview` command when setting up the
web server

`vite preview` targets add a `dependsOn["build"]`

Ensure `serve-static` has a dependsOn: ['build']

Cypress should use the `ciBaseUrl` if it exists when running the
`e2e-ci` targets

Migrations for Playwright and Cypress to use serve-static and preview
correctly

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Colum Ferry 2024-08-02 16:56:07 +01:00 committed by GitHub
parent 97d4184709
commit 1dcfbeeeee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 2010 additions and 63 deletions

View File

@ -18,7 +18,7 @@ describe('React Playwright e2e tests', () => {
packages: ['@nx/react'],
});
runCLI(
`generate @nx/react:app ${appName} --e2eTestRunner=playwright --projectNameAndRootFormat=as-provided --no-interactive`
`generate @nx/react:app ${appName} --e2eTestRunner=playwright --bundler=vite --projectNameAndRootFormat=as-provided --no-interactive`
);
});

View File

@ -88,6 +88,14 @@ exports[`Webpack Plugin (legacy) ConvertConfigToWebpackPlugin, should convert wi
"lint": {
"executor": "@nx/eslint:lint"
},
"serve-static": {
"executor": "@nx/web:file-server",
"dependsOn": ["build"],
"options": {
"buildTarget": "app3224373:build",
"spa": true
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],

View File

@ -113,17 +113,17 @@ describe('Webpack Plugin (legacy)', () => {
updateFile(
`${appName}/src/main.ts`,
`
document.querySelector('proj-root').innerHTML = '<h1>Welcome</h1>';
document.querySelector('proj-root')!.innerHTML = '<h1>Welcome</h1>';
`
);
updateFile(
`${appName}/webpack.config.js`,
`
const { join } = require('path');
const {NxWebpackPlugin} = require('@nx/webpack');
const {NxAppWebpackPlugin} = require('@nx/webpack/app-plugin');
module.exports = {
output: {
path: join(__dirname, '../dist/app9524918'),
path: join(__dirname, '../dist/${appName}'),
},
plugins: [
new NxAppWebpackPlugin({

View File

@ -29,6 +29,12 @@
"version": "18.1.0-beta.3",
"description": "Update to Cypress ^13.6.6 if the workspace is using Cypress v13 to ensure workspaces don't use v13.6.5 which has an issue when verifying Cypress.",
"implementation": "./src/migrations/update-18-1-0/update-cypress-version-13-6-6"
},
"update-19-6-0-update-ci-webserver-for-vite": {
"cli": "nx",
"version": "19.6.0-beta.0",
"description": "Update ciWebServerCommand to use previewTargetName if Vite is detected for the application.",
"implementation": "./src/migrations/update-19-6-0/update-ci-webserver-for-vite"
}
},
"packageJsonUpdates": {

View File

@ -131,6 +131,7 @@ export function nxE2EPreset(
webServerCommand: options?.webServerCommands?.default,
webServerCommands: options?.webServerCommands,
ciWebServerCommand: options?.ciWebServerCommand,
ciBaseUrl: options?.ciBaseUrl,
},
async setupNodeEvents(on, config) {
@ -268,6 +269,11 @@ export type NxCypressE2EPresetOptions = {
*/
ciWebServerCommand?: string;
/**
* The url of the web server for ciWebServerCommand
*/
ciBaseUrl?: string;
/**
* Configures how the web server command is started and monitored.
*/

View File

@ -47,6 +47,7 @@ export interface CypressE2EConfigSchema {
webServerCommands?: Record<string, string>;
ciWebServerCommand?: string;
ciBaseUrl?: string;
addPlugin?: boolean;
}
@ -218,10 +219,12 @@ async function addFiles(
let webServerCommands: Record<string, string>;
let ciWebServerCommand: string;
let ciBaseUrl: string;
if (hasPlugin && options.webServerCommands && options.ciWebServerCommand) {
webServerCommands = options.webServerCommands;
ciWebServerCommand = options.ciWebServerCommand;
ciBaseUrl = options.ciBaseUrl;
} else if (hasPlugin && options.devServerTarget) {
webServerCommands = {};
@ -253,6 +256,7 @@ async function addFiles(
bundler: options.bundler === 'vite' ? 'vite' : undefined,
webServerCommands,
ciWebServerCommand: ciWebServerCommand,
ciBaseUrl,
},
options.baseUrl
);

View File

@ -0,0 +1,262 @@
import updateCiWebserverForVite from './update-ci-webserver-for-vite';
import {
type Tree,
type ProjectGraph,
readNxJson,
updateNxJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
let projectGraph: ProjectGraph;
jest.mock('@nx/devkit', () => ({
...jest.requireActual<any>('@nx/devkit'),
createProjectGraphAsync: jest.fn().mockImplementation(async () => {
return projectGraph;
}),
}));
describe('updateCiWebserverForVite', () => {
let tree: Tree;
let tempFs: TempFs;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
tempFs = new TempFs('add-e2e-ci');
tree.root = tempFs.tempDir;
projectGraph = {
nodes: {},
dependencies: {},
externalNodes: {},
};
});
afterEach(() => {
tempFs.reset();
});
it('should do nothing if vite is not found for application', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins = [
{
plugin: '@nx/cypress/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'e2e-ci',
},
},
];
updateNxJson(tree, nxJson);
addProject(tree, tempFs, {
buildTargetName: 'build',
ciTargetName: 'e2e-ci',
appName: 'app',
noVite: true,
});
// ACT
await updateCiWebserverForVite(tree);
// ASSERT
expect(tree.read('app-e2e/cypress.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
...nxE2EPreset(__filename, {
cypressDir: 'src',
bundler: 'vite',
webServerCommands: {
default: 'nx run app:serve',
production: 'nx run app:preview',
},
ciWebServerCommand: 'nx run app:serve-static',
}),
baseUrl: 'http://localhost:4200',
},
});
"
`);
});
it('should update ciWebServerCommand to preview for vite app', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.plugins = [
{
plugin: '@nx/cypress/plugin',
options: {
targetName: 'e2e',
ciTargetName: 'e2e-ci',
},
},
{
plugin: '@nx/vite/plugin',
options: {
buildTargetName: 'build',
previewTargetName: 'preview',
},
},
];
updateNxJson(tree, nxJson);
addProject(tree, tempFs);
// ACT
await updateCiWebserverForVite(tree);
// ASSERT
expect(tree.read('app-e2e/cypress.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
...nxE2EPreset(__filename, {
cypressDir: 'src',
bundler: 'vite',
webServerCommands: {
default: 'nx run app:serve',
production: 'nx run app:preview',
},
ciWebServerCommand: 'nx run app:preview',
ciBaseUrl: 'http://localhost:4300',
}),
baseUrl: 'http://localhost:4200',
},
});
"
`);
});
});
function addProject(
tree: Tree,
tempFs: TempFs,
overrides: {
ciTargetName: string;
buildTargetName: string;
appName: string;
noCi?: boolean;
noVite?: boolean;
} = { ciTargetName: 'e2e-ci', buildTargetName: 'build', appName: 'app' }
) {
const appProjectConfig = {
name: overrides.appName,
root: overrides.appName,
sourceRoot: `${overrides.appName}/src`,
projectType: 'application',
};
const viteConfig = `/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
root: __dirname,
cacheDir: '../../node_modules/.vite/${overrides.appName}',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [react(), nxViteTsPaths()],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
build: {
outDir: '../../dist/${overrides.appName}',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
});`;
const e2eProjectConfig = {
name: `${overrides.appName}-e2e`,
root: `${overrides.appName}-e2e`,
sourceRoot: `${overrides.appName}-e2e/src`,
projectType: 'application',
};
const cypressConfig = `import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
...nxE2EPreset(__filename, {
cypressDir: 'src',
bundler: 'vite',
webServerCommands: {
default: 'nx run ${overrides.appName}:serve',
production: 'nx run ${overrides.appName}:preview',
},
${
!overrides.noCi
? `ciWebServerCommand: 'nx run ${overrides.appName}:serve-static',`
: ''
}
}),
baseUrl: 'http://localhost:4200',
},
});
`;
if (!overrides.noVite) {
tree.write(`${overrides.appName}/vite.config.ts`, viteConfig);
}
tree.write(
`${overrides.appName}/project.json`,
JSON.stringify(appProjectConfig)
);
tree.write(`${overrides.appName}-e2e/cypress.config.ts`, cypressConfig);
tree.write(
`${overrides.appName}-e2e/project.json`,
JSON.stringify(e2eProjectConfig)
);
if (!overrides.noVite) {
tempFs.createFile(`${overrides.appName}/vite.config.ts`, viteConfig);
}
tempFs.createFilesSync({
[`${overrides.appName}/project.json`]: JSON.stringify(appProjectConfig),
[`${overrides.appName}-e2e/cypress.config.ts`]: cypressConfig,
[`${overrides.appName}-e2e/project.json`]: JSON.stringify(e2eProjectConfig),
});
projectGraph.nodes[overrides.appName] = {
name: overrides.appName,
type: 'app',
data: {
projectType: 'application',
root: overrides.appName,
targets: {
[overrides.buildTargetName]: {},
'serve-static': {
options: {
buildTarget: overrides.buildTargetName,
},
},
},
},
};
projectGraph.nodes[`${overrides.appName}-e2e`] = {
name: `${overrides.appName}-e2e`,
type: 'app',
data: {
projectType: 'application',
root: `${overrides.appName}-e2e`,
targets: {
e2e: {},
[overrides.ciTargetName]: {},
},
},
};
}

View File

@ -0,0 +1,165 @@
import {
type Tree,
CreateNodesV2,
createProjectGraphAsync,
readNxJson,
parseTargetString,
joinPathFragments,
PluginConfiguration,
CreateNodes,
formatFiles,
} from '@nx/devkit';
import {
retrieveProjectConfigurations,
LoadedNxPlugin,
ProjectConfigurationsError,
findMatchingConfigFiles,
} from 'nx/src/devkit-internals';
import { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils';
import { tsquery } from '@phenomnomnominal/tsquery';
import { CypressPluginOptions } from '../../plugins/plugin';
export default async function (tree: Tree) {
const pluginName = '@nx/cypress/plugin';
const graph = await createProjectGraphAsync();
const nxJson = readNxJson(tree);
const matchingPluginRegistrations = nxJson.plugins?.filter((p) =>
typeof p === 'string' ? p === pluginName : p.plugin === pluginName
);
const {
createNodesV2,
}: { createNodesV2: CreateNodesV2<CypressPluginOptions> } = await import(
pluginName
);
for (const plugin of matchingPluginRegistrations) {
let projectConfigs: ConfigurationResult;
try {
const loadedPlugin = new LoadedNxPlugin(
{ createNodesV2, name: pluginName },
plugin
);
projectConfigs = await retrieveProjectConfigurations(
[loadedPlugin],
tree.root,
nxJson
);
} catch (e) {
if (e instanceof ProjectConfigurationsError) {
projectConfigs = e.partialProjectConfigurationsResult;
} else {
throw e;
}
}
for (const configFile of projectConfigs.matchingProjectFiles) {
const configFileContents = tree.read(configFile, 'utf-8');
if (!configFileContents.includes('ciWebServerCommand')) {
continue;
}
const ast = tsquery.ast(configFileContents);
const CI_WEBSERVER_COMMAND_SELECTOR =
'ObjectLiteralExpression PropertyAssignment:has(Identifier[name=ciWebServerCommand]) > StringLiteral';
const nodes = tsquery(ast, CI_WEBSERVER_COMMAND_SELECTOR, {
visitAllChildren: true,
});
if (!nodes.length) {
continue;
}
const ciWebServerCommand = nodes[0].getText();
const NX_TARGET_REGEX = "(?<=nx run )[^']+";
const matches = ciWebServerCommand.match(NX_TARGET_REGEX);
if (!matches) {
continue;
}
const targetString = matches[0];
const { project, target, configuration } = parseTargetString(
targetString,
graph
);
const pathToViteConfig = [
joinPathFragments(graph.nodes[project].data.root, 'vite.config.ts'),
joinPathFragments(graph.nodes[project].data.root, 'vite.config.js'),
].find((p) => tree.exists(p));
if (!pathToViteConfig) {
continue;
}
const viteConfigContents = tree.read(pathToViteConfig, 'utf-8');
if (!viteConfigContents.includes('preview:')) {
continue;
}
const matchingVitePlugin = await findPluginForConfigFile(
tree,
'@nx/vite/plugin',
pathToViteConfig
);
const previewTargetName = matchingVitePlugin
? typeof matchingVitePlugin === 'string'
? 'preview'
: (matchingVitePlugin.options as any)?.previewTargetName ?? 'preview'
: 'preview';
tree.write(
configFile,
`${configFileContents.slice(
0,
nodes[0].getStart()
)}'nx run ${project}:${previewTargetName}',
ciBaseUrl: "http://localhost:4300"${configFileContents.slice(
nodes[0].getEnd()
)}`
);
}
}
await formatFiles(tree);
}
async function findPluginForConfigFile(
tree: Tree,
pluginName: string,
pathToConfigFile: string
): Promise<PluginConfiguration> {
const nxJson = readNxJson(tree);
if (!nxJson.plugins) {
return;
}
const pluginRegistrations: PluginConfiguration[] = nxJson.plugins.filter(
(p) => (typeof p === 'string' ? p === pluginName : p.plugin === pluginName)
);
for (const plugin of pluginRegistrations) {
if (typeof plugin === 'string') {
return plugin;
}
if (!plugin.include && !plugin.exclude) {
return plugin;
}
if (plugin.include || plugin.exclude) {
const resolvedPlugin: {
createNodes?: CreateNodes;
createNodesV2?: CreateNodesV2;
} = await import(pluginName);
const pluginGlob =
resolvedPlugin.createNodesV2?.[0] ?? resolvedPlugin.createNodes?.[0];
const matchingConfigFile = findMatchingConfigFiles(
[pathToConfigFile],
pluginGlob,
plugin.include,
plugin.exclude
);
if (matchingConfigFile.length) {
return plugin;
}
}
}
}

View File

@ -257,6 +257,8 @@ async function buildCypressTargets(
excludeSpecPatterns
);
const ciBaseUrl = pluginPresetOptions?.ciBaseUrl;
const dependsOn: TargetConfiguration['dependsOn'] = [];
const outputs = getOutputs(projectRoot, cypressConfig, 'e2e');
const inputs = getInputs(namedInputs);
@ -273,7 +275,9 @@ async function buildCypressTargets(
outputs,
inputs,
cache: true,
command: `cypress run --env webServerCommand="${ciWebServerCommand}" --spec ${relativeSpecFilePath}`,
command: `cypress run --env webServerCommand="${ciWebServerCommand}" --spec ${relativeSpecFilePath}${
ciBaseUrl ? ` --config='{"baseUrl": "${ciBaseUrl}"}'` : ''
}`,
options: {
cwd: projectRoot,
},

View File

@ -8,7 +8,10 @@ export { getExecutorInformation } from './command-line/run/executor-utils';
export { readNxJson as readNxJsonFromDisk } from './config/nx-json';
export { calculateDefaultProjectName } from './config/calculate-default-project-name';
export { retrieveProjectConfigurationsWithAngularProjects } from './project-graph/utils/retrieve-workspace-files';
export { mergeTargetConfigurations } from './project-graph/utils/project-configuration-utils';
export {
mergeTargetConfigurations,
findMatchingConfigFiles,
} from './project-graph/utils/project-configuration-utils';
export { readProjectConfigurationsFromRootMap } from './project-graph/utils/project-configuration-utils';
export { splitTarget } from './utils/split-target';
export { combineOptionsForExecutor } from './utils/params';

View File

@ -505,12 +505,12 @@ function mergeCreateNodesResults(
return { projectRootMap, externalNodes, rootMap, configurationSourceMaps };
}
function findMatchingConfigFiles(
export function findMatchingConfigFiles(
projectFiles: string[],
pattern: string,
include: string[],
exclude: string[]
) {
): string[] {
const matchingConfigFiles: string[] = [];
for (const file of projectFiles) {

View File

@ -11,6 +11,12 @@
"version": "18.1.0-beta.3",
"description": "Remove invalid baseUrl option from @nx/playwright:playwright targets in project.json.",
"implementation": "./src/migrations/update-18-1-0/remove-baseUrl-from-project-json"
},
"19-6-0-use-serve-static-preview-for-command": {
"cli": "nx",
"version": "19.6.0-beta.0",
"description": "Use serve-static or preview for webServerCommand.",
"implementation": "./src/migrations/update-19-6-0/use-serve-static-preview-for-command"
}
}
}

View File

@ -0,0 +1,234 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { ProjectGraph, type Tree } from '@nx/devkit';
import useServeStaticPreviewForCommand from './use-serve-static-preview-for-command';
import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
let projectGraph: ProjectGraph;
jest.mock('@nx/devkit', () => ({
...jest.requireActual<any>('@nx/devkit'),
createProjectGraphAsync: jest.fn().mockImplementation(async () => {
return projectGraph;
}),
}));
describe('useServeStaticPreviewForCommand', () => {
let tree: Tree;
let tempFs: TempFs;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
tempFs = new TempFs('add-e2e-ci');
tree.root = tempFs.tempDir;
projectGraph = {
nodes: {},
dependencies: {},
externalNodes: {},
};
});
afterEach(() => {
tempFs.reset();
});
it('should update when it does not use serve-static for non-vite', async () => {
// ARRANGE
addProject(tree, tempFs, { noVite: true });
// ACT
await useServeStaticPreviewForCommand(tree);
// ASSERT
expect(tree.read('app-e2e/playwright.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { defineConfig, devices } from '@playwright/test';
import { nxE2EPreset } from '@nx/playwright/preset';
import { workspaceRoot } from '@nx/devkit';
const baseURL = process.env['BASE_URL'] || 'http://localhost:4200';
export default defineConfig({
...nxE2EPreset(__filename, { testDir: './src' }),
use: {
baseURL,
trace: 'on-first-retry',
},
webServer: {
command: 'npx nx run app:serve-static',
url: 'http://localhost:4200',
reuseExistingServer: !process.env.CI,
cwd: workspaceRoot,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});
"
`);
});
it('should update when it does not use preview for vite', async () => {
// ARRANGE
addProject(tree, tempFs);
// ACT
await useServeStaticPreviewForCommand(tree);
// ASSERT
expect(tree.read('app-e2e/playwright.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { defineConfig, devices } from '@playwright/test';
import { nxE2EPreset } from '@nx/playwright/preset';
import { workspaceRoot } from '@nx/devkit';
const baseURL = process.env['BASE_URL'] || 'http://localhost:4300';
export default defineConfig({
...nxE2EPreset(__filename, { testDir: './src' }),
use: {
baseURL,
trace: 'on-first-retry',
},
webServer: {
command: 'npx nx run app:preview',
url: 'http://localhost:4300',
reuseExistingServer: !process.env.CI,
cwd: workspaceRoot,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});
"
`);
});
});
const basePlaywrightConfig = (
appName: string
) => `import { defineConfig, devices } from '@playwright/test';
import { nxE2EPreset } from '@nx/playwright/preset';
import { workspaceRoot } from '@nx/devkit';
const baseURL = process.env['BASE_URL'] || 'http://localhost:4200';
export default defineConfig({
...nxE2EPreset(__filename, { testDir: './src' }),
use: {
baseURL,
trace: 'on-first-retry',
},
webServer: {
command: 'npx nx run ${appName}:serve',
url: 'http://localhost:4200',
reuseExistingServer: !process.env.CI,
cwd: workspaceRoot,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});`;
const viteConfig = `/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
root: __dirname,
cacheDir: '../../node_modules/.vite/app',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [react(), nxViteTsPaths()],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
build: {
outDir: '../../dist/app',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
});`;
function addProject(
tree: Tree,
tempFs: TempFs,
overrides: {
noVite?: boolean;
} = {}
) {
const appProjectConfig = {
name: 'app',
root: 'app',
sourceRoot: `${'app'}/src`,
projectType: 'application',
};
const e2eProjectConfig = {
name: `app-e2e`,
root: `app-e2e`,
sourceRoot: `app-e2e/src`,
projectType: 'application',
};
if (!overrides.noVite) {
tree.write(`app/vite.config.ts`, viteConfig);
} else {
tree.write(`app/webpack.config.ts`, ``);
}
tree.write(`app/project.json`, JSON.stringify(appProjectConfig));
tree.write(`app-e2e/playwright.config.ts`, basePlaywrightConfig('app'));
tree.write(`app-e2e/project.json`, JSON.stringify(e2eProjectConfig));
if (!overrides.noVite) {
tempFs.createFile(`app/vite.config.ts`, viteConfig);
} else {
tempFs.createFile(`app/webpack.config.ts`, ``);
}
tempFs.createFilesSync({
[`app/project.json`]: JSON.stringify(appProjectConfig),
[`app-e2e/playwright.config.ts`]: basePlaywrightConfig('app'),
[`app-e2e/project.json`]: JSON.stringify(e2eProjectConfig),
});
projectGraph.nodes['app'] = {
name: 'app',
type: 'app',
data: {
projectType: 'application',
root: 'app',
targets: {},
},
};
projectGraph.nodes[`app-e2e`] = {
name: `app-e2e`,
type: 'app',
data: {
projectType: 'application',
root: `app-e2e`,
targets: {
e2e: {},
},
},
};
}

View File

@ -0,0 +1,142 @@
import {
createProjectGraphAsync,
formatFiles,
getPackageManagerCommand,
joinPathFragments,
parseTargetString,
type Tree,
visitNotIgnoredFiles,
} from '@nx/devkit';
import { tsquery } from '@phenomnomnominal/tsquery';
export default async function (tree: Tree) {
const graph = await createProjectGraphAsync();
visitNotIgnoredFiles(tree, '', (path) => {
if (!path.endsWith('playwright.config.ts')) {
return;
}
let playwrightConfigFileContents = tree.read(path, 'utf-8');
const WEBSERVER_COMMAND_SELECTOR =
'PropertyAssignment:has(Identifier[name=webServer]) PropertyAssignment:has(Identifier[name=command]) > StringLiteral';
let ast = tsquery.ast(playwrightConfigFileContents);
const nodes = tsquery(ast, WEBSERVER_COMMAND_SELECTOR, {
visitAllChildren: true,
});
if (!nodes.length) {
return;
}
const commandValueNode = nodes[0];
const command = commandValueNode.getText();
let project: string;
if (command.includes('nx run')) {
const NX_TARGET_REGEX = "(?<=nx run )[^']+";
const matches = command.match(NX_TARGET_REGEX);
if (!matches) {
return;
}
const targetString = matches[0];
const parsedTargetString = parseTargetString(targetString, graph);
if (
parsedTargetString.target === 'serve-static' ||
parsedTargetString.target === 'preview'
) {
return;
}
project = parsedTargetString.project;
} else {
const NX_PROJECT_REGEX = "(?<=nx [^ ]+ )[^']+";
const matches = command.match(NX_PROJECT_REGEX);
if (!matches) {
return;
}
project = matches[0];
}
const pathToViteConfig = [
joinPathFragments(graph.nodes[project].data.root, 'vite.config.ts'),
joinPathFragments(graph.nodes[project].data.root, 'vite.config.js'),
].find((p) => tree.exists(p));
if (!pathToViteConfig) {
const newCommand = `${
getPackageManagerCommand().exec
} nx run ${project}:serve-static`;
tree.write(
path,
`${playwrightConfigFileContents.slice(
0,
commandValueNode.getStart()
)}"${newCommand}"${playwrightConfigFileContents.slice(
commandValueNode.getEnd()
)}`
);
} else {
const newCommand = `${
getPackageManagerCommand().exec
} nx run ${project}:preview`;
tree.write(
path,
`${playwrightConfigFileContents.slice(
0,
commandValueNode.getStart()
)}"${newCommand}"${playwrightConfigFileContents.slice(
commandValueNode.getEnd()
)}`
);
playwrightConfigFileContents = tree.read(path, 'utf-8');
ast = tsquery.ast(playwrightConfigFileContents);
const BASE_URL_SELECTOR =
'VariableDeclaration:has(Identifier[name=baseURL])';
const baseUrlNodes = tsquery(ast, BASE_URL_SELECTOR, {
visitAllChildren: true,
});
if (!baseUrlNodes.length) {
return;
}
const baseUrlNode = baseUrlNodes[0];
const newBaseUrlVariableDeclaration =
"baseURL = process.env['BASE_URL'] || 'http://localhost:4300';";
tree.write(
path,
`${playwrightConfigFileContents.slice(
0,
baseUrlNode.getStart()
)}${newBaseUrlVariableDeclaration}${playwrightConfigFileContents.slice(
baseUrlNode.getEnd()
)}`
);
playwrightConfigFileContents = tree.read(path, 'utf-8');
ast = tsquery.ast(playwrightConfigFileContents);
const WEB_SERVER_URL_SELECTOR =
'PropertyAssignment:has(Identifier[name=webServer]) PropertyAssignment:has(Identifier[name=url]) > StringLiteral';
const webServerUrlNodes = tsquery(ast, WEB_SERVER_URL_SELECTOR, {
visitAllChildren: true,
});
if (!webServerUrlNodes.length) {
return;
}
const webServerUrlNode = webServerUrlNodes[0];
const newWebServerUrl = "'http://localhost:4300'";
tree.write(
path,
`${playwrightConfigFileContents.slice(
0,
webServerUrlNode.getStart()
)}${newWebServerUrl}${playwrightConfigFileContents.slice(
webServerUrlNode.getEnd()
)}`
);
}
});
await formatFiles(tree);
}

View File

@ -18,6 +18,8 @@ export async function addE2e(
styledModule: null,
hasStyles: false,
unitTestRunner: 'none',
e2eCiWebServerTarget: options.e2eWebServerTarget,
e2eCiBaseUrl: options.e2eWebServerAddress,
});
case 'playwright':
return addE2eReact(host, {
@ -27,6 +29,8 @@ export async function addE2e(
styledModule: null,
hasStyles: false,
unitTestRunner: 'none',
e2eCiWebServerTarget: options.e2eWebServerTarget,
e2eCiBaseUrl: options.e2eWebServerAddress,
});
case 'detox':
const { detoxApplicationGenerator } = ensurePackage<

View File

@ -210,6 +210,60 @@ nxViteTsPaths()],
});"
`;
exports[`app not nested should add vite types to tsconfigs 1`] = `
"
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
root: __dirname,
cacheDir: '../node_modules/.vite/my-app',
server:{
port: 4200,
host: 'localhost',
},
preview:{
port: 4300,
host: 'localhost',
},
plugins: [react(),
nxViteTsPaths()],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
build: {
outDir: '../dist/my-app',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: '../coverage/my-app',
provider: 'v8',
}
},
});"
`;
exports[`app not nested should generate files 1`] = `
"// eslint-disable-next-line @typescript-eslint/no-unused-vars
import styles from './app.module.css';
@ -228,6 +282,133 @@ export default App;
"
`;
exports[`app not nested should setup playwright correctly for vite 1`] = `
"import { defineConfig, devices } from '@playwright/test';
import { nxE2EPreset } from '@nx/playwright/preset';
import { workspaceRoot } from '@nx/devkit';
// For CI, you may want to set BASE_URL to the deployed application.
const baseURL = process.env['BASE_URL'] || 'http://localhost:4300';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
...nxE2EPreset(__filename, { testDir: './src' }),
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
baseURL,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Run your local dev server before starting the tests */
webServer: {
command: 'npx nx run my-app:preview',
url: 'http://localhost:4300',
reuseExistingServer: !process.env.CI,
cwd: workspaceRoot
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
// Uncomment for mobile browsers support
/* {
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
}, */
// Uncomment for branded browsers
/* {
name: 'Microsoft Edge',
use: { ...devices['Desktop Edge'], channel: 'msedge' },
},
{
name: 'Google Chrome',
use: { ...devices['Desktop Chrome'], channel: 'chrome' },
} */
],
});
"
`;
exports[`app not nested should use preview vite types to tsconfigs 1`] = `
"
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
root: __dirname,
cacheDir: '../node_modules/.vite/my-app',
server:{
port: 4200,
host: 'localhost',
},
preview:{
port: 4300,
host: 'localhost',
},
plugins: [react(),
nxViteTsPaths()],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
build: {
outDir: '../dist/my-app',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: '../coverage/my-app',
provider: 'v8',
}
},
});"
`;
exports[`app setup React app with --bundler=vite should setup targets with vite configuration 1`] = `null`;
exports[`app should add custom webpack config 1`] = `

View File

@ -69,6 +69,78 @@ describe('app', () => {
'@nx/react/typings/cssmodule.d.ts',
'@nx/react/typings/image.d.ts',
]);
expect(appTree.read('my-app/vite.config.ts', 'utf-8')).toMatchSnapshot();
});
it('should setup cypress correctly for vite', async () => {
await applicationGenerator(appTree, {
...schema,
bundler: 'vite',
unitTestRunner: 'vitest',
addPlugin: true,
});
expect(appTree.read('my-app-e2e/cypress.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: { ...nxE2EPreset(__filename, {"cypressDir":"src","bundler":"vite","webServerCommands":{"default":"nx run my-app:serve","production":"nx run my-app:preview"},"ciWebServerCommand":"nx run my-app:preview","ciBaseUrl":"http://localhost:4300"}),
baseUrl: 'http://localhost:4200' }
});
"
`);
});
it('should setup playwright correctly for vite', async () => {
const nxJson = readNxJson(appTree);
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: '@nx/vite/plugin',
options: {
buildTargetName: 'build',
previewTargetName: 'preview',
},
});
updateNxJson(appTree, nxJson);
await applicationGenerator(appTree, {
...schema,
bundler: 'vite',
unitTestRunner: 'vitest',
e2eTestRunner: 'playwright',
addPlugin: true,
});
expect(
appTree.read('my-app-e2e/playwright.config.ts', 'utf-8')
).toMatchSnapshot();
});
it('should use preview vite types to tsconfigs', async () => {
await applicationGenerator(appTree, {
...schema,
bundler: 'vite',
unitTestRunner: 'vitest',
});
const tsconfigApp = readJson(appTree, 'my-app/tsconfig.app.json');
expect(tsconfigApp.compilerOptions.types).toEqual([
'node',
'@nx/react/typings/cssmodule.d.ts',
'@nx/react/typings/image.d.ts',
'vite/client',
]);
const tsconfigSpec = readJson(appTree, 'my-app/tsconfig.spec.json');
expect(tsconfigSpec.compilerOptions.types).toEqual([
'vitest/globals',
'vitest/importMeta',
'vite/client',
'node',
'vitest',
'@nx/react/typings/cssmodule.d.ts',
'@nx/react/typings/image.d.ts',
]);
expect(appTree.read('my-app/vite.config.ts', 'utf-8')).toMatchSnapshot();
});
it('should not overwrite default project if already set', async () => {

View File

@ -16,19 +16,18 @@ export async function addE2e(
tree: Tree,
options: NormalizedSchema
): Promise<GeneratorCallback> {
const hasNxBuildPlugin =
(options.bundler === 'webpack' && hasWebpackPlugin(tree)) ||
(options.bundler === 'vite' && hasVitePlugin(tree));
if (!hasNxBuildPlugin) {
await webStaticServeGenerator(tree, {
buildTarget: `${options.projectName}:build`,
targetName: 'serve-static',
spa: true,
});
}
switch (options.e2eTestRunner) {
case 'cypress': {
const hasNxBuildPlugin =
(options.bundler === 'webpack' && hasWebpackPlugin(tree)) ||
(options.bundler === 'vite' && hasVitePlugin(tree));
if (!hasNxBuildPlugin) {
await webStaticServeGenerator(tree, {
buildTarget: `${options.projectName}:build`,
targetName: 'serve-static',
spa: true,
});
}
const { configurationGenerator } = ensurePackage<
typeof import('@nx/cypress')
>('@nx/cypress', nxVersion);
@ -60,8 +59,10 @@ export async function addE2e(
}
: undefined,
ciWebServerCommand: hasNxBuildPlugin
? `nx run ${options.projectName}:serve-static`
? `nx run ${options.projectName}:${options.e2eCiWebServerTarget}`
: undefined,
ciBaseUrl:
options.bundler === 'vite' ? options.e2eCiBaseUrl : undefined,
});
}
case 'playwright': {
@ -83,10 +84,10 @@ export async function addE2e(
js: false,
linter: options.linter,
setParserOptionsProject: options.setParserOptionsProject,
webServerCommand: `${getPackageManagerCommand().exec} nx ${
options.e2eWebServerTarget
} ${options.name}`,
webServerAddress: options.e2eWebServerAddress,
webServerCommand: `${getPackageManagerCommand().exec} nx run ${
options.projectName
}:${options.e2eCiWebServerTarget}`,
webServerAddress: options.e2eCiBaseUrl,
rootProject: options.rootProject,
addPlugin: options.addPlugin,
});

View File

@ -46,32 +46,43 @@ export async function normalizeOptions<T extends Schema = Schema>(
options.rootProject = appProjectRoot === '.';
options.projectNameAndRootFormat = projectNameAndRootFormat;
let e2ePort = options.devServerPort ?? 4200;
let e2eWebServerTarget = 'serve';
let e2eCiWebServerTarget =
options.bundler === 'vite' ? 'preview' : 'serve-static';
if (options.addPlugin) {
if (nxJson.plugins) {
for (const plugin of nxJson.plugins) {
if (
options.bundler === 'vite' &&
typeof plugin === 'object' &&
plugin.plugin === '@nx/vite/plugin' &&
(plugin.options as VitePluginOptions).serveTargetName
plugin.plugin === '@nx/vite/plugin'
) {
e2eWebServerTarget = (plugin.options as VitePluginOptions)
.serveTargetName;
e2eCiWebServerTarget =
(plugin.options as VitePluginOptions)?.previewTargetName ??
e2eCiWebServerTarget;
e2eWebServerTarget =
(plugin.options as VitePluginOptions)?.serveTargetName ??
e2eWebServerTarget;
} else if (
options.bundler === 'webpack' &&
typeof plugin === 'object' &&
plugin.plugin === '@nx/webpack/plugin' &&
(plugin.options as WebpackPluginOptions).serveTargetName
plugin.plugin === '@nx/webpack/plugin'
) {
e2eWebServerTarget = (plugin.options as WebpackPluginOptions)
.serveTargetName;
e2eCiWebServerTarget =
(plugin.options as WebpackPluginOptions)?.serveStaticTargetName ??
e2eCiWebServerTarget;
e2eWebServerTarget =
(plugin.options as WebpackPluginOptions)?.serveTargetName ??
e2eWebServerTarget;
}
}
}
}
let e2ePort = options.devServerPort ?? 4200;
if (
nxJson.targetDefaults?.[e2eWebServerTarget] &&
nxJson.targetDefaults?.[e2eWebServerTarget].options?.port
@ -82,6 +93,10 @@ export async function normalizeOptions<T extends Schema = Schema>(
const e2eProjectName = options.rootProject ? 'e2e' : `${appProjectName}-e2e`;
const e2eProjectRoot = options.rootProject ? 'e2e' : `${appProjectRoot}-e2e`;
const e2eWebServerAddress = `http://localhost:${e2ePort}`;
const e2eCiBaseUrl =
options.bundler === 'vite'
? 'http://localhost:4300'
: `http://localhost:${e2ePort}`;
const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim())
@ -104,6 +119,8 @@ export async function normalizeOptions<T extends Schema = Schema>(
e2eProjectRoot,
e2eWebServerAddress,
e2eWebServerTarget,
e2eCiWebServerTarget,
e2eCiBaseUrl,
e2ePort,
parsedTags,
fileName,

View File

@ -38,6 +38,8 @@ export interface NormalizedSchema<T extends Schema = Schema> extends T {
e2eProjectRoot: string;
e2eWebServerAddress: string;
e2eWebServerTarget: string;
e2eCiWebServerTarget: string;
e2eCiBaseUrl: string;
e2ePort: number;
parsedTags: string[];
fileName: string;

View File

@ -32,6 +32,11 @@
"version": "17.3.0-beta.0",
"description": "Move the vitest coverage thresholds in their own object if exists and add reporters.",
"implementation": "./src/migrations/update-17-3-0/vitest-coverage-and-reporters"
},
"update-19-6-0-add-depends-on-for-preview-server": {
"version": "19.6.0-beta.0",
"description": "Add dependsOn: [build] to preview targets using preview-server",
"implementation": "./src/migrations/update-19-6-0/add-depends-on-for-preview"
}
},
"packageJsonUpdates": {

View File

@ -0,0 +1,74 @@
import addDependsOnForPreview from './add-depends-on-for-preview';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { readJson } from '@nx/devkit';
describe('addDependsOnForPreview', () => {
it('should update when preview target exists in project.json', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
tree.write(
'apps/app/project.json',
JSON.stringify({
name: 'app',
root: 'apps/app',
projectType: 'application',
targets: {
preview: {
executor: '@nx/vite:preview-server',
},
},
})
);
// ACT
await addDependsOnForPreview(tree);
// ASSERT
expect(readJson(tree, 'apps/app/project.json').targets)
.toMatchInlineSnapshot(`
{
"preview": {
"dependsOn": [
"build",
],
"executor": "@nx/vite:preview-server",
},
}
`);
});
it('should not update when preview target exists in project.json and has a dependsOn already', async () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
tree.write(
'apps/app/project.json',
JSON.stringify({
name: 'app',
root: 'apps/app',
projectType: 'application',
targets: {
preview: {
dependsOn: ['build'],
executor: '@nx/vite:preview-server',
},
},
})
);
// ACT
await addDependsOnForPreview(tree);
// ASSERT
expect(readJson(tree, 'apps/app/project.json').targets)
.toMatchInlineSnapshot(`
{
"preview": {
"dependsOn": [
"build",
],
"executor": "@nx/vite:preview-server",
},
}
`);
});
});

View File

@ -0,0 +1,26 @@
import {
type Tree,
formatFiles,
readProjectConfiguration,
updateProjectConfiguration,
} from '@nx/devkit';
import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils';
import { VitePreviewServerExecutorOptions } from '../../executors/preview-server/schema';
export default async function (tree: Tree) {
forEachExecutorOptions<VitePreviewServerExecutorOptions>(
tree,
'@nx/vite:preview-server',
(_, projectName, targetName) => {
const project = readProjectConfiguration(tree, projectName);
project.targets[targetName].dependsOn ??= [];
if (project.targets[targetName].dependsOn.includes('build')) {
return;
}
project.targets[targetName].dependsOn.push('build');
updateProjectConfiguration(tree, projectName, project);
}
);
await formatFiles(tree);
}

View File

@ -126,6 +126,9 @@ exports[`@nx/vite/plugin not root project should create nodes 1`] = `
},
"preview-site": {
"command": "vite preview",
"dependsOn": [
"build-something",
],
"metadata": {
"description": "Locally preview Vite production build",
"help": {
@ -209,6 +212,9 @@ exports[`@nx/vite/plugin root project should create nodes 1`] = `
},
"preview": {
"command": "vite preview",
"dependsOn": [
"build",
],
"metadata": {
"description": "Locally preview Vite production build",
"help": {

View File

@ -193,7 +193,10 @@ async function buildViteTargets(
// If running in library mode, then there is nothing to serve.
if (!viteConfig.build?.lib) {
targets[options.serveTargetName] = serveTarget(projectRoot);
targets[options.previewTargetName] = previewTarget(projectRoot);
targets[options.previewTargetName] = previewTarget(
projectRoot,
options.buildTargetName
);
targets[options.serveStaticTargetName] = serveStaticTarget(options) as {};
}
}
@ -272,9 +275,10 @@ function serveTarget(projectRoot: string) {
return targetConfig;
}
function previewTarget(projectRoot: string) {
function previewTarget(projectRoot: string, buildTargetName) {
const targetConfig: TargetConfiguration = {
command: `vite preview`,
dependsOn: [buildTargetName],
options: {
cwd: joinPathFragments(projectRoot),
},

View File

@ -204,6 +204,7 @@ export function addPreviewTarget(
// Adds a preview target.
project.targets.preview = {
dependsOn: ['build'],
executor: '@nx/vite:preview-server',
defaultConfiguration: 'development',
options: previewOptions,

View File

@ -1,5 +1,157 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`application generator should set up project correctly for cypress 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 for cypress 2`] = `
"/// <reference types='vitest' />
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
root: __dirname,
cacheDir: '../node_modules/.vite/test',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [vue(), nxViteTsPaths()],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
build: {
outDir: '../dist/test',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: '../coverage/test',
provider: 'v8',
},
},
});
"
`;
exports[`application generator should set up project correctly for cypress 3`] = `
"{
"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": {
"vue/multi-word-component-names": "off"
}
}
]
}
"
`;
exports[`application generator should set up project correctly for cypress 4`] = `
"import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import App from './App.vue';
describe('App', () => {
it('renders properly', async () => {
const wrapper = mount(App, {});
expect(wrapper.text()).toContain('Welcome test 👋');
});
});
"
`;
exports[`application generator should set up project correctly for cypress 5`] = `
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
...nxE2EPreset(__filename, {
cypressDir: 'src',
bundler: 'vite',
webServerCommands: {
default: 'nx run test:serve',
production: 'nx run test:preview',
},
ciWebServerCommand: 'nx run test:preview',
ciBaseUrl: 'http://localhost:4300',
}),
baseUrl: 'http://localhost:4200',
},
});
"
`;
exports[`application generator should set up project correctly with PascalCase name 1`] = `
"{
"root": true,
@ -290,6 +442,79 @@ describe('App', () => {
`;
exports[`application generator should set up project correctly with given options 5`] = `
"import { defineConfig, devices } from '@playwright/test';
import { nxE2EPreset } from '@nx/playwright/preset';
import { workspaceRoot } from '@nx/devkit';
// For CI, you may want to set BASE_URL to the deployed application.
const baseURL = process.env['BASE_URL'] || 'http://localhost:4300';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
...nxE2EPreset(__filename, { testDir: './src' }),
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
baseURL,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Run your local dev server before starting the tests */
webServer: {
command: 'npx nx run test:preview',
url: 'http://localhost:4300',
reuseExistingServer: !process.env.CI,
cwd: workspaceRoot,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// Uncomment for mobile browsers support
/* {
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
}, */
// Uncomment for branded browsers
/* {
name: 'Microsoft Edge',
use: { ...devices['Desktop Edge'], channel: 'msedge' },
},
{
name: 'Google Chrome',
use: { ...devices['Desktop Chrome'], channel: 'chrome' },
} */
],
});
"
`;
exports[`application generator should set up project correctly with given options 6`] = `
[
".eslintignore",
".eslintrc.json",

View File

@ -1,7 +1,12 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Tree, readProjectConfiguration } from '@nx/devkit';
import {
Tree,
readProjectConfiguration,
readNxJson,
updateNxJson,
} from '@nx/devkit';
import { applicationGenerator } from './application';
import { Schema } from './schema';
@ -21,14 +26,55 @@ describe('application generator', () => {
});
it('should set up project correctly with given options', async () => {
await applicationGenerator(tree, { ...options, unitTestRunner: 'vitest' });
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: '@nx/vite/plugin',
options: {
buildTargetName: 'build',
previewTargetName: 'preview',
},
});
updateNxJson(tree, nxJson);
await applicationGenerator(tree, {
...options,
unitTestRunner: 'vitest',
e2eTestRunner: 'playwright',
});
expect(tree.read('.eslintrc.json', 'utf-8')).toMatchSnapshot();
expect(tree.read('test/vite.config.ts', 'utf-8')).toMatchSnapshot();
expect(tree.read('test/.eslintrc.json', 'utf-8')).toMatchSnapshot();
expect(tree.read('test/src/app/App.spec.ts', 'utf-8')).toMatchSnapshot();
expect(
tree.read('test-e2e/playwright.config.ts', 'utf-8')
).toMatchSnapshot();
expect(listFiles(tree)).toMatchSnapshot();
});
it('should set up project correctly for cypress', async () => {
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: '@nx/vite/plugin',
options: {
buildTargetName: 'build',
previewTargetName: 'preview',
},
});
updateNxJson(tree, nxJson);
await applicationGenerator(tree, {
...options,
addPlugin: true,
unitTestRunner: 'vitest',
e2eTestRunner: 'cypress',
});
expect(tree.read('.eslintrc.json', 'utf-8')).toMatchSnapshot();
expect(tree.read('test/vite.config.ts', 'utf-8')).toMatchSnapshot();
expect(tree.read('test/.eslintrc.json', 'utf-8')).toMatchSnapshot();
expect(tree.read('test/src/app/App.spec.ts', 'utf-8')).toMatchSnapshot();
expect(tree.read('test-e2e/cypress.config.ts', 'utf-8')).toMatchSnapshot();
});
it('should set up project correctly with PascalCase name', async () => {
await applicationGenerator(tree, {
...options,

View File

@ -15,14 +15,24 @@ export async function addE2e(
tree: Tree,
options: NormalizedSchema
): Promise<GeneratorCallback> {
const nxJson = readNxJson(tree);
const hasPlugin = nxJson.plugins?.find((p) =>
typeof p === 'string'
? p === '@nx/vite/plugin'
: p.plugin === '@nx/vite/plugin'
);
const e2eWebServerTarget = hasPlugin
? typeof hasPlugin === 'string'
? 'serve'
: (hasPlugin.options as any)?.serveTargetName ?? 'serve'
: 'serve';
const e2eCiWebServerTarget = hasPlugin
? typeof hasPlugin === 'string'
? 'preview'
: (hasPlugin.options as any)?.previewTargetName ?? 'preview'
: 'preview';
switch (options.e2eTestRunner) {
case 'cypress': {
const nxJson = readNxJson(tree);
const hasPlugin = nxJson.plugins?.some((p) =>
typeof p === 'string'
? p === '@nx/vite/plugin'
: p.plugin === '@nx/vite/plugin'
);
if (!hasPlugin) {
await webStaticServeGenerator(tree, {
buildTarget: `${options.projectName}:build`,
@ -48,9 +58,17 @@ export async function addE2e(
directory: 'src',
bundler: 'vite',
skipFormat: true,
devServerTarget: `${options.projectName}:serve`,
devServerTarget: `${options.projectName}:${e2eWebServerTarget}`,
baseUrl: 'http://localhost:4200',
jsx: true,
webServerCommands: hasPlugin
? {
default: `nx run ${options.projectName}:${e2eWebServerTarget}`,
production: `nx run ${options.projectName}:preview`,
}
: undefined,
ciWebServerCommand: `nx run ${options.projectName}:${e2eCiWebServerTarget}`,
ciBaseUrl: 'http://localhost:4300',
});
}
case 'playwright': {
@ -73,10 +91,10 @@ export async function addE2e(
js: false,
linter: options.linter,
setParserOptionsProject: options.setParserOptionsProject,
webServerCommand: `${getPackageManagerCommand().exec} nx serve ${
options.name
}`,
webServerAddress: 'http://localhost:4200',
webServerCommand: `${getPackageManagerCommand().exec} nx run ${
options.projectName
}:${e2eCiWebServerTarget}`,
webServerAddress: 'http://localhost:4300',
});
}
case 'none':

View File

@ -1,5 +1,224 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`app not nested should generate files if bundler is vite 1`] = `
"import { defineConfig, devices } from '@playwright/test';
import { nxE2EPreset } from '@nx/playwright/preset';
import { workspaceRoot } from '@nx/devkit';
// For CI, you may want to set BASE_URL to the deployed application.
const baseURL = process.env['BASE_URL'] || 'http://localhost:4300';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
...nxE2EPreset(__filename, { testDir: './src' }),
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
baseURL,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Run your local dev server before starting the tests */
webServer: {
command: 'npx nx run my-app:preview',
url: 'http://localhost:4300',
reuseExistingServer: !process.env.CI,
cwd: workspaceRoot,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// Uncomment for mobile browsers support
/* {
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
}, */
// Uncomment for branded browsers
/* {
name: 'Microsoft Edge',
use: { ...devices['Desktop Edge'], channel: 'msedge' },
},
{
name: 'Google Chrome',
use: { ...devices['Desktop Chrome'], channel: 'chrome' },
} */
],
});
"
`;
exports[`app not nested should setup playwright e2e project correctly for webpack 1`] = `
"import { defineConfig, devices } from '@playwright/test';
import { nxE2EPreset } from '@nx/playwright/preset';
import { workspaceRoot } from '@nx/devkit';
// For CI, you may want to set BASE_URL to the deployed application.
const baseURL = process.env['BASE_URL'] || 'http://localhost:4200';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
...nxE2EPreset(__filename, { testDir: './src' }),
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
baseURL,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Run your local dev server before starting the tests */
webServer: {
command: 'npx nx run cool-app:serve-static',
url: 'http://localhost:4200',
reuseExistingServer: !process.env.CI,
cwd: workspaceRoot,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// Uncomment for mobile browsers support
/* {
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
}, */
// Uncomment for branded browsers
/* {
name: 'Microsoft Edge',
use: { ...devices['Desktop Edge'], channel: 'msedge' },
},
{
name: 'Google Chrome',
use: { ...devices['Desktop Chrome'], channel: 'chrome' },
} */
],
});
"
`;
exports[`app not nested should use serve target and port if bundler=vite, e2eTestRunner=playwright, addPlugin=false 1`] = `
"import { defineConfig, devices } from '@playwright/test';
import { nxE2EPreset } from '@nx/playwright/preset';
import { workspaceRoot } from '@nx/devkit';
// For CI, you may want to set BASE_URL to the deployed application.
const baseURL = process.env['BASE_URL'] || 'http://localhost:4300';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
...nxE2EPreset(__filename, { testDir: './src' }),
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
baseURL,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Run your local dev server before starting the tests */
webServer: {
command: 'npx nx run my-app:preview',
url: 'http://localhost:4300',
reuseExistingServer: !process.env.CI,
cwd: workspaceRoot,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// Uncomment for mobile browsers support
/* {
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
}, */
// Uncomment for branded browsers
/* {
name: 'Microsoft Edge',
use: { ...devices['Desktop Edge'], channel: 'msedge' },
},
{
name: 'Google Chrome',
use: { ...devices['Desktop Chrome'], channel: 'chrome' },
} */
],
});
"
`;
exports[`app setup web app with --bundler=vite should setup vite configuration 1`] = `null`;
exports[`app should setup eslint 1`] = `

View File

@ -106,6 +106,16 @@ describe('web app generator (legacy)', () => {
"buildTarget": "my-app:build",
},
},
"serve-static": {
"dependsOn": [
"build",
],
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "my-app:build",
"spa": true,
},
},
"test": {
"executor": "@nx/jest:jest",
"options": {
@ -179,6 +189,9 @@ describe('web app generator (legacy)', () => {
},
},
"defaultConfiguration": "development",
"dependsOn": [
"build",
],
"executor": "@nx/vite:preview-server",
"options": {
"buildTarget": "my-vite-app:build",
@ -201,6 +214,16 @@ describe('web app generator (legacy)', () => {
"buildTarget": "my-vite-app:build",
},
},
"serve-static": {
"dependsOn": [
"build",
],
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "my-vite-app:build",
"spa": true,
},
},
"test": {
"executor": "@nx/jest:jest",
"options": {

View File

@ -1,7 +1,12 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version';
import { readProjectConfiguration, Tree } from '@nx/devkit';
import {
readNxJson,
readProjectConfiguration,
Tree,
updateNxJson,
} from '@nx/devkit';
import { getProjects, readJson } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
@ -158,11 +163,103 @@ describe('app', () => {
expect(tree.exists('cool-app-e2e/playwright.config.ts')).toBeTruthy();
});
it('should setup cypress e2e project correctly for vite', async () => {
await applicationGenerator(tree, {
name: 'cool-app',
e2eTestRunner: 'cypress',
unitTestRunner: 'none',
projectNameAndRootFormat: 'as-provided',
bundler: 'vite',
addPlugin: true,
});
expect(tree.read('cool-app-e2e/cypress.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
...nxE2EPreset(__filename, {
cypressDir: 'src',
bundler: 'vite',
webServerCommands: {
default: 'nx run cool-app:serve',
production: 'nx run cool-app:preview',
},
ciWebServerCommand: 'nx run cool-app:preview',
ciBaseUrl: 'http://localhost:4300',
}),
baseUrl: 'http://localhost:4200',
},
});
"
`);
});
it('should setup cypress e2e project correctly for webpack', async () => {
await applicationGenerator(tree, {
name: 'cool-app',
e2eTestRunner: 'cypress',
unitTestRunner: 'none',
projectNameAndRootFormat: 'as-provided',
bundler: 'webpack',
addPlugin: true,
});
expect(tree.read('cool-app-e2e/cypress.config.ts', 'utf-8'))
.toMatchInlineSnapshot(`
"import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset';
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
...nxE2EPreset(__filename, {
cypressDir: 'src',
webServerCommands: {
default: 'nx run cool-app:serve',
production: 'nx run cool-app:preview',
},
ciWebServerCommand: 'nx run cool-app:serve-static',
}),
baseUrl: 'http://localhost:4200',
},
});
"
`);
});
it('should setup playwright e2e project correctly for webpack', async () => {
await applicationGenerator(tree, {
name: 'cool-app',
e2eTestRunner: 'playwright',
unitTestRunner: 'none',
projectNameAndRootFormat: 'as-provided',
bundler: 'webpack',
addPlugin: true,
});
expect(
tree.read('cool-app-e2e/playwright.config.ts', 'utf-8')
).toMatchSnapshot();
});
it('should generate files if bundler is vite', async () => {
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: '@nx/vite/plugin',
options: {
buildTargetName: 'build',
previewTargetName: 'preview',
},
});
updateNxJson(tree, nxJson);
await applicationGenerator(tree, {
name: 'my-app',
bundler: 'vite',
projectNameAndRootFormat: 'as-provided',
e2eTestRunner: 'playwright',
addPlugin: true,
});
expect(tree.exists('my-app/src/main.ts')).toBeTruthy();
expect(tree.exists('my-app/src/app/app.element.ts')).toBeTruthy();
@ -179,7 +276,9 @@ describe('app', () => {
path: './tsconfig.spec.json',
},
]);
expect(tree.exists('my-app-e2e/playwright.config.ts')).toBeTruthy();
expect(
tree.read('my-app-e2e/playwright.config.ts', 'utf-8')
).toMatchSnapshot();
expect(tree.exists('my-app/index.html')).toBeTruthy();
expect(tree.exists('my-app/vite.config.ts')).toBeTruthy();
expect(tree.exists(`my-app/environments/environment.ts`)).toBeFalsy();
@ -188,6 +287,18 @@ describe('app', () => {
).toBeFalsy();
});
it('should use serve target and port if bundler=vite, e2eTestRunner=playwright, addPlugin=false', async () => {
await applicationGenerator(tree, {
name: 'my-app',
bundler: 'vite',
projectNameAndRootFormat: 'as-provided',
e2eTestRunner: 'playwright',
});
expect(
tree.read('my-app-e2e/playwright.config.ts', 'utf-8')
).toMatchSnapshot();
});
it('should extend from root tsconfig.json when no tsconfig.base.json', async () => {
tree.rename('tsconfig.base.json', 'tsconfig.json');

View File

@ -41,6 +41,8 @@ import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-targ
import { logShowProjectCommand } from '@nx/devkit/src/utils/log-show-project-command';
import { VitePluginOptions } from '@nx/vite/src/plugins/plugin';
import { WebpackPluginOptions } from '@nx/webpack/src/plugins/plugin';
import { hasVitePlugin } from '../../utils/has-vite-plugin';
import staticServeConfiguration from '../static-serve/static-serve-configuration';
interface NormalizedSchema extends Schema {
projectName: string;
@ -49,6 +51,8 @@ interface NormalizedSchema extends Schema {
e2eProjectRoot: string;
e2eWebServerAddress: string;
e2eWebServerTarget: string;
e2eCiWebServerTarget: string;
e2eCiBaseUrl: string;
e2ePort: number;
parsedTags: string[];
}
@ -364,6 +368,15 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
tasks.push(lintTask);
}
const hasNxBuildPlugin =
(options.bundler === 'webpack' && hasWebpackPlugin(host)) ||
(options.bundler === 'vite' && hasVitePlugin(host));
if (!hasNxBuildPlugin) {
await staticServeConfiguration(host, {
buildTarget: `${options.projectName}:build`,
spa: true,
});
}
if (options.e2eTestRunner === 'cypress') {
const { configurationGenerator } = ensurePackage<
typeof import('@nx/cypress')
@ -383,6 +396,16 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
baseUrl: options.e2eWebServerAddress,
directory: 'src',
skipFormat: true,
webServerCommands: hasNxBuildPlugin
? {
default: `nx run ${options.projectName}:${options.e2eWebServerTarget}`,
production: `nx run ${options.projectName}:preview`,
}
: undefined,
ciWebServerCommand: hasNxBuildPlugin
? `nx run ${options.projectName}:${options.e2eCiWebServerTarget}`
: undefined,
ciBaseUrl: options.bundler === 'vite' ? options.e2eCiBaseUrl : undefined,
});
tasks.push(cypressTask);
} else if (options.e2eTestRunner === 'playwright') {
@ -405,10 +428,10 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
js: false,
linter: options.linter,
setParserOptionsProject: options.setParserOptionsProject,
webServerCommand: `${getPackageManagerCommand().exec} nx ${
options.e2eWebServerTarget
} ${options.name}`,
webServerAddress: options.e2eWebServerAddress,
webServerCommand: `${getPackageManagerCommand().exec} nx run ${
options.projectName
}:${options.e2eCiWebServerTarget}`,
webServerAddress: options.e2eCiBaseUrl,
addPlugin: options.addPlugin,
});
tasks.push(playwrightTask);
@ -493,32 +516,43 @@ async function normalizeOptions(
nxJson.useInferencePlugins !== false;
options.addPlugin ??= addPluginDefault;
let e2ePort = 4200;
let e2eWebServerTarget = 'serve';
let e2eCiWebServerTarget =
options.bundler === 'vite' ? 'preview' : 'serve-static';
if (options.addPlugin) {
if (nxJson.plugins) {
for (const plugin of nxJson.plugins) {
if (
options.bundler === 'vite' &&
typeof plugin === 'object' &&
plugin.plugin === '@nx/vite/plugin' &&
(plugin.options as VitePluginOptions).serveTargetName
plugin.plugin === '@nx/vite/plugin'
) {
e2eWebServerTarget = (plugin.options as VitePluginOptions)
.serveTargetName;
e2eCiWebServerTarget =
(plugin.options as VitePluginOptions)?.previewTargetName ??
e2eCiWebServerTarget;
e2eWebServerTarget =
(plugin.options as VitePluginOptions)?.serveTargetName ??
e2eWebServerTarget;
} else if (
options.bundler === 'webpack' &&
typeof plugin === 'object' &&
plugin.plugin === '@nx/webpack/plugin' &&
(plugin.options as WebpackPluginOptions).serveTargetName
plugin.plugin === '@nx/webpack/plugin'
) {
e2eWebServerTarget = (plugin.options as WebpackPluginOptions)
.serveTargetName;
e2eCiWebServerTarget =
(plugin.options as WebpackPluginOptions)?.serveStaticTargetName ??
e2eCiWebServerTarget;
e2eWebServerTarget =
(plugin.options as WebpackPluginOptions)?.serveTargetName ??
e2eWebServerTarget;
}
}
}
}
let e2ePort = 4200;
if (
nxJson.targetDefaults?.[e2eWebServerTarget] &&
nxJson.targetDefaults?.[e2eWebServerTarget].options?.port
@ -529,6 +563,10 @@ async function normalizeOptions(
const e2eProjectName = `${appProjectName}-e2e`;
const e2eProjectRoot = `${appProjectRoot}-e2e`;
const e2eWebServerAddress = `http://localhost:${e2ePort}`;
const e2eCiBaseUrl =
options.bundler === 'vite'
? 'http://localhost:4300'
: `http://localhost:${e2ePort}`;
const npmScope = getNpmScope(host);
@ -554,6 +592,8 @@ async function normalizeOptions(
e2eProjectName,
e2eWebServerAddress,
e2eWebServerTarget,
e2eCiWebServerTarget,
e2eCiBaseUrl,
e2ePort,
parsedTags,
};

View File

@ -25,6 +25,9 @@ describe('Static serve configuration generator', () => {
expect(readProjectConfiguration(tree, 'react-app').targets['serve-static'])
.toMatchInlineSnapshot(`
{
"dependsOn": [
"build",
],
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "react-app:build",
@ -40,6 +43,9 @@ describe('Static serve configuration generator', () => {
readProjectConfiguration(tree, 'angular-app').targets['serve-static']
).toMatchInlineSnapshot(`
{
"dependsOn": [
"build",
],
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "angular-app:build",
@ -54,6 +60,9 @@ describe('Static serve configuration generator', () => {
expect(readProjectConfiguration(tree, 'storybook').targets['serve-static'])
.toMatchInlineSnapshot(`
{
"dependsOn": [
"build-storybook",
],
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "storybook:build-storybook",
@ -75,6 +84,9 @@ describe('Static serve configuration generator', () => {
readProjectConfiguration(tree, 'react-app').targets['serve-static-custom']
).toMatchInlineSnapshot(`
{
"dependsOn": [
"build",
],
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "react-app:build",
@ -101,6 +113,9 @@ describe('Static serve configuration generator', () => {
readProjectConfiguration(tree, 'angular-app').targets['serve-static']
).toMatchInlineSnapshot(`
{
"dependsOn": [
"build",
],
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "angular-app:build",

View File

@ -23,6 +23,7 @@ interface NormalizedWebStaticServeSchema extends WebStaticServeSchema {
projectName: string;
targetName: string;
spa: boolean;
parsedBuildTarget: string;
}
export async function webStaticServeGenerator(
@ -49,6 +50,7 @@ async function normalizeOptions(
targetName: options.targetName || 'serve-static',
projectName: target.project,
spa: options.spa ?? true,
parsedBuildTarget: target.target,
};
const projectConfig = readProjectConfiguration(tree, target.project);
@ -109,6 +111,7 @@ function addStaticConfig(tree: Tree, opts: NormalizedWebStaticServeSchema) {
Partial<FileServerExecutorSchema>
> = {
executor: '@nx/web:file-server',
dependsOn: [opts.parsedBuildTarget],
options: {
buildTarget: opts.buildTarget,
staticFilePath: opts.outputPath,

View File

@ -0,0 +1,10 @@
import { readNxJson, Tree } from '@nx/devkit';
export function hasVitePlugin(tree: Tree) {
const nxJson = readNxJson(tree);
return !!nxJson.plugins?.some((p) =>
typeof p === 'string'
? p === '@nx/vite/plugin'
: p.plugin === '@nx/vite/plugin'
);
}

View File

@ -105,6 +105,9 @@ exports[`@nx/webpack/plugin should create nodes 1`] = `
},
},
"serve-static": {
"dependsOn": [
"build-something",
],
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "build-something",

View File

@ -235,6 +235,7 @@ async function createWebpackTargets(
};
targets[options.serveStaticTargetName] = {
dependsOn: [options.buildTargetName],
executor: '@nx/web:file-server',
options: {
buildTarget: options.buildTargetName,