diff --git a/package-lock.json b/package-lock.json index 9464aab..659d61d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 93639c9..acd03eb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/external-untyped-modules.d.ts b/src/external-untyped-modules.d.ts index d552ae8..4d735cd 100644 --- a/src/external-untyped-modules.d.ts +++ b/src/external-untyped-modules.d.ts @@ -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'; diff --git a/src/index.ts b/src/index.ts index 7b0e014..dc601bd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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)); diff --git a/src/postprocess.ts b/src/postprocess.ts index 3bd5dc0..4dcfd6f 100644 --- a/src/postprocess.ts +++ b/src/postprocess.ts @@ -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; } - diff --git a/src/transform.ts b/src/transform.ts index b194b02..dc1172a 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -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 => { - // 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(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; } - - diff --git a/src/transforms/inlineEquality.ts b/src/transforms/inlineEquality.ts index d6e4620..8ca7fe7 100644 --- a/src/transforms/inlineEquality.ts +++ b/src/transforms/inlineEquality.ts @@ -60,13 +60,13 @@ export const inlineEquality = (): ts.TransformerFactory => 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]; diff --git a/src/transforms/inlineWrappedFunctions.ts b/src/transforms/inlineWrappedFunctions.ts index fa9e3ab..f278a76 100644 --- a/src/transforms/inlineWrappedFunctions.ts +++ b/src/transforms/inlineWrappedFunctions.ts @@ -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 => context => { +export const createFunctionInlineTransformer = ( + logOverview: boolean +): ts.TransformerFactory => 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; diff --git a/transformations.md b/transformations.md index 423d77b..2b852a3 100644 --- a/transformations.md +++ b/transformations.md @@ -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