diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8785a071..9aad7624 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -116,6 +116,7 @@ The scope must be one of the following: * ionic * nativescript * nest +* elements * helpers #### Subject diff --git a/src/app.electron/index.ts b/src/app.electron/index.ts index 6f892961..2a1908e2 100644 --- a/src/app.electron/index.ts +++ b/src/app.electron/index.ts @@ -13,12 +13,12 @@ import { noop } from '@angular-devkit/schematics'; import { Schema as ApplicationOptions } from './schema'; -import { prerun, getPrefix, getNpmScope, stringUtils, addRootDeps, updateAngularProjects, updateNxProjects, formatFiles, getJsonFromFile, updatePackageScripts, addPostinstallers, applyAppNamingConvention, getGroupByName, getAppName, missingNameArgument } from '../utils'; +import { prerun, getPrefix, getNpmScope, stringUtils, addRootDeps, updateAngularProjects, updateNxProjects, formatFiles, getJsonFromFile, updatePackageScripts, addPostinstallers, applyAppNamingConvention, getGroupByName, getAppName, missingArgument } from '../utils'; export default function (options: ApplicationOptions) { if (!options.name) { throw new SchematicsException( - missingNameArgument('Provide a name for your Electron app.', 'ng g app.electron sample') + missingArgument('name', 'Provide a name for your Electron app.', 'ng g app.electron sample') ); } if (!options.target) { diff --git a/src/app.ionic/index.ts b/src/app.ionic/index.ts index 5eae902a..9375de52 100644 --- a/src/app.ionic/index.ts +++ b/src/app.ionic/index.ts @@ -13,13 +13,13 @@ import { schematic, noop, } from '@angular-devkit/schematics'; -import { stringUtils, prerun, getNpmScope, getPrefix, addRootDeps, updatePackageScripts, updateAngularProjects, updateNxProjects, formatFiles, applyAppNamingConvention, getAppName, missingNameArgument } from '../utils'; +import { stringUtils, prerun, getNpmScope, getPrefix, addRootDeps, updatePackageScripts, updateAngularProjects, updateNxProjects, formatFiles, applyAppNamingConvention, getAppName, missingArgument } from '../utils'; import { Schema as ApplicationOptions } from './schema'; export default function (options: ApplicationOptions) { if (!options.name) { throw new SchematicsException( - missingNameArgument('Provide a name for your Ionic app.', 'ng g app.ionic sample') + missingArgument('name', 'Provide a name for your Ionic app.', 'ng g app.ionic sample') ); } diff --git a/src/app.nativescript/index.ts b/src/app.nativescript/index.ts index f57cfa98..a9c41f3a 100644 --- a/src/app.nativescript/index.ts +++ b/src/app.nativescript/index.ts @@ -13,13 +13,13 @@ import { schematic, noop, } from '@angular-devkit/schematics'; -import { stringUtils, prerun, getNpmScope, getPrefix, addRootDeps, updatePackageScripts, updateAngularProjects, updateNxProjects, applyAppNamingConvention, getGroupByName, getAppName, missingNameArgument } from '../utils'; +import { stringUtils, prerun, getNpmScope, getPrefix, addRootDeps, updatePackageScripts, updateAngularProjects, updateNxProjects, applyAppNamingConvention, getGroupByName, getAppName, missingArgument } from '../utils'; import { Schema as ApplicationOptions } from './schema'; export default function (options: ApplicationOptions) { if (!options.name) { throw new SchematicsException( - missingNameArgument('Provide a name for your NativeScript app.', 'ng g app.nativescript sample') + missingArgument('name', 'Provide a name for your NativeScript app.', 'ng g app.nativescript sample') ); } if (options.setupSandbox) { diff --git a/src/app.nest/index.ts b/src/app.nest/index.ts index 2937aa91..e0fe2114 100644 --- a/src/app.nest/index.ts +++ b/src/app.nest/index.ts @@ -28,14 +28,14 @@ import { prerun, applyAppNamingConvention, getAppName, - missingNameArgument, - updateJsonInTree + updateJsonInTree, + missingArgument } from "../utils"; export default function (options: ApplicationOptions) { if (!options.name) { throw new SchematicsException( - missingNameArgument('Provide a name for your Nest app.', 'ng g app.nest sample') + missingArgument('name', 'Provide a name for your Nest app.', 'ng g app.nest sample') ); } diff --git a/src/app.web/_routing_files/src/app/features/home/components/home.component.html b/src/app.web/_routing_files/src/app/features/home/components/home.component.html index 4f004bbb..e99f34ef 100644 --- a/src/app.web/_routing_files/src/app/features/home/components/home.component.html +++ b/src/app.web/_routing_files/src/app/features/home/components/home.component.html @@ -1,12 +1,5 @@
-
-

- Welcome to an Angular CLI app built with Nrwl Nx and xplat! -

- - + - -
+ <<%= prefix %>-header title="<%= name %>">-header>

Nx

diff --git a/src/app.web/index.ts b/src/app.web/index.ts index 5665e39b..a9b14916 100644 --- a/src/app.web/index.ts +++ b/src/app.web/index.ts @@ -24,14 +24,14 @@ import { applyAppNamingConvention, updateJsonFile, formatFiles, - missingNameArgument + missingArgument } from "../utils"; import { Schema as ApplicationOptions } from "./schema"; export default function(options: ApplicationOptions) { if (!options.name) { throw new SchematicsException( - missingNameArgument('Provide a name for your Web app.', 'ng g app my-app') + missingArgument('name', 'Provide a name for your Web app.', 'ng g app my-app') ); } // ensure sass is used @@ -173,32 +173,22 @@ platformBrowserDynamic() function appCmpHtml(name: string) { return `
-
-

- Welcome to ${name}! -

-

- An Angular CLI app built with Nrwl Nx and xplat. -

- - + - -
- -

Nx

- - An open source toolkit for enterprise Angular applications. Nx is designed to help you create and build enterprise grade - Angular applications. It provides an opinionated approach to application project structure and patterns. - -

Quick Start & Documentation

- - Watch a 5-minute video on how to get started with Nx. - -

{{'hello' | translate}}

-

Try things out

- - Learn more about xplat generators. -
`; + <${getPrefix()}-header title="${name}"> + +

Nx

+ + An open source toolkit for enterprise Angular applications. Nx is designed to help you create and build enterprise grade + Angular applications. It provides an opinionated approach to application project structure and patterns. + +

Quick Start & Documentation

+ + Watch a 5-minute video on how to get started with Nx. + +

{{'hello' | translate}}

+

Try things out

+ + Learn more about xplat. +
`; } function appCmpContent() { diff --git a/src/collection.json b/src/collection.json index 4fcba48a..97fc0953 100644 --- a/src/collection.json +++ b/src/collection.json @@ -49,6 +49,11 @@ "schema": "./app.web/schema.json", "description": "Nx web app with xplat support." }, + "elements": { + "factory": "./elements", + "schema": "./elements/schema.json", + "description": "Create custom elements for the web." + }, "feature": { "factory": "./feature", "schema": "./feature/schema.json", diff --git a/src/component/_ionic_files/__name__/__name__.component.html b/src/component/_ionic_files/__name__/__name__.component.html index 80cac32a..5d7ad0d8 100644 --- a/src/component/_ionic_files/__name__/__name__.component.html +++ b/src/component/_ionic_files/__name__/__name__.component.html @@ -1 +1 @@ -
<% if (onlyProject || ignoreBase) { %><%= utils.classify(name) %><% } else { %>{{text}}<% } %>
\ No newline at end of file +
<% if (onlyProject || !createBase) { %><%= utils.classify(name) %><% } else { %>{{text}}<% } %>
\ No newline at end of file diff --git a/src/component/_ionic_files/__name__/__name__.component.ts b/src/component/_ionic_files/__name__/__name__.component.ts index 6d35e6b2..379c0d0c 100644 --- a/src/component/_ionic_files/__name__/__name__.component.ts +++ b/src/component/_ionic_files/__name__/__name__.component.ts @@ -1,13 +1,13 @@ import { Component } from '@angular/core'; -<% if (onlyProject || ignoreBase) { %>import { BaseComponent } from '@<%= npmScope %>/core';<% +<% if (onlyProject || !createBase) { %>import { BaseComponent } from '@<%= npmScope %>/core';<% } else { %>import { <%= utils.classify(name) %>BaseComponent } from '@<%= npmScope %>/features';<% } %> @Component({ selector: '<%= prefix %>-<%= name %>', templateUrl: '<%= name %>.component.html' }) -export class <%= utils.classify(name) %>Component extends <%= onlyProject || ignoreBase ? '' : utils.classify(name) %>BaseComponent { +export class <%= utils.classify(name) %>Component extends <%= onlyProject || !createBase ? '' : utils.classify(name) %>BaseComponent { constructor() { super(); diff --git a/src/component/_nativescript_files/__name__/__name__.component.html b/src/component/_nativescript_files/__name__/__name__.component.html index df93a12d..fa3f81c2 100644 --- a/src/component/_nativescript_files/__name__/__name__.component.html +++ b/src/component/_nativescript_files/__name__/__name__.component.html @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/src/component/_nativescript_files/__name__/__name__.component.ts b/src/component/_nativescript_files/__name__/__name__.component.ts index 46c8ddea..26e37dd3 100644 --- a/src/component/_nativescript_files/__name__/__name__.component.ts +++ b/src/component/_nativescript_files/__name__/__name__.component.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; -<% if (onlyProject || ignoreBase) { %>import { BaseComponent } from '@<%= npmScope %>/core';<% +<% if (onlyProject || !createBase) { %>import { BaseComponent } from '@<%= npmScope %>/core';<% } else { %>import { <%= utils.classify(name) %>BaseComponent } from '@<%= npmScope %>/features';<% } %> @Component({ @@ -8,7 +8,7 @@ import { Component } from '@angular/core'; selector: '<%= prefix %>-<%= name %>', templateUrl: './<%= name %>.component.html' }) -export class <%= utils.classify(name) %>Component extends <%= onlyProject || ignoreBase ? '' : utils.classify(name) %>BaseComponent { +export class <%= utils.classify(name) %>Component extends <%= onlyProject || !createBase ? '' : utils.classify(name) %>BaseComponent { constructor() { super(); diff --git a/src/component/_web_files/__name__/__name__.component.html b/src/component/_web_files/__name__/__name__.component.html index 80cac32a..5d7ad0d8 100644 --- a/src/component/_web_files/__name__/__name__.component.html +++ b/src/component/_web_files/__name__/__name__.component.html @@ -1 +1 @@ -
<% if (onlyProject || ignoreBase) { %><%= utils.classify(name) %><% } else { %>{{text}}<% } %>
\ No newline at end of file +
<% if (onlyProject || !createBase) { %><%= utils.classify(name) %><% } else { %>{{text}}<% } %>
\ No newline at end of file diff --git a/src/component/_web_files/__name__/__name__.component.ts b/src/component/_web_files/__name__/__name__.component.ts index 6d35e6b2..379c0d0c 100644 --- a/src/component/_web_files/__name__/__name__.component.ts +++ b/src/component/_web_files/__name__/__name__.component.ts @@ -1,13 +1,13 @@ import { Component } from '@angular/core'; -<% if (onlyProject || ignoreBase) { %>import { BaseComponent } from '@<%= npmScope %>/core';<% +<% if (onlyProject || !createBase) { %>import { BaseComponent } from '@<%= npmScope %>/core';<% } else { %>import { <%= utils.classify(name) %>BaseComponent } from '@<%= npmScope %>/features';<% } %> @Component({ selector: '<%= prefix %>-<%= name %>', templateUrl: '<%= name %>.component.html' }) -export class <%= utils.classify(name) %>Component extends <%= onlyProject || ignoreBase ? '' : utils.classify(name) %>BaseComponent { +export class <%= utils.classify(name) %>Component extends <%= onlyProject || !createBase ? '' : utils.classify(name) %>BaseComponent { constructor() { super(); diff --git a/src/component/index.ts b/src/component/index.ts index d37dccb8..a15c8153 100644 --- a/src/component/index.ts +++ b/src/component/index.ts @@ -212,7 +212,7 @@ export default function(options: featureOptions) { prerun(), // add component for base libs feature (tree: Tree, context: SchematicContext) => - !options.onlyProject && !options.ignoreBase + !options.onlyProject && options.createBase ? addToFeature("component", options, "libs", tree, "_base", true)( tree, context @@ -220,7 +220,7 @@ export default function(options: featureOptions) { : noop()(tree, context), // adjust libs barrel for subFolder (tree: Tree, context: SchematicContext) => - options.subFolder && !options.onlyProject && !options.ignoreBase + options.subFolder && !options.onlyProject && options.createBase ? adjustBarrelIndex( "component", options, @@ -239,7 +239,7 @@ export default function(options: featureOptions) { : noop()(tree, context), // adjust libs barrel (tree: Tree, context: SchematicContext) => - !options.onlyProject && !options.ignoreBase + !options.onlyProject && options.createBase ? adjustBarrelIndex( "component", options, diff --git a/src/component/index_spec.ts b/src/component/index_spec.ts index 5f4b2620..fd471aa2 100644 --- a/src/component/index_spec.ts +++ b/src/component/index_spec.ts @@ -14,7 +14,8 @@ describe('component schematic', () => { const defaultOptions: GenerateOptions = { name: 'signup', feature: 'foo', - platforms: 'nativescript,web' + platforms: 'nativescript,web', + createBase: true }; let appTree: Tree; diff --git a/src/component/schema.d.ts b/src/component/schema.d.ts index f40c3c2e..1251870b 100644 --- a/src/component/schema.d.ts +++ b/src/component/schema.d.ts @@ -21,9 +21,9 @@ export interface Schema { */ platforms?: string; /** - * Ignore base component generation + * Create a base component for maximum cross platform sharing */ - ignoreBase?: boolean; + createBase?: boolean; /** * Schematic processing helpers */ diff --git a/src/component/schema.json b/src/component/schema.json index 328bd91e..a519248e 100644 --- a/src/component/schema.json +++ b/src/component/schema.json @@ -33,9 +33,9 @@ "type": "string", "description": "Target platforms" }, - "ignoreBase": { + "createBase": { "type": "boolean", - "description": "Ignore base component generation.", + "description": "Create a base component for maximum cross platform sharing.", "default": false }, "skipFormat": { diff --git a/src/elements/_builder_files/builder/elements.ts b/src/elements/_builder_files/builder/elements.ts new file mode 100644 index 00000000..8d47e48f --- /dev/null +++ b/src/elements/_builder_files/builder/elements.ts @@ -0,0 +1,6 @@ +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { <%= utils.classify(name) %>Module } from '../<%= name %>.module'; + +platformBrowserDynamic() + .bootstrapModule(<%= utils.classify(name) %>Module) + .catch(err => console.log(err)); diff --git a/src/elements/_builder_files/builder/index.html b/src/elements/_builder_files/builder/index.html new file mode 100644 index 00000000..57d67081 --- /dev/null +++ b/src/elements/_builder_files/builder/index.html @@ -0,0 +1,13 @@ + + + + + Custom Elements + + + + + + <%= htmlElements %> + + \ No newline at end of file diff --git a/src/elements/_builder_files/builder/polyfills.ts b/src/elements/_builder_files/builder/polyfills.ts new file mode 100644 index 00000000..662fd655 --- /dev/null +++ b/src/elements/_builder_files/builder/polyfills.ts @@ -0,0 +1,81 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** IE9, IE10 and IE11 requires all of the following polyfills. **/ +// import 'core-js/es6/symbol'; +// import 'core-js/es6/object'; +// import 'core-js/es6/function'; +// import 'core-js/es6/parse-int'; +// import 'core-js/es6/parse-float'; +// import 'core-js/es6/number'; +// import 'core-js/es6/math'; +// import 'core-js/es6/string'; +// import 'core-js/es6/date'; +// import 'core-js/es6/array'; +// import 'core-js/es6/regexp'; +// import 'core-js/es6/map'; +// import 'core-js/es6/weak-map'; +// import 'core-js/es6/set'; + +/** IE10 and IE11 requires the following for NgClass support on SVG elements */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** IE10 and IE11 requires the following for the Reflect API. */ +// import 'core-js/es6/reflect'; + + +/** Evergreen browsers require these. **/ +// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. +import 'core-js/es7/reflect'; + + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + **/ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + */ + + // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + + /* + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + */ +// (window as any).__Zone_enable_cross_context_check = true; + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js/dist/zone'; // Included with Angular CLI. + + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ +import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js'; \ No newline at end of file diff --git a/src/elements/_builder_files/builder/tsconfig.elements.json b/src/elements/_builder_files/builder/tsconfig.elements.json new file mode 100644 index 00000000..ae7dc062 --- /dev/null +++ b/src/elements/_builder_files/builder/tsconfig.elements.json @@ -0,0 +1,15 @@ +{ + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc/xplat/web/elements", + "module": "es2015", + "types": [] + }, + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ], + "include": [ + "**/*.ts" + ] +} \ No newline at end of file diff --git a/src/elements/_files/__name__.module.ts b/src/elements/_files/__name__.module.ts new file mode 100644 index 00000000..7749d4e8 --- /dev/null +++ b/src/elements/_files/__name__.module.ts @@ -0,0 +1,20 @@ +import { NgModule, Injector } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { createCustomElement } from '@angular/elements'; +import { <%= componentSymbolList %> } from '<%= barrel %>'; + +@NgModule({ + imports: [BrowserModule], + entryComponents: [ + <%= componentSymbolList %> + ] +}) +export class <%= utils.classify(name) %>Module { + constructor(private injector: Injector) { + + } + + ngDoBootstrap() { + <%= customElementList %> + } +} diff --git a/src/elements/index.ts b/src/elements/index.ts new file mode 100644 index 00000000..0e0fcb2c --- /dev/null +++ b/src/elements/index.ts @@ -0,0 +1,308 @@ +import { + apply, + chain, + Tree, + Rule, + url, + move, + template, + mergeWith, + branchAndMerge, + SchematicContext, + SchematicsException, + externalSchematic, + noop +} from "@angular-devkit/schematics"; + +import { + stringUtils, + prerun, + updatePackageScripts, + getNpmScope, + getPrefix, + getJsonFromFile, + applyAppNamingConvention, + updateJsonFile, + formatFiles, + updateJsonInTree, + missingArgument, + updateAngularProjects +} from "../utils"; +import { Schema as ElementsOptions } from "./schema"; +import { getFileContent } from "@schematics/angular/utility/test"; + +let customElementList: string; +let componentSymbols: Array<{ symbol: string; selector: string }>; +let componentSymbolList: string; +let htmlElements: string; +export default function(options: ElementsOptions) { + if (!options.builderModule) { + const example = `ng g elements menu --barrel=@mycompany/ui --components=menu,footer`; + if (!options.name) { + throw new SchematicsException( + missingArgument( + "name", + "Provide a name for the custom element module.", + example + ) + ); + } + if (!options.barrel) { + throw new SchematicsException( + missingArgument( + "barrel", + "Provide the name of the workspace barrel where your components live.", + example + ) + ); + } + if (!options.components) { + throw new SchematicsException( + missingArgument( + "components", + `Provide a comma delimited list of components you'd like to create as custom elements.`, + example + ) + ); + } + } + + return chain([ + prerun(options), + (tree: Tree) => { + if (!options.builderModule) { + const workspacePrefix = options.prefix || getPrefix() || ""; + const htmlElementList = []; + componentSymbols = []; + // parse component names to standard convention + const componentNames = options.components.split(","); + for (let component of componentNames) { + // using short name ("menu" for a component named "MenuComponent") + // convert to fully best practice name + const isShortName = + component.toLowerCase().indexOf("component") === -1; + let selector = `${workspacePrefix ? `${workspacePrefix}-` : ""}`; + if (isShortName) { + selector += component.toLowerCase(); + } else { + const parts = component.toLowerCase().split("component"); + selector += parts[0]; + } + componentSymbols.push({ + selector, + symbol: `${stringUtils.classify(component)}${ + isShortName ? "Component" : "" + }` + }); + htmlElementList.push(`<${selector}>`); + } + componentSymbolList = componentSymbols.map(c => c.symbol).join(", "); + htmlElements = htmlElementList.join("\n"); + + customElementList = createCustomElementList(componentSymbols); + } + return tree; + }, + // add custom element module + (tree: Tree, context: SchematicContext) => { + return options.builderModule ? noop() : addFiles(options)(tree, context); + }, + // add builder files or update them + (tree: Tree, context: SchematicContext) => { + if (tree.exists('xplat/web/elements/builder/index.html')) { + return updateBuilder(tree, options); + } else { + return addFiles(options, 'builder')(tree, context); + } + }, + // adjust app files + // (tree: Tree) => adjustAppFiles(options, tree), + // add build scripts + (tree: Tree) => { + if (options.builderModule) { + return noop(); + } else { + const scripts = {}; + scripts[ + `build.web.elements` + ] = `ng build web-elements --prod --output-hashing=none --single-bundle=true --keep-polyfills=true`; + scripts[`preview.web.elements`] = `http-server dist/ngelements`; + return updatePackageScripts(tree, scripts); + } + }, + (tree: Tree) => { + if (options.builderModule) { + return noop(); + } else { + const projects = {}; + projects[`web-elements`] = { + root: "", + sourceRoot: "xplat/web/elements/builder", + projectType: "application", + prefix: "web-elements", + schematics: {}, + architect: { + build: { + builder: "ngx-build-plus:build", + options: { + outputPath: "dist/ngelements", + index: "xplat/web/elements/builder/index.html", + main: "xplat/web/elements/builder/elements.ts", + polyfills: "xplat/web/elements/builder/polyfills.ts", + tsConfig: "xplat/web/elements/builder/tsconfig.elements.json" + }, + configurations: { + production: { + optimization: true, + outputHashing: "all", + sourceMap: false, + extractCss: true, + namedChunks: false, + aot: true, + extractLicenses: true, + vendorChunk: false, + buildOptimizer: true + } + } + }, + serve: { + builder: "ngx-build-plus:dev-server", + options: { + browserTarget: "web-elements:build" + }, + configurations: { + production: { + browserTarget: "web-elements:build:production" + } + } + } + } + }; + return updateAngularProjects(tree, projects); + } + }, + // update dependencies + (tree: Tree, context: SchematicContext) => { + return options.builderModule ? noop() : updateWorkspaceSupport(options, tree, context); + }, + // update for builderModule if desired + (tree: Tree, context: SchematicContext) => { + if (options.builderModule) { + + } else { + return noop(); + } + }, + // formatting + options.skipFormat ? noop() : formatFiles(options) + ]); +} + +function addFiles(options: ElementsOptions, extra: string = ""): Rule { + extra = extra ? `${extra}_` : ""; + return branchAndMerge( + mergeWith( + apply(url(`./_${extra}files`), [ + template({ + ...(options as any), + name: options.name.toLowerCase(), + customElementList, + componentSymbolList, + componentSymbols, + htmlElements, + npmScope: getNpmScope(), + prefix: getPrefix(), + dot: ".", + utils: stringUtils + }), + move(`xplat/web/elements`) + ]) + ) + ); +} + +function updateWorkspaceSupport( + options: ElementsOptions, + tree: Tree, + context: SchematicContext +) { + return updateJsonInTree("package.json", json => { + json.scripts = json.scripts || {}; + json.dependencies = json.dependencies || {}; + const angularVersion = json.dependencies["@angular/core"]; + json.dependencies = { + ...json.dependencies, + "@angular/elements": angularVersion, + "@webcomponents/webcomponentsjs": "^2.2.7" + }; + json.devDependencies = json.devDependencies || {}; + json.devDependencies = { + ...json.devDependencies, + "http-server": "^0.11.1", + "ngx-build-plus": "^7.7.5" + }; + + return json; + })(tree, context); +} + +function createCustomElementList(componentSymbols) { + const customElements = ["let component;"]; + for (const comp of componentSymbols) { + customElements.push(`component = createCustomElement(${ + comp.symbol + }, { injector: this.injector }); + customElements.define('${comp.selector}', component);`); + } + return customElements.join("\n"); +} + +function updateBuilder(tree: Tree, options: ElementsOptions) { + if (options.builderModule) { + tree.overwrite(`xplat/web/elements/builder/elements.ts`, builderElementsContent(options.builderModule)); + const moduleFilePath = `xplat/web/elements/${options.builderModule}.module.ts`; + if (tree.exists(moduleFilePath)) { + const moduleFile = getFileContent(tree, moduleFilePath); + const selectorParts = moduleFile.split('.define('); + selectorParts.splice(0,1); // remove starting data + const customElements = []; + for (const part of selectorParts) { + let selector = part.split(',')[0].replace(/'/ig, '').replace(/"/ig, ''); + customElements.push(`<${selector}>`); + } + tree.overwrite(`xplat/web/elements/builder/index.html`, buildIndexContent(customElements.join('\n'))); + } else { + throw new SchematicsException(`${moduleFilePath} does not exist.`); + } + } else { + tree.overwrite(`xplat/web/elements/builder/elements.ts`, builderElementsContent(options.name)); + tree.overwrite(`xplat/web/elements/builder/index.html`, buildIndexContent(htmlElements)); + } + return tree; +} + +function builderElementsContent(name: string) { + return `import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { ${stringUtils.classify(name)}Module } from '../${name}.module'; + +platformBrowserDynamic() + .bootstrapModule(${stringUtils.classify(name)}Module) + .catch(err => console.log(err)); +`; +} + +function buildIndexContent(customElements: string) { + return ` + + + + Custom Elements + + + + + + ${customElements} + +`; +} diff --git a/src/elements/index_spec.ts b/src/elements/index_spec.ts new file mode 100644 index 00000000..bf3c5ca8 --- /dev/null +++ b/src/elements/index_spec.ts @@ -0,0 +1,161 @@ +import { Tree, VirtualTree } from '@angular-devkit/schematics'; +import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; +import { getFileContent } from '@schematics/angular/utility/test'; +import * as path from 'path'; + +import { Schema as ElementsOptions } from './schema'; +import { Schema as ComponentOptions } from '../component/schema'; +import { createXplatWithApps, isInModuleMetadata, createOrUpdate, createEmptyWorkspace } from '../utils'; + +describe('elements schematic', () => { + const schematicRunner = new SchematicTestRunner( + '@nstudio/schematics', + path.join(__dirname, '../collection.json'), + ); + const defaultOptions: ElementsOptions = { + name: 'ui-kit', + barrel: '@mycompany/web', + components: 'menu,footer' + }; + + let appTree: Tree; + + beforeEach(() => { + appTree = new VirtualTree(); + appTree = createEmptyWorkspace(appTree); + }); + + it('should create an elements module that provides the specified components', () => { + const options: ElementsOptions = { ...defaultOptions }; + // console.log('appTree:', appTree); + let tree = schematicRunner.runSchematic('xplat', { + prefix: 'tt', + platforms: 'web' + }, appTree); + const componentOptions: ComponentOptions = { + name: 'menu', + platforms: 'web' + }; + tree = schematicRunner.runSchematic('component', componentOptions, tree); + componentOptions.name = 'footer'; + tree = schematicRunner.runSchematic('component', componentOptions, tree); + let files = tree.files; + // console.log(files.slice(85,files.length)); + expect(files.indexOf('/xplat/web/features/ui/components/menu/menu.component.html')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/xplat/web/features/ui/components/menu/menu.component.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/xplat/web/features/ui/components/footer/footer.component.html')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/xplat/web/features/ui/components/footer/footer.component.ts')).toBeGreaterThanOrEqual(0); + + tree = schematicRunner.runSchematic('elements', options, tree); + files = tree.files; + + const elementModulePath = '/xplat/web/elements/ui-kit.module.ts'; + expect(files.indexOf(elementModulePath)).toBeGreaterThanOrEqual(0); + const elementModule = getFileContent(tree, elementModulePath); + // console.log(elementModule); + expect(elementModule.indexOf(`import { MenuComponent, FooterComponent } from '@mycompany/web';`)).toBeGreaterThanOrEqual(0); + expect(elementModule.indexOf(`createCustomElement(MenuComponent`)).toBeGreaterThanOrEqual(0); + expect(elementModule.indexOf(`define('tt-menu'`)).toBeGreaterThanOrEqual(0); + expect(elementModule.indexOf(`createCustomElement(FooterComponent`)).toBeGreaterThanOrEqual(0); + expect(elementModule.indexOf(`define('tt-footer'`)).toBeGreaterThanOrEqual(0); + + const packageFile = getFileContent(tree, 'package.json'); + // console.log(elementModule); + expect(packageFile.indexOf(`build.web.elements`)).toBeGreaterThanOrEqual(0); + expect(packageFile.indexOf(`preview.web.elements`)).toBeGreaterThanOrEqual(0); + }); + + it('--builderModule argument', () => { + const options: ElementsOptions = { ...defaultOptions }; + // console.log('appTree:', appTree); + let tree = schematicRunner.runSchematic('xplat', { + prefix: 'tt', + platforms: 'web' + }, appTree); + const componentOptions: ComponentOptions = { + name: 'menu', + platforms: 'web' + }; + tree = schematicRunner.runSchematic('component', componentOptions, tree); + componentOptions.name = 'footer'; + tree = schematicRunner.runSchematic('component', componentOptions, tree); + let files = tree.files; + // console.log(files.slice(85,files.length)); + expect(files.indexOf('/xplat/web/features/ui/components/menu/menu.component.html')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/xplat/web/features/ui/components/menu/menu.component.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/xplat/web/features/ui/components/footer/footer.component.html')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/xplat/web/features/ui/components/footer/footer.component.ts')).toBeGreaterThanOrEqual(0); + + tree = schematicRunner.runSchematic('elements', options, tree); + files = tree.files; + + let elementModulePath = '/xplat/web/elements/ui-kit.module.ts'; + expect(files.indexOf(elementModulePath)).toBeGreaterThanOrEqual(0); + let elementModule = getFileContent(tree, elementModulePath); + // console.log(elementModule); + expect(elementModule.indexOf(`import { MenuComponent, FooterComponent } from '@mycompany/web';`)).toBeGreaterThanOrEqual(0); + + let builderPath = '/xplat/web/elements/builder/elements.ts'; + expect(files.indexOf(builderPath)).toBeGreaterThanOrEqual(0); + let builderModule = getFileContent(tree, builderPath); + // console.log(builderModule); + expect(builderModule.indexOf(`../ui-kit.module`)).toBeGreaterThanOrEqual(0); + let builderIndexPath = '/xplat/web/elements/builder/index.html'; + expect(files.indexOf(builderPath)).toBeGreaterThanOrEqual(0); + let builderIndex = getFileContent(tree, builderIndexPath); + // console.log(builderIndex); + expect(builderIndex.indexOf(``)).toBeGreaterThanOrEqual(0); + expect(builderIndex.indexOf(``)).toBeGreaterThanOrEqual(0); + + const component2Options: ComponentOptions = { + name: 'dropdown', + platforms: 'web' + }; + tree = schematicRunner.runSchematic('component', component2Options, tree); + component2Options.name = 'link'; + tree = schematicRunner.runSchematic('component', component2Options, tree); + files = tree.files; + // console.log(files.slice(85,files.length)); + expect(files.indexOf('/xplat/web/features/ui/components/dropdown/dropdown.component.html')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/xplat/web/features/ui/components/dropdown/dropdown.component.ts')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/xplat/web/features/ui/components/link/link.component.html')).toBeGreaterThanOrEqual(0); + expect(files.indexOf('/xplat/web/features/ui/components/link/link.component.ts')).toBeGreaterThanOrEqual(0); + + const newElementOptions: ElementsOptions = { + name: 'widgets', + barrel: '@mycompany/web', + components: 'dropdown,link' + }; + tree = schematicRunner.runSchematic('elements', newElementOptions, tree); + files = tree.files; + + elementModulePath = '/xplat/web/elements/widgets.module.ts'; + expect(files.indexOf(elementModulePath)).toBeGreaterThanOrEqual(0); + elementModule = getFileContent(tree, elementModulePath); + // console.log(elementModule); + expect(elementModule.indexOf(`import { DropdownComponent, LinkComponent } from '@mycompany/web';`)).toBeGreaterThanOrEqual(0); + + builderModule = getFileContent(tree, builderPath); + // console.log(builderModule); + expect(builderModule.indexOf(`../widgets.module`)).toBeGreaterThanOrEqual(0); + builderIndex = getFileContent(tree, builderIndexPath); + // console.log(builderIndex); + expect(builderIndex.indexOf(``)).toBeGreaterThanOrEqual(0); + expect(builderIndex.indexOf(``)).toBeGreaterThanOrEqual(0); + + const builderOption: ElementsOptions = { + builderModule: 'ui-kit', + }; + tree = schematicRunner.runSchematic('elements', builderOption, tree); + files = tree.files; + builderModule = getFileContent(tree, builderPath); + // console.log(builderModule); + expect(builderModule.indexOf(`../ui-kit.module`)).toBeGreaterThanOrEqual(0); + builderIndex = getFileContent(tree, builderIndexPath); + // console.log(builderIndex); + expect(builderIndex.indexOf(``)).toBeGreaterThanOrEqual(0); + expect(builderIndex.indexOf(``)).toBeGreaterThanOrEqual(0); + + }); + +}); \ No newline at end of file diff --git a/src/elements/schema.d.ts b/src/elements/schema.d.ts new file mode 100644 index 00000000..9fe8fc8b --- /dev/null +++ b/src/elements/schema.d.ts @@ -0,0 +1,24 @@ +export interface Schema { + name?: string; + /** + * The barrel in your workspace that contains the components you'd like to create as custom elements. + */ + barrel?: string; + /** + * Comma delimited list of components from your barrel to create as custom elements. + */ + components?: string; + /** + * Update builder files to use a different Angular Element module + */ + builderModule?: string; + /** + * A unique prefix to add to each custom element. Defaults to workspace selector setting. + */ + prefix?: string; + /** + * Skip formatting + */ + skipFormat?: boolean; + +} diff --git a/src/elements/schema.json b/src/elements/schema.json new file mode 100644 index 00000000..43283700 --- /dev/null +++ b/src/elements/schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "elements", + "title": "Create custom elements for the web.", + "type": "object", + "properties": { + "name": { + "description": "The name of the custom element module.", + "type": "string", + "$default": { + "$source": "argv", + "index": 0 + } + }, + "barrel": { + "description": "The barrel in your workspace that contains the components you'd like to create as custom elements.", + "type": "string" + }, + "components": { + "description": "Comma delimited list of components from your barrel to create as custom elements.", + "type": "string" + }, + "builderModule": { + "type": "string", + "description": "Update builder files to use a different Angular Element module. Used in isolation with no other options." + }, + "prefix": { + "type": "string", + "description": "A unique prefix to add to each custom element. Defaults to workspace selector setting." + }, + "skipFormat": { + "description": "Skip formatting files", + "type": "boolean", + "default": false + } + }, + "required": [] + } \ No newline at end of file diff --git a/src/feature/_ionic_component_files/components/__name__/__name__.component.ts b/src/feature/_ionic_component_files/components/__name__/__name__.component.ts index 6d35e6b2..379c0d0c 100644 --- a/src/feature/_ionic_component_files/components/__name__/__name__.component.ts +++ b/src/feature/_ionic_component_files/components/__name__/__name__.component.ts @@ -1,13 +1,13 @@ import { Component } from '@angular/core'; -<% if (onlyProject || ignoreBase) { %>import { BaseComponent } from '@<%= npmScope %>/core';<% +<% if (onlyProject || !createBase) { %>import { BaseComponent } from '@<%= npmScope %>/core';<% } else { %>import { <%= utils.classify(name) %>BaseComponent } from '@<%= npmScope %>/features';<% } %> @Component({ selector: '<%= prefix %>-<%= name %>', templateUrl: '<%= name %>.component.html' }) -export class <%= utils.classify(name) %>Component extends <%= onlyProject || ignoreBase ? '' : utils.classify(name) %>BaseComponent { +export class <%= utils.classify(name) %>Component extends <%= onlyProject || !createBase ? '' : utils.classify(name) %>BaseComponent { constructor() { super(); diff --git a/src/feature/_lib_files/index.ts b/src/feature/_lib_files/index.ts index 8e07ebfa..8f6969f1 100644 --- a/src/feature/_lib_files/index.ts +++ b/src/feature/_lib_files/index.ts @@ -1,2 +1,2 @@ -<% if (!onlyProject && !ignoreBase && !onlyModule) { %>export * from './base';<% } %> +<% if (!onlyProject && createBase && !onlyModule) { %>export * from './base';<% } %> export * from './<%= name %>.module'; diff --git a/src/feature/_nativescript_component_files/components/__name__/__name__.component.ts b/src/feature/_nativescript_component_files/components/__name__/__name__.component.ts index 46c8ddea..26e37dd3 100644 --- a/src/feature/_nativescript_component_files/components/__name__/__name__.component.ts +++ b/src/feature/_nativescript_component_files/components/__name__/__name__.component.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; -<% if (onlyProject || ignoreBase) { %>import { BaseComponent } from '@<%= npmScope %>/core';<% +<% if (onlyProject || !createBase) { %>import { BaseComponent } from '@<%= npmScope %>/core';<% } else { %>import { <%= utils.classify(name) %>BaseComponent } from '@<%= npmScope %>/features';<% } %> @Component({ @@ -8,7 +8,7 @@ import { Component } from '@angular/core'; selector: '<%= prefix %>-<%= name %>', templateUrl: './<%= name %>.component.html' }) -export class <%= utils.classify(name) %>Component extends <%= onlyProject || ignoreBase ? '' : utils.classify(name) %>BaseComponent { +export class <%= utils.classify(name) %>Component extends <%= onlyProject || !createBase ? '' : utils.classify(name) %>BaseComponent { constructor() { super(); diff --git a/src/feature/_web_component_files/components/__name__/__name__.component.ts b/src/feature/_web_component_files/components/__name__/__name__.component.ts index 6d35e6b2..379c0d0c 100644 --- a/src/feature/_web_component_files/components/__name__/__name__.component.ts +++ b/src/feature/_web_component_files/components/__name__/__name__.component.ts @@ -1,13 +1,13 @@ import { Component } from '@angular/core'; -<% if (onlyProject || ignoreBase) { %>import { BaseComponent } from '@<%= npmScope %>/core';<% +<% if (onlyProject || !createBase) { %>import { BaseComponent } from '@<%= npmScope %>/core';<% } else { %>import { <%= utils.classify(name) %>BaseComponent } from '@<%= npmScope %>/features';<% } %> @Component({ selector: '<%= prefix %>-<%= name %>', templateUrl: '<%= name %>.component.html' }) -export class <%= utils.classify(name) %>Component extends <%= onlyProject || ignoreBase ? '' : utils.classify(name) %>BaseComponent { +export class <%= utils.classify(name) %>Component extends <%= onlyProject || !createBase ? '' : utils.classify(name) %>BaseComponent { constructor() { super(); diff --git a/src/feature/index.ts b/src/feature/index.ts index 9c444a26..708e6c94 100644 --- a/src/feature/index.ts +++ b/src/feature/index.ts @@ -164,7 +164,7 @@ export default function(options: featureOptions) { : addFiles(options)(tree, context), // libs (tree: Tree, context: SchematicContext) => - options.onlyProject || options.ignoreBase || options.onlyModule + options.onlyProject || !options.createBase || options.onlyModule ? noop()(tree, context) : addFiles(options, null, null, "_component")(tree, context), // update libs index diff --git a/src/feature/index_spec.ts b/src/feature/index_spec.ts index 4bb23cbd..4c10fdd0 100644 --- a/src/feature/index_spec.ts +++ b/src/feature/index_spec.ts @@ -13,7 +13,8 @@ describe('feature schematic', () => { ); const defaultOptions: FeatureOptions = { name: 'foo', - projects: 'nativescript-viewer,web-viewer' + projects: 'nativescript-viewer,web-viewer', + createBase: true }; let appTree: Tree; @@ -138,7 +139,7 @@ describe('feature schematic', () => { expect(featureModule).toMatch(`import { UIModule } from \'../ui/ui.module\'`); }); - it('should create feature module WITH a single starting component BUT IGNORE creating matching base component when using ignoreBase', () => { + it('should create feature module WITH a single starting component BUT IGNORE creating matching base component', () => { // console.log('appTree:', appTree); let tree = schematicRunner.runSchematic('xplat', { prefix: 'tt', @@ -150,8 +151,7 @@ describe('feature schematic', () => { }, tree); const options: FeatureOptions = { name: 'foo', - platforms: 'web', - ignoreBase: true + platforms: 'web' }; tree = schematicRunner.runSchematic('feature', options, tree); const files = tree.files; diff --git a/src/feature/schema.d.ts b/src/feature/schema.d.ts index 0d3014f1..b8c48e70 100644 --- a/src/feature/schema.d.ts +++ b/src/feature/schema.d.ts @@ -17,9 +17,9 @@ export interface Schema { */ onlyModule?: boolean; /** - * Ignore base component generation + * Create base component for maximum code sharing */ - ignoreBase?: boolean; + createBase?: boolean; /** * Configure routing */ diff --git a/src/feature/schema.json b/src/feature/schema.json index f333e6c8..f1dc6cb3 100644 --- a/src/feature/schema.json +++ b/src/feature/schema.json @@ -30,9 +30,9 @@ "description": "Generate just the module and ignore the default component.", "default": false }, - "ignoreBase": { + "createBase": { "type": "boolean", - "description": "Ignore base component generation.", + "description": "Create base component for maximum code sharing.", "default": false }, "routing": { diff --git a/src/utils/errors.ts b/src/utils/errors.ts index ab167f88..463c0758 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -20,8 +20,8 @@ export function helperMissingPlatforms() { return `Missing platforms argument. Example: ng g xplat-helper imports --platforms=nativescript`; } -export function missingNameArgument(description: string = '', example: string = '') { - return `Missing name argument. ${description} ${example ? 'Example: ' + example : ''}`; +export function missingArgument(argName: string, description: string = '', example: string = '') { + return `Missing ${argName} argument. ${description} ${example ? 'Example: ' + example : ''}`; } export function noPlatformError() { diff --git a/src/xplat-helper/index.ts b/src/xplat-helper/index.ts index 4bae50ac..1c561a78 100644 --- a/src/xplat-helper/index.ts +++ b/src/xplat-helper/index.ts @@ -34,7 +34,7 @@ import { unsupportedHelperError, updateTsConfig, helperTargetError, - missingNameArgument + missingArgument, } from "../utils"; import { Schema as HelperOptions } from "./schema"; // Helpers @@ -94,7 +94,7 @@ let platforms: Array = []; export default function(options: HelperOptions) { if (!options.name) { throw new SchematicsException( - missingNameArgument('Provide a comma delimited list of helpers to generate.', 'ng g xplat-helper imports') + missingArgument('name', 'Provide a comma delimited list of helpers to generate.', 'ng g xplat-helper imports') ); } helpers = options.name.split(","); diff --git a/src/xplat/_web_files/features/ui/components/header/header.component.html b/src/xplat/_web_files/features/ui/components/header/header.component.html new file mode 100644 index 00000000..b0105d43 --- /dev/null +++ b/src/xplat/_web_files/features/ui/components/header/header.component.html @@ -0,0 +1,7 @@ +
+

Welcome to an Angular CLI app built with Nrwl Nx and xplat!

+
apps/{{ title }} in your workspace.
+ + + + +
diff --git a/src/xplat/_web_files/features/ui/components/header/header.component.ts b/src/xplat/_web_files/features/ui/components/header/header.component.ts new file mode 100644 index 00000000..7bf8f64b --- /dev/null +++ b/src/xplat/_web_files/features/ui/components/header/header.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +import { HeaderBaseComponent } from '@<%= npmScope %>/features'; + +@Component({ + selector: '<%= prefix %>-header', + templateUrl: 'header.component.html' +}) +export class HeaderComponent extends HeaderBaseComponent { + +} diff --git a/src/xplat/_web_files/features/ui/components/index.ts b/src/xplat/_web_files/features/ui/components/index.ts new file mode 100644 index 00000000..82a8b3c6 --- /dev/null +++ b/src/xplat/_web_files/features/ui/components/index.ts @@ -0,0 +1,7 @@ +import { HeaderComponent } from './header/header.component'; + +export const UI_COMPONENTS = [ + HeaderComponent, +]; + +export * from './header/header.component'; diff --git a/src/xplat/_web_files/features/ui/index.ts b/src/xplat/_web_files/features/ui/index.ts index 516194eb..c65382f2 100644 --- a/src/xplat/_web_files/features/ui/index.ts +++ b/src/xplat/_web_files/features/ui/index.ts @@ -1 +1,2 @@ -export {UIModule} from './ui.module'; +export * from './components'; +export { UIModule } from './ui.module'; diff --git a/src/xplat/_web_files/features/ui/ui.module.ts b/src/xplat/_web_files/features/ui/ui.module.ts index 90ba70fc..e654df75 100644 --- a/src/xplat/_web_files/features/ui/ui.module.ts +++ b/src/xplat/_web_files/features/ui/ui.module.ts @@ -5,6 +5,7 @@ import { RouterModule } from '@angular/router'; // libs import { UISharedModule } from '@<%= npmScope %>/features'; +import { UI_COMPONENTS } from './components'; const MODULES = [ CommonModule, @@ -18,8 +19,12 @@ const MODULES = [ imports: [ ...MODULES ], + declarations: [ + ...UI_COMPONENTS + ], exports: [ - ...MODULES + ...MODULES, + ...UI_COMPONENTS ] }) export class UIModule { }