diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 1720c2e..77932d2 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -8,7 +8,7 @@ jobs: test: strategy: matrix: - node-version: [12, 14, 16] + node-version: [12, 14, 16, 18] platform: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: diff --git a/.gitignore b/.gitignore index 4e5f2ec..7c5c031 100644 --- a/.gitignore +++ b/.gitignore @@ -60,8 +60,7 @@ typings # next.js build output .next -*.js -*.js.map -*.d.ts *.DS_Store *.lock + +dist/ diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..5ed1ed6 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,5 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +export default { + preset: 'ts-jest', + testEnvironment: 'node', +}; diff --git a/package.json b/package.json index 6540c2d..7a1a38e 100644 --- a/package.json +++ b/package.json @@ -26,15 +26,18 @@ "build": "tsc --project tsconfig.json", "dryrun": "npm publish --dry-run", "lint": "xo && echo 'No lint errors. All good!'", - "test": "node tests/test.js" + "test": "jest tests/test.ts" }, "dependencies": { "type-fest": "^2.1.0", "typescript": "^4.4.2" }, "devDependencies": { + "@types/jest": "^29.5.4", "@types/node": "^18.0.0", + "jest": "^29.6.4", "prettier": "^2.3.2", + "ts-jest": "^29.1.1", "xo": "^0.44.0" }, "xo": { diff --git a/src/index.ts b/src/index.ts index cde2386..6e065ea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,5 @@ -import fs from 'fs'; -import typescript from 'typescript'; -import type { PackageJson } from 'type-fest'; -import type { +import fs from 'node:fs'; +import typescript, { Node, SourceFile, SyntaxKind, @@ -9,7 +7,12 @@ import type { TransformerFactory, TranspileOptions, Visitor, + ImportDeclaration, + ImportSpecifier, + StringLiteralLike, + Identifier, } from 'typescript'; +import type { PackageJson } from 'type-fest'; const { addSyntheticTrailingComment, @@ -20,6 +23,7 @@ const { createNotEmittedStatement, createObjectLiteralExpression, createPropertyAssignment, + createShorthandPropertyAssignment, createToken, createVariableDeclaration, createVariableDeclarationList, @@ -137,7 +141,8 @@ const ts2gas = (source: string, transpileOptions: Readonly = { /** * Filter any import declaration */ - const importNodeFilter: NodeFilter = (node: Node) => isImportEqualsDeclaration(node) || isImportDeclaration(node); + const importEqualsNodeFilter: NodeFilter = (node: Node) => isImportEqualsDeclaration(node); + const importNodeFilter: NodeFilter = (node: Node) => isImportDeclaration(node); /** * Filter any identifier @@ -160,14 +165,84 @@ const ts2gas = (source: string, transpileOptions: Readonly = { return ignoredNode; }; + /** + * Create a mock module import statement + * @param {Node} node The import node to mock the module import for. + */ + const createMockModuleImportStatement: Transformer = (node: Node) => { + const importNode = node as ImportDeclaration; + const moduleSpecifier = importNode.moduleSpecifier as StringLiteralLike; + const moduleName = moduleSpecifier.text.slice(moduleSpecifier.text.lastIndexOf('/') + 1); + + const defaultNameSpecifier = importNode.importClause?.name + ? [createPropertyAssignment(createIdentifier('default'), createIdentifier(idText(importNode.importClause.name)))] + : []; + + const namedSpecifiers = + importNode.importClause?.namedBindings + ?.getChildren() + .filter((child) => child.kind === SyntaxKind.SyntaxList) + .flatMap((list) => list.getChildren().filter((c) => c.kind === SyntaxKind.ImportSpecifier)) + .map((specefier) => specefier as ImportSpecifier) + .map((specifier) => idText(specifier.propertyName ?? specifier.name)) + .map((specifierName) => createShorthandPropertyAssignment(specifierName)) ?? []; + + const specifiers = [...defaultNameSpecifier, ...namedSpecifiers]; + + const namespaceSpecifier = importNode.importClause?.namedBindings + ?.getChildren() + .find((child) => child.kind === SyntaxKind.Identifier) as Identifier; + + const namespace = namespaceSpecifier ? idText(namespaceSpecifier) : null; + const specifiersNode = namespace ? createIdentifier('exports') : createObjectLiteralExpression(specifiers, false); + const mockModuleName = namespace ?? `${moduleName}_1`; + + const moduleImport = createVariableStatement( + undefined, + createVariableDeclarationList( + [ + createVariableDeclaration( + createIdentifier(mockModuleName), + undefined, + undefined, + createBinaryExpression( + createIdentifier(mockModuleName), + createToken(tsSyntaxKind.BarBarToken), + specifiersNode, + ), + ), + ], + NodeFlags.None, + ), + ); + addSyntheticTrailingComment( + moduleImport, + tsSyntaxKind.SingleLineCommentTrivia, + node.getText().replace(/\n/g, '\\n'), + ); + return moduleImport; + }; + // `before:` transformer factories + /** + * Create a 'before' Transformer callback function + * It create a global variable to mock a module import. + * @param {NodeFilter} nodeFilter The node visitor used to transform. + */ + const mockModuleImportBeforeBuilder: BeforeTransformerFactory = (nodeFilter: NodeFilter) => (context) => { + const visitor: Visitor = (node) => + nodeFilter(node) ? createMockModuleImportStatement(node) : visitEachChild(node, visitor, context); + + return (sourceFile: SourceFile) => visitNode(sourceFile, visitor); + }; + /** * Create a 'before' Transformer callback function * It use 'createCommentedStatement' to comment-out filtered node * @param {NodeFilter} nodeFilter The node visitor used to transform. */ - const ignoreNodeBeforeBuilder: BeforeTransformerFactory = (nodeFilter) => (context) => { + const ignoreNodeBeforeBuilder: BeforeTransformerFactory = (nodeFilter: NodeFilter) => (context) => { const visitor: Visitor = (node) => nodeFilter(node) ? createCommentedStatement(node) : visitEachChild(node, visitor, context); @@ -179,7 +254,7 @@ const ts2gas = (source: string, transpileOptions: Readonly = { * It use applies the 'NoSubstitution' flag on every node * @param {NodeFilter} nodeFilter The node visitor used to transform (unused here). */ - const noSubstitutionBeforeBuilder: BeforeTransformerFactory = (nodeFilter) => (context) => { + const noSubstitutionBeforeBuilder: BeforeTransformerFactory = (nodeFilter: NodeFilter) => (context) => { const visitor: Visitor = (node) => { if ( nodeFilter(node) && // Node kind is Identifier @@ -202,7 +277,7 @@ const ts2gas = (source: string, transpileOptions: Readonly = { * @param {SyntaxKind} kind the kind of node to filter. * @param {NodeFilter} nodeFilter The node visitor used to transform. */ - const ignoreNodeAfterBuilder: AfterTransformerFactory = (kind, nodeFilter) => (context) => { + const ignoreNodeAfterBuilder: AfterTransformerFactory = (kind: SyntaxKind, nodeFilter: NodeFilter) => (context) => { const previousOnSubstituteNode = context.onSubstituteNode; context.enableSubstitution(kind); @@ -227,9 +302,13 @@ const ts2gas = (source: string, transpileOptions: Readonly = { // ## Imports // Some editors (like IntelliJ) automatically import identifiers. - // Individual imports lines are commented out + // Individual imports lines are transformed into mock module imports // i.e. import GmailMessage = GoogleAppsScript.Gmail.GmailMessage; - const ignoreImport = ignoreNodeBeforeBuilder(importNodeFilter); + const mockImport = mockModuleImportBeforeBuilder(importNodeFilter); + + // ## Imports + // ignore imports like `import GmailMessage = GoogleAppsScript.Gmail.GmailMessage;` + const ignoreImportEquals = ignoreNodeBeforeBuilder(importEqualsNodeFilter); // ## Exports // ignore exports like `export * from 'file'` @@ -339,7 +418,7 @@ const ts2gas = (source: string, transpileOptions: Readonly = { module: ModuleKind.None, }, transformers: { - before: [noSubstitution, ignoreExportFrom, ignoreImport], + before: [noSubstitution, ignoreExportFrom, ignoreImportEquals, mockImport], after: [removeExportEsModule, removeExportsDefault, detectExportNodes, addDummyModuleNodes], }, }; diff --git a/tests/test.ts b/tests/test.ts index f50db1b..5c5b41b 100644 --- a/tests/test.ts +++ b/tests/test.ts @@ -1,294 +1,766 @@ -import ts2gas from '../src/index.js'; - -/** - * Prints the transpiled code before and after transpilation. - * @param {string} code The TypeScript code. - * @example printBeforeAndAfter('const hi:string = `Hi ${1 + 2}`;'); - */ -function printBeforeAndAfter(code: string): void { - console.log('v--TS--v'); - console.log(code); - console.log('---'); - console.log(`${ts2gas(code)}^--GS--^`); // Prevents newline -} +import fs from 'node:fs'; +import type { PackageJson } from 'type-fest'; +import { version } from 'typescript'; +import ts2gas from '../src/index'; -type Tests = Record void>; - -const tests: Tests = { - testConst: () => { - // eslint-disable-next-line no-template-curly-in-string - printBeforeAndAfter('const hi:string = `Hi ${1 + 2}`;'); - }, - testClass: () => { - printBeforeAndAfter( - `class Hamburger { - constructor() { - // This is the constructor. - } - listToppings() { - // This is a method. - } -}`, - ); - }, - testTemplateStrings: () => { - printBeforeAndAfter( - `var name = 'Grant'; -var age = 42; -console.log(\`Hello! My name is \${name}, and I am not \${age} years old.\`); -`, - ); - }, - testArraySpread: () => { - printBeforeAndAfter( - `let cde = ['c', 'd', 'e']; -let scale = ['a', 'b', ...cde, 'f', 'g'];`, - ); - }, - testDestructuring: () => { - printBeforeAndAfter( - `let jane = { firstName: 'Jane', lastName: 'Doe'}; -let john = { firstName: 'John', lastName: 'Doe', middleName: 'Smith' } -function sayName({firstName, lastName, middleName = 'N/A'}) { - console.log(\`Hello \${firstName} \${middleName} \${lastName}\`) -} -sayName(jane) // -> Hello Jane N/A Doe -sayName(john) // -> Hello John Smith Doe`, - ); - }, - testImport: () => { - printBeforeAndAfter( - `import ContentAlignment = GoogleAppsScript.Slides.ContentAlignment; -import GmailMessage = GoogleAppsScript.Gmail.GmailMessage; - -function getCurrentMessage():GmailMessage { - return GmailApp.createDraft("", "", "").send(); -}`, - ); - }, - testExport: () => { - printBeforeAndAfter(`export const pi = 3.141592;`); - }, - testExportDefault: () => { - printBeforeAndAfter(`export default const pi = 3.141592;`); - }, - testModuleExports: () => { - printBeforeAndAfter(`module.exports = 3.14;`); - }, - testModuleExportsObject: () => { - printBeforeAndAfter(`module.exports.foo = {};`); - }, - testRequire: () => { - printBeforeAndAfter(`const a = require('foo');`); - }, - testExportFrom: () => { - printBeforeAndAfter(`export * from 'file'\nexport { foo, bar } from "file"`); - }, - testMultilineImports: () => { - printBeforeAndAfter( - `// next statement will be ignored -import ContentAlignment -= GoogleAppsScript.Slides.ContentAlignment; -// now resume with next statement`, - ); - }, - testMultilineExports: () => { - printBeforeAndAfter( - `// next statement will be ignored -export { foo, bar } - from "file"; -// next statement will be preserved -/** Supported languages for localized data */ -export enum Languages { - /** English (USA) */ - eng_us = 'eng_us', - /** French (France) */ - fre_fr = 'fre_fr', -} -export class Client { - /** URL to the login endpoint */ - private readonly signinUrl: string; +const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8')) as Required; + +function trimWhitespace(template: string): string { + const newlineIndex = template.indexOf('\n'); + + if (newlineIndex === -1) return template; + if (template.slice(0, newlineIndex).trim().length === 0) return trimWhitespace(template.slice(newlineIndex + 1)); + + const tabOffset = template.search(/\S/); + return template.replaceAll(new RegExp(`^ {${tabOffset}}`, 'gm'), '').trim(); } -export function foo() {} -export default Client; -export - { ZipCodeValidator }; -// now resume with next statement`, - ); - }, - testImportFrom: () => { - printBeforeAndAfter( - `import Module from 'TypeScriptModule1'; -const module = new Module(); -import { SubModule } from "TypeScriptModule2"; -const subModule = new SubModule(); -import { SubModule2, SubModule3 } from "TypeScriptModule3"; -const subModule2 = new SubModule2(); -const subModule3 = new SubModule3(); -import { - SubModule4, - SubModule5 -} from "TypeScriptModule4"; -const subModule4 = new SubModule4(); -const subModule5 = new SubModule5();`, - ); - }, - testNamespace: () => { - printBeforeAndAfter( - `namespace Pop { -export const goes = 'Goes'; -export function The(): void {} -export class Wza {} -}`, - ); - }, - testThisKeyword: () => { - printBeforeAndAfter( - `// code in this test semantically incorrect -getAllInstancesByUnits(): UnitMemberInstances { - for (const row of data) { - // following 'this' keyword should remain as is - const instances = row.slice(this.columnOffset - 1); - instances.forEach((e, i) => { - // following 'this' keyword must be substitute with '_this' - const u = this.toUnitInstance(e); + +describe('ts2gas', () => { + test('Const', () => { + const typescript = `const hi:string = \`Hi \${1 + 2}\`;`; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var hi = "Hi ".concat(1 + 2); + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Class', () => { + const typescript = ` + class Hamburger { + constructor() { + // This is the constructor. + } + listToppings() { + // This is a method. + } + } + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var Hamburger = /** @class */ (function () { + function Hamburger() { + // This is the constructor. + } + Hamburger.prototype.listToppings = function () { + // This is a method. + }; + return Hamburger; + }()); + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Template Strings', () => { + const typescript = ` + var name = 'Grant'; + var age = 42; + console.log(\`Hello! My name is \${name}, and I am not \${age} years old.\`); + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var name = 'Grant'; + var age = 42; + console.log("Hello! My name is ".concat(name, ", and I am not ").concat(age, " years old.")); + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Array Spread', () => { + const typescript = ` + let cde = ['c', 'd', 'e']; + let scale = ['a', 'b', ...cde, 'f', 'g']; + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); + }; + var cde = ['c', 'd', 'e']; + var scale = __spreadArray(__spreadArray(['a', 'b'], cde, true), ['f', 'g'], false); + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Destructuring', () => { + const typescript = ` + let jane = { firstName: 'Jane', lastName: 'Doe'}; + let john = { firstName: 'John', lastName: 'Doe', middleName: 'Smith' } + function sayName({firstName, lastName, middleName = 'N/A'}) { + console.log(\`Hello \${firstName} \${middleName} \${lastName}\`) + } + sayName(jane) // -> Hello Jane N/A Doe + sayName(john) // -> Hello John Smith Doe + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var jane = { firstName: 'Jane', lastName: 'Doe' }; + var john = { firstName: 'John', lastName: 'Doe', middleName: 'Smith' }; + function sayName(_a) { + var firstName = _a.firstName, lastName = _a.lastName, _b = _a.middleName, middleName = _b === void 0 ? 'N/A' : _b; + console.log("Hello ".concat(firstName, " ").concat(middleName, " ").concat(lastName)); + } + sayName(jane); // -> Hello Jane N/A Doe + sayName(john); // -> Hello John Smith Doe + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Import', () => { + const typescript = ` + import ContentAlignment = GoogleAppsScript.Slides.ContentAlignment; + import GmailMessage = GoogleAppsScript.Gmail.GmailMessage; + + function getCurrentMessage():GmailMessage { + return GmailApp.createDraft("", "", "").send(); + } + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + //import ContentAlignment = GoogleAppsScript.Slides.ContentAlignment; + //import GmailMessage = GoogleAppsScript.Gmail.GmailMessage; + function getCurrentMessage() { + return GmailApp.createDraft("", "", "").send(); + } + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Export', () => { + const typescript = `export const pi = 3.141592;`; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var exports = exports || {}; + var module = module || { exports: exports }; + exports.pi = void 0; + exports.pi = 3.141592; + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Export Default', () => { + const typescript = `export default const pi = 3.141592;`; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var exports = exports || {}; + var module = module || { exports: exports }; + var pi = 3.141592; + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Module Exports', () => { + const typescript = `module.exports = 3.14;`; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var exports = exports || {}; + var module = module || { exports: exports }; + module.exports = 3.14; + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Module Exports Object', () => { + const typescript = `module.exports.foo = {};`; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var exports = exports || {}; + var module = module || { exports: exports }; + module.exports.foo = {}; + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Require', () => { + const typescript = `const a = require('foo');`; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var a = require('foo'); + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Export From', () => { + const typescript = `export * from 'file' + export { foo, bar } from "file"`; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var exports = exports || {}; + var module = module || { exports: exports }; + //export * from 'file' + //export { foo, bar } from "file" + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Multiline Imports', () => { + const typescript = ` + // next statement will be ignored + import ContentAlignment + = GoogleAppsScript.Slides.ContentAlignment; + // now resume with next statement + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + //import ContentAlignment\\n= GoogleAppsScript.Slides.ContentAlignment; + // now resume with next statement + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Multiline Exports', () => { + const typescript = ` + // next statement will be ignored + export { foo, bar } + from "file"; + // next statement will be preserved + /** Supported languages for localized data */ + export enum Languages { + /** English (USA) */ + eng_us = 'eng_us', + /** French (France) */ + fre_fr = 'fre_fr', + } + export class Client { + /** URL to the login endpoint */ + private readonly signinUrl: string; + } + export function foo() {} + export default Client; + export + { ZipCodeValidator }; + // now resume with next statement + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var exports = exports || {}; + var module = module || { exports: exports }; + exports.ZipCodeValidator = exports.foo = exports.Client = exports.Languages = void 0; + //export { foo, bar }\\n from "file"; + // next statement will be preserved + /** Supported languages for localized data */ + var Languages; + (function (Languages) { + /** English (USA) */ + Languages["eng_us"] = "eng_us"; + /** French (France) */ + Languages["fre_fr"] = "fre_fr"; + })(Languages = exports.Languages || (exports.Languages = {})); + var Client = /** @class */ (function () { + function Client() { + } + return Client; + }()); + exports.Client = Client; + function foo() { } + exports.foo = foo; + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Import From', () => { + const typescript = ` + import Module from 'TypeScriptModule1'; + const module = new Module(); + import { SubModule } from "TypeScriptModule2"; + const subModule = new SubModule(); + import { SubModule2, SubModule3 } from "TypeScriptModule3"; + const subModule2 = new SubModule2(); + const subModule3 = new SubModule3(); + import { + SubModule4, + SubModule5 + } from "TypeScriptModule4"; + const subModule4 = new SubModule4(); + const subModule5 = new SubModule5(); + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var exports = exports || {}; + var module = module || { exports: exports }; + var TypeScriptModule1_1 = TypeScriptModule1_1 || { "default": Module }; //import Module from 'TypeScriptModule1'; + var module = new Module(); + var TypeScriptModule2_1 = TypeScriptModule2_1 || { SubModule: SubModule }; //import { SubModule } from "TypeScriptModule2"; + var subModule = new SubModule(); + var TypeScriptModule3_1 = TypeScriptModule3_1 || { SubModule2: SubModule2, SubModule3: SubModule3 }; //import { SubModule2, SubModule3 } from "TypeScriptModule3"; + var subModule2 = new SubModule2(); + var subModule3 = new SubModule3(); + var TypeScriptModule4_1 = TypeScriptModule4_1 || { SubModule4: SubModule4, SubModule5: SubModule5 }; //import {\\n SubModule4,\\n SubModule5\\n} from "TypeScriptModule4"; + var subModule4 = new SubModule4(); + var subModule5 = new SubModule5(); + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Namespace', () => { + const typescript = ` + namespace Pop { + export const goes = 'Goes'; + export function The(): void {} + export class Wza {} + } + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var Pop; + (function (Pop) { + Pop.goes = 'Goes'; + function The() { } + Pop.The = The; + var Wza = /** @class */ (function () { + function Wza() { + } + return Wza; + }()); + Pop.Wza = Wza; + })(Pop || (Pop = {})); + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('This Keyword', () => { + const typescript = ` + // code in this test semantically incorrect + getAllInstancesByUnits(): UnitMemberInstances { + for (const row of data) { + // following 'this' keyword should remain as is + const instances = row.slice(this.columnOffset - 1); + instances.forEach((e, i) => { + // following 'this' keyword must be substitute with '_this' + const u = this.toUnitInstance(e); + }); + } + } + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var _this = this; + // code in this test semantically incorrect + getAllInstancesByUnits(); + UnitMemberInstances; + { + for (var _i = 0, data_1 = data; _i < data_1.length; _i++) { + var row = data_1[_i]; + // following 'this' keyword should remain as is + var instances = row.slice(this.columnOffset - 1); + instances.forEach(function (e, i) { + // following 'this' keyword must be substitute with '_this' + var u = _this.toUnitInstance(e); + }); + } + } + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Hello World', () => { + const typescript = ` + const writeToLog = (message: string) => console.info(message); + + let words = ['hello', 'world']; + writeToLog(\`\${words.join(' ')}\`);`; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var writeToLog = function (message) { return console.info(message); }; + var words = ['hello', 'world']; + writeToLog("".concat(words.join(' '))); + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Default Params', () => { + const typescript = ` + function JsonResponseHandler(url: string, + query = {}, + params = {muteHttpExceptions: true}, + cacheName: string, cacheTime = 3600) { + // ... + } + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + function JsonResponseHandler(url, query, params, cacheName, cacheTime) { + if (query === void 0) { query = {}; } + if (params === void 0) { params = { muteHttpExceptions: true }; } + if (cacheTime === void 0) { cacheTime = 3600; } + // ... + } + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Export Import Workaround Part1', () => { + const typescript = ` + export const ICONS = { + email: \`foo.png\`; + }; + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var exports = exports || {}; + var module = module || { exports: exports }; + exports.ICONS = void 0; + exports.ICONS = { + email: "foo.png" + }; + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Export Import Workaround Part2', () => { + const typescript = ` + import { ICONS } from './package'; + + type _i_ICONS = typeof ICONS; + declare namespace exports { + const ICONS: _i_ICONS; + } + + exports.ICONS.email; + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var exports = exports || {}; + var module = module || { exports: exports }; + var package_1 = package_1 || { ICONS: ICONS }; //import { ICONS } from './package'; + exports.ICONS.email; + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Export Import Namespace Workaround', () => { + const typescript = ` + namespace Package { + export function foo() {} + } + + Package.foo(); + + const nameIWantForMyImports = Package.foo; + nameIWantForMyImports();`; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var Package; + (function (Package) { + function foo() { } + Package.foo = foo; + })(Package || (Package = {})); + Package.foo(); + var nameIWantForMyImports = Package.foo; + nameIWantForMyImports(); + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + describe('TypeScript 3.4.x', () => { + test('High Order', () => { + const typescript = ` + // Higher order function type inference + declare function pipe(ab: (...args: A) => B, bc: (b: B) => C): (...args: A) => C; + + declare function list(a: T): T[]; + declare function box(x: V): { value: V }; + + const listBox = pipe(list, box); // (a: T) => { value: T[] } + const boxList = pipe(box, list); // (x: V) => { value: V }[] + + const x1 = listBox(42); // { value: number[] } + const x2 = boxList('hello'); // { value: string }[] + + const flip = (f: (a: A, b: B) => C) => (b: B, a: A) => f(a, b); + const zip = (x: T, y: U): [T, U] => [x, y]; + const flipped = flip(zip); // (b: U, a: T) => [T, U] + + const t1 = flipped(10, 'hello'); // [string, number] + const t2 = flipped(true, 0); // [number, boolean] + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var listBox = pipe(list, box); // (a: T) => { value: T[] } + var boxList = pipe(box, list); // (x: V) => { value: V }[] + var x1 = listBox(42); // { value: number[] } + var x2 = boxList('hello'); // { value: string }[] + var flip = function (f) { return function (b, a) { return f(a, b); }; }; + var zip = function (x, y) { return [x, y]; }; + var flipped = flip(zip); // (b: U, a: T) => [T, U] + var t1 = flipped(10, 'hello'); // [string, number] + var t2 = flipped(true, 0); // [number, boolean] + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); }); - } -}`, - ); - }, - testHelloWorld: () => { - printBeforeAndAfter( - `const writeToLog = (message: string) => console.info(message); - -let words = ['hello', 'world']; -writeToLog(\`\${words.join(' ')}\`);`, - ); - }, - testDefaultParams: () => { - printBeforeAndAfter( - `function JsonResponseHandler(url: string, - query = {}, - params = {muteHttpExceptions: true}, - cacheName: string, cacheTime = 3600) { - // ... -}`, - ); - }, - testExportImportWorkaroundPart1: () => { - printBeforeAndAfter( - `export const ICONS = { - email: \`foo.png\`, -};`, - ); - }, - testExportImportWorkaroundPart2: () => { - printBeforeAndAfter( - `import { ICONS } from './package'; - -type _i_ICONS = typeof ICONS; -declare namespace exports { - const ICONS: _i_ICONS; -} -exports.ICONS.email; -`, - ); - }, - testExportImportNamespaceWorkaround: () => { - printBeforeAndAfter( - `namespace Package { - export function foo() {} -} + test('Improved Readonly', () => { + const typescript = ` + // Improved support for read-only arrays and tuples + function f1(mt: [number, number], rt: readonly [number, number]) { + mt[0] = 1; // Ok + // rt[0] = 1; // Error, read-only element + } -Package.foo(); - -const nameIWantForMyImports = Package.foo; -nameIWantForMyImports();`, - ); - }, - testTypeScript_34x_highOrder: () => { - printBeforeAndAfter( - `// Higher order function type inference -declare function pipe(ab: (...args: A) => B, bc: (b: B) => C): (...args: A) => C; - -declare function list(a: T): T[]; -declare function box(x: V): { value: V }; - -const listBox = pipe(list, box); // (a: T) => { value: T[] } -const boxList = pipe(box, list); // (x: V) => { value: V }[] - -const x1 = listBox(42); // { value: number[] } -const x2 = boxList('hello'); // { value: string }[] - -const flip = (f: (a: A, b: B) => C) => (b: B, a: A) => f(a, b); -const zip = (x: T, y: U): [T, U] => [x, y]; -const flipped = flip(zip); // (b: U, a: T) => [T, U] - -const t1 = flipped(10, 'hello'); // [string, number] -const t2 = flipped(true, 0); // [number, boolean]`, - ); - }, - testTypeScript_34x_improvedReadonly: () => { - printBeforeAndAfter( - `// Improved support for read-only arrays and tuples -function f1(mt: [number, number], rt: readonly [number, number]) { - mt[0] = 1; // Ok - // rt[0] = 1; // Error, read-only element -} + // next function declaration crashes with Typescript version < 3.4.4 + function f2(ma: string[], ra: readonly string[], mt: [string, string], rt: readonly [string, string]) { + // ma = ra; // Error + ma = mt; // Ok + // ma = rt; // Error + ra = ma; // Ok + ra = mt; // Ok + ra = rt; // Ok + // mt = ma; // Error + // mt = ra; // Error + // mt = rt; // Error + // rt = ma; // Error + // rt = ra; // Error + rt = mt; // Ok + } -// next function declaration crashes with Typescript version < 3.4.4 -function f2(ma: string[], ra: readonly string[], mt: [string, string], rt: readonly [string, string]) { - // ma = ra; // Error - ma = mt; // Ok - // ma = rt; // Error - ra = ma; // Ok - ra = mt; // Ok - ra = rt; // Ok - // mt = ma; // Error - // mt = ra; // Error - // mt = rt; // Error - // rt = ma; // Error - // rt = ra; // Error - rt = mt; // Ok -} + type ReadWrite = { -readonly [P in keyof T] : T[P] }; -type ReadWrite = { -readonly [P in keyof T] : T[P] }; - -type T0 = Readonly; // readonly string[] -type T1 = Readonly<[number, number]>; // readonly [number, number] -type T2 = Partial>; // readonly (string | undefined)[] -type T3 = Readonly>; // readonly (string | undefined)[] -type T4 = ReadWrite>; // string[]`, - ); - }, - testTypeScript_34x_constContext: () => { - printBeforeAndAfter( - `// Const contexts for literal expressions -let x = 10 as const; // Type 10 -let y = [10, 20]; // Type readonly [10, 20] -let z = { text: "hello" } as const; // Type { readonly text: "hello" }`, - ); - }, - testTypeScript_34x_globalThis: () => { - printBeforeAndAfter( - `// \`globalThis\` -// Add globalThis -// @Filename: one.ts -var a = 1; -var b = 2; -// @Filename: two.js -this.c = 3; -const total = globalThis.a + this.b + window.c + this.unknown;`, - ); - }, -}; - -// Run tests -console.log('## TESTS ##'); -console.log('---------------------------------------------------------------------------------'); -for (const testName of Object.keys(tests)) { - console.log(`# ${testName}`); - tests[testName](); - console.log('---------------------------------------------------------------------------------'); -} + type T0 = Readonly; // readonly string[] + type T1 = Readonly<[number, number]>; // readonly [number, number] + type T2 = Partial>; // readonly (string | undefined)[] + type T3 = Readonly>; // readonly (string | undefined)[] + type T4 = ReadWrite>; // string[] + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + // Improved support for read-only arrays and tuples + function f1(mt, rt) { + mt[0] = 1; // Ok + // rt[0] = 1; // Error, read-only element + } + // next function declaration crashes with Typescript version < 3.4.4 + function f2(ma, ra, mt, rt) { + // ma = ra; // Error + ma = mt; // Ok + // ma = rt; // Error + ra = ma; // Ok + ra = mt; // Ok + ra = rt; // Ok + // mt = ma; // Error + // mt = ra; // Error + // mt = rt; // Error + // rt = ma; // Error + // rt = ra; // Error + rt = mt; // Ok + } + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Const Context', () => { + const typescript = ` + // Const contexts for literal expressions + let x = 10 as const; // Type 10 + let y = [10, 20]; // Type readonly [10, 20] + let z = { text: "hello" } as const; // Type { readonly text: "hello" }`; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + // Const contexts for literal expressions + var x = 10; // Type 10 + var y = [10, 20]; // Type readonly [10, 20] + var z = { text: "hello" }; // Type { readonly text: "hello" } + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Global This', () => { + const typescript = ` + // \`globalThis\` + // Add globalThis + // @Filename: one.ts + var a = 1; + var b = 2; + // @Filename: two.js + this.c = 3; + const total = globalThis.a + this.b + window.c + this.unknown; + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + // \`globalThis\` + // Add globalThis + // @Filename: one.ts + var a = 1; + var b = 2; + // @Filename: two.js + this.c = 3; + var total = globalThis.a + this.b + window.c + this.unknown; + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + }); + + describe('Imports', () => { + test('Constant ./ Relative Import', () => { + const typescript = ` + import { prop } from './util'; + prop.x; + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var exports = exports || {}; + var module = module || { exports: exports }; + var util_1 = util_1 || { prop: prop }; //import { prop } from './util'; + prop.x; + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Function Absolute Import', () => { + const typescript = ` + import { func } from 'bob'; + func(); + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var exports = exports || {}; + var module = module || { exports: exports }; + var bob_1 = bob_1 || { func: func }; //import { func } from 'bob'; + (0, bob_1.func)(); + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Function ./ Relative Import', () => { + const typescript = ` + import { func } from './util'; + func(); + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var exports = exports || {}; + var module = module || { exports: exports }; + var util_1 = util_1 || { func: func }; //import { func } from './util'; + (0, util_1.func)(); + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Function ../ Relative Import', () => { + const typescript = ` + import { func } from '../util'; + func(); + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var exports = exports || {}; + var module = module || { exports: exports }; + var util_1 = util_1 || { func: func }; //import { func } from '../util'; + (0, util_1.func)(); + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Function ../../ Relative Import', () => { + const typescript = ` + import { func } from '../../util'; + func(); + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var exports = exports || {}; + var module = module || { exports: exports }; + var util_1 = util_1 || { func: func }; //import { func } from '../../util'; + (0, util_1.func)(); + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Function ../src/ Relative Import', () => { + const typescript = ` + import { func } from '../src/util'; + func(); + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var exports = exports || {}; + var module = module || { exports: exports }; + var util_1 = util_1 || { func: func }; //import { func } from '../src/util'; + (0, util_1.func)(); + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Default Constant Import', () => { + const typescript = ` + import prop from './util'; + prop.x; + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var exports = exports || {}; + var module = module || { exports: exports }; + var util_1 = util_1 || { "default": prop }; //import prop from './util'; + prop.x; + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Default Function Import', () => { + const typescript = ` + import func from './util'; + func(); + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var exports = exports || {}; + var module = module || { exports: exports }; + var util_1 = util_1 || { "default": func }; //import func from './util'; + (0, util_1["default"])(); + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Default And Named Import', () => { + const typescript = ` + import func, { bob } from './util'; + func(); + bob(); + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var exports = exports || {}; + var module = module || { exports: exports }; + var util_1 = util_1 || { "default": func, bob: bob }; //import func, { bob } from './util'; + (0, util_1["default"])(); + (0, util_1.bob)(); + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Aliased Named Import', () => { + const typescript = ` + import { bob as bobette } from './util'; + bobette(); + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var exports = exports || {}; + var module = module || { exports: exports }; + var util_1 = util_1 || { bob: bob }; //import { bob as bobette } from './util'; + (0, util_1.bob)(); + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + + test('Namespace Import', () => { + const typescript = ` + import * as bob from './util'; + bob.add(); + `; + const gas = ` + // Compiled using ${packageJson.name} ${packageJson.version} (TypeScript ${version}) + var exports = exports || {}; + var module = module || { exports: exports }; + var bob = bob || exports; //import * as bob from './util'; + bob.add(); + `; + expect(ts2gas(trimWhitespace(typescript)).trim()).toBe(trimWhitespace(gas)); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index f4dfcb4..21f535b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,9 @@ "suppressExcessPropertyErrors": true, "suppressImplicitAnyIndexErrors": true, "target": "ES2019", - "lib": ["ESNext"], + "lib": [ + "ESNext" + ], "newLine": "lf", "noEmitOnError": true, "sourceMap": true, @@ -28,7 +30,8 @@ "stripInternal": true, "experimentalDecorators": true, "pretty": true, - "importHelpers": true // canary: will cause runtime error if `tslib` is necessary + "importHelpers": true, // canary: will cause runtime error if `tslib` is necessary + "outDir": "dist" }, "files": [ "src/index.ts",