Skip to content

Commit 4fb4604

Browse files
Dynamic Import for SIMPLE_OPTIMIZATIONS (#91)
Dynamic Import for SIMPLE_OPTIMIZATIONS
1 parent b7ec34d commit 4fb4604

18 files changed

+189
-38
lines changed

src/acorn.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Copyright 2018 The AMP HTML Authors. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS-IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { Program } from 'estree';
18+
import { DYNAMIC_IMPORT_DECLARATION } from './types';
19+
const acorn = require('acorn');
20+
const acornWalk = require('acorn-walk');
21+
const dynamicImport = require('acorn-dynamic-import');
22+
23+
const DYNAMIC_IMPORT_BASEVISITOR = Object.assign({}, acornWalk.base, {
24+
[DYNAMIC_IMPORT_DECLARATION]: () => {},
25+
});
26+
27+
export const walk = {
28+
simple(node: Program, visitors: any): void {
29+
acornWalk.simple(node, visitors, DYNAMIC_IMPORT_BASEVISITOR);
30+
},
31+
ancestor(node: Program, visitors: any): void {
32+
acornWalk.ancestor(node, visitors, DYNAMIC_IMPORT_BASEVISITOR);
33+
},
34+
};
35+
36+
const DEFAULT_ACORN_OPTIONS = {
37+
ecmaVersion: 2019,
38+
sourceType: 'module',
39+
preserveParens: false,
40+
ranges: true,
41+
};
42+
43+
export function parse(source: string): Program {
44+
return acorn.Parser.extend(dynamicImport.default).parse(source, DEFAULT_ACORN_OPTIONS);
45+
}

src/index.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import { CompileOptions } from 'google-closure-compiler';
1818
import * as fs from 'fs';
19+
import * as path from 'path';
1920
import { promisify } from 'util';
2021
import {
2122
OutputOptions,
@@ -65,26 +66,36 @@ const renderChunk = async (
6566
};
6667

6768
export default function closureCompiler(requestedCompileOptions: CompileOptions = {}): Plugin {
69+
const transforms: { [key: string]: Array<Transform> } = {};
6870
let inputOptions: InputOptions;
6971
let context: PluginContext;
70-
let transforms: Array<Transform>;
71-
let transformsDefined: boolean = false;
7272

7373
return {
7474
name: 'closure-compiler',
7575
options: options => (inputOptions = options),
7676
buildStart() {
7777
context = this;
78-
},
79-
load() {
80-
if (!transformsDefined) {
81-
transforms = createTransforms(context, inputOptions);
82-
transformsDefined = true;
78+
if (
79+
'compilation_level' in requestedCompileOptions &&
80+
requestedCompileOptions.compilation_level === 'ADVANCED_OPTIMIZATIONS' &&
81+
inputOptions.experimentalCodeSplitting
82+
) {
83+
context.warn(
84+
'Rollup experimentalCodeSplitting with Closure Compiler ADVANCED_OPTIMIZATIONS is not currently supported.',
85+
);
8386
}
8487
},
88+
load(id: string) {
89+
transforms[path.parse(id).base] = createTransforms(context, inputOptions);
90+
},
8591
renderChunk: async (code: string, chunk: RenderedChunk, outputOptions: OutputOptions) => {
86-
await deriveFromInputSource(code, chunk, transforms);
87-
return await renderChunk(transforms, requestedCompileOptions, code, outputOptions);
92+
await deriveFromInputSource(code, chunk, transforms[chunk.fileName]);
93+
return await renderChunk(
94+
transforms[chunk.fileName],
95+
requestedCompileOptions,
96+
code,
97+
outputOptions,
98+
);
8899
},
89100
};
90101
}

src/options.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { Transform } from './types';
1818
import { ModuleFormat, OutputOptions } from 'rollup';
1919
import { CompileOptions } from 'google-closure-compiler';
2020
import { sync } from 'temp-write';
21+
import { logSource } from './debug';
2122

2223
export const ERROR_WARNINGS_ENABLED_LANGUAGE_OUT_UNSPECIFIED =
2324
'Providing the warning_level=VERBOSE compile option also requires a valid language_out compile option.';
@@ -80,6 +81,10 @@ export const defaults = (
8081
? providedExterns
8182
: '';
8283

84+
if (typeof externs === 'string' && externs !== '') {
85+
logSource('externs', externs);
86+
}
87+
8388
return {
8489
language_out: 'NO_TRANSPILE',
8590
assume_function_wrapper: isESMFormat(options.format),

src/transformers/exports.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
ExportClosureMapping,
3333
} from '../types';
3434
import MagicString from 'magic-string';
35-
const walk = require('acorn-dynamic-import/lib/walk').default(require('acorn-walk'));
35+
import { parse, walk } from '../acorn';
3636

3737
/**
3838
* This Transform will apply only if the Rollup configuration is for 'esm' output.
@@ -54,7 +54,7 @@ export default class ExportTransform extends Transform implements TransformInter
5454
public async deriveFromInputSource(code: string, chunk: RenderedChunk): Promise<void> {
5555
const context = this.context;
5656
let originalExports: ExportNameToClosureMapping = {};
57-
const program = context.parse(code, { ranges: true });
57+
const program = parse(code);
5858

5959
walk.simple(program, {
6060
ExportNamedDeclaration(node: ExportNamedDeclaration) {
@@ -105,7 +105,7 @@ export default class ExportTransform extends Transform implements TransformInter
105105
// where exports were not part of the language.
106106
source.remove(this.originalExports[key].range[0], this.originalExports[key].range[1]);
107107
// Window scoped references for each key are required to ensure Closure Compilre retains the code.
108-
source.append(`\nwindow['${key}'] = ${key}`);
108+
source.append(`\nwindow['${key}'] = ${key};`);
109109
});
110110

111111
return {
@@ -134,7 +134,7 @@ export default class ExportTransform extends Transform implements TransformInter
134134
);
135135
} else if (isESMFormat(this.outputOptions.format)) {
136136
const source = new MagicString(code);
137-
const program = this.context.parse(code, { ranges: true });
137+
const program = parse(code);
138138
const collectedExportsToAppend: Array<string> = [];
139139

140140
const originalExports = this.originalExports;

src/transformers/imports.ts

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ import { Transform } from '../types';
1818
import { literalName, importLocalNames } from './parsing-utilities';
1919
import { TransformSourceDescription } from 'rollup';
2020
import MagicString from 'magic-string';
21-
import { ImportDeclaration } from 'estree';
22-
const walk = require('acorn-dynamic-import/lib/walk').default(require('acorn-walk'));
21+
import { ImportDeclaration, Identifier } from 'estree';
22+
import { parse, walk } from '../acorn';
23+
24+
const DYNAMIC_IMPORT_KEYWORD = 'import';
25+
const DYNAMIC_IMPORT_REPLACEMENT = `import_${new Date().getMilliseconds()}`;
2326

2427
const HEADER = `/**
2528
* @fileoverview Externs built via derived configuration from Rollup or input code.
@@ -28,9 +31,15 @@ const HEADER = `/**
2831
*/
2932
`;
3033

34+
interface RangedImport {
35+
type: string;
36+
range: [number, number];
37+
}
38+
3139
export default class ImportTransform extends Transform {
3240
private importedExternalsSyntax: { [key: string]: string } = {};
3341
private importedExternalsLocalNames: Array<string> = [];
42+
private dynamicImportPresent: boolean = false;
3443

3544
/**
3645
* Rollup allows configuration for 'external' imports.
@@ -56,16 +65,24 @@ export default class ImportTransform extends Transform {
5665
* @return string representing content of generated extern.
5766
*/
5867
public extern(): string {
68+
let extern = HEADER;
5969
if (this.importedExternalsLocalNames.length > 0) {
60-
let extern = HEADER;
6170
this.importedExternalsLocalNames.forEach(name => {
6271
extern += `function ${name}(){};\n`;
6372
});
73+
}
6474

65-
return extern;
75+
if (this.dynamicImportPresent) {
76+
extern += `
77+
/**
78+
* @param {string} path
79+
* @return {!Promise<?>}
80+
*/
81+
function ${DYNAMIC_IMPORT_REPLACEMENT}(path) { return Promise.resolve(path) };
82+
window['${DYNAMIC_IMPORT_REPLACEMENT}'] = ${DYNAMIC_IMPORT_REPLACEMENT};`;
6683
}
6784

68-
return '';
85+
return extern;
6986
}
7087

7188
/**
@@ -79,7 +96,7 @@ export default class ImportTransform extends Transform {
7996
public async preCompilation(code: string): Promise<TransformSourceDescription> {
8097
const self = this;
8198
const source = new MagicString(code);
82-
const program = self.context.parse(code, { ranges: true });
99+
const program = parse(code);
83100

84101
walk.simple(program, {
85102
async ImportDeclaration(node: ImportDeclaration) {
@@ -95,6 +112,18 @@ export default class ImportTransform extends Transform {
95112
);
96113
}
97114
},
115+
Import(node: RangedImport) {
116+
self.dynamicImportPresent = true;
117+
// Rename the `import` method to something we can put in externs.
118+
// CC doesn't understand dynamic import yet.
119+
source.overwrite(
120+
node.range[0],
121+
node.range[1],
122+
code
123+
.substring(node.range[0], node.range[1])
124+
.replace(DYNAMIC_IMPORT_KEYWORD, DYNAMIC_IMPORT_REPLACEMENT),
125+
);
126+
},
98127
});
99128

100129
return {
@@ -110,10 +139,21 @@ export default class ImportTransform extends Transform {
110139
*/
111140
public async postCompilation(code: string): Promise<TransformSourceDescription> {
112141
const source = new MagicString(code);
142+
const program = parse(code);
143+
113144
Object.values(this.importedExternalsSyntax).forEach(importedExternalSyntax =>
114145
source.prepend(importedExternalSyntax),
115146
);
116147

148+
walk.simple(program, {
149+
Identifier(node: Identifier) {
150+
if (node.name === DYNAMIC_IMPORT_REPLACEMENT) {
151+
const range: [number, number] = node.range ? [node.range[0], node.range[1]] : [0, 0];
152+
source.overwrite(range[0], range[1], DYNAMIC_IMPORT_KEYWORD);
153+
}
154+
},
155+
});
156+
117157
return {
118158
code: source.toString(),
119159
map: source.generateMap(),

src/transformers/literal-computed-keys.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { Transform } from '../types';
1818
import { TransformSourceDescription } from 'rollup';
1919
import MagicString from 'magic-string';
2020
import { ObjectExpression } from 'estree';
21-
const walk = require('acorn-dynamic-import/lib/walk').default(require('acorn-walk'));
21+
import { parse, walk } from '../acorn';
2222

2323
/**
2424
* Closure Compiler will not transform computed keys with literal values back to the literal value.
@@ -34,7 +34,7 @@ export default class LiteralComputedKeys extends Transform {
3434
*/
3535
public async postCompilation(code: string): Promise<TransformSourceDescription> {
3636
const source = new MagicString(code);
37-
const program = this.context.parse(code, { ranges: true });
37+
const program = parse(code);
3838

3939
walk.simple(program, {
4040
ObjectExpression(node: ObjectExpression) {

src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ import {
2323
InputOption,
2424
RenderedChunk,
2525
} from 'rollup';
26+
const dynamicImport = require('acorn-dynamic-import');
2627

2728
// @see https://github.com/estree/estree/blob/master/es2015.md#imports
2829
export const IMPORT_DECLARATION = 'ImportDeclaration';
29-
export const DYNAMIC_IMPORT_DECLARATION = 'Import';
30+
export const DYNAMIC_IMPORT_DECLARATION = dynamicImport.DynamicImportKey;
3031
export const IMPORT_SPECIFIER = 'ImportSpecifier';
3132
export const IMPORT_DEFAULT_SPECIFIER = 'ImportDefaultSpecifier';
3233
export const IMPORT_NAMESPACE_SPECIFIER = 'ImportNamespaceSpecifier';
33-
export const ALL_IMPORT_DECLARATIONS = [IMPORT_DECLARATION, DYNAMIC_IMPORT_DECLARATION];
3434

3535
// @see https://github.com/estree/estree/blob/master/es2015.md#exports
3636
export const EXPORT_NAMED_DECLARATION = 'ExportNamedDeclaration';

test/generator.js

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ function generate(
5353
shouldFail,
5454
category,
5555
name,
56+
codeSplit,
5657
formats,
5758
closureFlags,
5859
) {
@@ -65,26 +66,37 @@ function generate(
6566
input: fixtureLocation(category, name, format, optionKey, false),
6667
plugins: [compiler(closureFlags[optionKey])],
6768
external: ['lodash'],
69+
experimentalCodeSplitting: codeSplit,
6870
});
6971

70-
return {
71-
minified: await readFile(
72-
path.join(fixtureLocation(category, name, format, optionKey, true)),
73-
'utf8',
74-
),
75-
code: (await bundle.generate({
76-
format,
77-
sourcemap: true,
78-
})).code,
79-
};
72+
const bundles = await bundle.generate({
73+
format,
74+
sourcemap: true,
75+
});
76+
77+
const output = [];
78+
for (file in bundles.output) {
79+
output.push({
80+
minified: await readFile(
81+
path.join(fixtureLocation(category, path.parse(bundles.output[file].fileName).name, format, optionKey, true)),
82+
'utf8',
83+
),
84+
code: bundles.output[file].code
85+
});
86+
}
87+
88+
return output;
8089
}
8190

8291
Object.keys(closureFlags).forEach(optionKey => {
8392
const method = shouldFail ? test.failing : test;
8493
method(`${name}${format.padEnd(targetLength)}${optionKey.padEnd(optionLength)}`, async t => {
85-
const { minified, code } = await compile(optionKey);
94+
const output = await compile(optionKey);
8695

87-
t.is(code, minified);
96+
t.plan(output.length);
97+
output.forEach(result => {
98+
t.is(result.code, result.minified);
99+
})
88100
});
89101
});
90102
});
@@ -93,24 +105,27 @@ function generate(
93105
function failureGenerator(
94106
category,
95107
name,
108+
codeSplit = false,
96109
formats = [ESM_OUTPUT],
97110
closureFlags = defaultClosureFlags,
98111
) {
99-
generate(true, category, name, formats, closureFlags);
112+
generate(true, category, name, codeSplit, formats, closureFlags);
100113
}
101114

102115
function generator(
103116
category,
104117
name,
118+
codeSplit = false,
105119
formats = [ESM_OUTPUT],
106120
closureFlags = defaultClosureFlags,
107121
) {
108-
generate(false, category, name, formats, closureFlags);
122+
generate(false, category, name, codeSplit, formats, closureFlags);
109123
}
110124

111125
module.exports = {
112126
DEFAULT_CLOSURE_OPTIONS,
113127
ADVANCED_CLOSURE_OPTIONS,
128+
ES5_STRICT_CLOSURE_OPTIONS,
114129
ES_OUTPUT,
115130
ESM_OUTPUT,
116131
generator,

0 commit comments

Comments
 (0)