tachyon-dom 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +339 -0
  3. package/dist/adapters/lambda.d.ts +65 -0
  4. package/dist/adapters/lambda.js +211 -0
  5. package/dist/adapters/node.d.ts +25 -0
  6. package/dist/adapters/node.js +360 -0
  7. package/dist/adapters/workers.d.ts +149 -0
  8. package/dist/adapters/workers.js +272 -0
  9. package/dist/adapters.d.ts +3 -0
  10. package/dist/adapters.js +3 -0
  11. package/dist/app.d.ts +98 -0
  12. package/dist/app.js +306 -0
  13. package/dist/cli.d.ts +55 -0
  14. package/dist/cli.js +711 -0
  15. package/dist/compiler/expression.d.ts +55 -0
  16. package/dist/compiler/expression.js +858 -0
  17. package/dist/compiler/index.d.ts +8 -0
  18. package/dist/compiler/index.js +50 -0
  19. package/dist/compiler/ir.d.ts +4 -0
  20. package/dist/compiler/ir.js +255 -0
  21. package/dist/compiler/parser.d.ts +3 -0
  22. package/dist/compiler/parser.js +236 -0
  23. package/dist/compiler/sfc.d.ts +33 -0
  24. package/dist/compiler/sfc.js +439 -0
  25. package/dist/compiler/targets/client.d.ts +3 -0
  26. package/dist/compiler/targets/client.js +568 -0
  27. package/dist/compiler/targets/server.d.ts +7 -0
  28. package/dist/compiler/targets/server.js +396 -0
  29. package/dist/compiler/targets/stream.d.ts +2 -0
  30. package/dist/compiler/targets/stream.js +204 -0
  31. package/dist/compiler/types.d.ts +185 -0
  32. package/dist/compiler/types.js +1 -0
  33. package/dist/compiler/utils.d.ts +40 -0
  34. package/dist/compiler/utils.js +185 -0
  35. package/dist/compiler/whitespace.d.ts +2 -0
  36. package/dist/compiler/whitespace.js +66 -0
  37. package/dist/compiler.d.ts +1 -0
  38. package/dist/compiler.js +1 -0
  39. package/dist/constant-time.d.ts +1 -0
  40. package/dist/constant-time.js +26 -0
  41. package/dist/cookies.d.ts +44 -0
  42. package/dist/cookies.js +199 -0
  43. package/dist/diagnostics.d.ts +22 -0
  44. package/dist/diagnostics.js +45 -0
  45. package/dist/env.d.ts +27 -0
  46. package/dist/env.js +37 -0
  47. package/dist/html-escape.d.ts +2 -0
  48. package/dist/html-escape.js +20 -0
  49. package/dist/html-whitespace.d.ts +6 -0
  50. package/dist/html-whitespace.js +83 -0
  51. package/dist/i18n.d.ts +19 -0
  52. package/dist/i18n.js +93 -0
  53. package/dist/index.d.ts +20 -0
  54. package/dist/index.js +18 -0
  55. package/dist/language-server.d.ts +17 -0
  56. package/dist/language-server.js +79 -0
  57. package/dist/package-integrity.d.ts +9 -0
  58. package/dist/package-integrity.js +64 -0
  59. package/dist/result.d.ts +13 -0
  60. package/dist/result.js +4 -0
  61. package/dist/router-node.d.ts +2 -0
  62. package/dist/router-node.js +12 -0
  63. package/dist/router.d.ts +324 -0
  64. package/dist/router.js +1024 -0
  65. package/dist/runtime/attr.d.ts +3 -0
  66. package/dist/runtime/attr.js +66 -0
  67. package/dist/runtime/class.d.ts +2 -0
  68. package/dist/runtime/class.js +10 -0
  69. package/dist/runtime/conditional.d.ts +78 -0
  70. package/dist/runtime/conditional.js +140 -0
  71. package/dist/runtime/enhancement.d.ts +11 -0
  72. package/dist/runtime/enhancement.js +70 -0
  73. package/dist/runtime/error-boundary.d.ts +8 -0
  74. package/dist/runtime/error-boundary.js +27 -0
  75. package/dist/runtime/event.d.ts +1 -0
  76. package/dist/runtime/event.js +11 -0
  77. package/dist/runtime/form.d.ts +41 -0
  78. package/dist/runtime/form.js +167 -0
  79. package/dist/runtime/fragment.d.ts +6 -0
  80. package/dist/runtime/fragment.js +18 -0
  81. package/dist/runtime/html.d.ts +8 -0
  82. package/dist/runtime/html.js +6 -0
  83. package/dist/runtime/hydrate.d.ts +68 -0
  84. package/dist/runtime/hydrate.js +205 -0
  85. package/dist/runtime/keyed-rows.d.ts +61 -0
  86. package/dist/runtime/keyed-rows.js +192 -0
  87. package/dist/runtime/list.d.ts +84 -0
  88. package/dist/runtime/list.js +424 -0
  89. package/dist/runtime/portal.d.ts +6 -0
  90. package/dist/runtime/portal.js +12 -0
  91. package/dist/runtime/router.d.ts +102 -0
  92. package/dist/runtime/router.js +649 -0
  93. package/dist/runtime/signal.d.ts +30 -0
  94. package/dist/runtime/signal.js +275 -0
  95. package/dist/runtime/store.d.ts +1 -0
  96. package/dist/runtime/store.js +1 -0
  97. package/dist/runtime/stream-client.d.ts +8 -0
  98. package/dist/runtime/stream-client.js +33 -0
  99. package/dist/runtime/text.d.ts +2 -0
  100. package/dist/runtime/text.js +10 -0
  101. package/dist/runtime/virtual-list.d.ts +15 -0
  102. package/dist/runtime/virtual-list.js +89 -0
  103. package/dist/security.d.ts +27 -0
  104. package/dist/security.js +173 -0
  105. package/dist/server/form-action.d.ts +52 -0
  106. package/dist/server/form-action.js +102 -0
  107. package/dist/server/html.d.ts +19 -0
  108. package/dist/server/html.js +101 -0
  109. package/dist/server/stream.d.ts +4 -0
  110. package/dist/server/stream.js +72 -0
  111. package/dist/source-map.d.ts +17 -0
  112. package/dist/source-map.js +18 -0
  113. package/dist/tachyon-html.d.ts +110 -0
  114. package/dist/testing.d.ts +14 -0
  115. package/dist/testing.js +36 -0
  116. package/dist/typed.d.ts +9 -0
  117. package/dist/typed.js +4 -0
  118. package/dist/vite.d.ts +84 -0
  119. package/dist/vite.js +426 -0
  120. package/package.json +604 -0
package/dist/cli.js ADDED
@@ -0,0 +1,711 @@
1
+ #!/usr/bin/env node
2
+ import { access, mkdir, readFile, realpath, writeFile } from "node:fs/promises";
3
+ import { basename, dirname, join, relative } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { generateTachyonModuleTypes, generateTemplateTypes, pagesFromRouteFiles } from "./app.js";
6
+ import { generateClientModule, generateServerModule, generateServerStreamModule } from "./compiler/index.js";
7
+ import { generateScriptOnlyModule, transformSfcScript } from "./compiler/sfc.js";
8
+ import { diagnoseTachyonSfc, diagnosticFromCompilerError, formatDiagnostic } from "./diagnostics.js";
9
+ import { scanFileRoutes } from "./router-node.js";
10
+ import { appendInlineSourceMap, createSourceMap } from "./source-map.js";
11
+ import { err, ok } from "./result.js";
12
+ const usage = "Usage: tachyon-dom <compile|routes|dev|build|preview|add|typegen|init|language-server>. Use compile for templates, routes for file-route manifests, dev/build/preview with Vite, add for route files, typegen for template scopes, init for starters, and language-server for editor diagnostics.";
13
+ const commandUsage = {
14
+ add: "Usage: tachyon-dom add page <name> [--routes-dir src/routes] [--force]. Existing files are preserved unless --force is explicit.",
15
+ build: "Usage: tachyon-dom build [--host 127.0.0.1] [--port 4173]",
16
+ compile: "Usage: tachyon-dom compile <input> [--target client|server|stream] [--out file] [--reactive] [--no-sourcemap]",
17
+ dev: "Usage: tachyon-dom dev [--host 127.0.0.1] [--port 5173]",
18
+ init: "Usage: tachyon-dom init [--out dir] [--template basic|ssr] [--force]. Any conflict aborts all writes unless --force is explicit.",
19
+ "language-server": "Usage: tachyon-dom language-server --stdio",
20
+ preview: "Usage: tachyon-dom preview [--host 127.0.0.1] [--port 4173]",
21
+ routes: "Usage: tachyon-dom routes <routes-dir> [--out route-manifest.json]",
22
+ typegen: "Usage: tachyon-dom typegen <input> [--out file] [--type TemplateScope] [--module]",
23
+ };
24
+ export const isCreateEntrypoint = (entrypoint) => {
25
+ if (!entrypoint) {
26
+ return false;
27
+ }
28
+ const name = basename(entrypoint).replace(/\.(cmd|ps1)$/i, "");
29
+ return name === "create-tachyon-dom";
30
+ };
31
+ export const normalizeCliArgv = (argv, entrypoint = process.argv[1]) => {
32
+ if (!isCreateEntrypoint(entrypoint)) {
33
+ return argv;
34
+ }
35
+ if (argv[0] === "--help" || argv[0] === "-h" || argv[0] === "help") {
36
+ return ["init", "--help"];
37
+ }
38
+ const [outDir, ...rest] = argv;
39
+ if (!outDir || outDir.startsWith("-")) {
40
+ return ["init", ...argv];
41
+ }
42
+ return ["init", "--out", outDir, ...rest];
43
+ };
44
+ const packageVersion = async () => {
45
+ const packageUrl = new URL("../package.json", import.meta.url);
46
+ const packagePath = packageUrl.protocol === "file:" ? packageUrl : join(process.cwd(), "package.json");
47
+ const packageJson = JSON.parse(await readFile(packagePath, "utf8"));
48
+ return packageJson.version ?? "0.0.0";
49
+ };
50
+ const parseCompileArgs = (input, rest) => {
51
+ if (!input) {
52
+ return err("Usage: tachyon-dom compile <input> [--target client|server|stream] [--out file] [--reactive] [--no-sourcemap]");
53
+ }
54
+ const options = {
55
+ command: "compile",
56
+ input,
57
+ target: "client",
58
+ reactive: false,
59
+ sourcemap: true,
60
+ };
61
+ for (let index = 0; index < rest.length; index++) {
62
+ const arg = rest[index];
63
+ if (arg === "--target") {
64
+ const target = rest[++index];
65
+ if (target !== "client" && target !== "server" && target !== "stream") {
66
+ return err("--target must be client, server, or stream.");
67
+ }
68
+ options.target = target;
69
+ }
70
+ else if (arg === "--out") {
71
+ const output = rest[++index];
72
+ if (!output) {
73
+ return err("--out requires a file path.");
74
+ }
75
+ options.output = output;
76
+ }
77
+ else if (arg === "--reactive") {
78
+ options.reactive = true;
79
+ }
80
+ else if (arg === "--no-sourcemap") {
81
+ options.sourcemap = false;
82
+ }
83
+ else {
84
+ return err(`Unknown argument: ${arg}`);
85
+ }
86
+ }
87
+ return ok(options);
88
+ };
89
+ const parseRoutesArgs = (routesDir, rest) => {
90
+ if (!routesDir) {
91
+ return err("Usage: tachyon-dom routes <routes-dir> [--out route-manifest.json]");
92
+ }
93
+ const options = { command: "routes", routesDir };
94
+ for (let index = 0; index < rest.length; index++) {
95
+ const arg = rest[index];
96
+ if (arg === "--out") {
97
+ const output = rest[++index];
98
+ if (!output) {
99
+ return err("--out requires a file path.");
100
+ }
101
+ options.output = output;
102
+ }
103
+ else {
104
+ return err(`Unknown argument: ${arg}`);
105
+ }
106
+ }
107
+ return ok(options);
108
+ };
109
+ const parseServerArgs = (command, rest) => {
110
+ const options = { command, host: "127.0.0.1", port: command === "dev" ? 5173 : 4173 };
111
+ for (let index = 0; index < rest.length; index++) {
112
+ const arg = rest[index];
113
+ if (arg === "--host") {
114
+ const host = rest[++index];
115
+ if (!host) {
116
+ return err("--host requires a value.");
117
+ }
118
+ options.host = host;
119
+ }
120
+ else if (arg === "--port") {
121
+ const port = Number.parseInt(rest[++index] ?? "", 10);
122
+ if (!Number.isFinite(port)) {
123
+ return err("--port requires a number.");
124
+ }
125
+ options.port = port;
126
+ }
127
+ else {
128
+ return err(`Unknown argument: ${arg}`);
129
+ }
130
+ }
131
+ return ok(options);
132
+ };
133
+ const parseAddArgs = (kind, name, rest) => {
134
+ if (kind !== "page" || !name) {
135
+ return err("Usage: tachyon-dom add page <name> [--routes-dir src/routes]");
136
+ }
137
+ const options = { command: "add-page", name, routesDir: "src/routes" };
138
+ for (let index = 0; index < rest.length; index++) {
139
+ const arg = rest[index];
140
+ if (arg === "--routes-dir") {
141
+ const routesDir = rest[++index];
142
+ if (!routesDir) {
143
+ return err("--routes-dir requires a path.");
144
+ }
145
+ options.routesDir = routesDir;
146
+ }
147
+ else if (arg === "--force") {
148
+ options.force = true;
149
+ }
150
+ else {
151
+ return err(`Unknown argument: ${arg}`);
152
+ }
153
+ }
154
+ return ok(options);
155
+ };
156
+ const parseTypegenArgs = (input, rest) => {
157
+ if (!input) {
158
+ return err("Usage: tachyon-dom typegen <input> [--out file] [--type TemplateScope] [--module]");
159
+ }
160
+ const options = { command: "typegen", input, module: false };
161
+ for (let index = 0; index < rest.length; index++) {
162
+ const arg = rest[index];
163
+ if (arg === "--out") {
164
+ const output = rest[++index];
165
+ if (!output) {
166
+ return err("--out requires a file path.");
167
+ }
168
+ options.output = output;
169
+ }
170
+ else if (arg === "--type") {
171
+ const typeName = rest[++index];
172
+ if (!typeName) {
173
+ return err("--type requires a type name.");
174
+ }
175
+ options.typeName = typeName;
176
+ }
177
+ else if (arg === "--module") {
178
+ options.module = true;
179
+ }
180
+ else {
181
+ return err(`Unknown argument: ${arg}`);
182
+ }
183
+ }
184
+ return ok(options);
185
+ };
186
+ const parseInitArgs = (rest) => {
187
+ const options = { command: "init", outDir: ".", template: "basic" };
188
+ for (let index = 0; index < rest.length; index++) {
189
+ const arg = rest[index];
190
+ if (arg === "--out") {
191
+ const outDir = rest[++index];
192
+ if (!outDir) {
193
+ return err("--out requires a directory.");
194
+ }
195
+ options.outDir = outDir;
196
+ }
197
+ else if (arg === "--template") {
198
+ const template = rest[++index];
199
+ if (template !== "basic" && template !== "ssr") {
200
+ return err("--template must be basic or ssr.");
201
+ }
202
+ options.template = template;
203
+ }
204
+ else if (arg === "--force") {
205
+ options.force = true;
206
+ }
207
+ else {
208
+ return err(`Unknown argument: ${arg}`);
209
+ }
210
+ }
211
+ return ok(options);
212
+ };
213
+ const parseLanguageServerArgs = (rest) => rest.length === 1 && rest[0] === "--stdio"
214
+ ? ok({ command: "language-server", transport: "stdio" })
215
+ : err("Usage: tachyon-dom language-server --stdio");
216
+ export const parseArgs = (argv) => {
217
+ const [command, input, ...rest] = argv;
218
+ if (input === "--help" || input === "-h" || input === "help") {
219
+ return err(commandUsage[command ?? ""] ?? usage);
220
+ }
221
+ if (command === "compile") {
222
+ return parseCompileArgs(input ?? "", rest);
223
+ }
224
+ if (command === "routes") {
225
+ return parseRoutesArgs(input ?? "", rest);
226
+ }
227
+ if (command === "dev" || command === "preview" || command === "build") {
228
+ return parseServerArgs(command, [input, ...rest].filter((arg) => Boolean(arg)));
229
+ }
230
+ if (command === "add") {
231
+ return parseAddArgs(input ?? "", rest[0] ?? "", rest.slice(1));
232
+ }
233
+ if (command === "typegen") {
234
+ return parseTypegenArgs(input ?? "", rest);
235
+ }
236
+ if (command === "init") {
237
+ return parseInitArgs([input, ...rest].filter((arg) => Boolean(arg)));
238
+ }
239
+ if (command === "language-server") {
240
+ return parseLanguageServerArgs([input, ...rest].filter((arg) => Boolean(arg)));
241
+ }
242
+ return err(usage);
243
+ };
244
+ export const compileFile = async (options) => {
245
+ const source = await readFile(options.input, "utf8");
246
+ const result = diagnoseTachyonSfc(source);
247
+ if (!result.ok) {
248
+ return err(formatDiagnostic(result.error, options.input));
249
+ }
250
+ const script = transformSfcScript(result.value.descriptor.script);
251
+ if (!script.ok) {
252
+ return err(formatDiagnostic(diagnosticFromCompilerError(source, script.error), options.input));
253
+ }
254
+ const code = result.value.scriptOnly
255
+ ? generateScriptOnlyModule(options.target)
256
+ : options.target === "server"
257
+ ? generateServerModule(result.value.template)
258
+ : options.target === "stream"
259
+ ? generateServerStreamModule(result.value.template)
260
+ : generateClientModule(result.value.template, {
261
+ reactive: options.reactive,
262
+ ...(script.value.defaultScopeName ? { defaultScopeName: script.value.defaultScopeName } : {}),
263
+ });
264
+ const moduleCode = `${script.value.code}${code}`;
265
+ const output = options.sourcemap
266
+ ? appendInlineSourceMap(moduleCode, createSourceMap(source, options.input, options.output))
267
+ : moduleCode;
268
+ if (options.output) {
269
+ await writeFile(options.output, output);
270
+ }
271
+ return ok(output);
272
+ };
273
+ export const buildRouteManifestFile = async (options) => {
274
+ const manifest = await scanFileRoutes(options.routesDir);
275
+ const output = `${JSON.stringify(manifest, null, 2)}\n`;
276
+ if (options.output) {
277
+ await writeFile(options.output, output);
278
+ }
279
+ return ok(output);
280
+ };
281
+ const titleCase = (value) => value
282
+ .split(/[-_/\s]+/)
283
+ .filter(Boolean)
284
+ .map((part) => part.replace(/^\[\.\.\.(.+)]$/, "$1").replace(/^\[(.+)]$/, "$1"))
285
+ .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
286
+ .join(" ");
287
+ const generatedRoutesMarker = "// Generated by tachyon-dom. Do not edit by hand.";
288
+ const routeRegistrySource = (files, routesDir, registryFile) => {
289
+ const pages = pagesFromRouteFiles(files, { rootDir: routesDir }).sort((left, right) => left.path.localeCompare(right.path));
290
+ const imports = pages.map((page, index) => {
291
+ const specifier = relative(dirname(registryFile), page.file).replaceAll("\\", "/");
292
+ return `import routeSource${index} from ${JSON.stringify(`${specifier.startsWith(".") ? specifier : `./${specifier}`}?raw`)};`;
293
+ });
294
+ const entries = pages.map((page, index) => ` { assetPrefix: ${JSON.stringify(page.assetPrefix)}, fileName: ${JSON.stringify(page.fileName)}, path: ${JSON.stringify(page.path)}, template: routeSource${index} },`);
295
+ return `${generatedRoutesMarker}\n${imports.join("\n")}\n\nexport const pages = [\n${entries.join("\n")}\n] as const;\n`;
296
+ };
297
+ const pageSource = (title) => `<script lang="ts">
298
+ export const scope = () => ({
299
+ title: ${JSON.stringify(title)},
300
+ });
301
+ </script>
302
+ <section>
303
+ <h1>{title}</h1>
304
+ </section>
305
+ `;
306
+ export const addPageFiles = async (options) => {
307
+ const normalized = options.name.replace(/^\/+|\/+$/g, "");
308
+ const segments = normalized.split("/");
309
+ if (!normalized ||
310
+ normalized.includes("\\") ||
311
+ segments.some((segment) => !segment || segment === "." || segment === "..")) {
312
+ return err("Page name must be a route-local path without '..'.");
313
+ }
314
+ const targetDir = join(options.routesDir, normalized);
315
+ const title = titleCase(normalized);
316
+ const routeUrl = `/${normalized
317
+ .split("/")
318
+ .map((segment) => segment.startsWith("[...") && segment.endsWith("]")
319
+ ? `*${segment.slice(4, -1)}`
320
+ : segment.startsWith("[") && segment.endsWith("]")
321
+ ? `:${segment.slice(1, -1)}`
322
+ : segment)
323
+ .join("/")}/`;
324
+ const pageFile = join(targetDir, "page.td");
325
+ const declarationFile = `${pageFile}.d.ts`;
326
+ const registryFile = join(dirname(options.routesDir), "routes.generated.ts");
327
+ let overwriting = false;
328
+ let overwritingDeclarations = false;
329
+ let overwritingRegistry = false;
330
+ try {
331
+ await access(pageFile);
332
+ overwriting = true;
333
+ }
334
+ catch {
335
+ // The target does not exist yet.
336
+ }
337
+ try {
338
+ await access(declarationFile);
339
+ overwritingDeclarations = true;
340
+ }
341
+ catch {
342
+ // The declaration does not exist yet.
343
+ }
344
+ if (!options.force && (overwriting || overwritingDeclarations)) {
345
+ const conflicts = [
346
+ overwriting ? pageFile : undefined,
347
+ overwritingDeclarations ? declarationFile : undefined,
348
+ ].filter((file) => file !== undefined);
349
+ return err(`Refusing to overwrite existing page files:\n${conflicts.join("\n")}`);
350
+ }
351
+ try {
352
+ const registry = await readFile(registryFile, "utf8");
353
+ overwritingRegistry = true;
354
+ if (!registry.startsWith(generatedRoutesMarker) && !options.force) {
355
+ return err(`Refusing to replace non-generated route registry: ${registryFile}`);
356
+ }
357
+ }
358
+ catch (error) {
359
+ if (error.code !== "ENOENT")
360
+ throw error;
361
+ }
362
+ const source = pageSource(title);
363
+ const declarations = generateTachyonModuleTypes(source);
364
+ if (!declarations.ok)
365
+ return declarations;
366
+ let existingRoutes = [];
367
+ try {
368
+ existingRoutes = await scanFileRoutes(options.routesDir);
369
+ }
370
+ catch (error) {
371
+ if (error.code !== "ENOENT")
372
+ throw error;
373
+ }
374
+ const routeFiles = existingRoutes
375
+ .filter((route) => route.kind === "template" && route.file !== pageFile)
376
+ .map((route) => route.file);
377
+ routeFiles.push(pageFile);
378
+ await mkdir(targetDir, { recursive: true });
379
+ await writeFile(pageFile, source);
380
+ await writeFile(declarationFile, declarations.value);
381
+ await writeFile(registryFile, routeRegistrySource(routeFiles, options.routesDir, registryFile));
382
+ return ok([
383
+ overwriting ? `Overwrote ${pageFile}.` : `Created ${pageFile}.`,
384
+ `Route URL: ${routeUrl}`,
385
+ "Edit the generated page.td to define the route markup and scope.",
386
+ overwritingDeclarations ? `Overwrote ${declarationFile}.` : `Generated declarations: ${declarationFile}`,
387
+ overwritingRegistry ? `Updated route registry: ${registryFile}` : `Generated route registry: ${registryFile}`,
388
+ ].join("\n"));
389
+ };
390
+ export const generateTemplateTypesFile = async (options) => {
391
+ const source = await readFile(options.input, "utf8");
392
+ const result = options.module === true
393
+ ? generateTachyonModuleTypes(source, options.typeName ? { typeName: options.typeName } : {})
394
+ : generateTemplateTypes(source, options.typeName ? { typeName: options.typeName } : {});
395
+ if (!result.ok) {
396
+ return result;
397
+ }
398
+ if (options.output) {
399
+ await mkdir(dirname(options.output), { recursive: true });
400
+ await writeFile(options.output, result.value);
401
+ }
402
+ return result;
403
+ };
404
+ const starterPageSource = () => `<script lang="ts">
405
+ export const scope = () => ({
406
+ message: "Edit src/routes/index/page.td to start building.",
407
+ title: "Welcome",
408
+ });
409
+ </script>
410
+ <section>
411
+ <h1>{title}</h1>
412
+ <p>{message}</p>
413
+ </section>
414
+ `;
415
+ const starterAppSource = () => `import { defineApp } from "tachyon-dom/app";
416
+ import { pages } from "./routes.generated";
417
+
418
+ export const app = defineApp({
419
+ lang: "en",
420
+ title: "Tachyon App",
421
+ templateWhitespace: "condense",
422
+ pages,
423
+ });
424
+ `;
425
+ const starterClientSource = () => `// Client entry for Tachyon DOM runtime code.
426
+ // Add progressive enhancements, client routing, or island hydration imports here.
427
+ `;
428
+ const starterServerSource = () => `import { renderAppResponse } from "tachyon-dom/app";
429
+ import { app } from "./app";
430
+
431
+ export const render = (path = "/") => renderAppResponse(app, path, { minify: true });
432
+ `;
433
+ const starterSmokeTestSource = () => `import { describe, expect, it } from "vitest";
434
+ import { app } from "./app";
435
+
436
+ describe("starter app", () => {
437
+ it("renders the starter page", () => {
438
+ expect(app.renderRoute("/")).toContain("<h1>Welcome</h1>");
439
+ });
440
+ });
441
+ `;
442
+ const starterViteConfigSource = () => `import { defineConfig } from "vite";
443
+ import { loadRouteApp, tachyonApp, tachyonDom } from "tachyon-dom/vite";
444
+
445
+ const templateWhitespace = "condense" as const;
446
+ const app = await loadRouteApp({
447
+ lang: "en",
448
+ routesDir: "src/routes",
449
+ title: "Tachyon App",
450
+ templateWhitespace,
451
+ });
452
+
453
+ export default defineConfig({
454
+ build: {
455
+ rollupOptions: {
456
+ input: "src/client/main.ts",
457
+ },
458
+ },
459
+ plugins: [tachyonDom({ reactive: true, templateWhitespace }), tachyonApp(app, { appScript: "/src/client/main.ts" })],
460
+ });
461
+ `;
462
+ const starterTsConfigSource = () => `${JSON.stringify({
463
+ compilerOptions: {
464
+ exactOptionalPropertyTypes: true,
465
+ lib: ["ES2022", "DOM", "DOM.Iterable"],
466
+ module: "ESNext",
467
+ moduleResolution: "Bundler",
468
+ noUncheckedIndexedAccess: true,
469
+ skipLibCheck: false,
470
+ strict: true,
471
+ target: "ES2022",
472
+ types: ["vite/client", "node", "tachyon-dom/td-modules"],
473
+ },
474
+ include: ["src", "vite.config.ts"],
475
+ }, null, 2)}\n`;
476
+ const starterGitignoreSource = () => `node_modules/
477
+ dist/
478
+ .vite/
479
+ coverage/
480
+ `;
481
+ const starterCiSource = () => `name: CI
482
+
483
+ on:
484
+ push:
485
+ pull_request:
486
+
487
+ jobs:
488
+ test:
489
+ runs-on: ubuntu-latest
490
+ steps:
491
+ - uses: actions/checkout@v5
492
+ - uses: pnpm/action-setup@v4
493
+ - uses: actions/setup-node@v5
494
+ with:
495
+ node-version: 24
496
+ cache: pnpm
497
+ - run: pnpm install --frozen-lockfile
498
+ - run: pnpm typecheck
499
+ - run: pnpm test
500
+ - run: pnpm build
501
+ `;
502
+ const starterReadmeSource = (template) => `# Tachyon DOM Starter
503
+
504
+ Edit \`src/routes/index/page.td\` to start building. Route-local \`.td\` files are the source of truth for page markup.
505
+
506
+ Use \`src/client/main.ts\` for client-side runtime code that should be bundled by Vite. Do not put application code in \`public/client/main.js\`; reserve \`public/\` for static assets such as images, icons, manifests, and service workers.
507
+
508
+ Run:
509
+
510
+ \`\`\`sh
511
+ pnpm install
512
+ pnpm dev
513
+ pnpm typecheck
514
+ pnpm test
515
+ pnpm build
516
+ \`\`\`
517
+
518
+ Add a page with:
519
+
520
+ \`\`\`sh
521
+ tachyon-dom add page settings/profile
522
+ \`\`\`
523
+
524
+ ## Route registration
525
+
526
+ \`src/routes.generated.ts\` is the generated route registry used by the app, Vite, tests, and SSR. \`tachyon-dom add page\` updates it automatically. Do not edit it by hand. Dynamic directories use \`[id]\` and catch-all directories use \`[...slug]\` across the app and server route manifest.
527
+
528
+ The Vite plugin refreshes adjacent \`.td.d.ts\` declarations during development and builds. The starter and \`add page\` create the initial declaration, so \`pnpm typecheck\` works before the first Vite run. Do not run per-file type generation manually.
529
+
530
+ Adapters are lower-level deployment APIs for Node, Workers, and Lambda composition. A standard Tachyon DOM app should keep \`.td\` templates and the Vite plugins on the main development path.
531
+
532
+ Template: \`${template}\`.
533
+ ${template === "ssr" ? "\n## SSR entry\n\nThe `ssr` template includes `src/server.ts` with a small `render(path)` helper for server-side composition.\n" : ""}
534
+ `;
535
+ const starterPackageJsonSource = () => `${JSON.stringify({
536
+ scripts: {
537
+ build: "vite build",
538
+ dev: "vite",
539
+ preview: "vite preview",
540
+ test: "vitest run",
541
+ typecheck: "tsc --noEmit",
542
+ },
543
+ dependencies: {
544
+ "tachyon-dom": "^0.1.0",
545
+ },
546
+ devDependencies: {
547
+ "@types/node": "^24.0.3",
548
+ typescript: "^5.8.3",
549
+ vite: "^8.0.0",
550
+ vitest: "^4.1.9",
551
+ },
552
+ packageManager: "pnpm@10.32.1",
553
+ type: "module",
554
+ }, null, 2)}\n`;
555
+ export const createStarterFiles = async (options) => {
556
+ const managedFiles = [
557
+ "src/routes/index/page.td",
558
+ "src/routes/index/page.td.d.ts",
559
+ "src/routes.generated.ts",
560
+ "src/app.ts",
561
+ "src/client/main.ts",
562
+ "src/app.test.ts",
563
+ ...(options.template === "ssr" ? ["src/server.ts"] : []),
564
+ "vite.config.ts",
565
+ "tsconfig.json",
566
+ ".gitignore",
567
+ ".github/workflows/ci.yml",
568
+ "README.md",
569
+ "package.json",
570
+ ].map((file) => join(options.outDir, file));
571
+ const conflicts = [];
572
+ for (const file of managedFiles) {
573
+ try {
574
+ await access(file);
575
+ conflicts.push(file);
576
+ }
577
+ catch {
578
+ // Missing targets are safe to create after the full preflight.
579
+ }
580
+ }
581
+ if (conflicts.length > 0 && !options.force) {
582
+ return err(`Refusing to overwrite existing starter files:\n${conflicts.join("\n")}`);
583
+ }
584
+ await mkdir(join(options.outDir, "src", "routes", "index"), { recursive: true });
585
+ await mkdir(join(options.outDir, "src", "client"), { recursive: true });
586
+ await mkdir(join(options.outDir, ".github", "workflows"), { recursive: true });
587
+ const starterPageFile = join(options.outDir, "src", "routes", "index", "page.td");
588
+ const starterRegistryFile = join(options.outDir, "src", "routes.generated.ts");
589
+ const starterSource = starterPageSource();
590
+ const starterDeclarations = generateTachyonModuleTypes(starterSource);
591
+ if (!starterDeclarations.ok)
592
+ return starterDeclarations;
593
+ await writeFile(starterPageFile, starterSource);
594
+ await writeFile(`${starterPageFile}.d.ts`, starterDeclarations.value);
595
+ await writeFile(starterRegistryFile, routeRegistrySource([starterPageFile], join(options.outDir, "src", "routes"), starterRegistryFile));
596
+ await writeFile(join(options.outDir, "src", "app.ts"), starterAppSource());
597
+ await writeFile(join(options.outDir, "src", "client", "main.ts"), starterClientSource());
598
+ await writeFile(join(options.outDir, "src", "app.test.ts"), starterSmokeTestSource());
599
+ if (options.template === "ssr") {
600
+ await writeFile(join(options.outDir, "src", "server.ts"), starterServerSource());
601
+ }
602
+ await writeFile(join(options.outDir, "vite.config.ts"), starterViteConfigSource());
603
+ await writeFile(join(options.outDir, "tsconfig.json"), starterTsConfigSource());
604
+ await writeFile(join(options.outDir, ".gitignore"), starterGitignoreSource());
605
+ await writeFile(join(options.outDir, ".github", "workflows", "ci.yml"), starterCiSource());
606
+ await writeFile(join(options.outDir, "README.md"), starterReadmeSource(options.template));
607
+ await writeFile(join(options.outDir, "package.json"), starterPackageJsonSource());
608
+ return ok([
609
+ conflicts.length > 0
610
+ ? `Overwrote Tachyon DOM starter files in ${options.outDir}.`
611
+ : `Created Tachyon DOM starter in ${options.outDir}. Edit src/routes/index/page.td to start building.`,
612
+ ...conflicts.map((file) => `Overwrote ${file}.`),
613
+ ].join("\n"));
614
+ };
615
+ export const serverCommandMessage = (options) => options.command === "build"
616
+ ? "Build the tachyon-dom app with Vite."
617
+ : options.command === "dev"
618
+ ? `Start Vite dev server with tachyon-dom plugin on ${options.host}:${options.port}.`
619
+ : `Start Vite preview server for the built tachyon-dom app on ${options.host}:${options.port}.`;
620
+ const startViteServer = async (options) => {
621
+ const vite = await import("vite");
622
+ if (options.command === "build") {
623
+ await vite.build();
624
+ return ok(serverCommandMessage(options));
625
+ }
626
+ const server = options.command === "dev"
627
+ ? await vite.createServer({
628
+ server: { host: options.host, port: options.port },
629
+ })
630
+ : await vite.preview({
631
+ preview: { host: options.host, port: options.port },
632
+ });
633
+ if ("listen" in server && typeof server.listen === "function") {
634
+ await server.listen();
635
+ }
636
+ const urls = "resolvedUrls" in server ? server.resolvedUrls?.local : undefined;
637
+ return ok(urls?.join("\n") ?? serverCommandMessage(options));
638
+ };
639
+ export const runCli = async (argv = process.argv.slice(2), entrypoint = process.argv[1]) => {
640
+ argv = normalizeCliArgv(argv, entrypoint);
641
+ if (argv[0] === "--help" || argv[0] === "-h" || argv[0] === "help") {
642
+ console.log(usage);
643
+ return 0;
644
+ }
645
+ if (argv[0] === "--version" || argv[0] === "-v") {
646
+ console.log(await packageVersion());
647
+ return 0;
648
+ }
649
+ if ((argv[1] === "--help" || argv[1] === "-h" || argv[1] === "help") && commandUsage[argv[0] ?? ""]) {
650
+ console.log(commandUsage[argv[0] ?? ""]);
651
+ return 0;
652
+ }
653
+ const parsed = parseArgs(argv);
654
+ if (!parsed.ok) {
655
+ console.error(parsed.error);
656
+ return 1;
657
+ }
658
+ let result;
659
+ switch (parsed.value.command) {
660
+ case "language-server": {
661
+ const { startLanguageServer } = await import("./language-server.js");
662
+ startLanguageServer();
663
+ return 0;
664
+ }
665
+ case "dev":
666
+ case "build":
667
+ case "preview":
668
+ result = await startViteServer(parsed.value);
669
+ break;
670
+ case "init":
671
+ result = await createStarterFiles(parsed.value);
672
+ break;
673
+ case "add-page":
674
+ result = await addPageFiles(parsed.value);
675
+ break;
676
+ case "typegen":
677
+ result = await generateTemplateTypesFile(parsed.value);
678
+ break;
679
+ case "compile":
680
+ result = await compileFile(parsed.value);
681
+ break;
682
+ case "routes":
683
+ result = await buildRouteManifestFile({
684
+ routesDir: parsed.value.routesDir,
685
+ ...(parsed.value.output ? { output: parsed.value.output } : {}),
686
+ });
687
+ break;
688
+ }
689
+ if (!result.ok) {
690
+ console.error(result.error);
691
+ return 1;
692
+ }
693
+ if (!("output" in parsed.value) || !parsed.value.output) {
694
+ console.log(result.value);
695
+ }
696
+ return 0;
697
+ };
698
+ export const isCliEntrypoint = async (scriptPath = process.argv[1], moduleUrl = import.meta.url) => {
699
+ if (!scriptPath) {
700
+ return false;
701
+ }
702
+ try {
703
+ return pathToFileURL(await realpath(scriptPath)).href === moduleUrl;
704
+ }
705
+ catch {
706
+ return scriptPath.endsWith("cli.js") && moduleUrl.endsWith("/cli.js");
707
+ }
708
+ };
709
+ if (await isCliEntrypoint()) {
710
+ process.exitCode = await runCli();
711
+ }