@strav/view 0.2.4 → 0.2.8
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/package.json +4 -3
- package/src/cache.ts +1 -0
- package/src/client/index.ts +13 -0
- package/src/client/route_helper.ts +181 -0
- package/src/compiler.ts +40 -2
- package/src/engine.ts +69 -7
- package/src/index.ts +4 -0
- package/src/islands/island_builder.ts +58 -0
- package/src/tokenizer.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strav/view",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "View layer for the Strav framework — template engine, Vue SFC islands, and SPA client router",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,14 +22,15 @@
|
|
|
22
22
|
],
|
|
23
23
|
"exports": {
|
|
24
24
|
".": "./src/index.ts",
|
|
25
|
+
"./client": "./src/client/index.ts",
|
|
25
26
|
"./client/*": "./src/client/*.ts",
|
|
26
27
|
"./islands/*": "./src/islands/*.ts"
|
|
27
28
|
},
|
|
28
29
|
"peerDependencies": {
|
|
29
30
|
"vue": "^3.5.0",
|
|
30
31
|
"sass": "^1.80.0",
|
|
31
|
-
"@strav/kernel": "0.
|
|
32
|
-
"@strav/http": "0.
|
|
32
|
+
"@strav/kernel": "0.2.6",
|
|
33
|
+
"@strav/http": "0.2.6"
|
|
33
34
|
},
|
|
34
35
|
"peerDependenciesMeta": {
|
|
35
36
|
"sass": {
|
package/src/cache.ts
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe exports from @strav/view
|
|
3
|
+
*
|
|
4
|
+
* This module contains only client-side functionality that can be safely
|
|
5
|
+
* bundled for the browser without pulling in Node.js dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Re-export only browser-safe route helpers
|
|
9
|
+
export { route, routeUrl, registerRoutes } from './route_helper.ts'
|
|
10
|
+
export type { RouteOptions } from './route_helper.ts'
|
|
11
|
+
|
|
12
|
+
// Re-export client-side router (SPA router)
|
|
13
|
+
export * from './router.ts'
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-safe route helpers that work without the server-side router instance.
|
|
3
|
+
*
|
|
4
|
+
* These functions maintain the same API as the server-side route helpers but
|
|
5
|
+
* require route definitions to be provided at runtime or fallback to URL construction.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface RouteOptions extends Omit<RequestInit, 'body'> {
|
|
9
|
+
params?: Record<string, any>
|
|
10
|
+
body?: any
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Global registry for client-side route definitions
|
|
14
|
+
const clientRoutes = new Map<string, { method: string; pattern: string }>()
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Register route definitions for client-side use.
|
|
18
|
+
* This should be called during app initialization with route data from the server.
|
|
19
|
+
*/
|
|
20
|
+
export function registerRoutes(routes: Record<string, { method: string; pattern: string }>) {
|
|
21
|
+
Object.entries(routes).forEach(([name, def]) => {
|
|
22
|
+
clientRoutes.set(name, def)
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generate a URL for a named route with optional parameters.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* const profileUrl = routeUrl('users.profile', { id: 456 })
|
|
31
|
+
* // Returns '/users/456'
|
|
32
|
+
*/
|
|
33
|
+
export function routeUrl(name: string, params?: Record<string, any>): string {
|
|
34
|
+
const routeDef = clientRoutes.get(name)
|
|
35
|
+
if (!routeDef) {
|
|
36
|
+
throw new Error(`Route '${name}' not found. Make sure to call registerRoutes() with route definitions.`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return generateUrl(routeDef.pattern, params)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Invoke a named route with automatic method detection and smart defaults.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // Simple POST with JSON body
|
|
47
|
+
* await route('auth.register', {
|
|
48
|
+
* name: 'John',
|
|
49
|
+
* email: 'john@example.com',
|
|
50
|
+
* password: 'secret'
|
|
51
|
+
* })
|
|
52
|
+
*
|
|
53
|
+
* // GET with URL parameters
|
|
54
|
+
* await route('users.show', { params: { id: 123 } })
|
|
55
|
+
*/
|
|
56
|
+
export async function route(
|
|
57
|
+
name: string,
|
|
58
|
+
data?: any,
|
|
59
|
+
options?: RouteOptions
|
|
60
|
+
): Promise<Response> {
|
|
61
|
+
const routeDef = clientRoutes.get(name)
|
|
62
|
+
if (!routeDef) {
|
|
63
|
+
throw new Error(`Route '${name}' not found. Make sure to call registerRoutes() with route definitions.`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Determine if data is the body or options
|
|
67
|
+
let body: any
|
|
68
|
+
let opts: RouteOptions = {}
|
|
69
|
+
|
|
70
|
+
if (data !== undefined) {
|
|
71
|
+
// If data has params, body, or any RequestInit properties, treat it as options
|
|
72
|
+
if (
|
|
73
|
+
typeof data === 'object' &&
|
|
74
|
+
!Array.isArray(data) &&
|
|
75
|
+
!(data instanceof FormData) &&
|
|
76
|
+
!(data instanceof Blob) &&
|
|
77
|
+
!(data instanceof ArrayBuffer) &&
|
|
78
|
+
!(data instanceof URLSearchParams) &&
|
|
79
|
+
('params' in data || 'body' in data || 'headers' in data || 'cache' in data ||
|
|
80
|
+
'credentials' in data || 'mode' in data || 'redirect' in data || 'referrer' in data)
|
|
81
|
+
) {
|
|
82
|
+
opts = data
|
|
83
|
+
body = opts.body
|
|
84
|
+
} else {
|
|
85
|
+
// Otherwise, treat data as the body
|
|
86
|
+
body = data
|
|
87
|
+
opts = options || {}
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
opts = options || {}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Generate URL with parameters
|
|
94
|
+
const generatedUrl = generateUrl(routeDef.pattern, opts.params)
|
|
95
|
+
|
|
96
|
+
// Determine method from route definition
|
|
97
|
+
const method = opts.method || routeDef.method
|
|
98
|
+
|
|
99
|
+
// Build headers with smart defaults
|
|
100
|
+
const headers = new Headers(opts.headers)
|
|
101
|
+
|
|
102
|
+
// Set default Accept header if not provided
|
|
103
|
+
if (!headers.has('Accept')) {
|
|
104
|
+
headers.set('Accept', 'application/json')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Handle body and Content-Type
|
|
108
|
+
let requestBody: string | FormData | Blob | ArrayBuffer | URLSearchParams | undefined
|
|
109
|
+
if (body !== undefined && method !== 'GET' && method !== 'HEAD') {
|
|
110
|
+
if (body instanceof FormData || body instanceof Blob || body instanceof ArrayBuffer || body instanceof URLSearchParams) {
|
|
111
|
+
// Let fetch set the Content-Type for FormData, or use the existing type for Blob/ArrayBuffer
|
|
112
|
+
requestBody = body
|
|
113
|
+
} else if (typeof body === 'object') {
|
|
114
|
+
// JSON body
|
|
115
|
+
if (!headers.has('Content-Type')) {
|
|
116
|
+
headers.set('Content-Type', 'application/json')
|
|
117
|
+
}
|
|
118
|
+
requestBody = JSON.stringify(body)
|
|
119
|
+
} else {
|
|
120
|
+
// String or other primitive
|
|
121
|
+
requestBody = String(body)
|
|
122
|
+
if (!headers.has('Content-Type')) {
|
|
123
|
+
headers.set('Content-Type', 'text/plain')
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Set default credentials if not provided
|
|
129
|
+
const credentials = opts.credentials || 'same-origin'
|
|
130
|
+
|
|
131
|
+
// Build final fetch options
|
|
132
|
+
const fetchOptions: RequestInit = {
|
|
133
|
+
...opts,
|
|
134
|
+
method,
|
|
135
|
+
headers,
|
|
136
|
+
credentials,
|
|
137
|
+
...(requestBody !== undefined && { body: requestBody })
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Remove our custom properties
|
|
141
|
+
delete (fetchOptions as any).params
|
|
142
|
+
|
|
143
|
+
return fetch(generatedUrl, fetchOptions)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Generate URL from pattern and parameters (browser-safe version)
|
|
148
|
+
*/
|
|
149
|
+
function generateUrl(pattern: string, params?: Record<string, any>): string {
|
|
150
|
+
let url = pattern
|
|
151
|
+
const queryParams: Record<string, string> = {}
|
|
152
|
+
|
|
153
|
+
if (params) {
|
|
154
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
155
|
+
const paramPattern = `:${key}`
|
|
156
|
+
const wildcardPattern = `*${key}`
|
|
157
|
+
|
|
158
|
+
if (url.includes(paramPattern)) {
|
|
159
|
+
// Replace route parameter
|
|
160
|
+
url = url.replace(paramPattern, encodeURIComponent(String(value)))
|
|
161
|
+
} else if (url.includes(wildcardPattern)) {
|
|
162
|
+
// Replace wildcard parameter
|
|
163
|
+
url = url.replace(`/${wildcardPattern}`, `/${encodeURIComponent(String(value))}`)
|
|
164
|
+
} else {
|
|
165
|
+
// Add as query parameter
|
|
166
|
+
queryParams[key] = String(value)
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Append query parameters
|
|
172
|
+
const queryString = Object.keys(queryParams)
|
|
173
|
+
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key] ?? '')}`)
|
|
174
|
+
.join('&')
|
|
175
|
+
|
|
176
|
+
if (queryString) {
|
|
177
|
+
url += (url.includes('?') ? '&' : '?') + queryString
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return url
|
|
181
|
+
}
|
package/src/compiler.ts
CHANGED
|
@@ -7,9 +7,10 @@ export interface CompilationResult {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
interface StackEntry {
|
|
10
|
-
type: 'if' | 'each' | 'section'
|
|
10
|
+
type: 'if' | 'each' | 'section' | 'push' | 'prepend'
|
|
11
11
|
line: number
|
|
12
12
|
blockName?: string
|
|
13
|
+
stackName?: string
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
function escapeJs(str: string): string {
|
|
@@ -94,6 +95,7 @@ export function compile(tokens: Token[]): CompilationResult {
|
|
|
94
95
|
|
|
95
96
|
lines.push('let __out = "";')
|
|
96
97
|
lines.push('const __blocks = {};')
|
|
98
|
+
lines.push('const __stacks = {};')
|
|
97
99
|
|
|
98
100
|
for (const token of tokens) {
|
|
99
101
|
switch (token.type) {
|
|
@@ -147,7 +149,7 @@ export function compile(tokens: Token[]): CompilationResult {
|
|
|
147
149
|
throw new TemplateError(`Unclosed @${unclosed.type} block (opened at line ${unclosed.line})`)
|
|
148
150
|
}
|
|
149
151
|
|
|
150
|
-
lines.push('return { output: __out, blocks: __blocks };')
|
|
152
|
+
lines.push('return { output: __out, blocks: __blocks, stacks: __stacks };')
|
|
151
153
|
|
|
152
154
|
return { code: lines.join('\n'), layout }
|
|
153
155
|
}
|
|
@@ -274,6 +276,40 @@ function compileDirective(
|
|
|
274
276
|
)
|
|
275
277
|
break
|
|
276
278
|
|
|
279
|
+
case 'push': {
|
|
280
|
+
if (!token.args) throw new TemplateError(`@push requires a name at line ${token.line}`)
|
|
281
|
+
const name = token.args.replace(/^['"]|['"]$/g, '').trim()
|
|
282
|
+
const nameStr = JSON.stringify(name)
|
|
283
|
+
// Initialize stack array if it doesn't exist, then capture content and push
|
|
284
|
+
lines.push(`if (!__stacks[${nameStr}]) __stacks[${nameStr}] = [];`)
|
|
285
|
+
lines.push(`__stacks[${nameStr}].push((function() { let __out = "";`)
|
|
286
|
+
stack.push({ type: 'push', line: token.line, stackName: name })
|
|
287
|
+
break
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
case 'prepend': {
|
|
291
|
+
if (!token.args) throw new TemplateError(`@prepend requires a name at line ${token.line}`)
|
|
292
|
+
const name = token.args.replace(/^['"]|['"]$/g, '').trim()
|
|
293
|
+
const nameStr = JSON.stringify(name)
|
|
294
|
+
// Initialize stack array if it doesn't exist, then capture content and unshift
|
|
295
|
+
lines.push(`if (!__stacks[${nameStr}]) __stacks[${nameStr}] = [];`)
|
|
296
|
+
lines.push(`__stacks[${nameStr}].unshift((function() { let __out = "";`)
|
|
297
|
+
stack.push({ type: 'prepend', line: token.line, stackName: name })
|
|
298
|
+
break
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
case 'stack': {
|
|
302
|
+
if (!token.args) throw new TemplateError(`@stack requires a name at line ${token.line}`)
|
|
303
|
+
const name = token.args.replace(/^['"]|['"]$/g, '').trim()
|
|
304
|
+
const nameStr = JSON.stringify(name)
|
|
305
|
+
// Output joined stack content - merge local and passed stacks
|
|
306
|
+
lines.push(`{`)
|
|
307
|
+
lines.push(` const __mergedStack = [...((__data.__stacks && __data.__stacks[${nameStr}]) || []), ...(__stacks[${nameStr}] || [])];`)
|
|
308
|
+
lines.push(` __out += __mergedStack.join('');`)
|
|
309
|
+
lines.push(`}`)
|
|
310
|
+
break
|
|
311
|
+
}
|
|
312
|
+
|
|
277
313
|
case 'end': {
|
|
278
314
|
if (!stack.length) {
|
|
279
315
|
throw new TemplateError(`Unexpected @end at line ${token.line} — no open block`)
|
|
@@ -281,6 +317,8 @@ function compileDirective(
|
|
|
281
317
|
const top = stack.pop()!
|
|
282
318
|
if (top.type === 'section') {
|
|
283
319
|
lines.push(` return __out; })();`)
|
|
320
|
+
} else if (top.type === 'push' || top.type === 'prepend') {
|
|
321
|
+
lines.push(` return __out; })());`)
|
|
284
322
|
} else if (top.type === 'each') {
|
|
285
323
|
lines.push(` }`) // close for loop
|
|
286
324
|
lines.push(`}`) // close block scope
|
package/src/engine.ts
CHANGED
|
@@ -48,7 +48,8 @@ export default class ViewEngine {
|
|
|
48
48
|
private async renderWithDepth(
|
|
49
49
|
name: string,
|
|
50
50
|
data: Record<string, unknown>,
|
|
51
|
-
depth: number
|
|
51
|
+
depth: number,
|
|
52
|
+
parentStacks: Record<string, string[]> = {}
|
|
52
53
|
): Promise<string> {
|
|
53
54
|
if (depth > MAX_INCLUDE_DEPTH) {
|
|
54
55
|
throw new TemplateError(
|
|
@@ -58,21 +59,82 @@ export default class ViewEngine {
|
|
|
58
59
|
|
|
59
60
|
const entry = await this.resolve(name)
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
// Create an include function that merges stacks from includes
|
|
63
|
+
const includeFn: IncludeFn = async (includeName, includeData) => {
|
|
64
|
+
const includeResult = await this.renderWithDepthInternal(
|
|
65
|
+
includeName,
|
|
66
|
+
{ ...data, ...includeData },
|
|
67
|
+
depth + 1,
|
|
68
|
+
{}
|
|
69
|
+
)
|
|
70
|
+
// Merge include's stacks into parent stacks
|
|
71
|
+
this.mergeStacks(parentStacks, includeResult.stacks)
|
|
72
|
+
return includeResult.output
|
|
63
73
|
}
|
|
64
74
|
|
|
65
|
-
|
|
75
|
+
// Pass parent stacks to the template
|
|
76
|
+
const dataWithStacks = { ...data, __stacks: { ...parentStacks } }
|
|
77
|
+
const result = await entry.fn(dataWithStacks, includeFn)
|
|
78
|
+
|
|
79
|
+
// Merge current template's stacks with parent stacks
|
|
80
|
+
this.mergeStacks(parentStacks, result.stacks)
|
|
66
81
|
|
|
67
|
-
// Layout inheritance: render child first, then render layout with blocks merged
|
|
82
|
+
// Layout inheritance: render child first, then render layout with blocks and stacks merged
|
|
68
83
|
if (entry.layout) {
|
|
69
|
-
const layoutData = { ...data, ...result.blocks }
|
|
70
|
-
return this.renderWithDepth(entry.layout, layoutData, depth + 1)
|
|
84
|
+
const layoutData = { ...data, ...result.blocks, __stacks: parentStacks }
|
|
85
|
+
return this.renderWithDepth(entry.layout, layoutData, depth + 1, parentStacks)
|
|
71
86
|
}
|
|
72
87
|
|
|
73
88
|
return result.output
|
|
74
89
|
}
|
|
75
90
|
|
|
91
|
+
private async renderWithDepthInternal(
|
|
92
|
+
name: string,
|
|
93
|
+
data: Record<string, unknown>,
|
|
94
|
+
depth: number,
|
|
95
|
+
parentStacks: Record<string, string[]> = {}
|
|
96
|
+
): Promise<{ output: string; stacks: Record<string, string[]> }> {
|
|
97
|
+
if (depth > MAX_INCLUDE_DEPTH) {
|
|
98
|
+
throw new TemplateError(
|
|
99
|
+
`Maximum include depth (${MAX_INCLUDE_DEPTH}) exceeded — possible circular include`
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const entry = await this.resolve(name)
|
|
104
|
+
|
|
105
|
+
const includeFn: IncludeFn = async (includeName, includeData) => {
|
|
106
|
+
const includeResult = await this.renderWithDepthInternal(
|
|
107
|
+
includeName,
|
|
108
|
+
{ ...data, ...includeData },
|
|
109
|
+
depth + 1,
|
|
110
|
+
{}
|
|
111
|
+
)
|
|
112
|
+
this.mergeStacks(parentStacks, includeResult.stacks)
|
|
113
|
+
return includeResult.output
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const dataWithStacks = { ...data, __stacks: { ...parentStacks } }
|
|
117
|
+
const result = await entry.fn(dataWithStacks, includeFn)
|
|
118
|
+
this.mergeStacks(parentStacks, result.stacks)
|
|
119
|
+
|
|
120
|
+
if (entry.layout) {
|
|
121
|
+
const layoutData = { ...data, ...result.blocks, __stacks: parentStacks }
|
|
122
|
+
const layoutOutput = await this.renderWithDepth(entry.layout, layoutData, depth + 1, parentStacks)
|
|
123
|
+
return { output: layoutOutput, stacks: parentStacks }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { output: result.output, stacks: parentStacks }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private mergeStacks(target: Record<string, string[]>, source: Record<string, string[]>): void {
|
|
130
|
+
for (const [key, values] of Object.entries(source)) {
|
|
131
|
+
if (!target[key]) {
|
|
132
|
+
target[key] = []
|
|
133
|
+
}
|
|
134
|
+
target[key].push(...values)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
76
138
|
private async resolve(name: string): Promise<CacheEntry> {
|
|
77
139
|
const cached = this.cache.get(name)
|
|
78
140
|
|
package/src/index.ts
CHANGED
|
@@ -10,8 +10,12 @@ export { defineRoutes } from './route_types.ts'
|
|
|
10
10
|
export { spaRoutes } from './spa_routes.ts'
|
|
11
11
|
export { default as ViewProvider } from './providers/view_provider.ts'
|
|
12
12
|
|
|
13
|
+
// Client-side route helpers (browser-safe)
|
|
14
|
+
export { route, routeUrl, registerRoutes } from './client/route_helper.ts'
|
|
15
|
+
|
|
13
16
|
export type { Token, TokenType, VueAttr } from './tokenizer.ts'
|
|
14
17
|
export type { CompilationResult } from './compiler.ts'
|
|
15
18
|
export type { CacheEntry, RenderFunction, IncludeFn, RenderResult } from './cache.ts'
|
|
16
19
|
export type { CssOptions, IslandBuilderOptions, IslandManifest } from './islands/island_builder.ts'
|
|
17
20
|
export type { SpaRouteDefinition } from './route_types.ts'
|
|
21
|
+
export type { RouteOptions } from './client/route_helper.ts'
|
|
@@ -12,6 +12,15 @@ import { vueSfcPlugin } from './vue_plugin.ts'
|
|
|
12
12
|
import ViewEngine from '../engine.ts'
|
|
13
13
|
import type { BunPlugin } from 'bun'
|
|
14
14
|
|
|
15
|
+
// Router type for route injection (optional dependency)
|
|
16
|
+
interface Router {
|
|
17
|
+
getAllRoutes(): readonly {
|
|
18
|
+
name?: string
|
|
19
|
+
method: string
|
|
20
|
+
pattern: string
|
|
21
|
+
}[]
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
export interface CssOptions {
|
|
16
25
|
/** Sass entry file path. e.g. 'resources/css/app.scss' */
|
|
17
26
|
entry: string
|
|
@@ -62,6 +71,7 @@ export class IslandBuilder {
|
|
|
62
71
|
private _manifest: IslandManifest | null = null
|
|
63
72
|
private cssOpts: { entry: string; outFile: string; outDir: string; basePath: string } | null = null
|
|
64
73
|
private _cssVersion: string | null = null
|
|
74
|
+
private router: Router | null = null
|
|
65
75
|
|
|
66
76
|
constructor(options: IslandBuilderOptions = {}) {
|
|
67
77
|
this.islandsDir = resolve(options.islandsDir ?? './resources/islands')
|
|
@@ -137,6 +147,25 @@ export class IslandBuilder {
|
|
|
137
147
|
const lines: string[] = []
|
|
138
148
|
|
|
139
149
|
lines.push(`import { createApp, defineComponent, h, Teleport } from 'vue';`)
|
|
150
|
+
|
|
151
|
+
// Auto-inject route definitions if router is available
|
|
152
|
+
if (this.router) {
|
|
153
|
+
const routeDefinitions = this.extractRouteDefinitions()
|
|
154
|
+
const routeCount = Object.keys(routeDefinitions).length
|
|
155
|
+
|
|
156
|
+
if (routeCount > 0) {
|
|
157
|
+
lines.push(`import { registerRoutes } from '@strav/view/client';`)
|
|
158
|
+
lines.push('')
|
|
159
|
+
lines.push(`// Auto-injected route definitions (${routeCount} routes)`)
|
|
160
|
+
lines.push(`registerRoutes(${JSON.stringify(routeDefinitions, null, 2)});`)
|
|
161
|
+
console.log(`[islands] ✅ Auto-injecting ${routeCount} route definitions:`, Object.keys(routeDefinitions))
|
|
162
|
+
} else {
|
|
163
|
+
console.log(`[islands] ⚠️ Router provided but no named routes found - skipping route auto-injection`)
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
// Silent: No router provided - skipping route auto-injection
|
|
167
|
+
}
|
|
168
|
+
|
|
140
169
|
lines.push('')
|
|
141
170
|
|
|
142
171
|
if (setupPath) {
|
|
@@ -297,6 +326,35 @@ export class IslandBuilder {
|
|
|
297
326
|
)
|
|
298
327
|
}
|
|
299
328
|
|
|
329
|
+
/** Extract route definitions for client-side registration. */
|
|
330
|
+
private extractRouteDefinitions(): Record<string, { method: string; pattern: string }> {
|
|
331
|
+
if (!this.router) return {}
|
|
332
|
+
|
|
333
|
+
const routeMap: Record<string, { method: string; pattern: string }> = {}
|
|
334
|
+
|
|
335
|
+
for (const route of this.router.getAllRoutes()) {
|
|
336
|
+
if (route.name) { // Only include named routes
|
|
337
|
+
routeMap[route.name] = {
|
|
338
|
+
method: route.method,
|
|
339
|
+
pattern: route.pattern
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return routeMap
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Build the islands bundle with route auto-injection.
|
|
349
|
+
* @param router - Router instance containing routes to inject
|
|
350
|
+
*/
|
|
351
|
+
async buildWithRoutes(router: Router): Promise<boolean> {
|
|
352
|
+
this.router = router // Set the router for this build
|
|
353
|
+
const result = await this.build() // Use existing build method
|
|
354
|
+
this.router = null // Clear router reference after build
|
|
355
|
+
return result
|
|
356
|
+
}
|
|
357
|
+
|
|
300
358
|
/** Build the islands bundle. Returns true if islands were found and built. */
|
|
301
359
|
async build(): Promise<boolean> {
|
|
302
360
|
const islands = this.discoverIslands()
|