@weft-js/start 0.1.1 → 0.1.3
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 +1278 -12
- package/crate/Cargo.lock +1347 -0
- package/crate/Cargo.toml +72 -0
- package/crate/README.md +322 -0
- package/crate/rust-toolchain.toml +3 -0
- package/crate/src/build/emit.rs +103 -0
- package/crate/src/build.rs +367 -0
- package/crate/src/ctx.rs +186 -0
- package/crate/src/envelope.rs +163 -0
- package/crate/src/functions.rs +173 -0
- package/crate/src/lib.rs +118 -0
- package/crate/src/native.rs +212 -0
- package/crate/src/otel.rs +62 -0
- package/crate/src/server.rs +203 -0
- package/package.json +46 -37
- package/src/app.mjs +38 -8
- package/src/asset-receipt.d.mts +13 -0
- package/src/asset-receipt.mjs +61 -0
- package/src/auth/better-auth.ts +215 -17
- package/src/auth/drizzle.ts +1 -1
- package/src/auth.ts +21 -6
- package/src/boot.ts +19 -8
- package/src/bus/postgres.ts +32 -19
- package/src/bus.ts +7 -4
- package/src/{capture.mjs → capture.ts} +34 -1
- package/src/changes-sql.d.mts +8 -2
- package/src/changes-sql.mjs +26 -10
- package/src/changes.ts +165 -66
- package/src/channel/connections.ts +216 -0
- package/src/channel/socket.ts +396 -0
- package/src/channel-fake.ts +19 -4
- package/src/channel.ts +120 -492
- package/src/client.tsx +9 -2
- package/src/contract/describe.mjs +231 -0
- package/src/contract.mjs +144 -157
- package/src/data/cache.ts +354 -0
- package/src/data/definitions.ts +94 -0
- package/src/data/draft.ts +104 -0
- package/src/data/foundation.ts +95 -0
- package/src/data/live-api.ts +22 -0
- package/src/data/mutation.ts +138 -0
- package/src/data/primitives.ts +135 -0
- package/src/data/query.ts +106 -0
- package/src/data/transport.ts +121 -0
- package/src/data/types.ts +303 -0
- package/src/data.ts +69 -894
- package/src/db/bun-sqlite.d.ts +4 -1
- package/src/db/changes-mysql.ts +61 -21
- package/src/db/changes-postgres.ts +197 -118
- package/src/db/changes-sqlite.ts +131 -64
- package/src/db/core.ts +38 -13
- package/src/db/index.ts +20 -4
- package/src/db/mysql.ts +22 -6
- package/src/db/postgres.ts +49 -13
- package/src/db/sqlite.ts +53 -27
- package/src/derive.d.mts +4 -1
- package/src/derive.mjs +136 -39
- package/src/dev-runtime.mjs +8 -4
- package/src/exec/errors.ts +57 -0
- package/src/exec/landing.ts +66 -0
- package/src/exec/scope.ts +174 -0
- package/src/exec/touch.ts +80 -0
- package/src/exec/validation.ts +176 -0
- package/src/exec.ts +191 -341
- package/src/extension.ts +8 -2
- package/src/first-paint.ts +26 -9
- package/src/form/value.ts +15 -0
- package/src/form.tsx +132 -43
- package/src/hub/channels.ts +102 -0
- package/src/hub/connections.ts +147 -0
- package/src/hub/delivery.ts +62 -0
- package/src/hub/feed.ts +168 -0
- package/src/hub/presence.ts +64 -0
- package/src/hub/route.ts +158 -0
- package/src/hub/subscriptions.ts +212 -0
- package/src/hub/topic.ts +98 -0
- package/src/hub/tracked.ts +12 -0
- package/src/hub/types.ts +190 -0
- package/src/hub/view.ts +147 -0
- package/src/hub.ts +132 -758
- package/src/index.ts +84 -10
- package/src/inline.mjs +356 -0
- package/src/inspect/compare.mjs +162 -0
- package/src/inspect/routes.mjs +154 -0
- package/src/inspect/samples.mjs +59 -0
- package/src/inspect/url.mjs +2 -0
- package/src/inspect.mjs +146 -187
- package/src/link/election.ts +178 -0
- package/src/link/lines.ts +27 -0
- package/src/link.ts +90 -166
- package/src/list-drizzle.ts +104 -18
- package/src/list.ts +127 -36
- package/src/listen.ts +32 -6
- package/src/live/api.ts +18 -0
- package/src/live/backoff.ts +6 -0
- package/src/live/frames.ts +42 -0
- package/src/live/read.ts +77 -0
- package/src/live/sub-types.ts +24 -0
- package/src/live/transport.ts +86 -0
- package/src/live/types.ts +52 -0
- package/src/live-fake.ts +20 -5
- package/src/live-target.mjs +112 -37
- package/src/live.ts +116 -225
- package/src/media.ts +8 -1
- package/src/migrations.mjs +14 -6
- package/src/mux.ts +161 -64
- package/src/og/DefaultCard.tsx +40 -9
- package/src/og/fonts.mjs +16 -8
- package/src/og/images.mjs +43 -22
- package/src/og/index.mjs +119 -43
- package/src/og/native.mjs +11 -4
- package/src/range.mjs +15 -3
- package/src/router.ts +56 -6
- package/src/routes.mjs +73 -22
- package/src/run/admission.ts +80 -0
- package/src/run/boot.tsx +115 -0
- package/src/run/cache-presentation.ts +67 -0
- package/src/run/cache.ts +254 -0
- package/src/run/helpers.ts +197 -0
- package/src/run/listeners.ts +155 -0
- package/src/run/navigation.ts +382 -0
- package/src/run/page-skeleton.tsx +35 -0
- package/src/run/prefetch.ts +128 -0
- package/src/run/retain-view.ts +18 -0
- package/src/run/shape-preparation.ts +97 -0
- package/src/run/shape-styles.ts +72 -0
- package/src/run/source-context/pin.ts +124 -0
- package/src/run/source-context.ts +311 -0
- package/src/run.tsx +50 -178
- package/src/rust/common.ts +163 -0
- package/src/rust/native.ts +282 -0
- package/src/rust.ts +103 -229
- package/src/server/card.tsx +65 -0
- package/src/server/compression.ts +115 -0
- package/src/server/core.ts +70 -0
- package/src/server/dispatch.ts +70 -0
- package/src/server/form.ts +85 -0
- package/src/server/functions.ts +129 -0
- package/src/server/handler.ts +39 -0
- package/src/server/head.ts +96 -0
- package/src/server/media.ts +60 -0
- package/src/server/navigation-cache.ts +80 -0
- package/src/server/navigation.tsx +85 -0
- package/src/server/page-cold.ts +95 -0
- package/src/server/page-stream.tsx +181 -0
- package/src/server/page-types.ts +51 -0
- package/src/server/page-warm.ts +130 -0
- package/src/server/page.ts +69 -0
- package/src/server/payload.ts +89 -0
- package/src/server/prerender.ts +60 -0
- package/src/server/preview.tsx +67 -0
- package/src/server/render.ts +167 -0
- package/src/server/request.ts +98 -0
- package/src/server/resolver.ts +228 -0
- package/src/server/routes.ts +150 -0
- package/src/server/services.ts +69 -0
- package/src/server/text.ts +29 -0
- package/src/server/types.ts +160 -0
- package/src/server.tsx +82 -971
- package/src/shape.mjs +30 -6
- package/src/share/display.ts +50 -0
- package/src/share/network.ts +119 -0
- package/src/share/receive.ts +70 -0
- package/src/share/state.ts +30 -0
- package/src/share/tab-queue.ts +28 -0
- package/src/share-server.ts +128 -43
- package/src/share.ts +103 -209
- package/src/sheets.mjs +30 -18
- package/src/sidecar/build.mjs +170 -0
- package/src/sidecar/cargo.mjs +164 -0
- package/src/sidecar/common.mjs +17 -0
- package/src/sidecar/dev-build.mjs +91 -0
- package/src/sidecar/dev-watch.mjs +88 -0
- package/src/sidecar/manifest.mjs +19 -0
- package/src/sidecar/process.mjs +58 -0
- package/src/sidecar/refresh.mjs +19 -0
- package/src/sidecar.mjs +75 -416
- package/src/socket/check.ts +50 -0
- package/src/socket/deliver.ts +16 -0
- package/src/socket/publish.ts +56 -0
- package/src/socket/rooms.ts +41 -0
- package/src/socket/route.ts +79 -0
- package/src/socket/shared.ts +26 -0
- package/src/socket/types.ts +48 -0
- package/src/socket.ts +205 -194
- package/src/stream.ts +10 -1
- package/src/verify/app.ts +39 -0
- package/src/verify/cleanup.ts +27 -0
- package/src/verify/database.ts +11 -0
- package/src/verify/live.ts +63 -0
- package/src/verify/notify.ts +35 -0
- package/src/verify/skip.ts +19 -0
- package/src/verify/support.ts +149 -0
- package/src/verify/timing.ts +15 -0
- package/src/verify/trigger.ts +14 -0
- package/src/verify.ts +77 -176
- package/src/view.tsx +96 -11
- package/src/vite/asset-bytes.mjs +95 -0
- package/src/vite/assets.mjs +117 -0
- package/src/vite/config-hooks.mjs +166 -0
- package/src/vite/dev-hooks.mjs +103 -0
- package/src/vite/dev.mjs +102 -0
- package/src/vite/entries.mjs +145 -0
- package/src/vite/module-hooks.mjs +288 -0
- package/src/vite/route-modules.mjs +289 -0
- package/src/vite/server-functions.mjs +222 -0
- package/src/vite/shape-entry-css.mjs +187 -0
- package/src/vite/shape-override.mjs +226 -0
- package/src/vite/shared.mjs +51 -0
- package/src/vite/sheets.mjs +92 -0
- package/src/vite/state.mjs +89 -0
- package/src/vite/syntax.mjs +139 -0
- package/src/vite.mjs +152 -853
- package/src/capture.d.mts +0 -1
package/src/view.tsx
CHANGED
|
@@ -5,6 +5,20 @@
|
|
|
5
5
|
|
|
6
6
|
import { untrack } from '@weft-js/core'
|
|
7
7
|
import { active, level, type RouteState } from './router'
|
|
8
|
+
import type { PreparedRouteShape } from './run/shape-preparation'
|
|
9
|
+
import { retainRouteView } from './run/retain-view'
|
|
10
|
+
|
|
11
|
+
function ShapeTree(props: { shapes: PreparedRouteShape[]; depth: number }): unknown {
|
|
12
|
+
// A skeleton is a snapshot: the outlet swaps the whole tree when its shapes change.
|
|
13
|
+
const item = untrack(() => props.shapes[props.depth])
|
|
14
|
+
if (!item) return null
|
|
15
|
+
const Shape = item.component as (props: { children: unknown }) => unknown
|
|
16
|
+
return (
|
|
17
|
+
<Shape>
|
|
18
|
+
<ShapeTree shapes={props.shapes} depth={props.depth + 1} />
|
|
19
|
+
</Shape>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
8
22
|
|
|
9
23
|
// The route state travels as a prop, not a context: a level's view runs later, in the hole that shows it, under that
|
|
10
24
|
// hole's owner. `active.route` is the state of the render in progress (a server pass is synchronous; the client has one).
|
|
@@ -15,33 +29,104 @@ export function App(props: { route: RouteState }) {
|
|
|
15
29
|
|
|
16
30
|
export function Level(props: { route: RouteState; depth: number }) {
|
|
17
31
|
const route = untrack(() => props.route)
|
|
18
|
-
const lv = level(
|
|
32
|
+
const lv = level(
|
|
33
|
+
route,
|
|
34
|
+
untrack(() => props.depth),
|
|
35
|
+
)
|
|
19
36
|
const next = untrack(() => props.depth) + 1
|
|
20
37
|
const view = () => {
|
|
21
38
|
const id = lv.id.value
|
|
22
39
|
if (id === undefined) return null
|
|
23
40
|
const html = route.mount ? lv.html.value : undefined
|
|
24
|
-
if (html !== undefined)
|
|
25
|
-
|
|
41
|
+
if (html !== undefined)
|
|
42
|
+
return route.mount!(
|
|
43
|
+
html,
|
|
44
|
+
untrack(() => props.depth),
|
|
45
|
+
() => <Level route={route} depth={next} />,
|
|
46
|
+
)
|
|
47
|
+
const Route = route.comps[id] as
|
|
48
|
+
| ((props: { params: Record<string, string>; data: unknown; children: unknown }) => unknown)
|
|
49
|
+
| undefined
|
|
26
50
|
if (!Route) return null
|
|
27
|
-
const body = () =>
|
|
28
|
-
|
|
51
|
+
const body = () => (
|
|
52
|
+
<Route params={lv.params.value} data={lv.data.value}>
|
|
53
|
+
<Level route={route} depth={next} />
|
|
54
|
+
</Route>
|
|
55
|
+
)
|
|
56
|
+
return route.around
|
|
57
|
+
? route.around(
|
|
58
|
+
untrack(() => props.depth),
|
|
59
|
+
id,
|
|
60
|
+
body,
|
|
61
|
+
)
|
|
62
|
+
: body()
|
|
29
63
|
}
|
|
30
|
-
|
|
64
|
+
const display = retainRouteView(view, () => {
|
|
65
|
+
const shape = lv.shape.value
|
|
66
|
+
return shape ? (
|
|
67
|
+
<div inert aria-hidden="true" style="display:contents">
|
|
68
|
+
<ShapeTree shapes={shape} depth={0} />
|
|
69
|
+
</div>
|
|
70
|
+
) : undefined
|
|
71
|
+
})
|
|
72
|
+
return <>{display()}</>
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** True when `path` is `href`, or with `section`, any page under it (a top link for `/components` on `/components/modal`). */
|
|
76
|
+
export function isCurrent(path: string, href: string, section = false) {
|
|
77
|
+
if (path === href) return true
|
|
78
|
+
return section && href !== '/' && path.startsWith(`${href.replace(/\/$/, '')}/`)
|
|
31
79
|
}
|
|
32
80
|
|
|
33
81
|
/**
|
|
34
|
-
* `<a>` with `aria-current="page"` while its path is the current one
|
|
35
|
-
* client (start() listens for clicks); Link only
|
|
36
|
-
* and start() updates it after a navigation, so a
|
|
82
|
+
* `<a>` with `aria-current="page"` while its path is the current one, or with `match="section"` any page under it, as a
|
|
83
|
+
* site's top links want. Any same-origin `<a href>` navigates on the client (start() listens for clicks); Link only
|
|
84
|
+
* adds the current-page state. In a static level the server writes it and start() updates it after a navigation, so a
|
|
85
|
+
* Link needs no code there.
|
|
37
86
|
*
|
|
38
87
|
* @weft static
|
|
39
88
|
*/
|
|
40
|
-
export function Link({
|
|
89
|
+
export function Link({
|
|
90
|
+
href,
|
|
91
|
+
match,
|
|
92
|
+
children,
|
|
93
|
+
...rest
|
|
94
|
+
}: Record<string, unknown> & {
|
|
95
|
+
href: string
|
|
96
|
+
match?: 'page' | 'section'
|
|
97
|
+
class?: string
|
|
98
|
+
rel?: string
|
|
99
|
+
children?: unknown
|
|
100
|
+
}) {
|
|
41
101
|
const route = active.route
|
|
42
102
|
return (
|
|
43
|
-
<a
|
|
103
|
+
<a
|
|
104
|
+
{...rest}
|
|
105
|
+
href={href}
|
|
106
|
+
data-match={match === 'section' ? 'section' : undefined}
|
|
107
|
+
aria-current={
|
|
108
|
+
route && isCurrent(route.path.value.replace(/[?#].*/, ''), href, match === 'section')
|
|
109
|
+
? 'page'
|
|
110
|
+
: undefined
|
|
111
|
+
}
|
|
112
|
+
>
|
|
44
113
|
{children}
|
|
45
114
|
</a>
|
|
46
115
|
)
|
|
47
116
|
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The current page's path, without its search or hash: the request's on the server, and on the client the accepted
|
|
120
|
+
* destination from the moment a navigation starts. Reading it is reactive.
|
|
121
|
+
*
|
|
122
|
+
* Use it to keep shared chrome mounted across sections: one layout above them renders the sidebar and picks its items
|
|
123
|
+
* from the path, so moving between sections swaps the items and the page, and the sidebar stays. A layout per section
|
|
124
|
+
* would be replaced, sidebar and all, on every move between them.
|
|
125
|
+
*
|
|
126
|
+
* What reads it runs in the browser, since it changes there: a static level's HTML would be swapped whole on each move.
|
|
127
|
+
*
|
|
128
|
+
* @weft client
|
|
129
|
+
*/
|
|
130
|
+
export function currentPath(): string {
|
|
131
|
+
return active.route?.path.value.replace(/[?#].*/, '') ?? '/'
|
|
132
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const byText = (a, b) => (a < b ? -1 : a > b ? 1 : 0)
|
|
2
|
+
|
|
3
|
+
// A closure names emitted files, so callers can union routes without counting a shared file twice.
|
|
4
|
+
function closure(bundle, roots) {
|
|
5
|
+
const seen = new Set()
|
|
6
|
+
let complete = true
|
|
7
|
+
const visit = (file) => {
|
|
8
|
+
if (seen.has(file)) return
|
|
9
|
+
seen.add(file)
|
|
10
|
+
const item = bundle[file]
|
|
11
|
+
if (item?.type !== 'chunk') return
|
|
12
|
+
if (
|
|
13
|
+
!Array.isArray(item.imports) ||
|
|
14
|
+
!item.viteMetadata?.importedCss ||
|
|
15
|
+
!item.viteMetadata?.importedAssets
|
|
16
|
+
)
|
|
17
|
+
complete = false
|
|
18
|
+
for (const dep of item.imports ?? []) visit(dep)
|
|
19
|
+
for (const dep of item.viteMetadata?.importedCss ?? []) visit(dep)
|
|
20
|
+
for (const dep of item.viteMetadata?.importedAssets ?? []) visit(dep)
|
|
21
|
+
}
|
|
22
|
+
for (const file of roots) visit(file)
|
|
23
|
+
return { files: [...seen].sort(byText), complete }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function chunkFor(bundle, ids) {
|
|
27
|
+
const matches = Object.entries(bundle).filter(
|
|
28
|
+
([, item]) =>
|
|
29
|
+
item.type === 'chunk' &&
|
|
30
|
+
ids.some((id) => item.facadeModuleId === id || Object.hasOwn(item.modules ?? {}, id)),
|
|
31
|
+
)
|
|
32
|
+
return matches.length === 1 ? matches[0][0] : undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Byte lengths of emitted static closures. Dynamic and external dependencies are not inferred. */
|
|
36
|
+
export function assetBytes(bundle, read, manifest, abs) {
|
|
37
|
+
const routes = Object.fromEntries(
|
|
38
|
+
[...manifest.layouts, ...manifest.routes].map((route) => {
|
|
39
|
+
const file = abs(route.file)
|
|
40
|
+
const root = chunkFor(bundle, [file, `${file}?weft-assets`])
|
|
41
|
+
return [route.id, root ? closure(bundle, [root]) : null]
|
|
42
|
+
}),
|
|
43
|
+
)
|
|
44
|
+
const eagerRoot = chunkFor(bundle, ['\0virtual:weft/routes'])
|
|
45
|
+
const eager = eagerRoot ? closure(bundle, [eagerRoot]) : null
|
|
46
|
+
const owners = new Map()
|
|
47
|
+
for (const entry of [...Object.values(routes), eager]) {
|
|
48
|
+
for (const file of entry?.files ?? []) owners.set(file, (owners.get(file) ?? 0) + 1)
|
|
49
|
+
}
|
|
50
|
+
const shared = [...owners]
|
|
51
|
+
.filter(([, count]) => count > 1)
|
|
52
|
+
.map(([file]) => file)
|
|
53
|
+
.sort(byText)
|
|
54
|
+
const sharedSet = new Set(shared)
|
|
55
|
+
const files = Object.fromEntries(
|
|
56
|
+
[...owners.keys()].sort(byText).map((file) => {
|
|
57
|
+
const item = bundle[file]
|
|
58
|
+
let bytes = null
|
|
59
|
+
if (item) {
|
|
60
|
+
try {
|
|
61
|
+
bytes = read(file).length
|
|
62
|
+
} catch {
|
|
63
|
+
/* not emitted or not readable */
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const kind = !item
|
|
67
|
+
? 'unknown'
|
|
68
|
+
: item.type === 'chunk'
|
|
69
|
+
? 'js'
|
|
70
|
+
: file.endsWith('.css')
|
|
71
|
+
? 'css'
|
|
72
|
+
: 'asset'
|
|
73
|
+
return [file, { bytes, kind }]
|
|
74
|
+
}),
|
|
75
|
+
)
|
|
76
|
+
const size = (items) =>
|
|
77
|
+
items.some((file) => files[file].bytes === null)
|
|
78
|
+
? null
|
|
79
|
+
: items.reduce((sum, file) => sum + files[file].bytes, 0)
|
|
80
|
+
const describe = (entry) =>
|
|
81
|
+
entry && {
|
|
82
|
+
files: entry.files,
|
|
83
|
+
bytes: entry.complete ? size(entry.files) : null,
|
|
84
|
+
uniqueBytes: entry.complete ? size(entry.files.filter((file) => !sharedSet.has(file))) : null,
|
|
85
|
+
sharedBytes: entry.complete ? size(entry.files.filter((file) => sharedSet.has(file))) : null,
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
version: 1,
|
|
89
|
+
scope: 'vite-static-closure',
|
|
90
|
+
files,
|
|
91
|
+
shared,
|
|
92
|
+
routes: Object.fromEntries(Object.entries(routes).map(([id, items]) => [id, describe(items)])),
|
|
93
|
+
eager: describe(eager),
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { relative } from 'node:path'
|
|
3
|
+
import { parse } from './syntax.mjs'
|
|
4
|
+
import { isServerModule } from './server-functions.mjs'
|
|
5
|
+
import { NOT_ASSET, ASSETS } from './shared.mjs'
|
|
6
|
+
|
|
7
|
+
/** weft's own packages from an install: a static component's stylesheets are in them, and they ship source. */
|
|
8
|
+
const KIT = /\/node_modules\/@weft-js\/.*\.[jt]sx?$/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The stylesheets a kit import reaches, in import order, written straight into the static module: chained `?weft-assets`
|
|
12
|
+
* modules from a package with `sideEffects` would be shaken away. `names` follows a barrel only to the components named,
|
|
13
|
+
* so a page with one doesn't pull every component's sheet.
|
|
14
|
+
*/
|
|
15
|
+
async function kitAssets(ctx, file, names, seen, words) {
|
|
16
|
+
const id = names ? `${file}|${names.join()}` : file
|
|
17
|
+
if (seen.has(id)) return []
|
|
18
|
+
seen.add(id)
|
|
19
|
+
const code = readFileSync(file, 'utf8')
|
|
20
|
+
const body = parse(file, code).program.body
|
|
21
|
+
const wanted = (name) => !names || names.includes(name)
|
|
22
|
+
const makes = ({ declaration: d, specifiers }) =>
|
|
23
|
+
d?.declarations
|
|
24
|
+
? d.declarations.some((v) => v.id.type !== 'Identifier' || names.includes(v.id.name))
|
|
25
|
+
: names.includes(d?.id?.name) ||
|
|
26
|
+
specifiers.some((s) => names.includes(s.exported.name ?? s.exported.value))
|
|
27
|
+
// Only a file that makes a name itself brings its own imports: one that passes it on is a barrel.
|
|
28
|
+
const own =
|
|
29
|
+
!names ||
|
|
30
|
+
body.some(
|
|
31
|
+
(n) =>
|
|
32
|
+
n.type === 'ExportDefaultDeclaration' ||
|
|
33
|
+
(n.type === 'ExportNamedDeclaration' && !n.source && makes(n)),
|
|
34
|
+
)
|
|
35
|
+
if (own) words?.add(code)
|
|
36
|
+
const out = []
|
|
37
|
+
for (const n of body) {
|
|
38
|
+
if (!n.source || n.importKind === 'type' || n.exportKind === 'type') continue
|
|
39
|
+
const all = n.type === 'ExportAllDeclaration'
|
|
40
|
+
const space = all && n.exported && (n.exported.name ?? n.exported.value) // `export * as K`: K is the whole module
|
|
41
|
+
const passed =
|
|
42
|
+
n.type === 'ExportNamedDeclaration' &&
|
|
43
|
+
n.specifiers
|
|
44
|
+
.filter((s) => s.exportKind !== 'type' && wanted(s.exported.name ?? s.exported.value))
|
|
45
|
+
.map((s) => s.local.name ?? s.local.value)
|
|
46
|
+
// A bare import runs whatever the importer wants from the file.
|
|
47
|
+
if (
|
|
48
|
+
n.type === 'ImportDeclaration'
|
|
49
|
+
? !own && n.specifiers.length
|
|
50
|
+
: passed
|
|
51
|
+
? !passed.length
|
|
52
|
+
: !all || (space && !wanted(space))
|
|
53
|
+
)
|
|
54
|
+
continue
|
|
55
|
+
if (/^@weft-js\/(?:core|start)($|\/)/.test(n.source.value)) continue
|
|
56
|
+
const target = (await ctx.resolve(n.source.value, file, { skipSelf: true }))?.id
|
|
57
|
+
if (!target) continue
|
|
58
|
+
const bare = target.replace(/\?.*$/, '')
|
|
59
|
+
if (!NOT_ASSET.test(bare))
|
|
60
|
+
out.push(`import ${JSON.stringify(target.startsWith('\0') ? n.source.value : bare)}`)
|
|
61
|
+
else if (KIT.test(bare))
|
|
62
|
+
out.push(
|
|
63
|
+
...(await kitAssets(
|
|
64
|
+
ctx,
|
|
65
|
+
bare,
|
|
66
|
+
passed || (space ? undefined : all ? names : importedNames(n)),
|
|
67
|
+
seen,
|
|
68
|
+
words,
|
|
69
|
+
)),
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
return [...new Set(out)]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The value names an import takes, or undefined for the whole module (`import 'x'`, `import * as x`). */
|
|
76
|
+
const importedNames = (n) => {
|
|
77
|
+
const names = (n.specifiers ?? [])
|
|
78
|
+
.filter((s) => s.type === 'ImportSpecifier' && s.importKind !== 'type')
|
|
79
|
+
.map((s) => (s.imported ?? s.local).name ?? (s.imported ?? s.local).value)
|
|
80
|
+
return names.length && n.specifiers.every((s) => s.type === 'ImportSpecifier') ? names : undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A static module in the client build: what it imports that isn't code (stylesheets, images), and the same of the
|
|
85
|
+
* modules it imports. Its class names go to the purge, which only sees client files.
|
|
86
|
+
*/
|
|
87
|
+
export async function assetsOnly(ctx, code, path, root, words) {
|
|
88
|
+
words?.add(code)
|
|
89
|
+
const ast = parse(path, code)
|
|
90
|
+
const out = []
|
|
91
|
+
const seen = new Set()
|
|
92
|
+
for (const n of ast.program.body) {
|
|
93
|
+
if (
|
|
94
|
+
!/^(ImportDeclaration|ExportNamedDeclaration|ExportAllDeclaration)$/.test(n.type) ||
|
|
95
|
+
!n.source ||
|
|
96
|
+
n.importKind === 'type' ||
|
|
97
|
+
n.exportKind === 'type'
|
|
98
|
+
)
|
|
99
|
+
continue
|
|
100
|
+
const spec = n.source.value
|
|
101
|
+
if (/^@weft-js\/(?:core|start)($|\/)/.test(spec)) continue
|
|
102
|
+
const file = (await ctx.resolve(spec, path, { skipSelf: true }))?.id
|
|
103
|
+
if (!file) continue
|
|
104
|
+
const bare = file.replace(/\?.*$/, '')
|
|
105
|
+
if (!NOT_ASSET.test(bare)) out.push(`import ${JSON.stringify(spec)}`)
|
|
106
|
+
else if (KIT.test(bare))
|
|
107
|
+
out.push(...(await kitAssets(ctx, bare, importedNames(n), seen, words)))
|
|
108
|
+
else if (
|
|
109
|
+
!bare.includes('/node_modules/') &&
|
|
110
|
+
!isServerModule(relative(root, bare).split('\\').join('/'))
|
|
111
|
+
)
|
|
112
|
+
out.push(`import ${JSON.stringify(bare + ASSETS)}`)
|
|
113
|
+
}
|
|
114
|
+
if (code.includes('@weft-js/core/style')) out.push(`import 'virtual:weft/groups.css'`)
|
|
115
|
+
out.push('export default null') // a chunk of only CSS is dropped, and the server looks this one up
|
|
116
|
+
return out.join('\n') + '\n'
|
|
117
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { readFileSync, realpathSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import { isAbsolute, join, relative, resolve, sep, win32 } from 'node:path'
|
|
5
|
+
import { searchForWorkspaceRoot } from 'vite'
|
|
6
|
+
import { INLINE_CSS_MAX } from '../inline.mjs'
|
|
7
|
+
import { packageDirs } from './dev.mjs'
|
|
8
|
+
import { assetBytes } from './asset-bytes.mjs'
|
|
9
|
+
import { combineSheets, sheetPieces, inlineSheets } from './sheets.mjs'
|
|
10
|
+
import { here, V, KIT_CHUNKS, INLINE_SHEETS, SHEET_PIECES, ROUTE_ASSET_BYTES } from './shared.mjs'
|
|
11
|
+
|
|
12
|
+
export const configHook = (s) =>
|
|
13
|
+
function (config, env) {
|
|
14
|
+
s.root = config.root ? resolve(config.root) : s.root
|
|
15
|
+
s.dev = env.command === 'serve'
|
|
16
|
+
s.scan()
|
|
17
|
+
return {
|
|
18
|
+
environments: {
|
|
19
|
+
client: {
|
|
20
|
+
build: {
|
|
21
|
+
rollupOptions: {
|
|
22
|
+
input: s.clientEntry(),
|
|
23
|
+
output: { advancedChunks: { groups: KIT_CHUNKS } },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
ssr: { build: { rollupOptions: { input: V.server } } },
|
|
28
|
+
},
|
|
29
|
+
// The router-core server bundle and Takumi's addon stay external to the SSR graph; Nitro traces them.
|
|
30
|
+
ssr: {
|
|
31
|
+
noExternal: ['@weft-js/start', '@weft-js/warp', '@weft-js/warp-icons', '@weft-js/core'],
|
|
32
|
+
external: ['@takumi-rs/core'],
|
|
33
|
+
},
|
|
34
|
+
optimizeDeps: {
|
|
35
|
+
exclude: ['@weft-js/start', '@weft-js/warp', '@weft-js/warp-icons', '@weft-js/core'],
|
|
36
|
+
}, // they ship TSX for the weft plugin
|
|
37
|
+
// The runtime files of @weft-js/start, weft and Nitro are served from wherever they're installed (a linked package
|
|
38
|
+
// sits outside the app's root).
|
|
39
|
+
server: {
|
|
40
|
+
fs: { allow: [searchForWorkspaceRoot(s.root), join(here, '..'), ...packageDirs(s.root)] },
|
|
41
|
+
},
|
|
42
|
+
// One copy of the router: the app's route files and the generated entries import the same module.
|
|
43
|
+
resolve: {
|
|
44
|
+
alias: [
|
|
45
|
+
{ find: /^@weft-js\/start$/, replacement: join(here, 'index.ts') },
|
|
46
|
+
{ find: /^@weft-js\/start\/(client|server)$/, replacement: join(here, '$1.tsx') },
|
|
47
|
+
{ find: /^@weft-js\/start\/media$/, replacement: join(here, 'media.ts') },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function emittedReader(dir) {
|
|
54
|
+
const root = realpathSync(dir)
|
|
55
|
+
const inside = (path) => {
|
|
56
|
+
const rel = relative(root, path)
|
|
57
|
+
return rel !== '' && rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel)
|
|
58
|
+
}
|
|
59
|
+
return (file) => {
|
|
60
|
+
if (
|
|
61
|
+
typeof file !== 'string' ||
|
|
62
|
+
!file ||
|
|
63
|
+
isAbsolute(file) ||
|
|
64
|
+
win32.parse(file).root ||
|
|
65
|
+
file.includes('\\') ||
|
|
66
|
+
file.split('/').includes('..')
|
|
67
|
+
)
|
|
68
|
+
throw new Error('Invalid asset path')
|
|
69
|
+
const path = resolve(root, file)
|
|
70
|
+
if (!inside(path)) throw new Error('Asset outside output')
|
|
71
|
+
const real = realpathSync(path)
|
|
72
|
+
if (!inside(real)) throw new Error('Asset outside output')
|
|
73
|
+
return readFileSync(real)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const writeBundle = (s) => ({
|
|
78
|
+
order: 'post',
|
|
79
|
+
async handler(out, bundle) {
|
|
80
|
+
if (!s.dev && this.environment?.config.consumer === 'client' && out.dir) {
|
|
81
|
+
const name = 'weft-route-assets.json'
|
|
82
|
+
if (bundle[name]) throw new Error(`Cannot overwrite emitted ${name}`)
|
|
83
|
+
const bytes = assetBytes(bundle, emittedReader(out.dir), s.manifest ?? s.scan(), s.abs)
|
|
84
|
+
bytes.build = s.build || null
|
|
85
|
+
writeFileSync(join(out.dir, name), JSON.stringify(bytes))
|
|
86
|
+
s.routeAssetBytes = bytes
|
|
87
|
+
}
|
|
88
|
+
if (
|
|
89
|
+
s.dev ||
|
|
90
|
+
this.environment?.config.consumer !== 'client' ||
|
|
91
|
+
s.options.site?.criticalCss === false ||
|
|
92
|
+
s.options.site?.inlineCss === false
|
|
93
|
+
)
|
|
94
|
+
return
|
|
95
|
+
s.sheetPlan = combineSheets(
|
|
96
|
+
out.dir,
|
|
97
|
+
bundle,
|
|
98
|
+
s.manifest ?? s.scan(),
|
|
99
|
+
s.abs,
|
|
100
|
+
(await s.hydration(this))?.islands ?? [],
|
|
101
|
+
s.options.site?.base ?? '/',
|
|
102
|
+
)
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
export const renderChunk = (s) => ({
|
|
107
|
+
order: 'post',
|
|
108
|
+
handler(code) {
|
|
109
|
+
const receiptToken = new RegExp(`["'\`]${ROUTE_ASSET_BYTES}["'\`]`)
|
|
110
|
+
const hasReceipt = receiptToken.test(code)
|
|
111
|
+
if (hasReceipt) {
|
|
112
|
+
let receipt = null
|
|
113
|
+
try {
|
|
114
|
+
const top = this.environment.getTopLevelConfig()
|
|
115
|
+
const client = resolve(s.root, top.environments.client.build.outDir)
|
|
116
|
+
const saved = JSON.parse(readFileSync(join(client, 'weft-route-assets.json'), 'utf8'))
|
|
117
|
+
if (s.build && saved.build === s.build) receipt = saved
|
|
118
|
+
} catch {
|
|
119
|
+
/* no matching client receipt */
|
|
120
|
+
}
|
|
121
|
+
code = code.replace(receiptToken, () => JSON.stringify(receipt))
|
|
122
|
+
}
|
|
123
|
+
const token = new RegExp(`["'\`]${INLINE_SHEETS}["'\`]`)
|
|
124
|
+
if (!token.test(code)) return hasReceipt ? { code, map: null } : null
|
|
125
|
+
const top = this.environment.getTopLevelConfig()
|
|
126
|
+
const client = resolve(s.root, top.environments.client.build.outDir)
|
|
127
|
+
code = code.replace(new RegExp(`["'\`]${SHEET_PIECES}["'\`]`), () =>
|
|
128
|
+
JSON.stringify(
|
|
129
|
+
s.sheetPlan &&
|
|
130
|
+
sheetPieces(client, top.base ?? '/', s.sheetPlan, s.options.site?.criticalKeep, s.root),
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
// With a first-paint table the server reads none of these; without, a page too big to carry whole is split (`pageStyles`).
|
|
134
|
+
const sheets =
|
|
135
|
+
s.options.site?.inlineCss === false
|
|
136
|
+
? {}
|
|
137
|
+
: inlineSheets(
|
|
138
|
+
client,
|
|
139
|
+
top.base ?? '/',
|
|
140
|
+
s.options.site?.criticalCss === false ? Infinity : INLINE_CSS_MAX,
|
|
141
|
+
)
|
|
142
|
+
return { code: code.replace(token, () => JSON.stringify(sheets)), map: null }
|
|
143
|
+
},
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
export const configResolved = (s) =>
|
|
147
|
+
function (config) {
|
|
148
|
+
// Each build is its own version, unless it's pinned: instances built apart that serve one deploy, or a
|
|
149
|
+
// reproducible build (SOURCE_DATE_EPOCH), have to agree on it.
|
|
150
|
+
s.build =
|
|
151
|
+
process.env.WEFT_BUILD_ID ||
|
|
152
|
+
createHash('sha256')
|
|
153
|
+
.update(process.env.SOURCE_DATE_EPOCH || String(Date.now()))
|
|
154
|
+
.digest('hex')
|
|
155
|
+
.slice(0, 8)
|
|
156
|
+
if (s.dev) s.build = 'dev'
|
|
157
|
+
s.root = config.root
|
|
158
|
+
const main = config.plugins.find((p) => p.name === 'weft')
|
|
159
|
+
s.words = config.plugins.find((p) => p.name === 'weft:purge')?.api
|
|
160
|
+
s.words?.add('data-weft styled') // gate.css reveals a mark a static page's HTML may hold alone
|
|
161
|
+
if (main?.api) {
|
|
162
|
+
s.compile = createRequire(join(here, 'vite.mjs'))('@weft-js/core/compiler').transform
|
|
163
|
+
main.api.islands = async (ctx, file) =>
|
|
164
|
+
(await s.hydration(ctx))?.files.get(file.replace(/\?.*$/, ''))
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { existsSync, readFileSync, watch } from 'node:fs'
|
|
2
|
+
import { join, relative } from 'node:path'
|
|
3
|
+
import { waitingForAFix, reaches, refresh } from './dev.mjs'
|
|
4
|
+
import { serverExports } from './syntax.mjs'
|
|
5
|
+
import { isServerModule } from './server-functions.mjs'
|
|
6
|
+
import { SHEET, ASSETS, V } from './shared.mjs'
|
|
7
|
+
|
|
8
|
+
export const configureServer = (s) =>
|
|
9
|
+
function (server) {
|
|
10
|
+
s.vite = server
|
|
11
|
+
// A `<link>` to a virtual sheet (`weft:fonts`): Vite only serves those as modules.
|
|
12
|
+
const sheet = async (req, res, next) => {
|
|
13
|
+
const css = await server.environments.client.pluginContainer
|
|
14
|
+
.load(decodeURIComponent(req.url.slice(1)))
|
|
15
|
+
.catch(() => undefined)
|
|
16
|
+
if (css == null) return next()
|
|
17
|
+
res.setHeader('content-type', 'text/css')
|
|
18
|
+
res.end(typeof css === 'string' ? css : css.code)
|
|
19
|
+
}
|
|
20
|
+
server.middlewares.use(SHEET, (req, res, next) => void sheet(req, res, next))
|
|
21
|
+
server.middlewares.use(waitingForAFix)
|
|
22
|
+
// A route file added or removed changes the virtual modules; edits to existing ones are ordinary HMR/reloads.
|
|
23
|
+
const dir = join(s.root, 'src')
|
|
24
|
+
if (!existsSync(dir)) return
|
|
25
|
+
const routesDir = relative(dir, join(s.root, s.manifest.dir)).split('\\').join('/')
|
|
26
|
+
const shape = () =>
|
|
27
|
+
JSON.stringify([
|
|
28
|
+
s.manifest.routes.map((r) => r.file),
|
|
29
|
+
s.manifest.layouts.map((l) => l.file),
|
|
30
|
+
s.serverModules,
|
|
31
|
+
])
|
|
32
|
+
const changed = (file) => {
|
|
33
|
+
const rel = file.split('\\').join('/')
|
|
34
|
+
if (!rel.startsWith(routesDir + '/') && !isServerModule('src/' + rel)) return
|
|
35
|
+
// Saving a file can count as a rename, depending on the platform and the editor.
|
|
36
|
+
const before = shape()
|
|
37
|
+
s.scan()
|
|
38
|
+
if (shape() === before) return
|
|
39
|
+
for (const env of Object.values(server.environments)) {
|
|
40
|
+
for (const v of [V.routes, V.fns, V.context]) {
|
|
41
|
+
const m = env.moduleGraph.getModuleById('\0' + v)
|
|
42
|
+
if (m) env.moduleGraph.invalidateModule(m)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
server.config.logger.info(
|
|
46
|
+
`weft: full reload: ${rel} was added or removed, so the routes changed`,
|
|
47
|
+
{ timestamp: true },
|
|
48
|
+
)
|
|
49
|
+
server.ws.send({ type: 'full-reload' })
|
|
50
|
+
}
|
|
51
|
+
const w = watch(dir, { recursive: true }, (event, file) => {
|
|
52
|
+
if (event === 'rename' && file) changed(file)
|
|
53
|
+
})
|
|
54
|
+
// Not every runtime's recursive watch reports a new file, and Vite's watcher does.
|
|
55
|
+
for (const event of ['add', 'unlink'])
|
|
56
|
+
server.watcher.on(event, (file) => {
|
|
57
|
+
if (file.startsWith(dir)) changed(relative(dir, file))
|
|
58
|
+
})
|
|
59
|
+
// Close with the server (middleware mode, as `weft routes --meta` runs it, has no httpServer to listen on).
|
|
60
|
+
const close = server.close.bind(server)
|
|
61
|
+
server.close = () => (w.close(), close())
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const hotUpdate = (s) =>
|
|
65
|
+
async function ({ file, modules }) {
|
|
66
|
+
const rel = relative(s.root, file).split('\\').join('/')
|
|
67
|
+
if (this.environment.config.consumer === 'server') {
|
|
68
|
+
// An edit can add or drop a stylesheet import: the next server load of the routes walks the graph again.
|
|
69
|
+
const m = this.environment.moduleGraph.getModuleById('\0' + V.routes)
|
|
70
|
+
if (m) this.environment.moduleGraph.invalidateModule(m)
|
|
71
|
+
// A static component or a server helper is in no browser's module graph, so nothing else would tell the tab.
|
|
72
|
+
if (
|
|
73
|
+
!s.vite?.environments.client.moduleGraph.getModulesByFile(file)?.size &&
|
|
74
|
+
this.environment.moduleGraph.getModulesByFile(file)?.size
|
|
75
|
+
)
|
|
76
|
+
refresh(s.vite, rel)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
const route =
|
|
80
|
+
s.manifest.routes.some((r) => r.file === rel) ||
|
|
81
|
+
s.manifest.layouts.some((l) => l.file === rel)
|
|
82
|
+
if (route) {
|
|
83
|
+
const part = serverExports(readFileSync(file, 'utf8'), file)
|
|
84
|
+
// A level the server renders has no module in the page, and its `load` and `meta` are on the server too.
|
|
85
|
+
refresh(s.vite, rel, s.loaders.get(file) !== part, false)
|
|
86
|
+
s.loaders.set(file, part)
|
|
87
|
+
}
|
|
88
|
+
if (isServerModule(rel) && s.stubs.has(file) && modules.length) {
|
|
89
|
+
const before = s.stubs.get(file)
|
|
90
|
+
await Promise.all(
|
|
91
|
+
modules.map((m) => this.environment.transformRequest(m.url).catch(() => undefined)),
|
|
92
|
+
)
|
|
93
|
+
if (s.stubs.get(file) === before) return (refresh(s.vite, rel), [])
|
|
94
|
+
}
|
|
95
|
+
// A static route's stylesheet list: nothing runs it, so nothing has to take the update.
|
|
96
|
+
const live = modules.filter((m) => !m.url.endsWith(ASSETS))
|
|
97
|
+
if (live.some((m) => !reaches(m)))
|
|
98
|
+
this.environment.logger.info(
|
|
99
|
+
`weft: full reload: ${rel} exports more than components and nothing that imports it can take the update`,
|
|
100
|
+
{ timestamp: true },
|
|
101
|
+
)
|
|
102
|
+
if (live.length < modules.length) return live
|
|
103
|
+
}
|