Fix component name regex to support : and / in paths

The dep scanner regex only matched [a-zA-Z0-9_-] in component names,
missing the new path separators (/) and namespace delimiters (:).
Fixed in deps.sx spec + rebootstrapped sx_ref.py and sx-browser.js.
Also fixed the Python fallback in deps.py.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 22:27:52 +00:00
parent 6186cd1c53
commit 355f57a60b
4 changed files with 5 additions and 329 deletions

View File

@@ -14,7 +14,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-12T18:28:35Z";
var SX_VERSION = "2026-03-12T22:27:08Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -3409,7 +3409,7 @@ callExpr.push(dictGet(kwargs, k)); } }
// scan-components-from-source
var scanComponentsFromSource = function(source) { return (function() {
var matches = regexFindAll("\\(~([a-zA-Z_][a-zA-Z0-9_\\-]*)", source);
var matches = regexFindAll("\\(~([a-zA-Z_][a-zA-Z0-9_\\-:/]*)", source);
return map(function(m) { return (String("~") + String(m)); }, matches);
})(); };

View File

@@ -126,7 +126,7 @@ def _compute_all_io_refs_fallback(
def _scan_components_from_sx_fallback(source: str) -> set[str]:
import re
return {f"~{m}" for m in re.findall(r'\(~([a-zA-Z_][a-zA-Z0-9_\-]*)', source)}
return {f"~{m}" for m in re.findall(r'\(~([a-zA-Z_][a-zA-Z0-9_\-:/]*)', source)}
def _components_needed_fallback(page_sx: str, env: dict[str, Any]) -> set[str]:

View File

@@ -121,7 +121,7 @@
(define scan-components-from-source :effects []
(fn ((source :as string))
(let ((matches (regex-find-all "\\(~([a-zA-Z_][a-zA-Z0-9_\\-]*)" source)))
(let ((matches (regex-find-all "\\(~([a-zA-Z_][a-zA-Z0-9_\\-:/]*)" source)))
(map (fn ((m :as string)) (str "~" m)) matches))))

View File

@@ -2578,7 +2578,7 @@ def compute_all_deps(env):
# scan-components-from-source
def scan_components_from_source(source):
matches = regex_find_all('\\(~([a-zA-Z_][a-zA-Z0-9_\\-]*)', source)
matches = regex_find_all('\\(~([a-zA-Z_][a-zA-Z0-9_\\-:/]*)', source)
return map(lambda m: sx_str('~', m), matches)
# components-needed
@@ -2881,330 +2881,6 @@ def build_affinity_analysis(demo_components, page_plans):
return {'components': demo_components, 'page-plans': page_plans}
# === Transpiled from router (client-side route matching) ===
# split-path-segments
def split_path_segments(path):
trimmed = (slice(path, 1) if sx_truthy(starts_with_p(path, '/')) else path)
trimmed2 = (slice(trimmed, 0, (len(trimmed) - 1)) if sx_truthy(((not sx_truthy(empty_p(trimmed))) if not sx_truthy((not sx_truthy(empty_p(trimmed)))) else ends_with_p(trimmed, '/'))) else trimmed)
if sx_truthy(empty_p(trimmed2)):
return []
else:
return split(trimmed2, '/')
# make-route-segment
def make_route_segment(seg):
if sx_truthy((starts_with_p(seg, '<') if not sx_truthy(starts_with_p(seg, '<')) else ends_with_p(seg, '>'))):
param_name = slice(seg, 1, (len(seg) - 1))
d = {}
d['type'] = 'param'
d['value'] = param_name
return d
else:
d = {}
d['type'] = 'literal'
d['value'] = seg
return d
# parse-route-pattern
def parse_route_pattern(pattern):
segments = split_path_segments(pattern)
return map(make_route_segment, segments)
# match-route-segments
def match_route_segments(path_segs, parsed_segs):
_cells = {}
if sx_truthy((not sx_truthy((len(path_segs) == len(parsed_segs))))):
return NIL
else:
params = {}
_cells['matched'] = True
for_each_indexed(lambda i, parsed_seg: ((lambda path_seg: (lambda seg_type: ((_sx_cell_set(_cells, 'matched', False) if sx_truthy((not sx_truthy((path_seg == get(parsed_seg, 'value'))))) else NIL) if sx_truthy((seg_type == 'literal')) else (_sx_dict_set(params, get(parsed_seg, 'value'), path_seg) if sx_truthy((seg_type == 'param')) else _sx_cell_set(_cells, 'matched', False))))(get(parsed_seg, 'type')))(nth(path_segs, i)) if sx_truthy(_cells['matched']) else NIL), parsed_segs)
if sx_truthy(_cells['matched']):
return params
else:
return NIL
# match-route
def match_route(path, pattern):
path_segs = split_path_segments(path)
parsed_segs = parse_route_pattern(pattern)
return match_route_segments(path_segs, parsed_segs)
# find-matching-route
def find_matching_route(path, routes):
_cells = {}
match_path = ((sx_url_to_path(path) if sx_truthy(sx_url_to_path(path)) else path) if sx_truthy(starts_with_p(path, '/(')) else path)
path_segs = split_path_segments(match_path)
_cells['result'] = NIL
for route in routes:
if sx_truthy(is_nil(_cells['result'])):
params = match_route_segments(path_segs, get(route, 'parsed'))
if sx_truthy((not sx_truthy(is_nil(params)))):
matched = merge(route, {})
matched['params'] = params
_cells['result'] = matched
return _cells['result']
# _fn-to-segment
def _fn_to_segment(name):
_match = name
if _match == 'doc':
return 'docs'
elif _match == 'spec':
return 'specs'
elif _match == 'bootstrapper':
return 'bootstrappers'
elif _match == 'test':
return 'testing'
elif _match == 'example':
return 'examples'
elif _match == 'protocol':
return 'protocols'
elif _match == 'essay':
return 'essays'
elif _match == 'plan':
return 'plans'
elif _match == 'reference-detail':
return 'reference'
else:
return name
# sx-url-to-path
def sx_url_to_path(url):
if sx_truthy((not sx_truthy((starts_with_p(url, '/(') if not sx_truthy(starts_with_p(url, '/(')) else ends_with_p(url, ')'))))):
return NIL
else:
inner = slice(url, 2, (len(url) - 1))
s = replace(replace(replace(inner, '.', '/'), '(', ''), ')', '')
segs = filter(lambda s: (not sx_truthy(empty_p(s))), split(s, '/'))
return sx_str('/', join('/', map(_fn_to_segment, segs)))
# _count-leading-dots
def _count_leading_dots(s):
if sx_truthy(empty_p(s)):
return 0
else:
if sx_truthy(starts_with_p(s, '.')):
return (1 + _count_leading_dots(slice(s, 1)))
else:
return 0
# _strip-trailing-close
def _strip_trailing_close(s):
if sx_truthy(ends_with_p(s, ')')):
return _strip_trailing_close(slice(s, 0, (len(s) - 1)))
else:
return s
# _index-of-safe
def _index_of_safe(s, needle):
idx = index_of(s, needle)
if sx_truthy((is_nil(idx) if sx_truthy(is_nil(idx)) else (idx < 0))):
return NIL
else:
return idx
# _last-index-of
def _last_index_of(s, needle):
idx = _index_of_safe(s, needle)
if sx_truthy(is_nil(idx)):
return NIL
else:
rest_idx = _last_index_of(slice(s, (idx + 1)), needle)
if sx_truthy(is_nil(rest_idx)):
return idx
else:
return ((idx + 1) + rest_idx)
# _pop-sx-url-level
def _pop_sx_url_level(url):
stripped = _strip_trailing_close(url)
close_count = (len(url) - len(_strip_trailing_close(url)))
if sx_truthy((close_count <= 1)):
return '/'
else:
last_dp = _last_index_of(stripped, '.(')
if sx_truthy(is_nil(last_dp)):
return '/'
else:
return sx_str(slice(stripped, 0, last_dp), slice(url, (len(url) - (close_count - 1))))
# _pop-sx-url-levels
def _pop_sx_url_levels(url, n):
if sx_truthy((n <= 0)):
return url
else:
return _pop_sx_url_levels(_pop_sx_url_level(url), (n - 1))
# _split-pos-kw
def _split_pos_kw(tokens, i, pos, kw):
if sx_truthy((i >= len(tokens))):
return {'positional': join('.', pos), 'keywords': kw}
else:
tok = nth(tokens, i)
if sx_truthy(starts_with_p(tok, ':')):
val = (nth(tokens, (i + 1)) if sx_truthy(((i + 1) < len(tokens))) else '')
return _split_pos_kw(tokens, (i + 2), pos, append(kw, [[tok, val]]))
else:
return _split_pos_kw(tokens, (i + 1), append(pos, [tok]), kw)
# _parse-relative-body
def _parse_relative_body(body):
if sx_truthy(empty_p(body)):
return {'positional': '', 'keywords': []}
else:
return _split_pos_kw(split(body, '.'), 0, [], [])
# _extract-innermost
def _extract_innermost(url):
stripped = _strip_trailing_close(url)
suffix = slice(url, len(_strip_trailing_close(url)))
last_dp = _last_index_of(stripped, '.(')
if sx_truthy(is_nil(last_dp)):
return {'before': '/(', 'content': slice(stripped, 2), 'suffix': suffix}
else:
return {'before': slice(stripped, 0, (last_dp + 2)), 'content': slice(stripped, (last_dp + 2)), 'suffix': suffix}
# _find-kw-in-tokens
def _find_kw_in_tokens(tokens, i, kw):
if sx_truthy((i >= len(tokens))):
return NIL
else:
if sx_truthy(((nth(tokens, i) == kw) if not sx_truthy((nth(tokens, i) == kw)) else ((i + 1) < len(tokens)))):
return nth(tokens, (i + 1))
else:
return _find_kw_in_tokens(tokens, (i + 1), kw)
# _find-keyword-value
def _find_keyword_value(content, kw):
return _find_kw_in_tokens(split(content, '.'), 0, kw)
# _replace-kw-in-tokens
def _replace_kw_in_tokens(tokens, i, kw, value):
if sx_truthy((i >= len(tokens))):
return []
else:
if sx_truthy(((nth(tokens, i) == kw) if not sx_truthy((nth(tokens, i) == kw)) else ((i + 1) < len(tokens)))):
return append([kw, value], _replace_kw_in_tokens(tokens, (i + 2), kw, value))
else:
return cons(nth(tokens, i), _replace_kw_in_tokens(tokens, (i + 1), kw, value))
# _set-keyword-in-content
def _set_keyword_in_content(content, kw, value):
current = _find_keyword_value(content, kw)
if sx_truthy(is_nil(current)):
return sx_str(content, '.', kw, '.', value)
else:
return join('.', _replace_kw_in_tokens(split(content, '.'), 0, kw, value))
# _is-delta-value?
def _is_delta_value_p(s):
return ((not sx_truthy(empty_p(s))) if not sx_truthy((not sx_truthy(empty_p(s)))) else ((len(s) > 1) if not sx_truthy((len(s) > 1)) else (starts_with_p(s, '+') if sx_truthy(starts_with_p(s, '+')) else starts_with_p(s, '-'))))
# _apply-delta
def _apply_delta(current_str, delta_str):
cur = parse_int(current_str, NIL)
delta = parse_int(delta_str, NIL)
if sx_truthy((is_nil(cur) if sx_truthy(is_nil(cur)) else is_nil(delta))):
return delta_str
else:
return sx_str((cur + delta))
# _apply-kw-pairs
def _apply_kw_pairs(content, kw_pairs):
if sx_truthy(empty_p(kw_pairs)):
return content
else:
pair = first(kw_pairs)
kw = first(pair)
raw_val = nth(pair, 1)
actual_val = ((lambda current: (raw_val if sx_truthy(is_nil(current)) else _apply_delta(current, raw_val)))(_find_keyword_value(content, kw)) if sx_truthy(_is_delta_value_p(raw_val)) else raw_val)
return _apply_kw_pairs(_set_keyword_in_content(content, kw, actual_val), rest(kw_pairs))
# _apply-keywords-to-url
def _apply_keywords_to_url(url, kw_pairs):
if sx_truthy(empty_p(kw_pairs)):
return url
else:
parts = _extract_innermost(url)
new_content = _apply_kw_pairs(get(parts, 'content'), kw_pairs)
return sx_str(get(parts, 'before'), new_content, get(parts, 'suffix'))
# _normalize-relative
def _normalize_relative(url):
if sx_truthy(starts_with_p(url, '(')):
return url
else:
return sx_str('(', url, ')')
# resolve-relative-url
def resolve_relative_url(current, relative):
canonical = _normalize_relative(relative)
rel_inner = slice(canonical, 1, (len(canonical) - 1))
dots = _count_leading_dots(rel_inner)
body = slice(rel_inner, _count_leading_dots(rel_inner))
if sx_truthy((dots == 0)):
return current
else:
parsed = _parse_relative_body(body)
pos_body = get(parsed, 'positional')
kw_pairs = get(parsed, 'keywords')
after_nav = ((current if sx_truthy(empty_p(pos_body)) else (lambda stripped: (lambda suffix: sx_str(stripped, '.', pos_body, suffix))(slice(current, len(_strip_trailing_close(current)))))(_strip_trailing_close(current))) if sx_truthy((dots == 1)) else (lambda base: (base if sx_truthy(empty_p(pos_body)) else (sx_str('/(', pos_body, ')') if sx_truthy((base == '/')) else (lambda stripped: (lambda suffix: sx_str(stripped, '.(', pos_body, ')', suffix))(slice(base, len(_strip_trailing_close(base)))))(_strip_trailing_close(base)))))(_pop_sx_url_levels(current, (dots - 1))))
return _apply_keywords_to_url(after_nav, kw_pairs)
# relative-sx-url?
def relative_sx_url_p(url):
return ((starts_with_p(url, '(') if not sx_truthy(starts_with_p(url, '(')) else (not sx_truthy(starts_with_p(url, '/(')))) if sx_truthy((starts_with_p(url, '(') if not sx_truthy(starts_with_p(url, '(')) else (not sx_truthy(starts_with_p(url, '/('))))) else starts_with_p(url, '.'))
# _url-special-forms
def _url_special_forms():
return ['!source', '!inspect', '!diff', '!search', '!raw', '!json']
# url-special-form?
def url_special_form_p(name):
return (starts_with_p(name, '!') if not sx_truthy(starts_with_p(name, '!')) else contains_p(_url_special_forms(), name))
# parse-sx-url
def parse_sx_url(url):
if sx_truthy((url == '/')):
return {'type': 'home', 'raw': url}
elif sx_truthy(relative_sx_url_p(url)):
return {'type': 'relative', 'raw': url}
elif sx_truthy((starts_with_p(url, '/(!') if not sx_truthy(starts_with_p(url, '/(!')) else ends_with_p(url, ')'))):
inner = slice(url, 2, (len(url) - 1))
dot_pos = _index_of_safe(inner, '.')
paren_pos = _index_of_safe(inner, '(')
end_pos = (len(inner) if sx_truthy((is_nil(dot_pos) if not sx_truthy(is_nil(dot_pos)) else is_nil(paren_pos))) else (paren_pos if sx_truthy(is_nil(dot_pos)) else (dot_pos if sx_truthy(is_nil(paren_pos)) else min(dot_pos, paren_pos))))
form_name = slice(inner, 0, end_pos)
rest_part = slice(inner, end_pos)
inner_expr = (slice(rest_part, 1) if sx_truthy(starts_with_p(rest_part, '.')) else rest_part)
return {'type': 'special-form', 'form': form_name, 'inner': inner_expr, 'raw': url}
elif sx_truthy((starts_with_p(url, '/(~') if not sx_truthy(starts_with_p(url, '/(~')) else ends_with_p(url, ')'))):
name = slice(url, 2, (len(url) - 1))
return {'type': 'direct-component', 'name': name, 'raw': url}
elif sx_truthy((starts_with_p(url, '/(') if not sx_truthy(starts_with_p(url, '/(')) else ends_with_p(url, ')'))):
return {'type': 'absolute', 'raw': url}
else:
return {'type': 'path', 'raw': url}
# url-special-form-name
def url_special_form_name(url):
parsed = parse_sx_url(url)
if sx_truthy((get(parsed, 'type') == 'special-form')):
return get(parsed, 'form')
else:
return NIL
# url-special-form-inner
def url_special_form_inner(url):
parsed = parse_sx_url(url)
if sx_truthy((get(parsed, 'type') == 'special-form')):
return get(parsed, 'inner')
else:
return NIL
# === Transpiled from signals (reactive signal runtime) ===
# signal