@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.
- package/CHANGELOG.md +222 -0
- package/README.md +96 -17
- package/dist/asset-modules.d.ts +81 -0
- package/dist/base.css +9 -0
- package/dist/cli/check/config.js +41 -0
- package/dist/cli/check/css.js +131 -0
- package/dist/cli/check/diagnostics.js +32 -0
- package/dist/cli/check/index.js +87 -0
- package/dist/cli/check/loader.js +146 -0
- package/dist/cli/check/paths.js +15 -0
- package/dist/cli/check/relativize.js +18 -0
- package/dist/cli/check/render-invariants.js +24 -0
- package/dist/cli/check/resolve-tool.js +38 -0
- package/dist/cli/check/section-assets.js +58 -0
- package/dist/cli/check/stages/deps.js +337 -0
- package/dist/cli/check/stages/lint.js +46 -0
- package/dist/cli/check/stages/render.js +101 -0
- package/dist/cli/check/stages/size.js +274 -0
- package/dist/cli/check/stages/tree.js +206 -0
- package/dist/cli/check/stages/typecheck.js +46 -0
- package/dist/cli/check/stages/validate/catalog-exhaustiveness.js +18 -0
- package/dist/cli/check/stages/validate/facts.js +18 -0
- package/dist/cli/check/stages/validate/formatter-contract.js +14 -0
- package/dist/cli/check/stages/validate/group-contract.js +21 -0
- package/dist/cli/check/stages/validate/index.js +66 -0
- package/dist/cli/check/stages/validate/list-scalar-contract.js +22 -0
- package/dist/cli/check/stages/validate/nav-contract.js +93 -0
- package/dist/cli/check/stages/validate/nested-slot-contract.js +12 -0
- package/dist/cli/check/stages/validate/object-list-contract.js +31 -0
- package/dist/cli/check/stages/validate/orchestrator.js +241 -0
- package/dist/cli/check/stages/validate/produced-by-contract.js +18 -0
- package/dist/cli/check/stages/validate/row-contract.js +31 -0
- package/dist/cli/check/stages/validate/select-contract.js +13 -0
- package/dist/cli/check/stages/validate/source-contract.js +21 -0
- package/dist/cli/check/stages/validate/variant-contract.js +15 -0
- package/dist/cli/check/workspace.js +86 -0
- package/dist/cli/dev/build-css.js +91 -0
- package/dist/cli/dev/catalog.js +35 -0
- package/dist/cli/dev/compose-template.js +38 -0
- package/dist/cli/dev/content-override.js +58 -0
- package/dist/cli/dev/discover.js +35 -0
- package/dist/cli/dev/fill-state.js +75 -0
- package/dist/cli/dev/index.js +32 -0
- package/dist/cli/dev/inspect.js +38 -0
- package/dist/cli/dev/island-bootstrap.js +38 -0
- package/dist/cli/dev/island-map.js +35 -0
- package/dist/cli/dev/island-transform.js +31 -0
- package/dist/cli/dev/manifest-instances.js +31 -0
- package/dist/cli/dev/render-section.js +24 -0
- package/dist/cli/dev/screenshot-target.js +24 -0
- package/dist/cli/dev/section-page.js +54 -0
- package/dist/cli/dev/server.js +480 -0
- package/dist/cli/dev/slot-schema.js +12 -0
- package/dist/cli/dev/structure-summary.js +118 -0
- package/dist/cli/dev/tailwind.js +32 -0
- package/dist/cli/dev/vite-server.js +33 -0
- package/dist/cli/dev/workspace.js +63 -0
- package/dist/cli/link/index.js +74 -0
- package/dist/cli/link/overlay.js +59 -0
- package/dist/cli/link/watch.js +25 -0
- package/dist/cli/new/emit.js +48 -0
- package/dist/cli/new/index.js +69 -0
- package/dist/cli/new/scaffold/section/_Renderer.tsx +20 -0
- package/dist/cli/new/scaffold/section/_fill-spec.ts +18 -0
- package/dist/cli/new/scaffold/section/_fixtures.ts +13 -0
- package/dist/cli/new/scaffold/section/_schema.ts +24 -0
- package/dist/cli/new/scaffold/template/_manifest.json +9 -0
- package/dist/cli/new/scaffold/template/sections/hero/ExpandableText.tsx +45 -0
- package/dist/cli/new/scaffold/template/sections/hero/Renderer.tsx +40 -0
- package/dist/cli/new/scaffold/template/sections/hero/components/Feature.tsx +14 -0
- package/dist/cli/new/scaffold/template/sections/hero/fill-spec.ts +29 -0
- package/dist/cli/new/scaffold/template/sections/hero/fixtures.ts +41 -0
- package/dist/cli/new/scaffold/template/sections/hero/schema.ts +60 -0
- package/dist/cli/new/scaffold/template/theme.css +24 -0
- package/dist/cli/new/scaffold/template/theme.ts +27 -0
- package/dist/cli/new/scaffold-assets.js +20 -0
- package/dist/cli/pack/collect.js +44 -0
- package/dist/cli/pack/guards.js +57 -0
- package/dist/cli/pack/index.js +66 -0
- package/dist/cli/pack/manifest.js +15 -0
- package/dist/cli/pack/zip.js +86 -0
- package/dist/cli/theme/generate.js +43 -0
- package/dist/cli/theme/index.js +33 -0
- package/dist/cli/theme/load-theme.js +69 -0
- package/dist/cli.js +100 -16
- package/dist/contracts/fill-treatments.js +47 -0
- package/dist/design-system/theme.d.ts +30 -300
- package/dist/design-system/theme.js +112 -90
- package/dist/dev-client/assets/index-DfEfrp6P.css +1 -0
- package/dist/dev-client/assets/index-bpMP0b0W.js +50 -0
- package/dist/dev-client/index.html +13 -0
- package/dist/eslint/is-client-file.d.ts +4 -0
- package/dist/eslint/rules/no-hex.js +78 -6
- package/dist/eslint.js +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -2
- package/dist/islands/detect.d.ts +16 -0
- package/dist/islands/detect.js +22 -0
- package/dist/islands/discover.d.ts +5 -0
- package/dist/islands/discover.js +19 -0
- package/dist/islands/esbuild-plugin.d.ts +13 -0
- package/dist/islands/esbuild-plugin.js +39 -0
- package/dist/islands/wrap.d.ts +11 -0
- package/dist/islands/wrap.js +48 -0
- package/dist/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.js +77 -0
- package/dist/node_modules/magic-string/dist/magic-string.es.js +939 -0
- package/dist/package.js +1 -1
- package/dist/primitives/Image.js +1 -1
- package/dist/schema/fill-spec.d.ts +80 -590
- package/dist/schema/fixture-schema.d.ts +5 -81
- package/dist/schema/manifest.d.ts +39 -475
- package/dist/schema/resolve-section-ref.js +10 -0
- package/dist/schema/rows.js +10 -0
- package/dist/schema/section-nav.js +9 -0
- package/dist/schema/section-schema.d.ts +51 -437
- package/dist/schema/slot-types.d.ts +12 -16
- package/dist/ssr.d.ts +31 -0
- package/dist/ssr.js +46 -0
- package/dist/styles.css +31 -88
- package/docs/INDEX.md +12 -3
- package/docs/assets.md +128 -0
- package/docs/check.md +124 -0
- package/docs/dev.md +187 -0
- package/docs/eslint.md +18 -8
- package/docs/islands.md +5 -2
- package/docs/llms.txt +59 -9
- package/docs/new.md +27 -0
- package/docs/overview.md +57 -0
- package/docs/pack.md +46 -0
- package/docs/primitives.md +10 -6
- package/docs/quickstart.md +102 -0
- package/docs/recipes/INDEX.md +27 -0
- package/docs/recipes/bind-property-fact.md +58 -0
- package/docs/recipes/collection-slot.md +100 -0
- package/docs/recipes/fill-spec-decision.md +66 -0
- package/docs/recipes/fixture-states.md +68 -0
- package/docs/recipes/image-slot-crop.md +98 -0
- package/docs/recipes/interactive-island.md +100 -0
- package/docs/recipes/organize-section-folder.md +72 -0
- package/docs/recipes/prepare-submission.md +55 -0
- package/docs/recipes/second-template.md +49 -0
- package/docs/recipes/select-slot.md +59 -0
- package/docs/recipes/static-asset.md +103 -0
- package/docs/recipes/third-party-package.md +90 -0
- package/docs/rules/INDEX.md +14 -5
- package/docs/rules/bundle-binary-asset.md +83 -0
- package/docs/rules/bundle-incomplete.md +2 -2
- package/docs/rules/css-reason.md +3 -3
- package/docs/rules/fixtures-invalid.md +96 -0
- package/docs/rules/license-denied.md +11 -3
- package/docs/rules/manifest-invalid.md +99 -0
- package/docs/rules/no-bare-css-import.md +8 -8
- package/docs/rules/no-css-import-from-render-path.md +7 -8
- package/docs/rules/no-hex.md +76 -10
- package/docs/rules/no-templates.md +87 -0
- package/docs/rules/parse-error.md +76 -0
- package/docs/rules/schema-invalid.md +96 -0
- package/docs/rules/size-assets.md +88 -0
- package/docs/rules/size-island-bundle.md +123 -0
- package/docs/rules/size-section-css.md +19 -14
- package/docs/schema-system.md +28 -28
- package/docs/theme-and-css.md +157 -92
- package/docs/vocabulary.md +98 -0
- package/package.json +14 -11
- 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
|
|
25
|
-
check
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|