|
1 | | -(function() {"use strict"; function timeout(win, f, ms) { win.__setTimeout(f, ms) } |
2 | | - // The above is a kludge to make sure setTimeout calls are made from |
3 | | - // line 1, which is where FF will start counting for its line numbers. |
| 1 | +(function() { |
| 2 | + "use strict" |
4 | 3 |
|
5 | 4 | function parseStack(stack) { |
6 | 5 | let found = [], m |
7 | | - let re = /([\w$]*)@.*?:(\d+)|\bat (?:([^\s(]+) \()?.*?:(\d+)/g |
| 6 | + let re = /([\w$]*)@(.*?):(\d+)|\bat (?:([^\s(]+) \()?(.*?):(\d+)/g |
8 | 7 | while (m = re.exec(stack)) { |
9 | | - found.push({fn: m[1] || m[3] || null, |
10 | | - line: m[2] || m[4]}) |
| 8 | + let fn = m[1] || m[4] || null |
| 9 | + let file = m[2] || m[5] || null |
| 10 | + if (fn && /sandbox/i.test(fn) || file && /sandbox/i.test(file)) break |
| 11 | + found.push({fn, file, line: m[3] || m[6]}) |
11 | 12 | } |
12 | 13 | return found |
13 | 14 | } |
|
22 | 23 | // Used to cancel existing events when new code is loaded |
23 | 24 | this.timeouts = []; this.intervals = []; this.frames = []; this.framePos = 0 |
24 | 25 |
|
| 26 | + this.loaded = new Cached(name => resolved.compute(name).then(({name, code}) => this.evalModule(name, code))) |
| 27 | + |
25 | 28 | const loaded = () => { |
26 | 29 | frame.removeEventListener("load", loaded) |
27 | 30 | this.win = frame.contentWindow |
|
71 | 74 | } |
72 | 75 |
|
73 | 76 | run(code, output) { |
74 | | - if (output) |
75 | | - this.output = output |
| 77 | + if (output) this.output = output |
76 | 78 | this.startedAt = Date.now() |
77 | 79 | this.extraSecs = 2 |
78 | 80 | this.win.__c = 0 |
79 | | - timeout(this.win, preprocess(code, this), 0) |
| 81 | + this.prepare(code).then(code => this.win.eval(code)).catch(err => this.error(err)) |
| 82 | + } |
| 83 | + |
| 84 | + prepare(text) { |
| 85 | + let {code, dependencies} = preprocess(text) |
| 86 | + return Promise.all(dependencies.map(dep => this.loaded.compute(dep))).then(() => code) |
| 87 | + } |
| 88 | + |
| 89 | + evalModule(name, code) { |
| 90 | + if (/\.json$/.test(name)) |
| 91 | + return this.loaded.store(name, {exports: JSON.parse(code)}) |
| 92 | + |
| 93 | + let work = findDeps(code).map(dep => this.loaded.compute(resolveRelative(name, dep))) |
| 94 | + return Promise.all(work).then(() => { |
| 95 | + let f = new this.win.Function("require, exports, module, __dirname, __filename", |
| 96 | + code + "\n//# sourceURL=code" + randomID()) |
| 97 | + let module = this.loaded.store(name, {exports: {}}) |
| 98 | + f(dep => this.require(resolveRelative(name, dep)), module.exports, module, name, name) |
| 99 | + return module |
| 100 | + }) |
| 101 | + } |
| 102 | + |
| 103 | + require(name) { |
| 104 | + let found = resolved.get(name) |
| 105 | + if (!found) throw new Error(`Could not load module '${name}'`) |
| 106 | + return this.loaded.get(found.name).exports |
80 | 107 | } |
81 | 108 |
|
82 | 109 | setHTML(code, output, callback) { |
|
101 | 128 | if (/["']/.test(src.charAt(0))) src = src.slice(1, src.length - 1) |
102 | 129 | tag.src = src |
103 | 130 | } else { |
104 | | - tag.text = preprocess(content, sandbox) |
| 131 | + tag.text = preprocess(content, sandbox).code |
105 | 132 | } |
106 | 133 | scriptTags.push(tag) |
107 | 134 | return "" |
|
125 | 152 | if (tag.src) { |
126 | 153 | tag.addEventListener("load", function() { loadScript(i + 1) }) |
127 | 154 | } else { |
128 | | - let id = Math.floor(Math.random() * 0xffffff) |
| 155 | + let id = randomID() |
129 | 156 | sandbox.callbacks[id] = function() { delete sandbox.callbacks[id]; loadScript(i + 1) } |
130 | 157 | tag.text += ";__sandbox.callbacks[" + id + "]();" |
131 | 158 | } |
|
185 | 212 | this.frames.push(val) |
186 | 213 | return val |
187 | 214 | } |
| 215 | + |
| 216 | + win.require = name => this.require(name) |
188 | 217 | } |
189 | 218 |
|
190 | 219 | resizeFrame() { |
|
257 | 286 | {from: node.body.end, text: backJump + "}"}) |
258 | 287 | } |
259 | 288 | } |
| 289 | + let dependencies = [] |
| 290 | + |
260 | 291 | acorn.walk.simple(ast, { |
261 | 292 | ForStatement: loop, |
262 | 293 | ForInStatement: loop, |
263 | 294 | WhileStatement: loop, |
264 | | - DoWhileStatement: loop |
| 295 | + DoWhileStatement: loop, |
| 296 | + CallExpression(node) { |
| 297 | + if (node.callee.type == "Identifier" && node.callee.name == "require" && |
| 298 | + node.arguments.length == 1 && node.arguments[0].type == "Literal" && |
| 299 | + typeof node.arguments[0].value == "string" && !dependencies.includes(node.arguments[0].value)) |
| 300 | + dependencies.push(node.arguments[0].value) |
| 301 | + } |
265 | 302 | }) |
| 303 | + |
266 | 304 | let tryPos = 0, catchPos = ast.end |
267 | 305 | for (let i = strict ? 1 : 0; i < ast.body.length; i++) { |
268 | 306 | let stat = ast.body[i] |
|
275 | 313 | if (stat.type == "ClassDeclaration") |
276 | 314 | patches.push({from: stat.start, text: "var " + stat.id.name + " = "}) |
277 | 315 | } |
| 316 | + |
278 | 317 | patches.push({from: tryPos, text: "try{"}) |
279 | 318 | patches.push({from: catchPos, text: "}catch(e){__sandbox.error(e);}"}) |
280 | 319 | patches.sort(function(a, b) { return a.from - b.from || (a.to || a.from) - (b.to || b.from)}) |
|
285 | 324 | pos = patch.to || patch.from |
286 | 325 | } |
287 | 326 | out += code.slice(pos, code.length) |
288 | | - return (strict ? '"use strict";' : "") + out |
| 327 | + out += "\n//# sourceURL=code" + randomID() |
| 328 | + return {code: (strict ? '"use strict";' : "") + out, dependencies} |
289 | 329 | } |
290 | 330 |
|
291 | | - let Output = SandBox.Output = function(div) { |
292 | | - this.div = div |
| 331 | + function randomID() { |
| 332 | + return Math.floor(Math.random() * 0xffffffff).toString(16) |
293 | 333 | } |
294 | 334 |
|
295 | | - Output.prototype = { |
296 | | - clear: function() { |
| 335 | + function findDeps(code) { |
| 336 | + let deps = [], ast |
| 337 | + try { ast = acorn.parse(code) } |
| 338 | + catch(e) { return deps } |
| 339 | + acorn.walk.simple(ast, { |
| 340 | + CallExpression(node) { |
| 341 | + if (node.callee.type == "Identifier" && node.callee.name == "require" && |
| 342 | + node.arguments.length == 1 && node.arguments[0].type == "Literal" && |
| 343 | + typeof node.arguments[0].value == "string" && !deps.includes(node.arguments[0].value)) |
| 344 | + deps.push(node.arguments[0].value) |
| 345 | + } |
| 346 | + }) |
| 347 | + return deps |
| 348 | + } |
| 349 | + |
| 350 | + function resolveRelative(base, path) { |
| 351 | + if (!/\.\.?\//.test(path)) return path |
| 352 | + base = base.replace(/[^\/]+$/, "") |
| 353 | + let m |
| 354 | + while (m = /^\.(\.)?\//.exec(path)) { |
| 355 | + if (m[1]) base = base.replace(/\/[^\/]+\/$/, "/") |
| 356 | + path = path.slice(m[0].length) |
| 357 | + } |
| 358 | + return base + path |
| 359 | + } |
| 360 | + |
| 361 | + class Cached { |
| 362 | + constructor(mapping) { |
| 363 | + this.mapping = mapping |
| 364 | + this.work = Object.create(null) |
| 365 | + this.done = Object.create(null) |
| 366 | + } |
| 367 | + |
| 368 | + compute(value) { |
| 369 | + return this.work[value] || (this.work[value] = this.mapping(value).then(result => this.done[value] = result)) |
| 370 | + } |
| 371 | + |
| 372 | + store(value, result) { |
| 373 | + this.work[value] = Promise.resolve(result) |
| 374 | + return this.done[value] = result |
| 375 | + } |
| 376 | + |
| 377 | + get(value) { |
| 378 | + return this.done[value] |
| 379 | + } |
| 380 | + } |
| 381 | + |
| 382 | + // Cache for loaded code and resolved unpkg redirects |
| 383 | + const resolved = new Cached(name => fetch("https://unpkg.com/" + name.replace(/\/$/, "")).then(resp => { |
| 384 | + if (resp.status >= 400) throw new Error(`Failed to resolve package '${name}'`) |
| 385 | + let found = resp.url.replace(/.*unpkg\.com\//, "") |
| 386 | + let known = resolved.get(found) |
| 387 | + return known || resp.text().then(code => resolved.store(found, {name: found, code})) |
| 388 | + })) |
| 389 | + |
| 390 | + let Output = SandBox.Output = class { |
| 391 | + constructor(div) { this.div = div } |
| 392 | + |
| 393 | + clear() { |
297 | 394 | let clone = this.div.cloneNode(false) |
298 | 395 | this.div.parentNode.replaceChild(clone, this.div) |
299 | 396 | this.div = clone |
300 | | - }, |
301 | | - out: function(type, args) { |
| 397 | + } |
| 398 | + |
| 399 | + out(type, args) { |
302 | 400 | let wrap = document.createElement("pre") |
303 | 401 | wrap.className = "sandbox-output-" + type |
304 | 402 | for (let i = 0; i < args.length; ++i) { |
|
0 commit comments