Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import { buildApplication } from '../../index';
import { OutputMode, Platform } from '../../schema';
import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup';

/**
* `@angular/platform-server/init` (bundled into the server polyfills) and
* `@angular/platform-server` (bundled into the main server bundle) both import this file.
*/
const DOMINO_BUNDLE = 'third_party/domino/bundled-domino.mjs';

/** The property on globalThis where the Domino instance is stored. */
const DOMINO_GLOBAL_PROPERTY = /(?:ɵ|\\u0275)domino/;

describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
describe('Behavior: "Domino is bundled once for the server"', () => {
beforeEach(async () => {
await harness.modifyFile('src/tsconfig.app.json', (content) => {
const tsConfig = JSON.parse(content);
tsConfig.files ??= [];
tsConfig.files.push('main.server.ts', 'server.ts');

return JSON.stringify(tsConfig);
});

await harness.writeFile('src/server.ts', `console.log('Hello!');`);

// The default template uses i18n which requires `@angular/localize` to prerender.
await harness.writeFile('src/app/app.component.html', `<p>Hello!</p>`);
});

it('shares the server polyfills copy of Domino with the main server bundle', async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
server: 'src/main.server.ts',
ssr: true,
});

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();

harness.expectFile('dist/server/polyfills.server.mjs').content.toContain(DOMINO_BUNDLE);
harness
.expectFile('dist/server/polyfills.server.mjs')
.content.toMatch(DOMINO_GLOBAL_PROPERTY);

harness.expectFile('dist/server/main.server.mjs').content.not.toContain(DOMINO_BUNDLE);
harness.expectFile('dist/server/main.server.mjs').content.toMatch(DOMINO_GLOBAL_PROPERTY);
});

it('shares the server polyfills copy of Domino when "outputMode" is "server"', async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
server: 'src/main.server.ts',
ssr: { entry: 'src/server.ts' },
outputMode: OutputMode.Server,
});

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();

harness.expectFile('dist/server/polyfills.server.mjs').content.toContain(DOMINO_BUNDLE);
harness.expectFile('dist/server/main.server.mjs').content.not.toContain(DOMINO_BUNDLE);
harness.expectFile('dist/server/main.server.mjs').content.toMatch(DOMINO_GLOBAL_PROPERTY);
harness.expectFile('dist/server/server.mjs').content.not.toContain(DOMINO_BUNDLE);
});

it('shares the server polyfills copy of Domino when "ssr.platform" is "neutral"', async () => {
harness.useTarget('build', {
...BASE_OPTIONS,
server: 'src/main.server.ts',
ssr: { entry: 'src/server.ts', platform: Platform.Neutral },
outputMode: OutputMode.Server,
});

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();

harness.expectFile('dist/server/polyfills.server.mjs').content.toContain(DOMINO_BUNDLE);
harness
.expectFile('dist/server/polyfills.server.mjs')
.content.toMatch(DOMINO_GLOBAL_PROPERTY);
harness.expectFile('dist/server/main.server.mjs').content.not.toContain(DOMINO_BUNDLE);
harness.expectFile('dist/server/main.server.mjs').content.toMatch(DOMINO_GLOBAL_PROPERTY);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ import { createExternalPackagesPlugin } from './external-packages-plugin';
import { createAngularLocaleDataPlugin } from './i18n-locale-plugin';
import type { LoadResultCache } from './load-result-cache';
import { createLoaderImportAttributePlugin } from './loader-import-attribute-plugin';
import {
createPlatformServerDominoExposePlugin,
createPlatformServerDominoRedirectPlugin,
} from './platform-server-domino-plugin';
import { createRxjsEsmResolutionPlugin } from './rxjs-esm-resolution-plugin';
import { createServerBundleMetadata } from './server-bundle-metadata-plugin';
import { createSourcemapIgnorelistPlugin } from './sourcemap-ignorelist-plugin';
Expand Down Expand Up @@ -237,7 +241,7 @@ export function createServerPolyfillBundleOptions(
};

buildOptions.plugins ??= [];
buildOptions.plugins.push(createServerBundleMetadata());
buildOptions.plugins.push(createServerBundleMetadata(), createPlatformServerDominoExposePlugin());

return () => buildOptions;
}
Expand Down Expand Up @@ -321,6 +325,7 @@ export function createServerMainCodeBundleOptions(

buildOptions.plugins.push(
createServerBundleMetadata(),
createPlatformServerDominoRedirectPlugin(),
createVirtualModulePlugin({
namespace: mainServerInjectManifestNamespace,
cache: loadResultCache,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import type { Plugin } from 'esbuild';
import { toPosixPath } from '../../utils/path';

/**
* Filter matching the Domino bundle import inside `@angular/platform-server`.
*/
const BUNDLED_DOMINO_FILTER = /[\\/]third_party[\\/]domino[\\/]bundled-domino\.mjs$/;

/**
* The property name on `globalThis` where the Domino instance is stored.
*/
const DOMINO_GLOBAL_PROPERTY = 'ɵdomino';

/**
* Creates an esbuild plugin that attaches the Domino instance to `globalThis` when
* `@angular/platform-server/init` is bundled into the server polyfills bundle.
*
* @returns An esbuild plugin.
*/
export function createPlatformServerDominoExposePlugin(): Plugin {
return {
name: 'angular-platform-server-domino-expose',
setup(build) {
build.onResolve({ filter: BUNDLED_DOMINO_FILTER }, (args) =>
args.namespace !== 'angular:domino-expose' && args.kind === 'import-statement'
? {
path: 'angular:platform-server-domino',
namespace: 'angular:domino-expose',
pluginData: {
importPath: args.path,
resolveDir: args.resolveDir,
},
}
Comment thread
alan-agius4 marked this conversation as resolved.
: null,
);

build.onLoad({ filter: /./, namespace: 'angular:domino-expose' }, (args) => {
const { importPath, resolveDir } = args.pluginData as {
importPath: string;
resolveDir: string;
};

return {
contents: [
`import domino from ${JSON.stringify(toPosixPath(importPath))};`,
`globalThis['${DOMINO_GLOBAL_PROPERTY}'] = domino;`,
`export default domino;`,
].join('\n'),
loader: 'js',
resolveDir,
};
});
},
};
}

/**
* Creates an esbuild plugin that redirects imports of Domino in `@angular/platform-server`
* to the single copy attached to `globalThis` by the server polyfills bundle.
*
* @returns An esbuild plugin.
*/
export function createPlatformServerDominoRedirectPlugin(): Plugin {
return {
name: 'angular-platform-server-domino-redirect',
setup(build) {
build.onResolve({ filter: BUNDLED_DOMINO_FILTER }, (args) =>
args.kind === 'import-statement'
? {
path: 'angular:platform-server-domino',
namespace: 'angular:platform-server-domino',
}
: null,
);

build.onLoad({ filter: /./, namespace: 'angular:platform-server-domino' }, () => ({
contents: `export default globalThis['${DOMINO_GLOBAL_PROPERTY}'];`,
loader: 'js',
}));
},
};
}
Loading