Skip to content

Commit cc3afc2

Browse files
committed
Add crude support for CommonJS to the sandbox
Via unpkg.com
1 parent 2e3dfb8 commit cc3afc2

2 files changed

Lines changed: 118 additions & 20 deletions

File tree

html/empty.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<!doctype html>
1+
<!doctype html><meta charset=utf8>

html/js/sandbox.js

Lines changed: 117 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
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"
43

54
function parseStack(stack) {
65
let found = [], m
7-
let re = /([\w$]*)@.*?:(\d+)|\bat (?:([^\s(]+) \()?.*?:(\d+)/g
6+
let re = /([\w$]*)@(.*?):(\d+)|\bat (?:([^\s(]+) \()?(.*?):(\d+)/g
87
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]})
1112
}
1213
return found
1314
}
@@ -22,6 +23,8 @@
2223
// Used to cancel existing events when new code is loaded
2324
this.timeouts = []; this.intervals = []; this.frames = []; this.framePos = 0
2425

26+
this.loaded = new Cached(name => resolved.compute(name).then(({name, code}) => this.evalModule(name, code)))
27+
2528
const loaded = () => {
2629
frame.removeEventListener("load", loaded)
2730
this.win = frame.contentWindow
@@ -71,12 +74,36 @@
7174
}
7275

7376
run(code, output) {
74-
if (output)
75-
this.output = output
77+
if (output) this.output = output
7678
this.startedAt = Date.now()
7779
this.extraSecs = 2
7880
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
80107
}
81108

82109
setHTML(code, output, callback) {
@@ -101,7 +128,7 @@
101128
if (/["']/.test(src.charAt(0))) src = src.slice(1, src.length - 1)
102129
tag.src = src
103130
} else {
104-
tag.text = preprocess(content, sandbox)
131+
tag.text = preprocess(content, sandbox).code
105132
}
106133
scriptTags.push(tag)
107134
return ""
@@ -125,7 +152,7 @@
125152
if (tag.src) {
126153
tag.addEventListener("load", function() { loadScript(i + 1) })
127154
} else {
128-
let id = Math.floor(Math.random() * 0xffffff)
155+
let id = randomID()
129156
sandbox.callbacks[id] = function() { delete sandbox.callbacks[id]; loadScript(i + 1) }
130157
tag.text += ";__sandbox.callbacks[" + id + "]();"
131158
}
@@ -185,6 +212,8 @@
185212
this.frames.push(val)
186213
return val
187214
}
215+
216+
win.require = name => this.require(name)
188217
}
189218

190219
resizeFrame() {
@@ -257,12 +286,21 @@
257286
{from: node.body.end, text: backJump + "}"})
258287
}
259288
}
289+
let dependencies = []
290+
260291
acorn.walk.simple(ast, {
261292
ForStatement: loop,
262293
ForInStatement: loop,
263294
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+
}
265302
})
303+
266304
let tryPos = 0, catchPos = ast.end
267305
for (let i = strict ? 1 : 0; i < ast.body.length; i++) {
268306
let stat = ast.body[i]
@@ -275,6 +313,7 @@
275313
if (stat.type == "ClassDeclaration")
276314
patches.push({from: stat.start, text: "var " + stat.id.name + " = "})
277315
}
316+
278317
patches.push({from: tryPos, text: "try{"})
279318
patches.push({from: catchPos, text: "}catch(e){__sandbox.error(e);}"})
280319
patches.sort(function(a, b) { return a.from - b.from || (a.to || a.from) - (b.to || b.from)})
@@ -285,20 +324,79 @@
285324
pos = patch.to || patch.from
286325
}
287326
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}
289329
}
290330

291-
let Output = SandBox.Output = function(div) {
292-
this.div = div
331+
function randomID() {
332+
return Math.floor(Math.random() * 0xffffffff).toString(16)
293333
}
294334

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() {
297394
let clone = this.div.cloneNode(false)
298395
this.div.parentNode.replaceChild(clone, this.div)
299396
this.div = clone
300-
},
301-
out: function(type, args) {
397+
}
398+
399+
out(type, args) {
302400
let wrap = document.createElement("pre")
303401
wrap.className = "sandbox-output-" + type
304402
for (let i = 0; i < args.length; ++i) {

0 commit comments

Comments
 (0)