@weft-js/start 0.1.0 → 0.1.2
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 +36 -3
- package/crate/Cargo.lock +1347 -0
- package/crate/Cargo.toml +72 -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 +51 -10
- 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 +124 -490
- package/src/client.tsx +9 -2
- package/src/contract/describe.mjs +231 -0
- package/src/contract.mjs +146 -155
- 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 +97 -168
- 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 +113 -35
- 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 -174
- 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 -964
- 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
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs'
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path'
|
|
3
|
+
import { shapeBody, shapeCssSource, shapeStyleSpecifier } from './route-modules.mjs'
|
|
4
|
+
|
|
5
|
+
// Follow existing ancestors so a symlink cannot turn an in-project watch into an outside watch.
|
|
6
|
+
export function projectParent(root, file) {
|
|
7
|
+
try {
|
|
8
|
+
const project = realpathSync(root)
|
|
9
|
+
let dir = dirname(file)
|
|
10
|
+
while (!existsSync(dir) && dirname(dir) !== dir) dir = dirname(dir)
|
|
11
|
+
const parent = realpathSync(dir)
|
|
12
|
+
const rel = relative(project, parent)
|
|
13
|
+
if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return
|
|
14
|
+
return parent
|
|
15
|
+
} catch {
|
|
16
|
+
return undefined
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Resolvers may preserve symlinks, so both the apparent and actual file must be app source.
|
|
21
|
+
function appSource(root, file) {
|
|
22
|
+
const blocked = (base, path) => {
|
|
23
|
+
const rel = relative(base, path).replace(/\\/g, '/')
|
|
24
|
+
return (
|
|
25
|
+
rel === '..' ||
|
|
26
|
+
rel.startsWith('../') ||
|
|
27
|
+
isAbsolute(rel) ||
|
|
28
|
+
/(^|\/)node_modules\//.test(rel) ||
|
|
29
|
+
/(^|\/)server\//.test(rel) ||
|
|
30
|
+
/\.server\.[cm]?[jt]sx?$/.test(rel)
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return !blocked(root, file) && !blocked(realpathSync(root), realpathSync(file))
|
|
35
|
+
} catch {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Only static runtime import/export edges can lead to a stylesheet here.
|
|
41
|
+
function sources(file) {
|
|
42
|
+
return shapeBody(file).filter((node) => {
|
|
43
|
+
if (node.type === 'ImportDeclaration') {
|
|
44
|
+
return (
|
|
45
|
+
node.importKind !== 'type' &&
|
|
46
|
+
(!node.specifiers.length || node.specifiers.some((item) => item.importKind !== 'type'))
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
return (
|
|
50
|
+
(node.type === 'ExportNamedDeclaration' || node.type === 'ExportAllDeclaration') &&
|
|
51
|
+
node.exportKind !== 'type' &&
|
|
52
|
+
node.source
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function signature(nodes) {
|
|
58
|
+
return JSON.stringify(
|
|
59
|
+
nodes.map((node) => [
|
|
60
|
+
node.type,
|
|
61
|
+
node.source.value,
|
|
62
|
+
node.type === 'ImportDeclaration' && !node.specifiers.length,
|
|
63
|
+
]),
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Signature of the static dependency edges relevant to entry styles. */
|
|
68
|
+
export function entryCssSignature(file) {
|
|
69
|
+
return signature(sources(file))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Collect safely resolvable local entry dependencies and their CSS in source order. */
|
|
73
|
+
export async function entryCss(ctx, entry, root) {
|
|
74
|
+
const seen = new Set()
|
|
75
|
+
const styles = new Set()
|
|
76
|
+
const signatures = new Map()
|
|
77
|
+
const candidates = new Set()
|
|
78
|
+
const missing = new Set()
|
|
79
|
+
const key = (file) => file.replace(/\\/g, '/')
|
|
80
|
+
const visit = async (file) => {
|
|
81
|
+
if (seen.has(file) || seen.size >= 256) return
|
|
82
|
+
seen.add(file)
|
|
83
|
+
const nodes = sources(file)
|
|
84
|
+
signatures.set(file, signature(nodes))
|
|
85
|
+
for (const node of nodes) {
|
|
86
|
+
const spec = node.source.value
|
|
87
|
+
if (
|
|
88
|
+
node.type === 'ImportDeclaration' &&
|
|
89
|
+
!node.specifiers.length &&
|
|
90
|
+
shapeStyleSpecifier(spec)
|
|
91
|
+
) {
|
|
92
|
+
const css = await shapeCssSource(ctx, file, spec, () => {
|
|
93
|
+
if (/^\.{1,2}\//.test(spec)) {
|
|
94
|
+
const candidate = resolve(dirname(file), spec)
|
|
95
|
+
if (projectParent(root, candidate)) {
|
|
96
|
+
candidates.add(key(candidate))
|
|
97
|
+
missing.add(key(candidate))
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
if (css) {
|
|
102
|
+
styles.add(css)
|
|
103
|
+
candidates.add(key(css))
|
|
104
|
+
}
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
if (/[?#]/.test(spec) || /^(?:[a-z][\w+.-]*:|\/\/)/i.test(spec)) continue
|
|
108
|
+
const found = await ctx.resolve(spec, file, { skipSelf: true })
|
|
109
|
+
const local = /^\.{1,2}\//.test(spec)
|
|
110
|
+
const target = found?.id ?? (!found && local ? resolve(dirname(file), spec) : undefined)
|
|
111
|
+
if (found?.external || !target || !isAbsolute(target)) continue
|
|
112
|
+
const rel = relative(root, target).replace(/\\/g, '/')
|
|
113
|
+
if (
|
|
114
|
+
rel === '..' ||
|
|
115
|
+
rel.startsWith('../') ||
|
|
116
|
+
isAbsolute(rel) ||
|
|
117
|
+
/(^|\/)node_modules\//.test(rel) ||
|
|
118
|
+
/(^|\/)server\//.test(rel) ||
|
|
119
|
+
/\.server(?:\.[cm]?[jt]sx?)?$/.test(rel)
|
|
120
|
+
)
|
|
121
|
+
continue
|
|
122
|
+
if (!found) {
|
|
123
|
+
if (projectParent(root, target)) {
|
|
124
|
+
candidates.add(key(target))
|
|
125
|
+
missing.add(key(target))
|
|
126
|
+
}
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
if (!/\.[cm]?[jt]sx?$/.test(target) || !appSource(root, target)) continue
|
|
130
|
+
candidates.add(key(target))
|
|
131
|
+
await visit(target)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
await visit(entry)
|
|
135
|
+
return { styles: [...styles], signatures, candidates, missing }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Watch a missing candidate's nearest existing parent for later creation. */
|
|
139
|
+
export function watchEntryCandidates(server, candidates, root) {
|
|
140
|
+
if (!server || !root) return
|
|
141
|
+
const dirs = new Set()
|
|
142
|
+
for (const file of candidates) {
|
|
143
|
+
if (existsSync(file)) continue
|
|
144
|
+
const dir = projectParent(root, file)
|
|
145
|
+
if (dir) dirs.add(dir)
|
|
146
|
+
}
|
|
147
|
+
for (const dir of dirs) server.watcher.add(dir)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Clear this server's entry metadata without changing the shared Vite watcher. */
|
|
151
|
+
export function entryWatchLifecycle(state, server, modes) {
|
|
152
|
+
state.shapeScanGeneration = (state.shapeScanGeneration ?? 0) + 1
|
|
153
|
+
state.shapeEntryOwner = server
|
|
154
|
+
delete state.shapeEntryClosed
|
|
155
|
+
const close = server.close
|
|
156
|
+
server.close = async function (...args) {
|
|
157
|
+
if (state.shapeEntryOwner === server) {
|
|
158
|
+
state.shapeEntryClosed = true
|
|
159
|
+
state.shapeScanGeneration = (state.shapeScanGeneration ?? 0) + 1
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
return await close.apply(this, args)
|
|
163
|
+
} finally {
|
|
164
|
+
if (state.shapeEntryOwner === server) {
|
|
165
|
+
delete state.shapeEntryOwner
|
|
166
|
+
delete state.shapeEntrySignatures
|
|
167
|
+
delete state.shapeEntryCandidates
|
|
168
|
+
delete state.shapeEntryMissing
|
|
169
|
+
delete state.shapeOverrideDependencies
|
|
170
|
+
if (state.vite === server) state.vite = undefined
|
|
171
|
+
modes.clear()
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Match a Vite file event against exact or extensionless local candidates. */
|
|
178
|
+
export function entryCandidate(candidates, file) {
|
|
179
|
+
const path = file.replace(/\\/g, '/')
|
|
180
|
+
const stem = path.replace(/\.[cm]?[jt]sx?$/, '')
|
|
181
|
+
return (
|
|
182
|
+
candidates?.has(path) ||
|
|
183
|
+
candidates?.has(stem) ||
|
|
184
|
+
(stem.endsWith('/index') && candidates?.has(stem.slice(0, -'/index'.length))) ||
|
|
185
|
+
false
|
|
186
|
+
)
|
|
187
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { readFileSync, realpathSync } from 'node:fs'
|
|
2
|
+
import { isAbsolute, relative } from 'node:path'
|
|
3
|
+
import { parse } from './syntax.mjs'
|
|
4
|
+
|
|
5
|
+
const TAGS = new Set([
|
|
6
|
+
'div',
|
|
7
|
+
'span',
|
|
8
|
+
'main',
|
|
9
|
+
'section',
|
|
10
|
+
'article',
|
|
11
|
+
'header',
|
|
12
|
+
'footer',
|
|
13
|
+
'p',
|
|
14
|
+
'h1',
|
|
15
|
+
'h2',
|
|
16
|
+
'h3',
|
|
17
|
+
'h4',
|
|
18
|
+
'h5',
|
|
19
|
+
'h6',
|
|
20
|
+
'ul',
|
|
21
|
+
'ol',
|
|
22
|
+
'li',
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
function inert(node) {
|
|
26
|
+
if (node.type === 'JSXText') return true
|
|
27
|
+
if (node.type === 'JSXFragment') return node.children.every(inert)
|
|
28
|
+
if (node.type !== 'JSXElement') return false
|
|
29
|
+
const open = node.openingElement
|
|
30
|
+
if (open.name.type !== 'JSXIdentifier' || !TAGS.has(open.name.name)) return false
|
|
31
|
+
if (
|
|
32
|
+
!open.attributes.every(
|
|
33
|
+
(attr) =>
|
|
34
|
+
attr.type === 'JSXAttribute' &&
|
|
35
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
36
|
+
(attr.name.name === 'class' ||
|
|
37
|
+
attr.name.name === 'className' ||
|
|
38
|
+
/^data-[a-z0-9-]+$/.test(attr.name.name)) &&
|
|
39
|
+
attr.value?.type === 'Literal' &&
|
|
40
|
+
typeof attr.value.value === 'string',
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
return false
|
|
44
|
+
return node.children.every(inert)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// A function declaration has a mutable binding. Reject every other occurrence,
|
|
48
|
+
// including shadowed names, rather than guessing whether a write is reachable.
|
|
49
|
+
function isolated(body, declaration, reference) {
|
|
50
|
+
const name = declaration.id?.name
|
|
51
|
+
if (!name) return true
|
|
52
|
+
const allowed = new Set([declaration.id, reference])
|
|
53
|
+
const safe = (node) => {
|
|
54
|
+
if (!node || typeof node !== 'object') return true
|
|
55
|
+
if (Array.isArray(node)) return node.every(safe)
|
|
56
|
+
if (node.type === 'Identifier' && node.name === 'eval') return false
|
|
57
|
+
if (node.type === 'Identifier' && node.name === name && !allowed.has(node)) return false
|
|
58
|
+
return Object.values(node).every(safe)
|
|
59
|
+
}
|
|
60
|
+
return safe(body)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Extract only a closed, static local override. Never evaluate the source module. */
|
|
64
|
+
export function shapeOverride(file) {
|
|
65
|
+
const source = readFileSync(file, 'utf8')
|
|
66
|
+
const parsed = parse(file, source)
|
|
67
|
+
if (parsed.errors.length) return
|
|
68
|
+
const body = parsed.program.body
|
|
69
|
+
const exports = body.filter(
|
|
70
|
+
(n) =>
|
|
71
|
+
n.type === 'ExportNamedDeclaration' &&
|
|
72
|
+
n.declaration?.type === 'VariableDeclaration' &&
|
|
73
|
+
n.declaration.kind === 'const',
|
|
74
|
+
)
|
|
75
|
+
const declarations = exports
|
|
76
|
+
.flatMap((n) => n.declaration.declarations)
|
|
77
|
+
.filter((d) => d.id.type === 'Identifier' && d.id.name === 'skeleton')
|
|
78
|
+
if (declarations.length !== 1 || declarations[0].init?.type !== 'Identifier') return
|
|
79
|
+
if (
|
|
80
|
+
body.some(
|
|
81
|
+
(n) =>
|
|
82
|
+
n.type === 'ExportAllDeclaration' ||
|
|
83
|
+
(n.type === 'ExportNamedDeclaration' &&
|
|
84
|
+
n.specifiers.some((s) => (s.exported.name ?? s.exported.value) === 'skeleton')),
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
return
|
|
88
|
+
const name = declarations[0].init.name
|
|
89
|
+
const candidates = body.flatMap((n) => {
|
|
90
|
+
const decl =
|
|
91
|
+
n.type === 'ExportNamedDeclaration' || n.type === 'ExportDefaultDeclaration'
|
|
92
|
+
? n.declaration
|
|
93
|
+
: n
|
|
94
|
+
if (decl?.type === 'FunctionDeclaration' && decl.id?.name === name) return [decl]
|
|
95
|
+
if (decl?.type === 'VariableDeclaration' && decl.kind === 'const')
|
|
96
|
+
return decl.declarations.filter((d) => d.id.type === 'Identifier' && d.id.name === name)
|
|
97
|
+
return []
|
|
98
|
+
})
|
|
99
|
+
if (candidates.length !== 1) return
|
|
100
|
+
const candidate = candidates[0]
|
|
101
|
+
if (candidate.type === 'FunctionDeclaration') {
|
|
102
|
+
if (!isolated(body, candidate, declarations[0].init)) return
|
|
103
|
+
return component(source, candidate)
|
|
104
|
+
}
|
|
105
|
+
return component(source, candidate.init)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function component(source, fn) {
|
|
109
|
+
while (fn?.type === 'ParenthesizedExpression') fn = fn.expression
|
|
110
|
+
if (
|
|
111
|
+
!fn ||
|
|
112
|
+
!['ArrowFunctionExpression', 'FunctionExpression', 'FunctionDeclaration'].includes(fn.type) ||
|
|
113
|
+
fn.async ||
|
|
114
|
+
fn.generator ||
|
|
115
|
+
fn.params.length
|
|
116
|
+
)
|
|
117
|
+
return
|
|
118
|
+
let body =
|
|
119
|
+
fn.body.type === 'BlockStatement' &&
|
|
120
|
+
fn.body.body.length === 1 &&
|
|
121
|
+
fn.body.body[0].type === 'ReturnStatement'
|
|
122
|
+
? fn.body.body[0].argument
|
|
123
|
+
: fn.body
|
|
124
|
+
// `return (` and a multi-line tree, as a formatter writes it.
|
|
125
|
+
while (body?.type === 'ParenthesizedExpression') body = body.expression
|
|
126
|
+
if (!body || !inert(body)) return
|
|
127
|
+
const jsx = source.slice(body.start, body.end)
|
|
128
|
+
return `export default function Skeleton() { return (${jsx}); }`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Follow one direct import only; imported source must itself export the component.
|
|
132
|
+
export function importedOverride(file) {
|
|
133
|
+
const parsed = parse(file, readFileSync(file, 'utf8'))
|
|
134
|
+
if (parsed.errors.length) return
|
|
135
|
+
const body = parsed.program.body
|
|
136
|
+
const declarations = body
|
|
137
|
+
.flatMap((n) =>
|
|
138
|
+
n.type === 'ExportNamedDeclaration' &&
|
|
139
|
+
n.declaration?.type === 'VariableDeclaration' &&
|
|
140
|
+
n.declaration.kind === 'const'
|
|
141
|
+
? n.declaration.declarations
|
|
142
|
+
: [],
|
|
143
|
+
)
|
|
144
|
+
.filter((d) => d.id.name === 'skeleton')
|
|
145
|
+
if (declarations.length !== 1 || declarations[0].init?.type !== 'Identifier') return
|
|
146
|
+
if (
|
|
147
|
+
body.some(
|
|
148
|
+
(n) =>
|
|
149
|
+
n.type === 'ExportAllDeclaration' ||
|
|
150
|
+
(n.type === 'ExportNamedDeclaration' &&
|
|
151
|
+
n.specifiers.some((s) => (s.exported.name ?? s.exported.value) === 'skeleton')),
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
return
|
|
155
|
+
const name = declarations[0].init.name
|
|
156
|
+
const matches = body.flatMap((n) =>
|
|
157
|
+
n.type === 'ImportDeclaration' && n.importKind !== 'type'
|
|
158
|
+
? n.specifiers
|
|
159
|
+
.filter((s) => s.local.name === name && s.importKind !== 'type')
|
|
160
|
+
.map((s) => ({ spec: n.source.value, binding: s }))
|
|
161
|
+
: [],
|
|
162
|
+
)
|
|
163
|
+
if (matches.length !== 1) return
|
|
164
|
+
const { spec, binding } = matches[0]
|
|
165
|
+
if (binding.type === 'ImportDefaultSpecifier') return { spec, name: 'default' }
|
|
166
|
+
if (binding.type === 'ImportSpecifier') return { spec, name: binding.imported.name }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function dedicated(file, name) {
|
|
170
|
+
const source = readFileSync(file, 'utf8')
|
|
171
|
+
const parsed = parse(file, source)
|
|
172
|
+
if (parsed.errors.length) return
|
|
173
|
+
const matches = []
|
|
174
|
+
for (const node of parsed.program.body) {
|
|
175
|
+
if (
|
|
176
|
+
node.type === 'ExportAllDeclaration' ||
|
|
177
|
+
(node.type === 'ExportNamedDeclaration' && node.source)
|
|
178
|
+
)
|
|
179
|
+
return
|
|
180
|
+
if (name === 'default' && node.type === 'ExportDefaultDeclaration')
|
|
181
|
+
matches.push(node.declaration)
|
|
182
|
+
if (
|
|
183
|
+
node.type === 'ExportNamedDeclaration' &&
|
|
184
|
+
node.declaration?.type === 'FunctionDeclaration' &&
|
|
185
|
+
node.declaration.id?.name === name
|
|
186
|
+
)
|
|
187
|
+
matches.push(node.declaration)
|
|
188
|
+
if (
|
|
189
|
+
node.type === 'ExportNamedDeclaration' &&
|
|
190
|
+
node.declaration?.type === 'VariableDeclaration' &&
|
|
191
|
+
node.declaration.kind === 'const'
|
|
192
|
+
) {
|
|
193
|
+
for (const item of node.declaration.declarations)
|
|
194
|
+
if (item.id.type === 'Identifier' && item.id.name === name) matches.push(item.init)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (matches.length !== 1) return
|
|
198
|
+
if (matches[0].type === 'FunctionDeclaration' && !isolated(parsed.program.body, matches[0]))
|
|
199
|
+
return
|
|
200
|
+
return component(source, matches[0])
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Resolve a direct dedicated source through Vite, bounded to non-server project files. */
|
|
204
|
+
export async function resolveImportedOverride(ctx, file, root, onMissing) {
|
|
205
|
+
const requested = importedOverride(file)
|
|
206
|
+
if (!requested || /[?#]/.test(requested.spec) || /^(?:[a-z][\w+.-]*:|\/\/)/i.test(requested.spec))
|
|
207
|
+
return
|
|
208
|
+
const found = await ctx.resolve(requested.spec, file, { skipSelf: true })
|
|
209
|
+
if (!found && /^\.{1,2}\//.test(requested.spec)) onMissing?.(requested.spec)
|
|
210
|
+
if (!found || found.external || !isAbsolute(found.id) || !/\.[jt]sx$/.test(found.id)) return
|
|
211
|
+
const blocked = (base, target) => {
|
|
212
|
+
const rel = relative(base, target).replace(/\\/g, '/')
|
|
213
|
+
return (
|
|
214
|
+
rel === '..' ||
|
|
215
|
+
rel.startsWith('../') ||
|
|
216
|
+
isAbsolute(rel) ||
|
|
217
|
+
/(^|\/)node_modules\//.test(rel) ||
|
|
218
|
+
/(^|\/)server\//.test(rel) ||
|
|
219
|
+
/\.server\.[jt]sx$/.test(rel)
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
if (blocked(root, found.id) || blocked(realpathSync(root), realpathSync(found.id))) return
|
|
224
|
+
return { file: found.id, code: dedicated(found.id, requested.name) }
|
|
225
|
+
} catch {}
|
|
226
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { LATE_SHEETS, REFUSED_SHEETS } from '../inline.mjs'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
/** This folder, wherever the package is installed. */
|
|
7
|
+
export const here = dirname(fileURLToPath(new URL('../vite.mjs', import.meta.url)))
|
|
8
|
+
export const V = {
|
|
9
|
+
routes: 'virtual:weft/routes',
|
|
10
|
+
app: 'virtual:weft/app',
|
|
11
|
+
server: 'virtual:weft/entry-server',
|
|
12
|
+
client: 'virtual:weft/entry-client',
|
|
13
|
+
fns: 'virtual:weft/fns',
|
|
14
|
+
context: 'virtual:weft/context',
|
|
15
|
+
auth: 'virtual:weft/auth',
|
|
16
|
+
migrations: 'virtual:weft/migrations',
|
|
17
|
+
media: 'virtual:weft/media',
|
|
18
|
+
realtime: 'virtual:weft/realtime',
|
|
19
|
+
extensions: 'virtual:weft/extensions',
|
|
20
|
+
}
|
|
21
|
+
export const DEFAULT_FONTS = ['Inter:400,700']
|
|
22
|
+
export const SHEET = '/@weft-sheet/'
|
|
23
|
+
export const ASSETS = '?weft-assets'
|
|
24
|
+
export const ISLAND = 'virtual:weft/island/'
|
|
25
|
+
export const PRELOAD =
|
|
26
|
+
'Per page, from the HTML: an island that loads with the page, or a lazy one in about the first 900px, is modulepreloaded (fetched, not run). The rest load when needed. Save-Data: lazy ones are not preloaded, and wait for interaction.'
|
|
27
|
+
export const NOT_ASSET = /\.[cm]?[jt]sx?$/
|
|
28
|
+
export const INLINE_SHEETS = '__WEFT_INLINE_SHEETS__'
|
|
29
|
+
export const ROUTE_ASSET_BYTES = '__WEFT_ROUTE_ASSET_BYTES__'
|
|
30
|
+
// A page's critical CSS with `criticalCss: false` uses the build's own reachability rules.
|
|
31
|
+
export const purgeFile = createRequire(join(here, 'vite.mjs'))
|
|
32
|
+
.resolve('@weft-js/core/node-purge')
|
|
33
|
+
.split('\\')
|
|
34
|
+
.join('/')
|
|
35
|
+
export const SHEET_PIECES = '__WEFT_SHEET_PIECES__'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* First in any entry, an app's own too. The page's inline script is the fast path and a CSP can refuse it, so a page
|
|
39
|
+
* that runs at all never stays on its first-paint rules.
|
|
40
|
+
*/
|
|
41
|
+
export const ENTRY_SHEETS = `${LATE_SHEETS}\n${REFUSED_SHEETS}\n`
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* weft's own packages from an install, as one chunk of icons and one of what two routes share. Left alone they're a chunk
|
|
45
|
+
* per component: 31 scripts and 18 sheets for an admin page (Solid's: 16 in all), each its own request and its own
|
|
46
|
+
* compression, and 53 KB against 46. An app's own code splits as it likes.
|
|
47
|
+
*/
|
|
48
|
+
export const KIT_CHUNKS = [
|
|
49
|
+
{ name: 'icons', test: /[\\/]node_modules[\\/]@weft-js[\\/]warp-icons[\\/]/, priority: 2 },
|
|
50
|
+
{ name: 'kit', test: /[\\/]node_modules[\\/]@weft-js[\\/]/, minShareCount: 2, priority: 1 },
|
|
51
|
+
]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
statSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs'
|
|
10
|
+
import { join } from 'node:path'
|
|
11
|
+
import { pieces } from '@weft-js/core/node-purge'
|
|
12
|
+
import { planSheets, reach } from '../sheets.mjs'
|
|
13
|
+
import { INLINE_CSS_MAX } from '../inline.mjs'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The client build's stylesheets a page can carry, `{ href: css }`, read by the server build so every preset has them
|
|
17
|
+
* with no file system. Left out: one a relative `url()` would break, and one that could close its `<style>`.
|
|
18
|
+
*/
|
|
19
|
+
export function inlineSheets(outDir, base, limit = INLINE_CSS_MAX) {
|
|
20
|
+
const dir = join(outDir, 'assets')
|
|
21
|
+
if (!existsSync(dir)) return {}
|
|
22
|
+
const sheets = {}
|
|
23
|
+
for (const name of readdirSync(dir).filter((f) => f.endsWith('.css'))) {
|
|
24
|
+
const css = readFileSync(join(dir, name), 'utf8')
|
|
25
|
+
if (
|
|
26
|
+
css.length <= limit &&
|
|
27
|
+
!/<\/style|<!--/i.test(css) &&
|
|
28
|
+
!/url\(\s*(?!["']?(?:\/|data:|https?:|#|%23))/.test(css)
|
|
29
|
+
)
|
|
30
|
+
sheets[`${base}assets/${name}`] = css
|
|
31
|
+
}
|
|
32
|
+
return sheets
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** How long a replaced sheet stays in the build, for pages cached before the deploy that replaced it: a CDN's longest day. */
|
|
36
|
+
const KEEP_SHEETS_MS = 30 * 864e5
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Writes the build's combined stylesheets beside the chunks' own. A route's list is what its chunks import, the entry's
|
|
40
|
+
* included, plus the sheets of the islands in the files it imports.
|
|
41
|
+
*/
|
|
42
|
+
export function combineSheets(dir, bundle, manifest, abs, islands) {
|
|
43
|
+
const chunks = Object.values(bundle).filter((f) => f.type === 'chunk')
|
|
44
|
+
const holding = (id) => chunks.filter((c) => c.moduleIds.some((m) => m.split('?')[0] === id))
|
|
45
|
+
const sheetsOf = (start, seen = new Set()) =>
|
|
46
|
+
start.flatMap((c) => {
|
|
47
|
+
if (seen.has(c)) return []
|
|
48
|
+
seen.add(c)
|
|
49
|
+
return [
|
|
50
|
+
...(c.viteMetadata?.importedCss ?? []),
|
|
51
|
+
...sheetsOf(c.imports.map((i) => bundle[i]).filter(Boolean), seen),
|
|
52
|
+
]
|
|
53
|
+
})
|
|
54
|
+
const entry = sheetsOf(chunks.filter((c) => c.isEntry))
|
|
55
|
+
const routes = manifest.routes
|
|
56
|
+
.map((r) => {
|
|
57
|
+
const levels = [
|
|
58
|
+
...manifest.layouts.filter((l) => r.layouts.includes(l.id)).map((l) => abs(l.file)),
|
|
59
|
+
abs(r.file),
|
|
60
|
+
]
|
|
61
|
+
const files = reach(levels)
|
|
62
|
+
const held = islands.filter((i) => files.has(i.file)).map((i) => i.file)
|
|
63
|
+
return [...new Set([...entry, ...sheetsOf([...levels, ...held].flatMap(holding))])]
|
|
64
|
+
})
|
|
65
|
+
.filter((list) => list.length)
|
|
66
|
+
const plan = planSheets(routes, (name) => readFileSync(join(dir, name), 'utf8'))
|
|
67
|
+
for (const [name, css] of Object.entries(plan.files)) writeFileSync(join(dir, name), css)
|
|
68
|
+
return plan
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The first-paint table of the build's sheets (node/purge.mjs), and the last builds' combined sheets kept beside them: a
|
|
73
|
+
* page cached across a deploy links the ones it was built with. `.weft/sheets` carries them, so CI has to keep that folder.
|
|
74
|
+
*/
|
|
75
|
+
export function sheetPieces(outDir, base, plan, keep, root) {
|
|
76
|
+
const dir = join(outDir, 'assets')
|
|
77
|
+
const href = (name) => `${base}${name}`
|
|
78
|
+
const names = [...new Set([plan.shared, ...plan.routes].filter(Boolean).flatMap((s) => s.has))]
|
|
79
|
+
const sheets = names
|
|
80
|
+
.map((n) => [href(n), readFileSync(join(outDir, n), 'utf8')])
|
|
81
|
+
.filter(([, css]) => !/<\/style|<!--/i.test(css))
|
|
82
|
+
const old = join(root, '.weft/sheets')
|
|
83
|
+
mkdirSync(old, { recursive: true })
|
|
84
|
+
for (const f of Object.keys(plan.files))
|
|
85
|
+
writeFileSync(join(old, f.replace('assets/', '')), plan.files[f])
|
|
86
|
+
for (const f of readdirSync(old)) {
|
|
87
|
+
if (Date.now() - statSync(join(old, f)).mtimeMs > KEEP_SHEETS_MS) rmSync(join(old, f))
|
|
88
|
+
else if (!existsSync(join(dir, f))) writeFileSync(join(dir, f), readFileSync(join(old, f)))
|
|
89
|
+
}
|
|
90
|
+
const shape = (s) => s && { href: href(s.file), has: s.has.map(href) }
|
|
91
|
+
return { ...pieces(sheets, keep), shared: shape(plan.shared), routes: plan.routes.map(shape) }
|
|
92
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join, relative, resolve } from 'node:path'
|
|
3
|
+
import { solve } from '@weft-js/core/hydration'
|
|
4
|
+
import { scanRoutes } from '../routes.mjs'
|
|
5
|
+
import { scanServerModules } from './server-functions.mjs'
|
|
6
|
+
import { usesShapes } from './dev.mjs'
|
|
7
|
+
import { PRELOAD, V } from './shared.mjs'
|
|
8
|
+
|
|
9
|
+
/** Partial hydration's view of the project, redone in dev when a file it read changed. */
|
|
10
|
+
async function hydration(s, ctx) {
|
|
11
|
+
const { options } = s
|
|
12
|
+
if (options.hydration === 'full' || !s.compile) return undefined
|
|
13
|
+
if (s.plan && !s.dev) return s.plan
|
|
14
|
+
if (s.plan && Date.now() - s.planned < 200) return s.plan
|
|
15
|
+
s.planned = Date.now()
|
|
16
|
+
if (s.plan) {
|
|
17
|
+
const p = await s.plan
|
|
18
|
+
if (
|
|
19
|
+
!p.stamps.some(
|
|
20
|
+
(f) => statSync(f, { throwIfNoEntry: false })?.mtimeMs !== s.facts.get(f)?.mtime,
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
return p
|
|
24
|
+
}
|
|
25
|
+
const entries = [...s.manifest.routes, ...s.manifest.layouts].map((r) => s.abs(r.file))
|
|
26
|
+
s.plan = solve({
|
|
27
|
+
entries,
|
|
28
|
+
lazy: (options.hydration ?? 'lazy') === 'lazy',
|
|
29
|
+
transform: s.compile,
|
|
30
|
+
rel: (f) => relative(s.root, f).split('\\').join('/'),
|
|
31
|
+
cache: s.facts,
|
|
32
|
+
resolve: async (spec, from) => (await ctx.resolve(spec, from, { skipSelf: true }))?.id,
|
|
33
|
+
})
|
|
34
|
+
const done = await s.plan
|
|
35
|
+
try {
|
|
36
|
+
mkdirSync(join(s.root, '.weft'), { recursive: true })
|
|
37
|
+
writeFileSync(
|
|
38
|
+
join(s.root, '.weft', 'hydration.json'),
|
|
39
|
+
`${JSON.stringify({ islands: done.islands.map((i) => i.id), loads: done.loads, preload: PRELOAD, diagnostics: done.diagnostics, files: done.report }, null, 2)}\n`,
|
|
40
|
+
)
|
|
41
|
+
} catch {}
|
|
42
|
+
return done
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Read the query shape table once per edit. */
|
|
46
|
+
async function shapeTable(s, ctx) {
|
|
47
|
+
if (!usesShapes(s.root)) return undefined
|
|
48
|
+
const stamp = s.serverModules.map((f) => statSync(s.abs(f)).mtimeMs).join()
|
|
49
|
+
if (s.shapes?.stamp === stamp) return s.shapes.table
|
|
50
|
+
try {
|
|
51
|
+
const { readShapes } = await import('../contract.mjs')
|
|
52
|
+
s.shapes = { stamp, table: await readShapes({ root: s.root, site: s.options.site }) }
|
|
53
|
+
return s.shapes.table
|
|
54
|
+
} catch (e) {
|
|
55
|
+
ctx.warn(
|
|
56
|
+
`the query shapes couldn't be read, so skeletons only have last-seen shapes: ${e.message}`,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function createState(options) {
|
|
62
|
+
const s = {
|
|
63
|
+
options,
|
|
64
|
+
root: options.root ?? process.cwd(),
|
|
65
|
+
dev: false,
|
|
66
|
+
build: '',
|
|
67
|
+
serverModules: [],
|
|
68
|
+
loaders: new Map(),
|
|
69
|
+
stubs: new Map(),
|
|
70
|
+
planned: 0,
|
|
71
|
+
facts: new Map(),
|
|
72
|
+
}
|
|
73
|
+
s.abs = (f) => resolve(s.root, f)
|
|
74
|
+
s.id = (v) => (v.startsWith('virtual:') ? '\0' + v : v)
|
|
75
|
+
s.scan = () => {
|
|
76
|
+
s.serverModules = scanServerModules(s.root)
|
|
77
|
+
return (s.manifest = scanRoutes(
|
|
78
|
+
s.root,
|
|
79
|
+
options.routesDir ? resolve(s.root, options.routesDir) : undefined,
|
|
80
|
+
))
|
|
81
|
+
}
|
|
82
|
+
s.hydration = (ctx) => hydration(s, ctx)
|
|
83
|
+
s.shapeTable = (ctx) => shapeTable(s, ctx)
|
|
84
|
+
s.clientEntry = () =>
|
|
85
|
+
existsSync(join(s.root, 'src', 'entry.client.ts'))
|
|
86
|
+
? join(s.root, 'src', 'entry.client.ts')
|
|
87
|
+
: V.client
|
|
88
|
+
return s
|
|
89
|
+
}
|