UNPKG

12.5 kBPlain TextView Raw
1/**
2 * @license
3 * Copyright (c) 2018 The Polymer Project Authors. All rights reserved.
4 * This code may only be used under the BSD style license found at
5 * http://polymer.github.io/LICENSE.txt
6 * The complete set of authors may be found at
7 * http://polymer.github.io/AUTHORS.txt
8 * The complete set of contributors may be found at
9 * http://polymer.github.io/CONTRIBUTORS.txt
10 * Code distributed by Google as part of the polymer project is also
11 * subject to an additional IP rights grant found at
12 * http://polymer.github.io/PATENTS.txt
13 */
14
15import * as babelCore from '@babel/core';
16import * as babylon from 'babylon';
17import {JsCompileTarget, ModuleResolutionStrategy} from 'polymer-project-config';
18import * as uuid from 'uuid/v1';
19
20import {resolveBareSpecifiers} from './babel-plugin-bare-specifiers';
21import {dynamicImportAmd} from './babel-plugin-dynamic-import-amd';
22import {rewriteImportMeta} from './babel-plugin-import-meta';
23import * as externalJs from './external-js';
24
25// TODO(aomarks) Switch to babel-preset-env. But how do we get just syntax
26// plugins without turning on transformation, for the case where we are
27// minifying but not compiling?
28
29// Syntax and transform plugins for ES2015. This is roughly equivalent to
30// @babel/preset-es2015, with modules removed and
31// @babel/plugin-transform-classes pinned to v7.0.0-beta.35 to avoid
32// https://github.com/babel/babel/issues/7506 . As mentioned in the bug, native
33// constructors are wrapped with a ES5 'class' which has a constructor that does
34// nothing; however, the custom elements polyfill needs the polyfilled
35// constructor to be called so that it can supply the element being upgraded as
36// the object to use for `this`.
37const babelTransformEs2015 = [
38 require('@babel/plugin-transform-template-literals'),
39 require('@babel/plugin-transform-literals'),
40 require('@babel/plugin-transform-function-name'),
41 require('@babel/plugin-transform-arrow-functions'),
42 require('@babel/plugin-transform-block-scoped-functions'),
43 require('@babel/plugin-transform-classes'),
44 require('@babel/plugin-transform-object-super'),
45 require('@babel/plugin-transform-shorthand-properties'),
46 require('@babel/plugin-transform-duplicate-keys'),
47 require('@babel/plugin-transform-computed-properties'),
48 require('@babel/plugin-transform-for-of'),
49 require('@babel/plugin-transform-sticky-regex'),
50 require('@babel/plugin-transform-unicode-regex'),
51 require('@babel/plugin-transform-spread'),
52 require('@babel/plugin-transform-parameters'),
53 require('@babel/plugin-transform-destructuring'),
54 require('@babel/plugin-transform-block-scoping'),
55 require('@babel/plugin-transform-typeof-symbol'),
56 require('@babel/plugin-transform-instanceof'),
57 [
58 require('@babel/plugin-transform-regenerator'),
59 {async: false, asyncGenerators: false}
60 ],
61];
62
63const babelTransformEs2016 = [
64 require('@babel/plugin-transform-exponentiation-operator'),
65];
66
67const babelTransformEs2017 = [
68 require('@babel/plugin-transform-async-to-generator'),
69];
70
71const babelTransformEs2018 = [
72 require('@babel/plugin-proposal-object-rest-spread'),
73 require('@babel/plugin-proposal-async-generator-functions'),
74];
75
76// Loading this plugin removes inlined Babel helpers.
77const babelExternalHelpersPlugin = require('@babel/plugin-external-helpers');
78
79const babelTransformModulesAmd = [
80 dynamicImportAmd,
81 rewriteImportMeta,
82 require('@babel/plugin-transform-modules-amd'),
83];
84
85// We enumerate syntax plugins that would automatically be loaded by our
86// transform plugins because we need to support the configuration where we
87// minify but don't compile, and don't want Babel to error when it encounters
88// syntax that we support when compiling.
89const babelSyntaxPlugins = [
90 // ES2017 and below syntax plugins are included by default.
91 // ES2018 (partial)
92 require('@babel/plugin-syntax-object-rest-spread'),
93 require('@babel/plugin-syntax-async-generators'),
94 // Future
95 // require('@babel/plugin-syntax-export-extensions'),
96 require('@babel/plugin-syntax-dynamic-import'),
97 require('@babel/plugin-syntax-import-meta'),
98];
99
100const babelPresetMinify = require('babel-preset-minify')({}, {
101
102 // Disable this or you get `{ err: 'Couldn\'t find intersection' }` now.
103 // https://github.com/babel/minify/issues/904
104 builtIns: false,
105
106 // Disable the minify-constant-folding plugin because it has a bug relating
107 // to invalid substitution of constant values into export specifiers:
108 // https://github.com/babel/minify/issues/820
109 evaluate: false,
110
111 // TODO(aomarks) Find out why we disabled this plugin.
112 simplifyComparisons: false,
113
114 // Prevent removal of things that babel thinks are unreachable, but sometimes
115 // gets wrong: https://github.com/Polymer/tools/issues/724
116 deadcode: false,
117
118 // Prevents this `isPure` on null problem from blowing up minification.
119 // https://github.com/babel/minify/issues/790
120 removeUndefined: false,
121
122 // Disable the simplify plugin because it can eat some statements preceeding
123 // loops. https://github.com/babel/minify/issues/824
124 simplify: false,
125
126 // This is breaking ES6 output. https://github.com/Polymer/tools/issues/261
127 mangle: false,
128});
129
130/**
131 * Options for jsTransform.
132 */
133export interface JsTransformOptions {
134 // Whether to compile JavaScript to ES5.
135 compile?: boolean|JsCompileTarget;
136
137 // If true, do not include Babel helper functions in the output. Otherwise,
138 // any Babel helper functions that were required by this transform (e.g. ES5
139 // compilation or AMD module transformation) will be automatically included
140 // inline with this output. If you set this option, you must provide those
141 // required Babel helpers by some other means.
142 externalHelpers?: boolean;
143
144 // Whether to minify JavaScript.
145 minify?: boolean;
146
147 // What kind of ES module resolution/remapping to apply.
148 moduleResolution?: ModuleResolutionStrategy;
149
150 // The path of the file being transformed, used for module resolution.
151 // Must be an absolute filesystem path.
152 filePath?: string;
153
154 // The package name of the file being transformed, required when
155 // `isComponentRequest` is true.
156 packageName?: string;
157
158 // For Polyserve or other servers with similar component directory mounting
159 // behavior. Whether this is a request for a package in node_modules/.
160 isComponentRequest?: boolean;
161
162 // The component directory to use when rewriting bare specifiers to relative
163 // paths. A resolved path that begins with the component directory will be
164 // rewritten to be relative to the root.
165 componentDir?: string;
166
167 // The root directory of the package containing the component directory.
168 // Must be an absolute filesystem path.
169 rootDir?: string;
170
171 // Whether to replace ES modules with AMD modules. If `auto`, run the
172 // transform if the script contains any ES module import/export syntax.
173 transformModulesToAmd?: boolean|'auto';
174
175 // If true, parsing of invalid JavaScript will not throw an exception.
176 // Instead, a console error will be logged, and the original JavaScript will
177 // be returned with no changes. Use with caution!
178 softSyntaxError?: boolean;
179}
180
181/**
182 * Transform some JavaScript according to the given options.
183 */
184export function jsTransform(js: string, options: JsTransformOptions): string {
185 // Even with no transform plugins, parsing and serializing with Babel will
186 // make some minor formatting changes to the code. Skip Babel altogether
187 // if we have no meaningful changes to make.
188 let doBabelTransform = false;
189
190 // Note that Babel plugins run in this order:
191 // 1) plugins, first to last
192 // 2) presets, last to first
193 const plugins = [...babelSyntaxPlugins];
194 const presets = [];
195
196 if (options.externalHelpers) {
197 plugins.push(babelExternalHelpersPlugin);
198 }
199 if (options.minify) {
200 doBabelTransform = true;
201 // Minify last, so push first.
202 presets.push(babelPresetMinify);
203 }
204 if (options.compile === true || options.compile === 'es5') {
205 doBabelTransform = true;
206 plugins.push(...babelTransformEs2015);
207 plugins.push(...babelTransformEs2016);
208 plugins.push(...babelTransformEs2017);
209 plugins.push(...babelTransformEs2018);
210 } else if (options.compile === 'es2015') {
211 doBabelTransform = true;
212 plugins.push(...babelTransformEs2016);
213 plugins.push(...babelTransformEs2017);
214 plugins.push(...babelTransformEs2018);
215 } else if (options.compile === 'es2016') {
216 doBabelTransform = true;
217 plugins.push(...babelTransformEs2017);
218 plugins.push(...babelTransformEs2018);
219 } else if (options.compile === 'es2017') {
220 doBabelTransform = true;
221 plugins.push(...babelTransformEs2018);
222 }
223 if (options.moduleResolution === 'node') {
224 if (!options.filePath) {
225 throw new Error(
226 'Cannot perform node module resolution without filePath.');
227 }
228 doBabelTransform = true;
229 plugins.push(resolveBareSpecifiers(
230 options.filePath,
231 !!options.isComponentRequest,
232 options.packageName,
233 options.componentDir,
234 options.rootDir));
235 }
236
237 // When the AMD option is "auto", these options will change based on whether
238 // we have a module or not (unless they are already definitely true).
239 let transformModulesToAmd = options.transformModulesToAmd;
240 if (transformModulesToAmd === true) {
241 doBabelTransform = true;
242 }
243
244 const maybeDoBabelTransform =
245 doBabelTransform || transformModulesToAmd === 'auto';
246
247 if (maybeDoBabelTransform) {
248 let ast;
249 try {
250 ast = babylon.parse(js, {
251 // TODO(aomarks) Remove any when typings are updated for babylon 7.
252 // tslint:disable-next-line: no-any
253 sourceType: transformModulesToAmd === 'auto' ? 'unambiguous' as any :
254 'module',
255 plugins: [
256 'asyncGenerators',
257 'dynamicImport',
258 // tslint:disable-next-line: no-any
259 'importMeta' as any,
260 'objectRestSpread',
261 ],
262 });
263 } catch (e) {
264 if (options.softSyntaxError && e.constructor.name === 'SyntaxError') {
265 console.error(
266 'ERROR [polymer-build]: failed to parse JavaScript' +
267 (options.filePath ? ` (${options.filePath}):` : ':'),
268 e);
269 return js;
270 } else {
271 throw e;
272 }
273 }
274
275 if (transformModulesToAmd === 'auto' &&
276 ast.program.sourceType === 'module') {
277 transformModulesToAmd = true;
278 }
279
280 if (transformModulesToAmd) {
281 doBabelTransform = true;
282 plugins.push(...babelTransformModulesAmd);
283 }
284
285 if (doBabelTransform) {
286 const result = babelCore.transformFromAst(ast, js, {presets, plugins});
287 if (result.code === undefined) {
288 throw new Error(
289 'Babel transform failed: resulting code was undefined.');
290 }
291 js = result.code;
292
293 if (!options.externalHelpers && options.compile === 'es5' &&
294 js.includes('regeneratorRuntime')) {
295 js = externalJs.getRegeneratorRuntime() + js;
296 }
297 }
298 }
299
300 js = replaceTemplateObjectNames(js);
301
302 return js;
303}
304
305/**
306 * Modifies variables names of tagged template literals (`"_templateObject"`)
307 * from a given string so that they're all unique.
308 *
309 * This is needed to workaround a potential naming collision when individually
310 * transpiled scripts are bundled. See #950.
311 */
312function replaceTemplateObjectNames(js: string): string {
313 // Breakdown of regular expression to match "_templateObject" variables
314 //
315 // Pattern | Meaning
316 // -------------------------------------------------------------------
317 // ( | Group1
318 // _templateObject | Match "_templateObject"
319 // \d* | Match 0 or more digits
320 // \b | Match word boundary
321 // ) | End Group1
322 const searchValueRegex = /(_templateObject\d*\b)/g;
323
324 // The replacement pattern appends an underscore and UUID to the matches:
325 //
326 // Pattern | Meaning
327 // -------------------------------------------------------------------
328 // $1 | Insert matching Group1 (from above)
329 // _ | Insert "_"
330 // ${uniqueId} | Insert previously generated UUID
331 const uniqueId = uuid().replace(/-/g, '');
332 const replaceValue = `$1_${uniqueId}`;
333
334 // Example output:
335 // _templateObject -> _templateObject_200817b1154811e887be8b38cea68555
336 // _templateObject2 -> _templateObject2_5e44de8015d111e89b203116b5c54903
337
338 return js.replace(searchValueRegex, replaceValue);
339}