-
Notifications
You must be signed in to change notification settings - Fork 152
/
Copy pathindex.ts
156 lines (130 loc) · 5.48 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import {
Analysis,
Analyzer,
Block,
} from "@css-blocks/core";
import { TemplateIntegrationOptions } from "@opticss/template-api";
import { some, unwrap } from "@opticss/util";
import traverse from "babel-traverse";
import * as babylon from "babylon";
import * as debugGenerator from "debug";
import * as fs from "fs-extra";
import * as path from "path";
import { CssBlocksJSXOptions } from "../options";
import { JSXParseError } from "../utils/Errors";
import { JSXTemplate, TEMPLATE_TYPE } from "./Template";
import { elementVisitor, importVisitor } from "./visitors";
const debug = debugGenerator("css-blocks:jsx:Analyzer");
export type JSXAnalysis = Analysis<TEMPLATE_TYPE>;
export class CSSBlocksJSXAnalyzer extends Analyzer<TEMPLATE_TYPE> {
private options: CssBlocksJSXOptions;
public name: string;
public analysisPromises: Map<string, Promise<JSXAnalysis>>;
public blockPromises: Map<string, Promise<Block>>;
constructor(name: string, options: Partial<CssBlocksJSXOptions> = {}) {
let opts = new CssBlocksJSXOptions(options);
super(opts.compilationOptions);
this.name = name;
this.options = opts;
this.analysisPromises = new Map();
this.blockPromises = new Map();
}
public reset() {
super.reset();
this.analysisPromises = new Map();
this.blockPromises = new Map();
}
get optimizationOptions(): TemplateIntegrationOptions {
return {
rewriteIdents: {
id: false,
class: true,
omitIdents: {
id: [],
class: [],
},
},
analyzedAttributes: ["class"],
analyzedTagnames: false,
};
}
async analyze(dir: string, entryPoints: string[]): Promise<CSSBlocksJSXAnalyzer> {
if (!entryPoints.length) {
throw new JSXParseError("CSS Blocks JSX Analyzer must be passed at least one entry point.");
}
let promises: Promise<JSXAnalysis>[] = [];
for (let entryPoint of entryPoints) {
promises.push(this.parseFile(path.join(dir, entryPoint)));
}
await Promise.all(promises);
debug(`Found ${this.analysisPromises.size} analysis promises`);
return this;
}
private async crawl(template: JSXTemplate): Promise<JSXAnalysis> {
// If we're already analyzing this template, return the existing analysis promise.
if (this.analysisPromises.has(template.identifier)) {
return this.analysisPromises.get(template.identifier)!;
}
// Change our process working directory so relative node resolves work.
let oldDir = process.cwd();
process.chdir(this.options.baseDir);
let analysis: JSXAnalysis = this.newAnalysis(template);
// Parse the file into an AST.
try {
analysis.template.ast = some(babylon.parse(template.data, this.options.parserOptions));
} catch (e) {
process.chdir(oldDir);
throw new JSXParseError(`Error parsing '${template.identifier}'\n${e.message}\n\n${template.data}: ${e.message}`, { filename: template.identifier });
}
// The blocks importer will insert a promise that resolves to a `ResolvedBlock`
// for each CSS Blocks import it encounters. Every new `tsx` or `jsx` file discovered
// will kick of another `Analyzer.parse()` for that file.
let blockPromises: Promise<Block>[] = [];
let childTemplatePromises: Promise<JSXAnalysis>[] = [];
traverse(unwrap(analysis.template.ast), importVisitor(template, this, analysis, blockPromises, childTemplatePromises, this.options));
// Once all blocks this file is waiting for resolve, resolve with the File object.
// After import traversal, it is safe to move back to our old working directory.
process.chdir(oldDir);
// Wait for all block promises to resolve then resolve with the finished analysis.
debug(`Waiting for ${blockPromises.length} Block imported by "${template.identifier}" to finish compilation.`);
await Promise.all(blockPromises);
debug(`Waiting for ${childTemplatePromises.length} child templates to finish analysis before analysis of ${template.identifier}.`);
await Promise.all(childTemplatePromises);
debug(`All child compilations finished for "${template.identifier}".`);
return analysis;
}
/**
* Provided a code string, return a promise for the fully parsed analytics object.
* @param data The code string to parse.
* @param opts Optional analytics parser options.
*/
public async parse(filename: string, data: string): Promise<JSXAnalysis> {
let template: JSXTemplate = new JSXTemplate(filename, data);
debug(`Beginning imports crawl of ${filename}.`);
let analysisPromise = this.crawl(template);
this.analysisPromises.set(template.identifier, analysisPromise);
let analysis = await analysisPromise;
debug(`Finished imports crawl of ${filename}.`);
traverse(unwrap(analysis.template.ast), elementVisitor(analysis));
// No need to keep detailed template data anymore!
delete analysis.template.ast;
delete analysis.template.data;
return analysis;
}
/**
* Provided a file path, return a promise for the fully parsed analytics object.
* // TODO: Make streaming?
* @param file The file path to read in and parse.
* @param opts Optional analytics parser options.
*/
public async parseFile(file: string): Promise<JSXAnalysis> {
let data;
file = path.resolve(this.options.baseDir, file);
try {
data = await fs.readFile(file, "utf8");
} catch (err) {
throw new JSXParseError(`Cannot read JSX entry point file ${file}: ${err.message}`, { filename: file });
}
return this.parse(file, data);
}
}