@lightspeed-js/core 0.3.2 → 0.4.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/compile.js CHANGED
@@ -188,12 +188,14 @@ function emitMountPlaceholders(template, componentRegistry, depth = 0) {
188
188
  let result = template;
189
189
  result = result.replace(/<([A-Z][a-zA-Z0-9]*)((?:\s+[^>]*?)?)\s*\/>/g, (_match, name, attributes) => {
190
190
  warnUnknown(name);
191
- return buildPlaceholder(name, attributes || "", "", componentRegistry?.[name.toLowerCase()]?.needs);
191
+ const entry = componentRegistry?.[name.toLowerCase()];
192
+ return buildPlaceholder(entry?.id ?? name, attributes || "", "", entry?.needs);
192
193
  });
193
194
  result = result.replace(/<([A-Z][a-zA-Z0-9]*)((?:\s+[^>]*?)?)\s*>([\s\S]*?)<\/\1\s*>/g, (_match, name, attributes, content) => {
194
195
  warnUnknown(name);
195
196
  const inner = emitMountPlaceholders(content, componentRegistry, depth + 1);
196
- return buildPlaceholder(name, attributes || "", inner, componentRegistry?.[name.toLowerCase()]?.needs);
197
+ const entry = componentRegistry?.[name.toLowerCase()];
198
+ return buildPlaceholder(entry?.id ?? name, attributes || "", inner, entry?.needs);
197
199
  });
198
200
  return result;
199
201
  }
package/dist/index.js CHANGED
@@ -1330,7 +1330,11 @@ class Mounter {
1330
1330
  };
1331
1331
  if (adopt) {
1332
1332
  const adopted = [];
1333
- if (this.adoptNodes(Array.from(fragment.childNodes), adopt, context, adopted)) {
1333
+ if (!parent)
1334
+ context.rootSlots = groupSlotFills(slotNodes);
1335
+ const adoptedAll = this.adoptNodes(Array.from(fragment.childNodes), adopt, context, adopted);
1336
+ delete context.rootSlots;
1337
+ if (adoptedAll) {
1334
1338
  for (const marker of adopted) {
1335
1339
  await this.mountMarker(marker, instance, context);
1336
1340
  }
@@ -1588,9 +1592,15 @@ class Mounter {
1588
1592
  d = found.length;
1589
1593
  continue;
1590
1594
  }
1595
+ const name = found[d].data.slice(SLOT_MARK.length);
1596
+ const region = [];
1597
+ d++;
1591
1598
  while (d < found.length && !isSlotEnd(found[d]))
1592
- d++;
1599
+ region.push(found[d++]);
1593
1600
  d++;
1601
+ if (context.rootSlots && !this.adoptNodes(context.rootSlots.get(name) ?? [], region, context, markers)) {
1602
+ return false;
1603
+ }
1594
1604
  continue;
1595
1605
  }
1596
1606
  const target = found[d++];
@@ -3031,7 +3041,13 @@ function createLightspeedApp(options) {
3031
3041
  return new LightspeedRuntime(options);
3032
3042
  }
3033
3043
  function serializeNodes(nodes) {
3034
- return nodes.map((node) => node.nodeType === 1 ? node.outerHTML : node.textContent ?? "").join("");
3044
+ const document2 = nodes[0]?.ownerDocument;
3045
+ if (!document2)
3046
+ return "";
3047
+ const template = document2.createElement("template");
3048
+ for (const node of nodes)
3049
+ template.content.appendChild(node.cloneNode(true));
3050
+ return template.innerHTML;
3035
3051
  }
3036
3052
  // src/runtime/helpers.ts
3037
3053
  function reactive(target) {
package/dist/ssg.js CHANGED
@@ -1353,7 +1353,11 @@ class Mounter {
1353
1353
  };
1354
1354
  if (adopt) {
1355
1355
  const adopted = [];
1356
- if (this.adoptNodes(Array.from(fragment.childNodes), adopt, context, adopted)) {
1356
+ if (!parent)
1357
+ context.rootSlots = groupSlotFills(slotNodes);
1358
+ const adoptedAll = this.adoptNodes(Array.from(fragment.childNodes), adopt, context, adopted);
1359
+ delete context.rootSlots;
1360
+ if (adoptedAll) {
1357
1361
  for (const marker of adopted) {
1358
1362
  await this.mountMarker(marker, instance, context);
1359
1363
  }
@@ -1611,9 +1615,15 @@ class Mounter {
1611
1615
  d = found.length;
1612
1616
  continue;
1613
1617
  }
1618
+ const name = found[d].data.slice(SLOT_MARK.length);
1619
+ const region = [];
1620
+ d++;
1614
1621
  while (d < found.length && !isSlotEnd(found[d]))
1615
- d++;
1622
+ region.push(found[d++]);
1616
1623
  d++;
1624
+ if (context.rootSlots && !this.adoptNodes(context.rootSlots.get(name) ?? [], region, context, markers)) {
1625
+ return false;
1626
+ }
1617
1627
  continue;
1618
1628
  }
1619
1629
  const target = found[d++];
@@ -3136,7 +3146,13 @@ class LightspeedRuntime {
3136
3146
  }
3137
3147
  }
3138
3148
  function serializeNodes(nodes) {
3139
- return nodes.map((node) => node.nodeType === 1 ? node.outerHTML : node.textContent ?? "").join("");
3149
+ const document2 = nodes[0]?.ownerDocument;
3150
+ if (!document2)
3151
+ return "";
3152
+ const template = document2.createElement("template");
3153
+ for (const node of nodes)
3154
+ template.content.appendChild(node.cloneNode(true));
3155
+ return template.innerHTML;
3140
3156
  }
3141
3157
 
3142
3158
  // src/ssg/render.ts
@@ -3434,13 +3450,11 @@ function allComments(document2) {
3434
3450
  return comments;
3435
3451
  }
3436
3452
  function serialiseBetween(start, end) {
3437
- let html = "";
3438
- let current = start.nextSibling;
3439
- while (current && current !== end) {
3440
- html += current.nodeType === 1 ? current.outerHTML : current.textContent ?? "";
3441
- current = current.nextSibling;
3453
+ const template = start.ownerDocument.createElement("template");
3454
+ for (let current = start.nextSibling;current && current !== end; current = current.nextSibling) {
3455
+ template.content.appendChild(current.cloneNode(true));
3442
3456
  }
3443
- return html;
3457
+ return template.innerHTML;
3444
3458
  }
3445
3459
  // src/compile/expressions.ts
3446
3460
  function expressionKey(expr, params) {
@@ -3601,22 +3615,24 @@ function addMarkerBinds(payload, params, add) {
3601
3615
  }
3602
3616
 
3603
3617
  // src/project/component-ids.ts
3604
- function componentIdOf(file) {
3618
+ function componentIdOf(file, prefix = "") {
3605
3619
  const name = file.split(/[\\/]/).pop() ?? file;
3606
- return name.replace(/\.lsp$/, "").toLowerCase();
3620
+ return (prefix + name.replace(/\.lsp$/, "")).toLowerCase();
3607
3621
  }
3608
3622
  function resolveComponentIds(found, declaredOverrides = []) {
3609
3623
  const declared = new Set(declaredOverrides.map((id) => id.toLowerCase()));
3610
3624
  const files = new Map;
3611
3625
  const rootOf = new Map;
3612
- const result = { files, overrides: [], conflicts: [], unusedOverrides: [] };
3626
+ const roots = new Map;
3627
+ const result = { files, overrides: [], conflicts: [], unusedOverrides: [], roots };
3613
3628
  const used = new Set;
3614
- for (const { file, root } of found) {
3615
- const id = componentIdOf(file);
3629
+ for (const { file, root, prefix = "" } of found) {
3630
+ const id = componentIdOf(file, prefix);
3616
3631
  const kept = files.get(id);
3617
3632
  if (kept === undefined) {
3618
3633
  files.set(id, file);
3619
3634
  rootOf.set(id, root);
3635
+ roots.set(id, { root, prefix });
3620
3636
  continue;
3621
3637
  }
3622
3638
  if (rootOf.get(id) === root) {
@@ -3643,7 +3659,7 @@ function describeConflict(conflict, rel) {
3643
3659
  // src/project/components.ts
3644
3660
  import { readFile, readdir } from "fs/promises";
3645
3661
  import { createRequire as createRequire2 } from "node:module";
3646
- import { dirname as dirname2, join, relative as relative2, resolve as resolve2 } from "path";
3662
+ import { dirname as dirname2, join, relative as relative2, resolve as resolve2, sep } from "path";
3647
3663
 
3648
3664
  // src/specifiers.ts
3649
3665
  var PACKAGE_NAME = "@lightspeed-js/core";
@@ -4292,12 +4308,14 @@ function emitMountPlaceholders(template, componentRegistry, depth = 0) {
4292
4308
  let result = template;
4293
4309
  result = result.replace(/<([A-Z][a-zA-Z0-9]*)((?:\s+[^>]*?)?)\s*\/>/g, (_match, name, attributes) => {
4294
4310
  warnUnknown(name);
4295
- return buildPlaceholder(name, attributes || "", "", componentRegistry?.[name.toLowerCase()]?.needs);
4311
+ const entry = componentRegistry?.[name.toLowerCase()];
4312
+ return buildPlaceholder(entry?.id ?? name, attributes || "", "", entry?.needs);
4296
4313
  });
4297
4314
  result = result.replace(/<([A-Z][a-zA-Z0-9]*)((?:\s+[^>]*?)?)\s*>([\s\S]*?)<\/\1\s*>/g, (_match, name, attributes, content) => {
4298
4315
  warnUnknown(name);
4299
4316
  const inner = emitMountPlaceholders(content, componentRegistry, depth + 1);
4300
- return buildPlaceholder(name, attributes || "", inner, componentRegistry?.[name.toLowerCase()]?.needs);
4317
+ const entry = componentRegistry?.[name.toLowerCase()];
4318
+ return buildPlaceholder(entry?.id ?? name, attributes || "", inner, entry?.needs);
4301
4319
  });
4302
4320
  return result;
4303
4321
  }
@@ -4406,18 +4424,43 @@ function createComponentProject(options) {
4406
4424
  const announced = new Set;
4407
4425
  let rebuilding = null;
4408
4426
  const projectModules = new Map;
4409
- function roots() {
4427
+ function rootSpecs() {
4410
4428
  const root = rootOf();
4411
- return [resolve2(root, "src"), ...extraComponentDirs.map((dir) => resolve2(root, dir))];
4429
+ return [
4430
+ { dir: resolve2(root, "src"), prefix: "" },
4431
+ ...extraComponentDirs.map((entry) => typeof entry === "string" ? { dir: resolve2(root, entry), prefix: "" } : { dir: resolve2(root, entry.dir), prefix: entry.prefix ?? "" })
4432
+ ];
4433
+ }
4434
+ function roots() {
4435
+ return rootSpecs().map((spec) => spec.dir);
4412
4436
  }
4413
4437
  async function findComponentFiles() {
4438
+ const specs = rootSpecs();
4414
4439
  const found = [];
4415
- for (const dir of roots()) {
4416
- for (const file of await findLspFiles(dir))
4417
- found.push({ file, root: dir });
4440
+ for (const spec of specs) {
4441
+ const nested = new Set(specs.map((s) => s.dir).filter((d) => d !== spec.dir && d.startsWith(spec.dir + sep)));
4442
+ for (const file of await findLspFiles(spec.dir, nested)) {
4443
+ found.push({ file, root: spec.dir, ...spec.prefix ? { prefix: spec.prefix } : {} });
4444
+ }
4418
4445
  }
4419
4446
  return found;
4420
4447
  }
4448
+ let rootOfId = new Map;
4449
+ function registryFor(id) {
4450
+ const all = Object.fromEntries(registry);
4451
+ const home = rootOfId.get(id);
4452
+ if (!home?.prefix)
4453
+ return all;
4454
+ for (const [sibling, where] of rootOfId) {
4455
+ if (where.root !== home.root)
4456
+ continue;
4457
+ const local = sibling.slice(home.prefix.length);
4458
+ const entry = registry.get(sibling);
4459
+ if (local && entry)
4460
+ all[local] = { ...entry, id: sibling };
4461
+ }
4462
+ return all;
4463
+ }
4421
4464
  function announce(ids) {
4422
4465
  const rel = (file) => relative2(rootOf(), file);
4423
4466
  for (const conflict of ids.conflicts) {
@@ -4443,6 +4486,7 @@ function createComponentProject(options) {
4443
4486
  const nextFiles = new Map;
4444
4487
  try {
4445
4488
  const ids = resolveComponentIds(await findComponentFiles(), declaredOverrides);
4489
+ rootOfId = ids.roots;
4446
4490
  conflicts = ids.conflicts;
4447
4491
  announce(ids);
4448
4492
  for (const [componentId, filePath] of ids.files) {
@@ -4507,9 +4551,9 @@ function createComponentProject(options) {
4507
4551
  }
4508
4552
  async function definitions() {
4509
4553
  await rebuild();
4510
- const registryObj = Object.fromEntries(registry);
4511
4554
  const out = [];
4512
4555
  for (const [componentId, raw] of registry) {
4556
+ const registryObj = registryFor(componentId);
4513
4557
  const scopeId = scopeIdFor(componentId);
4514
4558
  const source = raw.logic?.trim() ? await compileLogic2(raw.logic) : "";
4515
4559
  const setup = await buildSetup(source, {
@@ -4545,6 +4589,7 @@ function createComponentProject(options) {
4545
4589
  return conflicts;
4546
4590
  },
4547
4591
  roots,
4592
+ registryFor,
4548
4593
  rebuild,
4549
4594
  definitions,
4550
4595
  compileMarkup: (html, id = "index") => transformTemplate(html, id, Object.fromEntries(registry)),
@@ -4560,14 +4605,15 @@ function withSharedStyles(parsed, watch) {
4560
4605
  watch(file);
4561
4606
  return inlineStyleSources(parsed, nodeStyleReader());
4562
4607
  }
4563
- async function findLspFiles(dir) {
4608
+ async function findLspFiles(dir, skip = new Set) {
4564
4609
  const found = [];
4565
4610
  try {
4566
4611
  const entries = (await readdir(dir, { withFileTypes: true })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
4567
4612
  for (const entry of entries) {
4568
4613
  const fullPath = join(dir, entry.name);
4569
4614
  if (entry.isDirectory()) {
4570
- found.push(...await findLspFiles(fullPath));
4615
+ if (!skip.has(fullPath))
4616
+ found.push(...await findLspFiles(fullPath, skip));
4571
4617
  } else if (entry.isFile() && entry.name.endsWith(".lsp")) {
4572
4618
  found.push(fullPath);
4573
4619
  }
@@ -5,6 +5,12 @@ interface ComponentRegistry {
5
5
  logic: string;
6
6
  /** Cell keys the component itself declared - see `LSPComponent.needs`. */
7
7
  needs?: string[];
8
+ /**
9
+ * The id the component registers under, when the tag is a local name for
10
+ * it: inside a prefixed component root, a sibling's unprefixed name
11
+ * (ADR-061). Absent, the tag lowercased is the id.
12
+ */
13
+ id?: string;
8
14
  };
9
15
  }
10
16
  /**
@@ -15,6 +15,8 @@ export interface FoundComponent {
15
15
  file: string;
16
16
  /** The component root it was found under. */
17
17
  root: string;
18
+ /** The root's prefix (ADR-061): its components register as prefix + name. */
19
+ prefix?: string;
18
20
  }
19
21
  export type Conflict = {
20
22
  kind: "same-root";
@@ -39,8 +41,14 @@ export interface ComponentIds {
39
41
  conflicts: Conflict[];
40
42
  /** Names in `overrides` that replace nothing - left over after a rename or removal. */
41
43
  unusedOverrides: string[];
44
+ /** Component id to the root it came from, and that root's prefix. */
45
+ roots: Map<string, {
46
+ root: string;
47
+ prefix: string;
48
+ }>;
42
49
  }
43
- export declare function componentIdOf(file: string): string;
50
+ /** A component's id: its root's prefix and its filename, lowercased (`Slv` + `Icon.lsp` → `slvicon`). */
51
+ export declare function componentIdOf(file: string, prefix?: string): string;
44
52
  /** `found` must be in precedence order: every file of the first root, then the next. */
45
53
  export declare function resolveComponentIds(found: FoundComponent[], declaredOverrides?: readonly string[]): ComponentIds;
46
54
  /** One message per conflict, each ending in the change that fixes it (ADR-055). */
@@ -11,11 +11,21 @@ import { parseLSP } from "../parser/index.js";
11
11
  import type { ComponentDefinition } from "../runtime/components/index.js";
12
12
  import { type Conflict } from "./component-ids.js";
13
13
  export type Compiler = "esbuild" | "tsc" | "swc";
14
+ /** A component root: a folder, or a folder with a prefix (ADR-061). */
15
+ export type ComponentDir = string | {
16
+ dir: string;
17
+ prefix?: string;
18
+ };
19
+ export declare const componentDirPath: (entry: ComponentDir) => string;
14
20
  export interface ComponentProjectOptions {
15
21
  /** The project root. A function when it is only known later (Vite's configResolved). */
16
22
  root: string | (() => string);
17
- /** Roots beyond the project's own `src`, relative to the root. */
18
- componentDirs?: string[];
23
+ /**
24
+ * Roots beyond the project's own `src`, relative to the root. An entry with
25
+ * a `prefix` registers its components as prefix + name (ADR-061), and may
26
+ * be a folder inside `src`, which then belongs to it rather than to `src`.
27
+ */
28
+ componentDirs?: ComponentDir[];
19
29
  /** Names the app replaces on purpose (ADR-058). */
20
30
  overrides?: string[];
21
31
  /** For component logic at build time. */
@@ -39,6 +49,10 @@ export interface ComponentProject {
39
49
  readonly conflicts: Conflict[];
40
50
  /** Every root components come from, in precedence order: `src` first. */
41
51
  roots(): string[];
52
+ /** The registry as `id`'s template sees it: with its root's siblings under their local names (ADR-061). */
53
+ registryFor(id: string): Record<string, RegistryEntry & {
54
+ id?: string;
55
+ }>;
42
56
  /** Rescan the roots. Concurrent callers share one rescan. */
43
57
  rebuild(): Promise<void>;
44
58
  /** Compile every component, as the client's modules do, for build-time rendering. */
@@ -13,12 +13,12 @@
13
13
  * `app.hydrate(container)` on the element it put the fragment in.
14
14
  */
15
15
  import type { TemplateExpression } from "../compile/expressions.js";
16
- import type { ComponentProject, Compiler } from "../project/components.js";
16
+ import type { ComponentDir, ComponentProject, Compiler } from "../project/components.js";
17
17
  export interface FragmentRendererOptions {
18
18
  /** The project root: components are found in its `src`, then `componentDirs`. */
19
19
  root: string;
20
- /** Further component roots, relative to the root - as `lightspeedPlugin`'s. */
21
- componentDirs?: string[];
20
+ /** Further component roots, relative to the root - as `lightspeedPlugin`'s, prefixes included. */
21
+ componentDirs?: ComponentDir[];
22
22
  /** Names the app replaces on purpose (ADR-058). */
23
23
  overrides?: string[];
24
24
  compiler?: Compiler;
@@ -1,4 +1,5 @@
1
1
  import type { Plugin } from "vite";
2
+ import { type ComponentDir } from "../project/components.js";
2
3
  interface LightspeedPluginOptions {
3
4
  typescript?: boolean;
4
5
  /**
@@ -62,8 +63,13 @@ interface LightspeedPluginOptions {
62
63
  * wherever it was found. The project's own `src` is searched first and wins,
63
64
  * so an app can replace a package's component by giving a file the same
64
65
  * name - once it says so in `overrides` (ADR-058).
66
+ *
67
+ * `{ dir, prefix }` namespaces a root (ADR-061): its components register as
68
+ * prefix + name (`{ dir: 'node_modules/ui', prefix: 'Ui' }` makes its
69
+ * `Button.lsp` `<UiButton>`), and inside it a sibling's plain name still
70
+ * finds the sibling. A prefixed folder inside `src` belongs to its entry.
65
71
  */
66
- componentDirs?: string[];
72
+ componentDirs?: ComponentDir[];
67
73
  /**
68
74
  * Component names the app replaces on purpose: a file in `src` (or an
69
75
  * earlier root) with the same name as one in a later `componentDirs` root.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/dist/vite.js CHANGED
@@ -18,22 +18,24 @@ var __toESM = (mod, isNodeMode, target) => {
18
18
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
19
 
20
20
  // src/project/component-ids.ts
21
- function componentIdOf(file) {
21
+ function componentIdOf(file, prefix = "") {
22
22
  const name = file.split(/[\\/]/).pop() ?? file;
23
- return name.replace(/\.lsp$/, "").toLowerCase();
23
+ return (prefix + name.replace(/\.lsp$/, "")).toLowerCase();
24
24
  }
25
25
  function resolveComponentIds(found, declaredOverrides = []) {
26
26
  const declared = new Set(declaredOverrides.map((id) => id.toLowerCase()));
27
27
  const files = new Map;
28
28
  const rootOf = new Map;
29
- const result = { files, overrides: [], conflicts: [], unusedOverrides: [] };
29
+ const roots = new Map;
30
+ const result = { files, overrides: [], conflicts: [], unusedOverrides: [], roots };
30
31
  const used = new Set;
31
- for (const { file, root } of found) {
32
- const id = componentIdOf(file);
32
+ for (const { file, root, prefix = "" } of found) {
33
+ const id = componentIdOf(file, prefix);
33
34
  const kept = files.get(id);
34
35
  if (kept === undefined) {
35
36
  files.set(id, file);
36
37
  rootOf.set(id, root);
38
+ roots.set(id, { root, prefix });
37
39
  continue;
38
40
  }
39
41
  if (rootOf.get(id) === root) {
@@ -60,7 +62,7 @@ function describeConflict(conflict, rel) {
60
62
  // src/project/components.ts
61
63
  import { readFile, readdir } from "fs/promises";
62
64
  import { createRequire as createRequire2 } from "node:module";
63
- import { dirname as dirname2, join, relative as relative2, resolve as resolve2 } from "path";
65
+ import { dirname as dirname2, join, relative as relative2, resolve as resolve2, sep } from "path";
64
66
 
65
67
  // src/runtime/diagnostics.ts
66
68
  var PREFIX = "[lightspeed]";
@@ -782,7 +784,11 @@ class Mounter {
782
784
  };
783
785
  if (adopt) {
784
786
  const adopted = [];
785
- if (this.adoptNodes(Array.from(fragment.childNodes), adopt, context, adopted)) {
787
+ if (!parent)
788
+ context.rootSlots = groupSlotFills(slotNodes);
789
+ const adoptedAll = this.adoptNodes(Array.from(fragment.childNodes), adopt, context, adopted);
790
+ delete context.rootSlots;
791
+ if (adoptedAll) {
786
792
  for (const marker of adopted) {
787
793
  await this.mountMarker(marker, instance, context);
788
794
  }
@@ -1040,9 +1046,15 @@ class Mounter {
1040
1046
  d = found.length;
1041
1047
  continue;
1042
1048
  }
1049
+ const name = found[d].data.slice(SLOT_MARK.length);
1050
+ const region = [];
1051
+ d++;
1043
1052
  while (d < found.length && !isSlotEnd(found[d]))
1044
- d++;
1053
+ region.push(found[d++]);
1045
1054
  d++;
1055
+ if (context.rootSlots && !this.adoptNodes(context.rootSlots.get(name) ?? [], region, context, markers)) {
1056
+ return false;
1057
+ }
1046
1058
  continue;
1047
1059
  }
1048
1060
  const target = found[d++];
@@ -2556,12 +2568,14 @@ function emitMountPlaceholders(template, componentRegistry, depth = 0) {
2556
2568
  let result = template;
2557
2569
  result = result.replace(/<([A-Z][a-zA-Z0-9]*)((?:\s+[^>]*?)?)\s*\/>/g, (_match, name, attributes) => {
2558
2570
  warnUnknown(name);
2559
- return buildPlaceholder(name, attributes || "", "", componentRegistry?.[name.toLowerCase()]?.needs);
2571
+ const entry = componentRegistry?.[name.toLowerCase()];
2572
+ return buildPlaceholder(entry?.id ?? name, attributes || "", "", entry?.needs);
2560
2573
  });
2561
2574
  result = result.replace(/<([A-Z][a-zA-Z0-9]*)((?:\s+[^>]*?)?)\s*>([\s\S]*?)<\/\1\s*>/g, (_match, name, attributes, content) => {
2562
2575
  warnUnknown(name);
2563
2576
  const inner = emitMountPlaceholders(content, componentRegistry, depth + 1);
2564
- return buildPlaceholder(name, attributes || "", inner, componentRegistry?.[name.toLowerCase()]?.needs);
2577
+ const entry = componentRegistry?.[name.toLowerCase()];
2578
+ return buildPlaceholder(entry?.id ?? name, attributes || "", inner, entry?.needs);
2565
2579
  });
2566
2580
  return result;
2567
2581
  }
@@ -2659,6 +2673,7 @@ function splitAttributes(componentId, attrString) {
2659
2673
  }
2660
2674
 
2661
2675
  // src/project/components.ts
2676
+ var componentDirPath = (entry) => typeof entry === "string" ? entry : entry.dir;
2662
2677
  function createComponentProject(options) {
2663
2678
  const rootOf = () => typeof options.root === "function" ? options.root() : options.root;
2664
2679
  const extraComponentDirs = options.componentDirs ?? [];
@@ -2670,18 +2685,43 @@ function createComponentProject(options) {
2670
2685
  const announced = new Set;
2671
2686
  let rebuilding = null;
2672
2687
  const projectModules = new Map;
2673
- function roots() {
2688
+ function rootSpecs() {
2674
2689
  const root = rootOf();
2675
- return [resolve2(root, "src"), ...extraComponentDirs.map((dir) => resolve2(root, dir))];
2690
+ return [
2691
+ { dir: resolve2(root, "src"), prefix: "" },
2692
+ ...extraComponentDirs.map((entry) => typeof entry === "string" ? { dir: resolve2(root, entry), prefix: "" } : { dir: resolve2(root, entry.dir), prefix: entry.prefix ?? "" })
2693
+ ];
2694
+ }
2695
+ function roots() {
2696
+ return rootSpecs().map((spec) => spec.dir);
2676
2697
  }
2677
2698
  async function findComponentFiles() {
2699
+ const specs = rootSpecs();
2678
2700
  const found = [];
2679
- for (const dir of roots()) {
2680
- for (const file of await findLspFiles(dir))
2681
- found.push({ file, root: dir });
2701
+ for (const spec of specs) {
2702
+ const nested = new Set(specs.map((s) => s.dir).filter((d) => d !== spec.dir && d.startsWith(spec.dir + sep)));
2703
+ for (const file of await findLspFiles(spec.dir, nested)) {
2704
+ found.push({ file, root: spec.dir, ...spec.prefix ? { prefix: spec.prefix } : {} });
2705
+ }
2682
2706
  }
2683
2707
  return found;
2684
2708
  }
2709
+ let rootOfId = new Map;
2710
+ function registryFor(id) {
2711
+ const all = Object.fromEntries(registry);
2712
+ const home = rootOfId.get(id);
2713
+ if (!home?.prefix)
2714
+ return all;
2715
+ for (const [sibling, where] of rootOfId) {
2716
+ if (where.root !== home.root)
2717
+ continue;
2718
+ const local = sibling.slice(home.prefix.length);
2719
+ const entry = registry.get(sibling);
2720
+ if (local && entry)
2721
+ all[local] = { ...entry, id: sibling };
2722
+ }
2723
+ return all;
2724
+ }
2685
2725
  function announce(ids) {
2686
2726
  const rel = (file) => relative2(rootOf(), file);
2687
2727
  for (const conflict of ids.conflicts) {
@@ -2707,6 +2747,7 @@ function createComponentProject(options) {
2707
2747
  const nextFiles = new Map;
2708
2748
  try {
2709
2749
  const ids = resolveComponentIds(await findComponentFiles(), declaredOverrides);
2750
+ rootOfId = ids.roots;
2710
2751
  conflicts = ids.conflicts;
2711
2752
  announce(ids);
2712
2753
  for (const [componentId, filePath] of ids.files) {
@@ -2771,9 +2812,9 @@ function createComponentProject(options) {
2771
2812
  }
2772
2813
  async function definitions() {
2773
2814
  await rebuild();
2774
- const registryObj = Object.fromEntries(registry);
2775
2815
  const out = [];
2776
2816
  for (const [componentId, raw] of registry) {
2817
+ const registryObj = registryFor(componentId);
2777
2818
  const scopeId = scopeIdFor(componentId);
2778
2819
  const source = raw.logic?.trim() ? await compileLogic2(raw.logic) : "";
2779
2820
  const setup = await buildSetup(source, {
@@ -2809,6 +2850,7 @@ function createComponentProject(options) {
2809
2850
  return conflicts;
2810
2851
  },
2811
2852
  roots,
2853
+ registryFor,
2812
2854
  rebuild,
2813
2855
  definitions,
2814
2856
  compileMarkup: (html, id = "index") => transformTemplate(html, id, Object.fromEntries(registry)),
@@ -2824,14 +2866,15 @@ function withSharedStyles(parsed, watch) {
2824
2866
  watch(file);
2825
2867
  return inlineStyleSources(parsed, nodeStyleReader());
2826
2868
  }
2827
- async function findLspFiles(dir) {
2869
+ async function findLspFiles(dir, skip = new Set) {
2828
2870
  const found = [];
2829
2871
  try {
2830
2872
  const entries = (await readdir(dir, { withFileTypes: true })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
2831
2873
  for (const entry of entries) {
2832
2874
  const fullPath = join(dir, entry.name);
2833
2875
  if (entry.isDirectory()) {
2834
- found.push(...await findLspFiles(fullPath));
2876
+ if (!skip.has(fullPath))
2877
+ found.push(...await findLspFiles(fullPath, skip));
2835
2878
  } else if (entry.isFile() && entry.name.endsWith(".lsp")) {
2836
2879
  found.push(fullPath);
2837
2880
  }
@@ -4492,7 +4535,13 @@ class LightspeedRuntime {
4492
4535
  }
4493
4536
  }
4494
4537
  function serializeNodes(nodes) {
4495
- return nodes.map((node) => node.nodeType === 1 ? node.outerHTML : node.textContent ?? "").join("");
4538
+ const document2 = nodes[0]?.ownerDocument;
4539
+ if (!document2)
4540
+ return "";
4541
+ const template = document2.createElement("template");
4542
+ for (const node of nodes)
4543
+ template.content.appendChild(node.cloneNode(true));
4544
+ return template.innerHTML;
4496
4545
  }
4497
4546
 
4498
4547
  // src/ssg/render.ts
@@ -4790,18 +4839,16 @@ function allComments(document2) {
4790
4839
  return comments;
4791
4840
  }
4792
4841
  function serialiseBetween(start, end) {
4793
- let html = "";
4794
- let current = start.nextSibling;
4795
- while (current && current !== end) {
4796
- html += current.nodeType === 1 ? current.outerHTML : current.textContent ?? "";
4797
- current = current.nextSibling;
4842
+ const template = start.ownerDocument.createElement("template");
4843
+ for (let current = start.nextSibling;current && current !== end; current = current.nextSibling) {
4844
+ template.content.appendChild(current.cloneNode(true));
4798
4845
  }
4799
- return html;
4846
+ return template.innerHTML;
4800
4847
  }
4801
4848
 
4802
4849
  // src/vite/layout.ts
4803
4850
  import { readFile as readFile2 } from "fs/promises";
4804
- import { dirname as dirname3, join as join2, relative as relative3, resolve as resolve3, sep } from "path";
4851
+ import { dirname as dirname3, join as join2, relative as relative3, resolve as resolve3, sep as sep2 } from "path";
4805
4852
 
4806
4853
  // src/parser/layout.ts
4807
4854
  var DECLARATION2 = /<!--\s*lsp:layout\s+([^\s>][^>]*?)\s*-->/;
@@ -4903,7 +4950,7 @@ async function findLayout(name, from, root) {
4903
4950
  return { path: candidate, html: await readFile2(candidate, "utf-8") };
4904
4951
  } catch {
4905
4952
  }
4906
- if (dir === stop || !dir.startsWith(stop + sep))
4953
+ if (dir === stop || !dir.startsWith(stop + sep2))
4907
4954
  return null;
4908
4955
  const parent = dirname3(dir);
4909
4956
  if (parent === dir)
@@ -5091,7 +5138,7 @@ function resolvedByHost(entry) {
5091
5138
  // src/vite/plugin.ts
5092
5139
  import { readFile as readFile3, readdir as readdir2, writeFile as writeFile2, mkdir as mkdir2, rm } from "fs/promises";
5093
5140
  import { existsSync } from "fs";
5094
- import { basename, dirname as dirname5, isAbsolute, join as join4, relative as relative4, resolve as resolve4, sep as sep2 } from "path";
5141
+ import { basename, dirname as dirname5, isAbsolute, join as join4, relative as relative4, resolve as resolve4, sep as sep3 } from "path";
5095
5142
  var VIRTUAL_COMPONENTS = "virtual:lightspeed/components";
5096
5143
  var RESOLVED_COMPONENTS = "\x00" + VIRTUAL_COMPONENTS;
5097
5144
  function lightspeedPlugin(options = {}) {
@@ -5214,7 +5261,9 @@ function lightspeedPlugin(options = {}) {
5214
5261
  },
5215
5262
  ...extraComponentDirs.length > 0 ? {
5216
5263
  server: {
5217
- fs: { allow: [userRoot, ...extraComponentDirs.map((dir) => resolve4(userRoot, dir))] }
5264
+ fs: {
5265
+ allow: [userRoot, ...extraComponentDirs.map((dir) => resolve4(userRoot, componentDirPath(dir)))]
5266
+ }
5218
5267
  }
5219
5268
  } : {}
5220
5269
  };
@@ -5224,7 +5273,7 @@ function lightspeedPlugin(options = {}) {
5224
5273
  const pages2 = await findPages(userRoot);
5225
5274
  if (pages2.length === 0)
5226
5275
  return fsAccess;
5227
- const named = pages2.map((page) => relative4(userRoot, page).split(sep2).join("/")).join(", ");
5276
+ const named = pages2.map((page) => relative4(userRoot, page).split(sep3).join("/")).join(", ");
5228
5277
  console.log(pages2.length === 1 ? `⚡ 1 page: ${named}` : `⚡ ${pages2.length} pages: ${named}`);
5229
5278
  return {
5230
5279
  ...fsAccess,
@@ -5232,7 +5281,7 @@ function lightspeedPlugin(options = {}) {
5232
5281
  build: {
5233
5282
  rollupOptions: {
5234
5283
  input: Object.fromEntries(pages2.map((page) => [
5235
- relative4(userRoot, page).split(sep2).join("/").replace(/\.html$/, ""),
5284
+ relative4(userRoot, page).split(sep3).join("/").replace(/\.html$/, ""),
5236
5285
  page
5237
5286
  ]))
5238
5287
  }
@@ -5293,12 +5342,12 @@ ${entries.join(`,
5293
5342
  await project.rebuild();
5294
5343
  const content = await readFile3(id, "utf-8");
5295
5344
  const parsed = withSharedStyles(parseLSP(content, id), (file) => this.addWatchFile?.(file));
5296
- const componentId = basename(id, ".lsp").toLowerCase();
5345
+ const componentId = [...componentFiles].find(([, file]) => resolve4(root, file) === resolve4(id))?.[0] ?? basename(id, ".lsp").toLowerCase();
5297
5346
  let compiledLogic = parsed.logic;
5298
5347
  if (useTypeScript && parsed.logic.trim()) {
5299
5348
  compiledLogic = await project.compileLogic(parsed.logic);
5300
5349
  }
5301
- const registryObj = Object.fromEntries(componentRegistry);
5350
+ const registryObj = project.registryFor(componentId);
5302
5351
  const transformedTemplate = transformTemplate(parsed.view, componentId, registryObj);
5303
5352
  const transformedSkeleton = parsed.skeleton ? transformTemplate(parsed.skeleton, componentId, registryObj) : "";
5304
5353
  const scopeId = scopeIdFor(componentId);
@@ -5450,13 +5499,27 @@ ${entries.join(`,
5450
5499
  const registryObj = Object.fromEntries(componentRegistry);
5451
5500
  const templates = {};
5452
5501
  for (const [componentId, raw] of componentRegistry) {
5453
- templates[componentId] = transformTemplate(raw.template, componentId, registryObj);
5502
+ templates[componentId] = transformTemplate(raw.template, componentId, project.registryFor(componentId));
5454
5503
  }
5504
+ const flattens = !isDevMode && shouldRender && options.flattenStatic !== false;
5505
+ const definitions = flattens ? await project.definitions() : null;
5455
5506
  const eager = new Set;
5456
5507
  for (const page of await findPages(root)) {
5457
5508
  const html = await readFile3(page, "utf-8");
5458
5509
  const compiled = transformTemplate(html, "index", registryObj);
5459
- eagerComponents(compiled, templates).forEach((id) => eager.add(id));
5510
+ let markup = compiled;
5511
+ if (definitions) {
5512
+ try {
5513
+ markup = (await renderPage({
5514
+ html: compiled,
5515
+ definitions,
5516
+ css: cssMode,
5517
+ pageExpressions: buildExpressionTable(extractExpressions(compiled))
5518
+ })).html;
5519
+ } catch {
5520
+ }
5521
+ }
5522
+ eagerComponents(markup, templates).forEach((id) => eager.add(id));
5460
5523
  }
5461
5524
  return [...eager];
5462
5525
  }
@@ -5606,7 +5669,7 @@ ${entries.join(`,
5606
5669
  return existsSync(join4(dir, ROUTES_DIR)) ? join4(dir, ROUTES_DIR) : dir;
5607
5670
  }
5608
5671
  function pageKey(file) {
5609
- return relative4(root, file).split(sep2).join("/");
5672
+ return relative4(root, file).split(sep3).join("/");
5610
5673
  }
5611
5674
  async function findPages(dir) {
5612
5675
  if (host)
@@ -5646,7 +5709,7 @@ ${entries.join(`,
5646
5709
  routeTable.length = 0;
5647
5710
  routeDeclarations.clear();
5648
5711
  for (const file of await findPages(root)) {
5649
- const rel = relative4(base, file).split(sep2).join("/");
5712
+ const rel = relative4(base, file).split(sep3).join("/");
5650
5713
  const key = pageKey(file);
5651
5714
  let declared = null;
5652
5715
  try {
@@ -5721,9 +5784,9 @@ ${entries.join(`,
5721
5784
  const absolute = resolve4(root, file);
5722
5785
  const within = relative4(root, absolute);
5723
5786
  if (!within.startsWith("..") && !isAbsolute(within)) {
5724
- return "/" + within.split(sep2).join("/");
5787
+ return "/" + within.split(sep3).join("/");
5725
5788
  }
5726
- return absolute.split(sep2).join("/");
5789
+ return absolute.split(sep3).join("/");
5727
5790
  }
5728
5791
  }
5729
5792
  function generateComponentModule(parsed, transformedTemplate, componentId, scopeId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightspeed-js/core",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "description": "Lightspeed: a compile-first framework for islands of reactivity in static HTML.",
5
5
  "keywords": [
6
6
  "lightspeed",