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
29 changes: 21 additions & 8 deletions packages/angular/build/src/builders/dev-server/vite-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,17 +138,14 @@ export async function* serveWithVite(
process.setSourceMapsEnabled(true);
}

// Enable to support component style hot reloading (`NG_HMR_CSTYLES=0` can be used to disable selectively)
// Enable to support link-based component style hot reloading (`NG_HMR_CSTYLES=0` can be used to disable selectively)
browserOptions.externalRuntimeStyles =
serverOptions.liveReload && serverOptions.hmr && useComponentStyleHmr;

// Enable to support component template hot replacement (`NG_HMR_TEMPLATE=1` can be used to enable)
browserOptions.templateUpdates = !!serverOptions.liveReload && useComponentTemplateHmr;
if (browserOptions.templateUpdates) {
context.logger.warn(
'Experimental support for component template hot replacement has been enabled via the "NG_HMR_TEMPLATE" environment variable.',
);
}
// Enable to support component template hot replacement (`NG_HMR_TEMPLATE=0` can be used to disable selectively)
// This will also replace file-based/inline styles as code if external runtime styles are not enabled.
browserOptions.templateUpdates =
serverOptions.liveReload && serverOptions.hmr && useComponentTemplateHmr;

// Setup the prebundling transformer that will be shared across Vite prebundling requests
const prebundleTransformer = new JavaScriptTransformer(
Expand Down Expand Up @@ -233,6 +230,12 @@ export async function* serveWithVite(
assetFiles.set('/' + normalizePath(outputPath), normalizePath(file.inputPath));
}
}

// Invalidate SSR module graph to ensure that only new rebuild is used and not stale component updates
if (server && browserOptions.ssr && templateUpdates.size > 0) {
server.moduleGraph.invalidateAll();
}

// Clear stale template updates on code rebuilds
templateUpdates.clear();

Expand All @@ -256,6 +259,16 @@ export async function* serveWithVite(
'Builder must provide an initial full build before component update results.',
);

// Invalidate SSR module graph to ensure that new component updates are used
// TODO: Use fine-grained invalidation of only the component update modules
if (browserOptions.ssr) {
server.moduleGraph.invalidateAll();
const { ɵresetCompiledComponents } = (await server.ssrLoadModule('/main.server.mjs')) as {
ɵresetCompiledComponents: () => void;
};
ɵresetCompiledComponents();
}

for (const componentUpdate of result.updates) {
if (componentUpdate.type === 'template') {
templateUpdates.set(componentUpdate.id, componentUpdate.content);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,9 @@ export function createServerMainCodeBundleOptions(
ɵgetOrCreateAngularServerApp,
} from '@angular/ssr';`,

// Need for HMR
`export { ɵresetCompiledComponents } from '@angular/core';`,

// Re-export all symbols including default export from 'main.server.ts'
`export { default } from '${mainServerEntryPointJsImport}';`,
`export * from '${mainServerEntryPointJsImport}';`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export async function createAngularMemoryPlugin(
const requestUrl = new URL(id.slice(1), 'http://localhost');
const componentId = requestUrl.searchParams.get('c');

return (componentId && options.templateUpdates?.get(componentId)) ?? '';
return (componentId && options.templateUpdates?.get(encodeURIComponent(componentId))) ?? '';
}

const [file] = id.split('?', 1);
Expand Down
2 changes: 1 addition & 1 deletion packages/angular/build/src/utils/environment-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export const useComponentStyleHmr =

const hmrComponentTemplateVariable = process.env['NG_HMR_TEMPLATES'];
export const useComponentTemplateHmr =
isPresent(hmrComponentTemplateVariable) && isEnabled(hmrComponentTemplateVariable);
!isPresent(hmrComponentTemplateVariable) || !isDisabled(hmrComponentTemplateVariable);

const partialSsrBuildVariable = process.env['NG_BUILD_PARTIAL_SSR'];
export const usePartialSsrBuild =
Expand Down
9 changes: 2 additions & 7 deletions tests/legacy-cli/e2e/tests/basic/rebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,8 @@ export default async function () {
const validBundleRegEx = esbuild ? /sent to client/ : /Compiled successfully\./;
const lazyBundleRegEx = esbuild ? /chunk-/ : /src_app_lazy_lazy_component_ts\.js/;

// Disable component stylesheet HMR to support page reload based rebuild testing.
// Ideally this environment variable would be passed directly to the new serve process
// but this would require signficant test changes due to the existing `ngServe` signature.
const oldHMRValue = process.env['NG_HMR_CSTYLES'];
process.env['NG_HMR_CSTYLES'] = '0';
const port = await ngServe();
process.env['NG_HMR_CSTYLES'] = oldHMRValue;
// Disable HMR to support page reload based rebuild testing.
const port = await ngServe('--no-hmr');

// Add a lazy route.
await silentNg('generate', 'component', 'lazy');
Expand Down
6 changes: 5 additions & 1 deletion tests/legacy-cli/e2e/tests/vite/ssr-entry-express.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export default async function () {
'src/app/home/home.component.html',
'home works',
'yay home works!!!',
true,
);
await validateResponse('/api/test', /foo/);
await validateResponse('/home', /yay home works/);
Expand All @@ -111,9 +112,12 @@ async function modifyFileAndWaitUntilUpdated(
filePath: string,
searchValue: string,
replaceValue: string,
hmr = false,
): Promise<void> {
await Promise.all([
waitForAnyProcessOutputToMatch(/Page reload sent to client/),
waitForAnyProcessOutputToMatch(
hmr ? /Component update sent to client/ : /Page reload sent to client/,
),
setTimeout(100).then(() => replaceInFile(filePath, searchValue, replaceValue)),
]);
}
6 changes: 5 additions & 1 deletion tests/legacy-cli/e2e/tests/vite/ssr-entry-fastify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export default async function () {
'src/app/home/home.component.html',
'home works',
'yay home works!!!',
true,
);
await validateResponse('/api/test', /foo/);
await validateResponse('/home', /yay home works/);
Expand All @@ -111,9 +112,12 @@ async function modifyFileAndWaitUntilUpdated(
filePath: string,
searchValue: string,
replaceValue: string,
hmr = false,
): Promise<void> {
await Promise.all([
waitForAnyProcessOutputToMatch(/Page reload sent to client/),
waitForAnyProcessOutputToMatch(
hmr ? /Component update sent to client/ : /Page reload sent to client/,
),
setTimeout(100).then(() => replaceInFile(filePath, searchValue, replaceValue)),
]);
}
6 changes: 5 additions & 1 deletion tests/legacy-cli/e2e/tests/vite/ssr-entry-h3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export default async function () {
'src/app/home/home.component.html',
'home works',
'yay home works!!!',
true,
);
await validateResponse('/api/test', /foo/);
await validateResponse('/home', /yay home works/);
Expand All @@ -102,9 +103,12 @@ async function modifyFileAndWaitUntilUpdated(
filePath: string,
searchValue: string,
replaceValue: string,
hmr = false,
): Promise<void> {
await Promise.all([
waitForAnyProcessOutputToMatch(/Page reload sent to client/),
waitForAnyProcessOutputToMatch(
hmr ? /Component update sent to client/ : /Page reload sent to client/,
),
setTimeout(100).then(() => replaceInFile(filePath, searchValue, replaceValue)),
]);
}
8 changes: 6 additions & 2 deletions tests/legacy-cli/e2e/tests/vite/ssr-entry-hono.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { setTimeout } from 'node:timers/promises';
import { replaceInFile, writeMultipleFiles } from '../../utils/fs';
import { ng, silentNg, waitForAnyProcessOutputToMatch } from '../../utils/process';
import { installPackage, installWorkspacePackages, uninstallPackage } from '../../utils/packages';
import { ngServe, updateJsonFile, useSha } from '../../utils/project';
import { ngServe, useSha } from '../../utils/project';
import { getGlobalVariable } from '../../utils/env';

export default async function () {
Expand Down Expand Up @@ -73,6 +73,7 @@ export default async function () {
'src/app/home/home.component.html',
'home works',
'yay home works!!!',
true,
);
await validateResponse('/api/test', /foo/);
await validateResponse('/home', /yay home works/);
Expand All @@ -94,9 +95,12 @@ async function modifyFileAndWaitUntilUpdated(
filePath: string,
searchValue: string,
replaceValue: string,
hmr = false,
): Promise<void> {
await Promise.all([
waitForAnyProcessOutputToMatch(/Page reload sent to client/),
waitForAnyProcessOutputToMatch(
hmr ? /Component update sent to client/ : /Page reload sent to client/,
),
setTimeout(100).then(() => replaceInFile(filePath, searchValue, replaceValue)),
]);
}
Loading