diff --git a/packages/angular/build/src/builders/application/index.ts b/packages/angular/build/src/builders/application/index.ts index afe657e59da7..80261c41277f 100644 --- a/packages/angular/build/src/builders/application/index.ts +++ b/packages/angular/build/src/builders/application/index.ts @@ -28,6 +28,8 @@ import { import { Result, ResultKind } from './results'; import { Schema as ApplicationBuilderOptions } from './schema'; +const isNodeV22orHigher = Number(process.versions.node.split('.', 1)[0]) >= 22; + export type { ApplicationBuilderOptions }; export async function* buildApplicationInternal( @@ -211,7 +213,17 @@ export async function* buildApplication( await fs.writeFile(fullFilePath, file.contents); } else { // Copy file contents - await fs.copyFile(file.inputPath, fullFilePath, fs.constants.COPYFILE_FICLONE); + if (isNodeV22orHigher) { + // Use newer `cp` API on Node.js 22+ (minimum v22 for CLI is 22.11) + await fs.cp(file.inputPath, fullFilePath, { + mode: fs.constants.COPYFILE_FICLONE, + preserveTimestamps: true, + }); + } else { + // For Node.js 20 use `copyFile` (`cp` is not stable for v20) + // TODO: Remove when Node.js 20 is no longer supported + await fs.copyFile(file.inputPath, fullFilePath, fs.constants.COPYFILE_FICLONE); + } } }); diff --git a/tests/legacy-cli/e2e/tests/build/assets.ts b/tests/legacy-cli/e2e/tests/build/assets.ts index 5c484f02d5cd..1875268c5afe 100644 --- a/tests/legacy-cli/e2e/tests/build/assets.ts +++ b/tests/legacy-cli/e2e/tests/build/assets.ts @@ -1,12 +1,17 @@ +import assert from 'node:assert/strict'; import * as fs from 'node:fs'; import { expectFileToExist, expectFileToMatch, writeFile } from '../../utils/fs'; import { ng } from '../../utils/process'; import { updateJsonFile } from '../../utils/project'; import { expectToFail } from '../../utils/utils'; +import { getGlobalVariable } from '../../utils/env'; + +const isNodeV22orHigher = Number(process.versions.node.split('.', 1)[0]) >= 22; export default async function () { await writeFile('public/.file', ''); await writeFile('public/test.abc', 'hello world'); + const originalStats = fs.statSync('public/test.abc', { bigint: true }); await ng('build', '--configuration=development'); @@ -15,6 +20,15 @@ export default async function () { await expectFileToMatch('dist/test-project/browser/test.abc', 'hello world'); await expectToFail(() => expectFileToExist('dist/test-project/browser/.gitkeep')); + // Timestamp preservation only supported with application build system on Node.js v22+ + if (isNodeV22orHigher && getGlobalVariable('argv')['esbuild']) { + const outputStats = fs.statSync('dist/test-project/browser/test.abc', { bigint: true }); + assert( + originalStats.mtimeMs === outputStats.mtimeMs, + 'Asset file modified timestamp should be preserved.', + ); + } + // Ensure `followSymlinks` option follows symlinks await updateJsonFile('angular.json', (workspaceJson) => { const appArchitect = workspaceJson.projects['test-project'].architect;