@nx/esbuild
Version:
394 lines (383 loc) • 16.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildEsbuildOptions = buildEsbuildOptions;
exports.createPathsFromTsConfigReferences = createPathsFromTsConfigReferences;
exports.getOutExtension = getOutExtension;
exports.getOutfile = getOutfile;
exports.getRegisterFileContent = getRegisterFileContent;
const path = require("path");
const fs_1 = require("fs");
const devkit_1 = require("@nx/devkit");
const environment_variables_1 = require("../../../utils/environment-variables");
const get_entry_points_1 = require("../../../utils/get-entry-points");
const path_1 = require("path");
const ESM_FILE_EXTENSION = '.js';
const CJS_FILE_EXTENSION = '.cjs';
function buildEsbuildOptions(format, options, context) {
const outExtension = getOutExtension(format, options, context);
const external = options.bundle
? [
...(options.userDefinedBuildOptions?.external ?? []),
...options.external,
].filter((packageName) => !options.excludeFromExternal?.includes(packageName))
: undefined;
const esbuildOptions = {
...options.userDefinedBuildOptions,
entryNames: options.outputHashing === 'all' ? '[dir]/[name].[hash]' : '[dir]/[name]',
bundle: options.bundle,
// Cannot use external with bundle option
external: external,
minify: options.minify,
platform: options.platform,
target: options.target,
metafile: options.metafile,
tsconfig: (0, path_1.relative)(process.cwd(), (0, path_1.join)(context.root, options.tsConfig)),
sourcemap: (options.sourcemap ?? options.userDefinedBuildOptions?.sourcemap) ||
false,
format,
outExtension: {
'.js': outExtension,
},
};
if (options.platform === 'browser') {
esbuildOptions.define = {
...(0, environment_variables_1.getClientEnvironment)(),
...options.userDefinedBuildOptions?.define,
};
}
if (!esbuildOptions.outfile && !esbuildOptions.outdir) {
if (options.singleEntry && options.bundle && !esbuildOptions.splitting) {
esbuildOptions.outfile = getOutfile(format, options, context);
}
else {
esbuildOptions.outdir = options.outputPath;
}
}
const entryPoints = options.additionalEntryPoints
? [options.main, ...options.additionalEntryPoints]
: [options.main];
if (options.bundle) {
esbuildOptions.entryPoints = entryPoints;
}
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 loader override in the main entry file so workspace libs can be loaded when running the app.
const paths = options.isTsSolutionSetup
? createPathsFromTsConfigReferences(context)
: getTsConfigCompilerPaths(context);
const entryPointsFromProjects = (0, get_entry_points_1.getEntryPoints)(context.projectName, context, {
initialTsConfigFileName: options.tsConfig,
initialEntryPoints: entryPoints,
recursive: true,
});
esbuildOptions.entryPoints = [
// Write a main entry file that registers workspace libs and then calls the user-defined main.
writeTmpEntryWithRequireOverrides(paths, outExtension, options, context, format),
...entryPointsFromProjects.map((f) => {
/**
* Maintain same directory structure as the workspace, so that other workspace libs may be used by the project.
* dist
* └── apps
* └── demo
* ├── apps
* │ └── demo
* │ └── src
* │ └── main.js (requires '@acme/utils' which is mapped to libs/utils/src/index.js)
* ├── libs
* │ └── utils
* │ └── src
* │ └── index.js
* └── main.js (entry with require overrides)
*/
const { dir, name } = path.parse(f);
return {
in: f,
out: path.join(dir, name),
};
}),
];
}
else {
// Otherwise, just transpile the project source files. Any workspace lib will need to be published separately.
esbuildOptions.entryPoints = (0, get_entry_points_1.getEntryPoints)(context.projectName, context, {
initialTsConfigFileName: options.tsConfig,
initialEntryPoints: entryPoints,
recursive: false,
});
}
return esbuildOptions;
}
/**
* When using TS project references we need to map the paths to the referenced projects.
* This is necessary because esbuild does not support project references out of the box.
* @param context ExecutorContext
*/
function createPathsFromTsConfigReferences(context) {
const { findAllProjectNodeDependencies, } = require('nx/src/utils/project-graph-utils');
const { isValidPackageJsonBuildConfig, } = require('@nx/js/src/plugins/typescript/util');
const { readTsConfig } = require('@nx/js');
const { findRuntimeTsConfigName, } = require('@nx/js/src/utils/typescript/ts-solution-setup');
const deps = findAllProjectNodeDependencies(context.projectName, context.projectGraph);
const tsConfig = (0, devkit_1.readJsonFile)((0, devkit_1.joinPathFragments)(context.root, 'tsconfig.json'));
const referencesAsPaths = new Set(tsConfig.references.reduce((acc, ref) => {
if (!ref.path)
return acc;
const fullPath = (0, devkit_1.joinPathFragments)(devkit_1.workspaceRoot, ref.path);
try {
if ((0, fs_1.lstatSync)(fullPath).isDirectory()) {
acc.push(fullPath);
}
}
catch {
// Ignore errors (e.g., path doesn't exist)
}
return acc;
}, []));
// for each dep we check if it contains a build target
// we only want to add the paths for projects that do not have a build target
return deps.reduce((acc, dep) => {
const projectNode = context.projectGraph.nodes[dep];
const projectPath = (0, devkit_1.joinPathFragments)(devkit_1.workspaceRoot, projectNode.data.root);
const resolvedTsConfigPath = findRuntimeTsConfigName(projectPath) ?? 'tsconfig.json';
const projTsConfig = readTsConfig(resolvedTsConfigPath);
const projectPkgJson = (0, devkit_1.readJsonFile)((0, devkit_1.joinPathFragments)(projectPath, 'package.json'));
if (projTsConfig &&
!isValidPackageJsonBuildConfig(projTsConfig, devkit_1.workspaceRoot, projectPath) &&
projectPkgJson?.name) {
const entryPoint = getProjectEntryPoint(projectPkgJson, projectPath);
if (referencesAsPaths.has(projectPath)) {
acc[projectPkgJson.name] = [path.relative(devkit_1.workspaceRoot, entryPoint)];
}
}
return acc;
}, {});
}
// Get the entry point for the project
function getProjectEntryPoint(projectPkgJson, projectPath) {
let entryPoint = null;
if (typeof projectPkgJson.exports === 'string') {
// If exports is a string, use it as the entry point
entryPoint = path.relative(devkit_1.workspaceRoot, (0, devkit_1.joinPathFragments)(projectPath, projectPkgJson.exports));
}
else if (typeof projectPkgJson.exports === 'object' &&
projectPkgJson.exports['.']) {
// If exports is an object and has a '.' key, process it
const exportEntry = projectPkgJson.exports['.'];
if (typeof exportEntry === 'object') {
entryPoint =
exportEntry.import ||
exportEntry.require ||
exportEntry.default ||
null;
}
else if (typeof exportEntry === 'string') {
entryPoint = exportEntry;
}
if (entryPoint) {
entryPoint = path.relative(devkit_1.workspaceRoot, (0, devkit_1.joinPathFragments)(projectPath, entryPoint));
}
}
// If no exports were found, fall back to main and module
if (!entryPoint) {
if (projectPkgJson.main) {
entryPoint = path.relative(devkit_1.workspaceRoot, (0, devkit_1.joinPathFragments)(projectPath, projectPkgJson.main));
}
else if (projectPkgJson.module) {
entryPoint = path.relative(devkit_1.workspaceRoot, (0, devkit_1.joinPathFragments)(projectPath, projectPkgJson.module));
}
}
return entryPoint;
}
function getOutExtension(format, options, context) {
const userDefinedExt = options.userDefinedBuildOptions?.outExtension?.['.js'];
// Allow users to change the output extensions from default CJS and ESM extensions.
// CJS -> .js
// ESM -> .mjs
return userDefinedExt === '.js' && format === 'cjs'
? '.js'
: userDefinedExt === '.mjs' && format === 'esm'
? '.mjs'
: format === 'esm'
? ESM_FILE_EXTENSION
: CJS_FILE_EXTENSION;
}
function getOutfile(format, options, context) {
const ext = getOutExtension(format, options, context);
const candidate = (0, devkit_1.joinPathFragments)(context.target.options.outputPath, options.outputFileName);
const { dir, name } = path.parse(candidate);
return `${dir}/${name}${ext}`;
}
function writeTmpEntryWithRequireOverrides(paths, outExtension, options, context, format = 'cjs') {
const project = context.projectGraph?.nodes[context.projectName];
// Write a temp main entry source that registers workspace libs.
const tmpPath = path.join(context.root, 'tmp', project.name);
(0, fs_1.mkdirSync)(tmpPath, { recursive: true });
const { name: mainFileName, dir: mainPathRelativeToDist } = path.parse(options.main);
const mainWithRequireOverridesInPath = path.join(tmpPath, `main-with-require-overrides.js`);
const mainFile = `./${path.join(mainPathRelativeToDist, `${mainFileName}${outExtension}`)}`;
(0, fs_1.writeFileSync)(mainWithRequireOverridesInPath, getRegisterFileContent(project, paths, mainFile, outExtension, format));
let mainWithRequireOverridesOutPath;
if (options.outputFileName) {
mainWithRequireOverridesOutPath = path.parse(options.outputFileName).name;
}
else if (mainPathRelativeToDist === '' || mainPathRelativeToDist === '.') {
// If the user customized their entry such that it is not inside `src/` folder
// then they have to provide the outputFileName
throw new Error(`There is a conflict between Nx-generated main file and the project's main file. Set --outputFileName=nx-main.js to fix this error.`);
}
else {
mainWithRequireOverridesOutPath = path.parse(mainFileName).name;
}
return {
in: mainWithRequireOverridesInPath,
out: mainWithRequireOverridesOutPath,
};
}
function getRegisterFileContent(project, paths, mainFile, outExtension = '.js', format = 'cjs') {
mainFile = (0, devkit_1.normalizePath)(mainFile);
// Sort by longest prefix so imports match the most specific path.
const sortedKeys = Object.keys(paths).sort((a, b) => getPrefixLength(b) - getPrefixLength(a));
const manifest = sortedKeys.reduce((acc, k) => {
let exactMatch;
// 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 = (0, devkit_1.joinPathFragments)(dir, `${name}${outExtension}`);
}
acc.push({ module: k, exactMatch, pattern });
return acc;
}, []);
if (format === 'esm') {
return `
/**
* IMPORTANT: Do not modify this file.
* This file allows the app to run without bundling in workspace libraries.
* Must be contained in the ".nx" folder inside the output path.
*/
import { pathToFileURL } from 'node:url';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { existsSync } from 'node:fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const distPath = __dirname;
const manifest = ${JSON.stringify(manifest)};
// Resolver for workspace libs
const originalResolve = import.meta.resolve;
if (originalResolve) {
import.meta.resolve = function(specifier, parent) {
const matchingEntry = manifest.find(
(entry) => specifier === entry.module || specifier.startsWith(entry.module + '/')
);
if (matchingEntry) {
if (matchingEntry.exactMatch) {
const candidate = join(distPath, matchingEntry.exactMatch);
if (existsSync(candidate)) {
return pathToFileURL(candidate).href;
}
} else {
const re = new RegExp(matchingEntry.module.replace(/\\*$/, "(?<rest>.*)"));
const match = specifier.match(re);
if (match?.groups) {
const candidate = join(distPath, matchingEntry.pattern.replace("*", ""), match.groups.rest);
if (existsSync(candidate)) {
return pathToFileURL(candidate).href;
}
}
}
}
return originalResolve.call(this, specifier, parent);
};
}
// Call the user-defined main.
await import(pathToFileURL(join(distPath, '${mainFile}')).href);
`;
}
else {
return `
/**
* IMPORTANT: Do not modify this file.
* This file allows the app to run without bundling in workspace libraries.
* Must be contained in the ".nx" folder inside the output path.
*/
const Module = require('module');
const path = require('path');
const fs = require('fs');
const originalResolveFilename = Module._resolveFilename;
const distPath = __dirname;
const manifest = ${JSON.stringify(manifest)};
Module._resolveFilename = function(request, parent) {
let found;
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);
if (isFile(candidate)) {
found = candidate;
}
}
}
}
if (found) {
const modifiedArguments = [found, ...[].slice.call(arguments, 1)];
return originalResolveFilename.apply(this, modifiedArguments);
} else {
return originalResolveFilename.apply(this, arguments);
}
};
function isFile(s) {
try {
require.resolve(s);
return true;
} catch (_e) {
return false;
}
}
// Call the user-defined main.
module.exports = require('${mainFile}');
`;
}
}
function getPrefixLength(pattern) {
const prefixIfWildcard = pattern.substring(0, pattern.indexOf('*')).length;
const prefixWithoutWildcard = pattern.substring(0, pattern.lastIndexOf('/')).length;
// if the pattern doesn't contain '*', then the length is always 0
// This causes issues when there are sub packages such as
// @nx/core
// @nx/core/testing
return prefixIfWildcard || prefixWithoutWildcard;
}
function getTsConfigCompilerPaths(context) {
const rootTsConfigPath = getRootTsConfigPath(context);
if (!rootTsConfigPath) {
return {};
}
const tsconfigPaths = require('tsconfig-paths');
const tsConfigResult = tsconfigPaths.loadConfig(rootTsConfigPath);
if (tsConfigResult.resultType !== 'success') {
throw new Error('Cannot load tsconfig file');
}
return tsConfigResult.paths;
}
function getRootTsConfigPath(context) {
for (const tsConfigName of ['tsconfig.base.json', 'tsconfig.json']) {
const tsConfigPath = path.join(context.root, tsConfigName);
if ((0, fs_1.existsSync)(tsConfigPath)) {
return tsConfigPath;
}
}
return null;
}