Skip to content

Commit

Permalink
fix(@angular-devkit/build-angular): use translation file in bundle ha…
Browse files Browse the repository at this point in the history
…sh calculations

This change ensures that any changes to translation files is represented in the output file names when output hashing is enabled.  This prevents the situation where a translation file only change to an application would result in built files with no change in output name.
  • Loading branch information
clydin authored and mgechev committed Feb 4, 2020
1 parent 441c606 commit 57796cf
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,37 @@ export async function generateI18nBrowserWebpackConfigFromContext(
config.resolve.alias = {};
}
config.resolve.alias['@angular/localize/init'] = require.resolve('./empty.js');

// Update file hashes to include translation file content
const i18nHash = Object.values(i18n.locales).reduce(
(data, locale) => data + (locale.integrity || ''),
'',
);
if (!config.plugins) {
config.plugins = [];
}
config.plugins.push({
apply(compiler) {
compiler.hooks.compilation.tap('build-angular', compilation => {
// Webpack typings do not contain template hashForChunk hook
// tslint:disable-next-line: no-any
(compilation.mainTemplate.hooks as any).hashForChunk.tap(
'build-angular',
(hash: { update(data: string): void }) => {
hash.update('$localize' + i18nHash);
},
);
// Webpack typings do not contain hooks property
// tslint:disable-next-line: no-any
(compilation.chunkTemplate as any).hooks.hashForChunk.tap(
'build-angular',
(hash: { update(data: string): void }) => {
hash.update('$localize' + i18nHash);
},
);
});
},
});
}

return { ...result, i18n };
Expand Down
67 changes: 67 additions & 0 deletions tests/legacy-cli/e2e/tests/i18n/ivy-localize-hashes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* @license
* Copyright Google Inc. 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 * as fs from 'fs';
import { appendToFile } from '../../utils/fs';
import { ng } from '../../utils/process';
import { langTranslations, setupI18nConfig } from './legacy';

const OUTPUT_RE = /^(?<name>(?:main|vendor|\d+)\-(?:es2015|es5))\.(?<hash>[a-z0-9]+)\.js$/i;

export default async function() {
// Setup i18n tests and config.
await setupI18nConfig(true);

// Build each locale and record output file hashes
const hashes = new Map<string, string>();
await ng('build', '--output-hashing=all');
for (const { lang, outputPath } of langTranslations) {
for (const entry of fs.readdirSync(outputPath)) {
const match = entry.match(OUTPUT_RE);
if (!match) {
continue;
}

hashes.set(`${lang}/${match.groups.name}`, match.groups.hash);
}
}

// Ensure hashes for output files were recorded
if (hashes.size === 0) {
throw new Error('No output entries found.');
}

// Alter content of a used translation file
await appendToFile('src/locale/messages.fr.xlf', '\n');

// Build each locale and ensure hashes are different
await ng('build', '--output-hashing=all');
for (const { lang, outputPath } of langTranslations) {
for (const entry of fs.readdirSync(outputPath)) {
const match = entry.match(OUTPUT_RE);
if (!match) {
continue;
}

const id = `${lang}/${match.groups.name}`;
const hash = hashes.get(id);
if (!hash) {
throw new Error('Unexpected output entry: ' + id);
}
if (hash === match.groups.hash) {
throw new Error('Hash value did not change for entry: ' + id);
}

hashes.delete(id);
}
}

// Check for missing entries in second build
if (hashes.size > 0) {
throw new Error('Missing output entries: ' + JSON.stringify(Array.from(hashes.values())));
}
}

0 comments on commit 57796cf

Please sign in to comment.