@weft-js/cli 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 (171) hide show
  1. package/README.md +38 -14
  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 +108 -0
  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 +174 -188
  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 +68 -444
  65. package/src/check/runtime.mjs +280 -0
  66. package/src/check.mjs +199 -207
  67. package/src/cli/authoring.mjs +213 -0
  68. package/src/cli/project.mjs +302 -0
  69. package/src/cli.mjs +228 -224
  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 +142 -0
  74. package/src/deploy/files.mjs +266 -0
  75. package/src/deploy/fit.mjs +166 -0
  76. package/src/deploy/hosts.mjs +126 -0
  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 +387 -0
  81. package/src/deploy/needs.mjs +149 -0
  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 -42
  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 +144 -116
  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 +194 -92
  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
package/src/trust.mjs CHANGED
@@ -3,23 +3,36 @@
3
3
  // already run by the time any of this looks at a plugin, and the audit reads source, which code can hide from.
4
4
 
5
5
  import { createHash } from 'node:crypto'
6
- import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
7
- import { builtinModules } from 'node:module'
6
+ import {
7
+ existsSync,
8
+ mkdirSync,
9
+ readdirSync,
10
+ readFileSync,
11
+ realpathSync,
12
+ writeFileSync,
13
+ } from 'node:fs'
8
14
  import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
9
- import ts from 'typescript'
10
15
  import { diagnostic, locate } from './diag.mjs'
16
+ import { scanFile } from './trust/scan.mjs'
11
17
 
12
18
  export const lockFile = '.weft/plugins.lock.json'
13
19
 
14
- const refused = message => Object.assign(new Error(message), { code: 'E_TRUST' })
15
- const posix = p => p.split(sep).join('/')
20
+ const refused = (message) => Object.assign(new Error(message), { code: 'E_TRUST' })
21
+ const posix = (p) => p.split(sep).join('/')
16
22
  /** @param {string[]} list */
17
- const sorted = list => [...new Set(list)].sort()
23
+ const sorted = (list) => [...new Set(list)].sort()
18
24
 
19
25
  /** Permissions as flat strings, one per grant, so a lock diff reads line by line: `fs.read:fonts/**`, `native`. */
20
26
  export function grants(p = {}) {
21
- const each = (kind, list) => (list ?? []).map(v => `${kind}:${v}`)
22
- return sorted([...each('fs.read', p.fs?.read), ...each('fs.write', p.fs?.write), ...each('net', p.net), ...each('env', p.env), ...each('spawn', p.spawn), ...(p.native ? ['native'] : [])])
27
+ const each = (kind, list) => (list ?? []).map((v) => `${kind}:${v}`)
28
+ return sorted([
29
+ ...each('fs.read', p.fs?.read),
30
+ ...each('fs.write', p.fs?.write),
31
+ ...each('net', p.net),
32
+ ...each('env', p.env),
33
+ ...each('spawn', p.spawn),
34
+ ...(p.native ? ['native'] : []),
35
+ ])
23
36
  }
24
37
 
25
38
  /** The lockfile, or `broken` when it can't be read: an unresolved merge shouldn't read as "nothing approved". */
@@ -28,7 +41,8 @@ function readLock(root) {
28
41
  if (!existsSync(file)) return { found: false, plugins: {} }
29
42
  try {
30
43
  const data = JSON.parse(readFileSync(file, 'utf8'))
31
- if (data?.version !== 1 || typeof data.plugins !== 'object' || data.plugins === null) return { found: true, plugins: {}, broken: 'it is not a version 1 lockfile' }
44
+ if (data?.version !== 1 || typeof data.plugins !== 'object' || data.plugins === null)
45
+ return { found: true, plugins: {}, broken: 'it is not a version 1 lockfile' }
32
46
  return { found: true, plugins: data.plugins }
33
47
  } catch (error) {
34
48
  return { found: true, plugins: {}, broken: String(error.message).split('\n')[0] }
@@ -36,7 +50,11 @@ function readLock(root) {
36
50
  }
37
51
 
38
52
  /** The package a plugin name is in: `@weft-js/db/mongo` is a subpath of `@weft-js/db`. */
39
- export const packageOf = name => name.split('/').slice(0, name.startsWith('@') ? 2 : 1).join('/')
53
+ export const packageOf = (name) =>
54
+ name
55
+ .split('/')
56
+ .slice(0, name.startsWith('@') ? 2 : 1)
57
+ .join('/')
40
58
 
41
59
  /** Where a plugin is installed: the nearest `node_modules/<package>`, through its symlink. Null for one defined in the config. */
42
60
  function packageDir(root, name) {
@@ -51,8 +69,8 @@ function packageDir(root, name) {
51
69
  function hashPackage(dir) {
52
70
  const hash = createHash('sha512')
53
71
  const files = readdirSync(dir, { recursive: true, withFileTypes: true })
54
- .filter(f => f.isFile() && !relative(dir, f.parentPath).split(sep).includes('node_modules'))
55
- .map(f => join(f.parentPath, f.name))
72
+ .filter((f) => f.isFile() && !relative(dir, f.parentPath).split(sep).includes('node_modules'))
73
+ .map((f) => join(f.parentPath, f.name))
56
74
  .sort()
57
75
  for (const file of files) {
58
76
  const bytes = readFileSync(file)
@@ -61,50 +79,21 @@ function hashPackage(dir) {
61
79
  return `sha512-${hash.digest('base64')}`
62
80
  }
63
81
 
64
- const builtins = new Set(builtinModules)
65
- const NEEDS = { fs: 'fs', 'fs/promises': 'fs', child_process: 'spawn', cluster: 'spawn', net: 'net', tls: 'net', dgram: 'net', dns: 'net', 'dns/promises': 'net', http: 'net', https: 'net', http2: 'net' }
66
- const NET_GLOBALS = new Set(['fetch', 'WebSocket', 'EventSource'])
67
- /** Bundlers inline it, and it says nothing about the machine. */
68
- const AMBIENT_ENV = new Set(['NODE_ENV'])
69
-
70
- /** What one file does directly, and what it imports. */
71
- function scanFile(text, file) {
72
- const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, /\.[cm]?tsx?$/.test(file) ? ts.ScriptKind.TSX : ts.ScriptKind.JSX)
73
- const uses = []
74
- const imports = []
75
- const use = (need, node) => uses.push({ need, line: sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1 })
76
- const specifier = (name, node) => {
77
- const bare = name.replace(/^node:/, '')
78
- if (name.endsWith('.node')) use('native', node)
79
- else if (builtins.has(bare)) NEEDS[bare] && use(NEEDS[bare], node)
80
- else imports.push(name)
81
- }
82
- const isProcess = node => ts.isIdentifier(node) && node.text === 'process'
83
- const visit = node => {
84
- if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) {
85
- if (!(node.importClause?.isTypeOnly || node.isTypeOnly)) specifier(node.moduleSpecifier.text, node)
86
- } else if (ts.isCallExpression(node)) {
87
- const callee = node.expression
88
- const [first] = node.arguments
89
- if ((callee.kind === ts.SyntaxKind.ImportKeyword || (ts.isIdentifier(callee) && callee.text === 'require')) && first && ts.isStringLiteralLike(first)) specifier(first.text, node)
90
- else if (ts.isIdentifier(callee) && NET_GLOBALS.has(callee.text)) use('net', node)
91
- else if (ts.isPropertyAccessExpression(callee) && isProcess(callee.expression) && callee.name.text === 'dlopen') use('native', node)
92
- } else if (ts.isNewExpression(node) && ts.isIdentifier(node.expression) && NET_GLOBALS.has(node.expression.text)) use('net', node)
93
- else if (ts.isPropertyAccessExpression(node) && isProcess(node.expression) && node.name.text === 'env') {
94
- const parent = node.parent
95
- if (ts.isPropertyAccessExpression(parent) && parent.expression === node) AMBIENT_ENV.has(parent.name.text) || use(`env:${parent.name.text}`, node)
96
- else if (ts.isElementAccessExpression(parent) && parent.expression === node && ts.isStringLiteralLike(parent.argumentExpression)) AMBIENT_ENV.has(parent.argumentExpression.text) || use(`env:${parent.argumentExpression.text}`, node)
97
- else use('env:*', node)
98
- }
99
- ts.forEachChild(node, visit)
100
- }
101
- visit(sf)
102
- return { uses, imports }
103
- }
104
-
105
- const EXTENSIONS = ['', '.mjs', '.js', '.cjs', '.ts', '.tsx', '.mts', '/index.mjs', '/index.js', '/index.ts']
82
+ const EXTENSIONS = [
83
+ '',
84
+ '.mjs',
85
+ '.js',
86
+ '.cjs',
87
+ '.ts',
88
+ '.tsx',
89
+ '.mts',
90
+ '/index.mjs',
91
+ '/index.js',
92
+ '/index.ts',
93
+ ]
106
94
  const sourceFile = /\.(?:[cm]?[jt]s|[jt]sx)$/
107
- const onDisk = path => EXTENSIONS.map(e => path + e).find(p => sourceFile.test(p) && existsSync(p)) ?? null
95
+ const onDisk = (path) =>
96
+ EXTENSIONS.map((e) => path + e).find((p) => sourceFile.test(p) && existsSync(p)) ?? null
108
97
 
109
98
  /** A package export's file for this runtime. */
110
99
  function exportTarget(value) {
@@ -115,9 +104,13 @@ function exportTarget(value) {
115
104
 
116
105
  function entryFile(dir, pkg, subpath) {
117
106
  const map = pkg.exports
118
- const keyed = map && typeof map === 'object' && Object.keys(map).some(k => k.startsWith('.'))
119
- const target = exportTarget(keyed ? map[subpath] : subpath === '.' ? (map ?? pkg.main ?? 'index.js') : null)
120
- return (target && onDisk(join(dir, target))) ?? (subpath === '.' ? null : onDisk(join(dir, subpath)))
107
+ const keyed = map && typeof map === 'object' && Object.keys(map).some((k) => k.startsWith('.'))
108
+ const target = exportTarget(
109
+ keyed ? map[subpath] : subpath === '.' ? (map ?? pkg.main ?? 'index.js') : null,
110
+ )
111
+ return (
112
+ (target && onDisk(join(dir, target))) ?? (subpath === '.' ? null : onDisk(join(dir, subpath)))
113
+ )
121
114
  }
122
115
 
123
116
  /**
@@ -127,17 +120,21 @@ function entryFile(dir, pkg, subpath) {
127
120
  export function audit(root, dir, contribution) {
128
121
  const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'))
129
122
  // A package can hold several plugins, `<package>/<sub>/plugin`, and each answers for its own files.
130
- const sub = contribution.name?.startsWith(`${pkg.name}/`) ? contribution.name.slice(pkg.name.length) : ''
123
+ const sub = contribution.name?.startsWith(`${pkg.name}/`)
124
+ ? contribution.name.slice(pkg.name.length)
125
+ : ''
131
126
  const roots = [entryFile(dir, pkg, `.${sub}/plugin`) ?? entryFile(dir, pkg, `.${sub}`)]
132
127
  for (const ref of contribution.manifest.provides ?? []) {
133
128
  for (const [side, { module }] of Object.entries(ref.sides ?? {})) {
134
129
  if (side === 'client') continue
135
- if (module === pkg.name || module.startsWith(`${pkg.name}/`)) roots.push(entryFile(dir, pkg, `.${module.slice(pkg.name.length)}`))
136
- else if (isAbsolute(module) || module.startsWith('.')) roots.push(onDisk(isAbsolute(module) ? module : resolve(root, module)))
130
+ if (module === pkg.name || module.startsWith(`${pkg.name}/`))
131
+ roots.push(entryFile(dir, pkg, `.${module.slice(pkg.name.length)}`))
132
+ else if (isAbsolute(module) || module.startsWith('.'))
133
+ roots.push(onDisk(isAbsolute(module) ? module : resolve(root, module)))
137
134
  }
138
135
  }
139
136
  const seen = new Set()
140
- const queue = roots.filter(f => f && !relative(dir, f).startsWith('..'))
137
+ const queue = roots.filter((f) => f && !relative(dir, f).startsWith('..'))
141
138
  const uses = []
142
139
  /** @type {Set<string>} */
143
140
  const dependencies = new Set()
@@ -149,7 +146,13 @@ export function audit(root, dir, contribution) {
149
146
  for (const u of found.uses) uses.push({ ...u, file: posix(relative(dir, file)) })
150
147
  for (const name of found.imports) {
151
148
  if (!name.startsWith('.')) {
152
- if (name !== pkg.name && !name.startsWith(`${pkg.name}/`)) dependencies.add(name.split('/').slice(0, name.startsWith('@') ? 2 : 1).join('/'))
149
+ if (name !== pkg.name && !name.startsWith(`${pkg.name}/`))
150
+ dependencies.add(
151
+ name
152
+ .split('/')
153
+ .slice(0, name.startsWith('@') ? 2 : 1)
154
+ .join('/'),
155
+ )
153
156
  continue
154
157
  }
155
158
  const next = onDisk(resolve(dirname(file), name))
@@ -158,19 +161,21 @@ export function audit(root, dir, contribution) {
158
161
  }
159
162
  const granted = grants(contribution.permissions)
160
163
  return {
161
- uses: uses.sort((a, b) => a.need.localeCompare(b.need) || a.file.localeCompare(b.file) || a.line - b.line),
162
- undeclared: sorted(uses.map(u => u.need).filter(need => !declared(need, granted))),
164
+ uses: uses.sort(
165
+ (a, b) => a.need.localeCompare(b.need) || a.file.localeCompare(b.file) || a.line - b.line,
166
+ ),
167
+ undeclared: sorted(uses.map((u) => u.need).filter((need) => !declared(need, granted))),
163
168
  dependencies: [...dependencies].sort(),
164
169
  }
165
170
  }
166
171
 
167
172
  /** Is `need` from the audit covered by what the plugin declared? */
168
173
  function declared(need, granted) {
169
- if (need === 'env:*') return granted.some(g => g.startsWith('env:'))
174
+ if (need === 'env:*') return granted.some((g) => g.startsWith('env:'))
170
175
  if (need.startsWith('env:')) return granted.includes(need) || granted.includes('env:*')
171
176
  if (need === 'native') return granted.includes('native')
172
177
  const kind = { fs: 'fs.', net: 'net:', spawn: 'spawn:' }[need]
173
- return granted.some(g => g.startsWith(kind))
178
+ return granted.some((g) => g.startsWith(kind))
174
179
  }
175
180
 
176
181
  /**
@@ -180,28 +185,62 @@ function declared(need, granted) {
180
185
  */
181
186
  export function trustStatus(root, loaded, { full = false } = {}) {
182
187
  const lock = readLock(root)
183
- const plugins = loaded.contributions.map(c => {
188
+ const plugins = loaded.contributions.map((c) => {
184
189
  const entry = lock.plugins[c.name] ?? null
185
190
  const dir = packageDir(root, c.name)
186
- const source = !dir ? 'config' : dir.split(sep).includes('node_modules') ? 'package' : 'workspace'
191
+ const source = !dir
192
+ ? 'config'
193
+ : dir.split(sep).includes('node_modules')
194
+ ? 'package'
195
+ : 'workspace'
187
196
  const integrity = source === 'package' ? hashPackage(dir) : null
188
197
  const permissions = grants(c.permissions)
189
- const provides = sorted((c.manifest.provides ?? []).map(p => (p.key ? `${p.slot.id}[${p.key}]` : p.slot.id)))
198
+ const provides = sorted(
199
+ (c.manifest.provides ?? []).map((p) => (p.key ? `${p.slot.id}[${p.key}]` : p.slot.id)),
200
+ )
190
201
  // A scaffold approves permissions before there's an install to hash (`integrity: null`), and that's filled in by `approve`.
191
- const changed = !!entry && (entry.integrity ?? null) !== integrity && !(entry.integrity == null && source === 'package')
192
- const scanned = dir && (full || !entry || !integrity || !entry.integrity || changed) ? audit(root, dir, c) : null
202
+ const changed =
203
+ !!entry &&
204
+ (entry.integrity ?? null) !== integrity &&
205
+ !(entry.integrity == null && source === 'package')
206
+ const scanned =
207
+ dir && (full || !entry || !integrity || !entry.integrity || changed)
208
+ ? audit(root, dir, c)
209
+ : null
193
210
  const uses = scanned?.uses ?? []
194
211
  // Without a scan the lock's own record stands: those are the same files.
195
212
  const undeclared = scanned?.undeclared ?? entry?.undeclared ?? []
196
- const added = permissions.filter(g => !entry?.permissions?.includes(g))
197
- const removed = (entry?.permissions ?? []).filter(g => !permissions.includes(g))
198
- const hidden = undeclared.filter(need => !entry?.undeclared?.includes(need))
213
+ const added = permissions.filter((g) => !entry?.permissions?.includes(g))
214
+ const removed = (entry?.permissions ?? []).filter((g) => !permissions.includes(g))
215
+ const hidden = undeclared.filter((need) => !entry?.undeclared?.includes(need))
199
216
  return {
200
- name: c.name, version: c.version ?? null, source, integrity, permissions, provides, uses, undeclared, dependencies: scanned?.dependencies ?? [],
201
- locked: !!entry, lockedVersion: entry?.version ?? null, added, removed, hidden, integrityChanged: changed, blocked: added.length > 0 || hidden.length > 0 || changed,
217
+ name: c.name,
218
+ version: c.version ?? null,
219
+ source,
220
+ integrity,
221
+ permissions,
222
+ provides,
223
+ uses,
224
+ undeclared,
225
+ dependencies: scanned?.dependencies ?? [],
226
+ locked: !!entry,
227
+ lockedVersion: entry?.version ?? null,
228
+ added,
229
+ removed,
230
+ hidden,
231
+ integrityChanged: changed,
232
+ blocked: added.length > 0 || hidden.length > 0 || changed,
202
233
  }
203
234
  })
204
- return { lock: lockFile, found: lock.found, broken: lock.broken, plugins, stale: Object.keys(lock.plugins).filter(name => !plugins.some(p => p.name === name)).sort() }
235
+ return {
236
+ lock: lockFile,
237
+ found: lock.found,
238
+ broken: lock.broken,
239
+ plugins,
240
+ stale: Object.keys(lock.plugins)
241
+ .filter((name) => !plugins.some((p) => p.name === name))
242
+ .sort(),
243
+ }
205
244
  }
206
245
 
207
246
  /** P109 and P110 for what the lock doesn't cover. `trust: 'warn'` in the config makes them warnings. */
@@ -210,29 +249,64 @@ export function trustDiagnostics(root, loaded, status = trustStatus(root, loaded
210
249
  const rel = loaded.file ? posix(relative(root, loaded.file)) : 'weft.config.ts'
211
250
  const text = loaded.file ? readFileSync(loaded.file, 'utf8') : ''
212
251
  const out = []
213
- const report = (code, name, why, help) => out.push(diagnostic({ code, severity, owner: name, file: rel, why, help, ...locate(text, name) }))
214
- if (status.broken) out.push(diagnostic({ code: 'P110', severity, file: lockFile, why: `${lockFile} can't be read: ${status.broken}.`, help: 'Fix it, or delete it and approve each plugin again.' }))
252
+ const report = (code, name, why, help) =>
253
+ out.push(
254
+ diagnostic({ code, severity, owner: name, file: rel, why, help, ...locate(text, name) }),
255
+ )
256
+ if (status.broken)
257
+ out.push(
258
+ diagnostic({
259
+ code: 'P110',
260
+ severity,
261
+ file: lockFile,
262
+ why: `${lockFile} can't be read: ${status.broken}.`,
263
+ help: 'Fix it, or delete it and approve each plugin again.',
264
+ }),
265
+ )
215
266
  for (const p of status.plugins) {
216
267
  const approve = `Review it, then run \`weft plugins approve ${p.name}\`.`
217
- if (p.added.length) report('P109', p.name, `\`${p.name}\` asks for permissions the lock doesn't give it: ${p.added.join(', ')}.`, approve)
218
- if (p.hidden.length) report('P109', p.name, `\`${p.name}\` uses ${p.hidden.join(', ')} without declaring it (${p.uses.find(u => p.hidden.includes(u.need))?.file ?? 'see `weft plugins audit`'}).`, `Have it declared, or review it and run \`weft plugins approve ${p.name}\`.`)
219
- if (p.integrityChanged) report('P110', p.name, `\`${p.name}\`${p.lockedVersion && p.lockedVersion !== p.version ? ` went from ${p.lockedVersion} to ${p.version} and its` : `'s installed`} code isn't what the lock recorded.`, `Reinstall the locked version, or review the update and run \`weft plugins approve ${p.name}\`.`)
268
+ if (p.added.length)
269
+ report(
270
+ 'P109',
271
+ p.name,
272
+ `\`${p.name}\` asks for permissions the lock doesn't give it: ${p.added.join(', ')}.`,
273
+ approve,
274
+ )
275
+ if (p.hidden.length)
276
+ report(
277
+ 'P109',
278
+ p.name,
279
+ `\`${p.name}\` uses ${p.hidden.join(', ')} without declaring it (${p.uses.find((u) => p.hidden.includes(u.need))?.file ?? 'see `weft plugins audit`'}).`,
280
+ `Have it declared, or review it and run \`weft plugins approve ${p.name}\`.`,
281
+ )
282
+ if (p.integrityChanged)
283
+ report(
284
+ 'P110',
285
+ p.name,
286
+ `\`${p.name}\`${p.lockedVersion && p.lockedVersion !== p.version ? ` went from ${p.lockedVersion} to ${p.version} and its` : `'s installed`} code isn't what the lock recorded.`,
287
+ `Reinstall the locked version, or review the update and run \`weft plugins approve ${p.name}\`.`,
288
+ )
220
289
  }
221
290
  return out
222
291
  }
223
292
 
224
293
  /** Stops `weft dev` and `weft build` before a blocked plugin's build hooks run. */
225
294
  export function requireTrust(root, loaded) {
226
- if (loaded.diagnostics.some(d => d.code === 'P007')) return
227
- const blocked = trustDiagnostics(root, loaded).filter(d => d.severity === 'error')
228
- if (blocked.length) throw refused(blocked.map(d => `${d.code} ${d.why} ${d.fix.text}`).join('\n'))
295
+ if (loaded.diagnostics.some((d) => d.code === 'P007')) return
296
+ const blocked = trustDiagnostics(root, loaded).filter((d) => d.severity === 'error')
297
+ if (blocked.length)
298
+ throw refused(blocked.map((d) => `${d.code} ${d.why} ${d.fix.text}`).join('\n'))
229
299
  }
230
300
 
231
301
  /** `weft plugins audit --json`. */
232
302
  export function auditCommand(root, loaded) {
233
303
  const status = trustStatus(root, loaded, { full: true })
234
304
  const diagnostics = trustDiagnostics(root, loaded, status)
235
- return { ok: !diagnostics.some(d => d.severity === 'error'), ...status, diagnostics: [...loaded.diagnostics, ...diagnostics] }
305
+ return {
306
+ ok: !diagnostics.some((d) => d.severity === 'error'),
307
+ ...status,
308
+ diagnostics: [...loaded.diagnostics, ...diagnostics],
309
+ }
236
310
  }
237
311
 
238
312
  /** `.weft/` in a .gitignore hides the lock, and a lock nobody commits protects nobody. */
@@ -251,32 +325,70 @@ function unignoreLock(root) {
251
325
  * said out loud, one plugin at a time or all at once.
252
326
  */
253
327
  export function approveCommand(root, loaded, { names = [], all = false } = {}) {
254
- const failed = loaded.diagnostics.find(d => d.code === 'P007')
328
+ const failed = loaded.diagnostics.find((d) => d.code === 'P007')
255
329
  if (failed) throw refused(`The config doesn't load, so nothing can be approved: ${failed.why}`)
256
330
  const status = trustStatus(root, loaded, { full: true })
257
- if (status.broken) throw refused(`${lockFile} can't be read: ${status.broken}. Fix it, or delete it and approve each plugin again.`)
258
- const pending = status.plugins.filter(p => p.blocked)
259
- const unknown = names.filter(n => !status.plugins.some(p => p.name === n))
260
- if (unknown.length) throw Object.assign(new Error(`not in \`plugins\`: ${unknown.join(', ')}`), { code: 'E_USAGE' })
261
- if (!all && !names.length) return { ok: !pending.length, written: false, lock: lockFile, approved: [], pending, stale: status.stale }
331
+ if (status.broken)
332
+ throw refused(
333
+ `${lockFile} can't be read: ${status.broken}. Fix it, or delete it and approve each plugin again.`,
334
+ )
335
+ const pending = status.plugins.filter((p) => p.blocked)
336
+ const unknown = names.filter((n) => !status.plugins.some((p) => p.name === n))
337
+ if (unknown.length)
338
+ throw Object.assign(new Error(`not in \`plugins\`: ${unknown.join(', ')}`), { code: 'E_USAGE' })
339
+ if (!all && !names.length)
340
+ return {
341
+ ok: !pending.length,
342
+ written: false,
343
+ lock: lockFile,
344
+ approved: [],
345
+ pending,
346
+ stale: status.stale,
347
+ }
262
348
 
263
- const chosen = status.plugins.filter(p => all || names.includes(p.name))
349
+ const chosen = status.plugins.filter((p) => all || names.includes(p.name))
264
350
  const entries = { ...readLock(root).plugins }
265
351
  if (all) for (const name of status.stale) delete entries[name]
266
- for (const p of chosen) entries[p.name] = { version: p.version, integrity: p.integrity, permissions: p.permissions, provides: p.provides, undeclared: p.undeclared }
267
- const ordered = Object.fromEntries(Object.keys(entries).sort().map(name => [name, entries[name]]))
352
+ for (const p of chosen)
353
+ entries[p.name] = {
354
+ version: p.version,
355
+ integrity: p.integrity,
356
+ permissions: p.permissions,
357
+ provides: p.provides,
358
+ undeclared: p.undeclared,
359
+ }
360
+ const ordered = Object.fromEntries(
361
+ Object.keys(entries)
362
+ .sort()
363
+ .map((name) => [name, entries[name]]),
364
+ )
268
365
  mkdirSync(join(root, '.weft'), { recursive: true })
269
- writeFileSync(join(root, lockFile), `${JSON.stringify({ version: 1, plugins: ordered }, null, 2)}\n`)
270
- return { ok: true, written: true, lock: lockFile, approved: chosen, pending: pending.filter(p => !chosen.includes(p)), stale: all ? [] : status.stale, gitignore: unignoreLock(root) }
366
+ writeFileSync(
367
+ join(root, lockFile),
368
+ `${JSON.stringify({ version: 1, plugins: ordered }, null, 2)}\n`,
369
+ )
370
+ return {
371
+ ok: true,
372
+ written: true,
373
+ lock: lockFile,
374
+ approved: chosen,
375
+ pending: pending.filter((p) => !chosen.includes(p)),
376
+ stale: all ? [] : status.stale,
377
+ gitignore: unignoreLock(root),
378
+ }
271
379
  }
272
380
 
273
381
  /** One line per plugin for the terminal. */
274
382
  export function formatTrust(p) {
275
383
  const parts = [
276
- ...p.added.map(g => `+${g}`),
277
- ...p.removed.map(g => `-${g}`),
278
- ...p.hidden.map(n => `undeclared ${n}`),
279
- ...(p.integrityChanged ? [`code changed${p.lockedVersion && p.lockedVersion !== p.version ? ` (${p.lockedVersion} → ${p.version})` : ''}`] : []),
384
+ ...p.added.map((g) => `+${g}`),
385
+ ...p.removed.map((g) => `-${g}`),
386
+ ...p.hidden.map((n) => `undeclared ${n}`),
387
+ ...(p.integrityChanged
388
+ ? [
389
+ `code changed${p.lockedVersion && p.lockedVersion !== p.version ? ` (${p.lockedVersion} → ${p.version})` : ''}`,
390
+ ]
391
+ : []),
280
392
  ]
281
393
  return `${p.blocked ? '!' : ' '} ${p.name}@${p.version ?? '?'} ${parts.join(' ') || (p.locked ? 'approved' : 'not locked, asks for nothing')}`
282
394
  }
package/src/weft.mjs CHANGED
@@ -9,7 +9,10 @@ const own = createRequire(import.meta.url)
9
9
 
10
10
  /** The project's own weft wins; the CLI's weft (its dev/peer dependency) is the fallback. */
11
11
  export function resolveWeft(root) {
12
- const tries = [() => createRequire(join(root, 'package.json')).resolve('@weft-js/core/compiler'), () => own.resolve('@weft-js/core/compiler')]
12
+ const tries = [
13
+ () => createRequire(join(root, 'package.json')).resolve('@weft-js/core/compiler'),
14
+ () => own.resolve('@weft-js/core/compiler'),
15
+ ]
13
16
  for (const [i, attempt] of tries.entries()) {
14
17
  let path
15
18
  try {
@@ -22,7 +25,12 @@ export function resolveWeft(root) {
22
25
  const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'))
23
26
  return { from: i === 0 ? 'project' : 'cli', dir, version: pkg.version, pkg, compilerPath: path }
24
27
  }
25
- throw Object.assign(new Error('weft is not installed: `@weft-js/core/compiler` resolves neither from the project nor from @weft-js/cli'), { code: 'E_NO_WEFT' })
28
+ throw Object.assign(
29
+ new Error(
30
+ 'weft is not installed: `@weft-js/core/compiler` resolves neither from the project nor from @weft-js/cli',
31
+ ),
32
+ { code: 'E_NO_WEFT' },
33
+ )
26
34
  }
27
35
 
28
36
  const compilers = new Map()
@@ -34,7 +42,19 @@ export function loadCompiler(weft) {
34
42
  }
35
43
 
36
44
  /** Exported names per public subpath, read from the installed package's entry files (no execution). */
37
- export function exportsOf(weft, subpaths = ['.', './dom', './ssr', './test', './browser', './interaction', './style', './surface']) {
45
+ export function exportsOf(
46
+ weft,
47
+ subpaths = [
48
+ '.',
49
+ './dom',
50
+ './ssr',
51
+ './test',
52
+ './browser',
53
+ './interaction',
54
+ './style',
55
+ './surface',
56
+ ],
57
+ ) {
38
58
  const out = {}
39
59
  for (const sub of subpaths) {
40
60
  const target = weft.pkg.exports?.[sub]
@@ -42,20 +62,32 @@ export function exportsOf(weft, subpaths = ['.', './dom', './ssr', './test', './
42
62
  if (!file || !/\.[mc]?[jt]sx?$/.test(file) || !existsSync(file)) continue
43
63
  /** @type {Set<string>} */
44
64
  const names = new Set()
45
- const read = path => {
65
+ const read = (path) => {
46
66
  const sf = ts.createSourceFile(path, readFileSync(path, 'utf8'), ts.ScriptTarget.Latest, true)
47
67
  for (const node of sf.statements) {
48
- const exported = ts.canHaveModifiers(node) && ts.getModifiers(node)?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)
68
+ const exported =
69
+ ts.canHaveModifiers(node) &&
70
+ ts.getModifiers(node)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
49
71
  if (ts.isExportDeclaration(node) && !node.exportClause && !node.isTypeOnly) {
50
72
  // `export * from './index'`: the `weft` entry is a thin one over the runtime.
51
73
  const next = join(dirname(path), `${node.moduleSpecifier.text}.ts`)
52
74
  if (existsSync(next)) read(next)
53
- } else if (ts.isExportDeclaration(node) && node.exportClause && ts.isNamedExports(node.exportClause)) {
54
- if (!node.isTypeOnly) for (const el of node.exportClause.elements) if (!el.isTypeOnly) names.add(el.name.text)
55
- } else if (exported && (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) && node.name) {
75
+ } else if (
76
+ ts.isExportDeclaration(node) &&
77
+ node.exportClause &&
78
+ ts.isNamedExports(node.exportClause)
79
+ ) {
80
+ if (!node.isTypeOnly)
81
+ for (const el of node.exportClause.elements) if (!el.isTypeOnly) names.add(el.name.text)
82
+ } else if (
83
+ exported &&
84
+ (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) &&
85
+ node.name
86
+ ) {
56
87
  names.add(node.name.text)
57
88
  } else if (exported && ts.isVariableStatement(node)) {
58
- for (const d of node.declarationList.declarations) if (ts.isIdentifier(d.name)) names.add(d.name.text)
89
+ for (const d of node.declarationList.declarations)
90
+ if (ts.isIdentifier(d.name)) names.add(d.name.text)
59
91
  }
60
92
  }
61
93
  }