From d78c66dad65e75f325893381cd71a1fa03784274 Mon Sep 17 00:00:00 2001 From: harttle Date: Mon, 16 Sep 2019 14:55:09 +0800 Subject: [PATCH] feat: mark san components, write to js and run --- .babelrc.js | 1 + .eslintrc.json | 1 + package-lock.json | 158 ++++++++++++++++++++++++ package.json | 1 + src/loaders/tsc.ts | 12 -- src/transpilers/ast-util.ts | 13 ++ src/transpilers/component-parser.ts | 59 +++++++++ src/transpilers/metadata-transformer.ts | 10 ++ src/transpilers/ts2js.ts | 53 ++++++-- src/transpilers/ts2php.ts | 25 +--- test/stub/obj.ts | 2 +- test/tsconfig.json | 1 + test/unit/ts2js.spec.ts | 44 +++++-- test/unit/tsc-loader.spec.ts | 20 --- 14 files changed, 325 insertions(+), 75 deletions(-) delete mode 100644 src/loaders/tsc.ts create mode 100644 src/transpilers/ast-util.ts create mode 100644 src/transpilers/component-parser.ts create mode 100644 src/transpilers/metadata-transformer.ts delete mode 100644 test/unit/tsc-loader.spec.ts diff --git a/.babelrc.js b/.babelrc.js index e0c0fd17..c8dde5b0 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -3,4 +3,5 @@ module.exports = { '@babel/preset-env', '@babel/preset-typescript' ], + "plugins": ["transform-class-properties"] }; \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index cde87212..dc18cb2d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -27,6 +27,7 @@ "@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/interface-name-prefix": "off", "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-unused-vars": "error", "no-unused-vars": "off", "import/export": "off" } diff --git a/package-lock.json b/package-lock.json index 4b4f1c52..fd4c27e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1863,6 +1863,73 @@ "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", "dev": true }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "babel-helper-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", + "dev": true, + "requires": { + "babel-helper-get-function-arity": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, "babel-jest": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-24.9.0.tgz", @@ -1886,6 +1953,15 @@ } } }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, "babel-plugin-dynamic-import-node": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz", @@ -1967,6 +2043,24 @@ "@types/babel__traverse": "^7.0.6" } }, + "babel-plugin-syntax-class-properties": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz", + "integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=", + "dev": true + }, + "babel-plugin-transform-class-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz", + "integrity": "sha1-anl2PqYdM9NvN7YRqp3vgagbRqw=", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-plugin-syntax-class-properties": "^6.8.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, "babel-preset-jest": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-24.9.0.tgz", @@ -1995,6 +2089,70 @@ } } }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + }, + "dependencies": { + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + } + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + }, + "dependencies": { + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + } + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true + }, "backo2": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", diff --git a/package.json b/package.json index 35eb442b..427601db 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@typescript-eslint/eslint-plugin": "^2.0.0", "@typescript-eslint/parser": "^2.0.0", "babel-jest": "^24.9.0", + "babel-plugin-transform-class-properties": "^6.24.1", "babel-preset-jest": "^24.9.0", "eslint": "^5.12.1", "eslint-config-standard": "^12.0.0", diff --git a/src/loaders/tsc.ts b/src/loaders/tsc.ts deleted file mode 100644 index a5171cba..00000000 --- a/src/loaders/tsc.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* eslint-disable node/no-deprecated-api */ - -const originJsLoader = require.extensions['.ts'] -const tsNode = require('ts-node') - -export function apply () { - tsNode.register() -} - -export function restore () { - require.extensions['.ts'] = originJsLoader -} diff --git a/src/transpilers/ast-util.ts b/src/transpilers/ast-util.ts new file mode 100644 index 00000000..45015a1a --- /dev/null +++ b/src/transpilers/ast-util.ts @@ -0,0 +1,13 @@ +export function getComponentClassIdentifier (sourceFile): string | undefined { + const declaration = sourceFile.getImportDeclaration( + node => node.getModuleSpecifierValue() === 'san' + ) + if (!declaration) return + const namedImports = declaration.getNamedImports() + for (const namedImport of namedImports) { + const propertyName = namedImport.getText() + if (propertyName === 'Component') { + return namedImport.getText() + } + } +} diff --git a/src/transpilers/component-parser.ts b/src/transpilers/component-parser.ts new file mode 100644 index 00000000..6de70df9 --- /dev/null +++ b/src/transpilers/component-parser.ts @@ -0,0 +1,59 @@ +import { getComponentClassIdentifier } from './ast-util' +import { Project, ts, SourceFile } from 'ts-morph' +import { getDefaultConfigPath } from './tsconfig' + +export class ComponentParser { + private componentFile: string + private root: string + private tsconfigPath: string + private project: Project + private idPropertyName: string + private id: number = 0 + + constructor ( + componentFile: string, + tsconfigPath = getDefaultConfigPath(), + idPropertyName = 'spsrId' + ) { + this.idPropertyName = idPropertyName + this.componentFile = componentFile + this.tsconfigPath = tsconfigPath + this.project = new Project({ + tsConfigFilePath: tsconfigPath + }) + } + + parseComponent () { + const files = this.getComponentFiles() + for (const file of files.values()) { + const componentClassIdentifier = getComponentClassIdentifier(file) + if (!componentClassIdentifier) continue + this.markComponents(file, componentClassIdentifier) + } + return files + } + + private getComponentFiles () { + return new Map([[ + this.componentFile, + this.project.getSourceFile(this.componentFile) + ]]) + } + + private markComponents (sourceFile: SourceFile, componentClassIdentifier: string) { + for (const clazz of sourceFile.getClasses()) { + const extendClause = clazz.getHeritageClauseByKind(ts.SyntaxKind.ExtendsKeyword) + if (!extendClause) return + + const typeNode = extendClause.getTypeNodes().find(x => x.getText() === componentClassIdentifier) + if (!typeNode) return + + clazz.addProperty({ + isStatic: true, + name: this.idPropertyName, + type: 'number', + initializer: '' + (this.id++) + }) + } + } +} diff --git a/src/transpilers/metadata-transformer.ts b/src/transpilers/metadata-transformer.ts new file mode 100644 index 00000000..0e1486ba --- /dev/null +++ b/src/transpilers/metadata-transformer.ts @@ -0,0 +1,10 @@ +import * as ts from 'typescript' +import * as path from 'path' + +export function transformer (): ts.TransformerFactory { + return (context: ts.TransformationContext) => (file: ts.SourceFile) => addMetadata(file, context) +} + +function addMetadata (node: ts.SourceFile, context: ts.TransformationContext) { + return node +} diff --git a/src/transpilers/ts2js.ts b/src/transpilers/ts2js.ts index 2b7178e3..93f1f385 100644 --- a/src/transpilers/ts2js.ts +++ b/src/transpilers/ts2js.ts @@ -1,15 +1,42 @@ -import { resolve } from 'path' -import { transpileModule, convertCompilerOptionsFromJson, TranspileOptions } from 'typescript' - -const tsConfigPath = resolve(__dirname, '../test/cases') -const compilerOptions = convertCompilerOptionsFromJson({}, tsConfigPath).options - -export function ts2js (source) { - const { diagnostics, outputText } = - transpileModule(source, { compilerOptions }) - if (diagnostics.length) { - console.log(diagnostics) - throw new Error('typescript compile error') +import { transpileModule } from 'typescript' +import { Project, SourceFile } from 'ts-morph' +import { getDefaultConfigPath } from './tsconfig' +import { sep } from 'path' + +export class Compiler { + private root: string + private tsconfig: object + private project: Project + + constructor ( + tsconfigPath = getDefaultConfigPath(), + root = tsconfigPath.split(sep).slice(0, -1).join(sep) + ) { + this.root = root + this.tsconfig = require(tsconfigPath) + this.project = new Project({ + tsConfigFilePath: tsconfigPath + }) + } + + compileAndRun (source: SourceFile) { + const js = this.compileToJS(source) + const fn = new Function('module', 'exports', 'require', js) // eslint-disable-line + const module = { + exports: {} + } + fn(module, module.exports, require) + return module.exports + } + + compileToJS (source: SourceFile) { + const compilerOptions = this.tsconfig['compilerOptions'] + const { diagnostics, outputText } = + transpileModule(source.getFullText(), { compilerOptions }) + if (diagnostics.length) { + console.log(diagnostics) + throw new Error('typescript compile error') + } + return outputText } - return outputText } diff --git a/src/transpilers/ts2php.ts b/src/transpilers/ts2php.ts index df0fca32..f3dac12c 100644 --- a/src/transpilers/ts2php.ts +++ b/src/transpilers/ts2php.ts @@ -1,4 +1,5 @@ import { Project, ts, SourceFile, TypeGuards, VariableDeclarationKind } from 'ts-morph' +import { getComponentClassIdentifier } from './ast-util' import { compile } from 'ts2php' import { getDefaultConfigPath } from './tsconfig' import { sep, extname } from 'path' @@ -21,7 +22,7 @@ export class Compiler { }) } - namespace (file) { + ns (file) { return file .slice(this.root.length, -extname(file).length) .split(sep).join('\\') @@ -32,7 +33,7 @@ export class Compiler { concat (files) { let code = '' for (const [file, content] of files) { - code += `namespace ${this.namespace(file)} {\n` + code += `namespace ${this.ns(file)} {\n` code += content code += `}` } @@ -53,7 +54,7 @@ export class Compiler { compileFile (filePath) { const file = this.project.getSourceFile(filePath) - const componentClassIdentifier = getAndRemoveComponentClassIdentifier(file) + const componentClassIdentifier = getComponentClassIdentifier(file) normalizeComponentClass(file, componentClassIdentifier) const code = this.compileToPHP(file.getFullText()) @@ -87,24 +88,6 @@ export class Compiler { } } -function getAndRemoveComponentClassIdentifier (sourceFile) { - const declaration = sourceFile.getImportDeclaration( - node => node.getModuleSpecifierValue() === 'san' - ) - const namedImports = declaration.getNamedImports() - // const defaul = declaration.getNam - for (const namedImport of namedImports) { - const propertyName = namedImport.getText() - if (propertyName === 'Component') { - const identifier = namedImport.getText() - - // declaration. - - return identifier - } - } -} - function normalizeComponentClass (sourceFile: SourceFile, componentClassIdentifier) { for (const clazz of sourceFile.getClasses()) { const extendClause = clazz.getHeritageClauseByKind(ts.SyntaxKind.ExtendsKeyword) diff --git a/test/stub/obj.ts b/test/stub/obj.ts index 32cf5de2..6847ef87 100644 --- a/test/stub/obj.ts +++ b/test/stub/obj.ts @@ -1,3 +1,3 @@ export default class Foo { - public static foo: 'FOO' + public static foo: string = 'FOO' } diff --git a/test/tsconfig.json b/test/tsconfig.json index f6d50bdb..a2a22bfe 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "target": "es6", + "module": "CommonJS", "outDir": "dist", "noImplicitAny": false, "allowSyntheticDefaultImports": true, diff --git a/test/unit/ts2js.spec.ts b/test/unit/ts2js.spec.ts index b5462401..0918b71c 100644 --- a/test/unit/ts2js.spec.ts +++ b/test/unit/ts2js.spec.ts @@ -1,16 +1,44 @@ -import { ts2js } from '../../src/transpilers/ts2js' +import { Compiler } from '../../src/transpilers/ts2js' +import { ComponentParser } from '../../src/transpilers/component-parser' +import { resolve } from 'path' describe('ts2js', function () { - it('should compile types to nothing', function () { - const src = 'let foo: string = "foobar"' + const tsconfig = resolve(__dirname, '../tsconfig.json') - expect(ts2js(src)).toEqual('var foo = "foobar";\n') + it('should a single class', function () { + const path = resolve(__dirname, '../stub/obj.ts') + const parser = new ComponentParser(path, tsconfig) + + const file = parser.parseComponent().get(path) + const cc = new Compiler(tsconfig) + const result = cc.compileToJS(file) + + expect(result).toContain('class Foo {') + expect(result).toContain('Foo.foo = \'FOO\'') }) - it('should compile import to require', function () { - const src = 'import bar from "./bar"\nbar()' + it('should mark component class with cid', function () { + const path = resolve(__dirname, '../stub/a.comp.ts') + const parser = new ComponentParser(path, tsconfig) + + const file = parser.parseComponent().get(path) + const cc = new Compiler(tsconfig) + const result = cc.compileToJS(file) + + expect(result).toContain('class A extends') + expect(result).toContain('A.template = \'A\'') + expect(result).toContain('A.spsrId = 0') + }) + + it('should compile and run a component', function () { + const path = resolve(__dirname, '../stub/a.comp.ts') + const parser = new ComponentParser(path, tsconfig) + + const file = parser.parseComponent().get(path) + const cc = new Compiler(tsconfig) + const componentClass = cc.compileAndRun(file)['default'] - expect(ts2js(src)).toContain('var bar_1 = require("./bar");') - expect(ts2js(src)).toContain('bar_1["default"]();') + expect(componentClass.template).toEqual('A') + expect(componentClass.spsrId).toEqual(0) }) }) diff --git a/test/unit/tsc-loader.spec.ts b/test/unit/tsc-loader.spec.ts deleted file mode 100644 index b5bc0591..00000000 --- a/test/unit/tsc-loader.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { apply, restore } from '../../src/loaders/tsc' - -describe('tsc loader', function () { - const filename = require.resolve('../stub/obj.ts') - - beforeEach(() => { - apply() - delete require.cache[filename] - }) - - afterEach(restore) - - it('should load simple class', function () { - const fn = require('../stub/obj.ts') - - expect(fn.default.toString()).toEqual( - 'class Foo {\n}' - ) - }) -})