mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-16 18:43:42 +08:00
feat(@angular-devkit/build-angular): add ssr
option in application builder
This commit adds an `ssr` option to the application builder, this can be either a `boolean` or an `object` with an `entryPoint` property. In the future, server bundles will only be emitted when the ssr option is truthy, as unlike SSR, SSG and AppShell do not require the server bundles to be written to disk.
This commit is contained in:
parent
a0a2c7aef6
commit
e6b377436a
@ -52,6 +52,7 @@ export async function executeBuild(
|
||||
cacheOptions,
|
||||
prerenderOptions,
|
||||
appShellOptions,
|
||||
ssrOptions,
|
||||
} = options;
|
||||
|
||||
const browsers = getSupportedBrowsers(projectRoot, context.logger);
|
||||
@ -167,8 +168,7 @@ export async function executeBuild(
|
||||
|
||||
executionResult.addOutputFile(indexHtmlOptions.output, content);
|
||||
|
||||
if (serverEntryPoint) {
|
||||
// TODO only add the below file when SSR is enabled.
|
||||
if (ssrOptions) {
|
||||
executionResult.addOutputFile('index.server.html', contentWithoutCriticalCssInlined);
|
||||
}
|
||||
}
|
||||
|
@ -186,6 +186,17 @@ export async function normalizeOptions(
|
||||
};
|
||||
}
|
||||
|
||||
let ssrOptions;
|
||||
if (options.ssr === true) {
|
||||
ssrOptions = {};
|
||||
} else if (typeof options.ssr === 'object') {
|
||||
const { entry } = options.ssr;
|
||||
|
||||
ssrOptions = {
|
||||
entry: entry && path.join(workspaceRoot, entry),
|
||||
};
|
||||
}
|
||||
|
||||
let appShellOptions;
|
||||
if (options.appShell) {
|
||||
appShellOptions = {
|
||||
@ -241,6 +252,7 @@ export async function normalizeOptions(
|
||||
serverEntryPoint,
|
||||
prerenderOptions,
|
||||
appShellOptions,
|
||||
ssrOptions,
|
||||
verbose,
|
||||
watch,
|
||||
workspaceRoot,
|
||||
|
@ -452,6 +452,26 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ssr": {
|
||||
"description": "Server side render (SSR) pages of your application during runtime.",
|
||||
"default": false,
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "Enable the server bundles to be written to disk."
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"entry": {
|
||||
"type": "string",
|
||||
"description": "The server entry-point that when executed will spawn the web server."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"appShell": {
|
||||
"type": "boolean",
|
||||
"description": "Generates an application shell during build time.",
|
||||
|
@ -30,7 +30,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
|
||||
const { result } = await harness.executeOnce();
|
||||
expect(result?.success).toBeTrue();
|
||||
|
||||
harness.expectFile('dist/server.mjs').toExist();
|
||||
harness.expectFile('dist/main.server.mjs').toExist();
|
||||
harness.expectFile('dist/main.js').toExist();
|
||||
});
|
||||
|
||||
@ -45,7 +45,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
|
||||
const { result } = await harness.executeOnce();
|
||||
expect(result?.success).toBeTrue();
|
||||
|
||||
harness.expectFile('dist/server.mjs').toExist();
|
||||
harness.expectFile('dist/main.server.mjs').toExist();
|
||||
});
|
||||
|
||||
it('fails and shows an error when file does not exist', async () => {
|
||||
@ -62,7 +62,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
|
||||
);
|
||||
|
||||
harness.expectFile('dist/main.js').toNotExist();
|
||||
harness.expectFile('dist/server.mjs').toNotExist();
|
||||
harness.expectFile('dist/main.server.mjs').toNotExist();
|
||||
});
|
||||
|
||||
it('throws an error when given an empty string', async () => {
|
||||
@ -88,8 +88,8 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
|
||||
const { result } = await harness.executeOnce();
|
||||
expect(result?.success).toBeTrue();
|
||||
|
||||
// Always uses the name `server.mjs` for the `server` option.
|
||||
harness.expectFile('dist/server.mjs').toExist();
|
||||
// Always uses the name `main.server.mjs` for the `server` option.
|
||||
harness.expectFile('dist/main.server.mjs').toExist();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @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.io/license
|
||||
*/
|
||||
|
||||
import { buildApplication } from '../../index';
|
||||
import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup';
|
||||
|
||||
describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
|
||||
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!');`);
|
||||
});
|
||||
|
||||
describe('Option: "ssr"', () => {
|
||||
it('uses a provided TypeScript file', async () => {
|
||||
harness.useTarget('build', {
|
||||
...BASE_OPTIONS,
|
||||
server: 'src/main.server.ts',
|
||||
ssr: {
|
||||
entry: 'src/server.ts',
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = await harness.executeOnce();
|
||||
expect(result?.success).toBeTrue();
|
||||
|
||||
harness.expectFile('dist/main.server.mjs').toExist();
|
||||
harness.expectFile('dist/server.mjs').toExist();
|
||||
});
|
||||
|
||||
it('resolves an absolute path as relative inside the workspace root', async () => {
|
||||
await harness.writeFile('file.mjs', `console.log('Hello!');`);
|
||||
|
||||
harness.useTarget('build', {
|
||||
...BASE_OPTIONS,
|
||||
server: 'src/main.server.ts',
|
||||
ssr: {
|
||||
entry: '/file.mjs',
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = await harness.executeOnce();
|
||||
expect(result?.success).toBeTrue();
|
||||
harness.expectFile('dist/server.mjs').toExist();
|
||||
});
|
||||
});
|
||||
});
|
@ -97,7 +97,7 @@ export function createServerCodeBundleOptions(
|
||||
target: string[],
|
||||
sourceFileCache: SourceFileCache,
|
||||
): BuildOptions {
|
||||
const { jit, serverEntryPoint, workspaceRoot } = options;
|
||||
const { jit, serverEntryPoint, workspaceRoot, ssrOptions } = options;
|
||||
|
||||
assert(
|
||||
serverEntryPoint,
|
||||
@ -110,7 +110,15 @@ export function createServerCodeBundleOptions(
|
||||
sourceFileCache,
|
||||
);
|
||||
|
||||
const namespace = 'angular:server-entry';
|
||||
const namespace = 'angular:main-server';
|
||||
const entryPoints: Record<string, string> = {
|
||||
'main.server': namespace,
|
||||
};
|
||||
|
||||
const ssrEntryPoint = ssrOptions?.entry;
|
||||
if (ssrEntryPoint) {
|
||||
entryPoints['server'] = ssrEntryPoint;
|
||||
}
|
||||
|
||||
const buildOptions: BuildOptions = {
|
||||
...getEsBuildCommonOptions(options),
|
||||
@ -131,9 +139,7 @@ export function createServerCodeBundleOptions(
|
||||
`globalThis['require'] ??= createRequire(import.meta.url);`,
|
||||
].join('\n'),
|
||||
},
|
||||
entryPoints: {
|
||||
'server': namespace,
|
||||
},
|
||||
entryPoints,
|
||||
supported: getFeatureSupport(target),
|
||||
plugins: [
|
||||
createSourcemapIngorelistPlugin(),
|
||||
@ -143,30 +149,6 @@ export function createServerCodeBundleOptions(
|
||||
// Component stylesheet options
|
||||
styleOptions,
|
||||
),
|
||||
createVirtualModulePlugin({
|
||||
namespace,
|
||||
loadContent: () => {
|
||||
const mainServerEntryPoint = path
|
||||
.relative(workspaceRoot, serverEntryPoint)
|
||||
.replace(/\\/g, '/');
|
||||
const importAndExportDec: string[] = [
|
||||
`import '@angular/platform-server/init';`,
|
||||
`import moduleOrBootstrapFn from './${mainServerEntryPoint}';`,
|
||||
`export default moduleOrBootstrapFn;`,
|
||||
`export { renderApplication, renderModule, ɵSERVER_CONTEXT } from '@angular/platform-server';`,
|
||||
];
|
||||
|
||||
if (jit) {
|
||||
importAndExportDec.unshift(`import '@angular/compiler';`);
|
||||
}
|
||||
|
||||
return {
|
||||
contents: importAndExportDec.join('\n'),
|
||||
loader: 'js',
|
||||
resolveDir: workspaceRoot,
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
@ -177,6 +159,33 @@ export function createServerCodeBundleOptions(
|
||||
buildOptions.plugins.push(createRxjsEsmResolutionPlugin());
|
||||
}
|
||||
|
||||
buildOptions.plugins.push(
|
||||
createVirtualModulePlugin({
|
||||
namespace,
|
||||
loadContent: () => {
|
||||
const mainServerEntryPoint = path
|
||||
.relative(workspaceRoot, serverEntryPoint)
|
||||
.replace(/\\/g, '/');
|
||||
const importAndExportDec: string[] = [
|
||||
`import '@angular/platform-server/init';`,
|
||||
`import moduleOrBootstrapFn from './${mainServerEntryPoint}';`,
|
||||
`export default moduleOrBootstrapFn;`,
|
||||
`export { renderApplication, renderModule, ɵSERVER_CONTEXT } from '@angular/platform-server';`,
|
||||
];
|
||||
|
||||
if (jit) {
|
||||
importAndExportDec.unshift(`import '@angular/compiler';`);
|
||||
}
|
||||
|
||||
return {
|
||||
contents: importAndExportDec.join('\n'),
|
||||
loader: 'js',
|
||||
resolveDir: workspaceRoot,
|
||||
};
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return buildOptions;
|
||||
}
|
||||
|
||||
|
@ -63,7 +63,7 @@ async function render({ route, serverContext }: RenderOptions): Promise<RenderRe
|
||||
ɵSERVER_CONTEXT,
|
||||
renderModule,
|
||||
renderApplication,
|
||||
} = await loadEsmModule<BundleExports>('./server.mjs');
|
||||
} = await loadEsmModule<BundleExports>('./main.server.mjs');
|
||||
|
||||
assert(ɵSERVER_CONTEXT, `ɵSERVER_CONTEXT was not exported.`);
|
||||
|
||||
|
@ -375,14 +375,17 @@ async function findPackageTars(): Promise<{ [pkg: string]: PkgInfo }> {
|
||||
}),
|
||||
);
|
||||
|
||||
return pkgs.reduce((all, pkg, i) => {
|
||||
const json = pkgJsons[i].toString('utf8');
|
||||
const { name, version } = JSON.parse(json);
|
||||
if (!name) {
|
||||
throw new Error(`Package ${pkg} - package.json name/version not found`);
|
||||
}
|
||||
return pkgs.reduce(
|
||||
(all, pkg, i) => {
|
||||
const json = pkgJsons[i].toString('utf8');
|
||||
const { name, version } = JSON.parse(json);
|
||||
if (!name) {
|
||||
throw new Error(`Package ${pkg} - package.json name/version not found`);
|
||||
}
|
||||
|
||||
all[name] = { path: realpathSync(pkg), name, version };
|
||||
return all;
|
||||
}, {} as { [pkg: string]: PkgInfo });
|
||||
all[name] = { path: realpathSync(pkg), name, version };
|
||||
return all;
|
||||
},
|
||||
{} as { [pkg: string]: PkgInfo },
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user