@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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import { dirname, join } from 'node:path'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
import { database } from '../map.mjs'
|
|
6
|
+
import { dbLayer } from '../db-layer.mjs'
|
|
7
|
+
import { refused } from './errors.mjs'
|
|
8
|
+
|
|
9
|
+
/** The plugin's `setup`, once the package is installed: a plain import, since the package can't run anything by being read. */
|
|
10
|
+
export async function setupOf(root, pkg) {
|
|
11
|
+
let path
|
|
12
|
+
try {
|
|
13
|
+
path = createRequire(join(root, 'package.json')).resolve(`${pkg}/plugin`)
|
|
14
|
+
} catch {
|
|
15
|
+
return undefined // no plugin entry, or a dry run before the install
|
|
16
|
+
}
|
|
17
|
+
const mod = await import(pathToFileURL(path).href)
|
|
18
|
+
const make = mod.default ?? Object.values(mod).find((v) => typeof v === 'function')
|
|
19
|
+
return make?.().contribute({ command: 'check', mode: 'development', root }).setup
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** What `setup` wants, checked in full before anything is written, so a refusal changes nothing. */
|
|
23
|
+
export async function planSetup(root, setup) {
|
|
24
|
+
const layer = await dbLayer(root)
|
|
25
|
+
if (setup.needs?.includes('db') && !layer)
|
|
26
|
+
throw refused('This needs a database: run `weft add db` first. Nothing was changed.')
|
|
27
|
+
if (setup.needs?.includes('auth') && !existsSync(join(root, 'src/auth.server.ts')))
|
|
28
|
+
throw refused('This needs users: run `weft add auth` first. Nothing was changed.')
|
|
29
|
+
const read = (rel) =>
|
|
30
|
+
existsSync(join(root, rel)) ? readFileSync(join(root, rel), 'utf8') : undefined
|
|
31
|
+
let plan
|
|
32
|
+
try {
|
|
33
|
+
plan = setup.run({
|
|
34
|
+
engine: layer ? database(root, layer).engine : null,
|
|
35
|
+
layer: layer?.layer ?? null,
|
|
36
|
+
read,
|
|
37
|
+
})
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw refused(`${error.message}. Nothing was changed.`)
|
|
40
|
+
}
|
|
41
|
+
const taken = Object.keys(plan.files ?? {}).filter((f) => existsSync(join(root, f)))
|
|
42
|
+
if (taken.length) throw refused(`${taken.join(', ')} already exist. Nothing was changed.`)
|
|
43
|
+
return plan
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function applySetup(root, plan) {
|
|
47
|
+
for (const [file, text] of Object.entries({ ...plan.files, ...plan.edits })) {
|
|
48
|
+
mkdirSync(dirname(join(root, file)), { recursive: true })
|
|
49
|
+
writeFileSync(join(root, file), text)
|
|
50
|
+
}
|
|
51
|
+
for (const name of ['.env', '.env.example']) {
|
|
52
|
+
const file = join(root, name)
|
|
53
|
+
const had = existsSync(file) ? readFileSync(file, 'utf8') : ''
|
|
54
|
+
const lines = Object.entries(plan.env ?? {})
|
|
55
|
+
.filter(([k]) => !had.includes(`${k}=`))
|
|
56
|
+
.map(([k, v]) => `${k}=${v}\n`)
|
|
57
|
+
if (lines.length) writeFileSync(file, `${had && `${had.trimEnd()}\n`}${lines.join('')}`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const setupActions = (plan) => [
|
|
62
|
+
...Object.keys({ ...plan.files, ...plan.edits }).map((f) => `write ${f}`),
|
|
63
|
+
...(plan.env ? [`add ${Object.keys(plan.env).join(', ')} to .env and .env.example`] : []),
|
|
64
|
+
...(plan.next ?? []).map((n) => `next: ${n}`),
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
export function authEdits({ root, files, context, tables, envText, layer }) {
|
|
68
|
+
const edits = { ...files, 'src/context.server.ts': context }
|
|
69
|
+
if (!envText.includes('BETTER_AUTH_SECRET'))
|
|
70
|
+
edits['src/env.server.ts'] = envText.replace(
|
|
71
|
+
'server: {\n',
|
|
72
|
+
'server: {\n BETTER_AUTH_SECRET: z.string().min(32),\n',
|
|
73
|
+
)
|
|
74
|
+
if (layer.layer === 'drizzle') {
|
|
75
|
+
edits['src/db/auth.schema.ts'] = tables
|
|
76
|
+
edits['src/db/schema.ts'] =
|
|
77
|
+
`${readFileSync(join(root, 'src/db/schema.ts'), 'utf8').trimEnd()}\n\nexport * from './auth.schema'\n`
|
|
78
|
+
} else if (layer.layer === 'prisma') {
|
|
79
|
+
edits['prisma/schema.prisma'] =
|
|
80
|
+
`${readFileSync(join(root, 'prisma/schema.prisma'), 'utf8').trimEnd()}\n${tables}`
|
|
81
|
+
} else {
|
|
82
|
+
const next = existsSync(join(root, 'migrations'))
|
|
83
|
+
? readdirSync(join(root, 'migrations')).filter((f) => f.endsWith('.sql')).length
|
|
84
|
+
: 0
|
|
85
|
+
edits[`migrations/${String(next).padStart(4, '0')}_auth.sql`] = tables
|
|
86
|
+
}
|
|
87
|
+
return edits
|
|
88
|
+
}
|
package/src/add-rust.mjs
CHANGED
|
@@ -6,28 +6,39 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
|
6
6
|
import { dirname, join, relative } from 'node:path'
|
|
7
7
|
import { start } from './start.mjs'
|
|
8
8
|
|
|
9
|
-
const refused = message => Object.assign(new Error(message), { code: 'E_ADD' })
|
|
10
|
-
const usage = message => Object.assign(new Error(message), { code: 'E_USAGE' })
|
|
9
|
+
const refused = (message) => Object.assign(new Error(message), { code: 'E_ADD' })
|
|
10
|
+
const usage = (message) => Object.assign(new Error(message), { code: 'E_USAGE' })
|
|
11
11
|
const TOOLCHAIN = '1.99.1'
|
|
12
12
|
|
|
13
|
-
const camel = name => name.replace(/[-_](.)/g, (_, c) => c.toUpperCase())
|
|
14
|
-
const pascal = name => {
|
|
15
|
-
const
|
|
13
|
+
const camel = (name) => name.replace(/[-_](.)/g, (_, c) => c.toUpperCase())
|
|
14
|
+
const pascal = (name) => {
|
|
15
|
+
const c = camel(name)
|
|
16
|
+
return c[0].toUpperCase() + c.slice(1)
|
|
17
|
+
}
|
|
18
|
+
const lib = (name) => name.replaceAll('-', '_')
|
|
16
19
|
|
|
17
20
|
const manifest = (name, crate, remote) => `[package]
|
|
18
21
|
name = "${name}"
|
|
19
22
|
version = "0.0.0"
|
|
20
23
|
edition = "2024"
|
|
21
24
|
publish = false
|
|
22
|
-
${
|
|
25
|
+
${
|
|
26
|
+
remote
|
|
27
|
+
? ''
|
|
28
|
+
: `
|
|
23
29
|
[lib]
|
|
24
30
|
crate-type = ["cdylib"]
|
|
25
|
-
`
|
|
31
|
+
`
|
|
32
|
+
}
|
|
26
33
|
[dependencies]
|
|
27
34
|
serde = { version = "1", features = ["derive"] }
|
|
28
35
|
serde_json = "1"
|
|
29
|
-
${
|
|
30
|
-
|
|
36
|
+
${
|
|
37
|
+
remote
|
|
38
|
+
? `tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
|
|
39
|
+
weft = { path = "${crate}" }`
|
|
40
|
+
: `weft = { path = "${crate}", default-features = false, features = ["native"] }`
|
|
41
|
+
}
|
|
31
42
|
|
|
32
43
|
[build-dependencies]
|
|
33
44
|
weft = { path = "${crate}", default-features = false, features = ["build"] }
|
|
@@ -43,7 +54,10 @@ codegen-units = 1
|
|
|
43
54
|
[workspace]
|
|
44
55
|
`
|
|
45
56
|
|
|
46
|
-
const body = (
|
|
57
|
+
const body = (
|
|
58
|
+
name,
|
|
59
|
+
remote,
|
|
60
|
+
) => `weft::contract!(); // types and a \`Service\` trait, generated from your Zod: never hand-written
|
|
47
61
|
use contract::*;
|
|
48
62
|
|
|
49
63
|
struct ${pascal(name)};
|
|
@@ -54,14 +68,18 @@ impl Service for ${pascal(name)} {
|
|
|
54
68
|
Ok(DigestOutput { bytes: input.text.len() as i64, hash: format!("{hash:08x}") })
|
|
55
69
|
}
|
|
56
70
|
}
|
|
57
|
-
${
|
|
71
|
+
${
|
|
72
|
+
remote
|
|
73
|
+
? `
|
|
58
74
|
#[tokio::main]
|
|
59
75
|
async fn main() -> std::io::Result<()> {
|
|
60
76
|
weft::serve(weft::router(contract::functions(${pascal(name)}))).await
|
|
61
77
|
}
|
|
62
|
-
`
|
|
78
|
+
`
|
|
79
|
+
: `
|
|
63
80
|
weft::native!(contract::functions(${pascal(name)}));
|
|
64
|
-
`
|
|
81
|
+
`
|
|
82
|
+
}`
|
|
65
83
|
|
|
66
84
|
const fn = (name, binding) => `import { query } from '@weft-js/start/data'
|
|
67
85
|
import { z } from 'zod'
|
|
@@ -78,33 +96,63 @@ export const digest = query({
|
|
|
78
96
|
/** The path Cargo gets. `crate/` is in a packed @weft-js/start; a linked one is in this repository, next to the crates. */
|
|
79
97
|
async function weftCrate(root, dir) {
|
|
80
98
|
const { startDir } = await start(root, 'vite')
|
|
81
|
-
const found = [
|
|
99
|
+
const found = [
|
|
100
|
+
join(startDir, '..', 'crate'),
|
|
101
|
+
join(startDir, '..', '..', '..', 'crates', 'weft'),
|
|
102
|
+
].find((d) => existsSync(join(d, 'Cargo.toml')))
|
|
82
103
|
if (!found) throw refused('this @weft-js/start has no `weft` crate in it. Nothing was changed.')
|
|
83
104
|
return relative(dir, found).replaceAll('\\', '/')
|
|
84
105
|
}
|
|
85
106
|
|
|
86
|
-
export async function addRust(
|
|
87
|
-
|
|
88
|
-
|
|
107
|
+
export async function addRust(
|
|
108
|
+
root,
|
|
109
|
+
name = 'media',
|
|
110
|
+
{ remote = false, dryRun = false, install, installSpecs },
|
|
111
|
+
) {
|
|
112
|
+
if (!/^[a-z][a-z0-9_-]*$/.test(name))
|
|
113
|
+
throw usage(
|
|
114
|
+
`\`${name}\` isn't a service name: lower case, then letters, digits, \`-\` or \`_\``,
|
|
115
|
+
)
|
|
116
|
+
if (!existsSync(join(root, 'package.json')))
|
|
117
|
+
throw refused('`weft add rust` needs a package.json. Nothing was changed.')
|
|
89
118
|
const dir = join(root, 'crates', name)
|
|
90
119
|
const module = join(root, 'src', 'server', `${name}.ts`)
|
|
91
|
-
const taken = [dir, module].filter(existsSync).map(f => relative(root, f))
|
|
92
|
-
if (taken.length)
|
|
120
|
+
const taken = [dir, module].filter(existsSync).map((f) => relative(root, f))
|
|
121
|
+
if (taken.length)
|
|
122
|
+
throw refused(
|
|
123
|
+
`${taken.join(', ')} already exist${taken.length > 1 ? '' : 's'}. Pick another name: \`weft add rust <name>\`. Nothing was changed.`,
|
|
124
|
+
)
|
|
93
125
|
const binding = camel(name)
|
|
94
126
|
const factory = remote ? 'remote' : 'native'
|
|
95
127
|
const call = remote ? `remote('rust://${name}')` : `native('${name}')`
|
|
96
128
|
const rustFile = join(root, 'src', 'server', 'rust.ts')
|
|
97
|
-
const had = existsSync(rustFile)
|
|
98
|
-
|
|
99
|
-
|
|
129
|
+
const had = existsSync(rustFile)
|
|
130
|
+
? readFileSync(rustFile, 'utf8')
|
|
131
|
+
: '// The only place Rust targets are named.\n'
|
|
132
|
+
if (new RegExp(`\\b${binding}\\b`).test(had))
|
|
133
|
+
throw refused(`src/server/rust.ts already names \`${binding}\`. Nothing was changed.`)
|
|
134
|
+
const imported = new RegExp(
|
|
135
|
+
`import\\s*\\{[^}]*\\b${factory}\\b[^}]*\\}\\s*from\\s*['"]@weft-js/start/rust['"]`,
|
|
136
|
+
).test(had)
|
|
100
137
|
const nextRust = `${imported ? had : had.replace(/^((?:\/\/.*\n)*)/, `$1import { ${factory} } from '@weft-js/start/rust'\n`)}${had.endsWith('\n') ? '' : '\n'}\nexport const ${binding} = ${call}\n`
|
|
101
138
|
|
|
102
139
|
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
|
103
140
|
const needsZod = !pkg.dependencies?.zod && !pkg.devDependencies?.zod
|
|
104
141
|
const gitignore = join(root, '.gitignore')
|
|
105
|
-
const ignored =
|
|
106
|
-
|
|
107
|
-
|
|
142
|
+
const ignored =
|
|
143
|
+
existsSync(gitignore) &&
|
|
144
|
+
/^\/?target\/?$|^\*\*\/target\/?$/m.test(readFileSync(gitignore, 'utf8'))
|
|
145
|
+
const actions = [
|
|
146
|
+
`create crates/${name}`,
|
|
147
|
+
`create src/server/${name}.ts`,
|
|
148
|
+
`add \`${binding}\` to src/server/rust.ts`,
|
|
149
|
+
...(needsZod && install ? ['install zod'] : []),
|
|
150
|
+
...(ignored ? [] : ['ignore target/']),
|
|
151
|
+
'this crate needs Rust (`cargo`, from rustup.rs); the rest of weft does not',
|
|
152
|
+
'next: `weft check`, then `weft dev`',
|
|
153
|
+
]
|
|
154
|
+
if (dryRun)
|
|
155
|
+
return { ok: true, feature: 'rust', service: name, kind: factory, actions, dryRun: true }
|
|
108
156
|
|
|
109
157
|
const crate = await weftCrate(root, dir)
|
|
110
158
|
const write = (file, text) => {
|
|
@@ -113,11 +161,18 @@ export async function addRust(root, name = 'media', { remote = false, dryRun = f
|
|
|
113
161
|
}
|
|
114
162
|
write(join(dir, 'Cargo.toml'), manifest(name, crate, remote))
|
|
115
163
|
write(join(dir, 'build.rs'), `fn main() {\n weft::build::generate("${name}");\n}\n`)
|
|
116
|
-
write(
|
|
164
|
+
write(
|
|
165
|
+
join(dir, 'rust-toolchain.toml'),
|
|
166
|
+
`[toolchain]\nchannel = "${TOOLCHAIN}"\n${remote ? '' : 'targets = ["wasm32-unknown-unknown"]\n'}`,
|
|
167
|
+
)
|
|
117
168
|
write(join(dir, 'src', remote ? 'main.rs' : 'lib.rs'), body(lib(name), remote))
|
|
118
169
|
write(module, fn(name, binding))
|
|
119
170
|
write(rustFile, nextRust)
|
|
120
|
-
if (!ignored)
|
|
171
|
+
if (!ignored)
|
|
172
|
+
writeFileSync(
|
|
173
|
+
gitignore,
|
|
174
|
+
`${existsSync(gitignore) ? readFileSync(gitignore, 'utf8').replace(/\n?$/, '\n') : ''}target/\n`,
|
|
175
|
+
)
|
|
121
176
|
if (needsZod && install) installSpecs(['zod'])
|
|
122
177
|
return { ok: true, feature: 'rust', service: name, kind: factory, actions }
|
|
123
178
|
}
|