uniweb 0.2.42 → 0.2.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/partials/agents-md.hbs +516 -24
- package/src/commands/docs.js +53 -1
- package/src/commands/i18n.js +97 -17
- package/src/templates/processor.js +8 -9
- package/src/utils/workspace.js +189 -0
- package/templates/_shared/package.json.hbs +2 -1
- package/templates/multi/foundations/default/package.json.hbs +0 -1
- package/templates/multi/foundations/default/src/_entry.generated.js +1 -0
- package/templates/multi/foundations/default/src/styles.css +1 -0
- package/templates/multi/package.json.hbs +2 -1
- package/templates/multi/sites/main/package.json.hbs +1 -1
- package/templates/multi/template.json +2 -1
- package/templates/single/foundation/package.json.hbs +0 -1
- package/templates/single/foundation/src/_entry.generated.js +1 -0
- package/templates/single/foundation/src/styles.css +1 -0
- package/templates/single/site/package.json.hbs +1 -1
- package/templates/template/template/AGENTS.md.hbs +1 -70
- package/templates/template/template/foundation/package.json.hbs +0 -1
- package/templates/template/template/foundation/src/_entry.generated.js +2 -1
- package/templates/template/template/foundation/src/styles.css +1 -0
- package/templates/template/template/package.json.hbs +2 -1
- package/templates/template/template/site/package.json.hbs +1 -1
- package/templates/multi/AGENTS.md.hbs +0 -1
- package/templates/multi/pnpm-workspace.yaml +0 -5
package/src/commands/docs.js
CHANGED
|
@@ -7,15 +7,24 @@
|
|
|
7
7
|
* uniweb docs # Generate docs for current directory
|
|
8
8
|
* uniweb docs --output README.md # Custom output filename
|
|
9
9
|
* uniweb docs --from-source # Build schema from source (no build required)
|
|
10
|
+
* uniweb docs --target <path> # Specify foundation directory explicitly
|
|
10
11
|
*
|
|
11
12
|
* When run from a site directory, automatically finds and documents the
|
|
12
13
|
* linked foundation, placing COMPONENTS.md in the site folder.
|
|
14
|
+
*
|
|
15
|
+
* When run from workspace root, auto-detects foundations. If multiple exist,
|
|
16
|
+
* prompts for selection.
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
19
|
import { existsSync } from 'node:fs'
|
|
16
20
|
import { readFile } from 'node:fs/promises'
|
|
17
21
|
import { resolve, join, dirname } from 'node:path'
|
|
18
22
|
import { generateDocs } from '@uniweb/build'
|
|
23
|
+
import {
|
|
24
|
+
isWorkspaceRoot,
|
|
25
|
+
findFoundations,
|
|
26
|
+
promptSelect,
|
|
27
|
+
} from '../utils/workspace.js'
|
|
19
28
|
|
|
20
29
|
// Colors for terminal output
|
|
21
30
|
const colors = {
|
|
@@ -51,6 +60,7 @@ function parseArgs(args) {
|
|
|
51
60
|
const options = {
|
|
52
61
|
output: 'COMPONENTS.md',
|
|
53
62
|
fromSource: false,
|
|
63
|
+
target: null,
|
|
54
64
|
}
|
|
55
65
|
|
|
56
66
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -60,6 +70,8 @@ function parseArgs(args) {
|
|
|
60
70
|
options.output = args[++i]
|
|
61
71
|
} else if (arg === '--from-source' || arg === '-s') {
|
|
62
72
|
options.fromSource = true
|
|
73
|
+
} else if (arg === '--target' || arg === '-t') {
|
|
74
|
+
options.target = args[++i]
|
|
63
75
|
} else if (arg === '--help' || arg === '-h') {
|
|
64
76
|
options.help = true
|
|
65
77
|
}
|
|
@@ -79,16 +91,19 @@ ${colors.dim}Usage:${colors.reset}
|
|
|
79
91
|
uniweb docs Generate COMPONENTS.md
|
|
80
92
|
uniweb docs --output DOCS.md Custom output filename
|
|
81
93
|
uniweb docs --from-source Build schema from source (no build required)
|
|
94
|
+
uniweb docs --target foundation Specify foundation directory explicitly
|
|
82
95
|
|
|
83
96
|
${colors.dim}Options:${colors.reset}
|
|
84
97
|
-o, --output <file> Output filename (default: COMPONENTS.md)
|
|
85
98
|
-s, --from-source Read meta.js files directly instead of schema.json
|
|
99
|
+
-t, --target <path> Foundation directory (auto-detected if not specified)
|
|
86
100
|
-h, --help Show this help message
|
|
87
101
|
|
|
88
102
|
${colors.dim}Notes:${colors.reset}
|
|
89
103
|
Run from a foundation directory to generate docs there.
|
|
90
104
|
Run from a site directory to auto-detect the linked foundation
|
|
91
105
|
and generate docs in the site folder for convenience.
|
|
106
|
+
Run from workspace root to auto-detect foundations (prompts if multiple).
|
|
92
107
|
`)
|
|
93
108
|
}
|
|
94
109
|
|
|
@@ -157,8 +172,45 @@ export async function docs(args) {
|
|
|
157
172
|
let foundationDir = projectDir
|
|
158
173
|
let outputDir = projectDir
|
|
159
174
|
|
|
175
|
+
// If --target specified, use it directly
|
|
176
|
+
if (options.target) {
|
|
177
|
+
foundationDir = resolve(projectDir, options.target)
|
|
178
|
+
outputDir = foundationDir
|
|
179
|
+
if (!isFoundation(foundationDir)) {
|
|
180
|
+
error(`Target directory does not appear to be a foundation: ${options.target}`)
|
|
181
|
+
log(`${colors.dim}Foundations have a src/components/ directory.${colors.reset}`)
|
|
182
|
+
process.exit(1)
|
|
183
|
+
}
|
|
184
|
+
info(`Using foundation: ${options.target}`)
|
|
185
|
+
}
|
|
186
|
+
// Check if we're at workspace root
|
|
187
|
+
else if (isWorkspaceRoot(projectDir)) {
|
|
188
|
+
const foundations = await findFoundations(projectDir)
|
|
189
|
+
|
|
190
|
+
if (foundations.length === 0) {
|
|
191
|
+
error('No foundations found in this workspace.')
|
|
192
|
+
log(`${colors.dim}Foundations have @uniweb/build in devDependencies.${colors.reset}`)
|
|
193
|
+
process.exit(1)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let targetFoundation
|
|
197
|
+
if (foundations.length === 1) {
|
|
198
|
+
targetFoundation = foundations[0]
|
|
199
|
+
info(`Found foundation: ${targetFoundation}`)
|
|
200
|
+
} else {
|
|
201
|
+
log(`${colors.dim}Multiple foundations found in workspace.${colors.reset}\n`)
|
|
202
|
+
targetFoundation = await promptSelect('Select foundation:', foundations)
|
|
203
|
+
if (!targetFoundation) {
|
|
204
|
+
log('Cancelled.')
|
|
205
|
+
process.exit(0)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
foundationDir = resolve(projectDir, targetFoundation)
|
|
210
|
+
outputDir = foundationDir
|
|
211
|
+
}
|
|
160
212
|
// Check if we're in a site directory
|
|
161
|
-
if (isSite(projectDir)) {
|
|
213
|
+
else if (isSite(projectDir)) {
|
|
162
214
|
const foundation = await resolveFoundationFromSite(projectDir)
|
|
163
215
|
if (!foundation) {
|
|
164
216
|
error('Could not find a linked foundation in this site.')
|
package/src/commands/i18n.js
CHANGED
|
@@ -4,15 +4,24 @@
|
|
|
4
4
|
* Commands for managing site content internationalization.
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
* uniweb i18n extract
|
|
8
|
-
* uniweb i18n sync
|
|
9
|
-
* uniweb i18n status
|
|
7
|
+
* uniweb i18n extract Extract translatable strings to manifest
|
|
8
|
+
* uniweb i18n sync Sync manifest with content changes
|
|
9
|
+
* uniweb i18n status Show translation coverage per locale
|
|
10
|
+
* uniweb i18n --target <path> Specify site directory explicitly
|
|
11
|
+
*
|
|
12
|
+
* When run from workspace root, auto-detects sites. If multiple exist,
|
|
13
|
+
* prompts for selection.
|
|
10
14
|
*/
|
|
11
15
|
|
|
12
16
|
import { resolve, join } from 'path'
|
|
13
17
|
import { existsSync } from 'fs'
|
|
14
18
|
import { readFile } from 'fs/promises'
|
|
15
19
|
import yaml from 'js-yaml'
|
|
20
|
+
import {
|
|
21
|
+
isWorkspaceRoot,
|
|
22
|
+
findSites,
|
|
23
|
+
promptSelect,
|
|
24
|
+
} from '../utils/workspace.js'
|
|
16
25
|
|
|
17
26
|
// Colors for terminal output
|
|
18
27
|
const colors = {
|
|
@@ -41,6 +50,21 @@ function error(message) {
|
|
|
41
50
|
console.error(`${colors.red}✗${colors.reset} ${message}`)
|
|
42
51
|
}
|
|
43
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Parse --target option from args
|
|
55
|
+
*/
|
|
56
|
+
function parseTargetOption(args) {
|
|
57
|
+
for (let i = 0; i < args.length; i++) {
|
|
58
|
+
if (args[i] === '--target' || args[i] === '-t') {
|
|
59
|
+
return {
|
|
60
|
+
target: args[i + 1],
|
|
61
|
+
remainingArgs: [...args.slice(0, i), ...args.slice(i + 2)],
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { target: null, remainingArgs: args }
|
|
66
|
+
}
|
|
67
|
+
|
|
44
68
|
/**
|
|
45
69
|
* Main i18n command handler
|
|
46
70
|
* @param {string[]} args - Command arguments
|
|
@@ -48,13 +72,21 @@ function error(message) {
|
|
|
48
72
|
export async function i18n(args) {
|
|
49
73
|
const subcommand = args[0]
|
|
50
74
|
|
|
51
|
-
if (
|
|
75
|
+
if (subcommand === '--help' || subcommand === '-h') {
|
|
52
76
|
showHelp()
|
|
53
77
|
return
|
|
54
78
|
}
|
|
55
79
|
|
|
80
|
+
// Parse --target option
|
|
81
|
+
const { target, remainingArgs } = parseTargetOption(args)
|
|
82
|
+
|
|
83
|
+
// Default to 'sync' if no subcommand (or if first arg is an option)
|
|
84
|
+
const firstArg = remainingArgs[0]
|
|
85
|
+
const effectiveSubcommand = !firstArg || firstArg.startsWith('-') ? 'sync' : firstArg
|
|
86
|
+
const effectiveArgs = !firstArg || firstArg.startsWith('-') ? remainingArgs : remainingArgs.slice(1)
|
|
87
|
+
|
|
56
88
|
// Find site root
|
|
57
|
-
const siteRoot = await findSiteRoot()
|
|
89
|
+
const siteRoot = await findSiteRoot(target)
|
|
58
90
|
if (!siteRoot) {
|
|
59
91
|
error('Could not find site root. Make sure you are in a Uniweb site directory.')
|
|
60
92
|
process.exit(1)
|
|
@@ -63,18 +95,18 @@ export async function i18n(args) {
|
|
|
63
95
|
// Load site config for locale settings
|
|
64
96
|
const config = await loadSiteConfig(siteRoot)
|
|
65
97
|
|
|
66
|
-
switch (
|
|
98
|
+
switch (effectiveSubcommand) {
|
|
67
99
|
case 'extract':
|
|
68
|
-
await runExtract(siteRoot, config,
|
|
100
|
+
await runExtract(siteRoot, config, effectiveArgs)
|
|
69
101
|
break
|
|
70
102
|
case 'sync':
|
|
71
|
-
await runSync(siteRoot, config,
|
|
103
|
+
await runSync(siteRoot, config, effectiveArgs)
|
|
72
104
|
break
|
|
73
105
|
case 'status':
|
|
74
|
-
await runStatus(siteRoot, config,
|
|
106
|
+
await runStatus(siteRoot, config, effectiveArgs)
|
|
75
107
|
break
|
|
76
108
|
default:
|
|
77
|
-
error(`Unknown subcommand: ${
|
|
109
|
+
error(`Unknown subcommand: ${effectiveSubcommand}`)
|
|
78
110
|
showHelp()
|
|
79
111
|
process.exit(1)
|
|
80
112
|
}
|
|
@@ -82,11 +114,52 @@ export async function i18n(args) {
|
|
|
82
114
|
|
|
83
115
|
/**
|
|
84
116
|
* Find site root by looking for site.yml
|
|
117
|
+
* Handles: explicit target, workspace root detection, or walking up directories
|
|
118
|
+
*
|
|
119
|
+
* @param {string|null} target - Explicit target path (from --target option)
|
|
85
120
|
*/
|
|
86
|
-
async function findSiteRoot() {
|
|
87
|
-
|
|
121
|
+
async function findSiteRoot(target) {
|
|
122
|
+
const cwd = process.cwd()
|
|
123
|
+
|
|
124
|
+
// If explicit target specified, use it
|
|
125
|
+
if (target) {
|
|
126
|
+
const targetPath = resolve(cwd, target)
|
|
127
|
+
if (existsSync(join(targetPath, 'site.yml'))) {
|
|
128
|
+
return targetPath
|
|
129
|
+
}
|
|
130
|
+
error(`Target directory does not appear to be a site: ${target}`)
|
|
131
|
+
log(`${colors.dim}Sites have a site.yml file.${colors.reset}`)
|
|
132
|
+
process.exit(1)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check if we're at workspace root
|
|
136
|
+
if (isWorkspaceRoot(cwd)) {
|
|
137
|
+
const sites = await findSites(cwd)
|
|
138
|
+
|
|
139
|
+
if (sites.length === 0) {
|
|
140
|
+
error('No sites found in this workspace.')
|
|
141
|
+
log(`${colors.dim}Sites have @uniweb/runtime in dependencies.${colors.reset}`)
|
|
142
|
+
process.exit(1)
|
|
143
|
+
}
|
|
88
144
|
|
|
89
|
-
|
|
145
|
+
let targetSite
|
|
146
|
+
if (sites.length === 1) {
|
|
147
|
+
targetSite = sites[0]
|
|
148
|
+
log(`${colors.cyan}→${colors.reset} Found site: ${targetSite}`)
|
|
149
|
+
} else {
|
|
150
|
+
log(`${colors.dim}Multiple sites found in workspace.${colors.reset}\n`)
|
|
151
|
+
targetSite = await promptSelect('Select site:', sites)
|
|
152
|
+
if (!targetSite) {
|
|
153
|
+
log('Cancelled.')
|
|
154
|
+
process.exit(0)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return resolve(cwd, targetSite)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Walk up directories looking for site.yml
|
|
162
|
+
let dir = cwd
|
|
90
163
|
for (let i = 0; i < 5; i++) {
|
|
91
164
|
if (existsSync(join(dir, 'site.yml'))) {
|
|
92
165
|
return dir
|
|
@@ -278,16 +351,18 @@ ${colors.cyan}${colors.bright}Uniweb i18n${colors.reset}
|
|
|
278
351
|
Site content internationalization commands.
|
|
279
352
|
|
|
280
353
|
${colors.bright}Usage:${colors.reset}
|
|
281
|
-
uniweb i18n
|
|
354
|
+
uniweb i18n [command] [options]
|
|
282
355
|
|
|
283
356
|
${colors.bright}Commands:${colors.reset}
|
|
357
|
+
(default) Same as sync - extract/update strings (runs if no command given)
|
|
284
358
|
extract Extract translatable strings to locales/manifest.json
|
|
285
359
|
sync Update manifest with content changes (detects moved/changed content)
|
|
286
360
|
status Show translation coverage per locale
|
|
287
361
|
|
|
288
362
|
${colors.bright}Options:${colors.reset}
|
|
289
|
-
--
|
|
290
|
-
--
|
|
363
|
+
-t, --target <path> Site directory (auto-detected if not specified)
|
|
364
|
+
--verbose Show detailed output
|
|
365
|
+
--dry-run (sync) Show changes without writing files
|
|
291
366
|
|
|
292
367
|
${colors.bright}Configuration:${colors.reset}
|
|
293
368
|
Optional site.yml settings:
|
|
@@ -301,7 +376,7 @@ ${colors.bright}Configuration:${colors.reset}
|
|
|
301
376
|
|
|
302
377
|
${colors.bright}Workflow:${colors.reset}
|
|
303
378
|
1. Build your site: uniweb build
|
|
304
|
-
2. Extract strings: uniweb i18n
|
|
379
|
+
2. Extract strings: uniweb i18n
|
|
305
380
|
3. Translate locale files: Edit locales/es.json, locales/fr.json, etc.
|
|
306
381
|
4. Build with translations: uniweb build (generates locale-specific output)
|
|
307
382
|
|
|
@@ -319,6 +394,11 @@ ${colors.bright}Examples:${colors.reset}
|
|
|
319
394
|
uniweb i18n sync --dry-run # Preview changes without writing
|
|
320
395
|
uniweb i18n status # Show coverage for all locales
|
|
321
396
|
uniweb i18n status es # Show coverage for Spanish only
|
|
397
|
+
uniweb i18n --target site # Specify site directory explicitly
|
|
398
|
+
|
|
399
|
+
${colors.bright}Notes:${colors.reset}
|
|
400
|
+
Run from a site directory to operate on that site.
|
|
401
|
+
Run from workspace root to auto-detect sites (prompts if multiple).
|
|
322
402
|
`)
|
|
323
403
|
}
|
|
324
404
|
|
|
@@ -197,15 +197,16 @@ async function processFile(sourcePath, targetPath, data, options = {}) {
|
|
|
197
197
|
* @param {Object} options - Processing options
|
|
198
198
|
* @param {string|null} options.variant - Template variant to use
|
|
199
199
|
* @param {string|null} options.basePath - Base template to merge with (files copied first)
|
|
200
|
+
* @param {boolean} options.isBase - Internal: true when processing base template (allows overwriting)
|
|
200
201
|
* @param {Function} options.onWarning - Warning callback
|
|
201
202
|
* @param {Function} options.onProgress - Progress callback
|
|
202
203
|
*/
|
|
203
204
|
export async function copyTemplateDirectory(sourcePath, targetPath, data, options = {}) {
|
|
204
|
-
const { variant = null, basePath = null, onWarning, onProgress } = options
|
|
205
|
+
const { variant = null, basePath = null, isBase = false, onWarning, onProgress } = options
|
|
205
206
|
|
|
206
|
-
// If a base template is specified, copy it first (
|
|
207
|
+
// If a base template is specified, copy it first (with isBase=true so main can overwrite)
|
|
207
208
|
if (basePath && existsSync(basePath)) {
|
|
208
|
-
await copyTemplateDirectory(basePath, targetPath, data, { variant, onWarning, onProgress })
|
|
209
|
+
await copyTemplateDirectory(basePath, targetPath, data, { variant, isBase: true, onWarning, onProgress })
|
|
209
210
|
}
|
|
210
211
|
|
|
211
212
|
await fs.mkdir(targetPath, { recursive: true })
|
|
@@ -223,7 +224,7 @@ export async function copyTemplateDirectory(sourcePath, targetPath, data, option
|
|
|
223
224
|
}
|
|
224
225
|
|
|
225
226
|
// Options for recursive calls (without basePath to avoid re-copying base at each level)
|
|
226
|
-
const recursionOptions = { variant, onWarning, onProgress }
|
|
227
|
+
const recursionOptions = { variant, isBase, onWarning, onProgress }
|
|
227
228
|
|
|
228
229
|
for (const entry of entries) {
|
|
229
230
|
const sourceName = entry.name
|
|
@@ -279,11 +280,9 @@ export async function copyTemplateDirectory(sourcePath, targetPath, data, option
|
|
|
279
280
|
const sourceFullPath = path.join(sourcePath, sourceName)
|
|
280
281
|
const targetFullPath = path.join(targetPath, targetName)
|
|
281
282
|
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
onWarning(`Skipping ${targetFullPath} - file already exists`)
|
|
286
|
-
}
|
|
283
|
+
// When processing base template, skip if target exists (main template files take precedence)
|
|
284
|
+
// When processing main template, overwrite any files from base
|
|
285
|
+
if (isBase && existsSync(targetFullPath)) {
|
|
287
286
|
continue
|
|
288
287
|
}
|
|
289
288
|
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Detection Utilities
|
|
3
|
+
*
|
|
4
|
+
* Detects pnpm workspace structure and classifies packages as foundations or sites.
|
|
5
|
+
* Used by commands to auto-detect targets when run from workspace root.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
9
|
+
import { readFile } from 'node:fs/promises'
|
|
10
|
+
import { resolve, dirname, join } from 'node:path'
|
|
11
|
+
import yaml from 'js-yaml'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Find workspace root by looking for pnpm-workspace.yaml
|
|
15
|
+
* @param {string} startDir - Directory to start searching from
|
|
16
|
+
* @returns {string|null} - Workspace root path or null
|
|
17
|
+
*/
|
|
18
|
+
export function findWorkspaceRoot(startDir = process.cwd()) {
|
|
19
|
+
let dir = startDir
|
|
20
|
+
while (dir !== dirname(dir)) {
|
|
21
|
+
if (existsSync(join(dir, 'pnpm-workspace.yaml'))) {
|
|
22
|
+
return dir
|
|
23
|
+
}
|
|
24
|
+
dir = dirname(dir)
|
|
25
|
+
}
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve workspace package patterns to actual directories
|
|
31
|
+
* Handles patterns like: "foundation", "site", "foundations/*", "sites/*"
|
|
32
|
+
*
|
|
33
|
+
* @param {string[]} patterns - Array of patterns from pnpm-workspace.yaml
|
|
34
|
+
* @param {string} workspaceRoot - Workspace root directory
|
|
35
|
+
* @returns {string[]} - Array of existing package directories (relative paths)
|
|
36
|
+
*/
|
|
37
|
+
function resolvePatterns(patterns, workspaceRoot) {
|
|
38
|
+
const packages = []
|
|
39
|
+
|
|
40
|
+
for (const pattern of patterns) {
|
|
41
|
+
// Remove quotes if present
|
|
42
|
+
const cleanPattern = pattern.replace(/^["']|["']$/g, '')
|
|
43
|
+
|
|
44
|
+
if (cleanPattern.endsWith('/*')) {
|
|
45
|
+
// Glob pattern like "foundations/*" - list subdirectories
|
|
46
|
+
const baseDir = cleanPattern.slice(0, -2)
|
|
47
|
+
const fullPath = join(workspaceRoot, baseDir)
|
|
48
|
+
|
|
49
|
+
if (existsSync(fullPath)) {
|
|
50
|
+
try {
|
|
51
|
+
const entries = readdirSync(fullPath, { withFileTypes: true })
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
54
|
+
packages.push(join(baseDir, entry.name))
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// Ignore read errors
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
// Direct path like "foundation" or "site"
|
|
63
|
+
const fullPath = join(workspaceRoot, cleanPattern)
|
|
64
|
+
if (existsSync(fullPath)) {
|
|
65
|
+
packages.push(cleanPattern)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return packages
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get workspace packages from pnpm-workspace.yaml
|
|
75
|
+
* @param {string} workspaceRoot
|
|
76
|
+
* @returns {Promise<string[]>} - Array of package directories (relative paths)
|
|
77
|
+
*/
|
|
78
|
+
export async function getWorkspacePackages(workspaceRoot) {
|
|
79
|
+
const configPath = join(workspaceRoot, 'pnpm-workspace.yaml')
|
|
80
|
+
const content = await readFile(configPath, 'utf-8')
|
|
81
|
+
const config = yaml.load(content)
|
|
82
|
+
|
|
83
|
+
if (!config?.packages || !Array.isArray(config.packages)) {
|
|
84
|
+
return []
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return resolvePatterns(config.packages, workspaceRoot)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Classify a package as foundation, site, or unknown
|
|
92
|
+
*
|
|
93
|
+
* Classification logic:
|
|
94
|
+
* - Site: has @uniweb/runtime in dependencies (checked first, more specific)
|
|
95
|
+
* - Foundation: has @uniweb/build in devDependencies but NOT @uniweb/runtime
|
|
96
|
+
*
|
|
97
|
+
* Note: Sites also have @uniweb/build for the Vite plugin, so we check
|
|
98
|
+
* for @uniweb/runtime first to distinguish them.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} packagePath - Full path to package directory
|
|
101
|
+
* @returns {Promise<'foundation'|'site'|null>}
|
|
102
|
+
*/
|
|
103
|
+
export async function classifyPackage(packagePath) {
|
|
104
|
+
const pkgJsonPath = join(packagePath, 'package.json')
|
|
105
|
+
if (!existsSync(pkgJsonPath)) return null
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const pkg = JSON.parse(await readFile(pkgJsonPath, 'utf-8'))
|
|
109
|
+
|
|
110
|
+
// Site: has @uniweb/runtime in dependencies (check first - more specific)
|
|
111
|
+
if (pkg.dependencies?.['@uniweb/runtime']) {
|
|
112
|
+
return 'site'
|
|
113
|
+
}
|
|
114
|
+
// Foundation: has @uniweb/build in devDependencies (and not a site)
|
|
115
|
+
if (pkg.devDependencies?.['@uniweb/build']) {
|
|
116
|
+
return 'foundation'
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
// Ignore parse errors
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Find all foundations in workspace
|
|
127
|
+
* @param {string} workspaceRoot
|
|
128
|
+
* @returns {Promise<string[]>} - Array of foundation paths (relative to workspace root)
|
|
129
|
+
*/
|
|
130
|
+
export async function findFoundations(workspaceRoot) {
|
|
131
|
+
const packages = await getWorkspacePackages(workspaceRoot)
|
|
132
|
+
const foundations = []
|
|
133
|
+
|
|
134
|
+
for (const pkg of packages) {
|
|
135
|
+
const fullPath = join(workspaceRoot, pkg)
|
|
136
|
+
if ((await classifyPackage(fullPath)) === 'foundation') {
|
|
137
|
+
foundations.push(pkg)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return foundations
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Find all sites in workspace
|
|
146
|
+
* @param {string} workspaceRoot
|
|
147
|
+
* @returns {Promise<string[]>} - Array of site paths (relative to workspace root)
|
|
148
|
+
*/
|
|
149
|
+
export async function findSites(workspaceRoot) {
|
|
150
|
+
const packages = await getWorkspacePackages(workspaceRoot)
|
|
151
|
+
const sites = []
|
|
152
|
+
|
|
153
|
+
for (const pkg of packages) {
|
|
154
|
+
const fullPath = join(workspaceRoot, pkg)
|
|
155
|
+
if ((await classifyPackage(fullPath)) === 'site') {
|
|
156
|
+
sites.push(pkg)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return sites
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Check if current directory is the workspace root
|
|
165
|
+
* @param {string} dir - Directory to check
|
|
166
|
+
* @returns {boolean}
|
|
167
|
+
*/
|
|
168
|
+
export function isWorkspaceRoot(dir = process.cwd()) {
|
|
169
|
+
return existsSync(join(dir, 'pnpm-workspace.yaml'))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Interactive prompt to select from multiple options
|
|
174
|
+
* @param {string} message - Prompt message
|
|
175
|
+
* @param {string[]} choices - Array of choices
|
|
176
|
+
* @returns {Promise<string|null>} - Selected choice or null if cancelled
|
|
177
|
+
*/
|
|
178
|
+
export async function promptSelect(message, choices) {
|
|
179
|
+
const prompts = (await import('prompts')).default
|
|
180
|
+
|
|
181
|
+
const response = await prompts({
|
|
182
|
+
type: 'select',
|
|
183
|
+
name: 'value',
|
|
184
|
+
message,
|
|
185
|
+
choices: choices.map(c => ({ title: c, value: c })),
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
return response.value || null
|
|
189
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "Multi-Site Workspace",
|
|
3
|
-
"description": "A Uniweb workspace supporting multiple sites and foundations. Use this when you need to manage several related sites or share foundations across projects."
|
|
3
|
+
"description": "A Uniweb workspace supporting multiple sites and foundations. Use this when you need to manage several related sites or share foundations across projects.",
|
|
4
|
+
"base": "_shared"
|
|
4
5
|
}
|