@supersoniks/concorde 5.1.2 → 5.1.3

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.
Files changed (130) hide show
  1. package/.agents/skills/concorde-docs/SKILL.md +1 -1
  2. package/README.md +1 -1
  3. package/ai/README.md +29 -41
  4. package/ai/skills/concorde/SKILL.md +54 -324
  5. package/ai/skills/concorde/rules/composition.md +32 -0
  6. package/ai/skills/concorde/rules/data.md +47 -0
  7. package/ai/skills/concorde/rules/http.md +25 -0
  8. package/ai/skills/concorde/rules/lists-forms.md +35 -0
  9. package/ai/skills/concorde/rules/styling.md +46 -0
  10. package/ai/skills/concorde/rules/troubleshooting.md +45 -0
  11. package/build-infos.json +1 -1
  12. package/concorde-core.bundle.js +21 -21
  13. package/concorde-core.es.js +977 -980
  14. package/custom-elements-manifest.public.config.mjs +8 -0
  15. package/{.tmp-cem/custom-elements.json → custom-elements.json} +11722 -8868
  16. package/dist/concorde-core.bundle.js +21 -21
  17. package/dist/concorde-core.es.js +977 -980
  18. package/dist/docs-mock-api-sw.js +58 -223
  19. package/dist/docs-mock-api-sw.js.map +4 -4
  20. package/dist/img/concorde/plane_01.jpg +0 -0
  21. package/dist/img/concorde/plane_02.jpg +0 -0
  22. package/dist/img/concorde/plane_03.jpg +0 -0
  23. package/dist/img/concorde/plane_04.jpg +0 -0
  24. package/dist/img/concorde/plane_05.jpg +0 -0
  25. package/dist/img/concorde/plane_06.jpg +0 -0
  26. package/dist/img/concorde/plane_07.jpg +0 -0
  27. package/dist/img/concorde/plane_08.jpg +0 -0
  28. package/dist/img/concorde/plane_09.jpg +0 -0
  29. package/dist/img/concorde/plane_10.jpg +0 -0
  30. package/dist/img/concorde/plane_11.jpg +0 -0
  31. package/dist/img/concorde/plane_12.jpg +0 -0
  32. package/dist/img/concorde/plane_13.jpg +0 -0
  33. package/dist/img/og-concorde.jpg +0 -0
  34. package/dist/robots.txt +11 -10
  35. package/docs/.htaccess +7 -0
  36. package/docs/about.html +111 -0
  37. package/docs/about.md +22 -0
  38. package/docs/assets/{index-BgG6FFPt.js → index-6sS76xWj.js} +482 -500
  39. package/docs/assets/{index-x89XINSG.css → index-DkdPbH0F.css} +1 -1
  40. package/docs/core/components/functional/date/date.md +1 -1
  41. package/docs/core/components/functional/list/list.html +7 -5
  42. package/docs/core/components/functional/list/list.md +6 -2
  43. package/docs/core/components/ui/card/card.html +1 -1
  44. package/docs/core/components/ui/card/card.md +1 -1
  45. package/docs/core/components/ui/image/image.html +17 -17
  46. package/docs/core/components/ui/image/image.md +17 -17
  47. package/docs/core/components/ui/modal/modal.html +2 -2
  48. package/docs/core/components/ui/modal/modal.md +2 -2
  49. package/docs/custom-elements.json +16559 -0
  50. package/docs/docs/decorators/bind.html +22 -21
  51. package/docs/docs/decorators/bind.md +23 -24
  52. package/docs/docs/getting-started/ai-agents.html +50 -94
  53. package/docs/docs/getting-started/ai-agents.md +29 -55
  54. package/docs/docs/getting-started/dataFlow.html +1 -1
  55. package/docs/docs/getting-started/dataFlow.md +1 -1
  56. package/docs/docs/index.html +1 -1
  57. package/docs/hubs/decorators.html +11 -16
  58. package/docs/hubs/remote-data.html +5 -9
  59. package/docs/img/og-concorde.jpg +0 -0
  60. package/docs/index.html +91 -2
  61. package/docs/index.md +2 -2
  62. package/docs/llms-full.txt +58 -74
  63. package/docs/llms.txt +6 -4
  64. package/docs/privacy.html +122 -0
  65. package/docs/privacy.md +32 -0
  66. package/docs/sitemap.xml +6 -0
  67. package/docs/src/core/components/functional/date/date.md +1 -1
  68. package/docs/src/core/components/functional/list/list.md +6 -2
  69. package/docs/src/core/components/functional/sdui/component-catalog.gaps.json +1 -1
  70. package/docs/src/core/components/functional/sdui/component-catalog.json +1 -1
  71. package/docs/src/core/components/ui/card/card.md +1 -1
  72. package/docs/src/core/components/ui/image/image.md +17 -17
  73. package/docs/src/core/components/ui/modal/modal.md +2 -2
  74. package/docs/src/docs/decorators/bind.md +23 -24
  75. package/docs/src/docs/getting-started/ai-agents.md +29 -55
  76. package/docs/src/docs/getting-started/dataFlow.md +1 -1
  77. package/docs/src/docs/search/docs-search.json +100 -55
  78. package/docs/src/tsconfig.json +11 -5
  79. package/index.html +89 -0
  80. package/package.json +8 -4
  81. package/public/img/og-concorde.jpg +0 -0
  82. package/scripts/ai-init.mjs +104 -379
  83. package/scripts/ai-init.test.mjs +81 -239
  84. package/scripts/docs-markdown-vite-plugin.ts +11 -5
  85. package/scripts/docs-public-url.mjs +6 -0
  86. package/scripts/docs.htaccess +7 -0
  87. package/scripts/generate-cem.mjs +244 -0
  88. package/scripts/generate-cem.test.mjs +122 -0
  89. package/scripts/generate-crawl-docs.mjs +29 -5
  90. package/scripts/generate-sdui-catalog.mjs +11 -10
  91. package/scripts/post-build-docs.js +3 -0
  92. package/scripts/pre-publish.mjs +2 -1
  93. package/scripts/site-pages.mjs +212 -0
  94. package/scripts/site-pages.test.mjs +116 -0
  95. package/site-pages/about.md +22 -0
  96. package/site-pages/privacy.md +32 -0
  97. package/src/core/components/functional/date/date.md +1 -1
  98. package/src/core/components/functional/list/list.md +6 -2
  99. package/src/core/components/functional/sdui/component-catalog.gaps.json +1 -1
  100. package/src/core/components/functional/sdui/component-catalog.json +1 -1
  101. package/src/core/components/ui/card/card.md +1 -1
  102. package/src/core/components/ui/image/image.md +17 -17
  103. package/src/core/components/ui/modal/modal.md +2 -2
  104. package/src/core/decorators/subscriber/bind.ts +9 -14
  105. package/src/docs/components/docs-demo-sources.ts +3 -3
  106. package/src/docs/decorators/bind.md +23 -24
  107. package/src/docs/docs-location.spec.ts +10 -0
  108. package/src/docs/docs-location.ts +10 -0
  109. package/src/docs/example/decorators-demo-bind-demos.ts +11 -28
  110. package/src/docs/example/docs-toggle-demos.ts +1 -1
  111. package/src/docs/getting-started/ai-agents.md +29 -55
  112. package/src/docs/getting-started/dataFlow.md +1 -1
  113. package/src/docs/home/home.ts +3 -1
  114. package/src/docs/navigation/navigation.ts +1 -1
  115. package/src/docs/search/docs-search.json +100 -55
  116. package/src/docs/search/markdown-renderer.ts +6 -1
  117. package/src/docs/tailwind/css/docs/content.css +4 -5
  118. package/src/tsconfig.json +11 -5
  119. package/src/tsconfig.tsbuildinfo +1 -1
  120. package/ai/AGENTS.md +0 -56
  121. package/ai/cursor/rules/concorde-menu.mdc +0 -15
  122. package/ai/cursor/rules/concorde-scope.mdc +0 -14
  123. package/ai/cursor/rules/concorde-theme.mdc +0 -13
  124. package/ai/cursor/rules/concorde.mdc +0 -66
  125. package/ai/jetbrains/rules/concorde.md +0 -53
  126. package/ai/skills/concorde-get-set-dp/SKILL.md +0 -194
  127. package/ai/skills/concorde-imports/SKILL.md +0 -78
  128. package/ai/skills/concorde-menu/SKILL.md +0 -74
  129. package/ai/skills/concorde-scope/SKILL.md +0 -70
  130. package/ai/skills/concorde-theme/SKILL.md +0 -57
@@ -3,257 +3,99 @@ import fs from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import test from "node:test";
6
- import {
7
- buildAgentsFile,
8
- decideAgentsAction,
9
- preambleDuplicatesBase,
10
- readOverlayAgents,
11
- slimPreamble,
12
- } from "./ai-init.mjs";
6
+ import {SKILLS_DIR, linkSkills, syncSkills} from "./ai-init.mjs";
13
7
 
14
- const base = `# Agents — Concorde
8
+ async function makeSkill(dir, name, body = "x") {
9
+ await fs.mkdir(path.join(dir, name), {recursive: true});
10
+ await fs.writeFile(path.join(dir, name, "SKILL.md"), `---\nname: ${name}\n---\n${body}\n`);
11
+ }
15
12
 
16
- Guide for Concorde.
17
-
18
- ## Skills / rules
19
-
20
- | File | Role |
21
- |------|------|
22
- | \`.cursor/skills/concorde/SKILL.md\` | Patterns |
23
-
24
- ## Conventions impératives
25
-
26
- - Always **DataProvider**
27
- - No **sonic-fetch**, no **PublisherManager**
28
- - Forms: **formDataProvider**
29
- - Skill **concorde-get-set-dp**
30
- `;
31
-
32
- const overlay = `## Conventions (project)
33
-
34
- - HTTP routes: \`endpoints.*\`
35
- `;
36
-
37
- test("decideAgentsAction: missing file → create", () => {
38
- assert.equal(decideAgentsAction({existingText: ""}), "create");
39
- });
40
-
41
- test("decideAgentsAction: project-owned AGENTS.md → skip by default", () => {
42
- assert.equal(
43
- decideAgentsAction({existingText: "# Billetterie\n\nProject notes.\n"}),
44
- "skip",
45
- );
46
- });
47
-
48
- test("decideAgentsAction: --skip-agents always skips", () => {
49
- assert.equal(
50
- decideAgentsAction({existingText: "", skipAgents: true}),
51
- "skip",
52
- );
53
- });
54
-
55
- test("decideAgentsAction: --merge-agents on project-owned → merge", () => {
56
- assert.equal(
57
- decideAgentsAction({
58
- existingText: "# Billetterie\n\nNotes.\n",
59
- mergeAgents: true,
60
- }),
61
- "merge",
62
- );
63
- });
64
-
65
- test("decideAgentsAction: --force-agents on project-owned → force", () => {
66
- assert.equal(
67
- decideAgentsAction({
68
- existingText: "# Billetterie\n\nNotes.\n",
69
- forceAgents: true,
70
- }),
71
- "force",
72
- );
73
- });
74
-
75
- test("decideAgentsAction: marked file → refresh", () => {
76
- assert.equal(
77
- decideAgentsAction({
78
- existingText: "# App\n\n<!-- concorde-ai -->\n\nold\n",
79
- }),
80
- "refresh",
81
- );
82
- });
83
-
84
- test("forceAgents replaces project-owned content", () => {
85
- const {markdown} = buildAgentsFile({
86
- existing: "# Old project notes\n\nkeep me",
87
- baseContent: base,
88
- overlayContent: "",
89
- forceAgents: true,
13
+ async function withProject(fn) {
14
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "concorde-skills-"));
15
+ try {
16
+ await fn(root);
17
+ } finally {
18
+ await fs.rm(root, {recursive: true, force: true});
19
+ }
20
+ }
21
+
22
+ test("syncSkills copies a skill with its rules/ folder into .agents/skills", async () => {
23
+ await withProject(async (root) => {
24
+ const pkg = path.join(root, "pkg");
25
+ await makeSkill(pkg, "concorde");
26
+ await fs.mkdir(path.join(pkg, "concorde", "rules"));
27
+ await fs.writeFile(path.join(pkg, "concorde", "rules", "data.md"), "# Data\n");
28
+ assert.deepEqual(syncSkills(path.join(root, "app"), [pkg]), ["concorde"]);
29
+ const rule = path.join(root, "app", SKILLS_DIR, "concorde", "rules", "data.md");
30
+ assert.equal(await fs.readFile(rule, "utf8"), "# Data\n");
90
31
  });
91
- assert.equal(markdown.includes("keep me"), false);
92
- assert.equal(markdown.includes("<!-- concorde-ai -->"), false);
93
- assert.match(markdown, /^# Agents — Concorde/m);
94
32
  });
95
33
 
96
- test("without force/merge, buildAgentsFile still builds content (caller decides skip)", () => {
97
- const {markdown} = buildAgentsFile({
98
- existing: "",
99
- baseContent: base,
100
- overlayContent: "",
34
+ test("syncSkills replaces its own skill and keeps project skills", async () => {
35
+ await withProject(async (root) => {
36
+ const pkg = path.join(root, "pkg");
37
+ const app = path.join(root, "app");
38
+ const installed = path.join(app, SKILLS_DIR);
39
+ await makeSkill(pkg, "concorde");
40
+ await makeSkill(installed, "concorde", "old copy");
41
+ await makeSkill(installed, "concorde-docs", "project");
42
+ syncSkills(app, [pkg]);
43
+ assert.deepEqual((await fs.readdir(installed)).sort(), ["concorde", "concorde-docs"]);
44
+ assert.doesNotMatch(
45
+ await fs.readFile(path.join(installed, "concorde", "SKILL.md"), "utf8"),
46
+ /old copy/,
47
+ );
48
+ assert.match(
49
+ await fs.readFile(path.join(installed, "concorde-docs", "SKILL.md"), "utf8"),
50
+ /project/,
51
+ );
101
52
  });
102
- assert.match(markdown, /^# Agents — Concorde/m);
103
53
  });
104
54
 
105
- test("merge without marker keeps short unique preamble", () => {
106
- const {markdown, warnings} = buildAgentsFile({
107
- existing: "# My app\n\nOnly project notes here.\n",
108
- baseContent: base,
109
- overlayContent: "",
110
- mergeAgents: true,
55
+ test("linkSkills links each skill from .claude and .junie", async () => {
56
+ await withProject(async (root) => {
57
+ await makeSkill(path.join(root, SKILLS_DIR), "concorde");
58
+ const results = linkSkills(root, ["concorde"]);
59
+ assert.deepEqual(
60
+ results.map((r) => r.mode),
61
+ ["linked", "linked"],
62
+ );
63
+ for (const dir of [".claude/skills", ".junie/skills"]) {
64
+ const link = path.join(root, dir, "concorde");
65
+ assert.ok((await fs.lstat(link)).isSymbolicLink(), `${dir} not a symlink`);
66
+ assert.equal(await fs.readlink(link), path.join("..", "..", SKILLS_DIR, "concorde"));
67
+ assert.match(await fs.readFile(path.join(link, "SKILL.md"), "utf8"), /concorde/);
68
+ }
111
69
  });
112
- assert.match(markdown, /# My app/);
113
- assert.match(markdown, /<!-- concorde-ai -->/);
114
- assert.match(markdown, /# Agents — Concorde/);
115
- assert.equal(warnings.length, 0);
116
70
  });
117
71
 
118
- test("re-run with marker replaces managed zone only (idempotent)", () => {
119
- const first = buildAgentsFile({
120
- existing: "# My app\n\nNote.\n",
121
- baseContent: base,
122
- overlayContent: "v1 overlay",
123
- mergeAgents: true,
124
- }).markdown;
125
-
126
- const second = buildAgentsFile({
127
- existing: first.replace("v1 overlay", "STALE"),
128
- baseContent: base,
129
- overlayContent: "v2 overlay",
130
- mergeAgents: true,
131
- }).markdown;
132
-
133
- assert.equal(second.includes("STALE"), false);
134
- assert.match(second, /v2 overlay/);
135
- assert.equal(second.split("<!-- concorde-ai -->").length, 2);
136
- });
137
-
138
- test("duplicate Concorde preamble is dropped", () => {
139
- const fork = `${base}\n\nExtra copy of the same guide.\n`;
140
- assert.equal(preambleDuplicatesBase(fork, base), true);
141
-
142
- const {markdown, warnings} = buildAgentsFile({
143
- existing: `${fork}\n\n<!-- concorde-ai -->\n\nold managed\n`,
144
- baseContent: base,
145
- overlayContent: overlay,
146
- mergeAgents: true,
72
+ test("linkSkills keeps project skills and whole-folder links", async () => {
73
+ await withProject(async (root) => {
74
+ await makeSkill(path.join(root, SKILLS_DIR), "concorde");
75
+ await makeSkill(path.join(root, SKILLS_DIR), "my-skill");
76
+ // .claude/skills is already a symlink to .agents/skills (this repo's layout)
77
+ await fs.mkdir(path.join(root, ".claude"), {recursive: true});
78
+ await fs.symlink(path.join("..", SKILLS_DIR), path.join(root, ".claude/skills"));
79
+ // .junie has its own skill with the same name as an overlay skill
80
+ await makeSkill(path.join(root, ".junie/skills"), "my-skill", "junie own");
81
+
82
+ const results = linkSkills(root, ["concorde", "my-skill"], {packageNames: ["concorde"]});
83
+ assert.equal(results.find((r) => r.dir === ".claude/skills").mode, "shared");
84
+ assert.ok((await fs.lstat(path.join(root, ".claude/skills"))).isSymbolicLink());
85
+ const own = path.join(root, ".junie/skills/my-skill");
86
+ assert.ok(!(await fs.lstat(own)).isSymbolicLink());
87
+ assert.match(await fs.readFile(path.join(own, "SKILL.md"), "utf8"), /junie own/);
88
+ assert.ok((await fs.lstat(path.join(root, ".junie/skills/concorde"))).isSymbolicLink());
147
89
  });
148
-
149
- assert.equal(markdown.split("<!-- concorde-ai -->").length, 2);
150
- assert.equal(
151
- markdown.indexOf("# Agents — Concorde"),
152
- markdown.lastIndexOf("# Agents — Concorde"),
153
- );
154
- assert.match(markdown, /Conventions \(project\)/);
155
- assert.ok(warnings.some((w) => /duplicated|slimmed/i.test(w)));
156
- });
157
-
158
- test("slimPreamble keeps unique ## sections", () => {
159
- const preamble = `# Agents — Concorde (my-app)
160
-
161
- Guide.
162
-
163
- ## Skills / rules
164
-
165
- dup
166
-
167
- ## Project only
168
-
169
- - use endpoints.ts
170
- `;
171
- const slim = slimPreamble(preamble, base);
172
- assert.match(slim, /Project only/);
173
- assert.equal(slim.includes("## Skills / rules"), false);
174
- assert.match(slim, /Concorde \(my-app\)/);
175
- });
176
-
177
- test("slimPreamble drops Skills even when package title has suffix", () => {
178
- const packageBase = `# Agents — Concorde
179
-
180
- ## Skills / rules (après installation)
181
-
182
- table
183
-
184
- ## Conventions impératives
185
-
186
- - sonic-fetch
187
- - PublisherManager
188
- - formDataProvider
189
- - concorde-get-set-dp
190
- `;
191
- const preamble = `# Agents — Concorde (app)
192
-
193
- ## Skills / rules
194
-
195
- dup table
196
-
197
- ## Installation / réinstallation
198
-
199
- custom install cmd
200
-
201
- ## Project endpoints
202
-
203
- - endpoints.ts
204
- `;
205
- const slim = slimPreamble(preamble, packageBase);
206
- assert.equal(slim.includes("## Skills / rules"), false);
207
- assert.match(slim, /Project endpoints/);
208
- // Installation may stay if not in package — package has no Installation ##
209
- assert.match(slim, /Installation \/ réinstallation|Project endpoints/);
210
- });
211
-
212
- test("readOverlayAgents: prefers AGENTS.concorde.md", async () => {
213
- const dir = await fs.mkdtemp(path.join(os.tmpdir(), "concorde-overlay-"));
214
- try {
215
- await fs.writeFile(
216
- path.join(dir, "AGENTS.concorde.md"),
217
- "# overlay preferred\n",
218
- );
219
- await fs.writeFile(
220
- path.join(dir, "AGENTS.starter.md"),
221
- "# overlay legacy\n",
222
- );
223
- const {content, warnings} = readOverlayAgents(dir);
224
- assert.match(content, /overlay preferred/);
225
- assert.equal(warnings.length, 0);
226
- } finally {
227
- await fs.rm(dir, {recursive: true, force: true});
228
- }
229
90
  });
230
91
 
231
- test("readOverlayAgents: falls back to AGENTS.starter.md with warning", async () => {
232
- const dir = await fs.mkdtemp(path.join(os.tmpdir(), "concorde-overlay-"));
233
- try {
234
- await fs.writeFile(
235
- path.join(dir, "AGENTS.starter.md"),
236
- "# overlay legacy\n",
237
- );
238
- const {content, warnings} = readOverlayAgents(dir);
239
- assert.match(content, /overlay legacy/);
240
- assert.match(warnings[0] ?? "", /deprecated AGENTS\.starter\.md/);
241
- } finally {
242
- await fs.rm(dir, {recursive: true, force: true});
243
- }
244
- });
245
-
246
- test("readOverlayAgents: falls back to AGENTS.overlay.md with warning", async () => {
247
- const dir = await fs.mkdtemp(path.join(os.tmpdir(), "concorde-overlay-"));
248
- try {
249
- await fs.writeFile(
250
- path.join(dir, "AGENTS.overlay.md"),
251
- "# overlay mid legacy\n",
252
- );
253
- const {content, warnings} = readOverlayAgents(dir);
254
- assert.match(content, /overlay mid legacy/);
255
- assert.match(warnings[0] ?? "", /deprecated AGENTS\.overlay\.md/);
256
- } finally {
257
- await fs.rm(dir, {recursive: true, force: true});
258
- }
92
+ test("linkSkills replaces an earlier copy of a package skill", async () => {
93
+ await withProject(async (root) => {
94
+ await makeSkill(path.join(root, SKILLS_DIR), "concorde", "new");
95
+ await makeSkill(path.join(root, ".junie/skills"), "concorde", "copied before");
96
+ linkSkills(root, ["concorde"], {packageNames: ["concorde"]});
97
+ const link = path.join(root, ".junie/skills/concorde");
98
+ assert.ok((await fs.lstat(link)).isSymbolicLink());
99
+ assert.match(await fs.readFile(path.join(link, "SKILL.md"), "utf8"), /new/);
100
+ });
259
101
  });
@@ -2,14 +2,18 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import { fileURLToPath } from "url";
4
4
  import type { Connect, Plugin } from "vite";
5
- import { toSourceId } from "./docs-public-url.mjs";
5
+ import { SITE_PAGE_SLUGS, toSourceId } from "./docs-public-url.mjs";
6
6
 
7
7
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
8
  const MD_PATH_RE = /^\/((?:docs|core)\/.+\.mdx?)(?:\?|$)/i;
9
+ const SITE_PAGE_MD_RE = new RegExp(
10
+ `^/(${SITE_PAGE_SLUGS.join("|")})\\.md(?:\\?|$)`,
11
+ );
9
12
 
10
13
  /**
11
14
  * Dev: serve public `/docs|core/…/*.md` from `src/…` (including underscore
12
- * sections and `name/name.md` component pages).
15
+ * sections and `name/name.md` component pages), and `/about.md` & co. from
16
+ * `site-pages/`.
13
17
  */
14
18
  export function docsMarkdownVitePlugin(): Plugin {
15
19
  return {
@@ -17,13 +21,15 @@ export function docsMarkdownVitePlugin(): Plugin {
17
21
  configureServer(server) {
18
22
  server.middlewares.use(((req, res, next) => {
19
23
  const raw = req.url || "";
24
+ const sitePage = SITE_PAGE_MD_RE.exec(raw);
20
25
  const match = MD_PATH_RE.exec(raw);
21
- if (!match) {
26
+ if (!match && !sitePage) {
22
27
  next();
23
28
  return;
24
29
  }
25
- const sourceRel = `${toSourceId(match[1])}.md`;
26
- const filePath = path.join(root, "src", sourceRel);
30
+ const filePath = sitePage
31
+ ? path.join(root, "site-pages", `${sitePage[1]}.md`)
32
+ : path.join(root, "src", `${toSourceId(match![1])}.md`);
27
33
  if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
28
34
  next();
29
35
  return;
@@ -16,6 +16,12 @@ export const COMPONENT_MD_EXCEPTIONS = new Set([
16
16
  "core/components/functional/sdui/sdui-extension-points",
17
17
  ]);
18
18
 
19
+ /**
20
+ * Root-level site pages (`/about` ↔ `site-pages/about.md`, published as
21
+ * `/about.md`). Kept outside src/ so they stay out of doc nav and search.
22
+ */
23
+ export const SITE_PAGE_SLUGS = ["about", "privacy"];
24
+
19
25
  /** Strip leading slash and optional .md/.mdx */
20
26
  export function stripMd(rel) {
21
27
  return rel.replace(/^\//, "").replace(/\.mdx?$/i, "");
@@ -17,6 +17,13 @@ RewriteCond %{HTTP:Accept} "text/markdown" [NC]
17
17
  RewriteCond %{HTTP:Accept} !"text/markdown\s*;\s*q\s*=\s*0(\.0+)?(\s*,|$)" [NC]
18
18
  RewriteRule ^/?$ index.md [END]
19
19
 
20
+ # --- Site pages: /about, /privacy (SPA shell with inline content + .md) ---
21
+ RewriteRule ^(about|privacy)/$ /$1 [R=301,L]
22
+ RewriteCond %{HTTP:Accept} "text/markdown" [NC]
23
+ RewriteCond %{HTTP:Accept} !"text/markdown\s*;\s*q\s*=\s*0(\.0+)?(\s*,|$)" [NC]
24
+ RewriteRule ^(about|privacy)$ /$1.md [END]
25
+ RewriteRule ^(about|privacy)$ /$1.html [END]
26
+
20
27
  # --- Components: /…/button(.md|.html) → /…/button/button.(md|html) ---
21
28
  RewriteCond %{DOCUMENT_ROOT}/$1/$2/$2.md -f
22
29
  RewriteRule ^(core/components/.+)/([^/]+)\.md$ /$1/$2/$2.md [END]
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Publishes the Custom Elements Manifest (custom-elements.json) at the package
3
+ * root: the standard machine-readable API of every sonic-* element, for IDEs
4
+ * and coding agents.
5
+ *
6
+ * Starts from the raw analyzer output and:
7
+ * - fills `tagName` (components use `@customElement(tagName)` with a constant
8
+ * the analyzer does not resolve);
9
+ * - expands string-literal union aliases (`ButtonType` → `"default" | …`) so
10
+ * allowed values are visible;
11
+ * - keeps custom elements and their public API only.
12
+ */
13
+ import fs from "fs";
14
+ import path from "path";
15
+ import { fileURLToPath } from "url";
16
+ import { extractTagNameFromSource, runCem } from "./generate-sdui-catalog.mjs";
17
+
18
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
+ const ROOT = path.resolve(__dirname, "..");
20
+ export const CEM_OUT = path.join(ROOT, "custom-elements.json");
21
+ const CEM_CONFIG = path.join(ROOT, "custom-elements-manifest.public.config.mjs");
22
+ const CEM_RAW = path.join(ROOT, ".tmp-cem-public", "custom-elements.json");
23
+
24
+ const LIFECYCLE = new Set([
25
+ "render",
26
+ "connectedCallback",
27
+ "disconnectedCallback",
28
+ "attributeChangedCallback",
29
+ "createRenderRoot",
30
+ "firstUpdated",
31
+ "updated",
32
+ "willUpdate",
33
+ "shouldUpdate",
34
+ "update",
35
+ "getUpdateComplete",
36
+ ]);
37
+
38
+ /**
39
+ * `export type X = "a" | "b"` aliases made only of string literals.
40
+ * A name defined twice with different values is left out.
41
+ * @param {string[]} sources TypeScript source texts
42
+ * @returns {Map<string, string>} alias name → `"a" | "b"`
43
+ */
44
+ export function collectLiteralUnions(sources) {
45
+ const unions = new Map();
46
+ const conflicts = new Set();
47
+ const re = /export\s+type\s+([A-Z]\w*)\s*=\s*([^;]+);/g;
48
+ for (const src of sources) {
49
+ for (const [, name, body] of src.matchAll(re)) {
50
+ const parts = body
51
+ .split("|")
52
+ .map((p) => p.trim())
53
+ .filter(Boolean);
54
+ if (!parts.length || !parts.every((p) => /^(["'])[^"']*\1$/.test(p))) continue;
55
+ const text = parts.map((p) => `"${p.slice(1, -1)}"`).join(" | ");
56
+ if (unions.has(name) && unions.get(name) !== text) conflicts.add(name);
57
+ unions.set(name, text);
58
+ }
59
+ }
60
+ for (const name of conflicts) unions.delete(name);
61
+ return unions;
62
+ }
63
+
64
+ /** Replace alias identifiers in a type text by their literal union. */
65
+ export function expandType(text, unions) {
66
+ if (!text) return text;
67
+ return text
68
+ .replace(/\b[A-Z]\w*\b/g, (id) => unions.get(id) ?? id)
69
+ .replace(/\s+/g, " ")
70
+ .replace(/^\|\s*/, "")
71
+ .trim();
72
+ }
73
+
74
+ /** Names declared with Lit `@property(…)` (fields and accessors) in a source. */
75
+ export function collectLitProperties(source) {
76
+ const names = new Set();
77
+ const re =
78
+ /@property\((?:[^()]|\([^()]*\))*\)\s*(?:\/\*[\s\S]*?\*\/\s*|\/\/[^\n]*\n\s*)*(?:(?:declare|readonly|public|override|accessor)\s+)*(?:(?:get|set)\s+)?(\w+)/g;
79
+ for (const [, name] of source.matchAll(re)) names.add(name);
80
+ return names;
81
+ }
82
+
83
+ function withType(entry, unions) {
84
+ if (!entry?.type?.text) return entry;
85
+ return { ...entry, type: { ...entry.type, text: expandType(entry.type.text, unions) } };
86
+ }
87
+
88
+ /**
89
+ * Public API: Lit properties (with or without attribute) and documented
90
+ * public methods. Undocumented helpers that merely lack `private` are dropped.
91
+ */
92
+ function isPublicMember(member, isLitProperty) {
93
+ if (member.static) return false;
94
+ if (member.privacy === "private" || member.privacy === "protected") return false;
95
+ if (member.name.startsWith("_") || member.name.startsWith("#")) return false;
96
+ if (member.kind === "field") return Boolean(member.attribute) || isLitProperty(member);
97
+ if (member.kind === "method") return !LIFECYCLE.has(member.name) && Boolean(member.description);
98
+ return false;
99
+ }
100
+
101
+ /**
102
+ * @param {object} raw analyzer manifest
103
+ * @param {{
104
+ * resolveTagName: (modulePath: string) => string | undefined,
105
+ * unions: Map<string, string>,
106
+ * litProperties: (modulePath: string) => Set<string>,
107
+ * exclude?: (modulePath: string) => boolean,
108
+ * }} options
109
+ */
110
+ export function buildManifest(raw, { resolveTagName, unions, litProperties, exclude = () => false }) {
111
+ const byModule = new Map((raw.modules || []).map((m) => [m.path, m]));
112
+ /** Mixin declaration for `{ name, module | package }` (package paths map to src/). */
113
+ const resolveMixin = (ref) => {
114
+ const modulePath =
115
+ ref.module ??
116
+ (ref.package?.startsWith("@supersoniks/concorde/")
117
+ ? `src/${ref.package.slice("@supersoniks/concorde/".length)}.ts`
118
+ : undefined);
119
+ const mod = modulePath && byModule.get(modulePath);
120
+ const decl = mod?.declarations?.find((d) => d.kind === "mixin");
121
+ return decl ? { decl, modulePath, name: ref.name } : undefined;
122
+ };
123
+ /** Own entries first, then each mixin's, skipping names already present. */
124
+ const withMixins = (d, field) => {
125
+ const out = [...(d[field] || [])];
126
+ const seen = new Set(out.map((e) => e.name));
127
+ for (const ref of d.mixins || []) {
128
+ const mixin = resolveMixin(ref);
129
+ for (const entry of mixin?.decl[field] || []) {
130
+ if (seen.has(entry.name)) continue;
131
+ seen.add(entry.name);
132
+ out.push({ ...entry, inheritedFrom: { name: mixin.name, module: mixin.modulePath } });
133
+ }
134
+ }
135
+ return out;
136
+ };
137
+ const modules = [];
138
+ for (const mod of raw.modules || []) {
139
+ if (exclude(mod.path)) continue;
140
+ const defined = new Set(
141
+ (mod.exports || [])
142
+ .filter((e) => e.kind === "custom-element-definition")
143
+ .map((e) => e.declaration?.name),
144
+ );
145
+ const elements = (mod.declarations || []).filter(
146
+ (d) => d.kind === "class" && (d.customElement || defined.has(d.name)),
147
+ );
148
+ const declarations = [];
149
+ for (const element of elements) {
150
+ const tagName = element.tagName || resolveTagName(mod.path);
151
+ if (!tagName) continue;
152
+ const d = {
153
+ ...element,
154
+ attributes: withMixins(element, "attributes"),
155
+ members: withMixins(element, "members"),
156
+ };
157
+ declarations.push({
158
+ kind: "class",
159
+ name: d.name,
160
+ tagName,
161
+ customElement: true,
162
+ ...(d.summary && { summary: d.summary }),
163
+ ...(d.description && { description: d.description }),
164
+ ...(d.attributes?.length && {
165
+ attributes: d.attributes.map((a) => withType(a, unions)),
166
+ }),
167
+ ...(d.members?.length && {
168
+ members: d.members
169
+ .filter((m) =>
170
+ isPublicMember(m, (member) =>
171
+ litProperties(member.inheritedFrom?.module ?? mod.path).has(member.name),
172
+ ),
173
+ )
174
+ .map((m) => withType(m, unions)),
175
+ }),
176
+ ...(d.events?.length && { events: d.events }),
177
+ ...(d.slots?.length && { slots: d.slots }),
178
+ ...(d.cssProperties?.length && { cssProperties: d.cssProperties }),
179
+ ...(d.cssParts?.length && { cssParts: d.cssParts }),
180
+ });
181
+ }
182
+ if (!declarations.length) continue;
183
+ modules.push({
184
+ kind: "javascript-module",
185
+ path: mod.path,
186
+ declarations,
187
+ exports: declarations.flatMap((d) => [
188
+ { kind: "js", name: d.name, declaration: { name: d.name, module: mod.path } },
189
+ {
190
+ kind: "custom-element-definition",
191
+ name: d.tagName,
192
+ declaration: { name: d.name, module: mod.path },
193
+ },
194
+ ]),
195
+ });
196
+ }
197
+ modules.sort((a, b) => a.path.localeCompare(b.path));
198
+ return { schemaVersion: raw.schemaVersion || "1.0.0", readme: "", modules };
199
+ }
200
+
201
+ function listTsFiles(dir, files = []) {
202
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
203
+ const p = path.join(dir, entry.name);
204
+ if (entry.isDirectory()) listTsFiles(p, files);
205
+ else if (entry.name.endsWith(".ts") && !/\.(spec|test|demo)\.ts$/.test(entry.name)) {
206
+ files.push(p);
207
+ }
208
+ }
209
+ return files;
210
+ }
211
+
212
+ /** Writes custom-elements.json at the package root and returns its path. */
213
+ export function generateCem() {
214
+ runCem(CEM_CONFIG);
215
+ const raw = JSON.parse(fs.readFileSync(CEM_RAW, "utf8"));
216
+ const unions = collectLiteralUnions(
217
+ listTsFiles(path.join(ROOT, "src/core")).map((f) => fs.readFileSync(f, "utf8")),
218
+ );
219
+ const propertyCache = new Map();
220
+ const manifest = buildManifest(raw, {
221
+ unions,
222
+ resolveTagName: (modulePath) => extractTagNameFromSource(path.join(ROOT, modulePath)),
223
+ litProperties: (modulePath) => {
224
+ if (!propertyCache.has(modulePath)) {
225
+ const file = path.join(ROOT, modulePath);
226
+ propertyCache.set(
227
+ modulePath,
228
+ fs.existsSync(file) ? collectLitProperties(fs.readFileSync(file, "utf8")) : new Set(),
229
+ );
230
+ }
231
+ return propertyCache.get(modulePath);
232
+ },
233
+ // Copy-paste starter, not a library element
234
+ exclude: (modulePath) => modulePath.includes("/functional/example/"),
235
+ });
236
+ fs.writeFileSync(CEM_OUT, JSON.stringify(manifest, null, 2) + "\n");
237
+ const count = manifest.modules.reduce((n, m) => n + m.declarations.length, 0);
238
+ console.log(`✅ custom-elements.json: ${count} elements`);
239
+ return CEM_OUT;
240
+ }
241
+
242
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
243
+ generateCem();
244
+ }