Wire deps.sx into both bootstrappers, rebootstrap Python + JS

deps.sx is now a spec module that both bootstrap_py.py and bootstrap_js.py
can include via --spec-modules deps. Platform functions (component-deps,
component-set-deps!, component-css-classes, env-components, regex-find-all,
scan-css-classes) implemented natively in both Python and JS.

- Fix deps.sx: env-get-or → env-get, extract nested define to top-level
- bootstrap_py.py: SPEC_MODULES, PLATFORM_DEPS_PY, mangle entries, CLI arg
- bootstrap_js.py: SPEC_MODULES, PLATFORM_DEPS_JS, mangle entries, CLI arg
- Regenerate sx_ref.py and sx-ref.js with deps module
- deps.py: thin dispatcher (SX_USE_REF=1 → bootstrapped, else fallback)
- scan_components_from_sx now returns ~prefixed names (consistent with spec)

Verified: 541 Python tests pass, JS deps tested with Node.js, both code
paths (fallback + bootstrapped) produce identical results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 11:55:32 +00:00
parent 6739343a06
commit 4c97b03dda
7 changed files with 692 additions and 84 deletions

View File

@@ -490,6 +490,21 @@ class JSEmitter:
"log-info": "logInfo",
"log-parse-error": "logParseError",
"parse-and-load-style-dict": "parseAndLoadStyleDict",
# deps.sx
"scan-refs": "scanRefs",
"scan-refs-walk": "scanRefsWalk",
"transitive-deps": "transitiveDeps",
"compute-all-deps": "computeAllDeps",
"scan-components-from-source": "scanComponentsFromSource",
"components-needed": "componentsNeeded",
"page-component-bundle": "pageComponentBundle",
"page-css-classes": "pageCssClasses",
"component-deps": "componentDeps",
"component-set-deps!": "componentSetDeps",
"component-css-classes": "componentCssClasses",
"env-components": "envComponents",
"regex-find-all": "regexFindAll",
"scan-css-classes": "scanCssClasses",
}
if name in RENAMES:
return RENAMES[name]
@@ -1001,6 +1016,10 @@ ADAPTER_DEPS = {
"parser": [],
}
SPEC_MODULES = {
"deps": ("deps.sx", "deps (component dependency analysis)"),
}
EXTENSION_NAMES = {"continuations"}
@@ -1091,6 +1110,7 @@ def compile_ref_to_js(
adapters: list[str] | None = None,
modules: list[str] | None = None,
extensions: list[str] | None = None,
spec_modules: list[str] | None = None,
) -> str:
"""Read reference .sx files and emit JavaScript.
@@ -1104,6 +1124,9 @@ def compile_ref_to_js(
extensions: List of optional extensions to include.
Valid names: continuations.
None = no extensions.
spec_modules: List of spec module names to include.
Valid names: deps.
None = no spec modules.
"""
ref_dir = os.path.dirname(os.path.abspath(__file__))
emitter = JSEmitter()
@@ -1131,7 +1154,16 @@ def compile_ref_to_js(
for dep in ADAPTER_DEPS.get(a, []):
adapter_set.add(dep)
# Core files always included, then selected adapters
# Resolve spec modules
spec_mod_set = set()
if spec_modules:
for sm in spec_modules:
if sm not in SPEC_MODULES:
raise ValueError(f"Unknown spec module: {sm!r}. Valid: {', '.join(SPEC_MODULES)}")
spec_mod_set.add(sm)
has_deps = "deps" in spec_mod_set
# Core files always included, then selected adapters, then spec modules
sx_files = [
("eval.sx", "eval"),
("render.sx", "render (core)"),
@@ -1139,6 +1171,8 @@ def compile_ref_to_js(
for name in ("parser", "html", "sx", "dom", "engine", "orchestration", "cssx", "boot"):
if name in adapter_set:
sx_files.append(ADAPTER_FILES[name])
for name in sorted(spec_mod_set):
sx_files.append(SPEC_MODULES[name])
all_sections = []
for filename, label in sx_files:
@@ -1190,6 +1224,9 @@ def compile_ref_to_js(
parts.append(_assemble_primitives_js(prim_modules))
parts.append(PLATFORM_JS_POST)
if has_deps:
parts.append(PLATFORM_DEPS_JS)
# Parser platform must come before compiled parser.sx
if has_parser:
parts.append(adapter_platform["parser"])
@@ -1211,7 +1248,7 @@ def compile_ref_to_js(
parts.append(fixups_js(has_html, has_sx, has_dom))
if has_continuations:
parts.append(CONTINUATIONS_JS)
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label))
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label, has_deps))
parts.append(EPILOGUE)
return "\n".join(parts)
@@ -1790,6 +1827,85 @@ PLATFORM_JS_POST = '''
return NIL;
}'''
PLATFORM_DEPS_JS = '''
// =========================================================================
// Platform: deps module — component dependency analysis
// =========================================================================
function componentDeps(c) {
return c.deps ? c.deps.slice() : [];
}
function componentSetDeps(c, deps) {
c.deps = deps;
}
function componentCssClasses(c) {
return c.cssClasses ? c.cssClasses.slice() : [];
}
function envComponents(env) {
var names = [];
for (var k in env) {
var v = env[k];
if (v && (v._component || v._macro)) names.push(k);
}
return names;
}
function regexFindAll(pattern, source) {
var re = new RegExp(pattern, "g");
var results = [];
var m;
while ((m = re.exec(source)) !== null) {
if (m[1] !== undefined) results.push(m[1]);
else results.push(m[0]);
}
return results;
}
function scanCssClasses(source) {
var classes = {};
var result = [];
var m;
var re1 = /:class\\s+"([^"]*)"/g;
while ((m = re1.exec(source)) !== null) {
var parts = m[1].split(/\\s+/);
for (var i = 0; i < parts.length; i++) {
if (parts[i] && !classes[parts[i]]) {
classes[parts[i]] = true;
result.push(parts[i]);
}
}
}
var re2 = /:class\\s+\\(str\\s+((?:"[^"]*"\\s*)+)\\)/g;
while ((m = re2.exec(source)) !== null) {
var re3 = /"([^"]*)"/g;
var m2;
while ((m2 = re3.exec(m[1])) !== null) {
var parts2 = m2[1].split(/\\s+/);
for (var j = 0; j < parts2.length; j++) {
if (parts2[j] && !classes[parts2[j]]) {
classes[parts2[j]] = true;
result.push(parts2[j]);
}
}
}
}
var re4 = /;;\\s*@css\\s+(.+)/g;
while ((m = re4.exec(source)) !== null) {
var parts3 = m[1].split(/\\s+/);
for (var k = 0; k < parts3.length; k++) {
if (parts3[k] && !classes[parts3[k]]) {
classes[parts3[k]] = true;
result.push(parts3[k]);
}
}
}
return result;
}
'''
PLATFORM_PARSER_JS = r"""
// =========================================================================
// Platform interface — Parser
@@ -2836,7 +2952,7 @@ def fixups_js(has_html, has_sx, has_dom):
return "\n".join(lines)
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label):
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label, has_deps=False):
# Parser: use compiled sxParse from parser.sx, or inline a minimal fallback
if has_parser:
parser = '''
@@ -2958,6 +3074,13 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has
api_lines.append(' init: typeof bootInit === "function" ? bootInit : null,')
elif has_orch:
api_lines.append(' init: typeof engineInit === "function" ? engineInit : null,')
if has_deps:
api_lines.append(' scanRefs: scanRefs,')
api_lines.append(' transitiveDeps: transitiveDeps,')
api_lines.append(' computeAllDeps: computeAllDeps,')
api_lines.append(' componentsNeeded: componentsNeeded,')
api_lines.append(' pageComponentBundle: pageComponentBundle,')
api_lines.append(' pageCssClasses: pageCssClasses,')
api_lines.append(f' _version: "{version}"')
api_lines.append(' };')
@@ -3015,6 +3138,8 @@ if __name__ == "__main__":
help="Comma-separated primitive modules (core.* always included). Default: all")
p.add_argument("--extensions",
help="Comma-separated extensions (continuations). Default: none.")
p.add_argument("--spec-modules",
help="Comma-separated spec modules (deps). Default: none.")
p.add_argument("--output", "-o",
help="Output file (default: stdout)")
args = p.parse_args()
@@ -3022,7 +3147,8 @@ if __name__ == "__main__":
adapters = args.adapters.split(",") if args.adapters else None
modules = args.modules.split(",") if args.modules else None
extensions = args.extensions.split(",") if args.extensions else None
js = compile_ref_to_js(adapters, modules, extensions)
spec_modules = args.spec_modules.split(",") if args.spec_modules else None
js = compile_ref_to_js(adapters, modules, extensions, spec_modules)
if args.output:
with open(args.output, "w") as f: