mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-26 01:01:13 +08:00
fix(@angular/build): exclude all entrypoints of a library from prebundling
The configuration now ensures that when a package is listed for exclusion, all paths within that package including sub-paths like `@foo/bar/baz` are marked as external and not prebundled by the development server. For example, specifying `@foo/bar` in the exclude list will prevent the development server from bundling any files from the `@foo/bar` package, including its sub-paths such as `@foo/bar/baz`. This aligns with esbuild external option behaviour https://esbuild.github.io/api/#external Closes #29170
This commit is contained in:
parent
03180fe035
commit
f0dd60be1e
@ -163,21 +163,49 @@ export async function executeBuild(
|
|||||||
// Analyze external imports if external options are enabled
|
// Analyze external imports if external options are enabled
|
||||||
if (options.externalPackages || bundlingResult.externalConfiguration) {
|
if (options.externalPackages || bundlingResult.externalConfiguration) {
|
||||||
const {
|
const {
|
||||||
externalConfiguration,
|
externalConfiguration = [],
|
||||||
externalImports: { browser, server },
|
externalImports: { browser = [], server = [] },
|
||||||
} = bundlingResult;
|
} = bundlingResult;
|
||||||
const implicitBrowser = browser ? [...browser] : [];
|
// Similar to esbuild, --external:@foo/bar automatically implies --external:@foo/bar/*,
|
||||||
const implicitServer = server ? [...server] : [];
|
// which matches import paths like @foo/bar/baz.
|
||||||
// TODO: Implement wildcard externalConfiguration filtering
|
// This means all paths within the @foo/bar package are also marked as external.
|
||||||
executionResult.setExternalMetadata(
|
const exclusionsPrefixes = externalConfiguration.map((exclusion) => exclusion + '/');
|
||||||
externalConfiguration
|
const exclusions = new Set(externalConfiguration);
|
||||||
? implicitBrowser.filter((value) => !externalConfiguration.includes(value))
|
const explicitExternal = new Set<string>();
|
||||||
: implicitBrowser,
|
|
||||||
externalConfiguration
|
const isExplicitExternal = (dep: string): boolean => {
|
||||||
? implicitServer.filter((value) => !externalConfiguration.includes(value))
|
if (exclusions.has(dep)) {
|
||||||
: implicitServer,
|
return true;
|
||||||
externalConfiguration,
|
}
|
||||||
);
|
|
||||||
|
for (const prefix of exclusionsPrefixes) {
|
||||||
|
if (dep.startsWith(prefix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const implicitBrowser: string[] = [];
|
||||||
|
for (const dep of browser) {
|
||||||
|
if (isExplicitExternal(dep)) {
|
||||||
|
explicitExternal.add(dep);
|
||||||
|
} else {
|
||||||
|
implicitBrowser.push(dep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const implicitServer: string[] = [];
|
||||||
|
for (const dep of server) {
|
||||||
|
if (isExplicitExternal(dep)) {
|
||||||
|
explicitExternal.add(dep);
|
||||||
|
} else {
|
||||||
|
implicitServer.push(dep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
executionResult.setExternalMetadata(implicitBrowser, implicitServer, [...explicitExternal]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { metafile, initialFiles, outputFiles } = bundlingResult;
|
const { metafile, initialFiles, outputFiles } = bundlingResult;
|
||||||
|
@ -433,7 +433,14 @@ export async function normalizeOptions(
|
|||||||
baseHref,
|
baseHref,
|
||||||
cacheOptions,
|
cacheOptions,
|
||||||
crossOrigin,
|
crossOrigin,
|
||||||
externalDependencies,
|
externalDependencies: normalizeExternals(externalDependencies),
|
||||||
|
externalPackages:
|
||||||
|
typeof externalPackages === 'object'
|
||||||
|
? {
|
||||||
|
...externalPackages,
|
||||||
|
exclude: normalizeExternals(externalPackages.exclude),
|
||||||
|
}
|
||||||
|
: externalPackages,
|
||||||
extractLicenses,
|
extractLicenses,
|
||||||
inlineStyleLanguage,
|
inlineStyleLanguage,
|
||||||
jit: !aot,
|
jit: !aot,
|
||||||
@ -441,7 +448,6 @@ export async function normalizeOptions(
|
|||||||
polyfills: polyfills === undefined || Array.isArray(polyfills) ? polyfills : [polyfills],
|
polyfills: polyfills === undefined || Array.isArray(polyfills) ? polyfills : [polyfills],
|
||||||
poll,
|
poll,
|
||||||
progress,
|
progress,
|
||||||
externalPackages,
|
|
||||||
preserveSymlinks,
|
preserveSymlinks,
|
||||||
stylePreprocessorOptions,
|
stylePreprocessorOptions,
|
||||||
subresourceIntegrity,
|
subresourceIntegrity,
|
||||||
@ -677,3 +683,23 @@ export function getLocaleBaseHref(
|
|||||||
|
|
||||||
return baseHrefSuffix !== '' ? urlJoin(baseHref, baseHrefSuffix) : undefined;
|
return baseHrefSuffix !== '' ? urlJoin(baseHref, baseHrefSuffix) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes an array of external dependency paths by ensuring that
|
||||||
|
* wildcard patterns (`/*`) are removed from package names.
|
||||||
|
*
|
||||||
|
* This avoids the need to handle this normalization repeatedly in our plugins,
|
||||||
|
* as esbuild already treats `--external:@foo/bar` as implicitly including
|
||||||
|
* `--external:@foo/bar/*`. By standardizing the input, we ensure consistency
|
||||||
|
* and reduce redundant checks across our plugins.
|
||||||
|
*
|
||||||
|
* @param value - An optional array of dependency paths to normalize.
|
||||||
|
* @returns A new array with wildcard patterns removed from package names, or `undefined` if input is `undefined`.
|
||||||
|
*/
|
||||||
|
function normalizeExternals(value: string[] | undefined): string[] | undefined {
|
||||||
|
if (!value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...new Set(value.map((d) => (d.endsWith('/*') ? d.slice(0, -2) : d)))];
|
||||||
|
}
|
||||||
|
@ -196,7 +196,7 @@
|
|||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"externalDependencies": {
|
"externalDependencies": {
|
||||||
"description": "Exclude the listed external dependencies from being bundled into the bundle. Instead, the created bundle relies on these dependencies to be available during runtime.",
|
"description": "Exclude the listed external dependencies from being bundled into the bundle. Instead, the created bundle relies on these dependencies to be available during runtime. Note: `@foo/bar` marks all paths within the `@foo/bar` package as external, including sub-paths like `@foo/bar/baz`.",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
@ -115,7 +115,7 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"exclude": {
|
"exclude": {
|
||||||
"description": "List of package imports that should not be prebundled by the development server. The packages will be bundled into the application code itself.",
|
"description": "List of package imports that should not be prebundled by the development server. The packages will be bundled into the application code itself. Note: specifying `@foo/bar` marks all paths within the `@foo/bar` package as excluded, including sub-paths like `@foo/bar/baz`.",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": { "type": "string" }
|
"items": { "type": "string" }
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT
|
|||||||
|
|
||||||
it('respects import specifiers when using baseHref with trailing slash', async () => {
|
it('respects import specifiers when using baseHref with trailing slash', async () => {
|
||||||
setupTarget(harness, {
|
setupTarget(harness, {
|
||||||
externalDependencies: ['rxjs', 'rxjs/operators'],
|
externalDependencies: ['rxjs'],
|
||||||
baseHref: '/test/',
|
baseHref: '/test/',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupT
|
|||||||
|
|
||||||
it('respects import specifiers when using baseHref without trailing slash', async () => {
|
it('respects import specifiers when using baseHref without trailing slash', async () => {
|
||||||
setupTarget(harness, {
|
setupTarget(harness, {
|
||||||
externalDependencies: ['rxjs', 'rxjs/operators'],
|
externalDependencies: ['rxjs/*'],
|
||||||
baseHref: '/test',
|
baseHref: '/test',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -359,17 +359,16 @@ export class BundlerContext {
|
|||||||
// Collect all external package names
|
// Collect all external package names
|
||||||
const externalImports = new Set<string>();
|
const externalImports = new Set<string>();
|
||||||
for (const { imports } of Object.values(result.metafile.outputs)) {
|
for (const { imports } of Object.values(result.metafile.outputs)) {
|
||||||
for (const importData of imports) {
|
for (const { external, kind, path } of imports) {
|
||||||
if (
|
if (
|
||||||
!importData.external ||
|
!external ||
|
||||||
SERVER_GENERATED_EXTERNALS.has(importData.path) ||
|
SERVER_GENERATED_EXTERNALS.has(path) ||
|
||||||
(importData.kind !== 'import-statement' &&
|
(kind !== 'import-statement' && kind !== 'dynamic-import' && kind !== 'require-call')
|
||||||
importData.kind !== 'dynamic-import' &&
|
|
||||||
importData.kind !== 'require-call')
|
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
externalImports.add(importData.path);
|
|
||||||
|
externalImports.add(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,9 +127,9 @@ export class ExecutionResult {
|
|||||||
setExternalMetadata(
|
setExternalMetadata(
|
||||||
implicitBrowser: string[],
|
implicitBrowser: string[],
|
||||||
implicitServer: string[],
|
implicitServer: string[],
|
||||||
explicit: string[] | undefined,
|
explicit: string[],
|
||||||
): void {
|
): void {
|
||||||
this.externalMetadata = { implicitBrowser, implicitServer, explicit: explicit ?? [] };
|
this.externalMetadata = { implicitBrowser, implicitServer, explicit };
|
||||||
}
|
}
|
||||||
|
|
||||||
get output() {
|
get output() {
|
||||||
|
@ -19,7 +19,14 @@ const EXTERNAL_PACKAGE_RESOLUTION = Symbol('EXTERNAL_PACKAGE_RESOLUTION');
|
|||||||
* @returns An esbuild plugin.
|
* @returns An esbuild plugin.
|
||||||
*/
|
*/
|
||||||
export function createExternalPackagesPlugin(options?: { exclude?: string[] }): Plugin {
|
export function createExternalPackagesPlugin(options?: { exclude?: string[] }): Plugin {
|
||||||
const exclusions = options?.exclude?.length ? new Set(options.exclude) : undefined;
|
const exclusions = new Set<string>(options?.exclude);
|
||||||
|
// Similar to esbuild, --external:@foo/bar automatically implies --external:@foo/bar/*,
|
||||||
|
// which matches import paths like @foo/bar/baz.
|
||||||
|
// This means all paths within the @foo/bar package are also marked as external.
|
||||||
|
const exclusionsPrefixes = options?.exclude?.map((exclusion) => exclusion + '/') ?? [];
|
||||||
|
const seenExclusions: Set<string> = new Set();
|
||||||
|
const seenExternals = new Set<string>();
|
||||||
|
const seenNonExclusions: Set<string> = new Set();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'angular-external-packages',
|
name: 'angular-external-packages',
|
||||||
@ -33,7 +40,7 @@ export function createExternalPackagesPlugin(options?: { exclude?: string[] }):
|
|||||||
.map(([key]) => key);
|
.map(([key]) => key);
|
||||||
|
|
||||||
// Safe to use native packages external option if no loader options or exclusions present
|
// Safe to use native packages external option if no loader options or exclusions present
|
||||||
if (!exclusions && !loaderOptionKeys?.length) {
|
if (!exclusions.size && !loaderOptionKeys?.length) {
|
||||||
build.initialOptions.packages = 'external';
|
build.initialOptions.packages = 'external';
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@ -47,10 +54,26 @@ export function createExternalPackagesPlugin(options?: { exclude?: string[] }):
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exclusions?.has(args.path)) {
|
if (seenExternals.has(args.path)) {
|
||||||
|
return { external: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exclusions.has(args.path) || seenExclusions.has(args.path)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!seenNonExclusions.has(args.path)) {
|
||||||
|
for (const exclusion of exclusionsPrefixes) {
|
||||||
|
if (args.path.startsWith(exclusion)) {
|
||||||
|
seenExclusions.add(args.path);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seenNonExclusions.add(args.path);
|
||||||
|
}
|
||||||
|
|
||||||
const { importer, kind, resolveDir, namespace, pluginData = {} } = args;
|
const { importer, kind, resolveDir, namespace, pluginData = {} } = args;
|
||||||
pluginData[EXTERNAL_PACKAGE_RESOLUTION] = true;
|
pluginData[EXTERNAL_PACKAGE_RESOLUTION] = true;
|
||||||
|
|
||||||
@ -62,11 +85,18 @@ export function createExternalPackagesPlugin(options?: { exclude?: string[] }):
|
|||||||
resolveDir,
|
resolveDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Return result if unable to resolve or explicitly marked external (externalDependencies option)
|
// Return result if unable to resolve
|
||||||
if (!result.path || result.external) {
|
if (!result.path) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return if explicitly marked external (externalDependencies option)
|
||||||
|
if (result.external) {
|
||||||
|
seenExternals.add(args.path);
|
||||||
|
|
||||||
|
return { external: true };
|
||||||
|
}
|
||||||
|
|
||||||
// Allow customized loaders to run against configured paths regardless of location
|
// Allow customized loaders to run against configured paths regardless of location
|
||||||
if (loaderFileExtensions.has(extname(result.path))) {
|
if (loaderFileExtensions.has(extname(result.path))) {
|
||||||
return result;
|
return result;
|
||||||
@ -74,10 +104,9 @@ export function createExternalPackagesPlugin(options?: { exclude?: string[] }):
|
|||||||
|
|
||||||
// Mark paths from a node modules directory as external
|
// Mark paths from a node modules directory as external
|
||||||
if (/[\\/]node_modules[\\/]/.test(result.path)) {
|
if (/[\\/]node_modules[\\/]/.test(result.path)) {
|
||||||
return {
|
seenExternals.add(args.path);
|
||||||
path: args.path,
|
|
||||||
external: true,
|
return { external: true };
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise return original result
|
// Otherwise return original result
|
||||||
|
@ -27,7 +27,7 @@ export function createRemoveIdPrefixPlugin(externals: string[]): Plugin {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const escapedExternals = externals.map(escapeRegexSpecialChars);
|
const escapedExternals = externals.map((e) => escapeRegexSpecialChars(e) + '(?:/.+)?');
|
||||||
const prefixedExternalRegex = new RegExp(
|
const prefixedExternalRegex = new RegExp(
|
||||||
`${resolvedConfig.base}${VITE_ID_PREFIX}(${escapedExternals.join('|')})`,
|
`${resolvedConfig.base}${VITE_ID_PREFIX}(${escapedExternals.join('|')})`,
|
||||||
'g',
|
'g',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user