@homepages/template-kit 0.9.0-dev-20260719224319 → 0.9.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 +29 -59
- package/README.md +14 -4
- package/dist/contracts/fill-treatments.d.ts +23 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/islands/discover.js +1 -1
- package/dist/islands/esbuild-plugin.js +1 -1
- package/dist/islands/wrap.d.ts +9 -1
- package/dist/media/resolve-fixture-media.d.ts +22 -0
- package/dist/package.js +1 -1
- package/dist/schema/resolve-section-ref.d.ts +4 -0
- package/dist/schema/rows.d.ts +9 -0
- package/dist/schema/section-nav.d.ts +5 -1
- package/dist/schema/slot-types.d.ts +1 -1
- package/dist/ssr.d.ts +7 -2
- package/dist/ssr.js +6 -2
- package/guide/INDEX.md +5 -0
- package/guide/check.md +1 -0
- package/guide/dev.md +1 -0
- package/guide/llms.txt +11 -3
- package/guide/upgrading.md +12 -8
- package/package.json +4 -16
- package/dist/canvas-core/chrome-css.d.ts +0 -24
- package/dist/canvas-core/chrome-css.js +0 -403
- package/dist/canvas-core/chrome.d.ts +0 -49
- package/dist/canvas-core/chrome.js +0 -147
- package/dist/canvas-core/frame.d.ts +0 -20
- package/dist/canvas-core/frame.js +0 -23
- package/dist/canvas-core/hidden-tabs.d.ts +0 -16
- package/dist/canvas-core/hidden-tabs.js +0 -84
- package/dist/canvas-core/resolve-target.js +0 -55
- package/dist/canvas-core/scroll.d.ts +0 -10
- package/dist/canvas-core/scroll.js +0 -15
- package/dist/canvas-core/section-dom.d.ts +0 -6
- package/dist/canvas-core/section-dom.js +0 -43
- package/dist/canvas-core/srcdoc.d.ts +0 -8
- package/dist/canvas-core/srcdoc.js +0 -77
- package/dist/canvas-core/types.d.ts +0 -76
- package/dist/canvas-core.d.ts +0 -9
- package/dist/canvas-core.js +0 -9
- package/dist/cli/check/config.js +0 -41
- package/dist/cli/check/css.js +0 -131
- package/dist/cli/check/diagnostics.js +0 -37
- package/dist/cli/check/index.js +0 -94
- package/dist/cli/check/loader.js +0 -149
- package/dist/cli/check/paths.js +0 -15
- package/dist/cli/check/relativize.js +0 -18
- package/dist/cli/check/render-invariants.js +0 -24
- package/dist/cli/check/resolve-tool.js +0 -38
- package/dist/cli/check/section-assets.js +0 -58
- package/dist/cli/check/stages/deps.js +0 -337
- package/dist/cli/check/stages/lint.js +0 -46
- package/dist/cli/check/stages/render.js +0 -98
- package/dist/cli/check/stages/size.js +0 -274
- package/dist/cli/check/stages/tree.js +0 -206
- package/dist/cli/check/stages/typecheck.js +0 -46
- package/dist/cli/check/stages/validate/catalog-exhaustiveness.js +0 -18
- package/dist/cli/check/stages/validate/facts.js +0 -19
- package/dist/cli/check/stages/validate/formatter-contract.js +0 -14
- package/dist/cli/check/stages/validate/group-contract.js +0 -21
- package/dist/cli/check/stages/validate/index.js +0 -66
- package/dist/cli/check/stages/validate/list-scalar-contract.js +0 -22
- package/dist/cli/check/stages/validate/nav-contract.js +0 -93
- package/dist/cli/check/stages/validate/nested-slot-contract.js +0 -12
- package/dist/cli/check/stages/validate/object-list-contract.js +0 -31
- package/dist/cli/check/stages/validate/orchestrator.js +0 -241
- package/dist/cli/check/stages/validate/produced-by-contract.js +0 -18
- package/dist/cli/check/stages/validate/row-contract.js +0 -31
- package/dist/cli/check/stages/validate/select-contract.js +0 -13
- package/dist/cli/check/stages/validate/source-contract.js +0 -21
- package/dist/cli/check/stages/validate/variant-contract.js +0 -15
- package/dist/cli/check/workspace.js +0 -99
- package/dist/cli/dev/build-css.js +0 -97
- package/dist/cli/dev/catalog.js +0 -35
- package/dist/cli/dev/compose-template.js +0 -39
- package/dist/cli/dev/content-override.js +0 -58
- package/dist/cli/dev/diagnostics.js +0 -68
- package/dist/cli/dev/discover.js +0 -35
- package/dist/cli/dev/fill-state.js +0 -77
- package/dist/cli/dev/index.js +0 -42
- package/dist/cli/dev/inspect.js +0 -38
- package/dist/cli/dev/island-bootstrap.js +0 -38
- package/dist/cli/dev/island-canvas-bootstrap.js +0 -204
- package/dist/cli/dev/island-map.js +0 -35
- package/dist/cli/dev/island-transform.js +0 -31
- package/dist/cli/dev/manifest-instances.js +0 -31
- package/dist/cli/dev/render-section.js +0 -24
- package/dist/cli/dev/screenshot-target.js +0 -24
- package/dist/cli/dev/section-page.js +0 -91
- package/dist/cli/dev/server.js +0 -511
- package/dist/cli/dev/slot-schema.js +0 -12
- package/dist/cli/dev/structure-summary.js +0 -119
- package/dist/cli/dev/tailwind.js +0 -32
- package/dist/cli/dev/tsconfig-paths-plugin.js +0 -90
- package/dist/cli/dev/vite-server.js +0 -39
- package/dist/cli/dev/workspace.js +0 -63
- package/dist/cli/link/index.js +0 -74
- package/dist/cli/link/overlay.js +0 -59
- package/dist/cli/link/watch.js +0 -25
- package/dist/cli/new/emit.js +0 -48
- package/dist/cli/new/index.js +0 -69
- package/dist/cli/new/scaffold/section/_Renderer.tsx +0 -20
- package/dist/cli/new/scaffold/section/_fill-spec.ts +0 -18
- package/dist/cli/new/scaffold/section/_fixtures.ts +0 -13
- package/dist/cli/new/scaffold/section/_schema.ts +0 -24
- package/dist/cli/new/scaffold/template/_manifest.json +0 -9
- package/dist/cli/new/scaffold/template/sections/hero/ExpandableText.tsx +0 -45
- package/dist/cli/new/scaffold/template/sections/hero/Renderer.tsx +0 -40
- package/dist/cli/new/scaffold/template/sections/hero/components/Feature.tsx +0 -14
- package/dist/cli/new/scaffold/template/sections/hero/fill-spec.ts +0 -29
- package/dist/cli/new/scaffold/template/sections/hero/fixtures.ts +0 -41
- package/dist/cli/new/scaffold/template/sections/hero/schema.ts +0 -60
- package/dist/cli/new/scaffold/template/theme.css +0 -24
- package/dist/cli/new/scaffold/template/theme.ts +0 -27
- package/dist/cli/new/scaffold-assets.js +0 -20
- package/dist/cli/pack/collect.js +0 -44
- package/dist/cli/pack/guards.js +0 -57
- package/dist/cli/pack/index.js +0 -66
- package/dist/cli/pack/manifest.js +0 -15
- package/dist/cli/pack/zip.js +0 -86
- package/dist/cli/theme/generate.js +0 -43
- package/dist/cli/theme/index.js +0 -33
- package/dist/cli/theme/load-theme.js +0 -69
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +0 -138
- package/dist/dev-client/assets/index-CJa9G1Na.css +0 -1
- package/dist/dev-client/assets/index-C_1atZs4.js +0 -450
- package/dist/dev-client/index.html +0 -13
- package/dist/tokens.css +0 -21
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { resolveFromWorkspace } from "../check/resolve-tool.js";
|
|
2
|
-
import { isAbsolute, join, resolve } from "node:path";
|
|
3
|
-
import { existsSync, statSync } from "node:fs";
|
|
4
|
-
|
|
5
|
-
//#region src/cli/dev/tsconfig-paths-plugin.ts
|
|
6
|
-
const RESOLVE_EXTENSIONS = [
|
|
7
|
-
"",
|
|
8
|
-
".ts",
|
|
9
|
-
".tsx",
|
|
10
|
-
".js",
|
|
11
|
-
".jsx",
|
|
12
|
-
".mjs",
|
|
13
|
-
".cjs",
|
|
14
|
-
".json"
|
|
15
|
-
];
|
|
16
|
-
function toPattern(key, targets, baseUrl) {
|
|
17
|
-
const star = key.indexOf("*");
|
|
18
|
-
const hasWildcard = star !== -1;
|
|
19
|
-
return {
|
|
20
|
-
prefix: hasWildcard ? key.slice(0, star) : key,
|
|
21
|
-
suffix: hasWildcard ? key.slice(star + 1) : "",
|
|
22
|
-
hasWildcard,
|
|
23
|
-
targets: targets.map((target) => isAbsolute(target) ? target : join(baseUrl, target))
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
/** The wildcard capture for a match, `""` for a wildcard-free exact match, or `undefined` when `specifier` doesn't match. */
|
|
27
|
-
function matchPattern(pattern, specifier) {
|
|
28
|
-
if (!pattern.hasWildcard) return specifier === pattern.prefix ? "" : void 0;
|
|
29
|
-
if (!specifier.startsWith(pattern.prefix) || !specifier.endsWith(pattern.suffix)) return void 0;
|
|
30
|
-
return specifier.slice(pattern.prefix.length, specifier.length - pattern.suffix.length);
|
|
31
|
-
}
|
|
32
|
-
function isFile(path) {
|
|
33
|
-
try {
|
|
34
|
-
return existsSync(path) && statSync(path).isFile();
|
|
35
|
-
} catch {
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
/** `candidate` resolved as tsc would: itself, itself with a resolvable extension, or a directory index. */
|
|
40
|
-
function resolveCandidate(candidate) {
|
|
41
|
-
for (const ext of RESOLVE_EXTENSIONS) {
|
|
42
|
-
const withExt = candidate + ext;
|
|
43
|
-
if (isFile(withExt)) return withExt;
|
|
44
|
-
}
|
|
45
|
-
for (const ext of RESOLVE_EXTENSIONS.slice(1)) {
|
|
46
|
-
const indexFile = join(candidate, `index${ext}`);
|
|
47
|
-
if (isFile(indexFile)) return indexFile;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Builds a Vite `resolveId` plugin from `root`'s tsconfig `paths`, or returns
|
|
52
|
-
* `null` when the workspace has none configured, or its tsconfig/`typescript`
|
|
53
|
-
* can't be read at all — `check` already owns reporting a broken tsconfig
|
|
54
|
-
* cleanly; `dev` degrades to its pre-existing behavior (no `paths` support)
|
|
55
|
-
* rather than introduce a new startup failure mode over it.
|
|
56
|
-
*/
|
|
57
|
-
async function createTsconfigPathsPlugin(root) {
|
|
58
|
-
try {
|
|
59
|
-
const ts = await resolveFromWorkspace(root, "typescript");
|
|
60
|
-
const configPath = join(root, "tsconfig.json");
|
|
61
|
-
const configFile = ts.readConfigFile(configPath, (file) => ts.sys.readFile(file));
|
|
62
|
-
if (configFile.error) return null;
|
|
63
|
-
const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, root);
|
|
64
|
-
const rawPaths = parsed.options.paths;
|
|
65
|
-
if (!rawPaths || Object.keys(rawPaths).length === 0) return null;
|
|
66
|
-
const baseUrl = parsed.options.baseUrl ? resolve(root, parsed.options.baseUrl) : root;
|
|
67
|
-
const patterns = Object.entries(rawPaths).map(([key, targets]) => toPattern(key, targets, baseUrl)).sort((a, b) => b.prefix.length - a.prefix.length);
|
|
68
|
-
return {
|
|
69
|
-
name: "template-kit:tsconfig-paths",
|
|
70
|
-
enforce: "pre",
|
|
71
|
-
resolveId(source) {
|
|
72
|
-
if (source.startsWith(".") || isAbsolute(source)) return null;
|
|
73
|
-
for (const pattern of patterns) {
|
|
74
|
-
const matched = matchPattern(pattern, source);
|
|
75
|
-
if (matched === void 0) continue;
|
|
76
|
-
for (const target of pattern.targets) {
|
|
77
|
-
const resolved = resolveCandidate(pattern.hasWildcard ? target.replace("*", () => matched) : target);
|
|
78
|
-
if (resolved) return resolved;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
} catch {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
//#endregion
|
|
90
|
-
export { createTsconfigPathsPlugin };
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { resolveFromWorkspace } from "../check/resolve-tool.js";
|
|
2
|
-
import { islandTransformPlugin } from "./island-transform.js";
|
|
3
|
-
import { createTsconfigPathsPlugin } from "./tsconfig-paths-plugin.js";
|
|
4
|
-
|
|
5
|
-
//#region src/cli/dev/vite-server.ts
|
|
6
|
-
async function createDevViteServer(root, opts = {}) {
|
|
7
|
-
const vite = await resolveFromWorkspace(root, "vite");
|
|
8
|
-
const reactPlugin = await resolveFromWorkspace(root, "@vitejs/plugin-react");
|
|
9
|
-
const tsconfigPathsPlugin = await createTsconfigPathsPlugin(root);
|
|
10
|
-
return vite.createServer({
|
|
11
|
-
root,
|
|
12
|
-
appType: "custom",
|
|
13
|
-
configFile: false,
|
|
14
|
-
plugins: [
|
|
15
|
-
reactPlugin.default(),
|
|
16
|
-
islandTransformPlugin({ root }),
|
|
17
|
-
...tsconfigPathsPlugin ? [tsconfigPathsPlugin] : []
|
|
18
|
-
],
|
|
19
|
-
server: {
|
|
20
|
-
middlewareMode: true,
|
|
21
|
-
fs: { allow: [root] },
|
|
22
|
-
...opts.hmrPort !== void 0 ? { hmr: { port: opts.hmrPort } } : {}
|
|
23
|
-
},
|
|
24
|
-
ssr: { external: [
|
|
25
|
-
"react",
|
|
26
|
-
"react-dom",
|
|
27
|
-
"react-dom/server",
|
|
28
|
-
"react-dom/client"
|
|
29
|
-
] },
|
|
30
|
-
optimizeDeps: { include: [
|
|
31
|
-
"react",
|
|
32
|
-
"react-dom",
|
|
33
|
-
"react-dom/client"
|
|
34
|
-
] }
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
//#endregion
|
|
39
|
-
export { createDevViteServer };
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
-
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
|
|
5
|
-
//#region src/cli/dev/workspace.ts
|
|
6
|
-
/** Walk up from `cwd` to the nearest ancestor that contains a `templates/` dir. */
|
|
7
|
-
function findWorkspaceRoot(cwd) {
|
|
8
|
-
let dir = resolve(cwd);
|
|
9
|
-
for (;;) {
|
|
10
|
-
if (existsSync(join(dir, "templates"))) return dir;
|
|
11
|
-
const parent = dirname(dir);
|
|
12
|
-
if (parent === dir) return null;
|
|
13
|
-
dir = parent;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Resolve the workspace at (or above) `cwd`: its root, every template under
|
|
18
|
-
* `templates/`, and the template inferred from the cwd. Throws if no workspace
|
|
19
|
-
* is found.
|
|
20
|
-
*/
|
|
21
|
-
async function resolveWorkspace(cwd) {
|
|
22
|
-
const root = findWorkspaceRoot(cwd);
|
|
23
|
-
if (!root) throw new Error(`no templates/ directory found in ${cwd} or any parent — run this from a workspace`);
|
|
24
|
-
const templatesDir = join(root, "templates");
|
|
25
|
-
const entries = await readdir(templatesDir, { withFileTypes: true });
|
|
26
|
-
const templates = [];
|
|
27
|
-
for (const entry of entries) {
|
|
28
|
-
if (!entry.isDirectory()) continue;
|
|
29
|
-
const dir = join(templatesDir, entry.name);
|
|
30
|
-
const manifestPath = join(dir, "manifest.json");
|
|
31
|
-
if (!existsSync(manifestPath)) continue;
|
|
32
|
-
templates.push({
|
|
33
|
-
key: await readTemplateKey(manifestPath, entry.name),
|
|
34
|
-
dir,
|
|
35
|
-
manifestPath,
|
|
36
|
-
themePath: existsSync(join(dir, "theme.ts")) ? join(dir, "theme.ts") : null
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
templates.sort((a, b) => a.key.localeCompare(b.key));
|
|
40
|
-
return {
|
|
41
|
-
root,
|
|
42
|
-
templatesDir,
|
|
43
|
-
templates,
|
|
44
|
-
cwdTemplate: inferTemplateFromCwd(templatesDir, cwd, templates)
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
async function readTemplateKey(manifestPath, fallback) {
|
|
48
|
-
try {
|
|
49
|
-
const raw = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
50
|
-
return typeof raw.key === "string" && raw.key.length > 0 ? raw.key : fallback;
|
|
51
|
-
} catch {
|
|
52
|
-
return fallback;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
function inferTemplateFromCwd(templatesDir, cwd, templates) {
|
|
56
|
-
const rel = relative(templatesDir, resolve(cwd));
|
|
57
|
-
if (rel === "" || rel.startsWith("..")) return null;
|
|
58
|
-
const folder = rel.split(sep)[0];
|
|
59
|
-
return templates.find((t) => basename(t.dir) === folder)?.key ?? null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
//#endregion
|
|
63
|
-
export { findWorkspaceRoot, resolveWorkspace };
|
package/dist/cli/link/index.js
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { KIT_PKG_SUBPATH, findKitRoot, overlayKit, readLinkMarker } from "./overlay.js";
|
|
2
|
-
import { findWorkspaceRoot } from "../check/workspace.js";
|
|
3
|
-
import { parseArgs, promisify } from "node:util";
|
|
4
|
-
import { readFile } from "node:fs/promises";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { execFile } from "node:child_process";
|
|
7
|
-
|
|
8
|
-
//#region src/cli/link/index.ts
|
|
9
|
-
const execFileAsync = promisify(execFile);
|
|
10
|
-
const defaultBuild = async (kitRoot) => {
|
|
11
|
-
await execFileAsync("npm", ["run", "build"], { cwd: kitRoot });
|
|
12
|
-
};
|
|
13
|
-
const defaultNpmCi = async (workspaceRoot) => {
|
|
14
|
-
await execFileAsync("npm", ["ci"], { cwd: workspaceRoot });
|
|
15
|
-
};
|
|
16
|
-
async function resolveWorkspace(argv, cwd) {
|
|
17
|
-
const { positionals } = parseArgs({
|
|
18
|
-
args: argv,
|
|
19
|
-
allowPositionals: true
|
|
20
|
-
});
|
|
21
|
-
return positionals[0] !== void 0 ? positionals[0] : findWorkspaceRoot(cwd);
|
|
22
|
-
}
|
|
23
|
-
async function installedKitVersion(workspaceRoot) {
|
|
24
|
-
try {
|
|
25
|
-
const raw = await readFile(join(workspaceRoot, KIT_PKG_SUBPATH, "package.json"), "utf8");
|
|
26
|
-
return JSON.parse(raw).version ?? "unknown";
|
|
27
|
-
} catch {
|
|
28
|
-
return "not installed";
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
async function runLinkOnce(argv, cwd, build = defaultBuild, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
32
|
-
const workspaceRoot = await resolveWorkspace(argv, cwd);
|
|
33
|
-
const kitRoot = await findKitRoot(cwd);
|
|
34
|
-
return {
|
|
35
|
-
code: 0,
|
|
36
|
-
output: `template-kit link\n overlaid kit ${(await overlayKit(kitRoot, workspaceRoot, build, now)).kitVersion} from ${kitRoot}\n into ${workspaceRoot}`,
|
|
37
|
-
watch: {
|
|
38
|
-
kitSrcDir: join(kitRoot, "src"),
|
|
39
|
-
kitRoot,
|
|
40
|
-
workspaceRoot
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
async function runStatus(argv, cwd) {
|
|
45
|
-
const workspaceRoot = await resolveWorkspace(argv, cwd);
|
|
46
|
-
const marker = await readLinkMarker(workspaceRoot);
|
|
47
|
-
if (marker !== void 0) return {
|
|
48
|
-
code: 0,
|
|
49
|
-
output: `template-kit status\n overlay — linked from ${marker.linkedFrom} (kit ${marker.kitVersion})`
|
|
50
|
-
};
|
|
51
|
-
return {
|
|
52
|
-
code: 0,
|
|
53
|
-
output: `template-kit status\n registry — kit ${await installedKitVersion(workspaceRoot)}`
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
async function runUnlink(argv, cwd, npmCi = defaultNpmCi) {
|
|
57
|
-
const workspaceRoot = await resolveWorkspace(argv, cwd);
|
|
58
|
-
if (await readLinkMarker(workspaceRoot) === void 0) return {
|
|
59
|
-
code: 0,
|
|
60
|
-
output: "template-kit unlink\n not linked — nothing to do."
|
|
61
|
-
};
|
|
62
|
-
await npmCi(workspaceRoot);
|
|
63
|
-
return {
|
|
64
|
-
code: 0,
|
|
65
|
-
output: "template-kit unlink\n restored the registry install (npm ci)."
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
/** One rebuild+recopy for the watch loop — a real tsdown build, then the overlay copy. */
|
|
69
|
-
async function rebuildOverlay(kitRoot, workspaceRoot) {
|
|
70
|
-
await overlayKit(kitRoot, workspaceRoot, defaultBuild, (/* @__PURE__ */ new Date()).toISOString());
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
//#endregion
|
|
74
|
-
export { rebuildOverlay, runLinkOnce, runStatus, runUnlink };
|
package/dist/cli/link/overlay.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { KIT_VERSION } from "../../index.js";
|
|
2
|
-
import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
|
|
5
|
-
//#region src/cli/link/overlay.ts
|
|
6
|
-
const KIT_PKG_SUBPATH = join("node_modules", "@homepages", "template-kit");
|
|
7
|
-
const LINK_MARKER_NAME = ".template-kit-link.json";
|
|
8
|
-
const PUBLISHABLE = [
|
|
9
|
-
"dist",
|
|
10
|
-
"guide",
|
|
11
|
-
"package.json",
|
|
12
|
-
"tsconfig.json",
|
|
13
|
-
"tsconfig.base.json",
|
|
14
|
-
"CHANGELOG.md"
|
|
15
|
-
];
|
|
16
|
-
async function readLinkMarker(workspaceRoot) {
|
|
17
|
-
try {
|
|
18
|
-
const raw = await readFile(join(workspaceRoot, KIT_PKG_SUBPATH, LINK_MARKER_NAME), "utf8");
|
|
19
|
-
return JSON.parse(raw);
|
|
20
|
-
} catch {
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
const isPackageManifest = (value) => typeof value === "object" && value !== null;
|
|
25
|
-
async function findKitRoot(from) {
|
|
26
|
-
let dir = resolve(from);
|
|
27
|
-
for (;;) {
|
|
28
|
-
try {
|
|
29
|
-
const raw = await readFile(join(dir, "package.json"), "utf8");
|
|
30
|
-
const parsed = JSON.parse(raw);
|
|
31
|
-
if (isPackageManifest(parsed) && parsed.name === "@homepages/template-kit") return dir;
|
|
32
|
-
} catch {}
|
|
33
|
-
const parent = dirname(dir);
|
|
34
|
-
if (parent === dir) throw new Error("not inside the template-kit repo — run `template-kit link` from a kit checkout.");
|
|
35
|
-
dir = parent;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
async function overlayKit(kitRoot, workspaceRoot, build, now) {
|
|
39
|
-
await build(kitRoot);
|
|
40
|
-
const dest = join(workspaceRoot, KIT_PKG_SUBPATH);
|
|
41
|
-
await rm(dest, {
|
|
42
|
-
recursive: true,
|
|
43
|
-
force: true
|
|
44
|
-
});
|
|
45
|
-
await mkdir(dest, { recursive: true });
|
|
46
|
-
for (const item of PUBLISHABLE) await cp(join(kitRoot, item), join(dest, item), { recursive: true }).catch((cause) => {
|
|
47
|
-
if (cause.code !== "ENOENT") throw cause;
|
|
48
|
-
});
|
|
49
|
-
const marker = {
|
|
50
|
-
linkedFrom: kitRoot,
|
|
51
|
-
kitVersion: KIT_VERSION,
|
|
52
|
-
linkedAt: now
|
|
53
|
-
};
|
|
54
|
-
await writeFile(join(dest, LINK_MARKER_NAME), `${JSON.stringify(marker, null, 2)}\n`);
|
|
55
|
-
return marker;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
//#endregion
|
|
59
|
-
export { KIT_PKG_SUBPATH, LINK_MARKER_NAME, PUBLISHABLE, findKitRoot, overlayKit, readLinkMarker };
|
package/dist/cli/link/watch.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { watch } from "node:fs";
|
|
2
|
-
|
|
3
|
-
//#region src/cli/link/watch.ts
|
|
4
|
-
function watchAndRebuild(kitSrcDir, onChange, onError, debounceMs = 150) {
|
|
5
|
-
let timer;
|
|
6
|
-
const watcher = watch(kitSrcDir, { recursive: true }, () => {
|
|
7
|
-
if (timer !== void 0) clearTimeout(timer);
|
|
8
|
-
timer = setTimeout(() => {
|
|
9
|
-
timer = void 0;
|
|
10
|
-
onChange();
|
|
11
|
-
}, debounceMs);
|
|
12
|
-
});
|
|
13
|
-
const stop = () => {
|
|
14
|
-
if (timer !== void 0) clearTimeout(timer);
|
|
15
|
-
watcher.close();
|
|
16
|
-
};
|
|
17
|
-
watcher.on("error", (err) => {
|
|
18
|
-
stop();
|
|
19
|
-
onError(err);
|
|
20
|
-
});
|
|
21
|
-
return stop;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
//#endregion
|
|
25
|
-
export { watchAndRebuild };
|
package/dist/cli/new/emit.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { join, relative } from "node:path";
|
|
3
|
-
|
|
4
|
-
//#region src/cli/new/emit.ts
|
|
5
|
-
const RENAME = {
|
|
6
|
-
"_gitignore": ".gitignore",
|
|
7
|
-
"_package.json": "package.json",
|
|
8
|
-
"_manifest.json": "manifest.json",
|
|
9
|
-
"_Renderer.tsx": "Renderer.tsx",
|
|
10
|
-
"_schema.ts": "schema.ts",
|
|
11
|
-
"_fill-spec.ts": "fill-spec.ts",
|
|
12
|
-
"_fixtures.ts": "fixtures.ts"
|
|
13
|
-
};
|
|
14
|
-
function substitute(body, tokens) {
|
|
15
|
-
return body.replace(/\{\{(\w+)\}\}/g, (_m, key) => {
|
|
16
|
-
const value = tokens[key];
|
|
17
|
-
if (value === void 0) throw new Error(`scaffold: no value for token {{${key}}}`);
|
|
18
|
-
return value;
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
async function walk(dir) {
|
|
22
|
-
const out = [];
|
|
23
|
-
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
24
|
-
const full = join(dir, entry.name);
|
|
25
|
-
if (entry.isDirectory()) out.push(...await walk(full));
|
|
26
|
-
else out.push(full);
|
|
27
|
-
}
|
|
28
|
-
return out;
|
|
29
|
-
}
|
|
30
|
-
/** Emit `srcDir` into `destDir`; returns dest-relative paths written. */
|
|
31
|
-
async function emitTree(srcDir, destDir, tokens) {
|
|
32
|
-
const files = await walk(srcDir);
|
|
33
|
-
const written = [];
|
|
34
|
-
for (const file of files) {
|
|
35
|
-
const parts = relative(srcDir, file).split("/");
|
|
36
|
-
const base = parts[parts.length - 1];
|
|
37
|
-
parts[parts.length - 1] = RENAME[base] ?? base;
|
|
38
|
-
const destRel = parts.join("/");
|
|
39
|
-
const destPath = join(destDir, destRel);
|
|
40
|
-
await mkdir(join(destPath, ".."), { recursive: true });
|
|
41
|
-
await writeFile(destPath, substitute(await readFile(file, "utf8"), tokens), "utf8");
|
|
42
|
-
written.push(destRel);
|
|
43
|
-
}
|
|
44
|
-
return written;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
//#endregion
|
|
48
|
-
export { emitTree };
|
package/dist/cli/new/index.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { findWorkspaceRoot } from "../check/workspace.js";
|
|
2
|
-
import { emitTree } from "./emit.js";
|
|
3
|
-
import { sectionAssetsDir, templateAssetsDir } from "./scaffold-assets.js";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { existsSync } from "node:fs";
|
|
6
|
-
|
|
7
|
-
//#region src/cli/new/index.ts
|
|
8
|
-
const KEY_RE = /^[a-z][a-z0-9-]*$/;
|
|
9
|
-
function pascal(key) {
|
|
10
|
-
return key.split("-").map((p) => p ? p[0].toUpperCase() + p.slice(1) : p).join("");
|
|
11
|
-
}
|
|
12
|
-
function titleCase(key) {
|
|
13
|
-
return key.split("-").map((p) => p ? p[0].toUpperCase() + p.slice(1) : p).join(" ");
|
|
14
|
-
}
|
|
15
|
-
async function runNew(argv, cwd) {
|
|
16
|
-
const name = argv[0];
|
|
17
|
-
if (!name || !KEY_RE.test(name)) return {
|
|
18
|
-
code: 1,
|
|
19
|
-
output: `new: expected a kebab-case template name (${KEY_RE.source}). Usage: template-kit new <template>`
|
|
20
|
-
};
|
|
21
|
-
const dest = join(await findWorkspaceRoot(cwd), "templates", name);
|
|
22
|
-
if (existsSync(dest)) return {
|
|
23
|
-
code: 1,
|
|
24
|
-
output: `new: templates/${name}/ already exists. Refusing to overwrite.`
|
|
25
|
-
};
|
|
26
|
-
await emitTree(templateAssetsDir(), dest, {
|
|
27
|
-
TEMPLATE_KEY: name,
|
|
28
|
-
TEMPLATE_NAME: titleCase(name)
|
|
29
|
-
});
|
|
30
|
-
return {
|
|
31
|
-
code: 0,
|
|
32
|
-
output: `Created templates/${name}/ (starter template)\n Next: npm run check -- ${name} then npm run dev -- ${name}`
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
async function runNewSection(argv, cwd) {
|
|
36
|
-
const arg = argv[0] ?? "";
|
|
37
|
-
const slash = arg.indexOf("/");
|
|
38
|
-
if (slash < 0) return {
|
|
39
|
-
code: 1,
|
|
40
|
-
output: `new-section: expected <template>/<section>. e.g. template-kit new-section starter/gallery`
|
|
41
|
-
};
|
|
42
|
-
const template = arg.slice(0, slash);
|
|
43
|
-
const section = arg.slice(slash + 1);
|
|
44
|
-
if (!KEY_RE.test(template) || !KEY_RE.test(section)) return {
|
|
45
|
-
code: 1,
|
|
46
|
-
output: `new-section: template and section must be kebab-case (${KEY_RE.source}).`
|
|
47
|
-
};
|
|
48
|
-
const templateDir = join(await findWorkspaceRoot(cwd), "templates", template);
|
|
49
|
-
if (!existsSync(templateDir)) return {
|
|
50
|
-
code: 1,
|
|
51
|
-
output: `new-section: templates/${template}/ does not exist. Create it with: template-kit new ${template}`
|
|
52
|
-
};
|
|
53
|
-
const dest = join(templateDir, "sections", section);
|
|
54
|
-
if (existsSync(dest)) return {
|
|
55
|
-
code: 1,
|
|
56
|
-
output: `new-section: ${dest} already exists. Refusing to overwrite.`
|
|
57
|
-
};
|
|
58
|
-
await emitTree(sectionAssetsDir(), dest, {
|
|
59
|
-
SECTION_PASCAL: pascal(section),
|
|
60
|
-
SECTION_DISPLAY: titleCase(section)
|
|
61
|
-
});
|
|
62
|
-
return {
|
|
63
|
-
code: 0,
|
|
64
|
-
output: `Created templates/${template}/sections/${section}/ (Renderer.tsx, schema.ts, fill-spec.ts, fixtures.ts)\n Reference it as "${section}" in templates/${template}/manifest.json, then npm run check -- ${template}`
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
//#endregion
|
|
69
|
-
export { runNew, runNewSection };
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
import { Section, Slot } from "@homepages/template-kit";
|
|
4
|
-
|
|
5
|
-
import type { Props } from "./schema";
|
|
6
|
-
|
|
7
|
-
// {{SECTION_DISPLAY}} — a pure, server-rendered function of its props. Wrap
|
|
8
|
-
// every editable slot in <Slot id="…"> so the editor can resolve clicks. For
|
|
9
|
-
// interactivity, add a "use client" island file beside this one.
|
|
10
|
-
export default function {{SECTION_PASCAL}}Renderer({ slots, nav }: Props): ReactNode {
|
|
11
|
-
return (
|
|
12
|
-
<Section id={nav.selfAnchor} className="bg-background text-ink">
|
|
13
|
-
<div className="mx-auto max-w-3xl px-6 py-12">
|
|
14
|
-
<Slot id="headline" textLeaf>
|
|
15
|
-
<h2 className="text-2xl font-semibold">{slots.headline}</h2>
|
|
16
|
-
</Slot>
|
|
17
|
-
</div>
|
|
18
|
-
</Section>
|
|
19
|
-
);
|
|
20
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import type { FillSpecShape } from "@homepages/template-kit";
|
|
2
|
-
|
|
3
|
-
// Decision ids must match the `produced_by` references in schema.ts.
|
|
4
|
-
export const fillSpec = {
|
|
5
|
-
reads: [],
|
|
6
|
-
decisions: [
|
|
7
|
-
{
|
|
8
|
-
id: "headline_text",
|
|
9
|
-
type: "text-block",
|
|
10
|
-
produces: ["headline"],
|
|
11
|
-
length_cap: 80,
|
|
12
|
-
structure: "single_sentence",
|
|
13
|
-
perspective: "third",
|
|
14
|
-
voice_prompt: "Write a short, confident heading for this section.",
|
|
15
|
-
content_prompt: "Anchor the section's purpose in one sentence.",
|
|
16
|
-
},
|
|
17
|
-
],
|
|
18
|
-
} as const satisfies FillSpecShape;
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { FixtureModule } from "@homepages/template-kit";
|
|
2
|
-
|
|
3
|
-
// Base content only. Nullability coverage is derived from schema.ts. Add a
|
|
4
|
-
// `states` entry for a designed content-length or cardinality branch.
|
|
5
|
-
const fixtures: FixtureModule = {
|
|
6
|
-
base: {
|
|
7
|
-
slots: {
|
|
8
|
-
headline: "A typical headline that reads well in a short line",
|
|
9
|
-
},
|
|
10
|
-
},
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export default fixtures;
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { SectionProps, SectionSchemaShape } from "@homepages/template-kit";
|
|
2
|
-
|
|
3
|
-
// Replace the slots/options with the real shape this section needs. Every
|
|
4
|
-
// editable slot declares either a `source` (baked from the property graph via
|
|
5
|
-
// `direct(...)`/`derived(...)`) or a `produced_by` naming a decision in
|
|
6
|
-
// fill-spec.ts.
|
|
7
|
-
export const schema = {
|
|
8
|
-
meta: {
|
|
9
|
-
displayName: "{{SECTION_DISPLAY}}",
|
|
10
|
-
},
|
|
11
|
-
slots: {
|
|
12
|
-
headline: {
|
|
13
|
-
type: "text",
|
|
14
|
-
size: "medium",
|
|
15
|
-
editable_by_user: true,
|
|
16
|
-
required: true,
|
|
17
|
-
produced_by: "headline_text",
|
|
18
|
-
display_name: "Headline",
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
} as const satisfies SectionSchemaShape;
|
|
22
|
-
|
|
23
|
-
// Renderer prop type — derived from `schema`; do not hand-write it.
|
|
24
|
-
export type Props = SectionProps<typeof schema>;
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"key": "{{TEMPLATE_KEY}}",
|
|
3
|
-
"name": "{{TEMPLATE_NAME}}",
|
|
4
|
-
"deliverable_type": "single_property_website",
|
|
5
|
-
"sections": [
|
|
6
|
-
{ "section": "hero", "instance_id": "hero-1", "options": {}, "locked": [], "required": true, "reorderable": false }
|
|
7
|
-
],
|
|
8
|
-
"reconcile": []
|
|
9
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { type ReactNode, useState } from "react";
|
|
4
|
-
|
|
5
|
-
import type { IslandEditorOptions } from "@homepages/template-kit";
|
|
6
|
-
|
|
7
|
-
// A "use client" island: interactive, hydrated in the browser as its own React
|
|
8
|
-
// root. State, effects, and event handlers are fine here (they are NOT in a
|
|
9
|
-
// Renderer). Its props cross the server→browser boundary as JSON, so they must
|
|
10
|
-
// be strictly serializable — here, two strings and a number.
|
|
11
|
-
export default function ExpandableText({
|
|
12
|
-
text,
|
|
13
|
-
collapsedChars,
|
|
14
|
-
moreLabel,
|
|
15
|
-
}: {
|
|
16
|
-
text: string;
|
|
17
|
-
collapsedChars: number;
|
|
18
|
-
moreLabel: string;
|
|
19
|
-
}): ReactNode {
|
|
20
|
-
const [open, setOpen] = useState(false);
|
|
21
|
-
const isLong = text.length > collapsedChars;
|
|
22
|
-
const shown = open || !isLong ? text : `${text.slice(0, collapsedChars).trimEnd()}…`;
|
|
23
|
-
return (
|
|
24
|
-
<p className="text-base leading-relaxed text-ink/80">
|
|
25
|
-
{shown}
|
|
26
|
-
{isLong ? (
|
|
27
|
-
<button
|
|
28
|
-
type="button"
|
|
29
|
-
data-testid="expand-toggle"
|
|
30
|
-
className="ml-2 font-medium text-accent underline"
|
|
31
|
-
onClick={() => setOpen((v) => !v)}
|
|
32
|
-
>
|
|
33
|
-
{open ? "Show less" : moreLabel}
|
|
34
|
-
</button>
|
|
35
|
-
) : null}
|
|
36
|
-
</p>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Optional: how this island behaves inside the editor canvas. Omit `editor`
|
|
41
|
-
// entirely and the island renders as static markup in the editor (right for
|
|
42
|
-
// most). Here we hydrate it live so the toggle works while editing.
|
|
43
|
-
export const editor: IslandEditorOptions = {
|
|
44
|
-
live: true,
|
|
45
|
-
};
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
import { Image, Section, Slot } from "@homepages/template-kit";
|
|
4
|
-
|
|
5
|
-
import { Feature } from "./components/Feature";
|
|
6
|
-
import ExpandableText from "./ExpandableText";
|
|
7
|
-
import type { Props } from "./schema";
|
|
8
|
-
|
|
9
|
-
// Hero — a pure server-rendered function of its props: no fetches, no globals,
|
|
10
|
-
// no state. Every editable slot's outer DOM is wrapped in <Slot id="…"> (or a
|
|
11
|
-
// primitive with slotId="…") so the editor can map a click back to the slot.
|
|
12
|
-
// The one interactive part is the <ExpandableText> island.
|
|
13
|
-
export default function HeroRenderer({ slots, options, nav }: Props): ReactNode {
|
|
14
|
-
const align = options.align === "left" ? "items-start text-left" : "items-center text-center";
|
|
15
|
-
return (
|
|
16
|
-
<Section id={nav.selfAnchor} className="bg-background text-ink">
|
|
17
|
-
<Image
|
|
18
|
-
slotId="hero_image"
|
|
19
|
-
src={slots.hero_image.url}
|
|
20
|
-
responsive={slots.hero_image.responsive}
|
|
21
|
-
alt={slots.hero_image.alt}
|
|
22
|
-
loading="eager"
|
|
23
|
-
className="w-full"
|
|
24
|
-
/>
|
|
25
|
-
<div className={`mx-auto flex max-w-3xl flex-col gap-4 px-6 py-12 ${align}`}>
|
|
26
|
-
<Slot id="headline" textLeaf>
|
|
27
|
-
<h1 className="text-3xl font-semibold">{slots.headline}</h1>
|
|
28
|
-
</Slot>
|
|
29
|
-
<Slot id="blurb">
|
|
30
|
-
{/* The island receives JSON-serializable props only. */}
|
|
31
|
-
<ExpandableText text={slots.blurb} collapsedChars={140} moreLabel="Read more" />
|
|
32
|
-
</Slot>
|
|
33
|
-
<div className="mt-2 grid w-full grid-cols-2 gap-3">
|
|
34
|
-
<Feature label="Status" value="For sale" />
|
|
35
|
-
<Feature label="Type" value="Single-family" />
|
|
36
|
-
</div>
|
|
37
|
-
</div>
|
|
38
|
-
</Section>
|
|
39
|
-
);
|
|
40
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
// A nested server component. Proof that a section folder is free-form: only the
|
|
4
|
-
// four contract files (Renderer/schema/fill-spec/fixtures) are reserved — a
|
|
5
|
-
// components/ dir (or hooks/, utils/, any nesting) is yours to organize. This
|
|
6
|
-
// renders as static HTML and ships zero JavaScript.
|
|
7
|
-
export function Feature({ label, value }: { label: string; value: string }): ReactNode {
|
|
8
|
-
return (
|
|
9
|
-
<div className="rounded-lg border border-ink/10 p-4">
|
|
10
|
-
<div className="text-sm text-ink/60">{label}</div>
|
|
11
|
-
<div className="text-lg font-semibold text-ink">{value}</div>
|
|
12
|
-
</div>
|
|
13
|
-
);
|
|
14
|
-
}
|