@vidact/start 0.2.0-beta.0 → 0.2.0-beta.10

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 (58) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/README.md +26 -5
  3. package/dist/client.d.ts +29 -16
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js +352 -165
  6. package/dist/client.js.map +1 -1
  7. package/dist/index-client.d.ts +5 -4
  8. package/dist/index-client.js +4 -4
  9. package/dist/index.d.ts +5 -4
  10. package/dist/index.js +4 -4
  11. package/dist/link-client.d.ts +6 -4
  12. package/dist/link-client.d.ts.map +1 -1
  13. package/dist/link-client.js +10 -4
  14. package/dist/link-client.js.map +1 -1
  15. package/dist/link-contract.d.ts +12 -7
  16. package/dist/link-contract.d.ts.map +1 -1
  17. package/dist/link-contract.js +12 -7
  18. package/dist/link-contract.js.map +1 -1
  19. package/dist/link.d.ts +6 -4
  20. package/dist/link.d.ts.map +1 -1
  21. package/dist/link.js +8 -4
  22. package/dist/link.js.map +1 -1
  23. package/dist/router.d.ts +57 -55
  24. package/dist/router.d.ts.map +1 -1
  25. package/dist/router.js +150 -157
  26. package/dist/router.js.map +1 -1
  27. package/dist/server.d.ts +29 -16
  28. package/dist/server.d.ts.map +1 -1
  29. package/dist/server.js +159 -100
  30. package/dist/server.js.map +1 -1
  31. package/dist/snapshot.d.ts +13 -10
  32. package/dist/snapshot.d.ts.map +1 -1
  33. package/dist/snapshot.js +19 -20
  34. package/dist/snapshot.js.map +1 -1
  35. package/dist/virtual.d.ts +3 -2
  36. package/dist/virtual.js +1 -2
  37. package/dist/vite.d.ts +18 -15
  38. package/dist/vite.d.ts.map +1 -1
  39. package/dist/vite.js +144 -171
  40. package/dist/vite.js.map +1 -1
  41. package/package.json +9 -8
  42. package/src/client.ts +577 -0
  43. package/src/index-client.ts +3 -0
  44. package/src/index.ts +3 -0
  45. package/src/link-client.ts +11 -0
  46. package/src/link-contract.ts +20 -0
  47. package/src/link.ts +10 -0
  48. package/src/router.ts +282 -0
  49. package/src/server.ts +295 -0
  50. package/src/snapshot.ts +40 -0
  51. package/src/virtual.ts +5 -0
  52. package/src/vite.ts +256 -0
  53. package/dist/index-client.d.ts.map +0 -1
  54. package/dist/index-client.js.map +0 -1
  55. package/dist/index.d.ts.map +0 -1
  56. package/dist/index.js.map +0 -1
  57. package/dist/virtual.d.ts.map +0 -1
  58. package/dist/virtual.js.map +0 -1
package/src/vite.ts ADDED
@@ -0,0 +1,256 @@
1
+ import { readdir } from 'node:fs/promises'
2
+ import type { IncomingMessage, ServerResponse } from 'node:http'
3
+ import path from 'node:path'
4
+
5
+ import { vidact, type VidactPluginOptions } from '@vidact/vite'
6
+ import {
7
+ isRunnableDevEnvironment,
8
+ normalizePath,
9
+ type Plugin,
10
+ type ResolvedConfig,
11
+ type ViteDevServer,
12
+ } from 'vite'
13
+
14
+ const PUBLIC_ROUTES_ID = 'virtual:vidact-start/routes'
15
+ const INTERNAL_ROUTES_ID = `\0${PUBLIC_ROUTES_ID}`
16
+ const ROUTE_FILE = /\.(?:[cm]?[jt]sx?)$/u
17
+ const IGNORED_ROUTE_FILE = /(?:^|\.)\b(?:test|spec)\.[cm]?[jt]sx?$/u
18
+
19
+ export interface VidactStartOptions {
20
+ readonly compiler?: Omit<VidactPluginOptions, 'target'>
21
+ readonly routesDirectory?: string
22
+ readonly serverEntry?: string | false
23
+ readonly serverExport?: string
24
+ }
25
+
26
+ export interface GeneratedRouteRecord {
27
+ readonly file: string
28
+ readonly id: string
29
+ readonly parentId: string | null
30
+ readonly path: string
31
+ }
32
+
33
+ export function vidactStart(options: VidactStartOptions = {}): Plugin[] {
34
+ const compiler = options.compiler ?? {}
35
+ const features = [...new Set(['framework', ...(compiler.features ?? [])])] as NonNullable<
36
+ VidactPluginOptions['features']
37
+ >
38
+ const plugins = [
39
+ vidactForEnvironment('client', { ...compiler, target: 'hydrate', features }),
40
+ vidactForEnvironment('ssr', { ...compiler, target: 'server', features }),
41
+ routeManifestPlugin(options.routesDirectory),
42
+ ]
43
+ if (options.serverEntry !== false) {
44
+ plugins.push(
45
+ developmentServerPlugin(
46
+ options.serverEntry ?? '/src/server.ts',
47
+ options.serverExport ?? 'default',
48
+ ),
49
+ )
50
+ }
51
+ return plugins
52
+ }
53
+
54
+ function routeManifestPlugin(configuredRoutesDirectory: string | undefined): Plugin {
55
+ let config: ResolvedConfig
56
+ let routesDirectory: string
57
+ let server: ViteDevServer | undefined
58
+ return {
59
+ name: 'vidact-start-routes',
60
+ configResolved(resolved) {
61
+ config = resolved
62
+ routesDirectory = path.resolve(config.root, configuredRoutesDirectory ?? 'src/routes')
63
+ },
64
+ configureServer(viteServer) {
65
+ server = viteServer
66
+ viteServer.watcher.add(routesDirectory)
67
+ },
68
+ resolveId(source) {
69
+ return source === PUBLIC_ROUTES_ID ? INTERNAL_ROUTES_ID : null
70
+ },
71
+ async load(id) {
72
+ if (id !== INTERNAL_ROUTES_ID) return null
73
+ return generateRouteModule(await discoverRouteRecords(routesDirectory))
74
+ },
75
+ handleHotUpdate(context) {
76
+ if (!isRouteFile(routesDirectory, context.file)) return
77
+ const module = server?.moduleGraph.getModuleById(INTERNAL_ROUTES_ID)
78
+ if (module !== undefined) server?.moduleGraph.invalidateModule(module)
79
+ server?.ws.send({ type: 'full-reload' })
80
+ return []
81
+ },
82
+ }
83
+ }
84
+
85
+ function vidactForEnvironment(
86
+ environmentName: 'client' | 'ssr',
87
+ options: VidactPluginOptions,
88
+ ): Plugin {
89
+ const plugin = vidact(options)
90
+ return {
91
+ ...plugin,
92
+ name: `${plugin.name}:start:${environmentName}`,
93
+ applyToEnvironment: (environment) => environment.name === environmentName,
94
+ }
95
+ }
96
+
97
+ function developmentServerPlugin(serverEntry: string, serverExport: string): Plugin {
98
+ return {
99
+ name: 'vidact-start-development-server',
100
+ config: () => ({ appType: 'custom' }),
101
+ configureServer(server) {
102
+ return () => {
103
+ server.middlewares.use((nodeRequest, nodeResponse, next) => {
104
+ void serveDevelopmentRequest(
105
+ server,
106
+ serverEntry,
107
+ serverExport,
108
+ nodeRequest,
109
+ nodeResponse,
110
+ ).catch((error: unknown) => {
111
+ if (error instanceof Error) server.ssrFixStacktrace(error)
112
+ next(error)
113
+ })
114
+ })
115
+ }
116
+ },
117
+ }
118
+ }
119
+
120
+ async function serveDevelopmentRequest(
121
+ server: ViteDevServer,
122
+ serverEntry: string,
123
+ serverExport: string,
124
+ nodeRequest: IncomingMessage,
125
+ nodeResponse: ServerResponse,
126
+ ): Promise<void> {
127
+ const environment = server.environments.ssr
128
+ if (!isRunnableDevEnvironment(environment)) {
129
+ throw new Error("Vidact Start requires Vite's runnable SSR development environment")
130
+ }
131
+ const module = (await environment.runner.import(serverEntry)) as Record<string, unknown>
132
+ const handler = module[serverExport]
133
+ if (typeof handler !== 'function') {
134
+ throw new TypeError(`${serverEntry} must export a ${serverExport} request handler`)
135
+ }
136
+ const request = await nodeRequestToRequest(nodeRequest)
137
+ let response = await (handler as (request: Request) => Response | Promise<Response>)(request)
138
+ if (!(response instanceof Response)) {
139
+ throw new TypeError(`${serverEntry} ${serverExport} must return a Response`)
140
+ }
141
+ if (response.headers.get('content-type')?.startsWith('text/html') === true) {
142
+ const html = await server.transformIndexHtml(nodeRequest.url ?? '/', await response.text())
143
+ response = new Response(html, response)
144
+ }
145
+ await sendNodeResponse(nodeResponse, response)
146
+ }
147
+
148
+ async function nodeRequestToRequest(request: IncomingMessage): Promise<Request> {
149
+ const headers = new Headers()
150
+ for (const [name, value] of Object.entries(request.headers)) {
151
+ if (Array.isArray(value)) for (const item of value) headers.append(name, item)
152
+ else if (value !== undefined) headers.set(name, value)
153
+ }
154
+ const forwardedProtocol = headers.get('x-forwarded-proto')?.split(',', 1)[0]?.trim()
155
+ const protocol =
156
+ forwardedProtocol ?? (Reflect.get(request.socket, 'encrypted') === true ? 'https' : 'http')
157
+ const url = new URL(request.url ?? '/', `${protocol}://${headers.get('host') ?? 'localhost'}`)
158
+ const body =
159
+ request.method === 'GET' || request.method === 'HEAD'
160
+ ? undefined
161
+ : await readRequestBody(request)
162
+ return new Request(url, {
163
+ method: request.method ?? 'GET',
164
+ headers,
165
+ ...(body === undefined ? {} : { body }),
166
+ })
167
+ }
168
+
169
+ async function readRequestBody(request: IncomingMessage): Promise<ArrayBuffer> {
170
+ const chunks: Uint8Array[] = []
171
+ for await (const chunk of request) {
172
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk)
173
+ }
174
+ return Uint8Array.from(Buffer.concat(chunks)).buffer
175
+ }
176
+
177
+ async function sendNodeResponse(response: ServerResponse, webResponse: Response): Promise<void> {
178
+ response.statusCode = webResponse.status
179
+ response.statusMessage = webResponse.statusText
180
+ for (const [name, value] of webResponse.headers) response.setHeader(name, value)
181
+ const cookies = webResponse.headers.getSetCookie()
182
+ if (cookies.length > 0) response.setHeader('set-cookie', cookies)
183
+ response.end(Buffer.from(await webResponse.arrayBuffer()))
184
+ }
185
+
186
+ export async function discoverRouteRecords(
187
+ routesDirectory: string,
188
+ ): Promise<readonly GeneratedRouteRecord[]> {
189
+ let filenames: string[]
190
+ try {
191
+ filenames = await readdir(routesDirectory, { recursive: true })
192
+ } catch (error) {
193
+ if (isMissingDirectory(error)) return []
194
+ throw error
195
+ }
196
+ const records = filenames
197
+ .filter((filename) => ROUTE_FILE.test(filename) && !IGNORED_ROUTE_FILE.test(filename))
198
+ .map((filename) => routeFileToRecord(filename, routesDirectory))
199
+ const ids = new Set(records.map((record) => record.id))
200
+ return Object.freeze(
201
+ records
202
+ .map((record) => Object.freeze({ ...record, parentId: parentRouteId(record.id, ids) }))
203
+ .toSorted((left, right) => left.id.localeCompare(right.id)),
204
+ )
205
+ }
206
+
207
+ export function routeFileToRecord(filename: string, routesDirectory: string): GeneratedRouteRecord {
208
+ const normalizedFilename = normalizePath(filename)
209
+ const id = normalizedFilename.replace(ROUTE_FILE, '')
210
+ if (id.length === 0) throw new TypeError(`invalid route filename ${filename}`)
211
+ const routeSegments = id
212
+ .split('/')
213
+ .filter((segment) => segment !== '__root' && segment !== 'index' && !segment.startsWith('_'))
214
+ .map((segment) =>
215
+ segment === '$' ? '*' : segment.startsWith('$') ? `:${segment.slice(1)}` : segment,
216
+ )
217
+ return {
218
+ file: normalizePath(path.resolve(routesDirectory, normalizedFilename)),
219
+ id,
220
+ parentId: null,
221
+ path: routeSegments.length === 0 ? '/' : `/${routeSegments.join('/')}`,
222
+ }
223
+ }
224
+
225
+ function parentRouteId(id: string, ids: ReadonlySet<string>): string | null {
226
+ if (id === '__root') return null
227
+ const segments = id.split('/')
228
+ if (segments.at(-1) === 'index') segments.pop()
229
+ while (segments.length > 0) {
230
+ const candidate = segments.join('/')
231
+ if (candidate !== id && ids.has(candidate)) return candidate
232
+ segments.pop()
233
+ }
234
+ return ids.has('__root') ? '__root' : null
235
+ }
236
+
237
+ function generateRouteModule(records: readonly GeneratedRouteRecord[]): string {
238
+ const entries = records
239
+ .map(
240
+ (record) =>
241
+ `{ id: ${JSON.stringify(record.id)}, parentId: ${JSON.stringify(record.parentId)}, path: ${JSON.stringify(record.path)}, load: () => import(${JSON.stringify(record.file)}) }`,
242
+ )
243
+ .join(',\n ')
244
+ return `import { createRouteManifest } from '@vidact/start'\n\nexport const routeManifest = createRouteManifest([\n ${entries}\n])\n`
245
+ }
246
+
247
+ function isRouteFile(routesDirectory: string, filename: string): boolean {
248
+ const relative = path.relative(routesDirectory, filename)
249
+ return (
250
+ !relative.startsWith('..') && ROUTE_FILE.test(relative) && !IGNORED_ROUTE_FILE.test(relative)
251
+ )
252
+ }
253
+
254
+ function isMissingDirectory(error: unknown): boolean {
255
+ return typeof error === 'object' && error !== null && Reflect.get(error, 'code') === 'ENOENT'
256
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"index-client.d.ts","sourceRoot":"","sources":["../src/index-client.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAA;AAChC,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index-client.js","sourceRoot":"","sources":["../src/index-client.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAA;AAChC,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA"}
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"virtual.d.ts","sourceRoot":"","sources":["../src/virtual.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,6BAA6B,CAAC;IACpC,MAAM,aAAa,EAAE,OAAO,aAAa,EAAE,aAAa,CAAA;CAChE"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"virtual.js","sourceRoot":"","sources":["../src/virtual.ts"],"names":[],"mappings":""}