Skip to content

Commit 1ff3e2f

Browse files
Refactor Exports, Add UTF-8 Tests (#277)
* Better debugging * utf8 import tests * Test now pass * reenable utf8 test for es5
1 parent 4eb87fa commit 1ff3e2f

40 files changed

+437
-239
lines changed

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
"@types/acorn": "4.0.5",
4646
"@types/estree": "0.0.41",
4747
"@types/node": "12.12.24",
48-
"@types/temp-write": "3.3.0",
4948
"@types/uuid": "3.4.6",
5049
"ava": "2.4.0",
5150
"builtins": "3.0.0",
@@ -65,6 +64,10 @@
6564
"*.ts": [
6665
"prettier --config .prettierrc --write",
6766
"git add"
67+
],
68+
"*.test.js": [
69+
"prettier --config .prettierrc --write",
70+
"git add"
6871
]
6972
},
7073
"husky": {

src/compiler.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const {
2121
} = require('google-closure-compiler/lib/utils.js');
2222
import { Transform } from './types';
2323
import { postCompilation } from './transforms';
24+
import { RenderedChunk } from 'rollup';
2425

2526
enum Platform {
2627
NATIVE = 'native',
@@ -71,6 +72,7 @@ function orderPlatforms(platformPreference: Platform | string): Array<Platform>
7172
*/
7273
export default function(
7374
compileOptions: CompileOptions,
75+
chunk: RenderedChunk,
7476
transforms: Array<Transform>,
7577
): Promise<string> {
7678
return new Promise((resolve: (stdOut: string) => void, reject: (error: any) => void) => {
@@ -95,7 +97,7 @@ export default function(
9597
} else if (exitCode !== 0) {
9698
reject(new Error(`Google Closure Compiler exit ${exitCode}: ${stdErr}`));
9799
} else {
98-
resolve(await postCompilation(code, transforms));
100+
resolve(await postCompilation(code, chunk, transforms));
99101
}
100102
});
101103
});

src/debug.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,30 @@ import { writeTempFile } from './temp-file';
1818

1919
const DEBUG_ENABLED = false;
2020

21-
/* c8 ignore next 12 */
22-
export const logSource = async (preamble: string, source: string, code?: string): Promise<void> => {
21+
/* c8 ignore next 16 */
22+
export async function logTransformChain(
23+
file: string,
24+
stage: string,
25+
messages: Array<[string, string]>,
26+
): Promise<void> {
2327
if (DEBUG_ENABLED) {
24-
const sourceLocation: string = await writeTempFile(source);
25-
const codeLocation: string = code ? await writeTempFile(code) : '';
26-
27-
console.log(preamble);
28-
console.log(sourceLocation);
29-
if (code) {
30-
console.log(codeLocation);
28+
let output: string = `\n${file} - ${stage}`;
29+
for (const [message, source] of messages) {
30+
output += `\n${message.substr(0, 15).padEnd(18, '.')} - file://${await writeTempFile(
31+
source,
32+
'.js',
33+
)}`;
3134
}
35+
console.log(output);
3236
}
33-
};
37+
}
3438

35-
/* c8 ignore next 6 */
36-
export const log = (preamble: string, message: string | object): void | null => {
39+
/* c8 ignore next 8 */
40+
export const log = (preamble: string | undefined, message: string | object): void | null => {
3741
if (DEBUG_ENABLED) {
38-
console.log(preamble);
42+
if (preamble) {
43+
console.log(preamble);
44+
}
3945
console.log(message);
4046
}
4147
};

src/index.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ const renderChunk = async (
4242
requestedCompileOptions: CompileOptions = {},
4343
sourceCode: string,
4444
outputOptions: OutputOptions,
45+
chunk: RenderedChunk,
4546
): Promise<{ code: string; map: SourceMapInput } | void> => {
46-
const code = await preCompilation(sourceCode, outputOptions, transforms);
47+
const code = await preCompilation(sourceCode, outputOptions, chunk, transforms);
4748
const [compileOptions, mapFile] = await options(
4849
requestedCompileOptions,
4950
outputOptions,
@@ -53,7 +54,7 @@ const renderChunk = async (
5354

5455
try {
5556
return {
56-
code: await compiler(compileOptions, transforms),
57+
code: await compiler(compileOptions, chunk, transforms),
5758
map: JSON.parse(await fsPromises.readFile(mapFile, 'utf8')),
5859
};
5960
} catch (error) {
@@ -82,7 +83,13 @@ export default function closureCompiler(requestedCompileOptions: CompileOptions
8283
},
8384
renderChunk: async (code: string, chunk: RenderedChunk, outputOptions: OutputOptions) => {
8485
const transforms = createTransforms(context, inputOptions);
85-
const output = await renderChunk(transforms, requestedCompileOptions, code, outputOptions);
86+
const output = await renderChunk(
87+
transforms,
88+
requestedCompileOptions,
89+
code,
90+
outputOptions,
91+
chunk,
92+
);
8693
return output || null;
8794
},
8895
};

src/options.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import { Transform } from './types';
18-
import { ModuleFormat, OutputOptions } from 'rollup';
18+
import { OutputOptions } from 'rollup';
1919
import { CompileOptions } from 'google-closure-compiler';
2020
import { writeTempFile } from './temp-file';
2121
import { log } from './debug';
@@ -27,10 +27,11 @@ export const ERROR_WARNINGS_ENABLED_LANGUAGE_OUT_INVALID =
2727

2828
/**
2929
* Checks if output format is ESM
30-
* @param format
30+
* @param outputOptions
3131
* @return boolean
3232
*/
33-
export const isESMFormat = (format?: ModuleFormat): boolean => format === 'esm' || format === 'es';
33+
export const isESMFormat = ({ format }: OutputOptions): boolean =>
34+
format === 'esm' || format === 'es';
3435

3536
/**
3637
* Throw Errors if compile options will result in unexpected behaviour.
@@ -69,13 +70,14 @@ export const defaults = async (
6970
for (const transform of transformers || []) {
7071
const extern = transform.extern(options);
7172
if (extern !== null) {
72-
transformerExterns.push(await writeTempFile(extern));
73+
const writtenExtern = await writeTempFile(extern);
74+
transformerExterns.push(writtenExtern);
7375
}
7476
}
7577

7678
return {
7779
language_out: 'NO_TRANSPILE',
78-
assume_function_wrapper: isESMFormat(options.format),
80+
assume_function_wrapper: isESMFormat(options),
7981
warning_level: 'QUIET',
8082
module_resolution: 'NODE',
8183
externs: transformerExterns.concat(providedExterns),

src/parsing/export-details.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,14 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { ExportNamedDeclaration, ExportDefaultDeclaration } from 'estree';
17+
import {
18+
ExportNamedDeclaration,
19+
ExportDefaultDeclaration,
20+
Node,
21+
ExpressionStatement,
22+
MemberExpression,
23+
Expression,
24+
} from 'estree';
1825
import { ExportDetails, Range, ExportClosureMapping } from '../types';
1926

2027
export function NamedDeclaration(declaration: ExportNamedDeclaration): Array<ExportDetails> {
@@ -26,7 +33,6 @@ export function NamedDeclaration(declaration: ExportNamedDeclaration): Array<Exp
2633
exportDetails.push({
2734
local: specifier.local.name,
2835
exported: specifier.exported.name,
29-
closureName: specifier.exported.name,
3036
type: ExportClosureMapping.NAMED_CONSTANT,
3137
range: declaration.range as Range,
3238
source,
@@ -46,7 +52,6 @@ export function DefaultDeclaration(
4652
{
4753
local: declaration.name,
4854
exported: declaration.name,
49-
closureName: declaration.name,
5055
type: ExportClosureMapping.NAMED_DEFAULT_FUNCTION,
5156
range: defaultDeclaration.range as Range,
5257
source: null,
@@ -56,3 +61,26 @@ export function DefaultDeclaration(
5661

5762
return [];
5863
}
64+
65+
export function NodeIsPreservedExport(node: Node): node is ExpressionStatement {
66+
return (
67+
node.type === 'ExpressionStatement' &&
68+
node.expression.type === 'AssignmentExpression' &&
69+
node.expression.left.type === 'MemberExpression' &&
70+
node.expression.left.object.type === 'Identifier' &&
71+
node.expression.left.object.name === 'window'
72+
);
73+
}
74+
75+
export function PreservedExportName(node: MemberExpression): string | null {
76+
const { property }: { property: Expression } = node;
77+
78+
if (property.type === 'Identifier') {
79+
return property.name;
80+
}
81+
if (property.type === 'Literal' && typeof property.value === 'string') {
82+
return property.value;
83+
}
84+
85+
return null;
86+
}

src/parsing/import-specifiers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import { IMPORT_SPECIFIER, IMPORT_NAMESPACE_SPECIFIER, IMPORT_DEFAULT_SPECIFIER } from '../types';
1818
import { ImportSpecifier, ImportDefaultSpecifier, ImportNamespaceSpecifier } from 'estree';
1919

20-
export interface Specifiers {
20+
interface Specifiers {
2121
default: string | null;
2222
specific: Array<string>;
2323
local: Array<string>;

src/parsing/literal-name.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,5 @@
1717
import { Literal, SimpleLiteral } from 'estree';
1818

1919
export function literalName(literal: Literal): string {
20-
const literalValue = (literal as SimpleLiteral).value;
21-
return typeof literalValue === 'string' ? literalValue : '';
20+
return (literal as SimpleLiteral).value as string;
2221
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Copyright 2020 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 { ExpressionStatement, AssignmentExpression, MemberExpression } from 'estree';
18+
import { ExportDetails, Range } from '../types';
19+
import MagicString from 'magic-string';
20+
21+
export function PreserveDefault(
22+
code: string,
23+
source: MagicString,
24+
ancestor: ExpressionStatement,
25+
exportDetails: ExportDetails,
26+
exportInline: boolean,
27+
): boolean {
28+
const assignmentExpression = ancestor.expression as AssignmentExpression;
29+
const memberExpression = assignmentExpression.left as MemberExpression;
30+
const [memberExpressionStart, memberExpressionEnd]: Range = memberExpression.range as Range;
31+
32+
source.overwrite(
33+
memberExpressionStart,
34+
memberExpressionEnd + assignmentExpression.operator.length,
35+
'export default ',
36+
);
37+
38+
return false;
39+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/**
2+
* Copyright 2020 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 {
18+
ExpressionStatement,
19+
AssignmentExpression,
20+
FunctionExpression,
21+
MemberExpression,
22+
} from 'estree';
23+
import { ExportDetails, Range } from '../types';
24+
import MagicString from 'magic-string';
25+
26+
function PreserveFunction(
27+
code: string,
28+
source: MagicString,
29+
ancestor: ExpressionStatement,
30+
exportDetails: ExportDetails,
31+
exportInline: boolean,
32+
): boolean {
33+
// Function Expressions can be inlined instead of preserved as variable references.
34+
// window['foo'] = function(){}; => export function foo(){} / function foo(){}
35+
const assignmentExpression = ancestor.expression as AssignmentExpression;
36+
const memberExpression = assignmentExpression.left as MemberExpression;
37+
const functionExpression = assignmentExpression.right as FunctionExpression;
38+
const [memberExpressionObjectStart] = memberExpression.object.range as Range;
39+
const functionName = exportInline ? exportDetails.exported : exportDetails.local;
40+
41+
if (functionExpression.params.length > 0) {
42+
const [paramsStart] = functionExpression.params[0].range as Range;
43+
// FunctionExpression has parameters.
44+
source.overwrite(
45+
memberExpressionObjectStart,
46+
paramsStart,
47+
`${exportInline ? 'export ' : ''}function ${functionName}(`,
48+
);
49+
} else {
50+
const [bodyStart] = functionExpression.body.range as Range;
51+
source.overwrite(
52+
memberExpressionObjectStart,
53+
bodyStart,
54+
`${exportInline ? 'export ' : ''}function ${functionName}()`,
55+
);
56+
}
57+
58+
return !exportInline;
59+
}
60+
61+
function PreserveIdentifier(
62+
code: string,
63+
source: MagicString,
64+
ancestor: ExpressionStatement,
65+
exportDetails: ExportDetails,
66+
exportInline: boolean,
67+
): boolean {
68+
const assignmentExpression = ancestor.expression as AssignmentExpression;
69+
const left = assignmentExpression.left;
70+
const right = assignmentExpression.right;
71+
const [ancestorStart, ancestorEnd]: Range = ancestor.range as Range;
72+
const [rightStart, rightEnd]: Range = right.range as Range;
73+
74+
if (exportInline) {
75+
source.overwrite(
76+
ancestorStart,
77+
ancestorEnd,
78+
`export var ${exportDetails.exported}=${code.substring(rightStart, rightEnd)};`,
79+
);
80+
} else if (exportDetails.source === null && 'name' in right) {
81+
// This is a locally defined identifier with a name we can use.
82+
exportDetails.local = right.name;
83+
source.remove((left.range as Range)[0], rightEnd + 1);
84+
return true;
85+
} else {
86+
// exportDetails.local =
87+
source.overwrite(
88+
ancestorStart,
89+
ancestorEnd,
90+
`var ${exportDetails.local}=${code.substring(rightStart, rightEnd)};`,
91+
);
92+
}
93+
94+
return !exportInline;
95+
}
96+
97+
export function PreserveNamedConstant(
98+
code: string,
99+
source: MagicString,
100+
ancestor: ExpressionStatement,
101+
exportDetails: ExportDetails,
102+
exportInline: boolean,
103+
): boolean {
104+
const assignmentExpression = ancestor.expression as AssignmentExpression;
105+
switch (assignmentExpression.right.type) {
106+
case 'FunctionExpression':
107+
return PreserveFunction(code, source, ancestor, exportDetails, exportInline);
108+
default:
109+
return PreserveIdentifier(code, source, ancestor, exportDetails, exportInline);
110+
}
111+
}

0 commit comments

Comments
 (0)