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
|
@@ -19,9 +19,6 @@ let partialsRegistered = false
|
|
|
19
19
|
// Store for version data (set by registerVersions)
|
|
20
20
|
let versionData = {}
|
|
21
21
|
|
|
22
|
-
// Track missing versions during processing
|
|
23
|
-
const missingVersions = new Set()
|
|
24
|
-
|
|
25
22
|
// Default fallback version when a package version is unknown
|
|
26
23
|
const DEFAULT_FALLBACK_VERSION = '^0.1.0'
|
|
27
24
|
|
|
@@ -32,23 +29,6 @@ const DEFAULT_FALLBACK_VERSION = '^0.1.0'
|
|
|
32
29
|
*/
|
|
33
30
|
export function registerVersions(versions) {
|
|
34
31
|
versionData = versions || {}
|
|
35
|
-
missingVersions.clear()
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Get the list of missing versions encountered during processing
|
|
40
|
-
*
|
|
41
|
-
* @returns {string[]} Array of package names that were missing versions
|
|
42
|
-
*/
|
|
43
|
-
export function getMissingVersions() {
|
|
44
|
-
return [...missingVersions]
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Clear the missing versions set
|
|
49
|
-
*/
|
|
50
|
-
export function clearMissingVersions() {
|
|
51
|
-
missingVersions.clear()
|
|
52
32
|
}
|
|
53
33
|
|
|
54
34
|
/**
|
|
@@ -100,8 +80,6 @@ Handlebars.registerHelper('version', function(packageName) {
|
|
|
100
80
|
return versionData[`@uniweb/${packageName}`]
|
|
101
81
|
}
|
|
102
82
|
|
|
103
|
-
// Track the missing version and return a fallback
|
|
104
|
-
missingVersions.add(packageName)
|
|
105
83
|
return DEFAULT_FALLBACK_VERSION
|
|
106
84
|
})
|
|
107
85
|
|
|
@@ -195,78 +173,29 @@ async function processFile(sourcePath, targetPath, data, options = {}) {
|
|
|
195
173
|
* @param {string} targetPath - Destination directory
|
|
196
174
|
* @param {Object} data - Template variables
|
|
197
175
|
* @param {Object} options - Processing options
|
|
198
|
-
* @param {string|null} options.variant - Template variant to use
|
|
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)
|
|
201
176
|
* @param {Function} options.onWarning - Warning callback
|
|
202
177
|
* @param {Function} options.onProgress - Progress callback
|
|
203
178
|
*/
|
|
204
179
|
export async function copyTemplateDirectory(sourcePath, targetPath, data, options = {}) {
|
|
205
|
-
const {
|
|
206
|
-
|
|
207
|
-
// If a base template is specified, copy it first (with isBase=true so main can overwrite)
|
|
208
|
-
if (basePath && existsSync(basePath)) {
|
|
209
|
-
await copyTemplateDirectory(basePath, targetPath, data, { variant, isBase: true, onWarning, onProgress })
|
|
210
|
-
}
|
|
180
|
+
const { onWarning, onProgress } = options
|
|
211
181
|
|
|
212
182
|
await fs.mkdir(targetPath, { recursive: true })
|
|
213
183
|
const entries = await fs.readdir(sourcePath, { withFileTypes: true })
|
|
214
184
|
|
|
215
|
-
// Build a set of base names that have variant-specific directories
|
|
216
|
-
const variantBases = new Set()
|
|
217
|
-
for (const entry of entries) {
|
|
218
|
-
if (entry.isDirectory()) {
|
|
219
|
-
const variantMatch = entry.name.match(/^(.+)\.([^.]+)$/)
|
|
220
|
-
if (variantMatch) {
|
|
221
|
-
variantBases.add(variantMatch[1]) // e.g., 'foundation' from 'foundation.tailwind4'
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Options for recursive calls (without basePath to avoid re-copying base at each level)
|
|
227
|
-
const recursionOptions = { variant, isBase, onWarning, onProgress }
|
|
228
|
-
|
|
229
185
|
for (const entry of entries) {
|
|
230
186
|
const sourceName = entry.name
|
|
231
187
|
|
|
232
|
-
// Check if this is a variant-specific item (e.g., "dir.variant")
|
|
233
|
-
const variantMatch = entry.isDirectory()
|
|
234
|
-
? sourceName.match(/^(.+)\.([^.]+)$/)
|
|
235
|
-
: null
|
|
236
|
-
|
|
237
188
|
if (entry.isDirectory()) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (dirVariant !== variant) {
|
|
248
|
-
continue
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Use the base name without variant suffix for the target
|
|
252
|
-
const sourceFullPath = path.join(sourcePath, sourceName)
|
|
253
|
-
const targetFullPath = path.join(targetPath, baseName)
|
|
254
|
-
|
|
255
|
-
await copyTemplateDirectory(sourceFullPath, targetFullPath, data, recursionOptions)
|
|
256
|
-
} else {
|
|
257
|
-
// Regular directory - skip if a variant override exists and we're using that variant
|
|
258
|
-
if (variant && variantBases.has(sourceName)) {
|
|
259
|
-
// Skip this directory because a variant-specific version exists
|
|
260
|
-
continue
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const sourceFullPath = path.join(sourcePath, sourceName)
|
|
264
|
-
const targetFullPath = path.join(targetPath, sourceName)
|
|
265
|
-
|
|
266
|
-
await copyTemplateDirectory(sourceFullPath, targetFullPath, data, recursionOptions)
|
|
267
|
-
}
|
|
189
|
+
const sourceFullPath = path.join(sourcePath, sourceName)
|
|
190
|
+
// Rename _prefix directories to .prefix (e.g., _vscode → .vscode)
|
|
191
|
+
// This allows dotfile directories to be committed without being gitignored
|
|
192
|
+
const targetName = sourceName.startsWith('_') && !sourceName.startsWith('__')
|
|
193
|
+
? `.${sourceName.slice(1)}`
|
|
194
|
+
: sourceName
|
|
195
|
+
const targetFullPath = path.join(targetPath, targetName)
|
|
196
|
+
|
|
197
|
+
await copyTemplateDirectory(sourceFullPath, targetFullPath, data, { onWarning, onProgress })
|
|
268
198
|
} else {
|
|
269
|
-
// File processing
|
|
270
199
|
// Skip template.json as it's metadata for the template, not for the output
|
|
271
200
|
if (sourceName === 'template.json') {
|
|
272
201
|
continue
|
|
@@ -280,12 +209,6 @@ export async function copyTemplateDirectory(sourcePath, targetPath, data, option
|
|
|
280
209
|
const sourceFullPath = path.join(sourcePath, sourceName)
|
|
281
210
|
const targetFullPath = path.join(targetPath, targetName)
|
|
282
211
|
|
|
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)) {
|
|
286
|
-
continue
|
|
287
|
-
}
|
|
288
|
-
|
|
289
212
|
if (onProgress) {
|
|
290
213
|
onProgress(`Creating ${targetName}`)
|
|
291
214
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Template resolver - parses template identifiers and determines source type
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
// Built-in templates (file-based
|
|
6
|
-
export const BUILTIN_TEMPLATES = ['
|
|
5
|
+
// Built-in templates (programmatic, not file-based)
|
|
6
|
+
export const BUILTIN_TEMPLATES = ['blank']
|
|
7
7
|
|
|
8
8
|
// Official templates from @uniweb/templates package (downloaded from GitHub releases)
|
|
9
9
|
export const OFFICIAL_TEMPLATES = ['marketing', 'academic', 'docs', 'international', 'dynamic', 'store', 'extensions']
|
|
@@ -11,7 +11,7 @@ export const OFFICIAL_TEMPLATES = ['marketing', 'academic', 'docs', 'internation
|
|
|
11
11
|
/**
|
|
12
12
|
* Parse a template identifier and determine its source type
|
|
13
13
|
*
|
|
14
|
-
* @param {string} identifier - Template identifier (e.g., '
|
|
14
|
+
* @param {string} identifier - Template identifier (e.g., 'blank', 'marketing', 'github:user/repo')
|
|
15
15
|
* @returns {Object} Parsed template info
|
|
16
16
|
*/
|
|
17
17
|
export function parseTemplateId(identifier) {
|
|
@@ -91,7 +91,7 @@ export class ValidationError extends Error {
|
|
|
91
91
|
export const ErrorCodes = {
|
|
92
92
|
MISSING_TEMPLATE_JSON: 'MISSING_TEMPLATE_JSON',
|
|
93
93
|
INVALID_TEMPLATE_JSON: 'INVALID_TEMPLATE_JSON',
|
|
94
|
-
|
|
94
|
+
MISSING_CONTENT_DIR: 'MISSING_CONTENT_DIR',
|
|
95
95
|
MISSING_REQUIRED_FIELD: 'MISSING_REQUIRED_FIELD',
|
|
96
96
|
VERSION_MISMATCH: 'VERSION_MISMATCH',
|
|
97
97
|
}
|
|
@@ -139,34 +139,76 @@ export async function validateTemplate(templateRoot, options = {}) {
|
|
|
139
139
|
)
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
// Check
|
|
143
|
-
const
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
`Missing template/ directory in ${templateRoot}`,
|
|
147
|
-
ErrorCodes.MISSING_TEMPLATE_DIR,
|
|
148
|
-
{ path: templateRoot }
|
|
149
|
-
)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Check version compatibility
|
|
153
|
-
if (uniwebVersion && metadata.uniweb) {
|
|
154
|
-
if (!satisfiesVersion(uniwebVersion, metadata.uniweb)) {
|
|
142
|
+
// Check version compatibility (support both `compatible` and `uniweb` fields)
|
|
143
|
+
const versionRange = metadata.compatible || metadata.uniweb
|
|
144
|
+
if (uniwebVersion && versionRange) {
|
|
145
|
+
if (!satisfiesVersion(uniwebVersion, versionRange)) {
|
|
155
146
|
throw new ValidationError(
|
|
156
|
-
`Template requires Uniweb ${
|
|
147
|
+
`Template requires Uniweb ${versionRange}, but current version is ${uniwebVersion}`,
|
|
157
148
|
ErrorCodes.VERSION_MISMATCH,
|
|
158
|
-
{ required:
|
|
149
|
+
{ required: versionRange, current: uniwebVersion }
|
|
159
150
|
)
|
|
160
151
|
}
|
|
161
152
|
}
|
|
162
153
|
|
|
154
|
+
// Format 2: content template — foundation/ and/or site/ directories alongside template.json
|
|
155
|
+
const contentDirs = resolveContentDirs(templateRoot, metadata)
|
|
156
|
+
|
|
157
|
+
if (contentDirs.length === 0) {
|
|
158
|
+
throw new ValidationError(
|
|
159
|
+
`No content directories found in ${templateRoot}. Templates need foundation/ and/or site/ directories alongside template.json.`,
|
|
160
|
+
ErrorCodes.MISSING_CONTENT_DIR,
|
|
161
|
+
{ path: templateRoot }
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
163
165
|
return {
|
|
164
166
|
...metadata,
|
|
165
|
-
|
|
167
|
+
format: 2,
|
|
168
|
+
contentDirs,
|
|
166
169
|
metadataPath
|
|
167
170
|
}
|
|
168
171
|
}
|
|
169
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Resolve content directories from a format 2 template
|
|
175
|
+
*
|
|
176
|
+
* @param {string} templateRoot - Root of the template (contains template.json)
|
|
177
|
+
* @param {Object} metadata - Parsed template.json
|
|
178
|
+
* @returns {Array<Object>} Content directories: [{ type, name, dir, foundation? }]
|
|
179
|
+
*/
|
|
180
|
+
export function resolveContentDirs(templateRoot, metadata) {
|
|
181
|
+
const dirs = []
|
|
182
|
+
|
|
183
|
+
if (metadata.packages) {
|
|
184
|
+
// Multi-package template: iterate declared packages
|
|
185
|
+
for (const pkg of metadata.packages) {
|
|
186
|
+
const dir = path.join(templateRoot, pkg.name)
|
|
187
|
+
if (existsSync(dir)) {
|
|
188
|
+
dirs.push({
|
|
189
|
+
type: pkg.type,
|
|
190
|
+
name: pkg.name,
|
|
191
|
+
dir,
|
|
192
|
+
...(pkg.foundation ? { foundation: pkg.foundation } : {}),
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
// Standard template: look for foundation/ and site/
|
|
198
|
+
const foundationDir = path.join(templateRoot, 'foundation')
|
|
199
|
+
if (existsSync(foundationDir)) {
|
|
200
|
+
dirs.push({ type: 'foundation', name: 'foundation', dir: foundationDir })
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const siteDir = path.join(templateRoot, 'site')
|
|
204
|
+
if (existsSync(siteDir)) {
|
|
205
|
+
dirs.push({ type: 'site', name: 'site', dir: siteDir })
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return dirs
|
|
210
|
+
}
|
|
211
|
+
|
|
170
212
|
/**
|
|
171
213
|
* Get list of available templates in a templates directory
|
|
172
214
|
*
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Config Management
|
|
3
|
+
*
|
|
4
|
+
* Read/write pnpm-workspace.yaml and root package.json.
|
|
5
|
+
* Used by both `create` and `add` commands.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from 'node:fs'
|
|
9
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
10
|
+
import { join } from 'node:path'
|
|
11
|
+
import yaml from 'js-yaml'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Read pnpm-workspace.yaml
|
|
15
|
+
* @param {string} rootDir - Workspace root directory
|
|
16
|
+
* @returns {Promise<{packages: string[]}>}
|
|
17
|
+
*/
|
|
18
|
+
export async function readWorkspaceConfig(rootDir) {
|
|
19
|
+
const configPath = join(rootDir, 'pnpm-workspace.yaml')
|
|
20
|
+
if (!existsSync(configPath)) {
|
|
21
|
+
return { packages: [] }
|
|
22
|
+
}
|
|
23
|
+
const content = await readFile(configPath, 'utf-8')
|
|
24
|
+
const config = yaml.load(content)
|
|
25
|
+
return { packages: config?.packages || [] }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Write pnpm-workspace.yaml
|
|
30
|
+
* @param {string} rootDir - Workspace root directory
|
|
31
|
+
* @param {{packages: string[]}} config
|
|
32
|
+
*/
|
|
33
|
+
export async function writeWorkspaceConfig(rootDir, config) {
|
|
34
|
+
const configPath = join(rootDir, 'pnpm-workspace.yaml')
|
|
35
|
+
const content = yaml.dump(config, { flowLevel: -1, quotingType: '"' })
|
|
36
|
+
await writeFile(configPath, content)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Add a glob pattern to pnpm-workspace.yaml if not already present
|
|
41
|
+
* @param {string} rootDir - Workspace root directory
|
|
42
|
+
* @param {string} glob - Glob pattern to add
|
|
43
|
+
*/
|
|
44
|
+
export async function addWorkspaceGlob(rootDir, glob) {
|
|
45
|
+
const config = await readWorkspaceConfig(rootDir)
|
|
46
|
+
if (!config.packages.includes(glob)) {
|
|
47
|
+
config.packages.push(glob)
|
|
48
|
+
await writeWorkspaceConfig(rootDir, config)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Read root package.json
|
|
54
|
+
* @param {string} rootDir - Workspace root directory
|
|
55
|
+
* @returns {Promise<Object>}
|
|
56
|
+
*/
|
|
57
|
+
export async function readRootPackageJson(rootDir) {
|
|
58
|
+
const pkgPath = join(rootDir, 'package.json')
|
|
59
|
+
if (!existsSync(pkgPath)) {
|
|
60
|
+
return {}
|
|
61
|
+
}
|
|
62
|
+
return JSON.parse(await readFile(pkgPath, 'utf-8'))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Write root package.json (2-space indent)
|
|
67
|
+
* @param {string} rootDir - Workspace root directory
|
|
68
|
+
* @param {Object} pkg - Package.json object
|
|
69
|
+
*/
|
|
70
|
+
export async function writeRootPackageJson(rootDir, pkg) {
|
|
71
|
+
const pkgPath = join(rootDir, 'package.json')
|
|
72
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Compute root scripts based on discovered sites
|
|
77
|
+
* @param {Array<{name: string, path: string}>} sites - Discovered sites
|
|
78
|
+
* @returns {Object} Scripts object for package.json
|
|
79
|
+
*/
|
|
80
|
+
export function computeRootScripts(sites) {
|
|
81
|
+
const scripts = {
|
|
82
|
+
build: 'uniweb build',
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (sites.length === 0) {
|
|
86
|
+
return scripts
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (sites.length === 1) {
|
|
90
|
+
scripts.dev = `pnpm --filter ${sites[0].name} dev`
|
|
91
|
+
scripts.preview = `pnpm --filter ${sites[0].name} preview`
|
|
92
|
+
} else {
|
|
93
|
+
// First site gets unqualified dev/preview
|
|
94
|
+
scripts.dev = `pnpm --filter ${sites[0].name} dev`
|
|
95
|
+
scripts.preview = `pnpm --filter ${sites[0].name} preview`
|
|
96
|
+
|
|
97
|
+
// Subsequent sites get qualified dev:{name}/preview:{name}
|
|
98
|
+
for (let i = 1; i < sites.length; i++) {
|
|
99
|
+
scripts[`dev:${sites[i].name}`] = `pnpm --filter ${sites[i].name} dev`
|
|
100
|
+
scripts[`preview:${sites[i].name}`] = `pnpm --filter ${sites[i].name} preview`
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return scripts
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Update root scripts after adding a new site
|
|
109
|
+
* @param {string} rootDir - Workspace root directory
|
|
110
|
+
* @param {Array<{name: string, path: string}>} sites - All sites (including new one)
|
|
111
|
+
*/
|
|
112
|
+
export async function updateRootScripts(rootDir, sites) {
|
|
113
|
+
const pkg = await readRootPackageJson(rootDir)
|
|
114
|
+
const newScripts = computeRootScripts(sites)
|
|
115
|
+
|
|
116
|
+
// If we're adding a second site, rename existing dev/preview to dev:{firstName}
|
|
117
|
+
if (sites.length === 2 && pkg.scripts?.dev) {
|
|
118
|
+
const firstName = sites[0].name
|
|
119
|
+
// Only rename if the existing dev matches the first site
|
|
120
|
+
if (pkg.scripts.dev === `pnpm --filter ${firstName} dev`) {
|
|
121
|
+
pkg.scripts[`dev:${firstName}`] = pkg.scripts.dev
|
|
122
|
+
pkg.scripts[`preview:${firstName}`] = pkg.scripts.preview
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
pkg.scripts = { ...pkg.scripts, ...newScripts }
|
|
127
|
+
await writeRootPackageJson(rootDir, pkg)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Discover foundations in the workspace
|
|
132
|
+
* @param {string} rootDir - Workspace root directory
|
|
133
|
+
* @returns {Promise<Array<{name: string, path: string}>>}
|
|
134
|
+
*/
|
|
135
|
+
export async function discoverFoundations(rootDir) {
|
|
136
|
+
const { packages } = await readWorkspaceConfig(rootDir)
|
|
137
|
+
const foundations = []
|
|
138
|
+
|
|
139
|
+
for (const pattern of packages) {
|
|
140
|
+
const dirs = await resolveGlob(rootDir, pattern)
|
|
141
|
+
for (const dir of dirs) {
|
|
142
|
+
const pkgPath = join(rootDir, dir, 'package.json')
|
|
143
|
+
if (!existsSync(pkgPath)) continue
|
|
144
|
+
try {
|
|
145
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'))
|
|
146
|
+
// Foundation: has @uniweb/build in devDeps but NOT @uniweb/runtime in deps
|
|
147
|
+
if (pkg.devDependencies?.['@uniweb/build'] && !pkg.dependencies?.['@uniweb/runtime']) {
|
|
148
|
+
foundations.push({ name: pkg.name, path: dir })
|
|
149
|
+
}
|
|
150
|
+
} catch {
|
|
151
|
+
// skip
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return foundations
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Discover sites in the workspace
|
|
161
|
+
* @param {string} rootDir - Workspace root directory
|
|
162
|
+
* @returns {Promise<Array<{name: string, path: string}>>}
|
|
163
|
+
*/
|
|
164
|
+
export async function discoverSites(rootDir) {
|
|
165
|
+
const { packages } = await readWorkspaceConfig(rootDir)
|
|
166
|
+
const sites = []
|
|
167
|
+
|
|
168
|
+
for (const pattern of packages) {
|
|
169
|
+
const dirs = await resolveGlob(rootDir, pattern)
|
|
170
|
+
for (const dir of dirs) {
|
|
171
|
+
const pkgPath = join(rootDir, dir, 'package.json')
|
|
172
|
+
if (!existsSync(pkgPath)) continue
|
|
173
|
+
try {
|
|
174
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'))
|
|
175
|
+
// Site: has @uniweb/runtime in deps
|
|
176
|
+
if (pkg.dependencies?.['@uniweb/runtime']) {
|
|
177
|
+
sites.push({ name: pkg.name, path: dir })
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// skip
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return sites
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Resolve a workspace glob pattern to actual directories
|
|
189
|
+
async function resolveGlob(rootDir, pattern) {
|
|
190
|
+
const clean = pattern.replace(/^["']|["']$/g, '')
|
|
191
|
+
|
|
192
|
+
if (clean.endsWith('/*')) {
|
|
193
|
+
// Pattern like "foundations/*" - list subdirectories
|
|
194
|
+
const baseDir = clean.slice(0, -2)
|
|
195
|
+
const fullPath = join(rootDir, baseDir)
|
|
196
|
+
if (!existsSync(fullPath)) return []
|
|
197
|
+
try {
|
|
198
|
+
const { readdirSync } = await import('node:fs')
|
|
199
|
+
const entries = readdirSync(fullPath, { withFileTypes: true })
|
|
200
|
+
return entries
|
|
201
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('.'))
|
|
202
|
+
.map(e => join(baseDir, e.name))
|
|
203
|
+
} catch {
|
|
204
|
+
return []
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (clean.startsWith('*/')) {
|
|
209
|
+
// Pattern like "*/foundation" - find subdirs with this child
|
|
210
|
+
const suffix = clean.slice(2)
|
|
211
|
+
const { readdirSync } = await import('node:fs')
|
|
212
|
+
try {
|
|
213
|
+
const entries = readdirSync(rootDir, { withFileTypes: true })
|
|
214
|
+
return entries
|
|
215
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules')
|
|
216
|
+
.filter(e => existsSync(join(rootDir, e.name, suffix)))
|
|
217
|
+
.map(e => join(e.name, suffix))
|
|
218
|
+
} catch {
|
|
219
|
+
return []
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Direct path like "foundation" or "site"
|
|
224
|
+
if (existsSync(join(rootDir, clean))) {
|
|
225
|
+
return [clean]
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return []
|
|
229
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffolding Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared scaffolding functions used by both `create` and `add` commands.
|
|
5
|
+
* Each function scaffolds a single package from its package template.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs/promises'
|
|
9
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
10
|
+
import { join, dirname } from 'node:path'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
import { copyTemplateDirectory, registerVersions } from '../templates/processor.js'
|
|
13
|
+
import { getVersionsForTemplates } from '../versions.js'
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
16
|
+
const TEMPLATES_DIR = join(__dirname, '..', '..', 'templates')
|
|
17
|
+
const STARTER_DIR = join(__dirname, '..', '..', 'starter')
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Scaffold a workspace root from the workspace package template
|
|
21
|
+
*
|
|
22
|
+
* @param {string} targetDir - Target directory
|
|
23
|
+
* @param {Object} context - Template context
|
|
24
|
+
* @param {string} context.projectName - Workspace/project name
|
|
25
|
+
* @param {string[]} context.workspaceGlobs - Workspace glob patterns
|
|
26
|
+
* @param {Object} context.scripts - Root package.json scripts
|
|
27
|
+
* @param {Object} [options] - Processing options
|
|
28
|
+
*/
|
|
29
|
+
export async function scaffoldWorkspace(targetDir, context, options = {}) {
|
|
30
|
+
registerVersions(getVersionsForTemplates())
|
|
31
|
+
|
|
32
|
+
const templatePath = join(TEMPLATES_DIR, 'workspace')
|
|
33
|
+
await copyTemplateDirectory(templatePath, targetDir, context, {
|
|
34
|
+
onProgress: options.onProgress,
|
|
35
|
+
onWarning: options.onWarning,
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Scaffold a foundation from the foundation package template
|
|
41
|
+
*
|
|
42
|
+
* @param {string} targetDir - Target directory for the foundation
|
|
43
|
+
* @param {Object} context - Template context
|
|
44
|
+
* @param {string} context.name - Package name
|
|
45
|
+
* @param {string} context.projectName - Workspace name
|
|
46
|
+
* @param {boolean} [context.isExtension] - Whether this is an extension
|
|
47
|
+
* @param {Object} [options] - Processing options
|
|
48
|
+
*/
|
|
49
|
+
export async function scaffoldFoundation(targetDir, context, options = {}) {
|
|
50
|
+
registerVersions(getVersionsForTemplates())
|
|
51
|
+
|
|
52
|
+
const templatePath = join(TEMPLATES_DIR, 'foundation')
|
|
53
|
+
await copyTemplateDirectory(templatePath, targetDir, context, {
|
|
54
|
+
onProgress: options.onProgress,
|
|
55
|
+
onWarning: options.onWarning,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Scaffold a site from the site package template
|
|
61
|
+
*
|
|
62
|
+
* @param {string} targetDir - Target directory for the site
|
|
63
|
+
* @param {Object} context - Template context
|
|
64
|
+
* @param {string} context.name - Package name
|
|
65
|
+
* @param {string} context.projectName - Workspace name
|
|
66
|
+
* @param {string} context.foundationName - Foundation package name
|
|
67
|
+
* @param {string} context.foundationPath - Relative file: path to foundation
|
|
68
|
+
* @param {string} [context.foundationRef] - Foundation ref for site.yml (when multiple foundations)
|
|
69
|
+
* @param {Object} [options] - Processing options
|
|
70
|
+
*/
|
|
71
|
+
export async function scaffoldSite(targetDir, context, options = {}) {
|
|
72
|
+
registerVersions(getVersionsForTemplates())
|
|
73
|
+
|
|
74
|
+
const templatePath = join(TEMPLATES_DIR, 'site')
|
|
75
|
+
await copyTemplateDirectory(templatePath, targetDir, context, {
|
|
76
|
+
onProgress: options.onProgress,
|
|
77
|
+
onWarning: options.onWarning,
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Apply content overlay from a content directory onto a target
|
|
83
|
+
*
|
|
84
|
+
* Content files overwrite scaffolded defaults. Structural files
|
|
85
|
+
* (package.json, vite.config.js, main.js, index.html) are NOT overwritten.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} contentDir - Source content directory (e.g., starter/foundation/)
|
|
88
|
+
* @param {string} targetDir - Target directory to overlay onto
|
|
89
|
+
* @param {Object} context - Handlebars context for .hbs files
|
|
90
|
+
* @param {Object} [options] - Processing options
|
|
91
|
+
*/
|
|
92
|
+
export async function applyContent(contentDir, targetDir, context, options = {}) {
|
|
93
|
+
if (!existsSync(contentDir)) return
|
|
94
|
+
|
|
95
|
+
registerVersions(getVersionsForTemplates())
|
|
96
|
+
|
|
97
|
+
// Structural files that content should never overwrite
|
|
98
|
+
const STRUCTURAL_FILES = new Set([
|
|
99
|
+
'package.json',
|
|
100
|
+
'vite.config.js',
|
|
101
|
+
'main.js',
|
|
102
|
+
'index.html',
|
|
103
|
+
'.gitignore',
|
|
104
|
+
])
|
|
105
|
+
|
|
106
|
+
await copyContentRecursive(contentDir, targetDir, context, STRUCTURAL_FILES, options)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Recursively copy content files, skipping structural files
|
|
111
|
+
*/
|
|
112
|
+
async function copyContentRecursive(sourceDir, targetDir, context, structuralFiles, options) {
|
|
113
|
+
await fs.mkdir(targetDir, { recursive: true })
|
|
114
|
+
|
|
115
|
+
const entries = readdirSync(sourceDir, { withFileTypes: true })
|
|
116
|
+
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
const sourcePath = join(sourceDir, entry.name)
|
|
119
|
+
|
|
120
|
+
if (entry.isDirectory()) {
|
|
121
|
+
const targetSubDir = join(targetDir, entry.name)
|
|
122
|
+
await copyContentRecursive(sourcePath, targetSubDir, context, structuralFiles, options)
|
|
123
|
+
} else {
|
|
124
|
+
// Determine the output filename (strip .hbs extension)
|
|
125
|
+
const outputName = entry.name.endsWith('.hbs')
|
|
126
|
+
? entry.name.slice(0, -4)
|
|
127
|
+
: entry.name
|
|
128
|
+
|
|
129
|
+
// Skip structural files
|
|
130
|
+
if (structuralFiles.has(outputName)) continue
|
|
131
|
+
|
|
132
|
+
const targetPath = join(targetDir, outputName)
|
|
133
|
+
|
|
134
|
+
if (entry.name.endsWith('.hbs')) {
|
|
135
|
+
// Process through Handlebars
|
|
136
|
+
const Handlebars = (await import('handlebars')).default
|
|
137
|
+
const content = await fs.readFile(sourcePath, 'utf-8')
|
|
138
|
+
const template = Handlebars.compile(content)
|
|
139
|
+
const result = template(context)
|
|
140
|
+
await fs.writeFile(targetPath, result)
|
|
141
|
+
} else {
|
|
142
|
+
// Copy as-is
|
|
143
|
+
await fs.copyFile(sourcePath, targetPath)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (options.onProgress) {
|
|
147
|
+
options.onProgress(`Creating ${outputName}`)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Apply the built-in starter content
|
|
155
|
+
*
|
|
156
|
+
* @param {string} projectDir - Root project directory
|
|
157
|
+
* @param {Object} context - Template context
|
|
158
|
+
* @param {Object} [options] - Processing options
|
|
159
|
+
* @param {string} [options.foundationDir] - Foundation directory name (default: 'foundation')
|
|
160
|
+
* @param {string} [options.siteDir] - Site directory name (default: 'site')
|
|
161
|
+
*/
|
|
162
|
+
export async function applyStarter(projectDir, context, options = {}) {
|
|
163
|
+
const foundationDir = options.foundationDir || 'foundation'
|
|
164
|
+
const siteDir = options.siteDir || 'site'
|
|
165
|
+
|
|
166
|
+
// Apply foundation starter content
|
|
167
|
+
const foundationContentDir = join(STARTER_DIR, 'foundation')
|
|
168
|
+
const foundationTargetDir = join(projectDir, foundationDir)
|
|
169
|
+
await applyContent(foundationContentDir, foundationTargetDir, context, options)
|
|
170
|
+
|
|
171
|
+
// Apply site starter content
|
|
172
|
+
const siteContentDir = join(STARTER_DIR, 'site')
|
|
173
|
+
const siteTargetDir = join(projectDir, siteDir)
|
|
174
|
+
await applyContent(siteContentDir, siteTargetDir, context, options)
|
|
175
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "{{name}}",
|
|
3
3
|
"version": "0.1.0",
|
|
4
|
-
"description": "{{projectName}} foundation
|
|
4
|
+
"description": "{{projectName}} foundation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/_entry.generated.js",
|
|
7
7
|
"exports": {
|
|
File without changes
|