Skip to content

Commit

Permalink
feat(@angular/build): utilize ssr.entry in Vite dev-server when ava…
Browse files Browse the repository at this point in the history
…ilable

When `ssr.entry` (`server.ts`) is defined, Vite will now use it in the dev-server. This feature requires the new `@angular/ssr` APIs, which are currently in developer preview.
  • Loading branch information
alan-agius4 committed Sep 20, 2024
1 parent c0315fb commit 765134d
Show file tree
Hide file tree
Showing 13 changed files with 721 additions and 134 deletions.
4 changes: 2 additions & 2 deletions packages/angular/build/src/builders/application/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,15 @@ export async function* buildApplicationInternal(

yield* runEsBuildBuildAction(
async (rebuildState) => {
const { serverEntryPoint, jsonLogs } = normalizedOptions;
const { serverEntryPoint, jsonLogs, disableFullServerManifestGeneration } = normalizedOptions;

const startTime = process.hrtime.bigint();
const result = await executeBuild(normalizedOptions, context, rebuildState);

if (jsonLogs) {
result.addLog(await createJsonBuildManifest(result, normalizedOptions));
} else {
if (serverEntryPoint) {
if (serverEntryPoint && !disableFullServerManifestGeneration) {
const prerenderedRoutesLength = Object.keys(result.prerenderedRoutes).length;
let prerenderMsg = `Prerendered ${prerenderedRoutesLength} static route`;
prerenderMsg += prerenderedRoutesLength !== 1 ? 's.' : '.';
Expand Down
99 changes: 62 additions & 37 deletions packages/angular/build/src/builders/dev-server/vite-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ import type { Connect, DepOptimizationConfig, InlineConfig, ViteDevServer } from
import { createAngularMemoryPlugin } from '../../tools/vite/angular-memory-plugin';
import { createAngularLocaleDataPlugin } from '../../tools/vite/i18n-locale-plugin';
import { createRemoveIdPrefixPlugin } from '../../tools/vite/id-prefix-plugin';
import {
ServerSsrMode,
createAngularSetupMiddlewaresPlugin,
} from '../../tools/vite/setup-middlewares-plugin';
import { createAngularSsrServerPlugin } from '../../tools/vite/ssr-server-plugin';
import { loadProxyConfiguration, normalizeSourceMaps } from '../../utils';
import { loadEsmModule } from '../../utils/load-esm';
import { Result, ResultFile, ResultKind } from '../application/results';
Expand Down Expand Up @@ -313,14 +318,25 @@ export async function* serveWithVite(
? browserOptions.polyfills
: [browserOptions.polyfills];

let ssrMode: ServerSsrMode = ServerSsrMode.NoSsr;
if (
browserOptions.outputMode &&
typeof browserOptions.ssr === 'object' &&
browserOptions.ssr.entry
) {
ssrMode = ServerSsrMode.ExternalSsrMiddleware;
} else if (browserOptions.server) {
ssrMode = ServerSsrMode.InternalSsrMiddleware;
}

// Setup server and start listening
const serverConfiguration = await setupServer(
serverOptions,
generatedFiles,
assetFiles,
browserOptions.preserveSymlinks,
externalMetadata,
!!browserOptions.ssr,
ssrMode,
prebundleTransformer,
target,
isZonelessApp(polyfills),
Expand All @@ -337,7 +353,10 @@ export async function* serveWithVite(
if (browserOptions.ssr && serverOptions.prebundle !== false) {
// Warm up the SSR request and begin optimizing dependencies.
// Without this, Vite will only start optimizing SSR modules when the first request is made.
void server.warmupRequest('./main.server.mjs', { ssr: true });
void Promise.allSettled([
server.warmupRequest('./server.mjs', { ssr: true }),
server.warmupRequest('./main.server.mjs', { ssr: true }),
]);
}

const urls = server.resolvedUrls;
Expand Down Expand Up @@ -385,34 +404,37 @@ async function handleUpdate(
usedComponentStyles: Map<string, string[]>,
): Promise<void> {
const updatedFiles: string[] = [];
let isServerFileUpdated = false;
let destroyAngularServerAppCalled = false;

// Invalidate any updated files
for (const [file, record] of generatedFiles) {
if (record.updated) {
updatedFiles.push(file);
isServerFileUpdated ||= record.type === BuildOutputFileType.ServerApplication;
for (const [file, { updated, type }] of generatedFiles) {
if (!updated) {
continue;
}

const updatedModules = server.moduleGraph.getModulesByFile(
normalizePath(join(server.config.root, file)),
);
updatedModules?.forEach((m) => server?.moduleGraph.invalidateModule(m));
if (type === BuildOutputFileType.ServerApplication && !destroyAngularServerAppCalled) {
// Clear the server app cache
// This must be done before module invalidation.
const { ɵdestroyAngularServerApp } = (await server.ssrLoadModule('/main.server.mjs')) as {
ɵdestroyAngularServerApp: typeof destroyAngularServerApp;
};

ɵdestroyAngularServerApp();
destroyAngularServerAppCalled = true;
}

updatedFiles.push(file);

const updatedModules = server.moduleGraph.getModulesByFile(
normalizePath(join(server.config.root, file)),
);
updatedModules?.forEach((m) => server.moduleGraph.invalidateModule(m));
}

if (!updatedFiles.length) {
return;
}

// clean server apps cache
if (isServerFileUpdated) {
const { ɵdestroyAngularServerApp } = (await server.ssrLoadModule('/main.server.mjs')) as {
ɵdestroyAngularServerApp: typeof destroyAngularServerApp;
};

ɵdestroyAngularServerApp();
}

if (serverOptions.liveReload || serverOptions.hmr) {
if (updatedFiles.every((f) => f.endsWith('.css'))) {
const timestamp = Date.now();
Expand Down Expand Up @@ -534,7 +556,7 @@ export async function setupServer(
assets: Map<string, string>,
preserveSymlinks: boolean | undefined,
externalMetadata: DevServerExternalResultMetadata,
ssr: boolean,
ssrMode: ServerSsrMode,
prebundleTransformer: JavaScriptTransformer,
target: string[],
zoneless: boolean,
Expand Down Expand Up @@ -571,6 +593,25 @@ export async function setupServer(
css: {
devSourcemap: true,
},
plugins: [
createAngularLocaleDataPlugin(),
createAngularSetupMiddlewaresPlugin({
outputFiles,
assets,
indexHtmlTransformer,
extensionMiddleware,
usedComponentStyles,
ssrMode,
}),
createRemoveIdPrefixPlugin(externalMetadata.explicitBrowser),
await createAngularSsrServerPlugin(serverOptions.workspaceRoot),
await createAngularMemoryPlugin({
workspaceRoot: serverOptions.workspaceRoot,
virtualProjectRoot,
outputFiles,
external: externalMetadata.explicitBrowser,
}),
],
// Ensure custom 'file' loader build option entries are handled by Vite in application code that
// reference third-party libraries. Relative usage is handled directly by the build and not Vite.
// Only 'file' loader entries are currently supported directly by Vite.
Expand Down Expand Up @@ -635,22 +676,6 @@ export async function setupServer(
thirdPartySourcemaps,
}),
},
plugins: [
createAngularLocaleDataPlugin(),
createAngularMemoryPlugin({
workspaceRoot: serverOptions.workspaceRoot,
virtualProjectRoot,
outputFiles,
assets,
ssr,
external: externalMetadata.explicitBrowser,
indexHtmlTransformer,
extensionMiddleware,
normalizePath,
usedComponentStyles,
}),
createRemoveIdPrefixPlugin(externalMetadata.explicitBrowser),
],
// Browser only optimizeDeps. (This does not run for SSR dependencies).
optimizeDeps: getDepOptimizationConfig({
// Only enable with caching since it causes prebundle dependencies to be cached
Expand Down
100 changes: 19 additions & 81 deletions packages/angular/build/src/tools/vite/angular-memory-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,46 +6,25 @@
* found in the LICENSE file at https://angular.dev/license
*/

import remapping, { SourceMapInput } from '@ampproject/remapping';
import assert from 'node:assert';
import { readFile } from 'node:fs/promises';
import { dirname, join, relative } from 'node:path';
import type { Connect, Plugin } from 'vite';
import {
angularHtmlFallbackMiddleware,
createAngularAssetsMiddleware,
createAngularHeadersMiddleware,
createAngularIndexHtmlMiddleware,
createAngularSSRMiddleware,
} from './middlewares';
import type { Plugin } from 'vite';
import { loadEsmModule } from '../../utils/load-esm';
import { AngularMemoryOutputFiles } from './utils';

export interface AngularMemoryPluginOptions {
workspaceRoot: string;
virtualProjectRoot: string;
outputFiles: AngularMemoryOutputFiles;
assets: Map<string, string>;
ssr: boolean;
external?: string[];
extensionMiddleware?: Connect.NextHandleFunction[];
indexHtmlTransformer?: (content: string) => Promise<string>;
normalizePath: (path: string) => string;
usedComponentStyles: Map<string, string[]>;
}

export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions): Plugin {
const {
workspaceRoot,
virtualProjectRoot,
outputFiles,
assets,
external,
ssr,
extensionMiddleware,
indexHtmlTransformer,
normalizePath,
usedComponentStyles,
} = options;
export async function createAngularMemoryPlugin(
options: AngularMemoryPluginOptions,
): Promise<Plugin> {
const { virtualProjectRoot, outputFiles, external, workspaceRoot } = options;
const { normalizePath } = await loadEsmModule<typeof import('vite')>('vite');

return {
name: 'vite:angular-memory',
Expand All @@ -59,12 +38,19 @@ export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions):
return source;
}

if (importer && source[0] === '.' && normalizePath(importer).startsWith(virtualProjectRoot)) {
// Remove query if present
const [importerFile] = importer.split('?', 1);
if (importer) {
let normalizedSource: string | undefined;
if (source[0] === '.' && normalizePath(importer).startsWith(virtualProjectRoot)) {
// Remove query if present
const [importerFile] = importer.split('?', 1);
normalizedSource = join(dirname(relative(virtualProjectRoot, importerFile)), source);
} else if (normalizePath(source).startsWith(workspaceRoot)) {
normalizedSource = relative(workspaceRoot, source);
}

source =
'/' + normalizePath(join(dirname(relative(virtualProjectRoot, importerFile)), source));
if (normalizedSource) {
source = '/' + normalizePath(normalizedSource);
}
}

const [file] = source.split('?', 1);
Expand Down Expand Up @@ -92,54 +78,6 @@ export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions):
map: mapContents && Buffer.from(mapContents).toString('utf-8'),
};
},
// eslint-disable-next-line max-lines-per-function
configureServer(server) {
const originalssrTransform = server.ssrTransform;
server.ssrTransform = async (code, map, url, originalCode) => {
const result = await originalssrTransform(code, null, url, originalCode);
if (!result || !result.map || !map) {
return result;
}

const remappedMap = remapping(
[result.map as SourceMapInput, map as SourceMapInput],
() => null,
);

// Set the sourcemap root to the workspace root. This is needed since we set a virtual path as root.
remappedMap.sourceRoot = normalizePath(workspaceRoot) + '/';

return {
...result,
map: remappedMap as (typeof result)['map'],
};
};

server.middlewares.use(createAngularHeadersMiddleware(server));

// Assets and resources get handled first
server.middlewares.use(
createAngularAssetsMiddleware(server, assets, outputFiles, usedComponentStyles),
);

if (extensionMiddleware?.length) {
extensionMiddleware.forEach((middleware) => server.middlewares.use(middleware));
}

// Returning a function, installs middleware after the main transform middleware but
// before the built-in HTML middleware
return () => {
if (ssr) {
server.middlewares.use(createAngularSSRMiddleware(server, indexHtmlTransformer));
}

server.middlewares.use(angularHtmlFallbackMiddleware);

server.middlewares.use(
createAngularIndexHtmlMiddleware(server, outputFiles, indexHtmlTransformer),
);
};
},
};
}

Expand Down
5 changes: 4 additions & 1 deletion packages/angular/build/src/tools/vite/middlewares/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@
export { createAngularAssetsMiddleware } from './assets-middleware';
export { angularHtmlFallbackMiddleware } from './html-fallback-middleware';
export { createAngularIndexHtmlMiddleware } from './index-html-middleware';
export { createAngularSSRMiddleware } from './ssr-middleware';
export {
createAngularSsrExternalMiddleware,
createAngularSsrInternalMiddleware,
} from './ssr-middleware';
export { createAngularHeadersMiddleware } from './headers-middleware';
Loading

0 comments on commit 765134d

Please sign in to comment.