@stacksjs/defaults 0.75.61 → 0.75.63

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.
@@ -1,268 +0,0 @@
1
- import { describe, expect, it } from 'bun:test'
2
- import {
3
- argumentHint,
4
- buddyCommandLine,
5
- choosePreviewUrl,
6
- configuredAppPath,
7
- freeTextCommandLine,
8
- isLoopbackUrl,
9
- loadProjectEnv,
10
- parseBuddyList,
11
- parseBuddyListJson,
12
- parseBuddyListText,
13
- parseDevBannerUrl,
14
- parseEnvFile,
15
- pickProjectRoot,
16
- planPreview,
17
- resolvePrettyDomain,
18
- } from '../src/project'
19
-
20
- describe('parseEnvFile', () => {
21
- it('reads plain, exported, quoted and commented values', () => {
22
- const env = parseEnvFile([
23
- '# a comment',
24
- 'APP_NAME=Stacks',
25
- 'export PORT=3100',
26
- 'APP_URL="my-app.localhost"',
27
- 'QUOTED=\'single # not a comment\'',
28
- 'TRAILING=value # a comment',
29
- 'EMPTY=',
30
- 'not a pair',
31
- '',
32
- ].join('\n'))
33
-
34
- expect(env).toEqual({
35
- APP_NAME: 'Stacks',
36
- PORT: '3100',
37
- APP_URL: 'my-app.localhost',
38
- QUOTED: 'single # not a comment',
39
- TRAILING: 'value',
40
- EMPTY: '',
41
- })
42
- })
43
-
44
- it('handles CRLF line endings', () => {
45
- expect(parseEnvFile('PORT=3200\r\nAPP_URL=x.test\r\n')).toEqual({ PORT: '3200', APP_URL: 'x.test' })
46
- })
47
- })
48
-
49
- describe('loadProjectEnv', () => {
50
- it('lets later dev env files override earlier ones, as @stacksjs/env loads them', () => {
51
- const files: Record<string, string> = {
52
- '.env': 'PORT=3000\nAPP_URL=stacks.localhost',
53
- '.env.local': 'PORT=3100',
54
- '.env.development.local': 'APP_URL=override.localhost',
55
- }
56
-
57
- expect(loadProjectEnv(file => files[file])).toEqual({ PORT: '3100', APP_URL: 'override.localhost' })
58
- })
59
-
60
- it('skips dotenvx ciphertext instead of returning it', () => {
61
- const env = loadProjectEnv(file => file === '.env' ? 'PORT=3000\nAPP_URL=encrypted:BCx9abc' : undefined)
62
- expect(env).toEqual({ PORT: '3000' })
63
- })
64
- })
65
-
66
- describe('resolvePrettyDomain', () => {
67
- it('mirrors buddy dev: a non-loopback APP_URL is a pretty domain', () => {
68
- expect(resolvePrettyDomain('stacks.localhost')).toBe('stacks.localhost')
69
- expect(resolvePrettyDomain('https://My-App.test/path')).toBe('my-app.test')
70
- expect(resolvePrettyDomain('localhost:3000')).toBeNull()
71
- expect(resolvePrettyDomain('http://127.0.0.1')).toBeNull()
72
- expect(resolvePrettyDomain(undefined)).toBeNull()
73
- })
74
- })
75
-
76
- describe('configuredAppPath', () => {
77
- it('prefers APP_PATH, then appPath in config/app.ts, then /', () => {
78
- const config = 'export default {\n name: \'x\',\n appPath: \'/dashboard\',\n}'
79
- expect(configuredAppPath({ APP_PATH: 'admin' }, config)).toBe('/admin')
80
- expect(configuredAppPath({}, config)).toBe('/dashboard')
81
- expect(configuredAppPath({}, 'export default { name: \'x\' }')).toBe('/')
82
- expect(configuredAppPath({})).toBe('/')
83
- })
84
- })
85
-
86
- describe('planPreview', () => {
87
- it('defaults to https://stacks.localhost with localhost:3000 behind it', () => {
88
- expect(planPreview({ env: {} })).toEqual({
89
- server: { host: 'localhost', port: 3000 },
90
- candidates: [
91
- { url: 'https://stacks.localhost', host: 'stacks.localhost', port: 443 },
92
- { url: 'http://localhost:3000', host: 'localhost', port: 3000 },
93
- ],
94
- })
95
- })
96
-
97
- it('uses PORT, APP_URL and the entry path', () => {
98
- const plan = planPreview({ env: { PORT: '3140', APP_URL: 'https://shop.test', APP_PATH: '/admin' } })
99
- expect(plan.server).toEqual({ host: 'localhost', port: 3140 })
100
- expect(plan.candidates.map(candidate => candidate.url)).toEqual(['https://shop.test/admin', 'http://localhost:3140/admin'])
101
- })
102
-
103
- it('stays on localhost for STACKS_DEV_LOCALHOST=1, a loopback APP_URL, or the setting', () => {
104
- const only = (plan: ReturnType<typeof planPreview>) => plan.candidates.map(candidate => candidate.url)
105
- expect(only(planPreview({ env: { STACKS_DEV_LOCALHOST: '1' } }))).toEqual(['http://localhost:3000'])
106
- expect(only(planPreview({ env: { APP_URL: 'http://localhost:3000' } }))).toEqual(['http://localhost:3000'])
107
- expect(only(planPreview({ env: {}, preferLocalhost: true }))).toEqual(['http://localhost:3000'])
108
- })
109
-
110
- it('ignores a PORT that is not a number', () => {
111
- expect(planPreview({ env: { PORT: 'abc', STACKS_DEV_LOCALHOST: '1' } }).server.port).toBe(3000)
112
- })
113
- })
114
-
115
- describe('choosePreviewUrl', () => {
116
- const plan = planPreview({ env: {} })
117
- const probeOf = (open: string[]) => async (host: string, port: number) => open.includes(`${host}:${port}`)
118
-
119
- it('returns nothing when the frontend port is closed, even if the shared proxy answers', async () => {
120
- expect(await choosePreviewUrl(plan, probeOf(['stacks.localhost:443']))).toBeUndefined()
121
- })
122
-
123
- it('prefers the pretty URL when both the server and the proxy answer', async () => {
124
- expect(await choosePreviewUrl(plan, probeOf(['localhost:3000', 'stacks.localhost:443']))).toBe('https://stacks.localhost')
125
- })
126
-
127
- it('falls back to localhost when the proxy is down', async () => {
128
- expect(await choosePreviewUrl(plan, probeOf(['localhost:3000']))).toBe('http://localhost:3000')
129
- })
130
- })
131
-
132
- describe('parseDevBannerUrl', () => {
133
- it('reads the App/Site line of the buddy dev banner, colors and all', () => {
134
- const banner = [
135
- '\u001B[32m➜\u001B[39m \u001B[1mApp \u001B[22m: \u001B[36mhttps://stacks.localhost/dashboard\u001B[39m',
136
- ' ➜ Site: https://stacks.localhost',
137
- ' ➜ API: https://stacks.localhost/api',
138
- ].join('\n')
139
-
140
- expect(parseDevBannerUrl(banner)).toBe('https://stacks.localhost/dashboard')
141
- expect(parseDevBannerUrl(' ➜ Site : http://localhost:3001')).toBe('http://localhost:3001')
142
- })
143
-
144
- it('returns undefined before the banner is printed', () => {
145
- expect(parseDevBannerUrl('Starting dev server...\n ➜ API: http://localhost:3008')).toBeUndefined()
146
- })
147
- })
148
-
149
- const LIST_TEXT = `[env] loaded 33/49 variables from .env
150
-
151
- Available Commands:
152
-
153
- Development:
154
- build Build any of your libraries (packages) for production use
155
- dev Start development server
156
-
157
- make:
158
- make:model Create a new Model
159
- make:action Create a new Action
160
-
161
- upgrade:
162
- upgrade:dependencies Upgrade your dependencies (pantry.yaml & package.json)
163
- upgrade:bun Upgrade Bun to the latest version
164
-
165
- Total: 6 commands
166
- `
167
-
168
- describe('parseBuddyListText', () => {
169
- it('reads the indented command lines and skips headings and the total', () => {
170
- expect(parseBuddyListText(LIST_TEXT).map(command => [command.name, command.description])).toEqual([
171
- ['build', 'Build any of your libraries (packages) for production use'],
172
- ['dev', 'Start development server'],
173
- ['make:action', 'Create a new Action'],
174
- ['make:model', 'Create a new Model'],
175
- ['upgrade:bun', 'Upgrade Bun to the latest version'],
176
- ['upgrade:dependencies', 'Upgrade your dependencies (pantry.yaml & package.json)'],
177
- ])
178
- })
179
- })
180
-
181
- const LIST_JSON = `[env] loaded 33/49 variables from .env
182
- ${JSON.stringify({
183
- commands: [
184
- { name: 'make:model', description: 'Create a new Model', usage: '$ buddy make:model <name>', arguments: [{ name: 'name', required: true, variadic: false }], options: [] },
185
- { name: 'dev', description: 'Start development server', usage: '$ buddy dev [server]', arguments: [{ name: 'server', required: false, variadic: false }], options: [] },
186
- { name: 'rm -rf /; x', description: 'not a command name' },
187
- ],
188
- total: 3,
189
- }, null, 2)}`
190
-
191
- describe('parseBuddyListJson', () => {
192
- it('reads the inventory past any leading log line and drops malformed names', () => {
193
- expect(parseBuddyListJson(LIST_JSON)).toEqual([
194
- { name: 'dev', description: 'Start development server', usage: 'buddy dev [server]', arguments: [{ name: 'server', required: false, variadic: false }] },
195
- { name: 'make:model', description: 'Create a new Model', usage: 'buddy make:model <name>', arguments: [{ name: 'name', required: true, variadic: false }] },
196
- ])
197
- })
198
-
199
- it('returns undefined for output that is not the JSON inventory', () => {
200
- expect(parseBuddyListJson(LIST_TEXT)).toBeUndefined()
201
- expect(parseBuddyListJson('{ not json')).toBeUndefined()
202
- expect(parseBuddyListJson('{"something":"else"}')).toBeUndefined()
203
- })
204
-
205
- it('parseBuddyList falls back to the text listing', () => {
206
- expect(parseBuddyList(LIST_TEXT)).toHaveLength(6)
207
- expect(parseBuddyList(LIST_JSON)).toHaveLength(2)
208
- })
209
- })
210
-
211
- describe('command lines', () => {
212
- it('always runs the project ./buddy', () => {
213
- expect(buddyCommandLine('dev')).toBe('./buddy dev')
214
- expect(buddyCommandLine('make:model', 'Post')).toBe('./buddy make:model Post')
215
- expect(buddyCommandLine('dev', ['frontend', '--with-localhost'])).toBe('./buddy dev frontend --with-localhost')
216
- expect(buddyCommandLine('migrate', ' ')).toBe('./buddy migrate')
217
- })
218
-
219
- it('refuses a name that is not a buddy command', () => {
220
- expect(() => buddyCommandLine('dev; rm -rf ~')).toThrow()
221
- expect(() => buddyCommandLine('')).toThrow()
222
- })
223
-
224
- it('turns free text into a ./buddy line, dropping a typed buddy/bud/stacks prefix', () => {
225
- expect(freeTextCommandLine('migrate --diff')).toBe('./buddy migrate --diff')
226
- expect(freeTextCommandLine('buddy migrate --diff')).toBe('./buddy migrate --diff')
227
- expect(freeTextCommandLine('./buddy make:model Post')).toBe('./buddy make:model Post')
228
- expect(freeTextCommandLine('bud test')).toBe('./buddy test')
229
- expect(freeTextCommandLine('stacks list')).toBe('./buddy list')
230
- expect(freeTextCommandLine('buddyfile')).toBe('./buddy buddyfile')
231
- expect(freeTextCommandLine('buddy')).toBe('./buddy')
232
- expect(freeTextCommandLine(' ')).toBeUndefined()
233
- })
234
-
235
- it('hints positional arguments', () => {
236
- expect(argumentHint({
237
- name: 'x',
238
- description: '',
239
- arguments: [
240
- { name: 'name', required: true, variadic: false },
241
- { name: 'files', required: false, variadic: true },
242
- ],
243
- })).toBe('<name> [files...]')
244
- })
245
- })
246
-
247
- describe('pickProjectRoot', () => {
248
- const projects = new Set(['/work/app', '/work/other-app'])
249
- const isProject = (folder: string) => projects.has(folder)
250
-
251
- it('prefers the active editor folder when it is a Stacks project', () => {
252
- expect(pickProjectRoot(['/work/docs', '/work/app', '/work/other-app'], '/work/other-app', isProject)).toBe('/work/other-app')
253
- })
254
-
255
- it('falls back to the first Stacks folder', () => {
256
- expect(pickProjectRoot(['/work/docs', '/work/app'], '/work/docs', isProject)).toBe('/work/app')
257
- expect(pickProjectRoot(['/work/docs'], undefined, isProject)).toBeUndefined()
258
- })
259
- })
260
-
261
- describe('isLoopbackUrl', () => {
262
- it('detects URLs that need port forwarding in a remote window', () => {
263
- expect(isLoopbackUrl('http://localhost:3000')).toBe(true)
264
- expect(isLoopbackUrl('http://[::1]:3000')).toBe(true)
265
- expect(isLoopbackUrl('https://stacks.localhost')).toBe(false)
266
- expect(isLoopbackUrl('not a url')).toBe(false)
267
- })
268
- })
@@ -1,39 +0,0 @@
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
- }
@@ -1,19 +0,0 @@
1
- {
2
- // The extension is its own little package: a Node (CommonJS) bundle loaded by
3
- // VS Code, not part of the app or the framework, so no other tsconfig
4
- // includes it. `bun run typecheck` here checks it.
5
- "compilerOptions": {
6
- "target": "es2022",
7
- "lib": ["es2022"],
8
- "module": "esnext",
9
- "moduleResolution": "bundler",
10
- "types": ["bun", "vscode"],
11
- "strict": true,
12
- "noUncheckedIndexedAccess": true,
13
- "noEmit": true,
14
- "skipLibCheck": true,
15
- "isolatedModules": true,
16
- "verbatimModuleSyntax": true
17
- },
18
- "include": ["src/**/*.ts", "scripts/**/*.ts", "tests/**/*.ts"]
19
- }