@stacksjs/defaults 0.75.62 → 0.75.64
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/app/Middleware/Throttle.ts +52 -13
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/ide/vscode/build.ts +0 -52
- package/ide/vscode/scripts/package.ts +0 -71
- package/ide/vscode/scripts/stx-contributes.ts +0 -78
- package/ide/vscode/scripts/ts-plugin.ts +0 -72
- package/ide/vscode/src/env-file.ts +0 -103
- package/ide/vscode/src/env.ts +0 -241
- package/ide/vscode/src/extension.ts +0 -163
- package/ide/vscode/src/pickier-protocol.ts +0 -51
- package/ide/vscode/src/pickier-worker.ts +0 -92
- package/ide/vscode/src/pickier.ts +0 -433
- package/ide/vscode/src/project.ts +0 -450
- package/ide/vscode/src/stacks.ts +0 -482
- package/ide/vscode/tests/build.test.ts +0 -82
- package/ide/vscode/tests/env.test.ts +0 -166
- package/ide/vscode/tests/extension.test.ts +0 -428
- package/ide/vscode/tests/fake-vscode.ts +0 -228
- package/ide/vscode/tests/manifest.test.ts +0 -141
- package/ide/vscode/tests/package.test.ts +0 -47
- package/ide/vscode/tests/pickier.test.ts +0 -202
- package/ide/vscode/tests/project.test.ts +0 -268
- package/ide/vscode/tests/ts-plugin-harness.ts +0 -39
- package/ide/vscode/tsconfig.json +0 -19
package/ide/vscode/src/env.ts
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stacks env files, built in: decrypted values on hover, keys missing against
|
|
3
|
-
* `.env.example`, and the `buddy env:*` commands from the Command Palette.
|
|
4
|
-
*
|
|
5
|
-
* Plaintext only ever exists in memory and on screen. The hover decrypts with
|
|
6
|
-
* `@stacksjs/env`'s own code (./env-file.ts) and nothing is cached, logged or
|
|
7
|
-
* written; the commands run the project's `./buddy` directly (no shell, so a
|
|
8
|
-
* value typed for `env:set` does not land in shell history).
|
|
9
|
-
*/
|
|
10
|
-
import type * as vscode from 'vscode'
|
|
11
|
-
import { basename, dirname, join, relative } from 'node:path'
|
|
12
|
-
import { decryptEnvValue, envEntryAt, isEncrypted, isEnvFile, missingKeys } from './env-file'
|
|
13
|
-
|
|
14
|
-
type Vscode = typeof vscode
|
|
15
|
-
|
|
16
|
-
export interface EnvHost {
|
|
17
|
-
exists: (path: string) => boolean
|
|
18
|
-
read: (path: string) => string | undefined
|
|
19
|
-
exec: (file: string, args: string[], cwd: string) => Promise<string>
|
|
20
|
-
environment: Record<string, string | undefined>
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export const ENV_COMMANDS = {
|
|
24
|
-
get: 'stacks.env.get',
|
|
25
|
-
set: 'stacks.env.set',
|
|
26
|
-
encrypt: 'stacks.env.encrypt',
|
|
27
|
-
decrypt: 'stacks.env.decrypt',
|
|
28
|
-
rotate: 'stacks.env.rotate',
|
|
29
|
-
} as const
|
|
30
|
-
|
|
31
|
-
function isProject(host: EnvHost, folder: string): boolean {
|
|
32
|
-
return host.exists(join(folder, 'buddy')) || host.exists(join(folder, 'config', 'app.ts'))
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* The `.env.keys` for an env file: beside it, else at the project root, where
|
|
37
|
-
* `./buddy env:*` looks for it.
|
|
38
|
-
*/
|
|
39
|
-
export function keysFileFor(host: EnvHost, envFile: string, root: string | undefined): string | undefined {
|
|
40
|
-
for (const dir of [dirname(envFile), root]) {
|
|
41
|
-
if (!dir)
|
|
42
|
-
continue
|
|
43
|
-
const source = host.read(join(dir, '.env.keys'))
|
|
44
|
-
if (source !== undefined)
|
|
45
|
-
return source
|
|
46
|
-
}
|
|
47
|
-
return undefined
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* A decryptor for `loadProjectEnv`, so the preview can read an encrypted
|
|
52
|
-
* `APP_URL` or `PORT` with the same keys the hover uses.
|
|
53
|
-
*/
|
|
54
|
-
export function projectDecryptor(host: EnvHost, root: string): (file: string, value: string) => string | undefined {
|
|
55
|
-
return (file, value) => {
|
|
56
|
-
const path = join(root, file)
|
|
57
|
-
const result = decryptEnvValue(path, value, keysFileFor(host, path, root), host.environment)
|
|
58
|
-
return result.ok ? result.value : undefined
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function createEnvSupport(api: Vscode, host: EnvHost): {
|
|
63
|
-
activate: (context: { subscriptions: Array<{ dispose: () => unknown }> }) => void
|
|
64
|
-
} {
|
|
65
|
-
const diagnostics = api.languages.createDiagnosticCollection('stacks-env')
|
|
66
|
-
const settings = () => api.workspace.getConfiguration('stacks')
|
|
67
|
-
|
|
68
|
-
function rootFor(uri: vscode.Uri): string | undefined {
|
|
69
|
-
const folder = api.workspace.getWorkspaceFolder(uri)?.uri.fsPath
|
|
70
|
-
return folder && isProject(host, folder) ? folder : undefined
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function projectRoot(): string | undefined {
|
|
74
|
-
const active = api.window.activeTextEditor?.document.uri
|
|
75
|
-
const fromActive = active ? rootFor(active) : undefined
|
|
76
|
-
if (fromActive)
|
|
77
|
-
return fromActive
|
|
78
|
-
return (api.workspace.workspaceFolders ?? []).map(folder => folder.uri.fsPath).find(folder => isProject(host, folder))
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const hover: vscode.HoverProvider = {
|
|
82
|
-
provideHover(document, position) {
|
|
83
|
-
if (!isEnvFile(document.uri.fsPath) || !settings().get<boolean>('env.decryptOnHover', true))
|
|
84
|
-
return undefined
|
|
85
|
-
|
|
86
|
-
const entry = envEntryAt(document.lineAt(position.line).text)
|
|
87
|
-
if (!entry || !isEncrypted(entry.value) || position.character < entry.start - 1 || position.character > entry.end + 1)
|
|
88
|
-
return undefined
|
|
89
|
-
|
|
90
|
-
const root = rootFor(document.uri)
|
|
91
|
-
const result = decryptEnvValue(document.uri.fsPath, entry.value, keysFileFor(host, document.uri.fsPath, root), host.environment)
|
|
92
|
-
const markdown = new api.MarkdownString()
|
|
93
|
-
const range = new api.Range(position.line, entry.start, position.line, entry.end)
|
|
94
|
-
|
|
95
|
-
if (result.ok) {
|
|
96
|
-
markdown.appendMarkdown(`**${entry.key}**, decrypted with \`${result.key.name}\` from ${result.key.from === '.env.keys' ? '`.env.keys`' : 'the environment'}:`)
|
|
97
|
-
markdown.appendCodeblock(result.value, 'text')
|
|
98
|
-
}
|
|
99
|
-
else if (result.reason === 'no-key') {
|
|
100
|
-
markdown.appendMarkdown(`**${entry.key}** is encrypted. No private key to decrypt it: looked for ${result.looked.map(name => `\`${name}\``).join(' and ')} in \`.env.keys\` and the environment.`)
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
markdown.appendMarkdown(`**${entry.key}** did not decrypt with \`${result.key.name}\`. The value or the key is wrong, or it was encrypted for another key.`)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return new api.Hover(markdown, range)
|
|
107
|
-
},
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function checkMissingKeys(document: vscode.TextDocument): void {
|
|
111
|
-
const path = document.uri.fsPath
|
|
112
|
-
if (basename(path) === '.env.example') {
|
|
113
|
-
const env = api.workspace.textDocuments.find(open => open.uri.fsPath === join(dirname(path), '.env'))
|
|
114
|
-
if (env)
|
|
115
|
-
checkMissingKeys(env)
|
|
116
|
-
return
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (basename(path) !== '.env' || !settings().get<boolean>('env.missingKeys', true)) {
|
|
120
|
-
diagnostics.delete(document.uri)
|
|
121
|
-
return
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const example = host.read(join(dirname(path), '.env.example'))
|
|
125
|
-
if (example === undefined) {
|
|
126
|
-
diagnostics.delete(document.uri)
|
|
127
|
-
return
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const top = new api.Range(0, 0, 0, Math.max(document.lineAt(0).text.length, 1))
|
|
131
|
-
diagnostics.set(document.uri, missingKeys(example, document.getText()).map((key) => {
|
|
132
|
-
const diagnostic = new api.Diagnostic(top, `${key} is in .env.example but not set here.`, api.DiagnosticSeverity.Information)
|
|
133
|
-
diagnostic.source = 'stacks'
|
|
134
|
-
diagnostic.code = 'missing-env-key'
|
|
135
|
-
return diagnostic
|
|
136
|
-
}))
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** The env file a command acts on: the open one, or `.env`. */
|
|
140
|
-
function targetFile(root: string): string {
|
|
141
|
-
const active = api.window.activeTextEditor?.document.uri.fsPath
|
|
142
|
-
return active && isEnvFile(active) && !relative(root, active).startsWith('..') ? relative(root, active) : '.env'
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async function buddy(root: string, args: string[]): Promise<string | undefined> {
|
|
146
|
-
try {
|
|
147
|
-
return await host.exec(join(root, 'buddy'), args, root)
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
// The error carries the command's stderr, never the value: values are
|
|
151
|
-
// only ever arguments, and execFile does not echo arguments.
|
|
152
|
-
void api.window.showErrorMessage(`Stacks: ./buddy ${args[0]} failed. ${error instanceof Error ? error.message.split('\n')[0] : ''}`)
|
|
153
|
-
return undefined
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function askKey(root: string, file: string, prompt: string): Promise<string | undefined> {
|
|
158
|
-
const keys = Object.keys(parseKeys(host.read(join(root, file))))
|
|
159
|
-
if (keys.length) {
|
|
160
|
-
const picked = await api.window.showQuickPick(keys, { title: `${prompt} (${file})`, placeHolder: 'Key' })
|
|
161
|
-
return picked
|
|
162
|
-
}
|
|
163
|
-
return (await api.window.showInputBox({ title: `${prompt} (${file})`, prompt: 'Key' }))?.trim() || undefined
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function parseKeys(source: string | undefined): Record<string, true> {
|
|
167
|
-
const keys: Record<string, true> = {}
|
|
168
|
-
for (const line of (source ?? '').split(/\r?\n/)) {
|
|
169
|
-
const entry = envEntryAt(line)
|
|
170
|
-
if (entry && !entry.key.startsWith('DOTENV_PUBLIC_KEY'))
|
|
171
|
-
keys[entry.key] = true
|
|
172
|
-
}
|
|
173
|
-
return keys
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async function getValue(): Promise<void> {
|
|
177
|
-
const root = projectRoot()
|
|
178
|
-
if (!root)
|
|
179
|
-
return
|
|
180
|
-
const file = targetFile(root)
|
|
181
|
-
const key = await askKey(root, file, 'Get an env value')
|
|
182
|
-
if (!key)
|
|
183
|
-
return
|
|
184
|
-
const value = await buddy(root, ['env:get', key, '--file', file])
|
|
185
|
-
if (value === undefined)
|
|
186
|
-
return
|
|
187
|
-
const choice = await api.window.showInformationMessage(`${key} is set in ${file}.`, 'Copy Value')
|
|
188
|
-
if (choice === 'Copy Value')
|
|
189
|
-
await api.env.clipboard.writeText(value.replace(/\r?\n$/, ''))
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
async function setValue(): Promise<void> {
|
|
193
|
-
const root = projectRoot()
|
|
194
|
-
if (!root)
|
|
195
|
-
return
|
|
196
|
-
const file = targetFile(root)
|
|
197
|
-
const key = (await api.window.showInputBox({ title: `Set an env value (${file})`, prompt: 'Key', validateInput: value => /^[A-Z_a-z][\w.-]*$/.test(value.trim()) ? undefined : 'Letters, digits and underscores, not starting with a digit' }))?.trim()
|
|
198
|
-
if (!key)
|
|
199
|
-
return
|
|
200
|
-
const value = await api.window.showInputBox({ title: `Set ${key} (${file})`, prompt: 'Value. It is encrypted when the file is.', password: true })
|
|
201
|
-
if (value === undefined)
|
|
202
|
-
return
|
|
203
|
-
if (await buddy(root, ['env:set', key, value, '--file', file]) !== undefined)
|
|
204
|
-
void api.window.showInformationMessage(`Stacks: set ${key} in ${file}.`)
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async function wholeFile(command: 'env:encrypt' | 'env:decrypt' | 'env:rotate', warning?: string): Promise<void> {
|
|
208
|
-
const root = projectRoot()
|
|
209
|
-
if (!root)
|
|
210
|
-
return
|
|
211
|
-
const file = targetFile(root)
|
|
212
|
-
if (warning) {
|
|
213
|
-
const confirmed = await api.window.showWarningMessage(warning.replace('{file}', file), { modal: true }, 'Continue')
|
|
214
|
-
if (confirmed !== 'Continue')
|
|
215
|
-
return
|
|
216
|
-
}
|
|
217
|
-
if (await buddy(root, [command, '--file', file]) !== undefined)
|
|
218
|
-
void api.window.showInformationMessage(`Stacks: ./buddy ${command} finished for ${file}.`)
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return {
|
|
222
|
-
activate(context) {
|
|
223
|
-
context.subscriptions.push(
|
|
224
|
-
diagnostics,
|
|
225
|
-
api.languages.registerHoverProvider([{ scheme: 'file', pattern: '**/.env*' }, { language: 'dotenv' }], hover),
|
|
226
|
-
api.workspace.onDidOpenTextDocument(checkMissingKeys),
|
|
227
|
-
api.workspace.onDidChangeTextDocument(event => checkMissingKeys(event.document)),
|
|
228
|
-
api.workspace.onDidSaveTextDocument(checkMissingKeys),
|
|
229
|
-
api.workspace.onDidCloseTextDocument(document => diagnostics.delete(document.uri)),
|
|
230
|
-
api.commands.registerCommand(ENV_COMMANDS.get, getValue),
|
|
231
|
-
api.commands.registerCommand(ENV_COMMANDS.set, setValue),
|
|
232
|
-
api.commands.registerCommand(ENV_COMMANDS.encrypt, () => wholeFile('env:encrypt')),
|
|
233
|
-
api.commands.registerCommand(ENV_COMMANDS.decrypt, () => wholeFile('env:decrypt', 'This writes every value in {file} to disk in plaintext. Encrypt it again before committing.')),
|
|
234
|
-
api.commands.registerCommand(ENV_COMMANDS.rotate, () => wholeFile('env:rotate', 'This generates a new keypair for {file} and re-encrypts every value with it. Deployments still holding the old private key will no longer decrypt it.')),
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
for (const document of api.workspace.textDocuments)
|
|
238
|
-
checkMissingKeys(document)
|
|
239
|
-
},
|
|
240
|
-
}
|
|
241
|
-
}
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The extension entry VS Code loads (`main` in package.json, bundled to
|
|
3
|
-
* dist/extension.js). It wires the real machine into the controllers and
|
|
4
|
-
* starts each part; the behavior lives in the modules it imports:
|
|
5
|
-
*
|
|
6
|
-
* - ./stacks.ts preview, dev server and buddy commands
|
|
7
|
-
* - ./pickier.ts lint diagnostics, formatting and fix-all from the project's pickier
|
|
8
|
-
* - ./env.ts decrypted env values on hover, missing keys, `buddy env:*` commands
|
|
9
|
-
* - stx the stx extension's language support, built in from `@stacksjs/stx-vscode`,
|
|
10
|
-
* and its TypeScript server plugin, which type-checks `.stx` files
|
|
11
|
-
*
|
|
12
|
-
* Nothing here depends on another extension being installed.
|
|
13
|
-
*/
|
|
14
|
-
import type { PickierProcess } from './pickier'
|
|
15
|
-
import type { Host } from './stacks'
|
|
16
|
-
import { execFile, spawn } from 'node:child_process'
|
|
17
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
18
|
-
import { connect } from 'node:net'
|
|
19
|
-
import { join } from 'node:path'
|
|
20
|
-
import process from 'node:process'
|
|
21
|
-
import { createInterface } from 'node:readline'
|
|
22
|
-
import { activateStxLanguage, configureTypeScriptPlugin, deactivateStxLanguage, STX_EXTENSION_ID } from '@stacksjs/stx-vscode'
|
|
23
|
-
import * as vscode from 'vscode'
|
|
24
|
-
import { createEnvSupport, projectDecryptor } from './env'
|
|
25
|
-
import { createPickierSupport } from './pickier'
|
|
26
|
-
import { createStacksExtension } from './stacks'
|
|
27
|
-
|
|
28
|
-
const PROBE_TIMEOUT_MS = 500
|
|
29
|
-
|
|
30
|
-
function read(path: string): string | undefined {
|
|
31
|
-
try {
|
|
32
|
-
return readFileSync(path, 'utf8')
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
return undefined
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function exec(file: string, args: string[], cwd: string): Promise<string> {
|
|
40
|
-
return new Promise((resolve, reject) => {
|
|
41
|
-
execFile(file, args, {
|
|
42
|
-
cwd,
|
|
43
|
-
timeout: 60_000,
|
|
44
|
-
maxBuffer: 32 * 1024 * 1024,
|
|
45
|
-
env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' },
|
|
46
|
-
}, (error, stdout) => error ? reject(error) : resolve(String(stdout)))
|
|
47
|
-
})
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const envHost = { exists: existsSync, read, exec, environment: process.env }
|
|
51
|
-
|
|
52
|
-
const host: Host = {
|
|
53
|
-
exists: path => existsSync(path),
|
|
54
|
-
read,
|
|
55
|
-
exec,
|
|
56
|
-
|
|
57
|
-
probe(hostname, port) {
|
|
58
|
-
return new Promise((resolve) => {
|
|
59
|
-
const socket = connect({ host: hostname, port })
|
|
60
|
-
const finish = (open: boolean) => {
|
|
61
|
-
socket.destroy()
|
|
62
|
-
resolve(open)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
socket.setTimeout(PROBE_TIMEOUT_MS)
|
|
66
|
-
socket.once('connect', () => finish(true))
|
|
67
|
-
socket.once('timeout', () => finish(false))
|
|
68
|
-
socket.once('error', () => finish(false))
|
|
69
|
-
})
|
|
70
|
-
},
|
|
71
|
-
|
|
72
|
-
sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
|
|
73
|
-
|
|
74
|
-
decryptEnv: root => projectDecryptor(envHost, root),
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* `bun --config=<empty> dist/pickier-worker.js` in the project root. The
|
|
79
|
-
* explicit config keeps the project's bunfig.toml out: its `preload` boots the
|
|
80
|
-
* framework (env decryption, auto-imports), which pickier does not need.
|
|
81
|
-
*/
|
|
82
|
-
function spawnBun(bun: string, script: string, cwd: string): PickierProcess {
|
|
83
|
-
const child = spawn(bun, [`--config=${join(script, '..', 'pickier-worker.bunfig.toml')}`, script], {
|
|
84
|
-
cwd,
|
|
85
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
86
|
-
env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' },
|
|
87
|
-
})
|
|
88
|
-
const lines = createInterface({ input: child.stdout })
|
|
89
|
-
let exited = false
|
|
90
|
-
const exitListeners: Array<() => void> = []
|
|
91
|
-
const exit = () => {
|
|
92
|
-
if (exited)
|
|
93
|
-
return
|
|
94
|
-
exited = true
|
|
95
|
-
for (const listener of exitListeners)
|
|
96
|
-
listener()
|
|
97
|
-
}
|
|
98
|
-
child.once('exit', exit)
|
|
99
|
-
child.once('error', exit)
|
|
100
|
-
child.stderr.resume()
|
|
101
|
-
|
|
102
|
-
return {
|
|
103
|
-
write: line => child.stdin.write(line),
|
|
104
|
-
onLine: listener => lines.on('line', listener),
|
|
105
|
-
onExit: listener => exitListeners.push(listener),
|
|
106
|
-
kill: () => child.kill(),
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const stacks = createStacksExtension(vscode, host)
|
|
111
|
-
const env = createEnvSupport(vscode, envHost)
|
|
112
|
-
let pickier: ReturnType<typeof createPickierSupport> | undefined
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* What `activate` resolves to. The stx extension (Stacks.vscode-stx) reads
|
|
116
|
-
* `stx.active` and stands down when it is true, so the two never register
|
|
117
|
-
* stx support twice in one window.
|
|
118
|
-
*/
|
|
119
|
-
export interface StacksExtensionApi {
|
|
120
|
-
stx: { active: boolean }
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async function startStx(context: vscode.ExtensionContext): Promise<boolean> {
|
|
124
|
-
try {
|
|
125
|
-
await activateStxLanguage(context, {
|
|
126
|
-
assetsPath: join(context.extensionPath, 'dist', 'stx'),
|
|
127
|
-
// The stx extension contributes the snippets in its manifest; offering
|
|
128
|
-
// them here as well would show each one twice.
|
|
129
|
-
snippets: !vscode.extensions.getExtension(STX_EXTENSION_ID),
|
|
130
|
-
})
|
|
131
|
-
return true
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
console.error('Stacks: stx support failed to start', error)
|
|
135
|
-
return false
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export async function activate(context: vscode.ExtensionContext): Promise<StacksExtensionApi> {
|
|
140
|
-
// The manifest contributes the stx TypeScript server plugin. VS Code's
|
|
141
|
-
// TypeScript extension does not activate on `stx` by itself, so this starts
|
|
142
|
-
// it, and forwards `stxTypescriptPlugin.enabled` to the plugin. The stx
|
|
143
|
-
// extension, when installed, does the same; the plugin decorates a project
|
|
144
|
-
// once however many extensions contribute it.
|
|
145
|
-
configureTypeScriptPlugin(vscode, context.subscriptions).catch((error) => {
|
|
146
|
-
console.error('Stacks: could not configure the stx TypeScript plugin', error)
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
stacks.activate(context)
|
|
150
|
-
env.activate(context)
|
|
151
|
-
pickier = createPickierSupport(vscode, { exists: existsSync, spawnBun }, {
|
|
152
|
-
workerPath: join(context.extensionPath, 'dist', 'pickier-worker.js'),
|
|
153
|
-
})
|
|
154
|
-
pickier.activate(context)
|
|
155
|
-
|
|
156
|
-
return { stx: { active: await startStx(context) } }
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function deactivate(): void {
|
|
160
|
-
stacks.deactivate()
|
|
161
|
-
pickier?.deactivate()
|
|
162
|
-
deactivateStxLanguage()
|
|
163
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The line protocol between the extension and dist/pickier-worker.js.
|
|
3
|
-
*
|
|
4
|
-
* Pickier runs on Bun (its config is a TypeScript file, and it uses Bun's glob
|
|
5
|
-
* and YAML APIs), and VS Code runs extensions on Node, so the extension keeps
|
|
6
|
-
* one Bun process per project and talks to it over stdin/stdout: one JSON
|
|
7
|
-
* object per line each way, answers matched to requests by `id`.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export type PickierOperation = 'lint' | 'fix' | 'format'
|
|
11
|
-
|
|
12
|
-
export interface PickierRequest {
|
|
13
|
-
id: number
|
|
14
|
-
op: PickierOperation
|
|
15
|
-
/** Absolute path, for config ignores and the per-extension rules. */
|
|
16
|
-
file: string
|
|
17
|
-
text: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** One issue, as pickier's `lintText` reports it: 1-based line and column. */
|
|
21
|
-
export interface PickierIssue {
|
|
22
|
-
line: number
|
|
23
|
-
column: number
|
|
24
|
-
ruleId: string
|
|
25
|
-
message: string
|
|
26
|
-
severity: 'error' | 'warning'
|
|
27
|
-
help?: string
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export type PickierResult =
|
|
31
|
-
| { op: 'lint', issues: PickierIssue[] }
|
|
32
|
-
/** `text` is null when the project's pickier predates `fixText`, or the file is ignored. */
|
|
33
|
-
| { op: 'fix' | 'format', text: string | null }
|
|
34
|
-
|
|
35
|
-
export type PickierResponse =
|
|
36
|
-
| { id: number, ok: true, result: PickierResult }
|
|
37
|
-
| { id: number, ok: false, error: string, missing?: boolean }
|
|
38
|
-
|
|
39
|
-
/** The first line the worker writes, once it has loaded the project's pickier and config. */
|
|
40
|
-
export interface PickierReady {
|
|
41
|
-
ready: true
|
|
42
|
-
version: string
|
|
43
|
-
fixText: boolean
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** What the worker writes instead when the project has no pickier it can load. */
|
|
47
|
-
export interface PickierUnavailable {
|
|
48
|
-
ready: false
|
|
49
|
-
error: string
|
|
50
|
-
missing: boolean
|
|
51
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The Bun side of the extension's pickier support (bundled to
|
|
3
|
-
* dist/pickier-worker.js, started with the project root as its working
|
|
4
|
-
* directory). See ./pickier-protocol.ts.
|
|
5
|
-
*
|
|
6
|
-
* It loads the PROJECT's pickier, not a copy bundled into the extension, and
|
|
7
|
-
* lets pickier find the project's config itself, the way `./buddy lint` does
|
|
8
|
-
* (core/actions/src/lint/lint.ts). Editor and command therefore agree on the
|
|
9
|
-
* rules, the version and the ignores.
|
|
10
|
-
*/
|
|
11
|
-
import type { PickierReady, PickierRequest, PickierResponse, PickierResult, PickierUnavailable } from './pickier-protocol'
|
|
12
|
-
import { relative } from 'node:path'
|
|
13
|
-
import process from 'node:process'
|
|
14
|
-
|
|
15
|
-
interface PickierModule {
|
|
16
|
-
lintText: (text: string, config: unknown, filePath?: string) => Promise<Array<{ line: number, column: number, ruleId: string, message: string, severity: 'error' | 'warning', help?: string }>>
|
|
17
|
-
formatCode: (text: string, config: unknown, filePath: string) => string
|
|
18
|
-
fixText?: (text: string, config: unknown, filePath?: string) => string
|
|
19
|
-
loadConfigFromPath: (path: string | undefined) => Promise<{ ignores?: string[] }>
|
|
20
|
-
shouldIgnorePath?: (absPath: string, ignoreGlobs: string[]) => boolean
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function send(message: PickierResponse | PickierReady | PickierUnavailable): void {
|
|
24
|
-
process.stdout.write(`${JSON.stringify(message)}\n`)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const root = process.cwd()
|
|
28
|
-
let pickier: PickierModule
|
|
29
|
-
let config: { ignores?: string[] }
|
|
30
|
-
let version = 'unknown'
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
const entry = Bun.resolveSync('pickier', root)
|
|
34
|
-
pickier = await import(entry) as PickierModule
|
|
35
|
-
try {
|
|
36
|
-
version = (await import(Bun.resolveSync('pickier/package.json', root))).version ?? version
|
|
37
|
-
}
|
|
38
|
-
catch {}
|
|
39
|
-
config = await pickier.loadConfigFromPath(undefined)
|
|
40
|
-
}
|
|
41
|
-
catch (error) {
|
|
42
|
-
// Not a pickier project (or not installed yet): say so and stop, so the
|
|
43
|
-
// extension stops asking instead of retrying on every keystroke.
|
|
44
|
-
send({ ready: false, error: String(error), missing: true })
|
|
45
|
-
process.exit(0)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
send({ ready: true, version, fixText: typeof pickier.fixText === 'function' })
|
|
49
|
-
|
|
50
|
-
function ignored(file: string): boolean {
|
|
51
|
-
const rel = relative(root, file)
|
|
52
|
-
if (rel.startsWith('..'))
|
|
53
|
-
return true
|
|
54
|
-
return pickier.shouldIgnorePath?.(file, config.ignores ?? []) ?? false
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function handle(request: PickierRequest): Promise<PickierResult> {
|
|
58
|
-
if (request.op === 'lint') {
|
|
59
|
-
if (ignored(request.file))
|
|
60
|
-
return { op: 'lint', issues: [] }
|
|
61
|
-
const issues = await pickier.lintText(request.text, config, request.file)
|
|
62
|
-
return { op: 'lint', issues: issues.map(({ line, column, ruleId, message, severity, help }) => ({ line, column, ruleId, message, severity, help })) }
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (ignored(request.file))
|
|
66
|
-
return { op: request.op, text: null }
|
|
67
|
-
|
|
68
|
-
if (request.op === 'fix')
|
|
69
|
-
return { op: 'fix', text: pickier.fixText ? pickier.fixText(request.text, config, request.file) : null }
|
|
70
|
-
|
|
71
|
-
return { op: 'format', text: pickier.formatCode(request.text, config, request.file) }
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
for await (const line of console) {
|
|
75
|
-
if (!line.trim())
|
|
76
|
-
continue
|
|
77
|
-
|
|
78
|
-
let request: PickierRequest
|
|
79
|
-
try {
|
|
80
|
-
request = JSON.parse(line)
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
continue
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
send({ id: request.id, ok: true, result: await handle(request) })
|
|
88
|
-
}
|
|
89
|
-
catch (error) {
|
|
90
|
-
send({ id: request.id, ok: false, error: error instanceof Error ? error.message : String(error) })
|
|
91
|
-
}
|
|
92
|
-
}
|