Skip to content

Commit 40c125d

Browse files
committed
some progress
1 parent 411ea45 commit 40c125d

File tree

6 files changed

+250
-65
lines changed

6 files changed

+250
-65
lines changed

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"pull": "rm -rf three.js && git clone git@github.com:mrdoob/three.js.git && cp -r three.js/src three.js/src.original"
1111
},
1212
"devDependencies": {
13+
"ansi-colors": "^2.0.5",
1314
"rollup": "^0.63.5",
1415
"rollup-plugin-commonjs": "^9.1.4",
1516
"rollup-plugin-node-resolve": "^3.3.0",
@@ -20,6 +21,7 @@
2021
"acorn": "^5.7.1",
2122
"estree-walker": "^0.5.2",
2223
"magic-string": "^0.25.0",
24+
"source-map-support": "^0.5.6",
2325
"tiny-glob": "^0.2.2"
2426
}
2527
}

rollup.config.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ export default {
66
input: 'src/index.ts',
77
output: {
88
file: 'dist/index.js',
9-
format: 'cjs'
9+
format: 'cjs',
10+
sourcemap: true
1011
},
1112
plugins: [
1213
resolve(),

src/Module.ts

+109-60
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@ import MagicString from "../node_modules/magic-string";
22
import * as assert from 'assert';
33
import * as fs from "fs";
44
import * as acorn from 'acorn';
5+
import { createMatcher } from "./match";
6+
7+
const matchers = {
8+
assign: createMatcher('Object.assign(_, _ObjectExpression_)', m => ({
9+
target: m[0],
10+
source: m[1]
11+
})),
12+
13+
subclass: createMatcher('_ = Object.assign(Object.create(_), _ObjectExpression_)', m => ({
14+
target: m[0],
15+
superclass: m[1],
16+
source: m[2]
17+
}))
18+
};
519

620
export default class Module {
721
file: string;
@@ -10,8 +24,11 @@ export default class Module {
1024
ast: any;
1125

1226
superclasses: Map<string, string>;
13-
methodBlocks: Map<string, string>;
27+
1428
constructors: Map<string, string>;
29+
methods: Map<string, string[]>;
30+
staticMethods: Map<string, string[]>;
31+
properties: Map<string, Array<{ key: string, value: string }>>;
1532

1633
constructor(file: string) {
1734
this.file = file;
@@ -24,57 +41,106 @@ export default class Module {
2441
this.code = new MagicString(this.source);
2542

2643
this.superclasses = new Map();
27-
this.methodBlocks = new Map();
44+
this.methods = new Map();
45+
this.staticMethods = new Map();
2846
this.constructors = new Map();
47+
this.properties = new Map();
2948

3049
this.convert();
3150
}
3251

3352
findSuperclasses() {
34-
const { superclasses } = this;
53+
this.ast.body.forEach(node => {
54+
// AnimationMixer.prototype = Object.assign( Object.create( EventDispatcher.prototype ), {
55+
const nodes = matchers.subclass(node);
56+
if (!nodes) return;
57+
58+
const { target, superclass, source } = nodes;
59+
60+
const match = /^(\w+)(\.prototype)?$/.exec(this.snip(target));
61+
assert.ok(!!match[2]);
62+
63+
const name = match[1];
64+
65+
if (name[0].toUpperCase() !== name[0]) return; // not a class
66+
67+
this.superclasses.set(name, superclass.name);
68+
});
3569
}
3670

3771
findAndConvertMethods() {
3872
this.ast.body.forEach(node => {
39-
if (node.type !== 'ExpressionStatement') return;
40-
if (node.expression.type !== 'CallExpression') return;
73+
const nodes = matchers.assign(node) || matchers.subclass(node);
74+
if (!nodes) return;
4175

42-
// check if this is an Object.assign( SomeClass.prototype, { methods })
43-
const callee = this.snip(node.expression.callee);
44-
if (callee !== 'Object.assign') return;
76+
const { target, source } = nodes;
4577

46-
assert.equal(node.expression.arguments.length, 2);
78+
const match = /^(\w+)(\.prototype)?$/.exec(this.snip(target));
4779

48-
const [targetNode, methodsNode] = node.expression.arguments;
80+
const name = match[1];
81+
const isStatic = !match[2];
4982

50-
const target = this.snip(targetNode);
51-
const match = /^(\w+)\.prototype$/.exec(target);
83+
let c: number = source.start + 1;
5284

53-
assert.ok(match, target);
85+
// remove constructor
86+
const index = source.properties.findIndex(prop => prop.key.name === 'constructor');
87+
assert.ok(index <= 0);
88+
if (index === 0) {
89+
assert.ok(source.properties.length > 1);
90+
const prop = source.properties.shift();
5491

55-
const name = match[1];
92+
this.code.remove(prop.start, source.properties[1].start);
93+
c = prop.end + 1;
94+
}
95+
96+
while (/\s/.test(this.source[c])) c += 1;
5697

5798
// check we're actually assigning methods
58-
assert.ok(methodsNode.properties.every(node => node.value.type === 'FunctionExpression'));
59-
60-
methodsNode.properties.forEach((prop, i) => {
61-
// `foo: function()` -> `foo()`
62-
let c = prop.key.end;
63-
while (this.source[c] !== '(') c += 1;
64-
this.code.overwrite(prop.key.end, c, ' ');
65-
66-
if (i < methodsNode.properties.length - 1) {
67-
// remove comma
68-
assert.equal(this.source[prop.end], ',');
69-
this.code.remove(prop.end, prop.end + 1);
99+
100+
source.properties.forEach(prop => {
101+
if (prop.value.type === 'FunctionExpression') {
102+
// `foo: function()` -> `foo()`
103+
let argsStart = prop.key.end;
104+
while (this.source[argsStart] !== '(') argsStart += 1;
105+
this.code.overwrite(prop.key.end, argsStart, ' ');
106+
107+
if (isStatic) {
108+
this.code.overwrite(prop.key.start, prop.key.end, `static ${prop.key.name}`);
109+
110+
if (!this.staticMethods.get(name)) {
111+
this.staticMethods.set(name, []);
112+
}
113+
114+
this.staticMethods.get(name).push(this.code.slice(c, prop.end));
115+
} else {
116+
if (!this.methods.get(name)) {
117+
this.methods.set(name, []);
118+
}
119+
120+
this.methods.get(name).push(this.code.slice(c, prop.end));
121+
}
70122
}
71-
});
72123

73-
const methodBlock = `\t${this.code.slice(methodsNode.start + 1, methodsNode.end - 1).trim()}`;
124+
else {
125+
if (!this.properties.has(name)) {
126+
this.properties.set(name, []);
127+
}
128+
129+
this.properties.get(name).push({
130+
key: prop.key.name,
131+
value: this.source.slice(prop.value.start, prop.value.end)
132+
});
133+
134+
this.code.remove(c, prop.end);
135+
}
74136

75-
this.methodBlocks.set(name, methodBlock);
137+
c = prop.end;
138+
while (c < this.source.length && this.source[c] !== ',') c += 1;
139+
c += 1;
140+
while (c < this.source.length && /\s/.test(this.source[c])) c += 1;
141+
});
76142

77-
let c = node.start;
143+
c = node.start;
78144
while (/\s/.test(this.source[c - 1])) c -= 1;
79145

80146
this.code.remove(c, node.end);
@@ -92,8 +158,6 @@ export default class Module {
92158

93159
if (name[0].toUpperCase() !== name[0]) return; // not a class
94160

95-
console.log('converting constructor', { name });
96-
97161
const superclass = this.superclasses.get(name);
98162

99163
const declaration = superclass
@@ -105,42 +169,27 @@ export default class Module {
105169
.replace(/^/gm, '\t')
106170
.slice(1);
107171

108-
const methodBlock = this.methodBlocks.get(name);
172+
const methods = this.methods.get(name) || [];
173+
const staticMethods = this.staticMethods.get(name) || [];
174+
175+
const combined = [...methods, ...staticMethods]
176+
.filter(Boolean)
177+
.join('\n\n\t');
178+
179+
const properties = (this.properties.get(name) || []).map(prop => {
180+
return `\n\n${name}.prototype.${prop.key} = ${prop.value.replace(/^\t/gm, '')};`;
181+
});
109182

110-
this.code.overwrite(node.start, node.end, `${declaration} {\n\tconstructor ${constructorArgsAndBody}\n\n${methodBlock}\n}`);
183+
const body = `{\n\tconstructor ${constructorArgsAndBody}\n\n\t${combined}\n}`;
184+
185+
this.code.overwrite(node.start, node.end, `${declaration} ${body}${properties.join('')}`);
111186
});
112187
}
113188

114189
convert() {
115190
this.findSuperclasses();
116191
this.findAndConvertMethods();
117192
this.findAndConvertConstructors();
118-
119-
// function handleDeclaration(node) {
120-
// const { name } = node.id;
121-
// const superclass = superclasses.get(name);
122-
123-
// const declaration = superclass
124-
// ? `class ${name} extends ${superclass}`
125-
// : `class ${name}`;
126-
127-
// console.log(node.start, node.id.end);
128-
129-
// code.overwrite(node.start, node.id.end, `${declaration} {\n\tconstructor`);
130-
131-
132-
// }
133-
134-
// ast.body.forEach(statement => {
135-
// if (statement.type === 'FunctionDeclaration') {
136-
// handleDeclaration(statement);
137-
// }
138-
139-
// if (statement.type === 'ExportNamedDeclaration') {
140-
// // TODO handle export named functions
141-
// console.log(statement);
142-
// }
143-
// });
144193
}
145194

146195
snip({ start, end }) {

src/index.ts

+47-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,57 @@
11
import * as fs from 'fs';
22
import glob from 'tiny-glob/sync';
3+
import * as acorn from 'acorn';
4+
import c from 'ansi-colors';
35
import Module from './Module';
46

7+
import sms from 'source-map-support';
8+
sms.install();
9+
510
const SRC = 'three.js/src.original';
611
const DEST = 'three.js/src';
712

8-
const files = glob('**/*.js', { cwd: SRC })
9-
.filter((file: string) => /AnimationAction/.test(file));
13+
function createRegex(filter: string) {
14+
filter = filter
15+
// .replace(/\//g, '\\/')
16+
.replace(/\*\*/g, '.+')
17+
.replace(/\*/g, '[^/]+');
18+
19+
return new RegExp(`^${filter}$`);
20+
}
21+
22+
const regex = process.argv[2]
23+
? createRegex(process.argv[2])
24+
: /./;
25+
26+
const files = glob('**/*.js', { cwd: SRC }).filter(file => regex.test(file));
27+
28+
function isValid(str: string) {
29+
try {
30+
acorn.parse(str, {
31+
ecmaVersion: 9,
32+
sourceType: 'module'
33+
});
34+
return true;
35+
} catch (err) {
36+
return false;
37+
}
38+
}
1039

1140
files.forEach((file: string) => {
12-
const mod = new Module(`${SRC}/${file}`);
13-
fs.writeFileSync(`${DEST}/${file}`, mod.toString());
41+
try {
42+
const mod = new Module(`${SRC}/${file}`);
43+
const output = mod.toString();
44+
45+
fs.writeFileSync(`${DEST}/${file}`, output);
46+
47+
if (isValid(output)) {
48+
console.log(`${c.bold.green('✔')} ${file}`);
49+
} else {
50+
console.log(c.bold.red(`! ${file}`));
51+
console.log(`Generated invalid code`);
52+
}
53+
} catch (err) {
54+
console.log(c.bold.red(`! ${file}`));
55+
console.log(err.stack);
56+
}
1457
});

src/match.ts

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import * as acorn from 'acorn';
2+
3+
function tryParse(str: string) {
4+
try {
5+
return acorn.parseExpressionAt(str, 0);
6+
} catch (err) {
7+
return null;
8+
}
9+
}
10+
11+
export function createMatcher(template: string, fn: (matches: any[]) => Record<string, any>) {
12+
const base = tryParse(template);
13+
14+
if (!base) {
15+
throw new Error(`Could not parse ${template}`);
16+
}
17+
18+
function match(a: any, b: any, matches: any[]) {
19+
if (a.type === 'Identifier') {
20+
if (a.name === '_') {
21+
matches.push(b);
22+
return true;
23+
}
24+
25+
const match = /^_(\w+)_$/.exec(a.name);
26+
if (match) {
27+
if (b.type !== match[1]) return false;
28+
29+
matches.push(b);
30+
return true;
31+
}
32+
}
33+
34+
const { type } = a;
35+
if (type !== b.type) return false;
36+
37+
if (type === 'Literal') return a.value === b.value;
38+
if (type === 'Identifier') return a.name === b.name;
39+
40+
const keys = Object.keys(a).filter(key => {
41+
return key !== 'start' && key !== 'end';
42+
});
43+
44+
const childKeys = keys.filter(key => typeof a[key] === 'object');
45+
const otherKeys = keys.filter(key => typeof a[key] !== 'object');
46+
47+
for (const key of otherKeys) {
48+
if (a[key] !== b[key]) return false;
49+
}
50+
51+
for (const key of childKeys) {
52+
if (!a[key] !== !b[key]) return false;
53+
}
54+
55+
childKeys.sort((p, q) => a[p].start - a[q].start);
56+
57+
for (const key of childKeys) {
58+
if (!match(a[key], b[key], matches)) return false;
59+
}
60+
61+
return true;
62+
}
63+
64+
return function(node: any) {
65+
const matches: any[] = [];
66+
67+
if (node.type === 'ExpressionStatement') node = node.expression;
68+
69+
return match(base, node, matches) ? fn(matches) : null;
70+
}
71+
}

0 commit comments

Comments
 (0)