@owdproject/core 3.1.4 → 3.3.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/README.md +6 -6
- package/bin/cli.js +395 -0
- package/bin/desktop.js +5 -0
- package/bin/lib/catalog.js +293 -0
- package/bin/lib/config.js +109 -0
- package/bin/lib/desktopConfig.js +56 -0
- package/bin/lib/generateTemplate.js +389 -0
- package/bin/lib/install.js +408 -0
- package/bin/lib/migrateModulePlayground.config.mjs +164 -0
- package/bin/lib/migrateModulePlayground.mjs +313 -0
- package/bin/lib/npmVersions.js +124 -0
- package/bin/lib/playgroundContext.js +188 -0
- package/bin/lib/scaffold.js +197 -0
- package/bin/lib/status.js +289 -0
- package/bin/lib/validateModule.js +697 -0
- package/bin/lib/workspace.js +182 -0
- package/bin/owd.js +2 -25
- package/bin/tui.js +1379 -0
- package/dist/module.d.mts +13 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +267 -0
- package/dist/types.d.mts +7 -0
- package/module.ts +69 -32
- package/package.json +81 -22
- package/runtime/components/{Core/Application/CoreApplicationRender.vue → Desktop/Application/DesktopApplicationRender.vue} +4 -2
- package/runtime/components/Desktop/Application/DesktopApplicationWindowsRender.vue +38 -0
- package/runtime/components/{Core/Window/CoreWindow.vue → Desktop/Window/DesktopWindow.vue} +42 -8
- package/runtime/components/{Core/Window/CoreWindowNav.vue → Desktop/Window/DesktopWindowNav.vue} +26 -8
- package/runtime/composables/useApplicationManager.ts +19 -14
- package/runtime/composables/useDesktopManager.ts +33 -9
- package/runtime/composables/useWorkspaceManager.ts +41 -0
- package/runtime/{core → internal}/controllers/ApplicationController.ts +114 -24
- package/runtime/{core → internal}/controllers/WindowController.ts +4 -9
- package/runtime/plugins/02.desktop-register-desktop-apps.client.ts +17 -0
- package/runtime/stores/storeApplicationMeta.ts +56 -12
- package/runtime/stores/storeDesktop.ts +68 -2
- package/runtime/stores/storeDesktopDefaultApps.ts +2 -2
- package/runtime/stores/storeDesktopWorkspace.ts +14 -2
- package/runtime/utils/captureElementToCanvas.ts +21 -0
- package/runtime/utils/explorerEntryPath.ts +10 -0
- package/runtime/utils/resolveDesktopConfigPath.ts +52 -0
- package/runtime/utils/splitDesktopUserConfig.ts +85 -0
- package/runtime/utils/utilCommon.ts +0 -32
- package/runtime/utils/utilDesktop.ts +27 -2
- package/runtime/utils/validateDesktopUserConfig.ts +44 -0
- package/types/index.d.ts +45 -12
- package/CHANGELOG.md +0 -25
- package/project.json +0 -4
- package/runtime/components/Core/Application/CoreApplicationWindowsRender.vue +0 -19
- package/runtime/components/Core/Explorer/CoreExplorerFile.vue +0 -76
- package/runtime/components/Core/Explorer/CoreExplorerFolder.vue +0 -10
- package/runtime/stores/storeApplication.ts +0 -236
- package/test/basic.test.ts +0 -15
- package/test/fixtures/basic/app.vue +0 -5
- package/test/fixtures/basic/nuxt.config.ts +0 -5
- package/test/fixtures/basic/package.json +0 -5
- package/tsconfig.app.json +0 -31
- package/tsconfig.json +0 -12
- package/tsconfig.spec.json +0 -37
- package/vitest.config.ts +0 -24
- /package/runtime/components/{Core/Background/CoreBackground.vue → Desktop/Background/DesktopBackground.vue} +0 -0
- /package/runtime/components/{Core/Desktop/CoreDesktop.vue → Desktop/Core/DesktopCore.vue} +0 -0
- /package/runtime/components/{Core/Time/CoreTime.vue → Desktop/Time/DesktopTime.vue} +0 -0
- /package/runtime/components/{Core/Window/CoreWindowContent.vue → Desktop/Window/DesktopWindowContent.vue} +0 -0
|
@@ -0,0 +1,697 @@
|
|
|
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
|
+
|
|
10
|
+
const REQUIRED_SCRIPTS = ['dev', 'dev:prepare', 'dev:generate', 'prepack']
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} pkgName
|
|
14
|
+
*/
|
|
15
|
+
export function isDesktopCorePackage(pkgName) {
|
|
16
|
+
return pkgName === DESKTOP_CORE_PACKAGE
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} pkgName
|
|
21
|
+
* @returns {'app' | 'theme' | 'module'}
|
|
22
|
+
*/
|
|
23
|
+
export function inferModuleKind(pkgName) {
|
|
24
|
+
const short = pkgName.startsWith(SCOPE) ? pkgName.slice(SCOPE.length) : pkgName
|
|
25
|
+
if (short.startsWith('app-')) return 'app'
|
|
26
|
+
if (short.startsWith('theme-')) return 'theme'
|
|
27
|
+
return 'module'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Walk up from cwd until a package.json with @owdproject/ name is found.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} [startDir]
|
|
34
|
+
* @returns {string | null}
|
|
35
|
+
*/
|
|
36
|
+
export function findModulePackageRoot(startDir = process.cwd()) {
|
|
37
|
+
let dir = resolve(startDir)
|
|
38
|
+
for (;;) {
|
|
39
|
+
const pkgPath = join(dir, 'package.json')
|
|
40
|
+
if (existsSync(pkgPath) && existsSync(join(dir, 'src', 'module.ts'))) {
|
|
41
|
+
try {
|
|
42
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
43
|
+
if (typeof pkg.name === 'string' && pkg.name.startsWith(SCOPE)) {
|
|
44
|
+
return dir
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
/* ignore */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const parent = dirname(dir)
|
|
51
|
+
if (parent === dir) return null
|
|
52
|
+
dir = parent
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} workspaceRoot
|
|
58
|
+
* @returns {string[]}
|
|
59
|
+
*/
|
|
60
|
+
export function discoverOwdModulePackages(workspaceRoot) {
|
|
61
|
+
const roots = []
|
|
62
|
+
const scanDirs = [
|
|
63
|
+
join(workspaceRoot, 'apps'),
|
|
64
|
+
join(workspaceRoot, 'themes'),
|
|
65
|
+
join(workspaceRoot, 'packages'),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
for (const base of scanDirs) {
|
|
69
|
+
if (!existsSync(base)) continue
|
|
70
|
+
for (const entry of readdirSync(base)) {
|
|
71
|
+
const dir = join(base, entry)
|
|
72
|
+
try {
|
|
73
|
+
if (!statSync(dir).isDirectory()) continue
|
|
74
|
+
} catch {
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
const pkgPath = join(dir, 'package.json')
|
|
78
|
+
if (!existsSync(pkgPath)) continue
|
|
79
|
+
try {
|
|
80
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
81
|
+
if (!pkg.name?.startsWith(SCOPE)) continue
|
|
82
|
+
if (isDesktopCorePackage(pkg.name)) continue
|
|
83
|
+
if (!existsSync(join(dir, 'src', 'module.ts'))) continue
|
|
84
|
+
roots.push(dir)
|
|
85
|
+
} catch {
|
|
86
|
+
/* ignore */
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return roots.sort()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {string} content
|
|
96
|
+
* @param {string} pkgName
|
|
97
|
+
*/
|
|
98
|
+
function desktopConfigReferencesPackage(content, pkgName, kind) {
|
|
99
|
+
if (kind === 'app') {
|
|
100
|
+
return (
|
|
101
|
+
new RegExp(`apps\\s*:\\s*\\[[^\\]]*['"]${escapeRegExp(pkgName)}['"]`, 's').test(
|
|
102
|
+
content,
|
|
103
|
+
) || new RegExp(`apps\\s*:\\s*\\[[^\\]]*${escapeRegExp(pkgName)}`, 's').test(content)
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
if (kind === 'theme') {
|
|
107
|
+
return (
|
|
108
|
+
new RegExp(`theme\\s*:\\s*['"]${escapeRegExp(pkgName)}['"]`).test(content) ||
|
|
109
|
+
new RegExp(`theme\\s*:\\s*${escapeRegExp(pkgName)}`).test(content)
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
return (
|
|
113
|
+
new RegExp(`modules\\s*:\\s*\\[[^\\]]*['"]${escapeRegExp(pkgName)}['"]`, 's').test(
|
|
114
|
+
content,
|
|
115
|
+
) || new RegExp(`modules\\s*:\\s*\\[[^\\]]*${escapeRegExp(pkgName)}`, 's').test(content)
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param {string} s
|
|
121
|
+
*/
|
|
122
|
+
function escapeRegExp(s) {
|
|
123
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {string} workspaceYaml
|
|
128
|
+
* @param {string} playgroundRel
|
|
129
|
+
*/
|
|
130
|
+
function workspaceListsPlayground(workspaceYaml, playgroundRel) {
|
|
131
|
+
const normalized = playgroundRel.replace(/\\/g, '/')
|
|
132
|
+
const lines = workspaceYaml.split('\n')
|
|
133
|
+
for (const line of lines) {
|
|
134
|
+
const trimmed = line.trim().replace(/^-\s*/, '')
|
|
135
|
+
if (!trimmed) continue
|
|
136
|
+
if (trimmed === normalized) return true
|
|
137
|
+
if (trimmed.endsWith('/*')) {
|
|
138
|
+
const prefix = trimmed.slice(0, -2)
|
|
139
|
+
if (normalized.startsWith(`${prefix}/`)) return true
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return false
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @typedef {object} ValidationIssue
|
|
147
|
+
* @property {'error' | 'warning'} level
|
|
148
|
+
* @property {string} code
|
|
149
|
+
* @property {string} message
|
|
150
|
+
*/
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @typedef {object} ValidationResult
|
|
154
|
+
* @property {boolean} ok
|
|
155
|
+
* @property {string} packageDir
|
|
156
|
+
* @property {string} pkgName
|
|
157
|
+
* @property {'app' | 'theme' | 'module'} kind
|
|
158
|
+
* @property {ValidationIssue[]} errors
|
|
159
|
+
* @property {ValidationIssue[]} warnings
|
|
160
|
+
*/
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {string} packageDir
|
|
164
|
+
* @param {{ workspaceRoot?: string | null, requireDist?: boolean, strict?: boolean }} [options]
|
|
165
|
+
* @returns {ValidationResult}
|
|
166
|
+
*/
|
|
167
|
+
export function validateOwdModule(packageDir, options = {}) {
|
|
168
|
+
const dir = resolve(packageDir)
|
|
169
|
+
const pkgPath = join(dir, 'package.json')
|
|
170
|
+
/** @type {ValidationIssue[]} */
|
|
171
|
+
const errors = []
|
|
172
|
+
/** @type {ValidationIssue[]} */
|
|
173
|
+
const warnings = []
|
|
174
|
+
|
|
175
|
+
const issue = (level, code, message) => {
|
|
176
|
+
;(level === 'error' ? errors : warnings).push({ level, code, message })
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!existsSync(pkgPath)) {
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
packageDir: dir,
|
|
183
|
+
pkgName: '',
|
|
184
|
+
kind: 'module',
|
|
185
|
+
errors: [{ level: 'error', code: 'missing-package-json', message: 'package.json not found' }],
|
|
186
|
+
warnings: [],
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** @type {Record<string, unknown>} */
|
|
191
|
+
let pkg
|
|
192
|
+
try {
|
|
193
|
+
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
194
|
+
} catch {
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
packageDir: dir,
|
|
198
|
+
pkgName: '',
|
|
199
|
+
kind: 'module',
|
|
200
|
+
errors: [
|
|
201
|
+
{
|
|
202
|
+
level: 'error',
|
|
203
|
+
code: 'invalid-package-json',
|
|
204
|
+
message: 'package.json is not valid JSON',
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
warnings: [],
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const pkgName = typeof pkg.name === 'string' ? pkg.name : ''
|
|
212
|
+
if (!pkgName.startsWith(SCOPE)) {
|
|
213
|
+
issue('error', 'invalid-scope', `package name must start with ${SCOPE}`)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const kind = inferModuleKind(pkgName)
|
|
217
|
+
const isCore = isDesktopCorePackage(pkgName)
|
|
218
|
+
|
|
219
|
+
if (pkg.type !== 'module') {
|
|
220
|
+
issue('error', 'type-module', 'package.json must set "type": "module"')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (isCore) {
|
|
224
|
+
const scripts = /** @type {Record<string, string>} */ (pkg.scripts ?? {})
|
|
225
|
+
if (!scripts['dev:prepare']?.includes('nuxt-module-build')) {
|
|
226
|
+
issue(
|
|
227
|
+
'error',
|
|
228
|
+
'dev-prepare-builder',
|
|
229
|
+
'dev:prepare must run nuxt-module-build build --stub',
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
if (!scripts['prepack']?.includes('nuxt-module-build')) {
|
|
233
|
+
issue('error', 'prepack-builder', 'prepack must run nuxt-module-build build')
|
|
234
|
+
}
|
|
235
|
+
const devDeps = /** @type {Record<string, string>} */ (pkg.devDependencies ?? {})
|
|
236
|
+
if (!devDeps['@nuxt/module-builder']) {
|
|
237
|
+
issue(
|
|
238
|
+
'error',
|
|
239
|
+
'module-builder-dep',
|
|
240
|
+
'devDependencies must include @nuxt/module-builder',
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
const distModule = join(dir, 'dist', 'module.mjs')
|
|
244
|
+
if (!existsSync(distModule)) {
|
|
245
|
+
issue(
|
|
246
|
+
options.requireDist ? 'error' : 'error',
|
|
247
|
+
'dist-module',
|
|
248
|
+
'dist/module.mjs missing — run: pnpm run dev:prepare',
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
const ok =
|
|
252
|
+
errors.length === 0 && (!options.strict || warnings.length === 0)
|
|
253
|
+
return {
|
|
254
|
+
ok,
|
|
255
|
+
packageDir: dir,
|
|
256
|
+
pkgName,
|
|
257
|
+
kind,
|
|
258
|
+
errors,
|
|
259
|
+
warnings,
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const exports = /** @type {Record<string, unknown>} */ (pkg.exports ?? {})
|
|
264
|
+
const rootExport = /** @type {Record<string, string> | undefined} */ (
|
|
265
|
+
typeof exports['.'] === 'object' ? exports['.'] : undefined
|
|
266
|
+
)
|
|
267
|
+
if (rootExport?.import !== './dist/module.mjs') {
|
|
268
|
+
issue(
|
|
269
|
+
'error',
|
|
270
|
+
'exports-import',
|
|
271
|
+
'exports["."].import must be "./dist/module.mjs"',
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
if (pkg.main !== './dist/module.mjs') {
|
|
275
|
+
issue('error', 'main', 'main must be "./dist/module.mjs"')
|
|
276
|
+
}
|
|
277
|
+
const files = /** @type {string[] | undefined} */ (pkg.files)
|
|
278
|
+
if (!Array.isArray(files) || !files.includes('dist')) {
|
|
279
|
+
issue('error', 'files-dist', 'files must include "dist"')
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const scripts = /** @type {Record<string, string>} */ (pkg.scripts ?? {})
|
|
283
|
+
for (const script of REQUIRED_SCRIPTS) {
|
|
284
|
+
if (!scripts[script]) {
|
|
285
|
+
issue('error', `script-${script}`, `package.json scripts must include "${script}"`)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (scripts['dev:prepare'] && !scripts['dev:prepare'].includes('nuxt-module-build')) {
|
|
289
|
+
issue(
|
|
290
|
+
'error',
|
|
291
|
+
'dev-prepare-builder',
|
|
292
|
+
'dev:prepare must run nuxt-module-build (stub + prepare)',
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
if (scripts['prepack'] && !scripts['prepack'].includes('nuxt-module-build')) {
|
|
296
|
+
issue('error', 'prepack-builder', 'prepack must run nuxt-module-build build')
|
|
297
|
+
}
|
|
298
|
+
if (scripts.prepare) {
|
|
299
|
+
issue(
|
|
300
|
+
'error',
|
|
301
|
+
'no-prepare-script',
|
|
302
|
+
'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.',
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
if (!rootExport?.development?.includes('src/module')) {
|
|
306
|
+
issue(
|
|
307
|
+
'warning',
|
|
308
|
+
'exports-development',
|
|
309
|
+
'exports["."].development should point to ./src/module.ts (monorepo dev without manual stub after every edit)',
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
if (!scripts['dev:generate']?.includes('NUXT_APP_BASE_URL')) {
|
|
313
|
+
issue(
|
|
314
|
+
'warning',
|
|
315
|
+
'dev-generate-base-url',
|
|
316
|
+
'dev:generate should set NUXT_APP_BASE_URL=/<repo-slug>/ for GitHub Pages',
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const devDeps = /** @type {Record<string, string>} */ (pkg.devDependencies ?? {})
|
|
321
|
+
if (!devDeps['@nuxt/module-builder']) {
|
|
322
|
+
issue(
|
|
323
|
+
'error',
|
|
324
|
+
'module-builder-dep',
|
|
325
|
+
'devDependencies must include @nuxt/module-builder',
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const peerDeps = /** @type {Record<string, string>} */ (pkg.peerDependencies ?? {})
|
|
330
|
+
if (!peerDeps['@owdproject/core']) {
|
|
331
|
+
issue(
|
|
332
|
+
'warning',
|
|
333
|
+
'peer-core',
|
|
334
|
+
'peerDependencies should declare @owdproject/core',
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (existsSync(join(dir, 'module.ts'))) {
|
|
339
|
+
issue(
|
|
340
|
+
'error',
|
|
341
|
+
'legacy-module-root',
|
|
342
|
+
'legacy layout: move module.ts to src/module.ts',
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
if (existsSync(join(dir, 'runtime')) && !existsSync(join(dir, 'src', 'runtime'))) {
|
|
346
|
+
issue(
|
|
347
|
+
'error',
|
|
348
|
+
'legacy-runtime-root',
|
|
349
|
+
'legacy layout: move runtime/ under src/runtime/',
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const moduleTs = join(dir, 'src', 'module.ts')
|
|
354
|
+
if (!existsSync(moduleTs)) {
|
|
355
|
+
issue('error', 'src-module', 'src/module.ts is required')
|
|
356
|
+
} else {
|
|
357
|
+
const moduleSrc = readFileSync(moduleTs, 'utf8')
|
|
358
|
+
if (!moduleSrc.includes('defineNuxtModule')) {
|
|
359
|
+
issue('error', 'define-nuxt-module', 'src/module.ts must use defineNuxtModule')
|
|
360
|
+
}
|
|
361
|
+
if (!moduleSrc.includes('registerTailwindPath')) {
|
|
362
|
+
issue(
|
|
363
|
+
'warning',
|
|
364
|
+
'tailwind-path',
|
|
365
|
+
'src/module.ts should call registerTailwindPath for Vue components',
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const distModule = join(dir, 'dist', 'module.mjs')
|
|
371
|
+
if (!existsSync(distModule)) {
|
|
372
|
+
issue(
|
|
373
|
+
options.requireDist ? 'error' : 'error',
|
|
374
|
+
'dist-module',
|
|
375
|
+
'dist/module.mjs missing — run: pnpm run dev:prepare',
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const playgroundDir = join(dir, 'playground')
|
|
380
|
+
if (!existsSync(playgroundDir)) {
|
|
381
|
+
issue('error', 'playground-dir', 'playground/ directory is required')
|
|
382
|
+
} else {
|
|
383
|
+
const pgPkgPath = join(playgroundDir, 'package.json')
|
|
384
|
+
if (!existsSync(pgPkgPath)) {
|
|
385
|
+
issue('error', 'playground-package-json', 'playground/package.json is required')
|
|
386
|
+
} else {
|
|
387
|
+
try {
|
|
388
|
+
const pgPkg = JSON.parse(readFileSync(pgPkgPath, 'utf8'))
|
|
389
|
+
const deps = {
|
|
390
|
+
.../** @type {Record<string, string>} */ (pgPkg.dependencies ?? {}),
|
|
391
|
+
.../** @type {Record<string, string>} */ (pgPkg.devDependencies ?? {}),
|
|
392
|
+
}
|
|
393
|
+
if (!deps[pkgName] && !deps[pkgName.replace(SCOPE, '')]) {
|
|
394
|
+
issue(
|
|
395
|
+
'error',
|
|
396
|
+
'playground-self-dep',
|
|
397
|
+
`playground/package.json must depend on ${pkgName}`,
|
|
398
|
+
)
|
|
399
|
+
}
|
|
400
|
+
if (!deps['@owdproject/core']) {
|
|
401
|
+
issue(
|
|
402
|
+
'error',
|
|
403
|
+
'playground-core-dep',
|
|
404
|
+
'playground/package.json must depend on @owdproject/core',
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
if (kind === 'app' && !deps['@owdproject/theme-nova']) {
|
|
408
|
+
issue(
|
|
409
|
+
'warning',
|
|
410
|
+
'playground-theme-nova',
|
|
411
|
+
'app playgrounds should use @owdproject/theme-nova in dependencies',
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
} catch {
|
|
415
|
+
issue('error', 'playground-package-json', 'playground/package.json is invalid JSON')
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const nuxtConfigPath = join(playgroundDir, 'nuxt.config.ts')
|
|
420
|
+
if (!existsSync(nuxtConfigPath)) {
|
|
421
|
+
issue('error', 'playground-nuxt-config', 'playground/nuxt.config.ts is required')
|
|
422
|
+
} else {
|
|
423
|
+
const nuxtCfg = readFileSync(nuxtConfigPath, 'utf8')
|
|
424
|
+
if (!nuxtCfg.includes('@owdproject/core')) {
|
|
425
|
+
issue(
|
|
426
|
+
'error',
|
|
427
|
+
'playground-nuxt-core',
|
|
428
|
+
'playground/nuxt.config.ts must register @owdproject/core',
|
|
429
|
+
)
|
|
430
|
+
}
|
|
431
|
+
if (!/\bssr\s*:\s*false\b/.test(nuxtCfg)) {
|
|
432
|
+
issue('error', 'playground-ssr-false', 'playground/nuxt.config.ts must set ssr: false')
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const desktopConfigPath = join(playgroundDir, 'desktop.config.ts')
|
|
437
|
+
if (!existsSync(desktopConfigPath)) {
|
|
438
|
+
issue('error', 'playground-desktop-config', 'playground/desktop.config.ts is required')
|
|
439
|
+
} else if (pkgName) {
|
|
440
|
+
const desktopCfg = readFileSync(desktopConfigPath, 'utf8')
|
|
441
|
+
if (!desktopConfigReferencesPackage(desktopCfg, pkgName, kind)) {
|
|
442
|
+
const field = kind === 'app' ? 'apps' : kind === 'theme' ? 'theme' : 'modules'
|
|
443
|
+
issue(
|
|
444
|
+
'error',
|
|
445
|
+
'playground-desktop-ref',
|
|
446
|
+
`playground/desktop.config.ts must reference ${pkgName} in \`${field}\``,
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
if (!desktopCfg.includes('defineDesktopConfig')) {
|
|
450
|
+
issue(
|
|
451
|
+
'error',
|
|
452
|
+
'playground-define-config',
|
|
453
|
+
'playground/desktop.config.ts must use defineDesktopConfig',
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const appVuePath = join(playgroundDir, 'app', 'app.vue')
|
|
459
|
+
if (!existsSync(appVuePath)) {
|
|
460
|
+
issue('error', 'playground-app-vue', 'playground/app/app.vue is required')
|
|
461
|
+
} else {
|
|
462
|
+
const appVue = readFileSync(appVuePath, 'utf8')
|
|
463
|
+
if (!/<Desktop\s*\/?>/.test(appVue) && !/<Desktop[\s>]/.test(appVue)) {
|
|
464
|
+
issue(
|
|
465
|
+
'error',
|
|
466
|
+
'playground-desktop-component',
|
|
467
|
+
'playground/app/app.vue must render <Desktop />',
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const workspaceRoot = options.workspaceRoot ?? findWorkspaceRoot(dir)
|
|
473
|
+
const relPkg =
|
|
474
|
+
workspaceRoot != null ? relative(workspaceRoot, dir).replace(/\\/g, '/') : ''
|
|
475
|
+
const isMonorepoModule =
|
|
476
|
+
workspaceRoot != null &&
|
|
477
|
+
/^(apps|themes|packages)\/[^/]+$/.test(relPkg) &&
|
|
478
|
+
options.checkWorkspace !== false
|
|
479
|
+
|
|
480
|
+
if (isMonorepoModule) {
|
|
481
|
+
const wsPath = join(workspaceRoot, 'pnpm-workspace.yaml')
|
|
482
|
+
if (existsSync(wsPath)) {
|
|
483
|
+
const relPlayground = relative(workspaceRoot, playgroundDir)
|
|
484
|
+
const wsYaml = readFileSync(wsPath, 'utf8')
|
|
485
|
+
if (!workspaceListsPlayground(wsYaml, relPlayground)) {
|
|
486
|
+
issue(
|
|
487
|
+
'error',
|
|
488
|
+
'workspace-playground',
|
|
489
|
+
`add "${relPlayground}" to pnpm-workspace.yaml packages`,
|
|
490
|
+
)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (kind === 'app') {
|
|
497
|
+
const appConfigPath = join(dir, 'src', 'runtime', 'app.config.ts')
|
|
498
|
+
if (!existsSync(appConfigPath)) {
|
|
499
|
+
issue('error', 'app-config', 'src/runtime/app.config.ts is required for apps')
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const pluginPath = join(dir, 'src', 'runtime', 'plugin.ts')
|
|
503
|
+
if (!existsSync(pluginPath)) {
|
|
504
|
+
issue('error', 'app-plugin', 'src/runtime/plugin.ts is required for apps')
|
|
505
|
+
} else {
|
|
506
|
+
const pluginSrc = readFileSync(pluginPath, 'utf8')
|
|
507
|
+
if (!pluginSrc.includes('defineDesktopApp')) {
|
|
508
|
+
issue('error', 'define-desktop-app', 'plugin.ts must call defineDesktopApp')
|
|
509
|
+
}
|
|
510
|
+
if (!/name\s*:\s*['"]owd-[^'"]+-register['"]/.test(pluginSrc)) {
|
|
511
|
+
issue(
|
|
512
|
+
'error',
|
|
513
|
+
'plugin-name',
|
|
514
|
+
'plugin.ts must set name: "owd-<slug>-register" for dependsOn ordering',
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
if (!pluginSrc.includes('import.meta.server')) {
|
|
518
|
+
issue(
|
|
519
|
+
'warning',
|
|
520
|
+
'plugin-server-guard',
|
|
521
|
+
'plugin.ts should guard with if (import.meta.server) return',
|
|
522
|
+
)
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const launchPlugins = existsSync(join(playgroundDir, 'app', 'plugins'))
|
|
527
|
+
? readdirSync(join(playgroundDir, 'app', 'plugins')).filter((f) =>
|
|
528
|
+
/^launch-.*\.client\.ts$/.test(f),
|
|
529
|
+
)
|
|
530
|
+
: []
|
|
531
|
+
if (launchPlugins.length === 0) {
|
|
532
|
+
issue(
|
|
533
|
+
'warning',
|
|
534
|
+
'launch-plugin',
|
|
535
|
+
'optional: add playground/app/plugins/launch-*.client.ts for dev auto-open',
|
|
536
|
+
)
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (!existsSync(join(dir, '.github', 'workflows', 'pages.yml'))) {
|
|
541
|
+
issue(
|
|
542
|
+
'warning',
|
|
543
|
+
'pages-workflow',
|
|
544
|
+
'optional for standalone repos: .github/workflows/pages.yml',
|
|
545
|
+
)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const ok =
|
|
549
|
+
errors.length === 0 && (!options.strict || warnings.length === 0)
|
|
550
|
+
|
|
551
|
+
return {
|
|
552
|
+
ok,
|
|
553
|
+
packageDir: dir,
|
|
554
|
+
pkgName,
|
|
555
|
+
kind,
|
|
556
|
+
errors,
|
|
557
|
+
warnings,
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* @param {ValidationResult} result
|
|
563
|
+
* @param {{ json?: boolean }} [options]
|
|
564
|
+
*/
|
|
565
|
+
export function formatValidationReport(result, options = {}) {
|
|
566
|
+
if (options.json) {
|
|
567
|
+
return JSON.stringify(result, null, 2)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const lines = []
|
|
571
|
+
const status = result.ok ? 'PASS' : 'FAIL'
|
|
572
|
+
lines.push(`${status} ${result.pkgName || result.packageDir} (${result.kind})`)
|
|
573
|
+
lines.push(` ${result.packageDir}`)
|
|
574
|
+
for (const e of result.errors) {
|
|
575
|
+
lines.push(` ✗ [${e.code}] ${e.message}`)
|
|
576
|
+
}
|
|
577
|
+
for (const w of result.warnings) {
|
|
578
|
+
lines.push(` ⚠ [${w.code}] ${w.message}`)
|
|
579
|
+
}
|
|
580
|
+
if (result.ok && result.warnings.length === 0) {
|
|
581
|
+
lines.push(' All checks passed.')
|
|
582
|
+
}
|
|
583
|
+
return lines.join('\n')
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* @param {string} packageDir
|
|
588
|
+
* @param {{ smoke?: boolean }} [options]
|
|
589
|
+
*/
|
|
590
|
+
export function runSmokeBuild(packageDir, options = {}) {
|
|
591
|
+
if (!options.smoke) return { ok: true, message: '' }
|
|
592
|
+
const dir = resolve(packageDir)
|
|
593
|
+
try {
|
|
594
|
+
execSync('pnpm run dev:prepare', { cwd: dir, stdio: 'pipe', encoding: 'utf8' })
|
|
595
|
+
execSync('pnpm exec nuxt build playground', {
|
|
596
|
+
cwd: dir,
|
|
597
|
+
stdio: 'pipe',
|
|
598
|
+
encoding: 'utf8',
|
|
599
|
+
timeout: 300_000,
|
|
600
|
+
})
|
|
601
|
+
return { ok: true, message: 'smoke build succeeded' }
|
|
602
|
+
} catch (error) {
|
|
603
|
+
const msg =
|
|
604
|
+
error instanceof Error
|
|
605
|
+
? error.message
|
|
606
|
+
: String(error)
|
|
607
|
+
return { ok: false, message: `smoke build failed: ${msg}` }
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* @param {string[]} paths
|
|
613
|
+
* @param {{ workspaceRoot?: string | null, json?: boolean, strict?: boolean, smoke?: boolean }} [options]
|
|
614
|
+
* @returns {{ exitCode: number, results: ValidationResult[] }}
|
|
615
|
+
*/
|
|
616
|
+
export function runValidateCli(paths, options = {}) {
|
|
617
|
+
const workspaceRoot = options.workspaceRoot ?? findWorkspaceRoot() ?? null
|
|
618
|
+
/** @type {string[]} */
|
|
619
|
+
let targets = []
|
|
620
|
+
|
|
621
|
+
if (paths.length === 0) {
|
|
622
|
+
const fromCwd = findModulePackageRoot()
|
|
623
|
+
if (fromCwd) {
|
|
624
|
+
targets = [fromCwd]
|
|
625
|
+
} else if (workspaceRoot) {
|
|
626
|
+
targets = discoverOwdModulePackages(workspaceRoot)
|
|
627
|
+
}
|
|
628
|
+
} else {
|
|
629
|
+
for (const p of paths) {
|
|
630
|
+
const resolved = resolve(p)
|
|
631
|
+
if (existsSync(join(resolved, 'package.json'))) {
|
|
632
|
+
targets.push(resolved)
|
|
633
|
+
} else if (workspaceRoot && (p === 'apps' || p === 'themes' || p === 'packages')) {
|
|
634
|
+
const base = join(workspaceRoot, p)
|
|
635
|
+
if (existsSync(base)) {
|
|
636
|
+
targets.push(...discoverOwdModulePackages(workspaceRoot).filter((t) =>
|
|
637
|
+
t.startsWith(base),
|
|
638
|
+
))
|
|
639
|
+
}
|
|
640
|
+
} else {
|
|
641
|
+
targets.push(resolved)
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (targets.length === 0) {
|
|
647
|
+
return {
|
|
648
|
+
exitCode: 1,
|
|
649
|
+
results: [
|
|
650
|
+
{
|
|
651
|
+
ok: false,
|
|
652
|
+
packageDir: process.cwd(),
|
|
653
|
+
pkgName: '',
|
|
654
|
+
kind: 'module',
|
|
655
|
+
errors: [
|
|
656
|
+
{
|
|
657
|
+
level: 'error',
|
|
658
|
+
code: 'no-target',
|
|
659
|
+
message:
|
|
660
|
+
'No package to validate. Run from an @owdproject/* package or pass a path.',
|
|
661
|
+
},
|
|
662
|
+
],
|
|
663
|
+
warnings: [],
|
|
664
|
+
},
|
|
665
|
+
],
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** @type {ValidationResult[]} */
|
|
670
|
+
const results = []
|
|
671
|
+
let exitCode = 0
|
|
672
|
+
|
|
673
|
+
for (const dir of [...new Set(targets)]) {
|
|
674
|
+
const result = validateOwdModule(dir, {
|
|
675
|
+
workspaceRoot,
|
|
676
|
+
requireDist: true,
|
|
677
|
+
strict: options.strict,
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
if (options.smoke && result.pkgName && !isDesktopCorePackage(result.pkgName)) {
|
|
681
|
+
const smoke = runSmokeBuild(dir, { smoke: true })
|
|
682
|
+
if (!smoke.ok) {
|
|
683
|
+
result.errors.push({
|
|
684
|
+
level: 'error',
|
|
685
|
+
code: 'smoke-build',
|
|
686
|
+
message: smoke.message,
|
|
687
|
+
})
|
|
688
|
+
result.ok = false
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
results.push(result)
|
|
693
|
+
if (!result.ok) exitCode = 1
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return { exitCode, results }
|
|
697
|
+
}
|