substrate-lang 0.10.1 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/build/compiler/{checker.4dd4556dd421@0+0.wasm → checker.1d67e1d4e91a@0+0.wasm} +0 -0
  2. package/build/compiler/{expander.967ce443d379@0+0.wasm → expander.34110a31a949@0+0.wasm} +0 -0
  3. package/build/compiler/{main.4fc52f204d54@0+0.wasm → main.ca9b5089c24d@0+0.wasm} +0 -0
  4. package/build/compiler/program +4 -4
  5. package/build/compiler/{roots.cefd10e93684@0+0.wasm → roots.2f1ad1a79459@0+0.wasm} +0 -0
  6. package/build/sub/{build.459c5b9412bd@0+0.wasm → build.eef7dfa467ec@0+0.wasm} +0 -0
  7. package/build/sub/{common.1edc613607a0@0+0.wasm → common.7617940db2dc@0+0.wasm} +0 -0
  8. package/build/sub/deflate.23f7c4679260@0+0.wasm +0 -0
  9. package/build/sub/{doc.0775df5de6fe@0+0.wasm → doc.92d6d82d99c1@0+0.wasm} +0 -0
  10. package/build/sub/{doc.ef9965d5d402@0+0.wasm → doc.dab689b811ca@0+0.wasm} +0 -0
  11. package/build/sub/{fetch.96b2d10f025e@0+0.wasm → fetch.3367ff4f88c0@0+0.wasm} +0 -0
  12. package/build/sub/{files.ad0469fdf6fa@0+0.wasm → files.5ae9d3cebeca@0+0.wasm} +0 -0
  13. package/build/sub/gzip.16e372eda1cf@0+0.wasm +0 -0
  14. package/build/sub/{hash.660682e4f491@0+0.wasm → hash.c57c141b4862@0+0.wasm} +0 -0
  15. package/build/sub/{html.a23126eb949a@0+0.wasm → html.803f73c35c4a@0+0.wasm} +0 -0
  16. package/build/sub/{manifest.ef317480deb0@0+0.wasm → manifest.da52a88c7af7@0+0.wasm} +0 -0
  17. package/build/sub/{map.f455cca6a42a@0+0.wasm → map.5c8084999550@0+0.wasm} +0 -0
  18. package/build/sub/program +22 -22
  19. package/build/sub/{root.f10a4906f460@0+0.wasm → root.78253db36643@0+0.wasm} +0 -0
  20. package/build/sub/{roots.cefd10e93684@0+0.wasm → roots.2f1ad1a79459@0+0.wasm} +0 -0
  21. package/build/sub/sha256.a6c32b04d799@0+0.wasm +0 -0
  22. package/build/sub/site.47fd989be8fb@0+0.wasm +0 -0
  23. package/build/sub/site.c908b1f6db12@0+0.wasm +0 -0
  24. package/build/sub/{store.a24d7a25daa6@0+0.wasm → store.048c77bb124d@0+0.wasm} +0 -0
  25. package/build/sub/sub.a618f89f2ec8@0+0.wasm +0 -0
  26. package/build/sub/{test.9bd59c73f65e@0+0.wasm → test.b529bf52ba3e@0+0.wasm} +0 -0
  27. package/build/sub/{text.1bc90482bd43@0+0.wasm → text.b214933a4984@0+0.wasm} +0 -0
  28. package/build/sub/{upgrade.39ee449b00d5@0+0.wasm → upgrade.7ec5651ed2e9@0+0.wasm} +0 -0
  29. package/package.json +1 -1
  30. package/std/http/server.sub +7 -7
  31. package/std/http/static.sub +66 -30
  32. package/std/package/manifest.sub +9 -4
  33. package/std/package/site.sub +209 -15
  34. package/std/package.sub +2 -2
  35. package/std/test/browser.sub +20 -14
  36. package/std/test/build.sub +8 -6
  37. package/std/test/page/agent.mjs +8 -2
  38. package/std/web/canvas.sub +1 -1
  39. package/std/web/dom.sub +5 -5
  40. package/std/web/patches.sub +8 -8
  41. package/substrate/VERSION +1 -1
  42. package/substrate/compiler/expander.sub +2 -3
  43. package/substrate/compiler/roots.sub +12 -5
  44. package/substrate/host/grants.mjs +3 -3
  45. package/substrate/host/node/net.mjs +17 -2
  46. package/substrate/host/node/worker.mjs +3 -1
  47. package/substrate/host/subhost.mjs +4 -1
  48. package/substrate/host/web/audio.mjs +1 -1
  49. package/substrate/host/web/dom.mjs +3 -3
  50. package/substrate/host/web/dom.sub +4 -4
  51. package/substrate/host/web/fs.mjs +3 -3
  52. package/substrate/host/web/gpu.mjs +2 -2
  53. package/substrate/host/web/location.sub +1 -1
  54. package/substrate/host/web/start.mjs +11 -7
  55. package/substrate/host/web/subhost-web.mjs +54 -33
  56. package/substrate/host/web/worker-script.mjs +4 -4
  57. package/substrate/host/web/worker.mjs +4 -4
  58. package/substrate/host/worker.mjs +1 -1
  59. package/build/sub/site.1732b58ec0e8@0+0.wasm +0 -0
  60. package/build/sub/site.ae3913b0b0f4@0+0.wasm +0 -0
  61. package/build/sub/sub.2e3092140706@0+0.wasm +0 -0
package/std/package.sub CHANGED
@@ -1,6 +1,6 @@
1
1
  (package std
2
- (version "0.10.1")
3
- (substrate "0.10.1")
2
+ (version "0.10.2")
3
+ (substrate "0.10.2")
4
4
  (exports archive/tar browser/chromium browser/devtools compress/deflate compress/gzip compress/inflate crypto/hmac
5
5
  crypto/password crypto/sha1
6
6
  crypto/sha256 crypto/sha512 doc/doc doc/html doc/root doc/text encoding/base64 encoding/json encoding/utf8
@@ -105,13 +105,13 @@
105
105
 
106
106
  (define (%browser-step! [s string]) (page-step! s))
107
107
 
108
- (define (browser-opened [spec string] [html string] [url string] [args (slice string)] [server (fn ())] [message string])
109
- -> browser
108
+ (define (browser-opened [spec string] [html string] [release bool] [url string] [args (slice string)] [server (fn ())]
109
+ [message string]) -> browser
110
110
  (let ([address string])
111
111
  (unless (env-get "SUBSTRATE_DEVTOOLS" (& address) 0)
112
112
  (throw test-failed (lines-joined message "a test that opens a browser needs (browser) in the tests clause of its package's package.sub")))
113
113
  (when (== (-> address length) 0) (throw test-skipped "need Chromium"))
114
- (let ([dir (site-built spec html message)] [why error] [p (ptr page) 0])
114
+ (let ([dir (site-built spec html release message)] [why error] [p (ptr page) 0])
115
115
  (unless (page-open dir (with-args url args) address server (& p) (& why))
116
116
  (throw test-failed (lines-joined message (error-text (& why)))))
117
117
  (browser #:page p))))
@@ -126,28 +126,31 @@
126
126
  (url-add-form-value! (& b) (@ args i)))
127
127
  (buffer-string (& b))))
128
128
 
129
- (struct built [spec string] [html string] [dir string])
129
+ (struct built [spec string] [html string] [release bool] [dir string])
130
130
 
131
131
  (define made buffer)
132
132
 
133
133
  ;; Builds spec's site in the test's scratch directory, once per test.
134
- (define (site-built [spec string] [html string] [message string]) -> string
134
+ (define (site-built [spec string] [html string] [release bool] [message string]) -> string
135
135
  (let* ([all (cast (ptr built) (-> made bytes))] [n (/ (-> made length) (sizeof built))]
136
136
  [base (base-of spec)] [name (if (ends? base ".sub") (slice base 0 (- (-> base length) 4)) base)]
137
137
  [dir (joined-path (joined-path (scratch-directory) "sites") name)] [info fs-info] [err (make-buffer)]
138
138
  [b built])
139
139
  (for ([i u32 0]) (< i n) (+= i 1)
140
140
  (let ([m (& (@ all i))])
141
- (when (and (== (-> m spec) spec) (== (-> m html) html)) (return (-> m dir)))
141
+ (when (and (== (-> m spec) spec) (== (-> m html) html) (== (-> m release) release)) (return (-> m dir)))
142
142
  (when (== (-> m dir) dir) (set! dir (numbered dir n)))))
143
143
  (let ([p (planned spec name message)])
144
144
  (when (> (-> html length) 0) (set! (-> p html) html))
145
+ (build-ready!)
145
146
  (unless (site-write! (& p) dir (toolchain) (& err))
146
147
  (throw test-failed (lines-joined message (buffer-string (& err)))))
147
148
  (unless (and (fs-stat (-> p program) (& info) 0) (== (-> info kind) fs-directory))
148
- (unless (program-write! (-> p program) (joined-path dir "program") (& err))
149
- (throw test-failed (lines-joined message (buffer-string (& err)))))))
150
- (set! b (built #:spec spec #:html html #:dir dir))
149
+ (unless (program-write! (-> p program) (joined-path dir "program") release (& err))
150
+ (throw test-failed (lines-joined message (buffer-string (& err))))))
151
+ (when (and release (not (site-release! dir (& err))))
152
+ (throw test-failed (lines-joined message (buffer-string (& err))))))
153
+ (set! b (built #:spec spec #:html html #:release release #:dir dir))
151
154
  (buffer-add! (& made) (slice (cast (ptr u8) (& b)) (sizeof built)))
152
155
  dir))
153
156
 
@@ -367,18 +370,21 @@
367
370
  (define (with-browser-form [s datum]) -> datum
368
371
  (syntax-match s
369
372
  [(_ ((? identifier? b) (? text? file) options ...) body ...)
370
- (let ([html (literal '"")] [url (literal '"")] [args (no-fields)] [server #'(cast (fn ()) 0)] [l options])
373
+ (let ([html (literal '"")] [release #'#f] [url (literal '"")] [args (no-fields)] [server #'(cast (fn ()) 0)]
374
+ [l options])
371
375
  (while (pair? l)
372
376
  (unless (and (keyword? (syntax-e (car l))) (pair? (cdr l)))
373
377
  (raise-syntax-error 'with-browser "expected a keyword and its value" (car l)))
374
378
  (let ([k (keyword->string (syntax-e (car l)))] [v (cadr l)])
375
379
  (cond [(and (== k "html") (text? v)) (set! html (path-of s v))]
380
+ [(== k "release") (set! release v)]
376
381
  [(and (== k "url") (text? v)) (set! url v)]
377
382
  [(== k "args") (set! args (arguments v))]
378
383
  [(== k "server") (set! server v)]
379
- [else (raise-syntax-error 'with-browser "the options are #:html \"file\", #:url \"url\", #:args and #:server function" (car l))]))
384
+ [else (raise-syntax-error 'with-browser "the options are #:html \"file\", #:release #t, #:url \"url\", #:args and #:server function" (car l))]))
380
385
  (set! l (cddr l)))
381
- #`(let ([#,b (browser-opened #,(if (named? (syntax-e file)) file (path-of s file)) #,html #,url #,args #,server
386
+ #`(let ([#,b (browser-opened #,(if (named? (syntax-e file)) file (path-of s file)) #,html #,release #,url #,args
387
+ #,server
382
388
  #,(literal (format "~a:~a: (with-browser (~s ~s) ...)"
383
389
  `(,(syntax-source s) ,(syntax-line s) ,(syntax->datum b)
384
390
  ,(syntax-e file)))))])
@@ -386,8 +392,8 @@
386
392
  [catch-all (browser-closed! #,b) (rethrow)])
387
393
  (%browser-step! "")
388
394
  (browser-closed! #,b)))]
389
- [_ (raise-syntax-error 'with-browser "expected (with-browser (name \"site or program.sub\" [#:html \"index.html\"] [#:url \"url\"] [#:args (\"argument\" ...)]
390
- [#:server function]) body ...)" s)]))
395
+ [_ (raise-syntax-error 'with-browser "expected (with-browser (name \"site or program.sub\" [#:html \"index.html\"] [#:release #t] [#:url \"url\"]
396
+ [#:args (\"argument\" ...)] [#:server function]) body ...)" s)]))
391
397
 
392
398
  ;; The program's arguments from #:args: either a list of strings, or an expression that gives a slice of strings.
393
399
  (define (arguments [v datum]) -> datum
@@ -1,4 +1,5 @@
1
1
  ;; Building programs during a test: expect-compile-error, and program-write!, which with-browser uses to build sites.
2
+ ;; build-ready! gives the compiler the test program's toolchain, module map and cache, once.
2
3
  (require-unqualified compiler/macros)
3
4
  (require-unqualified compiler/expander)
4
5
  (require-unqualified compiler/roots)
@@ -9,12 +10,12 @@
9
10
  (require-unqualified "test.sub")
10
11
  (require-unqualified "compare.sub")
11
12
 
12
- (provide expect-compile-error program-write!)
13
+ (provide expect-compile-error program-write! build-ready!)
13
14
 
14
15
  (define ready bool #f)
15
16
  (define directory string)
16
17
 
17
- (define (ready!)
18
+ (define (build-ready!)
18
19
  (unless ready
19
20
  (set! ready #t)
20
21
  (let ([why error] [toolchain string] [map-file string] [cache string])
@@ -29,19 +30,20 @@
29
30
 
30
31
  ;; Builds the program at path into out, both relative to the package's directory, with the test program's module map
31
32
  ;; and cache. Returns #f with the compile error in err if it does not compile.
32
- (define (program-write! [path string] [out string] [err (ptr buffer)]) -> bool
33
- (ready!)
33
+ (define (program-write! [path string] [out string] [strip bool] [err (ptr buffer)]) -> bool
34
+ (build-ready!)
34
35
  (let ([full (resolved-path directory path)] [to (resolved-path directory out)])
35
36
  (set-display-directory! (string->datum (parent-of full)))
36
37
  (fs-remove to #t 0)
37
- (try (begin (write-program! 0 (string->datum full) (string->datum to) #f) #t)
38
+ (try (begin (write-program! 0 (string->datum full) (string->datum to) strip) #t)
38
39
  [catch compile-error (message blamed loc)
39
40
  (write-error err message blamed loc)
40
41
  #f])))
41
42
 
42
43
  (define (compile-error-check [path string] [text string] [message string])
43
44
  (let ([err (make-buffer)])
44
- (when (program-write! path (joined-path (joined-path (scratch-directory) "programs") (program-name path)) (& err))
45
+ (when (program-write! path (joined-path (joined-path (scratch-directory) "programs") (program-name path)) #f
46
+ (& err))
45
47
  (failure! message "the file compiled"))
46
48
  (unless (contains? (buffer-string (& err)) text) (failure! message (text-of "its compile error is\n" (buffer-string (& err)))))
47
49
  (buffer-free! (& err))))
@@ -74,8 +74,14 @@ const same = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
74
74
  // nameOf names drawn images and sounds by the path of the site's asset with the same bytes.
75
75
  async function nameOf(bytes, otherwise) {
76
76
  if (!assets) {
77
- const listed = (await (await fetch('/assets.list')).text()).split('\n').filter((p) => p !== '');
78
- assets = await Promise.all(listed.map(async (p) => [`/${p}`, new Uint8Array(await (await fetch(`/${p}`)).arrayBuffer())]));
77
+ const start = document.querySelector('script[src$="host/web/start.mjs"]');
78
+ const list = start ? new URL('../../assets.list', start.src) : new URL('/assets.list', location.href);
79
+ const lines = (await (await fetch(list)).text()).split('\n').filter((l) => l !== '');
80
+ assets = await Promise.all(lines.map(async (line) => {
81
+ const [path, url = path] = line.split('\t');
82
+ const from = new URL(url.split('/').map(encodeURIComponent).join('/'), location.href);
83
+ return [`/${path}`, new Uint8Array(await (await fetch(from)).arrayBuffer())];
84
+ }));
79
85
  }
80
86
  const found = assets.find(([, b]) => same(b, bytes));
81
87
  return found ? found[0] : otherwise;
@@ -1,5 +1,5 @@
1
1
  ;; Drawing on a <canvas> element: rectangles, paths, circles, text, images and transforms. The calls write into the
2
- ;; page's patches (patches.sub) between canvas-begin! and canvas-end!, and a call made outside them draws nothing.
2
+ ;; patches (patches.sub) between canvas-begin! and canvas-end!, and a call made outside them draws nothing.
3
3
  (require-unqualified std/web/patches)
4
4
 
5
5
  (provide canvas-begin! canvas-end! canvas-drawing?
package/std/web/dom.sub CHANGED
@@ -1,8 +1,8 @@
1
- ;; The page: the host module "dom" and the patches that patches.sub writes. dom-flush! sends the patches. A read takes a
2
- ;; name that walks from a node, or from the current event, through the page's objects: a property's name, or names
3
- ;; joined by dots. It gives a number, with true as 1, or text. The text lasts until the patches are sent. dom-is and
4
- ;; dom-event-is also take a node, and say whether the name leads to it. (dom-is node dom-document "activeElement") says
5
- ;; whether the node has the focus. dom-event-call calls a method of the current event.
1
+ ;; The document: the host module "dom" and the patches that patches.sub writes. dom-flush! sends the patches. A read
2
+ ;; takes a name that walks from a node, or from the current event, through the document's objects: a property's name, or
3
+ ;; names joined by dots. It gives a number, with true as 1, or text. The text lasts until the patches are sent. dom-is
4
+ ;; and dom-event-is also take a node, and say whether the name leads to it. (dom-is node dom-document "activeElement")
5
+ ;; says whether the node has the focus. dom-event-call calls a method of the current event.
6
6
  (require-unqualified host-dom host/web/dom)
7
7
  (require-unqualified patches "patches.sub")
8
8
 
@@ -1,8 +1,8 @@
1
- ;; Changes to the page, written as patches for the host module "dom". dom.sub sends them. A template is HTML in which
2
- ;; the nodes a program will change are marked: elements with a data-n attribute, <!--t--> for a text node, and <!--a-->
3
- ;; for a place that other pieces can be put before. dom-add! puts a piece, a copy of a template, on the page and returns
4
- ;; the piece's number; its marked nodes are numbered after it, in document order. The strings these functions take are
5
- ;; copied, and the copies last until the patches are sent or dom-discard! drops them.
1
+ ;; Changes to the document, written as patches for the host module "dom". dom.sub sends them. A template is HTML in
2
+ ;; which the nodes a program will change are marked: elements with a data-n attribute, <!--t--> for a text node, and
3
+ ;; <!--a--> for a place that other pieces can be put before. dom-add! puts a piece, a copy of a template, in the document
4
+ ;; and returns the piece's number; its marked nodes are numbered after it, in document order. The strings these
5
+ ;; functions take are copied, and the copies last until the patches are sent or dom-discard! drops them.
6
6
  (provide dom-template dom-add! dom-remove! dom-forget! dom-move! dom-set-text! dom-set-attr! dom-remove-attr! dom-listen!
7
7
  dom-listen-window! dom-value dom-number dom-text dom-write-number! dom-write-text! dom-call! dom-call-with!
8
8
  dom-document dom-image! dom-stylesheet! dom-new-node!
@@ -49,8 +49,8 @@
49
49
  (words! (slice (array-of u32 2 template first n parent before)))
50
50
  first))
51
51
 
52
- ;; dom-remove! takes a piece off the page and forgets its nodes. dom-forget! only forgets them, for a piece that was
53
- ;; inside one taken off.
52
+ ;; dom-remove! takes a piece out of the document and forgets its nodes. dom-forget! only forgets them, for a piece that
53
+ ;; was inside one taken out.
54
54
  (define (dom-remove! [first u32] [template u32]) (words! (slice (array-of u32 3 first (size-of template)))))
55
55
  (define (dom-forget! [first u32] [template u32]) (words! (slice (array-of u32 4 first (size-of template)))))
56
56
 
@@ -110,7 +110,7 @@
110
110
  (let ([n (kept name)]) (words! (slice (array-of u32 8 node (cast u32 (-> n bytes)) (-> n length))))))
111
111
 
112
112
  ;; dom-listen! sends the node's event to tag's handler, and dom-listen-window! sends the window's event there while the
113
- ;; node is on the page.
113
+ ;; node is in the document.
114
114
  (define (dom-listen! [node u32] [event string] [tag (ptr void)])
115
115
  (let ([e (kept event)]) (words! (slice (array-of u32 9 node (cast u32 (-> e bytes)) (-> e length) (cast u32 tag))))))
116
116
 
package/substrate/VERSION CHANGED
@@ -1 +1 @@
1
- 0.10.1
1
+ 0.10.2
@@ -1069,10 +1069,9 @@
1069
1069
  (equal? (section d 'format) (make-integer cache-format))
1070
1070
  (equal? (section d 'compiler) compiler-hash)))
1071
1071
 
1072
- ;; load-cached loads a module from the cache, or from the program's compiled modules if that is where the module comes
1073
- ;; from.
1072
+ ;; load-cached loads the module at path from the program's compiled modules when they hold it, or else from the cache.
1074
1073
  (define (load-cached [path datum]) -> datum
1075
- (if (if (compiled-root? path) (== (compiled-module path) 0) (== (cache-compiler) 0))
1074
+ (if (and (== (compiled-module path) 0) (or (compiled-root? path) (== (cache-compiler) 0)))
1076
1075
  0
1077
1076
  (begin
1078
1077
  (begin-module-arena)
@@ -9,8 +9,8 @@
9
9
  (provide set-toolchain-directory! set-module-map! set-module-map-text! set-display-directory! toolchain-root? toolchain-roots
10
10
  toolchain-holding root-directories root-under-test? identity identity-label host-file entry-identity program-identity
11
11
  set-compiled-module-map! compiled-module-map compiled-module compiled-root? module-source-file module-path-from
12
- module-identity named-file program-forms language-path compiler-file module-source? string-literal? displayed
13
- write-error fail-at read-text file-text host-error module-refusal)
12
+ module-identity named-file named-directory program-forms language-path compiler-file module-source?
13
+ string-literal? displayed write-error fail-at read-text file-text host-error module-refusal)
14
14
 
15
15
  (define (symbol-of [s datum]) -> datum (string->symbol (datum->string s)))
16
16
 
@@ -183,10 +183,12 @@
183
183
  (define (set-module-map-text! [text datum] [source datum])
184
184
  (set-module-map! (map (lambda ([f datum]) -> datum (syntax->datum f)) (read-text text (symbol-of source)))))
185
185
 
186
- ;; set-compiled-module-map! replaces the roots with those of the program's compiled modules, which have no directories.
186
+ ;; set-compiled-module-map! gives sessions the program's compiled modules. When the program has given no roots with
187
+ ;; directories, it also replaces the roots with those of the compiled modules, which have no directories.
187
188
  (define (set-compiled-module-map! [forms datum] [modules datum])
188
- (set! toolchain-directory 0)
189
- (set-module-map! forms)
189
+ (when (null? (root-directories))
190
+ (set! toolchain-directory 0)
191
+ (set-module-map! forms))
190
192
  (set! compiled (keep modules)))
191
193
 
192
194
  ;; compiled-module-map returns the module map without directories, which a build writes next to the program's compiled
@@ -390,6 +392,11 @@
390
392
  (let ([file (concat rest '".sub")] [inner (file-in rest (concat (last-part rest) '".sub"))])
391
393
  (if (and (== (compiled-module (identity label file)) 0) (!= (compiled-module (identity label inner)) 0)) inner file)))
392
394
 
395
+ ;; named-directory gives the directory that name stands for in a require from file, or #f for a root of compiled modules.
396
+ (define (named-directory [file datum] [name datum]) -> datum
397
+ (let ([r (root-holding (absolute file))])
398
+ (cadr (root-in (named-root 0 name (if (== r 0) program-label (car r)) roots) roots))))
399
+
393
400
  ;; naming-label returns the root used to resolve module names in code from a given module: that module's root, or the
394
401
  ;; program's root for code outside any root, such as session input.
395
402
  (define (naming-label [from datum]) -> datum
@@ -1,7 +1,7 @@
1
1
  // What a program may do beyond computing: read and write files, reach hosts, read the environment and run
2
- // programs. The grants match Deno's permissions wherever the two overlap, except that the host never prompts, a page
3
- // cannot check where fetch is redirected or where names resolve, and a child may keep the dynamic loader's variables
4
- // the host started with.
2
+ // programs. The grants match Deno's permissions wherever the two overlap, except that the host never prompts, a web
3
+ // program cannot check where fetch is redirected or where names resolve, and a child may keep the dynamic loader's
4
+ // variables the host started with.
5
5
  const kinds = ['read', 'write', 'net', 'env', 'run'];
6
6
  export const flags = {
7
7
  read: '--allow-read', write: '--allow-write', net: '--allow-net', env: '--allow-env', run: '--allow-run',
@@ -1,4 +1,5 @@
1
- // The host module "net" (net.sub).
1
+ // The host module "net" (net.sub). A TLS server reloads its certificate and key when their files change.
2
+ import fs from 'node:fs';
2
3
  import net from 'node:net';
3
4
  import tls from 'node:tls';
4
5
  import { errorOf, reasonOf } from '../errors.mjs';
@@ -14,6 +15,19 @@ const ACCEPT = 2;
14
15
  const CONNECTED = 3;
15
16
  const FAILED = 4;
16
17
 
18
+ function renewed(server, certificate) {
19
+ const [certFile, keyFile] = certificate.files;
20
+ const reload = () => {
21
+ try {
22
+ server.setSecureContext({ cert: fs.readFileSync(certFile), key: fs.readFileSync(keyFile) });
23
+ } catch {}
24
+ };
25
+ for (const file of certificate.files) fs.watchFile(file, { interval: 1000, persistent: false }, reload);
26
+ server.once('close', () => {
27
+ for (const file of certificate.files) fs.unwatchFile(file, reload);
28
+ });
29
+ }
30
+
17
31
  export function netModule(grants, certificate) {
18
32
  const listeners = new Map();
19
33
  const ca = certificate ? [...tls.rootCertificates, certificate.cert] : undefined;
@@ -39,9 +53,10 @@ export function netModule(grants, certificate) {
39
53
  throw new Error('net.listen: TLS needs the host started with --tls-cert and --tls-key');
40
54
  }
41
55
  server = secure
42
- ? tls.createServer({ ...certificate, allowHalfOpen: true })
56
+ ? tls.createServer({ cert: certificate.cert, key: certificate.key, allowHalfOpen: true })
43
57
  : net.createServer({ allowHalfOpen: true });
44
58
  if (most > 0) server.maxConnections = most;
59
+ if (secure && certificate.files) renewed(server, certificate);
45
60
  if (addressDenials(grants.net) && !net.isIP(host)) {
46
61
  lookupWithin(grants, 'net.listen', port)(host, {}, (e, address) => {
47
62
  if (listeners.get(id) !== server) return;
@@ -30,7 +30,9 @@ export function workers(setup) {
30
30
  spawn(plan) {
31
31
  const { port1, port2 } = new MessageChannel();
32
32
  const signal = new Int32Array(new SharedArrayBuffer(4));
33
- const tls = plan.tls ? { cert: read(plan.tls.cert), key: read(plan.tls.key) } : setup.tls;
33
+ const tls = plan.tls
34
+ ? { cert: read(plan.tls.cert), key: read(plan.tls.key), files: [plan.tls.cert, plan.tls.key] }
35
+ : setup.tls;
34
36
  const thread = new Worker(new URL('./worker-thread.mjs', import.meta.url));
35
37
  // The messages a thread posted before it ended, which can still wait on the port when its exit is heard.
36
38
  let heard = null;
@@ -42,7 +42,10 @@ try {
42
42
  if (request.inherit.size > 0) fail('inherit is only for workers');
43
43
  setup.grants = joinedGrants(setup.grants, request.grants);
44
44
  for (const [name, at] of request.fakes) setup.fakes.set(name, at);
45
- if (request.tls) setup.tls = { cert: read(request.tls.cert), key: read(request.tls.key) };
45
+ if (request.tls) {
46
+ const files = [request.tls.cert, request.tls.key];
47
+ setup.tls = { cert: read(files[0]), key: read(files[1]), files };
48
+ }
46
49
  } catch (e) {
47
50
  fail(e.message);
48
51
  }
@@ -74,7 +74,7 @@ export function audio() {
74
74
  counted(tag, 1);
75
75
  const data = bytes
76
76
  ? Promise.resolve(bytes.slice().buffer)
77
- : fetch(name).then((response) => {
77
+ : fetch(app.url(name)).then((response) => {
78
78
  if (!response.ok) throw new Error(`the server answered ${response.status}`);
79
79
  return response.arrayBuffer();
80
80
  });
@@ -1,4 +1,4 @@
1
- // The host module "dom": applies a program's patches to the page, and sends it the page's events.
1
+ // The host module "dom": applies a program's patches to the document, and sends it the document's events.
2
2
  import { resolvePath } from './fs.mjs';
3
3
  import { errors } from '../errors.mjs';
4
4
 
@@ -64,7 +64,7 @@ export function dom(root) {
64
64
  x = step(x, parts[k], path);
65
65
  if (x === null || x === undefined) return [null, last];
66
66
  if (!reachable(x)) {
67
- caller.trap(`dom: ${path}: ${parts.slice(0, k + 1).join('.')} is not one of the page's objects`);
67
+ caller.trap(`dom: ${path}: ${parts.slice(0, k + 1).join('.')} is not one of the document's objects`);
68
68
  }
69
69
  }
70
70
  return [x, last];
@@ -372,7 +372,7 @@ export function dom(root) {
372
372
  const image = new Image();
373
373
  const bytes = app.files.get(resolvePath(app.cwd, name));
374
374
  const type = imageTypes[name.slice(name.lastIndexOf('.') + 1).toLowerCase()] || '';
375
- const url = bytes ? URL.createObjectURL(new Blob([bytes], { type })) : name;
375
+ const url = bytes ? URL.createObjectURL(new Blob([bytes], { type })) : app.url(name);
376
376
  const id = w[i + 1];
377
377
  const tag = w[i + 4];
378
378
  const finished = () => {
@@ -1,7 +1,7 @@
1
- ;; The page: the host module "dom". A heard event carries the id and tag its listener was given. A visibility listener
2
- ;; hears when its element comes into the viewport or leaves it. dom-bind-element gives the element with an id a node
3
- ;; number, for dom-element (std/web/dom.sub). The reads of the page are in std/web/dom.sub too. dom-images-loading
4
- ;; counts the images loading for a tag.
1
+ ;; The document: the host module "dom". A heard event carries the id and tag its listener was given. A visibility
2
+ ;; listener hears when its element comes into the viewport or leaves it. dom-bind-element gives the element with an id a
3
+ ;; node number, for dom-element (std/web/dom.sub). The reads of the document are in std/web/dom.sub too.
4
+ ;; dom-images-loading counts the images loading for a tag.
5
5
  (provide dom-source dom-heard dom-image-loaded dom-image-failed dom-apply dom-bind-element dom-images-loading)
6
6
 
7
7
  (const dom-source u32 1)
@@ -1,5 +1,5 @@
1
- // A page's files for fs.mjs, kept in memory under the program's working directory and checked against the program's
2
- // grants. A directory exists as long as it has a file in it, and there are no links or modes.
1
+ // A web program's files for fs.mjs, kept in memory under the program's working directory and checked against the
2
+ // program's grants. A directory exists as long as it has a file in it, and there are no links or modes.
3
3
  import { mayPath, mayTree, pathDenied, unlimited } from '../grants.mjs';
4
4
  import { sameFile, tempName } from '../fs.mjs';
5
5
 
@@ -17,7 +17,7 @@ const missing = () => Object.assign(new Error('No such file or directory'), { co
17
17
  const unsupported = () => new Error('Operation not supported');
18
18
  const under = (dir) => (dir === '/' ? '/' : `${dir}/`);
19
19
 
20
- export function pageFiles(files, cwd, grants) {
20
+ export function memoryFiles(files, cwd, grants) {
21
21
  const times = new Map();
22
22
  const at = (k, p, tree = false) => {
23
23
  const key = resolvePath(cwd, p);
@@ -144,7 +144,7 @@ export async function gpu(options = {}) {
144
144
  case 1: {
145
145
  const id = w[i + 1];
146
146
  const node = theDom && theDom.nodeOf ? theDom.nodeOf(w[i + 2]) : null;
147
- if (!node) caller.trap(`gpu: surface ${id}: the page gave this module no dom, or node ${w[i + 2]} is not on the page`);
147
+ if (!node) caller.trap(`gpu: surface ${id}: this module has no dom, or node ${w[i + 2]} is not in the document`);
148
148
  if (!(node instanceof HTMLCanvasElement)) caller.trap(`gpu: surface ${id}: node ${w[i + 2]} is not a canvas`);
149
149
  if (options.offscreen) {
150
150
  const texture = device.createTexture({
@@ -563,7 +563,7 @@ export async function gpu(options = {}) {
563
563
  counted(to, 1);
564
564
  try {
565
565
  const bytes = app && app.files.get(resolvePath(app.cwd, name));
566
- const blob = bytes ? new Blob([bytes]) : await fetch(name).then((r) => {
566
+ const blob = bytes ? new Blob([bytes]) : await fetch(app ? app.url(name) : name).then((r) => {
567
567
  if (!r.ok) throw new Error(`the server answered ${r.status}`);
568
568
  return r.blob();
569
569
  });
@@ -1,4 +1,4 @@
1
- ;; The page's URL and history: the host module "location". When the user goes back or forward, each listener's tag
1
+ ;; The document's URL and history: the host module "location". When the user goes back or forward, each listener's tag
2
2
  ;; gets a changed event with the URL as its bytes. location-listen gives the listener's number, and location-unlisten
3
3
  ;; takes it.
4
4
  (provide location-source location-changed location-get location-push location-replace location-back location-listen
@@ -1,5 +1,6 @@
1
- // Starts a site's program with the files in assets.list and files.pack, the URL's arg parameters as its arguments, and
2
- // the host modules it imports. It sets window.modules, sets window.ready once the program has started, and sets
1
+ // Starts a site's program with the files in files.pack, the URL's arg parameters as its arguments, and the host modules
2
+ // it imports. The program, files.pack and assets.list sit beside the host directory. The assets load from the site
3
+ // when the program asks for them. It sets window.modules, sets window.ready once the program has started, and sets
3
4
  // window.ended to the program's exit status and window.endReason to why it ended.
4
5
  import { boot } from './subhost-web.mjs';
5
6
  import { allGrants } from '../grants.mjs';
@@ -7,6 +8,7 @@ import { allGrants } from '../grants.mjs';
7
8
  const query = new URLSearchParams(location.search);
8
9
  const grants = allGrants();
9
10
  const root = document.getElementById('root');
11
+ const built = new URL('../../', import.meta.url);
10
12
  const made = {};
11
13
  const makers = {
12
14
  dom: async () => (await import('./dom.mjs')).dom(root),
@@ -26,9 +28,10 @@ const makers = {
26
28
  const make = (name) => (made[name] ||= makers[name]());
27
29
 
28
30
  window.app = await boot({
29
- program: 'program/',
30
- lists: [['assets.list', '/']],
31
- packs: [['files.pack', '/']],
31
+ program: new URL('program/', built).href,
32
+ packs: [[new URL('files.pack', built).href, '/']],
33
+ assets: new URL('assets.list', built).href,
34
+ site: document.baseURI,
32
35
  args: query.getAll('arg'),
33
36
  grants,
34
37
  onEnd: (code, reason) => {
@@ -36,8 +39,9 @@ window.app = await boot({
36
39
  window.endReason = reason;
37
40
  },
38
41
  async modules(wanted) {
39
- const modules = {};
40
- for (const name of wanted) if (Object.hasOwn(makers, name)) modules[name] = await make(name);
42
+ const names = [...wanted].filter((name) => Object.hasOwn(makers, name));
43
+ const given = await Promise.all(names.map(make));
44
+ const modules = Object.fromEntries(names.map((name, i) => [name, given[i]]));
41
45
  window.modules = modules;
42
46
  return modules;
43
47
  },
@@ -1,9 +1,9 @@
1
- // The host for a web page: runs a program in a browser, with its files kept in memory. A page has every grant and
1
+ // The web host: runs a program in a browser, with its files kept in memory. A web program has every grant and
2
2
  // no fakes. Its workers can be given fewer grants and some fakes (worker-script.mjs).
3
3
  import { createHost } from '../subhost-core.mjs';
4
4
  import { fsModule } from '../fs.mjs';
5
5
  import { allGrants, pairsFrom, requestOf } from '../grants.mjs';
6
- import { pageFiles, resolvePath } from './fs.mjs';
6
+ import { memoryFiles, resolvePath } from './fs.mjs';
7
7
 
8
8
  const text = new TextDecoder();
9
9
  const utf8 = new TextEncoder();
@@ -26,13 +26,13 @@ async function fetchBytes(url) {
26
26
  return new Uint8Array(await response.arrayBuffer());
27
27
  }
28
28
 
29
- async function fetchListed(url, dir, files) {
30
- const list = new URL(url, globalThis.document ? document.baseURI : globalThis.location.href);
31
- const paths = text.decode(await fetchBytes(list)).split('\n').filter((p) => p !== '');
32
- await Promise.all(paths.map(async (path) => {
33
- const from = new URL(path.split('/').map(encodeURIComponent).join('/'), list);
34
- files.set(resolvePath(dir, path), await fetchBytes(from));
35
- }));
29
+ async function fetchAssets(url) {
30
+ const urls = new Map();
31
+ for (const line of text.decode(await fetchBytes(url)).split('\n')) {
32
+ const tab = line.indexOf('\t');
33
+ if (tab >= 0) urls.set(resolvePath('/', line.slice(0, tab)), line.slice(tab + 1));
34
+ }
35
+ return urls;
36
36
  }
37
37
 
38
38
  function lines(line) {
@@ -92,36 +92,50 @@ export function importedModules(bytes) {
92
92
  return names;
93
93
  }
94
94
 
95
+ async function fetchProgram(base, wasm) {
96
+ const names = text.decode(await fetchBytes(`${base}program`)).split('\n').filter((n) => n !== '');
97
+ await Promise.all(names.map(async (name) => {
98
+ wasm.set(name, await fetchBytes(`${base}${encodeURIComponent(name)}.wasm`));
99
+ }));
100
+ return names;
101
+ }
102
+
103
+ // The compiled modules for sessions: the compiled file lists them, each one is in <hash>.subc, and compiled.map holds
104
+ // their module map.
105
+ async function fetchCompiled(base) {
106
+ const compiled = new Map();
107
+ const listed = await fetch(`${base}compiled`);
108
+ if (!listed.ok) return compiled;
109
+ const list = new Uint8Array(await listed.arrayBuffer());
110
+ const hashes = text.decode(list).split('\n').filter((l) => l !== '').map((l) => l.slice(0, l.indexOf(' ')));
111
+ const fetched = async (name) => compiled.set(name, await fetchBytes(`${base}${name}`));
112
+ const names = hashes.map((h) => `${h}.subc`);
113
+ compiled.set('compiled', list);
114
+ await Promise.all((hashes.length > 0 ? ['compiled.map', ...names] : []).map(fetched));
115
+ return compiled;
116
+ }
117
+
95
118
  // options.modules is the modules, or a function that takes the names the program imports and gives the modules.
119
+ // options.assets is the URL of the site's assets.list.
96
120
  export async function boot(options) {
97
121
  const base = options.program.endsWith('/') ? options.program : `${options.program}/`;
98
122
  const files = options.files || new Map();
99
- for (const [url, dir] of options.lists || []) await fetchListed(url, dir, files);
100
- for (const [url, dir] of options.packs || []) unpack(await fetchBytes(url), dir, files);
101
-
102
- const names = text.decode(await fetchBytes(`${base}program`)).split('\n').filter((n) => n !== '');
103
123
  const wasm = new Map();
104
- await Promise.all(names.map(async (name) => {
105
- wasm.set(name, await fetchBytes(`${base}${encodeURIComponent(name)}.wasm`));
106
- }));
107
- let { modules } = options;
108
- if (typeof modules === 'function') {
124
+ const names = fetchProgram(base, wasm);
125
+ const ask = options.modules;
126
+ const modules = typeof ask !== 'function' ? ask : names.then(() => {
109
127
  const wanted = new Set();
110
128
  for (const bytes of wasm.values()) for (const name of importedModules(bytes)) wanted.add(name);
111
- modules = await modules(wanted);
112
- }
113
- // The compiled modules for sessions: the compiled file lists them, each one is in <hash>.subc, and compiled.map holds
114
- // their module map.
115
- const compiled = new Map();
116
- const listed = await fetch(`${base}compiled`);
117
- if (listed.ok) {
118
- const list = new Uint8Array(await listed.arrayBuffer());
119
- const hashes = text.decode(list).split('\n').filter((l) => l !== '').map((l) => l.slice(0, l.indexOf(' ')));
120
- compiled.set('compiled', list);
121
- if (hashes.length > 0) compiled.set('compiled.map', await fetchBytes(`${base}compiled.map`));
122
- await Promise.all(hashes.map(async (h) => compiled.set(`${h}.subc`, await fetchBytes(`${base}${h}.subc`))));
123
- }
124
- return run({ ...options, files, modules, compiled }, names, wasm);
129
+ return ask(wanted);
130
+ });
131
+ const [listed, given, compiled, urls] = await Promise.all([
132
+ names,
133
+ modules,
134
+ fetchCompiled(base),
135
+ options.assets ? fetchAssets(options.assets) : new Map(),
136
+ ...(options.packs || []).map(async ([url, dir]) => unpack(await fetchBytes(url), dir, files)),
137
+ ]);
138
+ return run({ ...options, files, modules: given, compiled, urls }, listed, wasm);
125
139
  }
126
140
 
127
141
  export function run(options, names, wasm) {
@@ -136,9 +150,16 @@ export function run(options, names, wasm) {
136
150
  const output = options.output || ((fd, bytes) => write(fd, decoders[fd].decode(bytes, { stream: true })));
137
151
 
138
152
  const app = { files, cwd, entry: null, ended: false, realTime: [] };
153
+ // app.url gives the URL to load a file from when it is not in memory.
154
+ const site = options.site || (globalThis.document ? document.baseURI : globalThis.location.href);
155
+ const urls = options.urls || new Map();
156
+ app.url = (name) => {
157
+ const path = resolvePath(cwd, name);
158
+ return new URL((urls.get(path) || path.slice(1)).split('/').map(encodeURIComponent).join('/'), site).href;
159
+ };
139
160
  const modules = new Map();
140
161
  for (const [name, make] of Object.entries(options.modules || {})) modules.set(name, (caller) => make(caller, app));
141
- if (!modules.has('fs')) modules.set('fs', fsModule(pageFiles(files, cwd, grants)));
162
+ if (!modules.has('fs')) modules.set('fs', fsModule(memoryFiles(files, cwd, grants)));
142
163
  const report = (message) => output(2, utf8.encode(`error: ${message}\n`));
143
164
  const ended = (code, reason) => {
144
165
  app.ended = true;