Skip to content

Commit 2a5a7c0

Browse files
authored
feat: add automatic new products setup (#2534)
1 parent 6b7d806 commit 2a5a7c0

File tree

3 files changed

+264
-0
lines changed

3 files changed

+264
-0
lines changed

β€ŽMakefileβ€Ž

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ generate-packages:
4646
generate-global-sdk-package:
4747
pnpm run generateGlobalSdkPackage
4848

49+
setup-new-products:
50+
pnpm run setupNewProducts
51+
4952
publish: install-dependencies
5053
pnpm run build
5154
pnpm lerna changed

β€Žpackage.jsonβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"generateAlias": "pnpm dlx tsx ./scripts/generateAlias.ts",
1515
"generatePackages": "pnpm dlx tsx ./scripts/generatePackages.ts",
1616
"generateGlobalSdkPackage": "pnpm dlx tsx ./scripts/updateGlobalSdkPackage.ts",
17+
"setupNewProducts": "pnpm dlx tsx ./scripts/setupNewProducts.ts",
1718
"prebuild": "pnpm run generatePackages && pnpm run generateAlias && pnpm format",
1819
"build:packages": "pnpm turbo run build",
1920
"fix-import-extensions": "pnpm dlx tsx ./scripts/fix-import-extensions.ts",

β€Žscripts/setupNewProducts.tsβ€Ž

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
#!/usr/bin/env node
2+
/* eslint-disable no-console */
3+
/**
4+
* setupNewProducts.ts
5+
* Usage:
6+
* pnpm tsx scripts/setupNewProducts.ts [--dry-run] [--src packages_generated] [--sdk packages/sdk/package.json] [--scope @scaleway|@scaleway-internal] [--install] [--verbose]
7+
*/
8+
9+
import {
10+
existsSync,
11+
readFileSync,
12+
readdirSync,
13+
statSync,
14+
writeFileSync,
15+
} from 'node:fs'
16+
import { join, resolve } from 'node:path'
17+
import { cwd } from 'node:process'
18+
import { execSync } from 'node:child_process'
19+
import type { ParseArgsConfig } from 'node:util'
20+
import { parseArgs } from 'node:util'
21+
import { snakeToSlug } from './helpers'
22+
23+
type Scope = '@scaleway' | '@scaleway-internal'
24+
25+
const options: ParseArgsConfig['options'] = {
26+
'dry-run': { type: 'boolean', default: false },
27+
src: { type: 'string', default: 'packages_generated' },
28+
sdk: { type: 'string', default: 'packages/sdk/package.json' },
29+
scope: { type: 'string' }, // optional override
30+
install: { type: 'boolean', default: true }, // run pnpm install
31+
verbose: { type: 'boolean', default: true },
32+
quiet: { type: 'boolean', default: false },
33+
}
34+
35+
const { values } = parseArgs({ options })
36+
const DRY_RUN = Boolean(values['dry-run'])
37+
const INSTALL = Boolean(values.install)
38+
const VERBOSE = values.quiet ? false : Boolean(values.verbose)
39+
const PACKAGES_GENERATED_DIR = resolve(cwd(), String(values.src))
40+
const SDK_PACKAGE_JSON = resolve(cwd(), String(values.sdk))
41+
42+
interface Product {
43+
name: string
44+
path: string
45+
hasGenFiles: boolean
46+
hasPackageJson: boolean
47+
}
48+
49+
const log = (...args: unknown[]) => {
50+
if (VERBOSE) console.log(...args)
51+
}
52+
const info = (...args: unknown[]) => console.log(...args)
53+
const warn = (...args: unknown[]) => console.warn(...args)
54+
const err = (...args: unknown[]) => console.error(...args)
55+
56+
function safeReadJson(path: string): unknown {
57+
try {
58+
return JSON.parse(readFileSync(path, 'utf8'))
59+
} catch (e) {
60+
throw new Error(`Failed to read/parse JSON: ${path}\n${String(e)}`)
61+
}
62+
}
63+
64+
function writeJsonIfChanged(path: string, data: unknown) {
65+
const newContent = JSON.stringify(data, null, 2) + '\n'
66+
const oldContent = existsSync(path) ? readFileSync(path, 'utf8') : ''
67+
if (oldContent !== newContent) {
68+
if (DRY_RUN) {
69+
info(` πŸ” DRY RUN: Would write ${path}`)
70+
} else {
71+
writeFileSync(path, newContent, 'utf8')
72+
info(` βœ… Updated ${path}`)
73+
}
74+
} else {
75+
log(` ℹ️ No change in ${path}`)
76+
}
77+
}
78+
79+
function walkHasGenFiles(root: string): boolean {
80+
if (!existsSync(root)) return false
81+
const stack = [root]
82+
while (stack.length) {
83+
const p = stack.pop()!
84+
const st = statSync(p)
85+
if (st.isDirectory()) {
86+
for (const name of readdirSync(p)) stack.push(join(p, name))
87+
} else if (p.endsWith('.gen.ts')) {
88+
return true
89+
}
90+
}
91+
return false
92+
}
93+
94+
function scanProducts(dir: string): Product[] {
95+
if (!existsSync(dir)) {
96+
throw new Error(`Directory not found: ${dir}`)
97+
}
98+
return readdirSync(dir)
99+
.map(name => ({ name, full: join(dir, name) }))
100+
.filter(e => statSync(e.full).isDirectory())
101+
.map(({ name, full }) => {
102+
const srcPath = join(full, 'src')
103+
const hasGenFiles = walkHasGenFiles(srcPath)
104+
const hasPackageJson = existsSync(join(full, 'package.json'))
105+
return { name, path: full, hasGenFiles, hasPackageJson }
106+
})
107+
}
108+
109+
function detectNewProducts(products: Product[]): Product[] {
110+
return products.filter(p => p.hasGenFiles && !p.hasPackageJson)
111+
}
112+
113+
function detectPackageScope(sdkPackageJsonPath: string): Scope {
114+
if (values.scope) {
115+
const s = String(values.scope) as Scope
116+
return s === '@scaleway-internal' ? s : '@scaleway'
117+
}
118+
if (!existsSync(sdkPackageJsonPath)) {
119+
warn('⚠️ SDK package.json not found, using @scaleway scope')
120+
return '@scaleway'
121+
}
122+
const sdkPackage = safeReadJson(sdkPackageJsonPath) as any
123+
const deps: Record<string, string> = sdkPackage?.dependencies ?? {}
124+
const hasInternal = Object.keys(deps).some(k =>
125+
k.startsWith('@scaleway-internal/sdk-'),
126+
)
127+
return hasInternal ? '@scaleway-internal' : '@scaleway'
128+
}
129+
130+
function updateSdkPackageJson(
131+
sdkPackageJsonPath: string,
132+
newProducts: Product[],
133+
scope: Scope,
134+
): { added: string[] } {
135+
if (!existsSync(sdkPackageJsonPath)) {
136+
warn('⚠️ SDK package.json not found, skipping update')
137+
return { added: [] }
138+
}
139+
140+
const sdkPackage = safeReadJson(sdkPackageJsonPath) as any
141+
sdkPackage.dependencies = sdkPackage.dependencies ?? {}
142+
sdkPackage.devDependencies = sdkPackage.devDependencies ?? {}
143+
144+
const added: string[] = []
145+
for (const product of newProducts) {
146+
const packageName = `${scope}/sdk-${snakeToSlug(product.name)}`
147+
const hasInDeps = Boolean(sdkPackage.dependencies[packageName])
148+
const hasInDev = Boolean(sdkPackage.devDependencies[packageName])
149+
150+
if (!hasInDeps) {
151+
// si prΓ©sent en devDependencies β†’ dΓ©placer
152+
if (hasInDev) {
153+
delete sdkPackage.devDependencies[packageName]
154+
}
155+
sdkPackage.dependencies[packageName] = 'workspace:*'
156+
added.push(packageName)
157+
info(` βœ… Added dependency: ${packageName}`)
158+
}
159+
}
160+
161+
// sort deps
162+
const sortObj = (o: Record<string, string>) =>
163+
Object.fromEntries(
164+
Object.keys(o)
165+
.sort()
166+
.map(k => [k, o[k]]),
167+
)
168+
169+
sdkPackage.dependencies = sortObj(sdkPackage.dependencies)
170+
sdkPackage.devDependencies = sortObj(sdkPackage.devDependencies)
171+
172+
writeJsonIfChanged(sdkPackageJsonPath, sdkPackage)
173+
return { added }
174+
}
175+
176+
function runCommand(command: string, description: string): void {
177+
info(` πŸ”§ ${description}…`)
178+
if (DRY_RUN) {
179+
info(` πŸ” DRY RUN: Would run: ${command}`)
180+
return
181+
}
182+
try {
183+
execSync(command, { stdio: 'inherit', cwd: cwd() })
184+
info(` βœ… ${description} completed`)
185+
} catch (e) {
186+
throw new Error(`${description} failed.\nCommand: ${command}\n${String(e)}`)
187+
}
188+
}
189+
190+
async function main(): Promise<number> {
191+
info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
192+
info('πŸ€– Setup New Products')
193+
info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n')
194+
195+
if (DRY_RUN) info('πŸ” DRY RUN MODE - No changes will be made\n')
196+
197+
// 1) Scan
198+
info('πŸ” Step 1: Scanning for products…')
199+
const allProducts = scanProducts(PACKAGES_GENERATED_DIR)
200+
const newProducts = detectNewProducts(allProducts)
201+
info(` πŸ“¦ Total products: ${allProducts.length}`)
202+
info(` πŸ†• New products: ${newProducts.length}`)
203+
if (newProducts.length) {
204+
info(' πŸ“‹ New products detected:')
205+
for (const p of newProducts) info(` - ${p.name}`)
206+
}
207+
info('')
208+
209+
if (newProducts.length === 0) {
210+
info('βœ… No new products to configure\n')
211+
return 0
212+
}
213+
214+
// 2) generatePackages
215+
info('βš™οΈ Step 2: Generating package configuration files…')
216+
runCommand('pnpm run generatePackages', 'Generate package configs')
217+
info('')
218+
219+
// 3) Update SDK package.json
220+
info('πŸ“ Step 3: Updating SDK package.json…')
221+
const scope = detectPackageScope(SDK_PACKAGE_JSON)
222+
info(` πŸ“¦ Detected scope: ${scope}`)
223+
updateSdkPackageJson(SDK_PACKAGE_JSON, newProducts, scope)
224+
info('')
225+
226+
// 4) alias
227+
info('πŸ“ Step 4: Generating SDK exports…')
228+
runCommand('pnpm run generateAlias', 'Generate SDK exports')
229+
info('')
230+
231+
// 5) install
232+
if (INSTALL) {
233+
info('πŸ“¦ Step 5: Updating dependencies…')
234+
runCommand('pnpm install --no-frozen-lockfile', 'Update dependencies')
235+
info('')
236+
} else {
237+
info('πŸ“¦ Step 5: Skipped install (use --install to enable)\n')
238+
}
239+
240+
// Summary
241+
info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
242+
info('βœ… Setup Complete!')
243+
info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n')
244+
info('πŸ“Š Summary:')
245+
info(` - New products configured: ${newProducts.length}`)
246+
if (newProducts.length) {
247+
info(' - Products:')
248+
for (const p of newProducts) info(` β€’ ${p.name}`)
249+
}
250+
if (DRY_RUN) info('\nπŸ” DRY RUN MODE - No changes were made')
251+
info('')
252+
return 0
253+
}
254+
255+
main()
256+
.then(code => process.exit(code))
257+
.catch(e => {
258+
err('❌ Error:', e instanceof Error ? e.message : String(e))
259+
process.exit(1)
260+
})

0 commit comments

Comments
Β (0)