create-mercato-app 0.6.6-develop.6338.1.c32cd17821 → 0.6.6-develop.6339.1.193c6c7c71
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 +1 -1
- package/template/scripts/dev.mjs +27 -1
- package/template/scripts/watch-scope.mjs +490 -0
package/package.json
CHANGED
package/template/scripts/dev.mjs
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
resolveSplashUrl as resolveSplashAccessUrl,
|
|
36
36
|
} from './dev-splash-url.mjs'
|
|
37
37
|
import { resolveDatabaseNameOverride } from './dev-database-url.mjs'
|
|
38
|
+
import { parseWatchScopeArgs, resolveWatchScope } from './watch-scope.mjs'
|
|
38
39
|
|
|
39
40
|
function detectDevRuntimeMode() {
|
|
40
41
|
const cwd = process.cwd()
|
|
@@ -191,6 +192,10 @@ const classic = args.includes('--classic') || isEnabledEnvFlag(process.env.OM_DE
|
|
|
191
192
|
const verbose = args.includes('--verbose') || process.env.MERCATO_DEV_OUTPUT === 'verbose'
|
|
192
193
|
const greenfield = isMonorepo && args.includes('--greenfield')
|
|
193
194
|
const appOnly = args.includes('--app-only')
|
|
195
|
+
// Watch-scope CLI flags (e.g. `--watch=auto-optimized`, `--watch-popular`) are
|
|
196
|
+
// translated into env vars and forwarded to the spawned package watcher, which
|
|
197
|
+
// reads `OM_WATCH_SCOPE` / `OM_WATCH_PACKAGES`. CLI flags win over a pre-set env.
|
|
198
|
+
const watchScopeArgs = parseWatchScopeArgs(args)
|
|
194
199
|
const setupMode = !isMonorepo && args.includes('--setup')
|
|
195
200
|
const reinstall = setupMode && args.includes('--reinstall')
|
|
196
201
|
const standaloneLocalRegistryRefresh = !isMonorepo && shouldRefreshStandaloneRegistryPackages()
|
|
@@ -1557,14 +1562,29 @@ function resolveWatchPackagesScript() {
|
|
|
1557
1562
|
return raw === 'legacy' ? 'watch:packages:legacy' : 'watch:packages'
|
|
1558
1563
|
}
|
|
1559
1564
|
|
|
1565
|
+
function buildWatchScopeEnv() {
|
|
1566
|
+
const env = {}
|
|
1567
|
+
if (watchScopeArgs.mode) env.OM_WATCH_SCOPE = watchScopeArgs.mode
|
|
1568
|
+
if (watchScopeArgs.packages?.length) env.OM_WATCH_PACKAGES = watchScopeArgs.packages.join(',')
|
|
1569
|
+
if (watchScopeArgs.popularLimit) env.OM_WATCH_POPULAR_LIMIT = String(watchScopeArgs.popularLimit)
|
|
1570
|
+
return env
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
function resolveActiveWatchScope(watchScopeEnv = buildWatchScopeEnv()) {
|
|
1574
|
+
return resolveWatchScope({ env: { ...process.env, ...watchScopeEnv }, argv: [] }).mode
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1560
1577
|
function startPackageWatch() {
|
|
1561
1578
|
const watchScript = resolveWatchPackagesScript()
|
|
1579
|
+
const watchScopeEnv = buildWatchScopeEnv()
|
|
1580
|
+
const activeScope = resolveActiveWatchScope(watchScopeEnv)
|
|
1562
1581
|
|
|
1563
1582
|
if (classic) {
|
|
1564
1583
|
const child = spawnCommand(yarnCommand, [watchScript], {
|
|
1565
1584
|
label: watchScript,
|
|
1566
1585
|
logFile: getDevRunnerLog(),
|
|
1567
1586
|
mirrorOutput: true,
|
|
1587
|
+
env: watchScopeEnv,
|
|
1568
1588
|
})
|
|
1569
1589
|
|
|
1570
1590
|
child.on('close', (code, signal) => {
|
|
@@ -1586,9 +1606,14 @@ function startPackageWatch() {
|
|
|
1586
1606
|
const stageCurrent = greenfield ? 5 : 2
|
|
1587
1607
|
const stageTotal = greenfield ? 5 : 3
|
|
1588
1608
|
console.log(`👀 ${formatProgressLine('Watching workspace packages', stageCurrent, stageTotal, resolveProgressPercent(stageCurrent, stageTotal))}`)
|
|
1609
|
+
if (activeScope !== 'all') {
|
|
1610
|
+
console.log(` ↳ watch scope: ${activeScope} (set OM_WATCH_SCOPE=all or pass --watch=all to watch every package)`)
|
|
1611
|
+
}
|
|
1589
1612
|
updateSplashState({
|
|
1590
1613
|
phase: 'Watching workspace packages',
|
|
1591
|
-
detail:
|
|
1614
|
+
detail: activeScope === 'all'
|
|
1615
|
+
? 'Package watchers are running in the background'
|
|
1616
|
+
: `Package watchers are running (watch scope: ${activeScope})`,
|
|
1592
1617
|
progressCurrent: stageCurrent,
|
|
1593
1618
|
progressTotal: stageTotal,
|
|
1594
1619
|
progressPercent: resolveProgressPercent(stageCurrent, stageTotal),
|
|
@@ -1600,6 +1625,7 @@ function startPackageWatch() {
|
|
|
1600
1625
|
label: 'Watching workspace packages',
|
|
1601
1626
|
logFile: getDevRunnerLog(),
|
|
1602
1627
|
mirrorOutput: verbose,
|
|
1628
|
+
env: watchScopeEnv,
|
|
1603
1629
|
})
|
|
1604
1630
|
|
|
1605
1631
|
if (verbose) {
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Dev-mode watch-scope resolution.
|
|
3
|
+
//
|
|
4
|
+
// Lets developers narrow which workspace packages the consolidated package
|
|
5
|
+
// watcher (`scripts/watch-packages.mjs`) tracks, instead of always watching
|
|
6
|
+
// every package. Four modes are supported (see `apps/docs/docs/appendix/
|
|
7
|
+
// troubleshooting.mdx`):
|
|
8
|
+
//
|
|
9
|
+
// - `all` watch every discovered package (default, unchanged).
|
|
10
|
+
// - `auto-optimized` watch only packages touched recently (git working tree
|
|
11
|
+
// + current-branch diff), re-checking every 2 minutes and
|
|
12
|
+
// expanding watchers to newly-touched packages.
|
|
13
|
+
// - `popular` watch only the most frequently changed packages (ranked
|
|
14
|
+
// from recent git history; static fallback when no git).
|
|
15
|
+
// - `env` watch exactly the packages named in `OM_WATCH_PACKAGES`
|
|
16
|
+
// (or the interactive picker's persisted selection).
|
|
17
|
+
//
|
|
18
|
+
// Mode is selected via `OM_WATCH_SCOPE` or `--watch=<mode>` CLI flags forwarded
|
|
19
|
+
// by `scripts/dev.mjs`. CLI flags win over env. This module is deliberately
|
|
20
|
+
// pure: every git call goes through an injectable `runGit` so the logic stays
|
|
21
|
+
// unit-testable without a real repository.
|
|
22
|
+
|
|
23
|
+
import { execFileSync } from 'node:child_process'
|
|
24
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
25
|
+
import { basename, dirname, join, relative, sep } from 'node:path'
|
|
26
|
+
|
|
27
|
+
export const WATCH_SCOPE_ALL = 'all'
|
|
28
|
+
export const WATCH_SCOPE_AUTO = 'auto-optimized'
|
|
29
|
+
export const WATCH_SCOPE_POPULAR = 'popular'
|
|
30
|
+
export const WATCH_SCOPE_ENV = 'env'
|
|
31
|
+
|
|
32
|
+
export const WATCH_SCOPES = [
|
|
33
|
+
WATCH_SCOPE_ALL,
|
|
34
|
+
WATCH_SCOPE_AUTO,
|
|
35
|
+
WATCH_SCOPE_POPULAR,
|
|
36
|
+
WATCH_SCOPE_ENV,
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
// Re-check + expand interval for `auto-optimized`.
|
|
40
|
+
export const AUTO_EXPAND_INTERVAL_MS = 120000
|
|
41
|
+
|
|
42
|
+
// Default popular packages when git history cannot rank anything.
|
|
43
|
+
export const DEFAULT_POPULAR_PACKAGES = ['core', 'ui', 'shared']
|
|
44
|
+
|
|
45
|
+
export const DEFAULT_POPULAR_LIMIT = 6
|
|
46
|
+
|
|
47
|
+
// Candidate base refs for the current-branch diff, in priority order.
|
|
48
|
+
const DEFAULT_BASE_REFS = ['origin/develop', 'develop', 'origin/main', 'main']
|
|
49
|
+
|
|
50
|
+
const PERSISTED_SELECTION_RELATIVE = join('.mercato', 'watch-packages.local.json')
|
|
51
|
+
|
|
52
|
+
function safeReadPackageJson(packageDir) {
|
|
53
|
+
const pkgPath = join(packageDir, 'package.json')
|
|
54
|
+
if (!existsSync(pkgPath)) return null
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
57
|
+
} catch {
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Discover workspace packages that the consolidated watcher can track: any
|
|
63
|
+
// package with a `watch` script and a `src/` directory under `packages/` or
|
|
64
|
+
// `external/official-modules/packages/`. This is the single discovery source
|
|
65
|
+
// shared by the watcher and the interactive picker; it is intentionally
|
|
66
|
+
// dependency-light (node built-ins only) so it can run inside standalone apps
|
|
67
|
+
// that do not install esbuild/glob.
|
|
68
|
+
export function discoverWatchTargets(root) {
|
|
69
|
+
const roots = [
|
|
70
|
+
join(root, 'packages'),
|
|
71
|
+
join(root, 'external', 'official-modules', 'packages'),
|
|
72
|
+
]
|
|
73
|
+
const discovered = []
|
|
74
|
+
|
|
75
|
+
for (const parent of roots) {
|
|
76
|
+
if (!existsSync(parent)) continue
|
|
77
|
+
let entries
|
|
78
|
+
try {
|
|
79
|
+
entries = readdirSync(parent, { withFileTypes: true })
|
|
80
|
+
} catch {
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
if (!entry.isDirectory()) continue
|
|
86
|
+
const packageDir = join(parent, entry.name)
|
|
87
|
+
const pkg = safeReadPackageJson(packageDir)
|
|
88
|
+
if (!pkg) continue
|
|
89
|
+
if (!pkg.scripts?.watch) continue
|
|
90
|
+
const srcDir = join(packageDir, 'src')
|
|
91
|
+
if (!existsSync(srcDir)) continue
|
|
92
|
+
|
|
93
|
+
discovered.push({
|
|
94
|
+
name: pkg.name ?? basename(packageDir),
|
|
95
|
+
packageDir,
|
|
96
|
+
srcDir,
|
|
97
|
+
shortLabel: basename(packageDir),
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
discovered.sort((a, b) => a.shortLabel.localeCompare(b.shortLabel))
|
|
103
|
+
return discovered
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeMode(value) {
|
|
107
|
+
if (!value) return null
|
|
108
|
+
const raw = String(value).trim().toLowerCase()
|
|
109
|
+
if (!raw) return null
|
|
110
|
+
// Accept a few friendly aliases.
|
|
111
|
+
if (raw === 'auto' || raw === 'optimized' || raw === 'auto-optimised') return WATCH_SCOPE_AUTO
|
|
112
|
+
if (raw === 'popular' || raw === 'frequent' || raw === 'hot') return WATCH_SCOPE_POPULAR
|
|
113
|
+
if (raw === 'env' || raw === 'list' || raw === 'select' || raw === 'manual') return WATCH_SCOPE_ENV
|
|
114
|
+
if (raw === 'all' || raw === 'full' || raw === 'everything') return WATCH_SCOPE_ALL
|
|
115
|
+
return WATCH_SCOPES.includes(raw) ? raw : null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function parsePackageList(value) {
|
|
119
|
+
if (!value) return []
|
|
120
|
+
const seen = new Set()
|
|
121
|
+
const result = []
|
|
122
|
+
for (const token of String(value).split(/[\s,]+/)) {
|
|
123
|
+
const trimmed = token.trim()
|
|
124
|
+
if (!trimmed) continue
|
|
125
|
+
const key = trimmed.toLowerCase()
|
|
126
|
+
if (seen.has(key)) continue
|
|
127
|
+
seen.add(key)
|
|
128
|
+
result.push(trimmed)
|
|
129
|
+
}
|
|
130
|
+
return result
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parsePositiveInt(value, fallback) {
|
|
134
|
+
const parsed = Number.parseInt(String(value ?? '').trim(), 10)
|
|
135
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isDisabledFlag(value) {
|
|
139
|
+
if (value === undefined || value === null) return false
|
|
140
|
+
const raw = String(value).trim().toLowerCase()
|
|
141
|
+
return raw === '0' || raw === 'false' || raw === 'off' || raw === 'no'
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Parse the watch-scope-related argv flags. Recognized forms:
|
|
145
|
+
// --watch=<mode> (mode = all|auto-optimized|popular|env + aliases)
|
|
146
|
+
// --watch-auto shorthand for --watch=auto-optimized
|
|
147
|
+
// --watch-popular shorthand for --watch=popular
|
|
148
|
+
// --watch-all shorthand for --watch=all
|
|
149
|
+
// --watch-env shorthand for --watch=env
|
|
150
|
+
// --watch-packages=a,b,c explicit package list (implies env mode)
|
|
151
|
+
// --watch-popular-limit=N popular cap
|
|
152
|
+
export function parseWatchScopeArgs(argv = []) {
|
|
153
|
+
const out = {}
|
|
154
|
+
for (const arg of argv) {
|
|
155
|
+
if (typeof arg !== 'string') continue
|
|
156
|
+
const eq = arg.indexOf('=')
|
|
157
|
+
const key = eq === -1 ? arg : arg.slice(0, eq)
|
|
158
|
+
const value = eq === -1 ? '' : arg.slice(eq + 1)
|
|
159
|
+
switch (key) {
|
|
160
|
+
case '--watch':
|
|
161
|
+
case '--watch-scope': {
|
|
162
|
+
const mode = normalizeMode(value)
|
|
163
|
+
if (mode) out.mode = mode
|
|
164
|
+
break
|
|
165
|
+
}
|
|
166
|
+
case '--watch-auto':
|
|
167
|
+
case '--watch-optimized':
|
|
168
|
+
out.mode = WATCH_SCOPE_AUTO
|
|
169
|
+
break
|
|
170
|
+
case '--watch-popular':
|
|
171
|
+
out.mode = WATCH_SCOPE_POPULAR
|
|
172
|
+
break
|
|
173
|
+
case '--watch-env':
|
|
174
|
+
case '--watch-select':
|
|
175
|
+
out.mode = WATCH_SCOPE_ENV
|
|
176
|
+
break
|
|
177
|
+
case '--watch-all':
|
|
178
|
+
out.mode = WATCH_SCOPE_ALL
|
|
179
|
+
break
|
|
180
|
+
case '--watch-packages': {
|
|
181
|
+
const list = parsePackageList(value)
|
|
182
|
+
if (list.length) {
|
|
183
|
+
out.packages = list
|
|
184
|
+
if (!out.mode) out.mode = WATCH_SCOPE_ENV
|
|
185
|
+
}
|
|
186
|
+
break
|
|
187
|
+
}
|
|
188
|
+
case '--watch-popular-limit': {
|
|
189
|
+
const limit = parsePositiveInt(value, 0)
|
|
190
|
+
if (limit) out.popularLimit = limit
|
|
191
|
+
break
|
|
192
|
+
}
|
|
193
|
+
default:
|
|
194
|
+
break
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return out
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Resolve the effective scope config from env + argv. Argv wins over env.
|
|
201
|
+
export function resolveWatchScope({ env = {}, argv = [] } = {}) {
|
|
202
|
+
const fromArgs = parseWatchScopeArgs(argv)
|
|
203
|
+
const mode = fromArgs.mode || normalizeMode(env.OM_WATCH_SCOPE) || WATCH_SCOPE_ALL
|
|
204
|
+
const explicitPackages = fromArgs.packages?.length
|
|
205
|
+
? fromArgs.packages
|
|
206
|
+
: parsePackageList(env.OM_WATCH_PACKAGES)
|
|
207
|
+
const popularLimit = fromArgs.popularLimit
|
|
208
|
+
|| parsePositiveInt(env.OM_WATCH_POPULAR_LIMIT, DEFAULT_POPULAR_LIMIT)
|
|
209
|
+
const popularOverride = parsePackageList(env.OM_WATCH_POPULAR)
|
|
210
|
+
return {
|
|
211
|
+
mode,
|
|
212
|
+
explicitPackages,
|
|
213
|
+
popularLimit,
|
|
214
|
+
popularOverride,
|
|
215
|
+
gitStatusEnabled: !isDisabledFlag(env.OM_WATCH_GIT_STATUS),
|
|
216
|
+
gitBranchEnabled: !isDisabledFlag(env.OM_WATCH_GIT_BRANCH),
|
|
217
|
+
baseRef: typeof env.OM_WATCH_BASE_REF === 'string' && env.OM_WATCH_BASE_REF.trim()
|
|
218
|
+
? env.OM_WATCH_BASE_REF.trim()
|
|
219
|
+
: null,
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function packageKeySet(pkg) {
|
|
224
|
+
const keys = new Set()
|
|
225
|
+
if (pkg.shortLabel) keys.add(pkg.shortLabel.toLowerCase())
|
|
226
|
+
if (pkg.name) {
|
|
227
|
+
const name = pkg.name.toLowerCase()
|
|
228
|
+
keys.add(name)
|
|
229
|
+
const slash = name.lastIndexOf('/')
|
|
230
|
+
if (slash !== -1) keys.add(name.slice(slash + 1))
|
|
231
|
+
}
|
|
232
|
+
return keys
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Resolve label/name tokens against the discovered package list. Matches on
|
|
236
|
+
// short label and on package name (with or without the npm scope).
|
|
237
|
+
export function matchPackagesByLabels(packages, labels) {
|
|
238
|
+
if (!labels?.length) return []
|
|
239
|
+
const wanted = new Set(labels.map((label) => String(label).trim().toLowerCase()).filter(Boolean))
|
|
240
|
+
return packages.filter((pkg) => {
|
|
241
|
+
for (const key of packageKeySet(pkg)) {
|
|
242
|
+
if (wanted.has(key)) return true
|
|
243
|
+
}
|
|
244
|
+
return false
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function toRelativeDir(root, dir) {
|
|
249
|
+
const rel = relative(root, dir)
|
|
250
|
+
if (!rel || rel.startsWith('..')) return null
|
|
251
|
+
return rel.split(sep).join('/')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Map repo-relative changed file paths to the packages that own them.
|
|
255
|
+
export function mapChangedPathsToPackages(packages, root, changedPaths) {
|
|
256
|
+
if (!changedPaths?.length) return []
|
|
257
|
+
const prefixes = packages
|
|
258
|
+
.map((pkg) => {
|
|
259
|
+
const relDir = toRelativeDir(root, pkg.packageDir)
|
|
260
|
+
return relDir ? { pkg, prefix: `${relDir}/` } : null
|
|
261
|
+
})
|
|
262
|
+
.filter(Boolean)
|
|
263
|
+
const matched = new Map()
|
|
264
|
+
for (const rawPath of changedPaths) {
|
|
265
|
+
const normalized = String(rawPath).trim().replace(/\\/g, '/')
|
|
266
|
+
if (!normalized) continue
|
|
267
|
+
for (const { pkg, prefix } of prefixes) {
|
|
268
|
+
if (normalized.startsWith(prefix)) {
|
|
269
|
+
matched.set(pkg.shortLabel, pkg)
|
|
270
|
+
break
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return [...matched.values()]
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function defaultRunGit(root, gitArgs) {
|
|
278
|
+
return execFileSync('git', gitArgs, {
|
|
279
|
+
cwd: root,
|
|
280
|
+
encoding: 'utf8',
|
|
281
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
282
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function safeRunGit(runGit, root, gitArgs) {
|
|
287
|
+
try {
|
|
288
|
+
const output = runGit(root, gitArgs)
|
|
289
|
+
return typeof output === 'string' ? output : ''
|
|
290
|
+
} catch {
|
|
291
|
+
return ''
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function parseStatusPaths(output) {
|
|
296
|
+
const paths = []
|
|
297
|
+
for (const line of output.split('\n')) {
|
|
298
|
+
if (!line) continue
|
|
299
|
+
// `git status --porcelain` lines look like `XY path` or `XY old -> new`.
|
|
300
|
+
const body = line.slice(3)
|
|
301
|
+
if (!body) continue
|
|
302
|
+
const arrow = body.indexOf(' -> ')
|
|
303
|
+
paths.push(arrow === -1 ? body.trim() : body.slice(arrow + 4).trim())
|
|
304
|
+
}
|
|
305
|
+
return paths
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function resolveBaseRef(runGit, root, preferredBaseRef) {
|
|
309
|
+
const candidates = preferredBaseRef ? [preferredBaseRef, ...DEFAULT_BASE_REFS] : DEFAULT_BASE_REFS
|
|
310
|
+
for (const ref of candidates) {
|
|
311
|
+
const verified = safeRunGit(runGit, root, ['rev-parse', '--verify', '--quiet', ref]).trim()
|
|
312
|
+
if (verified) return ref
|
|
313
|
+
}
|
|
314
|
+
return null
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Discover packages "touched recently" via git: uncommitted working-tree
|
|
318
|
+
// changes (status) and the current branch's diff against a base ref.
|
|
319
|
+
export function detectTouchedPackages({
|
|
320
|
+
packages,
|
|
321
|
+
root,
|
|
322
|
+
config = {},
|
|
323
|
+
runGit = defaultRunGit,
|
|
324
|
+
}) {
|
|
325
|
+
const changedPaths = new Set()
|
|
326
|
+
|
|
327
|
+
if (config.gitStatusEnabled !== false) {
|
|
328
|
+
for (const path of parseStatusPaths(safeRunGit(runGit, root, ['status', '--porcelain']))) {
|
|
329
|
+
changedPaths.add(path)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (config.gitBranchEnabled !== false) {
|
|
334
|
+
const baseRef = resolveBaseRef(runGit, root, config.baseRef)
|
|
335
|
+
if (baseRef) {
|
|
336
|
+
const diff = safeRunGit(runGit, root, ['diff', '--name-only', `${baseRef}...HEAD`])
|
|
337
|
+
for (const path of diff.split('\n')) {
|
|
338
|
+
const trimmed = path.trim()
|
|
339
|
+
if (trimmed) changedPaths.add(trimmed)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return mapChangedPathsToPackages(packages, root, [...changedPaths])
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function parseLogFrequencies(output) {
|
|
348
|
+
const counts = new Map()
|
|
349
|
+
for (const line of output.split('\n')) {
|
|
350
|
+
const path = line.trim()
|
|
351
|
+
if (!path) continue
|
|
352
|
+
counts.set(path, (counts.get(path) ?? 0) + 1)
|
|
353
|
+
}
|
|
354
|
+
return counts
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Rank packages by how often their files changed in recent git history.
|
|
358
|
+
export function resolvePopularPackages({
|
|
359
|
+
packages,
|
|
360
|
+
root,
|
|
361
|
+
limit = DEFAULT_POPULAR_LIMIT,
|
|
362
|
+
override = [],
|
|
363
|
+
runGit = defaultRunGit,
|
|
364
|
+
}) {
|
|
365
|
+
if (override.length) {
|
|
366
|
+
const matched = matchPackagesByLabels(packages, override)
|
|
367
|
+
if (matched.length) return matched.slice(0, Math.max(limit, matched.length))
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const log = safeRunGit(runGit, root, [
|
|
371
|
+
'log',
|
|
372
|
+
'-n',
|
|
373
|
+
'400',
|
|
374
|
+
'--no-merges',
|
|
375
|
+
'--name-only',
|
|
376
|
+
'--pretty=format:',
|
|
377
|
+
])
|
|
378
|
+
const counts = parseLogFrequencies(log)
|
|
379
|
+
|
|
380
|
+
if (counts.size) {
|
|
381
|
+
const prefixes = packages
|
|
382
|
+
.map((pkg) => {
|
|
383
|
+
const relDir = toRelativeDir(root, pkg.packageDir)
|
|
384
|
+
return relDir ? { pkg, prefix: `${relDir}/` } : null
|
|
385
|
+
})
|
|
386
|
+
.filter(Boolean)
|
|
387
|
+
const score = new Map()
|
|
388
|
+
for (const [path, count] of counts) {
|
|
389
|
+
const normalized = path.replace(/\\/g, '/')
|
|
390
|
+
for (const { pkg, prefix } of prefixes) {
|
|
391
|
+
if (normalized.startsWith(prefix)) {
|
|
392
|
+
score.set(pkg.shortLabel, (score.get(pkg.shortLabel) ?? 0) + count)
|
|
393
|
+
break
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (score.size) {
|
|
398
|
+
const ranked = packages
|
|
399
|
+
.filter((pkg) => score.has(pkg.shortLabel))
|
|
400
|
+
.sort((a, b) => (score.get(b.shortLabel) ?? 0) - (score.get(a.shortLabel) ?? 0))
|
|
401
|
+
return ranked.slice(0, limit)
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Fallback: the canonical high-churn packages that actually exist here.
|
|
406
|
+
const fallback = matchPackagesByLabels(packages, DEFAULT_POPULAR_PACKAGES)
|
|
407
|
+
return (fallback.length ? fallback : packages).slice(0, limit)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function persistedSelectionPath(root) {
|
|
411
|
+
return join(root, PERSISTED_SELECTION_RELATIVE)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function readPersistedSelection(root) {
|
|
415
|
+
const file = persistedSelectionPath(root)
|
|
416
|
+
if (!existsSync(file)) return []
|
|
417
|
+
try {
|
|
418
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'))
|
|
419
|
+
if (Array.isArray(parsed)) return parsePackageList(parsed.join(','))
|
|
420
|
+
if (parsed && Array.isArray(parsed.packages)) return parsePackageList(parsed.packages.join(','))
|
|
421
|
+
return []
|
|
422
|
+
} catch {
|
|
423
|
+
return []
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function writePersistedSelection(root, labels) {
|
|
428
|
+
const file = persistedSelectionPath(root)
|
|
429
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
430
|
+
const payload = { packages: parsePackageList((labels ?? []).join(',')) }
|
|
431
|
+
writeFileSync(file, `${JSON.stringify(payload, null, 2)}\n`)
|
|
432
|
+
return file
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Resolve the final set of packages to watch for a given scope config.
|
|
436
|
+
// Returns { selected, mode, autoExpand, reason }.
|
|
437
|
+
export function selectWatchedPackages({
|
|
438
|
+
packages,
|
|
439
|
+
config,
|
|
440
|
+
root,
|
|
441
|
+
runGit = defaultRunGit,
|
|
442
|
+
}) {
|
|
443
|
+
const mode = config?.mode ?? WATCH_SCOPE_ALL
|
|
444
|
+
|
|
445
|
+
if (mode === WATCH_SCOPE_ALL || !packages?.length) {
|
|
446
|
+
return { selected: packages ?? [], mode: WATCH_SCOPE_ALL, autoExpand: false, reason: 'watching all packages' }
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (mode === WATCH_SCOPE_ENV) {
|
|
450
|
+
const labels = config.explicitPackages?.length
|
|
451
|
+
? config.explicitPackages
|
|
452
|
+
: readPersistedSelection(root)
|
|
453
|
+
const selected = matchPackagesByLabels(packages, labels)
|
|
454
|
+
if (!selected.length) {
|
|
455
|
+
return {
|
|
456
|
+
selected: packages,
|
|
457
|
+
mode: WATCH_SCOPE_ALL,
|
|
458
|
+
autoExpand: false,
|
|
459
|
+
reason: 'env scope requested but no packages matched OM_WATCH_PACKAGES / persisted selection — watching all',
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return { selected, mode, autoExpand: false, reason: `watching ${selected.length} package(s) from explicit selection` }
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (mode === WATCH_SCOPE_POPULAR) {
|
|
466
|
+
const selected = resolvePopularPackages({
|
|
467
|
+
packages,
|
|
468
|
+
root,
|
|
469
|
+
limit: config.popularLimit ?? DEFAULT_POPULAR_LIMIT,
|
|
470
|
+
override: config.popularOverride ?? [],
|
|
471
|
+
runGit,
|
|
472
|
+
})
|
|
473
|
+
return { selected, mode, autoExpand: false, reason: `watching ${selected.length} most-popular package(s)` }
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// auto-optimized
|
|
477
|
+
const touched = detectTouchedPackages({ packages, root, config, runGit })
|
|
478
|
+
if (touched.length) {
|
|
479
|
+
return { selected: touched, mode, autoExpand: true, reason: `watching ${touched.length} recently-touched package(s), expanding every 2m` }
|
|
480
|
+
}
|
|
481
|
+
// Nothing touched yet — seed with the popular fallback and grow from there.
|
|
482
|
+
const seed = resolvePopularPackages({
|
|
483
|
+
packages,
|
|
484
|
+
root,
|
|
485
|
+
limit: config.popularLimit ?? DEFAULT_POPULAR_LIMIT,
|
|
486
|
+
override: config.popularOverride ?? [],
|
|
487
|
+
runGit,
|
|
488
|
+
})
|
|
489
|
+
return { selected: seed, mode, autoExpand: true, reason: `no recent changes detected — seeding ${seed.length} package(s), expanding every 2m` }
|
|
490
|
+
}
|