@bakery-framework/core 1.0.0
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/LICENSE +19 -0
- package/README.md +89 -0
- package/package.json +69 -0
- package/src/cache/index.ts +8 -0
- package/src/cache/lru.ts +41 -0
- package/src/cache/shared-db.ts +51 -0
- package/src/cache/string.ts +150 -0
- package/src/cache/tiered.ts +493 -0
- package/src/client/globals.d.ts +74 -0
- package/src/client/livereload.ts +437 -0
- package/src/client/utils.ts +315 -0
- package/src/compiler/compiler.ts +263 -0
- package/src/compiler/dev-service.ts +660 -0
- package/src/compiler/index.ts +2 -0
- package/src/compiler/prompt-tracker.ts +36 -0
- package/src/compiler/tsconfig-sync.ts +71 -0
- package/src/core/bakery.ts +96 -0
- package/src/core/cache-version.ts +119 -0
- package/src/core/config.ts +296 -0
- package/src/core/context.ts +121 -0
- package/src/core/index.ts +61 -0
- package/src/core/init.ts +90 -0
- package/src/core/jsx.ts +152 -0
- package/src/core/paths.ts +24 -0
- package/src/core/plugins.ts +120 -0
- package/src/core/port.ts +73 -0
- package/src/global.d.ts +374 -0
- package/src/handlers/assets/google-font.ts +225 -0
- package/src/handlers/assets/image.ts +136 -0
- package/src/handlers/assets/nm.ts +73 -0
- package/src/handlers/assets/public.ts +17 -0
- package/src/handlers/assets/static.ts +86 -0
- package/src/handlers/assets/ts.ts +61 -0
- package/src/handlers/assets/tsx.ts +106 -0
- package/src/handlers/assets/virtual-asset.ts +104 -0
- package/src/handlers/core/$base.ts +256 -0
- package/src/handlers/core/$dynamic.ts +285 -0
- package/src/handlers/core/$error.ts +301 -0
- package/src/handlers/core/$middleware.ts +71 -0
- package/src/handlers/core/$mounts.ts +84 -0
- package/src/handlers/core/$registry.ts +153 -0
- package/src/handlers/core/$routing.ts +205 -0
- package/src/handlers/core/$static.ts +100 -0
- package/src/handlers/core/$websocket.ts +52 -0
- package/src/handlers/index.ts +21 -0
- package/src/handlers/routes/api.ts +95 -0
- package/src/handlers/routes/html.ts +95 -0
- package/src/handlers/routes/livereload.ts +54 -0
- package/src/handlers/routes/proxy.ts +74 -0
- package/src/logger/clients.ts +12 -0
- package/src/logger/index.ts +3 -0
- package/src/logger/logger.ts +375 -0
- package/src/logger/serve-log.ts +206 -0
- package/src/plugins/index.ts +15 -0
- package/src/plugins/routes.ts +110 -0
- package/src/plugins/types.ts +19 -0
- package/src/router.ts +351 -0
- package/src/session.ts +556 -0
- package/src/shared.d.ts +63 -0
- package/src/startup.ts +154 -0
- package/src/types.d.ts +111 -0
- package/src/utils/common/case.ts +11 -0
- package/src/utils/common/index.ts +5 -0
- package/src/utils/common/json.ts +35 -0
- package/src/utils/common/match.ts +6 -0
- package/src/utils/common/misc.ts +53 -0
- package/src/utils/common/try.ts +6 -0
- package/src/utils/constants.ts +153 -0
- package/src/utils/fs.ts +621 -0
- package/src/utils/http/body.ts +65 -0
- package/src/utils/http/csrf.ts +111 -0
- package/src/utils/http/dom.ts +238 -0
- package/src/utils/http/escape.ts +8 -0
- package/src/utils/http/etag.ts +318 -0
- package/src/utils/http/html.ts +525 -0
- package/src/utils/http/index.ts +8 -0
- package/src/utils/http/ip.ts +32 -0
- package/src/utils/http/response.ts +129 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/isomorphic/case.ts +52 -0
- package/src/utils/isomorphic/escape.ts +43 -0
- package/src/utils/isomorphic/index.ts +15 -0
- package/src/utils/isomorphic/is.ts +36 -0
- package/src/utils/isomorphic/match.ts +50 -0
- package/src/utils/isomorphic/math.ts +11 -0
- package/src/utils/isomorphic/misc.ts +22 -0
- package/src/utils/isomorphic/stringify.ts +42 -0
- package/src/utils/isomorphic/try.ts +94 -0
- package/src/utils/jsonc.ts +10 -0
- package/src/utils/shared-pool.ts +193 -0
- package/tsconfig.app.json +34 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/** biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: '*/
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Browser runtime: binds the shared utilities onto `globalThis` and adds the
|
|
5
|
+
* DOM-dependent pieces ($fmt, speculation rules, live navigation) on top.
|
|
6
|
+
*
|
|
7
|
+
* The utilities themselves live in `utils/isomorphic/` and are imported rather
|
|
8
|
+
* than redefined here. This file used to carry its own copy of each one, which
|
|
9
|
+
* is how the $fmt replacer-arity bug and the match() prototype-chain bug each
|
|
10
|
+
* had to be fixed twice.
|
|
11
|
+
*/
|
|
12
|
+
import type { MapOf } from '../types'
|
|
13
|
+
import { Case } from '../utils/isomorphic/case'
|
|
14
|
+
import { escapeHtml as escapeHTML } from '../utils/isomorphic/escape'
|
|
15
|
+
import { is } from '../utils/isomorphic/is'
|
|
16
|
+
import { match, matchDefault } from '../utils/isomorphic/match'
|
|
17
|
+
import { Math2 } from '../utils/isomorphic/math'
|
|
18
|
+
import { any, assert, repeat, throws } from '../utils/isomorphic/misc'
|
|
19
|
+
import { circularReplacer } from '../utils/isomorphic/stringify'
|
|
20
|
+
import { Try, tryCatch } from '../utils/isomorphic/try'
|
|
21
|
+
|
|
22
|
+
export { escapeHTML }
|
|
23
|
+
|
|
24
|
+
function processGetBody(
|
|
25
|
+
body: FormData | MapOf<any> | URLSearchParams | string,
|
|
26
|
+
) {
|
|
27
|
+
if (body instanceof URLSearchParams) {
|
|
28
|
+
return body.toString()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (body instanceof FormData || (is.object(body) && body !== null)) {
|
|
32
|
+
const urlSearchParams = new URLSearchParams()
|
|
33
|
+
const entries =
|
|
34
|
+
body instanceof FormData ? body.entries() : Object.entries(body)
|
|
35
|
+
|
|
36
|
+
for (const [key, value] of entries) {
|
|
37
|
+
urlSearchParams.append(key, (value as any).toString())
|
|
38
|
+
}
|
|
39
|
+
return urlSearchParams.toString()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return String(body)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// `randomId` and `request` are exported purely so `client/globals.d.ts` can
|
|
46
|
+
// declare the globals as `typeof import('./utils').randomId` / `.request`
|
|
47
|
+
// instead of restating their signatures — which is how both had drifted from
|
|
48
|
+
// this file. Nothing imports them; the bundle entry's exports are inert.
|
|
49
|
+
export function randomId(length = 8) {
|
|
50
|
+
const arr = new Uint8Array(Math.ceil(length / 2))
|
|
51
|
+
crypto.getRandomValues(arr)
|
|
52
|
+
return Array.from(arr, dec => dec.toString(16).padStart(2, '0'))
|
|
53
|
+
.join('')
|
|
54
|
+
.slice(0, length)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type RequestJson = RequestInit & { body?: any }
|
|
58
|
+
|
|
59
|
+
export async function request(
|
|
60
|
+
url: string,
|
|
61
|
+
init: RequestJson | string = {},
|
|
62
|
+
bodyData?: any,
|
|
63
|
+
): Promise<JsonResponse> {
|
|
64
|
+
let options: RequestJson = {}
|
|
65
|
+
if (typeof init === 'string') {
|
|
66
|
+
options = { method: init, body: bodyData }
|
|
67
|
+
} else {
|
|
68
|
+
options = init || {}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const method = (options.method || 'GET').toUpperCase()
|
|
72
|
+
const body = options.body || {}
|
|
73
|
+
|
|
74
|
+
if (method === 'GET' && body && Object.keys(body).length > 0) {
|
|
75
|
+
const query = processGetBody(body)
|
|
76
|
+
if (query) {
|
|
77
|
+
url = `${url}${url.includes('?') ? '&' : '?'}${query}`
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const isFormData = body instanceof FormData
|
|
82
|
+
|
|
83
|
+
const response = await fetch(url, {
|
|
84
|
+
...options,
|
|
85
|
+
method,
|
|
86
|
+
body:
|
|
87
|
+
method === 'GET' ? undefined : isFormData ? body : JSON.stringify(body),
|
|
88
|
+
headers: {
|
|
89
|
+
...(isFormData ? {} : { 'Content-Type': 'application/json' }),
|
|
90
|
+
...(options.headers || {}),
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const [err, data] = await tryCatch(response.json.bind(response))
|
|
95
|
+
|
|
96
|
+
if (err) {
|
|
97
|
+
throws(`Request failed: ${err.message || 'Unknown error'}`)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (
|
|
101
|
+
data &&
|
|
102
|
+
typeof data === 'object' &&
|
|
103
|
+
'status' in data &&
|
|
104
|
+
'message' in data
|
|
105
|
+
) {
|
|
106
|
+
const status = (data as any).status
|
|
107
|
+
if (status >= 200 && status < 300) {
|
|
108
|
+
return data as JsonResponse
|
|
109
|
+
}
|
|
110
|
+
throws((data as any).message)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return data
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function formatHTML(html: string, indentWidth: number = 2): string {
|
|
117
|
+
if (!html) return ''
|
|
118
|
+
|
|
119
|
+
const cleanHtml = html
|
|
120
|
+
.replace(/\n/g, '')
|
|
121
|
+
.replace(/[\s]{2,}/g, ' ')
|
|
122
|
+
.replace(/>\s*</g, '><')
|
|
123
|
+
.trim()
|
|
124
|
+
|
|
125
|
+
const tokens = cleanHtml.match(/<[^>]+>|[^<]+/g) || []
|
|
126
|
+
let indentLevel = 0
|
|
127
|
+
|
|
128
|
+
return tokens.reduce((formattedString, token) => {
|
|
129
|
+
const isClosing = /^<\//.test(token)
|
|
130
|
+
const isSelfClosing =
|
|
131
|
+
/^<(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)[^>]*>/i.test(
|
|
132
|
+
token,
|
|
133
|
+
) || /<[^>]+\/>/.test(token)
|
|
134
|
+
const isOpening = /^<[^/!?]/.test(token) && !isSelfClosing
|
|
135
|
+
|
|
136
|
+
indentLevel = isClosing ? Math.max(0, indentLevel - 1) : indentLevel
|
|
137
|
+
|
|
138
|
+
const indent = ' '.repeat(indentLevel * indentWidth)
|
|
139
|
+
const appendedToken =
|
|
140
|
+
formattedString === '' ? token : `\n${indent}${token.trim()}`
|
|
141
|
+
|
|
142
|
+
indentLevel = isOpening ? indentLevel + 1 : indentLevel
|
|
143
|
+
|
|
144
|
+
return formattedString + appendedToken
|
|
145
|
+
}, '')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
Object.assign(globalThis, {
|
|
149
|
+
match,
|
|
150
|
+
matchDefault,
|
|
151
|
+
Try,
|
|
152
|
+
tryCatch,
|
|
153
|
+
is,
|
|
154
|
+
Case,
|
|
155
|
+
Math2,
|
|
156
|
+
throws,
|
|
157
|
+
assert,
|
|
158
|
+
any,
|
|
159
|
+
escapeHTML,
|
|
160
|
+
repeat,
|
|
161
|
+
request,
|
|
162
|
+
randomId,
|
|
163
|
+
Bakery: {
|
|
164
|
+
version: import.meta.env.BAKERY_VERSION,
|
|
165
|
+
async virtual(path: string) {
|
|
166
|
+
const response = await fetch(path)
|
|
167
|
+
if (!response.ok) {
|
|
168
|
+
return null
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const contentType = response.headers.get('Content-Type') || ''
|
|
172
|
+
if (contentType.includes('application/json')) {
|
|
173
|
+
return response.json()
|
|
174
|
+
} else {
|
|
175
|
+
const text = await response.text()
|
|
176
|
+
|
|
177
|
+
if (path.endsWith('.css')) {
|
|
178
|
+
const style = document.createElement('style')
|
|
179
|
+
style.textContent = text
|
|
180
|
+
document.head.appendChild(style)
|
|
181
|
+
return null
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// The body was already consumed above; re-reading it throws.
|
|
185
|
+
return text
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
params<T = MapOf<any>>(): T {
|
|
190
|
+
return any(window).__PAGE_PARAMS__ as T
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
$fmt(data: any): string {
|
|
194
|
+
if (data == null) return ''
|
|
195
|
+
if (is.string(data)) return data
|
|
196
|
+
if (is.bigint(data)) return `${data}n`
|
|
197
|
+
if (is.symbol(data)) return data.toString()
|
|
198
|
+
|
|
199
|
+
if (data instanceof Element) return `\n${formatHTML(data.outerHTML)}`
|
|
200
|
+
if (data instanceof Error) return data.stack || data.message || String(data)
|
|
201
|
+
if (data instanceof Date) return data.toISOString()
|
|
202
|
+
if (data instanceof RegExp) return data.toString()
|
|
203
|
+
|
|
204
|
+
if (is.function(data)) {
|
|
205
|
+
return data.name ? `[Function: ${data.name}]` : '[Function]'
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (data instanceof Set) {
|
|
209
|
+
const fmt = (globalThis as any).$fmt
|
|
210
|
+
return `Set(${data.size}) { ${Array.from(data)
|
|
211
|
+
.map(v => fmt(v))
|
|
212
|
+
.join(', ')} }`
|
|
213
|
+
}
|
|
214
|
+
if (data instanceof Map) {
|
|
215
|
+
const fmt = (globalThis as any).$fmt
|
|
216
|
+
return `Map(${data.size}) { ${Array.from(data.entries())
|
|
217
|
+
.map(([k, v]) => `${fmt(k)} => ${fmt(v)}`)
|
|
218
|
+
.join(', ')} }`
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (
|
|
222
|
+
Array.isArray(data) ||
|
|
223
|
+
typeof data?.toJSON === 'function' ||
|
|
224
|
+
(typeof data === 'object' &&
|
|
225
|
+
Object.prototype.toString.call(data) === '[object Object]')
|
|
226
|
+
) {
|
|
227
|
+
try {
|
|
228
|
+
// Cycle handling comes from circularReplacer; this adds only the
|
|
229
|
+
// display rules for values JSON has no representation for.
|
|
230
|
+
return JSON.stringify(
|
|
231
|
+
data,
|
|
232
|
+
circularReplacer((_key, value) => {
|
|
233
|
+
if (is.bigint(value)) return `${value}n`
|
|
234
|
+
if (is.function(value))
|
|
235
|
+
return value.name ? `[Function: ${value.name}]` : '[Function]'
|
|
236
|
+
if (value instanceof RegExp) return String(value)
|
|
237
|
+
return value
|
|
238
|
+
}),
|
|
239
|
+
2,
|
|
240
|
+
)
|
|
241
|
+
} catch {
|
|
242
|
+
return String(data)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return String(data)
|
|
247
|
+
},
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
if (typeof document !== 'undefined') {
|
|
251
|
+
let debounceTimer: any
|
|
252
|
+
|
|
253
|
+
const updateSpeculationRules = () => {
|
|
254
|
+
const urls = new Set<string>()
|
|
255
|
+
const elements = document.querySelectorAll(
|
|
256
|
+
'[href]:not(link, base, use, image)',
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
const ignorePattern = /([?&](utm_|fbclid)|\.(pdf|zip)$)/i
|
|
260
|
+
for (const el of elements) {
|
|
261
|
+
const prefetchAttr = el.getAttribute('prefetch')
|
|
262
|
+
if (prefetchAttr === 'false') continue
|
|
263
|
+
|
|
264
|
+
const url = el.getAttribute('href')?.trim()
|
|
265
|
+
|
|
266
|
+
if (
|
|
267
|
+
!url ||
|
|
268
|
+
url.startsWith('#') ||
|
|
269
|
+
url.includes(':') ||
|
|
270
|
+
ignorePattern.test(url) ||
|
|
271
|
+
url.toLowerCase().includes('logout')
|
|
272
|
+
)
|
|
273
|
+
continue
|
|
274
|
+
|
|
275
|
+
urls.add(url)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (urls.size === 0) return
|
|
279
|
+
|
|
280
|
+
document.querySelector('script[type="speculationrules"]')?.remove()
|
|
281
|
+
|
|
282
|
+
const specScript = document.createElement('script')
|
|
283
|
+
specScript.type = 'speculationrules'
|
|
284
|
+
|
|
285
|
+
// Prerender the same filtered list as prefetch, not every link on the page.
|
|
286
|
+
// A `source: 'document'` rule with href_matches '/*' and eager eagerness
|
|
287
|
+
// ignored the exclusions above and fully loaded — and ran the JS of — every
|
|
288
|
+
// same-origin link, including things like /logout or a destructive GET.
|
|
289
|
+
const urlList = Array.from(urls)
|
|
290
|
+
specScript.textContent = JSON.stringify({
|
|
291
|
+
prefetch: [{ source: 'list', urls: urlList }],
|
|
292
|
+
prerender: [{ source: 'list', urls: urlList, eagerness: 'moderate' }],
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
document.head.appendChild(specScript)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const observer = new MutationObserver(() => {
|
|
299
|
+
clearTimeout(debounceTimer)
|
|
300
|
+
debounceTimer = setTimeout(updateSpeculationRules, 1000)
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
const initObserver = () => {
|
|
304
|
+
updateSpeculationRules()
|
|
305
|
+
if (document.body) {
|
|
306
|
+
observer.observe(document.body, { childList: true, subtree: true })
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (document.readyState === 'loading') {
|
|
311
|
+
document.addEventListener('DOMContentLoaded', initObserver)
|
|
312
|
+
} else {
|
|
313
|
+
initObserver()
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { Strings } from '../cache/string'
|
|
2
|
+
import { Bakery } from '../core/bakery'
|
|
3
|
+
import { PluginHooks } from '../core/plugins'
|
|
4
|
+
import {
|
|
5
|
+
compLog,
|
|
6
|
+
errorMsg,
|
|
7
|
+
errorWithPosition,
|
|
8
|
+
handlerLog,
|
|
9
|
+
} from '../logger/serve-log'
|
|
10
|
+
import type { MapOf } from '../types'
|
|
11
|
+
import { is, Try } from '../utils/common'
|
|
12
|
+
import { FileSystem as fs } from '../utils/fs'
|
|
13
|
+
|
|
14
|
+
const RX_IMPORT =
|
|
15
|
+
/import\s+(?:(?:\*\s+as\s+)?([a-zA-Z_$\d\s{},/*]+?)\s+from\s+)?['"]([^'"]+?\.([a-zA-Z0-9]+))['"](?:\s+(?:with|assert)\s*\{[^}]+\})?\s*;?/gm
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The build-time `define` table, resolved on first use rather than at module
|
|
19
|
+
* evaluation.
|
|
20
|
+
*
|
|
21
|
+
* This used to be a top-level `await` reading `<cwd>/package.json`, which put a
|
|
22
|
+
* filesystem read on the boot path of every process that loads this module —
|
|
23
|
+
* and `handlers/assets/ts.ts` imports it, so that is every worker and every dev
|
|
24
|
+
* restart. The value it produces is needed only when something actually
|
|
25
|
+
* compiles; a production process serving out of a warm compile cache never
|
|
26
|
+
* needs it at all.
|
|
27
|
+
*
|
|
28
|
+
* Memoised as a promise, not a value, so two concurrent first compiles share
|
|
29
|
+
* one read instead of racing to do it twice.
|
|
30
|
+
*/
|
|
31
|
+
let definesPromise: Promise<MapOf<string>> | null = null
|
|
32
|
+
|
|
33
|
+
async function buildDefines(): Promise<MapOf<string>> {
|
|
34
|
+
let bakeryVersion = '1.0.0'
|
|
35
|
+
try {
|
|
36
|
+
const file = await Bun.file(`${process.cwd()}/package.json`).json()
|
|
37
|
+
if (file?.version) bakeryVersion = file.version
|
|
38
|
+
} catch {
|
|
39
|
+
// No package.json in cwd, or it is unreadable. The version is cosmetic —
|
|
40
|
+
// it lands in a banner and a build define — so the default above stands.
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
'import.meta.env.DEV': JSON.stringify(!!import.meta.env.DEV),
|
|
45
|
+
'import.meta.env.PROD': JSON.stringify(!import.meta.env.DEV),
|
|
46
|
+
'import.meta.env.WORKER': JSON.stringify(!!import.meta.env.WORKER),
|
|
47
|
+
'import.meta.env.MODE': JSON.stringify(
|
|
48
|
+
import.meta.env.MODE || 'production',
|
|
49
|
+
),
|
|
50
|
+
'import.meta.env.BAKERY_VERSION': JSON.stringify(bakeryVersion),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getDefines(): Promise<MapOf<string>> {
|
|
55
|
+
definesPromise ??= buildDefines()
|
|
56
|
+
return definesPromise
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let transpilerPromise: Promise<Bun.Transpiler> | null = null
|
|
60
|
+
function getTranspiler(): Promise<Bun.Transpiler> {
|
|
61
|
+
transpilerPromise ??= getDefines().then(
|
|
62
|
+
define =>
|
|
63
|
+
new Bun.Transpiler({
|
|
64
|
+
loader: 'ts',
|
|
65
|
+
inline: true,
|
|
66
|
+
trimUnusedImports: false,
|
|
67
|
+
minifyWhitespace: true,
|
|
68
|
+
target: 'browser',
|
|
69
|
+
deadCodeElimination: false,
|
|
70
|
+
define,
|
|
71
|
+
}),
|
|
72
|
+
)
|
|
73
|
+
return transpilerPromise
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let virtualIdCounter = 0
|
|
77
|
+
function nextVirtualId(): string {
|
|
78
|
+
return (++virtualIdCounter).toString(36)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function preprocessImports(source: string, filePath: fs.AbsolutePath): string {
|
|
82
|
+
const fileDir = fs.resolve(filePath)
|
|
83
|
+
|
|
84
|
+
const matches = [...source.matchAll(RX_IMPORT)]
|
|
85
|
+
|
|
86
|
+
for (const [string, varName, importPath, ext] of matches) {
|
|
87
|
+
if (ext !== 'css' && ext !== 'json') continue
|
|
88
|
+
|
|
89
|
+
const assetPath = fs.resolve(fileDir, importPath)
|
|
90
|
+
|
|
91
|
+
// The key is the bare id, never the `/_virtual/` URL. Storing the full URL
|
|
92
|
+
// meant getKey() returned an already-prefixed value that got prefixed again
|
|
93
|
+
// on the next compile — which both threw a StringCache collision and left
|
|
94
|
+
// VirtualAssetHandler (which looks up by bare id) unable to resolve it.
|
|
95
|
+
const id =
|
|
96
|
+
Strings.getKey(assetPath) || `${Date.now()}_${nextVirtualId()}.${ext}`
|
|
97
|
+
|
|
98
|
+
const quotedUrl = JSON.stringify(`/_virtual/${id}`)
|
|
99
|
+
|
|
100
|
+
const replacement = varName
|
|
101
|
+
? `const ${varName} = await Bakery.virtual(${quotedUrl});`
|
|
102
|
+
: `await Bakery.virtual(${quotedUrl});`
|
|
103
|
+
|
|
104
|
+
source = source.replace(string, () => replacement)
|
|
105
|
+
Strings.set(id, assetPath)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return source
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Compile `source`, naming `path` so a failure can say where it happened.
|
|
113
|
+
*
|
|
114
|
+
* The two branches are not symmetric on purpose. Without a path there is no
|
|
115
|
+
* file to name and no caller that can render a 500 — the Vue plugin compiles
|
|
116
|
+
* fragments of an SFC it has already parsed — so a failure logs and hands the
|
|
117
|
+
* source back. With a path the failure is a served request, and the answer is
|
|
118
|
+
* `null`: `TSHandler` turns that into the 500 it has always had a branch for.
|
|
119
|
+
*
|
|
120
|
+
* It used to be neither. The transpiler was wrapped only on the pathless
|
|
121
|
+
* branch, so a syntax error in a `.ts` asset threw straight past `compile()`,
|
|
122
|
+
* past `TSHandler` — leaving its `'Compilation Failed'` arm permanently
|
|
123
|
+
* unreachable — and into the worker's catch-all, which printed
|
|
124
|
+
* `Unhandled Server Error: <message>` with no file and no line while the client
|
|
125
|
+
* got `An unexpected error occurred.` A total blackout for a one-character typo.
|
|
126
|
+
*/
|
|
127
|
+
export function compileText(source: string): Promise<string>
|
|
128
|
+
export function compileText(
|
|
129
|
+
source: string,
|
|
130
|
+
path: fs.AbsolutePath,
|
|
131
|
+
): Promise<string | null>
|
|
132
|
+
export async function compileText(source: string, path?: fs.AbsolutePath) {
|
|
133
|
+
if (!path) {
|
|
134
|
+
try {
|
|
135
|
+
const content = await (await getTranspiler()).transform(source)
|
|
136
|
+
return content
|
|
137
|
+
} catch (err) {
|
|
138
|
+
compLog.COMPILE_SOURCE_FAIL({ error: errorMsg(err) })
|
|
139
|
+
return source
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
source = preprocessImports(source, path)
|
|
144
|
+
|
|
145
|
+
const [failed, transformed] = await Try.catch(
|
|
146
|
+
(await getTranspiler()).transform(source),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if (failed) {
|
|
150
|
+
// `errorWithPosition`, not `errorMsg`: the thrown `BuildMessage` has no
|
|
151
|
+
// stack, so `errorMsg` degrades to the bare message and the line number —
|
|
152
|
+
// the only thing that makes this actionable — is dropped. The file comes
|
|
153
|
+
// from `{file}`; the diagnostic's own `position.file` is `input.ts`,
|
|
154
|
+
// because `transform()` was handed a string.
|
|
155
|
+
compLog.COMPILE_FAIL({ file: path, error: errorWithPosition(failed) })
|
|
156
|
+
return null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const content = transformed!
|
|
160
|
+
const importRegex = /\b(from|import)(\s*\(?\s*)(["'])([^"']+)\3(\)?)/g
|
|
161
|
+
const matches = [...content.matchAll(importRegex)]
|
|
162
|
+
|
|
163
|
+
if (!matches.length) return content
|
|
164
|
+
|
|
165
|
+
const { dir } = fs.parse(path)
|
|
166
|
+
const serveRoot = Bakery.serveRoot
|
|
167
|
+
const importMap = Bakery.config.importMap
|
|
168
|
+
const mapKeys = Object.keys(importMap)
|
|
169
|
+
|
|
170
|
+
const replacements = await Promise.all(
|
|
171
|
+
matches.map(
|
|
172
|
+
async ([fullMatch, keyword, spacing, quote, importPath, closing]) => {
|
|
173
|
+
const hasExtension = (importPath.split('/').pop() || '').includes('.')
|
|
174
|
+
if (hasExtension) return fullMatch
|
|
175
|
+
|
|
176
|
+
const prefix = mapKeys.find(k => importPath.startsWith(k))
|
|
177
|
+
|
|
178
|
+
if (!prefix && !importPath.startsWith('.')) return fullMatch
|
|
179
|
+
|
|
180
|
+
const targetPath = prefix
|
|
181
|
+
? fs.resolve(
|
|
182
|
+
serveRoot,
|
|
183
|
+
importMap[prefix],
|
|
184
|
+
importPath.slice(prefix.length),
|
|
185
|
+
)
|
|
186
|
+
: fs.resolve(dir, importPath)
|
|
187
|
+
|
|
188
|
+
const isDir = await fs.isDir(targetPath)
|
|
189
|
+
|
|
190
|
+
return `${keyword}${spacing}${quote}${importPath}${isDir ? '/index' : ''}${quote}${closing}`
|
|
191
|
+
},
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
const result = content.replace(importRegex, () => replacements.shift()!)
|
|
195
|
+
return await PluginHooks.onCompile(result, path)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function compile(
|
|
199
|
+
path: fs.AbsolutePath | Bun.BunFile,
|
|
200
|
+
): Promise<string | null> {
|
|
201
|
+
if (!fs.exists(path))
|
|
202
|
+
throw new Error(`File not found: ${is.string(path) ? path : path.name}`)
|
|
203
|
+
|
|
204
|
+
path = typeof path === 'string' ? Bun.file(path) : path
|
|
205
|
+
return compileText(await path.text(), path.name! as fs.AbsolutePath)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
type CompileResult = {
|
|
209
|
+
success: boolean
|
|
210
|
+
content?: string
|
|
211
|
+
errors?: string[]
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function bundleModule(
|
|
215
|
+
path: fs.AbsolutePath,
|
|
216
|
+
): Promise<CompileResult> {
|
|
217
|
+
const build = await Bun.build({
|
|
218
|
+
entrypoints: [path],
|
|
219
|
+
target: 'browser',
|
|
220
|
+
format: 'esm',
|
|
221
|
+
// Coerced, and not defensively: `Bun.build` *rejects* a non-boolean
|
|
222
|
+
// `minify` with "Expected minify to be a boolean or an object" rather than
|
|
223
|
+
// treating it as truthy, so anything that reaches this line as a string
|
|
224
|
+
// takes the whole bundle down.
|
|
225
|
+
//
|
|
226
|
+
// That is reachable from outside this repo. `PROD` is an accessor on
|
|
227
|
+
// `process.env`, and Bun's `process.env` proxy stringifies on write, so an
|
|
228
|
+
// embedder that assigns the flag at all — even `process.env.PROD = true` —
|
|
229
|
+
// leaves a string behind. The suite's own instance of this is fixed at the
|
|
230
|
+
// source (see `setModeFlag` in `src/tests/fixtures.ts`); this guards the
|
|
231
|
+
// writes we do not own.
|
|
232
|
+
minify: Boolean(import.meta.env.PROD),
|
|
233
|
+
define: await getDefines(),
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
if (build.success && build.outputs.length > 0) {
|
|
237
|
+
return { success: true, content: await build.outputs[0].text() }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const errors = build.logs
|
|
241
|
+
.map(log => {
|
|
242
|
+
if (!log) return ''
|
|
243
|
+
if (is.string(log)) return log
|
|
244
|
+
const msg = log.message || ''
|
|
245
|
+
const pos = log.position
|
|
246
|
+
if (!pos) return msg
|
|
247
|
+
|
|
248
|
+
return `${pos.file || ''}:${pos.line || 0}:${pos.column || 0} - ${msg}`
|
|
249
|
+
})
|
|
250
|
+
.filter(Boolean)
|
|
251
|
+
|
|
252
|
+
// `handlerLog.BUNDLE_ERR` was declared and never called, so a bundle that
|
|
253
|
+
// failed was silent here: every caller either swallowed the result
|
|
254
|
+
// (`nm.ts`, `virtual-asset.ts` fall back to serving nothing) or logged its
|
|
255
|
+
// own line, and none of them printed the diagnostics this function had
|
|
256
|
+
// already collected. The 500 the caller returns is not a description.
|
|
257
|
+
handlerLog.BUNDLE_ERR({
|
|
258
|
+
file: path,
|
|
259
|
+
error: errors.join('\n ') || 'no diagnostics reported',
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
return { success: false, errors }
|
|
263
|
+
}
|