UNPKG

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