1 | import Plugin, { Tree } from 'broccoli-plugin';
|
2 | import walkSync from 'walk-sync';
|
3 | import { unlinkSync, rmdirSync, mkdirSync, readFileSync, removeSync } from 'fs-extra';
|
4 | import FSTree from 'fs-tree-diff';
|
5 | import makeDebug from 'debug';
|
6 | import { join, extname } from 'path';
|
7 | import { isEqual, flatten } from 'lodash';
|
8 | import Package from './package';
|
9 | import symlinkOrCopy from 'symlink-or-copy';
|
10 | import { TransformOptions } from '@babel/core';
|
11 | import { File } from '@babel/types';
|
12 | import traverse from "@babel/traverse";
|
13 |
|
14 | makeDebug.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 |
|
27 | const debug = makeDebug('ember-auto-import:analyzer');
|
28 |
|
29 | export interface Import {
|
30 | path: string;
|
31 | package: Package;
|
32 | specifier: string;
|
33 | isDynamic: boolean;
|
34 | }
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 | export 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 |
|
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 |
|
163 |
|
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 |
|
201 | async function babel6Parser(babelOptions: unknown): Promise<(source: string) => File> {
|
202 | let core = import('babel-core');
|
203 | let babylon = import('babylon');
|
204 |
|
205 |
|
206 |
|
207 |
|
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 |
|
220 | async 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 | }
|