kitfly 0.1.2
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 +60 -0
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/VERSION +1 -0
- package/package.json +63 -0
- package/schemas/README.md +32 -0
- package/schemas/site.schema.json +5 -0
- package/schemas/theme.schema.json +5 -0
- package/schemas/v0/site.schema.json +172 -0
- package/schemas/v0/theme.schema.json +210 -0
- package/scripts/build-all.ts +121 -0
- package/scripts/build.ts +601 -0
- package/scripts/bundle.ts +781 -0
- package/scripts/dev.ts +777 -0
- package/scripts/generate-checksums.sh +78 -0
- package/scripts/release/export-release-key.sh +28 -0
- package/scripts/release/release-guard-tag-version.sh +79 -0
- package/scripts/release/sign-release-assets.sh +123 -0
- package/scripts/release/upload-release-assets.sh +76 -0
- package/scripts/release/upload-release-provenance.sh +52 -0
- package/scripts/release/verify-public-key.sh +48 -0
- package/scripts/release/verify-signatures.sh +117 -0
- package/scripts/version-sync.ts +82 -0
- package/src/__tests__/build.test.ts +240 -0
- package/src/__tests__/bundle.test.ts +786 -0
- package/src/__tests__/cli.test.ts +706 -0
- package/src/__tests__/crucible.test.ts +1043 -0
- package/src/__tests__/engine.test.ts +157 -0
- package/src/__tests__/init.test.ts +450 -0
- package/src/__tests__/pipeline.test.ts +1087 -0
- package/src/__tests__/productbook.test.ts +1206 -0
- package/src/__tests__/runbook.test.ts +974 -0
- package/src/__tests__/server-registry.test.ts +1251 -0
- package/src/__tests__/servicebook.test.ts +1248 -0
- package/src/__tests__/shared.test.ts +2005 -0
- package/src/__tests__/styles.test.ts +14 -0
- package/src/__tests__/theme-schema.test.ts +47 -0
- package/src/__tests__/theme.test.ts +554 -0
- package/src/cli.ts +582 -0
- package/src/commands/init.ts +92 -0
- package/src/commands/update.ts +444 -0
- package/src/engine.ts +20 -0
- package/src/logger.ts +15 -0
- package/src/migrations/0000_schema_versioning.ts +67 -0
- package/src/migrations/0001_server_port.ts +52 -0
- package/src/migrations/0002_brand_logo.ts +49 -0
- package/src/migrations/index.ts +26 -0
- package/src/migrations/schema.ts +24 -0
- package/src/server-registry.ts +405 -0
- package/src/shared.ts +1239 -0
- package/src/site/styles.css +931 -0
- package/src/site/template.html +193 -0
- package/src/templates/crucible.ts +1163 -0
- package/src/templates/driver.ts +876 -0
- package/src/templates/handbook.ts +339 -0
- package/src/templates/minimal.ts +139 -0
- package/src/templates/pipeline.ts +966 -0
- package/src/templates/productbook.ts +1032 -0
- package/src/templates/runbook.ts +829 -0
- package/src/templates/schema.ts +119 -0
- package/src/templates/servicebook.ts +1242 -0
- package/src/theme.ts +245 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for engine.ts path constants and helper functions
|
|
3
|
+
*
|
|
4
|
+
* Tests the path constants (ENGINE_ROOT, ENGINE_SITE_DIR, ENGINE_ASSETS_DIR)
|
|
5
|
+
* and the siteOverridePath helper function.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
import {
|
|
11
|
+
ENGINE_ASSETS_DIR,
|
|
12
|
+
ENGINE_ROOT,
|
|
13
|
+
ENGINE_SITE_DIR,
|
|
14
|
+
SITE_OVERRIDE_DIRNAME,
|
|
15
|
+
siteOverridePath,
|
|
16
|
+
} from "../engine";
|
|
17
|
+
|
|
18
|
+
describe("engine.ts", () => {
|
|
19
|
+
describe("ENGINE_ROOT constant", () => {
|
|
20
|
+
it("should be an absolute path", () => {
|
|
21
|
+
expect(ENGINE_ROOT).toBeTruthy();
|
|
22
|
+
expect(ENGINE_ROOT).toBe(resolve(ENGINE_ROOT));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should end with 'kitfly' (the project root)", () => {
|
|
26
|
+
expect(ENGINE_ROOT).toMatch(/kitfly$/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should align with ENGINE_SITE_DIR", () => {
|
|
30
|
+
// ENGINE_SITE_DIR is derived from ENGINE_ROOT
|
|
31
|
+
expect(ENGINE_SITE_DIR).toBe(join(ENGINE_ROOT, "src/site"));
|
|
32
|
+
expect(dirname(ENGINE_SITE_DIR)).toBe(join(ENGINE_ROOT, "src"));
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("ENGINE_SITE_DIR constant", () => {
|
|
37
|
+
it("should be an absolute path", () => {
|
|
38
|
+
expect(ENGINE_SITE_DIR).toBeTruthy();
|
|
39
|
+
expect(ENGINE_SITE_DIR).toBe(resolve(ENGINE_SITE_DIR));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should be under ENGINE_ROOT", () => {
|
|
43
|
+
expect(ENGINE_SITE_DIR).toContain(ENGINE_ROOT);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should end with 'src/site'", () => {
|
|
47
|
+
expect(ENGINE_SITE_DIR).toMatch(/src[/\\]site$/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should equal ENGINE_ROOT joined with 'src/site'", () => {
|
|
51
|
+
expect(ENGINE_SITE_DIR).toBe(join(ENGINE_ROOT, "src/site"));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("ENGINE_ASSETS_DIR constant", () => {
|
|
56
|
+
it("should be an absolute path", () => {
|
|
57
|
+
expect(ENGINE_ASSETS_DIR).toBeTruthy();
|
|
58
|
+
expect(ENGINE_ASSETS_DIR).toBe(resolve(ENGINE_ASSETS_DIR));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should be under ENGINE_ROOT", () => {
|
|
62
|
+
expect(ENGINE_ASSETS_DIR).toContain(ENGINE_ROOT);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should end with 'assets'", () => {
|
|
66
|
+
expect(ENGINE_ASSETS_DIR).toMatch(/assets$/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should equal ENGINE_ROOT joined with 'assets'", () => {
|
|
70
|
+
expect(ENGINE_ASSETS_DIR).toBe(join(ENGINE_ROOT, "assets"));
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("SITE_OVERRIDE_DIRNAME constant", () => {
|
|
75
|
+
it("should equal 'kitfly'", () => {
|
|
76
|
+
expect(SITE_OVERRIDE_DIRNAME).toBe("kitfly");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("siteOverridePath function", () => {
|
|
81
|
+
it("should return a path combining siteRoot, SITE_OVERRIDE_DIRNAME, and relPathFromKitflyDir", () => {
|
|
82
|
+
const siteRoot = "/home/user/my-site";
|
|
83
|
+
const relPath = "config.yaml";
|
|
84
|
+
|
|
85
|
+
const result = siteOverridePath(siteRoot, relPath);
|
|
86
|
+
|
|
87
|
+
expect(result).toBe(join(siteRoot, "kitfly", "config.yaml"));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should work with nested relative paths", () => {
|
|
91
|
+
const siteRoot = "/home/user/my-site";
|
|
92
|
+
const relPath = "templates/layout.html";
|
|
93
|
+
|
|
94
|
+
const result = siteOverridePath(siteRoot, relPath);
|
|
95
|
+
|
|
96
|
+
expect(result).toBe(join(siteRoot, "kitfly", "templates", "layout.html"));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should handle absolute siteRoot paths", () => {
|
|
100
|
+
const siteRoot = "/var/www/site";
|
|
101
|
+
const relPath = "styles/main.css";
|
|
102
|
+
|
|
103
|
+
const result = siteOverridePath(siteRoot, relPath);
|
|
104
|
+
|
|
105
|
+
expect(result).toBe(join(siteRoot, "kitfly", "styles", "main.css"));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should handle relative siteRoot paths", () => {
|
|
109
|
+
const siteRoot = "./my-site";
|
|
110
|
+
const relPath = "theme.yaml";
|
|
111
|
+
|
|
112
|
+
const result = siteOverridePath(siteRoot, relPath);
|
|
113
|
+
|
|
114
|
+
// join() normalizes away a leading './'
|
|
115
|
+
expect(result).toBe(join(siteRoot, "kitfly", relPath));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should handle trailing slashes in siteRoot", () => {
|
|
119
|
+
const siteRoot = "/home/user/my-site/";
|
|
120
|
+
const relPath = "config.yaml";
|
|
121
|
+
|
|
122
|
+
const result = siteOverridePath(siteRoot, relPath);
|
|
123
|
+
|
|
124
|
+
expect(result).toContain(join("kitfly", "config.yaml"));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should handle relative paths with nested directories", () => {
|
|
128
|
+
const siteRoot = "~";
|
|
129
|
+
const relPath = "subdir/nested/file.md";
|
|
130
|
+
|
|
131
|
+
const result = siteOverridePath(siteRoot, relPath);
|
|
132
|
+
|
|
133
|
+
expect(result).toBe(join("~", "kitfly", "subdir", "nested", "file.md"));
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should handle empty relative path", () => {
|
|
137
|
+
const siteRoot = "/home/user/my-site";
|
|
138
|
+
const relPath = "";
|
|
139
|
+
|
|
140
|
+
const result = siteOverridePath(siteRoot, relPath);
|
|
141
|
+
|
|
142
|
+
expect(result).toBe(join(siteRoot, "kitfly"));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should use join() for path handling (respects OS path separators)", () => {
|
|
146
|
+
const siteRoot = "/path/to/site";
|
|
147
|
+
const relPath = "dir/file.txt";
|
|
148
|
+
|
|
149
|
+
const result = siteOverridePath(siteRoot, relPath);
|
|
150
|
+
|
|
151
|
+
// The result should use proper path separators for the OS
|
|
152
|
+
expect(result).toContain("kitfly");
|
|
153
|
+
expect(result).toContain("dir");
|
|
154
|
+
expect(result).toContain("file.txt");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the template initialization system
|
|
3
|
+
*
|
|
4
|
+
* Covers: src/commands/init.ts and src/templates/driver.ts
|
|
5
|
+
* Strategy: run templates against real temp directories, verify output files and content
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
13
|
+
import { init } from "../commands/init.ts";
|
|
14
|
+
import { defaultBranding, getTemplate, listTemplates, runTemplate } from "../templates/driver.ts";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Helpers
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
let tempDir: string;
|
|
21
|
+
let originalCwd: string;
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
originalCwd = process.cwd();
|
|
25
|
+
tempDir = await mkdtemp(join(tmpdir(), "kitfly-init-test-"));
|
|
26
|
+
process.chdir(tempDir);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(async () => {
|
|
30
|
+
process.chdir(originalCwd);
|
|
31
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/** Read a file from the generated project as utf-8 text */
|
|
35
|
+
async function readGenerated(projectName: string, relPath: string): Promise<string> {
|
|
36
|
+
return readFile(join(tempDir, projectName, relPath), "utf-8");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Check whether a path exists inside the generated project */
|
|
40
|
+
function generatedExists(projectName: string, relPath: string): boolean {
|
|
41
|
+
return existsSync(join(tempDir, projectName, relPath));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Template Registry
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
describe("template registry", () => {
|
|
49
|
+
it("lists all registered templates", () => {
|
|
50
|
+
const templates = listTemplates();
|
|
51
|
+
const ids = templates.map((t) => t.id);
|
|
52
|
+
|
|
53
|
+
expect(ids).toContain("minimal");
|
|
54
|
+
expect(ids).toContain("handbook");
|
|
55
|
+
expect(templates.length).toBeGreaterThanOrEqual(2);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("retrieves a template by id", () => {
|
|
59
|
+
const tpl = getTemplate("handbook");
|
|
60
|
+
expect(tpl).toBeDefined();
|
|
61
|
+
expect(tpl?.id).toBe("handbook");
|
|
62
|
+
expect(tpl?.extends).toBe("minimal");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns undefined for unknown template", () => {
|
|
66
|
+
expect(getTemplate("nonexistent")).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Minimal Template
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
describe("minimal template", () => {
|
|
75
|
+
const projectName = "test-minimal";
|
|
76
|
+
|
|
77
|
+
it("creates the expected file structure", async () => {
|
|
78
|
+
await runTemplate({
|
|
79
|
+
name: projectName,
|
|
80
|
+
template: "minimal",
|
|
81
|
+
git: false,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Core files from minimal template
|
|
85
|
+
expect(generatedExists(projectName, "site.yaml")).toBe(true);
|
|
86
|
+
expect(generatedExists(projectName, "index.md")).toBe(true);
|
|
87
|
+
expect(generatedExists(projectName, ".gitignore")).toBe(true);
|
|
88
|
+
expect(generatedExists(projectName, "README.md")).toBe(true);
|
|
89
|
+
|
|
90
|
+
// Section directories
|
|
91
|
+
expect(generatedExists(projectName, "content")).toBe(true);
|
|
92
|
+
expect(generatedExists(projectName, "assets/brand")).toBe(true);
|
|
93
|
+
|
|
94
|
+
// Gitkeep files to preserve empty dirs
|
|
95
|
+
expect(generatedExists(projectName, "content/.gitkeep")).toBe(true);
|
|
96
|
+
expect(generatedExists(projectName, "assets/brand/.gitkeep")).toBe(true);
|
|
97
|
+
|
|
98
|
+
// Manifest metadata
|
|
99
|
+
expect(generatedExists(projectName, ".kitfly/manifest.json")).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("generates site.yaml with correct branding defaults", async () => {
|
|
103
|
+
await runTemplate({
|
|
104
|
+
name: projectName,
|
|
105
|
+
template: "minimal",
|
|
106
|
+
git: false,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const siteYaml = await readGenerated(projectName, "site.yaml");
|
|
110
|
+
|
|
111
|
+
// Default branding derives title-case name from project name
|
|
112
|
+
expect(siteYaml).toContain('title: "Test Minimal"');
|
|
113
|
+
expect(siteYaml).toContain('name: "Test Minimal"');
|
|
114
|
+
expect(siteYaml).toContain('url: "/"');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("generates index.md with frontmatter", async () => {
|
|
118
|
+
await runTemplate({
|
|
119
|
+
name: projectName,
|
|
120
|
+
template: "minimal",
|
|
121
|
+
git: false,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const indexMd = await readGenerated(projectName, "index.md");
|
|
125
|
+
|
|
126
|
+
// Has YAML frontmatter
|
|
127
|
+
expect(indexMd).toMatch(/^---\n/);
|
|
128
|
+
expect(indexMd).toContain("title: Welcome");
|
|
129
|
+
expect(indexMd).toContain("description: Test Minimal documentation");
|
|
130
|
+
// Body content references the site
|
|
131
|
+
expect(indexMd).toContain("Welcome to Test Minimal");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("writes manifest with template metadata", async () => {
|
|
135
|
+
await runTemplate({
|
|
136
|
+
name: projectName,
|
|
137
|
+
template: "minimal",
|
|
138
|
+
git: false,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const manifestRaw = await readGenerated(projectName, ".kitfly/manifest.json");
|
|
142
|
+
const manifest = JSON.parse(manifestRaw);
|
|
143
|
+
|
|
144
|
+
expect(manifest.template).toBe("minimal");
|
|
145
|
+
expect(manifest.templateVersion).toBe(1);
|
|
146
|
+
expect(manifest.standalone).toBe(false);
|
|
147
|
+
expect(manifest.created).toBeTruthy();
|
|
148
|
+
expect(manifest.kitflyVersion).toBeTruthy();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("does not create .git directory when git: false", async () => {
|
|
152
|
+
await runTemplate({
|
|
153
|
+
name: projectName,
|
|
154
|
+
template: "minimal",
|
|
155
|
+
git: false,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(generatedExists(projectName, ".git")).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Handbook Template (extends minimal)
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
describe("handbook template", () => {
|
|
167
|
+
const projectName = "test-handbook";
|
|
168
|
+
|
|
169
|
+
it("creates handbook sections and starter files", async () => {
|
|
170
|
+
await runTemplate({
|
|
171
|
+
name: projectName,
|
|
172
|
+
template: "handbook",
|
|
173
|
+
git: false,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Handbook-specific section directories
|
|
177
|
+
expect(generatedExists(projectName, "content/overview")).toBe(true);
|
|
178
|
+
expect(generatedExists(projectName, "content/guides")).toBe(true);
|
|
179
|
+
expect(generatedExists(projectName, "content/reference")).toBe(true);
|
|
180
|
+
|
|
181
|
+
// Handbook starter content files
|
|
182
|
+
expect(generatedExists(projectName, "content/overview/introduction.md")).toBe(true);
|
|
183
|
+
expect(generatedExists(projectName, "content/guides/getting-started.md")).toBe(true);
|
|
184
|
+
expect(generatedExists(projectName, "content/reference/glossary.md")).toBe(true);
|
|
185
|
+
|
|
186
|
+
// Handbook-specific extras
|
|
187
|
+
expect(generatedExists(projectName, "CUSTOMIZING.md")).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("inherits minimal template files", async () => {
|
|
191
|
+
await runTemplate({
|
|
192
|
+
name: projectName,
|
|
193
|
+
template: "handbook",
|
|
194
|
+
git: false,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Files from minimal (base) template should also be present
|
|
198
|
+
expect(generatedExists(projectName, ".gitignore")).toBe(true);
|
|
199
|
+
expect(generatedExists(projectName, "content/.gitkeep")).toBe(true);
|
|
200
|
+
expect(generatedExists(projectName, "assets/brand/.gitkeep")).toBe(true);
|
|
201
|
+
|
|
202
|
+
// Inherited section directories from minimal
|
|
203
|
+
expect(generatedExists(projectName, "content")).toBe(true);
|
|
204
|
+
expect(generatedExists(projectName, "assets/brand")).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("handbook site.yaml includes section definitions", async () => {
|
|
208
|
+
await runTemplate({
|
|
209
|
+
name: projectName,
|
|
210
|
+
template: "handbook",
|
|
211
|
+
git: false,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const siteYaml = await readGenerated(projectName, "site.yaml");
|
|
215
|
+
|
|
216
|
+
// Handbook overrides minimal's site.yaml with section config
|
|
217
|
+
expect(siteYaml).toContain("sections:");
|
|
218
|
+
expect(siteYaml).toContain('"Overview"');
|
|
219
|
+
expect(siteYaml).toContain('"Guides"');
|
|
220
|
+
expect(siteYaml).toContain('"Reference"');
|
|
221
|
+
expect(siteYaml).toContain("content/overview");
|
|
222
|
+
expect(siteYaml).toContain("content/guides");
|
|
223
|
+
expect(siteYaml).toContain("content/reference");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("handbook starter files contain correct frontmatter", async () => {
|
|
227
|
+
await runTemplate({
|
|
228
|
+
name: projectName,
|
|
229
|
+
template: "handbook",
|
|
230
|
+
git: false,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const intro = await readGenerated(projectName, "content/overview/introduction.md");
|
|
234
|
+
expect(intro).toMatch(/^---\n/);
|
|
235
|
+
expect(intro).toContain("title: Introduction");
|
|
236
|
+
expect(intro).toContain("description: Introduction to Test Handbook");
|
|
237
|
+
|
|
238
|
+
const guide = await readGenerated(projectName, "content/guides/getting-started.md");
|
|
239
|
+
expect(guide).toContain("title: Getting Started");
|
|
240
|
+
|
|
241
|
+
const glossary = await readGenerated(projectName, "content/reference/glossary.md");
|
|
242
|
+
expect(glossary).toContain("title: Glossary");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("manifest records handbook template", async () => {
|
|
246
|
+
await runTemplate({
|
|
247
|
+
name: projectName,
|
|
248
|
+
template: "handbook",
|
|
249
|
+
git: false,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const manifestRaw = await readGenerated(projectName, ".kitfly/manifest.json");
|
|
253
|
+
const manifest = JSON.parse(manifestRaw);
|
|
254
|
+
|
|
255
|
+
expect(manifest.template).toBe("handbook");
|
|
256
|
+
expect(manifest.standalone).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// Custom Branding
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
describe("custom branding", () => {
|
|
265
|
+
const projectName = "branded-site";
|
|
266
|
+
|
|
267
|
+
it("applies brand name override to generated files", async () => {
|
|
268
|
+
await runTemplate({
|
|
269
|
+
name: projectName,
|
|
270
|
+
template: "minimal",
|
|
271
|
+
git: false,
|
|
272
|
+
branding: {
|
|
273
|
+
brandName: "Acme Corp",
|
|
274
|
+
siteName: "Acme Corp",
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const siteYaml = await readGenerated(projectName, "site.yaml");
|
|
279
|
+
expect(siteYaml).toContain('name: "Acme Corp"');
|
|
280
|
+
expect(siteYaml).toContain('title: "Acme Corp"');
|
|
281
|
+
|
|
282
|
+
const indexMd = await readGenerated(projectName, "index.md");
|
|
283
|
+
expect(indexMd).toContain("Welcome to Acme Corp");
|
|
284
|
+
expect(indexMd).toContain("description: Acme Corp documentation");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("applies brand URL override", async () => {
|
|
288
|
+
await runTemplate({
|
|
289
|
+
name: projectName,
|
|
290
|
+
template: "minimal",
|
|
291
|
+
git: false,
|
|
292
|
+
branding: {
|
|
293
|
+
brandUrl: "https://acme.example.com",
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const siteYaml = await readGenerated(projectName, "site.yaml");
|
|
298
|
+
expect(siteYaml).toContain('url: "https://acme.example.com"');
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
// init() via CLI entry point
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
describe("init() entry point", () => {
|
|
307
|
+
const projectName = "test-via-init";
|
|
308
|
+
|
|
309
|
+
it("creates a site using the default minimal template", async () => {
|
|
310
|
+
await init(projectName, { git: false });
|
|
311
|
+
|
|
312
|
+
expect(generatedExists(projectName, "site.yaml")).toBe(true);
|
|
313
|
+
expect(generatedExists(projectName, "index.md")).toBe(true);
|
|
314
|
+
expect(generatedExists(projectName, ".gitignore")).toBe(true);
|
|
315
|
+
expect(generatedExists(projectName, ".kitfly/manifest.json")).toBe(true);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("creates a handbook site when template flag is set", async () => {
|
|
319
|
+
await init(projectName, { template: "handbook", git: false });
|
|
320
|
+
|
|
321
|
+
expect(generatedExists(projectName, "content/overview/introduction.md")).toBe(true);
|
|
322
|
+
expect(generatedExists(projectName, "content/guides/getting-started.md")).toBe(true);
|
|
323
|
+
expect(generatedExists(projectName, "CUSTOMIZING.md")).toBe(true);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("passes brand overrides through to template context", async () => {
|
|
327
|
+
await init(projectName, {
|
|
328
|
+
git: false,
|
|
329
|
+
brand: "Widget Co",
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const siteYaml = await readGenerated(projectName, "site.yaml");
|
|
333
|
+
expect(siteYaml).toContain('name: "Widget Co"');
|
|
334
|
+
expect(siteYaml).toContain('title: "Widget Co"');
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
// Standalone Mode
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
describe("standalone mode", () => {
|
|
343
|
+
const projectName = "test-standalone";
|
|
344
|
+
|
|
345
|
+
it("copies site scripts and generates package.json", async () => {
|
|
346
|
+
await runTemplate({
|
|
347
|
+
name: projectName,
|
|
348
|
+
template: "minimal",
|
|
349
|
+
git: false,
|
|
350
|
+
standalone: true,
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Standalone package.json
|
|
354
|
+
expect(generatedExists(projectName, "package.json")).toBe(true);
|
|
355
|
+
const pkgRaw = await readGenerated(projectName, "package.json");
|
|
356
|
+
const pkg = JSON.parse(pkgRaw);
|
|
357
|
+
expect(pkg.name).toBe(projectName);
|
|
358
|
+
expect(pkg.scripts.dev).toBe("bun run scripts/dev.ts");
|
|
359
|
+
expect(pkg.scripts.build).toBe("bun run scripts/build.ts");
|
|
360
|
+
expect(pkg.dependencies.marked).toBeTruthy();
|
|
361
|
+
|
|
362
|
+
// Standalone provenance tracking
|
|
363
|
+
expect(generatedExists(projectName, ".kitfly/provenance.json")).toBe(true);
|
|
364
|
+
const provRaw = await readGenerated(projectName, ".kitfly/provenance.json");
|
|
365
|
+
const prov = JSON.parse(provRaw);
|
|
366
|
+
expect(prov.template).toBe("minimal");
|
|
367
|
+
expect(prov.files.length).toBeGreaterThan(0);
|
|
368
|
+
|
|
369
|
+
// Manifest records standalone: true
|
|
370
|
+
const manifestRaw = await readGenerated(projectName, ".kitfly/manifest.json");
|
|
371
|
+
const manifest = JSON.parse(manifestRaw);
|
|
372
|
+
expect(manifest.standalone).toBe(true);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("copies core site engine files", async () => {
|
|
376
|
+
await runTemplate({
|
|
377
|
+
name: projectName,
|
|
378
|
+
template: "minimal",
|
|
379
|
+
git: false,
|
|
380
|
+
standalone: true,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Key standalone files should be copied from kitfly source
|
|
384
|
+
expect(generatedExists(projectName, "scripts/dev.ts")).toBe(true);
|
|
385
|
+
expect(generatedExists(projectName, "scripts/build.ts")).toBe(true);
|
|
386
|
+
expect(generatedExists(projectName, "scripts/bundle.ts")).toBe(true);
|
|
387
|
+
expect(generatedExists(projectName, "src/shared.ts")).toBe(true);
|
|
388
|
+
expect(generatedExists(projectName, "src/engine.ts")).toBe(true);
|
|
389
|
+
expect(generatedExists(projectName, "src/theme.ts")).toBe(true);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("generates standalone-specific README", async () => {
|
|
393
|
+
await runTemplate({
|
|
394
|
+
name: projectName,
|
|
395
|
+
template: "minimal",
|
|
396
|
+
git: false,
|
|
397
|
+
standalone: true,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const readme = await readGenerated(projectName, "README.md");
|
|
401
|
+
expect(readme).toContain("standalone mode");
|
|
402
|
+
expect(readme).toContain("bun install");
|
|
403
|
+
expect(readme).toContain("bun run dev");
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// ---------------------------------------------------------------------------
|
|
408
|
+
// Default Branding Derivation
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
|
|
411
|
+
describe("defaultBranding", () => {
|
|
412
|
+
it("converts hyphenated name to title case", () => {
|
|
413
|
+
const b = defaultBranding("my-cool-project");
|
|
414
|
+
expect(b.siteName).toBe("My Cool Project");
|
|
415
|
+
expect(b.brandName).toBe("My Cool Project");
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("converts underscore name to title case", () => {
|
|
419
|
+
const b = defaultBranding("team_handbook");
|
|
420
|
+
expect(b.siteName).toBe("Team Handbook");
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("defaults brandUrl to /", () => {
|
|
424
|
+
const b = defaultBranding("anything");
|
|
425
|
+
expect(b.brandUrl).toBe("/");
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("includes footer text with current year", () => {
|
|
429
|
+
const b = defaultBranding("acme");
|
|
430
|
+
const year = new Date().getFullYear();
|
|
431
|
+
expect(b.footerText).toContain(String(year));
|
|
432
|
+
expect(b.footerText).toContain("Acme");
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// ---------------------------------------------------------------------------
|
|
437
|
+
// Error: directory already exists and non-empty
|
|
438
|
+
// ---------------------------------------------------------------------------
|
|
439
|
+
|
|
440
|
+
describe("directory conflict", () => {
|
|
441
|
+
it("runTemplate throws for unknown template", async () => {
|
|
442
|
+
await expect(
|
|
443
|
+
runTemplate({
|
|
444
|
+
name: "whatever",
|
|
445
|
+
template: "does-not-exist",
|
|
446
|
+
git: false,
|
|
447
|
+
}),
|
|
448
|
+
).rejects.toThrow("Unknown template: does-not-exist");
|
|
449
|
+
});
|
|
450
|
+
});
|