From e41cbfb585d1e790e5ee8a8b9dc90be0986a0a00 Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Fri, 20 Sep 2019 16:05:45 +0100 Subject: [PATCH] feat(ivy): support ng-add in localize package (#32791) PR Close #32791 --- karma-js.conf.js | 1 + packages/localize/BUILD.bazel | 3 + packages/localize/schematics/BUILD.bazel | 21 +++ packages/localize/schematics/collection.json | 10 ++ .../localize/schematics/ng-add/BUILD.bazel | 51 ++++++ packages/localize/schematics/ng-add/README.md | 6 + packages/localize/schematics/ng-add/index.ts | 102 +++++++++++ .../localize/schematics/ng-add/index_spec.ts | 168 ++++++++++++++++++ .../localize/schematics/ng-add/schema.d.ts | 14 ++ .../localize/schematics/ng-add/schema.json | 17 ++ .../schematics/ng-add/tsconfig-build.json | 21 +++ 11 files changed, 414 insertions(+) create mode 100644 packages/localize/schematics/BUILD.bazel create mode 100644 packages/localize/schematics/collection.json create mode 100644 packages/localize/schematics/ng-add/BUILD.bazel create mode 100644 packages/localize/schematics/ng-add/README.md create mode 100644 packages/localize/schematics/ng-add/index.ts create mode 100644 packages/localize/schematics/ng-add/index_spec.ts create mode 100644 packages/localize/schematics/ng-add/schema.d.ts create mode 100644 packages/localize/schematics/ng-add/schema.json create mode 100644 packages/localize/schematics/ng-add/tsconfig-build.json diff --git a/karma-js.conf.js b/karma-js.conf.js index 7eacd89f86..5168d734cb 100644 --- a/karma-js.conf.js +++ b/karma-js.conf.js @@ -83,6 +83,7 @@ module.exports = function(config) { 'dist/all/@angular/examples/**/e2e_test/*', 'dist/all/@angular/language-service/**', 'dist/all/@angular/localize/**/test/**', + 'dist/all/@angular/localize/schematics/**', 'dist/all/@angular/router/**/test/**', 'dist/all/@angular/platform-browser/testing/e2e_util.js', 'dist/all/angular1_router.js', diff --git a/packages/localize/BUILD.bazel b/packages/localize/BUILD.bazel index 7c98d93264..a5113a202e 100644 --- a/packages/localize/BUILD.bazel +++ b/packages/localize/BUILD.bazel @@ -25,6 +25,9 @@ ng_package( "//packages/localize/init:package.json", ], entry_point = ":index.ts", + packages = [ + "//packages/localize/schematics:npm_package", + ], tags = [ "release-with-framework", ], diff --git a/packages/localize/schematics/BUILD.bazel b/packages/localize/schematics/BUILD.bazel new file mode 100644 index 0000000000..2f41c3f8a3 --- /dev/null +++ b/packages/localize/schematics/BUILD.bazel @@ -0,0 +1,21 @@ +load("//tools:defaults.bzl", "npm_package") + +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "package_assets", + srcs = [ + "collection.json", + ], + visibility = ["//packages/localize:__subpackages__"], +) + +npm_package( + name = "npm_package", + srcs = [ + "collection.json", + ], + deps = [ + "//packages/localize/schematics/ng-add", + ], +) diff --git a/packages/localize/schematics/collection.json b/packages/localize/schematics/collection.json new file mode 100644 index 0000000000..d75f26c2f5 --- /dev/null +++ b/packages/localize/schematics/collection.json @@ -0,0 +1,10 @@ +{ + "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json", + "schematics": { + "ng-add": { + "description": "Add @angular/localize polyfill to a project.", + "factory": "./ng-add", + "schema": "ng-add/schema.json" + } + } +} diff --git a/packages/localize/schematics/ng-add/BUILD.bazel b/packages/localize/schematics/ng-add/BUILD.bazel new file mode 100644 index 0000000000..20bce8ad46 --- /dev/null +++ b/packages/localize/schematics/ng-add/BUILD.bazel @@ -0,0 +1,51 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "jasmine_node_test", "ts_library") +load("@npm_bazel_typescript//:index.bzl", "ts_config") + +ts_config( + name = "tsconfig", + src = "tsconfig-build.json", + deps = ["//packages:tsconfig-build.json"], +) + +ts_library( + name = "ng-add", + srcs = [ + "index.ts", + "schema.d.ts", + ], + data = glob(["files/**/*"]) + [ + "schema.json", + ], + tsconfig = ":tsconfig", + deps = [ + "@npm//@angular-devkit/core", + "@npm//@angular-devkit/schematics", + "@npm//@schematics/angular", + ], +) + +ts_library( + name = "test_lib", + testonly = True, + srcs = [ + "index_spec.ts", + ], + data = [ + "//packages/localize/schematics:package_assets", + ], + deps = [ + ":ng-add", + "@npm//@angular-devkit/schematics", + ], +) + +jasmine_node_test( + name = "test", + bootstrap = ["angular/tools/testing/init_node_spec.js"], + deps = [ + ":test_lib", + "//tools/testing:node", + ], +) diff --git a/packages/localize/schematics/ng-add/README.md b/packages/localize/schematics/ng-add/README.md new file mode 100644 index 0000000000..4c125ae34b --- /dev/null +++ b/packages/localize/schematics/ng-add/README.md @@ -0,0 +1,6 @@ +# @angular/localize schematic for `ng add` + +This schematic will be executed when a 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. \ No newline at end of file diff --git a/packages/localize/schematics/ng-add/index.ts b/packages/localize/schematics/ng-add/index.ts new file mode 100644 index 0000000000..903d3eddca --- /dev/null +++ b/packages/localize/schematics/ng-add/index.ts @@ -0,0 +1,102 @@ +/** + * @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 + * + * @fileoverview Schematics for ng-new project that builds with Bazel. + */ + +import {virtualFs} from '@angular-devkit/core'; +import {Rule, Tree, chain} from '@angular-devkit/schematics'; +import {getWorkspace} from '@schematics/angular/utility/config'; +import {getProjectTargets} from '@schematics/angular/utility/project-targets'; +import {validateProjectName} from '@schematics/angular/utility/validation'; +import {BrowserBuilderTarget, Builders, ServeBuilderTarget} from '@schematics/angular/utility/workspace-models'; + +import {Schema} from './schema'; + + +export const localizePolyfill = `import '@angular/localize/init';`; + +function getAllOptionValues( + host: Tree, projectName: string, builderName: string, optionName: string) { + const targets = getProjectTargets(host, projectName); + + // Find all targets of a specific build in a project. + const builderTargets: (BrowserBuilderTarget | ServeBuilderTarget)[] = + Object.values(targets).filter( + (target: BrowserBuilderTarget | ServeBuilderTarget) => target.builder === builderName); + + // Get all options contained in target configuration partials. + const configurationOptions = builderTargets.filter(t => t.configurations) + .map(t => Object.values(t.configurations !)) + .reduce((acc, cur) => acc.concat(...cur), []); + + // Now we have all option sets. We can use it to find all references to a given property. + const allOptions = [ + ...builderTargets.map(t => t.options), + ...configurationOptions, + ]; + + // Get all values for the option name and dedupe them. + // Deduping will only work for primitives, but the keys we want here are strings so it's ok. + const optionValues: T[] = + allOptions.filter(o => o[optionName]) + .map(o => o[optionName]) + .reduce((acc, cur) => !acc.includes(cur) ? acc.concat(cur) : acc, []); + + return optionValues; +} + + +function prendendToTargetOptionFile( + projectName: string, builderName: string, optionName: string, str: string) { + return (host: Tree) => { + // Get all known polyfills for browser builders on this project. + const optionValues = getAllOptionValues(host, projectName, builderName, optionName); + + optionValues.forEach(path => { + const data = host.read(path); + if (!data) { + // If the file doesn't exist, just ignore it. + return; + } + + const content = virtualFs.fileBufferToString(data); + if (content.includes(localizePolyfill) || + content.includes(localizePolyfill.replace(/'/g, '"'))) { + // If the file already contains the polyfill (or variations), ignore it too. + return; + } + + // Add string at the start of the file. + const recorder = host.beginUpdate(path); + recorder.insertLeft(0, str); + host.commitUpdate(recorder); + }); + }; +} + +export default function(options: Schema): Rule { + return (host: Tree) => { + options.name = options.name || getWorkspace(host).defaultProject; + if (!options.name) { + throw new Error('Please specify a project using "--name project-name"'); + } + validateProjectName(options.name); + + const localizeStr = + `/*************************************************************************************************** + * Load \`$localize\` onto the global scope - used if i18n tags appear in Angular templates. + */ +${localizePolyfill} +`; + + return chain([ + prendendToTargetOptionFile(options.name, Builders.Browser, 'polyfills', localizeStr), + prendendToTargetOptionFile(options.name, Builders.Server, 'main', localizeStr), + ]); + }; +} diff --git a/packages/localize/schematics/ng-add/index_spec.ts b/packages/localize/schematics/ng-add/index_spec.ts new file mode 100644 index 0000000000..68588f8f4e --- /dev/null +++ b/packages/localize/schematics/ng-add/index_spec.ts @@ -0,0 +1,168 @@ +/** + * @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 {HostTree} from '@angular-devkit/schematics'; +import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing'; + +import {localizePolyfill} from './index'; + + +describe('ng-add schematic', () => { + + const countInstances = (str: string, substr: string) => str.split(substr).length - 1; + const defaultOptions = {name: 'demo'}; + let host: UnitTestTree; + let schematicRunner: SchematicTestRunner; + // The real polyfills file is bigger than this, but for the test it shouldn't matter. + const polyfillsContent = + `/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js/dist/zone';`; + 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';`; + + beforeEach(() => { + host = new UnitTestTree(new HostTree()); + 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({ + projects: { + 'demo': { + architect: { + build: { + builder: '@angular-devkit/build-angular:browser', + options: { + polyfills: 'src/polyfills.ts', + }, + configurations: { + production: { + polyfills: 'src/another-polyfills.ts', + } + } + }, + 'another-build': { + builder: '@angular-devkit/build-angular:browser', + options: { + polyfills: 'src/polyfills.ts', + }, + configurations: { + production: { + polyfills: 'src/another-polyfills.ts', + } + } + }, + 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', + } + } + }, + 'not-browser-or-server': { + builder: '@angular-devkit/build-angular:something-else', + 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', + } + } + }, + }, + } + }, + defaultProject: 'demo', + })); + schematicRunner = + new SchematicTestRunner('@angular/localize', require.resolve('../collection.json')); + }); + + it('should add localize polyfill to polyfill files', async() => { + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); + expect(host.readContent('/src/polyfills.ts')).toContain(localizePolyfill); + expect(host.readContent('/src/another-polyfills.ts')).toContain(localizePolyfill); + }); + + 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); + }); + + it('should add localize polyfill at the start of file', async() => { + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); + const content = host.readContent('/src/polyfills.ts'); + expect(content.indexOf(localizePolyfill)).toBeLessThan(content.indexOf(polyfillsContent)); + }); + + 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); + }); + + it('should only add localize polyfill once if multiple builds reference it', async() => { + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); + const content = host.readContent('/src/polyfills.ts'); + expect(countInstances(content, localizePolyfill)).toBe(1); + }); + + it('should not add localize polyfill if it\'s already there', async() => { + const polyfillVariation = localizePolyfill.replace(/'/g, '"'); + host.overwrite('/src/polyfills.ts', `${localizePolyfill}\n${polyfillsContent}`); + host.overwrite('/src/another-polyfills.ts', `${polyfillVariation}\n${polyfillsContent}`); + host = await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); + expect(countInstances(host.readContent('/src/polyfills.ts'), localizePolyfill)).toBe(1); + expect(countInstances(host.readContent('/src/another-polyfills.ts'), localizePolyfill)).toBe(0); + }); + + it('should not break when there are no polyfills', async() => { + host.overwrite('angular.json', JSON.stringify({ + projects: { + 'demo': { + architect: {}, + } + }, + defaultProject: 'demo', + })); + await schematicRunner.runSchematicAsync('ng-add', defaultOptions, host).toPromise(); + }); +}); diff --git a/packages/localize/schematics/ng-add/schema.d.ts b/packages/localize/schematics/ng-add/schema.d.ts new file mode 100644 index 0000000000..24e9c54d32 --- /dev/null +++ b/packages/localize/schematics/ng-add/schema.d.ts @@ -0,0 +1,14 @@ +/** + * @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 + */ + +export interface Schema { + /** + * The name of the project. + */ + name?: string; +} diff --git a/packages/localize/schematics/ng-add/schema.json b/packages/localize/schematics/ng-add/schema.json new file mode 100644 index 0000000000..775cbfcb51 --- /dev/null +++ b/packages/localize/schematics/ng-add/schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "SchematicsAngularLocalizeNgAdd", + "title": "Angular Localize Ng Add Schema", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the project.", + "$default": { + "$source": "projectName" + } + } + }, + "required": [ + ] +} diff --git a/packages/localize/schematics/ng-add/tsconfig-build.json b/packages/localize/schematics/ng-add/tsconfig-build.json new file mode 100644 index 0000000000..1e587c427c --- /dev/null +++ b/packages/localize/schematics/ng-add/tsconfig-build.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig-build.json", + "compilerOptions": { + "module": "commonjs", + "stripInternal": false, + "target": "es2015", + "lib": [ + "es2015", + "es2017.object", + ], + }, + "bazelOptions": { + "suppressTsconfigOverrideWarnings": true, + }, + "exclude": [ + "index_spec.ts", + ], + "files": [ + "index.ts", + ] +}