55* Use of this source code is governed by an MIT-style license that can be
66* found in the LICENSE file at https://angular.io/license
77*/
8- import { Path , join , normalize } from '@angular-devkit/core' ;
8+ import {
9+ JsonParseMode ,
10+ experimental ,
11+ getSystemPath ,
12+ join ,
13+ normalize ,
14+ parseJson ,
15+ } from '@angular-devkit/core' ;
916import {
1017 Rule ,
1118 SchematicContext ,
@@ -19,153 +26,190 @@ import {
1926 template ,
2027 url ,
2128} from '@angular-devkit/schematics' ;
22- import { getWorkspace , getWorkspacePath } from '../utility/config' ;
29+ import { Observable } from 'rxjs' ;
30+ import { Readable , Writable } from 'stream' ;
2331import { Schema as PwaOptions } from './schema' ;
2432
33+ const RewritingStream = require ( 'parse5-html-rewriting-stream' ) ;
2534
26- function addServiceWorker ( options : PwaOptions ) : Rule {
27- return ( host : Tree , context : SchematicContext ) => {
28- context . logger . debug ( 'Adding service worker...' ) ;
29-
30- const swOptions = {
31- ...options ,
32- } ;
33- delete swOptions . title ;
34-
35- return externalSchematic ( '@schematics/angular' , 'service-worker' , swOptions ) ;
36- } ;
37- }
3835
39- function getIndent ( text : string ) : string {
40- let indent = '' ;
36+ function getWorkspace (
37+ host : Tree ,
38+ ) : { path : string , workspace : experimental . workspace . WorkspaceSchema } {
39+ const possibleFiles = [ '/angular.json' , '/.angular.json' ] ;
40+ const path = possibleFiles . filter ( path => host . exists ( path ) ) [ 0 ] ;
4141
42- for ( const char of text ) {
43- if ( char === ' ' || char === '\t' ) {
44- indent += char ;
45- } else {
46- break ;
47- }
42+ const configBuffer = host . read ( path ) ;
43+ if ( configBuffer === null ) {
44+ throw new SchematicsException ( `Could not find (${ path } )` ) ;
4845 }
49-
50- return indent ;
46+ const content = configBuffer . toString ( ) ;
47+
48+ return {
49+ path,
50+ workspace : parseJson (
51+ content ,
52+ JsonParseMode . Loose ,
53+ ) as { } as experimental . workspace . WorkspaceSchema ,
54+ } ;
5155}
5256
53- function updateIndexFile ( options : PwaOptions ) : Rule {
54- return ( host : Tree , context : SchematicContext ) => {
55- const workspace = getWorkspace ( host ) ;
56- const project = workspace . projects [ options . project as string ] ;
57- let path : string ;
58- const projectTargets = project . targets || project . architect ;
59- if ( project && projectTargets && projectTargets . build && projectTargets . build . options . index ) {
60- path = projectTargets . build . options . index ;
61- } else {
62- throw new SchematicsException ( 'Could not find index file for the project' ) ;
63- }
57+ function updateIndexFile ( path : string ) : Rule {
58+ return ( host : Tree ) => {
6459 const buffer = host . read ( path ) ;
6560 if ( buffer === null ) {
6661 throw new SchematicsException ( `Could not read index file: ${ path } ` ) ;
6762 }
68- const content = buffer . toString ( ) ;
69- const lines = content . split ( '\n' ) ;
70- let closingHeadTagLineIndex = - 1 ;
71- let closingBodyTagLineIndex = - 1 ;
72- lines . forEach ( ( line , index ) => {
73- if ( closingHeadTagLineIndex === - 1 && / < \/ h e a d > / . test ( line ) ) {
74- closingHeadTagLineIndex = index ;
75- } else if ( closingBodyTagLineIndex === - 1 && / < \/ b o d y > / . test ( line ) ) {
76- closingBodyTagLineIndex = index ;
77- }
78- } ) ;
7963
80- const headIndent = getIndent ( lines [ closingHeadTagLineIndex ] ) + ' ' ;
81- const itemsToAddToHead = [
82- '<link rel="manifest" href="manifest.json">' ,
83- '<meta name="theme-color" content="#1976d2">' ,
84- ] ;
64+ const rewriter = new RewritingStream ( ) ;
8565
86- const bodyIndent = getIndent ( lines [ closingBodyTagLineIndex ] ) + ' ' ;
87- const itemsToAddToBody = [
88- '<noscript>Please enable JavaScript to continue using this application.</noscript>' ,
89- ] ;
66+ let needsNoScript = true ;
67+ rewriter . on ( 'startTag' , ( startTag : { tagName : string } ) => {
68+ if ( startTag . tagName === 'noscript' ) {
69+ needsNoScript = false ;
70+ }
9071
91- const updatedIndex = [
92- ...lines . slice ( 0 , closingHeadTagLineIndex ) ,
93- ...itemsToAddToHead . map ( line => headIndent + line ) ,
94- ...lines . slice ( closingHeadTagLineIndex , closingBodyTagLineIndex ) ,
95- ...itemsToAddToBody . map ( line => bodyIndent + line ) ,
96- ...lines . slice ( closingBodyTagLineIndex ) ,
97- ] . join ( '\n' ) ;
72+ rewriter . emitStartTag ( startTag ) ;
73+ } ) ;
9874
99- host . overwrite ( path , updatedIndex ) ;
75+ rewriter . on ( 'endTag' , ( endTag : { tagName : string } ) => {
76+ if ( endTag . tagName === 'head' ) {
77+ rewriter . emitRaw ( ' <link rel="manifest" href="manifest.json">\n' ) ;
78+ rewriter . emitRaw ( ' <meta name="theme-color" content="#1976d2">\n' ) ;
79+ } else if ( endTag . tagName === 'body' && needsNoScript ) {
80+ rewriter . emitRaw (
81+ ' <noscript>Please enable JavaScript to continue using this application.</noscript>\n' ,
82+ ) ;
83+ }
10084
101- return host ;
85+ rewriter . emitEndTag ( endTag ) ;
86+ } ) ;
87+
88+ return new Observable < Tree > ( obs => {
89+ const input = new Readable ( {
90+ encoding : 'utf8' ,
91+ read ( ) : void {
92+ this . push ( buffer ) ;
93+ this . push ( null ) ;
94+ } ,
95+ } ) ;
96+
97+ const chunks : Array < Buffer > = [ ] ;
98+ const output = new Writable ( {
99+ write ( chunk : string | Buffer , encoding : string , callback : Function ) : void {
100+ chunks . push ( typeof chunk === 'string' ? Buffer . from ( chunk , encoding ) : chunk ) ;
101+ callback ( ) ;
102+ } ,
103+ final ( callback : ( error ?: Error ) => void ) : void {
104+ const full = Buffer . concat ( chunks ) ;
105+ host . overwrite ( path , full . toString ( ) ) ;
106+ callback ( ) ;
107+ obs . next ( host ) ;
108+ obs . complete ( ) ;
109+ } ,
110+ } ) ;
111+
112+ input . pipe ( rewriter ) . pipe ( output ) ;
113+ } ) ;
102114 } ;
103115}
104116
105- function addManifestToAssetsConfig ( options : PwaOptions ) {
117+ export default function ( options : PwaOptions ) : Rule {
106118 return ( host : Tree , context : SchematicContext ) => {
119+ if ( ! options . title ) {
120+ options . title = options . project ;
121+ }
122+ const { path : workspacePath , workspace } = getWorkspace ( host ) ;
107123
108- const workspacePath = getWorkspacePath ( host ) ;
109- const workspace = getWorkspace ( host ) ;
110- const project = workspace . projects [ options . project as string ] ;
124+ if ( ! options . project ) {
125+ throw new SchematicsException ( 'Option "project" is required.' ) ;
126+ }
111127
128+ const project = workspace . projects [ options . project ] ;
112129 if ( ! project ) {
113- throw new Error ( `Project is not defined in this workspace.` ) ;
130+ throw new SchematicsException ( `Project is not defined in this workspace.` ) ;
114131 }
115132
116- const assetEntry = join ( normalize ( project . root ) , 'src' , 'manifest.json' ) ;
133+ if ( project . projectType !== 'application' ) {
134+ throw new SchematicsException ( `PWA requires a project type of "application".` ) ;
135+ }
117136
137+ // Find all the relevant targets for the project
118138 const projectTargets = project . targets || project . architect ;
119- if ( ! projectTargets ) {
120- throw new Error ( `Targets are not defined for this project.` ) ;
139+ if ( ! projectTargets || Object . keys ( projectTargets ) . length === 0 ) {
140+ throw new SchematicsException ( `Targets are not defined for this project.` ) ;
121141 }
122142
123- [ 'build' , 'test' ] . forEach ( ( target ) => {
124-
125- const applyTo = projectTargets [ target ] . options ;
126- const assets = applyTo . assets || ( applyTo . assets = [ ] ) ;
127-
128- assets . push ( assetEntry ) ;
143+ const buildTargets = [ ] ;
144+ const testTargets = [ ] ;
145+ for ( const targetName in projectTargets ) {
146+ const target = projectTargets [ targetName ] ;
147+ if ( ! target ) {
148+ continue ;
149+ }
129150
130- } ) ;
151+ if ( target . builder === '@angular-devkit/build-angular:browser' ) {
152+ buildTargets . push ( target ) ;
153+ } else if ( target . builder === '@angular-devkit/build-angular:karma' ) {
154+ testTargets . push ( target ) ;
155+ }
156+ }
131157
158+ // Add manifest to asset configuration
159+ const assetEntry = join ( normalize ( project . root ) , 'src' , 'manifest.json' ) ;
160+ for ( const target of [ ...buildTargets , ...testTargets ] ) {
161+ if ( target . options ) {
162+ if ( target . options . assets ) {
163+ target . options . assets . push ( assetEntry ) ;
164+ } else {
165+ target . options . assets = [ assetEntry ] ;
166+ }
167+ } else {
168+ target . options = { assets : [ assetEntry ] } ;
169+ }
170+ }
132171 host . overwrite ( workspacePath , JSON . stringify ( workspace , null , 2 ) ) ;
133172
134- return host ;
135- } ;
136- }
173+ // Find all index.html files in build targets
174+ const indexFiles = new Set < string > ( ) ;
175+ for ( const target of buildTargets ) {
176+ if ( target . options && target . options . index ) {
177+ indexFiles . add ( target . options . index ) ;
178+ }
137179
138- export default function ( options : PwaOptions ) : Rule {
139- return ( host : Tree , context : SchematicContext ) => {
140- const workspace = getWorkspace ( host ) ;
141- if ( ! options . project ) {
142- throw new SchematicsException ( 'Option "project" is required.' ) ;
143- }
144- const project = workspace . projects [ options . project ] ;
145- if ( project . projectType !== 'application' ) {
146- throw new SchematicsException ( `PWA requires a project type of "application".` ) ;
180+ if ( ! target . configurations ) {
181+ continue ;
182+ }
183+ for ( const configName in target . configurations ) {
184+ const configuration = target . configurations [ configName ] ;
185+ if ( configuration && configuration . index ) {
186+ indexFiles . add ( configuration . index ) ;
187+ }
188+ }
147189 }
148190
149- const sourcePath = join ( project . root as Path , 'src' ) ;
191+ // Setup sources for the assets files to add to the project
192+ const sourcePath = join ( normalize ( project . root ) , 'src' ) ;
150193 const assetsPath = join ( sourcePath , 'assets' ) ;
151-
152- options . title = options . title || options . project ;
153-
154194 const rootTemplateSource = apply ( url ( './files/root' ) , [
155195 template ( { ...options } ) ,
156- move ( sourcePath ) ,
196+ move ( getSystemPath ( sourcePath ) ) ,
157197 ] ) ;
158198 const assetsTemplateSource = apply ( url ( './files/assets' ) , [
159199 template ( { ...options } ) ,
160- move ( assetsPath ) ,
200+ move ( getSystemPath ( assetsPath ) ) ,
161201 ] ) ;
162202
203+ // Setup service worker schematic options
204+ const swOptions = { ...options } ;
205+ delete swOptions . title ;
206+
207+ // Chain the rules and return
163208 return chain ( [
164- addServiceWorker ( options ) ,
209+ externalSchematic ( '@schematics/angular' , 'service-worker' , swOptions ) ,
165210 mergeWith ( rootTemplateSource ) ,
166211 mergeWith ( assetsTemplateSource ) ,
167- updateIndexFile ( options ) ,
168- addManifestToAssetsConfig ( options ) ,
212+ ...[ ...indexFiles ] . map ( path => updateIndexFile ( path ) ) ,
169213 ] ) ( host , context ) ;
170214 } ;
171215}
0 commit comments