Skip to content

Commit a7a3e8f

Browse files
committed
feat: Option handling. Integration with ember-cli-htmlbars.
1 parent 974ff3d commit a7a3e8f

File tree

6 files changed

+246
-19
lines changed

6 files changed

+246
-19
lines changed

Diff for: packages/@css-blocks/ember/package.json

+6
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,20 @@
3838
"@types/glob": "^7.1.1",
3939
"broccoli-node-api": "^1.7.0",
4040
"broccoli-test-helper": "^2.0.0",
41+
"ember-cli-htmlbars": "^4.3.1",
4142
"typescript": "~3.8.3",
4243
"watch": "^1.0.2"
4344
},
45+
"peerDependencies": {
46+
"ember-cli-htmlbars": "^4.3.1"
47+
},
4448
"dependencies": {
49+
"@css-blocks/config": "^1.0.0",
4550
"@css-blocks/core": "^1.0.0",
4651
"@glimmer/compiler": "^0.43.0",
4752
"@glimmer/syntax": "^0.43.0",
4853
"@opticss/template-api": "^0.6.3",
54+
"@opticss/util": "^0.7.0",
4955
"broccoli-funnel": "^3.0.0",
5056
"broccoli-merge-trees": "^4.0.0",
5157
"broccoli-plugin": "^4.0.0",

Diff for: packages/@css-blocks/ember/src/index.ts

+182-5
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,196 @@
1-
import { AddonImplementation } from "ember-cli/lib/models/addon";
1+
import config from "@css-blocks/config";
2+
import { AnalysisOptions, NodeJsImporter, Options as ParserOptions, OutputMode } from "@css-blocks/core";
3+
import type { AST, ASTPlugin, ASTPluginEnvironment, NodeVisitor, Syntax } from "@glimmer/syntax";
4+
import { ObjectDictionary } from "@opticss/util";
5+
import type { InputNode } from "broccoli-node-api";
6+
import TemplateCompilerPlugin, { HtmlBarsOptions } from "ember-cli-htmlbars/lib/template-compiler-plugin";
7+
import type EmberApp from "ember-cli/lib/broccoli/ember-app";
8+
import type EmberAddon from "ember-cli/lib/models/addon";
9+
import type { AddonImplementation, ThisAddon, Tree } from "ember-cli/lib/models/addon";
10+
import * as FSTree from "fs-tree-diff";
11+
import { OptiCSSOptions } from "opticss";
12+
13+
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
14+
15+
const BLOCK_GLOB = "**/*.block.{css,scss,sass,less,styl}";
16+
interface EmberASTPluginEnvironment extends ASTPluginEnvironment {
17+
meta?: {
18+
moduleName?: string;
19+
};
20+
}
21+
22+
class Visitor implements NodeVisitor {
23+
moduleName: string;
24+
syntax: Syntax;
25+
constructor(moduleName: string, syntax: Syntax) {
26+
this.moduleName = moduleName;
27+
this.syntax = syntax;
28+
}
29+
ElementNode(node: AST.ElementNode) {
30+
console.log(`visited ${this.syntax.print(node)}`);
31+
}
32+
}
33+
const NOOP_VISITOR = {};
34+
35+
class CSSBlocksTemplateCompilerPlugin extends TemplateCompilerPlugin {
36+
previousSourceTree: FSTree;
37+
cssBlocksOptions: CSSBlocksEmberOptions;
38+
constructor(inputTree: InputNode, htmlbarsOptions: HtmlBarsOptions, cssBlocksOptions: CSSBlocksEmberOptions) {
39+
super(inputTree, htmlbarsOptions);
40+
this.cssBlocksOptions = cssBlocksOptions;
41+
this.previousSourceTree = new FSTree();
42+
}
43+
async build() {
44+
let cssBlockEntries = this.input.entries(".", [BLOCK_GLOB]);
45+
let currentFSTree = FSTree.fromEntries(cssBlockEntries);
46+
let patch = this.previousSourceTree.calculatePatch(currentFSTree);
47+
let removedFiles = patch.filter((change) => change[0] === "unlink");
48+
if (cssBlockEntries.length === 0 && removedFiles.length > 0) {
49+
console.warn(`[WARN] ${removedFiles[0][1]} was just removed and the output directory was not cleaned up.`);
50+
}
51+
if (cssBlockEntries.length > 0) {
52+
}
53+
await super.build();
54+
if (cssBlockEntries.length > 0) {
55+
await super.build();
56+
}
57+
}
58+
}
59+
60+
/**
61+
* The options that can be passed for css blocks to an ember-cli application.
62+
*/
63+
export interface CSSBlocksEmberAppOptions {
64+
"css-blocks"?: CSSBlocksEmberOptions;
65+
}
66+
67+
export interface CSSBlocksEmberOptions {
68+
output?: string;
69+
aliases?: ObjectDictionary<string>;
70+
analysisOpts?: AnalysisOptions;
71+
parserOpts?: Writeable<ParserOptions>;
72+
optimization?: Partial<OptiCSSOptions>;
73+
}
274

375
interface CSSBlocksAddon {
76+
findSiblingAddon<AddonType>(this: ThisAddon<CSSBlocksAddon>, name: string): ThisAddon<AddonType> | undefined;
77+
getOptions(this: ThisAddon<CSSBlocksAddon>): CSSBlocksEmberOptions;
78+
optionsForCacheInvalidation(this: ThisAddon<CSSBlocksAddon>): ObjectDictionary<unknown>;
79+
astPluginBuilder(env: EmberASTPluginEnvironment): ASTPlugin;
80+
_options?: CSSBlocksEmberOptions;
81+
}
82+
interface HTMLBarsAddon {
83+
getTemplateCompiler(inputTree: Tree, htmlbarsOptions: HtmlBarsOptions): TemplateCompilerPlugin;
84+
}
85+
86+
function isAddon(parent: EmberAddon | EmberApp): parent is EmberAddon {
87+
return !!parent["findOwnAddonByName"];
488
}
589

690
const EMBER_ADDON: AddonImplementation<CSSBlocksAddon> = {
791
name: "@css-blocks/ember",
92+
893
init(parent, project) {
9-
return this._super.init && this._super.init.call(this, parent, project);
94+
this._super.init.call(this, parent, project);
95+
this.app = this._findHost();
1096
},
97+
98+
findSiblingAddon(name) {
99+
if (isAddon(this.parent)) {
100+
return this.parent.findOwnAddonByName(name);
101+
} else {
102+
this.project.findAddonByName(name);
103+
}
104+
},
105+
11106
included(parent) {
12107
this._super.included.apply(this, [parent]);
108+
this._options = this.getOptions();
109+
let htmlBarsAddon = this.findSiblingAddon<HTMLBarsAddon>("ember-cli-htmlbars");
110+
if (!htmlBarsAddon) {
111+
throw new Error(`Using @css-blocks/ember on ${this.parent.name} also requires ember-cli-htmlbars to be an addon for ${this.parent.name}`);
112+
}
113+
htmlBarsAddon.getTemplateCompiler = (inputTree: Tree, htmlbarsOptions: HtmlBarsOptions) => {
114+
return new CSSBlocksTemplateCompilerPlugin(inputTree, htmlbarsOptions, this._options!);
115+
};
116+
},
117+
118+
astPluginBuilder(env: EmberASTPluginEnvironment): ASTPlugin {
119+
let {meta, syntax } = env;
120+
let moduleName = meta?.moduleName;
121+
return {
122+
name: `CSS Blocks AST Plugin for ${moduleName}`,
123+
visitor: moduleName ? new Visitor(moduleName, syntax) : NOOP_VISITOR,
124+
};
125+
},
126+
127+
setupPreprocessorRegistry(type, registry) {
128+
if (type !== "parent") { return; }
129+
// For Ember
130+
registry.add("htmlbars-ast-plugin", {
131+
name: "css-blocks-htmlbars",
132+
plugin: this.astPluginBuilder.bind(this),
133+
dependencyInvalidation: true,
134+
cacheKey: () => this.optionsForCacheInvalidation(),
135+
baseDir: () => __dirname,
136+
});
137+
},
138+
139+
getOptions() {
140+
let app = this.app!;
141+
let root = app.project.root;
142+
let appOptions = app.options;
143+
144+
if (!appOptions["css-blocks"]) {
145+
appOptions["css-blocks"] = {};
146+
}
147+
148+
// Get CSS Blocks options provided by the application, if present.
149+
const options = <CSSBlocksEmberOptions>appOptions["css-blocks"]; // Do not clone! Contains non-json-safe data.
150+
if (!options.aliases) options.aliases = {};
151+
if (!options.analysisOpts) options.analysisOpts = {};
152+
if (!options.optimization) options.optimization = {};
153+
154+
if (!options.parserOpts) {
155+
options.parserOpts = config.searchSync(root) || {};
156+
}
157+
158+
// Use the node importer by default.
159+
options.parserOpts.importer = options.parserOpts.importer || new NodeJsImporter(options.aliases);
160+
161+
// Optimization is always disabled for now, until we get project-wide analysis working.
162+
if (typeof options.optimization.enabled === "undefined") {
163+
options.optimization.enabled = app.isProduction;
164+
}
165+
166+
// Update parserOpts to include the absolute path to our application code directory.
167+
if (!options.parserOpts.rootDir) {
168+
options.parserOpts.rootDir = root;
169+
}
170+
options.parserOpts.outputMode = OutputMode.BEM_UNIQUE;
171+
172+
if (typeof options.output !== "string") {
173+
throw new Error(`Invalid css-blocks options in 'ember-cli-build.js': Output must be a string. Instead received ${options.output}.`);
174+
}
175+
return options;
176+
},
177+
178+
optionsForCacheInvalidation() {
179+
let aliases = this._options!.aliases;
180+
let analysisOpts = this._options!.analysisOpts;
181+
let optimization = this._options!.optimization;
182+
let parserOpts: Writeable<ParserOptions> & {importerName?: string} = {};
183+
Object.assign(parserOpts, this._options!.parserOpts);
184+
let constructor = parserOpts.importer && parserOpts.importer.constructor;
185+
parserOpts.importerName = constructor && constructor.name;
186+
187+
return {
188+
aliases,
189+
analysisOpts,
190+
optimization,
191+
parserOpts,
192+
};
13193
},
14-
compileTemplates(addonTree) {
15-
return this._super.compileTemplates.call(this, addonTree);
16-
}
17194
};
18195

19196
module.exports = EMBER_ADDON;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import Plugin from "broccoli-plugin";
2+
declare module "broccoli-persistent-filter" {
3+
class Filter extends Plugin {
4+
5+
}
6+
export = Filter;
7+
}
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,31 @@
1+
import { ASTPluginBuilder } from '@glimmer/syntax';
2+
13
// from https://github.com/typed-ember/ember-cli-typescript/blob/master/ts/types/ember-cli-preprocess-registry/index.d.ts
24
declare module 'ember-cli-preprocess-registry' {
35
import { Node as BroccoliNode } from 'broccoli-node-api';
46

57
export = PreprocessRegistry;
68

79
class PreprocessRegistry {
8-
add(type: string, plugin: PreprocessPlugin): void;
9-
load(type: string): Array<PreprocessPlugin>;
10+
add(type: string, plugin: Plugin): void;
11+
load(type: string): Array<Plugin>;
1012
extensionsForType(type: string): Array<string>;
11-
remove(type: string, plugin: PreprocessPlugin): void;
13+
remove(type: string, plugin: Plugin): void;
1214
}
1315

16+
type Plugin = PreprocessPlugin | ASTPlugin;
17+
1418
interface PreprocessPlugin {
1519
name: string;
1620
toTree(input: BroccoliNode, path: string): BroccoliNode;
21+
[key: string]: unknown;
22+
}
23+
24+
interface ASTPlugin {
25+
name: string;
26+
plugin: ASTPluginBuilder;
27+
dependencyInvalidation?: boolean,
28+
cacheKey?: () => object,
29+
baseDir?: () => string,
1730
}
1831
}

Diff for: packages/@css-blocks/ember/src/types/ember-cli/index.d.ts

+10-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
declare module 'ember-cli/lib/broccoli/ember-app' {
22
import CoreObject from 'core-object';
3+
import Project from 'ember-cli/lib/models/project';
34

45
export default class EmberApp extends CoreObject {
56
options: Record<string, unknown>;
7+
name: string;
8+
project: Project;
9+
isProduction: boolean;
610
}
711
}
812

@@ -51,7 +55,7 @@ declare module 'ember-cli/lib/models/addon' {
5155
/**
5256
* Find an addon of the current addon.
5357
*/
54-
findOwnAddonByName?(this: ThisAddon<A>): EmberAddon;
58+
findOwnAddonByName?(this: ThisAddon<A>, name: string): EmberAddon;
5559
/**
5660
* Check if the current addon intends to be hinted. Typically this is for hinting/linting libraries such as eslint or jshint.
5761
*/
@@ -137,11 +141,10 @@ declare module 'ember-cli/lib/models/addon' {
137141
* Can be used to exclude addons from being added as a child addon.
138142
*/
139143
shouldIncludeChildAddon?(this: ThisAddon<A>, addon: Addon): boolean;
140-
141144
/**
142145
* This method is called when the addon is included in a build. You would typically use this hook to perform additional imports.
143146
*/
144-
included?(this: ThisAddon<A>, includer: EmberApp | Project): void;
147+
included?(this: ThisAddon<A>, includer: EmberApp | Addon): void;
145148
/**
146149
* Allows the specification of custom addon commands. Expects you to return an object whose key is the name of the command and value is the command instance.
147150
*/
@@ -205,7 +208,7 @@ declare module 'ember-cli/lib/models/addon' {
205208
name: string;
206209
root: string;
207210
app?: EmberApp;
208-
parent: Addon | Project;
211+
parent: Addon | EmberApp;
209212
project: Project;
210213
addons: Addon[];
211214
ui: UI;
@@ -243,7 +246,7 @@ declare module 'ember-cli/lib/models/addon' {
243246
/**
244247
* Find an addon of the current addon.
245248
*/
246-
findOwnAddonByName(): EmberAddon;
249+
findOwnAddonByName(name: string): EmberAddon;
247250
/**
248251
* Check if the current addon intends to be hinted. Typically this is for hinting/linting libraries such as eslint or jshint.
249252
*/
@@ -336,7 +339,7 @@ declare module 'ember-cli/lib/models/addon' {
336339
/**
337340
* This method is called when the addon is included in a build. You would typically use this hook to perform additional imports.
338341
*/
339-
included(includer: EmberApp | Project): void;
342+
included(includer: EmberApp | Addon): void;
340343
}
341344
}
342345

@@ -394,5 +397,6 @@ declare module 'ember-cli/lib/models/project' {
394397
name(): string;
395398
isEmberCLIAddon(): boolean;
396399
require(module: string): unknown;
400+
findAddonByName(name): Addon;
397401
}
398402
}

Diff for: yarn.lock

+25-5
Original file line numberDiff line numberDiff line change
@@ -8213,6 +8213,26 @@ ember-cli-htmlbars@^4.0.0, ember-cli-htmlbars@^4.2.2, ember-cli-htmlbars@^4.2.3:
82138213
strip-bom "^4.0.0"
82148214
walk-sync "^2.0.2"
82158215

8216+
ember-cli-htmlbars@^4.3.1:
8217+
version "4.3.1"
8218+
resolved "https://registry.npmjs.org/ember-cli-htmlbars/-/ember-cli-htmlbars-4.3.1.tgz#4af8adc21ab3c4953f768956b7f7d207782cb175"
8219+
integrity sha512-CW6AY/yzjeVqoRtItOKj3hcYzc5dWPRETmeCzr2Iqjt5vxiVtpl0z5VTqHqIlT5fsFx6sGWBQXNHIe+ivYsxXQ==
8220+
dependencies:
8221+
"@ember/edition-utils" "^1.2.0"
8222+
babel-plugin-htmlbars-inline-precompile "^3.0.1"
8223+
broccoli-debug "^0.6.5"
8224+
broccoli-persistent-filter "^2.3.1"
8225+
broccoli-plugin "^3.1.0"
8226+
common-tags "^1.8.0"
8227+
ember-cli-babel-plugin-helpers "^1.1.0"
8228+
fs-tree-diff "^2.0.1"
8229+
hash-for-dep "^1.5.1"
8230+
heimdalljs-logger "^0.1.10"
8231+
json-stable-stringify "^1.0.1"
8232+
semver "^6.3.0"
8233+
strip-bom "^4.0.0"
8234+
walk-sync "^2.0.2"
8235+
82168236
ember-cli-inject-live-reload@^1.4.1, ember-cli-inject-live-reload@^1.8.2:
82178237
version "1.10.2"
82188238
resolved "https://registry.npmjs.org/ember-cli-inject-live-reload/-/ember-cli-inject-live-reload-1.10.2.tgz#43c59f7f1d1e717772da32e5e81d948fb9fe7c94"
@@ -18713,11 +18733,6 @@ [email protected]:
1871318733
resolved "https://registry.npmjs.org/typescript/-/typescript-3.2.4.tgz#c585cb952912263d915b462726ce244ba510ef3d"
1871418734
integrity sha512-0RNDbSdEokBeEAkgNbxJ+BLwSManFy9TeXz8uW+48j/xhEXv1ePME60olyzw2XzUqUBNAYFeJadIqAgNqIACwg==
1871518735

18716-
typescript@^3.8.3:
18717-
version "3.8.3"
18718-
resolved "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061"
18719-
integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==
18720-
1872118736
typescript@~3.5.3:
1872218737
version "3.5.3"
1872318738
resolved "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977"
@@ -18728,6 +18743,11 @@ typescript@~3.8:
1872818743
resolved "https://registry.npmjs.org/typescript/-/typescript-3.8.2.tgz#91d6868aaead7da74f493c553aeff76c0c0b1d5a"
1872918744
integrity sha512-EgOVgL/4xfVrCMbhYKUQTdF37SQn4Iw73H5BgCrF1Abdun7Kwy/QZsE/ssAy0y4LxBbvua3PIbFsbRczWWnDdQ==
1873018745

18746+
typescript@~3.8.3:
18747+
version "3.8.3"
18748+
resolved "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061"
18749+
integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==
18750+
1873118751
uc.micro@^1.0.1, uc.micro@^1.0.5:
1873218752
version "1.0.6"
1873318753
resolved "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"

0 commit comments

Comments
 (0)