fix(node): support custom import paths based on tsconfig when building node apps (#15154)
This commit is contained in:
parent
19bfd8ef6a
commit
a45d52e9e6
@ -20,16 +20,27 @@ describe('Node Applications + webpack', () => {
|
||||
|
||||
afterEach(() => cleanupProject());
|
||||
|
||||
function addLibImport(appName: string, libName: string) {
|
||||
function addLibImport(appName: string, libName: string, importPath?: string) {
|
||||
const content = readFile(`apps/${appName}/src/main.ts`);
|
||||
updateFile(
|
||||
`apps/${appName}/src/main.ts`,
|
||||
if (importPath) {
|
||||
updateFile(
|
||||
`apps/${appName}/src/main.ts`,
|
||||
`
|
||||
import { ${libName} } from '${importPath}';
|
||||
${content}
|
||||
console.log(${libName}());
|
||||
`
|
||||
);
|
||||
} else {
|
||||
updateFile(
|
||||
`apps/${appName}/src/main.ts`,
|
||||
`
|
||||
import { ${libName} } from '@${proj}/${libName}';
|
||||
${content}
|
||||
console.log(${libName}());
|
||||
`
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function runE2eTests(appName: string) {
|
||||
@ -48,12 +59,14 @@ describe('Node Applications + webpack', () => {
|
||||
}
|
||||
|
||||
it('should generate an app using webpack', async () => {
|
||||
const utilLib = uniq('util');
|
||||
const testLib1 = uniq('test1');
|
||||
const testLib2 = uniq('test2');
|
||||
const expressApp = uniq('expressapp');
|
||||
const fastifyApp = uniq('fastifyapp');
|
||||
const koaApp = uniq('koaapp');
|
||||
|
||||
runCLI(`generate @nrwl/node:lib ${utilLib}`);
|
||||
runCLI(`generate @nrwl/node:lib ${testLib1}`);
|
||||
runCLI(`generate @nrwl/node:lib ${testLib2} --importPath=@acme/test2`);
|
||||
runCLI(
|
||||
`generate @nrwl/node:app ${expressApp} --framework=express --no-interactive`
|
||||
);
|
||||
@ -79,9 +92,12 @@ describe('Node Applications + webpack', () => {
|
||||
// Only Fastify generates with unit tests since it supports them without additional libraries.
|
||||
expect(() => runCLI(`lint ${fastifyApp}`)).not.toThrow();
|
||||
|
||||
addLibImport(expressApp, utilLib);
|
||||
addLibImport(fastifyApp, utilLib);
|
||||
addLibImport(koaApp, utilLib);
|
||||
addLibImport(expressApp, testLib1);
|
||||
addLibImport(expressApp, testLib2, '@acme/test2');
|
||||
addLibImport(fastifyApp, testLib1);
|
||||
addLibImport(fastifyApp, testLib2, '@acme/test2');
|
||||
addLibImport(koaApp, testLib1);
|
||||
addLibImport(koaApp, testLib2, '@acme/test2');
|
||||
|
||||
await runE2eTests(expressApp);
|
||||
await runE2eTests(fastifyApp);
|
||||
|
||||
@ -36,7 +36,8 @@
|
||||
"dotenv": "~10.0.0",
|
||||
"fast-glob": "3.2.7",
|
||||
"fs-extra": "^11.1.0",
|
||||
"tslib": "^2.3.0"
|
||||
"tslib": "^2.3.0",
|
||||
"tsconfig-paths": "^4.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"esbuild": "~0.17.5"
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import * as esbuild from 'esbuild';
|
||||
import * as path from 'path';
|
||||
import { parse } from 'path';
|
||||
import { join, parse } from 'path';
|
||||
import {
|
||||
ExecutorContext,
|
||||
getImportPath,
|
||||
joinPathFragments,
|
||||
ProjectGraphProjectNode,
|
||||
} from '@nrwl/devkit';
|
||||
import { mkdirSync, writeFileSync } from 'fs';
|
||||
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
||||
|
||||
import { getClientEnvironment } from '../../../utils/environment-variables';
|
||||
import {
|
||||
@ -63,33 +63,19 @@ export function buildEsbuildOptions(
|
||||
} else if (options.platform === 'node' && format === 'cjs') {
|
||||
// When target platform Node and target format is CJS, then also transpile workspace libs used by the app.
|
||||
// Provide a `require` override in the main entry file so workspace libs can be loaded when running the app.
|
||||
const manifest: Array<{ module: string; root: string }> = []; // Manifest allows the built app to load compiled workspace libs.
|
||||
const paths = getTsConfigCompilerPaths(context);
|
||||
const entryPointsFromProjects = getEntryPoints(
|
||||
context.projectName,
|
||||
context,
|
||||
{
|
||||
initialEntryPoints: entryPoints,
|
||||
recursive: true,
|
||||
onProjectFilesMatched: (currProjectName) => {
|
||||
manifest.push({
|
||||
module: getImportPath(
|
||||
context.nxJsonConfiguration.npmScope,
|
||||
currProjectName
|
||||
),
|
||||
root: context.projectGraph.nodes[currProjectName].data.root,
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
esbuildOptions.entryPoints = [
|
||||
// Write a main entry file that registers workspace libs and then calls the user-defined main.
|
||||
writeTmpEntryWithRequireOverrides(
|
||||
manifest,
|
||||
outExtension,
|
||||
options,
|
||||
context
|
||||
),
|
||||
writeTmpEntryWithRequireOverrides(paths, outExtension, options, context),
|
||||
...entryPointsFromProjects.map((f) => {
|
||||
/**
|
||||
* Maintain same directory structure as the workspace, so that other workspace libs may be used by the project.
|
||||
@ -156,18 +142,14 @@ export function getOutfile(
|
||||
}
|
||||
|
||||
function writeTmpEntryWithRequireOverrides(
|
||||
manifest: Array<{ module: string; root: string }>,
|
||||
paths: Record<string, string[]>,
|
||||
outExtension: '.cjs' | '.js' | '.mjs',
|
||||
options: NormalizedEsBuildExecutorOptions,
|
||||
context: ExecutorContext
|
||||
): { in: string; out: string } {
|
||||
const project = context.projectGraph?.nodes[context.projectName];
|
||||
// Write a temp main entry source that registers workspace libs.
|
||||
const tmpPath = path.join(
|
||||
context.root,
|
||||
'tmp',
|
||||
context.projectGraph?.nodes[context.projectName].name
|
||||
);
|
||||
const tmpPath = path.join(context.root, 'tmp', project.name);
|
||||
mkdirSync(tmpPath, { recursive: true });
|
||||
|
||||
const { name: mainFileName, dir: mainPathRelativeToDist } = path.parse(
|
||||
@ -180,7 +162,8 @@ function writeTmpEntryWithRequireOverrides(
|
||||
writeFileSync(
|
||||
mainWithRequireOverridesInPath,
|
||||
getRegisterFileContent(
|
||||
manifest,
|
||||
project,
|
||||
paths,
|
||||
`./${path.join(
|
||||
mainPathRelativeToDist,
|
||||
`${mainFileName}${outExtension}`
|
||||
@ -208,11 +191,37 @@ function writeTmpEntryWithRequireOverrides(
|
||||
};
|
||||
}
|
||||
|
||||
function getRegisterFileContent(
|
||||
manifest: Array<{ module: string; root: string }>,
|
||||
export function getRegisterFileContent(
|
||||
project: ProjectGraphProjectNode,
|
||||
paths: Record<string, string[]>,
|
||||
mainFile: string,
|
||||
outExtension = '.js'
|
||||
) {
|
||||
// Sort by longest prefix so imports match the most specific path.
|
||||
const sortedKeys = Object.keys(paths).sort(
|
||||
(a: string, b: string) => getPrefixLength(b) - getPrefixLength(a)
|
||||
);
|
||||
const manifest: Array<{
|
||||
module: string;
|
||||
pattern: string;
|
||||
exactMatch?: string;
|
||||
}> = sortedKeys.reduce((acc, k) => {
|
||||
let exactMatch: string;
|
||||
|
||||
// Nx generates a single path entry.
|
||||
// If more sophisticated setup is needed, we can consider tsconfig-paths.
|
||||
const pattern = paths[k][0];
|
||||
|
||||
if (/.[cm]?ts$/.test(pattern)) {
|
||||
// Path specifies a single entry point e.g. "a/b/src/index.ts".
|
||||
// This is the default setup.
|
||||
const { dir, name } = path.parse(pattern);
|
||||
exactMatch = path.join(dir, `${name}${outExtension}`);
|
||||
}
|
||||
acc.push({ module: k, exactMatch, pattern });
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return `
|
||||
/**
|
||||
* IMPORTANT: Do not modify this file.
|
||||
@ -227,26 +236,28 @@ const distPath = __dirname;
|
||||
const manifest = ${JSON.stringify(manifest)};
|
||||
|
||||
Module._resolveFilename = function(request, parent) {
|
||||
const entry = manifest.find(x => request === x.module || request.startsWith(x.module + '/'));
|
||||
let found;
|
||||
if (entry) {
|
||||
if (request === entry.module) {
|
||||
// Known entry paths for libraries. Add more if missing.
|
||||
const candidates = [
|
||||
path.join(distPath, entry.root, 'src/index' + '${outExtension}'),
|
||||
path.join(distPath, entry.root, 'src/main' + '${outExtension}'),
|
||||
path.join(distPath, entry.root, 'index' + '${outExtension}'),
|
||||
path.join(distPath, entry.root, 'main' + '${outExtension}')
|
||||
];
|
||||
found = candidates.find(f => fs.statSync(f).isFile());
|
||||
} else {
|
||||
const candidate = path.join(distPath, entry.root, request.replace(entry.module, '') + '${outExtension}');
|
||||
if (fs.statSync(candidate).isFile()) {
|
||||
for (const entry of manifest) {
|
||||
if (request === entry.module && entry.exactMatch) {
|
||||
const entry = manifest.find((x) => request === x.module || request.startsWith(x.module + "/"));
|
||||
const candidate = path.join(distPath, entry.exactMatch);
|
||||
if (isFile(candidate)) {
|
||||
found = candidate;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
const re = new RegExp(entry.module.replace(/\\*$/, "(?<rest>.*)"));
|
||||
const match = request.match(re);
|
||||
|
||||
if (match?.groups) {
|
||||
const candidate = path.join(distPath, entry.pattern.replace("*", ""), match.groups.rest + ".js");
|
||||
if (isFile(candidate)) {
|
||||
found = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
const modifiedArguments = [found, ...[].slice.call(arguments, 1)];
|
||||
return originalResolveFilename.apply(this, modifiedArguments);
|
||||
@ -255,7 +266,43 @@ Module._resolveFilename = function(request, parent) {
|
||||
}
|
||||
};
|
||||
|
||||
function isFile(s) {
|
||||
try {
|
||||
return fs.statSync(s).isFile();
|
||||
} catch (_e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Call the user-defined main.
|
||||
require('${mainFile}');
|
||||
`;
|
||||
}
|
||||
|
||||
function getPrefixLength(pattern: string): number {
|
||||
return pattern.substring(0, pattern.indexOf('*')).length;
|
||||
}
|
||||
|
||||
function getTsConfigCompilerPaths(context: ExecutorContext): {
|
||||
[key: string]: string[];
|
||||
} {
|
||||
const tsconfigPaths = require('tsconfig-paths');
|
||||
const tsConfigResult = tsconfigPaths.loadConfig(getRootTsConfigPath(context));
|
||||
if (tsConfigResult.resultType !== 'success') {
|
||||
throw new Error('Cannot load tsconfig file');
|
||||
}
|
||||
return tsConfigResult.paths;
|
||||
}
|
||||
|
||||
function getRootTsConfigPath(context: ExecutorContext): string | null {
|
||||
for (const tsConfigName of ['tsconfig.base.json', 'tsconfig.json']) {
|
||||
const tsConfigPath = join(context.root, tsConfigName);
|
||||
if (existsSync(tsConfigPath)) {
|
||||
return tsConfigPath;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Could not find a root tsconfig.json or tsconfig.base.json file.'
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user