Skip to content
Draft
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
Expand Up @@ -138,13 +138,14 @@ export async function executePostBundleSteps(

// Pre-render (SSG) and App-shell
// If localization is enabled, prerendering is handled in the inlining process.
let indexHtmlOutputPath = indexHtmlOptions?.output;
if (
!partialSSRBuild &&
(prerenderOptions || appShellOptions || (outputMode && serverEntryPoint)) &&
!allErrors.length
) {
assert(
indexHtmlOptions,
indexHtmlOptions && indexHtmlOutputPath,
'The "index" option is required when using the "ssg" or "appShell" options.',
);

Expand All @@ -163,17 +164,19 @@ export async function executePostBundleSteps(
allErrors.push(...errors);
allWarnings.push(...warnings);

const indexHasBeenPrerendered = output[indexHtmlOptions.output];
const indexHasBeenPrerendered = output[indexHtmlOutputPath];
for (const [path, { content, appShellRoute }] of Object.entries(output)) {
// Update the index contents with the app shell under these conditions:
// - Replace 'index.html' with the app shell only if it hasn't been prerendered yet.
// - Always replace 'index.csr.html' with the app shell.
let filePath = path;
if (appShellRoute && !indexHasBeenPrerendered) {
if (outputMode !== OutputMode.Server && indexHtmlOptions.output === INDEX_HTML_CSR) {
if (outputMode !== OutputMode.Server && indexHtmlOutputPath === INDEX_HTML_CSR) {
filePath = 'index.html';
// Needed to update the ngsw.json "index" value.
indexHtmlOutputPath = filePath;
} else {
filePath = indexHtmlOptions.output;
filePath = indexHtmlOutputPath;
}
}

Expand Down Expand Up @@ -232,7 +235,7 @@ export async function executePostBundleSteps(
workspaceRoot,
serviceWorker,
baseHref,
options.indexHtmlOptions?.output,
indexHtmlOutputPath,
// Ensure additional files recently added are used
[...outputFiles, ...additionalOutputFiles],
assetFiles,
Expand Down
10 changes: 6 additions & 4 deletions packages/angular/build/src/builders/dev-server/vite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,21 @@
import type { BuilderContext } from '@angular-devkit/architect';
import type { Plugin } from 'esbuild';
import assert from 'node:assert';
import { builtinModules, isBuiltin } from 'node:module';
import { join } from 'node:path';
import type { Connect, ViteDevServer } from 'vite';
import type { ComponentStyleRecord } from '../../../tools/vite/middlewares';
import { ServerSsrMode } from '../../../tools/vite/plugins';
import { EsbuildLoaderOption, updateExternalMetadata } from '../../../tools/vite/utils';
import { normalizeSourceMaps } from '../../../utils';
import { useComponentStyleHmr, useComponentTemplateHmr } from '../../../utils/environment-options';
import {
useComponentStyleHmr,
useComponentTemplateHmr,
usePartialSsrBuild,
} from '../../../utils/environment-options';
import { Result, ResultKind } from '../../application/results';
import { OutputHashing } from '../../application/schema';
import {
type ApplicationBuilderInternalOptions,
type ExternalResultMetadata,
JavaScriptTransformer,
getSupportedBrowsers,
isZonelessApp,
Expand Down Expand Up @@ -119,7 +121,7 @@ export async function* serveWithVite(

// Disable generating a full manifest with routes.
// This is done during runtime when using the dev-server.
browserOptions.partialSSRBuild = true;
browserOptions.partialSSRBuild = usePartialSsrBuild;

// The development server currently only supports a single locale when localizing.
// This matches the behavior of the Webpack-based development server but could be expanded in the future.
Expand Down
43 changes: 18 additions & 25 deletions packages/angular/build/src/builders/dev-server/vite/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,42 +48,35 @@ export function updateResultRecord(
return;
}

let filePath;
if (outputPath === htmlIndexPath) {
// Convert custom index output path to standard index path for dev-server usage.
// This mimics the Webpack dev-server behavior.
filePath = '/index.html';
} else {
filePath = '/' + normalizePath(outputPath);
}

const servable =
file.type === BuildOutputFileType.Browser || file.type === BuildOutputFileType.Media;
const filePath = '/' + normalizePath(outputPath);
const generatedFile: OutputFileRecord = {
contents: file.contents,
size: file.contents.byteLength,
hash: file.hash,
// Consider the files updated except on the initial build result
updated: !initial,
type: file.type,
servable: file.type === BuildOutputFileType.Browser || file.type === BuildOutputFileType.Media,
};

// Skip analysis of sourcemaps
if (filePath.endsWith('.map')) {
generatedFiles.set(filePath, {
contents: file.contents,
servable,
size: file.contents.byteLength,
hash: file.hash,
type: file.type,
...generatedFile,
updated: false,
});

return;
}

// New or updated file
generatedFiles.set(filePath, {
contents: file.contents,
size: file.contents.byteLength,
hash: file.hash,
// Consider the files updated except on the initial build result
updated: !initial,
type: file.type,
servable,
});
generatedFiles.set(filePath, generatedFile);

if (outputPath === htmlIndexPath) {
// Convert custom index output path to standard index path for dev-server usage.
// This mimics the Webpack dev-server behavior.
generatedFiles.set('/index.html', generatedFile);
}

// Record any external component styles
if (filePath.endsWith('.css') && /^\/[a-f0-9]{64}\.css$/.test(filePath)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { readFileSync } from 'node:fs';
import type { ServerResponse } from 'node:http';
import { extname } from 'node:path';
import type { Connect, ViteDevServer } from 'vite';
import { INDEX_HTML_CSR } from '../../../builders/application/options';
import { ResultFile } from '../../../builders/application/results';
import { AngularMemoryOutputFiles, AngularOutputAssets, pathnameWithoutBasePath } from '../utils';

Expand Down Expand Up @@ -96,7 +97,9 @@ export function createAngularAssetsMiddleware(
// Resource files are handled directly.
// Global stylesheets (CSS files) are currently considered resources to workaround
// dev server sourcemap issues with stylesheets.
if (extension !== '.js' && extension !== '.html') {
// Vite will added the client code even when no-live-reload and no-hmr are passed thus we need to
// exclude .js and .html files when hmr is disabled.
if ((extension !== '.js' && extension !== '.html') || !server.config.server?.hmr) {
const outputFile = outputFiles.get(pathname);
if (outputFile?.servable) {
let data: Uint8Array | string = outputFile.contents;
Expand Down
40 changes: 21 additions & 19 deletions tests/e2e/tests/build/app-shell/app-shell-with-service-worker.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { setTimeout } from 'node:timers/promises';
import { getGlobalVariable } from '../../../utils/env';
import { appendToFile, expectFileToMatch, writeFile } from '../../../utils/fs';
import { appendToFile, expectFileToMatch } from '../../../utils/fs';
import { installPackage } from '../../../utils/packages';
import { ng } from '../../../utils/process';
import { updateJsonFile } from '../../../utils/project';
import { executeBrowserTest } from '../../../utils/puppeteer';

const snapshots = require('../../../ng-snapshot/package.json');

Expand Down Expand Up @@ -31,26 +33,26 @@ export default async function () {
}
}

await writeFile(
'e2e/app.e2e-spec.ts',
`
import { browser, by, element } from 'protractor';
await ng('build');
await expectFileToMatch('dist/test-project/browser/index.html', /app-shell works!/);

it('should have ngsw in normal state', () => {
browser.get('/');
await executeBrowserTest({
configuration: 'production',
checkFn: async (page) => {
// Wait for service worker to load.
browser.sleep(2000);
browser.waitForAngularEnabled(false);
browser.get('/ngsw/state');
// Should have updated, and be in normal state.
expect(element(by.css('pre')).getText()).not.toContain('Last update check: never');
expect(element(by.css('pre')).getText()).toContain('Driver state: NORMAL');
});
`,
);
await setTimeout(2000);

await ng('build');
await expectFileToMatch('dist/test-project/browser/index.html', /app-shell works!/);
const baseUrl = page.url();
await page.goto(new URL('/ngsw/state', baseUrl).href);

await ng('e2e', '--configuration=production');
// Should have updated, and be in normal state.
const preText = await page.$eval('pre', (el) => el.textContent);
if (preText?.includes('Last update check: never')) {
throw new Error(`Expected service worker to have checked for updates, but got: ${preText}`);
}
if (!preText?.includes('Driver state: NORMAL')) {
throw new Error(`Expected service worker driver state to be NORMAL, but got: ${preText}`);
}
},
});
}
5 changes: 4 additions & 1 deletion tests/e2e/utils/puppeteer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export async function executeBrowserTest(options: BrowserTestOptions = {}) {
if (!url) {
// Start serving and find address (1 - Webpack; 2 - Vite)
const match = /(?:open your browser on|Local:)\s+(http:\/\/localhost:\d+\/)/;
const serveArgs = ['serve', '--port=0'];
const serveArgs = ['serve', '--port=0', '--no-watch', '--no-live-reload'];
if (options.project) {
serveArgs.push(options.project);
}
Expand All @@ -29,11 +29,14 @@ export async function executeBrowserTest(options: BrowserTestOptions = {}) {
const { stdout } = await execAndWaitForOutputToMatch('ng', serveArgs, match, {
...process.env,
'NO_COLOR': '1',
'NG_BUILD_PARTIAL_SSR': '0',
});

url = stripVTControlCharacters(stdout).match(match)?.[1];
if (!url) {
throw new Error('Could not find serving URL');
}

hasStartedServer = true;
}

Expand Down
Loading