@stacksjs/defaults 0.75.45 → 0.75.47

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.
@@ -8,7 +8,9 @@ The Stacks commands, pickier and the env features activate in workspaces that co
8
8
 
9
9
  `.stx` files get the full stx language support: highlighting, completions for directives, components and props, hovers, diagnostics, go to definition, folding, semantic highlighting, snippets, and previews of utility classes with their colors.
10
10
 
11
- It is the same code as the standalone [stx extension](https://marketplace.visualstudio.com/items?itemName=Stacks.vscode-stx) (`Stacks.vscode-stx`), built in from `@stacksjs/stx-vscode`. You do not need that extension; if you have both, it stands down so nothing is registered twice.
11
+ The TypeScript in `.stx` files, `<script>` blocks and `{{ }}` template expressions alike, is type-checked by the TypeScript server, with its hovers, completions and go to definition, against the same declarations `stx typecheck` uses: `import { state } from 'stx'` resolves to exactly what the runtime binds. Turn it off with `stxTypescriptPlugin.enabled`.
12
+
13
+ It is the same code as the standalone [stx extension](https://marketplace.visualstudio.com/items?itemName=Stacks.vscode-stx) (`Stacks.vscode-stx`), built in from `@stacksjs/stx-vscode`. You do not need that extension; if you have both, it stands down so nothing is registered twice, and the TypeScript plugin both contribute checks each file once.
12
14
 
13
15
  ## Pickier
14
16
 
@@ -97,7 +99,7 @@ bun run package # build and create a .vsix
97
99
  code --install-extension vscode-stacks-<version>.vsix
98
100
  ```
99
101
 
100
- The extension has no runtime dependencies: `@stacksjs/stx-vscode` and the env decryption from `core/env` are bundled into `dist/extension.js`. It is published automatically by the Stacks release workflow, and `vscode:prepublish` builds `dist/` first.
102
+ The extension has no runtime dependencies: `@stacksjs/stx-vscode` and the env decryption from `core/env` are bundled into `dist/extension.js`. The one exception is the stx TypeScript plugin, which tsserver loads by package name from the extension's `node_modules`: `bun run package` (and `bun run release`, which publishes that VSIX) adds it there after `vsce package --no-dependencies`, which leaves `node_modules` out. It is published automatically by the Stacks release workflow, and `vscode:prepublish` builds `dist/` first.
101
103
 
102
104
  ## Relevant links
103
105
 
@@ -5,13 +5,18 @@
5
5
  * pickier-worker.js the Bun process that runs the project's pickier (src/pickier-worker.ts)
6
6
  * pickier-worker.bunfig.toml an empty bunfig, so the worker skips the project's preloads
7
7
  * stx/ grammar, language configuration and snippets from @stacksjs/stx-vscode
8
+ * stx-typescript-plugin/ the stx TypeScript server plugin from @stacksjs/stx-vscode
8
9
  *
9
10
  * `vsce package` runs this first (`vscode:prepublish`), and `files` in
10
- * package.json ships exactly these.
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).
11
15
  */
12
- import { cpSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
16
+ import { cpSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
13
17
  import { dirname, join } from 'node:path'
14
18
  import process from 'node:process'
19
+ import { copyTsPlugin, tsPluginName, writeTsPluginForwarder } from './scripts/ts-plugin'
15
20
 
16
21
  const here = import.meta.dir
17
22
  const dist = join(here, 'dist')
@@ -40,4 +45,8 @@ writeFileSync(join(dist, 'pickier-worker.bunfig.toml'), '# Intentionally empty:
40
45
  const stxAssets = join(dirname(Bun.resolveSync('@stacksjs/stx-vscode/contributes.json', here)), 'assets')
41
46
  cpSync(stxAssets, join(dist, 'stx'), { recursive: true })
42
47
 
48
+ copyTsPlugin(here)
49
+ const { version } = JSON.parse(readFileSync(join(here, 'package.json'), 'utf8'))
50
+ writeTsPluginForwarder(here, tsPluginName(here), version)
51
+
43
52
  console.log('Built dist/')
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.45",
5
+ "version": "0.75.47",
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",
@@ -44,6 +44,7 @@
44
44
  "dist/pickier-worker.js",
45
45
  "dist/pickier-worker.bunfig.toml",
46
46
  "dist/stx/**",
47
+ "dist/stx-typescript-plugin/**",
47
48
  "LICENSE.md",
48
49
  "README.md",
49
50
  "logo.png"
@@ -72,8 +73,8 @@
72
73
  "test": "bun test ./tests",
73
74
  "typecheck": "bun x --bun tsc --noEmit -p tsconfig.json",
74
75
  "sync:stx": "bun scripts/stx-contributes.ts --write",
75
- "package": "bunx --bun vsce package --no-dependencies",
76
- "release": "bunx --bun vsce publish --no-dependencies"
76
+ "package": "bun scripts/package.ts",
77
+ "release": "bun scripts/package.ts --publish"
77
78
  },
78
79
  "galleryBanner": {
79
80
  "color": "#d7e4f5",
@@ -258,7 +259,7 @@
258
259
  "stxTypescriptPlugin.enabled": {
259
260
  "type": "boolean",
260
261
  "default": true,
261
- "description": "Enable TypeScript language features in stx files"
262
+ "description": "Type-check the TypeScript in stx files (script blocks and template expressions) with the TypeScript server, and offer its hovers, completions and go to definition there"
262
263
  },
263
264
  "stx.diagnostics.enable": {
264
265
  "type": "boolean",
@@ -410,10 +411,19 @@
410
411
  "meta.component.parameters.stx": "typescript"
411
412
  }
412
413
  }
414
+ ],
415
+ "typescriptServerPlugins": [
416
+ {
417
+ "name": "@stacksjs/stx-typescript-plugin",
418
+ "enableForWorkspaceTypeScriptVersions": true,
419
+ "languages": [
420
+ "stx"
421
+ ]
422
+ }
413
423
  ]
414
424
  },
415
425
  "devDependencies": {
416
- "@stacksjs/stx-vscode": "^0.2.355",
426
+ "@stacksjs/stx-vscode": "^0.2.358",
417
427
  "@types/vscode": "~1.109.0",
418
428
  "@vscode/vsce": "^4.0.0"
419
429
  }
@@ -0,0 +1,71 @@
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
+ }
@@ -6,7 +6,8 @@
6
6
  * manifest, so the built-in stx support needs them declared here, pointing at
7
7
  * dist/stx/ (copied there by build.ts). The library describes them in
8
8
  * `contributes.json`; this derives the manifest from it rather than keeping a
9
- * second copy by hand. Snippets are left out on purpose: the extension offers
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
10
11
  * them as completions only when the stx extension is not installed, because
11
12
  * a manifest entry would show every snippet twice next to it.
12
13
  *
@@ -23,6 +24,7 @@ export interface StxContributes {
23
24
  grammars: Array<Record<string, unknown> & { language: string, path: string }>
24
25
  commands: Array<{ command: string, title: string, category?: string }>
25
26
  configuration: { title: string, properties: Record<string, unknown> }
27
+ typescriptServerPlugins: Array<Record<string, unknown> & { name: string, languages?: string[] }>
26
28
  }
27
29
 
28
30
  const STX_DIR = './dist/stx/'
@@ -54,6 +56,7 @@ export function withStxContributes(manifest: any, stx: StxContributes): any {
54
56
  grammars: stx.grammars.map(grammar => ({ ...grammar, path: fromStx(grammar.path) })),
55
57
  commands: [...contributes.commands.filter((command: { command: string }) => command.command.startsWith(OWN_COMMAND_PREFIX)), ...stx.commands],
56
58
  configuration: [...sections.filter(section => section.title !== stx.configuration.title), stx.configuration],
59
+ typescriptServerPlugins: stx.typescriptServerPlugins,
57
60
  },
58
61
  }
59
62
  }
@@ -0,0 +1,72 @@
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
+ }
@@ -6,7 +6,8 @@
6
6
  * - ./stacks.ts preview, dev server and buddy commands
7
7
  * - ./pickier.ts lint diagnostics, formatting and fix-all from the project's pickier
8
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`
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
10
11
  *
11
12
  * Nothing here depends on another extension being installed.
12
13
  */
@@ -18,7 +19,7 @@ import { connect } from 'node:net'
18
19
  import { join } from 'node:path'
19
20
  import process from 'node:process'
20
21
  import { createInterface } from 'node:readline'
21
- import { activateStxLanguage, deactivateStxLanguage, STX_EXTENSION_ID } from '@stacksjs/stx-vscode'
22
+ import { activateStxLanguage, configureTypeScriptPlugin, deactivateStxLanguage, STX_EXTENSION_ID } from '@stacksjs/stx-vscode'
22
23
  import * as vscode from 'vscode'
23
24
  import { createEnvSupport, projectDecryptor } from './env'
24
25
  import { createPickierSupport } from './pickier'
@@ -136,6 +137,15 @@ async function startStx(context: vscode.ExtensionContext): Promise<boolean> {
136
137
  }
137
138
 
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
+
139
149
  stacks.activate(context)
140
150
  env.activate(context)
141
151
  pickier = createPickierSupport(vscode, { exists: existsSync, spawnBun }, {
@@ -8,6 +8,8 @@ import { existsSync, readFileSync } from 'node:fs'
8
8
  import { builtinModules } from 'node:module'
9
9
  import { join } from 'node:path'
10
10
  import process from 'node:process'
11
+ import { TS_PLUGIN_DIST, tsPluginName } from '../scripts/ts-plugin'
12
+ import { expectDeclarationsFound, loadLikeTsserver } from './ts-plugin-harness'
11
13
 
12
14
  const root = join(import.meta.dir, '..')
13
15
  const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
@@ -39,6 +41,28 @@ describe('dist/extension.js', () => {
39
41
 
40
42
  expect(unshipped).toEqual([])
41
43
  })
44
+
45
+ it('starts the TypeScript extension and configures the stx plugin by the name the manifest contributes', () => {
46
+ const bundle = readFileSync(join(root, 'dist/extension.js'), 'utf8')
47
+ expect(bundle).toContain('vscode.typescript-language-features')
48
+ expect(bundle).toContain('configurePlugin')
49
+ for (const plugin of manifest.contributes.typescriptServerPlugins)
50
+ expect(bundle).toContain(plugin.name)
51
+ })
52
+ })
53
+
54
+ describe('the stx TypeScript plugin', () => {
55
+ it('is in dist/ as the package the manifest contributes', () => {
56
+ const plugin = JSON.parse(readFileSync(join(root, TS_PLUGIN_DIST, 'package.json'), 'utf8'))
57
+ expect(manifest.contributes.typescriptServerPlugins.map((entry: { name: string }) => entry.name)).toEqual([plugin.name])
58
+ expect(manifest.files).toContain(`${TS_PLUGIN_DIST}/**`)
59
+ })
60
+
61
+ it('loads from node_modules the way tsserver loads it, as in a development host', () => {
62
+ const name = tsPluginName(root)
63
+ expect(typeof loadLikeTsserver(root, name)).toBe('function')
64
+ expectDeclarationsFound(root, name, join(root, TS_PLUGIN_DIST, 'types'))
65
+ })
42
66
  })
43
67
 
44
68
  describe('dist/', () => {
@@ -92,7 +92,47 @@ describe('stx language support', () => {
92
92
  })
93
93
  })
94
94
 
95
+ // tsserver loads a plugin only by package name, from the extension's
96
+ // node_modules, and VS Code only sends it documents of a language a plugin
97
+ // claims (stacksjs/stx#2028).
98
+ describe('stx type checking', () => {
99
+ const plugins = manifest.contributes.typescriptServerPlugins as Array<{ name: string, languages?: string[], enableForWorkspaceTypeScriptVersions?: boolean }>
100
+
101
+ it('contributes the stx TypeScript server plugin by package name', () => {
102
+ expect(plugins.map(plugin => plugin.name)).toEqual(['@stacksjs/stx-typescript-plugin'])
103
+ for (const plugin of plugins) {
104
+ // The rule tsserver applies before loading anything (requestEnablePlugin).
105
+ expect(plugin.name).not.toMatch(/^(?:\.\.?(?:\/|$)|\/|[a-z]:)/i)
106
+ expect(plugin.name).not.toMatch(/[\\/]\.\.?(?:$|[\\/])/)
107
+ }
108
+ })
109
+
110
+ it('claims the stx language, also with the workspace TypeScript the project settings select', () => {
111
+ for (const plugin of plugins) {
112
+ expect(plugin.languages).toEqual(['stx'])
113
+ expect(plugin.enableForWorkspaceTypeScriptVersions).toBeTrue()
114
+ }
115
+ expect(manifest.contributes.configurationDefaults['typescript.tsdk']).toBe('node_modules/typescript/lib')
116
+ })
117
+
118
+ it('declares the setting that switches it off', () => {
119
+ const settings = Object.assign({}, ...manifest.contributes.configuration.map((section: { properties: object }) => section.properties))
120
+ expect(settings['stxTypescriptPlugin.enabled'].default).toBeTrue()
121
+ })
122
+ })
123
+
95
124
  describe('packaging', () => {
125
+ it('adds the plugin to the VSIX, which vsce --no-dependencies leaves without node_modules', () => {
126
+ expect(manifest.scripts.package).toBe('bun scripts/package.ts')
127
+ expect(manifest.scripts.release).toBe('bun scripts/package.ts --publish')
128
+ expect(readFileSync(join(root, 'scripts/package.ts'), 'utf8')).toContain(`'vsce', 'package', '--no-dependencies'`)
129
+ // The release workflow publishes through the script, not a bare `vsce publish`.
130
+ const workflow = readFileSync(join(root, '../../../../../.github/workflows/release.yml'), 'utf8')
131
+ const step = workflow.slice(workflow.indexOf('- name: Publish VS Code Extension'))
132
+ expect(step.slice(0, step.indexOf('- name:', 10))).toContain('bun run release')
133
+ expect(workflow).not.toContain('vsce publish')
134
+ })
135
+
96
136
  it('ships the files the extension loads at runtime', () => {
97
137
  for (const file of ['dist/extension.js', 'dist/pickier-worker.js', 'dist/pickier-worker.bunfig.toml'])
98
138
  expect(manifest.files).toContain(file)
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The VSIX, as VS Code installs it. It is packaged with `--no-dependencies`,
3
+ * which leaves node_modules out, so scripts/package.ts adds the stx TypeScript
4
+ * plugin's entry there afterwards; without it tsserver finds no plugin and no
5
+ * `.stx` file is type-checked.
6
+ */
7
+ import { describe, expect, it } from 'bun:test'
8
+ import { mkdtempSync, readFileSync, realpathSync, rmSync } from 'node:fs'
9
+ import { tmpdir } from 'node:os'
10
+ import { join } from 'node:path'
11
+ import { packageExtension } from '../scripts/package'
12
+ import { TS_PLUGIN_DIST, tsPluginModuleDir, tsPluginName } from '../scripts/ts-plugin'
13
+ import { expectDeclarationsFound, loadLikeTsserver } from './ts-plugin-harness'
14
+
15
+ const root = join(import.meta.dir, '..')
16
+ const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
17
+
18
+ // vsce runs `vscode:prepublish` through npm, so this needs npm on PATH; the
19
+ // release workflow, which runs these tests before publishing, has it.
20
+ const canPackage = Boolean(Bun.which('npm') && Bun.which('zip') && Bun.which('unzip'))
21
+
22
+ describe('the VSIX', () => {
23
+ it.skipIf(!canPackage)('carries the stx TypeScript plugin, and it loads from the installed layout', () => {
24
+ // Real path: require() resolves symlinks, and macOS's tmpdir is one.
25
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), 'stacks-vsix-test-')))
26
+ try {
27
+ const vsix = packageExtension(join(dir, 'stacks.vsix'))
28
+ const listing = Bun.spawnSync(['unzip', '-Z1', vsix]).stdout.toString().split('\n')
29
+ const name = tsPluginName(root)
30
+
31
+ expect(listing).toContain(`extension/${tsPluginModuleDir(name)}/package.json`)
32
+ expect(listing).toContain(`extension/${tsPluginModuleDir(name)}/index.js`)
33
+ // Nothing else of node_modules: the bundle carries its dependencies.
34
+ expect(listing.filter(file => file.startsWith('extension/node_modules/') && !file.startsWith(`extension/${tsPluginModuleDir(name)}/`))).toEqual([])
35
+ expect(listing).toContain(`extension/${TS_PLUGIN_DIST}/types/stx-module.d.ts`)
36
+
37
+ expect(Bun.spawnSync(['unzip', '-q', vsix, 'extension/*', '-d', dir]).exitCode).toBe(0)
38
+ const installed = join(dir, 'extension')
39
+ expect(JSON.parse(readFileSync(join(installed, 'package.json'), 'utf8')).version).toBe(manifest.version)
40
+ expect(typeof loadLikeTsserver(installed, name)).toBe('function')
41
+ expectDeclarationsFound(installed, name, join(installed, TS_PLUGIN_DIST, 'types'))
42
+ }
43
+ finally {
44
+ rmSync(dir, { recursive: true, force: true })
45
+ }
46
+ }, 180_000)
47
+ })
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Loading the stx TypeScript server plugin the way tsserver loads it, for the
3
+ * tests that check the built extension and the packaged VSIX.
4
+ */
5
+ import { expect } from 'bun:test'
6
+ import { existsSync } from 'node:fs'
7
+ import { createRequire } from 'node:module'
8
+ import { join } from 'node:path'
9
+
10
+ /**
11
+ * `require()` the plugin by name from `<extensionRoot>/node_modules` (VS Code
12
+ * passes the extension root as the probe location) and take `module.exports`
13
+ * as the factory, unwrapped, as tsserver does.
14
+ */
15
+ export function loadLikeTsserver(extensionRoot: string, name: string): unknown {
16
+ return createRequire(join(extensionRoot, 'node_modules', 'index.js'))(name)
17
+ }
18
+
19
+ /**
20
+ * Create the plugin on a stub language service and read the buffer it serves
21
+ * for a `.stx` file: line 1 references the declaration files the plugin found,
22
+ * which it can only find when the package tells it where they are (Bun inlines
23
+ * `__dirname` into the bundle at build time).
24
+ */
25
+ export function expectDeclarationsFound(extensionRoot: string, name: string, typesDir: string): void {
26
+ const factory = loadLikeTsserver(extensionRoot, name) as (modules: { typescript: unknown }) => { create: (info: unknown) => unknown }
27
+ expect(typeof factory).toBe('function')
28
+
29
+ const text = '<script client>\nconst n = state(0)\n</script>\n'
30
+ const snapshot = (value: string) => ({ getText: (start: number, end: number) => value.slice(start, end), getLength: () => value.length, getChangeRange: () => undefined })
31
+ const host: Record<string, any> = { getScriptSnapshot: () => snapshot(text), getScriptVersion: () => '1' }
32
+ factory({ typescript: { ScriptSnapshot: { fromString: snapshot } } })
33
+ .create({ languageService: {}, languageServiceHost: host, project: { projectService: { logger: { info: () => {} } } } })
34
+
35
+ const buffer = host.getScriptSnapshot('/tmp/page.stx')
36
+ const referenced = /^\/\/\/ <reference path="(.+)" \/>$/.exec(buffer.getText(0, buffer.getLength()).split('\n')[0])?.[1]
37
+ expect(referenced).toBe(join(typesDir, 'stx-module.d.ts'))
38
+ expect(existsSync(join(typesDir, 'stx.d.ts'))).toBeTrue()
39
+ }
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.45",
5
+ "version": "0.75.47",
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.45",
58
+ "@stacksjs/mobile": "^0.75.47",
59
59
  "@stacksjs/sanitizer": "^0.2.348",
60
60
  "ts-qr-codes": "^0.1.8"
61
61
  }
@@ -294,7 +294,10 @@ function clearFilters() {
294
294
  statusFilter.set('all')
295
295
  }
296
296
 
297
- async function openEditor(post: BlogPost) {
297
+ // `null` opens the editor on a new post: the "New post" buttons pass it, and
298
+ // the body below already handles it. Typed `BlogPost` alone, every one of those
299
+ // calls was a type error that a parse failure elsewhere in this file hid.
300
+ async function openEditor(post: BlogPost | null) {
298
301
  const requestId = ++editorRequestId
299
302
  formError.set('')
300
303
  editingPost.set(post)
@@ -83,9 +83,9 @@ jobs:
83
83
  persist-credentials: true
84
84
 
85
85
  - name: Setup Pantry
86
- uses: pantry-pm/pantry/packages/action@00f7f8d0f4b166db35d2e1405fdcefd442acd775 # v0.11.68
86
+ uses: pantry-pm/pantry/packages/action@605fd435116c4666ea81150add528760632e0bfb # v0.11.69
87
87
  with:
88
- version: '0.11.68'
88
+ version: '0.11.69'
89
89
  install: 'false'
90
90
 
91
91
  - name: Install Dependencies
@@ -30,7 +30,7 @@ jobs:
30
30
  uses: actions/checkout@v4
31
31
 
32
32
  - name: Setup Pantry
33
- uses: pantry-pm/pantry/packages/action@00f7f8d0f4b166db35d2e1405fdcefd442acd775 # v0.11.68
33
+ uses: pantry-pm/pantry/packages/action@605fd435116c4666ea81150add528760632e0bfb # v0.11.69
34
34
  with:
35
35
  install: 'false'
36
36
 
@@ -63,7 +63,7 @@ jobs:
63
63
  uses: actions/checkout@v4
64
64
 
65
65
  - name: Setup Pantry
66
- uses: pantry-pm/pantry/packages/action@00f7f8d0f4b166db35d2e1405fdcefd442acd775 # v0.11.68
66
+ uses: pantry-pm/pantry/packages/action@605fd435116c4666ea81150add528760632e0bfb # v0.11.69
67
67
  with:
68
68
  install: 'false'
69
69
 
@@ -99,7 +99,7 @@ jobs:
99
99
  uses: actions/checkout@v4
100
100
 
101
101
  - name: Setup Pantry
102
- uses: pantry-pm/pantry/packages/action@00f7f8d0f4b166db35d2e1405fdcefd442acd775 # v0.11.68
102
+ uses: pantry-pm/pantry/packages/action@605fd435116c4666ea81150add528760632e0bfb # v0.11.69
103
103
  with:
104
104
  install: 'false'
105
105
 
@@ -21,7 +21,7 @@ jobs:
21
21
  fetch-depth: 0
22
22
 
23
23
  - name: Setup Pantry
24
- uses: pantry-pm/pantry/packages/action@00f7f8d0f4b166db35d2e1405fdcefd442acd775 # v0.11.68
24
+ uses: pantry-pm/pantry/packages/action@605fd435116c4666ea81150add528760632e0bfb # v0.11.69
25
25
  with:
26
26
  install: 'false'
27
27