@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.
- package/README.md +534 -54
- package/bin/weft.mjs +8 -1
- package/docs/async.md +16 -6
- package/docs/browser.md +22 -8
- package/docs/config.md +39 -9
- package/docs/core.md +30 -0
- package/docs/deploy.md +33 -8
- package/docs/design.md +12 -3
- package/docs/entrypoints.md +79 -0
- package/docs/fonts.md +12 -2
- package/docs/icons.md +13 -7
- package/docs/lists.md +3 -1
- package/docs/meta.md +13 -3
- package/docs/plugins.md +32 -9
- package/docs/props.md +13 -2
- package/docs/reactivity.md +26 -0
- package/docs/rules.md +19 -12
- package/docs/rust.md +47 -17
- package/docs/start-short.md +3 -2
- package/docs/start.md +226 -52
- package/docs/state.md +33 -7
- package/docs/style.md +31 -8
- package/docs/surface.md +61 -15
- package/docs/testing.md +15 -5
- package/docs/theme.md +182 -0
- package/docs/ui-state.md +19 -6
- package/docs/verify.md +13 -0
- package/package.json +13 -4
- package/src/add/config.mjs +93 -0
- package/src/add/errors.mjs +2 -0
- package/src/add/setup.mjs +88 -0
- package/src/add-rust.mjs +82 -27
- package/src/add.mjs +209 -181
- package/src/agents-md.mjs +144 -43
- package/src/alias.mjs +16 -5
- package/src/audit/page.mjs +313 -0
- package/src/audit/report.mjs +69 -0
- package/src/audit/utils.mjs +5 -0
- package/src/audit.mjs +159 -206
- package/src/browser/cli.mjs +24 -5
- package/src/browser/client.mjs +189 -25
- package/src/browser/commands.mjs +166 -51
- package/src/catalog/audit.mjs +129 -0
- package/src/catalog/auth.mjs +40 -0
- package/src/catalog/compiler.mjs +337 -0
- package/src/catalog/contracts.mjs +18 -0
- package/src/catalog/database.mjs +52 -0
- package/src/catalog/deploy.mjs +117 -0
- package/src/catalog/design.mjs +159 -0
- package/src/catalog/forms.mjs +25 -0
- package/src/catalog/hydration.mjs +29 -0
- package/src/catalog/icons.mjs +24 -0
- package/src/catalog/media.mjs +163 -0
- package/src/catalog/metadata.mjs +116 -0
- package/src/catalog/plugins.mjs +228 -0
- package/src/catalog/realtime.mjs +195 -0
- package/src/catalog/rust.mjs +75 -0
- package/src/catalog/server-functions.mjs +92 -0
- package/src/catalog/state.mjs +39 -0
- package/src/catalog/style-groups.mjs +54 -0
- package/src/catalog/tests.mjs +127 -0
- package/src/catalog/text.mjs +200 -0
- package/src/catalog/ui-state.mjs +42 -0
- package/src/catalog.mjs +67 -460
- package/src/check/runtime.mjs +280 -0
- package/src/check.mjs +198 -208
- package/src/cli/authoring.mjs +213 -0
- package/src/cli/project.mjs +302 -0
- package/src/cli.mjs +222 -250
- package/src/config.mjs +1 -1
- package/src/contributions.mjs +75 -25
- package/src/db-layer.mjs +13 -5
- package/src/deploy/cdn.mjs +94 -16
- package/src/deploy/files.mjs +100 -21
- package/src/deploy/fit.mjs +127 -23
- package/src/deploy/hosts.mjs +90 -20
- package/src/deploy/index/cli.mjs +20 -0
- package/src/deploy/index/environment.mjs +69 -0
- package/src/deploy/index/steps.mjs +87 -0
- package/src/deploy/index.mjs +228 -114
- package/src/deploy/needs.mjs +88 -31
- package/src/design-checks/accessible-names.mjs +62 -0
- package/src/design-checks/contrast.mjs +43 -0
- package/src/design-checks/csp.mjs +60 -0
- package/src/design-checks/files.mjs +59 -0
- package/src/design-checks/heading-links.mjs +61 -0
- package/src/design-checks/interaction.mjs +65 -0
- package/src/design-checks/jsx.mjs +67 -0
- package/src/design-checks/layout-values.mjs +68 -0
- package/src/design-checks/native-controls.mjs +48 -0
- package/src/design-checks/outline.mjs +39 -0
- package/src/design-checks/reporter.mjs +37 -0
- package/src/design-checks/rules.mjs +274 -0
- package/src/design-checks/values.mjs +262 -0
- package/src/design-checks.mjs +10 -400
- package/src/diag.mjs +32 -8
- package/src/docs.mjs +138 -43
- package/src/doctor.mjs +20 -8
- package/src/files.mjs +18 -5
- package/src/fix.mjs +10 -2
- package/src/gate/after.mjs +40 -0
- package/src/gate/before.mjs +54 -0
- package/src/gate/classify.mjs +72 -0
- package/src/gate/mutation.mjs +209 -0
- package/src/gate/resolve-cases.mjs +22 -0
- package/src/gate/result.mjs +63 -0
- package/src/gate/run.mjs +75 -0
- package/src/gate.mjs +104 -202
- package/src/infer/box-props.cjs +140 -0
- package/src/infer/core-boxes.cjs +46 -0
- package/src/infer/derived-writes.cjs +95 -0
- package/src/infer/engine/checks.cjs +193 -0
- package/src/infer/engine/flow.cjs +293 -0
- package/src/infer/engine/report.cjs +158 -0
- package/src/infer/engine/utils.cjs +15 -0
- package/src/infer/engine.cjs +108 -368
- package/src/infer/facts/sites.cjs +114 -0
- package/src/infer/facts.cjs +210 -117
- package/src/infer/hosts.cjs +69 -22
- package/src/infer/lsp.mjs +149 -40
- package/src/infer/session.cjs +73 -17
- package/src/infer/shadow.cjs +13 -5
- package/src/infer/worker.cjs +57 -9
- package/src/jsx-scan.mjs +63 -21
- package/src/load-config/schema.mjs +281 -0
- package/src/load-config.mjs +137 -138
- package/src/map.mjs +93 -26
- package/src/mcp/browser-tools.mjs +155 -0
- package/src/mcp.mjs +147 -69
- package/src/meta-table.mjs +43 -8
- package/src/mutate.mjs +120 -36
- package/src/plugin-checks.mjs +52 -23
- package/src/plugins.mjs +125 -39
- package/src/project.mjs +170 -79
- package/src/props-checks.mjs +24 -6
- package/src/props-listener.mjs +27 -9
- package/src/react-checks.mjs +35 -4
- package/src/server-state-checks.mjs +34 -0
- package/src/share-checks.mjs +121 -37
- package/src/site.mjs +33 -6
- package/src/slots.mjs +104 -19
- package/src/start/build.mjs +116 -0
- package/src/start/routes.mjs +38 -0
- package/src/start.mjs +193 -95
- package/src/state-checks/core-state.mjs +223 -0
- package/src/state-checks/modules.mjs +17 -0
- package/src/state-checks/scan.mjs +232 -0
- package/src/state-checks/server-graph.mjs +65 -0
- package/src/state-checks.mjs +109 -133
- package/src/style/find.mjs +121 -0
- package/src/style.mjs +133 -127
- package/src/surface-checks.mjs +212 -46
- package/src/tailwind.mjs +5 -4
- package/src/tasks.mjs +189 -27
- package/src/test-rules/context.mjs +109 -0
- package/src/test-rules/file-rules.mjs +279 -0
- package/src/test-rules/rule-helpers.mjs +244 -0
- package/src/test-rules/syntax.mjs +223 -0
- package/src/test-rules/test-rules.mjs +127 -0
- package/src/testdiff.mjs +94 -32
- package/src/tests.mjs +31 -483
- package/src/text-checks.mjs +229 -69
- package/src/tokens.mjs +12 -4
- package/src/trust/scan.mjs +95 -0
- package/src/trust.mjs +214 -102
- package/src/weft.mjs +41 -9
- package/src/workspace.mjs +96 -19
- package/ts-plugin/context.js +57 -0
- package/ts-plugin/index.js +4 -124
- package/ts-plugin/mapping.js +65 -0
- 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 {
|
|
7
|
-
|
|
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([
|
|
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)
|
|
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 =
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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 =
|
|
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(
|
|
120
|
-
|
|
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}/`)
|
|
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}/`))
|
|
136
|
-
|
|
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}/`))
|
|
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(
|
|
162
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
192
|
-
|
|
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,
|
|
201
|
-
|
|
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 {
|
|
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) =>
|
|
214
|
-
|
|
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)
|
|
218
|
-
|
|
219
|
-
|
|
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)
|
|
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 {
|
|
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)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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)
|
|
267
|
-
|
|
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(
|
|
270
|
-
|
|
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
|
|
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 = [
|
|
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(
|
|
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(
|
|
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 =
|
|
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 (
|
|
54
|
-
|
|
55
|
-
|
|
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)
|
|
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
|
}
|