UNPKG

6.53 kBPlain TextView Raw
1import Plugin, { Tree } from 'broccoli-plugin';
2import walkSync from 'walk-sync';
3import { unlinkSync, rmdirSync, mkdirSync, readFileSync, removeSync } from 'fs-extra';
4import FSTree from 'fs-tree-diff';
5import makeDebug from 'debug';
6import { join, extname } from 'path';
7import { isEqual, flatten } from 'lodash';
8import Package from './package';
9import symlinkOrCopy from 'symlink-or-copy';
10import { TransformOptions } from '@babel/core';
11import { File } from '@babel/types';
12import traverse from "@babel/traverse";
13
14makeDebug.formatters.m = (modules: Import[]) => {
15 return JSON.stringify(
16 modules.map(m => ({
17 specifier: m.specifier,
18 path: m.path,
19 isDynamic: m.isDynamic,
20 package: m.package.name
21 })),
22 null,
23 2
24 );
25};
26
27const debug = makeDebug('ember-auto-import:analyzer');
28
29export interface Import {
30 path: string;
31 package: Package;
32 specifier: string;
33 isDynamic: boolean;
34}
35
36/*
37 Analyzer discovers and maintains info on all the module imports that
38 appear in a broccoli tree.
39*/
40export default class Analyzer extends Plugin {
41 private previousTree = new FSTree();
42 private modules: Import[] | null = [];
43 private paths: Map<string, Import[]> = new Map();
44
45 private parse: undefined | ((source: string) => File);
46
47 constructor(inputTree: Tree, private pack: Package) {
48 super([inputTree], {
49 annotation: 'ember-auto-import-analyzer',
50 persistentOutput: true
51 });
52 }
53
54 async setupParser(): Promise<void> {
55 if (this.parse) {
56 return;
57 }
58 switch (this.pack.babelMajorVersion) {
59 case 6:
60 this.parse = await babel6Parser(this.pack.babelOptions);
61 break;
62 case 7:
63 this.parse = await babel7Parser(this.pack.babelOptions);
64 break;
65 default:
66 throw new Error(`don't know how to setup a parser for Babel version ${this.pack.babelMajorVersion} (used by ${this.pack.name})`);
67 }
68 }
69
70 get imports(): Import[] {
71 if (!this.modules) {
72 this.modules = flatten([...this.paths.values()]);
73 debug('imports %m', this.modules);
74 }
75 return this.modules;
76 }
77
78 async build() {
79 await this.setupParser();
80 this.getPatchset().forEach(([operation, relativePath]) => {
81 let outputPath = join(this.outputPath, relativePath);
82
83 switch (operation) {
84 case 'unlink':
85 if (this.matchesExtension(relativePath)) {
86 this.removeImports(relativePath);
87 }
88 unlinkSync(outputPath);
89 break;
90 case 'rmdir':
91 rmdirSync(outputPath);
92 break;
93 case 'mkdir':
94 mkdirSync(outputPath);
95 break;
96 case 'change':
97 removeSync(outputPath);
98 // deliberate fallthrough
99 case 'create': {
100 let absoluteInputPath = join(this.inputPaths[0], relativePath);
101 if (this.matchesExtension(relativePath)) {
102 this.updateImports(
103 relativePath,
104 readFileSync(absoluteInputPath, 'utf8')
105 );
106 }
107 symlinkOrCopy.sync(absoluteInputPath, outputPath);
108 }
109 }
110 });
111 }
112
113 private getPatchset() {
114 let input = walkSync.entries(this.inputPaths[0]);
115 let previous = this.previousTree;
116 let next = (this.previousTree = FSTree.fromEntries(input));
117 return previous.calculatePatch(next);
118 }
119
120 private matchesExtension(path: string) {
121 return this.pack.fileExtensions.includes(extname(path).slice(1));
122 }
123
124 removeImports(relativePath: string) {
125 debug(`removing imports for ${relativePath}`);
126 let imports = this.paths.get(relativePath);
127 if (imports) {
128 if (imports.length > 0) {
129 this.modules = null; // invalidates cache
130 }
131 this.paths.delete(relativePath);
132 }
133 }
134
135 updateImports(relativePath: string, source: string) {
136 debug(`updating imports for ${relativePath}, ${source.length}`);
137 let newImports = this.parseImports(relativePath, source);
138 if (!isEqual(this.paths.get(relativePath), newImports)) {
139 this.paths.set(relativePath, newImports);
140 this.modules = null; // invalidates cache
141 }
142 }
143
144 private parseImports(relativePath :string, source: string): Import[] {
145 let ast: File | undefined;
146 try {
147 ast = this.parse!(source);
148 } catch (err) {
149 if (err.name !== 'SyntaxError') {
150 throw err;
151 }
152 debug('Ignoring an unparseable file');
153 }
154 let imports: Import[] = [];
155 if (!ast) {
156 return imports;
157 }
158
159 traverse(ast, {
160 CallExpression: (path) => {
161 if (path.node.callee.type === 'Import') {
162 // it's a syntax error to have anything other than exactly one
163 // argument, so we can just assume this exists
164 let argument = path.node.arguments[0];
165 if (argument.type !== 'StringLiteral') {
166 throw new Error(
167 'ember-auto-import only supports dynamic import() with a string literal argument.'
168 );
169 }
170 imports.push({
171 isDynamic: true,
172 specifier: argument.value,
173 path: relativePath,
174 package: this.pack
175 });
176 }
177 },
178 ImportDeclaration: (path) => {
179 imports.push({
180 isDynamic: false,
181 specifier: path.node.source.value,
182 path: relativePath,
183 package: this.pack
184 });
185 },
186 ExportNamedDeclaration: (path) => {
187 if (path.node.source) {
188 imports.push({
189 isDynamic: false,
190 specifier: path.node.source.value,
191 path: relativePath,
192 package: this.pack
193 });
194 }
195 }
196 });
197 return imports;
198 }
199}
200
201async function babel6Parser(babelOptions: unknown): Promise<(source: string) => File> {
202 let core = import('babel-core');
203 let babylon = import('babylon');
204
205 // missing upstream types (or we are using private API, because babel 6 didn't
206 // have a good way to construct a parser directly from the general babel
207 // options)
208 const { Pipeline, File } = (await core) as any;
209 const { parse } = await babylon;
210
211 let p = new Pipeline();
212 let f = new File(babelOptions, p);
213 let options = f.parserOpts;
214
215 return function(source) {
216 return parse(source, options) as unknown as File;
217 };
218}
219
220async function babel7Parser(babelOptions: TransformOptions): Promise<(source: string) => File> {
221 let core = import('@babel/core');
222
223 const { parseSync } = await core;
224 return function(source: string) {
225 return parseSync(source, babelOptions) as File;
226 };
227}