@twango/twiki 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/.github/actions/product-cache/action.yml +32 -0
  2. package/.github/actions/product-cache/index.mjs +450 -0
  3. package/LICENSE +21 -0
  4. package/README.md +146 -2
  5. package/bun.lock +1701 -0
  6. package/cli/runtime.test.ts +420 -0
  7. package/cli/runtime.ts +385 -0
  8. package/cli/twiki.ts +305 -0
  9. package/package.json +138 -4
  10. package/site/_headers +7 -0
  11. package/site/assets/fonts/OFL.txt +93 -0
  12. package/site/assets/fonts/OverusedGrotesk-SemiBold.ttf +0 -0
  13. package/site/assets/social-background.svg +38 -0
  14. package/site/bun.lock +1701 -0
  15. package/site/database/index.ts +7 -0
  16. package/site/database/schema.ts +95 -0
  17. package/site/drizzle.config.ts +7 -0
  18. package/site/migrations/0001_auth.sql +14 -0
  19. package/site/migrations/0002_site_access.sql +5 -0
  20. package/site/migrations/0003_github_username.sql +1 -0
  21. package/site/migrations/meta/0002_snapshot.json +432 -0
  22. package/site/migrations/meta/0003_snapshot.json +439 -0
  23. package/site/migrations/meta/_journal.json +27 -0
  24. package/site/package.json +120 -0
  25. package/site/src/app.d.ts +14 -0
  26. package/site/src/app.html +11 -0
  27. package/site/src/hooks.server.ts +12 -0
  28. package/site/src/lib/analytics-config.json +1 -0
  29. package/site/src/lib/analytics.ts +40 -0
  30. package/site/src/lib/auth-config.json +3 -0
  31. package/site/src/lib/components/SEO.svelte +113 -0
  32. package/site/src/lib/components/doc/AccessLink.svelte +27 -0
  33. package/site/src/lib/components/doc/Backlinks.svelte +59 -0
  34. package/site/src/lib/components/doc/Breadcrumbs.svelte +29 -0
  35. package/site/src/lib/components/doc/ContentMeta.svelte +61 -0
  36. package/site/src/lib/components/doc/DocPager.svelte +91 -0
  37. package/site/src/lib/components/doc/DocShell.svelte +115 -0
  38. package/site/src/lib/components/doc/LicenseNotice.svelte +26 -0
  39. package/site/src/lib/components/doc/LockedContent.svelte +29 -0
  40. package/site/src/lib/components/doc/NavTree.svelte +158 -0
  41. package/site/src/lib/components/doc/NoteActions.svelte +139 -0
  42. package/site/src/lib/components/doc/PublicationBadge.svelte +49 -0
  43. package/site/src/lib/components/doc/ReaderModeToggle.svelte +19 -0
  44. package/site/src/lib/components/doc/Sidebar.svelte +351 -0
  45. package/site/src/lib/components/doc/Toc.svelte +339 -0
  46. package/site/src/lib/components/doc/enhancements.ts +146 -0
  47. package/site/src/lib/components/doc/heading-tracker.svelte.ts +39 -0
  48. package/site/src/lib/components/doc/image-preview.ts +114 -0
  49. package/site/src/lib/components/doc/reader-mode.svelte.ts +40 -0
  50. package/site/src/lib/components/embeds/mermaid.ts +353 -0
  51. package/site/src/lib/components/files/FileCode.svelte +81 -0
  52. package/site/src/lib/components/files/FileExplorer.svelte +74 -0
  53. package/site/src/lib/components/files/FileHistory.svelte +170 -0
  54. package/site/src/lib/components/files/FileIcon.svelte +28 -0
  55. package/site/src/lib/components/files/FileRmd.svelte +209 -0
  56. package/site/src/lib/components/files/FileTabs.svelte +180 -0
  57. package/site/src/lib/components/files/FileTree.svelte +67 -0
  58. package/site/src/lib/components/files/blame-gutter.ts +45 -0
  59. package/site/src/lib/components/files/code-view.ts +98 -0
  60. package/site/src/lib/components/files/file-workspace.svelte.ts +100 -0
  61. package/site/src/lib/components/graph/GraphPanel.svelte +275 -0
  62. package/site/src/lib/components/graph/GraphView.svelte +100 -0
  63. package/site/src/lib/components/graph/graph-data.ts +137 -0
  64. package/site/src/lib/components/graph/graph-model.ts +103 -0
  65. package/site/src/lib/components/graph/index.ts +3 -0
  66. package/site/src/lib/components/graph/render-graph.ts +626 -0
  67. package/site/src/lib/components/popover/index.ts +1 -0
  68. package/site/src/lib/components/popover/link-popovers.ts +233 -0
  69. package/site/src/lib/components/popover/popover.css +82 -0
  70. package/site/src/lib/components/search/SearchButton.svelte +57 -0
  71. package/site/src/lib/components/search/SearchPalette.svelte +189 -0
  72. package/site/src/lib/components/search/index.ts +3 -0
  73. package/site/src/lib/components/search/pagefind-client.svelte.ts +129 -0
  74. package/site/src/lib/components/search/pagefind-query.ts +22 -0
  75. package/site/src/lib/components/search/pagefind-url.ts +12 -0
  76. package/site/src/lib/components/search/search-state.svelte.ts +22 -0
  77. package/site/src/lib/components/ui/IconButton.svelte +30 -0
  78. package/site/src/lib/components/ui/ThemeToggle.svelte +22 -0
  79. package/site/src/lib/components/ui/index.ts +1 -0
  80. package/site/src/lib/config.ts +9 -0
  81. package/site/src/lib/content-license.ts +7 -0
  82. package/site/src/lib/file-history.ts +37 -0
  83. package/site/src/lib/file-icons.ts +27 -0
  84. package/site/src/lib/file-tabs.ts +72 -0
  85. package/site/src/lib/files.ts +73 -0
  86. package/site/src/lib/github-user.ts +70 -0
  87. package/site/src/lib/metadata.ts +166 -0
  88. package/site/src/lib/notebook-nav.ts +63 -0
  89. package/site/src/lib/publication.ts +1 -0
  90. package/site/src/lib/rmd.ts +156 -0
  91. package/site/src/lib/server/access-admin.ts +33 -0
  92. package/site/src/lib/server/content-model.ts +253 -0
  93. package/site/src/lib/server/content-provider.ts +266 -0
  94. package/site/src/lib/server/content.ts +277 -0
  95. package/site/src/lib/server/runtime-content.ts +34 -0
  96. package/site/src/lib/site-config.json +9 -0
  97. package/site/src/lib/social-image.ts +13 -0
  98. package/site/src/lib/styles/article.css +607 -0
  99. package/site/src/lib/styles/foundation.css +138 -0
  100. package/site/src/lib/styles/print.css +115 -0
  101. package/site/src/lib/styles/shell.css +182 -0
  102. package/site/src/lib/text-exports.ts +5 -0
  103. package/site/src/lib/types.ts +140 -0
  104. package/site/src/lib/utils.ts +6 -0
  105. package/site/src/routes/+error.svelte +16 -0
  106. package/site/src/routes/+layout.server.ts +10 -0
  107. package/site/src/routes/+layout.svelte +72 -0
  108. package/site/src/routes/+layout.ts +3 -0
  109. package/site/src/routes/404/+page.svelte +13 -0
  110. package/site/src/routes/[...slug]/+page.server.ts +24 -0
  111. package/site/src/routes/[...slug]/+page.svelte +180 -0
  112. package/site/src/routes/[course]/files/[...file]/+page.svelte +373 -0
  113. package/site/src/routes/[course]/files/[...file]/+page.ts +38 -0
  114. package/site/src/routes/admin/access/+page.server.ts +53 -0
  115. package/site/src/routes/admin/access/+page.svelte +118 -0
  116. package/site/src/routes/graph.json/+server.ts +7 -0
  117. package/site/src/routes/index.xml/+server.ts +7 -0
  118. package/site/src/routes/layout.css +10 -0
  119. package/site/src/routes/robots.txt/+server.ts +9 -0
  120. package/site/src/routes/sitemap.xml/+server.ts +7 -0
  121. package/site/src/routes/tags/+page.server.ts +5 -0
  122. package/site/src/routes/tags/+page.svelte +38 -0
  123. package/site/src/routes/tags/[...tag]/+page.server.ts +22 -0
  124. package/site/src/routes/tags/[...tag]/+page.svelte +52 -0
  125. package/site/static/favicon.png +0 -0
  126. package/site/static/fonts/JetBrainsMono-Italic-VF.woff2 +0 -0
  127. package/site/static/fonts/JetBrainsMono-VF.woff2 +0 -0
  128. package/site/static/fonts/OverusedGrotesk-VF.woff2 +0 -0
  129. package/site/static/fonts/OverusedGroteskItalic-VF.woff2 +0 -0
  130. package/site/static/fonts/Virgil.woff2 +0 -0
  131. package/site/svelte.config.js +71 -0
  132. package/site/tooling/LICENSE-quartz +25 -0
  133. package/site/tooling/adapter.js +55 -0
  134. package/site/tooling/assets.ts +323 -0
  135. package/site/tooling/benchmark.ts +191 -0
  136. package/site/tooling/build.ts +407 -0
  137. package/site/tooling/cache.ts +133 -0
  138. package/site/tooling/check-diagrams.ts +164 -0
  139. package/site/tooling/check-links.ts +86 -0
  140. package/site/tooling/check-targets.ts +139 -0
  141. package/site/tooling/compiler.ts +1386 -0
  142. package/site/tooling/deploy.ts +112 -0
  143. package/site/tooling/fingerprint.ts +81 -0
  144. package/site/tooling/lib/app-version.js +92 -0
  145. package/site/tooling/lib/application-asset-urls.ts +27 -0
  146. package/site/tooling/lib/application-cache.ts +269 -0
  147. package/site/tooling/lib/asset-hash-cache.ts +116 -0
  148. package/site/tooling/lib/autotag.ts +99 -0
  149. package/site/tooling/lib/build-assembly.ts +148 -0
  150. package/site/tooling/lib/build-workspace.ts +386 -0
  151. package/site/tooling/lib/cache-selection.ts +70 -0
  152. package/site/tooling/lib/cloudflare-prewarm.js +40 -0
  153. package/site/tooling/lib/comments.ts +32 -0
  154. package/site/tooling/lib/content-cache.ts +244 -0
  155. package/site/tooling/lib/content-urls.ts +50 -0
  156. package/site/tooling/lib/course-cache.ts +298 -0
  157. package/site/tooling/lib/course-files.ts +281 -0
  158. package/site/tooling/lib/currency.ts +33 -0
  159. package/site/tooling/lib/deployment-config.ts +222 -0
  160. package/site/tooling/lib/deployment-manifest.ts +274 -0
  161. package/site/tooling/lib/deployment-prepare.ts +325 -0
  162. package/site/tooling/lib/deployment-upload.ts +293 -0
  163. package/site/tooling/lib/dev-git-state.ts +130 -0
  164. package/site/tooling/lib/dev-history.ts +86 -0
  165. package/site/tooling/lib/dev-social-images.ts +38 -0
  166. package/site/tooling/lib/dev-state.ts +107 -0
  167. package/site/tooling/lib/edition-builds.ts +184 -0
  168. package/site/tooling/lib/edition-paths.js +29 -0
  169. package/site/tooling/lib/empty-tag-route.js +23 -0
  170. package/site/tooling/lib/file-history.ts +201 -0
  171. package/site/tooling/lib/file-icons.ts +93 -0
  172. package/site/tooling/lib/file-policy.ts +38 -0
  173. package/site/tooling/lib/file-search.js +13 -0
  174. package/site/tooling/lib/git-revisions.ts +41 -0
  175. package/site/tooling/lib/history-pool.ts +74 -0
  176. package/site/tooling/lib/history-worker.ts +22 -0
  177. package/site/tooling/lib/knit-rmd.R +38 -0
  178. package/site/tooling/lib/lastmod.ts +201 -0
  179. package/site/tooling/lib/markdown-files.ts +51 -0
  180. package/site/tooling/lib/ofm.ts +634 -0
  181. package/site/tooling/lib/output.ts +60 -0
  182. package/site/tooling/lib/product-cache.ts +1006 -0
  183. package/site/tooling/lib/project-config.ts +288 -0
  184. package/site/tooling/lib/public-assets.ts +38 -0
  185. package/site/tooling/lib/public-links.ts +56 -0
  186. package/site/tooling/lib/publishing.ts +156 -0
  187. package/site/tooling/lib/r-runtime.ts +257 -0
  188. package/site/tooling/lib/rebuild-queue.ts +25 -0
  189. package/site/tooling/lib/rmd-preflight.ts +166 -0
  190. package/site/tooling/lib/rmd-preview-cache.ts +93 -0
  191. package/site/tooling/lib/rmd-previews.ts +129 -0
  192. package/site/tooling/lib/runtime-data-modules.ts +30 -0
  193. package/site/tooling/lib/runtime-packaging.ts +321 -0
  194. package/site/tooling/lib/search-cache.js +165 -0
  195. package/site/tooling/lib/search-inputs.js +129 -0
  196. package/site/tooling/lib/slug.ts +377 -0
  197. package/site/tooling/lib/social-images.ts +210 -0
  198. package/site/tooling/lib/text-exports.ts +51 -0
  199. package/site/tooling/lib/visit-elements.ts +17 -0
  200. package/site/tooling/live-test.ts +138 -0
  201. package/site/tooling/pipeline.ts +103 -0
  202. package/site/tooling/prepare.ts +3 -0
  203. package/site/tooling/publish.ts +28 -0
  204. package/site/tooling/r-cache.ts +45 -0
  205. package/site/tooling/r-render-profile.json +7 -0
  206. package/site/tooling/search.js +94 -0
  207. package/site/tooling/verify-deployment.ts +109 -0
  208. package/site/tooling/vite-content.ts +240 -0
  209. package/site/tooling/wrangler-paths.js +25 -0
  210. package/site/tooling/wrangler.ts +19 -0
  211. package/site/tsconfig.json +16 -0
  212. package/site/vite.config.ts +40 -0
  213. package/site/worker/access.ts +57 -0
  214. package/site/worker/analytics.ts +100 -0
  215. package/site/worker/auth-routes.ts +78 -0
  216. package/site/worker/auth.ts +78 -0
  217. package/site/worker/content-runtime.ts +226 -0
  218. package/site/worker/framing.ts +4 -0
  219. package/site/worker/gate.ts +147 -0
  220. package/site/worker/index.ts +9 -0
  221. package/site/worker/login.ts +62 -0
  222. package/site/worker/publication.ts +44 -0
  223. package/site/worker/runtime.ts +104 -0
  224. package/site/worker/tsconfig.json +13 -0
  225. package/site/wrangler.jsonc +37 -0
@@ -0,0 +1,420 @@
1
+ import { afterEach, expect, test } from "bun:test";
2
+ import {
3
+ cpSync,
4
+ existsSync,
5
+ lstatSync,
6
+ mkdirSync,
7
+ mkdtempSync,
8
+ readFileSync,
9
+ readdirSync,
10
+ realpathSync,
11
+ rmSync,
12
+ symlinkSync,
13
+ writeFileSync,
14
+ } from "node:fs";
15
+ import { tmpdir } from "node:os";
16
+ import path from "node:path";
17
+ import { prepareProject, publishOutput } from "./runtime";
18
+ import { argumentsFor, invocation, main } from "./twiki";
19
+ const fixtures: string[] = [];
20
+ function fixture() {
21
+ const root = mkdtempSync(path.join(tmpdir(), "twiki-cli-"));
22
+ fixtures.push(root);
23
+ const vault = path.join(root, "vault"),
24
+ engine = path.join(root, "engine"),
25
+ modules = path.join(root, "modules");
26
+ for (const directory of [
27
+ vault,
28
+ path.join(engine, "src/lib"),
29
+ path.join(engine, "tooling"),
30
+ path.join(engine, "database"),
31
+ path.join(engine, "static/fonts"),
32
+ path.join(modules, ".bin"),
33
+ path.join(modules, "vite"),
34
+ path.join(modules, ".vite"),
35
+ ])
36
+ mkdirSync(directory, { recursive: true });
37
+ writeFileSync(
38
+ path.join(vault, "README.md"),
39
+ "vault must stay byte-preserved",
40
+ );
41
+ writeFileSync(
42
+ path.join(engine, "src/lib/source.ts"),
43
+ "export const value=1;",
44
+ );
45
+ writeFileSync(path.join(engine, "src/lib/site-config.json"), "{}");
46
+ writeFileSync(path.join(engine, "src/lib/analytics-config.json"), "null");
47
+ writeFileSync(path.join(engine, "tooling/tool.ts"), 'console.log("tool");');
48
+ writeFileSync(path.join(engine, "static/fonts/font.woff2"), "font fixture");
49
+ writeFileSync(path.join(engine, "package.json"), '{"name":"engine"}');
50
+ writeFileSync(path.join(engine, "bun.lock"), "pinned engine");
51
+ writeFileSync(
52
+ path.join(engine, "database/schema.ts"),
53
+ "export const schema = {}; ",
54
+ );
55
+ writeFileSync(path.join(modules, "vite/package.json"), '{"name":"vite"}');
56
+ return {
57
+ root,
58
+ vault,
59
+ engine,
60
+ modules,
61
+ options: {
62
+ projectRoot: vault,
63
+ engineDirectory: engine,
64
+ installDependencies: (runtime: string) => {
65
+ const target = path.join(runtime, "node_modules");
66
+ mkdirSync(target, { recursive: true });
67
+ cpSync(modules, target, {
68
+ recursive: true,
69
+ filter: (source) =>
70
+ ![".vite", ".vite-temp", ".cache"].includes(path.basename(source)),
71
+ });
72
+ },
73
+ environment: { CLOUDFLARE_API_TOKEN: "never serialize this secret" },
74
+ },
75
+ };
76
+ }
77
+ afterEach(() => {
78
+ for (const root of fixtures.splice(0))
79
+ rmSync(root, { recursive: true, force: true });
80
+ });
81
+ test("stage code into a consumer-owned runtime and keep installation, vault and dependency caches isolated", () => {
82
+ const f = fixture();
83
+ const result = prepareProject(f.options);
84
+ expect(result.workDir).toBe(path.join(f.vault, ".twiki/site"));
85
+ expect(result.env.TWIKI_DEPENDENCY_DIR).toBe(
86
+ path.join(result.workDir, "node_modules"),
87
+ );
88
+ expect(
89
+ readFileSync(path.join(result.workDir, "src/lib/source.ts"), "utf8"),
90
+ ).toBe("export const value=1;");
91
+ expect(
92
+ lstatSync(path.join(result.workDir, "src/lib/source.ts")).ino,
93
+ ).not.toBe(lstatSync(path.join(f.engine, "src/lib/source.ts")).ino);
94
+ expect(
95
+ lstatSync(path.join(result.workDir, "node_modules")).isSymbolicLink(),
96
+ ).toBe(false);
97
+ expect(
98
+ lstatSync(path.join(result.workDir, "node_modules/vite")).isSymbolicLink(),
99
+ ).toBe(false);
100
+ expect(existsSync(path.join(result.workDir, "node_modules/.vite"))).toBe(
101
+ false,
102
+ );
103
+ mkdirSync(path.join(result.workDir, "node_modules/.vite"));
104
+ writeFileSync(
105
+ path.join(result.workDir, "node_modules/.vite/cache"),
106
+ "local cache",
107
+ );
108
+ expect(readdirSync(path.join(f.modules, ".vite"))).toEqual([]);
109
+ expect(JSON.stringify(result)).not.toContain("never serialize");
110
+ writeFileSync(path.join(result.workDir, "src/lib/source.ts"), "runtime edit");
111
+ expect(readFileSync(path.join(f.engine, "src/lib/source.ts"), "utf8")).toBe(
112
+ "export const value=1;",
113
+ );
114
+ expect(readFileSync(path.join(f.vault, "README.md"), "utf8")).toBe(
115
+ "vault must stay byte-preserved",
116
+ );
117
+ expect(readdirSync(f.vault).sort()).toEqual([".twiki", "README.md"]);
118
+ });
119
+ test("source upgrades remove retired owned files while preserving generated products and consumer branding", () => {
120
+ const f = fixture();
121
+ writeFileSync(
122
+ path.join(f.vault, "twiki.config.json"),
123
+ JSON.stringify({
124
+ version: 1,
125
+ publication: "public",
126
+ site: { name: "Personal vault", title: "Personal notes" },
127
+ }),
128
+ );
129
+ const first = prepareProject(f.options);
130
+ mkdirSync(path.join(first.workDir, "build/generated"), { recursive: true });
131
+ writeFileSync(
132
+ path.join(first.workDir, "build/generated/keep"),
133
+ "cached product",
134
+ );
135
+ rmSync(path.join(f.engine, "src/lib/source.ts"));
136
+ writeFileSync(path.join(f.engine, "src/lib/new.ts"), "new source");
137
+ const second = prepareProject(f.options);
138
+ expect(existsSync(path.join(second.workDir, "src/lib/source.ts"))).toBe(
139
+ false,
140
+ );
141
+ expect(
142
+ readFileSync(path.join(second.workDir, "build/generated/keep"), "utf8"),
143
+ ).toBe("cached product");
144
+ expect(
145
+ JSON.parse(
146
+ readFileSync(
147
+ path.join(second.workDir, "src/lib/site-config.json"),
148
+ "utf8",
149
+ ),
150
+ ).title,
151
+ ).toBe("Personal notes");
152
+ expect(
153
+ readFileSync(path.join(f.engine, "src/lib/site-config.json"), "utf8"),
154
+ ).toBe("{}");
155
+ });
156
+ test("the shipped frozen engine lock is independent of the consumer graph", () => {
157
+ const f = fixture();
158
+ writeFileSync(
159
+ path.join(f.vault, "bun.lock"),
160
+ "consumer resolved dependency graph",
161
+ );
162
+ const project = prepareProject(f.options);
163
+ expect(readFileSync(path.join(project.workDir, "bun.lock"), "utf8")).toBe(
164
+ "pinned engine",
165
+ );
166
+ expect(readFileSync(path.join(f.engine, "bun.lock"), "utf8")).toBe(
167
+ "pinned engine",
168
+ );
169
+ });
170
+
171
+ test("consumer Cloudflare resources and bindings are preserved while generated paths and default auth stay local", () => {
172
+ const f = fixture();
173
+ const resources = {
174
+ name: "personal-notes",
175
+ account_id: "example-account",
176
+ vars: {
177
+ OWNER_GITHUB_ID: "42",
178
+ GITHUB_CLIENT_ID: "public-client",
179
+ CUSTOM_VAR: "value",
180
+ },
181
+ d1_databases: [
182
+ { binding: "DB", database_name: "notes", database_id: "fixture-id" },
183
+ ],
184
+ routes: [{ pattern: "notes.example.com", custom_domain: true }],
185
+ workers_dev: false,
186
+ main: "/untrusted/output",
187
+ assets: { directory: "/untrusted/assets" },
188
+ };
189
+ writeFileSync(
190
+ path.join(f.vault, "twiki.config.json"),
191
+ JSON.stringify({
192
+ version: 1,
193
+ site: { url: "https://notes.example.com" },
194
+ cloudflare: resources,
195
+ }),
196
+ );
197
+ const project = prepareProject(f.options);
198
+ const config = JSON.parse(
199
+ readFileSync(path.join(project.workDir, "wrangler.jsonc"), "utf8"),
200
+ );
201
+ expect(config.name).toBe(resources.name);
202
+ expect(config.d1_databases).toEqual(resources.d1_databases);
203
+ expect(config.routes).toEqual(resources.routes);
204
+ expect(config.vars).toMatchObject({
205
+ ...resources.vars,
206
+ ORIGIN: "https://notes.example.com",
207
+ TWIKI_AUTH_MODE: "github",
208
+ DISABLE_ANALYTICS: "true",
209
+ });
210
+ expect(config.main).toBe("build/.svelte-kit/worker.js");
211
+ expect(config.assets.directory).toBe("build/.svelte-kit/cloudflare");
212
+ expect(
213
+ JSON.parse(
214
+ readFileSync(
215
+ path.join(project.workDir, "src/lib/analytics-config.json"),
216
+ "utf8",
217
+ ),
218
+ ),
219
+ ).toBeNull();
220
+ });
221
+ test("symlinked writable directories are rejected without modifying their targets", () => {
222
+ const f = fixture();
223
+ const outside = path.join(f.root, "outside");
224
+ mkdirSync(outside);
225
+ writeFileSync(path.join(outside, "keep"), "unchanged");
226
+ symlinkSync(outside, path.join(f.vault, ".twiki"), "dir");
227
+ expect(() => prepareProject(f.options)).toThrow("symlinks");
228
+ expect(readdirSync(outside)).toEqual(["keep"]);
229
+ });
230
+ test("invalid configuration fails before staging; a partial failure releases its preparation lock", () => {
231
+ const f = fixture();
232
+ writeFileSync(path.join(f.vault, "twiki.config.json"), '{"version":2}');
233
+ expect(() => prepareProject(f.options)).toThrow();
234
+ expect(existsSync(path.join(f.vault, ".twiki"))).toBe(false);
235
+ rmSync(path.join(f.vault, "twiki.config.json"));
236
+ expect(() =>
237
+ prepareProject({
238
+ ...f.options,
239
+ installDependencies: () => {
240
+ throw new Error("simulated installer failure");
241
+ },
242
+ }),
243
+ ).toThrow();
244
+ expect(existsSync(path.join(f.vault, ".twiki/prepare.lock"))).toBe(false);
245
+ expect(prepareProject(f.options).workDir).toContain(".twiki/site");
246
+ });
247
+ test("output is published only from a completed runtime and never replaces an arbitrary directory", () => {
248
+ const f = fixture();
249
+ const project = prepareProject(f.options);
250
+ expect(() => publishOutput(project, "static")).toThrow();
251
+ const published = path.join(
252
+ project.workDir,
253
+ "build/.svelte-kit/cloudflare/_published",
254
+ );
255
+ mkdirSync(published, { recursive: true });
256
+ writeFileSync(path.join(published, "index.html"), "public html");
257
+ publishOutput(project, "static");
258
+ expect(readFileSync(path.join(project.outputDir, "index.html"), "utf8")).toBe(
259
+ "public html",
260
+ );
261
+ rmSync(project.outputDir);
262
+ mkdirSync(project.outputDir);
263
+ writeFileSync(path.join(project.outputDir, "keep"), "existing");
264
+ expect(() => publishOutput(project, "static")).toThrow("Refusing");
265
+ expect(readFileSync(path.join(project.outputDir, "keep"), "utf8")).toBe(
266
+ "existing",
267
+ );
268
+ });
269
+ test("CLI validates explicit targets and keeps tool execution in the prepared runtime", () => {
270
+ const f = fixture();
271
+ const project = prepareProject(f.options);
272
+ expect(argumentsFor(["build", "--target", "static"]).target).toBe("static");
273
+ expect(() => argumentsFor(["build", "--target", "other"])).toThrow();
274
+ expect(() => argumentsFor(["exec"])).toThrow();
275
+ expect(
276
+ invocation("exec", project, ["tooling/cache.ts", "restore"]).slice(1),
277
+ ).toEqual(["tooling/cache.ts", "restore"]);
278
+ expect(invocation("preview", project)).toContain("build/.wrangler/state");
279
+ expect(
280
+ invocation("preview", project, ["--port", "4300"]).filter(
281
+ (argument) => argument === "--port",
282
+ ),
283
+ ).toHaveLength(1);
284
+ expect(invocation("preview", project, ["--port=4300"])).not.toContain("4173");
285
+ expect(invocation("deploy", project).slice(-1)).toEqual(["--upload"]);
286
+ });
287
+ test("init is explicit and never overwrites existing consumer configuration", async () => {
288
+ const f = fixture();
289
+ await main(["init", "--project", f.vault]);
290
+ const before = readFileSync(path.join(f.vault, "twiki.config.json"));
291
+ await expect(main(["init", "--project", f.vault])).rejects.toThrow();
292
+ expect(readFileSync(path.join(f.vault, "twiki.config.json"))).toEqual(before);
293
+ });
294
+
295
+ test("the packaged ESM compiler removes optional TypeScript parameter markers", async () => {
296
+ // Svelte's require entry bundles its printer, so exercise the ESM entry used by Vite.
297
+ const { compile } = await import("svelte/compiler");
298
+ const compiled = compile(
299
+ '<script lang="ts">function example(value?: string): string { return value ?? "ok" }</script><p>{example()}</p>',
300
+ { generate: "server" },
301
+ ).js.code;
302
+ expect(compiled).toContain("function example(value)");
303
+ expect(compiled).not.toContain("value?");
304
+ });
305
+
306
+ test("preview derives a matching local origin and preserves explicit origin overrides", () => {
307
+ const project = prepareProject(fixture().options);
308
+ expect(invocation("preview", project)).toEqual([
309
+ process.execPath,
310
+ "tooling/wrangler.ts",
311
+ "dev",
312
+ "--persist-to",
313
+ "build/.wrangler/state",
314
+ "--port",
315
+ "4173",
316
+ "--ip",
317
+ "127.0.0.1",
318
+ "--var",
319
+ "ORIGIN:http://127.0.0.1:4173",
320
+ ]);
321
+ expect(
322
+ invocation("preview", project, [
323
+ "--port=4301",
324
+ "--ip",
325
+ "localhost",
326
+ "--local-protocol=https",
327
+ ]),
328
+ ).toContain("ORIGIN:https://localhost:4301");
329
+ expect(
330
+ invocation("preview", project, ["--ip", "::1", "--port", "4302"]),
331
+ ).toContain("ORIGIN:http://[::1]:4302");
332
+ expect(invocation("preview", project, ["--ip=0.0.0.0"])).toContain(
333
+ "ORIGIN:http://127.0.0.1:4173",
334
+ );
335
+ for (const override of [
336
+ ["--var", "OTHER:yes", "ORIGIN:https://custom.example"],
337
+ ["--var=ORIGIN:https://custom.example"],
338
+ ]) {
339
+ expect(
340
+ invocation("preview", project, override).filter((value) =>
341
+ value.includes("ORIGIN:"),
342
+ ),
343
+ ).toHaveLength(1);
344
+ expect(invocation("preview", project, override)).not.toContain(
345
+ "ORIGIN:http://127.0.0.1:4173",
346
+ );
347
+ }
348
+ for (const invalid of [
349
+ ["--remote"],
350
+ ["-r"],
351
+ ["--port", "0"],
352
+ ["--ip", "user@localhost"],
353
+ ["--ip", "localhost/path"],
354
+ ["--local-protocol", "ftp"],
355
+ ])
356
+ expect(() => invocation("preview", project, invalid)).toThrow();
357
+ });
358
+
359
+ test("browser authentication matches the final Worker mode without changing packaged defaults", () => {
360
+ for (const [cloudflare, enabled] of [
361
+ [{}, false],
362
+ [
363
+ {
364
+ d1_databases: [
365
+ { binding: "DB", database_name: "notes", database_id: "fixture" },
366
+ ],
367
+ },
368
+ true,
369
+ ],
370
+ [
371
+ {
372
+ d1_databases: [
373
+ { binding: "DB", database_name: "notes", database_id: "fixture" },
374
+ ],
375
+ vars: { TWIKI_AUTH_MODE: "none" },
376
+ },
377
+ false,
378
+ ],
379
+ ] as const) {
380
+ const f = fixture();
381
+ const source = path.join(f.engine, "src/lib/auth-config.json");
382
+ writeFileSync(source, '{"enabled":true}');
383
+ writeFileSync(
384
+ path.join(f.vault, "twiki.config.json"),
385
+ JSON.stringify({ version: 1, cloudflare }),
386
+ );
387
+ const prepared = prepareProject(f.options);
388
+ const generated = path.join(prepared.workDir, "src/lib/auth-config.json");
389
+ expect(JSON.parse(readFileSync(generated, "utf8"))).toEqual({ enabled });
390
+ const first = lstatSync(generated).mtimeMs;
391
+ prepareProject(f.options);
392
+ expect(lstatSync(generated).mtimeMs).toBe(first);
393
+ expect(readFileSync(source, "utf8")).toBe('{"enabled":true}');
394
+ }
395
+ });
396
+
397
+ test("engine dependency declarations and lock match the shipped exact runtime graph", () => {
398
+ const packageRoot = path.resolve(import.meta.dirname, "..");
399
+ const root = JSON.parse(
400
+ readFileSync(path.join(packageRoot, "package.json"), "utf8"),
401
+ );
402
+ const engine = JSON.parse(
403
+ readFileSync(path.join(packageRoot, "site/package.json"), "utf8"),
404
+ );
405
+ expect(engine.dependencies).toEqual(root.dependencies);
406
+ expect(engine.devDependencies).toBeUndefined();
407
+ expect(
408
+ Object.values(engine.dependencies).every(
409
+ (version) =>
410
+ typeof version === "string" &&
411
+ /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(version),
412
+ ),
413
+ ).toBe(true);
414
+ expect(readFileSync(path.join(packageRoot, "site/bun.lock"), "utf8")).toBe(
415
+ readFileSync(path.join(packageRoot, "bun.lock"), "utf8").replace(
416
+ '"name": ' + JSON.stringify(root.name),
417
+ '"name": "twiki-engine"',
418
+ ),
419
+ );
420
+ });