Skip to content

Commit

Permalink
Refactor Exports, Add UTF-8 Tests (#277)
Browse files Browse the repository at this point in the history
* Better debugging
* utf8 import tests
* Test now pass
* reenable utf8 test for es5
  • Loading branch information
kristoferbaxter authored Jan 9, 2020
1 parent 4eb87fa commit 1ff3e2f
Show file tree
Hide file tree
Showing 40 changed files with 437 additions and 239 deletions.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
"@types/acorn": "4.0.5",
"@types/estree": "0.0.41",
"@types/node": "12.12.24",
"@types/temp-write": "3.3.0",
"@types/uuid": "3.4.6",
"ava": "2.4.0",
"builtins": "3.0.0",
Expand All @@ -65,6 +64,10 @@
"*.ts": [
"prettier --config .prettierrc --write",
"git add"
],
"*.test.js": [
"prettier --config .prettierrc --write",
"git add"
]
},
"husky": {
Expand Down
4 changes: 3 additions & 1 deletion src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const {
} = require('google-closure-compiler/lib/utils.js');
import { Transform } from './types';
import { postCompilation } from './transforms';
import { RenderedChunk } from 'rollup';

enum Platform {
NATIVE = 'native',
Expand Down Expand Up @@ -71,6 +72,7 @@ function orderPlatforms(platformPreference: Platform | string): Array<Platform>
*/
export default function(
compileOptions: CompileOptions,
chunk: RenderedChunk,
transforms: Array<Transform>,
): Promise<string> {
return new Promise((resolve: (stdOut: string) => void, reject: (error: any) => void) => {
Expand All @@ -95,7 +97,7 @@ export default function(
} else if (exitCode !== 0) {
reject(new Error(`Google Closure Compiler exit ${exitCode}: ${stdErr}`));
} else {
resolve(await postCompilation(code, transforms));
resolve(await postCompilation(code, chunk, transforms));
}
});
});
Expand Down
32 changes: 19 additions & 13 deletions src/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,30 @@ import { writeTempFile } from './temp-file';

const DEBUG_ENABLED = false;

/* c8 ignore next 12 */
export const logSource = async (preamble: string, source: string, code?: string): Promise<void> => {
/* c8 ignore next 16 */
export async function logTransformChain(
file: string,
stage: string,
messages: Array<[string, string]>,
): Promise<void> {
if (DEBUG_ENABLED) {
const sourceLocation: string = await writeTempFile(source);
const codeLocation: string = code ? await writeTempFile(code) : '';

console.log(preamble);
console.log(sourceLocation);
if (code) {
console.log(codeLocation);
let output: string = `\n${file} - ${stage}`;
for (const [message, source] of messages) {
output += `\n${message.substr(0, 15).padEnd(18, '.')} - file://${await writeTempFile(
source,
'.js',
)}`;
}
console.log(output);
}
};
}

/* c8 ignore next 6 */
export const log = (preamble: string, message: string | object): void | null => {
/* c8 ignore next 8 */
export const log = (preamble: string | undefined, message: string | object): void | null => {
if (DEBUG_ENABLED) {
console.log(preamble);
if (preamble) {
console.log(preamble);
}
console.log(message);
}
};
13 changes: 10 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ const renderChunk = async (
requestedCompileOptions: CompileOptions = {},
sourceCode: string,
outputOptions: OutputOptions,
chunk: RenderedChunk,
): Promise<{ code: string; map: SourceMapInput } | void> => {
const code = await preCompilation(sourceCode, outputOptions, transforms);
const code = await preCompilation(sourceCode, outputOptions, chunk, transforms);
const [compileOptions, mapFile] = await options(
requestedCompileOptions,
outputOptions,
Expand All @@ -53,7 +54,7 @@ const renderChunk = async (

try {
return {
code: await compiler(compileOptions, transforms),
code: await compiler(compileOptions, chunk, transforms),
map: JSON.parse(await fsPromises.readFile(mapFile, 'utf8')),
};
} catch (error) {
Expand Down Expand Up @@ -82,7 +83,13 @@ export default function closureCompiler(requestedCompileOptions: CompileOptions
},
renderChunk: async (code: string, chunk: RenderedChunk, outputOptions: OutputOptions) => {
const transforms = createTransforms(context, inputOptions);
const output = await renderChunk(transforms, requestedCompileOptions, code, outputOptions);
const output = await renderChunk(
transforms,
requestedCompileOptions,
code,
outputOptions,
chunk,
);
return output || null;
},
};
Expand Down
12 changes: 7 additions & 5 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

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

/**
* Checks if output format is ESM
* @param format
* @param outputOptions
* @return boolean
*/
export const isESMFormat = (format?: ModuleFormat): boolean => format === 'esm' || format === 'es';
export const isESMFormat = ({ format }: OutputOptions): boolean =>
format === 'esm' || format === 'es';

/**
* Throw Errors if compile options will result in unexpected behaviour.
Expand Down Expand Up @@ -69,13 +70,14 @@ export const defaults = async (
for (const transform of transformers || []) {
const extern = transform.extern(options);
if (extern !== null) {
transformerExterns.push(await writeTempFile(extern));
const writtenExtern = await writeTempFile(extern);
transformerExterns.push(writtenExtern);
}
}

return {
language_out: 'NO_TRANSPILE',
assume_function_wrapper: isESMFormat(options.format),
assume_function_wrapper: isESMFormat(options),
warning_level: 'QUIET',
module_resolution: 'NODE',
externs: transformerExterns.concat(providedExterns),
Expand Down
34 changes: 31 additions & 3 deletions src/parsing/export-details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@
* limitations under the License.
*/

import { ExportNamedDeclaration, ExportDefaultDeclaration } from 'estree';
import {
ExportNamedDeclaration,
ExportDefaultDeclaration,
Node,
ExpressionStatement,
MemberExpression,
Expression,
} from 'estree';
import { ExportDetails, Range, ExportClosureMapping } from '../types';

export function NamedDeclaration(declaration: ExportNamedDeclaration): Array<ExportDetails> {
Expand All @@ -26,7 +33,6 @@ export function NamedDeclaration(declaration: ExportNamedDeclaration): Array<Exp
exportDetails.push({
local: specifier.local.name,
exported: specifier.exported.name,
closureName: specifier.exported.name,
type: ExportClosureMapping.NAMED_CONSTANT,
range: declaration.range as Range,
source,
Expand All @@ -46,7 +52,6 @@ export function DefaultDeclaration(
{
local: declaration.name,
exported: declaration.name,
closureName: declaration.name,
type: ExportClosureMapping.NAMED_DEFAULT_FUNCTION,
range: defaultDeclaration.range as Range,
source: null,
Expand All @@ -56,3 +61,26 @@ export function DefaultDeclaration(

return [];
}

export function NodeIsPreservedExport(node: Node): node is ExpressionStatement {
return (
node.type === 'ExpressionStatement' &&
node.expression.type === 'AssignmentExpression' &&
node.expression.left.type === 'MemberExpression' &&
node.expression.left.object.type === 'Identifier' &&
node.expression.left.object.name === 'window'
);
}

export function PreservedExportName(node: MemberExpression): string | null {
const { property }: { property: Expression } = node;

if (property.type === 'Identifier') {
return property.name;
}
if (property.type === 'Literal' && typeof property.value === 'string') {
return property.value;
}

return null;
}
2 changes: 1 addition & 1 deletion src/parsing/import-specifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { IMPORT_SPECIFIER, IMPORT_NAMESPACE_SPECIFIER, IMPORT_DEFAULT_SPECIFIER } from '../types';
import { ImportSpecifier, ImportDefaultSpecifier, ImportNamespaceSpecifier } from 'estree';

export interface Specifiers {
interface Specifiers {
default: string | null;
specific: Array<string>;
local: Array<string>;
Expand Down
3 changes: 1 addition & 2 deletions src/parsing/literal-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,5 @@
import { Literal, SimpleLiteral } from 'estree';

export function literalName(literal: Literal): string {
const literalValue = (literal as SimpleLiteral).value;
return typeof literalValue === 'string' ? literalValue : '';
return (literal as SimpleLiteral).value as string;
}
39 changes: 39 additions & 0 deletions src/parsing/preserve-default-export.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Copyright 2020 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { ExpressionStatement, AssignmentExpression, MemberExpression } from 'estree';
import { ExportDetails, Range } from '../types';
import MagicString from 'magic-string';

export function PreserveDefault(
code: string,
source: MagicString,
ancestor: ExpressionStatement,
exportDetails: ExportDetails,
exportInline: boolean,
): boolean {
const assignmentExpression = ancestor.expression as AssignmentExpression;
const memberExpression = assignmentExpression.left as MemberExpression;
const [memberExpressionStart, memberExpressionEnd]: Range = memberExpression.range as Range;

source.overwrite(
memberExpressionStart,
memberExpressionEnd + assignmentExpression.operator.length,
'export default ',
);

return false;
}
111 changes: 111 additions & 0 deletions src/parsing/preserve-named-constant-export.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* Copyright 2020 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {
ExpressionStatement,
AssignmentExpression,
FunctionExpression,
MemberExpression,
} from 'estree';
import { ExportDetails, Range } from '../types';
import MagicString from 'magic-string';

function PreserveFunction(
code: string,
source: MagicString,
ancestor: ExpressionStatement,
exportDetails: ExportDetails,
exportInline: boolean,
): boolean {
// Function Expressions can be inlined instead of preserved as variable references.
// window['foo'] = function(){}; => export function foo(){} / function foo(){}
const assignmentExpression = ancestor.expression as AssignmentExpression;
const memberExpression = assignmentExpression.left as MemberExpression;
const functionExpression = assignmentExpression.right as FunctionExpression;
const [memberExpressionObjectStart] = memberExpression.object.range as Range;
const functionName = exportInline ? exportDetails.exported : exportDetails.local;

if (functionExpression.params.length > 0) {
const [paramsStart] = functionExpression.params[0].range as Range;
// FunctionExpression has parameters.
source.overwrite(
memberExpressionObjectStart,
paramsStart,
`${exportInline ? 'export ' : ''}function ${functionName}(`,
);
} else {
const [bodyStart] = functionExpression.body.range as Range;
source.overwrite(
memberExpressionObjectStart,
bodyStart,
`${exportInline ? 'export ' : ''}function ${functionName}()`,
);
}

return !exportInline;
}

function PreserveIdentifier(
code: string,
source: MagicString,
ancestor: ExpressionStatement,
exportDetails: ExportDetails,
exportInline: boolean,
): boolean {
const assignmentExpression = ancestor.expression as AssignmentExpression;
const left = assignmentExpression.left;
const right = assignmentExpression.right;
const [ancestorStart, ancestorEnd]: Range = ancestor.range as Range;
const [rightStart, rightEnd]: Range = right.range as Range;

if (exportInline) {
source.overwrite(
ancestorStart,
ancestorEnd,
`export var ${exportDetails.exported}=${code.substring(rightStart, rightEnd)};`,
);
} else if (exportDetails.source === null && 'name' in right) {
// This is a locally defined identifier with a name we can use.
exportDetails.local = right.name;
source.remove((left.range as Range)[0], rightEnd + 1);
return true;
} else {
// exportDetails.local =
source.overwrite(
ancestorStart,
ancestorEnd,
`var ${exportDetails.local}=${code.substring(rightStart, rightEnd)};`,
);
}

return !exportInline;
}

export function PreserveNamedConstant(
code: string,
source: MagicString,
ancestor: ExpressionStatement,
exportDetails: ExportDetails,
exportInline: boolean,
): boolean {
const assignmentExpression = ancestor.expression as AssignmentExpression;
switch (assignmentExpression.right.type) {
case 'FunctionExpression':
return PreserveFunction(code, source, ancestor, exportDetails, exportInline);
default:
return PreserveIdentifier(code, source, ancestor, exportDetails, exportInline);
}
}
Loading

0 comments on commit 1ff3e2f

Please sign in to comment.