From 301a8b502ed4a4e0881460f422d651493c539a19 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Thu, 13 Oct 2022 14:09:57 +0000 Subject: [PATCH] fix(localize): update ng add schematic to support Angular CLI version 15 Prior to this, the `@angular/localize/init` was added as a polyfill which caused the `@angular/localize` types not to be included in the TypeScript program which caused errors such as the below: ``` Error: src/app/app.component.ts:9:11 - error TS2304: Cannot find name '$localize'. ``` With the recent changes in the CLI (https://github.com/angular/angular-cli/pull/24032), adding `@angular/localize/init` as polyfil or in the `main.server.ts` is no longer necessary. Instead we add this as a TypeScript type. When users are running in JIT mode, we add `@angular/localize/init` as an additional entrypoint. This change also exposes the `$localize` method as a global when importing `@angular/localize`. Closes #47677 --- goldens/public-api/localize/init/index.md | 4 +- .../ng-add-localize/src/app/app.component.ts | 4 +- packages/localize/BUILD.bazel | 12 +- packages/localize/init/index.ts | 97 ------- packages/localize/localize.ts | 98 +++++++ .../localize/schematics/ng-add/BUILD.bazel | 1 + packages/localize/schematics/ng-add/README.md | 6 +- packages/localize/schematics/ng-add/index.ts | 117 ++++---- .../localize/schematics/ng-add/index_spec.ts | 269 ++++++++---------- 9 files changed, 274 insertions(+), 334 deletions(-) diff --git a/goldens/public-api/localize/init/index.md b/goldens/public-api/localize/init/index.md index b9bbfd73c8c3..bd1134a904d1 100644 --- a/goldens/public-api/localize/init/index.md +++ b/goldens/public-api/localize/init/index.md @@ -4,11 +4,11 @@ ```ts -import { ɵ$localize as $localize_2 } from '@angular/localize'; +import { ɵ$localize as $localize } from '@angular/localize'; import { ɵLocalizeFn as LocalizeFn } from '@angular/localize'; import { ɵTranslateFn as TranslateFn } from '@angular/localize'; -export { $localize_2 as $localize } +export { $localize } export { LocalizeFn } diff --git a/integration/ng-add-localize/src/app/app.component.ts b/integration/ng-add-localize/src/app/app.component.ts index c0e74a12fb9c..4f7c6300210c 100644 --- a/integration/ng-add-localize/src/app/app.component.ts +++ b/integration/ng-add-localize/src/app/app.component.ts @@ -23,10 +23,10 @@ import { Component } from '@angular/core';

Angular blog

- + `, styles: [] }) export class AppComponent { - title = 'ng-add-localize'; + title = $localize`ng-add-localize`; } diff --git a/packages/localize/BUILD.bazel b/packages/localize/BUILD.bazel index b326bb1f9825..3667b8dc539f 100644 --- a/packages/localize/BUILD.bazel +++ b/packages/localize/BUILD.bazel @@ -1,4 +1,4 @@ -load("//tools:defaults.bzl", "api_golden_test_npm_package", "ng_package", "ts_library") +load("//tools:defaults.bzl", "api_golden_test_npm_package", "extract_types", "ng_package", "ts_library") package(default_visibility = ["//visibility:public"]) @@ -17,20 +17,26 @@ ts_library( ], ) +extract_types( + name = "api_type_definitions", + deps = [":localize"], +) + ng_package( name = "npm_package", srcs = [ "package.json", + ":api_type_definitions", ], nested_packages = [ "//packages/localize/schematics:npm_package", "//packages/localize/tools:npm_package", ], skip_type_bundling = [ - # For the "init" entry-point we disable type bundling because API extractor + # For the primary entry-point we disable type bundling because API extractor # does not properly handle module augumentation. # TODO: Remove once https://github.com/microsoft/rushstack/issues/2090 is solved. - "init", + "", ], tags = [ "release-with-framework", diff --git a/packages/localize/init/index.ts b/packages/localize/init/index.ts index 1ae83945308b..06ba218d589f 100644 --- a/packages/localize/init/index.ts +++ b/packages/localize/init/index.ts @@ -11,100 +11,3 @@ export {$localize, LocalizeFn, TranslateFn}; // Attach $localize to the global context, as a side-effect of this module. _global.$localize = $localize; - -// `declare global` allows us to escape the current module and place types on the global namespace -declare global { - /** - * Tag a template literal string for localization. - * - * For example: - * - * ```ts - * $localize `some string to localize` - * ``` - * - * **Providing meaning, description and id** - * - * You can optionally specify one or more of `meaning`, `description` and `id` for a localized - * string by pre-pending it with a colon delimited block of the form: - * - * ```ts - * $localize`:meaning|description@@id:source message text`; - * - * $localize`:meaning|:source message text`; - * $localize`:description:source message text`; - * $localize`:@@id:source message text`; - * ``` - * - * This format is the same as that used for `i18n` markers in Angular templates. See the - * [Angular i18n guide](guide/i18n-common-prepare#mark-text-in-component-template). - * - * **Naming placeholders** - * - * If the template literal string contains expressions, then the expressions will be automatically - * associated with placeholder names for you. - * - * For example: - * - * ```ts - * $localize `Hi ${name}! There are ${items.length} items.`; - * ``` - * - * will generate a message-source of `Hi {$PH}! There are {$PH_1} items`. - * - * The recommended practice is to name the placeholder associated with each expression though. - * - * Do this by providing the placeholder name wrapped in `:` characters directly after the - * expression. These placeholder names are stripped out of the rendered localized string. - * - * For example, to name the `items.length` expression placeholder `itemCount` you write: - * - * ```ts - * $localize `There are ${items.length}:itemCount: items`; - * ``` - * - * **Escaping colon markers** - * - * If you need to use a `:` character directly at the start of a tagged string that has no - * metadata block, or directly after a substitution expression that has no name you must escape - * the `:` by preceding it with a backslash: - * - * For example: - * - * ```ts - * // message has a metadata block so no need to escape colon - * $localize `:some description::this message starts with a colon (:)`; - * // no metadata block so the colon must be escaped - * $localize `\:this message starts with a colon (:)`; - * ``` - * - * ```ts - * // named substitution so no need to escape colon - * $localize `${label}:label:: ${}` - * // anonymous substitution so colon must be escaped - * $localize `${label}\: ${}` - * ``` - * - * **Processing localized strings:** - * - * There are three scenarios: - * - * * **compile-time inlining**: the `$localize` tag is transformed at compile time by a - * transpiler, removing the tag and replacing the template literal string with a translated - * literal string from a collection of translations provided to the transpilation tool. - * - * * **run-time evaluation**: the `$localize` tag is a run-time function that replaces and - * reorders the parts (static strings and expressions) of the template literal string with strings - * from a collection of translations loaded at run-time. - * - * * **pass-through evaluation**: the `$localize` tag is a run-time function that simply evaluates - * the original template literal string without applying any translations to the parts. This - * version is used during development or where there is no need to translate the localized - * template literals. - * - * @param messageParts a collection of the static parts of the template string. - * @param expressions a collection of the values of each placeholder in the template string. - * @returns the translated string, with the `messageParts` and `expressions` interleaved together. - */ - const $localize: LocalizeFn; -} diff --git a/packages/localize/localize.ts b/packages/localize/localize.ts index 90ae8da520f7..ad2b7b1a8b0e 100644 --- a/packages/localize/localize.ts +++ b/packages/localize/localize.ts @@ -7,9 +7,107 @@ */ // This file contains the public API of the `@angular/localize` entry-point +import {LocalizeFn} from './src/localize'; export {clearTranslations, loadTranslations} from './src/translate'; export {MessageId, TargetMessage} from './src/utils'; // Exports that are not part of the public API export * from './private'; + +// `declare global` allows us to escape the current module and place types on the global namespace +declare global { + /** + * Tag a template literal string for localization. + * + * For example: + * + * ```ts + * $localize `some string to localize` + * ``` + * + * **Providing meaning, description and id** + * + * You can optionally specify one or more of `meaning`, `description` and `id` for a localized + * string by pre-pending it with a colon delimited block of the form: + * + * ```ts + * $localize`:meaning|description@@id:source message text`; + * + * $localize`:meaning|:source message text`; + * $localize`:description:source message text`; + * $localize`:@@id:source message text`; + * ``` + * + * This format is the same as that used for `i18n` markers in Angular templates. See the + * [Angular i18n guide](guide/i18n-common-prepare#mark-text-in-component-template). + * + * **Naming placeholders** + * + * If the template literal string contains expressions, then the expressions will be automatically + * associated with placeholder names for you. + * + * For example: + * + * ```ts + * $localize `Hi ${name}! There are ${items.length} items.`; + * ``` + * + * will generate a message-source of `Hi {$PH}! There are {$PH_1} items`. + * + * The recommended practice is to name the placeholder associated with each expression though. + * + * Do this by providing the placeholder name wrapped in `:` characters directly after the + * expression. These placeholder names are stripped out of the rendered localized string. + * + * For example, to name the `items.length` expression placeholder `itemCount` you write: + * + * ```ts + * $localize `There are ${items.length}:itemCount: items`; + * ``` + * + * **Escaping colon markers** + * + * If you need to use a `:` character directly at the start of a tagged string that has no + * metadata block, or directly after a substitution expression that has no name you must escape + * the `:` by preceding it with a backslash: + * + * For example: + * + * ```ts + * // message has a metadata block so no need to escape colon + * $localize `:some description::this message starts with a colon (:)`; + * // no metadata block so the colon must be escaped + * $localize `\:this message starts with a colon (:)`; + * ``` + * + * ```ts + * // named substitution so no need to escape colon + * $localize `${label}:label:: ${}` + * // anonymous substitution so colon must be escaped + * $localize `${label}\: ${}` + * ``` + * + * **Processing localized strings:** + * + * There are three scenarios: + * + * * **compile-time inlining**: the `$localize` tag is transformed at compile time by a + * transpiler, removing the tag and replacing the template literal string with a translated + * literal string from a collection of translations provided to the transpilation tool. + * + * * **run-time evaluation**: the `$localize` tag is a run-time function that replaces and + * reorders the parts (static strings and expressions) of the template literal string with strings + * from a collection of translations loaded at run-time. + * + * * **pass-through evaluation**: the `$localize` tag is a run-time function that simply evaluates + * the original template literal string without applying any translations to the parts. This + * version is used during development or where there is no need to translate the localized + * template literals. + * + * @param messageParts a collection of the static parts of the template string. + * @param expressions a collection of the values of each placeholder in the template string. + * @returns the translated string, with the `messageParts` and `expressions` interleaved together. + */ + const $localize: LocalizeFn; +} diff --git a/packages/localize/schematics/ng-add/BUILD.bazel b/packages/localize/schematics/ng-add/BUILD.bazel index 17f2f4098855..e8e976ba9bca 100644 --- a/packages/localize/schematics/ng-add/BUILD.bazel +++ b/packages/localize/schematics/ng-add/BUILD.bazel @@ -37,6 +37,7 @@ ts_library( deps = [ ":ng-add", "@npm//@angular-devkit/schematics", + "@npm//typescript", ], ) diff --git a/packages/localize/schematics/ng-add/README.md b/packages/localize/schematics/ng-add/README.md index 394ff4ef8a59..026f88343f69 100644 --- a/packages/localize/schematics/ng-add/README.md +++ b/packages/localize/schematics/ng-add/README.md @@ -2,9 +2,9 @@ This schematic will be executed when an Angular CLI user runs `ng add @angular/localize`. -It will search their `angular.json` file, and find polyfills and main files for server builders. -Then it will add the `@angular/localize/init` polyfill that `@angular/localize` needs to work. +It will search their `angular.json` file, and add `types: ["@angular/localize"]` in the TypeScript +configuration files of the project. If the user specifies that they want to use `$localize` at runtime then the dependency will be added to the `depdendencies` section of `package.json` rather than in the `devDependencies` which -is the default. \ No newline at end of file +is the default. diff --git a/packages/localize/schematics/ng-add/index.ts b/packages/localize/schematics/ng-add/index.ts index bbc6a49260ce..462166d57c9f 100644 --- a/packages/localize/schematics/ng-add/index.ts +++ b/packages/localize/schematics/ng-add/index.ts @@ -5,105 +5,85 @@ * 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 * - * @fileoverview Schematics for ng-new project that builds with Bazel. + * @fileoverview Schematics for `ng add @angular/localize` schematic. */ -import {tags} from '@angular-devkit/core'; import {chain, noop, Rule, SchematicContext, SchematicsException, Tree,} from '@angular-devkit/schematics'; import {NodePackageInstallTask} from '@angular-devkit/schematics/tasks'; import {addPackageJsonDependency, NodeDependencyType, removePackageJsonDependency,} from '@schematics/angular/utility/dependencies'; -import {allTargetOptions, getWorkspace, updateWorkspace,} from '@schematics/angular/utility/workspace'; +import {JSONFile, JSONPath} from '@schematics/angular/utility/json-file'; +import {getWorkspace} from '@schematics/angular/utility/workspace'; import {Builders} from '@schematics/angular/utility/workspace-models'; import {Schema} from './schema'; -export const localizePolyfill = `@angular/localize/init`; +const localizeType = `@angular/localize`; -function prependToMainFiles(projectName: string): Rule { +function addTypeScriptConfigTypes(projectName: string): Rule { return async (host: Tree) => { const workspace = await getWorkspace(host); const project = workspace.projects.get(projectName); if (!project) { - throw new SchematicsException(`Invalid project name (${projectName})`); + throw new SchematicsException(`Invalid project name '${projectName}'.`); } - const fileList = new Set(); + // We add the root workspace tsconfig for better IDE support. + const tsConfigFiles = new Set(['tsconfig.json']); for (const target of project.targets.values()) { - if (target.builder !== Builders.Server) { - continue; - } - - for (const [, options] of allTargetOptions(target)) { - const value = options['main']; - if (typeof value === 'string') { - fileList.add(value); - } + switch (target.builder) { + case Builders.Karma: + case Builders.Server: + case Builders.Browser: + const value = target.options?.['tsConfig']; + if (typeof value === 'string') { + tsConfigFiles.add(value); + } + + break; } } - for (const path of fileList) { - const content = host.readText(path); - if (content.includes(localizePolyfill)) { - // If the file already contains the polyfill (or variations), ignore it too. + const typesJsonPath: JSONPath = ['compilerOptions', 'types']; + for (const path of tsConfigFiles) { + if (!host.exists(path)) { continue; } - // Add string at the start of the file. - const recorder = host.beginUpdate(path); - - const localizeStr = - tags.stripIndents`/*************************************************************************************************** - * Load \`$localize\` onto the global scope - used if i18n tags appear in Angular templates. - */ - import '${localizePolyfill}'; - `; - recorder.insertLeft(0, localizeStr); - host.commitUpdate(recorder); - } - }; -} - -function addToPolyfillsOption(projectName: string): Rule { - return updateWorkspace((workspace) => { - const project = workspace.projects.get(projectName); - if (!project) { - throw new SchematicsException(`Invalid project name (${projectName})`); - } + const json = new JSONFile(host, path); + const types = json.get(typesJsonPath) ?? []; + if (!Array.isArray(types)) { + throw new SchematicsException(`TypeScript configuration file '${ + path}' has an invalid 'types' property. It must be an array.`); + } - for (const target of project.targets.values()) { - if (target.builder !== Builders.Browser && target.builder !== Builders.Karma) { + const hasLocalizeType = + types.some((t) => t === localizeType || t === '@angular/localize/init'); + if (hasLocalizeType) { + // Skip has already localize type. continue; } - target.options ??= {}; - target.options['polyfills'] ??= [localizePolyfill]; - - for (const [, options] of allTargetOptions(target)) { - // Convert polyfills option to array. - const polyfillsValue = typeof options['polyfills'] === 'string' ? [options['polyfills']] : - options['polyfills']; - if (Array.isArray(polyfillsValue) && !polyfillsValue.includes(localizePolyfill)) { - options['polyfills'] = [...polyfillsValue, localizePolyfill]; - } - } + json.modify(typesJsonPath, [...types, localizeType]); } - }); + }; } function moveToDependencies(host: Tree, context: SchematicContext): void { - if (host.exists('package.json')) { - // Remove the previous dependency and add in a new one under the desired type. - removePackageJsonDependency(host, '@angular/localize'); - addPackageJsonDependency(host, { - name: '@angular/localize', - type: NodeDependencyType.Default, - version: `~0.0.0-PLACEHOLDER`, - }); - - // Add a task to run the package manager. This is necessary because we updated - // "package.json" and we want lock files to reflect this. - context.addTask(new NodePackageInstallTask()); + if (!host.exists('package.json')) { + return; } + + // Remove the previous dependency and add in a new one under the desired type. + removePackageJsonDependency(host, '@angular/localize'); + addPackageJsonDependency(host, { + name: '@angular/localize', + type: NodeDependencyType.Default, + version: `~0.0.0-PLACEHOLDER`, + }); + + // Add a task to run the package manager. This is necessary because we updated + // "package.json" and we want lock files to reflect this. + context.addTask(new NodePackageInstallTask()); } export default function(options: Schema): Rule { @@ -117,8 +97,7 @@ export default function(options: Schema): Rule { } return chain([ - prependToMainFiles(projectName), - addToPolyfillsOption(projectName), + addTypeScriptConfigTypes(projectName), // If `$localize` will be used at runtime then must install `@angular/localize` // into `dependencies`, rather than the default of `devDependencies`. options.useAtRuntime ? moveToDependencies : noop(), diff --git a/packages/localize/schematics/ng-add/index_spec.ts b/packages/localize/schematics/ng-add/index_spec.ts index 6d967f6fb851..3ec1ee9c734d 100644 --- a/packages/localize/schematics/ng-add/index_spec.ts +++ b/packages/localize/schematics/ng-add/index_spec.ts @@ -6,26 +6,24 @@ * found in the LICENSE file at https://angular.io/license */ -import {HostTree} from '@angular-devkit/schematics'; -import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing'; +import {EmptyTree, Tree} from '@angular-devkit/schematics'; +import {SchematicTestRunner} from '@angular-devkit/schematics/testing'; +import ts from 'typescript'; -import {localizePolyfill} from './index'; +interface TsConfig { + compilerOptions?: ts.CompilerOptions; +} describe('ng-add schematic', () => { + const localizeType = '@angular/localize'; const defaultOptions = {project: 'demo'}; - let host: UnitTestTree; - let schematicRunner: SchematicTestRunner; - const polyfillsContent = ''; - const mainServerContent = `import { enableProdMode } from '@angular/core'; -import { environment } from './environments/environment'; -if (environment.production) { - enableProdMode(); -} -export { AppServerModule } from './app/app.server.module'; -export { renderModule, renderModuleFactory } from '@angular/platform-server';`; + const schematicRunner = + new SchematicTestRunner('@angular/localize', require.resolve('../collection.json')); + let host: Tree; beforeEach(() => { - host = new UnitTestTree(new HostTree()); + host = new EmptyTree(); + host.create('package.json', JSON.stringify({ 'devDependencies': { // The default (according to `ng-add` in its package.json) is for `@angular/localize` to be @@ -33,14 +31,7 @@ export { renderModule, renderModuleFactory } from '@angular/platform-server';`; '@angular/localize': '~0.0.0-PLACEHOLDER', }, })); - host.create('src/polyfills.ts', polyfillsContent); - host.create('src/another-polyfills.ts', polyfillsContent); - host.create('src/unrelated-polyfills.ts', polyfillsContent); - host.create('src/another-unrelated-polyfills.ts', polyfillsContent); - host.create('src/main.server.ts', mainServerContent); - host.create('src/another-main.server.ts', mainServerContent); - host.create('src/unrelated-main.server.ts', mainServerContent); - host.create('src/another-unrelated-main.server.ts', mainServerContent); + host.create('angular.json', JSON.stringify({ version: 1, projects: { @@ -50,178 +41,140 @@ export { renderModule, renderModuleFactory } from '@angular/platform-server';`; build: { builder: '@angular-devkit/build-angular:browser', options: { - polyfills: 'src/polyfills.ts', - }, - configurations: { - production: { - polyfills: 'src/another-polyfills.ts', - }, + tsConfig: './tsconfig.app.json', }, }, test: { builder: '@angular-devkit/build-angular:karma', options: { - polyfills: ['src/polyfills.ts'], - }, - configurations: { - production: { - polyfills: ['src/another-polyfills.ts'], - }, - dev: { - polyfills: [localizePolyfill], - }, + tsConfig: './tsconfig.spec.json', }, }, - 'another-test': { - builder: '@angular-devkit/build-angular:karma', - options: {}, - }, server: { builder: '@angular-devkit/build-angular:server', options: { - main: 'src/main.server.ts', - }, - configurations: { - production: { - main: 'src/another-main.server.ts', - }, - }, - }, - 'another-server': { - builder: '@angular-devkit/build-angular:server', - options: { - main: 'src/main.server.ts', - }, - configurations: { - production: { - main: 'src/another-main.server.ts', - }, + tsConfig: './tsconfig.server.json', }, }, - 'not-browser-or-server': { - builder: '@angular-devkit/build-angular:something-else', + unknown: { + builder: '@custom-builder/build-angular:unknown', options: { - polyfills: 'src/unrelated-polyfills.ts', - main: 'src/unrelated-main.server.ts', - }, - configurations: { - production: { - polyfills: ['src/other-unrelated-polyfills.ts'], - main: 'src/another-unrelated-main.server.ts', - }, + tsConfig: './tsconfig.unknown.json', }, }, }, }, }, })); - schematicRunner = - new SchematicTestRunner('@angular/localize', require.resolve('../collection.json')); }); - it(`should add localize polyfill to polyfill option when it's a string`, async () => { - host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); - const demoProjectBuild = - (host.readJson('angular.json') as any)['projects']['demo']['architect']['build']; - expect(demoProjectBuild['options']['polyfills']).toEqual([ - 'src/polyfills.ts', - localizePolyfill, - ]); - expect(demoProjectBuild['configurations']['production']['polyfills']).toEqual([ - 'src/another-polyfills.ts', - localizePolyfill, - ]); - }); + it(`should add '@angular/localize' in 'types' in the root level 'tsconfig.json'`, async () => { + host.create('tsconfig.json', JSON.stringify({ + compilerOptions: { + types: ['node'], + }, + })); - it(`should add localize polyfill to polyfill option when it's a array`, async () => { host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); - const demoProjectAnotherTest = - (host.readJson('angular.json') as any)['projects']['demo']['architect']['test']; - expect(demoProjectAnotherTest['options']['polyfills']).toEqual([ - 'src/polyfills.ts', - localizePolyfill, - ]); - expect(demoProjectAnotherTest['configurations']['production']['polyfills']).toEqual([ - 'src/another-polyfills.ts', - localizePolyfill, - ]); + const {compilerOptions} = host.readJson('tsconfig.json') as TsConfig; + const types = compilerOptions?.types; + expect(types).toContain(localizeType); + expect(types).toHaveSize(2); }); - it(`should not add localize polyfill to polyfill option when it's already set`, async () => { - host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); - const demoProjectAnotherTest = - (host.readJson('angular.json') as any)['projects']['demo']['architect']['test']; - expect(demoProjectAnotherTest['configurations']['dev']['polyfills']).toEqual([ - localizePolyfill, - ]); - }); - it(`should add localize polyfill when polyfills options is not set`, async () => { - host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); - const demoProjectAnotherTest = - (host.readJson('angular.json') as any)['projects']['demo']['architect']['another-test']; - expect(demoProjectAnotherTest['options']['polyfills']).toEqual([ - localizePolyfill, - ]); - }); + it(`should not add '@angular/localize' in 'types' tsconfig when '@angular/localize/init' is present`, + async () => { + host.create('tsconfig.json', JSON.stringify({ + compilerOptions: { + types: ['node', '@angular/localize/init'], + }, + })); - it('should add localize polyfill to server main files', async () => { - host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); - expect(host.readContent('/src/main.server.ts')).toContain(localizePolyfill); - expect(host.readContent('/src/another-main.server.ts')).toContain(localizePolyfill); - }); + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); + const {compilerOptions} = host.readJson('tsconfig.json') as TsConfig; + const types = compilerOptions?.types; + expect(types).not.toContain(localizeType); + expect(types).toHaveSize(2); + }); - it('should not add localize polyfill to files referenced in other targets files', async () => { - host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); - expect(host.readContent('/src/unrelated-polyfills.ts')).not.toContain(localizePolyfill); - expect(host.readContent('/src/another-unrelated-polyfills.ts')).not.toContain(localizePolyfill); - expect(host.readContent('/src/unrelated-main.server.ts')).not.toContain(localizePolyfill); - expect(host.readContent('/src/another-unrelated-main.server.ts')) - .not.toContain(localizePolyfill); - - const demoProjectBuild = - (host.readJson('angular.json') as - any)['projects']['demo']['architect']['not-browser-or-server']; - expect(demoProjectBuild['options']['polyfills']).toBe('src/unrelated-polyfills.ts'); - expect(demoProjectBuild['configurations']['production']['polyfills']).toEqual([ - 'src/other-unrelated-polyfills.ts', - ]); - }); - it('should not break when there are no polyfills', async () => { - host.overwrite('angular.json', JSON.stringify({ - version: 1, - projects: { - 'demo': { - root: '', - architect: {}, - }, - }, - defaultProject: 'demo', - })); - await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); - }); + it(`should not add '@angular/localize' in 'types' tsconfigs referenced in non official builders`, + async () => { + const tsConfig = JSON.stringify({ + compilerOptions: { + types: ['node'], + }, + }); - it('should add package to `devDependencies` by default', async () => { - host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); - const packageJsonText = host.readContent('/package.json'); - const packageJsonObj = JSON.parse(packageJsonText) as { - devDependencies: {[key: string]: string}; - dependencies: {[key: string]: string}; - }; - expect(packageJsonObj.devDependencies?.['@angular/localize']).toBe('~0.0.0-PLACEHOLDER'); - expect(packageJsonObj.dependencies?.['@angular/localize']).toBeUndefined(); - }); + host.create('tsconfig.unknown.json', tsConfig); + + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); + const {compilerOptions} = host.readJson('tsconfig.unknown.json') as TsConfig; + const types = compilerOptions?.types; + expect(types).not.toContain('@angular/localize'); + expect(types).toHaveSize(1); + }); + + it(`should add '@angular/localize' in 'types' tsconfigs referenced in browser builder`, + async () => { + const tsConfig = JSON.stringify({ + compilerOptions: { + types: ['node'], + }, + }); + + host.create('tsconfig.app.json', tsConfig); + + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); + const {compilerOptions} = host.readJson('tsconfig.app.json') as TsConfig; + const types = compilerOptions?.types; + expect(types).toContain('@angular/localize'); + expect(types).toHaveSize(2); + }); + + + it(`should add '@angular/localize' in 'types' tsconfigs referenced in karma builder`, + async () => { + const tsConfig = JSON.stringify({ + compilerOptions: { + types: ['node'], + }, + }); + + host.create('tsconfig.spec.json', tsConfig); + + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); + const {compilerOptions} = host.readJson('tsconfig.spec.json') as TsConfig; + const types = compilerOptions?.types; + expect(types).toContain('@angular/localize'); + expect(types).toHaveSize(2); + }); + + it(`should add '@angular/localize' in 'types' tsconfigs referenced in server builder`, + async () => { + const tsConfig = JSON.stringify({ + compilerOptions: {}, + }); + + host.create('tsconfig.server.json', tsConfig); + + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); + const {compilerOptions} = host.readJson('tsconfig.server.json') as TsConfig; + const types = compilerOptions?.types; + expect(types).toContain('@angular/localize'); + expect(types).toHaveSize(1); + }); it('should add package to `dependencies` if `useAtRuntime` is `true`', async () => { host = await schematicRunner .runSchematicAsync('ng-add', {...defaultOptions, useAtRuntime: true}, host) .toPromise(); - const packageJsonText = host.readContent('/package.json'); - const packageJsonObj = JSON.parse(packageJsonText) as { + + const {devDependencies, dependencies} = host.readJson('/package.json') as { devDependencies: {[key: string]: string}; dependencies: {[key: string]: string}; }; - expect(packageJsonObj.dependencies?.['@angular/localize']).toBe('~0.0.0-PLACEHOLDER'); - expect(packageJsonObj.devDependencies?.['@angular/localize']).toBeUndefined(); + expect(dependencies?.['@angular/localize']).toBe('~0.0.0-PLACEHOLDER'); + expect(devDependencies?.['@angular/localize']).toBeUndefined(); }); });