|
| 1 | +import * as fs from "fs-extra"; |
| 2 | +import * as path from "path"; |
| 3 | + |
| 4 | +import { Analyzer, BlockCompiler, StyleMapping } from "css-blocks"; |
| 5 | +import { Optimizer } from "opticss"; |
| 6 | +import * as postcss from "postcss"; |
| 7 | +import * as readdir from "recursive-readdir"; |
| 8 | + |
| 9 | +import { TemplateTypes } from "@opticss/template-api"; |
| 10 | + |
| 11 | +import { BroccoliPlugin } from "./utils"; |
| 12 | + |
| 13 | +export interface BroccoliOptions { |
| 14 | + entry: string[]; |
| 15 | + output: string; |
| 16 | + analyzer: Analyzer<keyof TemplateTypes>; |
| 17 | + transport: {[key: string]: object}; |
| 18 | +} |
| 19 | + |
| 20 | +class BroccoliCSSBlocks extends BroccoliPlugin { |
| 21 | + |
| 22 | + private analyzer: Analyzer<keyof TemplateTypes>; |
| 23 | + private entry: string[]; |
| 24 | + private output: string; |
| 25 | + private transport: { [key: string]: object }; |
| 26 | + private optimizationOptions: object = {}; |
| 27 | + |
| 28 | + // tslint:disable-next-line:prefer-whatever-to-any |
| 29 | + constructor(inputNode: any, options: BroccoliOptions) { |
| 30 | + super([inputNode], { name: "broccoli-css-blocks" }); |
| 31 | + |
| 32 | + this.entry = options.entry; |
| 33 | + this.output = options.output; |
| 34 | + this.analyzer = options.analyzer; |
| 35 | + this.transport = options.transport; |
| 36 | + |
| 37 | + if (!this.output) { |
| 38 | + throw new Error("CSS Blocks Broccoli Plugin requires an output file name."); |
| 39 | + } |
| 40 | + } |
| 41 | + |
| 42 | + async build() { |
| 43 | + let options = this.analyzer.cssBlocksOptions; |
| 44 | + let blockCompiler = new BlockCompiler(postcss, options); |
| 45 | + let optimizer = new Optimizer(this.optimizationOptions, this.analyzer.optimizationOptions); |
| 46 | + |
| 47 | + // This build step is *mostly* just a pass-through of all files! |
| 48 | + // QUESTION: Tom, is there a better way to do this in Broccoli? |
| 49 | + let files = await readdir(this.inputPaths[0]); |
| 50 | + for (let file of files) { |
| 51 | + file = path.relative(this.inputPaths[0], file); |
| 52 | + await fs.ensureDir(path.join(this.outputPath, path.dirname(file))); |
| 53 | + try { |
| 54 | + await fs.symlink( |
| 55 | + path.join(this.inputPaths[0], file), |
| 56 | + path.join(this.outputPath, file), |
| 57 | + ); |
| 58 | + } catch (e) { |
| 59 | + console.log("Error linking", path.join(this.inputPaths[0], file), "to output directory."); |
| 60 | + } |
| 61 | + } |
| 62 | + |
| 63 | + // Oh hey look, we're analyzing. |
| 64 | + await this.analyzer.analyze(...this.entry); |
| 65 | + |
| 66 | + // Compile all Blocks and add them as sources to the Optimizer. |
| 67 | + // TODO: handle a sourcemap from compiling the block file via a preprocessor. |
| 68 | + let blocks = this.analyzer.transitiveBlockDependencies(); |
| 69 | + for (let block of blocks) { |
| 70 | + if (block.stylesheet) { |
| 71 | + let root = blockCompiler.compile(block, block.stylesheet, this.analyzer); |
| 72 | + let result = root.toResult({ to: this.output, map: { inline: false, annotation: false } }); |
| 73 | + let filesystemPath = options.importer.filesystemPath(block.identifier, options); |
| 74 | + let filename = filesystemPath || options.importer.debugIdentifier(block.identifier, options); |
| 75 | + |
| 76 | + // If this Block has a representation on disk, remove it from our output tree. |
| 77 | + // TODO: This isn't working right now because `importer.filesystemPath` doesn't return the expected path... |
| 78 | + if (filesystemPath) { |
| 79 | + await fs.remove(path.join(this.outputPath, path.relative(options.rootDir, filesystemPath))); |
| 80 | + } |
| 81 | + |
| 82 | + // Add the compiled Block file to the optimizer. |
| 83 | + optimizer.addSource({ |
| 84 | + content: result.css, |
| 85 | + filename, |
| 86 | + sourceMap: result.map.toJSON(), |
| 87 | + }); |
| 88 | + } |
| 89 | + } |
| 90 | + |
| 91 | + // Add each Analysis to the Optimizer. |
| 92 | + this.analyzer.eachAnalysis((a) => optimizer.addAnalysis(a.forOptimizer(options))); |
| 93 | + |
| 94 | + // Run optimization and compute StyleMapping. |
| 95 | + let optimized = await optimizer.optimize(this.output); |
| 96 | + let styleMapping = new StyleMapping(optimized.styleMapping, blocks, options, this.analyzer.analyses()); |
| 97 | + |
| 98 | + // Attach all computed data to our magic shared memory transport object... |
| 99 | + this.transport.mapping = styleMapping; |
| 100 | + this.transport.blocks = blocks; |
| 101 | + this.transport.analyzer = this.analyzer; |
| 102 | + this.transport.css = optimized.output; |
| 103 | + |
| 104 | + // Write our compiled CSS to the output tree. |
| 105 | + // QUESTION: GUH! TOM! THIS DOESN'T APPEAR IN THE OUTPUT TREE! |
| 106 | + await fs.outputFile( |
| 107 | + path.join(this.outputPath, this.output), |
| 108 | + optimized.output.content.toString(), |
| 109 | + ); |
| 110 | + |
| 111 | + } |
| 112 | + |
| 113 | +} |
| 114 | + |
| 115 | +export { BroccoliCSSBlocks }; |
0 commit comments