@lightspeed-js/core 0.1.1 → 0.2.0

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.
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Which file answers to each component name (ADR-058).
3
+ *
4
+ * Names are a flat namespace: a component is its filename, lowercased, from
5
+ * whichever root it was found in. The first root to claim a name keeps it, and
6
+ * `src` comes first, so an app can replace a package's component.
7
+ *
8
+ * Two files answering to one name is either that deliberate override or a
9
+ * collision nobody meant, and the two look identical from the files alone. So
10
+ * the app says which it means: an override across roots has to be declared,
11
+ * and two files under the same root are always a mistake.
12
+ */
13
+ export interface FoundComponent {
14
+ /** Absolute path to the `.lsp` file. */
15
+ file: string;
16
+ /** The component root it was found under. */
17
+ root: string;
18
+ }
19
+ export type Conflict = {
20
+ kind: "same-root";
21
+ id: string;
22
+ kept: string;
23
+ other: string;
24
+ } | {
25
+ kind: "undeclared-override";
26
+ id: string;
27
+ kept: string;
28
+ other: string;
29
+ };
30
+ export interface ComponentIds {
31
+ /** Component id to the file that answers to it. */
32
+ files: Map<string, string>;
33
+ /** Declared overrides that replaced something. */
34
+ overrides: Array<{
35
+ id: string;
36
+ kept: string;
37
+ replaced: string;
38
+ }>;
39
+ conflicts: Conflict[];
40
+ /** Names in `overrides` that replace nothing - left over after a rename or removal. */
41
+ unusedOverrides: string[];
42
+ }
43
+ export declare function componentIdOf(file: string): string;
44
+ /** `found` must be in precedence order: every file of the first root, then the next. */
45
+ export declare function resolveComponentIds(found: FoundComponent[], declaredOverrides?: readonly string[]): ComponentIds;
46
+ /** One message per conflict, each ending in the change that fixes it (ADR-055). */
47
+ export declare function describeConflict(conflict: Conflict, rel: (file: string) => string): string;
@@ -61,9 +61,18 @@ interface LightspeedPluginOptions {
61
61
  * Names are a flat namespace: a component is its filename, lowercased, from
62
62
  * wherever it was found. The project's own `src` is searched first and wins,
63
63
  * so an app can replace a package's component by giving a file the same
64
- * name - deliberate, and announced in the build log rather than silent.
64
+ * name - once it says so in `overrides` (ADR-058).
65
65
  */
66
66
  componentDirs?: string[];
67
+ /**
68
+ * Component names the app replaces on purpose: a file in `src` (or an
69
+ * earlier root) with the same name as one in a later `componentDirs` root.
70
+ *
71
+ * Any other clash fails the build and warns in dev, because a deliberate
72
+ * override and an accidental collision look identical from the files alone.
73
+ * Two files under the same root are never an override.
74
+ */
75
+ overrides?: string[];
67
76
  }
68
77
  export declare function lightspeedPlugin(options?: LightspeedPluginOptions): Plugin[];
69
78
  export {};
@@ -0,0 +1 @@
1
+ export {};
package/dist/vite.js CHANGED
@@ -17,6 +17,46 @@ var __toESM = (mod, isNodeMode, target) => {
17
17
  };
18
18
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
19
 
20
+ // src/vite/component-ids.ts
21
+ function componentIdOf(file) {
22
+ const name = file.split(/[\\/]/).pop() ?? file;
23
+ return name.replace(/\.lsp$/, "").toLowerCase();
24
+ }
25
+ function resolveComponentIds(found, declaredOverrides = []) {
26
+ const declared = new Set(declaredOverrides.map((id) => id.toLowerCase()));
27
+ const files = new Map;
28
+ const rootOf = new Map;
29
+ const result = { files, overrides: [], conflicts: [], unusedOverrides: [] };
30
+ const used = new Set;
31
+ for (const { file, root } of found) {
32
+ const id = componentIdOf(file);
33
+ const kept = files.get(id);
34
+ if (kept === undefined) {
35
+ files.set(id, file);
36
+ rootOf.set(id, root);
37
+ continue;
38
+ }
39
+ if (rootOf.get(id) === root) {
40
+ result.conflicts.push({ kind: "same-root", id, kept, other: file });
41
+ } else if (declared.has(id)) {
42
+ used.add(id);
43
+ result.overrides.push({ id, kept, replaced: file });
44
+ } else {
45
+ result.conflicts.push({ kind: "undeclared-override", id, kept, other: file });
46
+ }
47
+ }
48
+ result.unusedOverrides = [...declared].filter((id) => !used.has(id)).sort();
49
+ return result;
50
+ }
51
+ function describeConflict(conflict, rel) {
52
+ const kept = rel(conflict.kept);
53
+ const other = rel(conflict.other);
54
+ if (conflict.kind === "same-root") {
55
+ 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.`;
56
+ }
57
+ return `${conflict.id}: ${kept} replaces ${other}. ` + `If that is deliberate, add "${conflict.id}" to lightspeedPlugin({ overrides }); otherwise rename one of them.`;
58
+ }
59
+
20
60
  // src/vite/csp.ts
21
61
  import { createHash } from "node:crypto";
22
62
  function styleHashes(html) {
@@ -4783,7 +4823,9 @@ function lightspeedPlugin(options = {}) {
4783
4823
  const cssMode = options.css ?? "inline";
4784
4824
  const cleanUrls = options.routes === true;
4785
4825
  const extraComponentDirs = options.componentDirs ?? [];
4826
+ const declaredOverrides = options.overrides ?? [];
4786
4827
  const announcedShadowing = new Set;
4828
+ let componentConflicts = [];
4787
4829
  const rewriteFormat = options.rewrites ?? "_redirects";
4788
4830
  const routeDeclarations = new Map;
4789
4831
  let relativeBase = false;
@@ -4921,6 +4963,10 @@ function lightspeedPlugin(options = {}) {
4921
4963
  },
4922
4964
  async buildStart() {
4923
4965
  await rebuildComponentRegistry();
4966
+ if (!isDevMode && componentConflicts.length) {
4967
+ this.error(componentConflicts.map((conflict) => describeConflict(conflict, (file) => relative3(root, file))).join(`
4968
+ `));
4969
+ }
4924
4970
  },
4925
4971
  resolveId(id) {
4926
4972
  return id === VIRTUAL_COMPONENTS ? RESOLVED_COMPONENTS : null;
@@ -5517,20 +5563,13 @@ ${entries.join(`,
5517
5563
  const registry = new Map;
5518
5564
  const files = new Map;
5519
5565
  try {
5520
- for (const filePath of await findComponentFiles()) {
5566
+ const ids = resolveComponentIds(await findComponentFiles(), declaredOverrides);
5567
+ componentConflicts = ids.conflicts;
5568
+ announceComponentIds(ids);
5569
+ for (const [componentId, filePath] of ids.files) {
5521
5570
  try {
5522
5571
  const content = await readFile2(filePath, "utf-8");
5523
5572
  const parsed = withSharedStyles(parseLSP(content, filePath));
5524
- const componentId = basename(filePath, ".lsp").toLowerCase();
5525
- const claimed = files.get(componentId);
5526
- if (claimed !== undefined) {
5527
- const pair = `${claimed}\x00${filePath}`;
5528
- if (!announcedShadowing.has(pair)) {
5529
- announcedShadowing.add(pair);
5530
- console.log(`⚡ ${componentId}: using ${relative3(root, claimed)}, shadowing ${relative3(root, filePath)}`);
5531
- }
5532
- continue;
5533
- }
5534
5573
  files.set(componentId, filePath);
5535
5574
  registry.set(componentId, {
5536
5575
  template: parsed.view,
@@ -5575,14 +5614,32 @@ ${entries.join(`,
5575
5614
  async function findComponentFiles() {
5576
5615
  const found = [];
5577
5616
  for (const dir of componentRoots()) {
5578
- found.push(...await findLspFiles(dir));
5617
+ for (const file of await findLspFiles(dir))
5618
+ found.push({ file, root: dir });
5579
5619
  }
5580
5620
  return found;
5581
5621
  }
5622
+ function announceComponentIds(ids) {
5623
+ const rel = (file) => relative3(root, file);
5624
+ for (const conflict of ids.conflicts) {
5625
+ const key = `${conflict.kind}\x00${conflict.kept}\x00${conflict.other}`;
5626
+ if (announcedShadowing.has(key))
5627
+ continue;
5628
+ announcedShadowing.add(key);
5629
+ console.warn(`⚠️ ${describeConflict(conflict, rel)}`);
5630
+ }
5631
+ for (const id of ids.unusedOverrides) {
5632
+ const key = `unused\x00${id}`;
5633
+ if (announcedShadowing.has(key))
5634
+ continue;
5635
+ announcedShadowing.add(key);
5636
+ console.warn(`⚠️ overrides lists "${id}", but nothing replaces a component of that name. Remove it from lightspeedPlugin({ overrides }).`);
5637
+ }
5638
+ }
5582
5639
  async function findLspFiles(dir) {
5583
5640
  const files = [];
5584
5641
  try {
5585
- const entries = await readdir(dir, { withFileTypes: true });
5642
+ const entries = (await readdir(dir, { withFileTypes: true })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
5586
5643
  for (const entry of entries) {
5587
5644
  const fullPath = join3(dir, entry.name);
5588
5645
  if (entry.isDirectory()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightspeed-js/core",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Lightspeed: a compile-first framework for islands of reactivity in static HTML.",
5
5
  "keywords": [
6
6
  "lightspeed",