@o/scripts 0.16.19 → 0.16.20-canary.1791839822276

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o/scripts",
3
- "version": "0.16.19",
3
+ "version": "0.16.20-canary.1791839822276",
4
4
  "type": "module",
5
5
  "main": "./src/cmd.ts",
6
6
  "sideEffects": false,
@@ -21,6 +21,7 @@
21
21
  },
22
22
  "scripts": {
23
23
  "lint": "oxlint src",
24
+ "test": "bun test src",
24
25
  "lint:fix": "oxfmt src && oxlint --fix --fix-suggestions src",
25
26
  "typecheck": "tko run typecheck"
26
27
  },
@@ -29,8 +30,8 @@
29
30
  },
30
31
  "dependencies": {
31
32
  "@clack/prompts": "^0.8.2",
32
- "@o/helpers": "0.16.19",
33
- "@o/run": "0.16.19",
33
+ "@o/helpers": "0.16.20-canary.1791839822276",
34
+ "@o/run": "0.16.20-canary.1791839822276",
34
35
  "picocolors": "^1.1.1",
35
36
  "@aws-sdk/client-s3": "^3.709.0",
36
37
  "fs-extra": "^11.2.0",
@@ -52,6 +53,5 @@
52
53
  "optional": true
53
54
  }
54
55
  },
55
- "devDependencies": null,
56
- "optionalDependencies": null
56
+ "releaseSourceCommit": "e7c02e302d6227ce686b136dcb361e5823d58d33"
57
57
  }
@@ -1,31 +1,29 @@
1
+ import { spawnSync } from 'node:child_process'
2
+
1
3
  import { cmd } from './cmd'
2
4
 
3
- const SQLITE_NODE_PATH =
4
- 'node_modules/@rocicorp/zero-sqlite3/build/Release/better_sqlite3.node'
5
+ await cmd`ensure zero-sqlite3 native module is available`.run(() => {
6
+ const result = spawnSync(
7
+ 'node',
8
+ [
9
+ '--input-type=commonjs',
10
+ '-e',
11
+ `const { createRequire } = require('node:module');
12
+ const { resolve } = require('node:path');
13
+ const load = createRequire(resolve('package.json'));
14
+ const Database = load('@rocicorp/zero-sqlite3');
15
+ const database = new Database(':memory:');
16
+ database.close();`,
17
+ ],
18
+ { stdio: 'inherit' }
19
+ )
5
20
 
6
- await cmd`ensure zero-sqlite3 native module is built`.run(async ({ $, fs }) => {
7
- if (fs.existsSync(SQLITE_NODE_PATH)) {
8
- return
21
+ if (result.error) {
22
+ throw result.error
9
23
  }
10
-
11
- // bun trustedDependencies doesn't reliably run zero-sqlite3's install script,
12
- // so build/fetch the native module ourselves. run the package's OWN install
13
- // chain (`npm run install` = `prebuild-install || <binding exists?> ||
14
- // node-gyp rebuild`), which prefers the PREBUILT binary and only source-builds
15
- // as a last resort. @rocicorp/zero-sqlite3 >=1.1.2 is prebuilt-ONLY — its npm
16
- // tarball omits generated headers (e.g. unicode_case_data.h), so a direct
17
- // `npm run build-release` (node-gyp) fails; prebuild-install is the supported
18
- // path. success is "the binding now exists" (the install chain can exit
19
- // non-zero from a failed prebuild-install fallback even when an earlier step
20
- // already produced the binding).
21
- const result = await $`cd node_modules/@rocicorp/zero-sqlite3 && npm run install`
22
- .quiet()
23
- .nothrow()
24
-
25
- if (!fs.existsSync(SQLITE_NODE_PATH)) {
26
- console.error(
27
- 'zero-sqlite3 native module unavailable, will fall back to wasm:',
28
- result.stderr.toString()
24
+ if (result.status !== 0) {
25
+ throw new Error(
26
+ 'zero-sqlite3 native module unavailable; install a compatible prebuilt for the current Node runtime'
29
27
  )
30
28
  }
31
29
  })
@@ -0,0 +1,72 @@
1
+ import { afterEach, expect, test } from 'bun:test'
2
+ import { execFileSync } from 'node:child_process'
3
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
4
+ import { tmpdir } from 'node:os'
5
+ import { join, resolve } from 'node:path'
6
+
7
+ const script = resolve(import.meta.dir, 'env-update.ts')
8
+ const directories: string[] = []
9
+
10
+ afterEach(() => {
11
+ for (const directory of directories.splice(0)) {
12
+ rmSync(directory, { recursive: true, force: true })
13
+ }
14
+ })
15
+
16
+ test('preserves explicit workflow bindings per block while adding new keys', () => {
17
+ const directory = mkdtempSync(join(tmpdir(), 'o-env-update-'))
18
+ directories.push(directory)
19
+ mkdirSync(join(directory, 'src'))
20
+ mkdirSync(join(directory, '.github/workflows'), { recursive: true })
21
+ writeFileSync(join(directory, 'package.json'), '{}')
22
+ writeFileSync(
23
+ join(directory, 'src/env.ts'),
24
+ `export const production = {
25
+ NEW_SERVICE_KEY: Symbol.for('take-out/env/expected'),
26
+ UNCHANGED_KEY: Symbol.for('take-out/env/expected'),
27
+ ADDED_KEY: Symbol.for('take-out/env/expected'),
28
+ }
29
+ export const versions = { ZERO_VERSION: '1.9.0' }
30
+ `
31
+ )
32
+
33
+ const workflow = join(directory, '.github/workflows/deploy.yml')
34
+ const initial = `jobs:
35
+ production:
36
+ env:
37
+ # 🔒 start - this is generated by "bun env:update"
38
+ NEW_SERVICE_KEY: \${{ secrets.DEPLOYED_SERVICE_KEY }}
39
+ UNCHANGED_KEY: \${{ secrets.UNCHANGED_KEY }}
40
+ ZERO_VERSION: \${{ secrets.ZERO_VERSION }}
41
+ REMOVED_KEY: \${{ secrets.REMOVED_KEY }}
42
+ # 🔒 end - this is generated by "bun env:update"
43
+ staging:
44
+ env:
45
+ # 🔒 start - this is generated by "bun env:update"
46
+ NEW_SERVICE_KEY: '\${{ secrets.STAGING_SERVICE_KEY }}' # explicit staging binding
47
+ UNCHANGED_KEY: \${{ secrets.UNCHANGED_KEY }}
48
+ # 🔒 end - this is generated by "bun env:update"
49
+ `
50
+ writeFileSync(workflow, initial)
51
+
52
+ const run = () => execFileSync(process.execPath, [script], { cwd: directory })
53
+ run()
54
+ const generated = readFileSync(workflow, 'utf8')
55
+ const expected = initial
56
+ .replaceAll(
57
+ ' NEW_SERVICE_KEY:',
58
+ ' ADDED_KEY: ${{ secrets.ADDED_KEY }}\n NEW_SERVICE_KEY:'
59
+ )
60
+ .replace('ZERO_VERSION: ${{ secrets.ZERO_VERSION }}', "ZERO_VERSION: '1.9.0'")
61
+ .replace(' REMOVED_KEY: ${{ secrets.REMOVED_KEY }}\n', '')
62
+ // the version is generated in every block, including newly populated blocks.
63
+ expect(generated).toBe(
64
+ expected.replace(
65
+ ' UNCHANGED_KEY: ${{ secrets.UNCHANGED_KEY }}\n # 🔒 end',
66
+ " UNCHANGED_KEY: ${{ secrets.UNCHANGED_KEY }}\n ZERO_VERSION: '1.9.0'\n # 🔒 end"
67
+ )
68
+ )
69
+
70
+ run()
71
+ expect(readFileSync(workflow, 'utf8')).toBe(generated)
72
+ })
package/src/env-update.ts CHANGED
@@ -201,49 +201,56 @@ await cmd`sync environment variables from src/env.ts to matching files`
201
201
  content: string,
202
202
  startMarker: string,
203
203
  endMarker: string,
204
- newSection: string
204
+ newSection: string | ((section: string, indent: string) => string)
205
205
  ): string {
206
206
  const re = new RegExp(
207
207
  `^([ \\t]*)${escapeRegExp(startMarker)}(.|\n)*?${escapeRegExp(endMarker)}`,
208
208
  'gm'
209
209
  )
210
- return content.replace(re, (_match, indent) => {
211
- return `${indent}${startMarker}\n${newSection}\n${indent}${endMarker}`
210
+ return content.replace(re, (match, indent) => {
211
+ const section =
212
+ typeof newSection === 'function' ? newSection(match, indent) : newSection
213
+ return `${indent}${startMarker}\n${section}\n${indent}${endMarker}`
212
214
  })
213
215
  }
214
216
 
215
217
  function applyYamlMarkers(filePath: string, content: string): string {
216
- const markerMatch = content.match(
217
- new RegExp(`^(\\s*)${escapeRegExp(yamlStart)}`, 'm')
218
- )
219
- const indent = markerMatch?.[1] || ' '
220
-
221
218
  const beforeMarker = content.slice(0, content.indexOf(yamlStart))
222
219
  const isMap = /x-[\w-]+:.*&[\w-]+/s.test(beforeMarker)
223
220
  const isGithubActions = filePath.includes('.github/workflows')
224
221
 
225
- const envLines = entries
226
- .map(({ key, defaultValue }) => {
227
- const dep = depResolved[key]
228
- const val = dep || defaultValue
229
-
230
- if (isGithubActions) {
231
- // deps-derived versions track package.json — emit a literal so a
232
- // stale repo secret can never shadow them (a stale ZERO_VERSION
233
- // secret once pinned an old zero-cache protocol against a newer
234
- // client). matches the .env codegen below, which already does this.
235
- return dep
236
- ? `${indent}${key}: '${dep}'`
237
- : `${indent}${key}: \${{ secrets.${key} }}`
222
+ return replaceMarkerSection(content, yamlStart, yamlEnd, (section, indent) => {
223
+ // bindings belong to their block; jobs may use different deployed secrets.
224
+ const bindings = new Map<string, string>()
225
+ if (isGithubActions) {
226
+ for (const match of section.matchAll(
227
+ /^[ \t]*([A-Za-z_][\w]*):[ \t]*(\S[^\r\n]*)/gm
228
+ )) {
229
+ bindings.set(match[1]!, match[2]!)
238
230
  }
231
+ }
239
232
 
240
- return isMap
241
- ? `${indent}${key}: \${${key}:-${val}}`
242
- : `${indent}- ${key}=\${${key}:-${val}}`
243
- })
244
- .join('\n')
245
-
246
- return replaceMarkerSection(content, yamlStart, yamlEnd, envLines)
233
+ return entries
234
+ .map(({ key, defaultValue }) => {
235
+ const dep = depResolved[key]
236
+ const val = dep || defaultValue
237
+
238
+ if (isGithubActions) {
239
+ // deps-derived versions track package.json — emit a literal so a
240
+ // stale repo secret can never shadow them (a stale ZERO_VERSION
241
+ // secret once pinned an old zero-cache protocol against a newer
242
+ // client). matches the .env codegen below, which already does this.
243
+ return dep
244
+ ? `${indent}${key}: '${dep}'`
245
+ : `${indent}${key}: ${bindings.get(key) ?? `\${{ secrets.${key} }}`}`
246
+ }
247
+
248
+ return isMap
249
+ ? `${indent}${key}: \${${key}:-${val}}`
250
+ : `${indent}- ${key}=\${${key}:-${val}}`
251
+ })
252
+ .join('\n')
253
+ })
247
254
  }
248
255
 
249
256
  function applyDotenvSection(_filePath: string, content: string): string {
package/src/up.ts CHANGED
@@ -7,6 +7,9 @@ interface PackageJson {
7
7
  devDependencies?: Record<string, string>
8
8
  peerDependencies?: Record<string, string>
9
9
  optionalDependencies?: Record<string, string>
10
+ workspaces?: string[] | { packages?: string[] }
11
+ upgradePackageJsonGlobs?: string[]
12
+ upgradeSets?: Record<string, string[]>
10
13
  }
11
14
 
12
15
  await cmd`upgrade packages by name or pattern`
@@ -22,6 +25,9 @@ await cmd`upgrade packages by name or pattern`
22
25
  fs.readFileSync(path.join(rootDir, 'package.json'), 'utf-8')
23
26
  )
24
27
  const upgradeSets: Record<string, string[]> = rootPackageJson.upgradeSets || {}
28
+ const upgradePackageJsonGlobs = Array.isArray(rootPackageJson.upgradePackageJsonGlobs)
29
+ ? rootPackageJson.upgradePackageJsonGlobs
30
+ : []
25
31
 
26
32
  for (const arg of args.rest) {
27
33
  if (arg in upgradeSets) {
@@ -48,18 +54,30 @@ await cmd`upgrade packages by name or pattern`
48
54
  process.exit(1)
49
55
  }
50
56
 
51
- function findPackageJsonFiles(dir: string): string[] {
57
+ function findPackageJsonFiles(
58
+ dir: string,
59
+ extraPackageJsonGlobs = upgradePackageJsonGlobs
60
+ ): string[] {
52
61
  const results: string[] = []
62
+ const seen = new Set<string>()
63
+
64
+ const addPackageJson = (packageJsonPath: string) => {
65
+ if (seen.has(packageJsonPath)) return
66
+ seen.add(packageJsonPath)
67
+ results.push(packageJsonPath)
68
+ }
53
69
 
54
70
  if (fs.existsSync(path.join(dir, 'package.json'))) {
55
- results.push(path.join(dir, 'package.json'))
71
+ addPackageJson(path.join(dir, 'package.json'))
56
72
  }
57
73
 
58
74
  // check if it's a monorepo with workspaces
59
75
  try {
60
76
  const packageJson = JSON.parse(
61
77
  fs.readFileSync(path.join(dir, 'package.json'), 'utf-8')
62
- )
78
+ ) as PackageJson
79
+ const packageJsonGlobs = [...extraPackageJsonGlobs]
80
+
63
81
  if (packageJson.workspaces) {
64
82
  let workspacePaths: string[] = []
65
83
 
@@ -69,9 +87,15 @@ await cmd`upgrade packages by name or pattern`
69
87
  workspacePaths = packageJson.workspaces.packages
70
88
  }
71
89
 
72
- for (const workspace of workspacePaths) {
90
+ packageJsonGlobs.unshift(...workspacePaths)
91
+ }
92
+
93
+ if (packageJsonGlobs.length > 0) {
94
+ for (const workspace of packageJsonGlobs) {
73
95
  // handle glob patterns like "packages/*", "code/**/*", "./code/ui/**/*"
74
- const normalizedWorkspace = workspace.replace(/^\.\//, '')
96
+ const normalizedWorkspace = workspace
97
+ .replace(/^\.\//, '')
98
+ .replace(/\/package\.json$/, '')
75
99
 
76
100
  if (normalizedWorkspace.includes('**')) {
77
101
  // nested glob pattern - use glob to find all package.json files
@@ -87,7 +111,7 @@ await cmd`upgrade packages by name or pattern`
87
111
  const subPath = path.join(searchDir, entry.name)
88
112
  const pkgPath = path.join(subPath, 'package.json')
89
113
  if (fs.existsSync(pkgPath)) {
90
- results.push(pkgPath)
114
+ addPackageJson(pkgPath)
91
115
  }
92
116
  // recurse into subdirectories
93
117
  findPackages(subPath)
@@ -112,7 +136,7 @@ await cmd`upgrade packages by name or pattern`
112
136
 
113
137
  for (const subdir of subdirs) {
114
138
  if (fs.existsSync(path.join(subdir, 'package.json'))) {
115
- results.push(path.join(subdir, 'package.json'))
139
+ addPackageJson(path.join(subdir, 'package.json'))
116
140
  }
117
141
  }
118
142
  }
@@ -120,7 +144,7 @@ await cmd`upgrade packages by name or pattern`
120
144
  // exact path like "code/tamagui.dev" or "./code/sandbox"
121
145
  const pkgPath = path.join(dir, normalizedWorkspace, 'package.json')
122
146
  if (fs.existsSync(pkgPath)) {
123
- results.push(pkgPath)
147
+ addPackageJson(pkgPath)
124
148
  }
125
149
  }
126
150
  }
@@ -219,7 +243,7 @@ await cmd`upgrade packages by name or pattern`
219
243
  }
220
244
 
221
245
  async function updatePackages(
222
- packagesByWorkspace: Map<string, { dir: string; packages: string[] }>,
246
+ packagesByManifest: Map<string, { dir: string; packages: string[] }>,
223
247
  rootDir: string,
224
248
  packageJsonFiles: string[]
225
249
  ) {
@@ -234,7 +258,7 @@ await cmd`upgrade packages by name or pattern`
234
258
 
235
259
  // collect all unique packages to update
236
260
  const allPackages = new Set<string>()
237
- for (const { packages } of packagesByWorkspace.values()) {
261
+ for (const { packages } of packagesByManifest.values()) {
238
262
  packages.forEach((pkg) => allPackages.add(pkg))
239
263
  }
240
264
 
@@ -272,14 +296,14 @@ await cmd`upgrade packages by name or pattern`
272
296
  let totalUpdates = 0
273
297
 
274
298
  for (const packageJsonPath of packageJsonFiles) {
275
- const packagesInWorkspace =
276
- packagesByWorkspace.get(getWorkspaceName(packageJsonPath, rootDir))?.packages ||
299
+ const packagesInManifest =
300
+ packagesByManifest.get(getWorkspaceName(packageJsonPath, rootDir))?.packages ||
277
301
  []
278
302
 
279
- if (packagesInWorkspace.length > 0) {
303
+ if (packagesInManifest.length > 0) {
280
304
  const updates = updatePackageJsonVersions(
281
305
  packageJsonPath,
282
- packagesInWorkspace,
306
+ packagesInManifest,
283
307
  versionMap
284
308
  )
285
309
  if (updates > 0) {
@@ -330,9 +354,11 @@ await cmd`upgrade packages by name or pattern`
330
354
  const packageJsonFiles = findPackageJsonFiles(rootDir)
331
355
  console.info(`Found ${packageJsonFiles.length} package.json files`)
332
356
 
357
+ const workspacePackageJsonFiles = findPackageJsonFiles(rootDir, [])
358
+
333
359
  // get workspace package names to exclude from updates
334
360
  const workspacePackageNames = new Set<string>()
335
- for (const packageJsonPath of packageJsonFiles) {
361
+ for (const packageJsonPath of workspacePackageJsonFiles) {
336
362
  if (packageJsonPath === path.join(rootDir, 'package.json')) continue
337
363
 
338
364
  try {
@@ -350,8 +376,8 @@ await cmd`upgrade packages by name or pattern`
350
376
  `Found ${workspacePackageNames.size} workspace packages to exclude from updates`
351
377
  )
352
378
 
353
- // build map of packages to update per workspace
354
- const packagesByWorkspace = new Map<string, { dir: string; packages: string[] }>()
379
+ // build map of packages to update per manifest
380
+ const packagesByManifest = new Map<string, { dir: string; packages: string[] }>()
355
381
  const allMatchingDeps = new Set<string>()
356
382
 
357
383
  for (const packageJsonPath of packageJsonFiles) {
@@ -374,7 +400,7 @@ await cmd`upgrade packages by name or pattern`
374
400
  if (matchingDeps.length > 0) {
375
401
  const dir = packageJsonPath.replace('/package.json', '')
376
402
  const name = getWorkspaceName(packageJsonPath, rootDir)
377
- packagesByWorkspace.set(name, { dir, packages: matchingDeps })
403
+ packagesByManifest.set(name, { dir, packages: matchingDeps })
378
404
  }
379
405
  }
380
406
 
@@ -397,7 +423,7 @@ await cmd`upgrade packages by name or pattern`
397
423
  allMatchingDeps.add(pkg)
398
424
  }
399
425
 
400
- packagesByWorkspace.set('root', {
426
+ packagesByManifest.set('root', {
401
427
  dir: rootDir,
402
428
  packages: exactPatterns,
403
429
  })
@@ -417,14 +443,14 @@ await cmd`upgrade packages by name or pattern`
417
443
  console.info(
418
444
  `Found ${allMatchingDeps.size} dependencies matching patterns: ${packagePatterns.join(', ')}`
419
445
  )
420
- console.info(`Found matches in ${packagesByWorkspace.size} workspace(s)`)
446
+ console.info(`Found matches in ${packagesByManifest.size} package manifest(s)`)
421
447
  }
422
448
 
423
449
  if (globalTag) {
424
450
  console.info(`🏷️ Using tag '${globalTag}'`)
425
451
  }
426
452
 
427
- await updatePackages(packagesByWorkspace, rootDir, packageJsonFiles)
453
+ await updatePackages(packagesByManifest, rootDir, packageJsonFiles)
428
454
 
429
455
  // sync resolved $dep: values (like ZERO_VERSION) to all env targets
430
456
  if (