fix(node): support custom import paths based on tsconfig when building node apps (#15154)
This commit is contained in:
parent
19bfd8ef6a
commit
a45d52e9e6
@ -20,8 +20,18 @@ describe('Node Applications + webpack', () => {
|
|||||||
|
|
||||||
afterEach(() => cleanupProject());
|
afterEach(() => cleanupProject());
|
||||||
|
|
||||||
function addLibImport(appName: string, libName: string) {
|
function addLibImport(appName: string, libName: string, importPath?: string) {
|
||||||
const content = readFile(`apps/${appName}/src/main.ts`);
|
const content = readFile(`apps/${appName}/src/main.ts`);
|
||||||
|
if (importPath) {
|
||||||
|
updateFile(
|
||||||
|
`apps/${appName}/src/main.ts`,
|
||||||
|
`
|
||||||
|
import { ${libName} } from '${importPath}';
|
||||||
|
${content}
|
||||||
|
console.log(${libName}());
|
||||||
|
`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
updateFile(
|
updateFile(
|
||||||
`apps/${appName}/src/main.ts`,
|
`apps/${appName}/src/main.ts`,
|
||||||
`
|
`
|
||||||
@ -31,6 +41,7 @@ describe('Node Applications + webpack', () => {
|
|||||||
`
|
`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function runE2eTests(appName: string) {
|
async function runE2eTests(appName: string) {
|
||||||
process.env.PORT = '5000';
|
process.env.PORT = '5000';
|
||||||
@ -48,12 +59,14 @@ describe('Node Applications + webpack', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
it('should generate an app using webpack', async () => {
|
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 expressApp = uniq('expressapp');
|
||||||
const fastifyApp = uniq('fastifyapp');
|
const fastifyApp = uniq('fastifyapp');
|
||||||
const koaApp = uniq('koaapp');
|
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(
|
runCLI(
|
||||||
`generate @nrwl/node:app ${expressApp} --framework=express --no-interactive`
|
`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.
|
// Only Fastify generates with unit tests since it supports them without additional libraries.
|
||||||
expect(() => runCLI(`lint ${fastifyApp}`)).not.toThrow();
|
expect(() => runCLI(`lint ${fastifyApp}`)).not.toThrow();
|
||||||
|
|
||||||
addLibImport(expressApp, utilLib);
|
addLibImport(expressApp, testLib1);
|
||||||
addLibImport(fastifyApp, utilLib);
|
addLibImport(expressApp, testLib2, '@acme/test2');
|
||||||
addLibImport(koaApp, utilLib);
|
addLibImport(fastifyApp, testLib1);
|
||||||
|
addLibImport(fastifyApp, testLib2, '@acme/test2');
|
||||||
|
addLibImport(koaApp, testLib1);
|
||||||
|
addLibImport(koaApp, testLib2, '@acme/test2');
|
||||||
|
|
||||||
await runE2eTests(expressApp);
|
await runE2eTests(expressApp);
|
||||||
await runE2eTests(fastifyApp);
|
await runE2eTests(fastifyApp);
|
||||||
|
|||||||
@ -36,7 +36,8 @@
|
|||||||
"dotenv": "~10.0.0",
|
"dotenv": "~10.0.0",
|
||||||
"fast-glob": "3.2.7",
|
"fast-glob": "3.2.7",
|
||||||
"fs-extra": "^11.1.0",
|
"fs-extra": "^11.1.0",
|
||||||
"tslib": "^2.3.0"
|
"tslib": "^2.3.0",
|
||||||
|
"tsconfig-paths": "^4.1.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"esbuild": "~0.17.5"
|
"esbuild": "~0.17.5"
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import * as esbuild from 'esbuild';
|
import * as esbuild from 'esbuild';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { parse } from 'path';
|
import { join, parse } from 'path';
|
||||||
import {
|
import {
|
||||||
ExecutorContext,
|
ExecutorContext,
|
||||||
getImportPath,
|
|
||||||
joinPathFragments,
|
joinPathFragments,
|
||||||
|
ProjectGraphProjectNode,
|
||||||
} from '@nrwl/devkit';
|
} from '@nrwl/devkit';
|
||||||
import { mkdirSync, writeFileSync } from 'fs';
|
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
||||||
|
|
||||||
import { getClientEnvironment } from '../../../utils/environment-variables';
|
import { getClientEnvironment } from '../../../utils/environment-variables';
|
||||||
import {
|
import {
|
||||||
@ -63,33 +63,19 @@ export function buildEsbuildOptions(
|
|||||||
} else if (options.platform === 'node' && format === 'cjs') {
|
} 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.
|
// 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.
|
// 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(
|
const entryPointsFromProjects = getEntryPoints(
|
||||||
context.projectName,
|
context.projectName,
|
||||||
context,
|
context,
|
||||||
{
|
{
|
||||||
initialEntryPoints: entryPoints,
|
initialEntryPoints: entryPoints,
|
||||||
recursive: true,
|
recursive: true,
|
||||||
onProjectFilesMatched: (currProjectName) => {
|
|
||||||
manifest.push({
|
|
||||||
module: getImportPath(
|
|
||||||
context.nxJsonConfiguration.npmScope,
|
|
||||||
currProjectName
|
|
||||||
),
|
|
||||||
root: context.projectGraph.nodes[currProjectName].data.root,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
esbuildOptions.entryPoints = [
|
esbuildOptions.entryPoints = [
|
||||||
// Write a main entry file that registers workspace libs and then calls the user-defined main.
|
// Write a main entry file that registers workspace libs and then calls the user-defined main.
|
||||||
writeTmpEntryWithRequireOverrides(
|
writeTmpEntryWithRequireOverrides(paths, outExtension, options, context),
|
||||||
manifest,
|
|
||||||
outExtension,
|
|
||||||
options,
|
|
||||||
context
|
|
||||||
),
|
|
||||||
...entryPointsFromProjects.map((f) => {
|
...entryPointsFromProjects.map((f) => {
|
||||||
/**
|
/**
|
||||||
* Maintain same directory structure as the workspace, so that other workspace libs may be used by the project.
|
* 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(
|
function writeTmpEntryWithRequireOverrides(
|
||||||
manifest: Array<{ module: string; root: string }>,
|
paths: Record<string, string[]>,
|
||||||
outExtension: '.cjs' | '.js' | '.mjs',
|
outExtension: '.cjs' | '.js' | '.mjs',
|
||||||
options: NormalizedEsBuildExecutorOptions,
|
options: NormalizedEsBuildExecutorOptions,
|
||||||
context: ExecutorContext
|
context: ExecutorContext
|
||||||
): { in: string; out: string } {
|
): { in: string; out: string } {
|
||||||
const project = context.projectGraph?.nodes[context.projectName];
|
const project = context.projectGraph?.nodes[context.projectName];
|
||||||
// Write a temp main entry source that registers workspace libs.
|
// Write a temp main entry source that registers workspace libs.
|
||||||
const tmpPath = path.join(
|
const tmpPath = path.join(context.root, 'tmp', project.name);
|
||||||
context.root,
|
|
||||||
'tmp',
|
|
||||||
context.projectGraph?.nodes[context.projectName].name
|
|
||||||
);
|
|
||||||
mkdirSync(tmpPath, { recursive: true });
|
mkdirSync(tmpPath, { recursive: true });
|
||||||
|
|
||||||
const { name: mainFileName, dir: mainPathRelativeToDist } = path.parse(
|
const { name: mainFileName, dir: mainPathRelativeToDist } = path.parse(
|
||||||
@ -180,7 +162,8 @@ function writeTmpEntryWithRequireOverrides(
|
|||||||
writeFileSync(
|
writeFileSync(
|
||||||
mainWithRequireOverridesInPath,
|
mainWithRequireOverridesInPath,
|
||||||
getRegisterFileContent(
|
getRegisterFileContent(
|
||||||
manifest,
|
project,
|
||||||
|
paths,
|
||||||
`./${path.join(
|
`./${path.join(
|
||||||
mainPathRelativeToDist,
|
mainPathRelativeToDist,
|
||||||
`${mainFileName}${outExtension}`
|
`${mainFileName}${outExtension}`
|
||||||
@ -208,11 +191,37 @@ function writeTmpEntryWithRequireOverrides(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRegisterFileContent(
|
export function getRegisterFileContent(
|
||||||
manifest: Array<{ module: string; root: string }>,
|
project: ProjectGraphProjectNode,
|
||||||
|
paths: Record<string, string[]>,
|
||||||
mainFile: string,
|
mainFile: string,
|
||||||
outExtension = '.js'
|
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 `
|
return `
|
||||||
/**
|
/**
|
||||||
* IMPORTANT: Do not modify this file.
|
* IMPORTANT: Do not modify this file.
|
||||||
@ -227,26 +236,28 @@ const distPath = __dirname;
|
|||||||
const manifest = ${JSON.stringify(manifest)};
|
const manifest = ${JSON.stringify(manifest)};
|
||||||
|
|
||||||
Module._resolveFilename = function(request, parent) {
|
Module._resolveFilename = function(request, parent) {
|
||||||
const entry = manifest.find(x => request === x.module || request.startsWith(x.module + '/'));
|
|
||||||
let found;
|
let found;
|
||||||
if (entry) {
|
for (const entry of manifest) {
|
||||||
if (request === entry.module) {
|
if (request === entry.module && entry.exactMatch) {
|
||||||
// Known entry paths for libraries. Add more if missing.
|
const entry = manifest.find((x) => request === x.module || request.startsWith(x.module + "/"));
|
||||||
const candidates = [
|
const candidate = path.join(distPath, entry.exactMatch);
|
||||||
path.join(distPath, entry.root, 'src/index' + '${outExtension}'),
|
if (isFile(candidate)) {
|
||||||
path.join(distPath, entry.root, 'src/main' + '${outExtension}'),
|
found = candidate;
|
||||||
path.join(distPath, entry.root, 'index' + '${outExtension}'),
|
break;
|
||||||
path.join(distPath, entry.root, 'main' + '${outExtension}')
|
}
|
||||||
];
|
|
||||||
found = candidates.find(f => fs.statSync(f).isFile());
|
|
||||||
} else {
|
} else {
|
||||||
const candidate = path.join(distPath, entry.root, request.replace(entry.module, '') + '${outExtension}');
|
const re = new RegExp(entry.module.replace(/\\*$/, "(?<rest>.*)"));
|
||||||
if (fs.statSync(candidate).isFile()) {
|
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;
|
found = candidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
if (found) {
|
if (found) {
|
||||||
const modifiedArguments = [found, ...[].slice.call(arguments, 1)];
|
const modifiedArguments = [found, ...[].slice.call(arguments, 1)];
|
||||||
return originalResolveFilename.apply(this, modifiedArguments);
|
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.
|
// Call the user-defined main.
|
||||||
require('${mainFile}');
|
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