@owdproject/cli 0.0.3 → 0.1.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 +127 -0
- package/bin/cli.js +433 -0
- package/bin/cp.js +3523 -0
- package/bin/desktop.js +5 -0
- package/bin/lib/catalog.js +501 -0
- package/bin/lib/config.js +130 -0
- package/bin/lib/cpAscii.js +39 -0
- package/bin/lib/cpFormat.js +343 -0
- package/bin/lib/desktopConfig.js +8 -0
- package/bin/lib/generateTemplate.js +426 -0
- package/bin/lib/install.js +525 -0
- package/bin/lib/installWizardFlow.js +726 -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 +365 -0
- package/bin/lib/playgroundContext.js +188 -0
- package/bin/lib/scaffold.js +205 -0
- package/bin/lib/status.js +317 -0
- package/bin/lib/validateModule.js +868 -0
- package/bin/lib/workspace.js +222 -0
- package/bin/owd.js +2 -118
- package/go/bridge/bridge.go +200 -0
- package/go/desktop-go +0 -0
- package/go/go.mod +26 -0
- package/go/go.sum +37 -0
- package/go/main.go +30 -0
- package/go/tui/tui.go +3014 -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,868 @@
|
|
|
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 (!rootExport?.development?.includes('src/module')) {
|
|
411
|
+
issue(
|
|
412
|
+
'warning',
|
|
413
|
+
'exports-development',
|
|
414
|
+
'exports["."].development should point to ./src/module.ts (monorepo dev without manual stub after every edit)',
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
if (!scripts['dev:generate']?.includes('NUXT_APP_BASE_URL')) {
|
|
418
|
+
issue(
|
|
419
|
+
'warning',
|
|
420
|
+
'dev-generate-base-url',
|
|
421
|
+
'dev:generate should set NUXT_APP_BASE_URL=/<repo-slug>/ for GitHub Pages',
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const devDeps = /** @type {Record<string, string>} */ (pkg.devDependencies ?? {})
|
|
426
|
+
if (!devDeps['@nuxt/module-builder']) {
|
|
427
|
+
issue(
|
|
428
|
+
'error',
|
|
429
|
+
'module-builder-dep',
|
|
430
|
+
'devDependencies must include @nuxt/module-builder',
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const peerDeps = /** @type {Record<string, string>} */ (pkg.peerDependencies ?? {})
|
|
435
|
+
if (!peerDeps['@owdproject/core']) {
|
|
436
|
+
issue(
|
|
437
|
+
'warning',
|
|
438
|
+
'peer-core',
|
|
439
|
+
'peerDependencies should declare @owdproject/core',
|
|
440
|
+
)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (existsSync(join(dir, 'module.ts'))) {
|
|
444
|
+
issue(
|
|
445
|
+
'error',
|
|
446
|
+
'legacy-module-root',
|
|
447
|
+
'legacy layout: move module.ts to src/module.ts',
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
if (existsSync(join(dir, 'runtime')) && !existsSync(join(dir, 'src', 'runtime'))) {
|
|
451
|
+
issue(
|
|
452
|
+
'error',
|
|
453
|
+
'legacy-runtime-root',
|
|
454
|
+
'legacy layout: move runtime/ under src/runtime/',
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const moduleTs = join(dir, 'src', 'module.ts')
|
|
459
|
+
if (!existsSync(moduleTs)) {
|
|
460
|
+
issue('error', 'src-module', 'src/module.ts is required')
|
|
461
|
+
} else {
|
|
462
|
+
const moduleSrc = readFileSync(moduleTs, 'utf8')
|
|
463
|
+
const hasModuleWrapper =
|
|
464
|
+
moduleSrc.includes('defineNuxtModule') ||
|
|
465
|
+
(kind === 'module' && moduleSrc.includes('defineDesktopModule')) ||
|
|
466
|
+
(kind === 'theme' && moduleSrc.includes('defineDesktopTheme'))
|
|
467
|
+
if (!hasModuleWrapper) {
|
|
468
|
+
const expected =
|
|
469
|
+
kind === 'module'
|
|
470
|
+
? 'defineDesktopModule (or defineNuxtModule)'
|
|
471
|
+
: kind === 'theme'
|
|
472
|
+
? 'defineDesktopTheme (or defineNuxtModule)'
|
|
473
|
+
: 'defineNuxtModule'
|
|
474
|
+
issue(
|
|
475
|
+
'error',
|
|
476
|
+
'define-nuxt-module',
|
|
477
|
+
`src/module.ts must use ${expected}`,
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
const hasKitTailwind =
|
|
481
|
+
moduleSrc.includes('registerTailwindPath') ||
|
|
482
|
+
moduleSrc.includes('registerThemeTailwindPath')
|
|
483
|
+
const hasKitPrimevue =
|
|
484
|
+
moduleSrc.includes("installModule('@owdproject/kit-primevue'") ||
|
|
485
|
+
moduleSrc.includes('installModule("@owdproject/kit-primevue"')
|
|
486
|
+
if (kind === 'theme') {
|
|
487
|
+
if (!hasKitPrimevue) {
|
|
488
|
+
issue(
|
|
489
|
+
'warning',
|
|
490
|
+
'kit-primevue',
|
|
491
|
+
'theme src/module.ts should install @owdproject/kit-primevue for PrimeVue + Tailwind',
|
|
492
|
+
)
|
|
493
|
+
}
|
|
494
|
+
if (!hasKitTailwind) {
|
|
495
|
+
issue(
|
|
496
|
+
'warning',
|
|
497
|
+
'tailwind-path',
|
|
498
|
+
'theme src/module.ts should call registerThemeTailwindPath from @owdproject/kit-primevue/kit/registerTailwindPath',
|
|
499
|
+
)
|
|
500
|
+
}
|
|
501
|
+
} else if (kind === 'app' && !hasKitTailwind) {
|
|
502
|
+
issue(
|
|
503
|
+
'warning',
|
|
504
|
+
'tailwind-path',
|
|
505
|
+
'src/module.ts should call registerTailwindPath from @owdproject/kit-primevue/kit/registerTailwindPath for Vue components',
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const legacyMetaName = findLegacyModuleMetaName(moduleSrc)
|
|
510
|
+
if (legacyMetaName) {
|
|
511
|
+
issue(
|
|
512
|
+
'warning',
|
|
513
|
+
'module-meta-name-legacy',
|
|
514
|
+
`src/module.ts meta.name is "${legacyMetaName}"; migrate to desktop-<kind>-<slug> (see MIGRATION_3.4.md)`,
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (!isInfrastructure) {
|
|
520
|
+
for (const finding of scanLegacyPiniaStoreIds(dir)) {
|
|
521
|
+
issue(
|
|
522
|
+
'warning',
|
|
523
|
+
'pinia-store-id-legacy',
|
|
524
|
+
`${finding.file}: Pinia store id still uses legacy "owd/" (${finding.snippets.join('; ')}); migrate to "desktop/" ids`,
|
|
525
|
+
)
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const distModule = join(dir, 'dist', 'module.mjs')
|
|
530
|
+
if (!existsSync(distModule)) {
|
|
531
|
+
issue(
|
|
532
|
+
options.requireDist ? 'error' : 'error',
|
|
533
|
+
'dist-module',
|
|
534
|
+
'dist/module.mjs missing — run: pnpm run dev:prepare',
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const playgroundDir = join(dir, 'playground')
|
|
539
|
+
if (!existsSync(playgroundDir)) {
|
|
540
|
+
issue('error', 'playground-dir', 'playground/ directory is required')
|
|
541
|
+
} else {
|
|
542
|
+
const pgPkgPath = join(playgroundDir, 'package.json')
|
|
543
|
+
if (!existsSync(pgPkgPath)) {
|
|
544
|
+
issue('error', 'playground-package-json', 'playground/package.json is required')
|
|
545
|
+
} else {
|
|
546
|
+
try {
|
|
547
|
+
const pgPkg = JSON.parse(readFileSync(pgPkgPath, 'utf8'))
|
|
548
|
+
const deps = {
|
|
549
|
+
.../** @type {Record<string, string>} */ (pgPkg.dependencies ?? {}),
|
|
550
|
+
.../** @type {Record<string, string>} */ (pgPkg.devDependencies ?? {}),
|
|
551
|
+
}
|
|
552
|
+
if (!deps[pkgName] && !deps[pkgName.replace(SCOPE, '')]) {
|
|
553
|
+
issue(
|
|
554
|
+
'error',
|
|
555
|
+
'playground-self-dep',
|
|
556
|
+
`playground/package.json must depend on ${pkgName}`,
|
|
557
|
+
)
|
|
558
|
+
}
|
|
559
|
+
if (!deps['@owdproject/core']) {
|
|
560
|
+
issue(
|
|
561
|
+
'error',
|
|
562
|
+
'playground-core-dep',
|
|
563
|
+
'playground/package.json must depend on @owdproject/core',
|
|
564
|
+
)
|
|
565
|
+
}
|
|
566
|
+
if (kind === 'app' && !deps['@owdproject/theme-nova']) {
|
|
567
|
+
issue(
|
|
568
|
+
'warning',
|
|
569
|
+
'playground-theme-nova',
|
|
570
|
+
'app playgrounds should use @owdproject/theme-nova in dependencies',
|
|
571
|
+
)
|
|
572
|
+
}
|
|
573
|
+
} catch {
|
|
574
|
+
issue('error', 'playground-package-json', 'playground/package.json is invalid JSON')
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const nuxtConfigPath = join(playgroundDir, 'nuxt.config.ts')
|
|
579
|
+
if (!existsSync(nuxtConfigPath)) {
|
|
580
|
+
issue('error', 'playground-nuxt-config', 'playground/nuxt.config.ts is required')
|
|
581
|
+
} else {
|
|
582
|
+
const nuxtCfg = readFileSync(nuxtConfigPath, 'utf8')
|
|
583
|
+
if (!nuxtCfg.includes('@owdproject/core')) {
|
|
584
|
+
issue(
|
|
585
|
+
'error',
|
|
586
|
+
'playground-nuxt-core',
|
|
587
|
+
'playground/nuxt.config.ts must register @owdproject/core',
|
|
588
|
+
)
|
|
589
|
+
}
|
|
590
|
+
if (!/\bssr\s*:\s*false\b/.test(nuxtCfg)) {
|
|
591
|
+
issue('error', 'playground-ssr-false', 'playground/nuxt.config.ts must set ssr: false')
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const desktopConfigPath = join(playgroundDir, 'desktop.config.ts')
|
|
596
|
+
if (!existsSync(desktopConfigPath)) {
|
|
597
|
+
issue('error', 'playground-desktop-config', 'playground/desktop.config.ts is required')
|
|
598
|
+
} else if (pkgName) {
|
|
599
|
+
const desktopCfg = readFileSync(desktopConfigPath, 'utf8')
|
|
600
|
+
if (!desktopConfigReferencesPackage(desktopCfg, pkgName, kind)) {
|
|
601
|
+
const field = kind === 'app' ? 'apps' : kind === 'theme' ? 'theme' : 'modules'
|
|
602
|
+
issue(
|
|
603
|
+
'error',
|
|
604
|
+
'playground-desktop-ref',
|
|
605
|
+
`playground/desktop.config.ts must reference ${pkgName} in \`${field}\``,
|
|
606
|
+
)
|
|
607
|
+
}
|
|
608
|
+
if (!desktopCfg.includes('defineDesktopConfig')) {
|
|
609
|
+
issue(
|
|
610
|
+
'error',
|
|
611
|
+
'playground-define-config',
|
|
612
|
+
'playground/desktop.config.ts must use defineDesktopConfig',
|
|
613
|
+
)
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const appVuePath = join(playgroundDir, 'app', 'app.vue')
|
|
618
|
+
if (!existsSync(appVuePath)) {
|
|
619
|
+
issue('error', 'playground-app-vue', 'playground/app/app.vue is required')
|
|
620
|
+
} else {
|
|
621
|
+
const appVue = readFileSync(appVuePath, 'utf8')
|
|
622
|
+
if (!/<Desktop\s*\/?>/.test(appVue) && !/<Desktop[\s>]/.test(appVue)) {
|
|
623
|
+
issue(
|
|
624
|
+
'error',
|
|
625
|
+
'playground-desktop-component',
|
|
626
|
+
'playground/app/app.vue must render <Desktop />',
|
|
627
|
+
)
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const workspaceRoot = options.workspaceRoot ?? findWorkspaceRoot(dir)
|
|
632
|
+
const relPkg =
|
|
633
|
+
workspaceRoot != null ? relative(workspaceRoot, dir).replace(/\\/g, '/') : ''
|
|
634
|
+
const isMonorepoModule =
|
|
635
|
+
workspaceRoot != null &&
|
|
636
|
+
/^(apps|themes|packages)\/[^/]+$/.test(relPkg) &&
|
|
637
|
+
options.checkWorkspace !== false
|
|
638
|
+
|
|
639
|
+
if (isMonorepoModule) {
|
|
640
|
+
const wsPath = join(workspaceRoot, 'pnpm-workspace.yaml')
|
|
641
|
+
if (existsSync(wsPath)) {
|
|
642
|
+
const relPlayground = relative(workspaceRoot, playgroundDir)
|
|
643
|
+
const wsYaml = readFileSync(wsPath, 'utf8')
|
|
644
|
+
if (!workspaceListsPlayground(wsYaml, relPlayground)) {
|
|
645
|
+
issue(
|
|
646
|
+
'error',
|
|
647
|
+
'workspace-playground',
|
|
648
|
+
`add "${relPlayground}" to pnpm-workspace.yaml packages`,
|
|
649
|
+
)
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (kind === 'app') {
|
|
656
|
+
const appConfigPath = join(dir, 'src', 'runtime', 'app.config.ts')
|
|
657
|
+
if (!existsSync(appConfigPath)) {
|
|
658
|
+
issue('error', 'app-config', 'src/runtime/app.config.ts is required for apps')
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const pluginPath = join(dir, 'src', 'runtime', 'plugin.ts')
|
|
662
|
+
if (!existsSync(pluginPath)) {
|
|
663
|
+
issue('error', 'app-plugin', 'src/runtime/plugin.ts is required for apps')
|
|
664
|
+
} else {
|
|
665
|
+
const pluginSrc = readFileSync(pluginPath, 'utf8')
|
|
666
|
+
if (!pluginSrc.includes('defineDesktopApp')) {
|
|
667
|
+
issue('error', 'define-desktop-app', 'plugin.ts must call defineDesktopApp')
|
|
668
|
+
}
|
|
669
|
+
const hasDesktopRegister = /name\s*:\s*['"]desktop-[^'"]+-register['"]/.test(
|
|
670
|
+
pluginSrc,
|
|
671
|
+
)
|
|
672
|
+
const hasLegacyRegister = /name\s*:\s*['"]owd-[^'"]+-register['"]/.test(
|
|
673
|
+
pluginSrc,
|
|
674
|
+
)
|
|
675
|
+
if (!hasDesktopRegister && !hasLegacyRegister) {
|
|
676
|
+
issue(
|
|
677
|
+
'error',
|
|
678
|
+
'plugin-name',
|
|
679
|
+
'plugin.ts must set name: "desktop-<slug>-register" for dependsOn ordering',
|
|
680
|
+
)
|
|
681
|
+
} else if (!hasDesktopRegister && hasLegacyRegister) {
|
|
682
|
+
issue(
|
|
683
|
+
'error',
|
|
684
|
+
'plugin-name-legacy',
|
|
685
|
+
'plugin.ts uses deprecated name "owd-<slug>-register"; rename to "desktop-<slug>-register"',
|
|
686
|
+
)
|
|
687
|
+
}
|
|
688
|
+
if (!pluginSrc.includes('import.meta.server')) {
|
|
689
|
+
issue(
|
|
690
|
+
'warning',
|
|
691
|
+
'plugin-server-guard',
|
|
692
|
+
'plugin.ts should guard with if (import.meta.server) return',
|
|
693
|
+
)
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const launchPlugins = existsSync(join(playgroundDir, 'app', 'plugins'))
|
|
698
|
+
? readdirSync(join(playgroundDir, 'app', 'plugins')).filter((f) =>
|
|
699
|
+
/^launch-.*\.client\.ts$/.test(f),
|
|
700
|
+
)
|
|
701
|
+
: []
|
|
702
|
+
if (launchPlugins.length === 0) {
|
|
703
|
+
issue(
|
|
704
|
+
'warning',
|
|
705
|
+
'launch-plugin',
|
|
706
|
+
'optional: add playground/app/plugins/launch-*.client.ts for dev auto-open',
|
|
707
|
+
)
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (!existsSync(join(dir, '.github', 'workflows', 'pages.yml'))) {
|
|
712
|
+
issue(
|
|
713
|
+
'warning',
|
|
714
|
+
'pages-workflow',
|
|
715
|
+
'optional for standalone repos: .github/workflows/pages.yml',
|
|
716
|
+
)
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const ok =
|
|
720
|
+
errors.length === 0 && (!options.strict || warnings.length === 0)
|
|
721
|
+
|
|
722
|
+
return {
|
|
723
|
+
ok,
|
|
724
|
+
packageDir: dir,
|
|
725
|
+
pkgName,
|
|
726
|
+
kind,
|
|
727
|
+
errors,
|
|
728
|
+
warnings,
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* @param {ValidationResult} result
|
|
734
|
+
* @param {{ json?: boolean }} [options]
|
|
735
|
+
*/
|
|
736
|
+
export function formatValidationReport(result, options = {}) {
|
|
737
|
+
if (options.json) {
|
|
738
|
+
return JSON.stringify(result, null, 2)
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const lines = []
|
|
742
|
+
const status = result.ok ? 'PASS' : 'FAIL'
|
|
743
|
+
lines.push(`${status} ${result.pkgName || result.packageDir} (${result.kind})`)
|
|
744
|
+
lines.push(` ${result.packageDir}`)
|
|
745
|
+
for (const e of result.errors) {
|
|
746
|
+
lines.push(` ✗ [${e.code}] ${e.message}`)
|
|
747
|
+
}
|
|
748
|
+
for (const w of result.warnings) {
|
|
749
|
+
lines.push(` ⚠ [${w.code}] ${w.message}`)
|
|
750
|
+
}
|
|
751
|
+
if (result.ok && result.warnings.length === 0) {
|
|
752
|
+
lines.push(' All checks passed.')
|
|
753
|
+
}
|
|
754
|
+
return lines.join('\n')
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* @param {string} packageDir
|
|
759
|
+
* @param {{ smoke?: boolean }} [options]
|
|
760
|
+
*/
|
|
761
|
+
export function runSmokeBuild(packageDir, options = {}) {
|
|
762
|
+
if (!options.smoke) return { ok: true, message: '' }
|
|
763
|
+
const dir = resolve(packageDir)
|
|
764
|
+
try {
|
|
765
|
+
execSync('pnpm run dev:prepare', { cwd: dir, stdio: 'pipe', encoding: 'utf8' })
|
|
766
|
+
execSync('pnpm exec nuxt build playground', {
|
|
767
|
+
cwd: dir,
|
|
768
|
+
stdio: 'pipe',
|
|
769
|
+
encoding: 'utf8',
|
|
770
|
+
timeout: 300_000,
|
|
771
|
+
})
|
|
772
|
+
return { ok: true, message: 'smoke build succeeded' }
|
|
773
|
+
} catch (error) {
|
|
774
|
+
const msg =
|
|
775
|
+
error instanceof Error
|
|
776
|
+
? error.message
|
|
777
|
+
: String(error)
|
|
778
|
+
return { ok: false, message: `smoke build failed: ${msg}` }
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* @param {string[]} paths
|
|
784
|
+
* @param {{ workspaceRoot?: string | null, json?: boolean, strict?: boolean, smoke?: boolean }} [options]
|
|
785
|
+
* @returns {{ exitCode: number, results: ValidationResult[] }}
|
|
786
|
+
*/
|
|
787
|
+
export function runValidateCli(paths, options = {}) {
|
|
788
|
+
const workspaceRoot = options.workspaceRoot ?? findWorkspaceRoot() ?? null
|
|
789
|
+
/** @type {string[]} */
|
|
790
|
+
let targets = []
|
|
791
|
+
|
|
792
|
+
if (paths.length === 0) {
|
|
793
|
+
const fromCwd = findModulePackageRoot()
|
|
794
|
+
if (fromCwd) {
|
|
795
|
+
targets = [fromCwd]
|
|
796
|
+
} else if (workspaceRoot) {
|
|
797
|
+
targets = discoverOwdModulePackages(workspaceRoot)
|
|
798
|
+
}
|
|
799
|
+
} else {
|
|
800
|
+
for (const p of paths) {
|
|
801
|
+
const resolved = resolve(p)
|
|
802
|
+
if (existsSync(join(resolved, 'package.json'))) {
|
|
803
|
+
targets.push(resolved)
|
|
804
|
+
} else if (workspaceRoot && (p === 'apps' || p === 'themes' || p === 'packages')) {
|
|
805
|
+
const base = join(workspaceRoot, p)
|
|
806
|
+
if (existsSync(base)) {
|
|
807
|
+
targets.push(...discoverOwdModulePackages(workspaceRoot).filter((t) =>
|
|
808
|
+
t.startsWith(base),
|
|
809
|
+
))
|
|
810
|
+
}
|
|
811
|
+
} else {
|
|
812
|
+
targets.push(resolved)
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (targets.length === 0) {
|
|
818
|
+
return {
|
|
819
|
+
exitCode: 1,
|
|
820
|
+
results: [
|
|
821
|
+
{
|
|
822
|
+
ok: false,
|
|
823
|
+
packageDir: process.cwd(),
|
|
824
|
+
pkgName: '',
|
|
825
|
+
kind: 'module',
|
|
826
|
+
errors: [
|
|
827
|
+
{
|
|
828
|
+
level: 'error',
|
|
829
|
+
code: 'no-target',
|
|
830
|
+
message:
|
|
831
|
+
'No package to validate. Run from an @owdproject/* package or pass a path.',
|
|
832
|
+
},
|
|
833
|
+
],
|
|
834
|
+
warnings: [],
|
|
835
|
+
},
|
|
836
|
+
],
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/** @type {ValidationResult[]} */
|
|
841
|
+
const results = []
|
|
842
|
+
let exitCode = 0
|
|
843
|
+
|
|
844
|
+
for (const dir of [...new Set(targets)]) {
|
|
845
|
+
const result = validateOwdModule(dir, {
|
|
846
|
+
workspaceRoot,
|
|
847
|
+
requireDist: true,
|
|
848
|
+
strict: options.strict,
|
|
849
|
+
})
|
|
850
|
+
|
|
851
|
+
if (options.smoke && result.pkgName && !isDesktopCorePackage(result.pkgName)) {
|
|
852
|
+
const smoke = runSmokeBuild(dir, { smoke: true })
|
|
853
|
+
if (!smoke.ok) {
|
|
854
|
+
result.errors.push({
|
|
855
|
+
level: 'error',
|
|
856
|
+
code: 'smoke-build',
|
|
857
|
+
message: smoke.message,
|
|
858
|
+
})
|
|
859
|
+
result.ok = false
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
results.push(result)
|
|
864
|
+
if (!result.ok) exitCode = 1
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
return { exitCode, results }
|
|
868
|
+
}
|