diff --git a/e2e/web/src/file-server.test.ts b/e2e/web/src/file-server.test.ts index 424c50cfbdeee..f706ff7f6f6c8 100644 --- a/e2e/web/src/file-server.test.ts +++ b/e2e/web/src/file-server.test.ts @@ -7,6 +7,7 @@ import { runCommandUntil, setMaxWorkers, uniq, + updateFile, updateJson, } from '@nx/e2e/utils'; import { join } from 'path'; @@ -45,6 +46,55 @@ describe('file-server', () => { } }, 300_000); + it('should read from directory from outputs if outputPath is not specified', async () => { + const appName = uniq('app'); + const port = 4301; + + runCLI(`generate @nx/web:app ${appName} --no-interactive`); + setMaxWorkers(join('apps', appName, 'project.json')); + // Used to copy index.html rather than the normal webpack build. + updateFile( + `copy-index.js`, + ` + const fs = require('node:fs'); + const path = require('node:path'); + fs.mkdirSync(path.join(__dirname, 'dist/foobar'), { recursive: true }); + fs.copyFileSync( + path.join(__dirname, 'apps/${appName}/src/index.html'), + path.join(__dirname, 'dist/foobar/index.html') + ); + ` + ); + updateJson(join('apps', appName, 'project.json'), (config) => { + // Point to same path as output.path in webpack config. + config.targets['build'] = { + command: `node copy-index.js`, + outputs: [`{workspaceRoot}/dist/foobar`], + }; + config.targets['serve'].executor = '@nx/web:file-server'; + // Check that buildTarget can exclude project name (e.g. build vs proj:build). + config.targets['serve'].options.buildTarget = 'build'; + return config; + }); + + const p = await runCommandUntil( + `serve ${appName} --port=${port}`, + (output) => { + return ( + output.indexOf(`localhost:${port}`) > -1 && + output.indexOf(`dist/foobar`) > -1 + ); + } + ); + + try { + await promisifiedTreeKill(p.pid, 'SIGKILL'); + await killPorts(port); + } catch { + // ignore + } + }, 300_000); + it('should setup and serve static files from app', async () => { const ngAppName = uniq('ng-app'); const reactAppName = uniq('react-app'); diff --git a/packages/web/src/executors/file-server/file-server.impl.ts b/packages/web/src/executors/file-server/file-server.impl.ts index a3c3fd8ad3457..12a432338eb44 100644 --- a/packages/web/src/executors/file-server/file-server.impl.ts +++ b/packages/web/src/executors/file-server/file-server.impl.ts @@ -12,6 +12,7 @@ import { join, resolve } from 'path'; import { readModulePackageJson } from 'nx/src/utils/package-json'; import * as detectPort from 'detect-port'; import { daemonClient } from 'nx/src/daemon/client/client'; +import { interpolate } from 'nx/src/tasks-runner/utils'; // platform specific command name const pmCmd = platform() === 'win32' ? `npx.cmd` : 'npx'; @@ -76,16 +77,26 @@ function getBuildTargetOutputPath(options: Schema, context: ExecutorContext) { return options.staticFilePath; } - let buildOptions; + let outputPath: string; try { const target = parseTargetString(options.buildTarget, context); - buildOptions = readTargetOptions(target, context); + const buildOptions = readTargetOptions(target, context); + if (buildOptions?.outputPath) { + outputPath = buildOptions.outputPath; + } else { + const project = context.projectGraph.nodes[context.projectName]; + const buildTarget = project.data.targets[target.target]; + outputPath = buildTarget.outputs?.[0]; + if (outputPath) + outputPath = interpolate(outputPath, { + projectName: project.data.name, + projectRoot: project.data.root, + }); + } } catch (e) { throw new Error(`Invalid buildTarget: ${options.buildTarget}`); } - // TODO: vsavkin we should also check outputs - const outputPath = buildOptions.outputPath; if (!outputPath) { throw new Error( `Unable to get the outputPath from buildTarget ${options.buildTarget}. Make sure ${options.buildTarget} has an outputPath property or manually provide an staticFilePath property`