Skip to content

Commit 790a181

Browse files
[bugfix] More careful treatment of export declarations.
Also, cache results of visiting, towards faster incremental builds.
1 parent 9a79e2f commit 790a181

File tree

3 files changed

+149
-26
lines changed

3 files changed

+149
-26
lines changed

src/build/bundle.ts

Lines changed: 82 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import acornGlobals from 'acorn-globals';
1010
import { ModuleRef, SourceFile, PackageDir, TransientCode, NodeModule, ShimModule,
1111
StubModule, ModuleDependency, FileNotFound } from './modules';
1212
import { Transpiler } from './transpile';
13+
import { BuildCache } from './cache';
1314
import { Report, ReportSilent } from './ui/report';
1415

1516

@@ -88,6 +89,7 @@ class SearchPath {
8889
class Environment {
8990
infra: Library[] = []
9091
compilers: Transpiler[] = []
92+
cache: BuildCache = new BuildCache
9193
report: Report = new ReportSilent
9294
}
9395

@@ -152,7 +154,7 @@ class AcornCrawl extends InEnvironment {
152154
while (wl.length > 0) {
153155
var u = wl.pop().normalize(), key = u.id;
154156
if (!vs.has(key)) {
155-
var r = this.visitModuleRef(u);
157+
var r = this.memo(u, u => this.visitModuleRef(u));
156158
wl.push(...r.deps.map(d => d.target));
157159
vs.set(key, r);
158160
}
@@ -165,6 +167,10 @@ class AcornCrawl extends InEnvironment {
165167
return vs.get(key) || this.visitModuleRef(ref);
166168
}
167169

170+
memo(m: ModuleRef, op: (m: ModuleRef) => VisitResult) {
171+
return this.env.cache.memo(m, 'visit', op);
172+
}
173+
168174
visitFile(ref: SourceFile, opts: VisitOptions = {}): VisitResult {
169175
var fn = ref.filename;
170176
if (fn.endsWith('.js')) return this.visitJSFile(ref, opts);
@@ -220,7 +226,8 @@ class AcornCrawl extends InEnvironment {
220226

221227
m.extractImports();
222228
var deps = m.imports.map(u => mkdep(u, lookup(u.source.value)))
223-
.concat(m.requires.map(u => mkdep(u, lookup(u.arguments[0].value))));
229+
.concat(m.requires.map(u => mkdep(u, lookup(u.arguments[0].value))))
230+
.concat(m.exportsFrom.map(u => mkdep(u, lookup(u.source.value))));
224231
return {compiled: m, deps};
225232
}
226233

@@ -496,6 +503,11 @@ class AcornJSModule extends InEnvironment implements CompilationUnit {
496503
return s;
497504
}
498505

506+
get exportsFrom() {
507+
return this.exports.filter(u => this.isExportFrom(u)) as
508+
AcornTypes.ExportNamedDeclaration[];
509+
}
510+
499511
isImport(node: acorn.Node): node is AcornTypes.ImportDeclaration {
500512
return AcornUtils.is(node, 'ImportDeclaration');
501513
}
@@ -508,7 +520,17 @@ class AcornJSModule extends InEnvironment implements CompilationUnit {
508520
}
509521

510522
isExport(node: acorn.Node): node is AcornTypes.ExportDeclaration {
511-
return AcornUtils.is(node, 'ExportNamedDeclaration') || AcornUtils.is(node, 'ExportDefaultDeclaration');
523+
return AcornUtils.is(node, 'ExportNamedDeclaration') ||
524+
AcornUtils.is(node, 'ExportDefaultDeclaration');
525+
}
526+
527+
isExportFrom(node: acorn.Node): node is AcornTypes.ExportNamedDeclaration {
528+
return AcornUtils.is(node, 'ExportNamedDeclaration') && !!node.source;
529+
}
530+
531+
_isShorthandProperty(node: AcornTypes.Identifier) {
532+
return node.parents.slice(-2).some(p =>
533+
AcornUtils.is(p, "Property") && p.shorthand);
512534
}
513535

514536
process(key: string, deps: ModuleDependency<acorn.Node>[]) {
@@ -520,9 +542,12 @@ class AcornJSModule extends InEnvironment implements CompilationUnit {
520542
}),
521543
requires = this.requires.map(req => {
522544
var dep = deps.find(d => d.source === req);
523-
return dep ? [this.processRequire(req, dep.target)] : [];
545+
return dep ? this.processRequire(req, dep.target) : [];
546+
}),
547+
exports = this.exports.map(exp => {
548+
var dep = deps.find(d => d.source === exp); // for `export .. from`
549+
return this.processExport(exp, dep && dep.target);
524550
}),
525-
exports = this.exports.map(exp => [this.processExport(exp)]),
526551

527552
prog = TextSource.interpolate(this.text,
528553
[].concat(...imports, ...requires, ...exports)
@@ -543,39 +568,55 @@ class AcornJSModule extends InEnvironment implements CompilationUnit {
543568
isDefault = true;
544569
}
545570
else {
546-
var locals = [];
547571
lhs = this._freshVar();
548572
for (let impspec of imp.specifiers) {
549573
assert(impspec.type === 'ImportSpecifier');
550574
let local = impspec.local.name, imported = impspec.imported.name;
551-
locals.push((local == imported) ? local : `${imported}:${local}`);
552-
for (let refnode of this.vars.globals.get(local) || []) {
553-
refs.push([refnode, `${lhs}.${imported}`]);
554-
}
575+
refs.push(...this.updateReferences(local, `${lhs}.${imported}`));
555576
}
556577
}
557-
return [[imp, `let ${lhs} = ${this.makeRequire(ref, isDefault)};`]].concat(refs);
578+
return [[imp, `let ${lhs} = ${this.makeRequire(ref, isDefault)};`]]
579+
.concat(refs);
558580
}
559581

560582
processRequire(req: RequireInvocation, ref: ModuleRef) {
561-
return [req, this.makeRequire(ref)];
583+
return [[req, this.makeRequire(ref)]];
562584
}
563585

564-
processExport(exp: AcornTypes.ExportDeclaration) {
565-
var locals = [], rhs: string, is = AcornUtils.is;
586+
processExport(exp: AcornTypes.ExportDeclaration, ref: ModuleRef) {
587+
var locals = [], rhs: string, is = AcornUtils.is, d = exp.declaration;
588+
566589
if (is(exp, 'ExportNamedDeclaration')) {
567-
for (let expspec of exp.specifiers) {
568-
assert(expspec.type === 'ExportSpecifier');
569-
let local = expspec.local.name, exported = expspec.exported.name;
570-
locals.push((local == exported) ? local : `${local}:${exported}`);
590+
if (d) { // <- `export const` etc.
591+
locals = (<any>d).declarations.map(u => u.id.name); /** @todo typing & test other cases */
592+
}
593+
else {
594+
for (let expspec of exp.specifiers) {
595+
assert(expspec.type === 'ExportSpecifier');
596+
let local = expspec.local.name, exported = expspec.exported.name;
597+
locals.push((local == exported) ? local : `${local}:${exported}`);
598+
}
571599
}
572-
rhs = `{${locals}}`;
600+
601+
rhs = (exp.source && ref) // <- `export .. from`
602+
? `${this.makeRequire(ref)}, ${JSON.stringify(locals)}`
603+
: `{${locals}}`;
604+
605+
if (d)
606+
return [[{start: exp.start, end: d.start}, ''], // <- remove `export` modifier
607+
[{start: exp.end, end:exp.end}, `\nkremlin.export(module, ${rhs});`]];
608+
else
609+
return [[exp, `kremlin.export(module, ${rhs});`]];
573610
}
574611
else if (is(exp, 'ExportDefaultDeclaration')) {
575-
rhs = `{default:${this.text.substring(exp.declaration.start, exp.declaration.end)}}`;
612+
assert(d);
613+
// Careful incision
614+
return [[{start: exp.start,
615+
end: d.start}, 'kremlin.export(module, {default:'],
616+
[{start: d.end,
617+
end: exp.end}, '});']];
576618
}
577619
else throw new Error(`invalid export '${exp.type}'`);
578-
return [exp, `kremlin.export(module, ${rhs});`];
579620
}
580621

581622
makeRequire(ref: ModuleRef, isDefault: boolean = false) {
@@ -588,6 +629,16 @@ class AcornJSModule extends InEnvironment implements CompilationUnit {
588629
}
589630
}
590631

632+
updateReferences(name: string, expr: string) {
633+
var refs = [];
634+
for (let refnode of this.vars.globals.get(name) || []) {
635+
var colon = this._isShorthandProperty(refnode);
636+
refs.push([refnode,
637+
`${colon ? name+':' : ''}${expr}`]);
638+
}
639+
return refs;
640+
}
641+
591642
_freshVar(base = "") {
592643
if (!this.vars) this.extractVars();
593644
for (let i = 0; ; i++) {
@@ -655,6 +706,7 @@ declare namespace AcornTypes {
655706

656707
class ExportDeclaration extends acorn.Node {
657708
type: "ExportNamedDeclaration" | "ExportDefaultDeclaration"
709+
declaration?: acorn.Node
658710
}
659711

660712
class ExportNamedDeclaration extends ExportDeclaration {
@@ -665,7 +717,7 @@ declare namespace AcornTypes {
665717

666718
class ExportDefaultDeclaration extends ExportDeclaration {
667719
type: "ExportDefaultDeclaration"
668-
declaration: acorn.Node
720+
declaration: acorn.Node // not optional
669721
}
670722

671723
class ExportSpecifier extends acorn.Node {
@@ -681,17 +733,23 @@ declare namespace AcornTypes {
681733

682734
class Identifier extends acorn.Node {
683735
name: string
736+
parents: acorn.Node[] // actually this exists in for all acorn.Nodes :/
737+
}
738+
739+
class Property extends acorn.Node {
740+
shorthand: boolean
684741
}
685742

686743
}
687744

688745
namespace AcornUtils {
689746
export function is(node: acorn.Node, type: "Program"): node is AcornTypes.Program
690747
export function is(node: acorn.Node, type: "ImportSpecifier"): node is AcornTypes.ImportSpecifier
691-
export function is(node: acorn.Node, type: "CallExpression"): node is AcornTypes.CallExpression
692-
export function is(node: acorn.Node, type: "Identifier"): node is AcornTypes.Identifier
693748
export function is(node: acorn.Node, type: "ExportNamedDeclaration"): node is AcornTypes.ExportNamedDeclaration
694749
export function is(node: acorn.Node, type: "ExportDefaultDeclaration"): node is AcornTypes.ExportDefaultDeclaration
750+
export function is(node: acorn.Node, type: "CallExpression"): node is AcornTypes.CallExpression
751+
export function is(node: acorn.Node, type: "Identifier"): node is AcornTypes.Identifier
752+
export function is(node: acorn.Node, type: "Property"): node is AcornTypes.Property
695753
export function is(node: acorn.Node, type: string): boolean
696754

697755
export function is(node: acorn.Node, type: string) { return node.type === type; }

src/build/cache.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
const fs = (0||require)('fs') as typeof import('fs');
2+
3+
import { ModuleRef, SourceFile } from './modules';
4+
import { VisitResult } from './bundle';
5+
6+
7+
8+
class BuildCache {
9+
10+
modules: Map<string, BuildCache.Entry>
11+
12+
constructor() {
13+
this.modules = new Map;
14+
}
15+
16+
get(m: ModuleRef): BuildCache.Entry {
17+
var stamp = this.stamp(m);
18+
if (!stamp) return {stamp: null}; // not cached
19+
20+
var key = m.id,
21+
value = this.modules.get(key);
22+
23+
if (!value || value.stamp !== stamp)
24+
this.modules.set(key, value = {stamp});
25+
return value;
26+
}
27+
28+
stamp(m: ModuleRef): BuildCache.Stamp {
29+
if (m instanceof SourceFile) {
30+
try {
31+
var stat = fs.statSync(m.filename);
32+
return +stat.mtime;
33+
}
34+
catch { }
35+
}
36+
}
37+
38+
memo<K extends keyof Entry>(m: ModuleRef, field: K,
39+
op: (m: ModuleRef) => Entry[K]): Entry[K] {
40+
var e = this.get(m), v = e[field];
41+
return v || (e[field] = op(m));
42+
}
43+
}
44+
45+
type Entry = BuildCache.Entry;
46+
47+
48+
namespace BuildCache {
49+
50+
export type Entry = {
51+
stamp: Stamp
52+
visit?: VisitResult
53+
};
54+
55+
export type Stamp = number;
56+
57+
}
58+
59+
60+
61+
export { BuildCache }

src/build/include.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@ kremlin = {m: {}, loaded: {},
2121
Object.assign(c, this.require(k)); // what if c is not an object?
2222
return c;
2323
},
24-
export(m, d) {
25-
m.exports = Object.assign(m.exports || {}, d);
24+
export(m, d, names) {
25+
m.exports = m.exports || {};
26+
if (names)
27+
for (let nm of names) m.exports[nm] = d[nm]
28+
else
29+
m.exports = Object.assign(m.exports, d);
2630
},
2731
node_require(nm) {
2832
var m = require(nm);

0 commit comments

Comments
 (0)