@owdproject/cli 0.0.3 → 0.2.0
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/LICENSE +20 -0
- package/README.md +50 -16
- package/bin/bridge.js +129 -0
- package/bin/cli.js +491 -0
- package/bin/cp.js +3542 -0
- package/bin/desktop.js +5 -0
- package/bin/lib/catalog.js +504 -0
- package/bin/lib/config.js +173 -0
- package/bin/lib/cpAscii.js +39 -0
- package/bin/lib/cpFormat.js +343 -0
- package/bin/lib/create.js +246 -0
- package/bin/lib/desktopConfig.js +48 -0
- package/bin/lib/generateTemplate.js +427 -0
- package/bin/lib/install.js +521 -0
- package/bin/lib/installWizardFlow.js +792 -0
- package/bin/lib/migrateModulePlayground.config.mjs +187 -0
- package/bin/lib/migrateModulePlayground.mjs +318 -0
- package/bin/lib/npmVersions.js +124 -0
- package/bin/lib/packageSources.js +375 -0
- package/bin/lib/playgroundContext.js +188 -0
- package/bin/lib/scaffold.js +205 -0
- package/bin/lib/status.js +319 -0
- package/bin/lib/validateModule.js +978 -0
- package/bin/lib/workspace.js +289 -0
- package/bin/owd.js +2 -118
- package/go/bridge/bridge.go +220 -0
- package/go/desktop-go +0 -0
- package/go/go.mod +33 -0
- package/go/go.sum +52 -0
- package/go/main.go +34 -0
- package/go/owd-cli +0 -0
- package/go/tui/engine.go +472 -0
- package/go/tui/engine_test.go +40 -0
- package/go/tui/handlers.go +604 -0
- package/go/tui/helpers.go +916 -0
- package/go/tui/helpers_test.go +97 -0
- package/go/tui/modals.go +423 -0
- package/go/tui/resolver.go +377 -0
- package/go/tui/resolver_test.go +86 -0
- package/go/tui/styles.go +153 -0
- package/go/tui/tasks.go +850 -0
- package/go/tui/tui.go +869 -0
- package/go/tui/tui_test.go +69 -0
- package/go/tui/types.go +230 -0
- package/go/tui/view.go +812 -0
- package/go/tui/workqueue.go +179 -0
- package/go/tui/workqueue_test.go +90 -0
- package/package.json +33 -22
- package/src/commands/create.js +0 -11
- package/src/commands/generate.js +0 -12
- package/src/commands/installApp.js +0 -9
- package/src/commands/installTheme.js +0 -9
- package/src/commands/start.js +0 -13
- package/src/utils/cloneRepo.js +0 -25
- package/src/utils/installPackage.js +0 -15
- package/src/utils/logging.js +0 -15
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
3
|
+
import { dirname, join, relative, resolve } from 'node:path'
|
|
4
|
+
import { findWorkspaceRoot } from './workspace.js'
|
|
5
|
+
|
|
6
|
+
const SCOPE = '@owdproject/'
|
|
7
|
+
|
|
8
|
+
export const DESKTOP_CORE_PACKAGE = '@owdproject/core'
|
|
9
|
+
export const DESKTOP_KIT_PRIMEVUE_PACKAGE = '@owdproject/kit-primevue'
|
|
10
|
+
|
|
11
|
+
const REQUIRED_SCRIPTS = ['dev', 'dev:prepare', 'dev:generate', 'prepack']
|
|
12
|
+
const INFRASTRUCTURE_PLAYBOOK_SCRIPTS = ['dev:prepare', 'prepack']
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} pkgName
|
|
16
|
+
*/
|
|
17
|
+
export function isDesktopCorePackage(pkgName) {
|
|
18
|
+
return pkgName === DESKTOP_CORE_PACKAGE
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Published Nuxt modules without an app/theme playground (core, kit-primevue).
|
|
23
|
+
*
|
|
24
|
+
* @param {string} pkgName
|
|
25
|
+
*/
|
|
26
|
+
export function isDesktopInfrastructurePackage(pkgName) {
|
|
27
|
+
return (
|
|
28
|
+
pkgName === DESKTOP_CORE_PACKAGE || pkgName === DESKTOP_KIT_PRIMEVUE_PACKAGE
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} pkgName
|
|
34
|
+
* @returns {'app' | 'theme' | 'module'}
|
|
35
|
+
*/
|
|
36
|
+
export function inferModuleKind(pkgName) {
|
|
37
|
+
const short = pkgName.startsWith(SCOPE) ? pkgName.slice(SCOPE.length) : pkgName
|
|
38
|
+
if (short.startsWith('app-')) return 'app'
|
|
39
|
+
if (short.startsWith('theme-')) return 'theme'
|
|
40
|
+
return 'module'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Walk up from cwd until a package.json with @owdproject/ name is found.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} [startDir]
|
|
47
|
+
* @returns {string | null}
|
|
48
|
+
*/
|
|
49
|
+
export function findModulePackageRoot(startDir = process.cwd()) {
|
|
50
|
+
let dir = resolve(startDir)
|
|
51
|
+
for (;;) {
|
|
52
|
+
const pkgPath = join(dir, 'package.json')
|
|
53
|
+
if (existsSync(pkgPath) && existsSync(join(dir, 'src', 'module.ts'))) {
|
|
54
|
+
try {
|
|
55
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
56
|
+
if (typeof pkg.name === 'string' && pkg.name.startsWith(SCOPE)) {
|
|
57
|
+
return dir
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
/* ignore */
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const parent = dirname(dir)
|
|
64
|
+
if (parent === dir) return null
|
|
65
|
+
dir = parent
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {string} workspaceRoot
|
|
71
|
+
* @returns {string[]}
|
|
72
|
+
*/
|
|
73
|
+
export function discoverOwdModulePackages(workspaceRoot) {
|
|
74
|
+
const roots = []
|
|
75
|
+
const scanDirs = [
|
|
76
|
+
join(workspaceRoot, 'apps'),
|
|
77
|
+
join(workspaceRoot, 'themes'),
|
|
78
|
+
join(workspaceRoot, 'packages'),
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
for (const base of scanDirs) {
|
|
82
|
+
if (!existsSync(base)) continue
|
|
83
|
+
for (const entry of readdirSync(base)) {
|
|
84
|
+
const dir = join(base, entry)
|
|
85
|
+
try {
|
|
86
|
+
if (!statSync(dir).isDirectory()) continue
|
|
87
|
+
} catch {
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
const pkgPath = join(dir, 'package.json')
|
|
91
|
+
if (!existsSync(pkgPath)) continue
|
|
92
|
+
try {
|
|
93
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
94
|
+
if (!pkg.name?.startsWith(SCOPE)) continue
|
|
95
|
+
if (isDesktopCorePackage(pkg.name)) continue
|
|
96
|
+
if (!existsSync(join(dir, 'src', 'module.ts'))) continue
|
|
97
|
+
roots.push(dir)
|
|
98
|
+
} catch {
|
|
99
|
+
/* ignore */
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return roots.sort()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {string} content
|
|
109
|
+
* @param {string} pkgName
|
|
110
|
+
*/
|
|
111
|
+
function desktopConfigReferencesPackage(content, pkgName, kind) {
|
|
112
|
+
if (kind === 'app') {
|
|
113
|
+
return (
|
|
114
|
+
new RegExp(`apps\\s*:\\s*\\[[^\\]]*['"]${escapeRegExp(pkgName)}['"]`, 's').test(
|
|
115
|
+
content,
|
|
116
|
+
) || new RegExp(`apps\\s*:\\s*\\[[^\\]]*${escapeRegExp(pkgName)}`, 's').test(content)
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
if (kind === 'theme') {
|
|
120
|
+
return (
|
|
121
|
+
new RegExp(`theme\\s*:\\s*['"]${escapeRegExp(pkgName)}['"]`).test(content) ||
|
|
122
|
+
new RegExp(`theme\\s*:\\s*${escapeRegExp(pkgName)}`).test(content)
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
return (
|
|
126
|
+
new RegExp(`modules\\s*:\\s*\\[[^\\]]*['"]${escapeRegExp(pkgName)}['"]`, 's').test(
|
|
127
|
+
content,
|
|
128
|
+
) || new RegExp(`modules\\s*:\\s*\\[[^\\]]*${escapeRegExp(pkgName)}`, 's').test(content)
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @param {string} s
|
|
134
|
+
*/
|
|
135
|
+
function escapeRegExp(s) {
|
|
136
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** @param {string} moduleSrc */
|
|
140
|
+
export function findLegacyModuleMetaName(moduleSrc) {
|
|
141
|
+
const match = moduleSrc.match(
|
|
142
|
+
/meta\s*:\s*\{[\s\S]*?\bname\s*:\s*['"](owd-[^'"]+)['"]/,
|
|
143
|
+
)
|
|
144
|
+
return match?.[1] ?? null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {string} content
|
|
149
|
+
* @returns {string[]}
|
|
150
|
+
*/
|
|
151
|
+
export function findLegacyPiniaStoreIdSnippets(content) {
|
|
152
|
+
/** @type {string[]} */
|
|
153
|
+
const hits = []
|
|
154
|
+
const patterns = [
|
|
155
|
+
/defineStore\s*\(\s*['"]owd\/[^'"]+['"]/g,
|
|
156
|
+
/defineStore\s*\(\s*`owd\/[^`]+`/g,
|
|
157
|
+
/(?:const|let)\s+\w*id\w*\s*=\s*['"]owd\/[^'"]+['"]/gi,
|
|
158
|
+
/(?:const|let)\s+\w*id\w*\s*=\s*`owd\/[^`]+`/gi,
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
for (const pattern of patterns) {
|
|
162
|
+
for (const match of content.matchAll(pattern)) {
|
|
163
|
+
const snippet = match[0].replace(/\s+/g, ' ').trim()
|
|
164
|
+
if (!hits.includes(snippet)) {
|
|
165
|
+
hits.push(snippet)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return hits
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param {string} packageDir
|
|
175
|
+
* @returns {{ file: string, snippets: string[] }[]}
|
|
176
|
+
*/
|
|
177
|
+
export function scanLegacyPiniaStoreIds(packageDir) {
|
|
178
|
+
const srcDir = join(packageDir, 'src')
|
|
179
|
+
if (!existsSync(srcDir)) return []
|
|
180
|
+
|
|
181
|
+
/** @type {{ file: string, snippets: string[] }[]} */
|
|
182
|
+
const findings = []
|
|
183
|
+
|
|
184
|
+
/** @param {string} current */
|
|
185
|
+
const walk = (current) => {
|
|
186
|
+
for (const entry of readdirSync(current)) {
|
|
187
|
+
const full = join(current, entry)
|
|
188
|
+
let stat
|
|
189
|
+
try {
|
|
190
|
+
stat = statSync(full)
|
|
191
|
+
} catch {
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
if (stat.isDirectory()) {
|
|
195
|
+
walk(full)
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
if (!/\.(ts|js|mjs|vue)$/i.test(entry)) continue
|
|
199
|
+
if (
|
|
200
|
+
/storeIds\.ts$/i.test(entry) ||
|
|
201
|
+
/migrate[A-Za-z]*\.ts$/i.test(entry) ||
|
|
202
|
+
/migrate[A-Za-z]*\.js$/i.test(entry)
|
|
203
|
+
) {
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const content = readFileSync(full, 'utf8')
|
|
208
|
+
const snippets = findLegacyPiniaStoreIdSnippets(content)
|
|
209
|
+
if (snippets.length > 0) {
|
|
210
|
+
findings.push({
|
|
211
|
+
file: relative(packageDir, full).replace(/\\/g, '/'),
|
|
212
|
+
snippets,
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
walk(srcDir)
|
|
219
|
+
return findings
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* @param {string} workspaceYaml
|
|
224
|
+
* @param {string} playgroundRel
|
|
225
|
+
*/
|
|
226
|
+
function workspaceListsPlayground(workspaceYaml, playgroundRel) {
|
|
227
|
+
const normalized = playgroundRel.replace(/\\/g, '/')
|
|
228
|
+
const lines = workspaceYaml.split('\n')
|
|
229
|
+
for (const line of lines) {
|
|
230
|
+
const trimmed = line.trim().replace(/^-\s*/, '')
|
|
231
|
+
if (!trimmed) continue
|
|
232
|
+
if (trimmed === normalized) return true
|
|
233
|
+
if (trimmed.endsWith('/*')) {
|
|
234
|
+
const prefix = trimmed.slice(0, -2)
|
|
235
|
+
if (normalized.startsWith(`${prefix}/`)) return true
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* @typedef {object} ValidationIssue
|
|
243
|
+
* @property {'error' | 'warning'} level
|
|
244
|
+
* @property {string} code
|
|
245
|
+
* @property {string} message
|
|
246
|
+
*/
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* @typedef {object} ValidationResult
|
|
250
|
+
* @property {boolean} ok
|
|
251
|
+
* @property {string} packageDir
|
|
252
|
+
* @property {string} pkgName
|
|
253
|
+
* @property {'app' | 'theme' | 'module'} kind
|
|
254
|
+
* @property {ValidationIssue[]} errors
|
|
255
|
+
* @property {ValidationIssue[]} warnings
|
|
256
|
+
*/
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* @param {string} packageDir
|
|
260
|
+
* @param {{ workspaceRoot?: string | null, requireDist?: boolean, strict?: boolean }} [options]
|
|
261
|
+
* @returns {ValidationResult}
|
|
262
|
+
*/
|
|
263
|
+
export function validateOwdModule(packageDir, options = {}) {
|
|
264
|
+
const dir = resolve(packageDir)
|
|
265
|
+
const pkgPath = join(dir, 'package.json')
|
|
266
|
+
/** @type {ValidationIssue[]} */
|
|
267
|
+
const errors = []
|
|
268
|
+
/** @type {ValidationIssue[]} */
|
|
269
|
+
const warnings = []
|
|
270
|
+
|
|
271
|
+
const issue = (level, code, message) => {
|
|
272
|
+
;(level === 'error' ? errors : warnings).push({ level, code, message })
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!existsSync(pkgPath)) {
|
|
276
|
+
return {
|
|
277
|
+
ok: false,
|
|
278
|
+
packageDir: dir,
|
|
279
|
+
pkgName: '',
|
|
280
|
+
kind: 'module',
|
|
281
|
+
errors: [{ level: 'error', code: 'missing-package-json', message: 'package.json not found' }],
|
|
282
|
+
warnings: [],
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** @type {Record<string, unknown>} */
|
|
287
|
+
let pkg
|
|
288
|
+
try {
|
|
289
|
+
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
290
|
+
} catch {
|
|
291
|
+
return {
|
|
292
|
+
ok: false,
|
|
293
|
+
packageDir: dir,
|
|
294
|
+
pkgName: '',
|
|
295
|
+
kind: 'module',
|
|
296
|
+
errors: [
|
|
297
|
+
{
|
|
298
|
+
level: 'error',
|
|
299
|
+
code: 'invalid-package-json',
|
|
300
|
+
message: 'package.json is not valid JSON',
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
warnings: [],
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const pkgName = typeof pkg.name === 'string' ? pkg.name : ''
|
|
308
|
+
if (!pkgName.startsWith(SCOPE)) {
|
|
309
|
+
issue('error', 'invalid-scope', `package name must start with ${SCOPE}`)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const kind = inferModuleKind(pkgName)
|
|
313
|
+
const isInfrastructure = isDesktopInfrastructurePackage(pkgName)
|
|
314
|
+
|
|
315
|
+
if (pkg.type !== 'module') {
|
|
316
|
+
issue('error', 'type-module', 'package.json must set "type": "module"')
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (isInfrastructure) {
|
|
320
|
+
const scripts = /** @type {Record<string, string>} */ (pkg.scripts ?? {})
|
|
321
|
+
for (const script of INFRASTRUCTURE_PLAYBOOK_SCRIPTS) {
|
|
322
|
+
if (!scripts[script]) {
|
|
323
|
+
issue(
|
|
324
|
+
'error',
|
|
325
|
+
`script-${script}`,
|
|
326
|
+
`package.json scripts must include "${script}"`,
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (!scripts['dev:prepare']?.includes('nuxt-module-build')) {
|
|
331
|
+
issue(
|
|
332
|
+
'error',
|
|
333
|
+
'dev-prepare-builder',
|
|
334
|
+
'dev:prepare must run nuxt-module-build build --stub',
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
if (!scripts['prepack']?.includes('nuxt-module-build')) {
|
|
338
|
+
issue('error', 'prepack-builder', 'prepack must run nuxt-module-build build')
|
|
339
|
+
}
|
|
340
|
+
const devDeps = /** @type {Record<string, string>} */ (pkg.devDependencies ?? {})
|
|
341
|
+
if (!devDeps['@nuxt/module-builder']) {
|
|
342
|
+
issue(
|
|
343
|
+
'error',
|
|
344
|
+
'module-builder-dep',
|
|
345
|
+
'devDependencies must include @nuxt/module-builder',
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
const distModule = join(dir, 'dist', 'module.mjs')
|
|
349
|
+
if (!existsSync(distModule)) {
|
|
350
|
+
issue(
|
|
351
|
+
options.requireDist ? 'error' : 'error',
|
|
352
|
+
'dist-module',
|
|
353
|
+
'dist/module.mjs missing — run: pnpm run dev:prepare',
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
const ok =
|
|
357
|
+
errors.length === 0 && (!options.strict || warnings.length === 0)
|
|
358
|
+
return {
|
|
359
|
+
ok,
|
|
360
|
+
packageDir: dir,
|
|
361
|
+
pkgName,
|
|
362
|
+
kind,
|
|
363
|
+
errors,
|
|
364
|
+
warnings,
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const exports = /** @type {Record<string, unknown>} */ (pkg.exports ?? {})
|
|
369
|
+
const rootExport = /** @type {Record<string, string> | undefined} */ (
|
|
370
|
+
typeof exports['.'] === 'object' ? exports['.'] : undefined
|
|
371
|
+
)
|
|
372
|
+
if (rootExport?.import !== './dist/module.mjs') {
|
|
373
|
+
issue(
|
|
374
|
+
'error',
|
|
375
|
+
'exports-import',
|
|
376
|
+
'exports["."].import must be "./dist/module.mjs"',
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
if (pkg.main !== './dist/module.mjs') {
|
|
380
|
+
issue('error', 'main', 'main must be "./dist/module.mjs"')
|
|
381
|
+
}
|
|
382
|
+
const files = /** @type {string[] | undefined} */ (pkg.files)
|
|
383
|
+
if (!Array.isArray(files) || !files.includes('dist')) {
|
|
384
|
+
issue('error', 'files-dist', 'files must include "dist"')
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const scripts = /** @type {Record<string, string>} */ (pkg.scripts ?? {})
|
|
388
|
+
for (const script of REQUIRED_SCRIPTS) {
|
|
389
|
+
if (!scripts[script]) {
|
|
390
|
+
issue('error', `script-${script}`, `package.json scripts must include "${script}"`)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (scripts['dev:prepare'] && !scripts['dev:prepare'].includes('nuxt-module-build')) {
|
|
394
|
+
issue(
|
|
395
|
+
'error',
|
|
396
|
+
'dev-prepare-builder',
|
|
397
|
+
'dev:prepare must run nuxt-module-build (stub + prepare)',
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
if (scripts['prepack'] && !scripts['prepack'].includes('nuxt-module-build')) {
|
|
401
|
+
issue('error', 'prepack-builder', 'prepack must run nuxt-module-build build')
|
|
402
|
+
}
|
|
403
|
+
if (scripts.prepare) {
|
|
404
|
+
issue(
|
|
405
|
+
'error',
|
|
406
|
+
'no-prepare-script',
|
|
407
|
+
'Do not add a "prepare" script on publishable packages — it runs on npm publish/install and breaks release. Use "prepack" (full build) and "dev:prepare" (local stub + playground) only.',
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
if (!scripts.validate?.includes('desktop validate')) {
|
|
411
|
+
issue(
|
|
412
|
+
'warning',
|
|
413
|
+
'validate-script',
|
|
414
|
+
'scripts.validate should run "desktop validate ."',
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
if (!rootExport?.development?.includes('src/module')) {
|
|
418
|
+
issue(
|
|
419
|
+
'warning',
|
|
420
|
+
'exports-development',
|
|
421
|
+
'exports["."].development should point to ./src/module.ts (monorepo dev without manual stub after every edit)',
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
if (!scripts['dev:generate']?.includes('NUXT_APP_BASE_URL')) {
|
|
425
|
+
issue(
|
|
426
|
+
'warning',
|
|
427
|
+
'dev-generate-base-url',
|
|
428
|
+
'dev:generate should set NUXT_APP_BASE_URL=/<repo-slug>/ for GitHub Pages',
|
|
429
|
+
)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const devDeps = /** @type {Record<string, string>} */ (pkg.devDependencies ?? {})
|
|
433
|
+
if (!devDeps['@nuxt/module-builder']) {
|
|
434
|
+
issue(
|
|
435
|
+
'error',
|
|
436
|
+
'module-builder-dep',
|
|
437
|
+
'devDependencies must include @nuxt/module-builder',
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const peerDeps = /** @type {Record<string, string>} */ (pkg.peerDependencies ?? {})
|
|
442
|
+
if (!peerDeps['@owdproject/core']) {
|
|
443
|
+
issue(
|
|
444
|
+
'warning',
|
|
445
|
+
'peer-core',
|
|
446
|
+
'peerDependencies should declare @owdproject/core',
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (existsSync(join(dir, 'module.ts'))) {
|
|
451
|
+
issue(
|
|
452
|
+
'error',
|
|
453
|
+
'legacy-module-root',
|
|
454
|
+
'legacy layout: move module.ts to src/module.ts',
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
if (existsSync(join(dir, 'runtime')) && !existsSync(join(dir, 'src', 'runtime'))) {
|
|
458
|
+
issue(
|
|
459
|
+
'error',
|
|
460
|
+
'legacy-runtime-root',
|
|
461
|
+
'legacy layout: move runtime/ under src/runtime/',
|
|
462
|
+
)
|
|
463
|
+
}
|
|
464
|
+
if (existsSync(join(dir, 'runtime')) && existsSync(join(dir, 'src', 'runtime'))) {
|
|
465
|
+
issue(
|
|
466
|
+
'error',
|
|
467
|
+
'duplicate-runtime-root',
|
|
468
|
+
'remove legacy runtime/ at package root — keep only src/runtime/ (duplicate layout fails CI and confuses module resolution)',
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const moduleTs = join(dir, 'src', 'module.ts')
|
|
473
|
+
if (!existsSync(moduleTs)) {
|
|
474
|
+
issue('error', 'src-module', 'src/module.ts is required')
|
|
475
|
+
} else {
|
|
476
|
+
const moduleSrc = readFileSync(moduleTs, 'utf8')
|
|
477
|
+
const hasModuleWrapper =
|
|
478
|
+
moduleSrc.includes('defineNuxtModule') ||
|
|
479
|
+
(kind === 'module' && moduleSrc.includes('defineDesktopModule')) ||
|
|
480
|
+
(kind === 'theme' && moduleSrc.includes('defineDesktopTheme'))
|
|
481
|
+
if (!hasModuleWrapper) {
|
|
482
|
+
const expected =
|
|
483
|
+
kind === 'module'
|
|
484
|
+
? 'defineDesktopModule (or defineNuxtModule)'
|
|
485
|
+
: kind === 'theme'
|
|
486
|
+
? 'defineDesktopTheme (or defineNuxtModule)'
|
|
487
|
+
: 'defineNuxtModule'
|
|
488
|
+
issue(
|
|
489
|
+
'error',
|
|
490
|
+
'define-nuxt-module',
|
|
491
|
+
`src/module.ts must use ${expected}`,
|
|
492
|
+
)
|
|
493
|
+
}
|
|
494
|
+
const hasKitTailwind =
|
|
495
|
+
moduleSrc.includes('registerTailwindPath') ||
|
|
496
|
+
moduleSrc.includes('registerThemeTailwindPath')
|
|
497
|
+
const hasKitPrimevue =
|
|
498
|
+
moduleSrc.includes("installModule('@owdproject/kit-primevue'") ||
|
|
499
|
+
moduleSrc.includes('installModule("@owdproject/kit-primevue"')
|
|
500
|
+
if (kind === 'theme') {
|
|
501
|
+
if (!hasKitPrimevue) {
|
|
502
|
+
issue(
|
|
503
|
+
'warning',
|
|
504
|
+
'kit-primevue',
|
|
505
|
+
'theme src/module.ts should install @owdproject/kit-primevue for PrimeVue + Tailwind',
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
if (!hasKitTailwind) {
|
|
509
|
+
issue(
|
|
510
|
+
'warning',
|
|
511
|
+
'tailwind-path',
|
|
512
|
+
'theme src/module.ts should call registerThemeTailwindPath from @owdproject/kit-primevue/kit/registerTailwindPath',
|
|
513
|
+
)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Check required theme components
|
|
517
|
+
const componentsDir = join(dir, 'src', 'runtime', 'components')
|
|
518
|
+
if (!existsSync(componentsDir)) {
|
|
519
|
+
issue(
|
|
520
|
+
'error',
|
|
521
|
+
'missing-components-dir',
|
|
522
|
+
'theme src/runtime/components/ directory is required',
|
|
523
|
+
)
|
|
524
|
+
} else {
|
|
525
|
+
const foundComponents = new Set()
|
|
526
|
+
const scanComponents = (current) => {
|
|
527
|
+
for (const entry of readdirSync(current)) {
|
|
528
|
+
const full = join(current, entry)
|
|
529
|
+
let stat
|
|
530
|
+
try {
|
|
531
|
+
stat = statSync(full)
|
|
532
|
+
} catch {
|
|
533
|
+
continue
|
|
534
|
+
}
|
|
535
|
+
if (stat.isDirectory()) {
|
|
536
|
+
scanComponents(full)
|
|
537
|
+
} else if (/\.(vue|ts|js)$/i.test(entry)) {
|
|
538
|
+
const name = entry.replace(/\.(vue|ts|js)$/i, '')
|
|
539
|
+
foundComponents.add(name)
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
scanComponents(componentsDir)
|
|
544
|
+
|
|
545
|
+
const requiredComponents = [
|
|
546
|
+
'DesktopWindow',
|
|
547
|
+
'DesktopWindowNav',
|
|
548
|
+
'DesktopWindowIframe',
|
|
549
|
+
]
|
|
550
|
+
for (const req of requiredComponents) {
|
|
551
|
+
if (!foundComponents.has(req)) {
|
|
552
|
+
issue(
|
|
553
|
+
'error',
|
|
554
|
+
`missing-theme-component-${req.toLowerCase()}`,
|
|
555
|
+
`theme must implement the "${req}" component under src/runtime/components/`,
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
} else if (kind === 'app' && !hasKitTailwind) {
|
|
561
|
+
issue(
|
|
562
|
+
'warning',
|
|
563
|
+
'tailwind-path',
|
|
564
|
+
'src/module.ts should call registerTailwindPath from @owdproject/kit-tailwind/kit/registerTailwindPath for Vue components',
|
|
565
|
+
)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const legacyMetaName = findLegacyModuleMetaName(moduleSrc)
|
|
569
|
+
if (legacyMetaName) {
|
|
570
|
+
issue(
|
|
571
|
+
'warning',
|
|
572
|
+
'module-meta-name-legacy',
|
|
573
|
+
`src/module.ts meta.name is "${legacyMetaName}"; migrate to desktop-<kind>-<slug> (see MIGRATION_3.4.md)`,
|
|
574
|
+
)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (!isInfrastructure) {
|
|
579
|
+
for (const finding of scanLegacyPiniaStoreIds(dir)) {
|
|
580
|
+
issue(
|
|
581
|
+
'warning',
|
|
582
|
+
'pinia-store-id-legacy',
|
|
583
|
+
`${finding.file}: Pinia store id still uses legacy "owd/" (${finding.snippets.join('; ')}); migrate to "desktop/" ids`,
|
|
584
|
+
)
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const distModule = join(dir, 'dist', 'module.mjs')
|
|
589
|
+
if (!existsSync(distModule)) {
|
|
590
|
+
issue(
|
|
591
|
+
options.requireDist ? 'error' : 'error',
|
|
592
|
+
'dist-module',
|
|
593
|
+
'dist/module.mjs missing — run: pnpm run dev:prepare',
|
|
594
|
+
)
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const playgroundDir = join(dir, 'playground')
|
|
598
|
+
if (!existsSync(playgroundDir)) {
|
|
599
|
+
issue('error', 'playground-dir', 'playground/ directory is required')
|
|
600
|
+
} else {
|
|
601
|
+
const pgPkgPath = join(playgroundDir, 'package.json')
|
|
602
|
+
if (!existsSync(pgPkgPath)) {
|
|
603
|
+
issue('error', 'playground-package-json', 'playground/package.json is required')
|
|
604
|
+
} else {
|
|
605
|
+
try {
|
|
606
|
+
const pgPkg = JSON.parse(readFileSync(pgPkgPath, 'utf8'))
|
|
607
|
+
const deps = {
|
|
608
|
+
.../** @type {Record<string, string>} */ (pgPkg.dependencies ?? {}),
|
|
609
|
+
.../** @type {Record<string, string>} */ (pgPkg.devDependencies ?? {}),
|
|
610
|
+
}
|
|
611
|
+
if (!deps[pkgName] && !deps[pkgName.replace(SCOPE, '')]) {
|
|
612
|
+
issue(
|
|
613
|
+
'error',
|
|
614
|
+
'playground-self-dep',
|
|
615
|
+
`playground/package.json must depend on ${pkgName}`,
|
|
616
|
+
)
|
|
617
|
+
}
|
|
618
|
+
if (!deps['@owdproject/core']) {
|
|
619
|
+
issue(
|
|
620
|
+
'error',
|
|
621
|
+
'playground-core-dep',
|
|
622
|
+
'playground/package.json must depend on @owdproject/core',
|
|
623
|
+
)
|
|
624
|
+
}
|
|
625
|
+
if (kind === 'app' && !deps['@owdproject/theme-nova']) {
|
|
626
|
+
issue(
|
|
627
|
+
'warning',
|
|
628
|
+
'playground-theme-nova',
|
|
629
|
+
'app playgrounds should use @owdproject/theme-nova in dependencies',
|
|
630
|
+
)
|
|
631
|
+
}
|
|
632
|
+
} catch {
|
|
633
|
+
issue('error', 'playground-package-json', 'playground/package.json is invalid JSON')
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const nuxtConfigPath = join(playgroundDir, 'nuxt.config.ts')
|
|
638
|
+
if (!existsSync(nuxtConfigPath)) {
|
|
639
|
+
issue('error', 'playground-nuxt-config', 'playground/nuxt.config.ts is required')
|
|
640
|
+
} else {
|
|
641
|
+
const nuxtCfg = readFileSync(nuxtConfigPath, 'utf8')
|
|
642
|
+
if (!nuxtCfg.includes('@owdproject/core')) {
|
|
643
|
+
issue(
|
|
644
|
+
'error',
|
|
645
|
+
'playground-nuxt-core',
|
|
646
|
+
'playground/nuxt.config.ts must register @owdproject/core',
|
|
647
|
+
)
|
|
648
|
+
}
|
|
649
|
+
if (!/\bssr\s*:\s*false\b/.test(nuxtCfg)) {
|
|
650
|
+
issue('error', 'playground-ssr-false', 'playground/nuxt.config.ts must set ssr: false')
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const desktopConfigPath = join(playgroundDir, 'desktop.config.ts')
|
|
655
|
+
if (!existsSync(desktopConfigPath)) {
|
|
656
|
+
issue('error', 'playground-desktop-config', 'playground/desktop.config.ts is required')
|
|
657
|
+
} else if (pkgName) {
|
|
658
|
+
const desktopCfg = readFileSync(desktopConfigPath, 'utf8')
|
|
659
|
+
if (!desktopConfigReferencesPackage(desktopCfg, pkgName, kind)) {
|
|
660
|
+
const field = kind === 'app' ? 'apps' : kind === 'theme' ? 'theme' : 'modules'
|
|
661
|
+
issue(
|
|
662
|
+
'error',
|
|
663
|
+
'playground-desktop-ref',
|
|
664
|
+
`playground/desktop.config.ts must reference ${pkgName} in \`${field}\``,
|
|
665
|
+
)
|
|
666
|
+
}
|
|
667
|
+
if (!desktopCfg.includes('defineDesktopConfig')) {
|
|
668
|
+
issue(
|
|
669
|
+
'error',
|
|
670
|
+
'playground-define-config',
|
|
671
|
+
'playground/desktop.config.ts must use defineDesktopConfig',
|
|
672
|
+
)
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const appVuePath = join(playgroundDir, 'app', 'app.vue')
|
|
677
|
+
if (!existsSync(appVuePath)) {
|
|
678
|
+
issue('error', 'playground-app-vue', 'playground/app/app.vue is required')
|
|
679
|
+
} else {
|
|
680
|
+
const appVue = readFileSync(appVuePath, 'utf8')
|
|
681
|
+
if (!/<Desktop\s*\/?>/.test(appVue) && !/<Desktop[\s>]/.test(appVue)) {
|
|
682
|
+
issue(
|
|
683
|
+
'error',
|
|
684
|
+
'playground-desktop-component',
|
|
685
|
+
'playground/app/app.vue must render <Desktop />',
|
|
686
|
+
)
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const workspaceRoot = options.workspaceRoot ?? findWorkspaceRoot(dir)
|
|
691
|
+
const relPkg =
|
|
692
|
+
workspaceRoot != null ? relative(workspaceRoot, dir).replace(/\\/g, '/') : ''
|
|
693
|
+
const isMonorepoModule =
|
|
694
|
+
workspaceRoot != null &&
|
|
695
|
+
/^(apps|themes|packages)\/[^/]+$/.test(relPkg) &&
|
|
696
|
+
options.checkWorkspace !== false
|
|
697
|
+
|
|
698
|
+
if (isMonorepoModule) {
|
|
699
|
+
const wsPath = join(workspaceRoot, 'pnpm-workspace.yaml')
|
|
700
|
+
if (existsSync(wsPath)) {
|
|
701
|
+
const relPlayground = relative(workspaceRoot, playgroundDir)
|
|
702
|
+
const wsYaml = readFileSync(wsPath, 'utf8')
|
|
703
|
+
if (!workspaceListsPlayground(wsYaml, relPlayground)) {
|
|
704
|
+
issue(
|
|
705
|
+
'error',
|
|
706
|
+
'workspace-playground',
|
|
707
|
+
`add "${relPlayground}" to pnpm-workspace.yaml packages`,
|
|
708
|
+
)
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (kind === 'app') {
|
|
715
|
+
const appDeps = /** @type {Record<string, string>} */ (pkg.dependencies ?? {})
|
|
716
|
+
if (appDeps[DESKTOP_KIT_PRIMEVUE_PACKAGE]) {
|
|
717
|
+
issue(
|
|
718
|
+
'warning',
|
|
719
|
+
'app-kit-primevue-dep',
|
|
720
|
+
'remove @owdproject/kit-primevue from app dependencies — PrimeVue comes from the theme at runtime; apps only need @owdproject/kit-tailwind',
|
|
721
|
+
)
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const appConfigPath = join(dir, 'src', 'runtime', 'app.config.ts')
|
|
725
|
+
if (!existsSync(appConfigPath)) {
|
|
726
|
+
issue('error', 'app-config', 'src/runtime/app.config.ts is required for apps')
|
|
727
|
+
} else if (/position\s*:\s*\{[\s\S]*?\bx\s*:/m.test(readFileSync(appConfigPath, 'utf8'))) {
|
|
728
|
+
issue(
|
|
729
|
+
'warning',
|
|
730
|
+
'app-config-position',
|
|
731
|
+
'omit windows.*.position in app.config.ts for pre-open centering; set explicit size.width and size.height',
|
|
732
|
+
)
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const pluginPath = join(dir, 'src', 'runtime', 'plugin.ts')
|
|
736
|
+
if (!existsSync(pluginPath)) {
|
|
737
|
+
issue('error', 'app-plugin', 'src/runtime/plugin.ts is required for apps')
|
|
738
|
+
} else {
|
|
739
|
+
const pluginSrc = readFileSync(pluginPath, 'utf8')
|
|
740
|
+
if (!pluginSrc.includes('defineDesktopApp')) {
|
|
741
|
+
issue('error', 'define-desktop-app', 'plugin.ts must call defineDesktopApp')
|
|
742
|
+
}
|
|
743
|
+
const hasDesktopRegister = /name\s*:\s*['"]desktop-[^'"]+-register['"]/.test(
|
|
744
|
+
pluginSrc,
|
|
745
|
+
)
|
|
746
|
+
const hasLegacyRegister = /name\s*:\s*['"]owd-[^'"]+-register['"]/.test(
|
|
747
|
+
pluginSrc,
|
|
748
|
+
)
|
|
749
|
+
if (!hasDesktopRegister && !hasLegacyRegister) {
|
|
750
|
+
issue(
|
|
751
|
+
'error',
|
|
752
|
+
'plugin-name',
|
|
753
|
+
'plugin.ts must set name: "desktop-<slug>-register" for dependsOn ordering',
|
|
754
|
+
)
|
|
755
|
+
} else if (!hasDesktopRegister && hasLegacyRegister) {
|
|
756
|
+
issue(
|
|
757
|
+
'error',
|
|
758
|
+
'plugin-name-legacy',
|
|
759
|
+
'plugin.ts uses deprecated name "owd-<slug>-register"; rename to "desktop-<slug>-register"',
|
|
760
|
+
)
|
|
761
|
+
}
|
|
762
|
+
if (!pluginSrc.includes('import.meta.server')) {
|
|
763
|
+
issue(
|
|
764
|
+
'warning',
|
|
765
|
+
'plugin-server-guard',
|
|
766
|
+
'plugin.ts should guard with if (import.meta.server) return',
|
|
767
|
+
)
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const launchPlugins = existsSync(join(playgroundDir, 'app', 'plugins'))
|
|
772
|
+
? readdirSync(join(playgroundDir, 'app', 'plugins')).filter((f) =>
|
|
773
|
+
/^launch-.*\.client\.ts$/.test(f),
|
|
774
|
+
)
|
|
775
|
+
: []
|
|
776
|
+
if (launchPlugins.length === 0) {
|
|
777
|
+
issue(
|
|
778
|
+
'warning',
|
|
779
|
+
'launch-plugin',
|
|
780
|
+
'optional: add playground/app/plugins/launch-*.client.ts for dev auto-open',
|
|
781
|
+
)
|
|
782
|
+
} else {
|
|
783
|
+
for (const launchFile of launchPlugins) {
|
|
784
|
+
const launchPath = join(playgroundDir, 'app', 'plugins', launchFile)
|
|
785
|
+
const launchSrc = readFileSync(launchPath, 'utf8')
|
|
786
|
+
if (/if\s*\(\s*!?\s*import\.meta\.dev\s*\)/.test(launchSrc)) {
|
|
787
|
+
issue(
|
|
788
|
+
'warning',
|
|
789
|
+
'launch-dev-guard',
|
|
790
|
+
`${launchFile}: remove import.meta.dev guard — GitHub Pages static generate needs the same auto-start path`,
|
|
791
|
+
)
|
|
792
|
+
}
|
|
793
|
+
if (!launchSrc.includes('autoStartPlaygroundApps')) {
|
|
794
|
+
issue(
|
|
795
|
+
'warning',
|
|
796
|
+
'launch-auto-start',
|
|
797
|
+
`${launchFile}: prefer autoStartPlaygroundApps(nuxtApp, [...]) from @owdproject/core (handles workArea + stale windows)`,
|
|
798
|
+
)
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const pagesWorkflowPath = join(dir, '.github', 'workflows', 'pages.yml')
|
|
805
|
+
if (existsSync(pagesWorkflowPath)) {
|
|
806
|
+
const pagesWorkflow = readFileSync(pagesWorkflowPath, 'utf8')
|
|
807
|
+
if (/\bnypm\b/.test(pagesWorkflow)) {
|
|
808
|
+
issue(
|
|
809
|
+
'warning',
|
|
810
|
+
'pages-nypm',
|
|
811
|
+
'pages.yml uses nypm in the app repo alone — breaks workspace:*; use client checkout overlay (see PLAYGROUND.md)',
|
|
812
|
+
)
|
|
813
|
+
}
|
|
814
|
+
if (!existsSync(join(dir, '.github', 'workflows', 'ci.yml'))) {
|
|
815
|
+
issue(
|
|
816
|
+
'warning',
|
|
817
|
+
'ci-workflow',
|
|
818
|
+
'add .github/workflows/ci.yml with validate-and-build (dev:prepare → validate → dev:generate)',
|
|
819
|
+
)
|
|
820
|
+
}
|
|
821
|
+
} else {
|
|
822
|
+
issue(
|
|
823
|
+
'warning',
|
|
824
|
+
'pages-workflow',
|
|
825
|
+
'optional for standalone repos: .github/workflows/pages.yml',
|
|
826
|
+
)
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const ok =
|
|
830
|
+
errors.length === 0 && (!options.strict || warnings.length === 0)
|
|
831
|
+
|
|
832
|
+
return {
|
|
833
|
+
ok,
|
|
834
|
+
packageDir: dir,
|
|
835
|
+
pkgName,
|
|
836
|
+
kind,
|
|
837
|
+
errors,
|
|
838
|
+
warnings,
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* @param {ValidationResult} result
|
|
844
|
+
* @param {{ json?: boolean }} [options]
|
|
845
|
+
*/
|
|
846
|
+
export function formatValidationReport(result, options = {}) {
|
|
847
|
+
if (options.json) {
|
|
848
|
+
return JSON.stringify(result, null, 2)
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const lines = []
|
|
852
|
+
const status = result.ok ? 'PASS' : 'FAIL'
|
|
853
|
+
lines.push(`${status} ${result.pkgName || result.packageDir} (${result.kind})`)
|
|
854
|
+
lines.push(` ${result.packageDir}`)
|
|
855
|
+
for (const e of result.errors) {
|
|
856
|
+
lines.push(` ✗ [${e.code}] ${e.message}`)
|
|
857
|
+
}
|
|
858
|
+
for (const w of result.warnings) {
|
|
859
|
+
lines.push(` ⚠ [${w.code}] ${w.message}`)
|
|
860
|
+
}
|
|
861
|
+
if (result.ok && result.warnings.length === 0) {
|
|
862
|
+
lines.push(' All checks passed.')
|
|
863
|
+
}
|
|
864
|
+
return lines.join('\n')
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* @param {string} packageDir
|
|
869
|
+
* @param {{ smoke?: boolean }} [options]
|
|
870
|
+
*/
|
|
871
|
+
export function runSmokeBuild(packageDir, options = {}) {
|
|
872
|
+
if (!options.smoke) return { ok: true, message: '' }
|
|
873
|
+
const dir = resolve(packageDir)
|
|
874
|
+
try {
|
|
875
|
+
execSync('pnpm run dev:prepare', { cwd: dir, stdio: 'pipe', encoding: 'utf8' })
|
|
876
|
+
execSync('pnpm exec nuxt build playground', {
|
|
877
|
+
cwd: dir,
|
|
878
|
+
stdio: 'pipe',
|
|
879
|
+
encoding: 'utf8',
|
|
880
|
+
timeout: 300_000,
|
|
881
|
+
})
|
|
882
|
+
return { ok: true, message: 'smoke build succeeded' }
|
|
883
|
+
} catch (error) {
|
|
884
|
+
const msg =
|
|
885
|
+
error instanceof Error
|
|
886
|
+
? error.message
|
|
887
|
+
: String(error)
|
|
888
|
+
return { ok: false, message: `smoke build failed: ${msg}` }
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* @param {string[]} paths
|
|
894
|
+
* @param {{ workspaceRoot?: string | null, json?: boolean, strict?: boolean, smoke?: boolean }} [options]
|
|
895
|
+
* @returns {{ exitCode: number, results: ValidationResult[] }}
|
|
896
|
+
*/
|
|
897
|
+
export function runValidateCli(paths, options = {}) {
|
|
898
|
+
const workspaceRoot = options.workspaceRoot ?? findWorkspaceRoot() ?? null
|
|
899
|
+
/** @type {string[]} */
|
|
900
|
+
let targets = []
|
|
901
|
+
|
|
902
|
+
if (paths.length === 0) {
|
|
903
|
+
const fromCwd = findModulePackageRoot()
|
|
904
|
+
if (fromCwd) {
|
|
905
|
+
targets = [fromCwd]
|
|
906
|
+
} else if (workspaceRoot) {
|
|
907
|
+
targets = discoverOwdModulePackages(workspaceRoot)
|
|
908
|
+
}
|
|
909
|
+
} else {
|
|
910
|
+
for (const p of paths) {
|
|
911
|
+
const resolved = resolve(p)
|
|
912
|
+
if (existsSync(join(resolved, 'package.json'))) {
|
|
913
|
+
targets.push(resolved)
|
|
914
|
+
} else if (workspaceRoot && (p === 'apps' || p === 'themes' || p === 'packages')) {
|
|
915
|
+
const base = join(workspaceRoot, p)
|
|
916
|
+
if (existsSync(base)) {
|
|
917
|
+
targets.push(...discoverOwdModulePackages(workspaceRoot).filter((t) =>
|
|
918
|
+
t.startsWith(base),
|
|
919
|
+
))
|
|
920
|
+
}
|
|
921
|
+
} else {
|
|
922
|
+
targets.push(resolved)
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (targets.length === 0) {
|
|
928
|
+
return {
|
|
929
|
+
exitCode: 1,
|
|
930
|
+
results: [
|
|
931
|
+
{
|
|
932
|
+
ok: false,
|
|
933
|
+
packageDir: process.cwd(),
|
|
934
|
+
pkgName: '',
|
|
935
|
+
kind: 'module',
|
|
936
|
+
errors: [
|
|
937
|
+
{
|
|
938
|
+
level: 'error',
|
|
939
|
+
code: 'no-target',
|
|
940
|
+
message:
|
|
941
|
+
'No package to validate. Run from an @owdproject/* package or pass a path.',
|
|
942
|
+
},
|
|
943
|
+
],
|
|
944
|
+
warnings: [],
|
|
945
|
+
},
|
|
946
|
+
],
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/** @type {ValidationResult[]} */
|
|
951
|
+
const results = []
|
|
952
|
+
let exitCode = 0
|
|
953
|
+
|
|
954
|
+
for (const dir of [...new Set(targets)]) {
|
|
955
|
+
const result = validateOwdModule(dir, {
|
|
956
|
+
workspaceRoot,
|
|
957
|
+
requireDist: true,
|
|
958
|
+
strict: options.strict,
|
|
959
|
+
})
|
|
960
|
+
|
|
961
|
+
if (options.smoke && result.pkgName && !isDesktopCorePackage(result.pkgName)) {
|
|
962
|
+
const smoke = runSmokeBuild(dir, { smoke: true })
|
|
963
|
+
if (!smoke.ok) {
|
|
964
|
+
result.errors.push({
|
|
965
|
+
level: 'error',
|
|
966
|
+
code: 'smoke-build',
|
|
967
|
+
message: smoke.message,
|
|
968
|
+
})
|
|
969
|
+
result.ok = false
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
results.push(result)
|
|
974
|
+
if (!result.ok) exitCode = 1
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
return { exitCode, results }
|
|
978
|
+
}
|