@bakery-framework/core 1.2.2 → 2.0.0-alpha.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/package.json +1 -1
- package/src/cache/shared-db.ts +7 -9
- package/src/client/globals.d.ts +1 -1
- package/src/client/utils.ts +41 -9
- package/src/compiler/compiler.ts +458 -39
- package/src/compiler/prompt-tracker.ts +19 -2
- package/src/compiler/tsconfig-sync.ts +99 -1
- package/src/core/bakery.ts +11 -12
- package/src/core/cache-version.ts +28 -1
- package/src/core/context.ts +42 -8
- package/src/core/index.ts +1 -0
- package/src/core/init.ts +6 -0
- package/src/handlers/assets/nm.ts +74 -15
- package/src/handlers/core/$base.ts +54 -8
- package/src/handlers/core/$dynamic.ts +21 -10
- package/src/handlers/core/$routing.ts +57 -8
- package/src/logger/serve-log.ts +15 -0
- package/src/plugins/types.ts +16 -0
- package/src/session.ts +0 -3
- package/src/shared.d.ts +7 -0
- package/src/types.d.ts +11 -0
- package/src/utils/http/credential.ts +70 -0
- package/src/utils/http/dom.ts +73 -64
- package/src/utils/http/html.ts +4 -4
- package/src/utils/http/index.ts +1 -0
- package/src/utils/isomorphic/misc.ts +16 -0
|
@@ -72,6 +72,43 @@ const routeGlobs = (
|
|
|
72
72
|
* is not a file and does not trigger the yield. `findDynamicRoute` applies
|
|
73
73
|
* the same rule on the cached path; the two must agree.
|
|
74
74
|
*/
|
|
75
|
+
/**
|
|
76
|
+
* Does the requested path name a file some handler serves at that URL?
|
|
77
|
+
*
|
|
78
|
+
* The "a real file always beats a catch-all" rule used to stat the literal
|
|
79
|
+
* path only, which misses every *compiled* URL: `TSHandler` serves
|
|
80
|
+
* `provides.ts` at `/teacher/provides` and `/teacher/provides.js`, so neither
|
|
81
|
+
* spelling named a file on disk and a Vue catch-all above it (priority 58 vs
|
|
82
|
+
* 50) served HTML to a browser that asked for a module. The probe now also
|
|
83
|
+
* tries the registered dynamic extensions against the extensionless base —
|
|
84
|
+
* the same mapping the serving handlers apply, read from the live registry so
|
|
85
|
+
* a plugin's extension (`.vue`) counts without core naming it.
|
|
86
|
+
*
|
|
87
|
+
* The caller clamps `target` inside the root before asking; appending an
|
|
88
|
+
* extension cannot escape it.
|
|
89
|
+
*/
|
|
90
|
+
export function servedSourceExists(target: string): boolean {
|
|
91
|
+
if (fs.isFileSync(target)) return true
|
|
92
|
+
|
|
93
|
+
const base = target.endsWith('.js') ? target.slice(0, -3) : target
|
|
94
|
+
for (const handler of Bakery.handlers.fetch.keys()) {
|
|
95
|
+
let exts: unknown
|
|
96
|
+
try {
|
|
97
|
+
exts = (handler as { config?: { ext?: unknown } }).config?.ext
|
|
98
|
+
} catch {
|
|
99
|
+
// A config getter that needs state this process lacks — a handler with
|
|
100
|
+
// no ext table cannot claim a source file either way.
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
if (!Array.isArray(exts)) continue
|
|
104
|
+
for (const ext of exts) {
|
|
105
|
+
if (fs.isFileSync(`${base}.${ext}`)) return true
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
|
|
75
112
|
async function getCatchAllRoute(
|
|
76
113
|
ext: string,
|
|
77
114
|
dir: fs.AbsolutePath,
|
|
@@ -98,7 +135,7 @@ async function getCatchAllRoute(
|
|
|
98
135
|
// whose name merely begins with it.
|
|
99
136
|
if (
|
|
100
137
|
(target === dir || target.startsWith(`${dir}/`)) &&
|
|
101
|
-
|
|
138
|
+
servedSourceExists(target)
|
|
102
139
|
) {
|
|
103
140
|
return null
|
|
104
141
|
}
|
|
@@ -106,7 +143,14 @@ async function getCatchAllRoute(
|
|
|
106
143
|
|
|
107
144
|
const file = fs.resolve(found.value)
|
|
108
145
|
if (fs.isForbidden(file, root)) return null
|
|
109
|
-
|
|
146
|
+
const info = new RouteData.Info(file, fs.relative(root, file))
|
|
147
|
+
|
|
148
|
+
// A bare-directory request (`/docs` with no index) reaches here with no
|
|
149
|
+
// rest segments, and only the `[...name!]` spelling opted into claiming
|
|
150
|
+
// it — the plain form keeps requiring at least one segment.
|
|
151
|
+
if (!restSegments.length && !info.optionalCatchAll) return null
|
|
152
|
+
|
|
153
|
+
return info
|
|
110
154
|
}
|
|
111
155
|
|
|
112
156
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: request-to-route dispatcher
|
|
@@ -177,12 +221,17 @@ export async function getRoute(
|
|
|
177
221
|
if (route) return route
|
|
178
222
|
}
|
|
179
223
|
|
|
180
|
-
// `first === 'index'`
|
|
181
|
-
//
|
|
182
|
-
// least one rest segment
|
|
183
|
-
//
|
|
184
|
-
if (!options.staticOnly
|
|
185
|
-
return await getCatchAllRoute(
|
|
224
|
+
// `first === 'index'` is the bare-directory request (`/docs` arrives here
|
|
225
|
+
// as an injected 'index' segment, after no index file matched). The plain
|
|
226
|
+
// `[...name]` pattern requires at least one rest segment and cannot claim
|
|
227
|
+
// it; `[...name!]` exists to — `getCatchAllRoute` tells them apart.
|
|
228
|
+
if (!options.staticOnly) {
|
|
229
|
+
return await getCatchAllRoute(
|
|
230
|
+
ext,
|
|
231
|
+
dir,
|
|
232
|
+
root,
|
|
233
|
+
first === 'index' ? [] : [first],
|
|
234
|
+
)
|
|
186
235
|
}
|
|
187
236
|
}
|
|
188
237
|
|
package/src/logger/serve-log.ts
CHANGED
|
@@ -98,6 +98,21 @@ const handlerMsgs = {
|
|
|
98
98
|
PROXY_REQ: 'I Proxying %y{path}%* -> %b{target}%*',
|
|
99
99
|
MIDDLEWARE_ERR: 'E Middleware error: %r{error}%*',
|
|
100
100
|
BUNDLE_ERR: 'E Failed to bundle module (%y{file}%*): %r{error}%*',
|
|
101
|
+
// **Keep each message one unbroken string literal.** Splitting a long one into
|
|
102
|
+
// `'…' + '…'` types as `string`, so `as const` preserves nothing,
|
|
103
|
+
// `messageLogger` extracts no `{placeholder}`, and every call site fails with
|
|
104
|
+
// "Expected 0 arguments, but got 1". Wrapping the value onto its own line, as
|
|
105
|
+
// below, is fine; joining with `+` is not.
|
|
106
|
+
BUNDLE_SIDE_EFFECTS_REPAIRED:
|
|
107
|
+
'I %y{file}%* tree-shook to an empty export list because its package declares %ysideEffects: false%*; re-bundled through a re-export shim, which keeps the code.',
|
|
108
|
+
BUNDLE_EMPTY_EXPORTS:
|
|
109
|
+
'E %y{file}%* bundled to an export list with no code behind it — every name it exports is undefined, so it is rejected rather than served. The bundler reported success, and re-bundling through a re-export shim did not recover it. Import the specific module you need instead of the package root.',
|
|
110
|
+
BUNDLE_CJS_INTEROP:
|
|
111
|
+
'I Generated named exports for the CommonJS package %y{file}%*, so a named import of it works in the browser.',
|
|
112
|
+
BUNDLE_CJS_PROBED:
|
|
113
|
+
'I Could not read %y{file}%* export names statically, so they were probed by importing it in a short-lived child process.',
|
|
114
|
+
BUNDLE_CJS_DEFAULT_ONLY:
|
|
115
|
+
'W %y{file}%* assigns %ymodule.exports%* wholesale and its members could not be read, so its browser bundle exports only %ydefault%* — a named import from it fails in the browser with "does not provide an export named …", and nothing fails here. Import the default and read the property off it, or use an ESM build.',
|
|
101
116
|
} as const
|
|
102
117
|
|
|
103
118
|
export const handlerLog = messageLogger(new Logger('handlers'), handlerMsgs)
|
package/src/plugins/types.ts
CHANGED
|
@@ -43,6 +43,22 @@ export interface PluginTsProject {
|
|
|
43
43
|
* A plugin whose project compiles browser code should set it.
|
|
44
44
|
*/
|
|
45
45
|
importMapPaths?: boolean
|
|
46
|
+
/**
|
|
47
|
+
* Does code in this project run on the server?
|
|
48
|
+
*
|
|
49
|
+
* Declared, not inferred. The first version read `bun-types` out of the base
|
|
50
|
+
* config and treated that as the marker — true today, and an inference about
|
|
51
|
+
* intent drawn from a detail that exists for another reason. A plugin saying
|
|
52
|
+
* what its project *is* cannot drift out of step with itself.
|
|
53
|
+
*
|
|
54
|
+
* What it controls: the app's schema registration, so the ORM's tables are the
|
|
55
|
+
* app's own rather than the permissive `any` fallback. Off for browser code on
|
|
56
|
+
* purpose — `@bakery-framework/orm` is server-only, so a `.ts` bound for the
|
|
57
|
+
* browser importing `DB` should fail to typecheck rather than be helpfully
|
|
58
|
+
* typed, and the package ships TypeScript source calling `Bun.*`, which a
|
|
59
|
+
* client config cannot compile anyway.
|
|
60
|
+
*/
|
|
61
|
+
server?: boolean
|
|
46
62
|
}
|
|
47
63
|
|
|
48
64
|
/** What a plugin contributes to the generated tsconfig projects. */
|
package/src/session.ts
CHANGED
|
@@ -12,9 +12,6 @@ import { DEFAULT_SESSION_PERSIST, DEFAULT_SESSION_TTL } from './utils/constants'
|
|
|
12
12
|
*/
|
|
13
13
|
export const RESERVED_SESSION_PREFIX = '__bakery.'
|
|
14
14
|
|
|
15
|
-
/** Marks a session as having passed the DASHPASS check. */
|
|
16
|
-
export const DASHPASS_SESSION_KEY = `${RESERVED_SESSION_PREFIX}dashpass`
|
|
17
|
-
|
|
18
15
|
export function isReservedSessionKey(key: string): boolean {
|
|
19
16
|
return key.startsWith(RESERVED_SESSION_PREFIX)
|
|
20
17
|
}
|
package/src/shared.d.ts
CHANGED
|
@@ -25,6 +25,13 @@ import type { MapOf } from './types'
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
declare global {
|
|
28
|
+
/**
|
|
29
|
+
* Bound by `client/utils.ts` in the browser and `core/init.ts` on the
|
|
30
|
+
* server — the same isomorphic implementation either side, so code moving
|
|
31
|
+
* between an SFC browser script and a server block keeps the name.
|
|
32
|
+
*/
|
|
33
|
+
var randomId: typeof import('./utils/isomorphic/misc').randomId
|
|
34
|
+
|
|
28
35
|
/** The one JSON envelope — see convention 7. */
|
|
29
36
|
type JsonResponse<T = any> = {
|
|
30
37
|
time: number
|
package/src/types.d.ts
CHANGED
|
@@ -65,6 +65,17 @@ export type Match<D extends symbol> = {
|
|
|
65
65
|
*/
|
|
66
66
|
export type RouteBody<P = {}> = P & MapOf<any>
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* What one dynamic route segment binds: `[id]` a string, `[...rest]` and
|
|
70
|
+
* `[...rest!]` an array of the remaining segments (`[]` for the bare
|
|
71
|
+
* directory under the `!` form).
|
|
72
|
+
*
|
|
73
|
+
* Exists so a route over mixed or unknown segments can say
|
|
74
|
+
* `defineRoute<MapOf<RouteParam>>` instead of hand-writing the union — and so
|
|
75
|
+
* the union has one definition to change if it ever grows.
|
|
76
|
+
*/
|
|
77
|
+
export type RouteParam = string | string[]
|
|
78
|
+
|
|
68
79
|
/**
|
|
69
80
|
* What a route module may actually return — read off `processResponse`
|
|
70
81
|
* (`router.ts`) and `ApiHandler.handle`: a `Response`, a `BunFile` (streamed
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared-credential check for plugin surfaces (`plugin({ credential:
|
|
3
|
+
* import.meta.env.SOME_KEY })`).
|
|
4
|
+
*
|
|
5
|
+
* One implementation, deliberately in core: the db-explorer and analytics
|
|
6
|
+
* plugins each need it and per-plugin copies of security code drift. It owns
|
|
7
|
+
* *only* the comparison — no logins, no sessions, no backoff.
|
|
8
|
+
*
|
|
9
|
+
* A credential is presented three ways, in this order of preference:
|
|
10
|
+
*
|
|
11
|
+
* - `x-<name>` header (scriptable, never logged by default)
|
|
12
|
+
* - `Authorization: Bearer <credential>`
|
|
13
|
+
* - `?<name>=<credential>` query, for a human opening a URL in a browser
|
|
14
|
+
* who cannot set a header — the caller is expected to strip it from the
|
|
15
|
+
* URL client-side; it *does* reach server logs, which is documented, and
|
|
16
|
+
* the header forms exist for anything automated.
|
|
17
|
+
*
|
|
18
|
+
* An empty or missing configured credential **disables** this path rather
|
|
19
|
+
* than matching everything: `credential: import.meta.env.KEY` with the
|
|
20
|
+
* variable unset must mean "off", not "open".
|
|
21
|
+
*
|
|
22
|
+
* The compare is constant-time. A plain `===` on a secret is a timing oracle,
|
|
23
|
+
* and `timingSafeEqual` costs one line — but it throws on length mismatch, so
|
|
24
|
+
* the length is checked first (which leaks only the length, never the bytes).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { timingSafeEqual } from 'node:crypto'
|
|
28
|
+
|
|
29
|
+
export function credentialMatches(
|
|
30
|
+
configured: string | undefined,
|
|
31
|
+
presented: string | null | undefined,
|
|
32
|
+
): boolean {
|
|
33
|
+
if (!configured || !presented) return false
|
|
34
|
+
|
|
35
|
+
const a = Buffer.from(configured)
|
|
36
|
+
const b = Buffer.from(presented)
|
|
37
|
+
if (a.length !== b.length) return false
|
|
38
|
+
return timingSafeEqual(a, b)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Pull a presented credential off a request: the `x-<name>` header, a Bearer
|
|
43
|
+
* token, or the `<name>` query parameter, in that order. Returns `null` when
|
|
44
|
+
* none is present. `name` is the header/query key — `db-key`, `analytics-key`.
|
|
45
|
+
*/
|
|
46
|
+
export function readCredential(req: Request, name: string): string | null {
|
|
47
|
+
const header = req.headers.get(`x-${name}`)
|
|
48
|
+
if (header) return header
|
|
49
|
+
|
|
50
|
+
const bearer = req.headers
|
|
51
|
+
.get('authorization')
|
|
52
|
+
?.replace(/^Bearer\s+/i, '')
|
|
53
|
+
.trim()
|
|
54
|
+
if (bearer) return bearer
|
|
55
|
+
|
|
56
|
+
return new URL(req.url).searchParams.get(name)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The whole check in one call: does the request present `configured` under
|
|
61
|
+
* `name`? `false` for an unset credential or an absent presentation.
|
|
62
|
+
*/
|
|
63
|
+
export function requestHasCredential(
|
|
64
|
+
req: Request,
|
|
65
|
+
configured: string | undefined,
|
|
66
|
+
name: string,
|
|
67
|
+
): boolean {
|
|
68
|
+
if (!configured) return false
|
|
69
|
+
return credentialMatches(configured, readCredential(req, name))
|
|
70
|
+
}
|
package/src/utils/http/dom.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises'
|
|
1
2
|
import { LRUCache } from '../../cache/lru'
|
|
2
3
|
import { Bakery, hostStore } from '../../core/bakery'
|
|
3
4
|
import type { MapOf } from '../../types'
|
|
@@ -16,16 +17,6 @@ export function clearHeadBodyCache() {
|
|
|
16
17
|
|
|
17
18
|
export { headBodyCache }
|
|
18
19
|
|
|
19
|
-
type PackageJson = {
|
|
20
|
-
name: string
|
|
21
|
-
version: string
|
|
22
|
-
main?: string
|
|
23
|
-
module?: string
|
|
24
|
-
browser?: string | MapOf<string>
|
|
25
|
-
dependencies?: MapOf<string>
|
|
26
|
-
devDependencies?: MapOf<string>
|
|
27
|
-
}
|
|
28
|
-
|
|
29
20
|
let depMap = ''
|
|
30
21
|
|
|
31
22
|
const hostDepMaps = new Map<string, string>()
|
|
@@ -34,22 +25,14 @@ const hostDepMaps = new Map<string, string>()
|
|
|
34
25
|
* Normalise one import-map entry, for both the process-level map and the
|
|
35
26
|
* per-host maps.
|
|
36
27
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* two callers, so they cannot disagree again.
|
|
28
|
+
* One helper, two callers, deliberately: they used to be separate copies that
|
|
29
|
+
* had drifted into testing different things — one switched on the entry key,
|
|
30
|
+
* the other on the entry value — so the same input normalised two ways
|
|
31
|
+
* depending on which path saw it.
|
|
42
32
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* `docs` or `tests`, and the directory they name no longer exists, so a value
|
|
47
|
-
* pointing at it is a broken path either way. It normalises as an ordinary
|
|
48
|
-
* relative specifier now.
|
|
49
|
-
*
|
|
50
|
-
* `@client/utils` stays, and stays keyed on the *key*: it is a live alias — it
|
|
51
|
-
* is the default `importMap` entry in `core/config.ts` — and the browser
|
|
52
|
-
* runtime is served from a fixed URL, so the target is not the app's to choose.
|
|
33
|
+
* `@client/utils` is keyed on the *key*: it is a live alias, the default
|
|
34
|
+
* `importMap` entry in `core/config.ts`, and the browser runtime is served from
|
|
35
|
+
* a fixed URL, so the target is not the app's to choose.
|
|
53
36
|
*/
|
|
54
37
|
function normalizeImportEntry(key: string, value: unknown): [string, string] {
|
|
55
38
|
const cleanKey = key.replace(/\*$/, '')
|
|
@@ -85,53 +68,79 @@ export function initHostImportMaps() {
|
|
|
85
68
|
}
|
|
86
69
|
}
|
|
87
70
|
|
|
88
|
-
|
|
89
|
-
if (is.string(pkgData.browser)) return pkgData.browser as string
|
|
71
|
+
let installedCache: Promise<string[]> | null = null
|
|
90
72
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Every installed package, top level and scoped, as `name` and `name/`.
|
|
75
|
+
*
|
|
76
|
+
* Read from `node_modules` rather than from `dependencies`, and the difference
|
|
77
|
+
* is the whole point: a package can be installed and imported without being
|
|
78
|
+
* declared — a transitive one, or a dependency someone forgot to add — and the
|
|
79
|
+
* browser's failure for a specifier the map misses names its own rule rather
|
|
80
|
+
* than the missing entry: *"Failed to resolve module specifier 'pkg'. Relative
|
|
81
|
+
* references must start with either "/", "./", or "../"."*
|
|
82
|
+
*
|
|
83
|
+
* Cheap — one `readdir` per scope, no `package.json` reads — and memoised per
|
|
84
|
+
* process besides: the import map reads it at boot, `bundleModule` on every
|
|
85
|
+
* bundle (as its `external` list), and a `readdir` sweep per bundle is pure
|
|
86
|
+
* waste. A dev restart is a new process, so an install still shows up.
|
|
87
|
+
*/
|
|
88
|
+
export function installedPackages(): Promise<string[]> {
|
|
89
|
+
installedCache ??= readInstalledPackages()
|
|
90
|
+
return installedCache
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function readInstalledPackages(): Promise<string[]> {
|
|
94
|
+
const root = fs.resolve(fs.cwd, 'node_modules')
|
|
95
|
+
const [err, entries] = await Try.catch(() => readdir(root))
|
|
96
|
+
if (err || !entries) return []
|
|
97
|
+
|
|
98
|
+
const names: string[] = []
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
// `.bin`, `.cache` and friends are not packages.
|
|
101
|
+
if (entry.startsWith('.')) continue
|
|
96
102
|
|
|
97
|
-
|
|
103
|
+
if (!entry.startsWith('@')) {
|
|
104
|
+
names.push(entry)
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const [scopeErr, scoped] = await Try.catch(() =>
|
|
109
|
+
readdir(`${root}/${entry}`),
|
|
110
|
+
)
|
|
111
|
+
if (scopeErr || !scoped) continue
|
|
112
|
+
for (const name of scoped) {
|
|
113
|
+
if (!name.startsWith('.')) names.push(`${entry}/${name}`)
|
|
114
|
+
}
|
|
98
115
|
}
|
|
99
116
|
|
|
100
|
-
return
|
|
117
|
+
return names
|
|
101
118
|
}
|
|
102
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Build the browser import map.
|
|
122
|
+
*
|
|
123
|
+
* **Resolution happens at the other end of the URL, and rewriting happens
|
|
124
|
+
* nowhere.** An entry maps a package to `/_nm/<name>`; `NMHandler` hands that to
|
|
125
|
+
* `Bun.build`, which applies real browser resolution — `exports` maps,
|
|
126
|
+
* conditions, the `browser` field. Naming an entry file here instead would mean
|
|
127
|
+
* reimplementing all of that, badly.
|
|
128
|
+
*
|
|
129
|
+
* Covering every installed package is what removes the need for a compile-time
|
|
130
|
+
* rewrite of bare specifiers, and the map reaches code the compiler never sees:
|
|
131
|
+
* an inline `<script type="module">` in an `.html` page arrives at the browser
|
|
132
|
+
* with its imports intact.
|
|
133
|
+
*
|
|
134
|
+
* App-declared `importMap` entries are applied last and win. One of them,
|
|
135
|
+
* `@client/utils`, does not point into `node_modules` at all.
|
|
136
|
+
*/
|
|
103
137
|
export async function initImportMap() {
|
|
104
|
-
const pkgContent = await Try(() =>
|
|
105
|
-
Bun.file(fs.resolve(fs.cwd, 'package.json')).json(),
|
|
106
|
-
)
|
|
107
|
-
const pkg: any = pkgContent || {}
|
|
108
138
|
const map = Bakery.config.importMap || {}
|
|
109
|
-
const deps = pkg.dependencies || {}
|
|
110
|
-
|
|
111
139
|
const resolvedMap: MapOf<string> = {}
|
|
112
140
|
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
Bun.file(`./node_modules/${dep}/package.json`).json(),
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
return { dep, pkgData: pkgData as PackageJson | null }
|
|
120
|
-
}),
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
for (const { dep, pkgData } of imports) {
|
|
124
|
-
if (!pkgData) continue
|
|
125
|
-
|
|
126
|
-
const actualName = pkgData.name || dep
|
|
127
|
-
resolvedMap[`${actualName}/`] = `/_nm/${actualName}/`
|
|
128
|
-
|
|
129
|
-
const baseMod = pkgData.module || pkgData.main || 'index.js'
|
|
130
|
-
const mod = resolveDepModule(pkgData, baseMod)
|
|
131
|
-
|
|
132
|
-
const finalMod = typeof mod === 'string' ? mod : 'index.js'
|
|
133
|
-
resolvedMap[actualName] =
|
|
134
|
-
`/_nm/${actualName}/${finalMod.replace(/^\.\//, '')}`
|
|
141
|
+
for (const name of await installedPackages()) {
|
|
142
|
+
resolvedMap[`${name}/`] = `/_nm/${name}/`
|
|
143
|
+
resolvedMap[name] = `/_nm/${name}`
|
|
135
144
|
}
|
|
136
145
|
|
|
137
146
|
for (const [k, v] of Object.entries(map)) {
|
|
@@ -149,8 +158,8 @@ export namespace DOMTools {
|
|
|
149
158
|
return `<script type="importmap">${map}</script>`
|
|
150
159
|
}
|
|
151
160
|
|
|
152
|
-
export function params(params: MapOf<
|
|
153
|
-
const newParams: MapOf<
|
|
161
|
+
export function params(params: MapOf<unknown>) {
|
|
162
|
+
const newParams: MapOf<unknown> = {}
|
|
154
163
|
|
|
155
164
|
for (const [k, v] of Object.entries(params)) {
|
|
156
165
|
if (k.startsWith('$$')) continue
|
package/src/utils/http/html.ts
CHANGED
|
@@ -80,7 +80,7 @@ export const STREAM_THRESHOLD_BYTES = 64 * 1024
|
|
|
80
80
|
|
|
81
81
|
export async function injectIfHtml(
|
|
82
82
|
data: string | Response | Blob,
|
|
83
|
-
params?: MapOf<
|
|
83
|
+
params?: MapOf<unknown>,
|
|
84
84
|
injects?: HtmlInjects,
|
|
85
85
|
): Promise<Response | null> {
|
|
86
86
|
if (data instanceof Response && isInjected(data)) return data
|
|
@@ -127,7 +127,7 @@ export async function injectIfHtml(
|
|
|
127
127
|
* streamed path's fallback both come through here. */
|
|
128
128
|
function bufferedResponse(
|
|
129
129
|
content: string,
|
|
130
|
-
params: MapOf<
|
|
130
|
+
params: MapOf<unknown> | undefined,
|
|
131
131
|
injects: HtmlInjects | undefined,
|
|
132
132
|
responseInit: ResponseInit & { headers?: any },
|
|
133
133
|
): Response {
|
|
@@ -182,7 +182,7 @@ function getConfigInjects() {
|
|
|
182
182
|
* into a pull callback, where the AsyncLocalStorage host context that
|
|
183
183
|
* `getConfigInjects` and `DOMTools.importMap` read may no longer be live.
|
|
184
184
|
*/
|
|
185
|
-
function computeInjects(params: MapOf<
|
|
185
|
+
function computeInjects(params: MapOf<unknown>, injects: HtmlInjects) {
|
|
186
186
|
const configInjects = getConfigInjects()
|
|
187
187
|
const paramsStr = DOMTools.params(params)
|
|
188
188
|
|
|
@@ -235,7 +235,7 @@ function rewriteFontsUrls(html: string): string {
|
|
|
235
235
|
|
|
236
236
|
export function assembleHtml(
|
|
237
237
|
content: string,
|
|
238
|
-
params: MapOf<
|
|
238
|
+
params: MapOf<unknown> = {},
|
|
239
239
|
injects: HtmlInjects = {},
|
|
240
240
|
) {
|
|
241
241
|
const frags = computeInjects(params, injects)
|
package/src/utils/http/index.ts
CHANGED
|
@@ -15,6 +15,22 @@ export function any<T = any>(value: any): T {
|
|
|
15
15
|
return value
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* A hex id of `length` characters, from `crypto.getRandomValues`.
|
|
20
|
+
*
|
|
21
|
+
* Isomorphic, and moving it here is what made it so: it lived in
|
|
22
|
+
* `client/utils.ts` as a browser global only, so the same call in a server
|
|
23
|
+
* block was a ReferenceError — reported from an app. `crypto` is a global in
|
|
24
|
+
* both runtimes; nothing here is browser-specific.
|
|
25
|
+
*/
|
|
26
|
+
export function randomId(length = 8) {
|
|
27
|
+
const arr = new Uint8Array(Math.ceil(length / 2))
|
|
28
|
+
crypto.getRandomValues(arr)
|
|
29
|
+
return Array.from(arr, dec => dec.toString(16).padStart(2, '0'))
|
|
30
|
+
.join('')
|
|
31
|
+
.slice(0, length)
|
|
32
|
+
}
|
|
33
|
+
|
|
18
34
|
export function repeat(n: number): number[]
|
|
19
35
|
export function repeat<T>(n: number, fn: (i: number) => T): T[]
|
|
20
36
|
export function repeat<T>(n: number, fn?: (i: number) => T): unknown[] {
|