@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
|
@@ -1,13 +1,40 @@
|
|
|
1
|
-
import { createRateLimitMiddleware, Middleware, parseThrottleString } from '@stacksjs/router'
|
|
1
|
+
import { clientAddress, createRateLimitMiddleware, Middleware, parseThrottleString } from '@stacksjs/router'
|
|
2
2
|
|
|
3
3
|
// Cache for rate limiters to avoid creating new instances for each request
|
|
4
4
|
const limiterCache = new Map<string, ReturnType<typeof createRateLimitMiddleware>>()
|
|
5
5
|
|
|
6
|
+
const RATE_LIMIT_HEADERS = ['X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-RateLimit-Reset'] as const
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Whose budget a request spends.
|
|
10
|
+
*
|
|
11
|
+
* A signed-in request spends its user's: the Auth middleware stamps
|
|
12
|
+
* `_authenticatedUser`, and Throttle runs after it (priority 2 to Auth's 1),
|
|
13
|
+
* so people sharing an address - an office, a phone carrier's NAT, every test
|
|
14
|
+
* user on 127.0.0.1 - do not exhaust each other. bun-router's own default
|
|
15
|
+
* reads `request.user.id`, which on a Stacks request is the async `user()`
|
|
16
|
+
* accessor, so it never saw a user and every request fell through to an
|
|
17
|
+
* address.
|
|
18
|
+
*
|
|
19
|
+
* Anything else spends its client address's, as `clientAddress()` resolves
|
|
20
|
+
* it: the socket peer, or - only when that peer is a trusted proxy - the
|
|
21
|
+
* forwarding headers it vouches for (`CF-Connecting-IP` only when the hop
|
|
22
|
+
* that delivered the request is Cloudflare). A client-written
|
|
23
|
+
* `X-Forwarded-For` no longer buys a fresh budget.
|
|
24
|
+
*/
|
|
25
|
+
function throttleKey(request: { _authenticatedUser?: unknown }): string {
|
|
26
|
+
const user = request._authenticatedUser as { id?: unknown } | null | undefined
|
|
27
|
+
const id = user?.id
|
|
28
|
+
if ((typeof id === 'number' && Number.isFinite(id)) || (typeof id === 'string' && id !== ''))
|
|
29
|
+
return `user:${id}`
|
|
30
|
+
return `ip:${clientAddress(request as unknown as Request) ?? 'unknown'}`
|
|
31
|
+
}
|
|
32
|
+
|
|
6
33
|
/**
|
|
7
34
|
* Throttle Middleware
|
|
8
35
|
*
|
|
9
|
-
* Rate limits requests
|
|
10
|
-
* Uses Laravel-style throttle patterns.
|
|
36
|
+
* Rate limits requests per authenticated user, or per client address for
|
|
37
|
+
* anonymous requests (see `throttleKey`). Uses Laravel-style throttle patterns.
|
|
11
38
|
*
|
|
12
39
|
* Usage:
|
|
13
40
|
* .middleware('throttle:60,1') // 60 requests per 1 minute
|
|
@@ -29,7 +56,7 @@ const limiterCache = new Map<string, ReturnType<typeof createRateLimitMiddleware
|
|
|
29
56
|
* "retryAfter": X
|
|
30
57
|
* }
|
|
31
58
|
*
|
|
32
|
-
* Headers added to all responses:
|
|
59
|
+
* Headers added to all responses (successful ones via `_responseHeaders`):
|
|
33
60
|
* - X-RateLimit-Limit: Maximum requests allowed
|
|
34
61
|
* - X-RateLimit-Remaining: Remaining requests in current window
|
|
35
62
|
* - X-RateLimit-Reset: Unix timestamp when the limit resets
|
|
@@ -37,7 +64,9 @@ const limiterCache = new Map<string, ReturnType<typeof createRateLimitMiddleware
|
|
|
37
64
|
*/
|
|
38
65
|
export default new Middleware({
|
|
39
66
|
name: 'throttle',
|
|
40
|
-
|
|
67
|
+
// After Auth (1), so an authenticated request is keyed on its user rather
|
|
68
|
+
// than on an address it may share with strangers.
|
|
69
|
+
priority: 2,
|
|
41
70
|
|
|
42
71
|
async handle(request) {
|
|
43
72
|
// Get throttle params from middleware params (e.g., '60,1' from 'throttle:60,1')
|
|
@@ -48,24 +77,25 @@ export default new Middleware({
|
|
|
48
77
|
if (!limiter) {
|
|
49
78
|
try {
|
|
50
79
|
const config = parseThrottleString(params as Parameters<typeof parseThrottleString>[0])
|
|
51
|
-
limiter = createRateLimitMiddleware(config, `throttle:${params}`)
|
|
80
|
+
limiter = createRateLimitMiddleware({ ...config, keyGenerator: throttleKey }, `throttle:${params}`)
|
|
52
81
|
limiterCache.set(params, limiter)
|
|
53
82
|
}
|
|
54
83
|
catch (error) {
|
|
55
84
|
console.error(`[Throttle] Invalid throttle pattern: ${params}`, error)
|
|
56
85
|
// Use default if pattern is invalid
|
|
57
86
|
const defaultConfig = parseThrottleString('60,1')
|
|
58
|
-
limiter = createRateLimitMiddleware(defaultConfig, 'throttle:default')
|
|
87
|
+
limiter = createRateLimitMiddleware({ ...defaultConfig, keyGenerator: throttleKey }, 'throttle:default')
|
|
59
88
|
limiterCache.set(params, limiter)
|
|
60
89
|
}
|
|
61
90
|
}
|
|
62
91
|
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
92
|
+
// The limiter stamps its X-RateLimit-* headers onto whatever `next()`
|
|
93
|
+
// answers, so hand it a placeholder and read them back. Passing `null`
|
|
94
|
+
// here meant a request that got through never learned its budget.
|
|
95
|
+
const result = await limiter(request as any, async () => new Response(null, { status: 204 }))
|
|
66
96
|
|
|
67
|
-
//
|
|
68
|
-
if (result instanceof Response) {
|
|
97
|
+
// A 429 short-circuits the request
|
|
98
|
+
if (result instanceof Response && result.status === 429) {
|
|
69
99
|
// Transform the response to match stacks format
|
|
70
100
|
const retryAfter = result.headers.get('Retry-After') || '60'
|
|
71
101
|
|
|
@@ -90,6 +120,15 @@ export default new Middleware({
|
|
|
90
120
|
})
|
|
91
121
|
}
|
|
92
122
|
|
|
93
|
-
//
|
|
123
|
+
// Within the limit: ask the router to put the budget on the real response.
|
|
124
|
+
if (result instanceof Response) {
|
|
125
|
+
const headers: Record<string, string> = { ...request._responseHeaders }
|
|
126
|
+
for (const name of RATE_LIMIT_HEADERS) {
|
|
127
|
+
const value = result.headers.get(name)
|
|
128
|
+
if (value !== null)
|
|
129
|
+
headers[name] = value
|
|
130
|
+
}
|
|
131
|
+
request._responseHeaders = headers
|
|
132
|
+
}
|
|
94
133
|
},
|
|
95
134
|
})
|
package/ide/vscode/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"publisher": "Stacks",
|
|
3
3
|
"name": "vscode-stacks",
|
|
4
4
|
"displayName": "Stacks",
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.64",
|
|
6
6
|
"description": "Stacks, built into VS Code: stx templates, pickier linting and formatting, encrypted env files, the dev server preview and every buddy command. No other extensions required.",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"funding": "https://github.com/sponsors/chrisbbreuer",
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/defaults",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.64",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/stacksjs/stacks.git",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@iconify-json/f7": "^1.2.2",
|
|
57
57
|
"@iconify-json/hugeicons": "^1.2.35",
|
|
58
|
-
"@stacksjs/mobile": "^0.75.
|
|
58
|
+
"@stacksjs/mobile": "^0.75.64",
|
|
59
59
|
"@stacksjs/sanitizer": "^0.2.348",
|
|
60
60
|
"ts-qr-codes": "^0.1.8"
|
|
61
61
|
}
|
package/ide/vscode/build.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Builds the extension into dist/:
|
|
3
|
-
*
|
|
4
|
-
* extension.js the extension, for VS Code's Node host; only `vscode` is external
|
|
5
|
-
* pickier-worker.js the Bun process that runs the project's pickier (src/pickier-worker.ts)
|
|
6
|
-
* pickier-worker.bunfig.toml an empty bunfig, so the worker skips the project's preloads
|
|
7
|
-
* stx/ grammar, language configuration and snippets from @stacksjs/stx-vscode
|
|
8
|
-
* stx-typescript-plugin/ the stx TypeScript server plugin from @stacksjs/stx-vscode
|
|
9
|
-
*
|
|
10
|
-
* `vsce package` runs this first (`vscode:prepublish`), and `files` in
|
|
11
|
-
* package.json ships exactly these. tsserver loads the plugin from
|
|
12
|
-
* node_modules, so a forwarder to it is written there as well, which is what
|
|
13
|
-
* a development host (`--extensionDevelopmentPath`) loads; scripts/package.ts
|
|
14
|
-
* adds the same forwarder to the VSIX (see scripts/ts-plugin.ts).
|
|
15
|
-
*/
|
|
16
|
-
import { cpSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
17
|
-
import { dirname, join } from 'node:path'
|
|
18
|
-
import process from 'node:process'
|
|
19
|
-
import { copyTsPlugin, tsPluginName, writeTsPluginForwarder } from './scripts/ts-plugin'
|
|
20
|
-
|
|
21
|
-
const here = import.meta.dir
|
|
22
|
-
const dist = join(here, 'dist')
|
|
23
|
-
|
|
24
|
-
rmSync(dist, { recursive: true, force: true })
|
|
25
|
-
mkdirSync(dist, { recursive: true })
|
|
26
|
-
|
|
27
|
-
const builds = await Promise.all([
|
|
28
|
-
// bunfig stays out: the stx utility-class engine imports it only to load a
|
|
29
|
-
// config file, which the extension never asks for, and its module-scope
|
|
30
|
-
// `await` and `import.meta.require` would make the CommonJS bundle a syntax
|
|
31
|
-
// error in VS Code's Node host. tests/build.test.ts parses the bundle.
|
|
32
|
-
Bun.build({ entrypoints: [join(here, 'src/extension.ts')], outdir: dist, target: 'node', format: 'cjs', external: ['vscode', 'bunfig'], minify: true }),
|
|
33
|
-
Bun.build({ entrypoints: [join(here, 'src/pickier-worker.ts')], outdir: dist, target: 'bun', format: 'esm' }),
|
|
34
|
-
])
|
|
35
|
-
|
|
36
|
-
for (const build of builds) {
|
|
37
|
-
if (!build.success) {
|
|
38
|
-
console.error(build.logs)
|
|
39
|
-
process.exit(1)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
writeFileSync(join(dist, 'pickier-worker.bunfig.toml'), '# Intentionally empty: the pickier worker must not run the project\'s bunfig preloads.\n')
|
|
44
|
-
|
|
45
|
-
const stxAssets = join(dirname(Bun.resolveSync('@stacksjs/stx-vscode/contributes.json', here)), 'assets')
|
|
46
|
-
cpSync(stxAssets, join(dist, 'stx'), { recursive: true })
|
|
47
|
-
|
|
48
|
-
copyTsPlugin(here)
|
|
49
|
-
const { version } = JSON.parse(readFileSync(join(here, 'package.json'), 'utf8'))
|
|
50
|
-
writeTsPluginForwarder(here, tsPluginName(here), version)
|
|
51
|
-
|
|
52
|
-
console.log('Built dist/')
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Package (and with `--publish`, publish) the extension.
|
|
3
|
-
*
|
|
4
|
-
* bun scripts/package.ts [--out <file.vsix>] [--publish]
|
|
5
|
-
*
|
|
6
|
-
* `vsce package --no-dependencies` keeps the VSIX to what `files` lists, and it
|
|
7
|
-
* leaves out everything under node_modules - including the one package the
|
|
8
|
-
* extension needs there: the stx TypeScript server plugin, which tsserver will
|
|
9
|
-
* only load by package name from `<extension>/node_modules` (see ts-plugin.ts).
|
|
10
|
-
* So its forwarder is added to the VSIX after vsce writes it, and a publish
|
|
11
|
-
* uploads that VSIX rather than letting vsce package a second time.
|
|
12
|
-
*/
|
|
13
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
|
14
|
-
import { tmpdir } from 'node:os'
|
|
15
|
-
import { join, resolve } from 'node:path'
|
|
16
|
-
import process from 'node:process'
|
|
17
|
-
import { TS_PLUGIN_DIST, tsPluginModuleDir, tsPluginName, writeTsPluginForwarder } from './ts-plugin'
|
|
18
|
-
|
|
19
|
-
const ROOT = join(import.meta.dir, '..')
|
|
20
|
-
|
|
21
|
-
function run(command: string[], cwd = ROOT, capture = false): string {
|
|
22
|
-
const result = Bun.spawnSync(command, { cwd, stdout: capture ? 'pipe' : 'inherit', stderr: 'inherit', env: process.env })
|
|
23
|
-
if (result.exitCode !== 0)
|
|
24
|
-
throw new Error(`${command.join(' ')} exited with ${result.exitCode}`)
|
|
25
|
-
return capture ? String(result.stdout ?? '') : ''
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Build the VSIX at `out`, with the TypeScript plugin's forwarder in it. */
|
|
29
|
-
export function packageExtension(out: string): string {
|
|
30
|
-
const manifest = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'))
|
|
31
|
-
const name = tsPluginName(ROOT)
|
|
32
|
-
|
|
33
|
-
// vsce runs `vscode:prepublish` (the build) first.
|
|
34
|
-
run(['bun', 'x', '--bun', 'vsce', 'package', '--no-dependencies', '--out', out])
|
|
35
|
-
|
|
36
|
-
// A staging tree whose layout is the VSIX's, so zip records the paths VS
|
|
37
|
-
// Code extracts: extension/node_modules/<package>/...
|
|
38
|
-
const stage = mkdtempSync(join(tmpdir(), 'stacks-vsix-'))
|
|
39
|
-
try {
|
|
40
|
-
writeTsPluginForwarder(join(stage, 'extension'), name, manifest.version)
|
|
41
|
-
run(['zip', '-q', '-r', '-X', '-D', out, `extension/${tsPluginModuleDir(name)}`], stage)
|
|
42
|
-
}
|
|
43
|
-
finally {
|
|
44
|
-
rmSync(stage, { recursive: true, force: true })
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const listing = run(['unzip', '-Z1', out], ROOT, true).split('\n')
|
|
48
|
-
for (const file of [`extension/${tsPluginModuleDir(name)}/index.js`, `extension/${TS_PLUGIN_DIST}/index.js`, `extension/${TS_PLUGIN_DIST}/typescript-stx-plugin.js`]) {
|
|
49
|
-
if (!listing.includes(file))
|
|
50
|
-
throw new Error(`${out} is missing ${file}`)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return out
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (import.meta.main) {
|
|
57
|
-
const args = process.argv.slice(2)
|
|
58
|
-
const outFlag = args.indexOf('--out')
|
|
59
|
-
const manifest = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'))
|
|
60
|
-
const out = resolve(outFlag === -1 ? join(ROOT, `${manifest.name}-${manifest.version}.vsix`) : args[outFlag + 1]!)
|
|
61
|
-
|
|
62
|
-
packageExtension(out)
|
|
63
|
-
console.log(`Packaged ${out}`)
|
|
64
|
-
|
|
65
|
-
if (args.includes('--publish')) {
|
|
66
|
-
if (!existsSync(out))
|
|
67
|
-
throw new Error(`${out} was not written`)
|
|
68
|
-
run(['bun', 'x', '--bun', 'vsce', 'publish', '--packagePath', out])
|
|
69
|
-
console.log(`Published ${manifest.publisher}.${manifest.name}@${manifest.version}`)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Keeps the stx half of package.json's `contributes` equal to what
|
|
3
|
-
* `@stacksjs/stx-vscode` ships.
|
|
4
|
-
*
|
|
5
|
-
* VS Code reads languages, grammars, commands and settings only from the
|
|
6
|
-
* manifest, so the built-in stx support needs them declared here, pointing at
|
|
7
|
-
* dist/stx/ (copied there by build.ts). The library describes them in
|
|
8
|
-
* `contributes.json`; this derives the manifest from it rather than keeping a
|
|
9
|
-
* second copy by hand. That includes the TypeScript server plugin entry, which
|
|
10
|
-
* type-checks `.stx` files (see ts-plugin.ts). Snippets are left out on purpose: the extension offers
|
|
11
|
-
* them as completions only when the stx extension is not installed, because
|
|
12
|
-
* a manifest entry would show every snippet twice next to it.
|
|
13
|
-
*
|
|
14
|
-
* bun scripts/stx-contributes.ts --write update package.json (`bun run sync:stx`)
|
|
15
|
-
*
|
|
16
|
-
* tests/manifest.test.ts fails while the two disagree.
|
|
17
|
-
*/
|
|
18
|
-
import { readFileSync, writeFileSync } from 'node:fs'
|
|
19
|
-
import { join } from 'node:path'
|
|
20
|
-
import process from 'node:process'
|
|
21
|
-
|
|
22
|
-
export interface StxContributes {
|
|
23
|
-
languages: Array<Record<string, unknown> & { id: string, configuration: string }>
|
|
24
|
-
grammars: Array<Record<string, unknown> & { language: string, path: string }>
|
|
25
|
-
commands: Array<{ command: string, title: string, category?: string }>
|
|
26
|
-
configuration: { title: string, properties: Record<string, unknown> }
|
|
27
|
-
typescriptServerPlugins: Array<Record<string, unknown> & { name: string, languages?: string[] }>
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const STX_DIR = './dist/stx/'
|
|
31
|
-
|
|
32
|
-
function fromStx(path: string): string {
|
|
33
|
-
return STX_DIR + path.replace(/^\.\//, '')
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** The namespace of the extension's own commands; every other one is stx's. */
|
|
37
|
-
export const OWN_COMMAND_PREFIX = 'stacks.'
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* `manifest` with its stx contributions replaced by the library's.
|
|
41
|
-
*
|
|
42
|
-
* Commands are told apart by namespace, not by the library's current list: a
|
|
43
|
-
* command stx renamed or dropped is no longer in that list, and filtering by
|
|
44
|
-
* it kept the old entry as if it were this extension's own (`css.reload`
|
|
45
|
-
* outlived its rename to `stx.reloadUtilityClasses` that way).
|
|
46
|
-
*/
|
|
47
|
-
export function withStxContributes(manifest: any, stx: StxContributes): any {
|
|
48
|
-
const contributes = manifest.contributes
|
|
49
|
-
const sections: Array<{ title: string }> = Array.isArray(contributes.configuration) ? contributes.configuration : [contributes.configuration]
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
...manifest,
|
|
53
|
-
contributes: {
|
|
54
|
-
...contributes,
|
|
55
|
-
languages: stx.languages.map(language => ({ ...language, configuration: fromStx(language.configuration) })),
|
|
56
|
-
grammars: stx.grammars.map(grammar => ({ ...grammar, path: fromStx(grammar.path) })),
|
|
57
|
-
commands: [...contributes.commands.filter((command: { command: string }) => command.command.startsWith(OWN_COMMAND_PREFIX)), ...stx.commands],
|
|
58
|
-
configuration: [...sections.filter(section => section.title !== stx.configuration.title), stx.configuration],
|
|
59
|
-
typescriptServerPlugins: stx.typescriptServerPlugins,
|
|
60
|
-
},
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function libraryContributes(root: string): StxContributes {
|
|
65
|
-
return JSON.parse(readFileSync(Bun.resolveSync('@stacksjs/stx-vscode/contributes.json', root), 'utf8'))
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (import.meta.main) {
|
|
69
|
-
const root = join(import.meta.dir, '..')
|
|
70
|
-
const path = join(root, 'package.json')
|
|
71
|
-
const manifest = JSON.parse(readFileSync(path, 'utf8'))
|
|
72
|
-
const next = `${JSON.stringify(withStxContributes(manifest, libraryContributes(root)), null, 2)}\n`
|
|
73
|
-
|
|
74
|
-
if (process.argv.includes('--write'))
|
|
75
|
-
writeFileSync(path, next)
|
|
76
|
-
else
|
|
77
|
-
process.stdout.write(next)
|
|
78
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The stx TypeScript server plugin, as this extension ships it.
|
|
3
|
-
*
|
|
4
|
-
* It is what type-checks the TypeScript in `.stx` files (script blocks and
|
|
5
|
-
* template expressions) in the editor. `@stacksjs/stx-vscode` publishes it as a
|
|
6
|
-
* complete package, `dist/typescript-plugin/`, and its `contributes.json`
|
|
7
|
-
* carries the `typescriptServerPlugins` entry that `bun run sync:stx` copies
|
|
8
|
-
* into package.json.
|
|
9
|
-
*
|
|
10
|
-
* tsserver loads a plugin only by package name, resolved from
|
|
11
|
-
* `<extension>/node_modules`, the probe location VS Code passes for every
|
|
12
|
-
* extension that contributes one. This extension is packaged with
|
|
13
|
-
* `vsce --no-dependencies`, which leaves node_modules out of the VSIX, so:
|
|
14
|
-
*
|
|
15
|
-
* dist/stx-typescript-plugin/ the package itself, copied by build.ts
|
|
16
|
-
* (shipped through `files`, like the rest of dist/)
|
|
17
|
-
* node_modules/<name>/{package.json,index.js}
|
|
18
|
-
* a forwarder to it, written by build.ts for
|
|
19
|
-
* a development host and added to the VSIX
|
|
20
|
-
* by scripts/package.ts after vsce writes it
|
|
21
|
-
*
|
|
22
|
-
* The stx extension (Stacks.vscode-stx) contributes the same plugin under the
|
|
23
|
-
* same name. With both installed tsserver loads it twice, and the plugin
|
|
24
|
-
* decorates each project only the first time (stacksjs/stx#2028).
|
|
25
|
-
*/
|
|
26
|
-
import { cpSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
27
|
-
import { dirname, join } from 'node:path'
|
|
28
|
-
import { libraryContributes } from './stx-contributes'
|
|
29
|
-
|
|
30
|
-
/** Where build.ts puts the plugin package, relative to the extension root. */
|
|
31
|
-
export const TS_PLUGIN_DIST = 'dist/stx-typescript-plugin'
|
|
32
|
-
|
|
33
|
-
/** The name the plugin is contributed and configured under. */
|
|
34
|
-
export function tsPluginName(root: string): string {
|
|
35
|
-
const [plugin] = libraryContributes(root).typescriptServerPlugins
|
|
36
|
-
if (!plugin)
|
|
37
|
-
throw new Error('@stacksjs/stx-vscode contributes no TypeScript server plugin')
|
|
38
|
-
return plugin.name
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Where tsserver resolves the plugin from, relative to the extension root. */
|
|
42
|
-
export function tsPluginModuleDir(name: string): string {
|
|
43
|
-
return `node_modules/${name}`
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** The forwarder's files, relative to its directory under node_modules. */
|
|
47
|
-
export function tsPluginForwarder(name: string, version: string): Record<string, string> {
|
|
48
|
-
const up = '../'.repeat(1 + name.split('/').length)
|
|
49
|
-
return {
|
|
50
|
-
'package.json': `${JSON.stringify({ name, version, private: true, main: 'index.js' }, null, 2)}\n`,
|
|
51
|
-
// tsserver calls `module.exports` as the factory, unwrapped.
|
|
52
|
-
'index.js': `module.exports = require('${up}${TS_PLUGIN_DIST}/index.js')\n`,
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Write the forwarder under `<root>/node_modules`, returning its directory. */
|
|
57
|
-
export function writeTsPluginForwarder(root: string, name: string, version: string): string {
|
|
58
|
-
const dir = join(root, tsPluginModuleDir(name))
|
|
59
|
-
mkdirSync(dir, { recursive: true })
|
|
60
|
-
for (const [file, content] of Object.entries(tsPluginForwarder(name, version)))
|
|
61
|
-
writeFileSync(join(dir, file), content)
|
|
62
|
-
return dir
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Copy the plugin package from `@stacksjs/stx-vscode` into `<root>/dist`. */
|
|
66
|
-
export function copyTsPlugin(root: string): string {
|
|
67
|
-
const source = dirname(Bun.resolveSync('@stacksjs/stx-vscode/typescript-plugin/package.json', root))
|
|
68
|
-
const target = join(root, TS_PLUGIN_DIST)
|
|
69
|
-
rmSync(target, { recursive: true, force: true })
|
|
70
|
-
cpSync(source, target, { recursive: true })
|
|
71
|
-
return target
|
|
72
|
-
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Env files, read the way Stacks reads them - the pure half of ./env.ts.
|
|
3
|
-
*
|
|
4
|
-
* Decryption and key lookup are not reimplemented here: they are
|
|
5
|
-
* `@stacksjs/env`'s own (core/env/src), bundled into the extension, so a value
|
|
6
|
-
* the hover decrypts is exactly what `./buddy env:get` prints.
|
|
7
|
-
*/
|
|
8
|
-
import { basename } from 'node:path'
|
|
9
|
-
import { envPrivateKeyNames } from '../../../../core/env/src/cli'
|
|
10
|
-
import { decryptValue } from '../../../../core/env/src/crypto'
|
|
11
|
-
import { parseEnvFile } from './project'
|
|
12
|
-
|
|
13
|
-
/** `.env`, `.env.local`, `.env.production`, ... - not `.env.keys`, which holds the private keys themselves. */
|
|
14
|
-
export function isEnvFile(path: string): boolean {
|
|
15
|
-
const name = basename(path)
|
|
16
|
-
return /^\.env(?:\..+)?$/.test(name) && name !== '.env.keys'
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function isEncrypted(value: string): boolean {
|
|
20
|
-
return value.startsWith('encrypted:')
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface EnvEntry {
|
|
24
|
-
key: string
|
|
25
|
-
value: string
|
|
26
|
-
/** Character offsets of the value on its line, quotes excluded. */
|
|
27
|
-
start: number
|
|
28
|
-
end: number
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** The `KEY=value` on one line, with where its value sits, by `parseEnvFile`'s rules. */
|
|
32
|
-
export function envEntryAt(line: string): EnvEntry | undefined {
|
|
33
|
-
const match = line.match(/^(\s*(?:export\s+)?([A-Z_a-z][\w.-]*)\s*=\s*)(.*)$/)
|
|
34
|
-
if (!match)
|
|
35
|
-
return undefined
|
|
36
|
-
|
|
37
|
-
const key = match[2] as string
|
|
38
|
-
const value = parseEnvFile(line)[key]
|
|
39
|
-
if (value === undefined)
|
|
40
|
-
return undefined
|
|
41
|
-
|
|
42
|
-
const rest = match[3] as string
|
|
43
|
-
const quoted = rest[0] === '"' || rest[0] === '\'' || rest[0] === '`'
|
|
44
|
-
const start = (match[1] as string).length + (quoted ? 1 : 0)
|
|
45
|
-
return { key, value, start, end: start + value.length }
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Keys `.env.example` declares that the env file does not set, in example order. */
|
|
49
|
-
export function missingKeys(example: string, actual: string): string[] {
|
|
50
|
-
const present = parseEnvFile(actual)
|
|
51
|
-
return Object.keys(parseEnvFile(example)).filter(key => !(key in present))
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface PrivateKey {
|
|
55
|
-
name: string
|
|
56
|
-
key: string
|
|
57
|
-
from: '.env.keys' | 'environment'
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* The private key for an env file, looked up as `./buddy env:get` does:
|
|
62
|
-
* `envPrivateKeyNames` (`DOTENV_PRIVATE_KEY_<ENV>`, then `DOTENV_PRIVATE_KEY`)
|
|
63
|
-
* in `.env.keys`, then in the environment, the dotenvx convention for CI and
|
|
64
|
-
* servers where `.env.keys` must not exist.
|
|
65
|
-
*/
|
|
66
|
-
export function findPrivateKey(envFile: string, keysFile: string | undefined, environment: Record<string, string | undefined>): PrivateKey | undefined {
|
|
67
|
-
const names = envPrivateKeyNames(basename(envFile))
|
|
68
|
-
const keys = keysFile === undefined ? {} : parseEnvFile(keysFile)
|
|
69
|
-
|
|
70
|
-
for (const name of names) {
|
|
71
|
-
if (keys[name])
|
|
72
|
-
return { name, key: keys[name] as string, from: '.env.keys' }
|
|
73
|
-
}
|
|
74
|
-
for (const name of names) {
|
|
75
|
-
if (environment[name])
|
|
76
|
-
return { name, key: environment[name] as string, from: 'environment' }
|
|
77
|
-
}
|
|
78
|
-
return undefined
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export type Decryption =
|
|
82
|
-
| { ok: true, value: string, key: PrivateKey }
|
|
83
|
-
| { ok: false, reason: 'no-key', looked: string[] }
|
|
84
|
-
| { ok: false, reason: 'failed', key: PrivateKey }
|
|
85
|
-
|
|
86
|
-
/** Decrypt one value in memory. The result is never cached or written anywhere. */
|
|
87
|
-
export function decryptEnvValue(
|
|
88
|
-
envFile: string,
|
|
89
|
-
value: string,
|
|
90
|
-
keysFile: string | undefined,
|
|
91
|
-
environment: Record<string, string | undefined>,
|
|
92
|
-
): Decryption {
|
|
93
|
-
const key = findPrivateKey(envFile, keysFile, environment)
|
|
94
|
-
if (!key)
|
|
95
|
-
return { ok: false, reason: 'no-key', looked: envPrivateKeyNames(basename(envFile)) }
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
return { ok: true, value: decryptValue(value, key.key), key }
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
return { ok: false, reason: 'failed', key }
|
|
102
|
-
}
|
|
103
|
-
}
|