uniweb 0.7.1 → 0.7.2
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 +64 -17
- package/package.json +4 -3
- package/src/commands/add.js +563 -0
- package/src/commands/build.js +49 -6
- package/src/commands/doctor.js +181 -2
- package/src/index.js +273 -131
- package/src/templates/index.js +0 -94
- package/src/templates/processor.js +10 -87
- package/src/templates/resolver.js +3 -3
- package/src/templates/validator.js +59 -17
- package/src/utils/config.js +229 -0
- package/src/utils/scaffold.js +175 -0
- package/templates/{single/foundation → foundation}/package.json.hbs +2 -2
- package/templates/foundation/src/foundation.js.hbs +7 -0
- package/templates/foundation/src/sections/.gitkeep +0 -0
- package/templates/{multi/sites/main → site}/package.json.hbs +2 -2
- package/templates/site/site.yml.hbs +10 -0
- package/templates/site/theme.yml +1 -0
- package/templates/{_shared → workspace}/package.json.hbs +3 -9
- package/templates/workspace/pnpm-workspace.yaml.hbs +4 -0
- package/templates/_shared/pnpm-workspace.yaml +0 -5
- package/templates/multi/README.md.hbs +0 -85
- package/templates/multi/foundations/default/package.json.hbs +0 -38
- package/templates/multi/foundations/default/src/foundation.js +0 -41
- package/templates/multi/package.json.hbs +0 -26
- package/templates/multi/sites/main/pages/home/1-welcome.md.hbs +0 -14
- package/templates/multi/sites/main/site.yml.hbs +0 -12
- package/templates/multi/sites/main/vite.config.js +0 -7
- package/templates/multi/template/.vscode/settings.json +0 -6
- package/templates/multi/template.json +0 -5
- package/templates/single/foundation/src/sections/Section/index.jsx +0 -121
- package/templates/single/foundation/src/sections/Section/meta.js +0 -61
- package/templates/single/foundation/src/styles.css +0 -5
- package/templates/single/foundation/vite.config.js +0 -3
- package/templates/single/site/index.html.hbs +0 -13
- package/templates/single/site/main.js +0 -7
- package/templates/single/site/package.json.hbs +0 -27
- package/templates/single/site/pages/about/1-about.md.hbs +0 -13
- package/templates/single/site/pages/about/page.yml +0 -2
- package/templates/single/site/pages/home/page.yml +0 -2
- package/templates/single/site/public/favicon.svg +0 -7
- package/templates/single/site/site.yml.hbs +0 -10
- package/templates/single/template.json +0 -10
- /package/{templates/single → starter}/foundation/src/foundation.js +0 -0
- /package/{templates/multi/foundations/default → starter/foundation}/src/sections/Section/index.jsx +0 -0
- /package/{templates/multi/foundations/default → starter/foundation}/src/sections/Section/meta.js +0 -0
- /package/{templates/multi/sites/main → starter/site}/pages/about/1-about.md.hbs +0 -0
- /package/{templates/multi/sites/main → starter/site}/pages/about/page.yml +0 -0
- /package/{templates/single → starter}/site/pages/home/1-welcome.md.hbs +0 -0
- /package/{templates/multi/sites/main → starter/site}/pages/home/page.yml +0 -0
- /package/templates/{multi/foundations/default → foundation}/src/styles.css +0 -0
- /package/templates/{multi/foundations/default → foundation}/vite.config.js +0 -0
- /package/templates/{multi/sites/main → site}/index.html.hbs +0 -0
- /package/templates/{multi/sites/main → site}/main.js +0 -0
- /package/templates/{multi/sites/main → site}/public/favicon.svg +0 -0
- /package/templates/{single/site → site}/vite.config.js +0 -0
- /package/templates/{_shared → workspace}/AGENTS.md.hbs +0 -0
- /package/templates/{single → workspace}/README.md.hbs +0 -0
- /package/templates/{single/template/.vscode → workspace/_vscode}/settings.json +0 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add Command
|
|
3
|
+
*
|
|
4
|
+
* Adds foundations, sites, or extensions to an existing workspace.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* uniweb add foundation [name] [--from <template>] [--path <dir>] [--project <name>]
|
|
8
|
+
* uniweb add site [name] [--from <template>] [--foundation <name>] [--path <dir>] [--project <name>]
|
|
9
|
+
* uniweb add extension [name] [--from <template>] [--site <name>] [--path <dir>]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync } from 'node:fs'
|
|
13
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
14
|
+
import { join, relative } from 'node:path'
|
|
15
|
+
import prompts from 'prompts'
|
|
16
|
+
import yaml from 'js-yaml'
|
|
17
|
+
import { scaffoldFoundation, scaffoldSite, applyContent } from '../utils/scaffold.js'
|
|
18
|
+
import {
|
|
19
|
+
readWorkspaceConfig,
|
|
20
|
+
addWorkspaceGlob,
|
|
21
|
+
discoverFoundations,
|
|
22
|
+
discoverSites,
|
|
23
|
+
updateRootScripts,
|
|
24
|
+
} from '../utils/config.js'
|
|
25
|
+
import { findWorkspaceRoot } from '../utils/workspace.js'
|
|
26
|
+
import { resolveTemplate } from '../templates/index.js'
|
|
27
|
+
import { validateTemplate } from '../templates/validator.js'
|
|
28
|
+
import { getVersionsForTemplates } from '../versions.js'
|
|
29
|
+
|
|
30
|
+
// Colors for terminal output
|
|
31
|
+
const colors = {
|
|
32
|
+
reset: '\x1b[0m',
|
|
33
|
+
bright: '\x1b[1m',
|
|
34
|
+
dim: '\x1b[2m',
|
|
35
|
+
cyan: '\x1b[36m',
|
|
36
|
+
green: '\x1b[32m',
|
|
37
|
+
yellow: '\x1b[33m',
|
|
38
|
+
red: '\x1b[31m',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function log(message) { console.log(message) }
|
|
42
|
+
function success(message) { console.log(`${colors.green}✓${colors.reset} ${message}`) }
|
|
43
|
+
function error(message) { console.error(`${colors.red}✗${colors.reset} ${message}`) }
|
|
44
|
+
function info(message) { console.log(`${colors.dim}${message}${colors.reset}`) }
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parse add command arguments
|
|
48
|
+
*/
|
|
49
|
+
function parseArgs(args) {
|
|
50
|
+
const result = {
|
|
51
|
+
subcommand: args[0], // foundation, site, extension
|
|
52
|
+
name: null,
|
|
53
|
+
path: null,
|
|
54
|
+
project: null,
|
|
55
|
+
foundation: null,
|
|
56
|
+
site: null,
|
|
57
|
+
from: null,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Find positional name (first arg after subcommand that's not a flag)
|
|
61
|
+
for (let i = 1; i < args.length; i++) {
|
|
62
|
+
if (args[i].startsWith('--')) {
|
|
63
|
+
i++ // skip flag value
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
if (!result.name) {
|
|
67
|
+
result.name = args[i]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Parse flags
|
|
72
|
+
for (let i = 1; i < args.length; i++) {
|
|
73
|
+
if (args[i] === '--path' && args[i + 1]) {
|
|
74
|
+
result.path = args[++i]
|
|
75
|
+
} else if (args[i] === '--project' && args[i + 1]) {
|
|
76
|
+
result.project = args[++i]
|
|
77
|
+
} else if (args[i] === '--foundation' && args[i + 1]) {
|
|
78
|
+
result.foundation = args[++i]
|
|
79
|
+
} else if (args[i] === '--site' && args[i + 1]) {
|
|
80
|
+
result.site = args[++i]
|
|
81
|
+
} else if (args[i] === '--from' && args[i + 1]) {
|
|
82
|
+
result.from = args[++i]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Main add command handler
|
|
91
|
+
*/
|
|
92
|
+
export async function add(args) {
|
|
93
|
+
if (!args.length || args[0] === '--help' || args[0] === '-h') {
|
|
94
|
+
showAddHelp()
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const parsed = parseArgs(args)
|
|
99
|
+
|
|
100
|
+
// Find workspace root
|
|
101
|
+
const rootDir = findWorkspaceRoot()
|
|
102
|
+
if (!rootDir) {
|
|
103
|
+
error('Not in a Uniweb workspace. Run this command from a project directory.')
|
|
104
|
+
error('Use "uniweb create" to create a new project first.')
|
|
105
|
+
process.exit(1)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Read root package.json for project name
|
|
109
|
+
const rootPkg = JSON.parse(
|
|
110
|
+
await readFile(join(rootDir, 'package.json'), 'utf-8').catch(() => '{}')
|
|
111
|
+
)
|
|
112
|
+
const projectName = rootPkg.name || 'my-project'
|
|
113
|
+
|
|
114
|
+
switch (parsed.subcommand) {
|
|
115
|
+
case 'foundation':
|
|
116
|
+
await addFoundation(rootDir, projectName, parsed)
|
|
117
|
+
break
|
|
118
|
+
case 'site':
|
|
119
|
+
await addSite(rootDir, projectName, parsed)
|
|
120
|
+
break
|
|
121
|
+
case 'extension':
|
|
122
|
+
await addExtension(rootDir, projectName, parsed)
|
|
123
|
+
break
|
|
124
|
+
default:
|
|
125
|
+
error(`Unknown subcommand: ${parsed.subcommand}`)
|
|
126
|
+
log(`Valid subcommands: foundation, site, extension`)
|
|
127
|
+
process.exit(1)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Add a foundation to the workspace
|
|
133
|
+
*/
|
|
134
|
+
async function addFoundation(rootDir, projectName, opts) {
|
|
135
|
+
const name = opts.name
|
|
136
|
+
const target = await resolveFoundationTarget(rootDir, name, opts)
|
|
137
|
+
const fullPath = join(rootDir, target)
|
|
138
|
+
|
|
139
|
+
if (existsSync(fullPath)) {
|
|
140
|
+
error(`Directory already exists: ${target}`)
|
|
141
|
+
process.exit(1)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Scaffold
|
|
145
|
+
await scaffoldFoundation(fullPath, {
|
|
146
|
+
name: name || 'foundation',
|
|
147
|
+
projectName,
|
|
148
|
+
isExtension: false,
|
|
149
|
+
}, {
|
|
150
|
+
onProgress: (msg) => info(` ${msg}`),
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// Apply template content if --from specified
|
|
154
|
+
if (opts.from) {
|
|
155
|
+
await applyFromTemplate(opts.from, 'foundation', fullPath, projectName)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Update workspace globs
|
|
159
|
+
const glob = computeGlob(target, 'foundation')
|
|
160
|
+
await addWorkspaceGlob(rootDir, glob)
|
|
161
|
+
|
|
162
|
+
// Update root scripts
|
|
163
|
+
const sites = await discoverSites(rootDir)
|
|
164
|
+
await updateRootScripts(rootDir, sites)
|
|
165
|
+
|
|
166
|
+
success(`Created foundation '${name || 'foundation'}' at ${target}/`)
|
|
167
|
+
log('')
|
|
168
|
+
log(`Next: ${colors.cyan}pnpm install${colors.reset}`)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Add a site to the workspace
|
|
173
|
+
*/
|
|
174
|
+
async function addSite(rootDir, projectName, opts) {
|
|
175
|
+
const name = opts.name
|
|
176
|
+
const target = await resolveSiteTarget(rootDir, name, opts)
|
|
177
|
+
const fullPath = join(rootDir, target)
|
|
178
|
+
|
|
179
|
+
if (existsSync(fullPath)) {
|
|
180
|
+
error(`Directory already exists: ${target}`)
|
|
181
|
+
process.exit(1)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Resolve foundation
|
|
185
|
+
const foundation = await resolveFoundation(rootDir, opts.foundation)
|
|
186
|
+
if (!foundation) {
|
|
187
|
+
error('No foundation found. Add a foundation first: uniweb add foundation')
|
|
188
|
+
process.exit(1)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Compute relative path from site to foundation
|
|
192
|
+
const foundationPath = computeFoundationPath(target, foundation.path)
|
|
193
|
+
const siteName = name || 'site'
|
|
194
|
+
|
|
195
|
+
// Scaffold
|
|
196
|
+
await scaffoldSite(fullPath, {
|
|
197
|
+
name: siteName,
|
|
198
|
+
projectName,
|
|
199
|
+
foundationName: foundation.name,
|
|
200
|
+
foundationPath,
|
|
201
|
+
foundationRef: foundation.name,
|
|
202
|
+
}, {
|
|
203
|
+
onProgress: (msg) => info(` ${msg}`),
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// Apply template content if --from specified
|
|
207
|
+
if (opts.from) {
|
|
208
|
+
await applyFromTemplate(opts.from, 'site', fullPath, projectName)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Update workspace globs
|
|
212
|
+
const glob = computeGlob(target, 'site')
|
|
213
|
+
await addWorkspaceGlob(rootDir, glob)
|
|
214
|
+
|
|
215
|
+
// Update root scripts (discover sites after glob is added — includes the new one)
|
|
216
|
+
const sites = await discoverSites(rootDir)
|
|
217
|
+
// If the new site wasn't discovered (glob may not match yet), add it
|
|
218
|
+
if (!sites.find(s => s.path === target)) {
|
|
219
|
+
sites.push({ name: siteName, path: target })
|
|
220
|
+
}
|
|
221
|
+
await updateRootScripts(rootDir, sites)
|
|
222
|
+
|
|
223
|
+
success(`Created site '${siteName}' at ${target}/ → foundation '${foundation.name}'`)
|
|
224
|
+
log('')
|
|
225
|
+
log(`Next: ${colors.cyan}pnpm install && pnpm --filter ${siteName} dev${colors.reset}`)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Add an extension to the workspace
|
|
230
|
+
*/
|
|
231
|
+
async function addExtension(rootDir, projectName, opts) {
|
|
232
|
+
const name = opts.name
|
|
233
|
+
|
|
234
|
+
if (!name) {
|
|
235
|
+
error('Extension name is required: uniweb add extension <name>')
|
|
236
|
+
process.exit(1)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Determine target
|
|
240
|
+
let target
|
|
241
|
+
if (opts.path) {
|
|
242
|
+
target = opts.path
|
|
243
|
+
} else {
|
|
244
|
+
target = `extensions/${name}`
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const fullPath = join(rootDir, target)
|
|
248
|
+
|
|
249
|
+
if (existsSync(fullPath)) {
|
|
250
|
+
error(`Directory already exists: ${target}`)
|
|
251
|
+
process.exit(1)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Scaffold foundation with extension flag
|
|
255
|
+
await scaffoldFoundation(fullPath, {
|
|
256
|
+
name,
|
|
257
|
+
projectName,
|
|
258
|
+
isExtension: true,
|
|
259
|
+
}, {
|
|
260
|
+
onProgress: (msg) => info(` ${msg}`),
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
// Apply template content if --from specified
|
|
264
|
+
if (opts.from) {
|
|
265
|
+
await applyFromTemplate(opts.from, 'extension', fullPath, projectName)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Update workspace globs
|
|
269
|
+
await addWorkspaceGlob(rootDir, 'extensions/*')
|
|
270
|
+
|
|
271
|
+
// Wire extension to site if specified (or only one site exists)
|
|
272
|
+
let wiredSite = null
|
|
273
|
+
if (opts.site) {
|
|
274
|
+
wiredSite = await wireExtensionToSite(rootDir, opts.site, name, target)
|
|
275
|
+
} else {
|
|
276
|
+
const sites = await discoverSites(rootDir)
|
|
277
|
+
if (sites.length === 1) {
|
|
278
|
+
wiredSite = await wireExtensionToSite(rootDir, sites[0].name, name, target)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Update root scripts
|
|
283
|
+
const sites = await discoverSites(rootDir)
|
|
284
|
+
await updateRootScripts(rootDir, sites)
|
|
285
|
+
|
|
286
|
+
let msg = `Created extension '${name}' at ${target}/`
|
|
287
|
+
if (wiredSite) {
|
|
288
|
+
msg += ` → wired to site '${wiredSite}'`
|
|
289
|
+
}
|
|
290
|
+
success(msg)
|
|
291
|
+
log('')
|
|
292
|
+
log(`Next: ${colors.cyan}pnpm install${colors.reset}`)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Resolve placement for a foundation
|
|
297
|
+
*/
|
|
298
|
+
async function resolveFoundationTarget(rootDir, name, opts) {
|
|
299
|
+
if (opts.path) return opts.path
|
|
300
|
+
|
|
301
|
+
if (opts.project) {
|
|
302
|
+
return `${opts.project}/foundation`
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Check existing layout
|
|
306
|
+
const { packages } = await readWorkspaceConfig(rootDir)
|
|
307
|
+
const hasColocated = packages.some(p => p.includes('*/foundation'))
|
|
308
|
+
const hasFoundationsGlob = packages.some(p => p.startsWith('foundations/'))
|
|
309
|
+
const hasSingleFoundation = existsSync(join(rootDir, 'foundation'))
|
|
310
|
+
|
|
311
|
+
if (hasColocated && opts.project) {
|
|
312
|
+
return `${opts.project}/foundation`
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// No name and no foundations exist → ./foundation/
|
|
316
|
+
if (!name && !hasSingleFoundation && !hasFoundationsGlob) {
|
|
317
|
+
return 'foundation'
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Named foundation or existing foundation → ./foundations/{name}/
|
|
321
|
+
return `foundations/${name || 'foundation'}`
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Resolve placement for a site
|
|
326
|
+
*/
|
|
327
|
+
async function resolveSiteTarget(rootDir, name, opts) {
|
|
328
|
+
if (opts.path) return opts.path
|
|
329
|
+
|
|
330
|
+
if (opts.project) {
|
|
331
|
+
return `${opts.project}/site`
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const { packages } = await readWorkspaceConfig(rootDir)
|
|
335
|
+
const hasColocated = packages.some(p => p.includes('*/site'))
|
|
336
|
+
const hasSitesGlob = packages.some(p => p.startsWith('sites/'))
|
|
337
|
+
const hasSingleSite = existsSync(join(rootDir, 'site'))
|
|
338
|
+
|
|
339
|
+
if (hasColocated && opts.project) {
|
|
340
|
+
return `${opts.project}/site`
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// No name and no sites exist → ./site/
|
|
344
|
+
if (!name && !hasSingleSite && !hasSitesGlob) {
|
|
345
|
+
return 'site'
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Named site or existing site → ./sites/{name}/
|
|
349
|
+
return `sites/${name || 'site'}`
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Resolve which foundation to wire a site to
|
|
354
|
+
*/
|
|
355
|
+
async function resolveFoundation(rootDir, foundationFlag) {
|
|
356
|
+
const foundations = await discoverFoundations(rootDir)
|
|
357
|
+
|
|
358
|
+
if (foundationFlag) {
|
|
359
|
+
// Find by name
|
|
360
|
+
const found = foundations.find(f => f.name === foundationFlag)
|
|
361
|
+
if (found) return found
|
|
362
|
+
|
|
363
|
+
// Not found — could be a URL or new foundation
|
|
364
|
+
error(`Foundation '${foundationFlag}' not found in workspace.`)
|
|
365
|
+
log(`Available foundations: ${foundations.map(f => f.name).join(', ') || 'none'}`)
|
|
366
|
+
process.exit(1)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (foundations.length === 0) {
|
|
370
|
+
return null
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (foundations.length === 1) {
|
|
374
|
+
info(`Using foundation: ${foundations[0].name}`)
|
|
375
|
+
return foundations[0]
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Multiple foundations — prompt
|
|
379
|
+
const response = await prompts({
|
|
380
|
+
type: 'select',
|
|
381
|
+
name: 'foundation',
|
|
382
|
+
message: 'Which foundation should this site use?',
|
|
383
|
+
choices: foundations.map(f => ({
|
|
384
|
+
title: f.name,
|
|
385
|
+
description: f.path,
|
|
386
|
+
value: f,
|
|
387
|
+
})),
|
|
388
|
+
}, {
|
|
389
|
+
onCancel: () => {
|
|
390
|
+
log('\nCancelled.')
|
|
391
|
+
process.exit(0)
|
|
392
|
+
},
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
return response.foundation
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Compute the file: path from site to foundation
|
|
400
|
+
*/
|
|
401
|
+
function computeFoundationPath(sitePath, foundationPath) {
|
|
402
|
+
// Compute relative path from site dir to foundation dir
|
|
403
|
+
const rel = relative(sitePath, foundationPath)
|
|
404
|
+
return `file:${rel}`
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Compute the appropriate glob pattern for a target directory
|
|
409
|
+
*/
|
|
410
|
+
function computeGlob(target, type) {
|
|
411
|
+
// e.g., "foundation" → "foundation"
|
|
412
|
+
// e.g., "foundations/marketing" → "foundations/*"
|
|
413
|
+
// e.g., "docs/foundation" → "*/foundation"
|
|
414
|
+
// e.g., "lib/mktg" → "lib/mktg"
|
|
415
|
+
|
|
416
|
+
const parts = target.split('/')
|
|
417
|
+
|
|
418
|
+
if (parts.length === 1) {
|
|
419
|
+
// Direct: "foundation", "site"
|
|
420
|
+
return target
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (parts.length === 2) {
|
|
424
|
+
// Could be "foundations/marketing" or "docs/foundation"
|
|
425
|
+
if (parts[1] === type) {
|
|
426
|
+
// Co-located: "docs/foundation" → "*/foundation"
|
|
427
|
+
return `*/${type}`
|
|
428
|
+
}
|
|
429
|
+
// Plural container: "foundations/marketing" → "foundations/*"
|
|
430
|
+
return `${parts[0]}/*`
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Custom path — return as-is
|
|
434
|
+
return target
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Apply content from a template to a scaffolded package
|
|
439
|
+
*
|
|
440
|
+
* @param {string} templateId - Template identifier (official name, local path, npm, github)
|
|
441
|
+
* @param {string} packageType - 'foundation', 'site', or 'extension'
|
|
442
|
+
* @param {string} targetDir - Absolute path to the scaffolded package
|
|
443
|
+
* @param {string} projectName - Project name for template context
|
|
444
|
+
*/
|
|
445
|
+
async function applyFromTemplate(templateId, packageType, targetDir, projectName) {
|
|
446
|
+
info(`Resolving template: ${templateId}...`)
|
|
447
|
+
|
|
448
|
+
const resolved = await resolveTemplate(templateId, {
|
|
449
|
+
onProgress: (msg) => info(` ${msg}`),
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
const metadata = await validateTemplate(resolved.path, {})
|
|
454
|
+
|
|
455
|
+
// Look in contentDirs for matching package type
|
|
456
|
+
let contentDir = null
|
|
457
|
+
const match = metadata.contentDirs.find(d => d.type === packageType) ||
|
|
458
|
+
metadata.contentDirs.find(d => d.name === packageType)
|
|
459
|
+
if (match) {
|
|
460
|
+
contentDir = match.dir
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (contentDir) {
|
|
464
|
+
info(`Applying ${metadata.name} content...`)
|
|
465
|
+
await applyContent(contentDir, targetDir, {
|
|
466
|
+
projectName,
|
|
467
|
+
versions: getVersionsForTemplates(),
|
|
468
|
+
}, {
|
|
469
|
+
onProgress: (msg) => info(` ${msg}`),
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
// If site content applied, inform about expected section types
|
|
473
|
+
if (packageType === 'site' && metadata.components) {
|
|
474
|
+
log('')
|
|
475
|
+
info(`This template expects section types: ${metadata.components.join(', ')}`)
|
|
476
|
+
info(`Make sure your foundation provides them.`)
|
|
477
|
+
}
|
|
478
|
+
} else {
|
|
479
|
+
info(`Template '${metadata.name}' has no ${packageType} content to apply.`)
|
|
480
|
+
}
|
|
481
|
+
} finally {
|
|
482
|
+
if (resolved.cleanup) await resolved.cleanup()
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Wire an extension URL to a site's site.yml
|
|
488
|
+
*/
|
|
489
|
+
async function wireExtensionToSite(rootDir, siteName, extensionName, extensionPath) {
|
|
490
|
+
// Find the site directory
|
|
491
|
+
const sites = await discoverSites(rootDir)
|
|
492
|
+
const site = sites.find(s => s.name === siteName)
|
|
493
|
+
if (!site) {
|
|
494
|
+
info(`Could not find site '${siteName}' to wire extension.`)
|
|
495
|
+
return null
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const siteYmlPath = join(rootDir, site.path, 'site.yml')
|
|
499
|
+
if (!existsSync(siteYmlPath)) {
|
|
500
|
+
info(`No site.yml found at ${site.path}`)
|
|
501
|
+
return null
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
try {
|
|
505
|
+
const content = await readFile(siteYmlPath, 'utf-8')
|
|
506
|
+
const config = yaml.load(content) || {}
|
|
507
|
+
|
|
508
|
+
// Add extension URL
|
|
509
|
+
const extensionUrl = `/${extensionPath}/dist/foundation.js`
|
|
510
|
+
if (!config.extensions) {
|
|
511
|
+
config.extensions = []
|
|
512
|
+
}
|
|
513
|
+
if (!config.extensions.includes(extensionUrl)) {
|
|
514
|
+
config.extensions.push(extensionUrl)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
await writeFile(siteYmlPath, yaml.dump(config, { flowLevel: -1, quotingType: "'" }))
|
|
518
|
+
return siteName
|
|
519
|
+
} catch (err) {
|
|
520
|
+
info(`Warning: Could not update site.yml: ${err.message}`)
|
|
521
|
+
return null
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Show help for the add command
|
|
527
|
+
*/
|
|
528
|
+
function showAddHelp() {
|
|
529
|
+
log(`
|
|
530
|
+
${colors.cyan}${colors.bright}Uniweb Add${colors.reset}
|
|
531
|
+
|
|
532
|
+
Add foundations, sites, or extensions to your workspace.
|
|
533
|
+
|
|
534
|
+
${colors.bright}Usage:${colors.reset}
|
|
535
|
+
uniweb add foundation [name] [options]
|
|
536
|
+
uniweb add site [name] [options]
|
|
537
|
+
uniweb add extension <name> [options]
|
|
538
|
+
|
|
539
|
+
${colors.bright}Common Options:${colors.reset}
|
|
540
|
+
--from <template> Apply content from a template after scaffolding
|
|
541
|
+
--path <dir> Custom directory for the package
|
|
542
|
+
|
|
543
|
+
${colors.bright}Foundation Options:${colors.reset}
|
|
544
|
+
--project <name> Group under a project directory (co-located layout)
|
|
545
|
+
|
|
546
|
+
${colors.bright}Site Options:${colors.reset}
|
|
547
|
+
--foundation <n> Foundation to wire to (prompted if multiple exist)
|
|
548
|
+
--project <name> Group under a project directory (co-located layout)
|
|
549
|
+
|
|
550
|
+
${colors.bright}Extension Options:${colors.reset}
|
|
551
|
+
--site <name> Site to wire extension URL into
|
|
552
|
+
|
|
553
|
+
${colors.bright}Examples:${colors.reset}
|
|
554
|
+
uniweb add foundation # Create ./foundation/
|
|
555
|
+
uniweb add foundation marketing # Create ./foundations/marketing/
|
|
556
|
+
uniweb add foundation marketing --from marketing # Scaffold + marketing sections
|
|
557
|
+
uniweb add site blog --foundation marketing # Create ./sites/blog/ wired to marketing
|
|
558
|
+
uniweb add site blog --from docs --foundation blog # Scaffold + docs pages
|
|
559
|
+
uniweb add extension effects --site site # Create ./extensions/effects/
|
|
560
|
+
uniweb add foundation --project docs # Create ./docs/foundation/ (co-located)
|
|
561
|
+
uniweb add site --project docs # Create ./docs/site/ (co-located)
|
|
562
|
+
`)
|
|
563
|
+
}
|
package/src/commands/build.js
CHANGED
|
@@ -496,11 +496,27 @@ async function buildSite(projectDir, options = {}) {
|
|
|
496
496
|
function isFoundation(dir) {
|
|
497
497
|
// Primary: has foundation.js config
|
|
498
498
|
if (existsSync(join(dir, 'src', 'foundation.js'))) return true
|
|
499
|
-
// Fallback: has src/
|
|
499
|
+
// Fallback: has src/sections/
|
|
500
|
+
if (existsSync(join(dir, 'src', 'sections'))) return true
|
|
501
|
+
// Legacy fallback: has src/components/
|
|
500
502
|
if (existsSync(join(dir, 'src', 'components'))) return true
|
|
501
503
|
return false
|
|
502
504
|
}
|
|
503
505
|
|
|
506
|
+
/**
|
|
507
|
+
* Check if a foundation directory declares extension: true in foundation.js
|
|
508
|
+
*/
|
|
509
|
+
function isExtensionDir(dir) {
|
|
510
|
+
const filePath = join(dir, 'src', 'foundation.js')
|
|
511
|
+
if (!existsSync(filePath)) return false
|
|
512
|
+
try {
|
|
513
|
+
const content = readFileSync(filePath, 'utf8')
|
|
514
|
+
return /extension\s*:\s*true/.test(content)
|
|
515
|
+
} catch {
|
|
516
|
+
return false
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
504
520
|
/**
|
|
505
521
|
* Check if a directory is a site
|
|
506
522
|
*/
|
|
@@ -517,6 +533,7 @@ function isSite(dir) {
|
|
|
517
533
|
*/
|
|
518
534
|
function discoverWorkspacePackages(workspaceDir) {
|
|
519
535
|
const foundations = []
|
|
536
|
+
const extensions = []
|
|
520
537
|
const sites = []
|
|
521
538
|
|
|
522
539
|
// Check standard locations
|
|
@@ -553,7 +570,18 @@ function discoverWorkspacePackages(workspaceDir) {
|
|
|
553
570
|
}
|
|
554
571
|
}
|
|
555
572
|
|
|
556
|
-
|
|
573
|
+
// Check extensions/*
|
|
574
|
+
const extensionsDir = join(workspaceDir, 'extensions')
|
|
575
|
+
if (existsSync(extensionsDir)) {
|
|
576
|
+
for (const name of readdirSync(extensionsDir)) {
|
|
577
|
+
const path = join(extensionsDir, name)
|
|
578
|
+
if (isFoundation(path)) {
|
|
579
|
+
extensions.push({ name, path })
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return { foundations, extensions, sites }
|
|
557
585
|
}
|
|
558
586
|
|
|
559
587
|
/**
|
|
@@ -565,13 +593,14 @@ async function buildWorkspace(workspaceDir, options = {}) {
|
|
|
565
593
|
log(`${colors.cyan}${colors.bright}Building workspace...${colors.reset}`)
|
|
566
594
|
log('')
|
|
567
595
|
|
|
568
|
-
const { foundations, sites } = discoverWorkspacePackages(workspaceDir)
|
|
596
|
+
const { foundations, extensions, sites } = discoverWorkspacePackages(workspaceDir)
|
|
569
597
|
|
|
570
|
-
if (foundations.length === 0 && sites.length === 0) {
|
|
571
|
-
error('No foundations or sites found in workspace')
|
|
598
|
+
if (foundations.length === 0 && extensions.length === 0 && sites.length === 0) {
|
|
599
|
+
error('No foundations, extensions, or sites found in workspace')
|
|
572
600
|
log('')
|
|
573
601
|
log('Expected structure:')
|
|
574
602
|
log(' foundation/ or foundations/*/')
|
|
603
|
+
log(' extensions/*/')
|
|
575
604
|
log(' site/ or sites/*/')
|
|
576
605
|
process.exit(1)
|
|
577
606
|
}
|
|
@@ -583,6 +612,14 @@ async function buildWorkspace(workspaceDir, options = {}) {
|
|
|
583
612
|
log('')
|
|
584
613
|
}
|
|
585
614
|
|
|
615
|
+
// Build extensions (they are foundations, but logged distinctly)
|
|
616
|
+
for (const extension of extensions) {
|
|
617
|
+
const label = isExtensionDir(extension.path) ? 'extension' : 'foundation'
|
|
618
|
+
log(`${colors.bright}[${extension.name}]${colors.reset} ${colors.dim}(${label})${colors.reset}`)
|
|
619
|
+
await buildFoundation(extension.path)
|
|
620
|
+
log('')
|
|
621
|
+
}
|
|
622
|
+
|
|
586
623
|
// Build sites
|
|
587
624
|
for (const site of sites) {
|
|
588
625
|
log(`${colors.bright}[${site.name}]${colors.reset}`)
|
|
@@ -602,9 +639,15 @@ async function buildWorkspace(workspaceDir, options = {}) {
|
|
|
602
639
|
}
|
|
603
640
|
|
|
604
641
|
// Summary
|
|
642
|
+
const totalBuilt = foundations.length + extensions.length + sites.length
|
|
643
|
+
const parts = []
|
|
644
|
+
if (foundations.length > 0) parts.push(`${foundations.length} foundation(s)`)
|
|
645
|
+
if (extensions.length > 0) parts.push(`${extensions.length} extension(s)`)
|
|
646
|
+
if (sites.length > 0) parts.push(`${sites.length} site(s)`)
|
|
647
|
+
|
|
605
648
|
log(`${colors.green}${colors.bright}Workspace build complete!${colors.reset}`)
|
|
606
649
|
log('')
|
|
607
|
-
log(`Built ${
|
|
650
|
+
log(`Built ${parts.join(', ')}`)
|
|
608
651
|
}
|
|
609
652
|
|
|
610
653
|
/**
|