fix(web): spa flag should correctly define redirect (#22487)
This commit is contained in:
parent
02075d5aed
commit
29c80a33de
@ -47,7 +47,7 @@
|
||||
},
|
||||
"proxyUrl": {
|
||||
"type": "string",
|
||||
"description": "URL to proxy unhandled requests to."
|
||||
"description": "URL to proxy unhandled requests to. _Note: If the 'spa' flag is set to true, manually setting this value will override the catch-all redirect functionality from http-server which may lead to unexpected behavior._"
|
||||
},
|
||||
"proxyOptions": {
|
||||
"type": "object",
|
||||
|
||||
@ -21,6 +21,11 @@
|
||||
"type": "string",
|
||||
"description": "Name of the serve target to add. Defaults to 'serve-static'.",
|
||||
"default": "serve-static"
|
||||
},
|
||||
"spa": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to set the 'spa' flag on the generated target.",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"required": ["buildTarget"],
|
||||
|
||||
@ -38,7 +38,7 @@ describe('Storybook executors for Angular', () => {
|
||||
}
|
||||
);
|
||||
p.kill();
|
||||
}, 200_000);
|
||||
}, 300_000);
|
||||
|
||||
// Increased timeout because 92% sealing asset processing TerserPlugin
|
||||
// TODO(meeroslav) this test is still flaky and breaks the PR runs. We need to investigate why.
|
||||
|
||||
@ -36,7 +36,7 @@ describe('file-server', () => {
|
||||
}
|
||||
);
|
||||
runCLI(
|
||||
`generate @nx/web:static-config --buildTarget=${ngAppName}:build --no-interactive`,
|
||||
`generate @nx/web:static-config --buildTarget=${ngAppName}:build --outputPath=dist/apps/${ngAppName}/browser --no-interactive`,
|
||||
{
|
||||
env: {
|
||||
NX_ADD_PLUGINS: 'false',
|
||||
|
||||
@ -287,6 +287,7 @@ exports[`app --project-name-and-root-format=derived should generate correctly wh
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "my-dir-my-app:build",
|
||||
"spa": true,
|
||||
"staticFilePath": "dist/apps/my-dir/my-app/browser",
|
||||
},
|
||||
},
|
||||
@ -486,6 +487,7 @@ exports[`app --project-name-and-root-format=derived should generate correctly wh
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "my-app:build",
|
||||
"spa": true,
|
||||
"staticFilePath": "dist/apps/my-app/browser",
|
||||
},
|
||||
},
|
||||
@ -980,6 +982,7 @@ exports[`app nested should create project configs 1`] = `
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "my-app:build",
|
||||
"spa": true,
|
||||
"staticFilePath": "dist/my-dir/my-app/browser",
|
||||
},
|
||||
},
|
||||
@ -1092,6 +1095,7 @@ exports[`app not nested should create project configs 1`] = `
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "my-app:build",
|
||||
"spa": true,
|
||||
"staticFilePath": "dist/my-app/browser",
|
||||
},
|
||||
},
|
||||
|
||||
@ -94,6 +94,7 @@ function addFileServerTarget(
|
||||
staticFilePath: isUsingApplicationBuilder
|
||||
? joinPathFragments(options.outputPath, 'browser')
|
||||
: undefined,
|
||||
spa: true,
|
||||
},
|
||||
};
|
||||
updateProjectConfiguration(tree, options.name, projectConfig);
|
||||
|
||||
@ -21,7 +21,7 @@ export async function addE2e(
|
||||
case 'cypress': {
|
||||
const hasNxExpoPlugin = hasExpoPlugin(tree);
|
||||
if (!hasNxExpoPlugin) {
|
||||
webStaticServeGenerator(tree, {
|
||||
await webStaticServeGenerator(tree, {
|
||||
buildTarget: `${options.projectName}:export`,
|
||||
targetName: 'serve-static',
|
||||
});
|
||||
|
||||
@ -26,10 +26,11 @@ export async function addE2e(host: Tree, options: NormalizedSchema) {
|
||||
>('@nx/cypress', nxVersion);
|
||||
|
||||
if (!hasPlugin) {
|
||||
webStaticServeGenerator(host, {
|
||||
await webStaticServeGenerator(host, {
|
||||
buildTarget: `${options.projectName}:build`,
|
||||
outputPath: `${options.outputPath}/out`,
|
||||
targetName: 'serve-static',
|
||||
spa: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -40,6 +40,7 @@ exports[`@nx/next/plugin integrated projects should create nodes 1`] = `
|
||||
"options": {
|
||||
"buildTarget": "my-build",
|
||||
"port": 3000,
|
||||
"spa": false,
|
||||
"staticFilePath": "{projectRoot}/out",
|
||||
},
|
||||
},
|
||||
@ -98,6 +99,7 @@ exports[`@nx/next/plugin root projects should create nodes 1`] = `
|
||||
"options": {
|
||||
"buildTarget": "build",
|
||||
"port": 3000,
|
||||
"spa": false,
|
||||
"staticFilePath": "{projectRoot}/out",
|
||||
},
|
||||
},
|
||||
|
||||
@ -162,6 +162,8 @@ function getStaticServeTargetConfig(options: NextPluginOptions) {
|
||||
buildTarget: options.buildTargetName,
|
||||
staticFilePath: '{projectRoot}/out',
|
||||
port: 3000,
|
||||
// Routes are found correctly with serve-static
|
||||
spa: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -33,6 +33,7 @@ exports[`@nx/nuxt/plugin not root project should create nodes 1`] = `
|
||||
"options": {
|
||||
"buildTarget": "acme-build-static",
|
||||
"port": 4200,
|
||||
"spa": false,
|
||||
"staticFilePath": "{projectRoot}/dist",
|
||||
},
|
||||
},
|
||||
@ -131,6 +132,7 @@ exports[`@nx/nuxt/plugin root project should create nodes 1`] = `
|
||||
"options": {
|
||||
"buildTarget": "build-static",
|
||||
"port": 4200,
|
||||
"spa": false,
|
||||
"staticFilePath": "{projectRoot}/dist",
|
||||
},
|
||||
},
|
||||
|
||||
@ -166,6 +166,8 @@ function serveStaticTarget(options: NuxtPluginOptions) {
|
||||
buildTarget: `${options.buildStaticTargetName}`,
|
||||
staticFilePath: '{projectRoot}/dist',
|
||||
port: 4200,
|
||||
// Routes are found correctly with serve-static
|
||||
spa: false,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -22,9 +22,10 @@ export async function addE2e(
|
||||
(options.bundler === 'webpack' && hasWebpackPlugin(tree)) ||
|
||||
(options.bundler === 'vite' && hasVitePlugin(tree));
|
||||
if (!hasNxBuildPlugin) {
|
||||
webStaticServeGenerator(tree, {
|
||||
await webStaticServeGenerator(tree, {
|
||||
buildTarget: `${options.projectName}:build`,
|
||||
targetName: 'serve-static',
|
||||
spa: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -154,7 +154,6 @@ export default defineConfig({
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
webServerCommands: { default: 'nx run test:serve:development' },
|
||||
ciWebServerCommand: 'nx run test:serve-static',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
},
|
||||
@ -668,7 +667,6 @@ export default defineConfig({
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
webServerCommands: { default: 'nx run test:serve:development' },
|
||||
ciWebServerCommand: 'nx run test:serve-static',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
},
|
||||
@ -1038,7 +1036,6 @@ export default defineConfig({
|
||||
...nxE2EPreset(__filename, {
|
||||
cypressDir: 'src',
|
||||
webServerCommands: { default: 'nx run test:serve:development' },
|
||||
ciWebServerCommand: 'nx run test:serve-static',
|
||||
}),
|
||||
baseUrl: 'http://localhost:4200',
|
||||
},
|
||||
|
||||
@ -16,7 +16,9 @@ export async function addE2E(tree: Tree, options: NormalizedSchema) {
|
||||
typeof import('@nx/cypress')
|
||||
>('@nx/cypress', getPackageVersion(tree, 'nx'));
|
||||
|
||||
addFileServerTarget(tree, options, 'serve-static');
|
||||
// TODO(colum): Remix needs a different approach to serve-static
|
||||
// Likely via remix start
|
||||
// addFileServerTarget(tree, options, 'serve-static');
|
||||
|
||||
addProjectConfiguration(tree, options.e2eProjectName, {
|
||||
projectType: 'application',
|
||||
|
||||
@ -186,7 +186,7 @@ export async function configurationGeneratorInternal(
|
||||
);
|
||||
}
|
||||
if (schema.configureStaticServe) {
|
||||
addStaticTarget(tree, schema);
|
||||
await addStaticTarget(tree, schema);
|
||||
}
|
||||
} else {
|
||||
devDeps['storybook'] = storybookVersion;
|
||||
|
||||
@ -135,9 +135,15 @@ export function addAngularStorybookTarget(
|
||||
updateProjectConfiguration(tree, projectName, projectConfig);
|
||||
}
|
||||
|
||||
export function addStaticTarget(tree: Tree, opts: StorybookConfigureSchema) {
|
||||
const nrwlWeb = ensurePackage<typeof import('@nx/web')>('@nx/web', nxVersion);
|
||||
nrwlWeb.webStaticServeGenerator(tree, {
|
||||
export async function addStaticTarget(
|
||||
tree: Tree,
|
||||
opts: StorybookConfigureSchema
|
||||
) {
|
||||
const { webStaticServeGenerator } = ensurePackage<typeof import('@nx/web')>(
|
||||
'@nx/web',
|
||||
nxVersion
|
||||
);
|
||||
await webStaticServeGenerator(tree, {
|
||||
buildTarget: `${opts.project}:build-storybook`,
|
||||
outputPath: joinPathFragments('dist/storybook', opts.project),
|
||||
targetName: 'static-storybook',
|
||||
|
||||
@ -44,6 +44,7 @@ exports[`@nx/vite/plugin not root project should create nodes 1`] = `
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "build-something",
|
||||
"spa": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -96,6 +97,7 @@ exports[`@nx/vite/plugin root project should create nodes 1`] = `
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "build",
|
||||
"spa": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -226,6 +226,7 @@ function serveStaticTarget(options: VitePluginOptions) {
|
||||
executor: '@nx/web:file-server',
|
||||
options: {
|
||||
buildTarget: `${options.buildTargetName}`,
|
||||
spa: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -24,9 +24,10 @@ export async function addE2e(
|
||||
: p.plugin === '@nx/vite/plugin'
|
||||
);
|
||||
if (!hasPlugin) {
|
||||
webStaticServeGenerator(tree, {
|
||||
await webStaticServeGenerator(tree, {
|
||||
buildTarget: `${options.projectName}:build`,
|
||||
targetName: 'serve-static',
|
||||
spa: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -181,6 +181,7 @@ export default async function* fileServerExecutor(
|
||||
run();
|
||||
}
|
||||
|
||||
const port = await detectPort(options.port || 8080);
|
||||
const outputPath = getBuildTargetOutputPath(options, context);
|
||||
|
||||
if (options.spa) {
|
||||
@ -189,6 +190,10 @@ export default async function* fileServerExecutor(
|
||||
|
||||
// See: https://github.com/http-party/http-server#magic-files
|
||||
copyFileSync(src, dst);
|
||||
|
||||
// We also need to ensure the proxyUrl is set, otherwise the browser will continue to throw a 404 error
|
||||
// This can cause unexpected behaviors and failures especially in automated test suites
|
||||
options.proxyUrl ??= `http${options.ssl ? 's' : ''}://localhost:${port}?`;
|
||||
}
|
||||
|
||||
const args = getHttpServerArgs(options);
|
||||
@ -205,7 +210,6 @@ export default async function* fileServerExecutor(
|
||||
|
||||
// detect port as close to when used to prevent port being used by another process
|
||||
// when running in parallel
|
||||
const port = await detectPort(options.port || 8080);
|
||||
args.push(`-p=${port}`);
|
||||
|
||||
const serve = fork(pathToHttpServer, [outputPath, ...args], {
|
||||
|
||||
@ -44,7 +44,7 @@
|
||||
},
|
||||
"proxyUrl": {
|
||||
"type": "string",
|
||||
"description": "URL to proxy unhandled requests to."
|
||||
"description": "URL to proxy unhandled requests to. _Note: If the 'spa' flag is set to true, manually setting this value will override the catch-all redirect functionality from http-server which may lead to unexpected behavior._"
|
||||
},
|
||||
"proxyOptions": {
|
||||
"type": "object",
|
||||
|
||||
@ -18,6 +18,11 @@
|
||||
"type": "string",
|
||||
"description": "Name of the serve target to add. Defaults to 'serve-static'.",
|
||||
"default": "serve-static"
|
||||
},
|
||||
"spa": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to set the 'spa' flag on the generated target.",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"required": ["buildTarget"]
|
||||
|
||||
@ -13,12 +13,12 @@ describe('Static serve configuration generator', () => {
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
});
|
||||
|
||||
it('should add a `serve-static` target to the project', () => {
|
||||
it('should add a `serve-static` target to the project', async () => {
|
||||
addReactConfig(tree, 'react-app');
|
||||
addAngularConfig(tree, 'angular-app');
|
||||
addStorybookConfig(tree, 'storybook');
|
||||
|
||||
webStaticServeGenerator(tree, {
|
||||
await webStaticServeGenerator(tree, {
|
||||
buildTarget: 'react-app:build',
|
||||
});
|
||||
|
||||
@ -28,10 +28,11 @@ describe('Static serve configuration generator', () => {
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "react-app:build",
|
||||
"spa": true,
|
||||
},
|
||||
}
|
||||
`);
|
||||
webStaticServeGenerator(tree, {
|
||||
await webStaticServeGenerator(tree, {
|
||||
buildTarget: 'angular-app:build',
|
||||
});
|
||||
|
||||
@ -42,11 +43,12 @@ describe('Static serve configuration generator', () => {
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "angular-app:build",
|
||||
"spa": true,
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
webStaticServeGenerator(tree, {
|
||||
await webStaticServeGenerator(tree, {
|
||||
buildTarget: 'storybook:build-storybook',
|
||||
});
|
||||
expect(readProjectConfiguration(tree, 'storybook').targets['serve-static'])
|
||||
@ -55,15 +57,16 @@ describe('Static serve configuration generator', () => {
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "storybook:build-storybook",
|
||||
"spa": true,
|
||||
"staticFilePath": "dist/storybook/storybook",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should support custom target name', () => {
|
||||
it('should support custom target name', async () => {
|
||||
addReactConfig(tree, 'react-app');
|
||||
webStaticServeGenerator(tree, {
|
||||
await webStaticServeGenerator(tree, {
|
||||
buildTarget: 'react-app:build',
|
||||
targetName: 'serve-static-custom',
|
||||
});
|
||||
@ -75,12 +78,13 @@ describe('Static serve configuration generator', () => {
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "react-app:build",
|
||||
"spa": true,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should infer outputPath via the buildTarget#outputs', () => {
|
||||
it('should infer outputPath via the buildTarget#outputs', async () => {
|
||||
addAngularConfig(tree, 'angular-app');
|
||||
const projectConfig = readProjectConfiguration(tree, 'angular-app');
|
||||
delete projectConfig.targets.build.options.outputPath;
|
||||
@ -89,7 +93,7 @@ describe('Static serve configuration generator', () => {
|
||||
|
||||
updateProjectConfiguration(tree, 'angular-app', projectConfig);
|
||||
|
||||
webStaticServeGenerator(tree, {
|
||||
await webStaticServeGenerator(tree, {
|
||||
buildTarget: 'angular-app:build',
|
||||
});
|
||||
|
||||
@ -100,13 +104,14 @@ describe('Static serve configuration generator', () => {
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "angular-app:build",
|
||||
"spa": true,
|
||||
"staticFilePath": "dist/angular-app",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not override targets', () => {
|
||||
it('should not override targets', async () => {
|
||||
addStorybookConfig(tree, 'storybook');
|
||||
|
||||
const pc = readProjectConfiguration(tree, 'storybook');
|
||||
@ -117,10 +122,10 @@ describe('Static serve configuration generator', () => {
|
||||
updateProjectConfiguration(tree, 'storybook', pc);
|
||||
|
||||
expect(() => {
|
||||
webStaticServeGenerator(tree, {
|
||||
return webStaticServeGenerator(tree, {
|
||||
buildTarget: 'storybook:build-storybook',
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(`
|
||||
}).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Project storybook already has a 'serve-static' target configured.
|
||||
Either rename or remove the existing 'serve-static' target and try again.
|
||||
Optionally, you can provide a different name with the --target-name option other than 'serve-static'"
|
||||
|
||||
@ -1,42 +1,54 @@
|
||||
import {
|
||||
createProjectGraphAsync,
|
||||
logger,
|
||||
parseTargetString,
|
||||
type ProjectGraph,
|
||||
readCachedProjectGraph,
|
||||
readProjectConfiguration,
|
||||
stripIndents,
|
||||
TargetConfiguration,
|
||||
Tree,
|
||||
type TargetConfiguration,
|
||||
type Tree,
|
||||
updateProjectConfiguration,
|
||||
} from '@nx/devkit';
|
||||
import { Schema as FileServerExecutorSchema } from '../../executors/file-server/schema.d';
|
||||
import type { Schema as FileServerExecutorSchema } from '../../executors/file-server/schema.d';
|
||||
|
||||
interface WebStaticServeSchema {
|
||||
buildTarget: string;
|
||||
outputPath?: string;
|
||||
targetName?: string;
|
||||
spa?: boolean;
|
||||
}
|
||||
|
||||
interface NormalizedWebStaticServeSchema extends WebStaticServeSchema {
|
||||
projectName: string;
|
||||
targetName: string;
|
||||
spa: boolean;
|
||||
}
|
||||
|
||||
export function webStaticServeGenerator(
|
||||
export async function webStaticServeGenerator(
|
||||
tree: Tree,
|
||||
options: WebStaticServeSchema
|
||||
) {
|
||||
const opts = normalizeOptions(tree, options);
|
||||
const opts = await normalizeOptions(tree, options);
|
||||
addStaticConfig(tree, opts);
|
||||
}
|
||||
|
||||
function normalizeOptions(
|
||||
async function normalizeOptions(
|
||||
tree: Tree,
|
||||
options: WebStaticServeSchema
|
||||
): NormalizedWebStaticServeSchema {
|
||||
const target = parseTargetString(options.buildTarget);
|
||||
): Promise<NormalizedWebStaticServeSchema> {
|
||||
let projectGraph: ProjectGraph;
|
||||
try {
|
||||
projectGraph = readCachedProjectGraph();
|
||||
} catch (e) {
|
||||
projectGraph = await createProjectGraphAsync();
|
||||
}
|
||||
const target = parseTargetString(options.buildTarget, projectGraph);
|
||||
const opts: NormalizedWebStaticServeSchema = {
|
||||
...options,
|
||||
targetName: options.targetName || 'serve-static',
|
||||
projectName: target.project,
|
||||
spa: options.spa ?? true,
|
||||
};
|
||||
|
||||
const projectConfig = readProjectConfiguration(tree, target.project);
|
||||
@ -54,7 +66,7 @@ Optionally, you can provide a different name with the --target-name option other
|
||||
|
||||
// NOTE: @nx/web:file-server only looks for the outputPath option
|
||||
if (!buildTargetConfig.options?.outputPath && !opts.outputPath) {
|
||||
// attempt to find the suiteable path from the outputs
|
||||
// attempt to find the suitable path from the outputs
|
||||
let maybeOutputValue: any;
|
||||
for (const o of buildTargetConfig?.outputs || []) {
|
||||
const isInterpolatedOutput = o.trim().startsWith('{options.');
|
||||
@ -100,6 +112,7 @@ function addStaticConfig(tree: Tree, opts: NormalizedWebStaticServeSchema) {
|
||||
options: {
|
||||
buildTarget: opts.buildTarget,
|
||||
staticFilePath: opts.outputPath,
|
||||
spa: opts.spa,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -53,6 +53,7 @@ exports[`@nx/webpack/plugin should create nodes 1`] = `
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "build-something",
|
||||
"spa": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -167,6 +167,7 @@ async function createWebpackTargets(
|
||||
executor: '@nx/web:file-server',
|
||||
options: {
|
||||
buildTarget: options.buildTargetName,
|
||||
spa: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user