-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathindex.ts
More file actions
199 lines (187 loc) · 5.44 KB
/
Copy pathindex.ts
File metadata and controls
199 lines (187 loc) · 5.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
/**
* index.ts
*
* Copyright (c) 2019 Guo Y.K. <hi@guoyk.net>
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
import fs = require("fs-extra");
import path = require("path");
import vm = require("vm");
interface ISandbox {
define: (...args: any[]) => void;
deps: string[];
object: any;
}
/** reusable sandbox for scriptlet definition */
const sandbox = {
define(...args: any[]) {
// AMD like , `define(['depA', 'depB'], function(depA, depB) {})`
if (args.length === 2 && Array.isArray(args[0])) {
args[0].push(args[1]);
args = args[0];
}
sandbox.object = args.pop();
sandbox.deps = args;
},
deps: [],
object: null,
} as ISandbox;
/** reusable vm.Context for scriptlet definition */
const sandboxContext = vm.createContext(sandbox);
/** mtime based scriptlet file cache policy */
export const MTIME = "mtime";
/** error code - dependency loop detected */
export const ERR_DEPENDENCY_LOOP = "ERR_DEPENDENCY_LOOP";
/** error code - missing dependency */
export const ERR_DEPENDENCY_MISSING = "ERR_DEPENDENCY_MISSING";
/** all cached scripts */
export const cachedScriptlets: Map<string, ICachedScriptlet> = new Map();
/** cached script */
export interface ICachedScriptlet {
/** full path for scriptlet */
fullPath: string;
/** mtime in milisecond of scriptlet file */
mtimeMs: number;
/** compiled vm.Script */
script: vm.Script;
}
/** scriptlet execution option */
export interface IScriptletOption {
/**
* extra dependencies for script, key prefixed with '$' is strongly suggested
*/
extra?: Map<string, any>;
/**
* cache policy, true for full cache, false for none cache, 'mtime' for
* mtime based file cache, default to 'false'
*/
cache?: boolean | string;
/**
* internal tracker for dependency loop detection
*/
_loopTracker?: Set<string>;
}
export class ScriptletError extends Error {
public code: string;
constructor(code: string, message: string) {
super(message);
this.code = code;
}
}
/**
* clone a scriptlet option, keeps '_loopTracker' reference, but clone 'extra',
* preventing lower level extra modification overrides upper level
* @param option scriptlet option
*/
function cloneScriptletOption(
option: IScriptletOption, moreExtra?: Map<string, any>): IScriptletOption {
const newOption = {} as IScriptletOption;
newOption.cache = option.cache;
newOption._loopTracker = option._loopTracker;
if (option.extra || moreExtra) {
newOption.extra = new Map<string, any>();
if (option.extra) {
for (const entry of option.extra) {
newOption.extra.set(entry[0], entry[1]);
}
}
if (moreExtra) {
for (const entry of moreExtra) {
newOption.extra.set(entry[0], entry[1]);
}
}
}
return newOption;
}
/**
* resolve a relative scriptlet path
* @param fullPath fullpath of the scriptlet
* @param name relative name of target scriptlet
*/
function resolveRelativeScriptlet(fullPath: string, name: string): string {
return path.resolve(path.dirname(fullPath), name + ".js");
}
/**
* run a scriptlet
* @param id scriptlet file to run
* @param option scriptlet execution option
*/
export async function run(
file: string, option: IScriptletOption = {}): Promise<any> {
// resolve full path
const fullPath = path.resolve(file);
// check dependency loop
option._loopTracker = option._loopTracker || new Set();
if (option._loopTracker.has(fullPath)) {
throw new ScriptletError(
ERR_DEPENDENCY_LOOP, `dependency loop detected in ${file}`);
}
option._loopTracker.add(fullPath);
// apply cache policy
let script = null;
let stat = null;
if (option.cache) {
const cached = cachedScriptlets.get(fullPath);
if (cached) {
if (option.cache === "mtime") {
stat = await fs.stat(fullPath);
if (stat.mtimeMs === cached.mtimeMs) {
script = cached.script;
}
} else {
script = cached.script;
}
}
}
// read script if not cached
if (!script) {
const content = await fs.readFile(fullPath, "utf8");
script =
new vm.Script(content, { filename: fullPath, produceCachedData: true });
if (!stat) {
stat = await fs.stat(fullPath);
}
cachedScriptlets.set(fullPath, { fullPath, mtimeMs: stat.mtimeMs, script });
}
// evaluate the script
sandbox.deps = [];
sandbox.object = null;
script.runInContext(sandboxContext);
const { deps, object } = sandbox;
// resolve dependencies
const args = [];
for (const dep of deps) {
if (dep === "$load") {
// buildtin $load function
args.push(async (name: string, extra?: Map<string, any>) => {
return run(
resolveRelativeScriptlet(fullPath, name),
cloneScriptletOption(option, extra));
});
} else if (option.extra && option.extra.has(dep)) {
// option.extra contains that dep, key prefixed with $ is suggested
args.push(option.extra.get(dep));
} else if (dep.startsWith(".")) {
// relative scriptlet
args.push(await run(
resolveRelativeScriptlet(fullPath, dep),
cloneScriptletOption(option)));
} else {
// node.js require()
try {
args.push(require(dep));
} catch (e) {
throw new ScriptletError(
ERR_DEPENDENCY_MISSING,
`failed to resolve dependency ${dep} in script ${fullPath}`);
}
}
}
if (typeof object === "function") {
return object(...args);
} else {
return object;
}
}