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 |
|
15 | import * as babelCore from '@babel/core';
|
16 | import * as babylon from 'babylon';
|
17 | import {JsCompileTarget, ModuleResolutionStrategy} from 'polymer-project-config';
|
18 | import * as uuid from 'uuid/v1';
|
19 |
|
20 | import {resolveBareSpecifiers} from './babel-plugin-bare-specifiers';
|
21 | import {dynamicImportAmd} from './babel-plugin-dynamic-import-amd';
|
22 | import {rewriteImportMeta} from './babel-plugin-import-meta';
|
23 | import * 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`.
|
37 | const 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 |
|
63 | const babelTransformEs2016 = [
|
64 | require('@babel/plugin-transform-exponentiation-operator'),
|
65 | ];
|
66 |
|
67 | const babelTransformEs2017 = [
|
68 | require('@babel/plugin-transform-async-to-generator'),
|
69 | ];
|
70 |
|
71 | const 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.
|
77 | const babelExternalHelpersPlugin = require('@babel/plugin-external-helpers');
|
78 |
|
79 | const 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.
|
89 | const 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 |
|
100 | const 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 | */
|
133 | export 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 | */
|
184 | export 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 | */
|
312 | function 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 | }
|