@take-out/scripts 0.2.1 → 0.2.2

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.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. package/src/env-update.ts +238 -103
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@take-out/scripts",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "main": "./src/cmd.ts",
6
6
  "sideEffects": false,
@@ -30,8 +30,8 @@
30
30
  "dependencies": {
31
31
  "@clack/prompts": "^0.8.2",
32
32
  "@lydell/node-pty": "^1.2.0-beta.3",
33
- "@take-out/helpers": "0.2.1",
34
- "@take-out/run": "0.2.1",
33
+ "@take-out/helpers": "0.2.2",
34
+ "@take-out/run": "0.2.2",
35
35
  "picocolors": "^1.1.1"
36
36
  },
37
37
  "peerDependencies": {
package/src/env-update.ts CHANGED
@@ -1,12 +1,35 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import fs from 'node:fs'
4
+ import path from 'node:path'
4
5
 
5
6
  import { cmd } from './cmd'
6
7
  import { resolveDepVersion } from './helpers/resolve-dep-version'
7
8
 
8
- await cmd`sync environment variables from package.json to CI and server configs`.run(
9
- async () => {
9
+ const PRESETS: Record<string, string[]> = {
10
+ takeout: [
11
+ '.github/workflows/*.yml',
12
+ 'src/uncloud/*.yml',
13
+ '.env*',
14
+ 'src/env.ts',
15
+ 'src/constants/env-server.ts',
16
+ 'src/server/env-server.ts',
17
+ ],
18
+ }
19
+
20
+ const IGNORE_DIRS = new Set([
21
+ 'node_modules',
22
+ '.git',
23
+ 'dist',
24
+ 'build',
25
+ '.next',
26
+ '.cache',
27
+ '.docker',
28
+ ])
29
+
30
+ await cmd`sync environment variables from package.json to matching files`
31
+ .args('--preset string')
32
+ .run(async ({ args }) => {
10
33
  const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
11
34
  const envVars = packageJson.env as Record<string, boolean | string>
12
35
 
@@ -15,75 +38,192 @@ await cmd`sync environment variables from package.json to CI and server configs`
15
38
  process.exit(1)
16
39
  }
17
40
 
41
+ // resolve $dep: values
42
+ const depResolved: Record<string, string> = {}
43
+ for (const [key, value] of Object.entries(envVars)) {
44
+ const dep = resolveDepVersion(value, packageJson.dependencies)
45
+ if (dep) {
46
+ depResolved[key] = dep
47
+ }
48
+ }
49
+
50
+ // load .gitignore patterns
51
+ const gitignorePatterns: string[] = []
52
+ try {
53
+ const gi = fs.readFileSync('.gitignore', 'utf-8')
54
+ for (const line of gi.split('\n')) {
55
+ const trimmed = line.trim()
56
+ if (trimmed && !trimmed.startsWith('#')) {
57
+ gitignorePatterns.push(trimmed.replace(/\/$/, ''))
58
+ }
59
+ }
60
+ } catch {}
61
+
62
+ function isIgnored(filePath: string): boolean {
63
+ const parts = filePath.split('/')
64
+ for (const part of parts) {
65
+ if (IGNORE_DIRS.has(part)) return true
66
+ }
67
+ for (const pattern of gitignorePatterns) {
68
+ if (filePath.startsWith(pattern) || parts.some((p) => p === pattern)) {
69
+ return true
70
+ }
71
+ }
72
+ return false
73
+ }
74
+
75
+ // simple glob: supports *.ext, dir/*.ext, dir/**/*.ext, .prefix*
76
+ function expandPattern(pattern: string): string[] {
77
+ const results: string[] = []
78
+
79
+ if (!pattern.includes('*')) {
80
+ if (fs.existsSync(pattern)) results.push(pattern)
81
+ return results
82
+ }
83
+
84
+ const isRecursive = pattern.includes('**')
85
+ const parts = pattern.split('**/')
86
+ const baseDir = parts.length > 1 ? parts[0]!.replace(/\/$/, '') || '.' : '.'
87
+ const filePattern =
88
+ parts.length > 1
89
+ ? parts[1]!
90
+ : pattern.includes('/')
91
+ ? pattern.split('/').pop()!
92
+ : pattern
93
+
94
+ // for non-recursive patterns with a dir prefix like "src/uncloud/*.yml"
95
+ const nonRecursiveDir =
96
+ !isRecursive && pattern.includes('/')
97
+ ? pattern.slice(0, pattern.lastIndexOf('/'))
98
+ : null
99
+
100
+ const matchRe = new RegExp(
101
+ '^' + filePattern.replace(/\./g, '\\.').replace(/\*/g, '[^/]*') + '$'
102
+ )
103
+
104
+ function walk(dir: string) {
105
+ let entries: fs.Dirent[]
106
+ try {
107
+ entries = fs.readdirSync(dir, { withFileTypes: true })
108
+ } catch {
109
+ return
110
+ }
111
+ for (const entry of entries) {
112
+ const full = dir === '.' ? entry.name : `${dir}/${entry.name}`
113
+ if (isRecursive && isIgnored(full)) continue
114
+ if (entry.isDirectory()) {
115
+ if (isRecursive) walk(full)
116
+ } else if (matchRe.test(entry.name)) {
117
+ results.push(full)
118
+ }
119
+ }
120
+ }
121
+
122
+ walk(nonRecursiveDir || baseDir)
123
+ return results
124
+ }
125
+
126
+ // resolve file list: --preset > CLI args > package.json config > preset:takeout
127
+ let patterns: string[]
128
+ if (args.preset) {
129
+ const preset = PRESETS[args.preset]
130
+ if (!preset) {
131
+ console.error(
132
+ `Unknown preset: ${args.preset}\nAvailable: ${Object.keys(PRESETS).join(', ')}`
133
+ )
134
+ process.exit(1)
135
+ }
136
+ patterns = preset
137
+ } else if (args.rest.length) {
138
+ patterns = args.rest
139
+ } else {
140
+ patterns = packageJson.envUpdateFiles || PRESETS.takeout!
141
+ }
142
+
143
+ // expand all patterns
144
+ const files: string[] = []
145
+ for (const p of patterns) {
146
+ files.push(...expandPattern(p))
147
+ }
148
+
149
+ // markers
18
150
  const markerStart = '🔒 start - this is generated by "bun env:update"'
19
151
  const markerEnd = '🔒 end - this is generated by "bun env:update"'
152
+ const yamlStart = `# ${markerStart}`
153
+ const yamlEnd = `# ${markerEnd}`
154
+ const jsStart = `// ${markerStart}`
155
+ const jsEnd = `// ${markerEnd}`
20
156
 
21
- const yamlStartMarker = `# ${markerStart}`
22
- const yamlEndMarker = `# ${markerEnd}`
157
+ type Strategy = 'yaml-markers' | 'js-markers' | 'dotenv-inline' | 'ts-inline'
23
158
 
24
- const jsStartMarker = `// ${markerStart}`
25
- const jsEndMarker = `// ${markerEnd}`
159
+ function detectStrategy(filePath: string, content: string): Strategy | null {
160
+ if (content.includes(yamlStart) && content.includes(yamlEnd)) {
161
+ return 'yaml-markers'
162
+ }
163
+ if (content.includes(jsStart) && content.includes(jsEnd)) {
164
+ return 'js-markers'
165
+ }
166
+ if (/^\.env/.test(path.basename(filePath))) {
167
+ return 'dotenv-inline'
168
+ }
169
+ if (filePath.endsWith('.ts') && Object.keys(depResolved).length > 0) {
170
+ return 'ts-inline'
171
+ }
172
+ return null
173
+ }
26
174
 
27
- function replaceYamlSection(
175
+ function escapeRegExp(s: string): string {
176
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
177
+ }
178
+
179
+ function replaceMarkerSection(
28
180
  content: string,
29
- lines: string,
30
- options: { indent: string }
181
+ startMarker: string,
182
+ endMarker: string,
183
+ newSection: string
31
184
  ): string {
32
- return content.replace(
33
- new RegExp(`${yamlStartMarker}(.|\n)*?${yamlEndMarker}`, 'gm'),
34
- `${yamlStartMarker}\n${lines}\n${options.indent}${yamlEndMarker}`
185
+ const re = new RegExp(
186
+ `^([ \\t]*)${escapeRegExp(startMarker)}(.|\n)*?${escapeRegExp(endMarker)}`,
187
+ 'gm'
35
188
  )
189
+ return content.replace(re, (_match, indent) => {
190
+ return `${indent}${startMarker}\n${newSection}\n${indent}${endMarker}`
191
+ })
36
192
  }
37
193
 
38
- function updateDeployYml() {
39
- const deployYmlPath = '.github/workflows/ci.yml'
40
- let deployYml = fs.readFileSync(deployYmlPath, 'utf-8')
194
+ function applyYamlMarkers(filePath: string, content: string): string {
195
+ const markerMatch = content.match(
196
+ new RegExp(`^(\\s*)${escapeRegExp(yamlStart)}`, 'm')
197
+ )
198
+ const indent = markerMatch?.[1] || ' '
41
199
 
42
- if (!deployYml.includes(yamlStartMarker) || !deployYml.includes(yamlEndMarker)) {
43
- throw new Error(`Markers not found in ${deployYmlPath}`)
44
- }
200
+ const beforeMarker = content.slice(0, content.indexOf(yamlStart))
201
+ const isMap = /x-[\w-]+:.*&[\w-]+/s.test(beforeMarker)
202
+ const isGithubActions = filePath.includes('.github/workflows')
45
203
 
46
- const indent = ` `
47
- const envSection = Object.entries(envVars)
204
+ const envLines = Object.entries(envVars)
48
205
  .map(([key, value]) => {
49
- const resolved = resolveDepVersion(value, packageJson.dependencies)
50
- if (resolved) {
51
- return `${indent}${key}: \${{ secrets.${key} || '${resolved}' }}`
206
+ const dep = resolveDepVersion(value, packageJson.dependencies)
207
+ const defaultVal = dep || (typeof value === 'string' ? value : '')
208
+
209
+ if (isGithubActions) {
210
+ return dep
211
+ ? `${indent}${key}: \${{ secrets.${key} || '${dep}' }}`
212
+ : `${indent}${key}: \${{ secrets.${key} }}`
52
213
  }
53
- return `${indent}${key}: \${{ secrets.${key} }}`
214
+
215
+ return isMap
216
+ ? `${indent}${key}: \${${key}:-${defaultVal}}`
217
+ : `${indent}- ${key}=\${${key}:-${defaultVal}}`
54
218
  })
55
219
  .join('\n')
56
220
 
57
- const newDeployYml = replaceYamlSection(deployYml, envSection, { indent })
58
-
59
- fs.writeFileSync(deployYmlPath, newDeployYml, 'utf-8')
60
- console.info('✅ Updated Github workflow')
221
+ return replaceMarkerSection(content, yamlStart, yamlEnd, envLines)
61
222
  }
62
223
 
63
- function updateEnvServerTs() {
64
- const candidates = ['src/constants/env-server.ts', 'src/server/env-server.ts']
65
- let envServerPath = ''
66
- let envServer = ''
67
-
68
- for (const p of candidates) {
69
- try {
70
- envServer = fs.readFileSync(p, 'utf-8')
71
- envServerPath = p
72
- break
73
- } catch {}
74
- }
75
-
76
- if (!envServerPath) {
77
- throw new Error(`env-server.ts not found at ${candidates.join(' or ')}`)
78
- }
79
-
80
- if (!envServer.includes(jsStartMarker) || !envServer.includes(jsEndMarker)) {
81
- throw new Error(`Markers not found in ${envServerPath}`)
82
- }
83
-
224
+ function applyJsMarkers(_filePath: string, content: string): string {
84
225
  const envExports = Object.entries(envVars)
85
226
  .map(([key, value]) => {
86
- // $dep: values have no compile-time default — resolved into .env at runtime
87
227
  if (typeof value === 'string' && value.startsWith('$dep:')) {
88
228
  return `export const ${key} = ensureEnv('${key}')`
89
229
  }
@@ -91,69 +231,64 @@ await cmd`sync environment variables from package.json to CI and server configs`
91
231
  })
92
232
  .join('\n')
93
233
 
94
- const newEnvServer = envServer.replace(
95
- new RegExp(`${jsStartMarker}(.|\n)*?${jsEndMarker}`, 'm'),
96
- `${jsStartMarker}\n${envExports}\n${jsEndMarker}`
97
- )
98
-
99
- fs.writeFileSync(envServerPath, newEnvServer, 'utf-8')
100
- console.info('✅ Updated server env')
234
+ return replaceMarkerSection(content, jsStart, jsEnd, envExports)
101
235
  }
102
236
 
103
- function updateDockerCompose() {
104
- const dockerComposePath = 'src/uncloud/docker-compose.yml'
105
-
106
- let dockerCompose = ''
107
- try {
108
- dockerCompose = fs.readFileSync(dockerComposePath, 'utf-8')
109
- } catch (_error) {
110
- // file doesn't exist, skip
111
- return
237
+ function applyDotenvInline(_filePath: string, content: string): string {
238
+ let result = content
239
+ for (const [key, value] of Object.entries(depResolved)) {
240
+ const re = new RegExp(`^(${key})=(.+)$`, 'm')
241
+ const match = result.match(re)
242
+ if (match && match[2] !== value) {
243
+ result = result.replace(re, `$1=${value}`)
244
+ }
112
245
  }
246
+ return result
247
+ }
113
248
 
114
- if (
115
- !dockerCompose.includes(yamlStartMarker) ||
116
- !dockerCompose.includes(yamlEndMarker)
117
- ) {
118
- // no markers, skip
119
- return
249
+ function applyTsInline(_filePath: string, content: string): string {
250
+ let result = content
251
+ for (const [key, value] of Object.entries(depResolved)) {
252
+ const re = new RegExp(`(${key}:\\s*)(['"])([^'"]+)\\2`, 'g')
253
+ result = result.replace(re, (_match, prefix, quote, oldValue) => {
254
+ if (oldValue !== value) return `${prefix}${quote}${value}${quote}`
255
+ return _match
256
+ })
120
257
  }
258
+ return result
259
+ }
121
260
 
122
- // detect indent from the marker line
123
- const markerMatch = dockerCompose.match(
124
- new RegExp(`^(\\s*)${yamlStartMarker}`, 'm')
125
- )
126
- const indent = markerMatch?.[1] || ' '
261
+ const strategies: Record<Strategy, (filePath: string, content: string) => string> = {
262
+ 'yaml-markers': applyYamlMarkers,
263
+ 'js-markers': applyJsMarkers,
264
+ 'dotenv-inline': applyDotenvInline,
265
+ 'ts-inline': applyTsInline,
266
+ }
127
267
 
128
- // detect format: if markers are under an x- anchor block, use map syntax (KEY: val)
129
- // otherwise use list syntax (- KEY=val)
130
- const beforeMarker = dockerCompose.slice(0, dockerCompose.indexOf(yamlStartMarker))
131
- const isMap = /x-[\w-]+:.*&[\w-]+/s.test(beforeMarker)
268
+ let updated = 0
132
269
 
133
- const envLines = Object.entries(envVars)
134
- .map(([key, value]) => {
135
- const resolved = resolveDepVersion(value, packageJson.dependencies)
136
- const defaultVal = resolved || (typeof value === 'string' ? value : '')
137
- return isMap
138
- ? `${indent}${key}: \${${key}:-${defaultVal}}`
139
- : `${indent}- ${key}=\${${key}:-${defaultVal}}`
140
- })
141
- .join('\n')
270
+ for (const filePath of files) {
271
+ let content = ''
272
+ try {
273
+ content = fs.readFileSync(filePath, 'utf-8')
274
+ } catch {
275
+ continue
276
+ }
142
277
 
143
- const newDockerCompose = replaceYamlSection(dockerCompose, envLines, { indent })
278
+ const strategy = detectStrategy(filePath, content)
279
+ if (!strategy) continue
144
280
 
145
- fs.writeFileSync(dockerComposePath, newDockerCompose, 'utf-8')
146
- console.info('✅ Updated docker-compose.yml')
281
+ const newContent = strategies[strategy](filePath, content)
282
+ if (newContent !== content) {
283
+ fs.writeFileSync(filePath, newContent, 'utf-8')
284
+ console.info(`✅ ${filePath} (${strategy})`)
285
+ updated++
286
+ }
147
287
  }
148
288
 
149
- try {
150
- updateDeployYml()
151
- updateEnvServerTs()
152
- updateDockerCompose()
153
- console.info('✅ All files updated successfully')
154
- } catch (error: any) {
155
- console.error('❌ Update failed:', error.message)
156
- process.exit(1)
289
+ if (updated > 0) {
290
+ console.info(`\n✅ Updated ${updated} file${updated > 1 ? 's' : ''}`)
291
+ } else {
292
+ console.info('All files up to date')
157
293
  }
158
- }
159
- )
294
+ })