├── serialize-closures ├── .npmignore ├── src │ ├── index.ts │ ├── serialize.ts │ ├── deserialize.ts │ ├── customs.ts │ ├── builtins.ts │ └── serializedGraph.ts ├── .mocharc.json ├── tsconfig.json ├── .vscode │ └── launch.json ├── package.json ├── README.md ├── LICENSE └── tests │ └── roundtripping.spec.ts ├── ts-closure-transform ├── .npmignore ├── tests │ ├── fixture │ │ ├── no-closure-arrow.out.js │ │ ├── no-closure-arrow.ts │ │ ├── post-def.ts │ │ ├── square-cube.ts │ │ ├── nested-closure.ts │ │ ├── toplevel-function.ts │ │ ├── toplevel-function.out.js │ │ ├── post-def.out.js │ │ ├── closure-arrow.ts │ │ ├── try-statement.ts │ │ ├── shared-mutable-var.ts │ │ ├── closure-function.ts │ │ ├── function-assignment.ts │ │ ├── square-cube.out.js │ │ ├── type-names.out.js │ │ ├── var-scope.ts │ │ ├── type-names.ts │ │ ├── for-in.ts │ │ ├── property-names.ts │ │ ├── closure-arrow.out.js │ │ ├── var-scope.out.js │ │ ├── function-assignment.out.js │ │ ├── closure-function.out.js │ │ ├── try-statement.out.js │ │ ├── nested-closure.out.js │ │ ├── property-names.out.js │ │ ├── object-literal-elements.ts │ │ ├── shared-mutable-var.out.js │ │ ├── for-in.out.js │ │ └── object-literal-elements.out.js │ ├── serialization │ │ ├── enums.ts │ │ ├── classDef.ts │ │ ├── accessors.ts │ │ ├── recursion.ts │ │ ├── shared-mutable-var.ts │ │ ├── with.ts │ │ ├── importLib.ts │ │ ├── timers.ts │ │ ├── roundtripping.ts │ │ └── fasta.ts │ ├── lib.ts │ └── index.spec.ts ├── .vscode │ ├── settings.json │ └── launch.json ├── .mocharc.json ├── tsconfig.json ├── src │ ├── hoist-functions.ts │ ├── index.ts │ ├── simplify.ts │ ├── flatten-destructured-imports.ts │ ├── box-mutable-captured-vars.ts │ ├── transform.ts │ └── variable-visitor.ts ├── README.md ├── package.json ├── LICENSE └── compile.ts ├── .gitignore ├── transform-benchmarker ├── tsconfig.json ├── package.json ├── process_results.py └── src │ └── index.ts ├── example ├── package.json ├── tsconfig.json ├── src │ └── example.ts ├── .vscode │ └── launch.json └── webpack.config.js ├── .gitlab-ci.yml ├── Makefile ├── .travis.yml ├── LICENSE └── README.md /serialize-closures/.npmignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | test/ 3 | -------------------------------------------------------------------------------- /ts-closure-transform/.npmignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | test/ 3 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/no-closure-arrow.out.js: -------------------------------------------------------------------------------- 1 | let noClosureArrow = () => 10; 2 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/no-closure-arrow.ts: -------------------------------------------------------------------------------- 1 | let noClosureArrow = () => 10; 2 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/post-def.ts: -------------------------------------------------------------------------------- 1 | function update() { 2 | i++; 3 | } 4 | var i = 0; 5 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/square-cube.ts: -------------------------------------------------------------------------------- 1 | let square = x => x * x; 2 | let cube = x => x * square(x); 3 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/nested-closure.ts: -------------------------------------------------------------------------------- 1 | let f1 = x => { 2 | return y => { 3 | return x * f1(y)(x); 4 | }; 5 | }; 6 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/toplevel-function.ts: -------------------------------------------------------------------------------- 1 | function f() { 2 | return 12; 3 | } 4 | 5 | function g() { 6 | return f(); 7 | } 8 | -------------------------------------------------------------------------------- /serialize-closures/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as serialize } from './serialize'; 2 | export { default as deserialize } from './deserialize'; 3 | export * from './builtins'; 4 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/toplevel-function.out.js: -------------------------------------------------------------------------------- 1 | function f() { 2 | return 12; 3 | } 4 | function g() { 5 | return f(); 6 | } 7 | g.__closure = () => ({ f }); 8 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/post-def.out.js: -------------------------------------------------------------------------------- 1 | function update() { 2 | ++i.value; 3 | } 4 | update.__closure = () => ({ i }); 5 | var i = { value: undefined }; 6 | i.value = 0; 7 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/closure-arrow.ts: -------------------------------------------------------------------------------- 1 | let closureArrowA = 10; 2 | let closureArrowB = 20; 3 | let closureArrow = x => { 4 | let y = x; 5 | return closureArrowA + y * x; 6 | }; 7 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/try-statement.ts: -------------------------------------------------------------------------------- 1 | let e = 10; 2 | let f = function() { 3 | e++; 4 | return e; 5 | } 6 | try { 7 | 8 | } catch (e) { 9 | console.log(e); 10 | } 11 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/shared-mutable-var.ts: -------------------------------------------------------------------------------- 1 | function createCounter() { 2 | let count = 0; 3 | return { 4 | get: (() => count), 5 | increment: (() => { count++; }) 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /ts-closure-transform/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Zyp's", 4 | "deserialization", 5 | "destructured", 6 | "desugared", 7 | "recurse" 8 | ] 9 | } -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/closure-function.ts: -------------------------------------------------------------------------------- 1 | let closureFunctionA = 10; 2 | let closureFunctionB = 20; 3 | let closureFunction = function() { 4 | return closureFunctionA + closureFunctionB; 5 | }; 6 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/function-assignment.ts: -------------------------------------------------------------------------------- 1 | function f() { 2 | return 2; 3 | } 4 | 5 | function g() { 6 | f = () => 4; 7 | return; 8 | } 9 | 10 | function h() { 11 | return f(); 12 | } 13 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/square-cube.out.js: -------------------------------------------------------------------------------- 1 | var _tct_transform_1; 2 | let square = x => x * x; 3 | let cube = (_tct_transform_1 = x => x * square(x), _tct_transform_1.__closure = () => ({ square }), _tct_transform_1); 4 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/type-names.out.js: -------------------------------------------------------------------------------- 1 | function repeat(value, count) { 2 | let results = []; 3 | for (let i = 0; i < count; i++) { 4 | results.push(value); 5 | } 6 | return results; 7 | } 8 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/var-scope.ts: -------------------------------------------------------------------------------- 1 | function f() { 2 | do { 3 | var x = 10; 4 | } while (false); 5 | function g() { 6 | x++; 7 | return x; 8 | } 9 | return g(); 10 | } 11 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/type-names.ts: -------------------------------------------------------------------------------- 1 | 2 | function repeat(value: T, count: number): T[] { 3 | let results: T[] = []; 4 | for (let i = 0; i < count; i++) { 5 | results.push(value); 6 | } 7 | return results; 8 | } 9 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/for-in.ts: -------------------------------------------------------------------------------- 1 | let x = 10; 2 | let obj = { y: 42 }; 3 | (function() { 4 | for (let x in obj) { 5 | let f = () => console.log(x); 6 | f(); 7 | } 8 | })(); 9 | function g() { 10 | x++; 11 | } 12 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/property-names.ts: -------------------------------------------------------------------------------- 1 | let noCapturePropNames = () => { 2 | let result = { a: { b: 10 } }; 3 | result.a.b += 10; 4 | return result; 5 | }; 6 | 7 | let noCapturePropNames2 = () => { 8 | return noCapturePropNames().a.b; 9 | }; 10 | -------------------------------------------------------------------------------- /serialize-closures/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | // Specify "require" for CommonJS 3 | "require": "ts-node/register", 4 | "extensions": ["ts", "tsx"], 5 | "spec": [ 6 | "tests/**/*.spec.*" 7 | ], 8 | "watch-files": [ 9 | "src", 10 | "tests" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /ts-closure-transform/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | // Specify "require" for CommonJS 3 | "require": "ts-node/register", 4 | "extensions": ["ts", "tsx"], 5 | "spec": [ 6 | "tests/**/*.spec.*" 7 | ], 8 | "watch-files": [ 9 | "src", 10 | "tests" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/closure-arrow.out.js: -------------------------------------------------------------------------------- 1 | var _tct_transform_1; 2 | let closureArrowA = 10; 3 | let closureArrowB = 20; 4 | let closureArrow = (_tct_transform_1 = x => { 5 | let y = x; 6 | return closureArrowA + y * x; 7 | }, _tct_transform_1.__closure = () => ({ closureArrowA }), _tct_transform_1); 8 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/var-scope.out.js: -------------------------------------------------------------------------------- 1 | function f() { 2 | function g() { 3 | ++x.value; 4 | return x.value; 5 | } 6 | g.__closure = () => ({ x }); 7 | do { 8 | var x = { value: undefined }; 9 | x.value = 10; 10 | } while (false); 11 | return g(); 12 | } 13 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/function-assignment.out.js: -------------------------------------------------------------------------------- 1 | var f = { value: undefined }; 2 | f.value = function () { 3 | return 2; 4 | }; 5 | function g() { 6 | f.value = () => 4; 7 | return; 8 | } 9 | g.__closure = () => ({ f }); 10 | function h() { 11 | return f.value(); 12 | } 13 | h.__closure = () => ({ f }); 14 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/closure-function.out.js: -------------------------------------------------------------------------------- 1 | var _tct_transform_1; 2 | let closureFunctionA = 10; 3 | let closureFunctionB = 20; 4 | let closureFunction = (_tct_transform_1 = function () { 5 | return closureFunctionA + closureFunctionB; 6 | }, _tct_transform_1.__closure = () => ({ closureFunctionA, closureFunctionB }), _tct_transform_1); 7 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/try-statement.out.js: -------------------------------------------------------------------------------- 1 | var _tct_transform_1; 2 | let e = { value: undefined }; 3 | e.value = 10; 4 | let f = (_tct_transform_1 = function () { 5 | ++e.value; 6 | return e.value; 7 | }, _tct_transform_1.__closure = () => ({ e }), _tct_transform_1); 8 | try { 9 | } 10 | catch (e) { 11 | console.log(e); 12 | } 13 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/nested-closure.out.js: -------------------------------------------------------------------------------- 1 | var _tct_transform_1; 2 | let f1 = (_tct_transform_1 = x => { 3 | var _tct_transform_2; 4 | return _tct_transform_2 = y => { 5 | return x * f1(y)(x); 6 | }, _tct_transform_2.__closure = () => ({ x, f1 }), _tct_transform_2; 7 | }, _tct_transform_1.__closure = () => ({ f1 }), _tct_transform_1); 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore output files. 2 | dist/ 3 | 4 | # Ignore generated files. 5 | **/*.js 6 | !webpack.config.js 7 | !ts-closure-transform/test/fixture/*.js 8 | 9 | # Ignore node modules. 10 | node_modules/ 11 | 12 | # Ignore package-lock.json file. 13 | package-lock.json 14 | 15 | # Ignore benchmark results. 16 | transform-benchmarker/results 17 | 18 | # Ignore misc files. 19 | .DS_Store 20 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/property-names.out.js: -------------------------------------------------------------------------------- 1 | var _tct_transform_1; 2 | let noCapturePropNames = () => { 3 | let result = { a: { b: 10 } }; 4 | result.a.b += 10; 5 | return result; 6 | }; 7 | let noCapturePropNames2 = (_tct_transform_1 = () => { 8 | return noCapturePropNames().a.b; 9 | }, _tct_transform_1.__closure = () => ({ noCapturePropNames }), _tct_transform_1); 10 | -------------------------------------------------------------------------------- /serialize-closures/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "target": "ES5", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "moduleResolution": "node", 8 | "stripInternal": true, 9 | "jsx": "react", 10 | "outDir": "dist" 11 | }, 12 | "exclude": [ 13 | "node_modules", 14 | "dist" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/object-literal-elements.ts: -------------------------------------------------------------------------------- 1 | let objLiteral = { 2 | get content() { 3 | return 10; 4 | }, 5 | set content(value) { 6 | }, 7 | value: function getValue() { 8 | var content = this.content; 9 | content = function() { 10 | return content(); 11 | }; 12 | return content(); 13 | }, 14 | getValue() { 15 | return this.content; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /ts-closure-transform/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "target": "ES5", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "moduleResolution": "node", 8 | "stripInternal": true, 9 | "jsx": "react", 10 | "outDir": "dist" 11 | }, 12 | "include": [ 13 | "src/**/*" 14 | ], 15 | "exclude": [ 16 | "node_modules", 17 | "dist" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /transform-benchmarker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "target": "ES5", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "moduleResolution": "node", 8 | "stripInternal": true, 9 | "jsx": "react", 10 | "outDir": "dist" 11 | }, 12 | "include": [ 13 | "src/**/*" 14 | ], 15 | "exclude": [ 16 | "node_modules", 17 | "dist" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/shared-mutable-var.out.js: -------------------------------------------------------------------------------- 1 | function createCounter() { 2 | let count = { value: undefined }; 3 | count.value = 0; 4 | return { 5 | get: (_tct_transform_1 = () => count.value, _tct_transform_1.__closure = () => ({ count }), _tct_transform_1), 6 | increment: (_tct_transform_2 = () => { ++count.value; }, _tct_transform_2.__closure = () => ({ count }), _tct_transform_2) 7 | }; 8 | } 9 | var _tct_transform_1, _tct_transform_2; 10 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/serialization/enums.ts: -------------------------------------------------------------------------------- 1 | import { equal } from 'node:assert'; 2 | import { serialize, deserialize } from '../../../serialize-closures/src'; 3 | 4 | function roundtrip(value: T): T { 5 | return deserialize(serialize(value)); 6 | } 7 | 8 | export enum Match { 9 | CONTAINS = "CONTAINS", 10 | CROSSES = "CROSSES", 11 | DISJOINT = "DISJOINT" 12 | } 13 | 14 | export function enumString() { 15 | equal(roundtrip(Match.CROSSES), "CROSSES"); 16 | } 17 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/lib.ts: -------------------------------------------------------------------------------- 1 | export class Person { 2 | 3 | constructor( 4 | readonly name: string, 5 | readonly email: string) { 6 | 7 | } 8 | 9 | toString(): string { 10 | return `${this.name} <${this.email}>`; 11 | } 12 | 13 | static create(name: string, email: string) { 14 | return new Person(name, email); 15 | } 16 | } 17 | 18 | export function formatPerson(name: string, email: string) { 19 | return new Person(name, email).toString(); 20 | } 21 | 22 | export default Person -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/example.js", 6 | "scripts": { 7 | "test": "webpack && node dist/example.bundle.js" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "util": "^0.12.5", 13 | "serialize-closures": "^0.2.7", 14 | "ts-closure-transform": "^0.1.7", 15 | "ts-loader": "^9.4.2", 16 | "typescript": "^4.9.4", 17 | "webpack": "^5.75.0", 18 | "webpack-cli": "^5.0.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "compilerOptions": { 4 | "target": "ES2017", 5 | "module": "commonjs", 6 | "noImplicitAny": false, 7 | "noImplicitThis": false, 8 | "strict": true, 9 | "strictNullChecks": false, 10 | "declaration": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "sourceMap": true, 14 | "outDir": "./dist" 15 | }, 16 | "exclude": [ 17 | "webpack.config.js", 18 | "dist" 19 | ], 20 | "compileOnSave": true, 21 | "buildOnSave": true 22 | } -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/for-in.out.js: -------------------------------------------------------------------------------- 1 | function g() { 2 | ++x.value; 3 | } 4 | var _tct_transform_1; 5 | g.__closure = () => ({ x }); 6 | let x = { value: undefined }; 7 | x.value = 10; 8 | let obj = { y: 42 }; 9 | (_tct_transform_1 = function () { 10 | var _tct_transform_2; 11 | for (let x in obj) { 12 | let f = (_tct_transform_2 = () => console.log(x), _tct_transform_2.__closure = () => ({ console, x }), _tct_transform_2); 13 | f(); 14 | } 15 | }, _tct_transform_1.__closure = () => ({ obj, console }), _tct_transform_1)(); 16 | -------------------------------------------------------------------------------- /example/src/example.ts: -------------------------------------------------------------------------------- 1 | import { serialize, deserialize } from 'serialize-closures'; 2 | // Just about anything can be serialized by calling `serialize`. 3 | let capturedVariable = 5; 4 | let serialized = serialize(() => capturedVariable); 5 | 6 | // Serialized representations can be stringified and parsed. 7 | let text = JSON.stringify(serialized); 8 | let parsed = JSON.parse(text); 9 | 10 | // Serialized representations can be deserialized by calling `deserialize`. 11 | console.log(deserialize(serialized)()); // Prints '5'. 12 | console.log(deserialize(parsed)()); // Prints '5'. 13 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/fixture/object-literal-elements.out.js: -------------------------------------------------------------------------------- 1 | let objLiteral = { 2 | get content() { 3 | return 10; 4 | }, 5 | set content(value) { 6 | }, 7 | value: function getValue() { 8 | var _tct_transform_1; 9 | var content = { value: undefined }; 10 | content.value = this.content; 11 | content.value = (_tct_transform_1 = function () { 12 | return content.value(); 13 | }, _tct_transform_1.__closure = () => ({ content }), _tct_transform_1); 14 | return content.value(); 15 | }, 16 | getValue() { 17 | return this.content; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:10.9.0 2 | 3 | all_tests: 4 | script: 5 | # Set up some proxies. 6 | - npm config set proxy http://135.245.192.7:8000 7 | - npm config set https-proxy http://135.245.192.7:8000 8 | # Install tsc. 9 | - npm install -g typescript 10 | # Install packages. 11 | - pushd serialize-closures; npm install; popd 12 | - pushd ts-closure-transform; npm install; popd 13 | # Build projects. 14 | - pushd serialize-closures; tsc; popd 15 | - pushd ts-closure-transform; tsc; popd 16 | # Run the tests. 17 | - pushd serialize-closures; npm test; popd 18 | - pushd ts-closure-transform; npm test; popd 19 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/serialization/classDef.ts: -------------------------------------------------------------------------------- 1 | 2 | import { equal } from 'node:assert'; 3 | import { serialize, deserialize } from '../../../serialize-closures/src'; 4 | 5 | function roundtrip(value: T): T { 6 | return deserialize(serialize(value)); 7 | } 8 | 9 | class Vector2 { 10 | x: number; 11 | y: number; 12 | 13 | constructor(x, y) { 14 | this.x = x; 15 | this.y = y; 16 | } 17 | 18 | get length() { 19 | return Math.sqrt(this.x * this.x + this.y * this.y); 20 | } 21 | } 22 | 23 | export function classObject() { 24 | equal(roundtrip(new Vector2(3, 4)).length, 5); 25 | } 26 | 27 | export function constructorCall() { 28 | equal(roundtrip(() => new Vector2(3, 4))().length, 5); 29 | } 30 | -------------------------------------------------------------------------------- /serialize-closures/src/serialize.ts: -------------------------------------------------------------------------------- 1 | import { SerializedGraph } from "./serializedGraph"; 2 | import { BuiltinList } from "./builtins"; 3 | import { CustomSerializerList } from "./customs"; 4 | 5 | /** 6 | * Serializes a value. This value may be a closure or an object 7 | * that contains a closure. 8 | * @param value The value to serialize. 9 | * @param builtins An optional list of builtins to use. 10 | * If not specified, the default builtins are assumed. 11 | * @param customs An optional list of custom serializers to use. 12 | */ 13 | export default function serialize(value: any, builtins?: BuiltinList, customs?: CustomSerializerList): any { 14 | return SerializedGraph.serialize(value, builtins, customs).toJSON(); 15 | } 16 | -------------------------------------------------------------------------------- /example/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Webpack", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/node_modules/webpack/bin/webpack.js", 15 | "preLaunchTask": "tsc: build - tsconfig.json", 16 | "outFiles": [ 17 | "${workspaceFolder}/dist/**/*.js" 18 | ] 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /serialize-closures/src/deserialize.ts: -------------------------------------------------------------------------------- 1 | import { SerializedGraph } from "./serializedGraph"; 2 | import { BuiltinList } from "./builtins"; 3 | import { CustomDeserializerList} from "./customs"; 4 | 5 | /** 6 | * Deserializes a serialized value. 7 | * @param value The serialized value to deserialize. 8 | * @param builtins An optional list of builtins to use. 9 | * If not specified, the default builtins are assumed. 10 | * @param customs An optional list of custom serializers to use. 11 | * @param evalImpl An optional `eval` implementation to use. 12 | */ 13 | export default function deserialize( 14 | value: any, 15 | builtins?: BuiltinList, 16 | customs?: CustomDeserializerList, 17 | evalImpl?: (code: string) => any) : any { 18 | 19 | return SerializedGraph.fromJSON(value, builtins, customs, evalImpl).root; 20 | } 21 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/serialization/accessors.ts: -------------------------------------------------------------------------------- 1 | import { equal } from 'node:assert'; 2 | import { serialize, deserialize } from '../../../serialize-closures/src'; 3 | 4 | function roundtrip(value: T): T { 5 | return deserialize(serialize(value)); 6 | } 7 | 8 | export function roundtripObjectWithAccessors() { 9 | let obj = { 10 | x: 1, 11 | get() { return this.x }, 12 | set(x) { this.x = x } 13 | } 14 | equal(roundtrip(obj).get(), 1); 15 | obj.set(2) 16 | equal(roundtrip(obj).get(), 2); 17 | } 18 | 19 | export function roundtripObjectWithNamedAccessors() { 20 | let obj = { 21 | x: 1, 22 | get latest() { return this.x }, 23 | set latest(x) { this.x = x } 24 | } 25 | equal(roundtrip(obj).latest, 1); 26 | obj.latest = 2 27 | equal(roundtrip(obj).latest, 2); 28 | } 29 | -------------------------------------------------------------------------------- /ts-closure-transform/src/hoist-functions.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | /** 4 | * Creates a node visitor that hoists all function declarations it encounters. 5 | * @param ctx A transformation context. 6 | */ 7 | export function createFunctionHoistingVisitor(ctx: ts.TransformationContext): ts.Visitor { 8 | function visit(node: ts.Node): ts.VisitResult { 9 | if (ts.isFunctionDeclaration(node)) { 10 | ctx.hoistFunctionDeclaration(ts.visitEachChild(node, visit, ctx)); 11 | return []; 12 | } else { 13 | return ts.visitEachChild(node, visit, ctx); 14 | } 15 | } 16 | 17 | return visit; 18 | } 19 | 20 | export default function () { 21 | return (ctx: ts.TransformationContext): ts.Transformer => { 22 | return (sf: ts.SourceFile) => ts.visitNode(sf, createFunctionHoistingVisitor(ctx)) as ts.SourceFile; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | const tsClosureTransform = require('ts-closure-transform'); 3 | const path = require('path'); 4 | module.exports = { 5 | devtool: 'inline-source-map', 6 | mode: 'development', 7 | module: { 8 | rules: [ 9 | { 10 | test: /.tsx?$/, 11 | loader: "ts-loader", 12 | options: { 13 | getCustomTransformers: () => ({ 14 | before: [tsClosureTransform.beforeTransform()], 15 | after: [tsClosureTransform.afterTransform()] 16 | }) 17 | } 18 | } 19 | ] 20 | }, 21 | resolve: { 22 | extensions: ['.tsx', '.ts', '.js'], 23 | fallback: { 24 | "util": require.resolve("util/"), 25 | }, 26 | }, 27 | entry: { 28 | example: './src/example.ts', 29 | }, 30 | output: { 31 | path: path.join(__dirname, 'dist'), 32 | filename: '[name].bundle.js' 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | .PHONY: all clean publish 4 | 5 | all: 6 | pushd serialize-closures; npm run build; popd 7 | pushd ts-closure-transform; npm run build; popd 8 | 9 | install: 10 | pushd serialize-closures; npm i; popd 11 | pushd ts-closure-transform; npm i; popd 12 | 13 | test: 14 | pushd serialize-closures; npm test; popd 15 | pushd ts-closure-transform; npm test; popd 16 | 17 | clean: 18 | rm -rf serialize-closures/dist 19 | rm -rf ts-closure-transform/dist 20 | rm -rf serialize-closures/src/*.js 21 | rm -rf ts-closure-transform/src/*.js 22 | 23 | publish: 24 | pushd serialize-closures; npm version patch && npm publish; popd 25 | pushd ts-closure-transform; npm version patch && npm publish; popd 26 | 27 | publish-prerelease: 28 | pushd serialize-closures; npm version prerelease --preid=next && npm publish; popd 29 | pushd ts-closure-transform; npm version prerelease --preid=next && npm publish; popd 30 | -------------------------------------------------------------------------------- /ts-closure-transform/README.md: -------------------------------------------------------------------------------- 1 | # ts-closure-transform 2 | 3 | This package defines TypeScript code transformations that enable the `serialize-closures` package to serialize functions. 4 | 5 | These transformations will rewrite all function definitions to include a special `__closure` property. The serializer uses that `__closure` property to figure out which variables are captured by the function. 6 | 7 | How you inject this transform depends on the webpack loader you're using. For `ts-loader` you apply the following: 8 | 9 | ```typescript 10 | import { beforeTransform, afterTransform } from 'ts-closure-transform'; 11 | // ... 12 | loader: 'ts-loader', 13 | options: { 14 | getCustomTransformers: () => ({ 15 | before: [beforeTransform()], 16 | after: [afterTransform()] 17 | }) 18 | } 19 | // ... 20 | ``` 21 | 22 | Note that `ts-closure-transform` is strictly a dev dependency: there's no need to package it with your application. 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | # The following is a list of all versions of node we will test 4 | # the closure serializer on. 5 | node_js: 6 | # Latest stable version of node. 7 | - node 8 | # Node v10.9.0 (same as GitLab CI config). 9 | - 10.9.0 10 | # Node v9 as some node APIs that we rely on behave differently on Node <10. 11 | # See comments in serialize-closures/src/{builtins,serializedGraph}.ts. 12 | - 9 13 | 14 | before_install: 15 | # Install tsc, the TypeScript compiler. 16 | - npm install -g typescript 17 | 18 | install: 19 | # Install NPM packages. 20 | - pushd serialize-closures && npm install && popd 21 | - pushd ts-closure-transform && npm install && popd 22 | 23 | script: 24 | # Build projects. 25 | - pushd serialize-closures && tsc && popd 26 | - pushd ts-closure-transform && tsc && popd 27 | # Run the tests. 28 | - pushd serialize-closures && npm test && popd 29 | - pushd ts-closure-transform && npm test && popd 30 | -------------------------------------------------------------------------------- /ts-closure-transform/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Mocha Tests", 11 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 12 | "args": [ 13 | "--timeout", 14 | "999999", 15 | "--colors", 16 | "${workspaceFolder}/test", 17 | ], 18 | "internalConsoleOptions": "openOnSessionStart" 19 | }, 20 | { 21 | "type": "node", 22 | "request": "launch", 23 | "name": "Launch Program", 24 | "program": "${workspaceFolder}/dist/index.js", 25 | "preLaunchTask": "tsc: build - tsconfig.json", 26 | "outFiles": [ 27 | "${workspaceFolder}/dist/**/*.js" 28 | ] 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /ts-closure-transform/tests/serialization/recursion.ts: -------------------------------------------------------------------------------- 1 | import { equal, deepEqual } from 'node:assert'; 2 | import { serialize, deserialize } from '../../../serialize-closures/src'; 3 | 4 | function roundtrip(value: T): T { 5 | return deserialize(serialize(value)); 6 | } 7 | 8 | export function simpleRecursiveRoundtrip() { 9 | equal(1, 1); 10 | equal(roundtrip(1), 1); 11 | equal(roundtrip(roundtrip(1)), 1); 12 | equal(roundtrip(roundtrip(roundtrip(1))), 1); 13 | } 14 | 15 | export function objectRecursiveRoundtrip() { 16 | deepEqual({f: 1}, {f: 1}); 17 | deepEqual(roundtrip({f: 1}), {f: 1}); 18 | deepEqual(roundtrip(roundtrip({f: 1})), {f: 1}); 19 | deepEqual(roundtrip(roundtrip(roundtrip({f: 1}))), {f: 1}); 20 | } 21 | 22 | let to5 = function(x: number) { 23 | return x < 5 ? to5(x + 1) : x 24 | } 25 | 26 | export function functionRecursiveRoundtrip() { 27 | equal(to5(1), 5); 28 | equal(roundtrip(to5)(1), 5); 29 | equal(roundtrip(roundtrip(to5))(1), 5); 30 | equal(roundtrip(roundtrip(roundtrip(to5)))(1), 5); 31 | } 32 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/serialization/shared-mutable-var.ts: -------------------------------------------------------------------------------- 1 | 2 | import { equal } from 'node:assert'; 3 | import { serialize, deserialize } from '../../../serialize-closures/src'; 4 | 5 | function roundtrip(value: T): T { 6 | return deserialize(serialize(value)); 7 | } 8 | 9 | function createCounter() { 10 | let count = 0; 11 | return { 12 | get: (() => count), 13 | increment: (() => { count++ }) 14 | }; 15 | } 16 | 17 | export function sharedMutableVariableTest() { 18 | let counter = createCounter(); 19 | let roundtrippedCounter = roundtrip(counter); 20 | 21 | equal(counter.get(), counter.get()); 22 | counter.increment(); 23 | roundtrippedCounter.increment(); 24 | equal(counter.get(), roundtrippedCounter.get()); 25 | } 26 | 27 | let sharedGlobal = 0; 28 | function incrementSharedGlobal() { 29 | return sharedGlobal++; 30 | } 31 | 32 | export function sharedMutableVariableTest2() { 33 | let inc = roundtrip(incrementSharedGlobal); 34 | 35 | equal(incrementSharedGlobal(), inc()); 36 | equal(incrementSharedGlobal(), inc()); 37 | } 38 | -------------------------------------------------------------------------------- /serialize-closures/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Mocha Tests", 11 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 12 | "args": [ 13 | "-u", 14 | "tdd", 15 | "--timeout", 16 | "999999", 17 | "--colors", 18 | "${workspaceFolder}/test", 19 | "--compilers", 20 | "ts:ts-node/register" 21 | ], 22 | "internalConsoleOptions": "openOnSessionStart" 23 | }, 24 | { 25 | "type": "node", 26 | "request": "launch", 27 | "name": "Launch Program", 28 | "program": "${workspaceFolder}/dist/index.js", 29 | "preLaunchTask": "tsc: build - tsconfig.json", 30 | "outFiles": [ 31 | "${workspaceFolder}/dist/**/*.js" 32 | ] 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /serialize-closures/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serialize-closures", 3 | "version": "0.3.0", 4 | "description": "Serializes closures processed by the ts-closure-transform package.", 5 | "main": "dist/src/index.js", 6 | "typings": "dist/src/index.d.ts", 7 | "scripts": { 8 | "test": "mocha --inspect=9228", 9 | "build": "tsc" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/nokia/ts-serialize-closures.git" 14 | }, 15 | "files": [ 16 | "dist/src" 17 | ], 18 | "keywords": [ 19 | "typescript", 20 | "transform", 21 | "modules" 22 | ], 23 | "author": "Jonathan Van der Cruysse", 24 | "contributors": [ 25 | "Lode Hoste (value: T): T { 6 | return deserialize(serialize(value)); 7 | } 8 | 9 | export function roundTripBasicWith() { 10 | let env = { counter: 10 }; 11 | 12 | let f; 13 | // @ts-ignore 14 | with (env) { 15 | f = () => { 16 | return ++counter; 17 | }; 18 | } 19 | 20 | let out = roundtrip(f); 21 | equal(out(), 11); 22 | equal(out(), 12); 23 | } 24 | 25 | // If uncommented, the assertions below will fail because FlashFreeze assumes 26 | // that all unqualified names refer to variables, an assumption that is invalidated 27 | // by this particular use of the 'with' statement. This is unfortunate, but 28 | // 'with' is deprecated anyway. 29 | /* 30 | class Counter 31 | { 32 | private c: number; 33 | 34 | constructor() 35 | { 36 | this.c = 0; 37 | } 38 | 39 | get currentCount(): number { 40 | return this.c++; 41 | } 42 | } 43 | 44 | export function roundTripPropertyWith() { 45 | let env = new Counter(); 46 | 47 | let f; 48 | with (env) { 49 | f = () => { 50 | return currentCount; 51 | }; 52 | } 53 | 54 | let out = roundtrip(f); 55 | equal(out(), 0); 56 | equal(out(), (1); 57 | } 58 | */ 59 | -------------------------------------------------------------------------------- /serialize-closures/README.md: -------------------------------------------------------------------------------- 1 | # serialize-closures 2 | 3 | This package is a runtime library for that allows for arbitrary object graphs to be serialized, including variable-capturing functions. **Note:** only functions whose code has first been processed by `ts-closure-transform` are eligible for serialization. 4 | 5 | `serialize-closures` defines the `serialize` and `deserialize` functions. These should work for any object graph as long as all source code has first been processed by `ts-closure-transform`. 6 | 7 | Here's some example usage of `serialize` and `deserialize`: 8 | 9 | ```typescript 10 | import { serialize, deserialize } from 'serialize-closures'; 11 | 12 | // Just about anything can be serialized by calling `serialize`. 13 | let capturedVariable = 5; 14 | let serialized = serialize(() => capturedVariable); 15 | 16 | // Serialized representations can be stringified and parsed. 17 | let text = JSON.stringify(serialized); 18 | let parsed = JSON.parse(text); 19 | 20 | // Serialized representations can be deserialized by calling `deserialize`. 21 | console.log(deserialize(serialized)()); // Prints '5'. 22 | ``` 23 | 24 | If you want to attach some data to an object that don't want to see serialized, then you can assign that property a name that starts with two underscores. The serializer will ignore those when serializing. 25 | 26 | ```typescript 27 | deserialize(serialize({ __dont_include_this: 10 })); // Produces '{ }'. 28 | ``` 29 | -------------------------------------------------------------------------------- /ts-closure-transform/src/index.ts: -------------------------------------------------------------------------------- 1 | import { default as closureTransform } from './transform'; 2 | import { default as flattenImports } from './flatten-destructured-imports'; 3 | import { default as boxMutableSharedVariables } from './box-mutable-captured-vars'; 4 | import { default as hoistFunctions } from './hoist-functions'; 5 | import * as ts from 'typescript'; 6 | 7 | type Transform = (ctx: ts.TransformationContext) => ts.Transformer; 8 | 9 | /** 10 | * Takes a list of transforms and turns it into a transform pipeline. 11 | */ 12 | function createPipeline(transforms: ReadonlyArray): Transform { 13 | if (transforms.length == 1) { 14 | return transforms[0]; 15 | } 16 | 17 | return ctx => { 18 | // Compose a pipeline of transforms. 19 | let pipeline = transforms.map(t => t(ctx)); 20 | 21 | // Apply each element of the pipeline to each source file. 22 | return node => { 23 | let result = node; 24 | for (let elem of pipeline) { 25 | result = elem(result); 26 | } 27 | return result; 28 | } 29 | }; 30 | } 31 | 32 | /** 33 | * Creates the 'before' part of the closure serialization transform. 34 | */ 35 | export function beforeTransform(): Transform { 36 | return flattenImports(); 37 | } 38 | 39 | /** 40 | * Creates the 'after' part of the closure serialization transform. 41 | */ 42 | export function afterTransform(): Transform { 43 | return createPipeline([ 44 | hoistFunctions(), 45 | boxMutableSharedVariables(), 46 | closureTransform() 47 | ]); 48 | } 49 | -------------------------------------------------------------------------------- /transform-benchmarker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "transform-benchmarker", 3 | "version": "0.1.1", 4 | "description": "A benchmarking utility that runs the Octane benchmarks with the closure transform enabled.", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "private": true, 8 | "scripts": { 9 | "prepublish": "tsc", 10 | "original": "tsc && node dist/transform-benchmarker/src/index.js original", 11 | "flash-freeze": "tsc && node dist/transform-benchmarker/src/index.js flash-freeze", 12 | "things-js": "tsc && node dist/transform-benchmarker/src/index.js things-js", 13 | "disclosure": "tsc && node dist/transform-benchmarker/src/index.js disclosure" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/nokia/ts-serialize-closures.git" 18 | }, 19 | "keywords": [ 20 | "typescript", 21 | "transform", 22 | "modules" 23 | ], 24 | "author": "Jonathan Van der Cruysse", 25 | "license": "BSD-3-Clause", 26 | "bugs": { 27 | "url": "https://github.com/nokia/ts-serialize-closures/issues" 28 | }, 29 | "files": [ 30 | "dist" 31 | ], 32 | "homepage": "https://github.com/nokia/ts-serialize-closures/blob/master/README.md", 33 | "devDependencies": { 34 | "@types/node": "^12.0.2", 35 | "benchmark-octane": "^1.0.0", 36 | "fs-extra": "^3.0.0", 37 | "glob": "^7.1.1", 38 | "things-js": "^2.0.0", 39 | "ts-node": "^3.0.2", 40 | "typescript": "^2.7.0-rc" 41 | }, 42 | "dependencies": { 43 | "sync-request": "^6.1.0", 44 | "then-request": "^6.0.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Nokia 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /serialize-closures/LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Nokia 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /ts-closure-transform/LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Nokia 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /ts-closure-transform/tests/serialization/importLib.ts: -------------------------------------------------------------------------------- 1 | import { equal } from "node:assert"; 2 | import { Person, formatPerson } from "../lib"; 3 | import PersonDefaultImport from "../lib"; 4 | import PersonDefaultEqualsImport = require("../lib"); 5 | import * as person from "../lib"; 6 | import { deserialize, serialize } from "../../../serialize-closures/src"; 7 | 8 | function roundtrip(value: T): T { 9 | return deserialize(serialize(value)); 10 | } 11 | 12 | let name = "Clark Kent"; 13 | let email = "clark.kent@gmail.com"; 14 | 15 | export function callImportTest() { 16 | let originalCreate = () => Person.create(name, email); 17 | let create = roundtrip(originalCreate); 18 | equal(create().toString(), originalCreate().toString()); 19 | } 20 | 21 | export function callImportTest2() { 22 | equal( 23 | roundtrip(formatPerson)(name, email), 24 | formatPerson(name, email).toString() 25 | ); 26 | } 27 | 28 | export function callDefaultImportTest() { 29 | let originalCreate = () => PersonDefaultImport.create(name, email); 30 | let create = roundtrip(originalCreate); 31 | equal(create().toString(), originalCreate().toString()); 32 | } 33 | 34 | export function callDefaultEqualsImportTest() { 35 | let originalCreate = () => 36 | PersonDefaultEqualsImport.Person.create(name, email); 37 | let create = roundtrip(originalCreate); 38 | equal(create().toString(), originalCreate().toString()); 39 | } 40 | 41 | export function callAliasImportTest() { 42 | let originalCreate = () => person.Person.create(name, email); 43 | let create = roundtrip(originalCreate); 44 | equal(create().toString(), originalCreate().toString()); 45 | } 46 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/serialization/timers.ts: -------------------------------------------------------------------------------- 1 | import { equal } from 'node:assert'; 2 | import { serialize, deserialize } from '../../../serialize-closures/src'; 3 | 4 | function roundtrip(value: T): T { 5 | return deserialize(serialize(value)); 6 | } 7 | 8 | export async function roundtripWithTimeout() { 9 | let delay = 10; 10 | let v = { 11 | value: 0, 12 | delayIncr: () => { 13 | setTimeout(() => v.value++, delay); 14 | } 15 | }; 16 | let vt = roundtrip(v); 17 | equal(vt.value, 0); 18 | vt.delayIncr(); 19 | await new Promise((resolve, reject) => setTimeout(resolve, delay * 2)); 20 | equal(vt.value, 1); 21 | equal(v.value, 0); 22 | } 23 | 24 | export async function roundtripWithCancelledTimeout() { 25 | let delay = 10; 26 | let v = { 27 | value: 0, 28 | delayIncr: () => { 29 | let t = setTimeout(() => v.value++, delay); 30 | clearTimeout(t); 31 | } 32 | }; 33 | let vt = roundtrip(v); 34 | equal(vt.value, 0); 35 | vt.delayIncr(); 36 | await new Promise((resolve, reject) => setTimeout(resolve, delay * 2)); 37 | equal(vt.value, 0); 38 | equal(v.value, 0); 39 | } 40 | 41 | export async function roundtripWithTimeoutPromise() { 42 | let v = { 43 | value: 0, 44 | delayIncr: () => { 45 | return new Promise((resolve, reject) => { 46 | setTimeout(() => { 47 | resolve(v.value + 1); 48 | }, 10); 49 | }); 50 | } 51 | } 52 | let vt = roundtrip(v); 53 | equal(vt.value, 0); 54 | let vtn = await v.delayIncr(); 55 | equal(vt.value, 0); 56 | equal(v.value, 0); 57 | equal(vtn, 1); 58 | } 59 | -------------------------------------------------------------------------------- /serialize-closures/src/customs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A record for a single custom serializer in a list of customs. 3 | * The name should be unique to map the serializer with a corresponding deserializer. 4 | */ 5 | export type CustomSerializerRecord = { 6 | name: string, 7 | value: any, 8 | serializer: () => string 9 | }; 10 | 11 | /** 12 | * A record for a single custom deserializer in a list of customs. 13 | * The name should reflect the name used during serialization. 14 | */ 15 | export type CustomDeserializerRecord = { 16 | name: string, 17 | deserializer: (string) => any 18 | }; 19 | 20 | /** 21 | * A read-only list of custom serializers. 22 | */ 23 | export type CustomSerializerList = ReadonlyArray; 24 | 25 | /** 26 | * A read-only list of custom deserializers. 27 | */ 28 | export type CustomDeserializerList = ReadonlyArray; 29 | 30 | /** 31 | * A default collection of customs to give special treatment. 32 | */ 33 | export const defaultCustoms: CustomSerializerList = []; 34 | 35 | /** 36 | * Returns a custom serializer for a particular value if it is contained in the customList. 37 | * @param value The value to be checked for a custom serializer. 38 | * @param customList An optional list of CustomRecords to search through. 39 | * @returns A custom serializer for `value`; otherwise, `undefined`. 40 | */ 41 | export function retrieveCustomSerializer(value: any, customList?: CustomSerializerList): CustomSerializerRecord | undefined { 42 | if (!customList) return undefined 43 | // Check if value requires a custom serializer 44 | for (let custom of customList) { 45 | if (custom.value === value) { 46 | return custom; 47 | } 48 | } 49 | return undefined; 50 | } 51 | 52 | /** 53 | * Returns a custom deserializer for a particular 'name' if it is contained in the customList. 54 | * @param name The name to find a custom deserializer. 55 | * @param customList An optional list of CustomDeserializerRecord to search through. 56 | * @returns A custom deserializer for `name`-values; otherwise, `undefined`. 57 | */ 58 | export function retrieveCustomDeserializer(name: any, customList?: CustomDeserializerList): (string) => any | undefined { 59 | if (!customList) return undefined 60 | // Check if value requires a custom deserializer 61 | for (let custom of customList) { 62 | if (custom.name === name) { 63 | return custom.deserializer; 64 | } 65 | } 66 | return undefined; 67 | } -------------------------------------------------------------------------------- /ts-closure-transform/tests/serialization/roundtripping.ts: -------------------------------------------------------------------------------- 1 | import { equal, deepEqual } from 'node:assert'; 2 | import { serialize, deserialize } from '../../../serialize-closures/src'; 3 | 4 | function roundtrip(value: T): T { 5 | return deserialize(serialize(value)); 6 | } 7 | 8 | export function roundTripObjectPropertyValueShorthand() { 9 | let createPoint = (x, y) => { 10 | return { 11 | x, 12 | y 13 | } 14 | } 15 | deepEqual(roundtrip(createPoint)(1, 2), { x: 1, y: 2 }); 16 | } 17 | 18 | export function roundTripObjectPropertyValueShorthand2() { 19 | let f = v => { 20 | let vs: any[] = [] 21 | vs.push({v}); 22 | return vs; 23 | } 24 | let f2 = roundtrip(f); 25 | deepEqual(f2(1), [{ v: 1 }]); 26 | } 27 | 28 | export function roundTripClosure() { 29 | let mul = (x, y) => x * y; 30 | let factorial = i => { 31 | if (i <= 0) { 32 | return 1; 33 | } else { 34 | return mul(i, factorial(i - 1)); 35 | } 36 | }; 37 | 38 | equal(roundtrip(factorial)(5), 120); 39 | } 40 | 41 | export function roundTripHoistingClosure() { 42 | let f = x => { 43 | let a = 1; 44 | let r = curriedAdd(x); 45 | function curriedAdd(x) { 46 | return a + x; 47 | } 48 | return r; 49 | }; 50 | 51 | let out = roundtrip(f); 52 | equal(out(4), 5); 53 | } 54 | 55 | export function roundTripHoistingClosure2() { 56 | let f = () => { 57 | let a = 1; 58 | return curriedAdd; 59 | function curriedAdd(x) { 60 | return a + x; 61 | } 62 | }; 63 | 64 | let out = roundtrip(f()); 65 | equal(out(4), 5); 66 | } 67 | 68 | export function roundTripHoistingClosure3() { 69 | let f = () => { 70 | const a = 1; 71 | const f = (v: number) => v + a 72 | return curriedAdd; 73 | function curriedAdd(x) { 74 | return f(x); 75 | } 76 | }; 77 | 78 | let out = roundtrip(f()); 79 | equal(out(4), 5); 80 | } 81 | 82 | export function roundTripNestedClosure() { 83 | let a = 10; 84 | let f = x => { 85 | return y => { 86 | return { result: a + x + y, f }; 87 | }; 88 | }; 89 | 90 | let out = roundtrip(f(10))(5); 91 | equal(out.result, 25); 92 | equal(out.f(10)(5).result, 25); 93 | } 94 | 95 | export function roundTripMathClosure() { 96 | let f = x => { 97 | return Math.sqrt(x); 98 | }; 99 | 100 | let out = roundtrip(f); 101 | equal(out(4), 2); 102 | } 103 | 104 | export function roundTripMathFunctionClosure() { 105 | let sqrt = Math.sqrt; 106 | let f = x => { 107 | return sqrt(x); 108 | }; 109 | 110 | let out = roundtrip(f); 111 | equal(out(4), 2); 112 | } 113 | -------------------------------------------------------------------------------- /ts-closure-transform/compile.ts: -------------------------------------------------------------------------------- 1 | // Based on compile.ts from Kris Zyp's https://github.com/DoctorEvidence/ts-transform-safely, 2 | // licensed under the MIT license. 3 | 4 | // MIT License 5 | 6 | // Copyright (c) 2017 Kris Zyp 7 | 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | 15 | // The above copyright notice and this permission notice shall be included in all 16 | // copies or substantial portions of the Software. 17 | 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | // SOFTWARE. 25 | 26 | import * as ts from 'typescript'; 27 | import { sync as globSync } from 'glob'; 28 | import { beforeTransform, afterTransform } from './src'; 29 | 30 | export const CJS_CONFIG = { 31 | module: ts.ModuleKind.CommonJS, 32 | moduleResolution: ts.ModuleResolutionKind.NodeJs, 33 | noEmitOnError: false, 34 | noUnusedLocals: true, 35 | noUnusedParameters: true, 36 | stripInternal: true, 37 | noImplicitUseStrict: true, 38 | target: ts.ScriptTarget.ES5 39 | }; 40 | 41 | export default function compile( 42 | input: string, 43 | options: ts.CompilerOptions = CJS_CONFIG, 44 | writeFile?: ts.WriteFileCallback, 45 | printDiagnostics: boolean = true, 46 | transformClosures: boolean = true) { 47 | 48 | const files = globSync(input); 49 | 50 | const compilerHost = ts.createCompilerHost(options); 51 | const program = ts.createProgram(files, options, compilerHost); 52 | 53 | const msgs = {}; 54 | 55 | let transformers = transformClosures 56 | ? { before: [beforeTransform()], after: [afterTransform()] } 57 | : undefined; 58 | 59 | let emitResult = program.emit(undefined, writeFile, undefined, undefined, transformers); 60 | 61 | if (printDiagnostics) { 62 | let allDiagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics); 63 | 64 | allDiagnostics.forEach(diagnostic => { 65 | let message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); 66 | if (diagnostic.file) { 67 | let { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!); 68 | console.log(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`); 69 | } else { 70 | console.log(`${message}`); 71 | } 72 | }); 73 | } 74 | 75 | return msgs; 76 | } 77 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/serialization/fasta.ts: -------------------------------------------------------------------------------- 1 | // This is a slightly modified version of a benchmark 2 | // from the Computer Language Benchmarks Game. The 3 | // goal of this test is to verify that the function 4 | // serialization system works even for non-trivial 5 | // programs such as this. 6 | // 7 | // Original header: 8 | // 9 | // The Computer Language Benchmarks Game 10 | // http://benchmarksgame.alioth.debian.org/ 11 | // 12 | // Contributed by Ian Osgood 13 | 14 | import { deepEqual } from 'node:assert'; 15 | import { serialize, deserialize } from '../../../serialize-closures/src'; 16 | 17 | function roundtrip(value: T): T { 18 | return deserialize(serialize(value)); 19 | } 20 | 21 | var last = 42, A = 3877, C = 29573, M = 139968; 22 | 23 | function rand(max) { 24 | last = (last * A + C) % M; 25 | return max * last / M; 26 | } 27 | 28 | var ALU = 29 | "GGCCGGGCGCGGTGGCTCACGCCTGTAATCCCAGCACTTTGG" + 30 | "GAGGCCGAGGCGGGCGGATCACCTGAGGTCAGGAGTTCGAGA" + 31 | "CCAGCCTGGCCAACATGGTGAAACCCCGTCTCTACTAAAAAT" + 32 | "ACAAAAATTAGCCGGGCGTGGTGGCGCGCGCCTGTAATCCCA" + 33 | "GCTACTCGGGAGGCTGAGGCAGGAGAATCGCTTGAACCCGGG" + 34 | "AGGCGGAGGTTGCAGTGAGCCGAGATCGCGCCACTGCACTCC" + 35 | "AGCCTGGGCGACAGAGCGAGACTCCGTCTCAAAAA"; 36 | 37 | var IUB = { 38 | a:0.27, c:0.12, g:0.12, t:0.27, 39 | B:0.02, D:0.02, H:0.02, K:0.02, 40 | M:0.02, N:0.02, R:0.02, S:0.02, 41 | V:0.02, W:0.02, Y:0.02 42 | } 43 | 44 | var HomoSap = { 45 | a: 0.3029549426680, 46 | c: 0.1979883004921, 47 | g: 0.1975473066391, 48 | t: 0.3015094502008 49 | } 50 | 51 | function makeCumulative(table) { 52 | var last : string | null = null; 53 | for (var c in table) { 54 | if (last) table[c] += table[last]; 55 | last = c; 56 | } 57 | } 58 | 59 | function fastaRepeat(n, seq) { 60 | let output: string[] = []; 61 | var seqi = 0, lenOut = 60; 62 | while (n>0) { 63 | if (n0) { 82 | if (n> = []; 100 | results.push(">ONE Homo sapiens alu"); 101 | results.push(fastaRepeat(2*n, ALU)); 102 | 103 | results.push(">TWO IUB ambiguity codes"); 104 | results.push(fastaRandom(3*n, IUB)); 105 | 106 | results.push(">THREE Homo sapiens frequency"); 107 | results.push(fastaRandom(5*n, HomoSap)); 108 | return results; 109 | } 110 | 111 | export function fastaTest() { 112 | let n = 20; 113 | deepEqual(roundtrip(runFastaTest)(n), runFastaTest(n)); 114 | } 115 | -------------------------------------------------------------------------------- /ts-closure-transform/src/simplify.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | /** 4 | * Simplifies a particular node. 5 | * @param node The node to simplify. 6 | */ 7 | export function simplify(node: ts.Node): ts.Node { 8 | if (ts.isBinaryExpression(node)) { 9 | if (ts.isBinaryExpression(node.right) 10 | && node.right.operatorToken.kind in assignmentTokenMapping 11 | && areEqual(node.left, node.right.left)) { 12 | 13 | // a = a b 14 | // --> 15 | // a = b 16 | 17 | return simplify( 18 | ts.factory.updateBinaryExpression( 19 | node, 20 | node.left, 21 | ts.factory.createToken(assignmentTokenMapping[node.right.operatorToken.kind]), 22 | node.right.right, 23 | )); 24 | } else if (ts.isLiteralExpression(node.right) 25 | && node.right.text === '1') { 26 | 27 | if (node.operatorToken.kind === ts.SyntaxKind.PlusEqualsToken) { 28 | // a += 1 29 | // --> 30 | // ++a 31 | return ts.factory.createPrefixUnaryExpression(ts.SyntaxKind.PlusPlusToken, node.left); 32 | } else if (node.operatorToken.kind === ts.SyntaxKind.MinusEqualsToken) { 33 | // a -= 1 34 | // --> 35 | // --a 36 | return ts.factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusMinusToken, node.left); 37 | } 38 | } 39 | } 40 | return node; 41 | } 42 | 43 | /** 44 | * Simplifies a particular expression. 45 | * @param expr The expression to simplify. 46 | */ 47 | export function simplifyExpression(expr: ts.Expression): ts.Expression { 48 | return simplify(expr); 49 | } 50 | 51 | /** 52 | * Simplifies a particular statement. 53 | * @param stmt The statement to simplify. 54 | */ 55 | export function simplifyStatement(stmt: ts.Statement): ts.Statement { 56 | return simplify(stmt); 57 | } 58 | 59 | /** 60 | * Tests if two nodes are definitely structurally equal. 61 | * May produce false negatives, but will never produce false positives. 62 | * @param left The left-hand side of the comparison. 63 | * @param right The right-hand side of the comparison. 64 | */ 65 | export function areEqual(left: ts.Node, right: ts.Node): boolean { 66 | if (left === right) { 67 | return true; 68 | } else if (ts.isIdentifier(left) && ts.isIdentifier(right)) { 69 | return left.text !== '' && left.text === right.text; 70 | } else if (ts.isPropertyAccessExpression(left) && ts.isPropertyAccessExpression(right)) { 71 | return areEqual(left.name, right.name) 72 | && areEqual(left.expression, right.expression); 73 | } else { 74 | return false; 75 | } 76 | } 77 | 78 | export const noAssignmentTokenMapping = { 79 | [ts.SyntaxKind.PlusEqualsToken]: ts.SyntaxKind.PlusToken, 80 | [ts.SyntaxKind.MinusEqualsToken]: ts.SyntaxKind.MinusToken, 81 | [ts.SyntaxKind.AsteriskEqualsToken]: ts.SyntaxKind.AsteriskToken, 82 | [ts.SyntaxKind.AsteriskAsteriskEqualsToken]: ts.SyntaxKind.AsteriskAsteriskToken, 83 | [ts.SyntaxKind.SlashEqualsToken]: ts.SyntaxKind.SlashToken, 84 | [ts.SyntaxKind.PercentEqualsToken]: ts.SyntaxKind.PercentToken, 85 | [ts.SyntaxKind.LessThanLessThanEqualsToken]: ts.SyntaxKind.LessThanLessThanToken, 86 | [ts.SyntaxKind.GreaterThanGreaterThanEqualsToken]: ts.SyntaxKind.GreaterThanGreaterThanToken, 87 | [ts.SyntaxKind.AmpersandEqualsToken]: ts.SyntaxKind.AmpersandToken, 88 | [ts.SyntaxKind.BarEqualsToken]: ts.SyntaxKind.BarToken, 89 | [ts.SyntaxKind.CaretEqualsToken]: ts.SyntaxKind.CaretToken, 90 | [ts.SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken]: ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken 91 | }; 92 | 93 | const assignmentTokenMapping = (() => { 94 | let results: any = {}; 95 | for (let key in noAssignmentTokenMapping) { 96 | results[noAssignmentTokenMapping[key]] = parseInt(key); 97 | } 98 | return results; 99 | })(); 100 | -------------------------------------------------------------------------------- /ts-closure-transform/tests/index.spec.ts: -------------------------------------------------------------------------------- 1 | import compile, { CJS_CONFIG } from '../compile'; 2 | import { resolve } from 'path'; 3 | import { equal } from 'node:assert'; 4 | import * as fs from 'fs'; 5 | import * as ts from 'typescript'; 6 | 7 | // Note to test authors: the test runner is designed to 8 | // extract and compile tests automatically. You don't need 9 | // to define tests manually here. 10 | // 11 | // * Compilation (input, output checks) are extracted as 12 | // (*.ts, *.out.js) pairs from the 'fixture' folder. 13 | // Each *.ts file is compiled to JavaScript (including 14 | // the ts-serialize-closures transformation) and then 15 | // the test runner checks that the resulting JavaScript 16 | // matches the code in the corresponding *.out.js file. 17 | // 18 | // To add a new test, simply create a new (*.ts, *.out.js) 19 | // pair in the 'fixture' folder. The test runner will 20 | // automatically include it. 21 | // 22 | // * Serialization tests are extracted from the 'serialization' 23 | // folder. Each *.ts file there is first compiled (with the 24 | // custom transformation on) and subsequently imported. 25 | // The compiled serialization test file's exports are treated as 26 | // unit tests. 27 | 28 | /** 29 | * Asserts that a particular source file compiles to 30 | * a particular output. 31 | * @param sourceFile The source file to test. 32 | * @param expectedOutput The source file's expected output. 33 | */ 34 | function assertCompilesTo(sourceFile: string, expectedOutput: string) { 35 | function writeFileCallback( 36 | fileName: string, 37 | data: string, 38 | writeByteOrderMark: boolean, 39 | onError: ((message: string) => void) | undefined, 40 | sourceFiles: ReadonlyArray | undefined): void { 41 | 42 | let trimmedData = data.trim(); 43 | let trimmedOutput = expectedOutput.trim(); 44 | 45 | if (trimmedOutput.length === 0 && trimmedData !== trimmedOutput) { 46 | // If the output is empty and the data is nonempty, then that's 47 | // probably an indication that the author of the test hasn't filled 48 | // out the output file. 49 | // Let's help them out by printing the expected output before 50 | // we hit them with the error message. 51 | 52 | console.log(`No expected output provided for ${fileName}. Actual output is:\n`); 53 | console.log(data); 54 | } 55 | 56 | equal(trimmedData, trimmedOutput); 57 | } 58 | 59 | compile( 60 | resolve(__dirname, `fixture/${sourceFile}`), 61 | { ...CJS_CONFIG, target: ts.ScriptTarget.Latest }, 62 | writeFileCallback, 63 | false); 64 | } 65 | 66 | /** 67 | * Reads a text file as a string. 68 | * @param fileName The name of the file to read. 69 | */ 70 | function readTextFile(fileName: string) { 71 | return fs.readFileSync(resolve(__dirname, fileName), { encoding: 'utf8' }); 72 | } 73 | 74 | /** 75 | * Gathers pairs of test files. Each pair contains a source file 76 | * and an output file. 77 | */ 78 | function getTestFilePairs(): { sourceFileName: string, outputFileName: string }[] { 79 | return getFilesWithExtension('fixture', 'ts').map(path => { 80 | let outputFileName = path.substring(0, path.length - ".ts".length) + ".out.js"; 81 | return { sourceFileName: path, outputFileName }; 82 | }); 83 | } 84 | 85 | /** 86 | * Gets a list of all files in a directory with a particular extension. 87 | * @param dir The directory to look in. 88 | * @param ext The extension to look for. 89 | */ 90 | function getFilesWithExtension(dir: string, ext: string): ReadonlyArray { 91 | return fs.readdirSync(resolve(__dirname, dir)) 92 | .filter(path => 93 | path.length > ext.length + 1 && path.substr(path.length - ext.length - 1) == "." + ext); 94 | } 95 | 96 | describe('Compilation', () => { 97 | // Compile all test files and make sure they match the expected output. 98 | for (let { sourceFileName, outputFileName } of getTestFilePairs()) { 99 | it(sourceFileName, () => { 100 | assertCompilesTo(sourceFileName, readTextFile(`fixture/${outputFileName}`)); 101 | }); 102 | } 103 | }); 104 | 105 | describe('Serialization', () => { 106 | for (let fileName of getFilesWithExtension('serialization', 'ts')) { 107 | compile(resolve(__dirname, `serialization/${fileName}`), undefined, undefined, false); 108 | 109 | let jsFileName = resolve(__dirname, `serialization/${fileName.substr(0, fileName.length - 3)}.js`); 110 | let compiledModule = require(jsFileName); 111 | for (let exportedTestName in compiledModule) { 112 | let exportedTest = compiledModule[exportedTestName]; 113 | if (exportedTest instanceof Function) { 114 | it(`${fileName}:${exportedTestName}`, exportedTest); 115 | } 116 | } 117 | } 118 | }); 119 | -------------------------------------------------------------------------------- /transform-benchmarker/process_results.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This script loads Octane scores and file sizes from the results/ 4 | # folder and aggregates them into two CSVs: one representing score- 5 | # related measurements and another representing size-related measurements. 6 | 7 | import csv 8 | import math 9 | from itertools import islice 10 | 11 | 12 | def read_results(file_name): 13 | """Reads a CSV file containing results. Produces a mapping of benchmark 14 | names to numerical results.""" 15 | results = {} 16 | with open(file_name, 'r') as csvfile: 17 | spamreader = csv.reader(csvfile, delimiter=',', quotechar='|') 18 | for row in islice(spamreader, 1, None): 19 | results[row[0]] = float(row[1]) 20 | return results 21 | 22 | 23 | def aggregate_results(baseline, *others): 24 | """Aggregates results. Takes a baseline and a number of other measurements, 25 | divides all measurements by the baseline on a per-benchmark basis.""" 26 | 27 | def aggregate_benchmark(key, results): 28 | if key in results and results[key] != 0.0: 29 | return results[key] / baseline[key] 30 | else: 31 | return float('nan') 32 | 33 | results = [] 34 | for key in sorted(baseline.keys()): 35 | results.append((key, 1.0) + tuple(aggregate_benchmark(key, xs) 36 | for xs in others)) 37 | return results 38 | 39 | 40 | def aggregate_category(baseline_file, *other_files): 41 | """Aggregates result files for a particular category of benchmarks.""" 42 | baseline = read_results(baseline_file) 43 | others = [read_results(name) for name in other_files] 44 | return aggregate_results(baseline, *others) 45 | 46 | 47 | def write_aggregated(destination, aggregated, *names): 48 | """Writes aggregated results back to a CSV file.""" 49 | with open(destination, 'w') as csvfile: 50 | fieldnames = ['benchmark'] + list(names) 51 | writer = csv.DictWriter(csvfile, fieldnames=fieldnames) 52 | 53 | writer.writeheader() 54 | for row in aggregated: 55 | writer.writerow( 56 | {key: value for key, value in zip(fieldnames, row)}) 57 | 58 | # We'll return a dictionary that maps names to their mean scores 59 | # relative to the baseline. 60 | results = {key: 0.0 for key in names} 61 | counts = {key: 0 for key in names} 62 | for row in aggregated: 63 | # Dropping numbers was fair back when we were comparing 64 | # FlashFreeze to ThingsMigrate only, but now that we're 65 | # having a three-way comparison between FlashFreeze, 66 | # ThingsMigrate and Disclosure, picking a subset of benchmarks 67 | # on which all techniques succeed is actually fairly disingenuous 68 | # because it really affects the results we get. 69 | # 70 | # For instance, ThingsMigrate performs really well on the 'zlib' 71 | # benchmark, which helped improve ThingsMigrate's overall score. 72 | # This means that if we put Disclosure in the mix (which fails on 73 | # 'zlib') and discard all benchmarks on which any technique fails, 74 | # then we're effectively penalizing ThingsMigrate for little reason. 75 | # That's hardly fair, explaining why the rule below has been removed. 76 | # 77 | # if any(filter(math.isnan, row[1:])): 78 | # # This benchmark errored for at least one instrumentation 79 | # # technique. We'll drop it entirely in the interest of fairness. 80 | # continue 81 | 82 | for key, value in zip(names, row[1:]): 83 | if not math.isnan(value): 84 | results[key] += value 85 | counts[key] += 1 86 | 87 | for key in names: 88 | results[key] /= counts[key] 89 | 90 | return results 91 | 92 | 93 | print('Score means:') 94 | print( 95 | write_aggregated( 96 | 'results/scores.csv', 97 | aggregate_category('results/original-scores.csv', 98 | 'results/flash-freeze-scores.csv', 99 | 'results/things-js-scores.csv', 100 | 'results/disclosure-scores.csv'), 101 | 'original', 102 | 'flash-freeze', 103 | 'things-js', 104 | 'disclosure')) 105 | 106 | print('Size means:') 107 | print( 108 | write_aggregated( 109 | 'results/sizes.csv', 110 | aggregate_category('results/original-sizes.csv', 111 | 'results/flash-freeze-sizes.csv', 112 | 'results/things-js-sizes.csv', 113 | 'results/disclosure-sizes.csv'), 114 | 'original', 115 | 'flash-freeze', 116 | 'things-js', 117 | 'disclosure')) 118 | 119 | print('Score coordinates:') 120 | print(','.join(sorted(read_results('results/original-scores.csv')))) 121 | 122 | print('Size coordinates:') 123 | print(','.join(sorted(read_results('results/original-sizes.csv')))) 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) 2 | [![npm version](https://badge.fury.io/js/serialize-closures.svg)](https://badge.fury.io/js/serialize-closures) 3 | [![npm version](https://badge.fury.io/js/ts-closure-transform.svg)](https://badge.fury.io/js/ts-closure-transform) 4 | [![Build Status](https://travis-ci.org/nokia/ts-serialize-closures.svg?branch=master)](https://travis-ci.org/nokia/ts-serialize-closures) 5 | 6 | # ts-serialize-closures 7 | 8 | Serialize your TypeScript functions! 9 | 10 | `ts-serialize-closures` can serialize and deserialize arbitrary TypeScript/JavaScript object graphs. That includes: 11 | 12 | * **functions, which may have captured data,** 13 | * `Date` and `RegExp` objects, 14 | * cyclic graphs, 15 | * prototypes, 16 | * references to built-in objects, 17 | * etc. 18 | 19 | The idea is that `serialize` creates a self-contained snapshot of all program state relevant to the object being serialized. The `deserialize` function decodes that snapshot back to a JavaScript object graph. 20 | 21 | This tool might be useful in a number of scenarios: 22 | 23 | * Exchanging functions between different processes. That's often a useful tool for building distributed systems. 24 | 25 | * Lightweight remote post-mortem debugging: have failing processes create a neat little snapshot of their current state and send yourself that snapshot for analysis. 26 | 27 | ## Usage 28 | 29 | The serializer (`serialize-closures`) requires a preprocessing step (`ts-closure-transform`). Therefore, the typical usage of this library is to configure webpack to automatically transform the source code using a hook in the TypeScript compiler (`tsc`). Take the following steps to set up a stand-alone example: 30 | 31 | 1. Prepare project and install dev-dependencies 32 | ```bash 33 | mkdir example && cd example 34 | npm init 35 | npm install --save-dev ts-closure-transform serialize-closures webpack webpack-cli typescript ts-loader util 36 | ``` 37 | 38 | 2.1 Configure `tsconfig.json`: 39 | ```json 40 | { 41 | "compileOnSave": true, 42 | "compilerOptions": { 43 | "target": "ES5", 44 | "module": "commonjs", 45 | "declaration": true, 46 | "moduleResolution": "node", 47 | "stripInternal": true, 48 | "jsx": "react", 49 | "outDir": "dist" 50 | }, 51 | "include": [ 52 | "src/**/*" 53 | ], 54 | "exclude": [ 55 | "node_modules", 56 | "dist" 57 | ] 58 | } 59 | ``` 60 | 61 | 2.2 Configure `webpack.config.js`: 62 | ```javascript 63 | const tsClosureTransform = require('ts-closure-transform'); 64 | const path = require('path'); 65 | module.exports = { 66 | entry: { 67 | example: './src/example.ts', 68 | }, 69 | mode: 'development', 70 | module: { 71 | rules: [ 72 | { 73 | test: /.tsx?$/, 74 | loader: 'ts-loader', // or 'awesome-typescript-loader' 75 | options: { 76 | getCustomTransformers: () => ({ 77 | before: [tsClosureTransform.beforeTransform()], 78 | after: [tsClosureTransform.afterTransform()] 79 | }) 80 | } 81 | } 82 | ] 83 | }, 84 | resolve: { 85 | extensions: [ '.tsx', '.ts', '.js' ], 86 | fallback: { 87 | "util": require.resolve("util/"), 88 | }, 89 | }, 90 | output: { 91 | path: path.join(__dirname, 'dist'), 92 | filename: '[name].bundle.js', 93 | } 94 | } 95 | ``` 96 | 97 | 3. Write code `src/example.ts` to serialize and deserialize arbitrary functions: 98 | ```typescript 99 | import { serialize, deserialize } from 'serialize-closures'; 100 | // Just about anything can be serialized by calling `serialize`. 101 | let capturedVariable = 5; 102 | let serialized = serialize(() => capturedVariable); 103 | 104 | // Serialized representations can be stringified and parsed. 105 | let text = JSON.stringify(serialized); 106 | let parsed = JSON.parse(text); 107 | 108 | // Serialized representations can be deserialized by calling `deserialize`. 109 | console.log(deserialize(serialized)()); // Prints '5'. 110 | console.log(deserialize(parsed)()); // Prints '5'. 111 | ``` 112 | 113 | 4. Compile with webpack and run the sample 114 | ```bash 115 | npx webpack 116 | node dist/example.bundle.js 117 | ``` 118 | 119 | ## Components 120 | 121 | The serializer consists of two components. 122 | 123 | 1. `ts-closure-transform`: a transformation to inject in the TypeScript compiler's pass pipeline. This transformation will rewrite all function definitions to include a special `__closure` property. The serializer uses that `__closure` property to figure out which variables are captured by the function. 124 | 125 | How you inject this transform depends on the webpack loader you're using. For `ts-loader` and `awesome-typescript-loader`, you can do the following: 126 | 127 | ```typescript 128 | import { beforeTransform, afterTransform } from 'ts-closure-transform'; 129 | // ... 130 | loader: 'ts-loader', 131 | options: { 132 | getCustomTransformers: () => ({ 133 | before: [beforeTransform()], 134 | after: [afterTransform()] 135 | }) 136 | } 137 | // ... 138 | ``` 139 | 140 | Note that `ts-closure-transform` is strictly a dev dependency: there's no need to package it with your application. 141 | 142 | 2. `serialize-closures`: a runtime library that defines the `serialize` and `deserialize` functions. These should work for any object graph as long as all source code has first been processed by `ts-closure-transform`. 143 | 144 | 145 | ## Limitations 146 | 147 | `ts-serialize-closures` works fairly well for modest object graphs, but it does have a number of limitations you should be aware of: 148 | 149 | * Variable-capturing functions defined in files that have not been transformed by `ts-closure-transform` cannot be deserialized correctly. 150 | 151 | * Serializing class definitions works, but only if they are first lowered to function definitions by the TypeScript compiler, i.e., the target is ES5 or lower. 152 | 153 | * Functions can only be serialized and deserialized *once.* There is no support for serializing a deserialized function. 154 | -------------------------------------------------------------------------------- /serialize-closures/src/builtins.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A record for a single builtin in a list of builtins. 3 | */ 4 | export type BuiltinRecord = { name: string, builtin: any }; 5 | 6 | /** 7 | * A read-only list of builtins. 8 | */ 9 | export type BuiltinList = ReadonlyArray; 10 | 11 | /** 12 | * A list of all global JavaScript objects. 13 | */ 14 | export const rootBuiltinNames: ReadonlyArray = [ 15 | // This list is based on the list of JavaScript global 16 | // objects at 17 | // 18 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects 19 | 20 | 'Object', 21 | 'Function', 22 | 'Boolean', 23 | 'Symbol', 24 | 'Error', 25 | 'EvalError', 26 | 'InternalError', 27 | 'RangeError', 28 | 'ReferenceError', 29 | 'SyntaxError', 30 | 'TypeError', 31 | 'URIError', 32 | 33 | // # Numbers and dates 34 | 'Number', 35 | 'BigInt', 36 | 'Math', 37 | 'Date', 38 | 39 | // # Text processing 40 | 'String', 41 | 'RegExp', 42 | 43 | // # Indexed collections 44 | 'Array', 45 | 'Int8Array', 46 | 'Uint8Array', 47 | 'Uint8ClampedArray', 48 | 'Int16Array', 49 | 'Uint16Array', 50 | 'Int32Array', 51 | 'Uint32Array', 52 | 'Float32Array', 53 | 'Float64Array', 54 | 'BigInt64Array', 55 | 'BigUint64Array', 56 | 57 | // # Keyed collections 58 | 'Map', 59 | 'Set', 60 | 'WeakMap', 61 | 'WeakSet', 62 | 63 | // # Structured data 64 | 'ArrayBuffer', 65 | 'SharedArrayBuffer', 66 | 'Atomics', 67 | 'DataView', 68 | 'JSON', 69 | 70 | // # Control abstraction objects 71 | 'Promise', 72 | 'Generator', 73 | 'GeneratorFunction', 74 | 'AsyncFunction', 75 | 76 | // # Reflection 77 | 'Reflect', 78 | 'Proxy', 79 | 80 | // # Internationalization 81 | 'Intl', 82 | 83 | // # WebAssembly 84 | 'WebAssembly', 85 | 86 | // # Value properties 87 | 'Infinity', 88 | 'NaN', 89 | 90 | // # Function properties 91 | 'eval', 92 | 'parseInt', 93 | 'parseFloat', 94 | 'isNaN', 95 | 'isFinite', 96 | 'decodeURI', 97 | 'decodeURIComponent', 98 | 'encodeURI', 99 | 'encodeURIComponent', 100 | 'escape', 101 | 'unescape', 102 | 103 | // # Extra additions: partial window / WorkerGlobalScope builtins 104 | 'console', 105 | 'clearInterval', 106 | 'clearTimeout', 107 | 'queueMicrotask', 108 | 'setInterval', 109 | 'setTimeout' 110 | ]; 111 | 112 | /** 113 | * A default collection of builtins to give special treatment. 114 | */ 115 | export const defaultBuiltins: BuiltinList = generateDefaultBuiltins(); 116 | 117 | /** 118 | * Generates the default collection of builtins in the current context. 119 | * This may differ from the value in `defaultBuiltins` if this function 120 | * is called from a different VM context or if a different `evalImpl` 121 | * function is specified. 122 | * @param rootNames A list of root builtin names. 123 | * If not specified, `rootBuiltinNames` is assumed. 124 | * @param evalImpl A custom `eval` function to use. 125 | */ 126 | export function generateDefaultBuiltins( 127 | rootNames?: ReadonlyArray, 128 | evalImpl?: (code: string) => any): BuiltinList { 129 | 130 | evalImpl = evalImpl || eval; 131 | rootNames = rootNames || rootBuiltinNames; 132 | return expandBuiltins( 133 | rootNames 134 | .filter(name => evalImpl(`typeof ${name}`) !== 'undefined') 135 | .map(name => ({ name, builtin: evalImpl(name) }))); 136 | } 137 | 138 | /** 139 | * Takes a list of root builtins and expands it to include all 140 | * properties reachable from those builtins. 141 | * @param roots The root builtins to start searching from. 142 | */ 143 | export function expandBuiltins(roots: BuiltinList): BuiltinList { 144 | 145 | let results: BuiltinRecord[] = []; 146 | let worklist: BuiltinRecord[] = []; 147 | worklist.push(...roots); 148 | 149 | function addToWorklist(baseName, propertyName, builtin) { 150 | if (!isPrimitive(builtin)) { 151 | worklist.push({ 152 | name: `${baseName}.${propertyName}`, 153 | builtin 154 | }); 155 | } 156 | } 157 | 158 | while (worklist.length > 0) { 159 | let record = worklist.shift(); 160 | if (getNameOfBuiltin(record.builtin, results) === undefined) { 161 | // Builtin does not exist already. Add it to the results. 162 | results.push(record); 163 | // Add the builtin's properties to the worklist. 164 | for (let propName of Object.getOwnPropertyNames(record.builtin)) { 165 | if (propName === 'callee' || propName === 'caller' || propName === 'arguments') { 166 | continue; 167 | } else { 168 | try { 169 | let desc = Object.getOwnPropertyDescriptor(record.builtin, propName); 170 | if (desc.value) { 171 | addToWorklist(record.name, propName, desc.value); 172 | } 173 | } catch (e) { 174 | // Ignore inaccessible methods in Node <10, e.g. TypeError: Method bytesRead called on incompatible receiver # 175 | } 176 | } 177 | } 178 | // Add the builtin's prototype to the worklist. 179 | addToWorklist(record.name, '__proto__', Object.getPrototypeOf(record.builtin)); 180 | addToWorklist(record.name, 'prototype', record.builtin.prototype); 181 | } 182 | } 183 | return results; 184 | } 185 | 186 | /** 187 | * Gets a builtin by name. 188 | * @param builtinName The name of the builtin to look for. 189 | * @param builtinList An optional list of builtins to search through. 190 | * If defined, `builtinList` is used instead of the default builtins. 191 | * @returns The builtin if there is a builtin matching the 192 | * given name; otherwise, `false`. 193 | */ 194 | export function getBuiltinByName(builtinName: string, builtinList?: BuiltinList): any { 195 | builtinList = builtinList || defaultBuiltins; 196 | for (let { name, builtin } of builtinList) { 197 | if (name === builtinName) { 198 | return builtin; 199 | } 200 | } 201 | return undefined; 202 | } 203 | 204 | /** 205 | * Gets the name of a builtin. 206 | * @param value The builtin to name. 207 | * @param builtinList An optional list of builtins to search through. 208 | * If defined, `builtinList` is used instead of the default builtins. 209 | * @returns The name of `value` if it is a builtin; otherwise, `undefined`. 210 | */ 211 | export function getNameOfBuiltin(value: any, builtinList?: BuiltinList): string | undefined { 212 | builtinList = builtinList || defaultBuiltins; 213 | for (let { name, builtin } of builtinList) { 214 | if (value === builtin) { 215 | return name; 216 | } 217 | } 218 | return undefined; 219 | } 220 | 221 | function isPrimitive(arg: any) { 222 | return arg === null || (typeof arg !== "object" && typeof arg !== "function") 223 | } -------------------------------------------------------------------------------- /ts-closure-transform/src/flatten-destructured-imports.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | // This module implements a transformation that flattens destructured import 4 | // statements. 5 | // 6 | // Essentially, what we're doing here is the following transformation: 7 | // 8 | // import { a, b } from 'c'; 9 | // 10 | // --> 11 | // 12 | // import * as temp from 'c'; 13 | // var a = temp.a; 14 | // var b = temp.b; 15 | // 16 | // The reason for why we do this is essentially just how the TypeScript 17 | // compiler works: imports and exports are rewritten in a pretty underhanded 18 | // way that breaks the closure transform. 19 | // 20 | // If we didn't do this transform, then we would get situations like the following: 21 | // 22 | // Source: 23 | // import { a } from 'c'; 24 | // let f = () => a; 25 | // 26 | // Closure transform: 27 | // import { a } from 'c'; 28 | // var temp; 29 | // let f = (temp = () => a, temp.__closure = () => ({ a: a }), temp); 30 | // 31 | // Module import/export transform: 32 | // var _c = require('c'); 33 | // var temp; 34 | // let f = (temp = () => _c.a, temp.__closure = () => ({ a: _c.a }), temp); 35 | // 36 | // The end result will fail horribly once deserialized because 'f' doesn't actually 37 | // capture 'a' in the final code. 38 | // 39 | // We can't control the module import/export transform, but one thing we can do is 40 | // insert a transform of our own prior to the closure transform. That's where this 41 | // transform comes in. 42 | 43 | /** 44 | * Applies a mapping to all unqualified identifiers in a node. 45 | * @param node The node to visit (recursively). 46 | * @param mapping The mapping to apply to all unqualified identifiers. 47 | * @param ctx A transformation context. 48 | */ 49 | function mapUnqualifiedIdentifiers( 50 | node: T, 51 | mapping: (identifier: ts.Identifier) => ts.Identifier, 52 | ctx: ts.TransformationContext): T { 53 | 54 | function visit(node: TNode): TNode { 55 | if (node === undefined) { 56 | return undefined; 57 | } else if (ts.isIdentifier(node)) { 58 | return mapping(node); 59 | } else if (ts.isPropertyAccessExpression(node)) { 60 | return ts.factory.updatePropertyAccessExpression( 61 | node, 62 | visit(node.expression), 63 | node.name); 64 | } else if (ts.isPropertyAssignment(node)) { 65 | return ts.factory.updatePropertyAssignment( 66 | node, 67 | node.name, 68 | visit(node.initializer)); 69 | } else if (ts.isShorthandPropertyAssignment(node)) { 70 | return ts.factory.updateShorthandPropertyAssignment( 71 | node, 72 | node.name, 73 | visit(node.objectAssignmentInitializer)); 74 | } else { 75 | return ts.visitEachChild(node, visit, ctx); 76 | } 77 | } 78 | 79 | return visit(node); 80 | } 81 | 82 | /** 83 | * Creates a visitor that rewrites imports. 84 | * @param ctx A transformation context. 85 | */ 86 | function createVisitor(ctx: ts.TransformationContext): ts.Visitor { 87 | function visitTopLevel(topLevel: T): T { 88 | 89 | // Maintain a set of all imports that have been flattened. 90 | let modifiedSet: string[] = []; 91 | 92 | function visit(node: ts.Node): ts.VisitResult { 93 | if (ts.isImportDeclaration(node)) { 94 | let clause = node.importClause; 95 | if (clause) { 96 | // Create a temporary name for the imported module. 97 | let temp = ts.factory.createUniqueName("_tct_flatten_destructured_imports"); 98 | // Bind each import to a variable. 99 | let importBindings = []; 100 | 101 | if (clause.name) { 102 | // Process the default import statement 103 | importBindings.push( 104 | ts.factory.createVariableStatement( 105 | [], 106 | [ 107 | ts.factory.createVariableDeclaration( 108 | clause.name, 109 | undefined, 110 | undefined, 111 | ts.factory.createPropertyAccessExpression(temp, "default")) 112 | ])); 113 | modifiedSet.push(clause.name.text); 114 | } 115 | let bindings = clause.namedBindings; 116 | if (bindings && ts.isNamespaceImport(bindings)) { 117 | importBindings.push( 118 | ts.factory.createVariableStatement( 119 | [], 120 | [ 121 | ts.factory.createVariableDeclaration( 122 | bindings.name, 123 | undefined, 124 | undefined, 125 | temp) 126 | ])); 127 | modifiedSet.push(bindings.name.text); 128 | } 129 | if (bindings && ts.isNamedImports(bindings)) { 130 | // Named imports. That's exactly what we're looking for. 131 | for (let specifier of bindings.elements) { 132 | importBindings.push( 133 | ts.factory.createVariableStatement( 134 | [], 135 | [ 136 | ts.factory.createVariableDeclaration( 137 | specifier.name, 138 | undefined, 139 | undefined, 140 | ts.factory.createPropertyAccessExpression(temp, specifier.propertyName || specifier.name)) 141 | ])); 142 | modifiedSet.push(specifier.name.text); 143 | } 144 | } 145 | return [ 146 | ts.factory.updateImportDeclaration( 147 | node, 148 | node.modifiers, 149 | ts.factory.updateImportClause( 150 | clause, 151 | clause.isTypeOnly, 152 | clause.name, 153 | ts.factory.createNamespaceImport(temp)), 154 | node.moduleSpecifier, node.attributes), 155 | ...importBindings 156 | ]; 157 | } 158 | return ts.visitEachChild(node, visit, ctx); 159 | } else { 160 | return ts.visitEachChild(node, visit, ctx); 161 | } 162 | } 163 | 164 | let visited = visit(topLevel); 165 | return mapUnqualifiedIdentifiers( 166 | visited, 167 | ident => { 168 | if (modifiedSet.indexOf(ident.text) >= 0) { 169 | // Replace the original identifier with a synthetic 170 | // identifier to keep the TypeScript compiler from 171 | // applying its import/export voodoo where it shouldn't. 172 | return ts.factory.createIdentifier(ident.text); 173 | } else { 174 | return ident; 175 | } 176 | }, 177 | ctx); 178 | } 179 | 180 | return visitTopLevel; 181 | } 182 | 183 | export default function () { 184 | return (ctx: ts.TransformationContext): ts.Transformer => { 185 | return (sf: ts.SourceFile) => ts.visitNode(sf, createVisitor(ctx)) as ts.SourceFile; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /ts-closure-transform/src/box-mutable-captured-vars.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { VariableVisitor, VariableId, VariableNumberingScope, VariableNumberingStore } from './variable-visitor'; 3 | 4 | // This is a compiler pass that takes mutable captured variables and wraps 5 | // them in 'box' objects. 6 | // 7 | // Here's a quick example: 8 | // 9 | // function counter() { 10 | // let ctr = 0; 11 | // return { 12 | // 'get': (() => ctr), 13 | // 'increment': (() => { ctr++; }) 14 | // }; 15 | // } 16 | // 17 | // will get transformed to 18 | // 19 | // function counter() { 20 | // let ctr = { value: undefined }; 21 | // ctr.value = 0; 22 | // return { 23 | // 'get': (() => ctr.value), 24 | // 'increment': (() => { ctr.value++; }) 25 | // }; 26 | // } 27 | // 28 | // By performing this transformation, functions capturing the mutable variable will 29 | // share an object reference to the shared value instead of the value itself. 30 | // We can't directly serialize shared values without un-sharing them, but we can 31 | // serialize object references to shared values. 32 | 33 | /** 34 | * A variable visitor that tracks down mutable shared variables. 35 | */ 36 | export class MutableSharedVariableFinder extends VariableVisitor { 37 | /** 38 | * A mapping of variable IDs to the number of times those variables 39 | * are updated. 40 | */ 41 | private readonly updateCounts: { [id: number]: number }; 42 | 43 | /** 44 | * A mapping of variable IDs to the scopes that define those IDs. 45 | */ 46 | private readonly defScopes: { [id: number]: VariableNumberingScope }; 47 | 48 | /** 49 | * A mapping of variable IDs to the scopes wherein they are used. 50 | * This mapping only contains IDs for variables that are still pending, 51 | * i.e., they have not been assigned a definition scope yet. 52 | */ 53 | private readonly pendingAppearanceScopes: { [id: number]: VariableNumberingScope[] }; 54 | 55 | /** 56 | * A list of all shared variable IDs. 57 | */ 58 | private readonly sharedVars: VariableId[]; 59 | 60 | /** 61 | * Creates a mutable shared variable finder. 62 | * @param ctx A transformation context. 63 | */ 64 | constructor(ctx: ts.TransformationContext) { 65 | super(ctx); 66 | this.updateCounts = {}; 67 | this.defScopes = {}; 68 | this.pendingAppearanceScopes = {}; 69 | this.sharedVars = []; 70 | } 71 | 72 | /** 73 | * Gets a list of all shared variables detected by this variable visitor. 74 | */ 75 | get sharedVariables(): ReadonlyArray { 76 | return this.sharedVars; 77 | } 78 | 79 | /** 80 | * Gets a list of all shared mutable variables detected by this variable visitor. 81 | */ 82 | get mutableSharedVariables(): ReadonlyArray { 83 | let results = []; 84 | for (let id of this.sharedVars) { 85 | if (this.updateCounts[id] > 1) { 86 | results.push(id); 87 | } 88 | } 89 | return results; 90 | } 91 | 92 | protected visitUse(node: ts.Identifier, id: VariableId): ts.Expression { 93 | this.noteAppearance(id); 94 | 95 | return node; 96 | } 97 | 98 | protected visitDef(node: ts.Identifier, id: VariableId): ts.Expression { 99 | this.defScopes[id] = this.scope.functionScope; 100 | 101 | if (id in this.pendingAppearanceScopes) { 102 | // Handle pending appearances. 103 | for (let scope of this.pendingAppearanceScopes[id]) { 104 | this.noteAppearanceIn(id, scope); 105 | } 106 | delete this.pendingAppearanceScopes[id]; 107 | } 108 | 109 | return undefined; 110 | } 111 | 112 | protected visitAssignment(name: ts.Identifier, id: VariableId): 113 | (assignment: ts.BinaryExpression) => ts.Expression { 114 | 115 | this.noteAppearance(id); 116 | 117 | if (!this.updateCounts[id]) { 118 | this.updateCounts[id] = 0; 119 | } 120 | this.updateCounts[id]++; 121 | 122 | return undefined; 123 | } 124 | 125 | private noteAppearance(id: VariableId) { 126 | if (id in this.defScopes) { 127 | this.noteAppearanceIn(id, this.scope); 128 | } else { 129 | if (!(id in this.pendingAppearanceScopes)) { 130 | this.pendingAppearanceScopes[id] = []; 131 | } 132 | this.pendingAppearanceScopes[id].push(this.scope.functionScope); 133 | } 134 | } 135 | 136 | private noteAppearanceIn(id: VariableId, scope: VariableNumberingScope) { 137 | if (this.defScopes[id] !== scope.functionScope) { 138 | if (this.sharedVars.indexOf(id) < 0) { 139 | this.sharedVars.push(id); 140 | } 141 | } 142 | } 143 | } 144 | 145 | /** 146 | * A variable visitor that boxes variables. 147 | */ 148 | class VariableBoxingVisitor extends VariableVisitor { 149 | /** 150 | * A list of all variables to box. 151 | */ 152 | readonly variablesToBox: ReadonlyArray; 153 | 154 | /** 155 | * Creates a variable boxing visitor. 156 | * @param ctx A transformation context. 157 | * @param store The variable numbering store to use. 158 | * @param variablesToBox The variables to box, numbered by `store`. 159 | */ 160 | constructor( 161 | ctx: ts.TransformationContext, 162 | store: VariableNumberingStore, 163 | variablesToBox: ReadonlyArray) { 164 | 165 | super(ctx, store); 166 | this.variablesToBox = variablesToBox; 167 | } 168 | 169 | protected visitUse(node: ts.Identifier, id: VariableId): ts.Expression { 170 | if (this.variablesToBox.indexOf(id) >= 0) { 171 | return ts.factory.createPropertyAccessExpression(node, "value"); 172 | } else { 173 | return node; 174 | } 175 | } 176 | 177 | protected visitDef(node: ts.Identifier, id: VariableId): ts.Expression { 178 | if (this.variablesToBox.indexOf(id) >= 0) { 179 | return ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment("value", ts.factory.createIdentifier("undefined"))]); 180 | } else { 181 | return undefined; 182 | } 183 | } 184 | 185 | protected visitAssignment(name: ts.Identifier, id: VariableId): (assignment: ts.BinaryExpression) => ts.Expression { 186 | if (this.variablesToBox.indexOf(id) >= 0) { 187 | return assignment => ts.factory.updateBinaryExpression( 188 | assignment, 189 | ts.factory.createPropertyAccessExpression(name, "value"), 190 | assignment.operatorToken, 191 | assignment.right, 192 | ); 193 | } else { 194 | return undefined; 195 | } 196 | } 197 | } 198 | 199 | function createVisitor(ctx: ts.TransformationContext): ts.Visitor { 200 | return node => { 201 | let analyzer = new MutableSharedVariableFinder(ctx); 202 | node = analyzer.visit(node); 203 | let rewriter = new VariableBoxingVisitor( 204 | ctx, 205 | analyzer.store, 206 | analyzer.mutableSharedVariables); 207 | return rewriter.visit(node); 208 | }; 209 | } 210 | 211 | export default function () { 212 | return (ctx: ts.TransformationContext): ts.Transformer => { 213 | return (sf: ts.SourceFile) => ts.visitNode(sf, createVisitor(ctx)) as ts.SourceFile; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /serialize-closures/tests/roundtripping.spec.ts: -------------------------------------------------------------------------------- 1 | import { equal, deepEqual, notEqual } from 'node:assert'; 2 | import { deserialize, serialize, BuiltinList, generateDefaultBuiltins } from '../src'; 3 | import * as vm from 'vm'; 4 | import { CustomSerializerList, CustomSerializerRecord, CustomDeserializerRecord, CustomDeserializerList } from '../src/customs'; 5 | 6 | describe('Roundtripping', () => { 7 | function roundtrip(value, builtins?: BuiltinList, customSerializers?: CustomSerializerList, customDeserializers?: CustomDeserializerList) { 8 | return deserialize(JSON.parse(JSON.stringify(serialize(value, builtins, customSerializers))), builtins, customDeserializers); 9 | } 10 | 11 | function expectRoundtrip(value, builtins?: BuiltinList) { 12 | deepEqual(roundtrip(value, builtins), value); 13 | } 14 | 15 | it("can round-trip primitives", () => { 16 | expectRoundtrip(10); 17 | expectRoundtrip("hi"); 18 | expectRoundtrip(null); 19 | expectRoundtrip(undefined); 20 | expectRoundtrip(true); 21 | }); 22 | 23 | it("can round-trip arrays of primitives", () => { 24 | expectRoundtrip([10, 40]); 25 | expectRoundtrip(["hi"]); 26 | expectRoundtrip([null]); 27 | expectRoundtrip([undefined]); 28 | expectRoundtrip([true]); 29 | }); 30 | 31 | it("can round-trip simple objects", () => { 32 | expectRoundtrip({ 'hi': 'there' }); 33 | }); 34 | 35 | it("can round-trip shorthand objects", () => { 36 | let hi = 'hi'; 37 | expectRoundtrip({ hi }); 38 | }); 39 | 40 | it("can round-trip class-like objects", () => { 41 | let obj = {}; 42 | Object.defineProperty(obj, 'hi', { get: () => 'there' }); 43 | equal(roundtrip(obj).hi, 'there'); 44 | }); 45 | 46 | it("can round-trip dates", () => { 47 | expectRoundtrip(new Date("Thu, 28 Apr 2016 22:02:17 GMT")); 48 | }); 49 | 50 | it("can round-trip regexes", () => { 51 | expectRoundtrip(/([^\s]+)/g); 52 | }); 53 | 54 | it("can round-trip functions without closures", () => { 55 | equal( 56 | roundtrip(function (x) { return x; })(10), 57 | 10); 58 | }); 59 | 60 | it("can round-trip functions with closures", () => { 61 | let a = 10; 62 | let f: any = function (x) { return a + x; }; 63 | f.__closure = () => ({ a }); 64 | 65 | equal(roundtrip(f)(42), 52); 66 | }); 67 | 68 | it("can round-trip recursive functions", function () { 69 | let f: any = function (x) { return x < 5 ? f(x + 1) : x; }; 70 | f.__closure = function () { return ({ f }); }; 71 | equal(roundtrip(f)(1), 5); 72 | }); 73 | 74 | it("can round-trip accessors in objects", () => { 75 | let obj = { 76 | x: 'there', 77 | get() { return this.x } 78 | }; 79 | equal(roundtrip(obj).get(), 'there'); 80 | }); 81 | 82 | it("can round-trip named accessors in objects", () => { 83 | let obj = { 84 | x: 'there', 85 | get hi() { return this.x } 86 | }; 87 | equal(roundtrip(obj).hi, 'there'); 88 | }); 89 | 90 | it("can round-trip builtins", () => { 91 | expectRoundtrip(Math); 92 | }); 93 | 94 | it("can round-trip constructors", () => { 95 | function Vector2(x, y) { 96 | this.x = x; 97 | this.y = y; 98 | } 99 | Vector2.prototype.lengthSquared = function() { return this.x * this.x + this.y * this.y; }; 100 | let builder: any = () => new Vector2(3, 4); 101 | builder.__closure = () => ({ Vector2 }); 102 | equal(roundtrip(builder)().lengthSquared(), 25); 103 | }); 104 | 105 | it("can round-trip static methods", () => { 106 | var Person = /** @class */ (function () { 107 | var _a; 108 | var Person: any = function (name, email) { 109 | this.name = name; 110 | this.email = email; 111 | } 112 | Person.prototype.toString = function () { 113 | return this.name + " <" + this.email + ">"; 114 | }; 115 | Person.create = (_a = function (name, email) { 116 | return new Person(name, email); 117 | }, _a.__closure = () => ({ Person }), _a); 118 | return Person; 119 | }()); 120 | 121 | var create: any = function () { return Person.create("Clark Kent", "clark.kent@gmail.com"); }; 122 | create.__closure = () => ({ Person }); 123 | 124 | equal(roundtrip(create)().toString(), create().toString()); 125 | }); 126 | 127 | it("can round-trip custom builtins", () => { 128 | let myBuiltin = { value: "Oh hi Mark!" }; 129 | equal( 130 | roundtrip( 131 | myBuiltin, 132 | [{ name: "myBuiltin", builtin: myBuiltin }]), 133 | myBuiltin); 134 | }); 135 | 136 | it("works with vm.runInContext", () => { 137 | let context = vm.createContext(); 138 | let evalImpl = code => vm.runInContext(code, context); 139 | let box = evalImpl('{ value: "Oh hi Mark!" }'); 140 | let builtins = generateDefaultBuiltins(undefined, evalImpl); 141 | let roundtrippedBox = roundtrip(box, builtins); 142 | deepEqual(roundtrippedBox, box); 143 | deepEqual( 144 | Object.getPrototypeOf(roundtrippedBox), 145 | Object.getPrototypeOf(box) 146 | ); 147 | }); 148 | 149 | it("elides twice-underscore-prefixed properties", () => { 150 | deepEqual(roundtrip({ "__elide_this": 10 }), { }); 151 | }); 152 | 153 | it("accepts a custom `eval` implementation", () => { 154 | // This test serializes an object in the current context, 155 | // then creates a new context and deserializes the object 156 | // in that context. This represents the use-case of serializing 157 | // objects in one sandbox and deserializing them in another. 158 | let createBox = () => ({ value: "Oh hi Mark!" }); 159 | let serialized = serialize(createBox); 160 | 161 | let context = vm.createContext({ generateDefaultBuiltins }); 162 | let evalImpl = code => vm.runInContext(code, context); 163 | let builtins = evalImpl('generateDefaultBuiltins()'); 164 | let deserializedBox = deserialize(serialized, builtins, [], evalImpl)(); 165 | deepEqual(deserializedBox, createBox()); 166 | // Prototypes should be different because they originate 167 | // from different environments. 168 | notEqual(Object.getPrototypeOf(deserializedBox), Object.getPrototypeOf(createBox())); 169 | equal(Object.getPrototypeOf(deserializedBox), evalImpl("Object.prototype")); 170 | }); 171 | 172 | it("can round-trip custom serializer", () => { 173 | // This test serializes an object using a custom serializer. 174 | // The goal is to deserialize the object using a mapping to the custom deserializer. 175 | let myValue = { value: "Oh hi Mark!" }; 176 | let serializer: CustomSerializerRecord = { 177 | name: "mark-serializer", 178 | value: myValue, 179 | serializer: () => { 180 | return "My-JSON: " + JSON.stringify(myValue); 181 | }, 182 | }; 183 | let deserializer: CustomDeserializerRecord = { 184 | name: "mark-serializer", 185 | deserializer: (str: string) => { 186 | let stripped = str.substring("My-JSON: ".length); 187 | return JSON.parse(stripped); 188 | }, 189 | }; 190 | equal( 191 | JSON.stringify(roundtrip(myValue, [], [serializer], [deserializer])), 192 | JSON.stringify(myValue) 193 | ); 194 | }); 195 | }); 196 | -------------------------------------------------------------------------------- /transform-benchmarker/src/index.ts: -------------------------------------------------------------------------------- 1 | import compile, { CJS_CONFIG } from '../../ts-closure-transform/compile'; 2 | import request from 'sync-request'; 3 | import { resolve, dirname, join, sep, isAbsolute } from 'path'; 4 | import { statSync, readdirSync, copyFileSync, mkdirSync, writeFileSync, readFileSync, existsSync } from 'fs'; 5 | 6 | // Recursive directory creation logic. Based on Mahmoud Mouneer's answer to 7 | // https://stackoverflow.com/questions/31645738/how-to-create-full-path-with-nodes-fs-mkdirsync 8 | function mkDirByPathSync(targetDir, { isRelativeToScript = false } = {}) { 9 | const initDir = isAbsolute(targetDir) ? sep : ''; 10 | const baseDir = isRelativeToScript ? __dirname : '.'; 11 | 12 | return targetDir.split(sep).reduce((parentDir, childDir) => { 13 | const curDir = resolve(baseDir, parentDir, childDir); 14 | try { 15 | mkdirSync(curDir); 16 | } catch (err) { 17 | if (err.code === 'EEXIST') { // curDir already exists! 18 | return curDir; 19 | } 20 | 21 | // To avoid `EISDIR` error on Mac and `EACCES`-->`ENOENT` and `EPERM` on Windows. 22 | if (err.code === 'ENOENT') { // Throw the original parentDir error on curDir `ENOENT` failure. 23 | throw new Error(`EACCES: permission denied, mkdir '${parentDir}'`); 24 | } 25 | 26 | const caughtErr = ['EACCES', 'EPERM', 'EISDIR'].indexOf(err.code) > -1; 27 | if (!caughtErr || caughtErr && curDir === resolve(targetDir)) { 28 | throw err; // Throw if it's just the last created dir. 29 | } 30 | } 31 | 32 | return curDir; 33 | }, initDir); 34 | } 35 | 36 | // Polyfill so we can still target old JS versions. 37 | function endsWith(value: string, suffix: string) { 38 | return value.indexOf(suffix, value.length - suffix.length) !== -1; 39 | } 40 | 41 | // A flag that allows us to exclude the Mandreel benchmark from the 42 | // instrumentation pipeline. Doing so is useful for development because 43 | // Mandreel is a hefty benchmark that takes a long time to process. 44 | const includeMandreel = true; 45 | 46 | // A flag that makes the benchmarker compile/instrument the benchmark 47 | // suite even if it already exists. 48 | const alwaysCompile = false; 49 | 50 | /** 51 | * Creates an instrumented version of the Octane benchmark suite. 52 | * @param configuration The name of the instrumentation tool that is used. 53 | * @param instrumentFile A function that instruments a file and writes the result to an output file. 54 | * @returns A function that runs the instrumented version of the Octane benchmark suite. 55 | */ 56 | function instrumentOctane( 57 | configuration: string, 58 | instrumentFile: (inputPath: string, outputPath: string, relativePath?: string | undefined) => void): 59 | () => void { 60 | 61 | let sourceRootDir = dirname(require.resolve('benchmark-octane/lib/octane')); 62 | let destRootDir = resolve(__dirname, `benchmark-octane/${configuration}`); 63 | 64 | function walk(relativePath: string, ignoreSource: boolean = false) { 65 | let sourcePath = resolve(sourceRootDir, relativePath); 66 | let destPath = resolve(destRootDir, relativePath); 67 | mkDirByPathSync(destPath); 68 | 69 | for (let name of readdirSync(sourcePath)) { 70 | let f = statSync(resolve(sourcePath, name)); 71 | if (f.isDirectory()) { 72 | // Recursively visit directories. Do not instrument the files 73 | // in the JS directory because they appear to trigger a bug in 74 | // the TypeScript compiler's handling of JSDoc tags. These files 75 | // aren't part of the benchmark code, so excluding them is harmless. 76 | walk(join(relativePath, name), ignoreSource || name == 'js'); 77 | } else if (!ignoreSource 78 | && endsWith(name, '.js') 79 | && (includeMandreel || name != 'mandreel.js')) { 80 | // Instrument JavaScript files, but only if they haven't been 81 | // instrumented already. Re-instrumenting files takes time that 82 | // we'd rather not waste. 83 | let destFilePath = resolve(destPath, name); 84 | if (alwaysCompile || !existsSync(destFilePath)) { 85 | instrumentFile(resolve(sourcePath, name), destFilePath, join(relativePath, name)); 86 | } 87 | } else { 88 | // Copy all other files. 89 | copyFileSync( 90 | resolve(sourcePath, name), 91 | resolve(destPath, name)); 92 | } 93 | } 94 | } 95 | 96 | walk('.'); 97 | 98 | let suite = require(resolve(destRootDir, 'octane.js')).BenchmarkSuite; 99 | function run() { 100 | let results = []; 101 | suite.RunSuites({ 102 | NotifyResult: (name, result) => { 103 | console.log((name + ' ').substr(0, 20) + ': ' + result); 104 | results.push([name, result]); 105 | }, 106 | NotifyError: (name, error) => { 107 | console.log((name + ' ').substr(0, 20) + ': ' + error); 108 | }, 109 | NotifyScore: (score) => { 110 | console.log('Score (version ' + suite.version + '): ' + score); 111 | } 112 | }); 113 | 114 | // Create a results directory. 115 | let resultsDir = resolve(process.cwd(), 'results'); 116 | mkDirByPathSync(resultsDir); 117 | 118 | // Write the scores to a CSV. 119 | let scoreHeader = 'benchmark,score\n'; 120 | let scoreCsv = scoreHeader + results.map(pair => pair[0] + ',' + pair[1]).join('\n'); 121 | let scoreFileName = resolve(resultsDir, `${configuration}-scores.csv`); 122 | writeFileSync(scoreFileName, scoreCsv, { encoding: 'utf8' }); 123 | 124 | // Write the files' sizes to a CSV. 125 | let sizes = []; 126 | for (let fileName of readdirSync(resolve(destRootDir, 'octane'))) { 127 | let absPath = resolve(destRootDir, 'octane', fileName); 128 | let f = statSync(absPath); 129 | if (!f.isDirectory() 130 | && endsWith(fileName, '.js') 131 | && !endsWith(fileName, 'base.js') 132 | && !endsWith(fileName, 'run.js')) { 133 | 134 | sizes.push([ 135 | fileName, 136 | Buffer.byteLength(readFileSync(absPath)) 137 | ]); 138 | } 139 | } 140 | let sizeHeader = 'benchmark,size\n'; 141 | let sizeCsv = sizeHeader + sizes.map(pair => pair[0] + ',' + pair[1]).join('\n'); 142 | let sizeFileName = resolve(resultsDir, `${configuration}-sizes.csv`); 143 | writeFileSync(sizeFileName, sizeCsv, { encoding: 'utf8' }); 144 | } 145 | 146 | return run; 147 | } 148 | 149 | /** 150 | * Patches an instrumented 'octane.js' file to export the BenchmarkSuite object. 151 | * @param code The code to patch. 152 | */ 153 | function exportBenchmarkSuite(code: string) { 154 | return code.replace( 155 | 'module.exports = {', 156 | 'module.exports = { BenchmarkSuite: BenchmarkSuite,'); 157 | } 158 | 159 | function instrumentWithTsc(inputFile: string, outputFile: string, transformClosures: boolean) { 160 | function writeFileCallback( 161 | fileName: string, 162 | data: string, 163 | writeByteOrderMark: boolean, 164 | onError: (message: string) => void | undefined, 165 | sourceFiles): void { 166 | 167 | if (transformClosures && endsWith(fileName, 'gbemu-part1.js')) { 168 | // The Gameboy benchmark is problematic for FlashFreeze because the Gameboy 169 | // benchmark shares mutable variables across files. Our transform assumes 170 | // that each file is a separate module, but the Gameboy benchmark breaks 171 | // that assumption. 172 | // 173 | // There's not much that can be done about this at a fundamental level: 174 | // FlashFreeze's approach really hinges on the module abstraction that the 175 | // Gameboy benchmark doesn't respect. 176 | // 177 | // As a workaround for this specific case, we can replace all references 178 | // to the 'gameboy' object with 'gameboy.value'. 179 | data = data 180 | .replace(/gameboy\./g, 'gameboy.value.') 181 | .replace('gameboy = null;', 'gameboy.value = null;'); 182 | } 183 | else if (endsWith(fileName, 'octane.js')) { 184 | data = exportBenchmarkSuite(data); 185 | } 186 | writeFileSync(outputFile, data, { encoding: 'utf8' }); 187 | } 188 | 189 | // Copy the file to a temporary path with a 'ts' suffix. 190 | let tsCopyPath = outputFile.substring(0, outputFile.length - '.js'.length) + '.ts'; 191 | copyFileSync(inputFile, tsCopyPath); 192 | // Then feed it to the TypeScript compiler. 193 | compile( 194 | tsCopyPath, 195 | { ...CJS_CONFIG, removeComments: true }, 196 | writeFileCallback, 197 | false, 198 | transformClosures); 199 | } 200 | 201 | function instrumentWithThingsJS(inputFile: string, outputFile: string) { 202 | if (endsWith(inputFile, 'base.js')) { 203 | // Just copy 'base.js'. Instrumenting it will produce a stack overflow 204 | // because an instrumented base.js will create ThingsJS stack frames in 205 | // its reimplementation of Math.random. ThingsJS stack frame creation 206 | // depends on Math.random, hence the stack overflow. 207 | // 208 | // This copy shouldn't affect our measurements: 'base.js' is not an 209 | // actual benchmark. It only contains some benchmark setup infrastructure. 210 | copyFileSync(inputFile, outputFile); 211 | return; 212 | } 213 | 214 | let code: string = require("child_process").spawnSync("things-js", ["inst", inputFile], { encoding: 'utf8' }).stdout; 215 | // ThingsJS generates code that calls 'require' at the top of the file but then 216 | // demands that 'require' is called as a property of the big sigma global. 217 | // This breaks the Octane benchmark, which depends on file inclusion. We will 218 | // work around this problem by patching the test runner and the tests. 219 | let remove = "require('things-js/lib/core/Code').bootstrap(module, function (Σ) {"; 220 | if (endsWith(outputFile, 'octane.js')) { 221 | let add = "require('things-js/lib/core/Code').bootstrap(module, function (Σ) { global.Σ = Σ; "; 222 | code = add + code.substr(remove.length); 223 | code = exportBenchmarkSuite(code); 224 | } else { 225 | let epilogueIndex = code.lastIndexOf("'mqtt://localhost'"); 226 | let epilogueStartIndex = code.lastIndexOf('\n', epilogueIndex); 227 | code = code.substr(0, epilogueStartIndex).substr(remove.length); 228 | } 229 | writeFileSync(outputFile, code, { encoding: 'utf8' }); 230 | } 231 | 232 | function downloadDisclosureInstrumented(relativePath: string, outputFile: string) { 233 | let prefix = 'https://raw.githubusercontent.com/jaeykim/Disclosure/master/lib/'; 234 | relativePath = relativePath.replace('octane/', 'octane/inst/'); 235 | let url = prefix + relativePath; 236 | 237 | console.log(`Downloading ${relativePath} from ${url}...`); 238 | if (relativePath == 'octane.js') { 239 | // Download octane.js and patch it. 240 | let body = request('GET', url).getBody('utf8'); 241 | body = exportBenchmarkSuite(body); 242 | body = body.replace('/octane/inst/', '/octane/'); 243 | writeFileSync(outputFile, body, { encoding: 'utf8' }); 244 | 245 | // Also download disclosure.js. 246 | downloadDisclosureInstrumented('disclosure.js', outputFile.replace('octane.js', 'disclosure.js')); 247 | } 248 | else { 249 | let response = request('GET', url); 250 | if (response.statusCode != 404) { 251 | writeFileSync(outputFile, response.getBody()); 252 | } 253 | } 254 | } 255 | 256 | if (process.argv.length <= 2 || process.argv[2] == 'original') { 257 | instrumentOctane('original', (from, to) => { 258 | instrumentWithTsc(from, to, false); 259 | })(); 260 | } else if (process.argv[2] == 'flash-freeze') { 261 | instrumentOctane('flash-freeze', (from, to) => { 262 | instrumentWithTsc(from, to, true); 263 | })(); 264 | } else if (process.argv[2] == 'things-js') { 265 | instrumentOctane('things-js', instrumentWithThingsJS)(); 266 | process.exit(0); 267 | } else if (process.argv[2] == 'disclosure') { 268 | instrumentOctane('disclosure', (from, to, rel) => downloadDisclosureInstrumented(rel, to))(); 269 | } else { 270 | console.log(`Unknown configuration '${process.argv[2]}'`); 271 | process.exit(1); 272 | } 273 | -------------------------------------------------------------------------------- /ts-closure-transform/src/transform.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | // Inspired by transform.ts from Kris Zyp's ts-transform-safely 4 | // (https://github.com/DoctorEvidence/ts-transform-safely) 5 | 6 | // MIT License 7 | 8 | // Copyright (c) 2017 Kris Zyp 9 | 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | /** 29 | * Tells if a variable declaration list's declared variables are (implicitly) hoisted. 30 | * 31 | * `let` and `const` declarations are not hoisted: variables declared by 32 | * these keywords cannot be accessed until they are declared. Consequently, 33 | * earlier uses of `let`/`const` variables must refer to some other variable 34 | * declared in an enclosing scope. 35 | * 36 | * `var` declarations, on the other hand, are hoisted. This means that 37 | * that earlier uses in this scope of names declared by a `var` declaration 38 | * actually refer to said declaration. 39 | * 40 | * @param node A variable declaration list to inspect. 41 | */ 42 | function isHoistedDeclaration(node: ts.VariableDeclarationList) { 43 | let isNotHoisted = (node.flags & ts.NodeFlags.Let) == ts.NodeFlags.Let 44 | || (node.flags & ts.NodeFlags.Const) == ts.NodeFlags.Const; 45 | 46 | return !isNotHoisted; 47 | } 48 | 49 | /** 50 | * A lexical scope data structure that keeps track of captured variables. 51 | */ 52 | class CapturedVariableScope { 53 | /** 54 | * A list of all used variable identifiers. 55 | */ 56 | private used: ts.Identifier[]; 57 | 58 | /** 59 | * A list of all used variable names. 60 | */ 61 | private usedNames: string[]; 62 | 63 | /** 64 | * A list of all declared variables in the current scope. 65 | */ 66 | private declared: string[]; 67 | 68 | /** 69 | * Creates a captured variable scope. 70 | * @param parent The parent node in the captured variable chain. 71 | */ 72 | constructor(public parent?: CapturedVariableScope) { 73 | this.used = []; 74 | this.usedNames = []; 75 | this.declared = []; 76 | } 77 | 78 | /** 79 | * Tells if a variable with a particular name is 80 | * captured by this scope. 81 | * @param name The name of the variable to check. 82 | */ 83 | isCaptured(name: ts.Identifier): boolean { 84 | return this.usedNames.indexOf(name.text) >= 0; 85 | } 86 | 87 | /** 88 | * Tells if a variable with a particular name is 89 | * declared by this scope. 90 | * @param name The name of the variable to check. 91 | */ 92 | isDeclared(name: ts.Identifier): boolean { 93 | return this.declared.indexOf(name.text) >= 0; 94 | } 95 | 96 | /** 97 | * Hints that the variable with the given name is 98 | * used by this scope. 99 | * @param name The name to capture. 100 | */ 101 | use(name: ts.Identifier): void { 102 | if (this.isCaptured(name) || this.isDeclared(name)) { 103 | return; 104 | } 105 | 106 | this.used.push(name); 107 | this.usedNames.push(name.text); 108 | if (this.parent) { 109 | this.parent.use(name); 110 | } 111 | } 112 | 113 | /** 114 | * Hints that the variable with the given name is 115 | * declared by this scope in the chain. 116 | * @param name The name to declare. 117 | * @param isHoisted Tells if the variable is hoisted to the top of this scope. 118 | */ 119 | declare(name: ts.Identifier, isHoisted: boolean): void { 120 | if (this.isDeclared(name)) { 121 | return; 122 | } 123 | 124 | this.declared.push(name.text); 125 | if (isHoisted) { 126 | // If the declaration is hoisted, then the uses we encountered previously 127 | // did not actually capture any external variables. We should delete them. 128 | let index = this.usedNames.indexOf(name.text); 129 | if (index >= 0) { 130 | this.usedNames.splice(index, 1); 131 | this.used.splice(index, 1); 132 | } 133 | } 134 | } 135 | 136 | /** 137 | * Gets a read-only array containing all captured variables 138 | * in this scope. 139 | */ 140 | get captured(): ReadonlyArray { 141 | return this.used; 142 | } 143 | } 144 | 145 | /** 146 | * Creates a lambda that can be evaluated to a key-value 147 | * mapping for captured variables. 148 | * @param capturedVariables The list of captured variables. 149 | */ 150 | function createClosureLambda(capturedVariables: ReadonlyArray) { 151 | // Synthesize a lambda that has the following format: 152 | // 153 | // () => { a, b, ... } 154 | // 155 | // where a, b, ... is the list of captured variables. 156 | // 157 | // First step: create the object literal returned by the lambda. 158 | let objLiteralElements: ts.ObjectLiteralElementLike[] = []; 159 | 160 | for (let variable of capturedVariables) { 161 | objLiteralElements.push( 162 | ts.factory.createShorthandPropertyAssignment(variable)); 163 | } 164 | 165 | // Create the lambda itself. 166 | return ts.factory.createArrowFunction( 167 | [], 168 | [], 169 | [], 170 | undefined, 171 | undefined, 172 | ts.factory.createObjectLiteralExpression(objLiteralElements)); 173 | } 174 | 175 | /** 176 | * Creates an expression that produces a closure lambda 177 | * and assigns it to the closure property. 178 | * @param closureFunction The function whose closure property 179 | * is to be set. 180 | * @param capturedVariables The list of captured variables to 181 | * include in the closure lambda. 182 | */ 183 | function createClosurePropertyAssignment( 184 | closureFunction: ts.Expression, 185 | capturedVariables: ReadonlyArray): ts.BinaryExpression { 186 | 187 | return ts.factory.createAssignment( 188 | ts.factory.createPropertyAccessExpression(closureFunction, "__closure"), 189 | createClosureLambda(capturedVariables)); 190 | } 191 | 192 | /** 193 | * Adds a closure property to a lambda. Returns 194 | * an expression that produces the exact same lambda 195 | * but with the closure property added. 196 | * @param ctx The transformation context to use. 197 | * @param lambda The lambda to transform. 198 | * @param capturedVariables The list of captured variables 199 | * to put in the closure property. 200 | */ 201 | function addClosurePropertyToLambda( 202 | ctx: ts.TransformationContext, 203 | lambda: ts.Expression, 204 | capturedVariables: ReadonlyArray) { 205 | 206 | // Tiny optimization: lambdas that don't 207 | // capture anything don't get a closure property. 208 | if (capturedVariables.length === 0) { 209 | return lambda; 210 | } 211 | 212 | // If we do have captured variables, then we'll 213 | // construct a closure property. 214 | let temp = ts.factory.createUniqueName("_tct_transform"); 215 | ctx.hoistVariableDeclaration(temp); 216 | 217 | // Use the comma operator to create an expression that looks 218 | // like this: 219 | // 220 | // (temp = , temp.__closure = () => { a, b, ... }, temp) 221 | return ts.factory.createCommaListExpression([ 222 | ts.factory.createAssignment(temp, lambda), 223 | createClosurePropertyAssignment(temp, capturedVariables), 224 | temp 225 | ]); 226 | } 227 | 228 | /** 229 | * Creates a node visitor from a transformation context. 230 | * @param ctx The transformation context to use. 231 | */ 232 | function visitor(ctx: ts.TransformationContext) { 233 | 234 | /** 235 | * Transforms an arrow function or function expression 236 | * to include a closure property. 237 | * @param node The node to transform. 238 | * @param parentChain The captured variable chain of the parent function. 239 | */ 240 | function transformLambda( 241 | node: ts.ArrowFunction | ts.FunctionExpression, 242 | parentChain: CapturedVariableScope): ts.VisitResult { 243 | 244 | let chain = new CapturedVariableScope(parentChain); 245 | 246 | // Declare the function expression's name. 247 | if (node.name) { 248 | parentChain.declare(node.name, false); 249 | chain.declare(node.name, false); 250 | } 251 | 252 | // Declare the function declaration's parameters. 253 | for (let param of node.parameters) { 254 | visitDeclaration(param.name, chain, false); 255 | } 256 | 257 | // Visit the lambda and extract captured symbols. 258 | let { visited, captured } = visitAndExtractCapturedSymbols( 259 | node, 260 | chain); 261 | 262 | return addClosurePropertyToLambda(ctx, visited, captured); 263 | } 264 | 265 | /** 266 | * Transforms a function declaration to include a closure property. 267 | * @param node The node to transform. 268 | * @param parentChain The captured variable chain of the parent function. 269 | */ 270 | function transformFunctionDeclaration( 271 | node: ts.FunctionDeclaration, 272 | parentChain: CapturedVariableScope): ts.VisitResult { 273 | 274 | let chain = new CapturedVariableScope(parentChain); 275 | 276 | // Declare the function declaration's name. 277 | if (node.name) { 278 | parentChain.declare(node.name, true); 279 | chain.declare(node.name, true); 280 | } 281 | 282 | // Declare the function declaration's parameters. 283 | for (let param of node.parameters) { 284 | visitDeclaration(param.name, chain, false); 285 | } 286 | 287 | // Visit the function and extract captured symbols. 288 | let { visited, captured } = visitAndExtractCapturedSymbols( 289 | node.body, 290 | chain, 291 | node); 292 | 293 | let visitedFunc = ts.factory.updateFunctionDeclaration( 294 | node, 295 | node.modifiers, 296 | node.asteriskToken, 297 | node.name, 298 | node.typeParameters, 299 | node.parameters, 300 | node.type, 301 | visited); 302 | 303 | if (captured.length === 0) { 304 | return visitedFunc; 305 | } else { 306 | return [ 307 | visitedFunc, 308 | ts.factory.createExpressionStatement( 309 | createClosurePropertyAssignment( 310 | node.name, 311 | captured)) 312 | ]; 313 | } 314 | } 315 | 316 | /** 317 | * Visits a node and extracts all used identifiers. 318 | * @param node The node to visit. 319 | * @param chain The captured variable chain of the node. 320 | * @param scopeNode A node that defines the scope from 321 | * which eligible symbols are extracted. 322 | */ 323 | function visitAndExtractCapturedSymbols( 324 | node: T, 325 | chain: CapturedVariableScope, 326 | scopeNode?: ts.Node): { visited: T, captured: ReadonlyArray } { 327 | 328 | scopeNode = scopeNode || node; 329 | 330 | // Visit the body of the arrow function. 331 | let visited = ts.visitEachChild( 332 | node, 333 | visitor(chain), 334 | ctx); 335 | 336 | // Figure out which symbols are captured and return. 337 | return { visited, captured: chain.captured } 338 | } 339 | 340 | function visitDeclaration(declaration: ts.Node, captured: CapturedVariableScope, isHoisted: boolean) { 341 | function visit(node: ts.Node): ts.VisitResult { 342 | if (ts.isIdentifier(node)) { 343 | captured.declare(node, isHoisted); 344 | return node; 345 | } else { 346 | return ts.visitEachChild(node, visit, ctx); 347 | } 348 | } 349 | 350 | return visit(declaration); 351 | } 352 | 353 | /** 354 | * Creates a visitor. 355 | * @param captured The captured variable chain to update. 356 | */ 357 | function visitor(captured: CapturedVariableScope): ts.Visitor { 358 | function recurse(node: T): T { 359 | return visitor(captured)(node); 360 | } 361 | 362 | return node => { 363 | if (ts.isIdentifier(node)) { 364 | if (node.text !== "undefined" 365 | && node.text !== "null" 366 | && node.text !== "arguments") { 367 | 368 | captured.use(node); 369 | } 370 | return node; 371 | } else if (ts.isTypeNode(node)) { 372 | // Don't visit type nodes. 373 | return node; 374 | } else if (ts.isPropertyAccessExpression(node)) { 375 | // Make sure we don't accidentally fool ourselves 376 | // into visiting property name identifiers. 377 | return ts.factory.updatePropertyAccessExpression( 378 | node, 379 | recurse(node.expression), 380 | node.name); 381 | } else if (ts.isQualifiedName(node)) { 382 | // Make sure we don't accidentally fool ourselves 383 | // into visiting the right-hand side of a qualified name. 384 | return ts.factory.updateQualifiedName( 385 | node, 386 | recurse(node.left), 387 | node.right); 388 | } else if (ts.isPropertyAssignment(node)) { 389 | // Make sure we don't accidentally fool ourselves 390 | // into visiting property name identifiers. 391 | return ts.factory.updatePropertyAssignment( 392 | node, 393 | node.name, 394 | recurse(node.initializer)); 395 | } else if (ts.isVariableDeclarationList(node)) { 396 | // Before we visit the individual variable declarations, we want to take 397 | // a moment to tell whether those variable declarations are implicitly 398 | // hoisted or not. 399 | let isHoisted = isHoistedDeclaration(node); 400 | 401 | // Now visit the individual declarations... 402 | let newDeclarations = []; 403 | for (let declaration of node.declarations) { 404 | // ...making sure that we take their hoisted-ness into account. 405 | visitDeclaration(declaration.name, captured, isHoisted); 406 | newDeclarations.push(ts.visitEachChild(declaration, visitor(captured), ctx)); 407 | } 408 | // Finally, update the declaration list. 409 | return ts.factory.updateVariableDeclarationList(node, newDeclarations); 410 | } else if (ts.isVariableDeclaration(node)) { 411 | visitDeclaration(node.name, captured, false); 412 | return ts.visitEachChild(node, visitor(captured), ctx); 413 | } else if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) { 414 | return transformLambda(node, captured); 415 | } else if (ts.isFunctionDeclaration(node)) { 416 | return transformFunctionDeclaration(node, captured); 417 | } else { 418 | return ts.visitEachChild(node, visitor(captured), ctx); 419 | } 420 | }; 421 | } 422 | 423 | return visitor(new CapturedVariableScope()); 424 | } 425 | 426 | export default function() { 427 | return (ctx: ts.TransformationContext): ts.Transformer => { 428 | return (sf: ts.SourceFile) => ts.visitNode(sf, visitor(ctx)) as ts.SourceFile; 429 | } 430 | } 431 | -------------------------------------------------------------------------------- /serialize-closures/src/serializedGraph.ts: -------------------------------------------------------------------------------- 1 | import { types } from "util"; 2 | import { getNameOfBuiltin, getBuiltinByName, BuiltinList, defaultBuiltins, generateDefaultBuiltins } from "./builtins"; 3 | import { retrieveCustomSerializer, CustomSerializerList, retrieveCustomDeserializer, CustomDeserializerList } from "./customs"; 4 | 5 | export interface ValueFlags { 6 | accessor?: "get" | "set" 7 | }; 8 | 9 | /** 10 | * Represents a graph of serialized values. 11 | */ 12 | export class SerializedGraph { 13 | private indexMap: { element: any, index: number }[]; 14 | private rootIndex: number; 15 | private contentArray: any[]; 16 | private builtins: BuiltinList; 17 | private customSerializers: CustomSerializerList; 18 | private customDeserializers: CustomDeserializerList; 19 | private evalImpl: undefined | ((code: string) => any); 20 | 21 | /** 22 | * Creates a new graph of serialized values. 23 | */ 24 | private constructor() { 25 | this.indexMap = []; 26 | this.rootIndex = -1; 27 | this.contentArray = []; 28 | this.builtins = defaultBuiltins; 29 | this.customSerializers = []; 30 | this.customDeserializers = []; 31 | this.evalImpl = undefined; 32 | } 33 | 34 | /** 35 | * Serializes a value, producing a serialized graph. 36 | * @param value The value to serialize. 37 | * @param builtins An optional list of builtins to use. 38 | * If not specified, the default builtins are used. 39 | * @param customSerializers An optional list of custom serialize functions to use for specified values. 40 | */ 41 | static serialize( 42 | value: any, 43 | builtins?: BuiltinList, 44 | customSerializers?: CustomSerializerList): SerializedGraph { 45 | 46 | let graph = new SerializedGraph(); 47 | graph.builtins = builtins; 48 | graph.customSerializers = customSerializers; 49 | graph.rootIndex = graph.add(value); 50 | return graph; 51 | } 52 | 53 | /** 54 | * Converts JSON to a serialized graph. 55 | * @param json The JSON to interpret as a serialized graph. 56 | * @param builtins An optional list of builtins to use. 57 | * If not specified, the default builtins are used. 58 | * @param customDeserializers An optional list of builtins to use. 59 | * If not specified, the default builtins are used. 60 | * @param evalImpl An `eval` implementation to use for 61 | * evaluating functions or regular expressions. 62 | */ 63 | static fromJSON( 64 | json: any, 65 | builtins?: BuiltinList, 66 | customDeserializers?: CustomDeserializerList, 67 | evalImpl?: (code: string) => any): SerializedGraph { 68 | 69 | let graph = new SerializedGraph(); 70 | graph.rootIndex = json.root; 71 | graph.contentArray = json.data; 72 | if (builtins) { 73 | graph.builtins = builtins; 74 | } else if (evalImpl) { 75 | graph.builtins = generateDefaultBuiltins(undefined, evalImpl); 76 | } 77 | graph.evalImpl = evalImpl; 78 | if (customDeserializers) { 79 | graph.customDeserializers = customDeserializers; 80 | } else { 81 | graph.customDeserializers = []; 82 | } 83 | return graph; 84 | } 85 | 86 | /** 87 | * Creates a JSON representation of this serialized graph. 88 | */ 89 | toJSON() { 90 | return { 91 | 'root': this.rootIndex, 92 | 'data': this.contentArray 93 | }; 94 | } 95 | 96 | /** 97 | * Adds a value to the graph and serializes it 98 | * if necessary. Returns the index of the value 99 | * in the content array. 100 | * @param value The value to add. 101 | */ 102 | private add(value: any, flags?: ValueFlags): number { 103 | // If the value is already in the graph, then we don't 104 | // need to serialize it. 105 | for (let { element, index } of this.indexMap) { 106 | if (element === value) { 107 | return index; 108 | } 109 | } 110 | 111 | let index = this.contentArray.length; 112 | this.contentArray.push(undefined); 113 | this.indexMap.push({ element: value, index }); 114 | this.contentArray[index] = this.serialize(value, flags); 115 | return index; 116 | } 117 | 118 | /** 119 | * Serializes an accessor function. 120 | * @param value The function to serialize. 121 | */ 122 | private serializeAccessorFunction(value: Function, kind: "get" | "set"): any { 123 | let result = this.serializeFunction(value) 124 | // Applying value.toString() on an accessor function can generate an invalid expression in two cases: 125 | // 1) named accessors: eliminate the `get` or `set` prefix. 126 | // 2) function application `()` in function name 127 | if (value.name.startsWith(kind + ' ')) { 128 | let source = "function " + value.toString().substring(4) 129 | result = Object.assign({}, result, { 130 | source 131 | }) 132 | } 133 | if (result.source.startsWith(kind + '(')) { 134 | let source = "function " + value.toString().substring(3) 135 | result = Object.assign({}, result, { 136 | source 137 | }) 138 | } 139 | return result; 140 | } 141 | 142 | /** 143 | * Serializes a function. 144 | * @param value The function to serialize. 145 | */ 146 | private serializeFunction(value: Function): any { 147 | let closure = (value).__closure; 148 | if ((value).__impl) { 149 | // To serialize functions that have been deserialized earlier, 150 | // we must reuse the __impl source. This is OK because functions are immutable 151 | value = (value).__impl; 152 | closure = (value).__closure; 153 | } 154 | let source = value.toString() 155 | if (source.endsWith("{ [native code] }")) { 156 | throw new Error(`Cannot serialize native code. Value missing from builtin list? '${value}'`) 157 | } 158 | if (source.startsWith("class ")) { 159 | throw new Error(`Cannot serialize classes. Value missing from builtin list? '${value}'`) 160 | } 161 | let result = { 162 | 'kind': 'function', 163 | 'source': source, 164 | 'closure': this.add(closure ? closure() : undefined), 165 | 'prototype': this.add(value.prototype) 166 | }; 167 | 168 | this.serializeProperties(value, result); 169 | 170 | return result; 171 | } 172 | 173 | /** 174 | * Serializes an object. 175 | * @param value The object to serialize. 176 | */ 177 | private serializeObject(value: any): any { 178 | let result = { 179 | 'kind': 'object', 180 | 'prototype': this.add(Object.getPrototypeOf(value)) 181 | }; 182 | 183 | this.serializeProperties(value, result); 184 | 185 | return result; 186 | } 187 | 188 | /** 189 | * Serializes a value's properties. 190 | * @param value The value whose properties to serialize. 191 | * @param serializedValue A serialized version of the value. 192 | * Its 'refs' and 'descriptions' properties will be updated by this 193 | * method. 194 | */ 195 | private serializeProperties(value: any, serializedValue: any): void { 196 | let refs = {}; 197 | let descriptions = {}; 198 | for (let key of Object.getOwnPropertyNames(value)) { 199 | if (key.length > 2 && key.substr(0, 2) === '__') { 200 | // Ignore keys that start with two underscores. There's 201 | // a reason those underscores are there. 202 | continue; 203 | } 204 | 205 | let desc = Object.getOwnPropertyDescriptor(value, key); 206 | if ('value' in desc && desc.configurable && desc.writable && desc.enumerable) { 207 | // Typical property. Just encode its value and be done with it. 208 | refs[key] = this.add(value[key]); 209 | } else { 210 | // Fancy property. We'll emit a description for it. 211 | let serializedDesc: any = {}; 212 | if (desc.get) { 213 | serializedDesc.get = this.add(desc.get, { accessor: 'get' }); 214 | } 215 | if (desc.set) { 216 | serializedDesc.set = this.add(desc.set, { accessor: 'set' }); 217 | } 218 | if ('value' in desc) { 219 | serializedDesc.value = this.add(desc.value); 220 | } 221 | serializedDesc.configurable = desc.configurable; 222 | if (serializedDesc.writable !== undefined) { 223 | serializedDesc.writable = desc.writable; 224 | } 225 | serializedDesc.enumerable = desc.enumerable; 226 | descriptions[key] = serializedDesc; 227 | } 228 | } 229 | 230 | serializedValue.refs = refs; 231 | serializedValue.descriptions = descriptions; 232 | } 233 | 234 | /** 235 | * Serializes a value. This value may be a closure or an object 236 | * that contains a closure. 237 | * @param value The value to serialize. 238 | */ 239 | private serialize(value: any, flags?: ValueFlags): any { 240 | // Check if the value requires a custom serializer 241 | let customSerializer = retrieveCustomSerializer(value, this.customSerializers) 242 | if (customSerializer !== undefined) { 243 | return { 244 | 'kind': 'custom', 245 | 'name': customSerializer.name, 246 | 'value': customSerializer.serializer() 247 | }; 248 | } 249 | // Check if the value is a builtin before proceeding. 250 | let builtinName = getNameOfBuiltin(value, this.builtins); 251 | if (builtinName !== undefined) { 252 | return { 253 | 'kind': 'builtin', 254 | 'name': builtinName 255 | }; 256 | } 257 | 258 | // Usual serialization logic. 259 | if ((typeof value !== 'object' && typeof value !== 'function') || value === null) { 260 | return { 261 | 'kind': 'primitive', 262 | 'value': value 263 | }; 264 | } else if (Array.isArray(value)) { 265 | return { 266 | 'kind': 'array', 267 | 'refs': value.map(v => this.add(v)) 268 | }; 269 | } else if (typeof value === 'function') { 270 | if (flags && flags.accessor) { 271 | return this.serializeAccessorFunction(value, flags.accessor); 272 | } else { 273 | return this.serializeFunction(value); 274 | } 275 | } else if ( 276 | (types && types.isDate(value))) { // Deprecated 277 | return { 278 | 'kind': 'date', 279 | 'value': JSON.stringify(value) 280 | }; 281 | } else if ( 282 | (types && types.isRegExp(value))) { // Deprecated 283 | return { 284 | 'kind': 'regex', 285 | 'value': value.toString() 286 | }; 287 | } else { 288 | return this.serializeObject(value); 289 | } 290 | } 291 | 292 | /** 293 | * Gets a deserialized version of the value 294 | * stored at a particular index. 295 | * @param valueIndex The index of the value to deserialize. 296 | */ 297 | private get(valueIndex: number): any { 298 | // If the value is already in the index map, then we don't 299 | // need to deserialize it. 300 | for (let { element, index } of this.indexMap) { 301 | if (valueIndex === index) { 302 | return element; 303 | } 304 | } 305 | 306 | let value = this.contentArray[valueIndex]; 307 | 308 | if (value.kind === 'primitive') { 309 | this.indexMap.push({ element: value.value, index: valueIndex }); 310 | return value.value; 311 | } else if (value.kind === 'array') { 312 | let results = []; 313 | // Push the (unfinished) array into the index map here because 314 | // there may be cycles in the graph of serialized objects. 315 | this.indexMap.push({ element: results, index: valueIndex }); 316 | for (let ref of value.refs) { 317 | results.push(this.get(ref)); 318 | } 319 | return results; 320 | } else if (value.kind === 'object') { 321 | // Push the (unfinished) object into the index map here because 322 | // there may be cycles in the graph of serialized objects. 323 | let results = Object.create(this.get(value.prototype)); 324 | this.indexMap.push({ element: results, index: valueIndex }); 325 | this.deserializeProperties(value, results); 326 | return results; 327 | } else if (value.kind === 'proxy') { 328 | // A proxy is serialized outside of the current scope 329 | // so deserialize and proxy the result through a function when accessed 330 | let results = value.value; 331 | if (value.value && 332 | typeof value.value === 'object' && 333 | value.value.constructor === Object && 334 | value.value.root === 0 && 335 | Array.isArray(value.value.data)) { 336 | let evalImpl = this.evalImpl; 337 | let fct = SerializedGraph.fromJSON(value.value, this.builtins, [], evalImpl).root 338 | results = new Proxy({}, { 339 | get: function (tgt, name, rcvr) { 340 | let res = fct(); 341 | return () => res; 342 | } 343 | }); 344 | } 345 | this.indexMap.push({ element: results, index: valueIndex }); 346 | return results; 347 | } else if (value.kind === 'function') { 348 | // Decoding functions is tricky because the closure of 349 | // a function may refer to that function. At the same 350 | // time, function implementations are immutable. 351 | // To get around that, we'll use a dirty little hack: create 352 | // a thunk that calls a property of itself. 353 | 354 | // let thunk = function () { 355 | // return (thunk).__impl.apply(this, arguments); 356 | // } 357 | // this.indexMap.push({ element: thunk, index: valueIndex }); 358 | 359 | // TODO: Temporary fix for bug in createClosureLambda (Part 1) 360 | // Synthesized variables to do map to literal properties 361 | var thunkObj = { __thunk: undefined }; 362 | thunkObj.__thunk = function () { 363 | return (thunkObj.__thunk).__impl.apply(this, arguments); 364 | } 365 | this.indexMap.push({ element: thunkObj.__thunk, index: valueIndex }); 366 | 367 | // Synthesize a snippet of code we can evaluate. 368 | // TODO: Temporary fix for bug in createClosureLambda (Part 2) 369 | // Synthesized variables to do map to literal properties 370 | // Should be 'let deserializedClosure' 371 | var deserializedClosure = this.get(value.closure) || {}; 372 | let capturedVarKeys = []; 373 | let capturedVarVals = []; 374 | for (let key in deserializedClosure) { 375 | capturedVarKeys.push(key); 376 | capturedVarVals.push(deserializedClosure[key]); 377 | } 378 | let code = `(function(${capturedVarKeys.join(", ")}) { return (${value.source}); })`; 379 | 380 | // Evaluate the code. 381 | let impl = this.evalInThisContext(code).apply(undefined, capturedVarVals); 382 | impl.prototype = this.get(value.prototype); 383 | impl.__closure = () => deserializedClosure; 384 | 385 | // Patch the thunk. 386 | // (thunk).__impl = impl; 387 | // (thunk).prototype = impl.prototype; 388 | // this.deserializeProperties(value, thunk); 389 | 390 | // return thunk; 391 | 392 | // TODO: Temporary fix for bug in createClosureLambda (Part 3) 393 | // Synthesized variables to do map to literal properties 394 | (thunkObj.__thunk).__impl = impl; 395 | (thunkObj.__thunk).prototype = impl.prototype; 396 | this.deserializeProperties(value, thunkObj.__thunk); 397 | 398 | return thunkObj.__thunk; 399 | } else if (value.kind === 'builtin') { 400 | let builtin = getBuiltinByName(value.name, this.builtins); 401 | if (builtin === undefined) { 402 | throw new Error(`Cannot deserialize unknown builtin '${value.name}'.`); 403 | } else { 404 | this.indexMap.push({ element: builtin, index: valueIndex }); 405 | return builtin; 406 | } 407 | } else if (value.kind === 'custom') { 408 | let customDeserializer = retrieveCustomDeserializer(value.name, this.customDeserializers); 409 | if (customDeserializer === undefined) { 410 | throw new Error(`Cannot deserialize unknown custom '${value.name}'.`); 411 | } 412 | let result = customDeserializer(value.value) 413 | this.indexMap.push({ element: result, index: valueIndex }); 414 | return result; 415 | } else if (value.kind === 'date') { 416 | let result = new Date(JSON.parse(value.value)); 417 | this.indexMap.push({ element: result, index: valueIndex }); 418 | return result; 419 | } else if (value.kind === 'regex') { 420 | // TODO: maybe figure out a better way to parse regexes 421 | // than a call to `eval`? 422 | let result = this.evalInThisContext(value.value); 423 | this.indexMap.push({ element: result, index: valueIndex }); 424 | return result; 425 | } else { 426 | throw new Error(`Cannot deserialize unrecognized content kind '${value.kind}'.`); 427 | } 428 | } 429 | 430 | /** 431 | * Tries to evaluate a string of code.. 432 | * @param code The code to evaluate. 433 | */ 434 | private evalInThisContext(code: string) { 435 | // Ideally, we'd like to use a custom `eval` implementation. 436 | // Otherwise, `eval` will just have to do. 437 | try { 438 | if (this.evalImpl) { 439 | return this.evalImpl(code); 440 | } else { 441 | return eval(code); 442 | } 443 | } catch (e) { 444 | throw new Error(`Failed to deserialize code: \`${code}\`: ${e}`) 445 | } 446 | } 447 | 448 | /** 449 | * Deserializes a serialized value's properties. 450 | * @param value The serialized value. 451 | * @param deserializedValue The deserialized value to update.s 452 | */ 453 | private deserializeProperties(value: any, deserializedValue: any): void { 454 | for (let key in value.refs) { 455 | deserializedValue[key] = this.get(value.refs[key]); 456 | } 457 | for (let key in value.descriptions) { 458 | // Object property descriptions require some extra love. 459 | let desc = value.descriptions[key]; 460 | let parsedDesc = { ...desc, }; 461 | if (desc.get) { 462 | parsedDesc.get = this.get(desc.get); 463 | } 464 | if (desc.set) { 465 | parsedDesc.set = this.get(desc.set); 466 | } 467 | if (desc.value) { 468 | parsedDesc.value = this.get(desc.value); 469 | } 470 | Object.defineProperty(deserializedValue, key, parsedDesc); 471 | } 472 | } 473 | 474 | /** 475 | * Gets a deserialized version of the root object serialized by this graph. 476 | */ 477 | get root(): any { 478 | return this.get(this.rootIndex); 479 | } 480 | } 481 | -------------------------------------------------------------------------------- /ts-closure-transform/src/variable-visitor.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import { simplifyExpression, noAssignmentTokenMapping } from './simplify'; 3 | 4 | /** 5 | * Gets a node's emit flags. 6 | * @param node A node to query. 7 | */ 8 | function getEmitFlags(node: ts.Node): ts.EmitFlags | undefined { 9 | // NOTE: this is a hack that inspects the TypeScript compiler's internals. 10 | // The reason we're resorting to this is that TypeScript does not export 11 | // its version of `getEmitFlags`---it only exports `setEmitFlags`. 12 | let castNode = node as ts.Node & { emitNode?: { flags: ts.EmitFlags } }; 13 | let emitNode = castNode.emitNode; 14 | return emitNode && emitNode.flags; 15 | } 16 | 17 | /** 18 | * Tells if an identifier is implicitly exported, i.e., if it should 19 | * really be treated as an `exports.Id` expression as opposed to just `Id`. 20 | * @param node An identifier to query. 21 | */ 22 | function isExportedName(node: ts.Identifier): boolean { 23 | let flags = getEmitFlags(node); 24 | if (flags) { 25 | return (flags & ts.EmitFlags.ExportName) === ts.EmitFlags.ExportName; 26 | } else { 27 | return false; 28 | } 29 | } 30 | 31 | /** 32 | * The type of a unique variable identifier. 33 | */ 34 | export type VariableId = number; 35 | 36 | /** 37 | * A backing store for variable numbering. 38 | */ 39 | export class VariableNumberingStore { 40 | private readonly numbering: { name: ts.Identifier, id: VariableId }[]; 41 | private counter: number; 42 | 43 | /** 44 | * Creates an empty variable numbering store. 45 | */ 46 | constructor() { 47 | this.numbering = []; 48 | this.counter = 0; 49 | } 50 | 51 | /** 52 | * Sets the variable id of an identifier. 53 | * @param identifier An identifier. 54 | * @param id The variable id to assign to `identifier`. 55 | */ 56 | setId(identifier: ts.Identifier, id: VariableId): void { 57 | for (let record of this.numbering) { 58 | if (record.name === identifier) { 59 | record.id = id; 60 | return; 61 | } 62 | } 63 | this.numbering.push({ name: identifier, id }); 64 | } 65 | 66 | /** 67 | * Gets the variable id for a particular identifier. 68 | * If the identifier has not been associated with a variable 69 | * id yet, then a new one is created. 70 | * @param identifier The identifier to find a variable id for. 71 | */ 72 | getOrCreateId(identifier: ts.Identifier): VariableId { 73 | for (let { name, id } of this.numbering) { 74 | if (name === identifier) { 75 | return id; 76 | } 77 | } 78 | let id = this.counter++; 79 | this.numbering.push({ name: identifier, id }); 80 | return id; 81 | } 82 | } 83 | 84 | /** 85 | * A scope data structure that assigns a unique id to each variable. 86 | */ 87 | export class VariableNumberingScope { 88 | /** 89 | * All local variables defined in this scope. 90 | */ 91 | private readonly localVariables: { [name: string]: VariableId }; 92 | 93 | /** 94 | * A set of variable IDs that have not been explicitly defined 95 | * in a variable numbering scope yet. This set is shared by 96 | * all variable numbering scopes and can be indexed by variable 97 | * names. 98 | */ 99 | private readonly pendingVariables: { [name: string]: VariableId }; 100 | 101 | /** 102 | * This scope's parent scope. 103 | */ 104 | readonly parent: VariableNumberingScope | undefined; 105 | 106 | /** 107 | * The variable numbering store for this scope. 108 | */ 109 | readonly store: VariableNumberingStore; 110 | 111 | /** 112 | * Tells if this scope is a function scope. 113 | */ 114 | readonly isFunctionScope: boolean; 115 | 116 | /** 117 | * Creates a variable numbering scope. 118 | * @param isFunctionScope Tells if the scope is a function scope. 119 | * @param parentOrStore A parent scope or a variable numbering store. 120 | */ 121 | constructor(isFunctionScope: boolean, parentOrStore?: VariableNumberingScope | VariableNumberingStore) { 122 | this.isFunctionScope = isFunctionScope; 123 | this.localVariables = {}; 124 | if (!parentOrStore) { 125 | this.parent = undefined; 126 | this.pendingVariables = {}; 127 | this.store = new VariableNumberingStore(); 128 | } else if (parentOrStore instanceof VariableNumberingScope) { 129 | this.parent = parentOrStore; 130 | this.pendingVariables = parentOrStore.pendingVariables; 131 | this.store = parentOrStore.store; 132 | } else { 133 | this.parent = undefined; 134 | this.pendingVariables = {}; 135 | this.store = parentOrStore; 136 | } 137 | } 138 | 139 | /** 140 | * Defines a variable with a particular name in 141 | * the current scope. 142 | * @param name The name of the variable to define. 143 | */ 144 | define(name: ts.Identifier): VariableId { 145 | // If the variable is pending, then we want to reuse 146 | // the pending ID. 147 | let newId: VariableId; 148 | if (name.text in this.pendingVariables) { 149 | newId = this.pendingVariables[name.text]; 150 | delete this.pendingVariables[name.text]; 151 | this.store.setId(name, newId); 152 | } else { 153 | newId = this.store.getOrCreateId(name); 154 | } 155 | 156 | if (name.text !== "") { 157 | this.localVariables[name.text] = newId; 158 | } 159 | return newId; 160 | } 161 | 162 | /** 163 | * Defines in this scope all variables specified by a binding name. 164 | * @param name A binding name that lists variables to define. 165 | */ 166 | defineVariables(name: ts.BindingName) { 167 | if (name === undefined) { 168 | 169 | } else if (ts.isIdentifier(name)) { 170 | if (!isExportedName(name)) { 171 | this.define(name); 172 | } 173 | } else if (ts.isArrayBindingPattern(name)) { 174 | for (let elem of name.elements) { 175 | if (ts.isBindingElement(elem)) { 176 | this.defineVariables(elem.name); 177 | } 178 | } 179 | } else { 180 | for (let elem of name.elements) { 181 | this.defineVariables(elem.name); 182 | } 183 | } 184 | } 185 | 186 | /** 187 | * Gets the identifier for a variable with a 188 | * particular name in the current scope. 189 | * @param name The name of the variable. 190 | */ 191 | getId(name: ts.Identifier): VariableId { 192 | let text = name.text; 193 | if (text in this.localVariables) { 194 | // If the name is defined in the local variables, 195 | // then just grab its id. Also, don't forget to 196 | // update the variable numbering store. 197 | let id = this.localVariables[text]; 198 | this.store.setId(name, id); 199 | return id; 200 | } else if (text in this.pendingVariables) { 201 | // If the name is defined in the pending variables, 202 | // then we'll essentially do the same thing. 203 | let id = this.pendingVariables[text]; 204 | this.store.setId(name, id); 205 | return id; 206 | } else if (this.parent) { 207 | // If the scope has a parent and the name is not defined 208 | // as a local variable, then defer to the parent. 209 | return this.parent.getId(name); 210 | } else { 211 | // Otherwise, add the name to the pending list. 212 | let id = this.store.getOrCreateId(name); 213 | this.pendingVariables[name.text] = id; 214 | return id; 215 | } 216 | } 217 | 218 | /** 219 | * Gets the function scope of this scope, 220 | * which is this scope if it is a function or top-level scope and 221 | * the enclosing scope's function scope otherwise. 222 | */ 223 | get functionScope(): VariableNumberingScope { 224 | if (this.isFunctionScope || !this.parent) { 225 | return this; 226 | } else { 227 | return this.parent.functionScope; 228 | } 229 | } 230 | } 231 | 232 | /** 233 | * A base class for visitors that visit variable uses, 234 | * definitions and assignments. 235 | */ 236 | export abstract class VariableVisitor { 237 | /** 238 | * The scope the variable visitor is currently in. 239 | */ 240 | protected scope: VariableNumberingScope; 241 | 242 | /** 243 | * The transformation context. 244 | */ 245 | readonly ctx: ts.TransformationContext; 246 | 247 | /** 248 | * Creates a variable visitor. 249 | * @param ctx The visitor's transformation context. 250 | * @param store An optional variable numbering store. 251 | */ 252 | constructor(ctx: ts.TransformationContext, store?: VariableNumberingStore) { 253 | this.ctx = ctx; 254 | this.scope = new VariableNumberingScope(true, store); 255 | } 256 | 257 | /** 258 | * Gets the variable numbering store used by this visitor. 259 | */ 260 | get store(): VariableNumberingStore { 261 | return this.scope.store; 262 | } 263 | 264 | /** 265 | * Visits a simple use of a variable. 266 | * @param node The variable use to visit. 267 | * @param id The variable's identifier. 268 | */ 269 | protected abstract visitUse(node: ts.Identifier, id: VariableId): ts.Expression; 270 | 271 | /** 272 | * Visits a variable definition. 273 | * @param node The name of the variable. 274 | * @param id The variable's identifier. 275 | * @returns An optional initial value for the definition. 276 | */ 277 | protected abstract visitDef(node: ts.Identifier, id: VariableId): undefined | ts.Expression; 278 | 279 | /** 280 | * Visits an expression that assigns a value to 281 | * a variable. 282 | * @param name The name of the variable. 283 | * @param id The variable's identifier. 284 | * @returns A function that rewrites the assignment 285 | * to the variable if said assignment should 286 | * be rewritten; otherwise, `undefined`. 287 | */ 288 | protected abstract visitAssignment( 289 | name: ts.Identifier, 290 | id: VariableId): undefined | ((assignment: ts.BinaryExpression) => ts.Expression); 291 | 292 | /** 293 | * Visits an expression node. 294 | * @param node The expression node to visit. 295 | */ 296 | protected visitExpression(node: ts.Expression): ts.Expression { 297 | return this.visit(node); 298 | } 299 | 300 | /** 301 | * Visits a statement node. If the statement expands into more than 302 | * one statement or no statements at all, then the result is wrapped 303 | * in a block. 304 | * @param node The statement node to visit. 305 | */ 306 | protected visitStatement(node: ts.Statement): ts.Statement { 307 | let result = this.visit(node); 308 | if (result === undefined) { 309 | return ts.factory.createBlock([]); 310 | } else if (Array.isArray(result)) { 311 | if (result.length == 1) { 312 | return result[0]; 313 | } else { 314 | return ts.factory.createBlock(result); 315 | } 316 | } else { 317 | return result; 318 | } 319 | } 320 | 321 | /** 322 | * Visits a particular node. 323 | * @param node The node to visit. 324 | */ 325 | visit(node: ts.Node): ts.VisitResult { 326 | if (node === undefined) { 327 | return undefined; 328 | 329 | } 330 | // Expressions 331 | else if (ts.isIdentifier(node)) { 332 | if (node.text !== "undefined" 333 | && node.text !== "null" 334 | && node.text !== "arguments" 335 | && !isExportedName(node)) { 336 | return this.visitUse(node, this.scope.getId(node)); 337 | } else { 338 | return node; 339 | } 340 | 341 | } else if (ts.isTypeNode(node)) { 342 | // Don't visit type nodes. 343 | return node; 344 | 345 | } else if (ts.isPropertyAccessExpression(node)) { 346 | return ts.factory.updatePropertyAccessExpression( 347 | node, 348 | this.visitExpression(node.expression), 349 | node.name); 350 | 351 | } else if (ts.isQualifiedName(node)) { 352 | return ts.factory.updateQualifiedName( 353 | node, 354 | this.visit(node.left), 355 | node.right); 356 | 357 | } else if (ts.isPropertyAssignment(node)) { 358 | return ts.factory.updatePropertyAssignment( 359 | node, 360 | node.name, 361 | this.visitExpression(node.initializer)); 362 | 363 | } else if (ts.isShorthandPropertyAssignment(node)) { 364 | return ts.factory.updateShorthandPropertyAssignment( 365 | node, 366 | node.name, 367 | this.visitExpression(node.objectAssignmentInitializer)); 368 | 369 | } else if (ts.isBinaryExpression(node)) { 370 | return this.visitBinaryExpression(node); 371 | 372 | } else if (ts.isPrefixUnaryExpression(node) 373 | && (node.operator === ts.SyntaxKind.PlusPlusToken 374 | || node.operator === ts.SyntaxKind.MinusMinusToken)) { 375 | return this.visitPreUpdateExpression(node); 376 | 377 | } else if (ts.isPostfixUnaryExpression(node) 378 | && (node.operator === ts.SyntaxKind.PlusPlusToken 379 | || node.operator === ts.SyntaxKind.MinusMinusToken)) { 380 | return this.visitPostUpdateExpression(node); 381 | 382 | } 383 | // Statements 384 | else if (ts.isVariableStatement(node)) { 385 | return this.visitVariableStatement(node); 386 | } else if (ts.isForStatement(node)) { 387 | return this.visitForStatement(node); 388 | } else if (ts.isForInStatement(node) || ts.isForOfStatement(node)) { 389 | return this.visitForInOrOfStatement(node); 390 | } else if (ts.isTryStatement(node)) { 391 | return this.visitTryStatement(node); 392 | } 393 | // Things that introduce scopes. 394 | else if (ts.isArrowFunction(node)) { 395 | let body = this.visitFunctionBody(node.parameters, node.body); 396 | return ts.factory.updateArrowFunction( 397 | node, 398 | node.modifiers, 399 | node.typeParameters, 400 | node.parameters, 401 | node.type, 402 | node.equalsGreaterThanToken, 403 | body); 404 | 405 | } else if (ts.isFunctionExpression(node)) { 406 | let body = this.visitFunctionBody(node.parameters, node.body, node.name); 407 | return ts.factory.updateFunctionExpression( 408 | node, 409 | node.modifiers, 410 | node.asteriskToken, 411 | node.name, 412 | node.typeParameters, 413 | node.parameters, 414 | node.type, 415 | body); 416 | 417 | } else if (ts.isGetAccessor(node)) { 418 | return ts.factory.updateGetAccessorDeclaration( 419 | node, 420 | node.modifiers, 421 | node.name, 422 | node.parameters, 423 | node.type, 424 | this.visitFunctionBody(node.parameters, node.body)); 425 | 426 | } else if (ts.isSetAccessor(node)) { 427 | return ts.factory.updateSetAccessorDeclaration( 428 | node, 429 | node.modifiers, 430 | node.name, 431 | node.parameters, 432 | this.visitFunctionBody(node.parameters, node.body)); 433 | 434 | } else if (ts.isMethodDeclaration(node)) { 435 | return ts.factory.updateMethodDeclaration( 436 | node, 437 | node.modifiers, 438 | node.asteriskToken, 439 | node.name, 440 | node.questionToken, 441 | node.typeParameters, 442 | node.parameters, 443 | node.type, 444 | this.visitFunctionBody(node.parameters, node.body)); 445 | 446 | } else if (ts.isFunctionDeclaration(node)) { 447 | return this.visitFunctionDeclaration(node); 448 | 449 | } else { 450 | let oldScope = this.scope; 451 | this.scope = new VariableNumberingScope(false, oldScope); 452 | let result = this.visitChildren(node); 453 | this.scope = oldScope; 454 | return result; 455 | } 456 | } 457 | 458 | private visitChildren(node: T): T { 459 | return ts.visitEachChild(node, n => this.visit(n), this.ctx); 460 | } 461 | 462 | private visitBlock(block: ts.Block): ts.Block { 463 | return ts.factory.updateBlock( 464 | block, 465 | ts.visitLexicalEnvironment( 466 | block.statements, 467 | n => this.visit(n), 468 | this.ctx)); 469 | } 470 | 471 | /** 472 | * Visits a binary expression. 473 | * @param node The expression to visit. 474 | */ 475 | private visitBinaryExpression(node: ts.BinaryExpression): ts.Expression { 476 | let lhs = node.left; 477 | if (ts.isIdentifier(lhs) && !isExportedName(lhs)) { 478 | // Syntax we'd like to handle: identifier [+,-,*,/,...]= rhs; 479 | let id = this.scope.getId(lhs); 480 | 481 | if (node.operatorToken.kind === ts.SyntaxKind.EqualsToken) { 482 | let visited = ts.factory.updateBinaryExpression( 483 | node, 484 | lhs, 485 | node.operatorToken, 486 | this.visitExpression(node.right), 487 | ); 488 | let rewrite = this.visitAssignment(lhs, id); 489 | if (rewrite) { 490 | return rewrite(visited); 491 | } else { 492 | return visited; 493 | } 494 | } else if (node.operatorToken.kind in noAssignmentTokenMapping) { 495 | let rewrite = this.visitAssignment(lhs, id); 496 | if (rewrite) { 497 | return rewrite( 498 | ts.factory.updateBinaryExpression( 499 | node, 500 | lhs, 501 | ts.factory.createToken(ts.SyntaxKind.EqualsToken), 502 | ts.factory.createBinaryExpression( 503 | this.visitUse(lhs, id), 504 | node.operatorToken, 505 | this.visitExpression(node.right)), 506 | )); 507 | } else { 508 | return ts.factory.updateBinaryExpression( 509 | node, 510 | lhs, 511 | node.operatorToken, 512 | this.visitExpression(node.right), 513 | ); 514 | } 515 | } 516 | return this.visitChildren(node); 517 | } else { 518 | return this.visitChildren(node); 519 | } 520 | } 521 | 522 | /** 523 | * Visits a pre-increment or pre-decrement expression. 524 | * @param expression The expression to visit. 525 | */ 526 | private visitPreUpdateExpression(expression: ts.PrefixUnaryExpression): ts.Expression { 527 | if (ts.isIdentifier(expression.operand) && !isExportedName(expression.operand)) { 528 | let id = this.scope.getId(expression.operand); 529 | let rewrite = this.visitAssignment(expression.operand, id); 530 | if (rewrite) { 531 | // Rewrite pre-increment and pre-decrement updates by desugaring them and 532 | // then rewriting the desugared version. 533 | let use = this.visitUse(expression.operand, id); 534 | let value = expression.operator === ts.SyntaxKind.PlusPlusToken 535 | ? ts.factory.createAdd(use, ts.factory.createNumericLiteral(1)) 536 | : ts.factory.createSubtract(use, ts.factory.createNumericLiteral(1)); 537 | 538 | return simplifyExpression( 539 | rewrite( 540 | ts.factory.createAssignment( 541 | expression.operand, 542 | value))); 543 | } else { 544 | return expression; 545 | } 546 | } else { 547 | return this.visitChildren(expression); 548 | } 549 | } 550 | 551 | /** 552 | * Visits a post-increment or post-decrement expression. 553 | * @param expression The expression to visit. 554 | */ 555 | private visitPostUpdateExpression(expression: ts.PostfixUnaryExpression): ts.Expression { 556 | if (ts.isIdentifier(expression.operand) && !isExportedName(expression.operand)) { 557 | let id = this.scope.getId(expression.operand); 558 | let rewrite = this.visitAssignment(expression.operand, id); 559 | if (rewrite) { 560 | // Rewrite post-increment and post-decrement updates by desugaring them and 561 | // then rewriting the desugared version. 562 | let secondUse = this.visitUse(expression.operand, id); 563 | let value = expression.operator === ts.SyntaxKind.PlusPlusToken 564 | ? ts.factory.createAdd(secondUse, ts.factory.createNumericLiteral(1)) 565 | : ts.factory.createSubtract(secondUse, ts.factory.createNumericLiteral(1)); 566 | 567 | if (expression.parent && 568 | (ts.isExpressionStatement(expression.parent) 569 | || ts.isForStatement(expression.parent))) { 570 | // If the postfix update's parent is an expression statement or a 571 | // 'for' statement then we don't need an extra variable. 572 | return simplifyExpression( 573 | rewrite( 574 | ts.factory.createAssignment( 575 | expression.operand, 576 | value))); 577 | } else { 578 | let temp = this.createTemporary(); 579 | this.ctx.hoistVariableDeclaration(temp); 580 | 581 | let firstUse = this.visitUse(expression.operand, id); 582 | 583 | return ts.factory.createCommaListExpression( 584 | [ 585 | ts.factory.createAssignment( 586 | temp, 587 | firstUse), 588 | simplifyExpression( 589 | rewrite( 590 | ts.factory.createAssignment( 591 | expression.operand, 592 | value))), 593 | temp 594 | ]); 595 | } 596 | } else { 597 | return expression; 598 | } 599 | } else { 600 | return this.visitChildren(expression); 601 | } 602 | } 603 | 604 | /** 605 | * Defines all variables in a binding name. 606 | * @param name A binding name. 607 | * @param scope The scope to define the variables in. 608 | */ 609 | private defineVariables(name: ts.BindingName) { 610 | // This is a little off for 'let' bindings, but we'll just 611 | // assume that those have all been lowered to 'var' already. 612 | this.scope.functionScope.defineVariables(name); 613 | } 614 | 615 | /** 616 | * Visits a variable statement. 617 | * @param statement The statement to visit. 618 | */ 619 | private visitVariableStatement(statement: ts.VariableStatement): ts.VisitResult { 620 | // Visiting variable statements is a bit of a pain because 621 | // we need to recursively visit destructured expressions. 622 | // 623 | // For example, suppose that we want to box `y` in 624 | // 625 | // let x = 10, { y, z } = expr, w = 20; 626 | // 627 | // then we need to add an extra statement and split everything up 628 | // 629 | // let x = 10; 630 | // let { tmp, z } = expr; 631 | // let y = { value: tmp }; 632 | // let w = 20; 633 | // 634 | 635 | let statements: ts.Statement[] = []; 636 | let fixups: ts.Statement[] = []; 637 | let declarations: ts.VariableDeclaration[] = []; 638 | 639 | function flushDeclarations() { 640 | if (declarations.length === 0) { 641 | return; 642 | } 643 | 644 | statements.push( 645 | ts.factory.updateVariableStatement( 646 | statement, 647 | statement.modifiers, 648 | ts.factory.updateVariableDeclarationList( 649 | statement.declarationList, 650 | declarations))); 651 | declarations = []; 652 | } 653 | 654 | let visitBinding = (name: ts.BindingName): ts.BindingName => { 655 | 656 | if (ts.isIdentifier(name)) { 657 | if (isExportedName(name)) { 658 | return name; 659 | } 660 | 661 | let id = this.scope.getId(name); 662 | let init = this.visitDef(name, id); 663 | let rewrite = this.visitAssignment(name, id); 664 | if (rewrite) { 665 | let temp = this.createTemporary(); 666 | fixups.push( 667 | ts.factory.createVariableStatement( 668 | [], 669 | [ 670 | ts.factory.createVariableDeclaration( 671 | name, 672 | undefined, 673 | undefined, 674 | init) 675 | ]), 676 | ts.factory.createExpressionStatement( 677 | rewrite( 678 | ts.factory.createAssignment(name, temp)))); 679 | return temp; 680 | } else { 681 | return name; 682 | } 683 | } else if (ts.isArrayBindingPattern(name)) { 684 | let newElements: ts.ArrayBindingElement[] = []; 685 | for (let elem of name.elements) { 686 | if (ts.isOmittedExpression(elem)) { 687 | newElements.push(elem); 688 | } else { 689 | newElements.push( 690 | ts.factory.updateBindingElement( 691 | elem, 692 | elem.dotDotDotToken, 693 | elem.propertyName, 694 | visitBinding(elem.name), 695 | elem.initializer)); 696 | } 697 | return ts.factory.updateArrayBindingPattern( 698 | name, 699 | newElements); 700 | } 701 | } else { 702 | let newElements: ts.BindingElement[] = []; 703 | for (let elem of name.elements) { 704 | newElements.push( 705 | ts.factory.updateBindingElement( 706 | elem, 707 | elem.dotDotDotToken, 708 | elem.propertyName, 709 | visitBinding(elem.name), 710 | elem.initializer)); 711 | } 712 | return ts.factory.updateObjectBindingPattern( 713 | name, 714 | newElements); 715 | } 716 | } 717 | 718 | for (let decl of statement.declarationList.declarations) { 719 | let name = decl.name; 720 | // Define the declaration's names. 721 | this.defineVariables(decl.name); 722 | // Visit the initializer expression. 723 | let initializer = this.visitExpression(decl.initializer); 724 | if (ts.isIdentifier(name)) { 725 | if (!isExportedName(name)) { 726 | // Simple initializations get special treatment because they 727 | // don't need a special fix-up statement, even if they are 728 | // rewritten. 729 | let id = this.scope.getId(name); 730 | let customInit = this.visitDef(name, id); 731 | if (initializer) { 732 | let rewrite = this.visitAssignment(name, id); 733 | if (rewrite) { 734 | fixups.push( 735 | ts.factory.createExpressionStatement( 736 | rewrite( 737 | ts.factory.createAssignment( 738 | name, 739 | initializer)))); 740 | initializer = undefined; 741 | } 742 | } 743 | if (customInit) { 744 | if (initializer) { 745 | fixups.push( 746 | ts.factory.createExpressionStatement( 747 | ts.factory.createAssignment( 748 | name, 749 | initializer))); 750 | } 751 | initializer = customInit; 752 | } 753 | } 754 | declarations.push( 755 | ts.factory.updateVariableDeclaration( 756 | decl, 757 | name, 758 | decl.exclamationToken, 759 | decl.type, 760 | initializer)); 761 | } else { 762 | // All other patterns are processed by visiting them recursively. 763 | declarations.push( 764 | ts.factory.updateVariableDeclaration( 765 | decl, 766 | visitBinding(decl.name), 767 | decl.exclamationToken, 768 | decl.type, 769 | initializer)); 770 | } 771 | 772 | // Split the variable statement if we have fix-up statements to emit. 773 | if (fixups.length > 0) { 774 | flushDeclarations(); 775 | statements.push(...fixups); 776 | fixups = []; 777 | } 778 | } 779 | 780 | flushDeclarations(); 781 | 782 | return statements; 783 | } 784 | 785 | /** 786 | * Visits a 'for' statement. 787 | * @param statement The statement to visit. 788 | */ 789 | private visitForStatement(statement: ts.ForStatement): ts.VisitResult { 790 | // Rewriting variables in 'for' statements is actually pretty hard 791 | // because definitions, uses and assignments may be rewritten in 792 | // such a way that a 'for' statement is no longer applicable. 793 | // 794 | // For example, consider this 'for' loop: 795 | // 796 | // for (let i = f(); i < 10; i++) { 797 | // g(); 798 | // } 799 | // 800 | // If `var i = f()` is rewritten as anything other than a single 801 | // variable declaration list, then the logic we want to set up 802 | // can no longer be expressed as a simple 'for' loop. Fortunately, 803 | // we can factor out the initialization part: 804 | // 805 | // { 806 | // let i = f(); 807 | // for (; i < 10; i++) { 808 | // g(); 809 | // } 810 | // } 811 | // 812 | 813 | // 'for' statements introduce a new scope, so let's handle that right away. 814 | let oldScope = this.scope; 815 | this.scope = new VariableNumberingScope(false, oldScope); 816 | let result; 817 | if (statement.initializer && ts.isVariableDeclarationList(statement.initializer)) { 818 | // If the 'for' has a variable declaration list as an initializer, then turn 819 | // the initializer into a variable declaration statement. 820 | let initializer = this.visitStatement(ts.factory.createVariableStatement([], statement.initializer)); 821 | 822 | // Also visit the condition, incrementor and body. 823 | let condition = this.visitExpression(statement.condition); 824 | let incrementor = this.visitExpression(statement.incrementor); 825 | let body = this.visitStatement(statement.statement); 826 | 827 | if (ts.isVariableStatement(initializer)) { 828 | // If the initializer has been rewritten as a variable declaration, then 829 | // we can create a simple 'for' loop. 830 | result = ts.factory.updateForStatement(statement, initializer.declarationList, condition, incrementor, body); 831 | } else { 832 | // Otherwise, we'll factor out the initializer. 833 | result = ts.factory.createBlock([ 834 | initializer, 835 | ts.factory.updateForStatement(statement, undefined, condition, incrementor, body) 836 | ]); 837 | } 838 | } else { 839 | result = this.visitChildren(statement); 840 | } 841 | // Restore the enclosing scope and return. 842 | this.scope = oldScope; 843 | return result; 844 | } 845 | 846 | /** 847 | * Visits a 'try' statement. 848 | * @param statement The statement to visit. 849 | */ 850 | private visitTryStatement(statement: ts.TryStatement): ts.VisitResult { 851 | let tryBlock = this.visitBlock(statement.tryBlock); 852 | let catchClause; 853 | if (statement.catchClause) { 854 | // Catch clauses may introduce a new, locally-scoped variable. 855 | let oldScope = this.scope; 856 | this.scope = new VariableNumberingScope(false, oldScope); 857 | if (statement.catchClause.variableDeclaration) { 858 | // FIXME: allow this variable to be rewritten. 859 | this.defineVariables(statement.catchClause.variableDeclaration.name); 860 | } 861 | catchClause = ts.factory.updateCatchClause( 862 | statement.catchClause, 863 | statement.catchClause.variableDeclaration, 864 | this.visitBlock(statement.catchClause.block)); 865 | this.scope = oldScope; 866 | } else { 867 | catchClause = statement.catchClause; 868 | } 869 | let finallyBlock = statement.finallyBlock 870 | ? this.visitBlock(statement.finallyBlock) 871 | : statement.finallyBlock; 872 | return ts.factory.updateTryStatement(statement, tryBlock, catchClause, finallyBlock); 873 | } 874 | 875 | /** 876 | * Visits a 'for...in/of' statement. 877 | * @param statement The statement to visit. 878 | */ 879 | private visitForInOrOfStatement(statement: ts.ForInOrOfStatement): ts.VisitResult { 880 | // 'for' statements may introduce a new, locally-scoped variable. 881 | let oldScope = this.scope; 882 | this.scope = new VariableNumberingScope(false, oldScope); 883 | let initializer = statement.initializer; 884 | if (ts.isVariableDeclarationList(initializer)) { 885 | // FIXME: allow declarations to be rewritten here. 886 | for (let element of initializer.declarations) { 887 | this.defineVariables(element.name); 888 | } 889 | } else { 890 | initializer = this.visitExpression(initializer); 891 | } 892 | let expr = this.visitExpression(statement.expression); 893 | let body = this.visitStatement(statement.statement); 894 | this.scope = oldScope; 895 | 896 | if (ts.isForInStatement(statement)) { 897 | return ts.factory.updateForInStatement(statement, initializer, expr, body); 898 | } else { 899 | return ts.factory.updateForOfStatement(statement, statement.awaitModifier, initializer, expr, body); 900 | } 901 | } 902 | 903 | /** 904 | * Visits a function body. 905 | * @param parameters The function's list of parameters. 906 | * @param body The function's body. 907 | * @param body The function's name, if any. 908 | */ 909 | private visitFunctionBody( 910 | parameters: ts.NodeArray, 911 | body: ts.Block | ts.Expression, 912 | name?: ts.Identifier) { 913 | 914 | let oldScope = this.scope; 915 | this.scope = new VariableNumberingScope(true, oldScope); 916 | 917 | if (name !== undefined) { 918 | this.defineVariables(name); 919 | } 920 | 921 | for (let param of parameters) { 922 | this.defineVariables(param.name); 923 | } 924 | 925 | let result; 926 | if (ts.isBlock(body)) { 927 | result = this.visitBlock(body); 928 | } else { 929 | result = this.visitExpression(body); 930 | } 931 | 932 | this.scope = oldScope; 933 | return result; 934 | } 935 | 936 | /** 937 | * Visits a function declaration node. 938 | * @param node The function declaration node to visit. 939 | */ 940 | private visitFunctionDeclaration(node: ts.FunctionDeclaration): ts.VisitResult { 941 | // We need to be careful here because function declarations 942 | // are actually variable definitions and assignments. If 943 | // the variable visitor decides to rewrite a function 944 | // declaration, then we need to rewrite it as an expression. 945 | let defInitializer: ts.Expression = undefined; 946 | let rewriteAssignment: ((assignment: ts.BinaryExpression) => ts.Expression) = undefined; 947 | if (node.name) { 948 | this.defineVariables(node.name); 949 | let id = this.scope.getId(node.name); 950 | defInitializer = this.visitDef(node.name, id); 951 | rewriteAssignment = this.visitAssignment(node.name, id); 952 | } 953 | 954 | let body = this.visitFunctionBody(node.parameters, node.body); 955 | 956 | if (defInitializer || rewriteAssignment) { 957 | let funExpr = ts.factory.createFunctionExpression( 958 | // @ts-ignore 959 | node.modifiers, 960 | node.asteriskToken, 961 | undefined, 962 | node.typeParameters, 963 | node.parameters, 964 | node.type, 965 | body); 966 | 967 | let funAssignment = ts.factory.createAssignment(node.name, funExpr); 968 | 969 | return [ 970 | ts.factory.createVariableStatement( 971 | [], 972 | [ts.factory.createVariableDeclaration(node.name, undefined, undefined,defInitializer)]), 973 | ts.factory.createExpressionStatement(rewriteAssignment ? rewriteAssignment(funAssignment) : funAssignment) 974 | ]; 975 | 976 | } else { 977 | return ts.factory.updateFunctionDeclaration( 978 | node, 979 | node.modifiers, 980 | node.asteriskToken, 981 | node.name, 982 | node.typeParameters, 983 | node.parameters, 984 | node.type, 985 | body); 986 | } 987 | } 988 | 989 | /** 990 | * Creates a temporary variable name. 991 | */ 992 | protected createTemporary(): ts.Identifier { 993 | let result = ts.factory.createUniqueName("_tct_variable_visitor"); 994 | this.scope.define(result); 995 | return result; 996 | } 997 | } 998 | --------------------------------------------------------------------------------