UNPKG

11.4 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const tslib_1 = require("tslib");
4const cli_utils_1 = require("@design-systems/cli-utils");
5const fs_1 = tslib_1.__importDefault(require("fs"));
6const path_1 = tslib_1.__importDefault(require("path"));
7const change_case_1 = require("change-case");
8const typescript_1 = tslib_1.__importDefault(require("typescript"));
9const postcss_1 = tslib_1.__importDefault(require("postcss"));
10const postcss_icss_selectors_1 = tslib_1.__importDefault(require("postcss-icss-selectors"));
11const icss_utils_1 = require("icss-utils");
12const minimatch_1 = tslib_1.__importDefault(require("minimatch"));
13const postcss_2 = require("./postcss");
14const CSS_EXTENSION_REGEX = /\.css['"]$/;
15const FORMAT_HOST = {
16 /** Implement ts compiler getCurrentDirectory */
17 getCurrentDirectory: () => typescript_1.default.sys.getCurrentDirectory(),
18 /** Implement ts compiler getNewLine */
19 getNewLine: () => typescript_1.default.sys.newLine,
20 /** Implement ts compiler getCanonicalFileName */
21 getCanonicalFileName: (filename) => typescript_1.default.sys.useCaseSensitiveFileNames ? filename : filename.toLowerCase()
22};
23/** Determine the relative file name to the file */
24function resolveCssPath(cssPath, { fileName }) {
25 const resolvedPath = cssPath.substring(1, cssPath.length - 1);
26 if (resolvedPath.startsWith('.')) {
27 const sourcePath = fileName;
28 return path_1.default.resolve(path_1.default.dirname(sourcePath), resolvedPath);
29 }
30 return resolvedPath;
31}
32/** Find a path relative to wherever the script was ran */
33function relativeFile(file) {
34 return Object.assign(Object.assign({}, file), { fileName: path_1.default.relative(process.env.INIT_CWD || process.cwd(), file.fileName) });
35}
36/** Find and process all the css files in a typescript AST */
37async function processCss(processor, project) {
38 const ignore = ['node_modules', '.d.ts'];
39 const files = project
40 .getSourceFiles()
41 .filter(f => !ignore.find(i => f.fileName.includes(i)));
42 const pendingCssResults = new Map();
43 const cssPromises = files.map(async (file) => {
44 const styles = new Map();
45 const results = [];
46 // 1. Find all css imports
47 file.forEachChild(node => {
48 // Dealing with "import * as css from 'foo.css'" only since namedImports variables get mangled
49 if (typescript_1.default.isImportDeclaration(node) &&
50 node.importClause &&
51 CSS_EXTENSION_REGEX.test(node.moduleSpecifier.getText())) {
52 const { importClause } = node;
53 const cssPath = resolveCssPath(node.moduleSpecifier.getText(), file);
54 // This is the "foo" from "import * as foo from 'foo.css'"
55 const importVar = importClause.getText();
56 if (!fs_1.default.existsSync(cssPath)) {
57 throw new Error(typescript_1.default.formatDiagnosticsWithColorAndContext([
58 {
59 category: 1,
60 messageText: `Could not find file ${node.moduleSpecifier.getText()}"`,
61 start: node.moduleSpecifier.getStart(),
62 length: node.moduleSpecifier.getText().length,
63 file: relativeFile(file),
64 code: 1337
65 }
66 ], FORMAT_HOST));
67 }
68 const pending = pendingCssResults.get(cssPath);
69 if (pending) {
70 results.push([importVar, pending]);
71 }
72 else {
73 const promise = processor.process(fs_1.default.readFileSync(cssPath, 'utf8'), {
74 from: cssPath
75 });
76 pendingCssResults.set(cssPath, promise);
77 results.push([importVar, promise]);
78 }
79 }
80 });
81 // 2. Process the css with postcss to an object containing all the classNames
82 await Promise.all(results.map(async ([name, promise]) => {
83 const result = await promise;
84 styles.set(name, result.root ? icss_utils_1.extractICSS(result.root, false).icssExports : {});
85 }));
86 return [file.fileName, styles];
87 });
88 // 3. Return a map of ts sources file => styles in file
89 return new Map(await Promise.all(cssPromises));
90}
91/**
92 * Builds type definition for your source files. Will only emit definitions.
93 * Also tracks usage of css classnames and provides type errors.
94 */
95class TypescriptCompiler {
96 constructor(args) {
97 this.logger = cli_utils_1.createLogger({ scope: 'build' });
98 /** Build the types for the project */
99 this.buildTypes = async (watch) => {
100 const isTrace = cli_utils_1.getLogLevel() === 'trace';
101 if (!fs_1.default.existsSync(path_1.default.join(process.cwd(), 'tsconfig.json'))) {
102 this.logger.debug('No tsconfig.json found, skipping type build.');
103 return;
104 }
105 this.logger.trace('Generating Types...');
106 const ignoredPatterns = [
107 "**/*.snippet.*",
108 ...Array.isArray(this.buildArgs.ignore)
109 ? this.buildArgs.ignore
110 : [this.buildArgs.ignore],
111 ];
112 /** Determine if a file should not be type-checked or emitted */
113 const isIgnored = (file) => ignoredPatterns.some(pattern => minimatch_1.default(file, pattern));
114 try {
115 const diagnostics = [];
116 const host = typescript_1.default.createSolutionBuilderHost(Object.assign(Object.assign({}, typescript_1.default.sys), { writeFile(fileName, content) {
117 if (isIgnored(fileName)) {
118 return;
119 }
120 fs_1.default.writeFileSync(fileName, content);
121 },
122 readFile(fileName, encoding = 'utf8') {
123 if (fs_1.default.existsSync(fileName)) {
124 let content = fs_1.default.readFileSync(fileName, encoding);
125 if (isIgnored(fileName)) {
126 // Don't type check stories
127 content = '// @ts-nocheck';
128 }
129 return content;
130 }
131 } }), undefined, d => diagnostics.push(d), d => this.logger.trace(d.messageText));
132 // The following options are not public but we want to override them
133 typescript_1.default.commonOptionsWithBuild.push({ name: 'emitDeclarationOnly' }, { name: 'declarationMap' }, { name: 'outDir' });
134 const solution = typescript_1.default.createSolutionBuilder(host, ['./tsconfig.json'], {
135 verbose: isTrace,
136 listEmittedFiles: isTrace,
137 outDir: this.buildArgs.outputDirectory || '',
138 incremental: true,
139 declarationMap: true,
140 emitDeclarationOnly: true,
141 });
142 const postcssConfig = postcss_2.getPostCssConfigSync({
143 cwd: cli_utils_1.getMonorepoRoot(),
144 useModules: false,
145 reportError: false
146 });
147 const cssProcessor = postcss_1.default([
148 ...postcssConfig.plugins,
149 postcss_icss_selectors_1.default({
150 mode: 'local',
151 /** Create the scope for the css selectors */
152 generateScopedName: name => name
153 })
154 ]);
155 let project = solution.getNextInvalidatedProject();
156 while (project) {
157 let css = new Map();
158 if ('getSourceFiles' in project) {
159 // eslint-disable-next-line no-await-in-loop
160 css = await processCss(cssProcessor, project);
161 }
162 project.done(undefined, undefined, {
163 afterDeclarations: [this.findStyleUsage(css)]
164 });
165 project = solution.getNextInvalidatedProject();
166 }
167 if (diagnostics.length > 0) {
168 const formattedDiagnostics = typescript_1.default.formatDiagnosticsWithColorAndContext(diagnostics
169 .sort((a, b) => (a.start || 0) - (b.start || 0))
170 .map(d => (Object.assign(Object.assign({}, d), { file: d.file ? relativeFile(d.file) : undefined }))), FORMAT_HOST);
171 throw new Error(formattedDiagnostics);
172 }
173 this.logger.complete('Generated Types');
174 }
175 catch (e) {
176 this.logger.error('\n');
177 // If we don't do this there is a weird space on the first line of the errors
178 // eslint-disable-next-line no-console
179 console.log(e.message);
180 this.logger.debug(e.stack);
181 this.logger.error('Failed to generate types');
182 if (!watch) {
183 process.exit(1);
184 }
185 }
186 };
187 this.buildArgs = args;
188 }
189 findStyleUsage(css) {
190 return (ctx) => sf => {
191 if (!('fileName' in sf)) {
192 return sf;
193 }
194 const styles = css.get(sf.fileName) || new Map();
195 /** Recursively visit all the node in the ts file looking for css usage and imports */
196 const visitor = (node) => {
197 if (typescript_1.default.isPropertyAccessExpression(node)) {
198 const variable = node.expression.getText();
199 const style = styles.get(variable);
200 if (style) {
201 const classes = Object.keys(style);
202 const camelClasses = classes.map(s => change_case_1.camelCase(s));
203 const className = node.name.getText();
204 const exists = Boolean(classes.includes(className) || camelClasses.includes(className));
205 if (!exists) {
206 // We're using internal APIs.... *shh*
207 ctx.addDiagnostic({
208 category: 1,
209 messageText: `ClassName "${className}" does not exists in "${variable}"`,
210 start: node.name.getStart(),
211 length: className.length,
212 file: sf,
213 code: 1337
214 });
215 }
216 }
217 }
218 return typescript_1.default.visitEachChild(node, visitor, ctx);
219 };
220 // Must visit source file instead of the .d.ts file, since that contains no actual code
221 const external = sf.externalModuleIndicator;
222 typescript_1.default.visitNode(external ? external.parent : sf, visitor);
223 return sf;
224 };
225 }
226}
227exports.default = TypescriptCompiler;
228//# sourceMappingURL=typescript.js.map
\No newline at end of file