@homepages/template-kit 0.2.0 → 0.4.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.
Files changed (165) hide show
  1. package/CHANGELOG.md +222 -0
  2. package/README.md +96 -17
  3. package/dist/asset-modules.d.ts +81 -0
  4. package/dist/base.css +9 -0
  5. package/dist/cli/check/config.js +41 -0
  6. package/dist/cli/check/css.js +131 -0
  7. package/dist/cli/check/diagnostics.js +32 -0
  8. package/dist/cli/check/index.js +87 -0
  9. package/dist/cli/check/loader.js +146 -0
  10. package/dist/cli/check/paths.js +15 -0
  11. package/dist/cli/check/relativize.js +18 -0
  12. package/dist/cli/check/render-invariants.js +24 -0
  13. package/dist/cli/check/resolve-tool.js +38 -0
  14. package/dist/cli/check/section-assets.js +58 -0
  15. package/dist/cli/check/stages/deps.js +337 -0
  16. package/dist/cli/check/stages/lint.js +46 -0
  17. package/dist/cli/check/stages/render.js +101 -0
  18. package/dist/cli/check/stages/size.js +274 -0
  19. package/dist/cli/check/stages/tree.js +206 -0
  20. package/dist/cli/check/stages/typecheck.js +46 -0
  21. package/dist/cli/check/stages/validate/catalog-exhaustiveness.js +18 -0
  22. package/dist/cli/check/stages/validate/facts.js +18 -0
  23. package/dist/cli/check/stages/validate/formatter-contract.js +14 -0
  24. package/dist/cli/check/stages/validate/group-contract.js +21 -0
  25. package/dist/cli/check/stages/validate/index.js +66 -0
  26. package/dist/cli/check/stages/validate/list-scalar-contract.js +22 -0
  27. package/dist/cli/check/stages/validate/nav-contract.js +93 -0
  28. package/dist/cli/check/stages/validate/nested-slot-contract.js +12 -0
  29. package/dist/cli/check/stages/validate/object-list-contract.js +31 -0
  30. package/dist/cli/check/stages/validate/orchestrator.js +241 -0
  31. package/dist/cli/check/stages/validate/produced-by-contract.js +18 -0
  32. package/dist/cli/check/stages/validate/row-contract.js +31 -0
  33. package/dist/cli/check/stages/validate/select-contract.js +13 -0
  34. package/dist/cli/check/stages/validate/source-contract.js +21 -0
  35. package/dist/cli/check/stages/validate/variant-contract.js +15 -0
  36. package/dist/cli/check/workspace.js +86 -0
  37. package/dist/cli/dev/build-css.js +91 -0
  38. package/dist/cli/dev/catalog.js +35 -0
  39. package/dist/cli/dev/compose-template.js +38 -0
  40. package/dist/cli/dev/content-override.js +58 -0
  41. package/dist/cli/dev/discover.js +35 -0
  42. package/dist/cli/dev/fill-state.js +75 -0
  43. package/dist/cli/dev/index.js +32 -0
  44. package/dist/cli/dev/inspect.js +38 -0
  45. package/dist/cli/dev/island-bootstrap.js +38 -0
  46. package/dist/cli/dev/island-map.js +35 -0
  47. package/dist/cli/dev/island-transform.js +31 -0
  48. package/dist/cli/dev/manifest-instances.js +31 -0
  49. package/dist/cli/dev/render-section.js +24 -0
  50. package/dist/cli/dev/screenshot-target.js +24 -0
  51. package/dist/cli/dev/section-page.js +54 -0
  52. package/dist/cli/dev/server.js +480 -0
  53. package/dist/cli/dev/slot-schema.js +12 -0
  54. package/dist/cli/dev/structure-summary.js +118 -0
  55. package/dist/cli/dev/tailwind.js +32 -0
  56. package/dist/cli/dev/vite-server.js +33 -0
  57. package/dist/cli/dev/workspace.js +63 -0
  58. package/dist/cli/link/index.js +74 -0
  59. package/dist/cli/link/overlay.js +59 -0
  60. package/dist/cli/link/watch.js +25 -0
  61. package/dist/cli/new/emit.js +48 -0
  62. package/dist/cli/new/index.js +69 -0
  63. package/dist/cli/new/scaffold/section/_Renderer.tsx +20 -0
  64. package/dist/cli/new/scaffold/section/_fill-spec.ts +18 -0
  65. package/dist/cli/new/scaffold/section/_fixtures.ts +13 -0
  66. package/dist/cli/new/scaffold/section/_schema.ts +24 -0
  67. package/dist/cli/new/scaffold/template/_manifest.json +9 -0
  68. package/dist/cli/new/scaffold/template/sections/hero/ExpandableText.tsx +45 -0
  69. package/dist/cli/new/scaffold/template/sections/hero/Renderer.tsx +40 -0
  70. package/dist/cli/new/scaffold/template/sections/hero/components/Feature.tsx +14 -0
  71. package/dist/cli/new/scaffold/template/sections/hero/fill-spec.ts +29 -0
  72. package/dist/cli/new/scaffold/template/sections/hero/fixtures.ts +41 -0
  73. package/dist/cli/new/scaffold/template/sections/hero/schema.ts +60 -0
  74. package/dist/cli/new/scaffold/template/theme.css +24 -0
  75. package/dist/cli/new/scaffold/template/theme.ts +27 -0
  76. package/dist/cli/new/scaffold-assets.js +20 -0
  77. package/dist/cli/pack/collect.js +44 -0
  78. package/dist/cli/pack/guards.js +57 -0
  79. package/dist/cli/pack/index.js +66 -0
  80. package/dist/cli/pack/manifest.js +15 -0
  81. package/dist/cli/pack/zip.js +86 -0
  82. package/dist/cli/theme/generate.js +43 -0
  83. package/dist/cli/theme/index.js +33 -0
  84. package/dist/cli/theme/load-theme.js +69 -0
  85. package/dist/cli.js +100 -16
  86. package/dist/contracts/fill-treatments.js +47 -0
  87. package/dist/design-system/theme.d.ts +30 -300
  88. package/dist/design-system/theme.js +112 -90
  89. package/dist/dev-client/assets/index-DfEfrp6P.css +1 -0
  90. package/dist/dev-client/assets/index-bpMP0b0W.js +50 -0
  91. package/dist/dev-client/index.html +13 -0
  92. package/dist/eslint/is-client-file.d.ts +4 -0
  93. package/dist/eslint/rules/no-hex.js +78 -6
  94. package/dist/eslint.js +1 -1
  95. package/dist/index.d.ts +3 -2
  96. package/dist/index.js +2 -2
  97. package/dist/islands/detect.d.ts +16 -0
  98. package/dist/islands/detect.js +22 -0
  99. package/dist/islands/discover.d.ts +5 -0
  100. package/dist/islands/discover.js +19 -0
  101. package/dist/islands/esbuild-plugin.d.ts +13 -0
  102. package/dist/islands/esbuild-plugin.js +39 -0
  103. package/dist/islands/wrap.d.ts +11 -0
  104. package/dist/islands/wrap.js +48 -0
  105. package/dist/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.js +77 -0
  106. package/dist/node_modules/magic-string/dist/magic-string.es.js +939 -0
  107. package/dist/package.js +1 -1
  108. package/dist/primitives/Image.js +1 -1
  109. package/dist/schema/fill-spec.d.ts +80 -590
  110. package/dist/schema/fixture-schema.d.ts +5 -81
  111. package/dist/schema/manifest.d.ts +39 -475
  112. package/dist/schema/resolve-section-ref.js +10 -0
  113. package/dist/schema/rows.js +10 -0
  114. package/dist/schema/section-nav.js +9 -0
  115. package/dist/schema/section-schema.d.ts +51 -437
  116. package/dist/schema/slot-types.d.ts +12 -16
  117. package/dist/ssr.d.ts +31 -0
  118. package/dist/ssr.js +46 -0
  119. package/dist/styles.css +31 -88
  120. package/docs/INDEX.md +12 -3
  121. package/docs/assets.md +128 -0
  122. package/docs/check.md +124 -0
  123. package/docs/dev.md +187 -0
  124. package/docs/eslint.md +18 -8
  125. package/docs/islands.md +5 -2
  126. package/docs/llms.txt +59 -9
  127. package/docs/new.md +27 -0
  128. package/docs/overview.md +57 -0
  129. package/docs/pack.md +46 -0
  130. package/docs/primitives.md +10 -6
  131. package/docs/quickstart.md +102 -0
  132. package/docs/recipes/INDEX.md +27 -0
  133. package/docs/recipes/bind-property-fact.md +58 -0
  134. package/docs/recipes/collection-slot.md +100 -0
  135. package/docs/recipes/fill-spec-decision.md +66 -0
  136. package/docs/recipes/fixture-states.md +68 -0
  137. package/docs/recipes/image-slot-crop.md +98 -0
  138. package/docs/recipes/interactive-island.md +100 -0
  139. package/docs/recipes/organize-section-folder.md +72 -0
  140. package/docs/recipes/prepare-submission.md +55 -0
  141. package/docs/recipes/second-template.md +49 -0
  142. package/docs/recipes/select-slot.md +59 -0
  143. package/docs/recipes/static-asset.md +103 -0
  144. package/docs/recipes/third-party-package.md +90 -0
  145. package/docs/rules/INDEX.md +14 -5
  146. package/docs/rules/bundle-binary-asset.md +83 -0
  147. package/docs/rules/bundle-incomplete.md +2 -2
  148. package/docs/rules/css-reason.md +3 -3
  149. package/docs/rules/fixtures-invalid.md +96 -0
  150. package/docs/rules/license-denied.md +11 -3
  151. package/docs/rules/manifest-invalid.md +99 -0
  152. package/docs/rules/no-bare-css-import.md +8 -8
  153. package/docs/rules/no-css-import-from-render-path.md +7 -8
  154. package/docs/rules/no-hex.md +76 -10
  155. package/docs/rules/no-templates.md +87 -0
  156. package/docs/rules/parse-error.md +76 -0
  157. package/docs/rules/schema-invalid.md +96 -0
  158. package/docs/rules/size-assets.md +88 -0
  159. package/docs/rules/size-island-bundle.md +123 -0
  160. package/docs/rules/size-section-css.md +19 -14
  161. package/docs/schema-system.md +28 -28
  162. package/docs/theme-and-css.md +157 -92
  163. package/docs/vocabulary.md +98 -0
  164. package/package.json +14 -11
  165. package/tsconfig.json +1 -1
@@ -0,0 +1,24 @@
1
+ /* Generated by `template-kit theme` from theme.ts — do not edit.
2
+ Re-run `template-kit theme` after changing theme.ts. */
3
+ @theme inline {
4
+ --color-accent: var(--tr-color-accent);
5
+ --color-background: var(--tr-color-background);
6
+ --color-ink: var(--tr-color-ink);
7
+ --font-body: var(--tr-font-body);
8
+ --text-base: var(--tr-text-base);
9
+ }
10
+
11
+ :root {
12
+ --tr-color-accent: #2f6df6;
13
+ --tr-color-background: #ffffff;
14
+ --tr-color-ink: #1a1d21;
15
+ --tr-font-body: system-ui, -apple-system, sans-serif;
16
+ --tr-text-base: 1rem;
17
+ }
18
+
19
+ body {
20
+ font-family: var(--tr-font-body);
21
+ color: var(--tr-color-ink);
22
+ background: var(--tr-color-background);
23
+ font-size: var(--tr-text-base);
24
+ }
@@ -0,0 +1,27 @@
1
+ import type { TokenTheme } from "@homepages/template-kit";
2
+
3
+ // A template's design tokens. `template-kit theme` compiles this to theme.css;
4
+ // `template-kit dev` reads it directly. The `document` refs must name declared
5
+ // tokens (color/background → colors, font → fonts, fontSize → type).
6
+ export const theme: TokenTheme = {
7
+ colors: {
8
+ ink: "#1a1d21",
9
+ background: "#ffffff",
10
+ accent: "#2f6df6",
11
+ },
12
+ fonts: {
13
+ body: "system-ui, -apple-system, sans-serif",
14
+ },
15
+ type: {
16
+ base: "1rem",
17
+ },
18
+ radii: {},
19
+ shadows: {},
20
+ layout: {},
21
+ document: {
22
+ font: "body",
23
+ color: "ink",
24
+ background: "background",
25
+ fontSize: "base",
26
+ },
27
+ };
@@ -0,0 +1,20 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ //#region src/cli/new/scaffold-assets.ts
6
+ const here = dirname(fileURLToPath(import.meta.url));
7
+ function assetsRoot() {
8
+ const dist = join(here, "scaffold");
9
+ if (existsSync(dist)) return dist;
10
+ return join(here, "..", "..", "..", "create-homepages-workspace", "scaffold");
11
+ }
12
+ function templateAssetsDir() {
13
+ return join(assetsRoot(), "template");
14
+ }
15
+ function sectionAssetsDir() {
16
+ return join(assetsRoot(), "section");
17
+ }
18
+
19
+ //#endregion
20
+ export { sectionAssetsDir, templateAssetsDir };
@@ -0,0 +1,44 @@
1
+ import { join, relative, sep } from "node:path";
2
+ import { createHash } from "node:crypto";
3
+ import { readFile, readdir } from "node:fs/promises";
4
+
5
+ //#region src/cli/pack/collect.ts
6
+ const toPosix = (p) => p.split(sep).join("/");
7
+ async function walk(dir, out) {
8
+ for (const entry of await readdir(dir, { withFileTypes: true })) if (entry.isDirectory()) {
9
+ if (entry.name === "node_modules") continue;
10
+ await walk(join(dir, entry.name), out);
11
+ } else if (entry.isFile()) out.push(join(dir, entry.name));
12
+ }
13
+ /**
14
+ * Reads a template's regular files into sorted, hashable entries.
15
+ *
16
+ * Only real files and directories are walked: `node_modules` is skipped at any
17
+ * depth, and symlinks are omitted (a `Dirent` is neither `isFile()` nor
18
+ * `isDirectory()`), so a symlinked source file will not appear in the pack.
19
+ * Templates are authored as plain trees, so this is a non-issue in practice —
20
+ * but a submission that relies on a symlinked file would pack without it.
21
+ */
22
+ async function collectTemplateFiles(workspaceRoot, templateDir) {
23
+ const absPaths = [];
24
+ await walk(templateDir, absPaths);
25
+ const files = await Promise.all(absPaths.map(async (abs) => ({
26
+ archivePath: toPosix(relative(workspaceRoot, abs)),
27
+ templatePath: toPosix(relative(templateDir, abs)),
28
+ bytes: await readFile(abs)
29
+ })));
30
+ files.sort((a, b) => a.archivePath < b.archivePath ? -1 : a.archivePath > b.archivePath ? 1 : 0);
31
+ return files;
32
+ }
33
+ function contentHash(files) {
34
+ const ordered = [...files].sort((a, b) => a.templatePath < b.templatePath ? -1 : a.templatePath > b.templatePath ? 1 : 0);
35
+ const top = createHash("sha256");
36
+ for (const file of ordered) {
37
+ const inner = createHash("sha256").update(file.bytes).digest("hex");
38
+ top.update(file.templatePath).update("\0").update(inner).update("\n");
39
+ }
40
+ return `sha256:${top.digest("hex")}`;
41
+ }
42
+
43
+ //#endregion
44
+ export { collectTemplateFiles, contentHash };
@@ -0,0 +1,57 @@
1
+ import { readLinkMarker } from "../link/overlay.js";
2
+ import { z } from "zod";
3
+ import { join } from "node:path";
4
+ import { readFile, stat } from "node:fs/promises";
5
+
6
+ //#region src/cli/pack/guards.ts
7
+ const depsSchema = z.object({
8
+ dependencies: z.record(z.string(), z.string()).optional(),
9
+ devDependencies: z.record(z.string(), z.string()).optional(),
10
+ optionalDependencies: z.record(z.string(), z.string()).optional()
11
+ }).passthrough();
12
+ async function isFile(p) {
13
+ try {
14
+ return (await stat(p)).isFile();
15
+ } catch {
16
+ return false;
17
+ }
18
+ }
19
+ function localProtocolDeps(pkg) {
20
+ const offenders = [];
21
+ for (const block of [
22
+ pkg.dependencies,
23
+ pkg.devDependencies,
24
+ pkg.optionalDependencies
25
+ ]) for (const [name, spec] of Object.entries(block ?? {})) if (spec.startsWith("file:") || spec.startsWith("link:")) offenders.push(`${name} (${spec})`);
26
+ return offenders;
27
+ }
28
+ async function preflightGuards(workspaceRoot) {
29
+ const refusals = [];
30
+ if (await readLinkMarker(workspaceRoot) !== void 0) refusals.push({
31
+ reason: "a linked kit overlay is active in this workspace.",
32
+ fix: "Run `template-kit unlink` to restore the published kit, then pack."
33
+ });
34
+ if (!await isFile(join(workspaceRoot, "package-lock.json"))) refusals.push({
35
+ reason: "missing lockfile (package-lock.json) at workspace root.",
36
+ fix: "Run npm install and commit the lockfile — a submission without one cannot be reproduced."
37
+ });
38
+ let pkgJson;
39
+ try {
40
+ pkgJson = JSON.parse(await readFile(join(workspaceRoot, "package.json"), "utf8"));
41
+ } catch {
42
+ refusals.push({
43
+ reason: "package.json at the workspace root is missing or not valid JSON.",
44
+ fix: "Ensure the workspace root has a well-formed package.json before packing."
45
+ });
46
+ return refusals;
47
+ }
48
+ const parsed = depsSchema.safeParse(pkgJson);
49
+ if (parsed.success) for (const dep of localProtocolDeps(parsed.data)) refusals.push({
50
+ reason: `dependency ${dep} uses a local file:/link: protocol.`,
51
+ fix: "Depend on published versions only — a submission must install from the registry."
52
+ });
53
+ return refusals;
54
+ }
55
+
56
+ //#endregion
57
+ export { preflightGuards };
@@ -0,0 +1,66 @@
1
+ import { findWorkspaceRoot, selectTemplates, templateFromCwd } from "../check/workspace.js";
2
+ import { runCheck } from "../check/index.js";
3
+ import { collectTemplateFiles, contentHash } from "./collect.js";
4
+ import { preflightGuards } from "./guards.js";
5
+ import { buildPackManifest } from "./manifest.js";
6
+ import { buildZip } from "./zip.js";
7
+ import { join } from "node:path";
8
+ import { parseArgs } from "node:util";
9
+ import { readFile, writeFile } from "node:fs/promises";
10
+
11
+ //#region src/cli/pack/index.ts
12
+ function refusal(lines) {
13
+ return {
14
+ code: 1,
15
+ output: `template-kit pack: cannot pack.\n${lines.join("\n")}\n`
16
+ };
17
+ }
18
+ async function runPack(argv, cwd) {
19
+ const { positionals } = parseArgs({
20
+ args: argv,
21
+ allowPositionals: true
22
+ });
23
+ const root = await findWorkspaceRoot(cwd);
24
+ const template = (await selectTemplates(root, positionals[0] ?? templateFromCwd(root, cwd), false, "pack"))[0];
25
+ if (template === void 0) return refusal([" this workspace has no template to pack: templates/ holds no template folder."]);
26
+ const preflight = await preflightGuards(root);
27
+ if (preflight.length > 0) return refusal(preflight.map((r) => ` ${r.reason}\n fix: ${r.fix}`));
28
+ const { code: checkCode, output: checkOut } = await runCheck([template.key], cwd);
29
+ if (checkCode !== 0) return {
30
+ code: 1,
31
+ output: `template-kit pack: \`check\` must pass before packing.\n\n${checkOut}`
32
+ };
33
+ const files = await collectTemplateFiles(root, template.dir);
34
+ const hash = contentHash(files);
35
+ const manifest = buildPackManifest({
36
+ template: template.key,
37
+ contentHash: hash,
38
+ packedAt: (/* @__PURE__ */ new Date()).toISOString()
39
+ });
40
+ const entries = [
41
+ {
42
+ path: "pack-manifest.json",
43
+ bytes: Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8")
44
+ },
45
+ {
46
+ path: "package.json",
47
+ bytes: await readFile(join(root, "package.json"))
48
+ },
49
+ {
50
+ path: "package-lock.json",
51
+ bytes: await readFile(join(root, "package-lock.json"))
52
+ },
53
+ ...files.map((f) => ({
54
+ path: f.archivePath,
55
+ bytes: f.bytes
56
+ }))
57
+ ];
58
+ await writeFile(join(root, `${template.key}.zip`), buildZip(entries));
59
+ return {
60
+ code: 0,
61
+ output: `template-kit pack\n wrote ${template.key}.zip\n ${hash}\n kit ${manifest.kitVersion}`
62
+ };
63
+ }
64
+
65
+ //#endregion
66
+ export { runPack };
@@ -0,0 +1,15 @@
1
+ import { KIT_VERSION } from "../../index.js";
2
+
3
+ //#region src/cli/pack/manifest.ts
4
+ function buildPackManifest(input) {
5
+ return {
6
+ kitVersion: KIT_VERSION,
7
+ template: input.template,
8
+ contentHash: input.contentHash,
9
+ packedAt: input.packedAt,
10
+ check: { ok: true }
11
+ };
12
+ }
13
+
14
+ //#endregion
15
+ export { buildPackManifest };
@@ -0,0 +1,86 @@
1
+ import { deflateRawSync } from "node:zlib";
2
+
3
+ //#region src/cli/pack/zip.ts
4
+ const DOS_DATE = 33;
5
+ const DOS_TIME = 0;
6
+ const CRC_TABLE = (() => {
7
+ const table = /* @__PURE__ */ new Uint32Array(256);
8
+ for (let n = 0; n < 256; n++) {
9
+ let c = n;
10
+ for (let k = 0; k < 8; k++) c = (c & 1) !== 0 ? 3988292384 ^ c >>> 1 : c >>> 1;
11
+ table[n] = c >>> 0;
12
+ }
13
+ return table;
14
+ })();
15
+ function crc32(bytes) {
16
+ let c = 4294967295;
17
+ for (const byte of bytes) c = CRC_TABLE[(c ^ byte) & 255] ^ c >>> 8;
18
+ return (c ^ 4294967295) >>> 0;
19
+ }
20
+ /** A standard DEFLATE zip built from `entries`, in order. No zip64 — pack inputs
21
+ * are source files, far under the 4 GiB per-entry limit. */
22
+ function buildZip(entries) {
23
+ const locals = [];
24
+ const centrals = [];
25
+ let offset = 0;
26
+ for (const entry of entries) {
27
+ const name = Buffer.from(entry.path, "utf8");
28
+ const source = Buffer.from(entry.bytes);
29
+ const crc = crc32(source);
30
+ const compressed = deflateRawSync(source);
31
+ const local = Buffer.alloc(30 + name.length);
32
+ local.writeUInt32LE(67324752, 0);
33
+ local.writeUInt16LE(20, 4);
34
+ local.writeUInt16LE(2048, 6);
35
+ local.writeUInt16LE(8, 8);
36
+ local.writeUInt16LE(DOS_TIME, 10);
37
+ local.writeUInt16LE(DOS_DATE, 12);
38
+ local.writeUInt32LE(crc, 14);
39
+ local.writeUInt32LE(compressed.length, 18);
40
+ local.writeUInt32LE(source.length, 22);
41
+ local.writeUInt16LE(name.length, 26);
42
+ local.writeUInt16LE(0, 28);
43
+ name.copy(local, 30);
44
+ locals.push(local, compressed);
45
+ const central = Buffer.alloc(46 + name.length);
46
+ central.writeUInt32LE(33639248, 0);
47
+ central.writeUInt16LE(788, 4);
48
+ central.writeUInt16LE(20, 6);
49
+ central.writeUInt16LE(2048, 8);
50
+ central.writeUInt16LE(8, 10);
51
+ central.writeUInt16LE(DOS_TIME, 12);
52
+ central.writeUInt16LE(DOS_DATE, 14);
53
+ central.writeUInt32LE(crc, 16);
54
+ central.writeUInt32LE(compressed.length, 20);
55
+ central.writeUInt32LE(source.length, 24);
56
+ central.writeUInt16LE(name.length, 28);
57
+ central.writeUInt16LE(0, 30);
58
+ central.writeUInt16LE(0, 32);
59
+ central.writeUInt16LE(0, 34);
60
+ central.writeUInt16LE(0, 36);
61
+ central.writeUInt32LE(33188 << 16 >>> 0, 38);
62
+ central.writeUInt32LE(offset, 42);
63
+ name.copy(central, 46);
64
+ centrals.push(central);
65
+ offset += local.length + compressed.length;
66
+ }
67
+ const localData = Buffer.concat(locals);
68
+ const centralDir = Buffer.concat(centrals);
69
+ const eocd = Buffer.alloc(22);
70
+ eocd.writeUInt32LE(101010256, 0);
71
+ eocd.writeUInt16LE(0, 4);
72
+ eocd.writeUInt16LE(0, 6);
73
+ eocd.writeUInt16LE(entries.length, 8);
74
+ eocd.writeUInt16LE(entries.length, 10);
75
+ eocd.writeUInt32LE(centralDir.length, 12);
76
+ eocd.writeUInt32LE(localData.length, 16);
77
+ eocd.writeUInt16LE(0, 20);
78
+ return Buffer.concat([
79
+ localData,
80
+ centralDir,
81
+ eocd
82
+ ]);
83
+ }
84
+
85
+ //#endregion
86
+ export { buildZip };
@@ -0,0 +1,43 @@
1
+ import { TokenThemeSchema, compileThemeToCss } from "../../design-system/theme.js";
2
+ import { loadTheme } from "./load-theme.js";
3
+ import { join } from "node:path";
4
+ import { writeFile } from "node:fs/promises";
5
+
6
+ //#region src/cli/theme/generate.ts
7
+ const OUT_FILE = "theme.css";
8
+ const HEADER = "/* Generated by `template-kit theme` from theme.ts — do not edit.\n Re-run `template-kit theme` after changing theme.ts. */\n";
9
+ /** theme.ts's `theme` export failed TokenThemeSchema; `issues` are `path: message` lines. */
10
+ var InvalidTheme = class extends Error {
11
+ path;
12
+ issues;
13
+ constructor(path, issues) {
14
+ super(`${path} is not a valid TokenTheme:\n${issues.map((i) => ` - ${i}`).join("\n")}`);
15
+ this.path = path;
16
+ this.issues = issues;
17
+ this.name = "InvalidTheme";
18
+ }
19
+ };
20
+ /**
21
+ * Compile `templates/<key>/theme.ts` to `templates/<key>/theme.css`. Throws
22
+ * MissingThemeFile / MissingThemeExport (load-theme.ts) or InvalidTheme.
23
+ */
24
+ async function generateTemplateTheme(root, template) {
25
+ const raw = await loadTheme(root, template);
26
+ const parsed = TokenThemeSchema.safeParse(raw);
27
+ if (!parsed.success) {
28
+ const issues = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`);
29
+ throw new InvalidTheme(join("templates", template.key, "theme.ts"), issues);
30
+ }
31
+ const theme = parsed.data;
32
+ const outPath = join(template.dir, OUT_FILE);
33
+ await writeFile(outPath, `${HEADER}${compileThemeToCss(theme)}`, "utf8");
34
+ const tokenCount = Object.keys(theme.colors).length + Object.keys(theme.fonts).length + Object.keys(theme.type).length + Object.keys(theme.radii).length + Object.keys(theme.shadows).length + Object.keys(theme.layout).length;
35
+ return {
36
+ outPath,
37
+ relPath: join("templates", template.key, OUT_FILE),
38
+ tokenCount
39
+ };
40
+ }
41
+
42
+ //#endregion
43
+ export { InvalidTheme, generateTemplateTheme };
@@ -0,0 +1,33 @@
1
+ import { findWorkspaceRoot, selectTemplates, templateFromCwd } from "../check/workspace.js";
2
+ import { generateTemplateTheme } from "./generate.js";
3
+ import { parseArgs } from "node:util";
4
+
5
+ //#region src/cli/theme/index.ts
6
+ async function runTheme(argv, cwd) {
7
+ const { values, positionals } = parseArgs({
8
+ args: argv,
9
+ options: { all: {
10
+ type: "boolean",
11
+ default: false
12
+ } },
13
+ allowPositionals: true
14
+ });
15
+ const root = await findWorkspaceRoot(cwd);
16
+ const selected = await selectTemplates(root, positionals[0] ?? (values.all ? void 0 : templateFromCwd(root, cwd)), values.all, "theme");
17
+ if (selected.length === 0) return {
18
+ code: 1,
19
+ output: "No template to generate theme CSS for: templates/ holds no template folder."
20
+ };
21
+ const lines = [];
22
+ for (const template of selected) {
23
+ const { relPath, tokenCount } = await generateTemplateTheme(root, template);
24
+ lines.push(` ${template.key} wrote ${relPath} (${tokenCount} tokens)`);
25
+ }
26
+ return {
27
+ code: 0,
28
+ output: `template-kit theme\n${lines.join("\n")}`
29
+ };
30
+ }
31
+
32
+ //#endregion
33
+ export { runTheme };
@@ -0,0 +1,69 @@
1
+ import { loadEsbuild } from "../check/resolve-tool.js";
2
+ import { join } from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { mkdir, readFile, rm, stat } from "node:fs/promises";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ //#region src/cli/theme/load-theme.ts
8
+ const THEME_FILE = "theme.ts";
9
+ const THEME_EXPORT = "theme";
10
+ /** A template has no theme.ts. `path` is workspace-relative. */
11
+ var MissingThemeFile = class extends Error {
12
+ path;
13
+ constructor(path) {
14
+ super(`${path} not found. A template's design tokens live in theme.ts — create it exporting \`${THEME_EXPORT}\` (a TokenTheme).`);
15
+ this.path = path;
16
+ this.name = "MissingThemeFile";
17
+ }
18
+ };
19
+ /** theme.ts built, but exports no `theme` binding. `path` is workspace-relative. */
20
+ var MissingThemeExport = class extends Error {
21
+ path;
22
+ constructor(path) {
23
+ super(`${path} does not export \`${THEME_EXPORT}\`. It must \`export const ${THEME_EXPORT}: TokenTheme = { … }\`.`);
24
+ this.path = path;
25
+ this.name = "MissingThemeExport";
26
+ }
27
+ };
28
+ const isFile = async (p) => {
29
+ try {
30
+ return (await stat(p)).isFile();
31
+ } catch {
32
+ return false;
33
+ }
34
+ };
35
+ /**
36
+ * Transpile + evaluate `templates/<key>/theme.ts` and return its `theme` export,
37
+ * unvalidated (the caller parses it with TokenThemeSchema). Throws MissingThemeFile
38
+ * when theme.ts is absent and MissingThemeExport when it builds without a `theme`
39
+ * binding.
40
+ */
41
+ async function loadTheme(root, template) {
42
+ const source = join(template.dir, THEME_FILE);
43
+ const rel = join("templates", template.key, THEME_FILE);
44
+ if (!await isFile(source)) throw new MissingThemeFile(rel);
45
+ const outdir = join(root, "node_modules", ".template-kit", "theme", createHash("sha256").update(template.dir).digest("hex").slice(0, 12));
46
+ await rm(outdir, {
47
+ recursive: true,
48
+ force: true
49
+ });
50
+ await mkdir(outdir, { recursive: true });
51
+ const { build } = await loadEsbuild(root);
52
+ await build({
53
+ entryPoints: [source],
54
+ outdir,
55
+ bundle: true,
56
+ format: "esm",
57
+ platform: "neutral",
58
+ packages: "external",
59
+ logLevel: "silent",
60
+ absWorkingDir: root
61
+ });
62
+ const version = createHash("sha256").update(await readFile(source)).digest("hex").slice(0, 12);
63
+ const value = (await import(`${pathToFileURL(join(outdir, "theme.js")).href}?v=${version}`))[THEME_EXPORT];
64
+ if (value === void 0) throw new MissingThemeExport(rel);
65
+ return value;
66
+ }
67
+
68
+ //#endregion
69
+ export { MissingThemeExport, MissingThemeFile, loadTheme };
package/dist/cli.js CHANGED
@@ -1,5 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { KIT_VERSION } from "./index.js";
3
+ import { runCheck } from "./cli/check/index.js";
4
+ import { runDev } from "./cli/dev/index.js";
5
+ import { rebuildOverlay, runLinkOnce, runStatus, runUnlink } from "./cli/link/index.js";
6
+ import { watchAndRebuild } from "./cli/link/watch.js";
7
+ import { runNew, runNewSection } from "./cli/new/index.js";
8
+ import { runPack } from "./cli/pack/index.js";
9
+ import { runTheme } from "./cli/theme/index.js";
3
10
 
4
11
  //#region src/cli/index.ts
5
12
  /**
@@ -8,31 +15,27 @@ import { KIT_VERSION } from "./index.js";
8
15
  * printed to template authors outside HomePages. The command → issue map is in
9
16
  * CLAUDE.md, which does not ship in the tarball.
10
17
  */
11
- const NOT_IMPLEMENTED = /* @__PURE__ */ new Set([
12
- "dev",
13
- "check",
14
- "pack",
15
- "link",
16
- "unlink",
17
- "status"
18
- ]);
18
+ const NOT_IMPLEMENTED = /* @__PURE__ */ new Set();
19
19
  const USAGE = `template-kit ${KIT_VERSION}
20
20
 
21
21
  Usage: template-kit <command>
22
22
 
23
23
  Commands:
24
- dev Local preview server (not implemented yet)
25
- check Typecheck, lint, and validate (not implemented yet)
26
- pack Build the submission artifact (not implemented yet)
27
- link Overlay a local kit build (not implemented yet)
28
- unlink Remove the overlay (not implemented yet)
29
- status Show overlay state (not implemented yet)
24
+ dev Start the local preview server
25
+ check Typecheck, lint, and validate a template
26
+ new Add a template to the workspace
27
+ new-section Add a section to a template
28
+ theme Generate a template's theme CSS from its theme.ts
29
+ pack Build a template's submission zip
30
+ link Overlay a local kit build into a consumer (kit devs)
31
+ unlink Restore the registry install (kit devs)
32
+ status Show overlay state (kit devs)
30
33
 
31
34
  Options:
32
35
  -v, --version Print the kit version
33
36
  -h, --help Print this message
34
37
  `;
35
- function main(argv) {
38
+ async function main(argv) {
36
39
  const cmd = argv[0];
37
40
  if (cmd === "--version" || cmd === "-v") {
38
41
  process.stdout.write(`${KIT_VERSION}\n`);
@@ -42,6 +45,87 @@ function main(argv) {
42
45
  process.stdout.write(USAGE);
43
46
  return 0;
44
47
  }
48
+ if (cmd === "check") try {
49
+ const { code, output } = await runCheck(argv.slice(1), process.cwd());
50
+ process.stdout.write(output.endsWith("\n") ? output : `${output}\n`);
51
+ return code;
52
+ } catch (cause) {
53
+ process.stderr.write(`template-kit check: ${cause instanceof Error ? cause.message : String(cause)}\n`);
54
+ return 1;
55
+ }
56
+ if (cmd === "dev") try {
57
+ return await runDev(argv.slice(1), process.cwd());
58
+ } catch (cause) {
59
+ process.stderr.write(`template-kit dev: ${cause instanceof Error ? cause.message : String(cause)}\n`);
60
+ return 1;
61
+ }
62
+ if (cmd === "theme") try {
63
+ const { code, output } = await runTheme(argv.slice(1), process.cwd());
64
+ process.stdout.write(output.endsWith("\n") ? output : `${output}\n`);
65
+ return code;
66
+ } catch (cause) {
67
+ process.stderr.write(`template-kit theme: ${cause instanceof Error ? cause.message : String(cause)}\n`);
68
+ return 1;
69
+ }
70
+ if (cmd === "pack") try {
71
+ const { code, output } = await runPack(argv.slice(1), process.cwd());
72
+ process.stdout.write(output.endsWith("\n") ? output : `${output}\n`);
73
+ return code;
74
+ } catch (cause) {
75
+ process.stderr.write(`template-kit pack: ${cause instanceof Error ? cause.message : String(cause)}\n`);
76
+ return 1;
77
+ }
78
+ if (cmd === "status" || cmd === "unlink") {
79
+ const run = cmd === "status" ? runStatus : runUnlink;
80
+ try {
81
+ const { code, output } = await run(argv.slice(1), process.cwd());
82
+ process.stdout.write(output.endsWith("\n") ? output : `${output}\n`);
83
+ return code;
84
+ } catch (cause) {
85
+ process.stderr.write(`template-kit ${cmd}: ${cause instanceof Error ? cause.message : String(cause)}\n`);
86
+ return 1;
87
+ }
88
+ }
89
+ if (cmd === "link") try {
90
+ const result = await runLinkOnce(argv.slice(1), process.cwd());
91
+ process.stdout.write(`${result.output}\n`);
92
+ const { kitSrcDir, kitRoot, workspaceRoot } = result.watch;
93
+ process.stdout.write(" watching for kit changes — Ctrl-C to stop\n");
94
+ let watchFailed = false;
95
+ await new Promise((resolvePromise) => {
96
+ const stop = watchAndRebuild(kitSrcDir, () => {
97
+ rebuildOverlay(kitRoot, workspaceRoot).then(() => process.stdout.write(" rebuilt + recopied\n"), (err) => process.stderr.write(` rebuild failed: ${err instanceof Error ? err.message : String(err)}\n`));
98
+ }, (err) => {
99
+ watchFailed = true;
100
+ process.stderr.write(` watch error — stopping: ${err instanceof Error ? err.message : String(err)}\n`);
101
+ resolvePromise();
102
+ });
103
+ process.on("SIGINT", () => {
104
+ stop();
105
+ resolvePromise();
106
+ });
107
+ });
108
+ return watchFailed ? 1 : 0;
109
+ } catch (cause) {
110
+ process.stderr.write(`template-kit link: ${cause instanceof Error ? cause.message : String(cause)}\n`);
111
+ return 1;
112
+ }
113
+ if (cmd === "new") try {
114
+ const { code, output } = await runNew(argv.slice(1), process.cwd());
115
+ process.stdout.write(output.endsWith("\n") ? output : `${output}\n`);
116
+ return code;
117
+ } catch (cause) {
118
+ process.stderr.write(`template-kit new: ${cause instanceof Error ? cause.message : String(cause)}\n`);
119
+ return 1;
120
+ }
121
+ if (cmd === "new-section") try {
122
+ const { code, output } = await runNewSection(argv.slice(1), process.cwd());
123
+ process.stdout.write(output.endsWith("\n") ? output : `${output}\n`);
124
+ return code;
125
+ } catch (cause) {
126
+ process.stderr.write(`template-kit new-section: ${cause instanceof Error ? cause.message : String(cause)}\n`);
127
+ return 1;
128
+ }
45
129
  if (NOT_IMPLEMENTED.has(cmd)) {
46
130
  process.stderr.write(`template-kit ${cmd}: not implemented yet.\nThis 0.0.x release ships the package skeleton only.\n`);
47
131
  return 1;
@@ -49,6 +133,6 @@ function main(argv) {
49
133
  process.stderr.write(`template-kit: unknown command '${cmd}'\n\n${USAGE}`);
50
134
  return 1;
51
135
  }
52
- process.exit(main(process.argv.slice(2)));
136
+ process.exit(await main(process.argv.slice(2)));
53
137
 
54
138
  //#endregion