volaro 0.1.0-alpha.6 → 0.1.0-alpha.8

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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Volaro
2
2
 
3
- **Limited alpha** (this is `0.1.0-alpha.6`), published under both the
3
+ **Limited alpha** (this is `0.1.0-alpha.8`), published under both the
4
4
  `latest` and `alpha` dist-tags: `npm install volaro`.
5
5
 
6
6
  Volaro is an experimental application language intended for AI authoring and
package/bin/vl.js CHANGED
@@ -69,6 +69,11 @@ function pyInvocation(pp, moduleName, moduleArgs) {
69
69
  ...process.env,
70
70
  PYTHONPATH: [pp, VALIDATOR_PP, process.env.PYTHONPATH].filter(Boolean).join(delimiter),
71
71
  PYTHONDONTWRITEBYTECODE: "1",
72
+ // Diagnostics and build summaries contain non-ASCII (box drawing, arrows).
73
+ // Without this, Windows encodes a redirected stdout with the legacy code
74
+ // page and the compiler dies with UnicodeEncodeError; the capturing path
75
+ // below also decodes the child's output as UTF-8.
76
+ PYTHONIOENCODING: "utf-8",
72
77
  };
73
78
  return { cmd: py.cmd, argv: ["-B", "-m", moduleName, ...moduleArgs], env };
74
79
  }
@@ -744,9 +749,10 @@ COMMANDS
744
749
  -o <dir> output directory
745
750
  --data <file> data.json to bundle
746
751
  --release fail on unresolved placeholders (e.g. img alt_todo:)
752
+ and ship the minified runtime
747
753
  --benchmark-log <file> append raw timing records as JSONL
748
754
  --benchmark-run <id> group attempts into one clean/recovery trial
749
- dev [file.vl] Build, serve on http://127.0.0.1:5173, and rebuild
755
+ dev [file.vl] Build, serve on http://127.0.0.1:5173, and rebuild
750
756
  when a .vl file changes. Defaults to ./app.vl. A
751
757
  full-stack build runs server.js instead (Node 22.5+)
752
758
  and restarts it after each successful rebuild.
@@ -1,6 +1,6 @@
1
1
  {
2
- "revision": "78304efecb7e728025c959e1fdf207749b3652ed",
2
+ "revision": "d2c835d1882b9ba794a8577752882c0e34dce872",
3
3
  "source": "git",
4
4
  "dirty": false,
5
- "content_sha256": "1747ce8d53c4ae2caed129b014f01f7e4af5619ab5358a833e0c888e940c830e"
5
+ "content_sha256": "35f3e74ec054f9136bf783eed2f30674cb73db5bc9e4aed9a5d0e131186f7713"
6
6
  }
@@ -1 +1 @@
1
- 78304efecb7e728025c959e1fdf207749b3652ed
1
+ d2c835d1882b9ba794a8577752882c0e34dce872
@@ -2851,7 +2851,10 @@ class Checker:
2851
2851
  if st is None:
2852
2852
  return
2853
2853
  if isinstance(st, A.Let):
2854
- self._expr(st.init, scopes, fallible)
2854
+ if isinstance(st.init, A.Try):
2855
+ self._try(st.init, scopes, fallible, in_loop)
2856
+ else:
2857
+ self._expr(st.init, scopes, fallible)
2855
2858
  self._declare(st.name, scopes, st)
2856
2859
  if st.name and st.name != "?" and isinstance(scopes[-1], _Scope):
2857
2860
  scopes[-1].kinds[st.name] = "mutable let" if st.mut else "`let` binding"
@@ -2889,10 +2892,7 @@ class Checker:
2889
2892
  elif isinstance(st, A.Spawn):
2890
2893
  self._block(st.body, scopes, fallible, in_loop=False)
2891
2894
  elif isinstance(st, A.Try):
2892
- self._expr(st.expr, scopes, fallible)
2893
- for pat, body in st.catches:
2894
- inner = scopes + [_Scope(getattr(pat, "binds", []), "catch binding")]
2895
- self._scoped(body, inner, fallible, in_loop)
2895
+ self._try(st, scopes, fallible, in_loop)
2896
2896
  elif isinstance(st, (A.Break, A.Skip)):
2897
2897
  if not in_loop:
2898
2898
  kw = "break" if isinstance(st, A.Break) else "skip"
@@ -2986,6 +2986,23 @@ class Checker:
2986
2986
  def _block(self, stmts, scopes: list[set], fallible: bool, in_loop: bool = False) -> None:
2987
2987
  self._scoped(stmts, scopes + [_Scope()], fallible, in_loop)
2988
2988
 
2989
+ def _try(self, t: A.Try, scopes: list[set], fallible: bool, in_loop: bool) -> None:
2990
+ """`try EXPR` / `catch ...` in either position (spec §6.2): the
2991
+ statement form and `let NAME = try EXPR`."""
2992
+ if isinstance(t.expr, A.TryOp):
2993
+ # `try call()?` reads two ways and neither helps: `?` says
2994
+ # "propagate to the caller", `try`/`catch` says "handle it here".
2995
+ # The view build ignores the `?` (the catch arms run), so at best
2996
+ # it is redundant. Seen twice in Testing Lab login runs.
2997
+ self._d("W-TRY-PROPAGATE", Severity.WARNING,
2998
+ "'?' inside 'try' is redundant -- the catch arms below "
2999
+ "already handle the error, so nothing is propagated", t.expr,
3000
+ "remove the trailing '?'")
3001
+ self._expr(t.expr, scopes, fallible)
3002
+ for pat, body in t.catches:
3003
+ inner = scopes + [_Scope(getattr(pat, "binds", []), "catch binding")]
3004
+ self._scoped(body, inner, fallible, in_loop)
3005
+
2989
3006
  def _expr(self, ex, scopes: list[set], fallible: bool) -> None:
2990
3007
  if ex is None:
2991
3008
  return
@@ -3022,6 +3039,12 @@ class Checker:
3022
3039
  elif isinstance(ex, A.Range):
3023
3040
  self._expr(ex.lo, scopes, fallible)
3024
3041
  self._expr(ex.hi, scopes, fallible)
3042
+ elif isinstance(ex, A.Try):
3043
+ # expression-position `try` anywhere but a `let` initializer (which
3044
+ # `_stmt` routes to `_try` with the real loop context). `in_loop`
3045
+ # is assumed true here so a `break` / `skip` in such an arm is never
3046
+ # misreported; the emitter rejects the shapes it cannot lower.
3047
+ self._try(ex, scopes, fallible, in_loop=True)
3025
3048
  elif isinstance(ex, A.TryOp):
3026
3049
  if not fallible:
3027
3050
  self._d("E-PROP-FALLIBLE", Severity.ERROR,
@@ -70,7 +70,10 @@ class Diagnostic:
70
70
  f"{pad}│ {' ' * (col - 1)}{'^' * span}",
71
71
  ]
72
72
  if self.fix:
73
- out.append(f"{pad}= fix: {self.fix}")
73
+ # A multi-line fix (a code snippet) keeps its own indentation under
74
+ # the first line, instead of restarting at column 0.
75
+ head = f"{pad}= fix: "
76
+ out.append(head + self.fix.replace("\n", "\n" + " " * len(head)))
74
77
  return "\n".join(out)
75
78
 
76
79
  def to_json(self, li: LineIndex) -> dict:
@@ -17,7 +17,9 @@ type only where the type is written down or trivially known, and reports:
17
17
  R-MISSING-ARG required parameter (no default, not optional-typed) not supplied
18
18
  R-FIELD `x.f` where x's type is a known record / model and f is neither a field nor a method
19
19
  R-MATCH `match` over a value of known union type, not exhaustive, no `_`
20
- R-AUTH-METHOD `auth.f(...)` where `f` is not one of the eight §11.3 calls
20
+ E-AUTH-METHOD `auth.f(...)` where `f` is not one of the eight §11.3 calls
21
+ (an E- code, not R-: `vlbuild` reports the same mistake
22
+ under the same code, so the check and build output match)
21
23
 
22
24
  Deliberately conservative — an unknown callee, an unknown receiver type, an
23
25
  element attribute value, or a `std.*` function (the spec gives no signature, and
@@ -39,7 +41,9 @@ exactly in `_route_name`.
39
41
 
40
42
  from __future__ import annotations
41
43
 
44
+ import difflib
42
45
  import os
46
+ import re
43
47
 
44
48
  from . import ast_nodes as A
45
49
  from .checks import BUILTINS, _PRIM_NAMES
@@ -93,6 +97,76 @@ _AUTH_API = {
93
97
  "verify_totp": (("code", True),),
94
98
  }
95
99
 
100
+ # The §11.3 calls the browser build target lowers today. The language accepts
101
+ # all eight above; `start_oauth` and `verify_totp` still fail `vlbuild`
102
+ # (E-AUTH-UNSUPPORTED) because OAuth and TOTP are not built (ROADMAP.md,
103
+ # features 04/05). Kept here, beside `_AUTH_API`, so `vlbuild`'s fail-closed
104
+ # check (`emit.py` `precheck`) and this checker's fix text read one list and
105
+ # cannot drift apart. Ordered for display: the password lifecycle.
106
+ AUTH_BUILT = ("sign_up", "sign_in", "sign_out", "reset_password",
107
+ "confirm_reset", "confirm_verify")
108
+
109
+ # Names agents carry over from other frameworks, mapped to the §11.3 call that
110
+ # does that job. Consulted before the spelling fallback so `auth.login`
111
+ # suggests `auth.sign_in`, which no edit-distance match would find.
112
+ _AUTH_ALIASES = {
113
+ "login": "sign_in", "log_in": "sign_in", "signin": "sign_in",
114
+ "authenticate": "sign_in", "sign_in_with_password": "sign_in",
115
+ "signup": "sign_up", "register": "sign_up", "create_account": "sign_up",
116
+ "create_user": "sign_up", "sign_up_with_password": "sign_up",
117
+ "logout": "sign_out", "log_out": "sign_out", "signout": "sign_out",
118
+ "forgot_password": "reset_password", "request_password_reset": "reset_password",
119
+ "send_password_reset": "reset_password", "reset": "reset_password",
120
+ "set_password": "confirm_reset", "update_password": "confirm_reset",
121
+ "verify_email": "confirm_verify", "confirm_email": "confirm_verify",
122
+ "verify": "confirm_verify",
123
+ "totp": "verify_totp", "verify_code": "verify_totp", "verify_otp": "verify_totp",
124
+ "oauth": "start_oauth", "sign_in_with_oauth": "start_oauth",
125
+ "login_with": "start_oauth",
126
+ }
127
+
128
+
129
+ def auth_signature(method: str) -> str:
130
+ """`auth.sign_in(email:, password:)` -- the §11.3 call as the spec's table
131
+ writes it (`start_oauth` takes its provider positionally)."""
132
+ params = _AUTH_API[method]
133
+ if method == "start_oauth":
134
+ return "auth.start_oauth(provider)"
135
+ return "auth.%s(%s)" % (method, ", ".join(n + ":" for n, _ in params))
136
+
137
+
138
+ def auth_method_suggestion(name: str) -> str | None:
139
+ """The §11.3 call an unknown `auth.<name>` most likely meant, or None.
140
+
141
+ Shared with `vlbuild` so both tools suggest the same call. Tries, in
142
+ order: the name in snake_case (`signIn` -> `sign_in`), a known alias from
143
+ another framework (`login` -> `sign_in`), then a close spelling
144
+ (`confirm_verfiy` -> `confirm_verify`)."""
145
+ key = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "_", name).lower()
146
+ if key in _AUTH_API:
147
+ return key
148
+ if key in _AUTH_ALIASES:
149
+ return _AUTH_ALIASES[key]
150
+ close = difflib.get_close_matches(key, list(_AUTH_API), n=1, cutoff=0.75)
151
+ return close[0] if close else None
152
+
153
+
154
+ def auth_method_diagnostic(name: str) -> tuple[str, str]:
155
+ """(message, fix) for an unknown `auth.<name>`. The checker prints them as
156
+ its message and `= fix:` line; `vlbuild`'s E-AUTH-METHOD joins the same two
157
+ strings, so both tools say the same thing."""
158
+ guess = auth_method_suggestion(name)
159
+ msg = "'auth.%s' is not part of the auth API" % name
160
+ msg += (" -- did you mean 'auth.%s'?" % guess) if guess else "."
161
+ msg += (" This build supports auth.%s and %s; verify_totp and "
162
+ "start_oauth are in the spec but not built"
163
+ % (", ".join(AUTH_BUILT[:-1]), AUTH_BUILT[-1]))
164
+ fix = auth_signature(guess) if guess else "use one of the calls listed above"
165
+ fix += ("; to call an existing HTTP endpoint such as /api/login, declare "
166
+ "an `api` block instead")
167
+ return msg, fix
168
+
169
+
96
170
  # member names that are methods, not record fields — never flag `x.<this>`
97
171
  _METHODS = {
98
172
  "len", "format", "reload", "loading", "all", "first", "get", "where",
@@ -567,6 +641,16 @@ class Resolver:
567
641
  self._expr(ex.hi, scopes)
568
642
  elif isinstance(ex, A.TryOp):
569
643
  self._expr(ex.operand, scopes)
644
+ elif isinstance(ex, A.Try):
645
+ # `let x = try CALL` / `x = try CALL`: the expression-position
646
+ # form (spec §6.2). Walked exactly like the statement form in
647
+ # `_stmt`. Without this branch nothing inside it was resolved, so
648
+ # `let r = try auth.login(...)` passed `volaro check` and only
649
+ # `vlbuild` refused it (Testing Lab 2026-09-27, volaro-F2).
650
+ self._expr(ex.expr, scopes)
651
+ for pat, body in ex.catches:
652
+ inner = scopes + [{b: None for b in getattr(pat, "binds", [])}]
653
+ self._scoped(body, inner)
570
654
  elif isinstance(ex, A.IfExpr):
571
655
  self._expr(ex.cond, scopes)
572
656
  self._expr(ex.then, scopes)
@@ -709,10 +793,8 @@ class Resolver:
709
793
  return
710
794
  params = _AUTH_API.get(m)
711
795
  if params is None:
712
- self._d("R-AUTH-METHOD",
713
- f"'auth.{m}' is not part of the auth API", ex,
714
- "the §11.3 calls are: "
715
- + ", ".join(sorted(_AUTH_API)))
796
+ msg, fix = auth_method_diagnostic(m)
797
+ self._d("E-AUTH-METHOD", msg, ex, fix)
716
798
  return
717
799
  # Reuse the ordinary signature check: positional overflow, unknown
718
800
  # named argument, and missing required argument all report the same way
@@ -31,8 +31,6 @@ from vlcheck import ast_nodes as A # noqa: E402
31
31
  from .emit import BuildError, Emitter # noqa: E402
32
32
  from .server_emit import ServerEmitter # noqa: E402
33
33
 
34
- _ASSETS = Path(__file__).resolve().parent / "assets"
35
-
36
34
 
37
35
  def _html(src_name: str, title: str | None = None, styled: bool = False) -> str:
38
36
  # spec §8.11: the `page title:` becomes the document <title> when it is a
@@ -175,10 +173,9 @@ def build_file(src_path: Path, out_dir: Path, data_path: Path | None,
175
173
  bundle.mkdir(parents=True, exist_ok=True)
176
174
  copy_assets(asset_plan, bundle)
177
175
  (bundle / "app.js").write_text(app_js, encoding="utf-8")
178
- (bundle / "vlrt.js").write_text((_ASSETS / "vlrt.js").read_text(encoding="utf-8"),
179
- encoding="utf-8")
180
- (bundle / "vlrt.css").write_text((_ASSETS / "vlrt.css").read_text(encoding="utf-8"),
181
- encoding="utf-8")
176
+ # --release ships the committed minified runtime; see runtime_assets.py.
177
+ from .runtime_assets import write_runtime
178
+ write_runtime(bundle, ("vlrt.js", "vlrt.css"), release)
182
179
  if app_css is not None:
183
180
  (bundle / "app.css").write_text(app_css, encoding="utf-8")
184
181
  (bundle / "index.html").write_text(
@@ -231,7 +228,8 @@ def main(argv: list[str]) -> int:
231
228
  ap.add_argument("--data", type=Path, default=None,
232
229
  help="data.json to bundle (default: look next to the source)")
233
230
  ap.add_argument("--release", action="store_true",
234
- help="fail on unresolved dev placeholders (e.g. img alt_todo:)")
231
+ help="fail on unresolved dev placeholders (e.g. img alt_todo:) "
232
+ "and ship the minified runtime")
235
233
  args = ap.parse_args(argv)
236
234
 
237
235
  src_path: Path = args.source
@@ -264,7 +262,8 @@ def main(argv: list[str]) -> int:
264
262
  try:
265
263
  config = load_project_config(src_path, args.config)
266
264
  client_manifest = build_project(src_path, out_dir, config=config,
267
- warnings=warnings)
265
+ warnings=warnings,
266
+ release=args.release)
268
267
  except BuildError as exc:
269
268
  raise SystemExit("vlbuild: %s" % exc)
270
269
  print("vlbuild: wrote %s/public/" % out_dir)
@@ -0,0 +1,2 @@
1
+ /* minified vlrouter.js; readable source in the volaro package */
2
+ !function(t){"use strict";var n=t.VL;var e={text:function(t){return t.length>0},int:function(t){return/^-?[0-9]+$/.test(t)},uuid:function(t){return/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(t)},slug:function(t){return/^[a-z0-9]+(-[a-z0-9]+)*$/.test(t)}};function r(t,n){return(e[t]||e.text)(n)?"int"===t?{ok:!0,value:parseInt(n,10)}:{ok:!0,value:n}:{ok:!1}}function o(t,n){var e=function(t){var n="/"===t?"/":t.replace(/\/+$/,"");if(""===n||"/"===n)return[];for(var e=n.replace(/^\/+/,"").split("/"),r=[],o=0;o<e.length;o++){var a;try{a=decodeURIComponent(e[o])}catch(t){return null}if(-1!==a.indexOf("/")||-1!==a.indexOf("\\"))return null;r.push(a)}return r}(n);if(null===e)return null;for(var r=0;r<t.pages.length;r++){var o=t.pages[r];if(o.segments.length===e.length){for(var a=!0,i=0;i<e.length;i++){var l=o.segments[i][0];if("static"===l&&o.segments[i][1]!==e[i]){a=!1;break}if("static"!==l&&"dynamic"!==l){a=!1;break}}if(a)return{page:o,rawSegments:e}}}return null}function a(t,n){for(var e={},r=0;r<n.length;r++)e[n[r]]=t[n[r]];return e}function i(t,n){var e=Object.keys(t),r=Object.keys(n);if(e.length!==r.length)return!1;for(var o=0;o<e.length;o++)if(t[e[o]]!==n[e[o]])return!1;return!0}var l=Date.now().toString(36)+Math.random().toString(36).slice(2,6)+".",u=0;function s(){return l+ ++u}function c(e,r){this.manifest=e,this.base=r||"",this.stack=[],this.generation=0,this.scrollMemory=Object.create(null),this.currentPage=null,this._currentKey=null,this._renderedUrl=null,this._nav=null,this._pending=null,this._announceTimer=null,this._announcer=function(t){if(!t||!t.body)return null;var n=t.getElementById("vl-announcer");return n||((n=t.createElement("div")).id="vl-announcer",n.setAttribute("role","status"),n.setAttribute("aria-live","polite"),n.setAttribute("aria-atomic","true"),n.style.cssText="position:absolute;width:1px;height:1px;margin:-1px;padding:0;border:0;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;",t.body.appendChild(n),n)}(t.document);var o=this;n._onUnhandledLoadError=function(t){o._handleUnhandledLoadError(t)},n._onLoadSettled=function(){queueMicrotask(function(){o._checkPending()})}}function d(){var t=document.activeElement;return!t||t===document.body||!document.contains(t)}function h(){var t=document.getElementById("vl-main")||document.querySelector("main");t&&"function"==typeof t.focus&&t.focus({preventScroll:!0})}function f(t){var n=t.hash&&document.getElementById(function(t){try{return decodeURIComponent(t)}catch(n){return t}}(t.hash.slice(1)));n?n.scrollIntoView():window.scrollTo(0,0)}function p(t,n){return n<t.layouts.length?(t.boundary_for_layout_failure||[])[n]:t.boundaries.error}c.prototype._announce=function(t){var n=this._announcer;t=String(t||"").replace(/\s+/g," ").trim(),n&&t&&(clearTimeout(this._announceTimer),n.textContent="",this._announceTimer=setTimeout(function(){n.textContent=t},100))},c.prototype._announcePage=function(t){t&&(n._mainLabel=t,n.wireSkipLink(),this._announce(t))},c.prototype._stackTitle=function(){for(var t=this.stack.length-1;t>=0;t--)if(this.stack[t].entryTitle)return this.stack[t].entryTitle;return null},c.prototype._moduleUrl=function(t){return this.base+t+".js"},c.prototype._unmountFrom=function(t){for(var n=this.stack.length-1;n>=t;n--){var e=this.stack[n];e.host&&e.host._abortController&&e.host._abortController.abort(),e.el&&e.el.parentNode&&e.el.parentNode.removeChild(e.el)}this.stack.length=t},c.prototype._childrenSlotOf=function(t){return t?t.querySelector("[data-vl-children]"):null},c.prototype._keyCurrentEntry=function(){var t=history.state;if(t&&null!=t.vlNav)this._currentKey=t.vlNav;else{var n=s();try{history.replaceState(Object.assign({},t,{vlNav:n}),"")}catch(t){}this._currentKey=n}},c.prototype._saveScroll=function(){null!=this._currentKey&&(this.scrollMemory[this._currentKey]={x:window.scrollX,y:window.scrollY})},c.prototype._persistScroll=function(){this._saveScroll();var t=history.state;if(t&&t.vlNav===this._currentKey)try{history.replaceState(Object.assign({},t,{vlScroll:{x:window.scrollX,y:window.scrollY}}),"")}catch(t){}},c.prototype._savedScrollFor=function(t){if(null!=t&&this.scrollMemory[t])return this.scrollMemory[t];var n=history.state;return n&&n.vlNav===t&&n.vlScroll||null},c.prototype.navigate=function(t,n){n=n||{};var e=this,a=++this.generation,i=!!n.isPopstate,l=!!n.isInitial,u=new URL(t,location.href),c=u.pathname;l?this._keyCurrentEntry():i||this._persistScroll(),this._cancelPending();var d={gen:a,isInitial:l,loadingShown:!1};this._nav=d;var p,m=o(this.manifest,c);if(null===m)p=this._renderGlobalNotFound(a);else{var g=function(t,n){for(var e={},o=[],a=0;a<t.segments.length;a++){var i=t.segments[a][0],l=t.segments[a][1];if("dynamic"===i){var u=r(t.param_types&&t.param_types[l]||"text",n[a]);u.ok?e[l]=u.value:o.push(l)}}return{params:e,bad:o}}(m.page,m.rawSegments);p=g.bad.length?this._renderParamNotFound(m.page,g,a):this._renderChain(m.page,g.params,a)}return p.then(function(){if(a===e.generation){if(e._nav=null,!l&&!i){var t=s();history.pushState({vlNav:t},"",u.pathname+u.search+u.hash),e._currentKey=t}e._renderedUrl=u.pathname+u.search;var n=e._stackTitle();n&&(document.title=n);var r=i||l?e._savedScrollFor(e._currentKey):null;r?window.scrollTo(r.x,r.y):f(u),h(),l&&!d.loadingShown||e._announcePage(n)}})},c.prototype._renderGlobalNotFound=function(t){var n=this.manifest.global_not_found_layouts||[],e={layouts:n,layout_params:n.map(function(){return[]}),boundary_for_layout_failure:n.map(function(){return null}),boundaries:{error:null}};return this._renderNotFoundIn(e,n.length,{},this.manifest.global_not_found,t)},c.prototype._renderParamNotFound=function(t,n,e){for(var r=Math.min(t.not_found_layouts||0,t.layouts.length),o=0;o<r;){for(var a=t.layout_params[o]||[],i=!0,l=0;l<a.length;l++)if(-1!==n.bad.indexOf(a[l])){i=!1;break}if(!i)break;o++}return this._renderNotFoundIn(t,o,n.params,t.boundaries.not_found,e)},c.prototype._renderNotFoundIn=function(t,n,e,r,o){this.currentPage=t;var i=t.layouts.slice(0,n).map(function(n,r){return{moduleId:n,kind:"layout",params:a(e,t.layout_params[r]||[])}}).concat([{moduleId:r||null,kind:"boundary",params:{},fallback:m}]);return this._renderDesired(i,o,function(e){return e<n?{id:(t.boundary_for_layout_failure||[])[e],fallback:g}:{id:null,fallback:m}})},c.prototype._renderChain=function(t,n,e){this.currentPage=t;var r=t.layouts.map(function(e,r){return{moduleId:e,kind:"layout",params:a(n,t.layout_params[r]||[])}}).concat([{moduleId:t.module,kind:"page",params:n,loading:t.boundaries.loading}]);return this._renderDesired(r,e,function(n){return{id:p(t,n),fallback:g}})},c.prototype._renderDesired=function(t,n,e){for(var r=0;r<t.length&&r<this.stack.length;){var o=this.stack[r],a=t[r];if(o.moduleId!==a.moduleId||o.kind!==a.kind||!i(o.params,a.params)||o.el&&o.el.hidden)break;r++}this._unmountFrom(r);var l=0===r?document.getElementById("vl-root"):this._childrenSlotOf(this.stack[r-1].el);return this._mountFrom(t,r,l,n,e)},c.prototype._mountFrom=function(t,e,r,o,a){var i=this;if(e>=t.length)return Promise.resolve();var l=t[e];if("boundary"===l.kind&&!l.moduleId)return r&&this._mountFallback(r,l.fallback),Promise.resolve();var u="page"===l.kind&&l.loading&&r?this._beginLoadingWait(l.loading,r,o):null,s=function(){u&&u.cancel();var t=a(e);return i._mountBoundaryInto(t.id,r,o,t.fallback)};return n.loadModule(l.moduleId,this._moduleUrl(l.moduleId)).then(function(c){if(o!==i.generation||!r)return null;var d,h=document.createElement("div");u&&(h.hidden=!0),r.appendChild(h);try{d=n.mount(c.render,h,l.params)}catch(t){return r.removeChild(h),s()}var f={moduleId:l.moduleId,kind:l.kind,host:d,el:h,params:l.params,entryTitle:c.title||l.fallback&&l.fallback.title};return i.stack.push(f),u?u.waitFor(f):i._mountFrom(t,e+1,i._childrenSlotOf(h),o,a)},function(){return o!==i.generation?null:s()})},c.prototype._beginLoadingWait=function(t,n,e){var r,o=this,a={gen:e,frame:null,loadingEl:null,loadingHost:null,done:!1,replacing:!1},i=new Promise(function(t){r=t}),l=setTimeout(function(){o._showLoading(a,t,n)},300);function u(){clearTimeout(l),a.done=!0,o._pending===a&&(o._pending=null),a.loadingHost&&a.loadingHost._abortController&&a.loadingHost._abortController.abort(),a.loadingEl&&a.loadingEl.parentNode&&a.loadingEl.parentNode.removeChild(a.loadingEl)}return a.cancel=function(){a.done||(u(),r())},a.reveal=function(){a.done||(u(),a.frame&&a.frame.el&&(a.frame.el.hidden=!1),r())},a.waitFor=function(t){return a.frame=t,o._checkPending(),i},this._pending=a,a},c.prototype._cancelPending=function(){this._pending&&this._pending.cancel()},c.prototype._checkPending=function(){var t=this._pending;t&&!t.done&&t.frame&&!t.replacing&&(t.gen===this.generation?-1!==this.stack.indexOf(t.frame)&&function(t){if(!t||!t._loads)return!1;for(var n in t._loads)if(t._loads[n]&&t._loads[n].pending)return!0;return!1}(t.frame.host)||t.reveal():t.cancel())},c.prototype._showLoading=function(t,e,r){var o=this;t.done||t.gen!==this.generation||n.loadModule(e,this._moduleUrl(e)).then(function(e){if(!t.done&&t.gen===o.generation){var a=document.createElement("div");r.appendChild(a);var i=n._appEl;try{t.loadingHost=n.mount(e.render,a,{})}catch(t){return void r.removeChild(a)}finally{n._appEl=i}t.loadingEl=a;var l=o._nav;l&&l.gen===t.gen&&(l.loadingShown=!0,d()&&h(),l.isInitial||o._announce(a.innerText||a.textContent))}},function(){})};var m={title:"Not found",heading:"Not found"},g={title:"Error",text:"Something went wrong."};c.prototype._mountFallback=function(t,e){var r=t.closest&&t.closest("main"),o=document.createElement(r?"div":"main");if(r||(o.id="vl-main",o.tabIndex=-1),e.heading){var a=document.createElement("h1");a.textContent=e.heading,o.appendChild(a)}else o.textContent=e.text;t.appendChild(o),this.stack.push({moduleId:null,kind:"boundary",host:null,el:o,params:{},entryTitle:e.title}),n.wireSkipLink()},c.prototype._mountBoundaryInto=function(t,e,r,o){var a=this;return t?n.loadModule(t,this._moduleUrl(t)).then(function(i){if(r===a.generation&&e){var l,u=document.createElement("div");e.appendChild(u);try{l=n.mount(i.render,u,{})}catch(t){return e.removeChild(u),void a._mountFallback(e,o)}a.stack.push({moduleId:t,kind:"boundary",host:l,el:u,params:{},entryTitle:i.title||o.title})}},function(){r===a.generation&&e&&a._mountFallback(e,o)}):(e&&this._mountFallback(e,o),Promise.resolve())},c.prototype._handleUnhandledLoadError=function(t){for(var n=this,e=-1,r=0;r<this.stack.length;r++)if(this.stack[r].host===t){e=r;break}if(-1!==e&&this.currentPage){var o=this.generation,a=0===e?document.getElementById("vl-root"):this._childrenSlotOf(this.stack[e-1].el),i=this.currentPage,l=this._pending,u=!!(l&&!l.done&&l.frame&&this.stack.indexOf(l.frame)>=e);u&&(l.replacing=!0),this._unmountFrom(e),this._mountBoundaryInto(p(i,e),a,o,g).then(function(){if(o===n.generation){if(u)return l.replacing=!1,void l.reveal();var t=n._stackTitle();t&&(document.title=t),d()&&h(),n._announcePage(t)}})}},c.prototype.interceptLinks=function(n){var e=this;"scrollRestoration"in history&&(history.scrollRestoration="manual"),(n||document).addEventListener("click",function(t){if(!(t.defaultPrevented||0!==t.button||t.metaKey||t.ctrlKey||t.shiftKey||t.altKey)){var n=t.target.closest&&t.target.closest("a");if(n&&n.href){var r;try{r=new URL(n.href,location.href)}catch(t){return}r.origin===location.origin&&(n.target&&""!==n.target&&"_self"!==n.target||(r.hash&&r.pathname===location.pathname&&r.search===location.search?e._persistScroll():(t.preventDefault(),e.navigate(r.pathname+r.search+r.hash))))}}}),t.addEventListener("popstate",function(){if(e._saveScroll(),e._keyCurrentEntry(),location.pathname+location.search!==e._renderedUrl)e.navigate(location.pathname+location.search+location.hash,{isPopstate:!0});else{var t=e._savedScrollFor(e._currentKey);t?window.scrollTo(t.x,t.y):f(new URL(location.href))}}),t.addEventListener("hashchange",function(){e._keyCurrentEntry()}),t.addEventListener("pagehide",function(){e._persistScroll()})},n.Router=c,n.createRouter=function(t,n){return new c(t,n)}}("undefined"!=typeof window?window:globalThis);
@@ -0,0 +1,2 @@
1
+ /* minified vlrt.css; readable source in the volaro package */
2
+ @layer vl-legacy{:root{--bg:#fbfbfa;--surface:#fff;--border:#e4e4e2;--ink:#1b1b1a;--muted:#6b6b68;--accent:#2f6f4f;--action-bg:#2f6f4f;--danger-bg:#b91c1c;--danger:#b91c1c;--warn:#b45309;color-scheme:light dark;accent-color:var(--accent)}@media (prefers-color-scheme:dark){:root{--bg:#16171a;--surface:#1e2024;--border:#33363c;--ink:#e9e9e7;--muted:#9a9a96;--accent:#6fcaa0;--danger:#e07070;--warn:#d99a3f}}*{box-sizing:border-box}body{background:var(--bg);color:var(--ink);margin:0;padding:24px;font:14px/1.5 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}code{color:var(--ink);background:var(--border);border-radius:3px;padding:1px 4px;font-size:12px}.vl-col{flex-direction:column;display:flex}.vl-row{flex-flow:wrap;display:flex}.vl-stack{display:grid}.vl-grid{grid-template-columns:repeat(auto-fit,minmax(150px,1fr));display:grid}.vl-text{display:inline-block}.vl-spacer{flex:1}.vl-fragment{display:contents}a.vl-link,a.vl-a{color:var(--accent);text-decoration:none}a.vl-link:hover,a.vl-a:hover{text-decoration:underline}input.vl-input,select.vl-select,textarea.vl-textarea{font:inherit;border:1px solid var(--border);background:var(--surface);color:var(--ink);border-radius:6px;padding:4px 8px}select.vl-select{min-height:32px}textarea.vl-textarea{resize:vertical;line-height:1.5}input.vl-input[aria-invalid=true],select.vl-select[aria-invalid=true],textarea.vl-textarea[aria-invalid=true]{border-color:var(--danger)}input.vl-input:is([type=checkbox],[type=radio],[type=range],[type=color]){background:0 0;border:0;padding:0}input.vl-input:is([type=checkbox],[type=radio]){width:16px;height:16px;margin:0}:is(input.vl-input,select.vl-select,textarea.vl-textarea,button.vl-button):disabled,fieldset.vl-fieldset:disabled{opacity:.55;cursor:not-allowed}.vl-field:has(>:disabled)>.vl-label{opacity:.55}.vl-field{flex-direction:column;gap:4px;display:flex}.vl-field>.vl-label{font-size:13px;font-weight:600}.vl-field>.vl-help{color:var(--muted);font-size:12px}.vl-field>.vl-error{white-space:normal;color:var(--danger);font-family:inherit;font-size:12px}.vl-field:has(>input.vl-input:is([type=checkbox],[type=radio])){grid-template-columns:auto 1fr;align-items:center;gap:2px 8px;display:grid}.vl-field:has(>input.vl-input:is([type=checkbox],[type=radio]))>input{grid-area:1/1}.vl-field:has(>input.vl-input:is([type=checkbox],[type=radio]))>.vl-label{font-size:inherit;grid-area:1/2;font-weight:400}.vl-field:has(>input.vl-input:is([type=checkbox],[type=radio]))>:is(.vl-help,.vl-error){grid-column:2}button.vl-button{font:inherit;border:1px solid var(--border);background:var(--surface);color:var(--ink);cursor:pointer;border-radius:6px;padding:6px 12px}button.vl-button[data-variant=primary]{background:var(--action-bg);color:#fff;border-color:#0000}button.vl-button[data-variant=ghost]{color:var(--accent);background:0 0;border-color:#0000}button.vl-button[data-variant=danger]{background:var(--danger-bg);color:#fff;border-color:#0000}button.vl-button[aria-pressed=true]{border-color:var(--accent);box-shadow:inset 0 0 0 2px var(--accent)}button.vl-button:not(:disabled):hover{border-color:var(--accent)}:is(input.vl-input,select.vl-select,textarea.vl-textarea,button.vl-button,a.vl-link,a.vl-a,summary.vl-summary):focus-visible{outline:3px solid var(--ink);outline-offset:3px}:is(.vl-col,.vl-row,.vl-stack,.vl-grid,.vl-card)>:is(.vl-p,.vl-h1,.vl-h2,.vl-h3,.vl-h4,.vl-h5,.vl-h6,.vl-ul,.vl-ol,.vl-menu,.vl-dl,.vl-table,.vl-figure,.vl-blockquote,.vl-pre,.vl-hr,.vl-fieldset,.vl-form,.vl-details){margin:0}fieldset.vl-fieldset{border:1px solid var(--border);border-radius:8px;flex-direction:column;gap:10px;min-width:0;padding:12px 16px 16px;display:flex}legend.vl-legend{padding:0 4px;font-size:13px;font-weight:600}table.vl-table{border-collapse:collapse;font-variant-numeric:tabular-nums}caption.vl-caption{text-align:start;padding-bottom:6px;font-weight:600}:is(th.vl-th,td.vl-td){text-align:start;border-bottom:1px solid var(--border);padding:6px 12px}thead.vl-thead th.vl-th{border-bottom-width:2px}details.vl-details{border:1px solid var(--border);background:var(--surface);border-radius:8px;padding:8px 12px}summary.vl-summary{cursor:pointer;font-weight:600}details.vl-details[open]>summary.vl-summary{margin-bottom:8px}progress.vl-progress,meter.vl-meter{width:100%;max-width:320px}.vl-overlay{z-index:1000;position:fixed;inset:0}.vl-overlay-dialog{background:#00000073;justify-content:center;align-items:center;padding:24px;display:flex}.vl-dialog{background:var(--surface);color:var(--ink);border:1px solid var(--border);border-radius:8px;width:100%;max-width:420px;padding:20px;box-shadow:0 8px 40px #00000040}.vl-dialog:focus{outline:none}.vl-dialog-title{margin:0 0 8px;font-size:18px;font-weight:700}.vl-tablist{border-bottom:1px solid var(--border);flex-direction:row;gap:4px;display:flex}.vl-tab{font:inherit;color:var(--muted);cursor:pointer;background:0 0;border:none;border-bottom:2px solid #0000;padding:6px 12px}.vl-tab[aria-selected=true]{color:var(--ink);border-bottom-color:var(--accent)}.vl-tab:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.vl-tabpanel{padding:16px 0}.vl-tabpanel:focus{outline:none}.vl-spinner{color:var(--muted);padding:24px}.vl-error{color:var(--danger);white-space:pre-wrap;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}.vl-built-note{color:var(--muted);margin-top:32px;font-size:12px}.vl-skip{z-index:1000;background:var(--action-bg,#2f6f4f);color:#fff;border-radius:6px;padding:8px 14px;text-decoration:none;transition:top .15s ease-in-out;position:absolute;top:-60px;left:8px}.vl-skip:focus{outline:3px solid var(--ink,#111);outline-offset:2px;top:8px}:where(main):focus{outline:none}}@layer theme,utilities;
@@ -0,0 +1,2 @@
1
+ /* minified vlrt.js; readable source in the volaro package */
2
+ "use strict";!function(e){var t={},n=null;function r(e){return null==e?[]:e}t.fetchData=function(){return n||(n=e.fetch("data.json",{cache:"no-store"}).then(function(e){if(!e.ok)throw new Error("HTTP "+e.status+" loading data.json");return e.json()})),n},t.db={},t.qb=function(e){var t=(e||[]).slice(),n={where:function(e){var r={lt:function(e,t){return e<t},lte:function(e,t){return e<=t},gt:function(e,t){return e>t},gte:function(e,t){return e>=t},ne:function(e,t){return e!==t}};return Object.keys(e||{}).forEach(function(n){var i=e[n];if(null!=i&&""!==i)if(i&&"object"==typeof i&&"string"==typeof i.op)if("between"===i.op)t=t.filter(function(e){return e[n]>=i.v[0]&&e[n]<=i.v[1]});else if("any_of"===i.op){var o=i.v||[];t=t.filter(function(e){return-1!==o.indexOf(e[n])})}else{var a=r[i.op];t=t.filter(function(e){return a(e[n],i.v)})}else t=t.filter(function(e){return e[n]===i})}),n},order:function(e){var r=Object.keys(e||{})[0];if(r){var i="desc"===e[r]?-1:1;t.sort(function(e,t){return e[r]<t[r]?-i:e[r]>t[r]?i:0})}return n},limit:function(e){return t=t.slice(0,e),n},all:function(){return t},first:function(){return t.length?t[0]:null}};return n},t.std={filter:function(e,t){return r(e).filter(function(e){return t(e)})},map:function(e,t){return r(e).map(function(e){return t(e)})},take:function(e,t){return r(e).slice(0,t)},chunk:function(e,t){for(var n=r(e),i=[],o=0;o<n.length;o+=t)i.push(n.slice(o,o+t));return i},count:function(e){return r(e).length},sum:function(e){return r(e).reduce(function(e,t){return e+(t||0)},0)},sort_by:function(e,t){return r(e).slice().sort(function(e,n){var r=t(e),i=t(n);return r<i?-1:r>i?1:0})},group_by:function(e,t){var n={};return r(e).forEach(function(e){var r=t(e);(n[r]=n[r]||[]).push(e)}),n},join:function(e,t){return r(e).join(void 0===t?"":t)},split:function(e,t){return String(e).split(t)},replace:function(e,t,n){return String(e).split(String(t)).join(String(n))},first_char:function(e){return String(e).slice(0,1)},trim:function(e){return String(e).trim()},upper:function(e){return String(e).toUpperCase()},lower:function(e){return String(e).toLowerCase()},keys:function(e){return Object.keys(e||{})},values:function(e){return Object.values(e||{})},get_or:function(e,t,n){return e&&t in e?e[t]:n},int_to_float:function(e){return e},float_to_int:function(e){return Math.trunc(e)},concat:function(e,t){return r(e).concat(r(t))},append:function(e,t){return r(e).concat([t])},matches:function(e,t){try{return new RegExp(String(t)).test(String(e))}catch(e){return!1}}},t.len=function(e){return null==e?0:e.length},t.err=function(e,t){var n=new Error(t||e);return n.code=e,n},t.decode=function(e,n,r){return function e(n,i,o){if(null==n||"object"!=typeof n)throw t.err("decode.missing_field",(r||"response")+": expected an object at "+(o||"/"));for(var a in i)if(Object.prototype.hasOwnProperty.call(i,a)){if(void 0===n[a])throw t.err("decode.missing_field",(r||"response")+": missing "+o+"/"+a);i[a]&&"object"==typeof i[a]&&e(n[a],i[a],o+"/"+a)}return n}(e,n||{},"")},t.api=async function(n,r,i,o,a){var l,s,u,c={method:n,headers:{},credentials:"same-origin"};if(void 0!==i&&(c.headers["Content-Type"]="application/json",c.body=JSON.stringify(i)),/^(POST|PUT|PATCH|DELETE)$/.test(n)&&e.document){var d=(l=new RegExp("(?:^|;\\s*)"+(o||"vl_csrf").replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"=([^;]+)"),(s=(e.document&&e.document.cookie||"").match(l))?decodeURIComponent(s[1]):null);d&&(c.headers["X-CSRF-Token"]=d)}try{u=await e.fetch(r,c)}catch(e){if(!a)throw e;var f=t.err("transport.network",n+" "+r+" -> network failure");throw f.kind="transport",f}if(!u.ok){var v=null;try{v=await u.json()}catch(e){}if(a){if(!v||"object"!=typeof v.error||null===v.error||"string"!=typeof v.error.code){var h=t.err("transport.malformed",n+" "+r+" -> "+u.status);throw h.kind="transport",h}var p=v.error.code,y=t.err(p,n+" "+r+" -> "+u.status),m=v.error.fields;throw y.fields=m&&"object"==typeof m?m:{},y.kind=a.indexOf(p)>=0?"domain":u.status>=500?"internal":"framework",y}var g=v&&v.error?v.error:404===u.status?"not_found":"http."+u.status,b=t.err(g,n+" "+r+" -> "+u.status);throw v&&"object"==typeof v&&Object.keys(v).forEach(function(e){"error"!==e&&(b[e]=v[e])}),b}return 204===u.status?null:u.json()},t.authApi=async function(n,r,i){async function o(){var n=await e.fetch("/__vl/auth/csrf",{method:"GET",credentials:"same-origin",cache:"no-store"});if(!n.ok)throw t.err("http."+n.status,"could not establish CSRF token")}await o();try{return await t.api(n,r,i,"vl_csrf_pre")}catch(e){if(e&&"forbidden"===e.code)return await o(),t.api(n,r,i,"vl_csrf_pre");throw e}};var i=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function o(e,n){if(null!=e&&!0!==e&&!1!==e){if(Array.isArray(e))throw t.err("render.multi_root_row","a keyed list row must be a single element; key "+JSON.stringify(n)+" maps to "+e.length+" sibling nodes -- wrap the row in one `col` / `row`");e.nodeType&&(e._vlKey=n)}}function a(e,t){null!=t&&!1!==t&&!0!==t&&(Array.isArray(t)?t.forEach(function(t){a(e,t)}):"undefined"!=typeof Node&&t instanceof Node||t&&t.nodeType?e.appendChild(t):e.appendChild(document.createTextNode(String(t))))}function l(){return t._appEl||e.document&&e.document.getElementById("vl-root")||null}function s(e){for(var n=0;n<t._overlays.length;n++)if(t._overlays[n].id===e)return n;return-1}function u(e,t){if(e)try{t?(e.inert=!0,e.setAttribute("aria-hidden","true")):(e.inert=!1,e.removeAttribute("aria-hidden"))}catch(e){}}function c(e,t){if(!e||!t)return!1;if("boolean"==typeof e.isConnected)return e.isConnected;if("function"==typeof t.contains)return t.contains(e);for(var n=e;n.parentNode;)n=n.parentNode;return n===t||n===t.body||n===t.documentElement}function d(e){if(!e||!e.querySelectorAll)return[];for(var t=e.querySelectorAll("a[href],button,input,select,textarea,[tabindex]"),n=[],r=0;r<t.length;r++){var i=t[r];!i.disabled&&(null==i.tabIndex||i.tabIndex>=0||"-1"!==i.getAttribute("tabindex"))&&n.push(i)}return n}function f(e){var n=t.overlay._dismiss[e];"function"==typeof n&&n()}function v(e,t){if(!e||e.nodeType!==t.nodeType||e.nodeName!==t.nodeName)return t;if(3===t.nodeType)return e.nodeValue!==t.nodeValue&&(e.nodeValue=t.nodeValue),e;if(1!==t.nodeType||!e.attributes||!t.attributes)return t;if(e.getAttribute("data-vl-bind")!==t.getAttribute("data-vl-bind"))return t;if(!t._vlHandlers||!e._vlHandlers)return t;Object.keys(e._vlHandlers).forEach(function(t){e.removeEventListener(t,e._vlHandlers[t])}),Object.keys(t._vlHandlers).forEach(function(n){e.addEventListener(n,t._vlHandlers[n])}),e._vlHandlers=t._vlHandlers;var n="DETAILS"===e.nodeName?"open":null;return Array.from(e.attributes).forEach(function(r){r.name!==n&&(t.hasAttribute(r.name)||e.removeAttribute(r.name))}),Array.from(t.attributes).forEach(function(t){t.name!==n&&e.getAttribute(t.name)!==t.value&&e.setAttribute(t.name,t.value)}),h(e,Array.from(t.childNodes)),t.hasAttribute&&t.hasAttribute("value")&&e.value!==t.value&&(e.value=t.value),t.hasAttribute&&t.hasAttribute("data-vl-bind")&&e.checked!==t.checked&&(e.checked=t.checked),e}function h(e,t){for(var n=0;n<t.length;n++)if(t[n]&&null!=t[n]._vlKey)return p(e,t);for(t.forEach(function(t,n){var r=e.childNodes[n],i=v(r,t);r?i!==r&&e.replaceChild(i,r):e.appendChild(i)});e.childNodes.length>t.length;)e.removeChild(e.lastChild)}function p(n,r){var i=Array.prototype.slice.call(n.childNodes),o=Object.create(null),a=[];i.forEach(function(e){e&&null!=e._vlKey?e._vlKey in o||(o[e._vlKey]=e):a.push(e)});var l=Object.create(null);r.forEach(function(e){e&&null!=e._vlKey&&(l[e._vlKey]=!0)}),Object.keys(o).forEach(function(e){if(!l[e]){var n=o[e];n&&n._vlPfx&&t.pruneInstPrefix(n._vlHost,n._vlPfx)}});var s=e.document,u=s&&s.activeElement,c=null;u&&"number"==typeof u.selectionStart&&(c={start:u.selectionStart,end:u.selectionEnd,dir:u.selectionDirection});for(var f=[],h=0;h<t._overlays.length;h++){var p=t._overlays[h];if(p.restoreFocus)for(var y=0;y<i.length;y++){var m=i[y];if(m&&null!=m._vlKey&&!l[m._vlKey]&&m.contains&&m.contains(p.restoreFocus)){f.push({rec:p,index:y});break}}}var g=-1;if(u)for(var b=0;b<i.length;b++){var _=i[b];if(_&&null!=_._vlKey&&!l[_._vlKey]&&_.contains&&_.contains(u)){g=b;break}}var S=0,k=Object.create(null),w=[];r.forEach(function(e){if(e&&null!=e._vlKey){var n=e._vlKey;if(k[n])throw t.err("render.duplicate_key","list key "+JSON.stringify(n)+" is used by more than one row; keys must be unique within a list");k[n]=!0;var r=o[n],i=r?v(r,e):e;i._vlKey=n,w.push(i)}else{var l=a[S++];w.push(l?v(l,e):e)}});for(var E=0;E<w.length;E++)n.childNodes[E]!==w[E]&&n.insertBefore(w[E],n.childNodes[E]||null);for(;n.childNodes.length>w.length;)n.removeChild(n.lastChild);if(u&&s&&s.contains&&s.contains(u)&&s.activeElement!==u&&"function"==typeof u.focus){try{u.focus({preventScroll:!0})}catch(e){}if(c&&"function"==typeof u.setSelectionRange)try{u.setSelectionRange(c.start,c.end,c.dir)}catch(e){}}function A(e){for(var t=[],r=0;r<n.childNodes.length;r++)n.childNodes[r]&&null!=n.childNodes[r]._vlKey&&t.push(n.childNodes[r]);if(!t.length)return null;for(var o=0,a=0;a<e;a++){var s=i[a];s&&null!=s._vlKey&&l[s._vlKey]&&o++}var u=t[Math.min(o,t.length-1)];return d(u)[0]||u}if(g>=0&&s&&(!s.contains||!s.contains(u))){var C=A(g);if(C&&"function"==typeof C.focus)try{C.focus({preventScroll:!0})}catch(e){}}for(var x=0;x<f.length;x++)f[x].rec.restoreFallback=A(f[x].index)}t.fmt=function(e,t){var n=e instanceof Date?e:new Date(e);return isNaN(n.getTime())?String(e):String(t||"yyyy-MM-dd").replace(/yyyy/g,n.getFullYear()).replace(/MMM/g,i[n.getMonth()]).replace(/MM/g,("0"+(n.getMonth()+1)).slice(-2)).replace(/dd/g,("0"+n.getDate()).slice(-2)).replace(/\bd\b/g,n.getDate())},t.range=function(e,t,n){for(var r=[],i=e;n?i<=t:i<t;i++)r.push(i);return r},t.each=function(e,t){return r(e).map(function(e,n){return t(e,n)})},t.keyed=function(e,n,i){for(var a=r(e),l=[],s=Object.create(null),u=0;u<a.length;u++){var c=i(a[u],u);if(null==c)throw t.err("render.missing_key","list key expression produced "+String(c)+" for item "+u+"; a keyed list needs a stable, defined key per row");if(s[c=String(c)])throw t.err("render.duplicate_key","list key "+JSON.stringify(c)+" is used by more than one row; keys must be unique within a list");s[c]=!0;var d=n(a[u],u);o(d,c),l.push(d)}return l},t.escKey=function(e){return String(e).replace(/\\/g,"\\\\").replace(/\//g,"\\/")},t.tagInst=function(e,t,n){return n&&n.nodeType&&(n._vlPfx=e,n._vlHost=t),n},t.pruneInstPrefix=function(e,t){e&&t&&["_state","_mounted","_loads"].forEach(function(n){var r=e[n];r&&Object.keys(r).forEach(function(e){0===e.indexOf(t)&&delete r[e]})})},t.appendKids=a,t.testIdsEnabled=!1,t._testFrame=null,t._testScopes=[],t.withTestScope=function(e,n){if("string"!=typeof e||!/^[a-z][a-z0-9-]{0,79}$/.test(e))throw t.err("render.invalid_test_scope","test_scope must be a public lowercase slug");t._testScopes.push(e);try{return n()}finally{t._testScopes.pop()}},t.testId=function(e){var n=t._testScopes.concat([e]).join("/");if(t._testFrame){if(t._testFrame.ids.has(n))throw t.err("render.duplicate_test_id","duplicate automation ID in the next rendered document");t._testFrame.ids.add(n)}return n},t.linkSafeSchemes={http:1,https:1,mailto:1,tel:1},t.linkHref=function(e){if(null==e)return"";var n=String(e),r=n.replace(/[\u0000-\u0020]/g,""),i=/^([A-Za-z][A-Za-z0-9+\-.]*):/.exec(r);return i?t.linkSafeSchemes[i[1].toLowerCase()]?n:("undefined"!=typeof console&&console.warn&&console.warn("VL: blocked link scheme "+i[1].toLowerCase()+":"),"about:blank#blocked"):n},t.fetchSafeSchemes={http:1,https:1},t.fetchUrl=function(e){if(null==e)return"";var n=String(e),r=n.replace(/[\u0000-\u0020]/g,""),i=/^([A-Za-z][A-Za-z0-9+\-.]*):/.exec(r);return i?t.fetchSafeSchemes[i[1].toLowerCase()]?n:("undefined"!=typeof console&&console.warn&&console.warn("VL: blocked resource scheme "+i[1].toLowerCase()+":"),""):n},t.fetchSrcset=function(e){if(null==e)return"";for(var n=String(e),r=n.split(","),i=0;i<r.length;i++){var o=r[i].trim().split(/\s+/)[0];if(o&&""===t.fetchUrl(o))return""}return n},t.detailsToggle=function(e){return function(t){var n=t.currentTarget;if(n&&n.isConnected){if(!0===n._vlToggleSeeded)return e.call(this,t);n._vlToggleSeeded=!0}}},t.el=function(e,t,n){var r=document.createElement(e);return(t=t||{}).class&&(r.className=t.class),t.style&&Object.keys(t.style).forEach(function(e){r.style[e]=t.style[e]}),t.attrs&&Object.keys(t.attrs).forEach(function(e){var n=t.attrs[e];return"checked"===e?(r.checked=!!n,void(n?r.setAttribute("checked",""):r.removeAttribute("checked"))):"muted"===e?(r.muted=!!n,r.defaultMuted=!!n,void(n?r.setAttribute("muted",""):r.removeAttribute("muted"))):void(null!=n&&!1!==n&&r.setAttribute(e,!0===n?"":String(n)))}),void 0!==t.text&&null!==t.text&&(r.textContent=String(t.text)),"details"===e&&(r._vlToggleSeeded=!r.hasAttribute("open")),t.on&&Object.keys(t.on).forEach(function(e){r.addEventListener(e,t.on[e])}),r._vlHandlers=t.on||{},a(r,n),t.attrs&&"value"in t.attrs&&null!==t.attrs.value&&void 0!==t.attrs.value&&!1!==t.attrs.value&&(r.value=t.attrs.value),r},t.Spinner=function(){return t.el("div",{class:"vl-spinner",text:"loading…"},[])},t.live=function(e,n,r){if(null!=e&&"string"!=typeof e)throw t.err("render.invalid_status","status/error message must be a string or nil");r=Object.assign({},r||{},{role:"status","aria-live":"polite","aria-atomic":"true"});var i=t.el("span",{class:n||"vl-status",attrs:r},[]);return i.appendChild(document.createTextNode(e||"")),i},t._fieldSeq=0,t.field=function(e){var n=e.name;if(null==n||""===String(n).trim())throw t.err("render.empty_input_name","an input's accessible name ("+(e.aria?"aria_label:":"label:")+") is "+(null==n?String(n):JSON.stringify(String(n)))+" at render time; a control must always have a non-empty name");n=String(n);var r="vlf"+ ++t._fieldSeq,i=r+"c",o=r+"h",a=r+"e",l=e.input||{},s=l.attrs=l.attrs||{};s.id=i;var u=void 0!==e.help&&null!==e.help,c=e.hasError?e.error:void 0,d=null!=c&&""!==c,f=[];u&&f.push(o),d&&f.push(a),f.length?s["aria-describedby"]=f.join(" "):delete s["aria-describedby"],e.hasError&&(s["aria-invalid"]=d?"true":null),e.aria&&(s["aria-label"]=n);var v=[];return e.aria||v.push(t.el("label",{class:"vl-label",attrs:{for:i},text:n},[])),v.push(t.el(e.tag||"input",l,e.children||[])),u&&v.push(t.el("span",{class:"vl-help",attrs:{id:o},text:e.help},[])),e.hasError&&v.push(t.live(c,"vl-error",{id:a})),t.el("div",{class:"vl-field"},v)},t.reqAlt=function(e){if(null==e||""===String(e).trim())throw t.err("render.empty_alt","an img `alt:` resolved to "+(null==e?String(e):JSON.stringify(String(e)))+" at render time; use `decorative` for a presentational image or give real alternative text");return String(e)},t.setTitle=function(n){if(null==n||""===String(n).trim())throw t.err("render.empty_page_title","the page `title:` resolved to "+(null==n?String(n):JSON.stringify(String(n)))+" at load time; a page must have a non-empty title");var r=e.document;return r&&(r.title=String(n)),String(n)},t.wireSkipLink=function(){var n=e.document;if(n&&n.querySelector&&n.body){var r=n.querySelector("nav.vl-skip-nav"),i=n.querySelector("a.vl-skip"),o=n.querySelector("main");if(o){o.id||(o.id="vl-main"),o.getAttribute&&null==o.getAttribute("tabindex")&&o.setAttribute("tabindex","-1");var a=t._mainLabel;if(a&&o.getAttribute){var l=null!=o.getAttribute("data-vl-route-label");(null==o.getAttribute("aria-label")||l)&&(o.getAttribute("aria-label")!==a&&o.setAttribute("aria-label",a),l||o.setAttribute("data-vl-route-label",""))}if(i)i.getAttribute("href")!=="#"+o.id&&i.setAttribute("href","#"+o.id);else{var s=t.el("a",{class:"vl-skip",attrs:{href:"#"+o.id},text:"Skip to main content"},[]);s.addEventListener("click",function(t){t.preventDefault&&t.preventDefault();var r=n.querySelector("main")||n.getElementById("vl-main");r&&r.focus&&r.focus();try{r&&r.id&&e.history&&e.history.replaceState&&e.history.replaceState(null,"","#"+r.id)}catch(e){}}),r||(r=t.el("nav",{class:"vl-skip-nav",attrs:{"aria-label":"Skip navigation"}},[]),n.body.insertBefore(r,n.body.firstChild)),r.appendChild(s)}}else{var u=r||i;u&&u.parentNode&&u.parentNode.removeChild(u)}}},t.queryProps=function(t){var n={};try{var r=e.location,i=new URLSearchParams(r&&r.search||"");(t||[]).forEach(function(e){i.has(e)&&(n[e]=i.get(e))})}catch(e){}return n},t._overlays=[],t._appEl=null,t.overlay={open:function(n){var r=s(n);if(r>=0)return t._overlays[r].el;var i=e.document,o=i.createElement("div");o.className="vl-overlay",o.setAttribute("data-overlay",String(n));var a=i.body||l()&&l().parentNode||null;a&&a.appendChild(o);var c=i.activeElement;return t._overlays.push({id:n,el:o,restoreFocus:c&&c!==i.body?c:null}),1===t._overlays.length&&u(l(),!0),o},close:function(n){var r=s(n);if(!(r<0)){var i=t._overlays.splice(r,1)[0],o=e.document;i._onClick&&i.el.removeEventListener&&i.el.removeEventListener("click",i._onClick),i._onTab&&i.el.removeEventListener&&i.el.removeEventListener("keydown",i._onTab),i._onKey&&o&&o.removeEventListener&&o.removeEventListener("keydown",i._onKey),delete t.overlay._dismiss[n],i.el&&i.el.parentNode&&i.el.parentNode.removeChild(i.el),0===t._overlays.length&&(u(l(),!1),function(e,t){for(var n=[e.restoreFocus,e.restoreFallback,t&&t.getElementById&&t.getElementById("vl-main")],r=0;r<n.length;r++){var i=n[r];if(i&&c(i,t)&&"function"==typeof i.focus){try{i.focus()}catch(e){}if(!t||t.activeElement===i)return}}var o=e.restoreFocus;if(o&&"function"==typeof o.focus)try{o.focus()}catch(e){}}(i,o))}},focusInto:function(e){if(e){var t=d(e)[0]||e;if(t===e&&(null==e.tabIndex||e.tabIndex<0)&&(e.tabIndex=-1),"function"==typeof t.focus)try{t.focus()}catch(e){}}},count:function(){return t._overlays.length},_dismiss:Object.create(null),_live:null,beginFrame:function(){this._live=Object.create(null)},endFrame:function(){var e=this._live;if(this._live=null,e)for(var n=t._overlays.length-1;n>=0;n--){var r=t._overlays[n].id;r in e||this.close(r)}},paint:function(n,r,i,o){var a;if(t._testFrame)return t._testFrame.portals.push(function(){t.overlay.paint(n,r,function(){return a},o)}),a=i(),null;var l=s(n)<0,u=this.open(n);u.className="vl-overlay vl-overlay-dialog",this._live&&(this._live[n]=!0),this._dismiss[n]=o;var c=t._overlays[s(n)],h="vl-dlg-title-"+String(n).replace(/[^\w-]/g,"_"),p=t.el("div",{class:"vl-dialog",attrs:{role:"dialog","aria-modal":"true","aria-labelledby":h,tabindex:"-1"}},[t.el("h2",{class:"vl-dialog-title",attrs:{id:h},text:r},[]),i()]);if(c.panel&&c.panel.parentNode===u&&u.replaceChild){var y=v(c.panel,p);y!==c.panel&&(u.replaceChild(y,c.panel),c.panel=y)}else if(u.replaceChildren)u.replaceChildren(p),c.panel=p;else{for(;u.firstChild;)u.removeChild(u.firstChild);u.appendChild(p),c.panel=p}return l&&(c._onTab=function(t){if("Tab"===(t.key||"")||9===t.keyCode){var n=c.panel,r=d(n);if(r.length){var i=r[0],o=r[r.length-1],a=e.document.activeElement;t.shiftKey&&a===i?(t.preventDefault&&t.preventDefault(),o.focus()):t.shiftKey||a!==o&&n.contains(a)||(t.preventDefault&&t.preventDefault(),i.focus())}else t.preventDefault&&t.preventDefault()}},u.addEventListener&&u.addEventListener("keydown",c._onTab),c._onClick=function(e){e.target===u&&f(n)},u.addEventListener&&u.addEventListener("click",c._onClick),c._onKey=function(e){"Escape"!==(e.key||"")&&27!==e.keyCode||f(n)},e.document.addEventListener&&e.document.addEventListener("keydown",c._onKey),this.focusInto(c.panel)),null}},t.route={to:function(t){if("string"==typeof t&&t)try{if("?"===t[0]||"#"===t[0]){var n=e.history;return void(n&&n.replaceState&&n.replaceState(null,"",t))}var r=e.location;r&&r.assign&&r.assign(t)}catch(e){}}},t.tabs=function(e){for(var n=(e=e||{}).tabs||[],r=0,i=0;i<n.length;i++)if(n[i].id===e.selected){r=i;break}var o=n.length?n[r].id:null;function a(t){n.length&&e.onSelect&&e.onSelect(n[(t+n.length)%n.length].id)}var l=t.el("div",{class:"vl-tablist",attrs:{role:"tablist"},on:{keydown:function(e){var t=e.key||"",i=function(){e.preventDefault&&e.preventDefault()};"ArrowRight"===t||"ArrowDown"===t?(i(),a(r+1)):"ArrowLeft"===t||"ArrowUp"===t?(i(),a(r-1)):"Home"===t?(i(),a(0)):"End"===t&&(i(),a(n.length-1))}}},n.map(function(n){var r=n.id===o;return t.el("button",{class:"vl-tab",attrs:{role:"tab",id:"vl-tab-"+n.id,"aria-selected":r?"true":"false","aria-controls":"vl-panel-"+n.id,tabindex:r?"0":"-1"},text:n.label,on:{click:function(){e.onSelect&&e.onSelect(n.id)}}},[])})),s=n[r],u=t.el("div",{class:"vl-tabpanel",attrs:{role:"tabpanel",id:"vl-panel-"+o,"aria-labelledby":"vl-tab-"+o,tabindex:"0"}},s?[s.panel()]:[]);return t.el("div",{class:"vl-tabs"},[l,u])},t.subctx=function(e,t){var n=e._host,r=(e._pfx||"")+(t||"");return{_pfx:r,_host:n,signal:n.signal,state:function(e,t){return n.state(r+e,t)},setState:function(e,t){return n.setState(r+e,t)},load:function(e,t,i,o){return n.load(r+e,t,i,o)},reload:function(e){return n.reload(r+e)},mountOnce:function(e,t){return n.mountOnce(r+e,t)}}},t.mount=function(n,r,i){var o=i||{};t._appEl=r;var l="undefined"!=typeof AbortController?new AbortController:null,s={_state:Object.create(null),_loads:Object.create(null),_mounted:Object.create(null),_scheduled:!1,_composing:!1,_abortController:l,signal:l?l.signal:void 0,state:function(e,t){return e in this._state||(this._state[e]=t),this._state[e]},mountOnce:function(e,t){this._mounted[e]||(this._mounted[e]=!0,t())},setState:function(e,t){return this._state[e]=t,this.schedule(),t},load:function(e,n,r,i){var o=JSON.stringify(void 0===n?null:n),a=this._loads[e];if(!a||a.key!==o){a=this._loads[e]={key:o,pending:!0,error:null,value:void 0};var l=this;Promise.resolve().then(r).then(function(n){l._loads[e]===a&&(a.pending=!1,a.value=n,l.schedule(),t._onLoadSettled&&t._onLoadSettled(l))},function(n){l._loads[e]===a&&(a.pending=!1,a.error=n,l.schedule(),!i&&t._onUnhandledLoadError&&t._onUnhandledLoadError(l,e,n),t._onLoadSettled&&t._onLoadSettled(l))})}return a},reload:function(e){delete this._loads[e],this.schedule()},schedule:function(){var e=this;e._scheduled||(e._scheduled=!0,queueMicrotask(function(){e._scheduled=!1,e.render()}))},render:function(){if(!this._composing){var i,l=e.document,s=l&&l.activeElement,c=null;s&&s.getAttribute&&s.getAttribute("data-vl-bind")&&(!l.contains||l.contains(s))&&(c={node:s,key:s.getAttribute("data-vl-bind"),start:s.selectionStart,end:s.selectionEnd,direction:s.selectionDirection,scrollLeft:s.scrollLeft,scrollTop:s.scrollTop}),t.overlay.beginFrame(),t.testIdsEnabled&&(t._testFrame={ids:new Set,portals:[]});try{i=n(o,u);var d=t._testFrame;t._testFrame=null,d&&d.portals.forEach(function(e){e()})}catch(e){if(t._testFrame=null,t._testScopes=[],e&&/^render\.(duplicate_test_id|invalid_test_scope)$/.test(e.code||""))throw t.overlay._live=null,e;throw r.replaceChildren(t.el("pre",{class:"vl-error",text:String(e&&e.stack||e)},[])),e}if(Array.isArray(i)){var f=document.createDocumentFragment();a(f,i),r.replaceChild?h(r,Array.from(f.childNodes)):r.replaceChildren(f)}else r.replaceChild?h(r,[i]):r.replaceChildren(i);if(t.overlay.endFrame(),t.wireSkipLink(),c&&l){var v=l.contains&&l.contains(c.node)?c.node:null;if(!v&&l.querySelectorAll){var p=Array.prototype.filter.call(l.querySelectorAll("[data-vl-bind]"),function(e){return e.getAttribute("data-vl-bind")===c.key});1===p.length&&(v=p[0])}if(v&&l.activeElement!==v){if(v.focus({preventScroll:!0}),null!=c.start&&"function"==typeof v.setSelectionRange)try{v.setSelectionRange(c.start,c.end,c.direction)}catch(e){}v.scrollLeft=c.scrollLeft,v.scrollTop=c.scrollTop}}}}},u=t.subctx({_pfx:"",_host:s},""),c=e.document;return c&&c.addEventListener&&(c.addEventListener("compositionstart",function(){s._composing=!0}),c.addEventListener("compositionend",function(){s._composing=!1,s.schedule()})),s.render(),s},t._onUnhandledLoadError=null,t._onLoadSettled=null,t._mainLabel=null,t._modules=Object.create(null);var y=Object.create(null);t.registerModule=function(e,n){t._modules[e]=n},t.loadModule=function(n,r){if(t._modules[n])return Promise.resolve(t._modules[n]);if(y[n])return y[n];var i=e.document,o=new Promise(function(e,o){var a=i.createElement("script");a.src=r,a.onload=function(){t._modules[n]?e(t._modules[n]):o(new Error("module '"+n+"' did not register itself (loaded "+r+")"))},a.onerror=function(){o(new Error("failed to load module '"+n+"' from "+r))},i.head.appendChild(a)});return y[n]=o,o},e.VL=t}("undefined"!=typeof window?window:globalThis);
@@ -62,7 +62,8 @@ from vlcheck.elements import (
62
62
  from vlcheck.checks import _lit_int, _lit_text, _rendered_children
63
63
  from vlcheck.lexer import Lexer
64
64
  from vlcheck.parser import Parser
65
- from vlcheck.resolve import _route_name
65
+ from vlcheck.resolve import (_AUTH_API, AUTH_BUILT, _route_name,
66
+ auth_method_diagnostic)
66
67
 
67
68
 
68
69
  def table_name(model_name: str) -> str:
@@ -637,15 +638,26 @@ class Emitter:
637
638
  faithfully, rather than write a bundle that silently does less than the
638
639
  source says. The counterpart to `ServerEmitter.precheck` for the browser
639
640
  target. A narrow subset is fine; failing open is not."""
640
- supported_auth = {"sign_up", "sign_in", "sign_out", "reset_password",
641
- "confirm_reset", "confirm_verify"}
642
- unsupported_auth = sorted(self.auth_methods - supported_auth)
643
- if unsupported_auth:
641
+ # Two different mistakes, two codes. A name outside spec §11.3 is
642
+ # E-AUTH-METHOD, the code `volaro check` reports for it too (resolve.py),
643
+ # so this is the backstop for source that never went through the
644
+ # checker. A §11.3 call this target does not build (OAuth, TOTP) is
645
+ # E-AUTH-UNSUPPORTED. Both fail closed: emitting either call would
646
+ # produce a client with no server endpoint behind it.
647
+ unknown_auth = sorted(self.auth_methods - set(_AUTH_API))
648
+ if unknown_auth:
649
+ msg, fix = auth_method_diagnostic(unknown_auth[0])
650
+ raise BuildError("E-AUTH-METHOD: view target: %s. fix: %s"
651
+ % (msg, fix))
652
+ unbuilt_auth = sorted(self.auth_methods - set(AUTH_BUILT))
653
+ if unbuilt_auth:
644
654
  raise BuildError(
645
- "view target: auth.%s is not implemented by this server target; "
646
- "password signup/login/reset are the only credential operations "
647
- "in this pass, so emitting the call would fail open"
648
- % (" / auth.".join(unsupported_auth)))
655
+ "E-AUTH-UNSUPPORTED: view target: auth.%s is in the spec but "
656
+ "this build does not implement it (OAuth and TOTP are not "
657
+ "built), so emitting the call would fail open. The auth calls "
658
+ "this build supports are %s"
659
+ % (" / auth.".join(unbuilt_auth),
660
+ ", ".join("auth." + x for x in AUTH_BUILT)))
649
661
  if self.auth_methods and self.auth is None:
650
662
  raise BuildError(
651
663
  "view target: auth.%s is called but no project `auth` block was "
@@ -36,6 +36,7 @@ from vlcheck.pagemanifest import (build_manifest, resolve_boundary,
36
36
  from vlcheck.resolve import resolve
37
37
 
38
38
  from .emit import BuildError, Emitter
39
+ from .static_assets import copy_assets, prepare_assets
39
40
 
40
41
  _SAFE_RE = re.compile(r"[^A-Za-z0-9_]+")
41
42
 
@@ -209,6 +210,7 @@ def build_pages(root: Path, out_public: Path,
209
210
  collected: list[str] = [] if warnings is None else warnings
210
211
 
211
212
  compiled: dict[str, str] = {} # module id -> emitted JS
213
+ asset_plan = None # the same for every file; set per compiled file
212
214
  for path, info in files.items():
213
215
  mod, parse_diags, src = _parse(path)
214
216
  if titles is not None and info["role"] == "page":
@@ -259,6 +261,15 @@ def build_pages(root: Path, out_public: Path,
259
261
  except BuildError as exc:
260
262
  problems.append(f"{exc} ({path})")
261
263
  continue
264
+ # A project's entry is its root, so its sibling assets/ is
265
+ # root/assets/, shipped at public/assets/ (spec: the build copies the
266
+ # entry's assets/ into the browser bundle). Each file's literal image
267
+ # references are checked here, before anything is written.
268
+ try:
269
+ asset_plan = prepare_assets(root / "app.vl", out_public, emitter)
270
+ except BuildError as exc:
271
+ problems.append(f"{exc} ({path})")
272
+ continue
262
273
  compiled[mid] = js
263
274
  style_candidates.extend(emitter.style_candidates)
264
275
  collected.extend("%s (%s)" % (w, os.path.relpath(path, root)) for w in emitter.warnings)
@@ -286,6 +297,8 @@ def build_pages(root: Path, out_public: Path,
286
297
  pages_dir.mkdir(parents=True, exist_ok=True)
287
298
  for mid, js in compiled.items():
288
299
  (pages_dir / f"{mid}.js").write_text(js, encoding="utf-8")
300
+ if asset_plan is not None:
301
+ copy_assets(asset_plan, out_public)
289
302
 
290
303
  def boundary_id(path: str | None) -> str | None:
291
304
  return module_id_for(str(root), path) if path else None
@@ -354,8 +367,6 @@ def static_routes(client_manifest: dict) -> list[dict]:
354
367
  if not any(kind == "dynamic" for kind, _ in p["segments"])]
355
368
 
356
369
 
357
- _ASSETS = Path(__file__).resolve().parent / "assets"
358
-
359
370
  # The route-agnostic shell the server fallback answers with (a dynamic route,
360
371
  # or any path the manifest does not know). A static route's own shell is the
361
372
  # same file with that page's title (`build_project`).
@@ -423,7 +434,8 @@ def bootstrap_js() -> str:
423
434
 
424
435
 
425
436
  def build_project(root: Path, out_dir: Path, config=None,
426
- warnings: list[str] | None = None) -> dict:
437
+ warnings: list[str] | None = None,
438
+ release: bool = False) -> dict:
427
439
  """Full multi-page project build: browser pages/layouts/boundaries
428
440
  (`build_pages`), the application shell, static-route HTML shells
429
441
  (architecture decision §2), the runtime assets (`vlrt.js`, `vlrt.css`,
@@ -437,7 +449,8 @@ def build_project(root: Path, out_dir: Path, config=None,
437
449
  when the project has no `app/**/index.vl` pages at all (nothing is
438
450
  written — the caller falls back to the existing single-entry build).
439
451
  `warnings`, when given, receives every build warning (per-file skipped
440
- constructs and the styling warnings) so the caller can print them."""
452
+ constructs and the styling warnings) so the caller can print them.
453
+ `release` selects the minified runtime files (`runtime_assets.py`)."""
441
454
  out_public = out_dir / "public"
442
455
  titles: dict[str, str] = {}
443
456
  client_manifest = build_pages(root, out_public, config=config,
@@ -446,12 +459,9 @@ def build_project(root: Path, out_dir: Path, config=None,
446
459
  return {}
447
460
 
448
461
  out_public.mkdir(parents=True, exist_ok=True)
449
- (out_public / "vlrt.js").write_text(
450
- (_ASSETS / "vlrt.js").read_text(encoding="utf-8"), encoding="utf-8")
451
- (out_public / "vlrt.css").write_text(
452
- (_ASSETS / "vlrt.css").read_text(encoding="utf-8"), encoding="utf-8")
453
- (out_public / "vlrouter.js").write_text(
454
- (_ASSETS / "vlrouter.js").read_text(encoding="utf-8"), encoding="utf-8")
462
+ # --release ships the committed minified runtime; see runtime_assets.py.
463
+ from .runtime_assets import write_runtime
464
+ write_runtime(out_public, ("vlrt.js", "vlrt.css", "vlrouter.js"), release)
455
465
  (out_public / "bootstrap.js").write_text(bootstrap_js(), encoding="utf-8")
456
466
  styled = (out_public / "app.css").is_file()
457
467
  shell = shell_html(styled=styled)
@@ -0,0 +1,41 @@
1
+ """Write the browser runtime files (`vlrt.js`, `vlrt.css`, `vlrouter.js`).
2
+
3
+ A plain build copies the readable, commented sources from `assets/`, as it
4
+ always has. A `--release` build copies the minified siblings
5
+ (`vlrt.min.js`, `vlrt.min.css`, `vlrouter.min.js`) under the plain names, so
6
+ `index.html` and the page shells do not change.
7
+
8
+ The minified files are generated by the maintainer tool in
9
+ `prototype/vlbuild/minify/` and committed. Nothing here runs a JS tool: a
10
+ user's build only copies bytes. Gate 2 checks the committed files against a
11
+ fresh minify, and the release copy is byte for byte, so what the gates test
12
+ is what ships.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from pathlib import Path
18
+
19
+ from .emit import BuildError
20
+
21
+ ASSETS = Path(__file__).resolve().parent / "assets"
22
+
23
+
24
+ def minified_name(name: str) -> str:
25
+ stem, dot, ext = name.rpartition(".")
26
+ return "%s.min.%s" % (stem, ext)
27
+
28
+
29
+ def write_runtime(out_dir: Path, names: tuple[str, ...], release: bool) -> None:
30
+ for name in names:
31
+ if release:
32
+ src = ASSETS / minified_name(name)
33
+ if not src.is_file():
34
+ raise BuildError(
35
+ "the minified runtime %s is missing from this compiler "
36
+ "install; rebuild without --release, or reinstall volaro"
37
+ % src.name)
38
+ (out_dir / name).write_bytes(src.read_bytes())
39
+ else:
40
+ (out_dir / name).write_text(
41
+ (ASSETS / name).read_text(encoding="utf-8"), encoding="utf-8")
@@ -2,7 +2,9 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  import json
5
+ import os
5
6
  import shutil
7
+ import stat
6
8
  from pathlib import Path
7
9
  from urllib.parse import unquote, urlsplit
8
10
 
@@ -10,6 +12,57 @@ from vlcheck import ast_nodes as A
10
12
  from .emit import BuildError, _walk
11
13
 
12
14
 
15
+ # A Windows reparse tag with this bit set names another file or directory:
16
+ # symbolic links, directory junctions (mount points) and similar redirects.
17
+ # Cloud-file placeholders (OneDrive) do not set it, so they still count as files.
18
+ _NAME_SURROGATE = 0x20000000
19
+
20
+
21
+ def _is_link(path: Path) -> bool:
22
+ """True for a symbolic link or a Windows junction; neither is followed.
23
+
24
+ `Path.is_symlink()` is False for an NTFS junction, and `Path.is_junction()`
25
+ only exists from Python 3.12, so older Pythons read the reparse tag.
26
+ """
27
+ try:
28
+ st = os.lstat(path)
29
+ except OSError:
30
+ return False
31
+ if stat.S_ISLNK(st.st_mode):
32
+ return True
33
+ is_junction = getattr(path, "is_junction", None)
34
+ if is_junction is not None and is_junction():
35
+ return True
36
+ return bool(getattr(st, "st_reparse_tag", 0) & _NAME_SURROGATE)
37
+
38
+
39
+ def _asset_files(root: Path) -> list[Path]:
40
+ """Every regular file under root, refusing links and never descending into one."""
41
+ files = []
42
+ pending = [root]
43
+ while pending:
44
+ folder = pending.pop()
45
+ for path in sorted(folder.iterdir()):
46
+ if _is_link(path):
47
+ raise BuildError(
48
+ f"E-ASSET-LINK: symbolic links and junctions are not supported in assets/: {path}")
49
+ if path.is_dir():
50
+ pending.append(path)
51
+ elif path.is_file():
52
+ files.append(path)
53
+ else:
54
+ raise BuildError(f"E-ASSET-PATH: assets/ supports regular files only: {path}")
55
+ # Defence in depth: whatever the checks above missed, every file must
56
+ # still resolve to somewhere inside the resolved assets/ root.
57
+ real_root = root.resolve()
58
+ for path in files:
59
+ real = path.resolve()
60
+ if real != real_root and real_root not in real.parents:
61
+ raise BuildError(
62
+ f"E-ASSET-LINK: {path} resolves outside assets/; symbolic links and junctions are not supported")
63
+ return sorted(files)
64
+
65
+
13
66
  def prepare_assets(entry: Path, bundle: Path, emitter) -> tuple[Path, list[Path]]:
14
67
  root = entry.parent / "assets"
15
68
  dest = bundle / "assets"
@@ -17,23 +70,17 @@ def prepare_assets(entry: Path, bundle: Path, emitter) -> tuple[Path, list[Path]
17
70
  if root.exists() and (dest.resolve() == root.resolve() or root.resolve() in dest.resolve().parents
18
71
  or dest.resolve() in root.resolve().parents):
19
72
  raise BuildError("E-ASSET-OUTPUT: output assets/ overlaps source assets/; choose another -o directory")
20
- if dest.is_symlink():
21
- raise BuildError("E-ASSET-OUTPUT: output assets/ must not be a symbolic link")
73
+ if _is_link(dest):
74
+ raise BuildError("E-ASSET-OUTPUT: output assets/ must not be a symbolic link or junction")
22
75
  if dest.exists() and not dest.is_dir():
23
76
  raise BuildError("E-ASSET-OUTPUT: output assets/ must be a directory")
24
77
  files = []
25
- if root.is_symlink():
26
- raise BuildError("E-ASSET-LINK: assets/ must not be a symbolic link")
78
+ if _is_link(root):
79
+ raise BuildError("E-ASSET-LINK: assets/ must not be a symbolic link or junction")
27
80
  if root.exists():
28
81
  if not root.is_dir():
29
82
  raise BuildError("E-ASSET-PATH: assets must be a directory beside the entry file")
30
- for path in sorted(root.rglob("*")):
31
- if path.is_symlink():
32
- raise BuildError(f"E-ASSET-LINK: symbolic links are not supported in assets/: {path}")
33
- if path.is_file():
34
- files.append(path)
35
- elif not path.is_dir():
36
- raise BuildError(f"E-ASSET-PATH: assets/ supports regular files only: {path}")
83
+ files = _asset_files(root)
37
84
  # Check literal img references into this convention directory. Other URLs
38
85
  # and dynamic strings retain their existing meaning; no guessing/rewrite.
39
86
  for view in emitter.views:
package/language/crib.md CHANGED
@@ -748,7 +748,7 @@ No `alt` is `E-IMG-ALT`. Put local files in `assets/` beside the entry file:
748
748
  `img src:"assets/logo.svg" alt:"Logo"`. Build and dev copy the directory into
749
749
  the served bundle; dev rebuilds on asset additions, edits and deletions.
750
750
  A missing literal `assets/...` image is `E-ASSET-MISSING` at build time.
751
- The output `assets/` directory is compiler-owned; source symlinks are rejected.
751
+ The output `assets/` directory is compiler-owned; source symlinks and junctions are rejected.
752
752
  Other relative paths are not copied automatically; `data:` and external URLs work.
753
753
 
754
754
  Every build that renders a page needs a module-level `page title:"…"` (becomes
@@ -801,8 +801,12 @@ A network failure, timeout or undecodable body has no `http.` code, so the
801
801
  the browser, so only unauthenticated APIs work: `auth`, `retry`, `rate` and
802
802
  `headers` directives fail the build (`E-API-DIRECTIVE-UNSUPPORTED`). `par` and
803
803
  `spawn` are in the spec but not built; the build rejects them.
804
- Client calls: `auth.sign_in(email:, password:)`, `auth.sign_up(...)`,
805
- `auth.sign_out()`, `auth.verify_totp(code:)`, `route.to("/path")`.
804
+ `auth.*` talks to this project's own `auth` block and server; an existing
805
+ endpoint such as `/api/login` goes through an `api` block (above). Built:
806
+ `auth.sign_in(email:, password:)`, `sign_up(email:, password:)`, `sign_out()`,
807
+ `reset_password(email:)`, `confirm_reset(token:, password:)`,
808
+ `confirm_verify(token:)`; any other name is `E-AUTH-METHOD`. Navigate with
809
+ `route.to("/path")`.
806
810
 
807
811
  ## Styling
808
812
 
package/language/spec.md CHANGED
@@ -1652,6 +1652,8 @@ The client-side surface is eight calls. Each returns a fallible value with a doc
1652
1652
 
1653
1653
  `start_oauth` takes a bare provider name matching a `provider oauth <name>` declaration in the `auth` block (`auth.start_oauth(google)`).
1654
1654
 
1655
+ The surface is closed: `auth.<name>` for any name outside this table is `E-AUTH-METHOD`, reported by the checker and the build alike, with the intended call suggested where one is likely (`auth.login` → `auth.sign_in`). `auth.*` reaches only this project's own `auth` block and its compiler-owned endpoints; an existing HTTP endpoint such as `/api/login` is called through an outbound `api` block (§9).
1656
+
1655
1657
  ---
1656
1658
 
1657
1659
  ## 12. Concurrency
@@ -2536,5 +2538,6 @@ hex digits representing a Unicode scalar value; surrogates are invalid.
2536
2538
  The prototype copies the entry file's sibling `assets/` directory into the
2537
2539
  browser bundle at `assets/` for both build and dev. Literal image references
2538
2540
  inside this directory must exist at build time; dynamic URLs are not checked.
2539
- The directory contains public files only, cannot contain symbolic links, and
2540
- is copied without transformations. Output `assets/` is compiler-owned.
2541
+ The directory contains public files only, cannot contain symbolic links or
2542
+ Windows junctions, and is copied without transformations. Output `assets/` is
2543
+ compiler-owned.
@@ -388,6 +388,8 @@ audio video source track picture map area
388
388
  bare call is `E-METHOD-AS-FN`. The spec's `sorted`, `first`, `fold`,
389
389
  `merge` and `to_int` free functions are not built; a call to one in a view
390
390
  or route is `E-FN-NOT-BUILT`.
391
+ - `try call()?` is a warning (`W-TRY-PROPAGATE`): the `catch` arms already
392
+ handle the error, so the `?` does nothing. Drop it.
391
393
 
392
394
  ### Data / services
393
395
  - `service` with `db.<model>` query builder (`where` / `order` / `limit` /
@@ -401,6 +403,10 @@ audio video source track picture map area
401
403
  the build, at any nesting depth. Use `?` to propagate errors.
402
404
  - Password auth: `auth.sign_up` / `sign_in` / `sign_out` / `reset_password` /
403
405
  `confirm_reset` / `confirm_verify` against compiler-owned endpoints.
406
+ Any other `auth.` name is `E-AUTH-METHOD` in `volaro check` and the build,
407
+ with the likely call suggested (`auth.login` -> `auth.sign_in`). The spec's
408
+ `auth.verify_totp` and `auth.start_oauth` pass the check but fail the build
409
+ with `E-AUTH-UNSUPPORTED`: TOTP and OAuth are not built.
404
410
  - Outbound `api` blocks → a typed `fetch` client. `base` and each endpoint's
405
411
  `path` are joined with exactly one `/` between them, so `base "/"` (or no
406
412
  `base`) with `path "/api/login"` calls `/api/login` on the page's own
@@ -482,8 +488,8 @@ Dev uses the same build path and watches asset additions, edits and deletions.
482
488
  Refresh the browser after a rebuild; this does not add automatic browser reload.
483
489
 
484
490
  Literal image URLs into `assets/` are checked before output is written.
485
- Missing files fail with `E-ASSET-MISSING`; traversal paths and symbolic links
486
- are rejected. Query strings and URL-encoded filenames are supported. Dynamic
491
+ Missing files fail with `E-ASSET-MISSING`; traversal paths, symbolic links and
492
+ Windows junctions are rejected (`E-ASSET-LINK`). Query strings and URL-encoded filenames are supported. Dynamic
487
493
  URLs are not existence-checked, but all files in the directory are copied.
488
494
  All regular files in `assets/`, including dotfiles, are public: put only intended
489
495
  public resources there. Output `assets/` is compiler-owned and replaced on each
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "volaro",
3
- "version": "0.1.0-alpha.6",
3
+ "version": "0.1.0-alpha.8",
4
4
  "description": "Volaro \u2014 an application language written to be authored by an AI agent and read by a person. Ships the language reference and a working compiler for a supported subset (volaro check / build / dev). CLI: `volaro` (alias `vl`).",
5
5
  "repository": {
6
6
  "type": "git",
@@ -43,11 +43,30 @@ try {
43
43
  check("build wrote app.js", existsSync(join(out, "app.js")));
44
44
  check("build wrote vlrt.js", existsSync(join(out, "vlrt.js")));
45
45
 
46
+ // --release copies the minified runtime the package ships; no JS tool runs.
47
+ const rel = join(dir, "release");
48
+ const rbld = spawnSync(node, [vl, "build", src, "-o", rel, "--release"], { encoding: "utf8" });
49
+ check("vl build --release exits 0", rbld.status === 0, (rbld.stderr || "").trim());
50
+ const assets = join(pkgRoot, "compiler", "vlbuild", "vlbuild", "assets");
51
+ check("build --release ships the minified runtime",
52
+ ["vlrt.js", "vlrt.css"].every((n) => existsSync(join(rel, n))
53
+ && readFileSync(join(rel, n)).equals(readFileSync(join(assets, n.replace(/\.(js|css)$/, ".min.$1"))))));
54
+
46
55
  const bad = join(dir, "bad.vl");
47
56
  writeFileSync(bad, "view Broken(\n h1 \"unterminated\n");
48
57
  const badChk = spawnSync(node, [vl, "check", bad], { encoding: "utf8" });
49
58
  check("vl check exits non-zero on broken source", badChk.status !== 0);
50
59
 
60
+ // A diagnostic's source frame uses box-drawing characters. Windows gives a
61
+ // redirected Python stdout a legacy code page (cp1252), which cannot
62
+ // encode them; PYTHONIOENCODING=ascii reproduces that on every OS.
63
+ const narrow = spawnSync(node, [vl, "check", bad],
64
+ { encoding: "utf8", env: { ...process.env, PYTHONIOENCODING: "ascii" } });
65
+ const narrowOut = (narrow.stdout || "") + (narrow.stderr || "");
66
+ check("vl check prints its diagnostic through a non-UTF-8 stdout",
67
+ !narrowOut.includes("UnicodeEncodeError") && narrowOut.includes("error[E-") && narrowOut.includes("┌─"),
68
+ narrowOut.trim().split("\n").pop());
69
+
51
70
  // `volaro routes <project>`: page-server route manifest inspection
52
71
  const posts = join(dir, "app", "posts");
53
72
  mkdirSync(posts, { recursive: true });
@@ -39,6 +39,18 @@ copy() {
39
39
  copy "$src_validator" "$dest/validator/vlcheck"
40
40
  copy "$src_vlbuild" "$dest/vlbuild/vlbuild"
41
41
 
42
+ # `--release` builds copy the committed minified runtime (assets/*.min.*).
43
+ # Refuse to pack if it is stale against a fresh minify from the pinned
44
+ # maintainer tool, so the tarball ships the bytes the gates tested. The tool
45
+ # itself (prototype/vlbuild/minify) is never copied into the package.
46
+ minify="$repo/prototype/vlbuild/minify"
47
+ if [ ! -d "$minify/node_modules/terser" ]; then
48
+ echo "sync-compiler: the runtime minifier is not installed; run from the repo root:" >&2
49
+ echo " npm ci --ignore-scripts --no-audit --no-fund --prefix prototype/vlbuild/minify" >&2
50
+ exit 1
51
+ fi
52
+ node "$minify/minify-runtime.mjs" --check >&2
53
+
42
54
  # Styling scripts only — never node_modules. Unstyled builds (the MVP path)
43
55
  # do not touch this; styled builds need a one-time `npm ci` inside it.
44
56
  copy "$src_styling" "$dest/vlbuild/styling"