@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.
- package/.agents/skills/concorde-docs/SKILL.md +1 -1
- package/README.md +1 -1
- package/ai/README.md +29 -41
- package/ai/skills/concorde/SKILL.md +54 -324
- package/ai/skills/concorde/rules/composition.md +32 -0
- package/ai/skills/concorde/rules/data.md +47 -0
- package/ai/skills/concorde/rules/http.md +25 -0
- package/ai/skills/concorde/rules/lists-forms.md +35 -0
- package/ai/skills/concorde/rules/styling.md +46 -0
- package/ai/skills/concorde/rules/troubleshooting.md +45 -0
- package/build-infos.json +1 -1
- package/concorde-core.bundle.js +21 -21
- package/concorde-core.es.js +977 -980
- package/custom-elements-manifest.public.config.mjs +8 -0
- package/{.tmp-cem/custom-elements.json → custom-elements.json} +11722 -8868
- package/dist/concorde-core.bundle.js +21 -21
- package/dist/concorde-core.es.js +977 -980
- package/dist/docs-mock-api-sw.js +58 -223
- package/dist/docs-mock-api-sw.js.map +4 -4
- package/dist/img/concorde/plane_01.jpg +0 -0
- package/dist/img/concorde/plane_02.jpg +0 -0
- package/dist/img/concorde/plane_03.jpg +0 -0
- package/dist/img/concorde/plane_04.jpg +0 -0
- package/dist/img/concorde/plane_05.jpg +0 -0
- package/dist/img/concorde/plane_06.jpg +0 -0
- package/dist/img/concorde/plane_07.jpg +0 -0
- package/dist/img/concorde/plane_08.jpg +0 -0
- package/dist/img/concorde/plane_09.jpg +0 -0
- package/dist/img/concorde/plane_10.jpg +0 -0
- package/dist/img/concorde/plane_11.jpg +0 -0
- package/dist/img/concorde/plane_12.jpg +0 -0
- package/dist/img/concorde/plane_13.jpg +0 -0
- package/dist/img/og-concorde.jpg +0 -0
- package/dist/robots.txt +11 -10
- package/docs/.htaccess +7 -0
- package/docs/about.html +111 -0
- package/docs/about.md +22 -0
- package/docs/assets/{index-BgG6FFPt.js → index-6sS76xWj.js} +482 -500
- package/docs/assets/{index-x89XINSG.css → index-DkdPbH0F.css} +1 -1
- package/docs/core/components/functional/date/date.md +1 -1
- package/docs/core/components/functional/list/list.html +7 -5
- package/docs/core/components/functional/list/list.md +6 -2
- package/docs/core/components/ui/card/card.html +1 -1
- package/docs/core/components/ui/card/card.md +1 -1
- package/docs/core/components/ui/image/image.html +17 -17
- package/docs/core/components/ui/image/image.md +17 -17
- package/docs/core/components/ui/modal/modal.html +2 -2
- package/docs/core/components/ui/modal/modal.md +2 -2
- package/docs/custom-elements.json +16559 -0
- package/docs/docs/decorators/bind.html +22 -21
- package/docs/docs/decorators/bind.md +23 -24
- package/docs/docs/getting-started/ai-agents.html +50 -94
- package/docs/docs/getting-started/ai-agents.md +29 -55
- package/docs/docs/getting-started/dataFlow.html +1 -1
- package/docs/docs/getting-started/dataFlow.md +1 -1
- package/docs/docs/index.html +1 -1
- package/docs/hubs/decorators.html +11 -16
- package/docs/hubs/remote-data.html +5 -9
- package/docs/img/og-concorde.jpg +0 -0
- package/docs/index.html +91 -2
- package/docs/index.md +2 -2
- package/docs/llms-full.txt +58 -74
- package/docs/llms.txt +6 -4
- package/docs/privacy.html +122 -0
- package/docs/privacy.md +32 -0
- package/docs/sitemap.xml +6 -0
- package/docs/src/core/components/functional/date/date.md +1 -1
- package/docs/src/core/components/functional/list/list.md +6 -2
- package/docs/src/core/components/functional/sdui/component-catalog.gaps.json +1 -1
- package/docs/src/core/components/functional/sdui/component-catalog.json +1 -1
- package/docs/src/core/components/ui/card/card.md +1 -1
- package/docs/src/core/components/ui/image/image.md +17 -17
- package/docs/src/core/components/ui/modal/modal.md +2 -2
- package/docs/src/docs/decorators/bind.md +23 -24
- package/docs/src/docs/getting-started/ai-agents.md +29 -55
- package/docs/src/docs/getting-started/dataFlow.md +1 -1
- package/docs/src/docs/search/docs-search.json +100 -55
- package/docs/src/tsconfig.json +11 -5
- package/index.html +89 -0
- package/package.json +8 -4
- package/public/img/og-concorde.jpg +0 -0
- package/scripts/ai-init.mjs +104 -379
- package/scripts/ai-init.test.mjs +81 -239
- package/scripts/docs-markdown-vite-plugin.ts +11 -5
- package/scripts/docs-public-url.mjs +6 -0
- package/scripts/docs.htaccess +7 -0
- package/scripts/generate-cem.mjs +244 -0
- package/scripts/generate-cem.test.mjs +122 -0
- package/scripts/generate-crawl-docs.mjs +29 -5
- package/scripts/generate-sdui-catalog.mjs +11 -10
- package/scripts/post-build-docs.js +3 -0
- package/scripts/pre-publish.mjs +2 -1
- package/scripts/site-pages.mjs +212 -0
- package/scripts/site-pages.test.mjs +116 -0
- package/site-pages/about.md +22 -0
- package/site-pages/privacy.md +32 -0
- package/src/core/components/functional/date/date.md +1 -1
- package/src/core/components/functional/list/list.md +6 -2
- package/src/core/components/functional/sdui/component-catalog.gaps.json +1 -1
- package/src/core/components/functional/sdui/component-catalog.json +1 -1
- package/src/core/components/ui/card/card.md +1 -1
- package/src/core/components/ui/image/image.md +17 -17
- package/src/core/components/ui/modal/modal.md +2 -2
- package/src/core/decorators/subscriber/bind.ts +9 -14
- package/src/docs/components/docs-demo-sources.ts +3 -3
- package/src/docs/decorators/bind.md +23 -24
- package/src/docs/docs-location.spec.ts +10 -0
- package/src/docs/docs-location.ts +10 -0
- package/src/docs/example/decorators-demo-bind-demos.ts +11 -28
- package/src/docs/example/docs-toggle-demos.ts +1 -1
- package/src/docs/getting-started/ai-agents.md +29 -55
- package/src/docs/getting-started/dataFlow.md +1 -1
- package/src/docs/home/home.ts +3 -1
- package/src/docs/navigation/navigation.ts +1 -1
- package/src/docs/search/docs-search.json +100 -55
- package/src/docs/search/markdown-renderer.ts +6 -1
- package/src/docs/tailwind/css/docs/content.css +4 -5
- package/src/tsconfig.json +11 -5
- package/src/tsconfig.tsbuildinfo +1 -1
- package/ai/AGENTS.md +0 -56
- package/ai/cursor/rules/concorde-menu.mdc +0 -15
- package/ai/cursor/rules/concorde-scope.mdc +0 -14
- package/ai/cursor/rules/concorde-theme.mdc +0 -13
- package/ai/cursor/rules/concorde.mdc +0 -66
- package/ai/jetbrains/rules/concorde.md +0 -53
- package/ai/skills/concorde-get-set-dp/SKILL.md +0 -194
- package/ai/skills/concorde-imports/SKILL.md +0 -78
- package/ai/skills/concorde-menu/SKILL.md +0 -74
- package/ai/skills/concorde-scope/SKILL.md +0 -70
- package/ai/skills/concorde-theme/SKILL.md +0 -57
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
buildManifest,
|
|
5
|
+
collectLitProperties,
|
|
6
|
+
collectLiteralUnions,
|
|
7
|
+
expandType,
|
|
8
|
+
} from "./generate-cem.mjs";
|
|
9
|
+
|
|
10
|
+
test("collectLiteralUnions keeps string-literal aliases only", () => {
|
|
11
|
+
const unions = collectLiteralUnions([
|
|
12
|
+
`export type Size = "sm" | "md";\nexport type Route = string | URL;`,
|
|
13
|
+
`export type Shape =\n | "default"\n | "circle";`,
|
|
14
|
+
]);
|
|
15
|
+
assert.equal(unions.get("Size"), `"sm" | "md"`);
|
|
16
|
+
assert.equal(unions.get("Shape"), `"default" | "circle"`);
|
|
17
|
+
assert.equal(unions.has("Route"), false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("collectLiteralUnions drops names defined twice with different values", () => {
|
|
21
|
+
const unions = collectLiteralUnions([`export type Mode = "a" | "b";`, `export type Mode = "c" | "d";`]);
|
|
22
|
+
assert.equal(unions.has("Mode"), false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("expandType replaces aliases and normalizes whitespace", () => {
|
|
26
|
+
const unions = new Map([["Size", `"sm" | "md"`]]);
|
|
27
|
+
assert.equal(expandType("Size | undefined", unions), `"sm" | "md" | undefined`);
|
|
28
|
+
assert.equal(expandType(`| "a"\n | "b"`, unions), `"a" | "b"`);
|
|
29
|
+
assert.equal(expandType("Route | string", unions), "Route | string");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("collectLitProperties finds fields and accessors, with comments in between", () => {
|
|
33
|
+
const names = collectLitProperties(`
|
|
34
|
+
@property({ type: String }) label = "";
|
|
35
|
+
@property({
|
|
36
|
+
type: Function,
|
|
37
|
+
attribute: false,
|
|
38
|
+
}) /** doc */
|
|
39
|
+
get items() { return this._items; }
|
|
40
|
+
@state() open = false;
|
|
41
|
+
`);
|
|
42
|
+
assert.deepEqual([...names].sort(), ["items", "label"]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const raw = {
|
|
46
|
+
schemaVersion: "1.0.0",
|
|
47
|
+
modules: [
|
|
48
|
+
{
|
|
49
|
+
path: "src/core/mixins/FormElement.ts",
|
|
50
|
+
declarations: [
|
|
51
|
+
{
|
|
52
|
+
kind: "mixin",
|
|
53
|
+
name: "Form",
|
|
54
|
+
attributes: [{ name: "name", type: { text: "string" }, fieldName: "name" }],
|
|
55
|
+
members: [{ kind: "field", name: "name", attribute: "name" }],
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
path: "src/core/components/ui/button/button.ts",
|
|
61
|
+
declarations: [
|
|
62
|
+
{
|
|
63
|
+
kind: "class",
|
|
64
|
+
name: "Button",
|
|
65
|
+
customElement: true,
|
|
66
|
+
mixins: [{ name: "FormElement", package: "@supersoniks/concorde/core/mixins/FormElement" }],
|
|
67
|
+
attributes: [{ name: "size", type: { text: "Size" }, fieldName: "size" }],
|
|
68
|
+
members: [
|
|
69
|
+
{ kind: "field", name: "size", attribute: "size", type: { text: "Size" } },
|
|
70
|
+
{ kind: "field", name: "items", type: { text: "ListItems" } },
|
|
71
|
+
{ kind: "field", name: "pointerDownTime", type: { text: "number" } },
|
|
72
|
+
{ kind: "field", name: "_internal" },
|
|
73
|
+
{ kind: "method", name: "render" },
|
|
74
|
+
{ kind: "method", name: "handleClick" },
|
|
75
|
+
{ kind: "method", name: "show", description: "Opens it." },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
exports: [{ kind: "custom-element-definition", declaration: { name: "Button" } }],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
path: "src/core/components/functional/example/example.ts",
|
|
83
|
+
declarations: [{ kind: "class", name: "Example", customElement: true }],
|
|
84
|
+
exports: [{ kind: "custom-element-definition", declaration: { name: "Example" } }],
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
function build() {
|
|
90
|
+
return buildManifest(raw, {
|
|
91
|
+
unions: new Map([["Size", `"sm" | "md"`]]),
|
|
92
|
+
resolveTagName: (p) => (p.includes("button") ? "sonic-button" : "sonic-example"),
|
|
93
|
+
litProperties: (p) => (p.includes("button") ? new Set(["size", "items"]) : new Set()),
|
|
94
|
+
exclude: (p) => p.includes("/functional/example/"),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
test("buildManifest keeps elements with their tag name and definition export", () => {
|
|
99
|
+
const manifest = build();
|
|
100
|
+
assert.equal(manifest.modules.length, 1);
|
|
101
|
+
const [mod] = manifest.modules;
|
|
102
|
+
const [el] = mod.declarations;
|
|
103
|
+
assert.equal(el.tagName, "sonic-button");
|
|
104
|
+
assert.deepEqual(
|
|
105
|
+
mod.exports.find((e) => e.kind === "custom-element-definition"),
|
|
106
|
+
{ kind: "custom-element-definition", name: "sonic-button", declaration: { name: "Button", module: mod.path } },
|
|
107
|
+
);
|
|
108
|
+
assert.equal(el.attributes.find((a) => a.name === "size").type.text, `"sm" | "md"`);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("buildManifest keeps the public API only and merges mixins", () => {
|
|
112
|
+
const [el] = build().modules[0].declarations;
|
|
113
|
+
assert.deepEqual(
|
|
114
|
+
el.members.map((m) => m.name),
|
|
115
|
+
["size", "items", "show", "name"],
|
|
116
|
+
);
|
|
117
|
+
const inherited = el.attributes.find((a) => a.name === "name");
|
|
118
|
+
assert.deepEqual(inherited.inheritedFrom, {
|
|
119
|
+
name: "FormElement",
|
|
120
|
+
module: "src/core/mixins/FormElement.ts",
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -14,6 +14,7 @@ import { fileURLToPath } from "url";
|
|
|
14
14
|
import { marked } from "marked";
|
|
15
15
|
import { gfmHeadingId } from "marked-gfm-heading-id";
|
|
16
16
|
import { toPublicPath } from "./docs-public-url.mjs";
|
|
17
|
+
import { STATIC_PAGES, renderSitePage } from "./site-pages.mjs";
|
|
17
18
|
|
|
18
19
|
const SITE_ORIGIN = "https://concorde.supersoniks.org";
|
|
19
20
|
const SRC_DIR = path.resolve("src");
|
|
@@ -297,7 +298,7 @@ function writeFileEnsuringDir(filePath, content) {
|
|
|
297
298
|
fs.writeFileSync(filePath, content);
|
|
298
299
|
}
|
|
299
300
|
|
|
300
|
-
function generateLlmsTxt(pages, hubs = []) {
|
|
301
|
+
export function generateLlmsTxt(pages, hubs = []) {
|
|
301
302
|
const lines = [
|
|
302
303
|
"# Concorde by Supersoniks",
|
|
303
304
|
"",
|
|
@@ -314,8 +315,10 @@ function generateLlmsTxt(pages, hubs = []) {
|
|
|
314
315
|
`- [Sitemap](${SITE_ORIGIN}/sitemap.xml)`,
|
|
315
316
|
`- [npm package @supersoniks/concorde](https://www.npmjs.com/package/@supersoniks/concorde)`,
|
|
316
317
|
`- [Getting started](${SITE_ORIGIN}/docs/getting-started/start)`,
|
|
317
|
-
`- [AI agents (
|
|
318
|
+
`- [AI agents (skill)](${SITE_ORIGIN}/docs/getting-started/ai-agents)`,
|
|
318
319
|
`- [Components index](${SITE_ORIGIN}/docs/getting-started/components)`,
|
|
320
|
+
`- [custom-elements.json](${SITE_ORIGIN}/custom-elements.json) — Custom Elements Manifest: attributes, properties, events and slots of every \`sonic-*\` element`,
|
|
321
|
+
`- [About](${SITE_ORIGIN}/about) · [Privacy](${SITE_ORIGIN}/privacy)`,
|
|
319
322
|
"",
|
|
320
323
|
"Doc pages use the same pathnames as the interactive site (`/docs/…`, `/core/…`).",
|
|
321
324
|
"Markdown: same URL with `.md` (e.g. `/docs/getting-started/start.md`), or `Accept: text/markdown` on the pathname.",
|
|
@@ -495,13 +498,16 @@ function generateTopicHubs(pages) {
|
|
|
495
498
|
return hubs;
|
|
496
499
|
}
|
|
497
500
|
|
|
498
|
-
function generateSitemap(pages, hubs = []) {
|
|
501
|
+
export function generateSitemap(pages, hubs = []) {
|
|
499
502
|
const homeUrl = ` <url>\n <loc>${SITE_ORIGIN}/</loc>\n </url>`;
|
|
500
503
|
const homeMdUrl = ` <url>\n <loc>${SITE_ORIGIN}/index.md</loc>\n </url>`;
|
|
501
504
|
const indexUrl = ` <url>\n <loc>${SITE_ORIGIN}/docs/index.html</loc>\n </url>`;
|
|
502
505
|
const hubUrls = hubs
|
|
503
506
|
.map((h) => ` <url>\n <loc>${SITE_ORIGIN}/${h.hubPath}</loc>\n </url>`)
|
|
504
507
|
.join("\n");
|
|
508
|
+
const staticUrls = STATIC_PAGES.map(
|
|
509
|
+
(p) => ` <url>\n <loc>${SITE_ORIGIN}/${p.slug}</loc>\n </url>`,
|
|
510
|
+
).join("\n");
|
|
505
511
|
const urls = pages
|
|
506
512
|
.map((p) => ` <url>\n <loc>${SITE_ORIGIN}${p.pagePath}</loc>\n </url>`)
|
|
507
513
|
.join("\n");
|
|
@@ -511,6 +517,7 @@ function generateSitemap(pages, hubs = []) {
|
|
|
511
517
|
${homeUrl}
|
|
512
518
|
${homeMdUrl}
|
|
513
519
|
${indexUrl}
|
|
520
|
+
${staticUrls}
|
|
514
521
|
${hubUrls}
|
|
515
522
|
${urls}
|
|
516
523
|
</urlset>
|
|
@@ -533,14 +540,14 @@ Install the npm package [\`@supersoniks/concorde\`](https://www.npmjs.com/packag
|
|
|
533
540
|
- Docs index: ${SITE_ORIGIN}/docs/index.html
|
|
534
541
|
- Sitemap: ${SITE_ORIGIN}/sitemap.xml
|
|
535
542
|
- Getting started: ${SITE_ORIGIN}/docs/getting-started/start
|
|
536
|
-
- AI agents (
|
|
543
|
+
- AI agents (skill): ${SITE_ORIGIN}/docs/getting-started/ai-agents
|
|
537
544
|
- Components: ${SITE_ORIGIN}/docs/getting-started/components
|
|
538
545
|
|
|
539
546
|
Doc pages share pathnames with the interactive site (\`/docs/…\`, \`/core/…\`). Markdown is at the same path with a \`.md\` suffix (e.g. \`/docs/getting-started/start.md\`), or via \`Accept: text/markdown\` on the pathname (\`Content-Type: text/markdown\`, \`Vary: Accept\`).
|
|
540
547
|
|
|
541
548
|
## What Concorde includes
|
|
542
549
|
|
|
543
|
-
Custom elements (\`sonic-*\`) for buttons, forms, lists, queues, SDUI trees and more. Decorators such as \`@get\`, \`@post\`, \`@subscribe\` and \`@bind\` wire components to a shared DataProvider without framework lock-in. Concorde is MIT-licensed and maintained by Supersoniks.
|
|
550
|
+
Custom elements (\`sonic-*\`) for buttons, forms, lists, queues, SDUI trees and more. Decorators such as \`@get\`, \`@post\`, \`@subscribe\` and \`@bind\` wire components to a shared DataProvider without framework lock-in. Concorde is MIT-licensed and maintained by Supersoniks ([about](${SITE_ORIGIN}/about), [privacy](${SITE_ORIGIN}/privacy)).
|
|
544
551
|
`;
|
|
545
552
|
}
|
|
546
553
|
|
|
@@ -581,11 +588,28 @@ export function generateHtaccess() {
|
|
|
581
588
|
return fs.readFileSync(htaccessPath, "utf8");
|
|
582
589
|
}
|
|
583
590
|
|
|
591
|
+
/** SPA shell for site pages: the built docs/index.html, else the source one. */
|
|
592
|
+
function readSpaShell(docsOut) {
|
|
593
|
+
const built = path.join(docsOut, "index.html");
|
|
594
|
+
return fs.readFileSync(
|
|
595
|
+
fs.existsSync(built) ? built : path.resolve("index.html"),
|
|
596
|
+
"utf8",
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
584
600
|
export function writeAgentSiteArtifacts(docsOut = DOCS_OUT) {
|
|
585
601
|
fs.writeFileSync(path.join(docsOut, "index.md"), generateHomepageMarkdown());
|
|
586
602
|
fs.writeFileSync(path.join(docsOut, "404.md"), generateNotFoundMarkdown());
|
|
587
603
|
fs.writeFileSync(path.join(docsOut, "404.html"), generateNotFoundHtml());
|
|
588
604
|
fs.writeFileSync(path.join(docsOut, ".htaccess"), generateHtaccess());
|
|
605
|
+
const shell = readSpaShell(docsOut);
|
|
606
|
+
for (const page of STATIC_PAGES) {
|
|
607
|
+
fs.writeFileSync(path.join(docsOut, `${page.slug}.md`), page.markdown);
|
|
608
|
+
fs.writeFileSync(
|
|
609
|
+
path.join(docsOut, `${page.slug}.html`),
|
|
610
|
+
renderSitePage(page, marked.parse(page.markdown), shell),
|
|
611
|
+
);
|
|
612
|
+
}
|
|
589
613
|
}
|
|
590
614
|
|
|
591
615
|
function removeLegacyCrawlDir() {
|
|
@@ -14,7 +14,7 @@ import { fileURLToPath } from "url";
|
|
|
14
14
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
15
|
const ROOT = path.resolve(__dirname, "..");
|
|
16
16
|
const CEM_DIR = path.join(ROOT, ".tmp-cem");
|
|
17
|
-
const CEM_PATH = path.join(CEM_DIR, "custom-elements.json");
|
|
17
|
+
export const CEM_PATH = path.join(CEM_DIR, "custom-elements.json");
|
|
18
18
|
const OUT_CATALOG = path.join(
|
|
19
19
|
ROOT,
|
|
20
20
|
"src/core/components/functional/sdui/component-catalog.json"
|
|
@@ -24,7 +24,7 @@ const OUT_GAPS = path.join(
|
|
|
24
24
|
"src/core/components/functional/sdui/component-catalog.gaps.json"
|
|
25
25
|
);
|
|
26
26
|
|
|
27
|
-
function extractTagNameFromSource(filePath) {
|
|
27
|
+
export function extractTagNameFromSource(filePath) {
|
|
28
28
|
if (!fs.existsSync(filePath)) return undefined;
|
|
29
29
|
const src = fs.readFileSync(filePath, "utf8");
|
|
30
30
|
const literal = src.match(/@customElement\(\s*["'`]([^"'`]+)["'`]\s*\)/);
|
|
@@ -176,8 +176,12 @@ export function cemToCatalog(cem, { resolveTagName } = {}) {
|
|
|
176
176
|
};
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
function runCem(
|
|
179
|
+
export function runCem(
|
|
180
|
+
configPath = path.join(ROOT, "custom-elements-manifest.config.mjs"),
|
|
181
|
+
) {
|
|
180
182
|
fs.mkdirSync(CEM_DIR, { recursive: true });
|
|
183
|
+
const pkgPath = path.join(ROOT, "package.json");
|
|
184
|
+
const pkgBefore = fs.readFileSync(pkgPath, "utf8");
|
|
181
185
|
const cemBin = path.join(
|
|
182
186
|
ROOT,
|
|
183
187
|
"node_modules/@custom-elements-manifest/analyzer/cem.js"
|
|
@@ -188,16 +192,13 @@ function runCem() {
|
|
|
188
192
|
cemBin,
|
|
189
193
|
"analyze",
|
|
190
194
|
"--config",
|
|
191
|
-
|
|
195
|
+
configPath,
|
|
192
196
|
],
|
|
193
197
|
{ cwd: ROOT, stdio: ["ignore", "pipe", "pipe"] }
|
|
194
198
|
);
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (pkg.customElements) {
|
|
199
|
-
delete pkg.customElements;
|
|
200
|
-
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
199
|
+
// cem may rewrite package.json (customElements): keep it as it was
|
|
200
|
+
if (fs.readFileSync(pkgPath, "utf8") !== pkgBefore) {
|
|
201
|
+
fs.writeFileSync(pkgPath, pkgBefore);
|
|
201
202
|
}
|
|
202
203
|
}
|
|
203
204
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { generateCrawlDocs } from "./generate-crawl-docs.mjs";
|
|
4
|
+
import { generateCem } from "./generate-cem.mjs";
|
|
4
5
|
|
|
5
6
|
const src = path.resolve("src");
|
|
6
7
|
const docs = path.resolve("docs");
|
|
@@ -52,6 +53,8 @@ function copyDocs() {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
copyDocs();
|
|
56
|
+
// Custom Elements Manifest, served at /custom-elements.json
|
|
57
|
+
fs.copyFileSync(generateCem(), path.join(docs, "custom-elements.json"));
|
|
55
58
|
generateCrawlDocs();
|
|
56
59
|
|
|
57
60
|
function copyToConcordeDoc() {
|
package/scripts/pre-publish.mjs
CHANGED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Homepage JSON-LD and the root site pages (/about, /privacy).
|
|
3
|
+
*
|
|
4
|
+
* Page content lives in site-pages/*.md: the SPA renders it at /about, and
|
|
5
|
+
* the build publishes `slug.md` + `slug.html` (SPA shell with the page
|
|
6
|
+
* inlined for crawlers and link previews).
|
|
7
|
+
*/
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
|
|
11
|
+
export const SITE_ORIGIN = "https://concorde.supersoniks.org";
|
|
12
|
+
export const NPM_URL = "https://www.npmjs.com/package/@supersoniks/concorde";
|
|
13
|
+
|
|
14
|
+
/** Publisher details, from https://supersoniks.com/mentions-legales */
|
|
15
|
+
export const ORGANIZATION = {
|
|
16
|
+
name: "Supersoniks",
|
|
17
|
+
legalName: "SUPERSONIKS",
|
|
18
|
+
url: "https://supersoniks.com/",
|
|
19
|
+
contactUrl: "https://supersoniks.com/un-projet",
|
|
20
|
+
telephone: "+33 2 47 68 93 25",
|
|
21
|
+
streetAddress: "22 rue du Docteur Bretonneau",
|
|
22
|
+
postalCode: "37000",
|
|
23
|
+
addressLocality: "Tours",
|
|
24
|
+
addressCountry: "FR",
|
|
25
|
+
sameAs: [
|
|
26
|
+
"https://www.linkedin.com/company/supersoniks/",
|
|
27
|
+
"https://www.facebook.com/supersoniks",
|
|
28
|
+
"https://www.instagram.com/agencesupersoniks/",
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function escapeHtml(text) {
|
|
33
|
+
return String(text)
|
|
34
|
+
.replace(/&/g, "&")
|
|
35
|
+
.replace(/</g, "<")
|
|
36
|
+
.replace(/>/g, ">")
|
|
37
|
+
.replace(/"/g, """);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* -------------------------------------------------------------------------- */
|
|
41
|
+
/* JSON-LD */
|
|
42
|
+
/* -------------------------------------------------------------------------- */
|
|
43
|
+
|
|
44
|
+
export function organizationJsonLd() {
|
|
45
|
+
return {
|
|
46
|
+
"@type": "Organization",
|
|
47
|
+
"@id": `${ORGANIZATION.url}#organization`,
|
|
48
|
+
name: ORGANIZATION.name,
|
|
49
|
+
legalName: ORGANIZATION.legalName,
|
|
50
|
+
url: ORGANIZATION.url,
|
|
51
|
+
telephone: ORGANIZATION.telephone,
|
|
52
|
+
address: {
|
|
53
|
+
"@type": "PostalAddress",
|
|
54
|
+
streetAddress: ORGANIZATION.streetAddress,
|
|
55
|
+
postalCode: ORGANIZATION.postalCode,
|
|
56
|
+
addressLocality: ORGANIZATION.addressLocality,
|
|
57
|
+
addressCountry: ORGANIZATION.addressCountry,
|
|
58
|
+
},
|
|
59
|
+
contactPoint: {
|
|
60
|
+
"@type": "ContactPoint",
|
|
61
|
+
contactType: "customer service",
|
|
62
|
+
telephone: ORGANIZATION.telephone,
|
|
63
|
+
url: ORGANIZATION.contactUrl,
|
|
64
|
+
availableLanguage: ["fr", "en"],
|
|
65
|
+
},
|
|
66
|
+
sameAs: ORGANIZATION.sameAs,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Homepage graph: WebSite + SoftwareApplication + Organization. */
|
|
71
|
+
export function homepageJsonLd() {
|
|
72
|
+
const org = { "@id": `${ORGANIZATION.url}#organization` };
|
|
73
|
+
return {
|
|
74
|
+
"@context": "https://schema.org",
|
|
75
|
+
"@graph": [
|
|
76
|
+
{
|
|
77
|
+
"@type": "WebSite",
|
|
78
|
+
"@id": `${SITE_ORIGIN}/#website`,
|
|
79
|
+
name: "Concorde",
|
|
80
|
+
alternateName: "Concorde by Supersoniks",
|
|
81
|
+
url: `${SITE_ORIGIN}/`,
|
|
82
|
+
inLanguage: "en",
|
|
83
|
+
publisher: org,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"@type": "SoftwareApplication",
|
|
87
|
+
"@id": `${SITE_ORIGIN}/#software`,
|
|
88
|
+
name: "Concorde",
|
|
89
|
+
alternateName: "@supersoniks/concorde",
|
|
90
|
+
description:
|
|
91
|
+
"Concorde is an open-source web components framework built on Lit: UI components, data binding, forms, routing helpers and theming that work in React, Vue, PHP or plain HTML.",
|
|
92
|
+
url: `${SITE_ORIGIN}/`,
|
|
93
|
+
applicationCategory: "DeveloperApplication",
|
|
94
|
+
applicationSubCategory: "Web components framework",
|
|
95
|
+
operatingSystem: "Any (web browser)",
|
|
96
|
+
programmingLanguage: "TypeScript",
|
|
97
|
+
license: "https://opensource.org/licenses/MIT",
|
|
98
|
+
downloadUrl: NPM_URL,
|
|
99
|
+
softwareHelp: `${SITE_ORIGIN}/docs/getting-started/start`,
|
|
100
|
+
image: `${SITE_ORIGIN}/img/og-concorde.jpg`,
|
|
101
|
+
offers: { "@type": "Offer", price: "0", priceCurrency: "EUR" },
|
|
102
|
+
author: org,
|
|
103
|
+
publisher: org,
|
|
104
|
+
sameAs: [NPM_URL],
|
|
105
|
+
},
|
|
106
|
+
organizationJsonLd(),
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* -------------------------------------------------------------------------- */
|
|
112
|
+
/* Site pages: /about, /privacy */
|
|
113
|
+
/* -------------------------------------------------------------------------- */
|
|
114
|
+
|
|
115
|
+
const SITE_PAGES_DIR = fileURLToPath(new URL("../site-pages/", import.meta.url));
|
|
116
|
+
|
|
117
|
+
export const STATIC_PAGES = [
|
|
118
|
+
{
|
|
119
|
+
slug: "about",
|
|
120
|
+
title: "About Concorde — web components framework by Supersoniks",
|
|
121
|
+
description:
|
|
122
|
+
"Concorde is an open-source Lit web components framework designed and maintained by Supersoniks, Tours, France.",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
slug: "privacy",
|
|
126
|
+
title: "Privacy notice — Concorde by Supersoniks",
|
|
127
|
+
description:
|
|
128
|
+
"Privacy notice of the Concorde documentation site: no cookies, no analytics, third-party hosts and your GDPR rights.",
|
|
129
|
+
},
|
|
130
|
+
].map((page) => ({
|
|
131
|
+
...page,
|
|
132
|
+
markdown: fs.readFileSync(`${SITE_PAGES_DIR}${page.slug}.md`, "utf8"),
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
function pageJsonLd(page) {
|
|
136
|
+
return {
|
|
137
|
+
"@context": "https://schema.org",
|
|
138
|
+
"@type": page.slug === "about" ? "AboutPage" : "WebPage",
|
|
139
|
+
name: page.title,
|
|
140
|
+
description: page.description,
|
|
141
|
+
url: `${SITE_ORIGIN}/${page.slug}`,
|
|
142
|
+
isPartOf: { "@type": "WebSite", name: "Concorde", url: `${SITE_ORIGIN}/` },
|
|
143
|
+
publisher: organizationJsonLd(),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function replaceOnce(html, re, replacement, what) {
|
|
148
|
+
if (!re.test(html)) throw new Error(`site page shell: ${what} not found`);
|
|
149
|
+
return html.replace(re, () => replacement);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* `slug.html`: the SPA shell (built docs/index.html) with the page's
|
|
154
|
+
* metadata and its rendered content in the no-JS section. Browsers boot the
|
|
155
|
+
* SPA, which renders the same Markdown; crawlers read the inline copy.
|
|
156
|
+
*/
|
|
157
|
+
export function renderSitePage(page, bodyHtml, shellHtml) {
|
|
158
|
+
const url = `${SITE_ORIGIN}/${page.slug}`;
|
|
159
|
+
const title = escapeHtml(page.title);
|
|
160
|
+
const description = escapeHtml(page.description);
|
|
161
|
+
let html = shellHtml;
|
|
162
|
+
html = replaceOnce(html, /<title>[\s\S]*?<\/title>/, `<title>${title}</title>`, "title");
|
|
163
|
+
html = replaceOnce(
|
|
164
|
+
html,
|
|
165
|
+
/<meta\s+name="description"\s+content="[^"]*"\s*\/?>/,
|
|
166
|
+
`<meta name="description" content="${description}" />`,
|
|
167
|
+
"description",
|
|
168
|
+
);
|
|
169
|
+
html = replaceOnce(
|
|
170
|
+
html,
|
|
171
|
+
/<link rel="canonical" href="[^"]*"\s*\/?>/,
|
|
172
|
+
`<link rel="canonical" href="${url}" />`,
|
|
173
|
+
"canonical",
|
|
174
|
+
);
|
|
175
|
+
html = replaceOnce(
|
|
176
|
+
html,
|
|
177
|
+
/<link rel="alternate" type="text\/markdown" href="[^"]*"/,
|
|
178
|
+
`<link rel="alternate" type="text/markdown" href="/${page.slug}.md"`,
|
|
179
|
+
"markdown alternate",
|
|
180
|
+
);
|
|
181
|
+
html = replaceOnce(
|
|
182
|
+
html,
|
|
183
|
+
/<meta property="og:title" content="[^"]*"/,
|
|
184
|
+
`<meta property="og:title" content="${title}"`,
|
|
185
|
+
"og:title",
|
|
186
|
+
);
|
|
187
|
+
html = replaceOnce(
|
|
188
|
+
html,
|
|
189
|
+
/<meta property="og:description" content="[^"]*"/,
|
|
190
|
+
`<meta property="og:description" content="${description}"`,
|
|
191
|
+
"og:description",
|
|
192
|
+
);
|
|
193
|
+
html = replaceOnce(
|
|
194
|
+
html,
|
|
195
|
+
/<meta property="og:url" content="[^"]*"/,
|
|
196
|
+
`<meta property="og:url" content="${url}"`,
|
|
197
|
+
"og:url",
|
|
198
|
+
);
|
|
199
|
+
html = replaceOnce(
|
|
200
|
+
html,
|
|
201
|
+
/<script type="application\/ld\+json">[\s\S]*?<\/script>/,
|
|
202
|
+
`<script type="application/ld+json">${JSON.stringify(pageJsonLd(page))}</script>`,
|
|
203
|
+
"JSON-LD",
|
|
204
|
+
);
|
|
205
|
+
html = replaceOnce(
|
|
206
|
+
html,
|
|
207
|
+
/<section id="docs-agent-content"([^>]*)>[\s\S]*?<\/section>/,
|
|
208
|
+
`<section id="docs-agent-content" aria-hidden="true">\n${bodyHtml}\n </section>`,
|
|
209
|
+
"page section",
|
|
210
|
+
);
|
|
211
|
+
return html;
|
|
212
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { STATIC_PAGES, homepageJsonLd } from "./site-pages.mjs";
|
|
7
|
+
import {
|
|
8
|
+
generateHomepageMarkdown,
|
|
9
|
+
generateHtaccess,
|
|
10
|
+
generateSitemap,
|
|
11
|
+
writeAgentSiteArtifacts,
|
|
12
|
+
} from "./generate-crawl-docs.mjs";
|
|
13
|
+
|
|
14
|
+
function stripTags(html) {
|
|
15
|
+
return html
|
|
16
|
+
.replace(/<[^>]+>/g, " ")
|
|
17
|
+
.replace(/\s+/g, " ")
|
|
18
|
+
.trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test("homepage JSON-LD has SoftwareApplication and complete Organization", () => {
|
|
22
|
+
const ld = homepageJsonLd();
|
|
23
|
+
assert.equal(ld["@context"], "https://schema.org");
|
|
24
|
+
const byType = Object.fromEntries(ld["@graph"].map((n) => [n["@type"], n]));
|
|
25
|
+
const app = byType.SoftwareApplication;
|
|
26
|
+
assert.ok(app.name && app.description && app.url && app.offers);
|
|
27
|
+
assert.equal(app.publisher["@id"], byType.Organization["@id"]);
|
|
28
|
+
const org = byType.Organization;
|
|
29
|
+
assert.equal(org.url, "https://supersoniks.com/");
|
|
30
|
+
assert.equal(org.address["@type"], "PostalAddress");
|
|
31
|
+
assert.equal(org.address.addressLocality, "Tours");
|
|
32
|
+
assert.equal(org.contactPoint["@type"], "ContactPoint");
|
|
33
|
+
assert.ok(org.contactPoint.contactType && org.contactPoint.telephone);
|
|
34
|
+
assert.ok(org.sameAs.every((u) => u.startsWith("https://")));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("source index.html embeds the generated JSON-LD and Open Graph tags", () => {
|
|
38
|
+
const html = fs.readFileSync(path.resolve("index.html"), "utf8");
|
|
39
|
+
const block = html.match(
|
|
40
|
+
/<script type="application\/ld\+json">([\s\S]*?)<\/script>/,
|
|
41
|
+
);
|
|
42
|
+
assert.ok(block, "JSON-LD script missing");
|
|
43
|
+
assert.deepEqual(JSON.parse(block[1]), homepageJsonLd());
|
|
44
|
+
for (const prop of ["og:type", "og:title", "og:description", "og:url", "og:image"]) {
|
|
45
|
+
assert.match(html, new RegExp(`<meta property="${prop}" content="[^"]+"`), prop);
|
|
46
|
+
}
|
|
47
|
+
assert.match(
|
|
48
|
+
html,
|
|
49
|
+
/<meta property="og:image" content="https:\/\/concorde\.supersoniks\.org\/img\/og-concorde\.jpg"/,
|
|
50
|
+
);
|
|
51
|
+
assert.ok(fs.existsSync(path.resolve("public/img/og-concorde.jpg")));
|
|
52
|
+
assert.match(html, /<meta name="twitter:card" content="summary_large_image"/);
|
|
53
|
+
assert.match(html, /href="\/about"/);
|
|
54
|
+
assert.match(html, /href="\/privacy"/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("site page Markdown is read from site-pages/", () => {
|
|
58
|
+
assert.deepEqual(
|
|
59
|
+
STATIC_PAGES.map((p) => p.slug),
|
|
60
|
+
["about", "privacy"],
|
|
61
|
+
);
|
|
62
|
+
for (const { slug, markdown } of STATIC_PAGES) {
|
|
63
|
+
assert.equal(
|
|
64
|
+
markdown,
|
|
65
|
+
fs.readFileSync(path.resolve(`site-pages/${slug}.md`), "utf8"),
|
|
66
|
+
);
|
|
67
|
+
assert.match(markdown, /^# /);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("site pages are SPA shells with their own metadata and inline content", () => {
|
|
72
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "concorde-pages-"));
|
|
73
|
+
try {
|
|
74
|
+
writeAgentSiteArtifacts(dir);
|
|
75
|
+
for (const { slug } of STATIC_PAGES) {
|
|
76
|
+
const md = fs.readFileSync(path.join(dir, `${slug}.md`), "utf8");
|
|
77
|
+
const html = fs.readFileSync(path.join(dir, `${slug}.html`), "utf8");
|
|
78
|
+
assert.ok(md.length >= 500, `${slug}.md too short (${md.length})`);
|
|
79
|
+
const section = html.match(
|
|
80
|
+
/<section id="docs-agent-content"[^>]*>([\s\S]*?)<\/section>/,
|
|
81
|
+
);
|
|
82
|
+
assert.ok(section, `${slug}.html page section missing`);
|
|
83
|
+
assert.ok(stripTags(section[1]).length >= 500, `${slug}.html too short`);
|
|
84
|
+
assert.match(section[1], /<h1/);
|
|
85
|
+
assert.doesNotMatch(section[1], /<h1>Concorde by Supersoniks<\/h1>/);
|
|
86
|
+
assert.match(html, /<sonic-router>/, "SPA shell expected");
|
|
87
|
+
const url = `https://concorde\\.supersoniks\\.org/${slug}`;
|
|
88
|
+
assert.match(html, new RegExp(`<link rel="canonical" href="${url}" />`));
|
|
89
|
+
assert.match(html, new RegExp(`<meta property="og:url" content="${url}"`));
|
|
90
|
+
assert.match(html, new RegExp(`href="/${slug}\\.md"`));
|
|
91
|
+
assert.equal((html.match(/<title>/g) || []).length, 1);
|
|
92
|
+
const ld = JSON.parse(
|
|
93
|
+
html.match(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/)[1],
|
|
94
|
+
);
|
|
95
|
+
assert.equal(ld.url, `https://concorde.supersoniks.org/${slug}`);
|
|
96
|
+
assert.equal(ld.publisher.address.addressLocality, "Tours");
|
|
97
|
+
}
|
|
98
|
+
} finally {
|
|
99
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("sitemap and homepage Markdown link the site pages", () => {
|
|
104
|
+
const xml = generateSitemap([], []);
|
|
105
|
+
const md = generateHomepageMarkdown();
|
|
106
|
+
for (const slug of ["about", "privacy"]) {
|
|
107
|
+
assert.match(xml, new RegExp(`<loc>https://concorde\\.supersoniks\\.org/${slug}</loc>`));
|
|
108
|
+
assert.match(md, new RegExp(`https://concorde\\.supersoniks\\.org/${slug}\\)`));
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("htaccess serves site pages as HTML or Markdown", () => {
|
|
113
|
+
const conf = generateHtaccess();
|
|
114
|
+
assert.match(conf, /\^\(about\|privacy\)\$ \/\$1\.md \[END\]/);
|
|
115
|
+
assert.match(conf, /\^\(about\|privacy\)\$ \/\$1\.html \[END\]/);
|
|
116
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# About Concorde
|
|
2
|
+
|
|
3
|
+
Concorde is an open-source web components framework built on [Lit](https://lit.dev). It is designed, written and maintained by [Supersoniks](https://supersoniks.com/), a company based in Tours, France, which uses it in production.
|
|
4
|
+
|
|
5
|
+
## Why it exists
|
|
6
|
+
|
|
7
|
+
Supersoniks works on many stacks: PHP back offices, static pages, React and Vue front ends. Rewriting the same buttons, forms and data lists for each stack was slow and led to inconsistent interfaces. Concorde solves this with standard custom elements (`sonic-*`) that work the same way in any host page, plus a small data layer (DataProvider, decorators) that connects components to each other and to REST APIs without a framework.
|
|
8
|
+
|
|
9
|
+
## What it includes
|
|
10
|
+
|
|
11
|
+
- UI components: buttons, inputs, selects, checkboxes, modals, toasts, cards, menus, tooltips and more.
|
|
12
|
+
- Data components: lists, queues, fetch, submit, router, states and server-driven UI from JSON.
|
|
13
|
+
- Lit decorators such as `@get`, `@post`, `@subscribe` and `@bind`.
|
|
14
|
+
- Theming through CSS custom properties, with light and dark modes.
|
|
15
|
+
|
|
16
|
+
## Licence and distribution
|
|
17
|
+
|
|
18
|
+
Concorde is published on npm as [`@supersoniks/concorde`](https://www.npmjs.com/package/@supersoniks/concorde) under the MIT licence. This site (https://concorde.supersoniks.org) is its official documentation.
|
|
19
|
+
|
|
20
|
+
## Publisher
|
|
21
|
+
|
|
22
|
+
Supersoniks, 22 rue du Docteur Bretonneau, 37000 Tours, France · +33 2 47 68 93 25 · [contact Supersoniks](https://supersoniks.com/un-projet). See also the [privacy notice](/privacy).
|