|
| 1 | +import * as path from "path"; |
| 2 | + |
| 3 | +import { Analyzer, BlockCompiler, StyleMapping } from "@css-blocks/core"; |
| 4 | +import { TemplateTypes } from "@opticss/template-api"; |
| 5 | + |
| 6 | +import * as debugGenerator from "debug"; |
| 7 | +import * as fs from "fs-extra"; |
| 8 | +import * as FSTree from "fs-tree-diff"; |
| 9 | +import * as glob from "glob"; |
| 10 | +import { OptiCSSOptions, Optimizer, postcss } from "opticss"; |
| 11 | +import * as walkSync from "walk-sync"; |
| 12 | + |
| 13 | +import { Transport } from "./Transport"; |
| 14 | +import { BroccoliPlugin, symlinkOrCopy } from "./utils"; |
| 15 | + |
| 16 | +const debug = debugGenerator("css-blocks:broccoli"); |
| 17 | + |
| 18 | +export interface BroccoliOptions { |
| 19 | + entry: string[]; |
| 20 | + output: string; |
| 21 | + root: string; |
| 22 | + analyzer: Analyzer<keyof TemplateTypes>; |
| 23 | + optimization?: Partial<OptiCSSOptions>; |
| 24 | +} |
| 25 | + |
| 26 | +/** |
| 27 | + * Runs analysis on an `inputNode` that represents the entire |
| 28 | + * application. `options.transport` will be populated with |
| 29 | + * analysis results. Output is the same application tree |
| 30 | + * with all Block files removed. |
| 31 | + */ |
| 32 | +export class CSSBlocksAnalyze extends BroccoliPlugin { |
| 33 | + |
| 34 | + private analyzer: Analyzer<keyof TemplateTypes>; |
| 35 | + private entries: string[]; |
| 36 | + private output: string; |
| 37 | + private root: string; |
| 38 | + private transport: Transport; |
| 39 | + private optimizationOptions: Partial<OptiCSSOptions>; |
| 40 | + private previous: FSTree = new FSTree(); |
| 41 | + |
| 42 | + /** |
| 43 | + * Initialize this new instance with the app tree, transport, and analysis options. |
| 44 | + * @param inputNode Single Broccoli tree node containing *entire* app. |
| 45 | + * @param transport Magical shared-memory Transport object shared with the aggregator and Template transformer. |
| 46 | + * @param options Analysis options. |
| 47 | + */ |
| 48 | + // tslint:disable-next-line:prefer-whatever-to-any |
| 49 | + constructor(inputNode: any, transport: Transport, options: BroccoliOptions) { |
| 50 | + super([inputNode], { |
| 51 | + name: "broccoli-css-blocks-analyze", |
| 52 | + persistentOutput: true, |
| 53 | + }); |
| 54 | + this.transport = transport; |
| 55 | + this.entries = options.entry.slice(0); |
| 56 | + this.output = options.output || "css-blocks.css"; |
| 57 | + this.optimizationOptions = options.optimization || {}; |
| 58 | + this.analyzer = options.analyzer; |
| 59 | + this.root = options.root || process.cwd(); |
| 60 | + this.transport.css = this.transport.css ? this.transport.css : ""; |
| 61 | + } |
| 62 | + |
| 63 | + /** |
| 64 | + * Re-run the broccoli build over supplied inputs. |
| 65 | + */ |
| 66 | + async build() { |
| 67 | + let input = this.inputPaths[0]; |
| 68 | + let output = this.outputPath; |
| 69 | + let options = this.analyzer.cssBlocksOptions; |
| 70 | + let blockCompiler = new BlockCompiler(postcss, options); |
| 71 | + let optimizer = new Optimizer(this.optimizationOptions, this.analyzer.optimizationOptions); |
| 72 | + |
| 73 | + // Test if anything has changed since last time. If not, skip all analysis work. |
| 74 | + let newFsTree = FSTree.fromEntries(walkSync.entries(input)); |
| 75 | + let diff = this.previous.calculatePatch(newFsTree); |
| 76 | + if (!diff.length) { return; } |
| 77 | + this.previous = newFsTree; |
| 78 | + FSTree.applyPatch(input, output, diff); |
| 79 | + |
| 80 | + // When no entry points are passed, we treat *every* template as an entry point. |
| 81 | + this.entries = this.entries.length ? this.entries : glob.sync("**/*.hbs", { cwd: input }); |
| 82 | + |
| 83 | + // The glimmer-analyzer package tries to require() package.json |
| 84 | + // in the root of the directory it is passed. We pass it our broccoli |
| 85 | + // tree, so it needs to contain package.json too. |
| 86 | + // TODO: Ideally this is configurable in glimmer-analyzer. We can |
| 87 | + // contribute that option back to the project. However, |
| 88 | + // other template integrations may want this available too... |
| 89 | + let pjsonLink = path.join(input, "package.json"); |
| 90 | + if (!fs.existsSync(pjsonLink)) { |
| 91 | + symlinkOrCopy(path.join(this.root, "package.json"), pjsonLink); |
| 92 | + } |
| 93 | + |
| 94 | + // Oh hey look, we're analyzing. |
| 95 | + this.analyzer.reset(); |
| 96 | + this.transport.reset(); |
| 97 | + await this.analyzer.analyze(input, this.entries); |
| 98 | + |
| 99 | + // Compile all Blocks and add them as sources to the Optimizer. |
| 100 | + // TODO: handle a sourcemap from compiling the block file via a preprocessor. |
| 101 | + let blocks = this.analyzer.transitiveBlockDependencies(); |
| 102 | + for (let block of blocks) { |
| 103 | + if (block.stylesheet) { |
| 104 | + let root = blockCompiler.compile(block, block.stylesheet, this.analyzer); |
| 105 | + let result = root.toResult({ to: this.output, map: { inline: false, annotation: false } }); |
| 106 | + let filesystemPath = options.importer.filesystemPath(block.identifier, options); |
| 107 | + let filename = filesystemPath || options.importer.debugIdentifier(block.identifier, options); |
| 108 | + |
| 109 | + // If this Block has a representation on disk, remove it from our output tree. |
| 110 | + if (filesystemPath) { |
| 111 | + let outputStylesheet = path.join(output, path.relative(input, filesystemPath)); |
| 112 | + debug(`Removing block file ${outputStylesheet} from output.`); |
| 113 | + if (fs.existsSync(outputStylesheet)) { fs.removeSync(outputStylesheet); } |
| 114 | + } |
| 115 | + |
| 116 | + // Add the compiled Block file to the optimizer. |
| 117 | + optimizer.addSource({ |
| 118 | + content: result.css, |
| 119 | + filename, |
| 120 | + sourceMap: result.map.toJSON(), |
| 121 | + }); |
| 122 | + } |
| 123 | + } |
| 124 | + |
| 125 | + // Add each Analysis to the Optimizer. |
| 126 | + this.analyzer.eachAnalysis((a) => optimizer.addAnalysis(a.forOptimizer(options))); |
| 127 | + |
| 128 | + // Run optimization and compute StyleMapping. |
| 129 | + let optimized = await optimizer.optimize(this.output); |
| 130 | + let styleMapping = new StyleMapping<keyof TemplateTypes>(optimized.styleMapping, blocks, options, this.analyzer.analyses()); |
| 131 | + |
| 132 | + // Attach all computed data to our magic shared memory transport object... |
| 133 | + this.transport.mapping = styleMapping; |
| 134 | + this.transport.blocks = blocks; |
| 135 | + this.transport.analyzer = this.analyzer; |
| 136 | + this.transport.css += optimized.output.content.toString(); |
| 137 | + |
| 138 | + debug(`Compilation Finished: ${this.transport.id}`); |
| 139 | + |
| 140 | + } |
| 141 | + |
| 142 | +} |
0 commit comments