Skip to content

Commit

Permalink
feat(@angular-devkit/build-angular): support dev-server package prebu…
Browse files Browse the repository at this point in the history
…ndling with esbuild builder

When using the development server with the esbuild-based browser application builder, the underlying
Vite server will now prebundle packages present in an application. During the prebundling process,
the Angular linker will also be invoked to ensure that APF packages are processed for AOT usage.
The Vite prebundling also provides automatic persistent caching of processed packages. This allows
reuse of processed packages across `ng serve` invocations. To support the use of prebundling at the
development server level, all packages are considered external from the build level. The first time
a package is used within an application there may be a short delay upon accessing the page as the
package is processed. Due to the persistent nature of the prebundling, the `ng cache` command is used
to control the use of the feature. Please note, however, disabling the cache will also disable TypeScript
incremental compilation if not otherwise specifically disabled.
  • Loading branch information
clydin committed Jun 1, 2023
1 parent ee5763d commit 3d1c09b
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,46 @@ function createCodeBundleOptions(
},
};

if (options.externalPackages) {
// Add a plugin that marks any resolved path as external if it is within a node modules directory.
// This is used instead of the esbuild `packages` option to avoid marking bare specifiers that use
// tsconfig path mapping to resolve to a workspace relative path. This is common for monorepos that
// contain libraries that are built along with the application. These libraries should not be considered
// external even though the imports appear to be packages.
const EXTERNAL_PACKAGE_RESOLUTION = Symbol('EXTERNAL_PACKAGE_RESOLUTION');
buildOptions.plugins ??= [];
buildOptions.plugins.push({
name: 'angular-external-packages',
setup(build) {
build.onResolve({ filter: /./ }, async (args) => {
if (args.pluginData?.[EXTERNAL_PACKAGE_RESOLUTION]) {
return null;
}

const { importer, kind, resolveDir, namespace, pluginData = {} } = args;
pluginData[EXTERNAL_PACKAGE_RESOLUTION] = true;

const result = await build.resolve(args.path, {
importer,
kind,
namespace,
pluginData,
resolveDir,
});

if (result.path && /[\\/]node_modules[\\/]/.test(result.path)) {
return {
path: args.path,
external: true,
};
}

return result;
});
},
});
}

const polyfills = options.polyfills ? [...options.polyfills] : [];
if (jit) {
polyfills.push('@angular/compiler');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ interface InternalOptions {

/** File extension to use for the generated output files. */
outExtension?: 'js' | 'mjs';

/**
* Indicates whether all node packages should be marked as external.
* Currently used by the dev-server to support prebundling.
*/
externalPackages?: boolean;
}

/** Full set of options for `browser-esbuild` builder. */
Expand Down Expand Up @@ -180,6 +186,7 @@ export async function normalizeOptions(
verbose,
watch,
progress,
externalPackages,
} = options;

// Return all the normalized options
Expand All @@ -197,6 +204,7 @@ export async function normalizeOptions(
polyfills: polyfills === undefined || Array.isArray(polyfills) ? polyfills : [polyfills],
poll,
progress: progress ?? true,
externalPackages,
// If not explicitly set, default to the Node.js process argument
preserveSymlinks: preserveSymlinks ?? process.execArgv.includes('--preserve-symlinks'),
stylePreprocessorOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import { readFile } from 'node:fs/promises';
import type { AddressInfo } from 'node:net';
import path from 'node:path';
import { InlineConfig, ViteDevServer, createServer, normalizePath } from 'vite';
import { buildEsbuildBrowser } from '../browser-esbuild';
import { buildEsbuildBrowserInternal } from '../browser-esbuild';
import { JavaScriptTransformer } from '../browser-esbuild/javascript-transformer';
import { BrowserEsbuildOptions } from '../browser-esbuild/options';
import type { Schema as BrowserBuilderOptions } from '../browser-esbuild/schema';
import { loadProxyConfiguration, normalizeProxyConfiguration } from './load-proxy-config';
import type { NormalizedDevServerOptions } from './options';
Expand Down Expand Up @@ -52,7 +54,9 @@ export async function* serveWithVite(
verbose: serverOptions.verbose,
} as json.JsonObject & BrowserBuilderOptions,
builderName,
)) as json.JsonObject & BrowserBuilderOptions;
)) as json.JsonObject & BrowserEsbuildOptions;
// Set all packages as external to support Vite's prebundle caching
browserOptions.externalPackages = serverOptions.cacheOptions.enabled;

if (serverOptions.servePath === undefined && browserOptions.baseHref !== undefined) {
serverOptions.servePath = browserOptions.baseHref;
Expand All @@ -63,7 +67,9 @@ export async function* serveWithVite(
const generatedFiles = new Map<string, OutputFileRecord>();
const assetFiles = new Map<string, string>();
// TODO: Switch this to an architect schedule call when infrastructure settings are supported
for await (const result of buildEsbuildBrowser(browserOptions, context, { write: false })) {
for await (const result of buildEsbuildBrowserInternal(browserOptions, context, {
write: false,
})) {
assert(result.outputFiles, 'Builder did not provide result files.');

// Analyze result files for changes
Expand Down Expand Up @@ -96,7 +102,13 @@ export async function* serveWithVite(
}
} else {
// Setup server and start listening
const serverConfiguration = await setupServer(serverOptions, generatedFiles, assetFiles);
const serverConfiguration = await setupServer(
serverOptions,
generatedFiles,
assetFiles,
browserOptions.preserveSymlinks,
browserOptions.externalDependencies,
);
server = await createServer(serverConfiguration);

await server.listen();
Expand Down Expand Up @@ -173,10 +185,13 @@ function analyzeResultFiles(
}
}

// eslint-disable-next-line max-lines-per-function
export async function setupServer(
serverOptions: NormalizedDevServerOptions,
outputFiles: Map<string, OutputFileRecord>,
assets: Map<string, string>,
preserveSymlinks: boolean | undefined,
prebundleExclude: string[] | undefined,
): Promise<InlineConfig> {
const proxy = await loadProxyConfiguration(
serverOptions.workspaceRoot,
Expand All @@ -199,6 +214,10 @@ export async function setupServer(
devSourcemap: true,
},
base: serverOptions.servePath,
resolve: {
mainFields: ['es2020', 'browser', 'module', 'main'],
preserveSymlinks,
},
server: {
port: serverOptions.port,
strictPort: true,
Expand Down Expand Up @@ -236,12 +255,13 @@ export async function setupServer(
return;
}

const code = Buffer.from(codeContents).toString('utf-8');
const mapContents = outputFiles.get(file + '.map')?.contents;

return {
// Remove source map URL comments from the code if a sourcemap is present.
// Vite will inline and add an additional sourcemap URL for the sourcemap.
code: Buffer.from(codeContents).toString('utf-8'),
code: mapContents ? code.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '') : code,
map: mapContents && Buffer.from(mapContents).toString('utf-8'),
};
},
Expand Down Expand Up @@ -276,7 +296,7 @@ export async function setupServer(
// Resource files are handled directly.
// Global stylesheets (CSS files) are currently considered resources to workaround
// dev server sourcemap issues with stylesheets.
if (extension !== '.html') {
if (extension !== '.js' && extension !== '.html') {
const outputFile = outputFiles.get(pathname);
if (outputFile) {
const mimeType = lookupMimeType(extension);
Expand Down Expand Up @@ -345,8 +365,34 @@ export async function setupServer(
},
],
optimizeDeps: {
// TODO: Consider enabling for known safe dependencies (@angular/* ?)
disabled: true,
// Only enable with caching since it causes prebundle dependencies to be cached
disabled: !serverOptions.cacheOptions.enabled,
// Exclude any provided dependencies (currently build defined externals)
exclude: prebundleExclude,
// Skip automatic file-based entry point discovery
include: [],
// Add an esbuild plugin to run the Angular linker on dependencies
esbuildOptions: {
plugins: [
{
name: 'angular-vite-optimize-deps',
setup(build) {
const transformer = new JavaScriptTransformer(
{ sourcemap: !!build.initialOptions.sourcemap },
1,
);

build.onLoad({ filter: /\.[cm]?js$/ }, async (args) => {
return {
contents: await transformer.transformFile(args.path),
loader: 'js',
};
});
build.onEnd(() => transformer.close());
},
},
],
},
},
};

Expand Down

0 comments on commit 3d1c09b

Please sign in to comment.