@lightspeed-js/core 0.2.0 → 0.3.1

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/dist/ssg.js CHANGED
@@ -1292,10 +1292,13 @@ function findMarkerEnd(nodes, start) {
1292
1292
  }
1293
1293
  return -1;
1294
1294
  }
1295
+ function isSlotStash(node) {
1296
+ return node.nodeType === 1 && node.tagName === "TEMPLATE" && node.hasAttribute("data-lsp-slot");
1297
+ }
1295
1298
  function slotNodesFor(payload, between) {
1296
1299
  if (!payload.ssr)
1297
1300
  return between;
1298
- const stash = between.find((node) => node.nodeType === 1 && node.tagName === "TEMPLATE" && node.hasAttribute("data-lsp-slot"));
1301
+ const stash = between.find(isSlotStash);
1299
1302
  return stash ? Array.from(stash.content.childNodes) : [];
1300
1303
  }
1301
1304
 
@@ -2084,7 +2087,7 @@ class Mounter {
2084
2087
  return islands;
2085
2088
  }
2086
2089
  async mountRange(range, props, slotNodes) {
2087
- const child = await this.mount(range.payload.c, null, props, slotNodes, undefined, range.payload.ssr ? nodesBetween(range.start, range.end) : undefined, range.payload.ssr ? () => this.rescueRendered(range) : undefined);
2090
+ const child = await this.mount(range.payload.c, null, props, slotNodes, undefined, range.payload.ssr ? nodesBetween(range.start, range.end).filter((node) => !isSlotStash(node)) : undefined, range.payload.ssr ? () => this.rescueRendered(range) : undefined);
2088
2091
  if (!child)
2089
2092
  return null;
2090
2093
  const root = child.rootNodes.find((node) => node.nodeType === 1);
@@ -2333,6 +2336,17 @@ function componentsForPage(pageHtml, manifest) {
2333
2336
  }
2334
2337
  return dynamic ? Object.keys(manifest.components) : [...reachable];
2335
2338
  }
2339
+ function manifestForPage(pageHtml, manifest) {
2340
+ const components = {};
2341
+ for (const id of componentsForPage(pageHtml, manifest)) {
2342
+ const entry = manifest.components[id];
2343
+ if (!entry)
2344
+ continue;
2345
+ const { css, ...rest } = entry;
2346
+ components[id] = rest;
2347
+ }
2348
+ return { version: manifest.version, runtime: manifest.runtime, components };
2349
+ }
2336
2350
 
2337
2351
  // src/runtime/dom/StyleManager.ts
2338
2352
  var PENDING_STYLES = `
@@ -3428,9 +3442,1292 @@ function serialiseBetween(start, end) {
3428
3442
  }
3429
3443
  return html;
3430
3444
  }
3445
+ // src/compile/expressions.ts
3446
+ function expressionKey(expr, params) {
3447
+ return `${params.join(",")}|${expr}`;
3448
+ }
3449
+ var VOID_ELEMENTS = new Set([
3450
+ "area",
3451
+ "base",
3452
+ "br",
3453
+ "col",
3454
+ "embed",
3455
+ "hr",
3456
+ "img",
3457
+ "input",
3458
+ "link",
3459
+ "meta",
3460
+ "param",
3461
+ "source",
3462
+ "track",
3463
+ "wbr"
3464
+ ]);
3465
+ var NODE = /<!--lsp ([\s\S]*?)-->|(<!--[\s\S]*?-->)|<\/?([a-zA-Z][\w-]*)((?:"[^"]*"|'[^']*'|[^>"'])*)\/?>/g;
3466
+ var ATTR = /([a-zA-Z@:$][\w:@.-]*)\s*=\s*"([^"]*)"/g;
3467
+ function parseForExpression2(value) {
3468
+ const match = value.match(/^\s*(?:\(([^)]*)\)|([A-Za-z_$][\w$]*))\s+in\s+([\s\S]+)$/);
3469
+ if (!match)
3470
+ return { items: value.trim() };
3471
+ const alias = (match[1] ?? match[2] ?? "").split(",")[0].trim();
3472
+ return { alias: alias || undefined, items: match[3].trim() };
3473
+ }
3474
+ function extractExpressions(template, options = {}) {
3475
+ const chains = new Map;
3476
+ const found = [];
3477
+ const seen = new Set;
3478
+ const stack = [];
3479
+ const add = (expr, params, kind, writable = false) => {
3480
+ const trimmed = expr.trim();
3481
+ if (!trimmed)
3482
+ return;
3483
+ const key = `${kind}|${expressionKey(trimmed, params)}`;
3484
+ if (seen.has(key))
3485
+ return;
3486
+ seen.add(key);
3487
+ found.push({ expr: trimmed, params: [...params], kind, writable });
3488
+ };
3489
+ const aliases = () => stack.map((frame) => frame.alias).filter((alias) => !!alias);
3490
+ const flushChain = (depth) => {
3491
+ const chain = chains.get(depth);
3492
+ if (!chain)
3493
+ return;
3494
+ chains.delete(depth);
3495
+ add(composeBranchExpression(chain.branches), chain.params, "branch");
3496
+ };
3497
+ NODE.lastIndex = 0;
3498
+ let match;
3499
+ while ((match = NODE.exec(template)) !== null) {
3500
+ const [raw, markerPayload, comment, tag, attrText = ""] = match;
3501
+ if (markerPayload !== undefined) {
3502
+ addMarkerBinds(markerPayload, aliases(), add);
3503
+ continue;
3504
+ }
3505
+ if (comment !== undefined)
3506
+ continue;
3507
+ if (raw.startsWith("</")) {
3508
+ for (let i = stack.length - 1;i >= 0; i--) {
3509
+ if (stack[i].tag === tag) {
3510
+ stack.length = i;
3511
+ break;
3512
+ }
3513
+ }
3514
+ continue;
3515
+ }
3516
+ const outer = aliases();
3517
+ let alias;
3518
+ const forMatch = attrText.match(/\sdata-bind-for\s*=\s*"([^"]*)"/);
3519
+ if (forMatch) {
3520
+ const parsed = parseForExpression2(forMatch[1]);
3521
+ add(parsed.items, outer, "for");
3522
+ alias = parsed.alias;
3523
+ }
3524
+ const inner = alias ? [...outer, alias] : outer;
3525
+ ATTR.lastIndex = 0;
3526
+ let attr;
3527
+ while ((attr = ATTR.exec(attrText)) !== null) {
3528
+ const [, name, value] = attr;
3529
+ if (name === "data-bind-for")
3530
+ continue;
3531
+ if (name === "data-text")
3532
+ add(value, inner, "text");
3533
+ else if (name === "data-bind-model")
3534
+ add(value, inner, "model", true);
3535
+ else if (name === "data-bind-key")
3536
+ add(value, inner, "key");
3537
+ else if (name.startsWith("data-on-")) {
3538
+ if (/^[A-Za-z_$][\w$]*$/.test(value.trim()))
3539
+ continue;
3540
+ const handlers = (options.handlers ?? []).filter((handler) => !inner.includes(handler));
3541
+ add(value, [...inner, ...handlers, "$event"], "event");
3542
+ } else if (name.startsWith("data-bind-")) {
3543
+ const kind = name.slice("data-bind-".length);
3544
+ if (kind === "else" || kind === "hydrate" || kind === "needs")
3545
+ continue;
3546
+ add(value, inner, kind);
3547
+ }
3548
+ }
3549
+ const depth = stack.length;
3550
+ const ifAttr = attrText.match(/\sdata-bind-if\s*=\s*"([^"]*)"/);
3551
+ const elseIf = attrText.match(/\sdata-bind-else-if\s*=\s*"([^"]*)"/);
3552
+ const hasElse = /\sdata-bind-else(?![-\w])/.test(attrText);
3553
+ if (ifAttr) {
3554
+ flushChain(depth);
3555
+ chains.set(depth, { params: outer, branches: [{ condition: ifAttr[1] }] });
3556
+ } else if (elseIf && chains.has(depth)) {
3557
+ chains.get(depth).branches.push({ condition: elseIf[1] });
3558
+ } else if (hasElse && chains.has(depth)) {
3559
+ chains.get(depth).branches.push({ condition: null });
3560
+ flushChain(depth);
3561
+ } else {
3562
+ flushChain(depth);
3563
+ }
3564
+ const selfClosing = raw.endsWith("/>") || VOID_ELEMENTS.has(tag.toLowerCase());
3565
+ if (!selfClosing)
3566
+ stack.push({ tag, alias });
3567
+ }
3568
+ for (const depth of [...chains.keys()])
3569
+ flushChain(depth);
3570
+ return found;
3571
+ }
3572
+ function buildExpressionTable(found) {
3573
+ const expressions = {};
3574
+ const writers = {};
3575
+ for (const one of found) {
3576
+ const signature = ["state", "props", "shared", "pending", ...one.params];
3577
+ const key = expressionKey(one.expr, one.params);
3578
+ try {
3579
+ expressions[key] = new Function(...signature, `return (${one.expr});`);
3580
+ if (one.writable) {
3581
+ writers[key] = new Function("state", "props", "shared", "pending", "$value", ...one.params, `${one.expr} = $value;`);
3582
+ }
3583
+ } catch {
3584
+ }
3585
+ }
3586
+ return { expressions, writers };
3587
+ }
3588
+ function addMarkerBinds(payload, params, add) {
3589
+ let parsed;
3590
+ try {
3591
+ parsed = JSON.parse(payload.trim());
3592
+ } catch {
3593
+ return;
3594
+ }
3595
+ for (const expr of Object.values(parsed?.bind ?? {})) {
3596
+ if (typeof expr === "string")
3597
+ add(expr, params, "prop");
3598
+ }
3599
+ if (typeof parsed?.dyn === "string")
3600
+ add(parsed.dyn, params, "dynamic");
3601
+ }
3602
+
3603
+ // src/project/component-ids.ts
3604
+ function componentIdOf(file) {
3605
+ const name = file.split(/[\\/]/).pop() ?? file;
3606
+ return name.replace(/\.lsp$/, "").toLowerCase();
3607
+ }
3608
+ function resolveComponentIds(found, declaredOverrides = []) {
3609
+ const declared = new Set(declaredOverrides.map((id) => id.toLowerCase()));
3610
+ const files = new Map;
3611
+ const rootOf = new Map;
3612
+ const result = { files, overrides: [], conflicts: [], unusedOverrides: [] };
3613
+ const used = new Set;
3614
+ for (const { file, root } of found) {
3615
+ const id = componentIdOf(file);
3616
+ const kept = files.get(id);
3617
+ if (kept === undefined) {
3618
+ files.set(id, file);
3619
+ rootOf.set(id, root);
3620
+ continue;
3621
+ }
3622
+ if (rootOf.get(id) === root) {
3623
+ result.conflicts.push({ kind: "same-root", id, kept, other: file });
3624
+ } else if (declared.has(id)) {
3625
+ used.add(id);
3626
+ result.overrides.push({ id, kept, replaced: file });
3627
+ } else {
3628
+ result.conflicts.push({ kind: "undeclared-override", id, kept, other: file });
3629
+ }
3630
+ }
3631
+ result.unusedOverrides = [...declared].filter((id) => !used.has(id)).sort();
3632
+ return result;
3633
+ }
3634
+ function describeConflict(conflict, rel) {
3635
+ const kept = rel(conflict.kept);
3636
+ const other = rel(conflict.other);
3637
+ if (conflict.kind === "same-root") {
3638
+ return `${conflict.id}: ${kept} and ${other} both answer to <${conflict.id}>. ` + `A component's name is its filename, lowercased, across every component directory. Rename one of them.`;
3639
+ }
3640
+ return `${conflict.id}: ${kept} replaces ${other}. ` + `If that is deliberate, add "${conflict.id}" to lightspeedPlugin({ overrides }); otherwise rename one of them.`;
3641
+ }
3642
+
3643
+ // src/project/components.ts
3644
+ import { readFile, readdir } from "fs/promises";
3645
+ import { createRequire as createRequire2 } from "node:module";
3646
+ import { dirname as dirname2, join, relative as relative2, resolve as resolve2 } from "path";
3647
+
3648
+ // src/specifiers.ts
3649
+ var PACKAGE_NAME = "@lightspeed-js/core";
3650
+ var LEGACY_NAME = "lightspeed";
3651
+ var RUNTIME_SPECIFIERS = [PACKAGE_NAME, LEGACY_NAME];
3652
+ var SPECIFIER_PATTERN = "(?:@lightspeed-js\\/core|lightspeed)";
3653
+
3654
+ // src/compile/logic.ts
3655
+ var CONTEXT_KEYS = [
3656
+ "_componentInstance",
3657
+ "__lightspeedRuntime",
3658
+ "_getComponent",
3659
+ "_addHandler"
3660
+ ];
3661
+ var LIGHTSPEED_NAMED = new RegExp(`^import\\s*\\{([^}]+)\\}\\s*from\\s*['"]${SPECIFIER_PATTERN}['"]\\s*;?\\s*$`);
3662
+ var DEFAULT_IMPORT = /^import\s+([A-Za-z_$][\w$]*)\s+from\s*['"]([^'"]+)['"]\s*;?\s*$/;
3663
+ var ANY_IMPORT = /^import\s.+from\s*['"][^'"]+['"]\s*;?\s*$|^import\s*['"][^'"]+['"]\s*;?\s*$/;
3664
+ function componentIdFrom(path) {
3665
+ const file = path.split("/").pop() ?? path;
3666
+ return file.replace(/\.lsp$/, "").toLowerCase();
3667
+ }
3668
+ function joinWrappedImports(logic) {
3669
+ const out = [];
3670
+ let pending = [];
3671
+ const STARTS = /^import\s+[^(]/;
3672
+ const ENDS = /from\s*['"][^'"]*['"]\s*;?$|^import\s*['"][^'"]*['"]\s*;?$/;
3673
+ for (const line of logic.split(`
3674
+ `)) {
3675
+ const trimmed = line.trim();
3676
+ if (pending.length) {
3677
+ pending.push(trimmed);
3678
+ if (ENDS.test(trimmed)) {
3679
+ out.push(pending.join(" "), ...pending.slice(1).map(() => ""));
3680
+ pending = [];
3681
+ }
3682
+ continue;
3683
+ }
3684
+ if (STARTS.test(trimmed) && !ANY_IMPORT.test(trimmed)) {
3685
+ pending.push(trimmed);
3686
+ continue;
3687
+ }
3688
+ out.push(line);
3689
+ }
3690
+ out.push(...pending);
3691
+ return out.join(`
3692
+ `);
3693
+ }
3694
+ function compileLogic(logic) {
3695
+ const imports = [];
3696
+ const bindings = [];
3697
+ const components = [];
3698
+ const prelude = [];
3699
+ const rest = [];
3700
+ for (const line of joinWrappedImports(logic).split(`
3701
+ `)) {
3702
+ const trimmed = line.trim();
3703
+ const named = trimmed.match(LIGHTSPEED_NAMED);
3704
+ if (named) {
3705
+ for (const raw of named[1].split(",")) {
3706
+ const name = raw.trim();
3707
+ if (!name)
3708
+ continue;
3709
+ const [source, local = source] = name.split(/\s+as\s+/).map((s) => s.trim());
3710
+ prelude.push(`const ${local} = __lightspeedRuntime.${source};`);
3711
+ }
3712
+ continue;
3713
+ }
3714
+ const asDefault = trimmed.match(DEFAULT_IMPORT);
3715
+ if (asDefault && asDefault[2].endsWith(".lsp")) {
3716
+ const id = componentIdFrom(asDefault[2]);
3717
+ components.push(id);
3718
+ prelude.push(`const ${asDefault[1]} = _getComponent(${JSON.stringify(id)});`);
3719
+ continue;
3720
+ }
3721
+ if (ANY_IMPORT.test(trimmed)) {
3722
+ imports.push(trimmed);
3723
+ bindings.push(parseImport(trimmed));
3724
+ continue;
3725
+ }
3726
+ rest.push(line);
3727
+ }
3728
+ const body = [
3729
+ `const { ${CONTEXT_KEYS.join(", ")} } = __ctx;`,
3730
+ ...prelude,
3731
+ "",
3732
+ ...rest,
3733
+ "",
3734
+ captureEpilogue(declaredNames(logic))
3735
+ ].join(`
3736
+ `);
3737
+ return { imports, bindings, body, components };
3738
+ }
3739
+ function declaredNames(logic) {
3740
+ const names = new Set;
3741
+ const patterns = [
3742
+ /^(?:export\s+)?(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)/gm,
3743
+ /^(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/gm
3744
+ ];
3745
+ for (const pattern of patterns) {
3746
+ let match;
3747
+ while ((match = pattern.exec(logic)) !== null)
3748
+ names.add(match[1]);
3749
+ }
3750
+ return [...names];
3751
+ }
3752
+ function captureEpilogue(names) {
3753
+ const handlers = names.map((name) => ` if (typeof ${name} === "function") _addHandler(${JSON.stringify(name)}, ${name});`).join(`
3754
+ `);
3755
+ return [
3756
+ "// Hand the component's state and handlers to the runtime. Captured after",
3757
+ "// the body runs rather than rewritten in place: a regex over an",
3758
+ "// initialiser could not survive nested braces, which is exactly what a",
3759
+ "// list of objects is.",
3760
+ 'if (typeof state !== "undefined") _componentInstance.state = state;',
3761
+ handlers
3762
+ ].join(`
3763
+ `);
3764
+ }
3765
+ function parseImport(statement) {
3766
+ const source = statement.match(/from\s*['"]([^'"]+)['"]/)?.[1] ?? statement.match(/^import\s*['"]([^'"]+)['"]/)?.[1] ?? "";
3767
+ const names = [];
3768
+ const clause = statement.match(/^import\s+([\s\S]+?)\s+from/)?.[1]?.trim();
3769
+ if (clause) {
3770
+ const star = clause.match(/^\*\s+as\s+([A-Za-z_$][\w$]*)$/);
3771
+ if (star) {
3772
+ names.push({ local: star[1], imported: "*" });
3773
+ } else {
3774
+ const braced = clause.match(/\{([^}]*)\}/)?.[1];
3775
+ const bare = clause.replace(/\{[^}]*\}/, "").replace(/,/g, "").trim();
3776
+ if (bare)
3777
+ names.push({ local: bare, imported: "default" });
3778
+ for (const raw of (braced ?? "").split(",")) {
3779
+ const entry = raw.trim();
3780
+ if (!entry)
3781
+ continue;
3782
+ const [imported, local = imported] = entry.split(/\s+as\s+/).map((s) => s.trim());
3783
+ names.push({ local, imported });
3784
+ }
3785
+ }
3786
+ }
3787
+ return { source, names, statement };
3788
+ }
3789
+ async function buildSetup(logic, options = {}) {
3790
+ if (!logic.trim())
3791
+ return;
3792
+ const compiled = compileLogic(options.transpile ? options.transpile(logic) : logic);
3793
+ const locals = [];
3794
+ const values = [];
3795
+ for (const binding of compiled.bindings) {
3796
+ if (!binding.names.length)
3797
+ continue;
3798
+ if (!options.load) {
3799
+ throw new Error(`[lightspeed] Component logic imports "${binding.source}", but this ` + `build has no way to load project modules.`);
3800
+ }
3801
+ const namespace = await options.load(binding.source);
3802
+ for (const name of binding.names) {
3803
+ locals.push(name.local);
3804
+ values.push(name.imported === "*" ? namespace : namespace[name.imported]);
3805
+ }
3806
+ }
3807
+ const fn = new Function(...locals, "__ctx", compiled.body);
3808
+ return (ctx) => fn(...values, ctx);
3809
+ }
3810
+ var RESERVED = new Set(["state", "props", "shared", "pending"]);
3811
+ function handlerParams(logic) {
3812
+ return declaredNames(logic).filter((name) => !RESERVED.has(name));
3813
+ }
3814
+
3815
+ // src/parser/lsp.ts
3816
+ function parseLSP(content, filename) {
3817
+ const view = extractSection(content, "view");
3818
+ const skeleton = extractSection(content, "skeleton");
3819
+ const logic = extractSection(content, "logic");
3820
+ const styles = extractStyles(content);
3821
+ return {
3822
+ view,
3823
+ skeleton,
3824
+ logic,
3825
+ needs: extractSection(content, "needs").split(/[,\n]/).map((key) => key.trim()).filter(Boolean),
3826
+ ...styles,
3827
+ filename
3828
+ };
3829
+ }
3830
+ function inlineStyleSources(component, read) {
3831
+ const join = (sources, own) => {
3832
+ if (!sources.length)
3833
+ return own;
3834
+ const shared = sources.map((source) => read(source, component.filename));
3835
+ return [...shared, own].filter((block) => block.trim()).join(`
3836
+
3837
+ `);
3838
+ };
3839
+ return {
3840
+ ...component,
3841
+ style: join(component.styleSources, component.style),
3842
+ styleDeferred: join(component.styleDeferredSources, component.styleDeferred)
3843
+ };
3844
+ }
3845
+ function extractSection(content, sectionName) {
3846
+ const regex = new RegExp(`<${sectionName}[^>]*>([\\s\\S]*?)<\\/${sectionName}>`, "i");
3847
+ const match = content.match(regex);
3848
+ return match ? match[1].trim() : "";
3849
+ }
3850
+ function extractStyles(content) {
3851
+ const blocks = content.matchAll(/<style([^>]*)>([\s\S]*?)<\/style>/gi);
3852
+ const critical = [];
3853
+ const deferred = [];
3854
+ const criticalSources = [];
3855
+ const deferredSources = [];
3856
+ for (const [, attributes, body] of blocks) {
3857
+ const isDeferred = /\bdeferred\b/i.test(attributes);
3858
+ const src = attributes.match(/\bsrc\s*=\s*["']([^"']+)["']/i);
3859
+ if (src)
3860
+ (isDeferred ? deferredSources : criticalSources).push(src[1]);
3861
+ (isDeferred ? deferred : critical).push(body.trim());
3862
+ }
3863
+ return {
3864
+ style: critical.join(`
3865
+
3866
+ `),
3867
+ styleDeferred: deferred.join(`
3868
+
3869
+ `),
3870
+ styleSources: criticalSources,
3871
+ styleDeferredSources: deferredSources
3872
+ };
3873
+ }
3874
+ // src/parser/style-source-node.ts
3875
+ import { readFileSync } from "fs";
3876
+ import { dirname, relative, resolve } from "path";
3877
+ function nodeStyleReader() {
3878
+ return (specifier, from) => {
3879
+ const file = resolve(dirname(from), specifier);
3880
+ try {
3881
+ return readFileSync(file, "utf-8");
3882
+ } catch {
3883
+ throw new Error(`${relative(process.cwd(), from)}: <style src="${specifier}"> could not be read ` + `(looked in ${relative(process.cwd(), file)})`);
3884
+ }
3885
+ };
3886
+ }
3887
+ function styleSourcePaths(component) {
3888
+ return [...component.styleSources, ...component.styleDeferredSources].map((specifier) => resolve(dirname(component.filename), specifier));
3889
+ }
3890
+
3891
+ // src/parser/style.ts
3892
+ function scopeIdFor(componentId) {
3893
+ let hash = 5381;
3894
+ for (let i = 0;i < componentId.length; i++) {
3895
+ hash = (hash << 5) + hash + componentId.charCodeAt(i) >>> 0;
3896
+ }
3897
+ return `lsp-${hash.toString(36)}`;
3898
+ }
3899
+ function scopeStyles(css, scopeId) {
3900
+ if (!css.trim())
3901
+ return css;
3902
+ const attribute = `[data-lsp~="${scopeId}"]`;
3903
+ const scopes = {
3904
+ normal: `:where(${attribute})`,
3905
+ deep: `${attribute}${attribute}`
3906
+ };
3907
+ const renames = collectKeyframeRenames(css, scopeId);
3908
+ return transformRules(css, scopes, scopeId, renames);
3909
+ }
3910
+ function collectKeyframeRenames(css, scopeId) {
3911
+ const renames = new Map;
3912
+ const pattern = /@[-\w]*keyframes\s+([\w-]+)/gi;
3913
+ let match;
3914
+ while ((match = pattern.exec(css)) !== null) {
3915
+ renames.set(match[1], `${match[1]}-${scopeId}`);
3916
+ }
3917
+ return renames;
3918
+ }
3919
+ var NESTED_AT_RULES = new Set([
3920
+ "@media",
3921
+ "@supports",
3922
+ "@container",
3923
+ "@layer",
3924
+ "@scope"
3925
+ ]);
3926
+ function transformRules(css, scopes, scopeId, renames) {
3927
+ let out = "";
3928
+ let i = 0;
3929
+ while (i < css.length) {
3930
+ const prelude = readPrelude(css, i);
3931
+ i = prelude.end;
3932
+ if (i >= css.length) {
3933
+ out += prelude.text;
3934
+ break;
3935
+ }
3936
+ if (css[i] === ";") {
3937
+ out += `${prelude.text};`;
3938
+ i++;
3939
+ continue;
3940
+ }
3941
+ const bodyEnd = matchBrace(css, i);
3942
+ const body = css.slice(i + 1, bodyEnd);
3943
+ i = bodyEnd + 1;
3944
+ const trimmed = prelude.text.trim();
3945
+ if (trimmed.startsWith("@")) {
3946
+ const name = trimmed.split(/[\s({]/)[0].toLowerCase();
3947
+ if (name.endsWith("keyframes")) {
3948
+ out += `${renameKeyframe(prelude.text, scopeId)}{${body}}`;
3949
+ } else if (NESTED_AT_RULES.has(name)) {
3950
+ out += `${prelude.text}{${transformRules(body, scopes, scopeId, renames)}}`;
3951
+ } else {
3952
+ out += `${prelude.text}{${body}}`;
3953
+ }
3954
+ continue;
3955
+ }
3956
+ out += `${scopeSelectorList(prelude.text, scopes)}{${rewriteAnimations(body, renames)}}`;
3957
+ }
3958
+ return out;
3959
+ }
3960
+ function renameKeyframe(prelude, scopeId) {
3961
+ return prelude.replace(/(@[-\w]*keyframes\s+)([\w-]+)/i, (_match, keyword, name) => `${keyword}${name}-${scopeId}`);
3962
+ }
3963
+ function rewriteAnimations(body, renames) {
3964
+ if (renames.size === 0)
3965
+ return body;
3966
+ return body.replace(/(animation(?:-name)?\s*:)([^;}]*)/gi, (_match, property, value) => {
3967
+ let next = value;
3968
+ for (const [from, to] of renames) {
3969
+ next = next.replace(new RegExp(`\\b${escapeRegExp(from)}\\b`, "g"), to);
3970
+ }
3971
+ return property + next;
3972
+ });
3973
+ }
3974
+ function readPrelude(css, start) {
3975
+ let text = "";
3976
+ let i = start;
3977
+ let depth = 0;
3978
+ while (i < css.length) {
3979
+ const char = css[i];
3980
+ if (char === "/" && css[i + 1] === "*") {
3981
+ const close = css.indexOf("*/", i + 2);
3982
+ const stop = close < 0 ? css.length : close + 2;
3983
+ text += css.slice(i, stop);
3984
+ i = stop;
3985
+ continue;
3986
+ }
3987
+ if (char === '"' || char === "'") {
3988
+ const stop = skipString(css, i);
3989
+ text += css.slice(i, stop);
3990
+ i = stop;
3991
+ continue;
3992
+ }
3993
+ if (char === "(")
3994
+ depth++;
3995
+ if (char === ")")
3996
+ depth--;
3997
+ if (depth === 0 && (char === "{" || char === ";"))
3998
+ break;
3999
+ text += char;
4000
+ i++;
4001
+ }
4002
+ return { text, end: i };
4003
+ }
4004
+ function skipString(css, start) {
4005
+ const quote = css[start];
4006
+ let i = start + 1;
4007
+ while (i < css.length) {
4008
+ if (css[i] === "\\") {
4009
+ i += 2;
4010
+ continue;
4011
+ }
4012
+ if (css[i] === quote)
4013
+ return i + 1;
4014
+ i++;
4015
+ }
4016
+ return i;
4017
+ }
4018
+ function matchBrace(css, openIndex) {
4019
+ let depth = 0;
4020
+ let i = openIndex;
4021
+ while (i < css.length) {
4022
+ const char = css[i];
4023
+ if (char === "/" && css[i + 1] === "*") {
4024
+ const close = css.indexOf("*/", i + 2);
4025
+ i = close < 0 ? css.length : close + 2;
4026
+ continue;
4027
+ }
4028
+ if (char === '"' || char === "'") {
4029
+ i = skipString(css, i);
4030
+ continue;
4031
+ }
4032
+ if (char === "{")
4033
+ depth++;
4034
+ if (char === "}") {
4035
+ depth--;
4036
+ if (depth === 0)
4037
+ return i;
4038
+ }
4039
+ i++;
4040
+ }
4041
+ return css.length - 1;
4042
+ }
4043
+ function scopeSelectorList(selectors, scopes) {
4044
+ const comments = [];
4045
+ const bare = selectors.replace(/\/\*[\s\S]*?\*\//g, (comment) => {
4046
+ comments.push(comment);
4047
+ return "";
4048
+ });
4049
+ const scoped = splitTopLevel(bare, ",").map((selector) => scopeSelector(selector, scopes)).join(",");
4050
+ return comments.join("") + scoped;
4051
+ }
4052
+ function scopeSelector(selector, scopes) {
4053
+ const leading = selector.match(/^\s*/)?.[0] ?? "";
4054
+ const trailing = selector.match(/\s*$/)?.[0] ?? "";
4055
+ const trimmed = selector.trim();
4056
+ if (!trimmed)
4057
+ return selector;
4058
+ const parts = splitCompounds(trimmed);
4059
+ const deepIndex = parts.findIndex((part) => part.deep);
4060
+ if (deepIndex >= 0) {
4061
+ if (deepIndex === 0) {
4062
+ const rendered3 = parts.map((part) => part.combinator + unwrap(part.text)).join("").trimStart();
4063
+ return `${leading}${scopes.deep} ${rendered3}${trailing}`;
4064
+ }
4065
+ let anchor = -1;
4066
+ for (let i = deepIndex - 1;i >= 0; i--) {
4067
+ if (!parts[i].global) {
4068
+ anchor = i;
4069
+ break;
4070
+ }
4071
+ }
4072
+ const rendered2 = parts.map((part, index) => {
4073
+ const text = unwrap(part.text);
4074
+ return part.combinator + (index === anchor ? insertScope(text, scopes.deep) : text);
4075
+ });
4076
+ return leading + rendered2.join("") + trailing;
4077
+ }
4078
+ let targetIndex = -1;
4079
+ for (let i = parts.length - 1;i >= 0; i--) {
4080
+ if (!parts[i].global) {
4081
+ targetIndex = i;
4082
+ break;
4083
+ }
4084
+ }
4085
+ const rendered = parts.map((part, index) => {
4086
+ const text = unwrap(part.text);
4087
+ const scoped = index === targetIndex ? insertScope(text, scopes.normal) : text;
4088
+ return part.combinator + scoped;
4089
+ });
4090
+ return leading + rendered.join("") + trailing;
4091
+ }
4092
+ function unwrap(text) {
4093
+ return text.replace(/:(?:global|deep)\(\s*([\s\S]*?)\s*\)/g, "$1");
4094
+ }
4095
+ function insertScope(compound, attribute) {
4096
+ let depth = 0;
4097
+ for (let i = 0;i < compound.length; i++) {
4098
+ const char = compound[i];
4099
+ if (char === "(" || char === "[")
4100
+ depth++;
4101
+ if (char === ")" || char === "]")
4102
+ depth--;
4103
+ if (char === ":" && depth === 0) {
4104
+ return compound.slice(0, i) + attribute + compound.slice(i);
4105
+ }
4106
+ }
4107
+ return compound + attribute;
4108
+ }
4109
+ function splitCompounds(selector) {
4110
+ const parts = [];
4111
+ let depth = 0;
4112
+ let current = "";
4113
+ let combinator = "";
4114
+ let i = 0;
4115
+ const push = () => {
4116
+ if (!current)
4117
+ return;
4118
+ parts.push({
4119
+ combinator,
4120
+ text: current,
4121
+ global: /^:global\(/.test(current.trim()),
4122
+ deep: /^:deep\(/.test(current.trim())
4123
+ });
4124
+ current = "";
4125
+ combinator = "";
4126
+ };
4127
+ while (i < selector.length) {
4128
+ const char = selector[i];
4129
+ if (char === "(" || char === "[")
4130
+ depth++;
4131
+ if (char === ")" || char === "]")
4132
+ depth--;
4133
+ if (depth === 0 && /[\s>+~]/.test(char)) {
4134
+ let separator = "";
4135
+ while (i < selector.length && /[\s>+~]/.test(selector[i]) && depth === 0) {
4136
+ separator += selector[i];
4137
+ i++;
4138
+ }
4139
+ push();
4140
+ combinator = separator;
4141
+ continue;
4142
+ }
4143
+ current += char;
4144
+ i++;
4145
+ }
4146
+ push();
4147
+ return parts;
4148
+ }
4149
+ function splitTopLevel(input, delimiter) {
4150
+ const parts = [];
4151
+ let depth = 0;
4152
+ let current = "";
4153
+ let i = 0;
4154
+ while (i < input.length) {
4155
+ const char = input[i];
4156
+ if (char === '"' || char === "'") {
4157
+ const stop = skipString(input, i);
4158
+ current += input.slice(i, stop);
4159
+ i = stop;
4160
+ continue;
4161
+ }
4162
+ if (char === "(" || char === "[")
4163
+ depth++;
4164
+ if (char === ")" || char === "]")
4165
+ depth--;
4166
+ if (char === delimiter && depth === 0) {
4167
+ parts.push(current);
4168
+ current = "";
4169
+ i++;
4170
+ continue;
4171
+ }
4172
+ current += char;
4173
+ i++;
4174
+ }
4175
+ parts.push(current);
4176
+ return parts;
4177
+ }
4178
+ function escapeRegExp(value) {
4179
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4180
+ }
4181
+
4182
+ // src/parser/template.ts
4183
+ function transformTemplate(template, componentId, componentRegistry) {
4184
+ const comments = protectComments(template);
4185
+ const verbatim = protectVerbatim(comments.text, componentRegistry);
4186
+ let transformed = verbatim.text;
4187
+ transformed = hoistBlockDirectives(transformed);
4188
+ transformed = emitMountPlaceholders(transformed, componentRegistry);
4189
+ for (const [stray] of transformed.matchAll(/\$cell:[\w.-]*="[^"]*"/g)) {
4190
+ warn(`${stray} is only meaningful on a component tag - ` + `an element reads a cell with {{ shared['key'] }} or $attr="shared['key']".`);
4191
+ }
4192
+ transformed = transformed.replace(/@(\w+)="([^"]+)"/g, 'data-on-$1="$2"');
4193
+ transformed = transformed.replace(/\$needs="([^"]*)"/g, 'data-lsp-needs="$1"');
4194
+ transformed = transformed.replace(/\$shimmer="([^"]*)"/g, 'data-lsp-shimmer="$1"');
4195
+ transformed = transformed.replace(/\$shimmer(?=[\s/>])/g, 'data-lsp-shimmer=""');
4196
+ transformed = transformed.replace(/\$needs(?=[\s/>])/g, 'data-lsp-needs=""');
4197
+ transformed = transformed.replace(/\$ref="([^"]+)"/g, 'data-lsp-ref="$1"');
4198
+ transformed = transformed.replace(/\$text="([^"]+)"/g, 'data-text="$1"');
4199
+ transformed = transformed.replace(/\$([\w-]+)="([^"]+)"/g, 'data-bind-$1="$2"');
4200
+ transformed = transformed.replace(/\$else(?=[\s/>])/g, "data-bind-else");
4201
+ transformed = transformed.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_match, expression) => `<span data-text="${expression.trim()}"></span>`);
4202
+ return comments.restore(verbatim.restore(transformed));
4203
+ }
4204
+ function protectComments(template) {
4205
+ const stash = [];
4206
+ const text = template.replace(/<!--[\s\S]*?-->/g, (comment) => {
4207
+ stash.push(comment);
4208
+ return `<!--lsp-comment-${stash.length - 1}-->`;
4209
+ });
4210
+ return {
4211
+ text,
4212
+ restore: (html) => html.replace(/<!--lsp-comment-(\d+)-->/g, (_match, index) => stash[Number(index)])
4213
+ };
4214
+ }
4215
+ var VERBATIM_TAGS = /<(pre|code)\b[^>]*>[\s\S]*?<\/\1\s*>/gi;
4216
+ var RAW_SLOT = /<slot\b[^>]*\braw\b[^>]*>/i;
4217
+ function protectVerbatim(template, componentRegistry) {
4218
+ const stash = [];
4219
+ const hold = (value) => {
4220
+ stash.push(value);
4221
+ return `<!--lsp-verbatim-${stash.length - 1}-->`;
4222
+ };
4223
+ let text = template.replace(VERBATIM_TAGS, hold);
4224
+ if (componentRegistry) {
4225
+ text = text.replace(/<([A-Z][a-zA-Z0-9]*)((?:\s+[^>]*?)?)\s*>([\s\S]*?)<\/\1\s*>/g, (match, name, attributes, content) => {
4226
+ const definition = componentRegistry[name.toLowerCase()];
4227
+ if (!definition || !RAW_SLOT.test(definition.template ?? "")) {
4228
+ return match;
4229
+ }
4230
+ return `<${name}${attributes}>${hold(content)}</${name}>`;
4231
+ });
4232
+ }
4233
+ const restore = (html) => {
4234
+ let result = html;
4235
+ for (let pass = 0;pass < 10; pass++) {
4236
+ const next = result.replace(/<!--lsp-verbatim-(\d+)-->/g, (_match, index) => stash[Number(index)] ?? "");
4237
+ if (next === result)
4238
+ return result;
4239
+ result = next;
4240
+ }
4241
+ return result;
4242
+ };
4243
+ return { text, restore };
4244
+ }
4245
+ function hoistBlockDirectives(template) {
4246
+ const wrap = (tag, attributes, rebuild) => {
4247
+ const { block, rest } = splitBlockDirectives(attributes);
4248
+ if (!block.length)
4249
+ return tag;
4250
+ return `<template ${block.join(" ")}>${rebuild(rest)}</template>`;
4251
+ };
4252
+ let result = template.replace(/<([A-Z][a-zA-Z0-9]*)((?:\s+[^>]*?)?)\s*\/>/g, (match, name, attributes) => wrap(match, attributes || "", (rest) => `<${name}${rest} />`));
4253
+ result = result.replace(/<([A-Z][a-zA-Z0-9]*)((?:\s+[^>]*?)?)\s*>([\s\S]*?)<\/\1\s*>/g, (match, name, attributes, content) => wrap(match, attributes || "", (rest) => `<${name}${rest}>${content}</${name}>`));
4254
+ return result;
4255
+ }
4256
+ function splitBlockDirectives(attributes) {
4257
+ const block = [];
4258
+ let rest = attributes;
4259
+ for (const name of ["for", "if", "else-if"]) {
4260
+ const pattern = new RegExp(`\\s\\$${name}="([^"]*)"`);
4261
+ const match = rest.match(pattern);
4262
+ if (!match)
4263
+ continue;
4264
+ block.push(`data-bind-${name}="${match[1]}"`);
4265
+ rest = rest.replace(pattern, "");
4266
+ }
4267
+ const valueless = /\s\$else(?=[\s/>])/;
4268
+ if (valueless.test(rest)) {
4269
+ block.push("data-bind-else");
4270
+ rest = rest.replace(valueless, "");
4271
+ }
4272
+ const key = /\s\$key="([^"]*)"/;
4273
+ const keyMatch = rest.match(key);
4274
+ if (keyMatch && block.length) {
4275
+ block.push(`data-bind-key="${keyMatch[1]}"`);
4276
+ rest = rest.replace(key, "");
4277
+ }
4278
+ return { block, rest };
4279
+ }
4280
+ function emitMountPlaceholders(template, componentRegistry, depth = 0) {
4281
+ if (depth > 10) {
4282
+ console.warn("Max nesting reached in component placeholders - possible circular reference");
4283
+ return template;
4284
+ }
4285
+ const warnUnknown = (name) => {
4286
+ if (name.toLowerCase() === "dynamic")
4287
+ return;
4288
+ if (componentRegistry && !componentRegistry[name.toLowerCase()]) {
4289
+ console.warn(`Component not found: ${name}`);
4290
+ }
4291
+ };
4292
+ let result = template;
4293
+ result = result.replace(/<([A-Z][a-zA-Z0-9]*)((?:\s+[^>]*?)?)\s*\/>/g, (_match, name, attributes) => {
4294
+ warnUnknown(name);
4295
+ return buildPlaceholder(name, attributes || "", "", componentRegistry?.[name.toLowerCase()]?.needs);
4296
+ });
4297
+ result = result.replace(/<([A-Z][a-zA-Z0-9]*)((?:\s+[^>]*?)?)\s*>([\s\S]*?)<\/\1\s*>/g, (_match, name, attributes, content) => {
4298
+ warnUnknown(name);
4299
+ const inner = emitMountPlaceholders(content, componentRegistry, depth + 1);
4300
+ return buildPlaceholder(name, attributes || "", inner, componentRegistry?.[name.toLowerCase()]?.needs);
4301
+ });
4302
+ return result;
4303
+ }
4304
+ function splitCellKeys(value) {
4305
+ if (!value)
4306
+ return [];
4307
+ return value.split(",").map((key) => key.trim()).filter(Boolean);
4308
+ }
4309
+ var DYNAMIC_TAG = "dynamic";
4310
+ var CELL_PROP = "cell:";
4311
+ var BARE_CELL_READ = /^shared\s*(?:\[\s*(['"])([^'"]+)\1\s*\]|\.([A-Za-z_$][\w$]*))$/;
4312
+ var nudged = new Set;
4313
+ function nudgeCellProps(componentId, payload) {
4314
+ for (const [name, expr] of Object.entries(payload.bind ?? {})) {
4315
+ if (name === "class" || name === "style")
4316
+ continue;
4317
+ const match = BARE_CELL_READ.exec(expr.trim());
4318
+ if (!match)
4319
+ continue;
4320
+ const key = match[2] ?? match[3];
4321
+ const said = `${componentId}|${name}|${expr}`;
4322
+ if (nudged.has(said))
4323
+ continue;
4324
+ nudged.add(said);
4325
+ warn(`<${componentId} $${name}="${expr}"> is delivered by whoever mounts it. ` + `Write $cell:${name}="${key}" and the child subscribes to the cell itself.`);
4326
+ }
4327
+ }
4328
+ function buildPlaceholder(name, attributes, content, declared) {
4329
+ const payload = splitAttributes(name.toLowerCase(), attributes);
4330
+ if (declared?.length) {
4331
+ const seen = new Set(declared);
4332
+ payload.needs = [
4333
+ ...declared,
4334
+ ...(payload.needs ?? []).filter((key) => !seen.has(key))
4335
+ ];
4336
+ }
4337
+ nudgeCellProps(name.toLowerCase(), payload);
4338
+ if (payload.c === DYNAMIC_TAG) {
4339
+ const is = payload.bind?.is;
4340
+ delete payload.bind?.is;
4341
+ if (payload.bind && !Object.keys(payload.bind).length)
4342
+ delete payload.bind;
4343
+ if (!is) {
4344
+ warn('<Dynamic> needs $is, e.g. <Dynamic $is="field.type" />');
4345
+ }
4346
+ payload.c = "";
4347
+ payload.dyn = is ?? "";
4348
+ }
4349
+ return serializeMarker(payload) + content + MARKER_END;
4350
+ }
4351
+ function splitAttributes(componentId, attrString) {
4352
+ const payload = { c: componentId };
4353
+ if (!attrString.trim())
4354
+ return payload;
4355
+ const attrRegex = /([@$]?[A-Za-z_:][\w:.-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
4356
+ let match;
4357
+ const put = (bag, key, value) => {
4358
+ const target = payload[bag] ??= {};
4359
+ target[key] = value;
4360
+ };
4361
+ while ((match = attrRegex.exec(attrString)) !== null) {
4362
+ const [, name, doubleQuoted, singleQuoted, unquoted] = match;
4363
+ const value = doubleQuoted ?? singleQuoted ?? unquoted;
4364
+ if (name.startsWith("@")) {
4365
+ put("on", name.slice(1), value ?? "");
4366
+ continue;
4367
+ }
4368
+ if (name.startsWith("$")) {
4369
+ const directive = name.slice(1);
4370
+ if (directive === "hydrate")
4371
+ payload.hydrate = value ?? "load";
4372
+ else if (directive === "needs")
4373
+ payload.needs = splitCellKeys(value);
4374
+ else if (directive.startsWith(CELL_PROP)) {
4375
+ const propName = directive.slice(CELL_PROP.length).trim();
4376
+ const key = (value ?? "").trim();
4377
+ if (!propName) {
4378
+ warn(`<${componentId}> has $cell: with no prop name - e.g. $cell:count="ui:clicks"`);
4379
+ } else if (!key) {
4380
+ warn(`<${componentId}> $cell:${propName} needs a cell key, e.g. $cell:${propName}="ui:clicks"`);
4381
+ } else {
4382
+ put("cells", propName, key);
4383
+ }
4384
+ } else
4385
+ put("bind", directive, value ?? "");
4386
+ continue;
4387
+ }
4388
+ if (name === "class" || name === "style") {
4389
+ put("attrs", name, value ?? "");
4390
+ continue;
4391
+ }
4392
+ put("props", name, value === undefined ? true : value);
4393
+ }
4394
+ return payload;
4395
+ }
4396
+
4397
+ // src/project/components.ts
4398
+ function createComponentProject(options) {
4399
+ const rootOf = () => typeof options.root === "function" ? options.root() : options.root;
4400
+ const extraComponentDirs = options.componentDirs ?? [];
4401
+ const declaredOverrides = options.overrides ?? [];
4402
+ const compiler = options.compiler ?? "esbuild";
4403
+ const registry = new Map;
4404
+ const files = new Map;
4405
+ let conflicts = [];
4406
+ const announced = new Set;
4407
+ let rebuilding = null;
4408
+ const projectModules = new Map;
4409
+ function roots() {
4410
+ const root = rootOf();
4411
+ return [resolve2(root, "src"), ...extraComponentDirs.map((dir) => resolve2(root, dir))];
4412
+ }
4413
+ async function findComponentFiles() {
4414
+ const found = [];
4415
+ for (const dir of roots()) {
4416
+ for (const file of await findLspFiles(dir))
4417
+ found.push({ file, root: dir });
4418
+ }
4419
+ return found;
4420
+ }
4421
+ function announce(ids) {
4422
+ const rel = (file) => relative2(rootOf(), file);
4423
+ for (const conflict of ids.conflicts) {
4424
+ const key = `${conflict.kind}\x00${conflict.kept}\x00${conflict.other}`;
4425
+ if (announced.has(key))
4426
+ continue;
4427
+ announced.add(key);
4428
+ console.warn(`⚠️ ${describeConflict(conflict, rel)}`);
4429
+ }
4430
+ for (const id of ids.unusedOverrides) {
4431
+ const key = `unused\x00${id}`;
4432
+ if (announced.has(key))
4433
+ continue;
4434
+ announced.add(key);
4435
+ console.warn(`⚠️ overrides lists "${id}", but nothing replaces a component of that name. Remove it from lightspeedPlugin({ overrides }).`);
4436
+ }
4437
+ }
4438
+ function rebuild() {
4439
+ if (rebuilding)
4440
+ return rebuilding;
4441
+ rebuilding = (async () => {
4442
+ const nextRegistry = new Map;
4443
+ const nextFiles = new Map;
4444
+ try {
4445
+ const ids = resolveComponentIds(await findComponentFiles(), declaredOverrides);
4446
+ conflicts = ids.conflicts;
4447
+ announce(ids);
4448
+ for (const [componentId, filePath] of ids.files) {
4449
+ try {
4450
+ const content = await readFile(filePath, "utf-8");
4451
+ const parsed = withSharedStyles(parseLSP(content, filePath));
4452
+ nextFiles.set(componentId, filePath);
4453
+ nextRegistry.set(componentId, {
4454
+ template: parsed.view,
4455
+ skeleton: parsed.skeleton || "",
4456
+ style: parsed.style || "",
4457
+ styleDeferred: parsed.styleDeferred || "",
4458
+ logic: parsed.logic || "",
4459
+ needs: parsed.needs
4460
+ });
4461
+ } catch (error) {
4462
+ console.warn(`Failed to parse ${filePath}:`, error);
4463
+ }
4464
+ }
4465
+ registry.clear();
4466
+ for (const [id, entry] of nextRegistry)
4467
+ registry.set(id, entry);
4468
+ files.clear();
4469
+ for (const [id, file] of nextFiles)
4470
+ files.set(id, file);
4471
+ } catch (error) {
4472
+ console.warn("Failed to rebuild component registry:", error);
4473
+ } finally {
4474
+ rebuilding = null;
4475
+ }
4476
+ })();
4477
+ return rebuilding;
4478
+ }
4479
+ async function loadProjectModule(specifier, componentId) {
4480
+ const root = rootOf();
4481
+ const file = files.get(componentId);
4482
+ const from = file ? dirname2(resolve2(root, file)) : root;
4483
+ const cacheKey = `${from}|${specifier}`;
4484
+ const cached = projectModules.get(cacheKey);
4485
+ if (cached)
4486
+ return cached;
4487
+ try {
4488
+ const esbuild = await import("esbuild");
4489
+ const built = await esbuild.build({
4490
+ entryPoints: [specifier.startsWith(".") ? join(from, specifier) : specifier],
4491
+ absWorkingDir: from,
4492
+ bundle: true,
4493
+ write: false,
4494
+ format: "cjs",
4495
+ platform: "node",
4496
+ target: "es2022",
4497
+ logLevel: "silent",
4498
+ external: RUNTIME_SPECIFIERS
4499
+ });
4500
+ const module = { exports: {} };
4501
+ new Function("module", "exports", "require", built.outputFiles[0].text)(module, module.exports, createRequire2(import.meta.url));
4502
+ projectModules.set(cacheKey, module.exports);
4503
+ return module.exports;
4504
+ } catch (error) {
4505
+ throw new Error(`[lightspeed] ${componentId} imports "${specifier}", which could not be loaded for build-time rendering: ${error.message}`);
4506
+ }
4507
+ }
4508
+ async function definitions() {
4509
+ await rebuild();
4510
+ const registryObj = Object.fromEntries(registry);
4511
+ const out = [];
4512
+ for (const [componentId, raw] of registry) {
4513
+ const scopeId = scopeIdFor(componentId);
4514
+ const source = raw.logic?.trim() ? await compileLogic2(raw.logic) : "";
4515
+ const setup = await buildSetup(source, {
4516
+ load: (specifier) => loadProjectModule(specifier, componentId)
4517
+ });
4518
+ out.push({
4519
+ id: componentId,
4520
+ ...raw.needs?.length ? { needs: raw.needs } : {},
4521
+ template: transformTemplate(raw.template, componentId, registryObj),
4522
+ skeleton: raw.skeleton ? transformTemplate(raw.skeleton, componentId, registryObj) : "",
4523
+ style: scopeStyles(raw.style || "", scopeId),
4524
+ styleDeferred: scopeStyles(raw.styleDeferred || "", scopeId),
4525
+ setup,
4526
+ handlers: handlerParams(raw.logic || ""),
4527
+ ...buildExpressionTable([
4528
+ ...extractExpressions(transformTemplate(raw.template, componentId, registryObj), {
4529
+ handlers: handlerParams(raw.logic || "")
4530
+ }),
4531
+ ...extractExpressions(raw.skeleton ? transformTemplate(raw.skeleton, componentId, registryObj) : "", { handlers: handlerParams(raw.logic || "") })
4532
+ ]),
4533
+ scopeId
4534
+ });
4535
+ }
4536
+ return out;
4537
+ }
4538
+ function compileLogic2(code) {
4539
+ return compileTypeScriptAtBuildTime(code, compiler);
4540
+ }
4541
+ return {
4542
+ registry,
4543
+ files,
4544
+ get conflicts() {
4545
+ return conflicts;
4546
+ },
4547
+ roots,
4548
+ rebuild,
4549
+ definitions,
4550
+ compileMarkup: (html, id = "index") => transformTemplate(html, id, Object.fromEntries(registry)),
4551
+ compileLogic: compileLogic2
4552
+ };
4553
+ }
4554
+ function withSharedStyles(parsed, watch) {
4555
+ if (!parsed.styleSources.length && !parsed.styleDeferredSources.length) {
4556
+ return parsed;
4557
+ }
4558
+ if (watch)
4559
+ for (const file of styleSourcePaths(parsed))
4560
+ watch(file);
4561
+ return inlineStyleSources(parsed, nodeStyleReader());
4562
+ }
4563
+ async function findLspFiles(dir) {
4564
+ const found = [];
4565
+ try {
4566
+ const entries = (await readdir(dir, { withFileTypes: true })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
4567
+ for (const entry of entries) {
4568
+ const fullPath = join(dir, entry.name);
4569
+ if (entry.isDirectory()) {
4570
+ found.push(...await findLspFiles(fullPath));
4571
+ } else if (entry.isFile() && entry.name.endsWith(".lsp")) {
4572
+ found.push(fullPath);
4573
+ }
4574
+ }
4575
+ } catch {
4576
+ }
4577
+ return found;
4578
+ }
4579
+ async function compileTypeScriptAtBuildTime(code, compiler) {
4580
+ try {
4581
+ switch (compiler) {
4582
+ case "tsc":
4583
+ return await compileWithNodeTypeScript(code);
4584
+ case "esbuild":
4585
+ return await compileWithNodeEsbuild(code);
4586
+ case "swc":
4587
+ return await compileWithNodeSwc(code);
4588
+ default:
4589
+ throw new Error(`Unknown compiler: ${compiler}`);
4590
+ }
4591
+ } catch (error) {
4592
+ console.warn(`${compiler} compilation failed, falling back to type stripping:`, error);
4593
+ return stripTypeScriptTypes(code);
4594
+ }
4595
+ }
4596
+ async function compileWithNodeTypeScript(code) {
4597
+ try {
4598
+ const ts = await import("typescript");
4599
+ const result = ts.transpileModule(code, {
4600
+ compilerOptions: {
4601
+ target: ts.ScriptTarget.ES2020,
4602
+ module: ts.ModuleKind.ESNext,
4603
+ strict: false,
4604
+ removeComments: false,
4605
+ preserveConstEnums: false,
4606
+ declaration: false,
4607
+ noEmitHelpers: true,
4608
+ importHelpers: false
4609
+ }
4610
+ });
4611
+ return result.outputText;
4612
+ } catch (error) {
4613
+ throw new Error(`TypeScript compiler not available: ${error}`);
4614
+ }
4615
+ }
4616
+ async function compileWithNodeEsbuild(code) {
4617
+ try {
4618
+ const esbuild = await import("esbuild");
4619
+ const result = await esbuild.transform(code, {
4620
+ loader: "ts",
4621
+ target: "es2020",
4622
+ format: "esm",
4623
+ minify: false,
4624
+ sourcemap: false
4625
+ });
4626
+ return result.code;
4627
+ } catch (error) {
4628
+ throw new Error(`esbuild not available: ${error}`);
4629
+ }
4630
+ }
4631
+ async function compileWithNodeSwc(code) {
4632
+ try {
4633
+ const swc = await import("@swc/core");
4634
+ const result = await swc.transform(code, {
4635
+ jsc: {
4636
+ parser: {
4637
+ syntax: "typescript",
4638
+ tsx: false,
4639
+ decorators: false,
4640
+ dynamicImport: true
4641
+ },
4642
+ target: "es2020",
4643
+ loose: false
4644
+ },
4645
+ module: {
4646
+ type: "es6"
4647
+ },
4648
+ minify: false,
4649
+ sourceMaps: false
4650
+ });
4651
+ return result.code;
4652
+ } catch (error) {
4653
+ throw new Error(`SWC not available: ${error}`);
4654
+ }
4655
+ }
4656
+ function stripTypeScriptTypes(code) {
4657
+ code = code.replace(/const\s+(\w+):\s*\([^)]*\)\s*=>\s*\w+\s*=/g, "const $1 =");
4658
+ code = code.replace(/const\s+(\w+):\s*[^=]+=/g, "const $1 =");
4659
+ code = code.replace(/let\s+(\w+):\s*[^=]+=/g, "let $1 =");
4660
+ code = code.replace(/var\s+(\w+):\s*[^=]+=/g, "var $1 =");
4661
+ code = code.replace(/\(\s*(\w+):\s*[^)]+\s*\)/g, "($1)");
4662
+ code = code.replace(/function\s+(\w+)\s*\(\s*([^)]*?):\s*[^),)]+/g, "function $1($2");
4663
+ code = code.replace(/\):\s*[^{=]+\s*=>/g, ") =>");
4664
+ code = code.replace(/\):\s*[^{]+\s*{/g, ") {");
4665
+ code = code.replace(/^\s*interface\s+[^{]*{[^}]*}\s*$/gm, "");
4666
+ code = code.replace(/^\s*type\s+[^=]*=\s*[^;]*;\s*$/gm, "");
4667
+ code = code.replace(/(\w+)<[^<>]*>/g, "$1");
4668
+ code = code.replace(/(\w+)\s+as\s+\w+/g, "$1");
4669
+ return code.replace(/\s+/g, " ").trim();
4670
+ }
4671
+
4672
+ // src/ssg/fragment.ts
4673
+ import { relative as relative3 } from "path";
4674
+ var START2 = (i) => `<!--lightspeed-fragment:${i}-->`;
4675
+ var END2 = (i) => `<!--/lightspeed-fragment:${i}-->`;
4676
+ function createFragmentRenderer(options) {
4677
+ const project = createComponentProject({
4678
+ root: options.root,
4679
+ ...options.componentDirs ? { componentDirs: options.componentDirs } : {},
4680
+ ...options.overrides ? { overrides: options.overrides } : {},
4681
+ ...options.compiler ? { compiler: options.compiler } : {}
4682
+ });
4683
+ async function render(fragments, renderOptions = {}) {
4684
+ await project.rebuild();
4685
+ if (project.conflicts.length) {
4686
+ throw new Error(project.conflicts.map((conflict) => describeConflict(conflict, (file) => relative3(options.root, file))).join(`
4687
+ `));
4688
+ }
4689
+ const definitions = await project.definitions();
4690
+ const compiled = fragments.map((fragment) => project.compileMarkup(fragment));
4691
+ const markup = compiled.map((c, i) => `${START2(i)}${c}${END2(i)}`).join("");
4692
+ const expressions = new Map;
4693
+ for (const one of extractExpressions(compiled.join(`
4694
+ `))) {
4695
+ expressions.set(expressionKey(one.expr, one.params), one);
4696
+ }
4697
+ const result = await renderPage({
4698
+ html: `<!doctype html><html><head></head><body>${markup}</body></html>`,
4699
+ definitions,
4700
+ css: "external",
4701
+ flattenStatic: renderOptions.flattenStatic !== false,
4702
+ pageExpressions: buildExpressionTable([...expressions.values()])
4703
+ });
4704
+ const body = result.html.slice(result.html.indexOf("<body>") + "<body>".length, result.html.lastIndexOf("</body>"));
4705
+ const html = compiled.map((_, i) => {
4706
+ const start = body.indexOf(START2(i));
4707
+ const end = body.indexOf(END2(i));
4708
+ return start === -1 || end === -1 ? "" : body.slice(start + START2(i).length, end);
4709
+ });
4710
+ const subset = manifestForPage(compiled.join(`
4711
+ `), buildManifest(definitions));
4712
+ const metadata = Object.fromEntries(Object.entries(subset.components).map(([id, entry]) => [id, { scope: entry.scope, skeleton: entry.skeleton }]));
4713
+ const pageHead = result.html.slice(result.html.indexOf("<head>") + "<head>".length, result.html.indexOf("</head>"));
4714
+ const head = (Object.keys(metadata).length ? `<script type="application/json" id="lightspeed-manifest">${JSON.stringify(metadata)}</script>` : "") + pageHead.trim();
4715
+ return {
4716
+ html,
4717
+ head,
4718
+ css: result.css,
4719
+ cssDeferred: result.cssDeferred,
4720
+ used: result.used,
4721
+ rendered: result.rendered,
4722
+ flattened: result.flattened,
4723
+ expressions: [...expressions.values()]
4724
+ };
4725
+ }
4726
+ return { render, project };
4727
+ }
3431
4728
  // src/ssg/rebuild.ts
3432
- import { readFile, writeFile, mkdir, rm } from "fs/promises";
3433
- import { join, dirname } from "path";
4729
+ import { readFile as readFile2, writeFile, mkdir, rm } from "fs/promises";
4730
+ import { join as join2, dirname as dirname3 } from "path";
3434
4731
 
3435
4732
  // src/vite/routes.ts
3436
4733
  function rowsFrom(state, from, page) {
@@ -3471,10 +4768,10 @@ function toRoutePage(page, shell, declaration, shellDepth = 0) {
3471
4768
 
3472
4769
  // src/ssg/rebuild.ts
3473
4770
  async function rebuildPages(options) {
3474
- const manifestPath = options.manifest ?? join(MANIFEST_DIR, MANIFEST_FILE);
4771
+ const manifestPath = options.manifest ?? join2(MANIFEST_DIR, MANIFEST_FILE);
3475
4772
  let manifest;
3476
4773
  try {
3477
- manifest = JSON.parse(await readFile(manifestPath, "utf-8"));
4774
+ manifest = JSON.parse(await readFile2(manifestPath, "utf-8"));
3478
4775
  } catch {
3479
4776
  throw new Error(`No build to rebuild from: ${manifestPath} is missing or unreadable. Run a full build first.`);
3480
4777
  }
@@ -3512,8 +4809,8 @@ async function rebuildPages(options) {
3512
4809
  css: options.css ?? "inline"
3513
4810
  });
3514
4811
  const file = outputFor.get(name) ?? name;
3515
- await mkdir(dirname(join(options.dist, file)), { recursive: true });
3516
- await writeFile(join(options.dist, file), result.html);
4812
+ await mkdir(dirname3(join2(options.dist, file)), { recursive: true });
4813
+ await writeFile(join2(options.dist, file), result.html);
3517
4814
  rebuilt.push(file);
3518
4815
  if (result.content.length !== entry.content.length) {
3519
4816
  warn(`${name} read different content this time (${result.content.join(", ") || "none"} vs ${entry.content.join(", ")}). The manifest is stale - a full build will correct it.`);
@@ -3542,8 +4839,8 @@ async function rebuildPages(options) {
3542
4839
  route: { path: route.path, pattern: route.pattern, params: route.params }
3543
4840
  }
3544
4841
  });
3545
- const file = join(options.dist, outputFileFor(route.path));
3546
- await mkdir(dirname(file), { recursive: true });
4842
+ const file = join2(options.dist, outputFileFor(route.path));
4843
+ await mkdir(dirname3(file), { recursive: true });
3547
4844
  await writeFile(file, result.html);
3548
4845
  rebuilt.push(route.path);
3549
4846
  if (result.content.length && result.content.join("|") !== route.content.join("|")) {
@@ -3592,8 +4889,8 @@ async function rebuildPages(options) {
3592
4889
  route: { path: one.path, pattern: sample.pattern, params: one.params }
3593
4890
  }
3594
4891
  });
3595
- const file = join(options.dist, one.file);
3596
- await mkdir(dirname(file), { recursive: true });
4892
+ const file = join2(options.dist, one.file);
4893
+ await mkdir(dirname3(file), { recursive: true });
3597
4894
  await writeFile(file, result.html);
3598
4895
  rebuilt.push(one.path);
3599
4896
  added.push({ ...sample, path: one.path, params: one.params, content: result.content });
@@ -3601,7 +4898,7 @@ async function rebuildPages(options) {
3601
4898
  for (const path of had) {
3602
4899
  if (now.has(path))
3603
4900
  continue;
3604
- await rm(join(options.dist, outputFileFor(path)), { force: true });
4901
+ await rm(join2(options.dist, outputFileFor(path)), { force: true });
3605
4902
  removed.push(path);
3606
4903
  }
3607
4904
  }
@@ -3616,5 +4913,6 @@ export {
3616
4913
  rebuildPages,
3617
4914
  parseRoute,
3618
4915
  outputFileFor,
3619
- matchPath
4916
+ matchPath,
4917
+ createFragmentRenderer
3620
4918
  };