UNPKG

8.68 kBJavaScriptView Raw
1const fs = require('fs');
2const path = require('path');
3const acorn = require('acorn-dynamic-import').default;
4const walk = require('acorn/dist/walk');
5const GoogDependency = require('./dependencies/goog-dependency');
6const GoogBaseGlobalDependency = require('./dependencies/goog-base-global');
7const GoogLoaderPrefixDependency = require('./dependencies/goog-loader-prefix-dependency');
8const GoogLoaderSuffixDependency = require('./dependencies/goog-loader-suffix-dependency');
9const GoogLoaderEs6PrefixDependency = require('./dependencies/goog-loader-es6-prefix-dependency');
10const GoogLoaderEs6SuffixDependency = require('./dependencies/goog-loader-es6-suffix-dependency');
11
12const PLUGIN = { name: 'ClosureLibraryPlugin' };
13
14const isProductionLikeMode = (options) =>
15 options.mode === 'production' || !options.mode;
16
17class GoogRequireParserPlugin {
18 constructor(options) {
19 this.options = Object.assign({ deps: [], extraDeps: {} }, options);
20
21 if (Array.isArray(this.options.deps)) {
22 this.deps = this.options.deps.slice();
23 } else {
24 this.deps = [this.options.deps];
25 }
26
27 this.basePath = path.resolve(this.options.closureLibraryBase);
28 const baseDir = path.dirname(this.basePath);
29 const googPathsByNamespace = new Map();
30 this.googPathsByNamespace = googPathsByNamespace;
31 const googDepsByPath = new Map();
32
33 Object.keys(this.options.extraDeps).forEach((namespace) => {
34 this.googPathsByNamespace.set(
35 namespace,
36 this.options.extraDeps[namespace]
37 );
38 });
39
40 this.deps.forEach((depFilePath) => {
41 const depFileContents = fs.readFileSync(depFilePath, 'utf8');
42 const ast = acorn.parse(depFileContents, {
43 ranges: true,
44 locations: false,
45 ecmaVersion: 2017,
46 plugins: {
47 dynamicImport: true,
48 },
49 });
50 walk.simple(ast, {
51 CallExpression(node) {
52 if (
53 node.callee.type === 'MemberExpression' &&
54 node.callee.object.type === 'Identifier' &&
55 node.callee.object.name === 'goog' &&
56 node.callee.property.type === 'Identifier' &&
57 node.callee.property.name === 'addDependency'
58 ) {
59 const filePath = path.resolve(baseDir, node.arguments[0].value);
60 node.arguments[1].elements.forEach((arg) =>
61 googPathsByNamespace.set(arg.value, filePath)
62 );
63 if (
64 !googDepsByPath.has(filePath) &&
65 node.arguments[2] &&
66 node.arguments[2].elements.length > 0
67 ) {
68 googDepsByPath.set(
69 filePath,
70 node.arguments[2].elements.map((nodeVal) => nodeVal.value)
71 );
72 }
73 }
74 },
75 });
76 });
77 }
78
79 apply(parser) {
80 const googRequireProvideCallback = (expr) => {
81 if (
82 !parser.state.current.hasDependencies(
83 (dep) => dep.request === this.basePath
84 )
85 ) {
86 this.addGoogDependency(parser, this.basePath, true);
87 }
88
89 // For goog.provide calls, add loader code and exit
90 if (expr.callee.property.name === 'provide') {
91 if (
92 !isProductionLikeMode(this.options) &&
93 !parser.state.current.dependencies.find(
94 (dep) => dep instanceof GoogLoaderPrefixDependency
95 )
96 ) {
97 this.addLoaderDependency(parser, false);
98 }
99 return false;
100 }
101
102 try {
103 const param = expr.arguments[0].value;
104 const modulePath = this.googPathsByNamespace.get(param);
105 if (!modulePath) {
106 parser.state.compilation.warnings.push(
107 new Error(`Unable to locate module for namespace: ${param}`)
108 );
109 return false;
110 }
111 const isRequireType = expr.callee.property.name === 'requireType';
112 this.addGoogDependency(parser, modulePath, false, isRequireType);
113 } catch (e) {
114 parser.state.compilation.errors.push(e);
115 }
116 return false;
117 };
118 parser.hooks.call
119 .for('goog.require')
120 .tap(PLUGIN, googRequireProvideCallback);
121 parser.hooks.call
122 .for('goog.requireType')
123 .tap(PLUGIN, googRequireProvideCallback);
124 parser.hooks.call
125 .for('goog.provide')
126 .tap(PLUGIN, googRequireProvideCallback);
127
128 // When closure-compiler is not bundling the output, shim base.js of closure-library
129 if (!isProductionLikeMode(this.options)) {
130 parser.hooks.statement.tap(PLUGIN, (expr) => {
131 if (
132 expr.type === 'VariableDeclaration' &&
133 expr.declarations.length === 1 &&
134 expr.declarations[0].id.name === 'goog' &&
135 parser.state.current.userRequest === this.basePath
136 ) {
137 parser.state.current.addVariable(
138 'goog',
139 'window.goog = window.goog || {}',
140 []
141 );
142 parser.state.current.contextArgument = function() {
143 return 'window';
144 };
145 parser.state.current.addDependency(new GoogBaseGlobalDependency());
146 }
147 });
148 parser.hooks.call.for('goog.module').tap(PLUGIN, (expr) => {
149 if (!isProductionLikeMode(this.options)) {
150 if (
151 !parser.state.current.hasDependencies(
152 (dep) => dep.request === this.basePath
153 )
154 ) {
155 this.addGoogDependency(parser, this.basePath);
156 }
157
158 const prefixDep = parser.state.current.dependencies.find(
159 (dep) => dep instanceof GoogLoaderPrefixDependency
160 );
161 const suffixDep = parser.state.current.dependencies.find(
162 (dep) => dep instanceof GoogLoaderSuffixDependency
163 );
164 if (prefixDep && suffixDep) {
165 prefixDep.isGoogModule = true;
166 suffixDep.isGoogModule = true;
167 } else {
168 this.addLoaderDependency(parser, true);
169 }
170 }
171 });
172 const googModuleDeclareCallback = () => {
173 if (
174 !parser.state.current.hasDependencies(
175 (dep) => dep.request === this.basePath
176 )
177 ) {
178 this.addGoogDependency(parser, this.basePath);
179 }
180
181 parser.state.current.addVariable(
182 '$jscomp',
183 'window.$jscomp = window.$jscomp || {}',
184 []
185 );
186
187 this.addEs6LoaderDependency(parser);
188 };
189 parser.hooks.call
190 .for('goog.module.declareNamespace')
191 .tap(PLUGIN, googModuleDeclareCallback);
192 parser.hooks.call
193 .for('goog.declareModuleId')
194 .tap(PLUGIN, googModuleDeclareCallback);
195
196 parser.hooks.import.tap(PLUGIN, () => {
197 parser.state.current.addVariable(
198 '$jscomp',
199 'window.$jscomp = window.$jscomp || {}',
200 []
201 );
202 this.addEs6LoaderDependency(parser);
203 });
204 parser.hooks.export.tap(PLUGIN, () => {
205 parser.state.current.addVariable(
206 '$jscomp',
207 'window.$jscomp = window.$jscomp || {}',
208 []
209 );
210 this.addEs6LoaderDependency(parser);
211 });
212 }
213 }
214
215 addGoogDependency(parser, request, addAsBaseJs, isRequireType) {
216 // ES6 prefixing must happen after all requires have loaded otherwise
217 // Closure library can think an ES6 module is calling goog.provide/module.
218 const baseInsertPos = !isProductionLikeMode(this.options) ? -1 : null;
219 parser.state.current.addDependency(
220 new GoogDependency(request, baseInsertPos, addAsBaseJs, isRequireType)
221 );
222 }
223
224 addLoaderDependency(parser, isModule) {
225 parser.state.current.addDependency(
226 new GoogLoaderPrefixDependency(this.basePath, isModule, 0)
227 );
228 const sourceLength = parser.state.current._source.source().length;
229 parser.state.current.addDependency(
230 new GoogLoaderSuffixDependency(this.basePath, isModule, sourceLength)
231 );
232 }
233
234 addEs6LoaderDependency(parser) {
235 if (
236 parser.state.current.dependencies.some(
237 (dep) => dep instanceof GoogLoaderEs6PrefixDependency
238 )
239 ) {
240 return;
241 }
242
243 // ES6 prefixing must happen after all requires have loaded otherwise
244 // Closure library can think an ES6 module is calling goog.provide/module.
245 const baseInsertPos = !isProductionLikeMode(this.options) ? 0 : null;
246 const sourceLength = !isProductionLikeMode(this.options)
247 ? parser.state.current._source.source().length
248 : null;
249 parser.state.current.addDependency(
250 new GoogLoaderEs6PrefixDependency(baseInsertPos)
251 );
252 parser.state.current.addDependency(
253 new GoogLoaderEs6SuffixDependency(sourceLength)
254 );
255 }
256}
257
258module.exports = GoogRequireParserPlugin;