@timber-js/app 0.1.0 → 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.
Files changed (141) hide show
  1. package/dist/index.js +5 -2
  2. package/dist/index.js.map +1 -1
  3. package/dist/plugins/entries.d.ts.map +1 -1
  4. package/package.json +43 -58
  5. package/src/adapters/cloudflare.ts +325 -0
  6. package/src/adapters/nitro.ts +366 -0
  7. package/src/adapters/types.ts +63 -0
  8. package/src/cache/index.ts +91 -0
  9. package/src/cache/redis-handler.ts +91 -0
  10. package/src/cache/register-cached-function.ts +99 -0
  11. package/src/cache/singleflight.ts +26 -0
  12. package/src/cache/stable-stringify.ts +21 -0
  13. package/src/cache/timber-cache.ts +116 -0
  14. package/src/cli.ts +201 -0
  15. package/src/client/browser-entry.ts +663 -0
  16. package/src/client/error-boundary.tsx +209 -0
  17. package/src/client/form.tsx +200 -0
  18. package/src/client/head.ts +61 -0
  19. package/src/client/history.ts +46 -0
  20. package/src/client/index.ts +60 -0
  21. package/src/client/link-navigate-interceptor.tsx +62 -0
  22. package/src/client/link-status-provider.tsx +40 -0
  23. package/src/client/link.tsx +310 -0
  24. package/src/client/nuqs-adapter.tsx +117 -0
  25. package/src/client/router-ref.ts +25 -0
  26. package/src/client/router.ts +563 -0
  27. package/src/client/segment-cache.ts +194 -0
  28. package/src/client/segment-context.ts +57 -0
  29. package/src/client/ssr-data.ts +95 -0
  30. package/src/client/types.ts +4 -0
  31. package/src/client/unload-guard.ts +34 -0
  32. package/src/client/use-cookie.ts +122 -0
  33. package/src/client/use-link-status.ts +46 -0
  34. package/src/client/use-navigation-pending.ts +47 -0
  35. package/src/client/use-params.ts +71 -0
  36. package/src/client/use-pathname.ts +43 -0
  37. package/src/client/use-query-states.ts +133 -0
  38. package/src/client/use-router.ts +77 -0
  39. package/src/client/use-search-params.ts +74 -0
  40. package/src/client/use-selected-layout-segment.ts +110 -0
  41. package/src/content/index.ts +13 -0
  42. package/src/cookies/define-cookie.ts +137 -0
  43. package/src/cookies/index.ts +9 -0
  44. package/src/fonts/ast.ts +359 -0
  45. package/src/fonts/css.ts +68 -0
  46. package/src/fonts/fallbacks.ts +248 -0
  47. package/src/fonts/google.ts +332 -0
  48. package/src/fonts/local.ts +177 -0
  49. package/src/fonts/types.ts +88 -0
  50. package/src/index.ts +413 -0
  51. package/src/plugins/adapter-build.ts +118 -0
  52. package/src/plugins/build-manifest.ts +323 -0
  53. package/src/plugins/build-report.ts +353 -0
  54. package/src/plugins/cache-transform.ts +199 -0
  55. package/src/plugins/chunks.ts +90 -0
  56. package/src/plugins/content.ts +136 -0
  57. package/src/plugins/dev-error-overlay.ts +230 -0
  58. package/src/plugins/dev-logs.ts +280 -0
  59. package/src/plugins/dev-server.ts +389 -0
  60. package/src/plugins/dynamic-transform.ts +161 -0
  61. package/src/plugins/entries.ts +207 -0
  62. package/src/plugins/fonts.ts +581 -0
  63. package/src/plugins/mdx.ts +179 -0
  64. package/src/plugins/react-prod.ts +56 -0
  65. package/src/plugins/routing.ts +419 -0
  66. package/src/plugins/server-action-exports.ts +220 -0
  67. package/src/plugins/server-bundle.ts +113 -0
  68. package/src/plugins/shims.ts +168 -0
  69. package/src/plugins/static-build.ts +207 -0
  70. package/src/routing/codegen.ts +396 -0
  71. package/src/routing/index.ts +14 -0
  72. package/src/routing/interception.ts +173 -0
  73. package/src/routing/scanner.ts +487 -0
  74. package/src/routing/status-file-lint.ts +114 -0
  75. package/src/routing/types.ts +100 -0
  76. package/src/search-params/analyze.ts +192 -0
  77. package/src/search-params/codecs.ts +153 -0
  78. package/src/search-params/create.ts +314 -0
  79. package/src/search-params/index.ts +23 -0
  80. package/src/search-params/registry.ts +31 -0
  81. package/src/server/access-gate.tsx +142 -0
  82. package/src/server/action-client.ts +473 -0
  83. package/src/server/action-handler.ts +325 -0
  84. package/src/server/actions.ts +236 -0
  85. package/src/server/asset-headers.ts +81 -0
  86. package/src/server/body-limits.ts +102 -0
  87. package/src/server/build-manifest.ts +234 -0
  88. package/src/server/canonicalize.ts +90 -0
  89. package/src/server/client-module-map.ts +58 -0
  90. package/src/server/csrf.ts +79 -0
  91. package/src/server/deny-renderer.ts +302 -0
  92. package/src/server/dev-logger.ts +419 -0
  93. package/src/server/dev-span-processor.ts +78 -0
  94. package/src/server/dev-warnings.ts +282 -0
  95. package/src/server/early-hints-sender.ts +55 -0
  96. package/src/server/early-hints.ts +142 -0
  97. package/src/server/error-boundary-wrapper.ts +69 -0
  98. package/src/server/error-formatter.ts +184 -0
  99. package/src/server/flush.ts +182 -0
  100. package/src/server/form-data.ts +176 -0
  101. package/src/server/form-flash.ts +93 -0
  102. package/src/server/html-injectors.ts +445 -0
  103. package/src/server/index.ts +222 -0
  104. package/src/server/instrumentation.ts +136 -0
  105. package/src/server/logger.ts +145 -0
  106. package/src/server/manifest-status-resolver.ts +215 -0
  107. package/src/server/metadata-render.ts +527 -0
  108. package/src/server/metadata-routes.ts +189 -0
  109. package/src/server/metadata.ts +263 -0
  110. package/src/server/middleware-runner.ts +32 -0
  111. package/src/server/nuqs-ssr-provider.tsx +63 -0
  112. package/src/server/pipeline.ts +555 -0
  113. package/src/server/prerender.ts +139 -0
  114. package/src/server/primitives.ts +264 -0
  115. package/src/server/proxy.ts +43 -0
  116. package/src/server/request-context.ts +554 -0
  117. package/src/server/route-element-builder.ts +395 -0
  118. package/src/server/route-handler.ts +153 -0
  119. package/src/server/route-matcher.ts +316 -0
  120. package/src/server/rsc-entry/api-handler.ts +112 -0
  121. package/src/server/rsc-entry/error-renderer.ts +177 -0
  122. package/src/server/rsc-entry/helpers.ts +147 -0
  123. package/src/server/rsc-entry/index.ts +688 -0
  124. package/src/server/rsc-entry/ssr-bridge.ts +18 -0
  125. package/src/server/slot-resolver.ts +359 -0
  126. package/src/server/ssr-entry.ts +161 -0
  127. package/src/server/ssr-render.ts +200 -0
  128. package/src/server/status-code-resolver.ts +282 -0
  129. package/src/server/tracing.ts +281 -0
  130. package/src/server/tree-builder.ts +354 -0
  131. package/src/server/types.ts +150 -0
  132. package/src/shims/font-google.ts +67 -0
  133. package/src/shims/headers.ts +11 -0
  134. package/src/shims/image.ts +48 -0
  135. package/src/shims/link.ts +9 -0
  136. package/src/shims/navigation-client.ts +52 -0
  137. package/src/shims/navigation.ts +31 -0
  138. package/src/shims/server-only-noop.js +5 -0
  139. package/src/utils/directive-parser.ts +529 -0
  140. package/src/utils/format.ts +10 -0
  141. package/src/utils/startup-timer.ts +102 -0
@@ -0,0 +1,99 @@
1
+ import { createHash } from 'node:crypto';
2
+ import type { CacheHandler } from './index';
3
+ import { stableStringify } from './stable-stringify';
4
+ import { createSingleflight } from './singleflight';
5
+
6
+ const singleflight = createSingleflight();
7
+
8
+ // Prop names that suggest request-specific data — triggers dev warning for "use cache" components.
9
+ const REQUEST_SPECIFIC_PROPS = new Set([
10
+ 'cookies',
11
+ 'cookie',
12
+ 'session',
13
+ 'sessionId',
14
+ 'token',
15
+ 'authorization',
16
+ 'auth',
17
+ 'headers',
18
+ ]);
19
+
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ export interface RegisterCachedFunctionOptions<Fn extends (...args: any[]) => any> {
22
+ ttl: number;
23
+ id: string;
24
+ tags?: string[] | ((...args: Parameters<Fn>) => string[]);
25
+ /** True when the cached function is a React component (PascalCase name). */
26
+ isComponent?: boolean;
27
+ }
28
+
29
+ /**
30
+ * Generate a SHA-256 cache key from a stable function ID and serialized args.
31
+ */
32
+ function generateKey(id: string, args: unknown[]): string {
33
+ const raw = id + ':' + stableStringify(args);
34
+ return createHash('sha256').update(raw).digest('hex');
35
+ }
36
+
37
+ /**
38
+ * Resolve tags from options — supports static array or function form.
39
+ */
40
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
+ function resolveTags<Fn extends (...args: any[]) => any>(
42
+ opts: RegisterCachedFunctionOptions<Fn>,
43
+ args: Parameters<Fn>
44
+ ): string[] {
45
+ if (!opts.tags) return [];
46
+ if (Array.isArray(opts.tags)) return opts.tags;
47
+ return opts.tags(...args);
48
+ }
49
+
50
+ /**
51
+ * Checks if component props contain request-specific keys and emits a dev warning.
52
+ * Only runs when process.env.NODE_ENV !== 'production'.
53
+ */
54
+ function warnRequestSpecificProps(id: string, props: unknown): void {
55
+ if (typeof props !== 'object' || props === null) return;
56
+ const keys = Object.keys(props);
57
+ const suspicious = keys.filter((k) => REQUEST_SPECIFIC_PROPS.has(k.toLowerCase()));
58
+ if (suspicious.length > 0) {
59
+ console.warn(
60
+ `[timber] "use cache" component ${id} received request-specific props: ${suspicious.join(', ')}. ` +
61
+ `This may serve one user's cached render to another user. ` +
62
+ `Remove request-specific data from props or remove "use cache".`
63
+ );
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Runtime for the "use cache" directive transform. Wraps an async function
69
+ * with caching using the same cache handler as timber.cache.
70
+ *
71
+ * The stable `id` (file path + function name) ensures cache keys are consistent
72
+ * across builds. Args/props are hashed with SHA-256 for the per-call key.
73
+ */
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
+ export function registerCachedFunction<Fn extends (...args: any[]) => Promise<any>>(
76
+ fn: Fn,
77
+ opts: RegisterCachedFunctionOptions<Fn>,
78
+ handler: CacheHandler
79
+ ): (...args: Parameters<Fn>) => Promise<Awaited<ReturnType<Fn>>> {
80
+ return async (...args: Parameters<Fn>): Promise<Awaited<ReturnType<Fn>>> => {
81
+ // Dev-mode warning for components with request-specific props
82
+ if (opts.isComponent && process.env.NODE_ENV !== 'production' && args.length > 0) {
83
+ warnRequestSpecificProps(opts.id, args[0]);
84
+ }
85
+
86
+ const key = generateKey(opts.id, args);
87
+ const cached = await handler.get(key);
88
+
89
+ if (cached && !cached.stale) {
90
+ return cached.value as Awaited<ReturnType<Fn>>;
91
+ }
92
+
93
+ // Cache miss or stale — execute with singleflight
94
+ const result = await singleflight.do(key, () => fn(...args));
95
+ const tags = resolveTags(opts, args);
96
+ await handler.set(key, result, { ttl: opts.ttl, tags });
97
+ return result as Awaited<ReturnType<Fn>>;
98
+ };
99
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Singleflight coalesces concurrent calls with the same key into a single
3
+ * execution. All callers receive the same result (or error).
4
+ *
5
+ * Per-process, in-memory. Each process coalesces independently.
6
+ */
7
+ export interface Singleflight {
8
+ do<T>(key: string, fn: () => Promise<T>): Promise<T>;
9
+ }
10
+
11
+ export function createSingleflight(): Singleflight {
12
+ const inflight = new Map<string, Promise<unknown>>();
13
+
14
+ return {
15
+ do<T>(key: string, fn: () => Promise<T>): Promise<T> {
16
+ const existing = inflight.get(key);
17
+ if (existing) return existing as Promise<T>;
18
+
19
+ const promise = fn().finally(() => {
20
+ inflight.delete(key);
21
+ });
22
+ inflight.set(key, promise);
23
+ return promise;
24
+ },
25
+ };
26
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Deterministic JSON serialization with sorted object keys.
3
+ * Used for cache key generation — ensures { a: 1, b: 2 } and { b: 2, a: 1 }
4
+ * produce the same string.
5
+ */
6
+ export function stableStringify(value: unknown): string {
7
+ if (value === null || value === undefined) return JSON.stringify(value);
8
+ if (typeof value !== 'object') return JSON.stringify(value);
9
+ if (Array.isArray(value)) {
10
+ return '[' + value.map((item) => stableStringify(item)).join(',') + ']';
11
+ }
12
+
13
+ const obj = value as Record<string, unknown>;
14
+ const keys = Object.keys(obj).sort();
15
+ const pairs: string[] = [];
16
+ for (const key of keys) {
17
+ if (obj[key] === undefined) continue;
18
+ pairs.push(JSON.stringify(key) + ':' + stableStringify(obj[key]));
19
+ }
20
+ return '{' + pairs.join(',') + '}';
21
+ }
@@ -0,0 +1,116 @@
1
+ import { createHash } from 'node:crypto';
2
+ import type { CacheHandler, CacheOptions } from './index';
3
+ import { stableStringify } from './stable-stringify';
4
+ import { createSingleflight } from './singleflight';
5
+ import { addSpanEvent } from '#/server/tracing.js';
6
+
7
+ const singleflight = createSingleflight();
8
+
9
+ /**
10
+ * Generate a SHA-256 cache key from function identity and serialized args.
11
+ */
12
+ function defaultKeyGenerator(fnId: string, args: unknown[]): string {
13
+ const raw = fnId + ':' + stableStringify(args);
14
+ return createHash('sha256').update(raw).digest('hex');
15
+ }
16
+
17
+ /**
18
+ * Resolve tags from the options — supports static array or function form.
19
+ */
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ function resolveTags<Fn extends (...args: any[]) => any>(
22
+ opts: CacheOptions<Fn>,
23
+ args: Parameters<Fn>
24
+ ): string[] {
25
+ if (!opts.tags) return [];
26
+ if (Array.isArray(opts.tags)) return opts.tags;
27
+ return opts.tags(...args);
28
+ }
29
+
30
+ // Counter for generating unique function IDs when no explicit key is provided.
31
+ let fnIdCounter = 0;
32
+
33
+ /**
34
+ * Creates a cached wrapper around an async function.
35
+ *
36
+ * - SHA-256 default keys with normalized JSON args
37
+ * - Singleflight: concurrent misses → single execution
38
+ * - SWR: serve stale immediately, background refetch
39
+ * - Tags as string[] or function of args
40
+ * - No ALS dependency
41
+ *
42
+ * Cache hits/misses are recorded as OTEL span events on the enclosing
43
+ * span (not child spans). The DevSpanProcessor reads these for dev log output.
44
+ */
45
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
+ export function createCache<Fn extends (...args: any[]) => Promise<any>>(
47
+ fn: Fn,
48
+ opts: CacheOptions<Fn>,
49
+ handler: CacheHandler
50
+ ): (...args: Parameters<Fn>) => Promise<Awaited<ReturnType<Fn>>> {
51
+ const fnId = `timber-cache:${fnIdCounter++}`;
52
+
53
+ return async (...args: Parameters<Fn>): Promise<Awaited<ReturnType<Fn>>> => {
54
+ const key = opts.key ? opts.key(...args) : defaultKeyGenerator(fnId, args);
55
+
56
+ const cacheStart = performance.now();
57
+ const cached = await handler.get(key);
58
+
59
+ if (cached && !cached.stale) {
60
+ // Record as OTEL span event on enclosing span (not a child span)
61
+ await addSpanEvent('timber.cache.hit', {
62
+ key,
63
+ duration_ms: Math.round(performance.now() - cacheStart),
64
+ });
65
+ return cached.value as Awaited<ReturnType<Fn>>;
66
+ }
67
+
68
+ if (cached && cached.stale && opts.staleWhileRevalidate) {
69
+ // Record stale cache hit as OTEL span event
70
+ await addSpanEvent('timber.cache.hit', {
71
+ key,
72
+ duration_ms: Math.round(performance.now() - cacheStart),
73
+ stale: true,
74
+ });
75
+ // Serve stale immediately, trigger background refetch
76
+ singleflight
77
+ .do(`swr:${key}`, async () => {
78
+ try {
79
+ const fresh = await fn(...args);
80
+ const tags = resolveTags(opts, args);
81
+ await handler.set(key, fresh, { ttl: opts.ttl, tags });
82
+ } catch {
83
+ // Failed refetch — stale entry continues to be served.
84
+ // Error is swallowed per design doc: "Error is logged."
85
+ }
86
+ })
87
+ .catch(() => {
88
+ // Singleflight promise rejection handled — stale continues.
89
+ });
90
+ return cached.value as Awaited<ReturnType<Fn>>;
91
+ }
92
+
93
+ // Cache miss (or stale without SWR) — execute with singleflight
94
+ const result = await singleflight.do(key, () => fn(...args));
95
+ const tags = resolveTags(opts, args);
96
+ await handler.set(key, result, { ttl: opts.ttl, tags });
97
+
98
+ // Record cache miss as OTEL span event
99
+ await addSpanEvent('timber.cache.miss', {
100
+ key,
101
+ duration_ms: Math.round(performance.now() - cacheStart),
102
+ });
103
+
104
+ return result as Awaited<ReturnType<Fn>>;
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Invalidate cache entries by tag or key.
110
+ */
111
+ createCache.invalidate = async function invalidate(
112
+ handler: CacheHandler,
113
+ opts: { key?: string; tag?: string }
114
+ ): Promise<void> {
115
+ await handler.invalidate(opts);
116
+ };
package/src/cli.ts ADDED
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+
3
+ // timber.js CLI
4
+ //
5
+ // Wraps Vite commands with timber-specific behavior.
6
+ // See design/18-build-system.md §"CLI".
7
+ //
8
+ // Commands:
9
+ // timber dev — Start Vite dev server with HMR
10
+ // timber build — Run multi-environment build via createBuilder/buildApp
11
+ // timber preview — Serve the production build
12
+ // timber check — Validate types + routes without building
13
+
14
+ const COMMANDS = ['dev', 'build', 'preview', 'check'] as const;
15
+ type Command = (typeof COMMANDS)[number];
16
+
17
+ export interface ParsedArgs {
18
+ command: Command;
19
+ config: string | undefined;
20
+ }
21
+
22
+ export interface CommandOptions {
23
+ config?: string;
24
+ }
25
+
26
+ /**
27
+ * Parse CLI arguments into a structured command + options.
28
+ * Accepts: timber <command> [--config|-c <path>]
29
+ */
30
+ export function parseArgs(args: string[]): ParsedArgs {
31
+ if (args.length === 0) {
32
+ throw new Error(
33
+ 'No command provided. Usage: timber <dev|build|preview|check> [--config <path>]'
34
+ );
35
+ }
36
+
37
+ const command = args[0];
38
+ if (!COMMANDS.includes(command as Command)) {
39
+ throw new Error(`Unknown command: ${command}. Available commands: ${COMMANDS.join(', ')}`);
40
+ }
41
+
42
+ let config: string | undefined;
43
+ for (let i = 1; i < args.length; i++) {
44
+ if (args[i] === '--config' || args[i] === '-c') {
45
+ config = args[++i];
46
+ if (!config) {
47
+ throw new Error('--config requires a path argument');
48
+ }
49
+ }
50
+ }
51
+
52
+ return { command: command as Command, config };
53
+ }
54
+
55
+ // ─── Command Implementations ─────────────────────────────────────────────────
56
+
57
+ /**
58
+ * Start the Vite dev server.
59
+ * Middleware re-runs on file change via HMR wiring in timber-routing.
60
+ */
61
+ export async function runDev(options: CommandOptions): Promise<void> {
62
+ const { createServer } = await import('vite');
63
+ const server = await createServer({
64
+ configFile: options.config,
65
+ });
66
+ await server.listen();
67
+ server.printUrls();
68
+ }
69
+
70
+ /**
71
+ * Run the production build using createBuilder + buildApp.
72
+ * Direct build() calls do NOT trigger the RSC plugin's multi-environment
73
+ * pipeline — createBuilder/buildApp is required.
74
+ */
75
+ export async function runBuild(options: CommandOptions): Promise<void> {
76
+ const { createBuilder } = await import('vite');
77
+ const builder = await createBuilder({
78
+ configFile: options.config,
79
+ });
80
+ await builder.buildApp();
81
+ }
82
+
83
+ /**
84
+ * Determine whether to use the adapter's preview or Vite's built-in preview.
85
+ * Exported for testing — the actual runPreview function uses this internally.
86
+ */
87
+ export function resolvePreviewStrategy(
88
+ adapter: import('./adapters/types').TimberPlatformAdapter | undefined
89
+ ): 'adapter' | 'vite' {
90
+ if (adapter && typeof adapter.preview === 'function') {
91
+ return 'adapter';
92
+ }
93
+ return 'vite';
94
+ }
95
+
96
+ /**
97
+ * Load timber.config.ts from the project root.
98
+ * Returns the config object with adapter, output, etc.
99
+ * Returns null if no config file is found.
100
+ */
101
+ async function loadTimberConfig(
102
+ root: string
103
+ ): Promise<{ adapter?: import('./adapters/types').TimberPlatformAdapter; output?: string } | null> {
104
+ const { existsSync } = await import('node:fs');
105
+ const { join } = await import('node:path');
106
+ const { pathToFileURL } = await import('node:url');
107
+
108
+ const configNames = ['timber.config.ts', 'timber.config.js', 'timber.config.mjs'];
109
+
110
+ for (const name of configNames) {
111
+ const configPath = join(root, name);
112
+ if (existsSync(configPath)) {
113
+ // Use Vite's built-in config loading to handle TypeScript
114
+ const mod = await import(pathToFileURL(configPath).href);
115
+ return mod.default ?? mod;
116
+ }
117
+ }
118
+ return null;
119
+ }
120
+
121
+ /**
122
+ * Serve the production build for local testing.
123
+ * If the adapter provides a preview() method, it takes priority.
124
+ * Otherwise falls back to Vite's built-in preview server.
125
+ */
126
+ export async function runPreview(options: CommandOptions): Promise<void> {
127
+ const { join } = await import('node:path');
128
+
129
+ // Try to load timber config for adapter-specific preview
130
+ const root = process.cwd();
131
+ const config = await loadTimberConfig(root).catch(() => null);
132
+ const adapter = config?.adapter as import('./adapters/types').TimberPlatformAdapter | undefined;
133
+
134
+ if (resolvePreviewStrategy(adapter) === 'adapter') {
135
+ const buildDir = join(root, '.timber', 'build');
136
+ const timberConfig = { output: (config?.output ?? 'server') as 'server' | 'static' };
137
+ await adapter!.preview!(timberConfig, buildDir);
138
+ return;
139
+ }
140
+
141
+ // Fallback: Vite's built-in preview server
142
+ const { preview } = await import('vite');
143
+ const server = await preview({
144
+ configFile: options.config,
145
+ });
146
+ server.printUrls();
147
+ }
148
+
149
+ /**
150
+ * Validate types and routes without producing build output.
151
+ * Runs tsgo --noEmit for type checking.
152
+ */
153
+ export async function runCheck(options: CommandOptions): Promise<void> {
154
+ const { execFile } = await import('node:child_process');
155
+
156
+ await new Promise<void>((resolve, reject) => {
157
+ const configArgs = options.config ? ['--project', options.config] : [];
158
+ execFile('tsgo', ['--noEmit', ...configArgs], (err, stdout, stderr) => {
159
+ if (stdout) process.stdout.write(stdout);
160
+ if (stderr) process.stderr.write(stderr);
161
+ if (err) {
162
+ reject(new Error(`Type check failed with exit code ${err.code}`));
163
+ } else {
164
+ resolve();
165
+ }
166
+ });
167
+ });
168
+ }
169
+
170
+ // ─── Main Entry Point ────────────────────────────────────────────────────────
171
+
172
+ async function main(): Promise<void> {
173
+ const parsed = parseArgs(process.argv.slice(2));
174
+ const options: CommandOptions = { config: parsed.config };
175
+
176
+ switch (parsed.command) {
177
+ case 'dev':
178
+ await runDev(options);
179
+ break;
180
+ case 'build':
181
+ await runBuild(options);
182
+ break;
183
+ case 'preview':
184
+ await runPreview(options);
185
+ break;
186
+ case 'check':
187
+ await runCheck(options);
188
+ break;
189
+ }
190
+ }
191
+
192
+ // Only run main when executed directly (not imported in tests)
193
+ const isDirectExecution =
194
+ typeof process !== 'undefined' && process.argv[1] && import.meta.url.endsWith(process.argv[1]);
195
+
196
+ if (isDirectExecution) {
197
+ main().catch((err) => {
198
+ console.error(err.message);
199
+ process.exit(1);
200
+ });
201
+ }