@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.
Files changed (213) hide show
  1. package/README.md +36 -3
  2. package/crate/Cargo.lock +1347 -0
  3. package/crate/Cargo.toml +72 -0
  4. package/crate/rust-toolchain.toml +3 -0
  5. package/crate/src/build/emit.rs +103 -0
  6. package/crate/src/build.rs +367 -0
  7. package/crate/src/ctx.rs +186 -0
  8. package/crate/src/envelope.rs +163 -0
  9. package/crate/src/functions.rs +173 -0
  10. package/crate/src/lib.rs +118 -0
  11. package/crate/src/native.rs +212 -0
  12. package/crate/src/otel.rs +62 -0
  13. package/crate/src/server.rs +203 -0
  14. package/package.json +46 -37
  15. package/src/app.mjs +51 -10
  16. package/src/asset-receipt.d.mts +13 -0
  17. package/src/asset-receipt.mjs +61 -0
  18. package/src/auth/better-auth.ts +215 -17
  19. package/src/auth/drizzle.ts +1 -1
  20. package/src/auth.ts +21 -6
  21. package/src/boot.ts +19 -8
  22. package/src/bus/postgres.ts +32 -19
  23. package/src/bus.ts +7 -4
  24. package/src/{capture.mjs → capture.ts} +34 -1
  25. package/src/changes-sql.d.mts +8 -2
  26. package/src/changes-sql.mjs +26 -10
  27. package/src/changes.ts +165 -66
  28. package/src/channel/connections.ts +216 -0
  29. package/src/channel/socket.ts +396 -0
  30. package/src/channel-fake.ts +19 -4
  31. package/src/channel.ts +124 -490
  32. package/src/client.tsx +9 -2
  33. package/src/contract/describe.mjs +231 -0
  34. package/src/contract.mjs +146 -155
  35. package/src/data/cache.ts +354 -0
  36. package/src/data/definitions.ts +94 -0
  37. package/src/data/draft.ts +104 -0
  38. package/src/data/foundation.ts +95 -0
  39. package/src/data/live-api.ts +22 -0
  40. package/src/data/mutation.ts +138 -0
  41. package/src/data/primitives.ts +135 -0
  42. package/src/data/query.ts +106 -0
  43. package/src/data/transport.ts +121 -0
  44. package/src/data/types.ts +303 -0
  45. package/src/data.ts +69 -894
  46. package/src/db/bun-sqlite.d.ts +4 -1
  47. package/src/db/changes-mysql.ts +61 -21
  48. package/src/db/changes-postgres.ts +197 -118
  49. package/src/db/changes-sqlite.ts +131 -64
  50. package/src/db/core.ts +38 -13
  51. package/src/db/index.ts +20 -4
  52. package/src/db/mysql.ts +22 -6
  53. package/src/db/postgres.ts +49 -13
  54. package/src/db/sqlite.ts +53 -27
  55. package/src/derive.d.mts +4 -1
  56. package/src/derive.mjs +136 -39
  57. package/src/dev-runtime.mjs +8 -4
  58. package/src/exec/errors.ts +57 -0
  59. package/src/exec/landing.ts +66 -0
  60. package/src/exec/scope.ts +174 -0
  61. package/src/exec/touch.ts +80 -0
  62. package/src/exec/validation.ts +176 -0
  63. package/src/exec.ts +191 -341
  64. package/src/extension.ts +8 -2
  65. package/src/first-paint.ts +26 -9
  66. package/src/form/value.ts +15 -0
  67. package/src/form.tsx +132 -43
  68. package/src/hub/channels.ts +102 -0
  69. package/src/hub/connections.ts +147 -0
  70. package/src/hub/delivery.ts +62 -0
  71. package/src/hub/feed.ts +168 -0
  72. package/src/hub/presence.ts +64 -0
  73. package/src/hub/route.ts +158 -0
  74. package/src/hub/subscriptions.ts +212 -0
  75. package/src/hub/topic.ts +98 -0
  76. package/src/hub/tracked.ts +12 -0
  77. package/src/hub/types.ts +190 -0
  78. package/src/hub/view.ts +147 -0
  79. package/src/hub.ts +132 -758
  80. package/src/index.ts +84 -10
  81. package/src/inline.mjs +356 -0
  82. package/src/inspect/compare.mjs +162 -0
  83. package/src/inspect/routes.mjs +154 -0
  84. package/src/inspect/samples.mjs +59 -0
  85. package/src/inspect/url.mjs +2 -0
  86. package/src/inspect.mjs +146 -187
  87. package/src/link/election.ts +178 -0
  88. package/src/link/lines.ts +27 -0
  89. package/src/link.ts +97 -168
  90. package/src/list-drizzle.ts +104 -18
  91. package/src/list.ts +127 -36
  92. package/src/listen.ts +32 -6
  93. package/src/live/api.ts +18 -0
  94. package/src/live/backoff.ts +6 -0
  95. package/src/live/frames.ts +42 -0
  96. package/src/live/read.ts +77 -0
  97. package/src/live/sub-types.ts +24 -0
  98. package/src/live/transport.ts +86 -0
  99. package/src/live/types.ts +52 -0
  100. package/src/live-fake.ts +20 -5
  101. package/src/live-target.mjs +113 -35
  102. package/src/live.ts +116 -225
  103. package/src/media.ts +8 -1
  104. package/src/migrations.mjs +14 -6
  105. package/src/mux.ts +161 -64
  106. package/src/og/DefaultCard.tsx +40 -9
  107. package/src/og/fonts.mjs +16 -8
  108. package/src/og/images.mjs +43 -22
  109. package/src/og/index.mjs +119 -43
  110. package/src/og/native.mjs +11 -4
  111. package/src/range.mjs +15 -3
  112. package/src/router.ts +56 -6
  113. package/src/routes.mjs +73 -22
  114. package/src/run/admission.ts +80 -0
  115. package/src/run/boot.tsx +115 -0
  116. package/src/run/cache-presentation.ts +67 -0
  117. package/src/run/cache.ts +254 -0
  118. package/src/run/helpers.ts +197 -0
  119. package/src/run/listeners.ts +155 -0
  120. package/src/run/navigation.ts +382 -0
  121. package/src/run/page-skeleton.tsx +35 -0
  122. package/src/run/prefetch.ts +128 -0
  123. package/src/run/retain-view.ts +18 -0
  124. package/src/run/shape-preparation.ts +97 -0
  125. package/src/run/shape-styles.ts +72 -0
  126. package/src/run/source-context/pin.ts +124 -0
  127. package/src/run/source-context.ts +311 -0
  128. package/src/run.tsx +50 -174
  129. package/src/rust/common.ts +163 -0
  130. package/src/rust/native.ts +282 -0
  131. package/src/rust.ts +103 -229
  132. package/src/server/card.tsx +65 -0
  133. package/src/server/compression.ts +115 -0
  134. package/src/server/core.ts +70 -0
  135. package/src/server/dispatch.ts +70 -0
  136. package/src/server/form.ts +85 -0
  137. package/src/server/functions.ts +129 -0
  138. package/src/server/handler.ts +39 -0
  139. package/src/server/head.ts +96 -0
  140. package/src/server/media.ts +60 -0
  141. package/src/server/navigation-cache.ts +80 -0
  142. package/src/server/navigation.tsx +85 -0
  143. package/src/server/page-cold.ts +95 -0
  144. package/src/server/page-stream.tsx +181 -0
  145. package/src/server/page-types.ts +51 -0
  146. package/src/server/page-warm.ts +130 -0
  147. package/src/server/page.ts +69 -0
  148. package/src/server/payload.ts +89 -0
  149. package/src/server/prerender.ts +60 -0
  150. package/src/server/preview.tsx +67 -0
  151. package/src/server/render.ts +167 -0
  152. package/src/server/request.ts +98 -0
  153. package/src/server/resolver.ts +228 -0
  154. package/src/server/routes.ts +150 -0
  155. package/src/server/services.ts +69 -0
  156. package/src/server/text.ts +29 -0
  157. package/src/server/types.ts +160 -0
  158. package/src/server.tsx +82 -964
  159. package/src/shape.mjs +30 -6
  160. package/src/share/display.ts +50 -0
  161. package/src/share/network.ts +119 -0
  162. package/src/share/receive.ts +70 -0
  163. package/src/share/state.ts +30 -0
  164. package/src/share/tab-queue.ts +28 -0
  165. package/src/share-server.ts +128 -43
  166. package/src/share.ts +103 -209
  167. package/src/sheets.mjs +30 -18
  168. package/src/sidecar/build.mjs +170 -0
  169. package/src/sidecar/cargo.mjs +164 -0
  170. package/src/sidecar/common.mjs +17 -0
  171. package/src/sidecar/dev-build.mjs +91 -0
  172. package/src/sidecar/dev-watch.mjs +88 -0
  173. package/src/sidecar/manifest.mjs +19 -0
  174. package/src/sidecar/process.mjs +58 -0
  175. package/src/sidecar/refresh.mjs +19 -0
  176. package/src/sidecar.mjs +75 -416
  177. package/src/socket/check.ts +50 -0
  178. package/src/socket/deliver.ts +16 -0
  179. package/src/socket/publish.ts +56 -0
  180. package/src/socket/rooms.ts +41 -0
  181. package/src/socket/route.ts +79 -0
  182. package/src/socket/shared.ts +26 -0
  183. package/src/socket/types.ts +48 -0
  184. package/src/socket.ts +205 -194
  185. package/src/stream.ts +10 -1
  186. package/src/verify/app.ts +39 -0
  187. package/src/verify/cleanup.ts +27 -0
  188. package/src/verify/database.ts +11 -0
  189. package/src/verify/live.ts +63 -0
  190. package/src/verify/notify.ts +35 -0
  191. package/src/verify/skip.ts +19 -0
  192. package/src/verify/support.ts +149 -0
  193. package/src/verify/timing.ts +15 -0
  194. package/src/verify/trigger.ts +14 -0
  195. package/src/verify.ts +77 -176
  196. package/src/view.tsx +96 -11
  197. package/src/vite/asset-bytes.mjs +95 -0
  198. package/src/vite/assets.mjs +117 -0
  199. package/src/vite/config-hooks.mjs +166 -0
  200. package/src/vite/dev-hooks.mjs +103 -0
  201. package/src/vite/dev.mjs +102 -0
  202. package/src/vite/entries.mjs +145 -0
  203. package/src/vite/module-hooks.mjs +288 -0
  204. package/src/vite/route-modules.mjs +289 -0
  205. package/src/vite/server-functions.mjs +222 -0
  206. package/src/vite/shape-entry-css.mjs +187 -0
  207. package/src/vite/shape-override.mjs +226 -0
  208. package/src/vite/shared.mjs +51 -0
  209. package/src/vite/sheets.mjs +92 -0
  210. package/src/vite/state.mjs +89 -0
  211. package/src/vite/syntax.mjs +139 -0
  212. package/src/vite.mjs +152 -853
  213. 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
+ }