@weft-js/cli 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.
Files changed (171) hide show
  1. package/README.md +534 -54
  2. package/bin/weft.mjs +8 -1
  3. package/docs/async.md +16 -6
  4. package/docs/browser.md +22 -8
  5. package/docs/config.md +39 -9
  6. package/docs/core.md +30 -0
  7. package/docs/deploy.md +33 -8
  8. package/docs/design.md +12 -3
  9. package/docs/entrypoints.md +79 -0
  10. package/docs/fonts.md +12 -2
  11. package/docs/icons.md +13 -7
  12. package/docs/lists.md +3 -1
  13. package/docs/meta.md +13 -3
  14. package/docs/plugins.md +32 -9
  15. package/docs/props.md +13 -2
  16. package/docs/reactivity.md +26 -0
  17. package/docs/rules.md +19 -12
  18. package/docs/rust.md +47 -17
  19. package/docs/start-short.md +3 -2
  20. package/docs/start.md +226 -52
  21. package/docs/state.md +33 -7
  22. package/docs/style.md +31 -8
  23. package/docs/surface.md +61 -15
  24. package/docs/testing.md +15 -5
  25. package/docs/theme.md +182 -0
  26. package/docs/ui-state.md +19 -6
  27. package/docs/verify.md +13 -0
  28. package/package.json +13 -4
  29. package/src/add/config.mjs +93 -0
  30. package/src/add/errors.mjs +2 -0
  31. package/src/add/setup.mjs +88 -0
  32. package/src/add-rust.mjs +82 -27
  33. package/src/add.mjs +209 -181
  34. package/src/agents-md.mjs +144 -43
  35. package/src/alias.mjs +16 -5
  36. package/src/audit/page.mjs +313 -0
  37. package/src/audit/report.mjs +69 -0
  38. package/src/audit/utils.mjs +5 -0
  39. package/src/audit.mjs +159 -206
  40. package/src/browser/cli.mjs +24 -5
  41. package/src/browser/client.mjs +189 -25
  42. package/src/browser/commands.mjs +166 -51
  43. package/src/catalog/audit.mjs +129 -0
  44. package/src/catalog/auth.mjs +40 -0
  45. package/src/catalog/compiler.mjs +337 -0
  46. package/src/catalog/contracts.mjs +18 -0
  47. package/src/catalog/database.mjs +52 -0
  48. package/src/catalog/deploy.mjs +117 -0
  49. package/src/catalog/design.mjs +159 -0
  50. package/src/catalog/forms.mjs +25 -0
  51. package/src/catalog/hydration.mjs +29 -0
  52. package/src/catalog/icons.mjs +24 -0
  53. package/src/catalog/media.mjs +163 -0
  54. package/src/catalog/metadata.mjs +116 -0
  55. package/src/catalog/plugins.mjs +228 -0
  56. package/src/catalog/realtime.mjs +195 -0
  57. package/src/catalog/rust.mjs +75 -0
  58. package/src/catalog/server-functions.mjs +92 -0
  59. package/src/catalog/state.mjs +39 -0
  60. package/src/catalog/style-groups.mjs +54 -0
  61. package/src/catalog/tests.mjs +127 -0
  62. package/src/catalog/text.mjs +200 -0
  63. package/src/catalog/ui-state.mjs +42 -0
  64. package/src/catalog.mjs +67 -460
  65. package/src/check/runtime.mjs +280 -0
  66. package/src/check.mjs +198 -208
  67. package/src/cli/authoring.mjs +213 -0
  68. package/src/cli/project.mjs +302 -0
  69. package/src/cli.mjs +222 -250
  70. package/src/config.mjs +1 -1
  71. package/src/contributions.mjs +75 -25
  72. package/src/db-layer.mjs +13 -5
  73. package/src/deploy/cdn.mjs +94 -16
  74. package/src/deploy/files.mjs +100 -21
  75. package/src/deploy/fit.mjs +127 -23
  76. package/src/deploy/hosts.mjs +90 -20
  77. package/src/deploy/index/cli.mjs +20 -0
  78. package/src/deploy/index/environment.mjs +69 -0
  79. package/src/deploy/index/steps.mjs +87 -0
  80. package/src/deploy/index.mjs +228 -114
  81. package/src/deploy/needs.mjs +88 -31
  82. package/src/design-checks/accessible-names.mjs +62 -0
  83. package/src/design-checks/contrast.mjs +43 -0
  84. package/src/design-checks/csp.mjs +60 -0
  85. package/src/design-checks/files.mjs +59 -0
  86. package/src/design-checks/heading-links.mjs +61 -0
  87. package/src/design-checks/interaction.mjs +65 -0
  88. package/src/design-checks/jsx.mjs +67 -0
  89. package/src/design-checks/layout-values.mjs +68 -0
  90. package/src/design-checks/native-controls.mjs +48 -0
  91. package/src/design-checks/outline.mjs +39 -0
  92. package/src/design-checks/reporter.mjs +37 -0
  93. package/src/design-checks/rules.mjs +274 -0
  94. package/src/design-checks/values.mjs +262 -0
  95. package/src/design-checks.mjs +10 -400
  96. package/src/diag.mjs +32 -8
  97. package/src/docs.mjs +138 -43
  98. package/src/doctor.mjs +20 -8
  99. package/src/files.mjs +18 -5
  100. package/src/fix.mjs +10 -2
  101. package/src/gate/after.mjs +40 -0
  102. package/src/gate/before.mjs +54 -0
  103. package/src/gate/classify.mjs +72 -0
  104. package/src/gate/mutation.mjs +209 -0
  105. package/src/gate/resolve-cases.mjs +22 -0
  106. package/src/gate/result.mjs +63 -0
  107. package/src/gate/run.mjs +75 -0
  108. package/src/gate.mjs +104 -202
  109. package/src/infer/box-props.cjs +140 -0
  110. package/src/infer/core-boxes.cjs +46 -0
  111. package/src/infer/derived-writes.cjs +95 -0
  112. package/src/infer/engine/checks.cjs +193 -0
  113. package/src/infer/engine/flow.cjs +293 -0
  114. package/src/infer/engine/report.cjs +158 -0
  115. package/src/infer/engine/utils.cjs +15 -0
  116. package/src/infer/engine.cjs +108 -368
  117. package/src/infer/facts/sites.cjs +114 -0
  118. package/src/infer/facts.cjs +210 -117
  119. package/src/infer/hosts.cjs +69 -22
  120. package/src/infer/lsp.mjs +149 -40
  121. package/src/infer/session.cjs +73 -17
  122. package/src/infer/shadow.cjs +13 -5
  123. package/src/infer/worker.cjs +57 -9
  124. package/src/jsx-scan.mjs +63 -21
  125. package/src/load-config/schema.mjs +281 -0
  126. package/src/load-config.mjs +137 -138
  127. package/src/map.mjs +93 -26
  128. package/src/mcp/browser-tools.mjs +155 -0
  129. package/src/mcp.mjs +147 -69
  130. package/src/meta-table.mjs +43 -8
  131. package/src/mutate.mjs +120 -36
  132. package/src/plugin-checks.mjs +52 -23
  133. package/src/plugins.mjs +125 -39
  134. package/src/project.mjs +170 -79
  135. package/src/props-checks.mjs +24 -6
  136. package/src/props-listener.mjs +27 -9
  137. package/src/react-checks.mjs +35 -4
  138. package/src/server-state-checks.mjs +34 -0
  139. package/src/share-checks.mjs +121 -37
  140. package/src/site.mjs +33 -6
  141. package/src/slots.mjs +104 -19
  142. package/src/start/build.mjs +116 -0
  143. package/src/start/routes.mjs +38 -0
  144. package/src/start.mjs +193 -95
  145. package/src/state-checks/core-state.mjs +223 -0
  146. package/src/state-checks/modules.mjs +17 -0
  147. package/src/state-checks/scan.mjs +232 -0
  148. package/src/state-checks/server-graph.mjs +65 -0
  149. package/src/state-checks.mjs +109 -133
  150. package/src/style/find.mjs +121 -0
  151. package/src/style.mjs +133 -127
  152. package/src/surface-checks.mjs +212 -46
  153. package/src/tailwind.mjs +5 -4
  154. package/src/tasks.mjs +189 -27
  155. package/src/test-rules/context.mjs +109 -0
  156. package/src/test-rules/file-rules.mjs +279 -0
  157. package/src/test-rules/rule-helpers.mjs +244 -0
  158. package/src/test-rules/syntax.mjs +223 -0
  159. package/src/test-rules/test-rules.mjs +127 -0
  160. package/src/testdiff.mjs +94 -32
  161. package/src/tests.mjs +31 -483
  162. package/src/text-checks.mjs +229 -69
  163. package/src/tokens.mjs +12 -4
  164. package/src/trust/scan.mjs +95 -0
  165. package/src/trust.mjs +214 -102
  166. package/src/weft.mjs +41 -9
  167. package/src/workspace.mjs +96 -19
  168. package/ts-plugin/context.js +57 -0
  169. package/ts-plugin/index.js +4 -124
  170. package/ts-plugin/mapping.js +65 -0
  171. package/ts-plugin/proxy.js +144 -0
@@ -7,23 +7,56 @@ import { join, relative } from 'node:path'
7
7
  import { fileURLToPath, pathToFileURL } from 'node:url'
8
8
  import ts from 'typescript'
9
9
  import { z } from 'zod'
10
+ import { createSchemas } from './load-config/schema.mjs'
10
11
  import { diagnostic, locate } from './diag.mjs'
11
12
  import { checkSite } from './site.mjs'
12
13
  import { contributions } from './contributions.mjs'
13
14
  import { bindSlots } from './slots.mjs'
14
15
 
15
- export const configNames = ['weft.config.ts', 'weft.config.mts', 'weft.config.js', 'weft.config.mjs', 'weft.config.json', 'weft.config.yaml', 'weft.config.yml']
16
- export const builtinNames = ['add', 'map', 'dev', 'build', 'start', 'db', 'test', 'check', 'doctor', 'fix', 'tokens', 'browser', 'mcp', 'tasks', 'run', 'create', 'docs', 'agents-md', 'plugins', 'deploy']
16
+ export const configNames = [
17
+ 'weft.config.ts',
18
+ 'weft.config.mts',
19
+ 'weft.config.js',
20
+ 'weft.config.mjs',
21
+ 'weft.config.json',
22
+ 'weft.config.yaml',
23
+ 'weft.config.yml',
24
+ ]
25
+ export const builtinNames = [
26
+ 'add',
27
+ 'map',
28
+ 'dev',
29
+ 'build',
30
+ 'start',
31
+ 'db',
32
+ 'test',
33
+ 'check',
34
+ 'doctor',
35
+ 'fix',
36
+ 'tokens',
37
+ 'browser',
38
+ 'mcp',
39
+ 'tasks',
40
+ 'run',
41
+ 'create',
42
+ 'docs',
43
+ 'agents-md',
44
+ 'plugins',
45
+ 'deploy',
46
+ ]
17
47
 
18
- const configModule = pathToFileURL(join(fileURLToPath(new URL('.', import.meta.url)), 'config.mjs')).href
48
+ const configModule = pathToFileURL(
49
+ join(fileURLToPath(new URL('.', import.meta.url)), 'config.mjs'),
50
+ ).href
19
51
  const nativeRequire = createRequire(import.meta.url)
20
52
  /** Node's sync module hooks. Bun has none, and Deno's break `.node` requires, like Rolldown's; both read TypeScript without help (`importConfig`). */
21
- const registerHooks = process.versions.bun || process.versions.deno ? null : nodeModule.registerHooks
53
+ const registerHooks =
54
+ process.versions.bun || process.versions.deno ? null : nodeModule.registerHooks
22
55
  /** `weft.config.ts.weft-load-<n>`: a fresh module id per load, so a long-lived `weft mcp` sees edits. */
23
56
  const tag = /\.weft-load-\d+$/
24
57
  let hooked = false
25
58
 
26
- const transpile = path =>
59
+ const transpile = (path) =>
27
60
  ts.transpileModule(readFileSync(path, 'utf8'), {
28
61
  fileName: path,
29
62
  compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
@@ -38,7 +71,8 @@ function hook() {
38
71
  hooked = true
39
72
  registerHooks({
40
73
  resolve(specifier, context, next) {
41
- if (tag.test(specifier)) return { url: pathToFileURL(specifier).href, format: 'module', shortCircuit: true }
74
+ if (tag.test(specifier))
75
+ return { url: pathToFileURL(specifier).href, format: 'module', shortCircuit: true }
42
76
  if (specifier === '@weft-js/core/config' || specifier === '@weft-js/cli/config') {
43
77
  try {
44
78
  return next(specifier, context)
@@ -48,7 +82,16 @@ function hook() {
48
82
  }
49
83
  // the app's own imports are bundler-style (`./src/expression`): try what Vite would before failing
50
84
  if (/^\.{1,2}\//.test(specifier) && !/\.[cm]?[jt]sx?$|\.json$/.test(specifier)) {
51
- for (const end of ['', '.ts', '.tsx', '.mts', '.js', '/index.ts', '/index.tsx', '/index.js']) {
85
+ for (const end of [
86
+ '',
87
+ '.ts',
88
+ '.tsx',
89
+ '.mts',
90
+ '.js',
91
+ '/index.ts',
92
+ '/index.tsx',
93
+ '/index.js',
94
+ ]) {
52
95
  try {
53
96
  return next(specifier + end, context)
54
97
  } catch {}
@@ -61,9 +104,14 @@ function hook() {
61
104
  const path = fileURLToPath(url)
62
105
  if (tag.test(path)) {
63
106
  const real = path.replace(tag, '')
64
- return { format: 'module', source: /\.[mc]?ts$/.test(real) ? transpile(real) : readFileSync(real, 'utf8'), shortCircuit: true }
107
+ return {
108
+ format: 'module',
109
+ source: /\.[mc]?ts$/.test(real) ? transpile(real) : readFileSync(real, 'utf8'),
110
+ shortCircuit: true,
111
+ }
65
112
  }
66
- if (/\.[mc]?ts$/.test(path) && !path.includes('/node_modules/')) return { format: 'module', source: transpile(path), shortCircuit: true }
113
+ if (/\.[mc]?ts$/.test(path) && !path.includes('/node_modules/'))
114
+ return { format: 'module', source: transpile(path), shortCircuit: true }
67
115
  return next(url, context)
68
116
  },
69
117
  })
@@ -80,7 +128,13 @@ async function importConfig(file) {
80
128
  if (!/weft\/config|@weft-js\/cli\/config/.test(error.message)) throw error
81
129
  }
82
130
  const copy = `${file}.weft-load-${generation++}.mjs`
83
- writeFileSync(copy, transpile(file).replace(/(['"])(?:weft\/config|@weft-js\/cli\/config)\1/g, JSON.stringify(configModule)))
131
+ writeFileSync(
132
+ copy,
133
+ transpile(file).replace(
134
+ /(['"])(?:weft\/config|@weft-js\/cli\/config)\1/g,
135
+ JSON.stringify(configModule),
136
+ ),
137
+ )
84
138
  try {
85
139
  return (await import(pathToFileURL(copy).href)).default ?? {}
86
140
  } finally {
@@ -88,110 +142,12 @@ async function importConfig(file) {
88
142
  }
89
143
  }
90
144
 
91
- const zodSchema = z.custom(v => v != null && typeof v === 'object' && ('_zod' in v || '~standard' in v))
92
- const globs = z.array(z.string())
93
-
94
- const task = z.strictObject({
95
- run: z.string().min(1),
96
- description: z.string().optional(),
97
- sideEffects: z.array(z.string()).optional(),
98
- env: z.array(z.string()).optional(),
99
- args: z.union([zodSchema, z.record(z.string(), z.unknown())]).optional(),
100
- })
101
-
102
- const pluginEntry = z.looseObject({
103
- name: z.string().min(1),
104
- version: z.string().optional(),
105
- provides: z.array(z.union([z.string(), z.strictObject({ slot: z.string(), key: z.string() })])).optional(),
106
- })
107
-
108
- const image = z.strictObject({ url: z.string().min(1), width: z.int().positive().optional(), height: z.int().positive().optional(), alt: z.string().optional() })
109
-
110
- const hex = z.string().regex(/^#[0-9a-fA-F]{6}$/, { error: 'must be #rrggbb' })
111
- const colors = { background: hex.optional(), text: hex.optional(), subtle: hex.optional(), accent: hex.optional() }
112
- const cardFont = z.strictObject({ family: z.string().regex(/^[\w -]+$/, { error: 'must be a font family name' }), src: z.string().min(1), weight: z.number().optional() })
113
- const card = z.strictObject({ ...colors, scheme: z.enum(['dark', 'light']).optional(), dark: z.strictObject(colors).optional(), light: z.strictObject(colors).optional(), fonts: z.strictObject({ display: cardFont.optional(), body: cardFont.optional() }).optional() })
114
-
115
- const site = z.strictObject({
116
- headers: z.record(z.string(), z.record(z.string(), z.string())).optional(),
117
- name: z.string().min(1).optional(),
118
- url: z.url({ protocol: /^https?$/, error: 'must be an absolute http(s) URL' }).optional(),
119
- title: z.string().includes('%s', { error: 'must contain `%s` (the page title)' }).optional(),
120
- description: z.string().optional(),
121
- locale: z.string().regex(/^[a-z]{2,3}(_[A-Z]{2})?$/, { error: 'must look like `en_US`' }).optional(),
122
- inlineCss: z.boolean().optional(),
123
- criticalCss: z.boolean().optional(),
124
- criticalKeep: z.array(z.string()).optional(),
125
- image: z.union([z.literal('auto'), z.literal(false), z.string().min(1), image]).optional(),
126
- card: card.optional(),
127
- head: z.string().optional(),
128
- twitter: z
129
- .strictObject({ card: z.enum(['summary', 'summary_large_image', 'app', 'player']).optional(), site: z.string().optional(), creator: z.string().optional() })
130
- .optional(),
131
- })
132
-
133
- export const configSchema = z.strictObject({
134
- scope: z.strictObject({ write: globs.optional(), read: globs.optional(), forbid: globs.optional() }).optional(),
135
- tasks: z.record(z.string(), task).optional(),
136
- environments: z.record(z.string(), z.looseObject({ url: z.string().optional() })).optional(),
137
- test: z
138
- .strictObject({
139
- include: globs.optional(),
140
- lanes: z.array(z.enum(['fast', 'browser', 'e2e'])).optional(),
141
- gate: z.strictObject({ mutationBudgetSec: z.number().positive().optional(), maxMutants: z.number().int().positive().optional() }).optional(),
142
- })
143
- .optional(),
144
- build: z.strictObject({
145
- audit: z.union([z.boolean(), z.strictObject({ routes: z.array(z.string()).optional(), all: z.boolean().optional(), max: z.number().int().positive().optional() })]).optional(),
146
- unsafeAllow: z.record(z.string().regex(/^(all|[A-Z]|[A-Z]+\d{3})$/, 'a code (R002), a family (L) or all'), z.string().min(1, 'say why')).optional(),
147
- }).optional(),
148
- check: z.strictObject({ include: globs.optional(), exclude: globs.optional(), design: z.boolean().optional(), routes: z.boolean().optional(), props: z.union([z.boolean(), z.literal('manual')]).optional() }).optional(),
149
- plugins: z.array(pluginEntry).optional(),
150
- slots: z.record(z.string(), z.union([z.string(), z.record(z.string(), z.string())])).optional(),
151
- site: site.optional(),
152
- runtime: z.enum(['node', 'bun', 'deno']).optional(),
153
- hydration: z.enum(['lazy', 'partial', 'full']).optional(),
154
- rust: z.strictObject({
155
- unions: z.literal('allow').optional(),
156
- services: z.record(z.string(), z.strictObject({ crate: z.string().optional(), external: z.boolean().optional(), wasm: z.literal(false).optional() })).optional(),
157
- }).optional(),
158
- /** `weft deploy` (research 46): names, regions and sizes. Credentials come from the environment, never here (E008). */
159
- deploy: z.strictObject({
160
- target: z.enum(['netlify', 'vercel', 'cloudflare', 'fly', 'render']).optional(),
161
- netlify: z.strictObject({ site: z.string().min(1).optional() }).optional(),
162
- vercel: z.strictObject({ project: z.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/i, 'a name: letters, digits and dashes').optional() }).optional(),
163
- cloudflare: z.strictObject({ name: z.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/i, 'a name: letters, digits and dashes').optional() }).optional(),
164
- fly: z.strictObject({
165
- app: z.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/i, 'a name: letters, digits and dashes').optional(), region: z.string().regex(/^[a-z]{3}$/, 'a Fly region code, like `iad`').optional(), memory: z.string().regex(/^\d+(mb|gb)$/, 'like `512mb`').optional(),
166
- volume: z.strictObject({ name: z.string().regex(/^[a-z0-9_]+$/, 'lowercase letters, digits and underscores').optional(), path: z.string().startsWith('/').optional(), sizeGb: z.number().int().positive().optional() }).optional(),
167
- }).optional(),
168
- render: z.strictObject({
169
- service: z.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/i, 'a name: letters, digits and dashes').optional(), plan: z.string().optional(), region: z.string().optional(),
170
- disk: z.strictObject({ path: z.string().startsWith('/').optional(), sizeGb: z.number().int().positive().optional() }).optional(),
171
- }).optional(),
172
- ci: z.union([z.literal('github'), z.literal(false)]).optional(),
173
- branch: z.string().min(1).optional(),
174
- cdn: z.strictObject({
175
- proxy: z.literal('cloudflare').optional(),
176
- images: z.strictObject({ provider: z.literal('cloudinary'), cloud: z.string().regex(/^[\w-]+$/, "the cloud's name, not its URL").optional() }).optional(),
177
- }).optional(),
178
- }).optional(),
179
- /** Named targets for a query's `live: { verify: 'staging' }`. `db` is an env var's name. */
180
- live: z.strictObject({
181
- targets: z.record(z.string(), z.strictObject({ url: z.string().optional(), db: z.string().optional(), production: z.boolean().optional() })).optional(),
182
- }).optional(),
183
- trust: z.enum(['enforce', 'warn']).optional(),
184
- })
145
+ const { configSchema, dataSchema } = createSchemas(z)
146
+ export { configSchema }
185
147
 
186
- /** What a JSON or YAML file may hold: data, so a plugin is `name` or `[name, options]`. Also the editor schema. */
187
- const dataSchema = configSchema.extend({
188
- $schema: z.string().optional(),
189
- plugins: z.array(z.union([z.string(), z.tuple([z.string(), z.unknown()]), pluginEntry])).optional(),
190
- })
148
+ export const isDataConfig = (file) => /\.(json|ya?ml)$/.test(file)
191
149
 
192
- export const isDataConfig = file => /\.(json|ya?ml)$/.test(file)
193
-
194
- const configFiles = root => configNames.map(name => join(root, name)).filter(existsSync)
150
+ const configFiles = (root) => configNames.map((name) => join(root, name)).filter(existsSync)
195
151
 
196
152
  export function findConfig(root) {
197
153
  return configFiles(root)[0] ?? null
@@ -200,16 +156,24 @@ export function findConfig(root) {
200
156
  /** The schema `$schema` points at, for completion in `weft.config.json` and `.yaml`. */
201
157
  export function writeConfigSchema(root) {
202
158
  mkdirSync(join(root, '.weft'), { recursive: true })
203
- writeFileSync(join(root, '.weft', 'config.schema.json'), `${JSON.stringify(z.toJSONSchema(dataSchema, { unrepresentable: 'any' }), null, 2)}\n`)
159
+ writeFileSync(
160
+ join(root, '.weft', 'config.schema.json'),
161
+ `${JSON.stringify(z.toJSONSchema(dataSchema, { unrepresentable: 'any' }), null, 2)}\n`,
162
+ )
204
163
  }
205
164
 
206
165
  /** `@weft-js/text` → `text`, `@weft-js/db/plugin-native` → `native`: the name a plugin's named export goes by. */
207
- export const pluginName = pkg => pkg.replace(/^@[^/]+\/(?:[^/]+\/)?/, '').replace(/^plugin-/, '').replace(/[^a-zA-Z0-9]+(.)?/g, (_, c = '') => c.toUpperCase())
166
+ export const pluginName = (pkg) =>
167
+ pkg
168
+ .replace(/^@[^/]+\/(?:[^/]+\/)?/, '')
169
+ .replace(/^plugin-/, '')
170
+ .replace(/[^a-zA-Z0-9]+(.)?/g, (_, c = '') => c.toUpperCase())
208
171
 
209
172
  /** A plugin module's factory, and its import name: the default, or a named export (`surface-keel` may export `keel`). */
210
173
  export function pluginFactory(mod, pkg) {
211
- if (typeof mod.default === 'function') return { value: mod.default, name: pluginName(pkg), named: false }
212
- const name = [pluginName(pkg), pkg.split('-').at(-1)].find(n => typeof mod[n] === 'function')
174
+ if (typeof mod.default === 'function')
175
+ return { value: mod.default, name: pluginName(pkg), named: false }
176
+ const name = [pluginName(pkg), pkg.split('-').at(-1)].find((n) => typeof mod[n] === 'function')
213
177
  return name ? { value: mod[name], name, named: true } : null
214
178
  }
215
179
 
@@ -236,12 +200,38 @@ function loadPlugin(root, entry) {
236
200
  function readDataConfig(root, file, text) {
237
201
  const data = file.endsWith('.json') ? JSON.parse(text) : nativeRequire('yaml').parse(text)
238
202
  const { $schema: _schema, ...config } = data ?? {}
239
- if (Array.isArray(config.plugins)) config.plugins = config.plugins.map(p => loadPlugin(root, p))
203
+ if (Array.isArray(config.plugins)) config.plugins = config.plugins.map((p) => loadPlugin(root, p))
240
204
  return config
241
205
  }
242
206
 
243
207
  let generation = 0
244
208
 
209
+ function validateTasks(config, report) {
210
+ const tasks = config.tasks ?? {}
211
+ const commands = new Map()
212
+ for (const [name, t] of Object.entries(tasks)) {
213
+ if (builtinNames.includes(name))
214
+ report('P003', `Task \`${name}\` has the same name as the built-in \`weft ${name}\`.`, name)
215
+ const twin = commands.get(t.run.trim())
216
+ if (twin)
217
+ report('P003', `Task \`${name}\` runs the same command as \`${twin}\` (\`${t.run}\`).`, name)
218
+ else commands.set(t.run.trim(), name)
219
+ const missing = [
220
+ !t.description?.trim() && '`description`',
221
+ !t.sideEffects && '`sideEffects`',
222
+ ].filter(Boolean)
223
+ if (missing.length) report('P004', `Task \`${name}\` has no ${missing.join(' or ')}.`, name)
224
+ for (const env of t.env ?? []) {
225
+ if (config.environments && !(env in config.environments))
226
+ report(
227
+ 'P007',
228
+ `Task \`${name}\` allows environment \`${env}\`, which \`environments\` does not define.`,
229
+ name,
230
+ )
231
+ }
232
+ }
233
+ }
234
+
245
235
  /**
246
236
  * @param {string} root project root
247
237
  * @returns {Promise<{ file: string | null, config: import('./config').WeftConfig, diagnostics: object[], slots: object, contributions: ReturnType<typeof contributions> }>}
@@ -251,18 +241,31 @@ export async function loadConfig(root) {
251
241
  const diagnostics = []
252
242
  const rel = file ? relative(root, file).split('\\').join('/') : 'weft.config.ts'
253
243
  const text = file ? readFileSync(file, 'utf8') : ''
254
- const report = (code, why, near, extra) => diagnostics.push(diagnostic({ code, file: rel, why, ...locate(text, near), ...extra }))
255
- const others = configFiles(root).slice(1).map(f => relative(root, f))
256
- if (others.length) report('P010', `\`${rel}\` is the config in use, and \`${others.join('`, `')}\` would be ignored.`, null)
244
+ const report = (code, why, near, extra) =>
245
+ diagnostics.push(diagnostic({ code, file: rel, why, ...locate(text, near), ...extra }))
246
+ const others = configFiles(root)
247
+ .slice(1)
248
+ .map((f) => relative(root, f))
249
+ if (others.length)
250
+ report(
251
+ 'P010',
252
+ `\`${rel}\` is the config in use, and \`${others.join('`, `')}\` would be ignored.`,
253
+ null,
254
+ )
257
255
 
258
256
  let config = {}
259
257
  if (file) {
260
258
  if (registerHooks) hook()
261
259
  try {
262
- config = isDataConfig(file) ? readDataConfig(root, file, text) : registerHooks ? (nativeRequire(`${file}.weft-load-${generation++}`).default ?? {}) : await importConfig(file)
260
+ config = isDataConfig(file)
261
+ ? readDataConfig(root, file, text)
262
+ : registerHooks
263
+ ? (nativeRequire(`${file}.weft-load-${generation++}`).default ?? {})
264
+ : await importConfig(file)
263
265
  } catch (error) {
264
266
  // A plugin factory rejecting its options throws while the config module evaluates (@weft-js/core/plugin).
265
- if (error?.code === 'P115') report('P115', error.message.split('\n')[0], error.message.match(/^`([^`]+)`/)?.[1] ?? null)
267
+ if (error?.code === 'P115')
268
+ report('P115', error.message.split('\n')[0], error.message.match(/^`([^`]+)`/)?.[1] ?? null)
266
269
  else report('P007', `weft.config could not be loaded: ${error.message.split('\n')[0]}`, null)
267
270
  return { file, config: {}, diagnostics, slots: bindSlots({}, () => {}), contributions: [] }
268
271
  }
@@ -270,32 +273,28 @@ export async function loadConfig(root) {
270
273
  if (!parsed.success) {
271
274
  for (const issue of parsed.error.issues) {
272
275
  const path = issue.path.join('.')
273
- report('P007', `\`${path || '(root)'}\`: ${issue.message}`, String(issue.path.at(-1) ?? ''), { help: 'Fix the key at the path shown (see `weft docs config`).' })
276
+ report(
277
+ 'P007',
278
+ `\`${path || '(root)'}\`: ${issue.message}`,
279
+ String(issue.path.at(-1) ?? ''),
280
+ { help: 'Fix the key at the path shown (see `weft docs config`).' },
281
+ )
274
282
  }
275
283
  return { file, config, diagnostics, slots: bindSlots({}, () => {}), contributions: [] }
276
284
  }
277
285
  }
278
286
 
279
- const tasks = config.tasks ?? {}
280
- const commands = new Map()
281
- for (const [name, t] of Object.entries(tasks)) {
282
- if (builtinNames.includes(name)) report('P003', `Task \`${name}\` has the same name as the built-in \`weft ${name}\`.`, name)
283
- const twin = commands.get(t.run.trim())
284
- if (twin) report('P003', `Task \`${name}\` runs the same command as \`${twin}\` (\`${t.run}\`).`, name)
285
- else commands.set(t.run.trim(), name)
286
- const missing = [!t.description?.trim() && '`description`', !t.sideEffects && '`sideEffects`'].filter(Boolean)
287
- if (missing.length) report('P004', `Task \`${name}\` has no ${missing.join(' or ')}.`, name)
288
- for (const env of t.env ?? []) {
289
- if (config.environments && !(env in config.environments)) report('P007', `Task \`${name}\` allows environment \`${env}\`, which \`environments\` does not define.`, name)
290
- }
291
- }
287
+ validateTasks(config, report)
292
288
 
293
289
  const seen = new Set()
294
290
  for (const p of config.plugins ?? []) {
295
- if (seen.has(p.name)) report('P116', `Plugin \`${p.name}\` is listed twice in \`plugins\`.`, p.name)
291
+ if (seen.has(p.name))
292
+ report('P116', `Plugin \`${p.name}\` is listed twice in \`plugins\`.`, p.name)
296
293
  seen.add(p.name)
297
294
  }
298
- const unique = (config.plugins ?? []).filter((p, i, all) => all.findIndex(q => q.name === p.name) === i)
295
+ const unique = (config.plugins ?? []).filter(
296
+ (p, i, all) => all.findIndex((q) => q.name === p.name) === i,
297
+ )
299
298
  const contributed = contributions(unique, report, { root })
300
299
  const slots = bindSlots({ ...config, plugins: unique }, report)
301
300
  if (config.site) checkSite(config.site, report)
package/src/map.mjs CHANGED
@@ -9,39 +9,68 @@ import { loadConfig } from './load-config.mjs'
9
9
  import { dbLayer } from './db-layer.mjs'
10
10
  import { listSurfaces } from './surface-checks.mjs'
11
11
 
12
- const read = (root, rel) => (existsSync(join(root, rel)) ? readFileSync(join(root, rel), 'utf8') : '')
13
- const names = (text, re) => [...text.matchAll(re)].map(m => m[1])
12
+ const read = (root, rel) =>
13
+ existsSync(join(root, rel)) ? readFileSync(join(root, rel), 'utf8') : ''
14
+ const names = (text, re) => [...text.matchAll(re)].map((m) => m[1])
14
15
  /** Up to `max` names: enough to recognise the app, while the row's call gives the whole list. */
15
- const sample = (list, max = 6) => (list.length > max ? `${list.slice(0, max).join(', ')}, +${list.length - max}` : list.join(', '))
16
+ const sample = (list, max = 6) =>
17
+ list.length > max ? `${list.slice(0, max).join(', ')}, +${list.length - max}` : list.join(', ')
16
18
 
17
19
  function serverFunctions(root) {
18
- const files = discover(root, { include: ['src/server/**/*.{ts,mts}', 'src/**/*.server.{ts,mts}'], exclude: [] })
20
+ const files = discover(root, {
21
+ include: ['src/server/**/*.{ts,mts}', 'src/**/*.server.{ts,mts}'],
22
+ exclude: [],
23
+ })
19
24
  const out = { functions: [], channels: [] }
20
25
  for (const rel of files) {
21
- const found = [...read(root, rel).matchAll(/export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(query|mutation|stream|channel)\s*\(/g)]
22
- const fns = found.filter(m => m[2] !== 'channel').map(m => m[1])
26
+ const found = [
27
+ ...read(root, rel).matchAll(
28
+ /export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(query|mutation|stream|channel)\s*\(/g,
29
+ ),
30
+ ]
31
+ const fns = found.filter((m) => m[2] !== 'channel').map((m) => m[1])
23
32
  if (fns.length) out.functions.push(`\`${rel}\`: ${fns.join(', ')}`)
24
- out.channels.push(...found.filter(m => m[2] === 'channel').map(m => m[1]))
33
+ out.channels.push(...found.filter((m) => m[2] === 'channel').map((m) => m[1]))
25
34
  }
26
35
  return out
27
36
  }
28
37
 
29
- const sqlText = (root, dir) => (existsSync(join(root, dir)) ? readdirSync(join(root, dir)).filter(f => f.endsWith('.sql')).map(f => read(root, join(dir, f))).join('\n') : '')
38
+ const sqlText = (root, dir) =>
39
+ existsSync(join(root, dir))
40
+ ? readdirSync(join(root, dir))
41
+ .filter((f) => f.endsWith('.sql'))
42
+ .map((f) => read(root, join(dir, f)))
43
+ .join('\n')
44
+ : ''
30
45
  const ENGINE = { PrismaLibSql: 'sqlite', PrismaPg: 'postgres', PrismaMariaDb: 'mysql' }
31
46
 
32
47
  export function database(root, layer) {
33
48
  if (!layer) return null
34
49
  const entry = read(root, 'src/db/index.server.ts')
35
- const engine = entry.match(/@weft-js\/(?:start\/db|db\/plugin-native)\/(\w+)/)?.[1] ?? ENGINE[entry.match(/Prisma(?:LibSql|Pg|MariaDb)/)?.[0]] ?? 'unknown'
36
- const tables = layer.layer === 'prisma' ? names(read(root, layer.schema), /^model\s+(\w+)/gm)
37
- : layer.layer === 'native' ? names(sqlText(root, layer.migrations), /create\s+table\s+(?:if\s+not\s+exists\s+)?["`]?(\w+)/gi)
38
- : names(read(root, 'src/db/schema.ts'), /export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*\w+Table\s*\(/g)
50
+ const engine =
51
+ entry.match(/@weft-js\/(?:start\/db|db\/plugin-native)\/(\w+)/)?.[1] ??
52
+ ENGINE[entry.match(/Prisma(?:LibSql|Pg|MariaDb)/)?.[0]] ??
53
+ 'unknown'
54
+ const tables =
55
+ layer.layer === 'prisma'
56
+ ? names(read(root, layer.schema), /^model\s+(\w+)/gm)
57
+ : layer.layer === 'native'
58
+ ? names(
59
+ sqlText(root, layer.migrations),
60
+ /create\s+table\s+(?:if\s+not\s+exists\s+)?["`]?(\w+)/gi,
61
+ )
62
+ : names(
63
+ read(root, 'src/db/schema.ts'),
64
+ /export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*\w+Table\s*\(/g,
65
+ )
39
66
  return { engine, layer: layer.layer, tables }
40
67
  }
41
68
 
42
69
  function routes(root) {
43
70
  const files = discover(root, { include: ['src/routes/**/*.{ts,tsx}'], exclude: [] })
44
- return files.map(f => f.replace(/^src\/routes\//, '').replace(/\.[jt]sx?$/, '')).filter(f => !/(^|\/)_/.test(f))
71
+ return files
72
+ .map((f) => f.replace(/^src\/routes\//, '').replace(/\.[jt]sx?$/, ''))
73
+ .filter((f) => !/(^|\/)_/.test(f))
45
74
  }
46
75
 
47
76
  /** The map as data. */
@@ -51,44 +80,82 @@ export async function agentMap(root) {
51
80
  const { config } = loaded
52
81
  const pages = routes(root)
53
82
  const fns = serverFunctions(root)
54
- const env = ['src/env.ts', 'src/env.server.ts'].find(f => existsSync(join(root, f)))
83
+ const env = ['src/env.ts', 'src/env.server.ts'].find((f) => existsSync(join(root, f)))
55
84
  const layer = await dbLayer(root, loaded)
56
85
  const db = database(root, layer)
57
86
  const authFile = read(root, 'src/auth.server.ts')
58
87
  const auth = authFile && (authFile.includes('better-auth') ? 'better-auth' : 'custom')
59
- const surfaces = listSurfaces(root).map(s => s.name)
60
- const styled = existsSync(join(root, '.weft/styles.json')) ? Object.keys(JSON.parse(read(root, '.weft/styles.json')).groups ?? {}) : []
61
- const bound = Object.entries(loaded.slots ?? {}).filter(([, v]) => typeof v === 'string' && !v.startsWith('weft:default')).map(([slot]) => slot)
88
+ const surfaces = listSurfaces(root).map((s) => s.name)
89
+ const styled = existsSync(join(root, '.weft/styles.json'))
90
+ ? Object.keys(JSON.parse(read(root, '.weft/styles.json')).groups ?? {})
91
+ : []
92
+ const bound = Object.entries(loaded.slots ?? {})
93
+ .filter(([, v]) => typeof v === 'string' && !v.startsWith('weft:default'))
94
+ .map(([slot]) => slot)
62
95
  const tasks = Object.keys(config.tasks ?? {})
63
96
  const row = (want, call, have) => ({ want, call, ...(have && { have }) })
64
97
 
65
98
  const rows = [
66
99
  pages.length && row('Pages, their data and metadata', '`weft routes --meta`', sample(pages)),
67
- fns.functions.length && row('Server functions', 'The file named here; signatures in `.weft/contract.json` after a build', fns.functions.join('; ')),
100
+ fns.functions.length &&
101
+ row(
102
+ 'Server functions',
103
+ 'The file named here; signatures in `.weft/contract.json` after a build',
104
+ fns.functions.join('; '),
105
+ ),
68
106
  fns.channels.length && row('Channels', '`weft docs start`', sample(fns.channels)),
69
- db && row('Tables and migrations', layer.schema ? `\`${layer.schema}\`, then \`weft db generate\`` : `\`weft db generate <name>\`, then SQL in \`${layer.migrations}/\``, sample(db.tables)),
107
+ db &&
108
+ row(
109
+ 'Tables and migrations',
110
+ layer.schema
111
+ ? `\`${layer.schema}\`, then \`weft db generate\``
112
+ : `\`weft db generate <name>\`, then SQL in \`${layer.migrations}/\``,
113
+ sample(db.tables),
114
+ ),
70
115
  auth && row('Who the user is', '`ctx.user` on the server; `src/auth.server.ts`', auth),
71
- row('Styles', 'Before writing one: `weft style find <what it is>`', styled.length ? sample(styled) : undefined),
116
+ row(
117
+ 'Styles',
118
+ 'Before writing one: `weft style find <what it is>`',
119
+ styled.length ? sample(styled) : undefined,
120
+ ),
72
121
  env && row('Environment variables', `\`${env}\`; values in \`.env\``),
73
122
  surfaces.length && row('Canvas and WebGL', '`weft surfaces`', sample(surfaces)),
74
- ...loaded.contributions.flatMap(c => c.manifest.map ?? []).map(r => row(r.want, r.call, r.have)),
75
- (config.plugins?.length || bound.length) && row('Plugins and slots', '`weft plugins`, `weft plugins explain <slot>`', sample(bound)),
123
+ ...loaded.contributions
124
+ .flatMap((c) => c.manifest.map ?? [])
125
+ .map((r) => row(r.want, r.call, r.have)),
126
+ (config.plugins?.length || bound.length) &&
127
+ row('Plugins and slots', '`weft plugins`, `weft plugins explain <slot>`', sample(bound)),
76
128
  tasks.length && row('Project tasks', '`weft tasks --json`', sample(tasks)),
77
129
  row('A rule or a diagnostic', '`weft docs <topic or code>`'),
78
130
  row('The running app', '`weft browser snapshot`'),
79
131
  ].filter(Boolean)
80
132
 
81
- return { version: 1, app: config.site?.name ?? null, stack: { runtime: config.runtime ?? 'node', ui: config.plugins?.map(p => p.name) ?? [], db, auth: auth || null }, rows }
133
+ return {
134
+ version: 1,
135
+ app: config.site?.name ?? null,
136
+ stack: {
137
+ runtime: config.runtime ?? 'node',
138
+ ui: config.plugins?.map((p) => p.name) ?? [],
139
+ db,
140
+ auth: auth || null,
141
+ },
142
+ rows,
143
+ }
82
144
  }
83
145
 
84
146
  export function formatMap(map) {
85
147
  const { stack } = map
86
- const parts = [`runtime ${stack.runtime}`, stack.db && `${stack.db.layer} on ${stack.db.engine}`, stack.auth, ...stack.ui].filter(Boolean)
87
- const table = map.rows.map(r => `| ${r.want} | ${r.call} | ${r.have ?? ''} |`)
148
+ const parts = [
149
+ `runtime ${stack.runtime}`,
150
+ stack.db && `${stack.db.layer} on ${stack.db.engine}`,
151
+ stack.auth,
152
+ ...stack.ui,
153
+ ].filter(Boolean)
154
+ const table = map.rows.map((r) => `| ${r.want} | ${r.call} | ${r.have ?? ''} |`)
88
155
  return [
89
156
  `# ${map.app ?? 'This app'}: agent map`,
90
157
  '',
91
- '<!-- Generated by `weft map` from this app\'s config and code. Do not edit. -->',
158
+ "<!-- Generated by `weft map` from this app's config and code. Do not edit. -->",
92
159
  '',
93
160
  `Stack: ${parts.join(', ')}.`,
94
161
  '',