uniweb 0.8.13 → 0.8.15
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 +34 -4
- package/package.json +5 -5
- package/src/commands/add.js +20 -9
- package/src/commands/build.js +30 -42
- package/src/commands/deploy.js +272 -0
- package/src/commands/docs.js +1 -1
- package/src/commands/handoff.js +254 -0
- package/src/commands/i18n.js +5 -5
- package/src/commands/invite.js +326 -0
- package/src/commands/login.js +87 -0
- package/src/commands/publish.js +300 -0
- package/src/commands/template.js +230 -0
- package/src/index.js +88 -1
- package/src/templates/processor.js +8 -2
- package/src/utils/auth.js +150 -0
- package/src/utils/registry.js +361 -0
- package/src/utils/scaffold.js +7 -0
package/README.md
CHANGED
|
@@ -36,10 +36,10 @@ Edit files in `site/pages/` and `foundation/src/sections/` to see changes instan
|
|
|
36
36
|
| **Store** | E-commerce with product grid |
|
|
37
37
|
| **Extensions** | Multi-foundation with visual effects extension |
|
|
38
38
|
|
|
39
|
-
Use `--blank` for an empty workspace (no packages) — grow with `uniweb add`.
|
|
40
|
-
|
|
41
39
|
**See them live:** [View all template demos](https://uniweb.github.io/templates/)
|
|
42
40
|
|
|
41
|
+
Use `--blank` for an empty workspace (no packages) — grow with `uniweb add`.
|
|
42
|
+
|
|
43
43
|
Or skip the interactive prompt:
|
|
44
44
|
|
|
45
45
|
```bash
|
|
@@ -164,6 +164,36 @@ export default function Hero({ content, params }) {
|
|
|
164
164
|
|
|
165
165
|
Standard React. Standard Tailwind. The `{ content, params }` interface is only for _section types_ — components that content creators select in markdown frontmatter. Everything else uses regular React props.
|
|
166
166
|
|
|
167
|
+
### Composition
|
|
168
|
+
|
|
169
|
+
Sections aren't limited to flat content. Content authors can embed interactive React components using markdown image syntax:
|
|
170
|
+
|
|
171
|
+
```markdown
|
|
172
|
+
---
|
|
173
|
+
type: SplitContent
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
# See it in action
|
|
177
|
+
|
|
178
|
+
The architecture handles the hard parts so you can focus on what matters.
|
|
179
|
+
|
|
180
|
+
{period=30d}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`@PerformanceChart` is a full React component — a ThreeJS animation, an interactive diagram, a live data visualization — placed by the content author, rendered in the component's visual slot via `<Visual>`. It looks like an image reference, but it can be anything.
|
|
184
|
+
|
|
185
|
+
Authors can also compose layouts from reusable section types using child sections:
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
pages/home/
|
|
189
|
+
├── highlights.md # type: Grid, columns: 3
|
|
190
|
+
├── @stats.md # type: StatCard
|
|
191
|
+
├── @testimonial.md # type: Testimonial
|
|
192
|
+
└── @demo.md # type: SplitContent (with an embedded @LiveDemo inset)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Three different section types, arranged in a grid, one with an interactive component inside it — all authored in markdown. The developer builds reusable pieces; the content author composes them. See the [Component Patterns guide](https://github.com/uniweb/docs/blob/main/development/component-patterns.md) for the full composition model.
|
|
196
|
+
|
|
167
197
|
## Next Steps
|
|
168
198
|
|
|
169
199
|
After creating your project:
|
|
@@ -386,7 +416,7 @@ packages:
|
|
|
386
416
|
|
|
387
417
|
**How is this different from MDX?**
|
|
388
418
|
|
|
389
|
-
MDX blends markdown and JSX—content authors write code. Uniweb keeps them separate: content stays in markdown, components stay in React.
|
|
419
|
+
MDX blends markdown and JSX—content authors write code. Uniweb keeps them separate: content stays in markdown, components stay in React. Authors can still embed interactive components (via `` syntax), but they never see JSX. Component updates don't require content changes, and content changes can't break components.
|
|
390
420
|
|
|
391
421
|
**How is this different from Astro?**
|
|
392
422
|
|
|
@@ -420,4 +450,4 @@ Yes — documentation is a natural fit. Content stays in markdown (easy to versi
|
|
|
420
450
|
|
|
421
451
|
## License
|
|
422
452
|
|
|
423
|
-
Apache 2.0
|
|
453
|
+
Apache 2.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.15",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
-
"@uniweb/build": "0.8.
|
|
44
|
+
"@uniweb/build": "0.8.14",
|
|
45
45
|
"@uniweb/content-reader": "1.1.4",
|
|
46
|
-
"@uniweb/core": "0.5.
|
|
47
|
-
"@uniweb/
|
|
48
|
-
"@uniweb/
|
|
46
|
+
"@uniweb/core": "0.5.10",
|
|
47
|
+
"@uniweb/runtime": "0.6.11",
|
|
48
|
+
"@uniweb/kit": "0.7.10",
|
|
49
49
|
"@uniweb/semantic-parser": "1.1.6"
|
|
50
50
|
}
|
|
51
51
|
}
|
package/src/commands/add.js
CHANGED
|
@@ -121,7 +121,7 @@ export async function add(rawArgs) {
|
|
|
121
121
|
error(`Missing subcommand.\n`)
|
|
122
122
|
log(formatOptions([
|
|
123
123
|
{ label: 'project', description: 'Co-located foundation + site pair' },
|
|
124
|
-
{ label: 'foundation', description: 'Component
|
|
124
|
+
{ label: 'foundation', description: 'Component system for content authors' },
|
|
125
125
|
{ label: 'site', description: 'Content site' },
|
|
126
126
|
{ label: 'extension', description: 'Additional component package' },
|
|
127
127
|
{ label: 'section', description: 'Section type in a foundation' },
|
|
@@ -137,7 +137,7 @@ export async function add(rawArgs) {
|
|
|
137
137
|
message: 'What would you like to add?',
|
|
138
138
|
choices: [
|
|
139
139
|
{ title: 'Project', value: 'project', description: 'Co-located foundation + site pair' },
|
|
140
|
-
{ title: 'Foundation', value: 'foundation', description: 'Component
|
|
140
|
+
{ title: 'Foundation', value: 'foundation', description: 'Component system for content authors' },
|
|
141
141
|
{ title: 'Site', value: 'site', description: 'Content site' },
|
|
142
142
|
{ title: 'Extension', value: 'extension', description: 'Additional component package' },
|
|
143
143
|
{ title: 'Section', value: 'section', description: 'Section type in a foundation' },
|
|
@@ -594,7 +594,8 @@ async function addProject(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
594
594
|
* - --project: {project}/foundation (co-located)
|
|
595
595
|
* - Existing co-located glob: follow pattern
|
|
596
596
|
* - Existing segregated glob: follow pattern
|
|
597
|
-
* -
|
|
597
|
+
* - Named (e.g., "marketing"): segregated at foundations/{name}
|
|
598
|
+
* - Unnamed: root-level 'foundation'
|
|
598
599
|
* - Already have one: error in non-interactive, ask in interactive
|
|
599
600
|
*/
|
|
600
601
|
async function resolveFoundationTarget(rootDir, name, opts) {
|
|
@@ -619,8 +620,13 @@ async function resolveFoundationTarget(rootDir, name, opts) {
|
|
|
619
620
|
return `foundations/${name || 'foundation'}`
|
|
620
621
|
}
|
|
621
622
|
|
|
622
|
-
//
|
|
623
|
-
|
|
623
|
+
// Named foundation → segregated layout (foundations/{name})
|
|
624
|
+
if (name) {
|
|
625
|
+
return `foundations/${name}`
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Unnamed → root-level 'foundation'
|
|
629
|
+
const dirName = 'foundation'
|
|
624
630
|
|
|
625
631
|
// Check if target already exists
|
|
626
632
|
if (!existsSync(join(rootDir, dirName))) {
|
|
@@ -667,8 +673,13 @@ async function resolveSiteTarget(rootDir, name, opts) {
|
|
|
667
673
|
return `sites/${name || 'site'}`
|
|
668
674
|
}
|
|
669
675
|
|
|
670
|
-
//
|
|
671
|
-
|
|
676
|
+
// Named site → segregated layout (sites/{name})
|
|
677
|
+
if (name) {
|
|
678
|
+
return `sites/${name}`
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Unnamed → root-level 'site'
|
|
682
|
+
const dirName = 'site'
|
|
672
683
|
|
|
673
684
|
// Check if target already exists
|
|
674
685
|
if (!existsSync(join(rootDir, dirName))) {
|
|
@@ -1050,9 +1061,9 @@ ${colors.bright}Examples:${colors.reset}
|
|
|
1050
1061
|
uniweb add project docs # Create docs/foundation/ + docs/site/
|
|
1051
1062
|
uniweb add project docs --from academic # Co-located pair + academic content
|
|
1052
1063
|
uniweb add foundation # Create ./foundation/ at root
|
|
1053
|
-
uniweb add foundation ui # Create ./ui/
|
|
1064
|
+
uniweb add foundation ui # Create ./foundations/ui/
|
|
1054
1065
|
uniweb add site # Create ./site/ at root
|
|
1055
|
-
uniweb add site blog --foundation marketing # Create ./blog/ wired to marketing
|
|
1066
|
+
uniweb add site blog --foundation marketing # Create ./sites/blog/ wired to marketing
|
|
1056
1067
|
uniweb add extension effects --site site # Create ./extensions/effects/
|
|
1057
1068
|
uniweb add section Hero # Create Hero section type
|
|
1058
1069
|
uniweb add section Hero --foundation ui # Target specific foundation
|
package/src/commands/build.js
CHANGED
|
@@ -18,9 +18,7 @@ import { writeFile, mkdir } from 'node:fs/promises'
|
|
|
18
18
|
// Import build utilities from @uniweb/build
|
|
19
19
|
import {
|
|
20
20
|
generateEntryPoint,
|
|
21
|
-
buildSchema,
|
|
22
21
|
discoverComponents,
|
|
23
|
-
processAllPreviews,
|
|
24
22
|
} from '@uniweb/build'
|
|
25
23
|
import { readSiteConfig } from '@uniweb/build/site'
|
|
26
24
|
import { readWorkspaceConfig, resolveGlob } from '../utils/config.js'
|
|
@@ -130,7 +128,6 @@ function runCommand(command, args, cwd) {
|
|
|
130
128
|
*/
|
|
131
129
|
async function buildFoundation(projectDir, options = {}) {
|
|
132
130
|
const srcDir = join(projectDir, 'src')
|
|
133
|
-
const distDir = join(projectDir, 'dist')
|
|
134
131
|
|
|
135
132
|
info('Building foundation...')
|
|
136
133
|
|
|
@@ -163,50 +160,19 @@ async function buildFoundation(projectDir, options = {}) {
|
|
|
163
160
|
// For now, just run the standard vite build
|
|
164
161
|
await runCommand('npx', ['vite', 'build'], projectDir)
|
|
165
162
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
// 4. Generate schema.json
|
|
169
|
-
log('')
|
|
170
|
-
info('Generating schema.json...')
|
|
171
|
-
let schema = await buildSchema(srcDir)
|
|
172
|
-
|
|
173
|
-
await mkdir(distDir, { recursive: true })
|
|
163
|
+
// Vite's foundation plugin generates dist/meta/schema.json
|
|
164
|
+
// and processes preview images during the build.
|
|
174
165
|
|
|
175
|
-
|
|
176
|
-
log('')
|
|
177
|
-
info('Processing preview images...')
|
|
178
|
-
const isProduction = process.env.NODE_ENV === 'production' || !process.env.NODE_ENV
|
|
179
|
-
const { schema: updatedSchema, totalImages } = await processAllPreviews(
|
|
180
|
-
srcDir,
|
|
181
|
-
distDir,
|
|
182
|
-
schema,
|
|
183
|
-
isProduction
|
|
184
|
-
)
|
|
185
|
-
schema = updatedSchema
|
|
186
|
-
|
|
187
|
-
if (totalImages > 0) {
|
|
188
|
-
success(`Processed ${totalImages} preview image${totalImages > 1 ? 's' : ''} (converted to webp)`)
|
|
189
|
-
} else {
|
|
190
|
-
log(` ${colors.dim}No preview images found${colors.reset}`)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// 6. Write schema.json
|
|
194
|
-
const schemaPath = join(distDir, 'schema.json')
|
|
195
|
-
await writeFile(schemaPath, JSON.stringify(schema, null, 2), 'utf-8')
|
|
196
|
-
|
|
197
|
-
success(`Generated schema.json with ${componentNames.length} components`)
|
|
166
|
+
success('Vite build complete')
|
|
198
167
|
|
|
199
168
|
// Summary
|
|
200
169
|
log('')
|
|
201
170
|
log(`${colors.green}${colors.bright}Build complete!${colors.reset}`)
|
|
171
|
+
|
|
202
172
|
log('')
|
|
203
|
-
log(`
|
|
204
|
-
log(` ${colors.
|
|
205
|
-
log(` ${colors.
|
|
206
|
-
log(` ${colors.dim}dist/schema.json${colors.reset} - Component schemas`)
|
|
207
|
-
if (totalImages > 0) {
|
|
208
|
-
log(` ${colors.dim}dist/assets/[component]/${colors.reset} - Preview images`)
|
|
209
|
-
}
|
|
173
|
+
log(`${colors.bright}Share with clients:${colors.reset}`)
|
|
174
|
+
log(` ${colors.bright}uniweb publish${colors.reset} Register your foundation (one-time setup)`)
|
|
175
|
+
log(` ${colors.bright}uniweb handoff <email>${colors.reset} Hand off a site to a client`)
|
|
210
176
|
}
|
|
211
177
|
|
|
212
178
|
/**
|
|
@@ -225,7 +191,7 @@ async function loadI18nConfig(projectDir, siteConfig = null) {
|
|
|
225
191
|
|
|
226
192
|
// Resolve locales (undefined/'*' → all available, array → specific)
|
|
227
193
|
const { resolveLocales } = await import('@uniweb/build/i18n')
|
|
228
|
-
const locales = await resolveLocales(config.
|
|
194
|
+
const locales = await resolveLocales(config.languages, localesPath)
|
|
229
195
|
|
|
230
196
|
if (locales.length === 0) return null
|
|
231
197
|
|
|
@@ -489,6 +455,8 @@ async function buildSite(projectDir, options = {}) {
|
|
|
489
455
|
process.exit(1)
|
|
490
456
|
}
|
|
491
457
|
}
|
|
458
|
+
|
|
459
|
+
showNextSteps(false, true)
|
|
492
460
|
}
|
|
493
461
|
|
|
494
462
|
/**
|
|
@@ -625,6 +593,26 @@ async function buildWorkspace(workspaceDir, options = {}) {
|
|
|
625
593
|
log(`${colors.green}${colors.bright}Workspace build complete!${colors.reset}`)
|
|
626
594
|
log('')
|
|
627
595
|
log(`Built ${parts.join(', ')}`)
|
|
596
|
+
|
|
597
|
+
showNextSteps(foundations.length > 0, sites.length > 0)
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Show next-step hints after workspace build
|
|
602
|
+
*/
|
|
603
|
+
function showNextSteps(hasFoundations, hasSites) {
|
|
604
|
+
if (hasFoundations) {
|
|
605
|
+
log('')
|
|
606
|
+
log(`${colors.bright}Share with clients:${colors.reset}`)
|
|
607
|
+
log(` ${colors.bright}uniweb publish${colors.reset} Register your foundation (one-time setup)`)
|
|
608
|
+
log(` ${colors.bright}uniweb handoff <email>${colors.reset} Hand off a site to a client`)
|
|
609
|
+
}
|
|
610
|
+
if (hasSites) {
|
|
611
|
+
log('')
|
|
612
|
+
log(`${colors.bright}Deploy:${colors.reset}`)
|
|
613
|
+
log(` ${colors.bright}uniweb deploy${colors.reset} Uniweb hosting`)
|
|
614
|
+
log(` Or upload ${colors.cyan}dist/${colors.reset} to any static host`)
|
|
615
|
+
}
|
|
628
616
|
}
|
|
629
617
|
|
|
630
618
|
/**
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deploy Command
|
|
3
|
+
*
|
|
4
|
+
* Deploys a built site to Uniweb hosting.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* uniweb deploy # Deploy to Uniweb hosting
|
|
8
|
+
* uniweb deploy --local # Deploy to local server (no auth)
|
|
9
|
+
* uniweb deploy --registry <url> # Deploy to a specific server URL
|
|
10
|
+
* uniweb deploy --dry-run # Show what would be deployed
|
|
11
|
+
* uniweb deploy --prod # Deploy to production (future)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync } from 'node:fs'
|
|
15
|
+
import { readFile, readdir } from 'node:fs/promises'
|
|
16
|
+
import { resolve, join, basename, relative } from 'node:path'
|
|
17
|
+
import { execSync } from 'node:child_process'
|
|
18
|
+
|
|
19
|
+
import { ensureAuth } from '../utils/auth.js'
|
|
20
|
+
import { findWorkspaceRoot, findSites, findFoundations, classifyPackage, promptSelect } from '../utils/workspace.js'
|
|
21
|
+
import { isNonInteractive, getCliPrefix } from '../utils/interactive.js'
|
|
22
|
+
|
|
23
|
+
// Colors for terminal output
|
|
24
|
+
const colors = {
|
|
25
|
+
reset: '\x1b[0m',
|
|
26
|
+
bright: '\x1b[1m',
|
|
27
|
+
dim: '\x1b[2m',
|
|
28
|
+
cyan: '\x1b[36m',
|
|
29
|
+
green: '\x1b[32m',
|
|
30
|
+
yellow: '\x1b[33m',
|
|
31
|
+
red: '\x1b[31m',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function success(message) {
|
|
35
|
+
console.log(`${colors.green}✓${colors.reset} ${message}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function error(message) {
|
|
39
|
+
console.error(`${colors.red}✗${colors.reset} ${message}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function info(message) {
|
|
43
|
+
console.log(`${colors.cyan}→${colors.reset} ${message}`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the site directory to deploy.
|
|
48
|
+
*
|
|
49
|
+
* Priority:
|
|
50
|
+
* 1. In a site directory → use it
|
|
51
|
+
* 2. At workspace root, one site → use it
|
|
52
|
+
* 3. At workspace root, multiple → prompt (or error if non-interactive)
|
|
53
|
+
* 4. No site → educational error with alternatives
|
|
54
|
+
*
|
|
55
|
+
* @param {string[]} args
|
|
56
|
+
* @returns {Promise<string>} Absolute path to the site directory
|
|
57
|
+
*/
|
|
58
|
+
async function resolveSiteDir(args) {
|
|
59
|
+
const cwd = process.cwd()
|
|
60
|
+
const prefix = getCliPrefix()
|
|
61
|
+
|
|
62
|
+
// Check if current directory is a site
|
|
63
|
+
const type = await classifyPackage(cwd)
|
|
64
|
+
if (type === 'site') {
|
|
65
|
+
return cwd
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check workspace
|
|
69
|
+
const workspaceRoot = findWorkspaceRoot(cwd)
|
|
70
|
+
if (workspaceRoot) {
|
|
71
|
+
const sites = await findSites(workspaceRoot)
|
|
72
|
+
|
|
73
|
+
if (sites.length === 1) {
|
|
74
|
+
return resolve(workspaceRoot, sites[0])
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (sites.length > 1) {
|
|
78
|
+
if (isNonInteractive(args)) {
|
|
79
|
+
error('Multiple sites found. Specify which one to deploy.')
|
|
80
|
+
console.log('')
|
|
81
|
+
for (const s of sites) {
|
|
82
|
+
console.log(` ${colors.cyan}cd ${s} && ${prefix} deploy${colors.reset}`)
|
|
83
|
+
}
|
|
84
|
+
process.exit(1)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const choice = await promptSelect('Which site?', sites)
|
|
88
|
+
if (!choice) {
|
|
89
|
+
console.log('\nDeploy cancelled.')
|
|
90
|
+
process.exit(0)
|
|
91
|
+
}
|
|
92
|
+
return resolve(workspaceRoot, choice)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// No site found — educational error
|
|
97
|
+
error('No site found in this workspace.')
|
|
98
|
+
console.log('')
|
|
99
|
+
console.log(` ${colors.dim}\`deploy\` uploads your built site to Uniweb hosting.${colors.reset}`)
|
|
100
|
+
console.log('')
|
|
101
|
+
console.log(` ${colors.dim}The site is a standard Vite build — you can also upload dist/${colors.reset}`)
|
|
102
|
+
console.log(` ${colors.dim}to any static host.${colors.reset}`)
|
|
103
|
+
process.exit(1)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Parse --registry <url> from args.
|
|
108
|
+
* @param {string[]} args
|
|
109
|
+
* @returns {string|null}
|
|
110
|
+
*/
|
|
111
|
+
function parseRegistryUrl(args) {
|
|
112
|
+
const idx = args.indexOf('--registry')
|
|
113
|
+
if (idx === -1 || !args[idx + 1]) return null
|
|
114
|
+
return args[idx + 1]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Derive a siteId from the site's package.json or directory name.
|
|
119
|
+
* @param {string} siteDir
|
|
120
|
+
* @returns {Promise<string>}
|
|
121
|
+
*/
|
|
122
|
+
async function deriveSiteId(siteDir) {
|
|
123
|
+
const pkgPath = join(siteDir, 'package.json')
|
|
124
|
+
if (existsSync(pkgPath)) {
|
|
125
|
+
try {
|
|
126
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
127
|
+
if (pkg.name) return pkg.name
|
|
128
|
+
} catch {
|
|
129
|
+
// Fall through
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return basename(siteDir)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Walk a directory recursively and collect all files as base64.
|
|
137
|
+
* @param {string} dir
|
|
138
|
+
* @returns {Promise<Object<string, string>>} Map of relative paths to base64 content
|
|
139
|
+
*/
|
|
140
|
+
async function collectFiles(dir) {
|
|
141
|
+
const files = {}
|
|
142
|
+
const entries = await readdir(dir, { withFileTypes: true, recursive: true })
|
|
143
|
+
|
|
144
|
+
for (const entry of entries) {
|
|
145
|
+
if (!entry.isFile()) continue
|
|
146
|
+
const fullPath = join(entry.parentPath || entry.path, entry.name)
|
|
147
|
+
const relPath = relative(dir, fullPath)
|
|
148
|
+
const content = await readFile(fullPath)
|
|
149
|
+
files[relPath] = content.toString('base64')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return files
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Main deploy command handler
|
|
157
|
+
*/
|
|
158
|
+
export async function deploy(args = []) {
|
|
159
|
+
const isLocal = args.includes('--local')
|
|
160
|
+
const isDryRun = args.includes('--dry-run')
|
|
161
|
+
const registryUrl = parseRegistryUrl(args)
|
|
162
|
+
const prefix = getCliPrefix()
|
|
163
|
+
|
|
164
|
+
// 1. Resolve site directory
|
|
165
|
+
const siteDir = await resolveSiteDir(args)
|
|
166
|
+
|
|
167
|
+
// 2. Check auth (unless --local)
|
|
168
|
+
let token = null
|
|
169
|
+
if (!isLocal) {
|
|
170
|
+
token = await ensureAuth({ command: 'Deploying' })
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 3. Auto-build if dist/ is missing
|
|
174
|
+
const distDir = join(siteDir, 'dist')
|
|
175
|
+
const indexHtml = join(distDir, 'index.html')
|
|
176
|
+
|
|
177
|
+
if (!existsSync(indexHtml)) {
|
|
178
|
+
console.log(`${colors.yellow}⚠${colors.reset} No build found. Building site...`)
|
|
179
|
+
console.log('')
|
|
180
|
+
execSync('npx uniweb build', {
|
|
181
|
+
cwd: siteDir,
|
|
182
|
+
stdio: 'inherit',
|
|
183
|
+
})
|
|
184
|
+
console.log('')
|
|
185
|
+
|
|
186
|
+
if (!existsSync(indexHtml)) {
|
|
187
|
+
error('Build did not produce dist/index.html')
|
|
188
|
+
process.exit(1)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 4. Derive siteId
|
|
193
|
+
const siteId = await deriveSiteId(siteDir)
|
|
194
|
+
|
|
195
|
+
// 5. Collect files from dist/
|
|
196
|
+
const files = await collectFiles(distDir)
|
|
197
|
+
const filesCount = Object.keys(files).length
|
|
198
|
+
|
|
199
|
+
// 6. Dry-run check
|
|
200
|
+
if (isDryRun) {
|
|
201
|
+
console.log('')
|
|
202
|
+
info(`Would deploy ${colors.bright}${siteId}${colors.reset} (${filesCount} files)`)
|
|
203
|
+
console.log(` ${colors.dim}Source: ${distDir}${colors.reset}`)
|
|
204
|
+
const serverUrl = registryUrl || process.env.UNIWEB_REGISTRY_URL || 'http://localhost:4001'
|
|
205
|
+
console.log(` ${colors.dim}Target: ${serverUrl}/sites/${siteId}/${colors.reset}`)
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 7. Deploy
|
|
210
|
+
const serverUrl = registryUrl || process.env.UNIWEB_REGISTRY_URL || 'http://localhost:4001'
|
|
211
|
+
info(`Deploying ${colors.bright}${siteId}${colors.reset} (${filesCount} files)...`)
|
|
212
|
+
|
|
213
|
+
const headers = { 'Content-Type': 'application/json' }
|
|
214
|
+
if (token) {
|
|
215
|
+
headers['Authorization'] = `Bearer ${token}`
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const payload = {
|
|
219
|
+
siteId,
|
|
220
|
+
files,
|
|
221
|
+
metadata: {
|
|
222
|
+
deployedBy: isLocal ? 'local' : 'cli',
|
|
223
|
+
},
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let res
|
|
227
|
+
try {
|
|
228
|
+
res = await fetch(`${serverUrl}/deploy`, {
|
|
229
|
+
method: 'POST',
|
|
230
|
+
headers,
|
|
231
|
+
body: JSON.stringify(payload),
|
|
232
|
+
})
|
|
233
|
+
} catch (err) {
|
|
234
|
+
error(`Could not connect to ${serverUrl}`)
|
|
235
|
+
console.log('')
|
|
236
|
+
console.log(` ${colors.dim}Make sure the cloud server is running:${colors.reset}`)
|
|
237
|
+
console.log(` ${colors.cyan}cd packages/cloud && pnpm dev${colors.reset}`)
|
|
238
|
+
process.exit(1)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const body = await res.json()
|
|
242
|
+
|
|
243
|
+
if (!res.ok) {
|
|
244
|
+
if (res.status === 401) {
|
|
245
|
+
error('Authentication failed.')
|
|
246
|
+
console.log(` Run ${colors.cyan}${prefix} login${colors.reset} to refresh your credentials.`)
|
|
247
|
+
process.exit(1)
|
|
248
|
+
}
|
|
249
|
+
error(body.error || `Deploy failed (${res.status})`)
|
|
250
|
+
process.exit(1)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
console.log('')
|
|
254
|
+
success(`Deployed ${colors.bright}${siteId}${colors.reset}`)
|
|
255
|
+
|
|
256
|
+
const siteUrl = body.siteUrl
|
|
257
|
+
? `${serverUrl}${body.siteUrl}`
|
|
258
|
+
: `${serverUrl}/sites/${siteId}/`
|
|
259
|
+
console.log(` ${colors.cyan}${siteUrl}${colors.reset}`)
|
|
260
|
+
|
|
261
|
+
// Cross-promotion: if workspace has a foundation, tip about publish
|
|
262
|
+
const workspaceRoot = findWorkspaceRoot(siteDir)
|
|
263
|
+
if (workspaceRoot) {
|
|
264
|
+
const foundations = await findFoundations(workspaceRoot)
|
|
265
|
+
if (foundations.length > 0) {
|
|
266
|
+
console.log('')
|
|
267
|
+
console.log(` ${colors.dim}Tip: Run \`${prefix} publish\` to register your foundation and invite clients.${colors.reset}`)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export default deploy
|
package/src/commands/docs.js
CHANGED
|
@@ -80,7 +80,7 @@ ${colors.bright}Page Ordering:${colors.reset}
|
|
|
80
80
|
Rest are auto-discovered
|
|
81
81
|
|
|
82
82
|
${colors.bright}Internationalization:${colors.reset}
|
|
83
|
-
${colors.cyan}
|
|
83
|
+
${colors.cyan}languages${colors.reset} Languages to build (array or "*" for all)
|
|
84
84
|
Example: [es, fr, de]
|
|
85
85
|
${colors.cyan}i18n.localesDir${colors.reset} Translation files directory (default: "locales")
|
|
86
86
|
|