volaro 0.1.0-alpha.6 → 0.1.0-alpha.7

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.7`), 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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
- "revision": "78304efecb7e728025c959e1fdf207749b3652ed",
2
+ "revision": "3469656bab89d0e379c1e31cb19013a21fdef556",
3
3
  "source": "git",
4
4
  "dirty": false,
5
- "content_sha256": "1747ce8d53c4ae2caed129b014f01f7e4af5619ab5358a833e0c888e940c830e"
5
+ "content_sha256": "656e9ef8c494c763da68868cf0f80ce509bf51bfd9e71b79ae153407eeb16e83"
6
6
  }
@@ -1 +1 @@
1
- 78304efecb7e728025c959e1fdf207749b3652ed
1
+ 3469656bab89d0e379c1e31cb19013a21fdef556
@@ -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
package/language/spec.md CHANGED
@@ -2536,5 +2536,6 @@ hex digits representing a Unicode scalar value; surrogates are invalid.
2536
2536
  The prototype copies the entry file's sibling `assets/` directory into the
2537
2537
  browser bundle at `assets/` for both build and dev. Literal image references
2538
2538
  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.
2539
+ The directory contains public files only, cannot contain symbolic links or
2540
+ Windows junctions, and is copied without transformations. Output `assets/` is
2541
+ compiler-owned.
@@ -482,8 +482,8 @@ Dev uses the same build path and watches asset additions, edits and deletions.
482
482
  Refresh the browser after a rebuild; this does not add automatic browser reload.
483
483
 
484
484
  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
485
+ Missing files fail with `E-ASSET-MISSING`; traversal paths, symbolic links and
486
+ Windows junctions are rejected (`E-ASSET-LINK`). Query strings and URL-encoded filenames are supported. Dynamic
487
487
  URLs are not existence-checked, but all files in the directory are copied.
488
488
  All regular files in `assets/`, including dotfiles, are public: put only intended
489
489
  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.7",
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",
@@ -48,6 +48,16 @@ try {
48
48
  const badChk = spawnSync(node, [vl, "check", bad], { encoding: "utf8" });
49
49
  check("vl check exits non-zero on broken source", badChk.status !== 0);
50
50
 
51
+ // A diagnostic's source frame uses box-drawing characters. Windows gives a
52
+ // redirected Python stdout a legacy code page (cp1252), which cannot
53
+ // encode them; PYTHONIOENCODING=ascii reproduces that on every OS.
54
+ const narrow = spawnSync(node, [vl, "check", bad],
55
+ { encoding: "utf8", env: { ...process.env, PYTHONIOENCODING: "ascii" } });
56
+ const narrowOut = (narrow.stdout || "") + (narrow.stderr || "");
57
+ check("vl check prints its diagnostic through a non-UTF-8 stdout",
58
+ !narrowOut.includes("UnicodeEncodeError") && narrowOut.includes("error[E-") && narrowOut.includes("┌─"),
59
+ narrowOut.trim().split("\n").pop());
60
+
51
61
  // `volaro routes <project>`: page-server route manifest inspection
52
62
  const posts = join(dir, "app", "posts");
53
63
  mkdirSync(posts, { recursive: true });