@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.
- package/CHANGELOG.md +109 -0
- package/README.md +26 -5
- package/dist/client.d.ts +29 -16
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +352 -165
- package/dist/client.js.map +1 -1
- package/dist/index-client.d.ts +5 -4
- package/dist/index-client.js +4 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -4
- package/dist/link-client.d.ts +6 -4
- package/dist/link-client.d.ts.map +1 -1
- package/dist/link-client.js +10 -4
- package/dist/link-client.js.map +1 -1
- package/dist/link-contract.d.ts +12 -7
- package/dist/link-contract.d.ts.map +1 -1
- package/dist/link-contract.js +12 -7
- package/dist/link-contract.js.map +1 -1
- package/dist/link.d.ts +6 -4
- package/dist/link.d.ts.map +1 -1
- package/dist/link.js +8 -4
- package/dist/link.js.map +1 -1
- package/dist/router.d.ts +57 -55
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +150 -157
- package/dist/router.js.map +1 -1
- package/dist/server.d.ts +29 -16
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +159 -100
- package/dist/server.js.map +1 -1
- package/dist/snapshot.d.ts +13 -10
- package/dist/snapshot.d.ts.map +1 -1
- package/dist/snapshot.js +19 -20
- package/dist/snapshot.js.map +1 -1
- package/dist/virtual.d.ts +3 -2
- package/dist/virtual.js +1 -2
- package/dist/vite.d.ts +18 -15
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +144 -171
- package/dist/vite.js.map +1 -1
- package/package.json +9 -8
- package/src/client.ts +577 -0
- package/src/index-client.ts +3 -0
- package/src/index.ts +3 -0
- package/src/link-client.ts +11 -0
- package/src/link-contract.ts +20 -0
- package/src/link.ts +10 -0
- package/src/router.ts +282 -0
- package/src/server.ts +295 -0
- package/src/snapshot.ts +40 -0
- package/src/virtual.ts +5 -0
- package/src/vite.ts +256 -0
- package/dist/index-client.d.ts.map +0 -1
- package/dist/index-client.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/virtual.d.ts.map +0 -1
- 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"}
|
package/dist/index-client.js.map
DELETED
|
@@ -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"}
|
package/dist/index.d.ts.map
DELETED
|
@@ -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"}
|
package/dist/virtual.d.ts.map
DELETED
|
@@ -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"}
|
package/dist/virtual.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"virtual.js","sourceRoot":"","sources":["../src/virtual.ts"],"names":[],"mappings":""}
|