uniweb 0.8.2 → 0.8.3
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 +80 -15
- package/partials/search-docs.hbs +1 -1
- package/src/commands/add.js +53 -18
- package/src/utils/names.js +91 -0
- package/templates/foundation/src/styles.css +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
-
"@uniweb/
|
|
45
|
-
"@uniweb/
|
|
46
|
-
"@uniweb/
|
|
47
|
-
"@uniweb/
|
|
44
|
+
"@uniweb/runtime": "0.6.4",
|
|
45
|
+
"@uniweb/build": "0.8.3",
|
|
46
|
+
"@uniweb/core": "0.5.4",
|
|
47
|
+
"@uniweb/kit": "0.7.3"
|
|
48
48
|
}
|
|
49
49
|
}
|
package/partials/agents.md
CHANGED
|
@@ -100,7 +100,7 @@ content = {
|
|
|
100
100
|
icons: [], // { library, name, role }
|
|
101
101
|
videos: [], // Video embeds
|
|
102
102
|
insets: [], // Inline @Component references — { refId }
|
|
103
|
-
lists: [], //
|
|
103
|
+
lists: [], // [[{ paragraphs, links, lists, ... }]] — each list item is an object, not a string
|
|
104
104
|
quotes: [], // Blockquotes
|
|
105
105
|
data: {}, // From tagged code blocks (```yaml:tagname)
|
|
106
106
|
headings: [], // Overflow headings after subtitle2
|
|
@@ -123,6 +123,35 @@ Lightning quick. ← items[0].paragraphs[0]
|
|
|
123
123
|
Enterprise-grade. ← items[1].paragraphs[0]
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
+
**Lists** contain bullet or ordered list items. Each list item is an object with the same content shape — not a plain string:
|
|
127
|
+
|
|
128
|
+
```markdown
|
|
129
|
+
# Features ← title
|
|
130
|
+
|
|
131
|
+
- Fast builds ← lists[0][0].paragraphs[0]
|
|
132
|
+
- **Hot** reload ← lists[0][1].paragraphs[0] (HTML: "<strong>Hot</strong> reload")
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Items can contain lists:
|
|
136
|
+
|
|
137
|
+
```markdown
|
|
138
|
+
### Starter ← items[0].title
|
|
139
|
+
$9/month ← items[0].paragraphs[0]
|
|
140
|
+
|
|
141
|
+
- Feature A ← items[0].lists[0][0].paragraphs[0]
|
|
142
|
+
- Feature B ← items[0].lists[0][1].paragraphs[0]
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Render list item text with kit components (see [kit section](#uniwebkit) below):
|
|
146
|
+
|
|
147
|
+
```jsx
|
|
148
|
+
import { Span } from '@uniweb/kit'
|
|
149
|
+
|
|
150
|
+
content.lists[0]?.map((listItem, i) => (
|
|
151
|
+
<li key={i}><Span text={listItem.paragraphs[0]} /></li>
|
|
152
|
+
))
|
|
153
|
+
```
|
|
154
|
+
|
|
126
155
|
### Icons
|
|
127
156
|
|
|
128
157
|
Use image syntax with library prefix: ``. Supported libraries: `lu` (Lucide), `hi2` (Heroicons), `fi` (Feather), `pi` (Phosphor), `tb` (Tabler), `bs` (Bootstrap), `md` (Material), `fa6` (Font Awesome 6), and others. Browse at [react-icons.github.io/react-icons](https://react-icons.github.io/react-icons/).
|
|
@@ -270,16 +299,15 @@ nest:
|
|
|
270
299
|
- Children are ordered by their position in the `nest:` array
|
|
271
300
|
- Orphaned `@` files (no parent in `nest:`) appear at top-level with a warning
|
|
272
301
|
|
|
273
|
-
Components receive children via `block.childBlocks
|
|
302
|
+
Components receive children via `block.childBlocks`. Use `ChildBlocks` from kit to render them — the runtime handles component resolution:
|
|
274
303
|
|
|
275
304
|
```jsx
|
|
305
|
+
import { ChildBlocks } from '@uniweb/kit'
|
|
306
|
+
|
|
276
307
|
export default function Grid({ block, params }) {
|
|
277
308
|
return (
|
|
278
309
|
<div className={`grid grid-cols-${params.columns || 2} gap-6`}>
|
|
279
|
-
{block
|
|
280
|
-
const Comp = child.initComponent()
|
|
281
|
-
return Comp ? <Comp key={child.id} block={child} /> : null
|
|
282
|
-
})}
|
|
310
|
+
<ChildBlocks from={block} />
|
|
283
311
|
</div>
|
|
284
312
|
)
|
|
285
313
|
}
|
|
@@ -520,9 +548,45 @@ All defaults belong in `meta.js`, not inline in component code.
|
|
|
520
548
|
|
|
521
549
|
### @uniweb/kit
|
|
522
550
|
|
|
523
|
-
|
|
551
|
+
Content fields (`title`, `pretitle`, `paragraphs[]`, list item text) are **HTML strings** — they contain markup like `<strong>`, `<em>`, `<a>` from the author's markdown. The kit provides components to render them correctly.
|
|
552
|
+
|
|
553
|
+
**Rendering text** (`@uniweb/kit`):
|
|
554
|
+
|
|
555
|
+
```jsx
|
|
556
|
+
import { H2, P, Span } from '@uniweb/kit'
|
|
557
|
+
|
|
558
|
+
<H2 text={content.title} className="text-heading text-3xl font-bold" />
|
|
559
|
+
<P text={content.paragraphs[0]} className="text-body" />
|
|
560
|
+
<P text={content.paragraphs} /> // array → each string becomes its own <p>
|
|
561
|
+
<Span text={listItem.paragraphs[0]} className="text-subtle" />
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
`H1`–`H6`, `P`, `Span`, `Div` are all wrappers around `Text` with a preset tag:
|
|
565
|
+
|
|
566
|
+
```jsx
|
|
567
|
+
<Text text={content.title} as="h2" className="..." /> // explicit tag
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
Don't render content strings with `{content.paragraphs[0]}` in JSX — that shows HTML tags as visible text. Use `<P>`, `<H2>`, `<Span>`, etc. for content strings.
|
|
571
|
+
|
|
572
|
+
**Rendering full content** (`@uniweb/kit`):
|
|
524
573
|
|
|
525
|
-
|
|
574
|
+
```jsx
|
|
575
|
+
import { Section, Render } from '@uniweb/kit'
|
|
576
|
+
|
|
577
|
+
<Render content={block.parsedContent} block={block} /> // ProseMirror nodes → React
|
|
578
|
+
<Section block={block} width="lg" padding="md" /> // Render + prose styling + layout
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
`Render` processes ProseMirror nodes into React elements — paragraphs, headings, images, code blocks, lists, tables, alerts, and insets. `Section` wraps `Render` with prose typography and layout options. Use these when rendering a block's complete content. Use `P`, `H2`, etc. when you extract specific fields and arrange them with custom layout.
|
|
582
|
+
|
|
583
|
+
**Rendering visuals** (`@uniweb/kit`):
|
|
584
|
+
|
|
585
|
+
`<Visual>` renders the first non-empty candidate from the props you pass (inset, video, image). See Insets section below.
|
|
586
|
+
|
|
587
|
+
**Other primitives** (`@uniweb/kit`): `Link`, `Image`, `Icon`, `Media`, `Asset`, `SafeHtml`, `SocialIcon`, `FileLogo`, `cn()`
|
|
588
|
+
|
|
589
|
+
**Other styled** (`@uniweb/kit`): `SidebarLayout`, `Prose`, `Article`, `Code`, `Alert`, `Table`, `Details`, `Divider`, `Disclaimer`
|
|
526
590
|
|
|
527
591
|
**Hooks:**
|
|
528
592
|
- `useScrolled(threshold)` → boolean for scroll-based header styling
|
|
@@ -575,25 +639,26 @@ website.getLocaleUrl('es')
|
|
|
575
639
|
Components access inline `@` references via `block.insets` (separate from `block.childBlocks`):
|
|
576
640
|
|
|
577
641
|
```jsx
|
|
578
|
-
import { Visual } from '@uniweb/kit
|
|
642
|
+
import { Visual } from '@uniweb/kit'
|
|
579
643
|
|
|
580
|
-
// Visual renders the first
|
|
581
|
-
function SplitContent({ content, block
|
|
644
|
+
// Visual renders the first non-empty candidate: inset > video > image
|
|
645
|
+
function SplitContent({ content, block }) {
|
|
582
646
|
return (
|
|
583
647
|
<div className="flex gap-12">
|
|
584
648
|
<div className="flex-1">
|
|
585
649
|
<h2 className="text-heading">{content.title}</h2>
|
|
586
650
|
</div>
|
|
587
|
-
<Visual
|
|
651
|
+
<Visual inset={block.insets[0]} video={content.videos[0]} image={content.imgs[0]} className="flex-1 rounded-lg" />
|
|
588
652
|
</div>
|
|
589
653
|
)
|
|
590
654
|
}
|
|
591
655
|
```
|
|
592
656
|
|
|
657
|
+
- `<Visual>` — renders first non-empty candidate from the props you pass (`inset`, `video`, `image`)
|
|
658
|
+
- `<Render>` / `<Section>` — automatically handles `@Component` references placed in content flow
|
|
593
659
|
- `block.insets` — array of Block instances from `@` references
|
|
594
660
|
- `block.getInset(refId)` — lookup by refId (used by sequential renderers)
|
|
595
661
|
- `content.insets` — flat array of `{ refId }` entries (parallel to `content.imgs`)
|
|
596
|
-
- `<Visual>` — renders first inset > video > image from content (from `@uniweb/kit/styled`)
|
|
597
662
|
|
|
598
663
|
Inset components declare `inset: true` in meta.js. Use `hidden: true` for inset-only components:
|
|
599
664
|
|
|
@@ -624,7 +689,7 @@ function SplitContent({ content, block, params }) {
|
|
|
624
689
|
<h2 className="text-heading text-3xl font-bold">{content.title}</h2>
|
|
625
690
|
<p className="text-body mt-4">{content.paragraphs[0]}</p>
|
|
626
691
|
</div>
|
|
627
|
-
<Visual
|
|
692
|
+
<Visual inset={block.insets[0]} video={content.videos[0]} image={content.imgs[0]} className="flex-1 rounded-2xl" />
|
|
628
693
|
</div>
|
|
629
694
|
)
|
|
630
695
|
}
|
|
@@ -731,7 +796,7 @@ Uniweb section types do more with less because the framework handles concerns th
|
|
|
731
796
|
- **Dispatcher pattern** — one section type with a `variant` param replaces multiple near-duplicate components (`HeroHomepage` + `HeroPricing` → `Hero` with `variant: homepage | pricing`)
|
|
732
797
|
- **Section nesting** — `@`-prefixed child files replace wrapper components that exist only to arrange children
|
|
733
798
|
- **Insets** — `` replaces prop-drilling of visual components into containers
|
|
734
|
-
- **Visual component** — `<Visual>` renders
|
|
799
|
+
- **Visual component** — `<Visual>` renders the first non-empty visual from explicit candidates (inset, video, image), replacing manual media handling
|
|
735
800
|
- **Semantic theming** — the runtime orchestrates context classes and token resolution, replacing per-component dark mode logic
|
|
736
801
|
- **Engine backgrounds** — the runtime renders section backgrounds from frontmatter, replacing background-handling code in every section
|
|
737
802
|
- **Rich params** — `meta.js` params with types, defaults, and presets replace config objects and conditional logic
|
package/partials/search-docs.hbs
CHANGED
|
@@ -14,7 +14,7 @@ pnpm add fuse.js
|
|
|
14
14
|
Then use the search helpers from `@uniweb/kit`:
|
|
15
15
|
|
|
16
16
|
```jsx
|
|
17
|
-
import { useSearch } from '@uniweb/kit
|
|
17
|
+
import { useSearch } from '@uniweb/kit'
|
|
18
18
|
|
|
19
19
|
function SearchComponent({ website }) {
|
|
20
20
|
const { query, results, isLoading, clear } = useSearch(website)
|
package/src/commands/add.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
discoverSites,
|
|
23
23
|
updateRootScripts,
|
|
24
24
|
} from '../utils/config.js'
|
|
25
|
+
import { validatePackageName, getExistingPackageNames, resolveUniqueName } from '../utils/names.js'
|
|
25
26
|
import { findWorkspaceRoot } from '../utils/workspace.js'
|
|
26
27
|
import { detectPackageManager, filterCmd, installCmd } from '../utils/pm.js'
|
|
27
28
|
import { resolveTemplate } from '../templates/index.js'
|
|
@@ -157,6 +158,16 @@ export async function add(args) {
|
|
|
157
158
|
*/
|
|
158
159
|
async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
|
|
159
160
|
let name = opts.name
|
|
161
|
+
const existingNames = await getExistingPackageNames(rootDir)
|
|
162
|
+
|
|
163
|
+
// Reject reserved names (format + reserved check only — collisions handled at package name level)
|
|
164
|
+
if (name) {
|
|
165
|
+
const valid = validatePackageName(name)
|
|
166
|
+
if (valid !== true) {
|
|
167
|
+
error(valid)
|
|
168
|
+
process.exit(1)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
160
171
|
|
|
161
172
|
// Interactive name prompt when name not provided and no --path
|
|
162
173
|
if (!name && !opts.path) {
|
|
@@ -167,11 +178,7 @@ async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
167
178
|
name: 'name',
|
|
168
179
|
message: 'Foundation name:',
|
|
169
180
|
initial: hasDefault ? 'foundation' : undefined,
|
|
170
|
-
validate: (value) =>
|
|
171
|
-
if (!value) return 'Name is required'
|
|
172
|
-
if (!/^[a-z0-9-]+$/.test(value)) return 'Use lowercase letters, numbers, and hyphens'
|
|
173
|
-
return true
|
|
174
|
-
},
|
|
181
|
+
validate: (value) => validatePackageName(value),
|
|
175
182
|
}, {
|
|
176
183
|
onCancel: () => {
|
|
177
184
|
log('\nCancelled.')
|
|
@@ -193,9 +200,15 @@ async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
193
200
|
process.exit(1)
|
|
194
201
|
}
|
|
195
202
|
|
|
203
|
+
// Compute package name — auto-suffix if it collides with an existing package
|
|
204
|
+
let packageName = name || opts.project || 'foundation'
|
|
205
|
+
if (existingNames.has(packageName)) {
|
|
206
|
+
packageName = resolveUniqueName(packageName, '-foundation', existingNames)
|
|
207
|
+
}
|
|
208
|
+
|
|
196
209
|
// Scaffold
|
|
197
210
|
await scaffoldFoundation(fullPath, {
|
|
198
|
-
name:
|
|
211
|
+
name: packageName,
|
|
199
212
|
projectName,
|
|
200
213
|
isExtension: false,
|
|
201
214
|
}, {
|
|
@@ -215,7 +228,7 @@ async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
215
228
|
const sites = await discoverSites(rootDir)
|
|
216
229
|
await updateRootScripts(rootDir, sites, pm)
|
|
217
230
|
|
|
218
|
-
success(`Created foundation '${
|
|
231
|
+
success(`Created foundation '${packageName}' at ${target}/`)
|
|
219
232
|
log('')
|
|
220
233
|
log(`Next: ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
221
234
|
}
|
|
@@ -225,6 +238,16 @@ async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
225
238
|
*/
|
|
226
239
|
async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
|
|
227
240
|
let name = opts.name
|
|
241
|
+
const existingNames = await getExistingPackageNames(rootDir)
|
|
242
|
+
|
|
243
|
+
// Reject reserved names (format + reserved check only — collisions handled at package name level)
|
|
244
|
+
if (name) {
|
|
245
|
+
const valid = validatePackageName(name)
|
|
246
|
+
if (valid !== true) {
|
|
247
|
+
error(valid)
|
|
248
|
+
process.exit(1)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
228
251
|
|
|
229
252
|
// Interactive name prompt when name not provided and no --path
|
|
230
253
|
if (!name && !opts.path) {
|
|
@@ -235,11 +258,7 @@ async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
235
258
|
name: 'name',
|
|
236
259
|
message: 'Site name:',
|
|
237
260
|
initial: hasDefault ? 'site' : undefined,
|
|
238
|
-
validate: (value) =>
|
|
239
|
-
if (!value) return 'Name is required'
|
|
240
|
-
if (!/^[a-z0-9-]+$/.test(value)) return 'Use lowercase letters, numbers, and hyphens'
|
|
241
|
-
return true
|
|
242
|
-
},
|
|
261
|
+
validate: (value) => validatePackageName(value),
|
|
243
262
|
}, {
|
|
244
263
|
onCancel: () => {
|
|
245
264
|
log('\nCancelled.')
|
|
@@ -271,6 +290,11 @@ async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
271
290
|
siteName = name || 'site'
|
|
272
291
|
}
|
|
273
292
|
|
|
293
|
+
// Auto-suffix package name if it collides with an existing package
|
|
294
|
+
if (existingNames.has(siteName)) {
|
|
295
|
+
siteName = resolveUniqueName(siteName, '-site', existingNames)
|
|
296
|
+
}
|
|
297
|
+
|
|
274
298
|
if (foundation) {
|
|
275
299
|
// Compute relative path from site to foundation
|
|
276
300
|
const foundationPath = computeFoundationPath(target, foundation.path)
|
|
@@ -334,6 +358,16 @@ async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
334
358
|
*/
|
|
335
359
|
async function addExtension(rootDir, projectName, opts, pm = 'pnpm') {
|
|
336
360
|
let name = opts.name
|
|
361
|
+
const existingNames = await getExistingPackageNames(rootDir)
|
|
362
|
+
|
|
363
|
+
// Reject reserved names (format + reserved check only — collisions handled at package name level)
|
|
364
|
+
if (name) {
|
|
365
|
+
const valid = validatePackageName(name)
|
|
366
|
+
if (valid !== true) {
|
|
367
|
+
error(valid)
|
|
368
|
+
process.exit(1)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
337
371
|
|
|
338
372
|
// Interactive name prompt when name not provided
|
|
339
373
|
if (!name) {
|
|
@@ -341,11 +375,7 @@ async function addExtension(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
341
375
|
type: 'text',
|
|
342
376
|
name: 'name',
|
|
343
377
|
message: 'Extension name:',
|
|
344
|
-
validate: (value) =>
|
|
345
|
-
if (!value) return 'Name is required'
|
|
346
|
-
if (!/^[a-z0-9-]+$/.test(value)) return 'Use lowercase letters, numbers, and hyphens'
|
|
347
|
-
return true
|
|
348
|
-
},
|
|
378
|
+
validate: (value) => validatePackageName(value),
|
|
349
379
|
}, {
|
|
350
380
|
onCancel: () => {
|
|
351
381
|
log('\nCancelled.')
|
|
@@ -355,6 +385,11 @@ async function addExtension(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
355
385
|
name = response.name
|
|
356
386
|
}
|
|
357
387
|
|
|
388
|
+
// Auto-suffix package name if it collides with an existing package
|
|
389
|
+
const extensionPackageName = existingNames.has(name)
|
|
390
|
+
? resolveUniqueName(name, '-ext', existingNames)
|
|
391
|
+
: name
|
|
392
|
+
|
|
358
393
|
// Determine target
|
|
359
394
|
let target
|
|
360
395
|
if (opts.path) {
|
|
@@ -372,7 +407,7 @@ async function addExtension(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
372
407
|
|
|
373
408
|
// Scaffold foundation with extension flag
|
|
374
409
|
await scaffoldFoundation(fullPath, {
|
|
375
|
-
name,
|
|
410
|
+
name: extensionPackageName,
|
|
376
411
|
projectName,
|
|
377
412
|
isExtension: true,
|
|
378
413
|
}, {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package Name Validation
|
|
3
|
+
*
|
|
4
|
+
* Validates package names for the `add` command — rejects reserved names
|
|
5
|
+
* and detects collisions with existing workspace packages.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { discoverFoundations, discoverSites, readWorkspaceConfig, resolveGlob } from './config.js'
|
|
9
|
+
import { existsSync } from 'node:fs'
|
|
10
|
+
import { readFile } from 'node:fs/promises'
|
|
11
|
+
import { join } from 'node:path'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Names that must not be used as package names.
|
|
15
|
+
* - JS module keywords: default, undefined, null, true, false
|
|
16
|
+
* - Node/filesystem: node_modules, package
|
|
17
|
+
* - Common directory names that would cause confusion: src, dist, build
|
|
18
|
+
*/
|
|
19
|
+
const RESERVED_NAMES = new Set([
|
|
20
|
+
'default', 'undefined', 'null', 'true', 'false',
|
|
21
|
+
'node_modules', 'package',
|
|
22
|
+
'src', 'dist', 'build',
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validate a package name.
|
|
27
|
+
* @param {string} name
|
|
28
|
+
* @param {Set<string>} [existingNames] - Names already in the workspace
|
|
29
|
+
* @returns {string|true} true if valid, or an error message string
|
|
30
|
+
*/
|
|
31
|
+
export function validatePackageName(name, existingNames) {
|
|
32
|
+
if (!name) return 'Name is required'
|
|
33
|
+
if (!/^[a-z0-9-]+$/.test(name)) return 'Use lowercase letters, numbers, and hyphens'
|
|
34
|
+
if (RESERVED_NAMES.has(name)) return `"${name}" is a reserved name — choose a different one`
|
|
35
|
+
if (existingNames?.has(name)) return `"${name}" already exists in this workspace`
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Discover all package names in the workspace (foundations + sites + extensions).
|
|
41
|
+
* @param {string} rootDir - Workspace root directory
|
|
42
|
+
* @returns {Promise<Set<string>>}
|
|
43
|
+
*/
|
|
44
|
+
export async function getExistingPackageNames(rootDir) {
|
|
45
|
+
const names = new Set()
|
|
46
|
+
|
|
47
|
+
// Foundations and sites via existing discovery
|
|
48
|
+
const foundations = await discoverFoundations(rootDir)
|
|
49
|
+
const sites = await discoverSites(rootDir)
|
|
50
|
+
|
|
51
|
+
for (const f of foundations) names.add(f.name)
|
|
52
|
+
for (const s of sites) names.add(s.name)
|
|
53
|
+
|
|
54
|
+
// Extensions (foundations with @uniweb/runtime absent — already captured above)
|
|
55
|
+
// Also scan extensions/* if it exists
|
|
56
|
+
const extensionsDir = join(rootDir, 'extensions')
|
|
57
|
+
if (existsSync(extensionsDir)) {
|
|
58
|
+
const dirs = await resolveGlob(rootDir, 'extensions/*')
|
|
59
|
+
for (const dir of dirs) {
|
|
60
|
+
const pkgPath = join(rootDir, dir, 'package.json')
|
|
61
|
+
if (!existsSync(pkgPath)) continue
|
|
62
|
+
try {
|
|
63
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'))
|
|
64
|
+
if (pkg.name) names.add(pkg.name)
|
|
65
|
+
} catch {
|
|
66
|
+
// skip
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return names
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve a unique name by appending a suffix if there's a collision.
|
|
76
|
+
* @param {string} name - Proposed name
|
|
77
|
+
* @param {string} suffix - Suffix to append (e.g., '-site', '-foundation')
|
|
78
|
+
* @param {Set<string>} existingNames
|
|
79
|
+
* @returns {string} The resolved unique name
|
|
80
|
+
*/
|
|
81
|
+
export function resolveUniqueName(name, suffix, existingNames) {
|
|
82
|
+
if (!existingNames.has(name)) return name
|
|
83
|
+
const suffixed = `${name}${suffix}`
|
|
84
|
+
if (!existingNames.has(suffixed)) return suffixed
|
|
85
|
+
// Unlikely: both name and name-suffix taken — append number
|
|
86
|
+
for (let i = 2; i < 100; i++) {
|
|
87
|
+
const numbered = `${name}${suffix}-${i}`
|
|
88
|
+
if (!existingNames.has(numbered)) return numbered
|
|
89
|
+
}
|
|
90
|
+
return suffixed // give up
|
|
91
|
+
}
|