@mono-labs/project 0.0.248
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/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/loadFromRoot.d.ts +33 -0
- package/dist/loadFromRoot.d.ts.map +1 -0
- package/dist/loadFromRoot.js +115 -0
- package/dist/project/build-mono-readme.d.ts +2 -0
- package/dist/project/build-mono-readme.d.ts.map +1 -0
- package/dist/project/build-mono-readme.js +451 -0
- package/dist/project/build-readme.d.ts +3 -0
- package/dist/project/build-readme.d.ts.map +1 -0
- package/dist/project/build-readme.js +4 -0
- package/dist/project/generate-docs.d.ts +12 -0
- package/dist/project/generate-docs.d.ts.map +1 -0
- package/dist/project/generate-docs.js +73 -0
- package/dist/project/generate-readme.d.ts +2 -0
- package/dist/project/generate-readme.d.ts.map +1 -0
- package/dist/project/generate-readme.js +309 -0
- package/dist/project/index.d.ts +41 -0
- package/dist/project/index.d.ts.map +1 -0
- package/dist/project/index.js +120 -0
- package/dist/project/merge-env.d.ts +2 -0
- package/dist/project/merge-env.d.ts.map +1 -0
- package/dist/project/merge-env.js +31 -0
- package/package.json +36 -0
- package/src/index.ts +19 -0
- package/src/loadFromRoot.ts +145 -0
- package/src/project/build-mono-readme.ts +540 -0
- package/src/project/build-readme.ts +2 -0
- package/src/project/generate-docs.ts +83 -0
- package/src/project/generate-readme.ts +351 -0
- package/src/project/index.ts +187 -0
- package/src/project/merge-env.ts +32 -0
- package/tsconfig.json +19 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// scripts/generate-repo-help.mjs
|
|
2
|
+
// Generates a developer-friendly workspace command reference.
|
|
3
|
+
//
|
|
4
|
+
// Output: docs/workspaces.md
|
|
5
|
+
//
|
|
6
|
+
// Run (from repo root):
|
|
7
|
+
// node ./scripts/generate-repo-help.mjs
|
|
8
|
+
//
|
|
9
|
+
// Philosophy:
|
|
10
|
+
// - Optimize for onboarding and day-to-day use
|
|
11
|
+
// - Keep raw yarn workspace commands for reference
|
|
12
|
+
// - Emphasize `yarn mono` as the primary interface
|
|
13
|
+
|
|
14
|
+
import { promises as fs } from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
|
|
17
|
+
// Type definitions
|
|
18
|
+
export interface GenerateDocsIndexOptions {
|
|
19
|
+
docsDir: string;
|
|
20
|
+
excludeFile?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function createSpacer() {
|
|
24
|
+
return '\n\n';
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Generate a docs index from markdown files.
|
|
28
|
+
*
|
|
29
|
+
* @param options - Options for docs index generation
|
|
30
|
+
* @returns Markdown-formatted index
|
|
31
|
+
*/
|
|
32
|
+
export async function generateDocsIndex({
|
|
33
|
+
docsDir,
|
|
34
|
+
excludeFile,
|
|
35
|
+
}: GenerateDocsIndexOptions): Promise<string> {
|
|
36
|
+
// Always resolve docsDir relative to the working directory
|
|
37
|
+
const dirPath = path.resolve(process.cwd(), docsDir);
|
|
38
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
39
|
+
|
|
40
|
+
const links: string[] = [];
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
if (!entry.isFile()) continue;
|
|
43
|
+
if (!entry.name.endsWith('.md')) continue;
|
|
44
|
+
|
|
45
|
+
// Always ignore docs/readme.md (case-insensitive)
|
|
46
|
+
if (entry.name.toLowerCase() === 'readme.md') continue;
|
|
47
|
+
|
|
48
|
+
// Optionally ignore a caller-specified file
|
|
49
|
+
if (excludeFile && entry.name === excludeFile) continue;
|
|
50
|
+
|
|
51
|
+
const filePath = path.join(dirPath, entry.name);
|
|
52
|
+
const contents = await fs.readFile(filePath, 'utf8');
|
|
53
|
+
|
|
54
|
+
// Find first markdown H1
|
|
55
|
+
const match = contents.match(/^#\s+(.+)$/m);
|
|
56
|
+
if (!match) continue;
|
|
57
|
+
|
|
58
|
+
const rawTitle = match[1].trim();
|
|
59
|
+
const relativeLink = `./${entry.name}`;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Detect leading non-alphanumeric characters (emoji / symbols).
|
|
63
|
+
* This matches one or more Unicode characters that are NOT letters or numbers.
|
|
64
|
+
*/
|
|
65
|
+
const leadingSymbolMatch = rawTitle.match(/^([^\p{L}\p{N}]+)\s*(.+)$/u);
|
|
66
|
+
|
|
67
|
+
if (leadingSymbolMatch) {
|
|
68
|
+
const [, symbol, title] = leadingSymbolMatch;
|
|
69
|
+
links.push(`- ${symbol.trim()} [${title.trim()}](${relativeLink})`);
|
|
70
|
+
} else {
|
|
71
|
+
links.push(`- [${rawTitle.trim()}](${relativeLink})`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Sort alphabetically by rendered text (stable output)
|
|
76
|
+
links.sort((a, b) => a.localeCompare(b));
|
|
77
|
+
|
|
78
|
+
// Append Back to Readme
|
|
79
|
+
links.push('');
|
|
80
|
+
links.push('🏠 ← [Back to README](../README.md)');
|
|
81
|
+
|
|
82
|
+
return ['', '---', '', ...links].join('\n');
|
|
83
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// scripts/generate-repo-help.mjs
|
|
2
|
+
// Generates a developer-friendly workspace command reference.
|
|
3
|
+
//
|
|
4
|
+
// Output: docs/workspaces.md
|
|
5
|
+
//
|
|
6
|
+
// Run (from repo root):
|
|
7
|
+
// node ./scripts/generate-repo-help.mjs
|
|
8
|
+
//
|
|
9
|
+
// Philosophy:
|
|
10
|
+
// - Optimize for onboarding and day-to-day use
|
|
11
|
+
// - Keep raw yarn workspace commands for reference
|
|
12
|
+
// - Emphasize `yarn mono` as the primary interface
|
|
13
|
+
|
|
14
|
+
import { promises as fs } from 'node:fs'
|
|
15
|
+
import path from 'node:path'
|
|
16
|
+
import { generateDocsIndex } from './generate-docs'
|
|
17
|
+
|
|
18
|
+
interface PackageInfo {
|
|
19
|
+
name: string
|
|
20
|
+
dir: string
|
|
21
|
+
scripts: Record<string, string>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ----------------- config -----------------
|
|
25
|
+
// Always use the working directory as the root for all file actions
|
|
26
|
+
const REPO_ROOT = path.resolve(process.cwd())
|
|
27
|
+
const ROOT_PKG_JSON = path.join(REPO_ROOT, 'package.json')
|
|
28
|
+
const OUTPUT_PATH = path.join(REPO_ROOT, 'docs', 'workspaces.md')
|
|
29
|
+
|
|
30
|
+
// ----------------- helpers -----------------
|
|
31
|
+
async function exists(p: string): Promise<boolean> {
|
|
32
|
+
// Always resolve path relative to working directory
|
|
33
|
+
const absPath = path.resolve(process.cwd(), p)
|
|
34
|
+
try {
|
|
35
|
+
await fs.access(absPath)
|
|
36
|
+
return true
|
|
37
|
+
} catch {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isObject(v: unknown): v is Record<string, unknown> {
|
|
43
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function toPosix(p: string): string {
|
|
47
|
+
return p.split(path.sep).join('/')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function mdEscapeInline(s: string): string {
|
|
51
|
+
return String(s ?? '').replaceAll('`', '\`')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function slugifyForGithubAnchor(title: string): string {
|
|
55
|
+
return String(title ?? '')
|
|
56
|
+
.trim()
|
|
57
|
+
.toLowerCase()
|
|
58
|
+
.replace(/[^\w\s-]/g, '')
|
|
59
|
+
.replace(/\s+/g, '-')
|
|
60
|
+
.replace(/-+/g, '-')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function readJson<T = any>(filePath: string): Promise<T> {
|
|
64
|
+
// Always resolve filePath relative to working directory
|
|
65
|
+
const absPath = path.resolve(process.cwd(), filePath)
|
|
66
|
+
const raw = await fs.readFile(absPath, 'utf8')
|
|
67
|
+
return JSON.parse(raw)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeWorkspacePatterns(workspacesField: unknown): string[] {
|
|
71
|
+
if (Array.isArray(workspacesField)) return workspacesField as string[]
|
|
72
|
+
if (isObject(workspacesField) && Array.isArray((workspacesField as any).packages))
|
|
73
|
+
return (workspacesField as any).packages
|
|
74
|
+
return []
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ----------------- glob expansion -----------------
|
|
78
|
+
function matchSegment(patternSeg: string, name: string): boolean {
|
|
79
|
+
if (patternSeg === '*') return true
|
|
80
|
+
if (!patternSeg.includes('*')) return patternSeg === name
|
|
81
|
+
|
|
82
|
+
const escaped = patternSeg.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
|
83
|
+
const regex = new RegExp('^' + escaped.replaceAll('*', '.*') + '$')
|
|
84
|
+
return regex.test(name)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function expandWorkspacePattern(root: string, pattern: string): Promise<string[]> {
|
|
88
|
+
const segs = toPosix(pattern).split('/').filter(Boolean)
|
|
89
|
+
|
|
90
|
+
async function expandFrom(dir: string, segIndex: number): Promise<string[]> {
|
|
91
|
+
// Always resolve dir relative to working directory
|
|
92
|
+
const absDir = path.resolve(process.cwd(), dir)
|
|
93
|
+
if (segIndex >= segs.length) return [absDir]
|
|
94
|
+
|
|
95
|
+
const seg = segs[segIndex]
|
|
96
|
+
|
|
97
|
+
if (seg === '**') {
|
|
98
|
+
const results: string[] = []
|
|
99
|
+
results.push(...(await expandFrom(absDir, segIndex + 1)))
|
|
100
|
+
|
|
101
|
+
const entries = await fs.readdir(absDir, { withFileTypes: true }).catch(() => [])
|
|
102
|
+
for (const e of entries) {
|
|
103
|
+
if (e.isDirectory()) {
|
|
104
|
+
results.push(...(await expandFrom(path.join(absDir, e.name), segIndex)))
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return results
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const entries = await fs.readdir(absDir, { withFileTypes: true }).catch(() => [])
|
|
111
|
+
const results: string[] = []
|
|
112
|
+
for (const e of entries) {
|
|
113
|
+
if (e.isDirectory() && matchSegment(seg, e.name)) {
|
|
114
|
+
results.push(...(await expandFrom(path.join(absDir, e.name), segIndex + 1)))
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return results
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return [...new Set(await expandFrom(root, 0))]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function findWorkspaceRoots(
|
|
124
|
+
repoRoot: string,
|
|
125
|
+
workspacePatterns: string[]
|
|
126
|
+
): Promise<string[]> {
|
|
127
|
+
const roots: string[] = []
|
|
128
|
+
for (const pat of workspacePatterns) {
|
|
129
|
+
const expandedDirs = await expandWorkspacePattern(repoRoot, pat)
|
|
130
|
+
roots.push(...expandedDirs)
|
|
131
|
+
}
|
|
132
|
+
return [...new Set(roots)]
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ----------------- package discovery -----------------
|
|
136
|
+
const SKIP_DIRS = new Set([
|
|
137
|
+
'node_modules',
|
|
138
|
+
'.git',
|
|
139
|
+
'.next',
|
|
140
|
+
'dist',
|
|
141
|
+
'build',
|
|
142
|
+
'out',
|
|
143
|
+
'coverage',
|
|
144
|
+
'.turbo',
|
|
145
|
+
])
|
|
146
|
+
|
|
147
|
+
async function findPackageJsonFilesRecursive(startDir: string): Promise<string[]> {
|
|
148
|
+
const found: string[] = []
|
|
149
|
+
|
|
150
|
+
async function walk(dir: string): Promise<void> {
|
|
151
|
+
// Always resolve dir relative to working directory
|
|
152
|
+
const absDir = path.resolve(process.cwd(), dir)
|
|
153
|
+
const entries = await fs.readdir(absDir, { withFileTypes: true }).catch(() => [])
|
|
154
|
+
for (const e of entries) {
|
|
155
|
+
const full = path.join(absDir, e.name)
|
|
156
|
+
if (e.isDirectory()) {
|
|
157
|
+
if (!SKIP_DIRS.has(e.name)) await walk(full)
|
|
158
|
+
} else if (e.isFile() && e.name === 'package.json') {
|
|
159
|
+
found.push(full)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await walk(startDir)
|
|
165
|
+
return found
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function collectPackagesFromWorkspaceRoots(workspaceRoots: string[]): Promise<PackageInfo[]> {
|
|
169
|
+
const pkgJsonFiles: string[] = []
|
|
170
|
+
|
|
171
|
+
for (const root of workspaceRoots) {
|
|
172
|
+
if (await exists(root)) {
|
|
173
|
+
pkgJsonFiles.push(...(await findPackageJsonFilesRecursive(root)))
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const packages: PackageInfo[] = []
|
|
178
|
+
for (const file of [...new Set(pkgJsonFiles)]) {
|
|
179
|
+
if (path.resolve(file) === path.resolve(ROOT_PKG_JSON)) continue
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const pj = await readJson<any>(file)
|
|
183
|
+
const dir = path.dirname(file)
|
|
184
|
+
packages.push({
|
|
185
|
+
name: pj.name || toPosix(path.relative(REPO_ROOT, dir)),
|
|
186
|
+
dir,
|
|
187
|
+
scripts: isObject(pj.scripts) ? (pj.scripts as Record<string, string>) : {},
|
|
188
|
+
})
|
|
189
|
+
} catch {}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const seen = new Set<string>()
|
|
193
|
+
return packages
|
|
194
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
195
|
+
.filter((p) => (seen.has(p.name) ? false : seen.add(p.name)))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ----------------- classification -----------------
|
|
199
|
+
function classifyPackage(pkg: PackageInfo): string {
|
|
200
|
+
const p = toPosix(pkg.dir)
|
|
201
|
+
if (p.startsWith('apps/')) return 'Apps'
|
|
202
|
+
if (p.startsWith('packages/')) return 'Libraries'
|
|
203
|
+
return 'Other'
|
|
204
|
+
}
|
|
205
|
+
// ----------------- markdown generation -----------------
|
|
206
|
+
function formatQuickStart(pkgMgr: string): string[] {
|
|
207
|
+
return [
|
|
208
|
+
'# 🗂️ Workspace Overview',
|
|
209
|
+
'',
|
|
210
|
+
'This document explains how to run and discover commands in this monorepo.',
|
|
211
|
+
'',
|
|
212
|
+
'---',
|
|
213
|
+
'',
|
|
214
|
+
'## 🚀 Quick Start',
|
|
215
|
+
'',
|
|
216
|
+
'Most developers only need the following:',
|
|
217
|
+
'',
|
|
218
|
+
'```bash',
|
|
219
|
+
`${pkgMgr} dev`,
|
|
220
|
+
`${pkgMgr} serve`,
|
|
221
|
+
`${pkgMgr} mobile`,
|
|
222
|
+
`${pkgMgr} help`,
|
|
223
|
+
'```',
|
|
224
|
+
'',
|
|
225
|
+
'Use `yarn mono` whenever possible. It handles environment setup,',
|
|
226
|
+
'workspace routing, and service coordination automatically.',
|
|
227
|
+
'',
|
|
228
|
+
'---',
|
|
229
|
+
'',
|
|
230
|
+
]
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function formatReferenceIntro(): string[] {
|
|
234
|
+
return [
|
|
235
|
+
'## 📖 Reference',
|
|
236
|
+
'',
|
|
237
|
+
'This section lists all workspace packages and their available scripts.',
|
|
238
|
+
'',
|
|
239
|
+
'Use this when:',
|
|
240
|
+
'- Debugging',
|
|
241
|
+
'- Working on internal libraries',
|
|
242
|
+
'- Running CI or low-level tooling',
|
|
243
|
+
'',
|
|
244
|
+
]
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function formatIndex(packages: PackageInfo[]): string[] {
|
|
248
|
+
const groups: Record<string, PackageInfo[]> = {}
|
|
249
|
+
for (const p of packages) {
|
|
250
|
+
const g = classifyPackage(p)
|
|
251
|
+
groups[g] ||= []
|
|
252
|
+
groups[g].push(p)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const lines: string[] = ['## Workspace Index', '']
|
|
256
|
+
for (const group of Object.keys(groups)) {
|
|
257
|
+
lines.push(`### ${group}`)
|
|
258
|
+
lines.push('')
|
|
259
|
+
for (const p of groups[group]) {
|
|
260
|
+
lines.push(`- [\`${mdEscapeInline(p.name)}\`](#${slugifyForGithubAnchor(p.name)})`)
|
|
261
|
+
}
|
|
262
|
+
lines.push('')
|
|
263
|
+
}
|
|
264
|
+
return lines
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function formatPackages(packages: PackageInfo[]): string[] {
|
|
268
|
+
const lines: string[] = []
|
|
269
|
+
|
|
270
|
+
for (const p of packages) {
|
|
271
|
+
lines.push(`### ${p.name}`)
|
|
272
|
+
lines.push('')
|
|
273
|
+
lines.push(`_Location: \`${toPosix(path.relative(REPO_ROOT, p.dir))}\`_`)
|
|
274
|
+
lines.push('')
|
|
275
|
+
|
|
276
|
+
const scripts = Object.keys(p.scripts).sort()
|
|
277
|
+
if (!scripts.length) {
|
|
278
|
+
lines.push('_No scripts defined._')
|
|
279
|
+
lines.push('')
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
lines.push('| Script | Recommended |')
|
|
284
|
+
lines.push('|------|-------------|')
|
|
285
|
+
for (const s of scripts) {
|
|
286
|
+
lines.push(`| \`${s}\` | \`yarn mono ${p.name} ${s}\` |`)
|
|
287
|
+
}
|
|
288
|
+
lines.push('')
|
|
289
|
+
lines.push('<details>')
|
|
290
|
+
lines.push('<summary>Raw yarn workspace commands</summary>')
|
|
291
|
+
lines.push('')
|
|
292
|
+
for (const s of scripts) {
|
|
293
|
+
lines.push(`- \`yarn workspace ${p.name} ${s}\``)
|
|
294
|
+
}
|
|
295
|
+
lines.push('</details>')
|
|
296
|
+
lines.push('')
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return lines
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function ensureParentDir(filePath: string): Promise<void> {
|
|
303
|
+
// Always resolve parent dir relative to working directory
|
|
304
|
+
const dir = path.resolve(process.cwd(), path.dirname(filePath))
|
|
305
|
+
await fs.mkdir(dir, { recursive: true })
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ----------------- main -----------------
|
|
309
|
+
async function main(): Promise<void> {
|
|
310
|
+
// Always resolve all paths relative to working directory
|
|
311
|
+
if (!(await exists(ROOT_PKG_JSON))) {
|
|
312
|
+
throw new Error('Root package.json not found')
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const rootPkg = await readJson<any>(ROOT_PKG_JSON)
|
|
316
|
+
const workspacePatterns = normalizeWorkspacePatterns(rootPkg.workspaces)
|
|
317
|
+
|
|
318
|
+
const pkgMgr = `${(rootPkg.packageManager || 'yarn').split('@')[0]} mono`
|
|
319
|
+
|
|
320
|
+
const workspaceRoots = await findWorkspaceRoots(REPO_ROOT, workspacePatterns)
|
|
321
|
+
const fallbackRoots = ['packages', 'apps'].map((p) => path.join(REPO_ROOT, p))
|
|
322
|
+
const roots = workspaceRoots.length ? workspaceRoots : fallbackRoots
|
|
323
|
+
|
|
324
|
+
const packages = await collectPackagesFromWorkspaceRoots(roots)
|
|
325
|
+
|
|
326
|
+
const lines: string[] = []
|
|
327
|
+
lines.push(...formatQuickStart(pkgMgr))
|
|
328
|
+
lines.push(...formatReferenceIntro())
|
|
329
|
+
lines.push(...formatIndex(packages))
|
|
330
|
+
lines.push('## Packages')
|
|
331
|
+
lines.push('')
|
|
332
|
+
lines.push(...formatPackages(packages))
|
|
333
|
+
|
|
334
|
+
const val = await generateDocsIndex({
|
|
335
|
+
docsDir: path.join(REPO_ROOT, 'docs'),
|
|
336
|
+
excludeFile: 'workspaces.md',
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
val.split('\n').forEach((line) => lines.push(line))
|
|
340
|
+
|
|
341
|
+
await ensureParentDir(OUTPUT_PATH)
|
|
342
|
+
await fs.writeFile(OUTPUT_PATH, lines.join('\n'), 'utf8')
|
|
343
|
+
|
|
344
|
+
console.log(`✅ Generated ${OUTPUT_PATH}`)
|
|
345
|
+
console.log(`📦 Packages found: ${packages.length}`)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
main().catch((err) => {
|
|
349
|
+
console.error(err)
|
|
350
|
+
process.exitCode = 1
|
|
351
|
+
})
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
/* ──────────────────────────────────────────────────────────
|
|
5
|
+
* Types
|
|
6
|
+
* ────────────────────────────────────────────────────────── */
|
|
7
|
+
|
|
8
|
+
type WorkspaceDetectResult = {
|
|
9
|
+
cwd: string
|
|
10
|
+
workspaceRoot: string | null
|
|
11
|
+
isWorkspaceRoot: boolean
|
|
12
|
+
configDir: string
|
|
13
|
+
configPath: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type DefaultAppConfig = {
|
|
17
|
+
appleAppId?: string
|
|
18
|
+
androidAppId?: string
|
|
19
|
+
appName?: string
|
|
20
|
+
easProjectId?: string
|
|
21
|
+
appScheme?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type DefaultDeployConfig = {
|
|
25
|
+
baseDomain?: string
|
|
26
|
+
webSubdomain?: string
|
|
27
|
+
apiSubdomain?: string
|
|
28
|
+
defaultKeyPair?: string
|
|
29
|
+
regions: string[]
|
|
30
|
+
ec2User: string
|
|
31
|
+
warehouseRegion: string
|
|
32
|
+
dbInstanceType: string
|
|
33
|
+
appInstanceType: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const requiredSystemDefaults = {
|
|
37
|
+
ec2User: 'ec2-user',
|
|
38
|
+
regions: ['us-east-1'],
|
|
39
|
+
warehouseRegion: 'us-east-1',
|
|
40
|
+
dbInstanceType: 't3.micro',
|
|
41
|
+
appInstanceType: 't3.large',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type ConfigTypeMap = {
|
|
45
|
+
app: DefaultAppConfig
|
|
46
|
+
deployment: DefaultDeployConfig
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* If TType is a known key, use the mapped type.
|
|
51
|
+
* Otherwise use TCustom (default = unknown).
|
|
52
|
+
*/
|
|
53
|
+
type ResolveConfig<TType extends string, TCustom = unknown> = TType extends keyof ConfigTypeMap
|
|
54
|
+
? ConfigTypeMap[TType]
|
|
55
|
+
: TCustom
|
|
56
|
+
|
|
57
|
+
/* ──────────────────────────────────────────────────────────
|
|
58
|
+
* Environment helpers
|
|
59
|
+
* ────────────────────────────────────────────────────────── */
|
|
60
|
+
|
|
61
|
+
function isLambdaRuntime(): boolean {
|
|
62
|
+
return !!process.env.AWS_LAMBDA_FUNCTION_NAME
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* ──────────────────────────────────────────────────────────
|
|
66
|
+
* Workspace detection (CLI / local dev only)
|
|
67
|
+
* ────────────────────────────────────────────────────────── */
|
|
68
|
+
|
|
69
|
+
function detectWorkspaceAndConfigPath(
|
|
70
|
+
startDir: string,
|
|
71
|
+
configFileName: string
|
|
72
|
+
): WorkspaceDetectResult {
|
|
73
|
+
const cwd = path.resolve(startDir)
|
|
74
|
+
|
|
75
|
+
const isWorkspaceRootDir = (dir: string): boolean => {
|
|
76
|
+
const pkgPath = path.join(dir, 'package.json')
|
|
77
|
+
if (fs.existsSync(pkgPath)) {
|
|
78
|
+
try {
|
|
79
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
80
|
+
if (pkg?.workspaces) return true
|
|
81
|
+
} catch {
|
|
82
|
+
// ignore
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const markers = ['pnpm-workspace.yaml', 'lerna.json', 'turbo.json', 'nx.json', '.git']
|
|
87
|
+
|
|
88
|
+
return markers.some((m) => fs.existsSync(path.join(dir, m)))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let dir = cwd
|
|
92
|
+
while (true) {
|
|
93
|
+
if (isWorkspaceRootDir(dir)) {
|
|
94
|
+
return {
|
|
95
|
+
cwd,
|
|
96
|
+
workspaceRoot: dir,
|
|
97
|
+
isWorkspaceRoot: dir === cwd,
|
|
98
|
+
configDir: dir,
|
|
99
|
+
configPath: path.join(dir, configFileName),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const parent = path.dirname(dir)
|
|
104
|
+
if (parent === dir) break
|
|
105
|
+
dir = parent
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
cwd,
|
|
110
|
+
workspaceRoot: null,
|
|
111
|
+
isWorkspaceRoot: false,
|
|
112
|
+
configDir: cwd,
|
|
113
|
+
configPath: path.join(cwd, configFileName),
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* ──────────────────────────────────────────────────────────
|
|
118
|
+
* Bundled config loader (Lambda runtime)
|
|
119
|
+
* ────────────────────────────────────────────────────────── */
|
|
120
|
+
|
|
121
|
+
function loadConfigFromBundle(fileName: string): unknown | null {
|
|
122
|
+
const bundledPath = path.join(__dirname, fileName)
|
|
123
|
+
|
|
124
|
+
if (fs.existsSync(bundledPath)) {
|
|
125
|
+
return JSON.parse(fs.readFileSync(bundledPath, 'utf8'))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* ──────────────────────────────────────────────────────────
|
|
132
|
+
* Public API
|
|
133
|
+
* ────────────────────────────────────────────────────────── */
|
|
134
|
+
|
|
135
|
+
export function loadAppConfig<TCustom = unknown, TType extends string = 'app'>(
|
|
136
|
+
configType: TType = 'app' as TType,
|
|
137
|
+
startDir: string = process.cwd()
|
|
138
|
+
): { config: ResolveConfig<TType, TCustom>; meta: WorkspaceDetectResult } {
|
|
139
|
+
const fileName = `mono.${configType}.json`
|
|
140
|
+
|
|
141
|
+
// 1. Lambda runtime: load bundled config if present
|
|
142
|
+
if (isLambdaRuntime()) {
|
|
143
|
+
const bundled = loadConfigFromBundle(fileName)
|
|
144
|
+
|
|
145
|
+
if (bundled) {
|
|
146
|
+
return {
|
|
147
|
+
config: bundled as ResolveConfig<TType, TCustom>,
|
|
148
|
+
meta: {
|
|
149
|
+
cwd: __dirname,
|
|
150
|
+
workspaceRoot: null,
|
|
151
|
+
isWorkspaceRoot: false,
|
|
152
|
+
configDir: __dirname,
|
|
153
|
+
configPath: path.join(__dirname, fileName),
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 2. CLI / local dev: workspace discovery
|
|
160
|
+
const meta = detectWorkspaceAndConfigPath(startDir, fileName)
|
|
161
|
+
|
|
162
|
+
if (!fs.existsSync(meta.configPath)) {
|
|
163
|
+
const where = meta.workspaceRoot ? `workspace root: ${meta.workspaceRoot}` : `cwd: ${meta.cwd}`
|
|
164
|
+
|
|
165
|
+
throw new Error(`Could not find ${fileName} at ${meta.configPath} (detected from ${where}).`)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const raw = fs.readFileSync(meta.configPath, 'utf8')
|
|
169
|
+
const config = JSON.parse(raw) as ResolveConfig<TType, TCustom>
|
|
170
|
+
|
|
171
|
+
// Apply required system defaults
|
|
172
|
+
if (typeof config === 'object' && config !== null) {
|
|
173
|
+
for (const key of Object.keys(requiredSystemDefaults)) {
|
|
174
|
+
// @ts-ignore: index signature
|
|
175
|
+
if (config[key] === undefined || config[key] === null) {
|
|
176
|
+
// @ts-ignore: index signature
|
|
177
|
+
config[key] = requiredSystemDefaults[key]
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { config, meta }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export const loadProjectConfig = loadAppConfig
|
|
186
|
+
|
|
187
|
+
export { loadMergedEnv } from './merge-env'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import dotenv from 'dotenv';
|
|
4
|
+
|
|
5
|
+
export function loadMergedEnv(): NodeJS.ProcessEnv {
|
|
6
|
+
const ENV_PATH = path.resolve(process.cwd(), '.env');
|
|
7
|
+
const ENV_LOCAL_PATH = path.resolve(process.cwd(), '.env.local');
|
|
8
|
+
|
|
9
|
+
// Load base .env
|
|
10
|
+
const base =
|
|
11
|
+
fs.existsSync(ENV_PATH) ? dotenv.parse(fs.readFileSync(ENV_PATH)) : {};
|
|
12
|
+
|
|
13
|
+
// Load overrides .env.local
|
|
14
|
+
const local =
|
|
15
|
+
fs.existsSync(ENV_LOCAL_PATH) ?
|
|
16
|
+
dotenv.parse(fs.readFileSync(ENV_LOCAL_PATH))
|
|
17
|
+
: {};
|
|
18
|
+
|
|
19
|
+
// Merge: local overrides base
|
|
20
|
+
const merged = {
|
|
21
|
+
...base,
|
|
22
|
+
...local,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Inject into process.env (do NOT overwrite existing real env vars)
|
|
26
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
27
|
+
if (process.env[key] === undefined) {
|
|
28
|
+
process.env[key] = value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return process.env;
|
|
32
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"rootDir": "./src",
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"target": "ES2022",
|
|
6
|
+
"module": "CommonJS",
|
|
7
|
+
"moduleResolution": "Node",
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"allowSyntheticDefaultImports": true,
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"declarationMap": true,
|
|
12
|
+
"composite": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"resolveJsonModule": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|