1
1
import config from "@css-blocks/config" ;
2
2
import { AnalysisOptions , Block , BlockCompiler , BlockFactory , Configuration , NodeJsImporter , Options as ParserOptions , OutputMode , resolveConfiguration } from "@css-blocks/core" ;
3
- import type { AST , ASTPlugin , ASTPluginEnvironment , NodeVisitor , Syntax } from "@glimmer/syntax" ;
3
+ import type { ASTPlugin , ASTPluginEnvironment } from "@glimmer/syntax" ;
4
4
import { ObjectDictionary } from "@opticss/util" ;
5
+ import BroccoliDebug = require( "broccoli-debug" ) ;
6
+ import funnel = require( "broccoli-funnel" ) ;
5
7
import type { InputNode } from "broccoli-node-api" ;
6
- import TemplateCompilerPlugin , { HtmlBarsOptions } from "ember-cli-htmlbars/lib/template-compiler-plugin" ;
8
+ import outputWrapper = require( "broccoli-output-wrapper" ) ;
9
+ import TemplateCompilerPlugin = require( "ember-cli-htmlbars/lib/template-compiler-plugin" ) ;
7
10
import type EmberApp from "ember-cli/lib/broccoli/ember-app" ;
8
11
import type EmberAddon from "ember-cli/lib/models/addon" ;
9
12
import type { AddonImplementation , ThisAddon , Tree } from "ember-cli/lib/models/addon" ;
13
+ import type Project from "ember-cli/lib/models/project" ;
14
+ import FSMerger = require( "fs-merger" ) ;
10
15
import * as FSTree from "fs-tree-diff" ;
11
16
import { OptiCSSOptions , postcss } from "opticss" ;
12
17
import * as path from "path" ;
13
18
14
19
import { AnalyzingRewriteManager } from "./AnalyzingRewriteManager" ;
20
+ import { BroccoliFileLocator } from "./BroccoliFileLocator" ;
15
21
import { BroccoliTreeImporter , identToPath , isBroccoliTreeIdentifier } from "./BroccoliTreeImporter" ;
16
22
import { EmberAnalysis } from "./EmberAnalysis" ;
17
- import { BroccoliFileLocator } from "./BroccoliFileLocator " ;
23
+ import { ASTPluginWithDeps } from "./TemplateAnalyzingRewriter " ;
18
24
19
25
type Writeable < T > = { - readonly [ P in keyof T ] : T [ P ] } ;
20
26
@@ -25,37 +31,33 @@ interface EmberASTPluginEnvironment extends ASTPluginEnvironment {
25
31
} ;
26
32
}
27
33
28
- class Visitor implements NodeVisitor {
29
- moduleName : string ;
30
- syntax : Syntax ;
31
- constructor ( moduleName : string , syntax : Syntax ) {
32
- this . moduleName = moduleName ;
33
- this . syntax = syntax ;
34
- }
35
- ElementNode ( node : AST . ElementNode ) {
36
- console . log ( `visited ${ this . syntax . print ( node ) } ` ) ;
37
- }
34
+ function withoutCssBlockFiles ( tree : InputNode | undefined ) {
35
+ if ( ! tree ) return tree ;
36
+ return funnel ( tree , {
37
+ exclude : [ "**/*.block.{css,scss,sass,less,styl}" ] ,
38
+ } ) ;
38
39
}
39
- const NOOP_VISITOR = { } ;
40
40
41
41
class CSSBlocksTemplateCompilerPlugin extends TemplateCompilerPlugin {
42
42
previousSourceTree : FSTree ;
43
43
cssBlocksOptions : CSSBlocksEmberOptions ;
44
44
parserOpts : Readonly < Configuration > ;
45
45
analyzingRewriter : AnalyzingRewriteManager | undefined ;
46
- constructor ( inputTree : InputNode , htmlbarsOptions : HtmlBarsOptions , cssBlocksOptions : CSSBlocksEmberOptions ) {
47
- htmlbarsOptions . plugins = htmlbarsOptions . plugins || { } ;
48
- htmlbarsOptions . plugins . ast = htmlbarsOptions . plugins . ast || [ ] ;
49
- htmlbarsOptions . plugins . ast . unshift ( ( env ) => this . astPluginBuilder ( env ) ) ;
46
+ input ! : FSMerger . FS ;
47
+ output ! : outputWrapper . FSOutput ;
48
+ constructor ( inputTree : InputNode , htmlbarsOptions : TemplateCompilerPlugin . HtmlBarsOptions , cssBlocksOptions : CSSBlocksEmberOptions ) {
50
49
super ( inputTree , htmlbarsOptions ) ;
51
50
this . cssBlocksOptions = cssBlocksOptions ;
52
51
this . parserOpts = resolveConfiguration ( cssBlocksOptions . parserOpts ) ;
53
52
this . previousSourceTree = new FSTree ( ) ;
54
53
}
55
- astPluginBuilder ( env : EmberASTPluginEnvironment ) {
54
+ astPluginBuilder ( env : EmberASTPluginEnvironment ) : ASTPluginWithDeps {
56
55
let moduleName = env . meta ?. [ "moduleName" ] ;
57
56
if ( ! moduleName ) {
58
- throw new Error ( "[internal error] moduleName expected." ) ;
57
+ return {
58
+ name : "css-blocks-noop" ,
59
+ visitor : { } ,
60
+ } ;
59
61
}
60
62
if ( ! this . analyzingRewriter ) {
61
63
throw new Error ( "[internal error] analyzing rewriter expected." ) ;
@@ -67,7 +69,14 @@ class CSSBlocksTemplateCompilerPlugin extends TemplateCompilerPlugin {
67
69
// write additional output files to the output tree.
68
70
return this . analyzingRewriter . templateAnalyzerAndRewriter ( moduleName , env . syntax ) ;
69
71
}
72
+
70
73
async build ( ) {
74
+ if ( ! this . input ) {
75
+ this . input = new FSMerger ( this . inputPaths ) . fs ;
76
+ }
77
+ if ( ! this . output ) {
78
+ this . output = outputWrapper ( this ) ;
79
+ }
71
80
let cssBlockEntries = this . input . entries ( "." , [ BLOCK_GLOB ] ) ;
72
81
let currentFSTree = FSTree . fromEntries ( cssBlockEntries ) ;
73
82
let patch = this . previousSourceTree . calculatePatch ( currentFSTree ) ;
@@ -85,12 +94,22 @@ class CSSBlocksTemplateCompilerPlugin extends TemplateCompilerPlugin {
85
94
// the blocks and associate them to their corresponding templates.
86
95
await this . analyzingRewriter . discoverTemplatesWithBlocks ( ) ;
87
96
// Compiles the handlebars files, runs our plugin for each file
88
- await super . build ( ) ;
97
+ // we have to wrap this RSVP Promise that's returned in a native promise or
98
+ // else await won't work.
99
+ let builder = new Promise ( ( resolve , reject ) => {
100
+ try {
101
+ let buildResult = super . build ( ) || Promise . resolve ( ) ;
102
+ buildResult . then ( resolve , reject ) ;
103
+ } catch ( e ) {
104
+ reject ( e ) ;
105
+ }
106
+ } ) ;
107
+ await builder ;
89
108
// output compiled block files and template analyses
90
109
let blocks = new Set < Block > ( ) ;
91
110
let blockOutputPaths = new Map < Block , string > ( ) ;
92
111
let analyses = new Array < EmberAnalysis > ( ) ;
93
- for ( let analyzedTemplate of this . analyzingRewriter . analyzedTemplates ( ) ) {
112
+ for ( let analyzedTemplate of this . analyzingRewriter ! . analyzedTemplates ( ) ) {
94
113
let { block, analysis } = analyzedTemplate ;
95
114
analyses . push ( analysis ) ;
96
115
blocks . add ( block ) ;
@@ -105,15 +124,15 @@ class CSSBlocksTemplateCompilerPlugin extends TemplateCompilerPlugin {
105
124
if ( ! block . stylesheet ) {
106
125
throw new Error ( "[internal error] block stylesheet expected." ) ;
107
126
}
108
- // TODO generate definition file too
109
- let compiledAST = compiler . compile ( block , block . stylesheet , this . analyzingRewriter . reservedClassNames ( ) ) ;
127
+ // TODO generate definition file too1
128
+ let compiledAST = compiler . compile ( block , block . stylesheet , this . analyzingRewriter ! . reservedClassNames ( ) ) ;
110
129
// TODO disable source maps in production?
111
130
let result = compiledAST . toResult ( { to : outputPath , map : { inline : true } } ) ;
112
- this . output . writeFileSync ( outputPath , result . css , "utf8" ) ;
131
+ this . output . writeFileSync ( outputPath , wrapCSSWithDelimiterComments ( block . guid , result . css ) , "utf8" ) ;
113
132
}
114
133
for ( let analysis of analyses ) {
115
134
let analysisOutputPath = analysisPath ( analysis . template . relativePath ) ;
116
- this . output . mkdirSync ( analysisOutputPath , { recursive : true } ) ;
135
+ this . output . mkdirSync ( path . dirname ( analysisOutputPath ) , { recursive : true } ) ;
117
136
this . output . writeFileSync (
118
137
analysisOutputPath ,
119
138
JSON . stringify ( analysis . serialize ( blockOutputPaths ) ) ,
@@ -123,6 +142,12 @@ class CSSBlocksTemplateCompilerPlugin extends TemplateCompilerPlugin {
123
142
}
124
143
}
125
144
145
+ // This is a placeholder, eventually the block compiler should add this.
146
+ function wrapCSSWithDelimiterComments ( guid : string , css : string ) {
147
+ return `/*#css-blocks ${ guid } */\n${ css } \n/*#css-blocks end*/\n` ;
148
+ }
149
+
150
+
126
151
function analysisPath ( templatePath : string ) : string {
127
152
let analysisPath = path . parse ( templatePath ) ;
128
153
delete analysisPath . base ;
@@ -132,7 +157,7 @@ function analysisPath(templatePath: string): string {
132
157
133
158
function getOutputPath ( block : Block ) : string {
134
159
if ( isBroccoliTreeIdentifier ( block . identifier ) ) {
135
- return identToPath ( block . identifier ) ;
160
+ return identToPath ( block . identifier ) . replace ( ".block" , "" ) ;
136
161
} else {
137
162
throw new Error ( "Implement me!" ) ;
138
163
}
@@ -154,17 +179,18 @@ export interface CSSBlocksEmberOptions {
154
179
}
155
180
156
181
interface CSSBlocksAddon {
182
+ templateCompiler ?: CSSBlocksTemplateCompilerPlugin ;
157
183
findSiblingAddon < AddonType > ( this : ThisAddon < CSSBlocksAddon > , name : string ) : ThisAddon < AddonType > | undefined ;
158
184
getOptions ( this : ThisAddon < CSSBlocksAddon > ) : CSSBlocksEmberOptions ;
159
185
optionsForCacheInvalidation ( this : ThisAddon < CSSBlocksAddon > ) : ObjectDictionary < unknown > ;
160
186
astPluginBuilder ( env : EmberASTPluginEnvironment ) : ASTPlugin ;
161
187
_options ?: CSSBlocksEmberOptions ;
162
188
}
163
189
interface HTMLBarsAddon {
164
- getTemplateCompiler ( inputTree : Tree , htmlbarsOptions : HtmlBarsOptions ) : TemplateCompilerPlugin ;
190
+ getTemplateCompiler ( inputTree : Tree , htmlbarsOptions : TemplateCompilerPlugin . HtmlBarsOptions ) : TemplateCompilerPlugin ;
165
191
}
166
192
167
- function isAddon ( parent : EmberAddon | EmberApp ) : parent is EmberAddon {
193
+ function isAddon ( parent : EmberAddon | EmberApp | Project ) : parent is EmberAddon {
168
194
return ! ! parent [ "findOwnAddonByName" ] ;
169
195
}
170
196
@@ -173,36 +199,52 @@ const EMBER_ADDON: AddonImplementation<CSSBlocksAddon> = {
173
199
174
200
init ( parent , project ) {
175
201
this . _super . init . call ( this , parent , project ) ;
176
- this . app = this . _findHost ( ) ;
177
202
} ,
178
203
179
204
findSiblingAddon ( name ) {
180
205
if ( isAddon ( this . parent ) ) {
181
206
return this . parent . findOwnAddonByName ( name ) ;
182
207
} else {
183
- this . project . findAddonByName ( name ) ;
208
+ return this . project . findAddonByName ( name ) ;
184
209
}
185
210
} ,
186
211
187
212
included ( parent ) {
188
213
this . _super . included . apply ( this , [ parent ] ) ;
214
+ this . app = this . _findHost ( ) ;
215
+ let parentName = typeof parent . name === "string" ? parent . name : parent . name ( ) ;
189
216
this . _options = this . getOptions ( ) ;
190
217
let htmlBarsAddon = this . findSiblingAddon < HTMLBarsAddon > ( "ember-cli-htmlbars" ) ;
191
218
if ( ! htmlBarsAddon ) {
192
- throw new Error ( `Using @css-blocks/ember on ${ this . parent . name } also requires ember-cli-htmlbars to be an addon for ${ this . parent . name } ` ) ;
219
+ throw new Error ( `Using @css-blocks/ember on ${ parentName } also requires ember-cli-htmlbars to be an addon for ${ parentName } (ember-cli-htmlbars should be a dependency in package.json, not a devDependency) ` ) ;
193
220
}
194
- htmlBarsAddon . getTemplateCompiler = ( inputTree : Tree , htmlbarsOptions : HtmlBarsOptions ) => {
195
- return new CSSBlocksTemplateCompilerPlugin ( inputTree , htmlbarsOptions , this . _options ! ) ;
221
+ if ( ! htmlBarsAddon . getTemplateCompiler ) {
222
+ throw new Error ( "This version of ember-cli-htmlbars is not compatible with @css-blocks/ember. Please upgrade." ) ;
223
+ }
224
+ htmlBarsAddon . getTemplateCompiler = ( inputTree : Tree , htmlbarsOptions : TemplateCompilerPlugin . HtmlBarsOptions ) => {
225
+ this . templateCompiler = new CSSBlocksTemplateCompilerPlugin ( inputTree , htmlbarsOptions , this . _options ! ) ;
226
+ return this . templateCompiler ;
196
227
} ;
197
228
} ,
198
229
199
- astPluginBuilder ( env : EmberASTPluginEnvironment ) : ASTPlugin {
200
- let { meta, syntax } = env ;
201
- let moduleName = meta ?. moduleName ;
202
- return {
203
- name : `CSS Blocks AST Plugin for ${ moduleName } ` ,
204
- visitor : moduleName ? new Visitor ( moduleName , syntax ) : NOOP_VISITOR ,
205
- } ;
230
+ astPluginBuilder ( env ) {
231
+ return this . templateCompiler ! . astPluginBuilder ( env ) ;
232
+ } ,
233
+
234
+ preprocessTree ( type , tree ) {
235
+ if ( type !== "css" ) return tree ;
236
+ // We compile CSS Block files in the template tree, so in the CSS Tree all
237
+ // we need to do is prune them out of the build before the tree gets
238
+ // built.
239
+ return withoutCssBlockFiles ( tree ) ;
240
+ } ,
241
+
242
+ postprocessTree ( type , tree ) {
243
+ if ( type !== "template" ) return tree ;
244
+ tree = withoutCssBlockFiles ( tree ) ;
245
+ let parentName = typeof this . parent . name === "string" ? this . parent . name : this . parent . name ( ) ;
246
+ let isAddon = typeof this . parent . name === "string" ;
247
+ return new BroccoliDebug ( tree , `css-blocks:template-output:${ parentName } :${ isAddon ? "addon" : "app" } ` ) ;
206
248
} ,
207
249
208
250
setupPreprocessorRegistry ( type , registry ) {
@@ -250,7 +292,7 @@ const EMBER_ADDON: AddonImplementation<CSSBlocksAddon> = {
250
292
}
251
293
options . parserOpts . outputMode = OutputMode . BEM_UNIQUE ;
252
294
253
- if ( typeof options . output !== "string" ) {
295
+ if ( options . output !== undefined && typeof options . output !== "string" ) {
254
296
throw new Error ( `Invalid css-blocks options in 'ember-cli-build.js': Output must be a string. Instead received ${ options . output } .` ) ;
255
297
}
256
298
return options ;
0 commit comments