@mantiq/vite 0.0.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.
package/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # @mantiq/vite
2
+
3
+ Vite dev server integration, SSR support, and static file serving for MantiqJS.
4
+
5
+ Part of [MantiqJS](https://github.com/abdullahkhan/mantiq) — a batteries-included TypeScript web framework for Bun.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ bun add @mantiq/vite
11
+ ```
12
+
13
+ ## Documentation
14
+
15
+ See the [MantiqJS repository](https://github.com/abdullahkhan/mantiq) for full documentation.
16
+
17
+ ## License
18
+
19
+ MIT
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@mantiq/vite",
3
+ "version": "0.0.1",
4
+ "description": "Vite dev server & manifest integration",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Abdullah Khan",
8
+ "homepage": "https://github.com/abdullahkhan/mantiq/tree/main/packages/vite",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/abdullahkhan/mantiq.git",
12
+ "directory": "packages/vite"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/abdullahkhan/mantiq/issues"
16
+ },
17
+ "keywords": [
18
+ "mantiq",
19
+ "mantiqjs",
20
+ "bun",
21
+ "typescript",
22
+ "framework",
23
+ "vite"
24
+ ],
25
+ "engines": {
26
+ "bun": ">=1.1.0"
27
+ },
28
+ "main": "./src/index.ts",
29
+ "types": "./src/index.ts",
30
+ "exports": {
31
+ ".": {
32
+ "bun": "./src/index.ts",
33
+ "default": "./src/index.ts"
34
+ }
35
+ },
36
+ "files": [
37
+ "src/",
38
+ "package.json",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "scripts": {
43
+ "build": "bun build ./src/index.ts --outdir ./dist --target bun",
44
+ "test": "bun test",
45
+ "typecheck": "tsc --noEmit",
46
+ "clean": "rm -rf dist"
47
+ },
48
+ "dependencies": {
49
+ "@mantiq/core": "workspace:*"
50
+ },
51
+ "devDependencies": {
52
+ "bun-types": "latest",
53
+ "typescript": "^5.7.0",
54
+ "vite": "^6.0.0"
55
+ },
56
+ "peerDependencies": {
57
+ "vite": ">=5.0.0"
58
+ }
59
+ }
package/src/Vite.ts ADDED
@@ -0,0 +1,486 @@
1
+ import type { ViteConfig, ViteManifest, ManifestChunk, PageOptions, SSRModule, SSRResult, RenderOptions } from './contracts/Vite.ts'
2
+ import { ViteManifestNotFoundError, ViteEntrypointNotFoundError, ViteSSRBundleNotFoundError, ViteSSREntryError } from './errors/ViteError.ts'
3
+
4
+ /**
5
+ * Core Vite integration class.
6
+ *
7
+ * Handles dev/prod detection, asset tag generation, manifest reading,
8
+ * and HTML shell rendering. Framework-agnostic — works with React, Vue,
9
+ * Svelte, or vanilla JS.
10
+ */
11
+ export class Vite {
12
+ private readonly config: ViteConfig
13
+ private manifestCache: ViteManifest | null = null
14
+ /** null = unchecked, false = not found, string = dev server URL */
15
+ private hotFileCache: string | false | null = null
16
+
17
+ // ── SSR state ───────────────────────────────────────────────────────────
18
+ private readonly ssrEntry: string | null
19
+ private readonly ssrBundle: string
20
+ private viteDevServer: any | null = null
21
+ private ssrModuleCache: SSRModule | null = null
22
+ /** Application base path (for resolving SSR bundle). Set via setBasePath(). */
23
+ private basePath: string = ''
24
+
25
+ constructor(config: Partial<ViteConfig> = {}) {
26
+ this.config = {
27
+ devServerUrl: config.devServerUrl ?? 'http://localhost:5173',
28
+ buildDir: config.buildDir ?? 'build',
29
+ publicDir: config.publicDir ?? 'public',
30
+ manifest: config.manifest ?? '.vite/manifest.json',
31
+ reactRefresh: config.reactRefresh ?? false,
32
+ rootElement: config.rootElement ?? 'app',
33
+ hotFile: config.hotFile ?? 'hot',
34
+ ...(config.ssr ? { ssr: config.ssr } : {}),
35
+ }
36
+ this.ssrEntry = config.ssr?.entry ?? null
37
+ this.ssrBundle = config.ssr?.bundle ?? 'bootstrap/ssr/ssr.js'
38
+ }
39
+
40
+ // ── Initialization ───────────────────────────────────────────────────────
41
+
42
+ /**
43
+ * Check for the hot file to determine dev/prod mode.
44
+ * Called during ViteServiceProvider.boot().
45
+ */
46
+ async initialize(): Promise<void> {
47
+ const hotPath = this.hotFilePath()
48
+ const file = Bun.file(hotPath)
49
+ if (await file.exists()) {
50
+ const url = (await file.text()).trim()
51
+ this.hotFileCache = url || this.config.devServerUrl
52
+ } else {
53
+ this.hotFileCache = false
54
+ }
55
+ }
56
+
57
+ /** Whether the Vite dev server is running (hot file exists). */
58
+ isDev(): boolean {
59
+ return typeof this.hotFileCache === 'string'
60
+ }
61
+
62
+ /** The dev server URL (from hot file or config fallback). */
63
+ devServerUrl(): string {
64
+ return typeof this.hotFileCache === 'string'
65
+ ? this.hotFileCache
66
+ : this.config.devServerUrl
67
+ }
68
+
69
+ // ── Asset Tag Generation ─────────────────────────────────────────────────
70
+
71
+ /**
72
+ * Generate `<script>` and `<link>` tags for the given entrypoint(s).
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * const tags = await vite.assets('src/main.tsx')
77
+ * const tags = await vite.assets(['src/main.tsx', 'src/extra.css'])
78
+ * ```
79
+ */
80
+ async assets(entrypoints: string | string[]): Promise<string> {
81
+ const entries = Array.isArray(entrypoints) ? entrypoints : [entrypoints]
82
+
83
+ if (this.isDev()) {
84
+ return this.devAssets(entries)
85
+ }
86
+ return this.prodAssets(entries)
87
+ }
88
+
89
+ private devAssets(entries: string[]): string {
90
+ const url = this.devServerUrl()
91
+ const tags: string[] = []
92
+
93
+ // React Fast Refresh preamble (must come before any other script)
94
+ if (this.config.reactRefresh) {
95
+ tags.push(this.reactRefreshTag())
96
+ }
97
+
98
+ // Vite client for HMR
99
+ tags.push(`<script type="module" src="${url}/@vite/client"></script>`)
100
+
101
+ for (const entry of entries) {
102
+ if (entry.endsWith('.css')) {
103
+ tags.push(`<link rel="stylesheet" href="${url}/${entry}">`)
104
+ } else {
105
+ tags.push(`<script type="module" src="${url}/${entry}"></script>`)
106
+ }
107
+ }
108
+
109
+ return tags.join('\n ')
110
+ }
111
+
112
+ private async prodAssets(entries: string[]): Promise<string> {
113
+ const manifest = await this.loadManifest()
114
+ const tags: string[] = []
115
+ const cssEmitted = new Set<string>()
116
+ const preloadedPaths = new Set<string>()
117
+
118
+ for (const entry of entries) {
119
+ const chunk = manifest[entry]
120
+ if (!chunk) {
121
+ throw new ViteEntrypointNotFoundError(entry, this.manifestPath())
122
+ }
123
+
124
+ // Collect all CSS (from this chunk + transitively imported chunks)
125
+ const allCss = this.collectCss(manifest, entry, new Set<string>())
126
+ for (const cssPath of allCss) {
127
+ if (!cssEmitted.has(cssPath)) {
128
+ cssEmitted.add(cssPath)
129
+ tags.push(`<link rel="stylesheet" href="/${this.config.buildDir}/${cssPath}">`)
130
+ }
131
+ }
132
+
133
+ // Module preloads for statically imported chunks
134
+ const preloads = this.collectPreloads(manifest, entry, new Set<string>())
135
+ for (const preloadPath of preloads) {
136
+ if (!preloadedPaths.has(preloadPath) && preloadPath !== chunk.file) {
137
+ preloadedPaths.add(preloadPath)
138
+ tags.push(`<link rel="modulepreload" href="/${this.config.buildDir}/${preloadPath}">`)
139
+ }
140
+ }
141
+
142
+ // CSS-only entries: emit the file itself as a stylesheet
143
+ if (entry.endsWith('.css')) {
144
+ if (!cssEmitted.has(chunk.file)) {
145
+ cssEmitted.add(chunk.file)
146
+ tags.push(`<link rel="stylesheet" href="/${this.config.buildDir}/${chunk.file}">`)
147
+ }
148
+ } else {
149
+ tags.push(`<script type="module" src="/${this.config.buildDir}/${chunk.file}"></script>`)
150
+ }
151
+ }
152
+
153
+ return tags.join('\n ')
154
+ }
155
+
156
+ /** Recursively collect CSS from a chunk and all its static imports. */
157
+ private collectCss(
158
+ manifest: ViteManifest,
159
+ key: string,
160
+ visited: Set<string>,
161
+ ): string[] {
162
+ if (visited.has(key)) return []
163
+ visited.add(key)
164
+
165
+ const chunk = manifest[key]
166
+ if (!chunk) return []
167
+
168
+ const css: string[] = [...(chunk.css ?? [])]
169
+
170
+ for (const imp of chunk.imports ?? []) {
171
+ css.push(...this.collectCss(manifest, imp, visited))
172
+ }
173
+
174
+ return css
175
+ }
176
+
177
+ /** Recursively collect JS file paths from statically imported chunks for modulepreload. */
178
+ private collectPreloads(
179
+ manifest: ViteManifest,
180
+ key: string,
181
+ visited: Set<string>,
182
+ ): string[] {
183
+ if (visited.has(key)) return []
184
+ visited.add(key)
185
+
186
+ const chunk = manifest[key]
187
+ if (!chunk) return []
188
+
189
+ const preloads: string[] = []
190
+
191
+ for (const imp of chunk.imports ?? []) {
192
+ const importedChunk = manifest[imp]
193
+ if (importedChunk) {
194
+ preloads.push(importedChunk.file)
195
+ preloads.push(...this.collectPreloads(manifest, imp, visited))
196
+ }
197
+ }
198
+
199
+ return preloads
200
+ }
201
+
202
+ // ── Manifest ─────────────────────────────────────────────────────────────
203
+
204
+ /**
205
+ * Load and cache the Vite manifest from disk.
206
+ * @throws ViteManifestNotFoundError if the manifest file does not exist.
207
+ */
208
+ async loadManifest(): Promise<ViteManifest> {
209
+ if (this.manifestCache) return this.manifestCache
210
+
211
+ const path = this.manifestPath()
212
+ const file = Bun.file(path)
213
+
214
+ if (!(await file.exists())) {
215
+ throw new ViteManifestNotFoundError(path)
216
+ }
217
+
218
+ this.manifestCache = (await file.json()) as ViteManifest
219
+ return this.manifestCache
220
+ }
221
+
222
+ /** Clear the cached manifest (useful for testing or watch-mode rebuilds). */
223
+ flushManifest(): void {
224
+ this.manifestCache = null
225
+ }
226
+
227
+ private manifestPath(): string {
228
+ return `${this.config.publicDir}/${this.config.buildDir}/${this.config.manifest}`
229
+ }
230
+
231
+ private hotFilePath(): string {
232
+ return `${this.config.publicDir}/${this.config.hotFile}`
233
+ }
234
+
235
+ // ── SSR ─────────────────────────────────────────────────────────────────
236
+
237
+ /** Whether SSR is enabled (ssr.entry is configured). */
238
+ isSSR(): boolean {
239
+ return this.ssrEntry !== null
240
+ }
241
+
242
+ /** Set the application base path (used to resolve SSR bundle in production). */
243
+ setBasePath(path: string): void {
244
+ this.basePath = path
245
+ }
246
+
247
+ /**
248
+ * Universal page render — the Inertia-like protocol.
249
+ *
250
+ * - If `X-Mantiq: true` header is present → returns JSON (client navigation).
251
+ * - Otherwise → returns full HTML with SSR content (if enabled) or CSR shell.
252
+ *
253
+ * @example
254
+ * ```ts
255
+ * return vite().render(request, {
256
+ * page: 'Dashboard',
257
+ * entry: ['src/style.css', 'src/main.tsx'],
258
+ * data: { users },
259
+ * })
260
+ * ```
261
+ */
262
+ async render(
263
+ request: { header(name: string): string | undefined; path(): string },
264
+ options: RenderOptions,
265
+ ): Promise<Response> {
266
+ const url = request.path()
267
+ const pageData: Record<string, unknown> = { _page: options.page, _url: url, ...(options.data ?? {}) }
268
+
269
+ // Client-side navigation → JSON only
270
+ if (request.header('X-Mantiq') === 'true') {
271
+ return new Response(JSON.stringify(pageData), {
272
+ headers: { 'Content-Type': 'application/json', 'X-Mantiq': 'true' },
273
+ })
274
+ }
275
+
276
+ // First load → full HTML (with SSR if enabled)
277
+ const html = await this.page({
278
+ entry: options.entry,
279
+ title: options.title ?? '',
280
+ head: options.head ?? '',
281
+ data: pageData,
282
+ url,
283
+ page: options.page,
284
+ })
285
+
286
+ return new Response(html, {
287
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
288
+ })
289
+ }
290
+
291
+ /**
292
+ * Get or lazily create the embedded Vite dev server (for SSR module loading).
293
+ * Only used in development mode with SSR enabled.
294
+ */
295
+ private async getViteDevServer(): Promise<any> {
296
+ if (this.viteDevServer) return this.viteDevServer
297
+
298
+ const { createServer } = await import('vite')
299
+ this.viteDevServer = await createServer({
300
+ server: { middlewareMode: true },
301
+ appType: 'custom',
302
+ })
303
+
304
+ return this.viteDevServer
305
+ }
306
+
307
+ /**
308
+ * Load the SSR module. In dev mode, uses Vite's ssrLoadModule for HMR.
309
+ * In production, imports the pre-built bundle.
310
+ */
311
+ private async loadSSRModule(): Promise<SSRModule> {
312
+ if (this.ssrModuleCache) return this.ssrModuleCache
313
+
314
+ if (!this.ssrEntry) {
315
+ throw new ViteSSREntryError('(none)', 'SSR is not configured. Set ssr.entry in your vite config.')
316
+ }
317
+
318
+ let mod: any
319
+
320
+ if (this.isDev()) {
321
+ // Dev: use Vite's ssrLoadModule for HMR + transform
322
+ const server = await this.getViteDevServer()
323
+ mod = await server.ssrLoadModule(this.ssrEntry)
324
+ } else {
325
+ // Prod: import the pre-built SSR bundle
326
+ const bundlePath = this.basePath
327
+ ? `${this.basePath}/${this.ssrBundle}`
328
+ : this.ssrBundle
329
+
330
+ const file = Bun.file(bundlePath)
331
+ if (!(await file.exists())) {
332
+ throw new ViteSSRBundleNotFoundError(bundlePath)
333
+ }
334
+
335
+ mod = await import(bundlePath)
336
+ }
337
+
338
+ if (typeof mod.render !== 'function') {
339
+ throw new ViteSSREntryError(
340
+ this.ssrEntry,
341
+ 'Module does not export a render() function.',
342
+ )
343
+ }
344
+
345
+ // Only cache in production (dev needs fresh modules for HMR)
346
+ if (!this.isDev()) {
347
+ this.ssrModuleCache = mod as SSRModule
348
+ }
349
+
350
+ return mod as SSRModule
351
+ }
352
+
353
+ /**
354
+ * Perform SSR render for a given URL and page data.
355
+ * Returns the rendered HTML string and optional head tags.
356
+ */
357
+ private async renderSSR(url: string, data?: Record<string, unknown>): Promise<SSRResult> {
358
+ const ssrModule = await this.loadSSRModule()
359
+
360
+ try {
361
+ return await ssrModule.render(url, data)
362
+ } catch (err) {
363
+ // In dev, fix the stack trace for better DX
364
+ if (this.isDev() && this.viteDevServer && err instanceof Error) {
365
+ this.viteDevServer.ssrFixStacktrace(err)
366
+ }
367
+ throw err
368
+ }
369
+ }
370
+
371
+ /** Close the embedded Vite dev server (cleanup). */
372
+ async closeDevServer(): Promise<void> {
373
+ if (this.viteDevServer) {
374
+ await this.viteDevServer.close()
375
+ this.viteDevServer = null
376
+ }
377
+ }
378
+
379
+ // ── HTML Shell ───────────────────────────────────────────────────────────
380
+
381
+ /**
382
+ * Render a full HTML page with Vite assets injected.
383
+ *
384
+ * @example
385
+ * ```ts
386
+ * const html = await vite.page({
387
+ * entry: 'src/main.tsx',
388
+ * title: 'My App',
389
+ * data: { users: [...] },
390
+ * })
391
+ * return MantiqResponse.html(html)
392
+ * ```
393
+ */
394
+ async page(options: PageOptions): Promise<string> {
395
+ const {
396
+ entry,
397
+ title = '',
398
+ data,
399
+ rootElement = this.config.rootElement,
400
+ head = '',
401
+ url,
402
+ } = options
403
+
404
+ const assetTags = await this.assets(entry)
405
+
406
+ const dataScript = data
407
+ ? `\n <script>window.__MANTIQ_DATA__ = ${JSON.stringify(data)}</script>`
408
+ : ''
409
+
410
+ // SSR: render the page component to HTML on the server
411
+ let ssrHtml = ''
412
+ let ssrHead = ''
413
+ if (this.isSSR() && url) {
414
+ try {
415
+ const result = await this.renderSSR(url, data)
416
+ ssrHtml = result.html ?? ''
417
+ ssrHead = result.head ?? ''
418
+ } catch {
419
+ // SSR failure falls back to CSR shell
420
+ ssrHtml = ''
421
+ ssrHead = ''
422
+ }
423
+ }
424
+
425
+ const headContent = [head, ssrHead].filter(Boolean).join('\n ')
426
+
427
+ return `<!DOCTYPE html>
428
+ <html lang="en">
429
+ <head>
430
+ <meta charset="UTF-8">
431
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
432
+ <title>${escapeHtml(title)}</title>
433
+ ${headContent}
434
+ ${assetTags}
435
+ </head>
436
+ <body>
437
+ <div id="${escapeHtml(rootElement)}">${ssrHtml}</div>${dataScript}
438
+ </body>
439
+ </html>`
440
+ }
441
+
442
+ // ── React Refresh ────────────────────────────────────────────────────────
443
+
444
+ /** Generate the React Fast Refresh preamble for dev mode. */
445
+ reactRefreshTag(): string {
446
+ const url = this.devServerUrl()
447
+ return `<script type="module">
448
+ import RefreshRuntime from '${url}/@react-refresh'
449
+ RefreshRuntime.injectIntoGlobalHook(window)
450
+ window.$RefreshReg$ = () => {}
451
+ window.$RefreshSig$ = () => (type) => type
452
+ window.__vite_plugin_react_preamble_installed__ = true
453
+ </script>`
454
+ }
455
+
456
+ // ── Testing Helpers ──────────────────────────────────────────────────────
457
+
458
+ /** @internal Set the manifest directly (for testing without file I/O). */
459
+ setManifest(manifest: ViteManifest): void {
460
+ this.manifestCache = manifest
461
+ }
462
+
463
+ /** @internal Set dev mode state directly (for testing without file I/O). */
464
+ setDevMode(url: string | false): void {
465
+ this.hotFileCache = url
466
+ }
467
+
468
+ /** @internal Set an SSR module directly (for testing without file I/O). */
469
+ setSSRModule(mod: SSRModule | null): void {
470
+ this.ssrModuleCache = mod
471
+ }
472
+
473
+ /** Returns the resolved config (read-only). */
474
+ getConfig(): Readonly<ViteConfig> {
475
+ return this.config
476
+ }
477
+ }
478
+
479
+ /** Escape HTML special characters to prevent XSS. */
480
+ export function escapeHtml(str: string): string {
481
+ return str
482
+ .replace(/&/g, '&amp;')
483
+ .replace(/</g, '&lt;')
484
+ .replace(/>/g, '&gt;')
485
+ .replace(/"/g, '&quot;')
486
+ }
@@ -0,0 +1,48 @@
1
+ import { ServiceProvider, ConfigRepository } from '@mantiq/core'
2
+ import { Vite } from './Vite.ts'
3
+ import { ServeStaticFiles } from './middleware/ServeStaticFiles.ts'
4
+
5
+ export const VITE = Symbol('Vite')
6
+
7
+ /**
8
+ * Registers the Vite integration in the application container.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { ViteServiceProvider } from '@mantiq/vite'
13
+ * await app.registerProviders([CoreServiceProvider, ViteServiceProvider])
14
+ * ```
15
+ */
16
+ export class ViteServiceProvider extends ServiceProvider {
17
+ override register(): void {
18
+ this.app.singleton(Vite, (c) => {
19
+ let viteConfig = {}
20
+ try {
21
+ viteConfig = c.make(ConfigRepository).get('vite') ?? {}
22
+ } catch {
23
+ // No vite config file — use all defaults
24
+ }
25
+ return new Vite(viteConfig)
26
+ })
27
+
28
+ this.app.alias(Vite, VITE)
29
+
30
+ // Register static files middleware with Vite instance injected
31
+ this.app.bind(ServeStaticFiles, (c) => new ServeStaticFiles(c.make(Vite)))
32
+ }
33
+
34
+ override async boot(): Promise<void> {
35
+ const vite = this.app.make(Vite)
36
+
37
+ // Set the base path so SSR can resolve the production bundle
38
+ try {
39
+ const config = this.app.make(ConfigRepository)
40
+ const basePath = config.get('app.basePath')
41
+ if (basePath) vite.setBasePath(basePath)
42
+ } catch {
43
+ // Config may not be available in all contexts
44
+ }
45
+
46
+ await vite.initialize()
47
+ }
48
+ }
@@ -0,0 +1,90 @@
1
+ /** Configuration for the Vite integration. */
2
+ export interface ViteConfig {
3
+ /** Vite dev server URL. Default: 'http://localhost:5173' */
4
+ devServerUrl: string
5
+ /** Build output directory, relative to publicDir. Default: 'build' */
6
+ buildDir: string
7
+ /** Public directory (absolute path). Default: 'public' */
8
+ publicDir: string
9
+ /** Path to manifest.json inside buildDir. Default: '.vite/manifest.json' */
10
+ manifest: string
11
+ /** Enable React Fast Refresh preamble in dev mode. Default: false */
12
+ reactRefresh: boolean
13
+ /** Root element ID for the app mount point. Default: 'app' */
14
+ rootElement: string
15
+ /** Name of the hot file (relative to publicDir). Default: 'hot' */
16
+ hotFile: string
17
+ /** SSR configuration. When set, enables server-side rendering. */
18
+ ssr?: {
19
+ /** SSR entry module path (e.g. 'src/ssr.tsx') */
20
+ entry: string
21
+ /** Production SSR bundle path. Default: 'bootstrap/ssr/ssr.js' */
22
+ bundle?: string
23
+ }
24
+ }
25
+
26
+ /** A single chunk entry in the Vite 5+ manifest. */
27
+ export interface ManifestChunk {
28
+ /** The hashed output file path, e.g. 'assets/main-abc123.js' */
29
+ file: string
30
+ /** The original source path (present on entry chunks) */
31
+ src?: string
32
+ /** Whether this chunk is an entry point */
33
+ isEntry?: boolean
34
+ /** Whether this chunk is a dynamic import */
35
+ isDynamicEntry?: boolean
36
+ /** CSS files extracted from this chunk */
37
+ css?: string[]
38
+ /** Asset files referenced by this chunk (images, fonts, etc.) */
39
+ assets?: string[]
40
+ /** Chunk keys for static imports this chunk depends on */
41
+ imports?: string[]
42
+ /** Chunk keys for dynamic imports */
43
+ dynamicImports?: string[]
44
+ }
45
+
46
+ /** The full Vite manifest — a map from source path to chunk info. */
47
+ export type ViteManifest = Record<string, ManifestChunk>
48
+
49
+ /** Options passed to `vite.page()` for rendering a full HTML document. */
50
+ export interface PageOptions {
51
+ /** Entrypoint path(s), e.g. 'src/main.tsx' or ['src/main.tsx', 'src/extra.css'] */
52
+ entry: string | string[]
53
+ /** HTML document title */
54
+ title?: string
55
+ /** Data to inject as window.__MANTIQ_DATA__ for the client */
56
+ data?: Record<string, unknown>
57
+ /** Root element ID override (defaults to config value) */
58
+ rootElement?: string
59
+ /** Extra HTML to inject inside <head> (meta tags, fonts, etc.) */
60
+ head?: string
61
+ /** Request URL — passed to SSR render() for route-aware rendering */
62
+ url?: string
63
+ /** Page component identifier (e.g. 'Dashboard') — used for SSR page lookup */
64
+ page?: string
65
+ }
66
+
67
+ /** Result returned by an SSR module's render() function. */
68
+ export interface SSRResult {
69
+ html: string
70
+ head?: string
71
+ }
72
+
73
+ /** The SSR module must export a render() function matching this shape. */
74
+ export interface SSRModule {
75
+ render(url: string, data?: Record<string, unknown>): Promise<SSRResult> | SSRResult
76
+ }
77
+
78
+ /** Options passed to `vite.render()` for universal (Inertia-like) page responses. */
79
+ export interface RenderOptions {
80
+ /** Page component name (e.g. 'Dashboard', 'Login') */
81
+ page: string
82
+ /** Client entrypoint(s) for asset tags */
83
+ entry: string | string[]
84
+ /** Page data passed to the component */
85
+ data?: Record<string, unknown>
86
+ /** HTML document title */
87
+ title?: string
88
+ /** Extra HTML to inject inside <head> */
89
+ head?: string
90
+ }
@@ -0,0 +1,41 @@
1
+ import { MantiqError } from '@mantiq/core'
2
+
3
+ /** Thrown when the Vite manifest file cannot be found (forgot to run `vite build`). */
4
+ export class ViteManifestNotFoundError extends MantiqError {
5
+ constructor(manifestPath: string) {
6
+ super(
7
+ `Vite manifest not found at "${manifestPath}". Did you run "vite build"?`,
8
+ { manifestPath },
9
+ )
10
+ }
11
+ }
12
+
13
+ /** Thrown when a requested entrypoint is not present in the Vite manifest. */
14
+ export class ViteEntrypointNotFoundError extends MantiqError {
15
+ constructor(entrypoint: string, manifestPath: string) {
16
+ super(
17
+ `Entrypoint "${entrypoint}" not found in Vite manifest at "${manifestPath}".`,
18
+ { entrypoint, manifestPath },
19
+ )
20
+ }
21
+ }
22
+
23
+ /** Thrown when the SSR bundle cannot be found in production. */
24
+ export class ViteSSRBundleNotFoundError extends MantiqError {
25
+ constructor(bundlePath: string) {
26
+ super(
27
+ `SSR bundle not found at "${bundlePath}". Did you run "vite build --ssr"?`,
28
+ { bundlePath },
29
+ )
30
+ }
31
+ }
32
+
33
+ /** Thrown when the SSR module does not export a valid render() function. */
34
+ export class ViteSSREntryError extends MantiqError {
35
+ constructor(entry: string, reason: string) {
36
+ super(
37
+ `SSR entry "${entry}" is invalid: ${reason}`,
38
+ { entry, reason },
39
+ )
40
+ }
41
+ }
@@ -0,0 +1,15 @@
1
+ import { Application } from '@mantiq/core'
2
+ import { Vite } from '../Vite.ts'
3
+
4
+ /**
5
+ * Get the Vite instance from the application container.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { vite } from '@mantiq/vite'
10
+ * const html = await vite().page({ entry: 'src/main.tsx', title: 'Home' })
11
+ * ```
12
+ */
13
+ export function vite(): Vite {
14
+ return Application.getInstance().make(Vite)
15
+ }
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ // @mantiq/vite — public API exports
2
+
3
+ // ── Contracts ────────────────────────────────────────────────────────────────
4
+ export type {
5
+ ViteConfig,
6
+ ViteManifest,
7
+ ManifestChunk,
8
+ PageOptions,
9
+ SSRResult,
10
+ SSRModule,
11
+ RenderOptions,
12
+ } from './contracts/Vite.ts'
13
+
14
+ // ── Errors ───────────────────────────────────────────────────────────────────
15
+ export {
16
+ ViteManifestNotFoundError,
17
+ ViteEntrypointNotFoundError,
18
+ ViteSSRBundleNotFoundError,
19
+ ViteSSREntryError,
20
+ } from './errors/ViteError.ts'
21
+
22
+ // ── Main Class ───────────────────────────────────────────────────────────────
23
+ export { Vite, escapeHtml } from './Vite.ts'
24
+
25
+ // ── Service Provider ─────────────────────────────────────────────────────────
26
+ export { ViteServiceProvider, VITE } from './ViteServiceProvider.ts'
27
+
28
+ // ── Middleware ────────────────────────────────────────────────────────────────
29
+ export { ServeStaticFiles } from './middleware/ServeStaticFiles.ts'
30
+
31
+ // ── Helpers ──────────────────────────────────────────────────────────────────
32
+ export { vite } from './helpers/vite.ts'
@@ -0,0 +1,69 @@
1
+ import type { Middleware, NextFunction, MantiqRequest } from '@mantiq/core'
2
+ import { Vite } from '../Vite.ts'
3
+
4
+ /**
5
+ * Serves static files from the public directory.
6
+ * Resolves the public dir from the Vite config automatically.
7
+ * Useful during development — in production, use a reverse proxy (nginx/CDN).
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * kernel.registerMiddleware('static', ServeStaticFiles)
12
+ * kernel.setGlobalMiddleware(['static', 'log', 'cors'])
13
+ * ```
14
+ */
15
+ export class ServeStaticFiles implements Middleware {
16
+ private publicDir: string | null = null
17
+
18
+ constructor(private vite?: Vite) {}
19
+
20
+ setParameters(params: string[]): void {
21
+ if (params[0]) this.publicDir = params[0]
22
+ }
23
+
24
+ private getPublicDir(): string {
25
+ if (this.publicDir) return this.publicDir
26
+ if (this.vite) return this.vite.getConfig().publicDir
27
+ return 'public'
28
+ }
29
+
30
+ async handle(request: MantiqRequest, next: NextFunction): Promise<Response> {
31
+ // Only serve static files for GET/HEAD requests
32
+ const method = request.method()
33
+ if (method !== 'GET' && method !== 'HEAD') {
34
+ return next()
35
+ }
36
+
37
+ const urlPath = request.path()
38
+
39
+ // Prevent directory traversal
40
+ if (urlPath.includes('..') || urlPath.includes('\0')) {
41
+ return next()
42
+ }
43
+
44
+ // Skip the hot file — it's internal
45
+ if (urlPath === '/hot') {
46
+ return next()
47
+ }
48
+
49
+ const filePath = `${this.getPublicDir()}${urlPath}`
50
+ const file = Bun.file(filePath)
51
+
52
+ if (await file.exists()) {
53
+ // Skip directories (files without extensions that are size 0)
54
+ if (file.size === 0 && !urlPath.includes('.')) {
55
+ return next()
56
+ }
57
+
58
+ return new Response(file, {
59
+ headers: {
60
+ 'Content-Type': file.type,
61
+ 'Content-Length': String(file.size),
62
+ 'Cache-Control': 'no-cache',
63
+ },
64
+ })
65
+ }
66
+
67
+ return next()
68
+ }
69
+ }