fixed typescript errors and added a little more explanation for passing unwrapped functions

This commit is contained in:
Simon 2020-08-16 21:15:04 -07:00
parent 79ccf4ac7f
commit d004d9f5dc
9 changed files with 263 additions and 212 deletions

79
package-lock.json generated
View File

@ -1755,6 +1755,12 @@
"minimatch": "^3.0.4"
}
},
"immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=",
"dev": true
},
"import-fresh": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
@ -2007,6 +2013,15 @@
"pako": "~0.2.5"
}
},
"lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"dev": true,
"requires": {
"immediate": "~3.0.5"
}
},
"lines-and-columns": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
@ -2509,8 +2524,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
"dev": true,
"optional": true
"dev": true
},
"osenv": {
"version": "0.1.5",
@ -2923,6 +2937,46 @@
"integrity": "sha512-9A+PDmgm+2du77B5i0Ip2cxOqqHjgNxnBgglxLcX78A2D6c2rTo61z4jnVABpF4cKeDMDG+cmXXvdnqse2VqMA==",
"dev": true
},
"selenium-webdriver": {
"version": "4.0.0-alpha.7",
"resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-4.0.0-alpha.7.tgz",
"integrity": "sha512-D4qnTsyTr91jT8f7MfN+OwY0IlU5+5FmlO5xlgRUV6hDEV8JyYx2NerdTEqDDkNq7RZDYc4VoPALk8l578RBHw==",
"dev": true,
"requires": {
"jszip": "^3.2.2",
"rimraf": "^2.7.1",
"tmp": "0.0.30"
},
"dependencies": {
"jszip": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.5.0.tgz",
"integrity": "sha512-WRtu7TPCmYePR1nazfrtuF216cIVon/3GWOvHS9QR5bIwSbnxtdpma6un3jyGGNhHsKCSzn5Ypk+EkDRvTGiFA==",
"dev": true,
"requires": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"set-immediate-shim": "~1.0.1"
}
},
"pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"dev": true
},
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
}
}
},
"semver-compare": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
@ -2940,6 +2994,12 @@
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
},
"set-immediate-shim": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz",
"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=",
"dev": true
},
"setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
@ -3159,9 +3219,9 @@
}
},
"terser": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.0.0.tgz",
"integrity": "sha512-olH2DwGINoSuEpSGd+BsPuAQaA3OrHnHnFL/rDB2TVNc3srUbz/rq/j2BlF4zDXI+JqAvGr86bIm1R2cJgZ3FA==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.1.0.tgz",
"integrity": "sha512-pwC1Jbzahz1ZPU87NQ8B3g5pKbhyJSiHih4gLH6WZiPU8mmS1IlGbB0A2Nuvkj/LCNsgIKctg6GkYwWCeTvXZQ==",
"dev": true,
"requires": {
"commander": "^2.20.0",
@ -3189,6 +3249,15 @@
"integrity": "sha1-84sK6B03R9YoAB9B2vxlKs5nHAo=",
"dev": true
},
"tmp": {
"version": "0.0.30",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz",
"integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=",
"dev": true,
"requires": {
"os-tmpdir": "~1.0.1"
}
},
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",

View File

@ -41,7 +41,7 @@
"node-elm-compiler": "^5.0.4",
"prepack": "^0.2.54",
"selenium-webdriver": "^4.0.0-alpha.7",
"terser": "^5.0.0",
"terser": "^5.1.0",
"ts-node": "^8.10.2",
"tslib": "^2.0.0",
"typescript": "^3.9.7",

View File

@ -1,5 +1,5 @@
declare module 'node-elm-compiler' {
export function compileSync(files: string[], options: object): void;
export function compileToStringSync(files: string[], options: object): string;
}
declare module 'tree-sitter-elm';

View File

@ -2,7 +2,7 @@
import program from 'commander';
import * as path from 'path';
import * as Transform from './transform';
import { ObjectUpdate, Transforms, InlineLists } from './types';
import { Transforms } from './types';
import { compileToStringSync } from 'node-elm-compiler';
import * as fs from 'fs';
const { version } = require('../package.json');
@ -38,19 +38,15 @@ program
// 'transform into a more modern JS to save size (es2018)',
// false
// )
.option(
'--output',
'The name of the javascript file to create.',
'elm.js'
)
.option('--output', 'The name of the javascript file to create.', 'elm.js')
.parse(process.argv);
type CLIOptions = {
modernize: boolean;
excludeTransforms: string[];
};
// type CLIOptions = {
// modernize: boolean;
// excludeTransforms: string[];
// };
async function run(filePath: string | undefined, options: CLIOptions) {
async function run(filePath: string | undefined) {
if (!filePath || !filePath.endsWith('.elm')) {
console.error('Please provide a path to an Elm file.');
program.outputHelp();
@ -78,16 +74,15 @@ async function run(filePath: string | undefined, options: CLIOptions) {
// withExcluded.inlineFunctions && withExcluded.passUnwrappedFunctions,
// };
const source: string = compileToStringSync([fileName], {
output: 'output/elm.opt.js',
cwd: dirname,
optimize: true,
processOpts:
// ignore stdout
{
stdio: ['pipe', 'ignore', 'pipe']
}
// ignore stdout
{
stdio: ['pipe', 'ignore', 'pipe'],
},
});
const transformed = await Transform.transform(
dirname,
@ -95,15 +90,13 @@ async function run(filePath: string | undefined, options: CLIOptions) {
source,
false,
defaultOptions
)
);
fs.writeFileSync(program.output, transformed);
console.log("Success!");
console.log("")
console.log(` ${fileName} ---> ${program.output}`)
console.log("")
console.log('Success!');
console.log('');
console.log(` ${fileName} ---> ${program.output}`);
console.log('');
}
run(program.args[0], program.opts() as any).catch(e => console.error(e));
run(program.args[0]).catch(e => console.error(e));

View File

@ -1,5 +1,3 @@
/*
This handles functions for operations that this tool doesnt provide as a CLI, but we likely want to study when capturing metrics.
So
@ -13,85 +11,74 @@ So
import * as fs from 'fs';
import { prepackFileSync } from 'prepack';
import * as Terser from 'terser';
import { execSync } from 'child_process';
import * as Compress from "@gfx/zopfli";
import { resolveModuleName } from 'typescript';
import * as Compress from '@gfx/zopfli';
export function prepack(input: string): string {
const { code } = prepackFileSync([input], {
debugNames: true,
inlineExpressions: true,
maxStackDepth: 1200, // that didn't help
});
return code;
const { code } = prepackFileSync([input], {
debugNames: true,
inlineExpressions: true,
maxStackDepth: 1200, // that didn't help
});
return code;
}
export async function minify(inputFilename: string, outputFilename: string) {
const compress = {
toplevel: true,
mangle: false,
compress: {
pure_getters: true,
keep_fargs: false,
unsafe_comps: true,
unsafe: true,
pure_funcs: [
'F2',
'F3',
'F4',
'F5',
'F6',
'F7',
'F8',
'F9',
'A2',
'A3',
'A4',
'A5',
'A6',
'A7',
'A8',
'A9',
],
},
};
const mangle = {
mangle: true,
compress: false,
};
const input = fs.readFileSync(inputFilename, 'utf8');
const compressed = await Terser.minify(input, compress);
const compress = {
toplevel: true,
mangle: false,
compress: {
pure_getters: true,
keep_fargs: false,
unsafe_comps: true,
unsafe: true,
pure_funcs: [
'F2',
'F3',
'F4',
'F5',
'F6',
'F7',
'F8',
'F9',
'A2',
'A3',
'A4',
'A5',
'A6',
'A7',
'A8',
'A9',
],
},
};
const mangle = {
mangle: true,
compress: false,
};
const input = fs.readFileSync(inputFilename, 'utf8');
const compressed = await Terser.minify(input, compress);
let mangled = null;
if (compressed && compressed.code) {
mangled = await Terser.minify(compressed.code, mangle);
} else {
console.log('Error compressing with Terser');
}
// console.log('mangled', mangled.error);
if (mangled && mangled.code) {
fs.writeFileSync(outputFilename, mangled.code);
} else {
console.log('Error mangling with Terser');
}
let mangled = null;
if (compressed && compressed.code) {
mangled = await Terser.minify(compressed.code, mangle);
} else {
console.log('Error compressing with Terser');
}
// console.log('mangled', mangled.error);
if (mangled && mangled.code) {
fs.writeFileSync(outputFilename, mangled.code);
} else {
console.log('Error mangling with Terser');
}
}
export async function gzip(file: string, output: string) {
// --keep = keep the original file
// --force = overwrite the exisign gzip file if it's there
// execSync('gzip --keep --force ' + file);
const fileContents = fs.readFileSync(file, 'utf8');
const promise = Compress.gzipAsync(fileContents, {})
.then(
(compressed) => {
fs.writeFileSync(
output,
compressed
);
}
);
// --keep = keep the original file
// --force = overwrite the exisign gzip file if it's there
// execSync('gzip --keep --force ' + file);
const fileContents = fs.readFileSync(file, 'utf8');
const promise = Compress.gzipAsync(fileContents, {}).then(compressed => {
fs.writeFileSync(output, compressed);
});
await promise;
await promise;
}

View File

@ -1,22 +1,21 @@
import * as fs from 'fs';
import * as path from 'path';
import { parseElm, parseDir, primitives } from './parseElm';
import ts from 'typescript';
import { createCustomTypesTransformer } from './transforms/variantShapes';
import { Mode, Transforms, InlineLists } from './types';
import {
createFunctionInlineTransformer,
InlineContext,
createFunctionInlineTransformer,
InlineContext,
} from './transforms/inlineWrappedFunctions';
import {
InlineMode,
createInlineListFromArrayTransformer,
InlineMode,
createInlineListFromArrayTransformer,
} from './transforms/inlineListFromArray';
import { inlineEquality } from './transforms/inlineEquality';
import {
objectUpdate,
convertFunctionExpressionsToArrowFuncs,
objectUpdate,
convertFunctionExpressionsToArrowFuncs,
} from './transforms/modernizeJS';
import { createRemoveUnusedLocalsTransform } from './transforms/removeUnusedLocals';
import { createPassUnwrappedFunctionsTransformer } from './transforms/passUnwrappedFunctions';
@ -24,102 +23,94 @@ import { replaceVDomNode } from './transforms/adjustVirtualDom';
import { inlineNumberToString } from './transforms/inlineNumberToString';
export type Options = {
compile: boolean;
minify: boolean;
gzip: boolean;
verbose: boolean;
compile: boolean;
minify: boolean;
gzip: boolean;
verbose: boolean;
};
export const transform = async (
dir: string,
jsSource: string,
elmfile: string,
verbose: boolean,
transforms: Transforms
dir: string,
jsSource: string,
elmfile: string,
verbose: boolean,
transforms: Transforms
): Promise<string> => {
// Compile examples in `testcases/*` folder as js
// Run whatever transformations we want on them, saving steps as `elm.{transformation}.js`
let source = ts.createSourceFile(
'elm.js',
jsSource,
ts.ScriptTarget.ES2018
);
// Compile examples in `testcases/*` folder as js
// Run whatever transformations we want on them, saving steps as `elm.{transformation}.js`
let source = ts.createSourceFile('elm.js', jsSource, ts.ScriptTarget.ES2018);
const elmSource = fs.readFileSync(elmfile, 'utf8');
let parsedVariants = parseElm({
author: 'author',
project: 'project',
source: elmSource,
}).concat(primitives);
const elmSource = fs.readFileSync(elmfile, 'utf8');
let parsedVariants = parseElm({
author: 'author',
project: 'project',
source: elmSource,
}).concat(primitives);
parsedVariants = parsedVariants
.concat(parseDir('elm-packages'))
.concat(parseDir(dir));
parsedVariants = parsedVariants.concat(parseDir('elm-packages')).concat(parseDir(dir));
// we dont care about types that have no slots on any variants
parsedVariants = parsedVariants.filter(variant => {
return variant.totalTypeSlotCount != 0;
});
// we dont care about types that have no slots on any variants
parsedVariants = parsedVariants.filter((variant) => { return variant.totalTypeSlotCount != 0 });
const normalizeVariantShapes = createCustomTypesTransformer(
parsedVariants,
Mode.Prod
);
// We have to ensure that this transformation takes place before everything else
if (transforms.replaceVDomNode) {
const results = ts.transform(source, [replaceVDomNode()]);
source = results.transformed[0];
}
const normalizeVariantShapes = createCustomTypesTransformer(
parsedVariants,
Mode.Prod
);
let inlineCtx: InlineContext | undefined;
const transformations: any[] = removeDisabled([
[transforms.variantShapes, normalizeVariantShapes],
[transforms.inlineFunctions, createFunctionInlineTransformer(verbose)],
[transforms.inlineEquality, inlineEquality()],
[transforms.inlineNumberToString, inlineNumberToString()],
[
transforms.listLiterals == InlineLists.AsObjects,
createInlineListFromArrayTransformer(
InlineMode.UsingLiteralObjects(Mode.Prod)
),
],
[
transforms.listLiterals == InlineLists.AsCons,
createInlineListFromArrayTransformer(InlineMode.UsingConsFunc),
],
[
transforms.passUnwrappedFunctions,
createPassUnwrappedFunctionsTransformer(() => inlineCtx),
],
[
!!transforms.objectUpdate,
transforms.objectUpdate && objectUpdate(transforms.objectUpdate),
],
[transforms.arrowFns, convertFunctionExpressionsToArrowFuncs],
[transforms.unusedValues, createRemoveUnusedLocalsTransform()],
]);
// We have to ensure that this transformation takes place before everything else
if (transforms.replaceVDomNode) {
const results = ts.transform(source, [replaceVDomNode()]);
source = results.transformed[0];
}
const {
transformed: [result],
} = ts.transform(source, transformations);
let inlineCtx: InlineContext | undefined;
const transformations: any[] = removeDisabled([
const printer = ts.createPrinter();
[transforms.variantShapes, normalizeVariantShapes],
[
transforms.inlineFunctions,
createFunctionInlineTransformer(verbose),
],
[transforms.inlineEquality, inlineEquality()],
[transforms.inlineNumberToString, inlineNumberToString()],
[
transforms.listLiterals == InlineLists.AsObjects,
createInlineListFromArrayTransformer(
InlineMode.UsingLiteralObjects(Mode.Prod)
),
],
[
transforms.listLiterals == InlineLists.AsCons,
createInlineListFromArrayTransformer(InlineMode.UsingConsFunc),
],
[
transforms.passUnwrappedFunctions,
createPassUnwrappedFunctionsTransformer(() => inlineCtx),
],
[
!!transforms.objectUpdate,
transforms.objectUpdate && objectUpdate(transforms.objectUpdate),
],
[transforms.arrowFns, convertFunctionExpressionsToArrowFuncs],
[transforms.unusedValues, createRemoveUnusedLocalsTransform()],
]);
const {
transformed: [result],
} = ts.transform(source, transformations);
const printer = ts.createPrinter();
return printer.printFile(result);
return printer.printFile(result);
};
function removeDisabled<T>(list: [null | boolean | undefined, T][]): T[] {
let newList: T[] = [];
list.forEach(([cond, val]) => {
if (![null, false, undefined].includes(cond)) {
newList.push(val);
}
});
let newList: T[] = [];
list.forEach(([cond, val]) => {
if (![null, false, undefined].includes(cond)) {
newList.push(val);
}
});
return newList;
return newList;
}

View File

@ -60,13 +60,13 @@ export const inlineEquality = (): ts.TransformerFactory<ts.SourceFile> => contex
// NOTE: we're cheating here with the source.
// I've manually verified that these are number or string comparisons
// So they can safely be converted to ===
const overrideIdentifiers: string[] = [
'leftFringeRank',
'end_',
'c',
'startTagName',
'openChar',
];
// const overrideIdentifiers: string[] = [
// 'leftFringeRank',
// 'end_',
// 'c',
// 'startTagName',
// 'openChar',
// ];
function inferIsPrimitive(node: any): boolean {
let kind = ts.SyntaxKind[node.kind];

View File

@ -89,16 +89,16 @@ export const createInlineContext = (): InlineContext => ({
});
function reportInlineTransformResult(ctx: InlineContext) {
const { splits, partialApplications, inlined } = ctx;
const { inlined } = ctx;
console.log(`inlining function calls
inlined ${inlined.fromRawFunc}
`);
}
export const createFunctionInlineTransformer = (logOverview: boolean): ts.TransformerFactory<ts.SourceFile> => context => {
export const createFunctionInlineTransformer = (
logOverview: boolean
): ts.TransformerFactory<ts.SourceFile> => context => {
return sourceFile => {
const inlineContext: InlineContext = createInlineContext();
@ -344,8 +344,8 @@ const createSplitterVisitor = (
(partialApplication &&
partialApplication.funcReturnsWrapper &&
partialApplication.appliedArgs.length +
appliedArgsNodes.length ===
partialApplication.split.arity)
appliedArgsNodes.length ===
partialApplication.split.arity)
) {
const rawFunName = deriveRawLambdaName(node.name.text);
@ -428,7 +428,7 @@ const createInlinerVisitor = (
if (
partialApplication &&
partialApplication.appliedArgs.length + arity ===
partialApplication.split.arity
partialApplication.split.arity
) {
inlineContext.inlined.partialApplications += 1;
@ -452,7 +452,7 @@ const createInlinerVisitor = (
partialApplication &&
node.arguments.length === 1 &&
partialApplication.appliedArgs.length ===
partialApplication.split.arity - 1
partialApplication.split.arity - 1
) {
inlineContext.inlined.partialApplications += 1;

View File

@ -65,9 +65,7 @@ We generate two definitions for a function, but in most cases a function is eith
If a function is always called with the full number of arguments, the minifier can eliminate our wrapped version (`F2(MyFunction_fn)`) and *also* eliminate the `A2` call, which is explicitly smaller than before.
# Direct call of Lambdas
Similar to the above, but focused on lambdas.
# Passing unwrapped functions and calling them directly
Let's say we have some elm code that produces the following js.
@ -84,12 +82,15 @@ we can transform it to
var f = function(func, a, b) {
return A2(func, a, b)
}, f_unwrapped = function(func, a, b) {
return func(a, b)
return func(a, b) // <-- direct function call!
};
// note that the lambda is unwrapped as well
f_unwrapped(function (a,b) {return a + b;}, 1, 2);
```
This transformation works with separately defined functions too.
@ -264,10 +265,20 @@ updateSingleRecordManually record =
}
```
It's worth exploring automating this transformation, though of course there's a question of how much this affects asset size on larger projects.
However, it's hard to explore further without knowing the actual shape of the records being updated.
**Future work**
Explore more approaches. Next on TODO list:
```
_Utils_update(old, {a: newA})
```
to
```
{...old, a: newA}
```
# Inline Equality