Skip to content

[WIP] Ember cli classic #185

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Aug 11, 2018
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
512a11f
feat: Make ember-cli work with classic ember structure.
amiller-gh Jul 11, 2018
463fe10
feat: Finish wiring up analysis and css output for addons.
amiller-gh Jul 12, 2018
7c1c001
feat: Added functionality.
amiller-gh Jul 17, 2018
94a4a1c
fix: Make ember-cli plugin work for Glimmer apps again.
amiller-gh Jul 18, 2018
ad83f78
fix: Improve transports to not duplicate output.
amiller-gh Jul 18, 2018
85772e1
feat: Improvements to ember-cli addon.
amiller-gh Jul 23, 2018
ae2ed0a
fix: Remove last lingering async fs methods.
amiller-gh Jul 23, 2018
f669473
fix: Glimmer works again with broccoli updates.
amiller-gh Jul 23, 2018
769aa6a
chore: Document helper hack for now.
amiller-gh Jul 23, 2018
1b852a4
feat: Tweak Analyzer.analyze method to accept working directory.
amiller-gh Jul 26, 2018
f6f0150
fix: Fix existing tests.
amiller-gh Jul 26, 2018
d77b7d1
feat: Glimmer helper injection.
amiller-gh Jul 29, 2018
9712b70
chore: Depend on experimental fork of glimmer-analyzer for now.
amiller-gh Jul 29, 2018
8362757
feat: Ember Template Discovery Tests.
amiller-gh Jul 31, 2018
fe3be18
fix: Remove addons' treeForAddonStyles to work around ember-cli bug.
amiller-gh Jul 31, 2018
a78802e
fix: Build @css-blocks/glimmer to AMD so Ember can import the runtime.
amiller-gh Jul 31, 2018
58dc295
feat: Engine integration.
amiller-gh Aug 5, 2018
474862c
test: Have Travis use node versions 8 and 10.
amiller-gh Aug 5, 2018
589f283
fix: Ensure file output in broccoli-aggregator.
amiller-gh Aug 5, 2018
5566127
feat: Add global disable option for apps.
amiller-gh Aug 8, 2018
aa702ed
fix: Ensure written output file is not linked to previous tree.
amiller-gh Aug 11, 2018
af52f92
docs: Add ember-cli README.
amiller-gh Aug 11, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"chai": "^3.5.0",
"husky": "^0.14.3",
"istanbul": "^0.4.5",
"lerna": "^3.0.0-beta.1",
"lerna": "^2.11.0",
"loader-utils": "^1.1.0",
"markdown-toc": "^1.2.0",
"mocha": "^3.4.2",
Expand All @@ -36,7 +36,6 @@
"mock-require": "^2.0.2",
"outdent": "^0.4.1",
"perfectionist": "^2.4.0",
"postcss": "^6.0.21",
"prettier": "^1.8.2",
"remap-istanbul": "^0.9.5",
"source-map-support": "^0.4.15",
Expand All @@ -45,7 +44,7 @@
"tslint": "^5.9.1",
"typedoc": "^0.11.0",
"typedoc-plugin-monorepo": "^0.1.0",
"typescript": "^2.8.1",
"typescript": "~2.8.0",
"watch": "^1.0.2"
},
"workspaces": [
Expand Down
1 change: 0 additions & 1 deletion packages/@css-blocks/broccoli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
"debug": "^3.1.0",
"fs-extra": "^5.0.0",
"opticss": "^0.3.0",
"postcss": "^6.0.21",
"recursive-readdir": "^2.2.2",
"walk-sync": "^0.3.2"
}
Expand Down
73 changes: 47 additions & 26 deletions packages/@css-blocks/broccoli/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,73 +1,99 @@
import * as fs from "fs-extra";
import * as path from "path";

import { Analyzer, BlockCompiler, StyleMapping } from "@css-blocks/core";
import { Analyzer, Block, BlockCompiler, StyleMapping } from "@css-blocks/core";
import { TemplateTypes } from "@opticss/template-api";

import * as debugGenerator from "debug";
import { OptiCSSOptions, Optimizer } from "opticss";
import * as postcss from "postcss";
import { OptiCSSOptions, Optimizer, postcss } from "opticss";
import * as readdir from "recursive-readdir";

import { BroccoliPlugin } from "./utils";

const debug = debugGenerator("css-blocks:broccoli");

export interface Transport {
id: string;
mapping?: StyleMapping<keyof TemplateTypes>;
blocks?: Set<Block>;
analyzer?: Analyzer<keyof TemplateTypes>;
css?: string;
}

export interface BroccoliOptions {
entry: string[];
output: string;
root: string;
analyzer: Analyzer<keyof TemplateTypes>;
transport: {[key: string]: object};
transport: Transport;
optimization?: Partial<OptiCSSOptions>;
}

class BroccoliCSSBlocks extends BroccoliPlugin {

private analyzer: Analyzer<keyof TemplateTypes>;
private entry: string[];
private entries: string[];
private output: string;
private transport: { [key: string]: object };
private root: string;
private transport: Transport;
private optimizationOptions: Partial<OptiCSSOptions>;

// tslint:disable-next-line:prefer-whatever-to-any
constructor(inputNode: any, options: BroccoliOptions) {
super([inputNode], { name: "broccoli-css-blocks" });

this.entry = options.entry;
this.output = options.output;
this.analyzer = options.analyzer;
this.entries = options.entry.slice(0);
this.output = options.output || "css-blocks.css";
this.transport = options.transport;
this.optimizationOptions = options.optimization || {};

if (!this.output) {
throw new Error("CSS Blocks Broccoli Plugin requires an output file name.");
}
this.analyzer = options.analyzer;
this.root = options.root || process.cwd();
this.transport.css = this.transport.css ? this.transport.css : "";
}

async build() {
let options = this.analyzer.cssBlocksOptions;
let blockCompiler = new BlockCompiler(postcss, options);
let optimizer = new Optimizer(this.optimizationOptions, this.analyzer.optimizationOptions);

// When no entry points are passed, we treat *every* template as an entry point.
let discover = !this.entries.length;

// This build step is *mostly* just a pass-through of all files!
// QUESTION: Tom, is there a better way to do this in Broccoli?
let files = await readdir(this.inputPaths[0]);
for (let file of files) {
file = path.relative(this.inputPaths[0], file);
await fs.ensureDir(path.join(this.outputPath, path.dirname(file)));

// If we're in Classic or Pods mode, every hbs file is an entry point.
if (discover && path.extname(file) === ".hbs") { this.entries.push(file); }

fs.ensureDirSync(path.join(this.outputPath, path.dirname(file)));
try {
await fs.symlink(
fs.symlinkSync(
path.join(this.inputPaths[0], file),
path.join(this.outputPath, file),
);
} catch (e) {
// tslint:disable-next-line:no-console
console.log("Error linking", path.join(this.inputPaths[0], file), "to output directory.");
}

}

// The glimmer-analyzer package tries to require() package.json
// in the root of the directory it is passed. We pass it our broccoli
// tree, so it needs to contain package.json too.
// TODO: Ideally this is configurable in glimmer-analyzer. We can
// contribute that option back to the project. However,
// other template integrations may want this available too...
fs.writeFileSync(
path.join(this.outputPath, "package.json"),
fs.readFileSync(path.join(this.root, "package.json")),
);

// Oh hey look, we're analyzing.
this.analyzer.reset();
await this.analyzer.analyze(...this.entry);
await this.analyzer.analyze(this.outputPath, this.entries);

// Compile all Blocks and add them as sources to the Optimizer.
// TODO: handle a sourcemap from compiling the block file via a preprocessor.
Expand All @@ -80,10 +106,9 @@ class BroccoliCSSBlocks extends BroccoliPlugin {
let filename = filesystemPath || options.importer.debugIdentifier(block.identifier, options);

// If this Block has a representation on disk, remove it from our output tree.
// TODO: This isn't working right now because `importer.filesystemPath` doesn't return the expected path...
if (filesystemPath) {
debug(`Removing block file ${path.relative(options.rootDir, filesystemPath)} from output.`);
await fs.unlink(path.join(this.outputPath, path.relative(options.rootDir, filesystemPath)));
debug(`Removing block file ${filesystemPath} from output.`);
fs.unlinkSync(filesystemPath);
}

// Add the compiled Block file to the optimizer.
Expand All @@ -106,13 +131,9 @@ class BroccoliCSSBlocks extends BroccoliPlugin {
this.transport.mapping = styleMapping;
this.transport.blocks = blocks;
this.transport.analyzer = this.analyzer;
this.transport.css = optimized.output;
this.transport.css += optimized.output.content.toString();

// Write our compiled CSS to the output tree.
await fs.writeFile(
path.join(this.outputPath, this.output),
optimized.output.content.toString(),
);
debug(`Compilation Finished: ${this.transport.id}`);

}

Expand Down
11 changes: 5 additions & 6 deletions packages/@css-blocks/broccoli/test/plugin-test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as assert from "assert";
import * as fs from "fs-extra";
// import * as path from "path";
import * as path from "path";

import { GlimmerAnalyzer } from "@css-blocks/glimmer";
import { TempDir, buildOutput, createTempDir } from "broccoli-test-helper";
Expand Down Expand Up @@ -50,8 +49,8 @@ describe("Broccoli Plugin Test", function () {
},
});

let transport = {};
let analyzer = new GlimmerAnalyzer(input.path(), {
let transport = { id: "test-transport" };
let analyzer = new GlimmerAnalyzer({}, {}, {
app: { name: "test" },
types: {
stylesheet: { definitiveCollection: "components" },
Expand All @@ -64,18 +63,18 @@ describe("Broccoli Plugin Test", function () {

let compiler = new BroccoliCSSBlocks(input.path(), {
entry: [entryComponentName],
root: path.join(__dirname, "../.."),
output: "css-blocks.css",
transport,
analyzer,
});
let output = await buildOutput(compiler);
await buildOutput(compiler);

assert.ok(Object.keys(transport).length, "Transport Object populated");
assert.ok(transport["mapping"], "Mapping property is populated in Transport Object");
assert.ok(transport["blocks"], "Blocks property is populated in Transport Object");
assert.ok(transport["analyzer"], "Analyzer property is populated in Transport Object");
assert.ok(transport["css"], "CSS property is populated in Transport Object");
assert.equal(await fs.readFile(output.path("css-blocks.css"), "utf8"), transport["css"].content, "CSS File generated");
});
});
});
2 changes: 1 addition & 1 deletion packages/@css-blocks/core/src/Analyzer/Analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export abstract class Analyzer<K extends keyof TemplateTypes> {
this.dynamicStyles = new MultiMap();
}

abstract analyze(...entryPoints: string[]): Promise<Analyzer<K>>;
abstract analyze(dir: string, entryPoints: string[]): Promise<Analyzer<K>>;
abstract get optimizationOptions(): TemplateIntegrationOptions;

// TODO: We don't really want to burn the world here.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ export class ConflictResolver {
}

// Wrap our list of CompoundSelectors in ParsedSelector containers and return.
return mergedSelectors.map(sel => new ParsedSelector(sel));
return mergedSelectors.map(sel => new ParsedSelector(sel, sel.toString()));
}
sourceLocation(block: Block, node: postcss.Node): SourceLocation | undefined {
let blockPath = this.config.importer.debugIdentifier(block.identifier, this.config);
Expand Down
2 changes: 2 additions & 0 deletions packages/@css-blocks/core/src/BlockTree/AttrValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ export class AttrValue extends Style<AttrValue, Block, Attribute, never> {
switch (config.outputMode) {
case OutputMode.BEM:
return `${this.parent.cssClass(config)}${ this.isPresenceRule ? "" : `-${this.value}`}`;
case OutputMode.BEM_UNIQUE:
return `${this.parent.cssClass(config)}${ this.isPresenceRule ? "" : `-${this.value}`}`;
default:
return assertNever(config.outputMode);
}
Expand Down
5 changes: 3 additions & 2 deletions packages/@css-blocks/core/src/BlockTree/Attribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,9 @@ export class Attribute extends Inheritable<Attribute, Block, BlockClass, AttrVal
cssClass(config: ResolvedConfiguration) {
switch (config.outputMode) {
case OutputMode.BEM:
let cssClassName = this.blockClass.cssClass(config);
return `${cssClassName}--${this.token.name}`;
return `${this.blockClass.cssClass(config)}--${this.token.name}`;
case OutputMode.BEM_UNIQUE:
return `${this.blockClass.cssClass(config)}--${this.token.name}`;
default:
return assertNever(config.outputMode);
}
Expand Down
23 changes: 23 additions & 0 deletions packages/@css-blocks/core/src/BlockTree/Block.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as crypto from "crypto";

import { MultiMap, ObjectDictionary } from "@opticss/util";
import { whatever } from "@opticss/util";
import {
Expand All @@ -20,6 +22,24 @@ import { BlockClass } from "./BlockClass";
import { Inheritable } from "./Inheritable";
import { Styles } from "./Styles";

// Random seed for `get_guid`. Consistent for life of process.
const RAND_SEED = Math.floor(Math.random() * 1000);

/**
* Generates a 5 digit, process-wide globally unique
* identifier from a given input identifier. This
* generated identifier hash will remain consistent
* for the life of the process that spawned it.
* @prop identifier string Input Block identifier.
* @returns This Block's guid hash.
*/
function gen_guid(identifier: string): string {
return crypto.createHash("md5")
.update(identifier + RAND_SEED)
.digest("hex")
.slice(0, 5);
}

export class Block
extends Inheritable<Block, Block, never, BlockClass> {

Expand All @@ -29,6 +49,8 @@ export class Block
private _implements: Block[] = [];
private hasHadNameReset = false;

public readonly guid: string;

/**
* array of paths that this block depends on and, if changed, would
* invalidate the compiled css of this block. This is usually only useful in
Expand All @@ -45,6 +67,7 @@ export class Block
this._dependencies = new Set<string>();
this.rootClass = new BlockClass(ROOT_CLASS, this);
this.stylesheet = stylesheet;
this.guid = gen_guid(identifier);
this.addClass(this.rootClass);
}

Expand Down
8 changes: 3 additions & 5 deletions packages/@css-blocks/core/src/BlockTree/BlockClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,9 @@ export class BlockClass extends Style<BlockClass, Block, Block, Attribute> {
public cssClass(config: ResolvedConfiguration): string {
switch (config.outputMode) {
case OutputMode.BEM:
if (this.isRoot) {
return `${this.block.name}`;
} else {
return `${this.block.name}__${this.name}`;
}
return this.isRoot ? `${this.block.name}` : `${this.block.name}__${this.name}`;
case OutputMode.BEM_UNIQUE:
return this.isRoot ? `${this.block.name}_${this.block.guid}` : `${this.block.name}_${this.block.guid}__${this.name}`;
default:
return assertNever(config.outputMode);
}
Expand Down
1 change: 1 addition & 0 deletions packages/@css-blocks/core/src/configuration/OutputMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
*/
export enum OutputMode {
BEM = "BEM",
BEM_UNIQUE = "BEM_UNIQUE",
}
65 changes: 65 additions & 0 deletions packages/@css-blocks/core/test/output-mode-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { assert } from "chai";
import { suite, test } from "mocha-typescript";

import { OutputMode } from "../src/configuration";

import { BEMProcessor } from "./util/BEMProcessor";

// Reduce whitespace.
function minify(s: string) {
return s.replace(/(^[\s\n]+|[\s\n]+$)/gm, " ").replace(/[\s\n][\s\n]+/gm, " ").replace(/\n/gm, " ").trim();
}

@suite("Output mode")
export class BEMOutputMode extends BEMProcessor {
@test "outputs BEM"() {
let filename = "foo/bar/test-block.css";
let inputCSS = `
:scope { color: red; }
:scope[state|active] { color: orange; }
.foo {color: yellow; }
.foo[state|color="green"] { color: green; }
.foo[state|color="blue"] { color: blue; }
`;
return this.process(filename, inputCSS, { outputMode: OutputMode.BEM }).then((result) => {
assert.deepEqual(
minify(result.css.toString()),
minify(`
.test-block { color: red; }
.test-block--active { color: orange; }
.test-block__foo { color: yellow; }
.test-block__foo--color-green { color: green; }
.test-block__foo--color-blue { color: blue; }
`),
);
});
}
@test "outputs BEM_UNIQUE"() {
let filename = "foo/bar/test-block.css";
let inputCSS = `
:scope { color: red; }
:scope[state|active] { color: orange; }
.foo {color: yellow; }
.foo[state|color="green"] { color: green; }
.foo[state|color="blue"] { color: blue; }
`;
return this.process(filename, inputCSS, { outputMode: OutputMode.BEM_UNIQUE }).then((result) => {
let css = result.css.toString();

// Discover the generated GUID for this block.
// It changes every time the process is killed.
let uid = (css.match(/test-block_(.....)/) || [])[1];

assert.deepEqual(
minify(css),
minify(`
.test-block_${uid} { color: red; }
.test-block_${uid}--active { color: orange; }
.test-block_${uid}__foo { color: yellow; }
.test-block_${uid}__foo--color-green { color: green; }
.test-block_${uid}__foo--color-blue { color: blue; }
`),
);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { classnames } from "@css-blocks/helpers/classnames";
import { helper } from "@ember/component/helper";

export default helper(classnames);
Loading