uniweb 0.2.2 → 0.2.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/README.md +2 -2
- package/package.json +4 -2
- package/src/index.js +58 -19
- package/src/templates/fetchers/github.js +98 -0
- package/src/templates/fetchers/npm.js +96 -0
- package/src/templates/index.js +209 -0
- package/src/templates/resolver.js +116 -0
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ my-project/
|
|
|
40
40
|
└── index.jsx
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
**Pages are folders.** Create `pages/about/` with
|
|
43
|
+
**Pages are folders.** Create `pages/about/` with markdown files inside → visit `/about`. That's the whole routing model.
|
|
44
44
|
|
|
45
45
|
### Content as Markdown
|
|
46
46
|
|
|
@@ -240,7 +240,7 @@ A monorepo for multi-site or multi-foundation development.
|
|
|
240
240
|
```
|
|
241
241
|
my-workspace/
|
|
242
242
|
├── package.json # Workspace root (includes workspaces field for npm)
|
|
243
|
-
├── pnpm-workspace.yaml # Same config as
|
|
243
|
+
├── pnpm-workspace.yaml # Same config as single template
|
|
244
244
|
│
|
|
245
245
|
├── sites/
|
|
246
246
|
│ ├── marketing/ # Main marketing site
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@uniweb/build": "^0.1.0",
|
|
35
|
-
"
|
|
35
|
+
"@uniweb/templates": "workspace:*",
|
|
36
|
+
"prompts": "^2.4.2",
|
|
37
|
+
"tar": "^7.0.0"
|
|
36
38
|
}
|
|
37
39
|
}
|
package/src/index.js
CHANGED
|
@@ -18,6 +18,13 @@ import { resolve, join, dirname } from 'node:path'
|
|
|
18
18
|
import { fileURLToPath } from 'node:url'
|
|
19
19
|
import prompts from 'prompts'
|
|
20
20
|
import { build } from './commands/build.js'
|
|
21
|
+
import {
|
|
22
|
+
resolveTemplate,
|
|
23
|
+
applyExternalTemplate,
|
|
24
|
+
parseTemplateId,
|
|
25
|
+
listAvailableTemplates,
|
|
26
|
+
BUILTIN_TEMPLATES,
|
|
27
|
+
} from './templates/index.js'
|
|
21
28
|
|
|
22
29
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
23
30
|
|
|
@@ -93,9 +100,11 @@ async function main() {
|
|
|
93
100
|
const templateIndex = args.indexOf('--template')
|
|
94
101
|
if (templateIndex !== -1 && args[templateIndex + 1]) {
|
|
95
102
|
templateType = args[templateIndex + 1]
|
|
96
|
-
if
|
|
97
|
-
|
|
98
|
-
|
|
103
|
+
// Validate template identifier (will throw if invalid)
|
|
104
|
+
try {
|
|
105
|
+
parseTemplateId(templateType)
|
|
106
|
+
} catch (err) {
|
|
107
|
+
error(`Invalid template: ${err.message}`)
|
|
99
108
|
process.exit(1)
|
|
100
109
|
}
|
|
101
110
|
}
|
|
@@ -155,16 +164,40 @@ async function main() {
|
|
|
155
164
|
process.exit(1)
|
|
156
165
|
}
|
|
157
166
|
|
|
158
|
-
|
|
167
|
+
// Resolve and create project based on template
|
|
168
|
+
const parsed = parseTemplateId(templateType)
|
|
159
169
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
170
|
+
if (parsed.type === 'builtin') {
|
|
171
|
+
log(`\nCreating ${templates[templateType].name.toLowerCase()}...`)
|
|
172
|
+
|
|
173
|
+
// Generate project based on built-in template
|
|
174
|
+
switch (templateType) {
|
|
175
|
+
case 'single':
|
|
176
|
+
await createSingleProject(projectDir, projectName)
|
|
177
|
+
break
|
|
178
|
+
case 'multi':
|
|
179
|
+
await createMultiProject(projectDir, projectName)
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
// External template (official, npm, or github)
|
|
184
|
+
log(`\nResolving template: ${templateType}...`)
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const resolved = await resolveTemplate(templateType, {
|
|
188
|
+
onProgress: (msg) => log(` ${colors.dim}${msg}${colors.reset}`),
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
log(`\nCreating project from ${resolved.name || resolved.package || `${resolved.owner}/${resolved.repo}`}...`)
|
|
192
|
+
|
|
193
|
+
await applyExternalTemplate(resolved, projectDir, { projectName }, {
|
|
194
|
+
onProgress: (msg) => log(` ${colors.dim}${msg}${colors.reset}`),
|
|
195
|
+
onWarning: (msg) => log(` ${colors.yellow}Warning: ${msg}${colors.reset}`),
|
|
196
|
+
})
|
|
197
|
+
} catch (err) {
|
|
198
|
+
error(`Failed to apply template: ${err.message}`)
|
|
199
|
+
process.exit(1)
|
|
200
|
+
}
|
|
168
201
|
}
|
|
169
202
|
|
|
170
203
|
// Success message
|
|
@@ -189,22 +222,27 @@ ${colors.bright}Commands:${colors.reset}
|
|
|
189
222
|
build Build the current project
|
|
190
223
|
|
|
191
224
|
${colors.bright}Create Options:${colors.reset}
|
|
192
|
-
--template <type> Project template
|
|
225
|
+
--template <type> Project template
|
|
193
226
|
|
|
194
227
|
${colors.bright}Build Options:${colors.reset}
|
|
195
228
|
--target <type> Build target (foundation, site) - auto-detected if not specified
|
|
196
229
|
--platform <name> Deployment platform (e.g., vercel) for platform-specific output
|
|
197
230
|
|
|
231
|
+
${colors.bright}Template Types:${colors.reset}
|
|
232
|
+
single One site + one foundation (default)
|
|
233
|
+
multi Multiple sites and foundations
|
|
234
|
+
marketing Official marketing template
|
|
235
|
+
@scope/template-name npm package
|
|
236
|
+
github:user/repo GitHub repository
|
|
237
|
+
https://github.com/user/repo GitHub URL
|
|
238
|
+
|
|
198
239
|
${colors.bright}Examples:${colors.reset}
|
|
199
240
|
npx uniweb create my-project
|
|
200
241
|
npx uniweb create my-project --template single
|
|
201
|
-
npx uniweb create my-
|
|
242
|
+
npx uniweb create my-project --template marketing
|
|
243
|
+
npx uniweb create my-project --template github:myorg/template
|
|
202
244
|
npx uniweb build
|
|
203
245
|
npx uniweb build --target foundation
|
|
204
|
-
|
|
205
|
-
${colors.bright}Templates:${colors.reset}
|
|
206
|
-
single One site + one foundation in site/ and foundation/ (default)
|
|
207
|
-
multi Multiple sites and foundations in sites/* and foundations/*
|
|
208
246
|
`)
|
|
209
247
|
}
|
|
210
248
|
|
|
@@ -843,7 +881,7 @@ async function createFoundation(projectDir, projectName, isWorkspace = false) {
|
|
|
843
881
|
files: ['dist', 'src'],
|
|
844
882
|
scripts: {
|
|
845
883
|
dev: 'vite',
|
|
846
|
-
build: '
|
|
884
|
+
build: 'uniweb build',
|
|
847
885
|
'build:vite': 'vite build',
|
|
848
886
|
preview: 'vite preview',
|
|
849
887
|
},
|
|
@@ -858,6 +896,7 @@ async function createFoundation(projectDir, projectName, isWorkspace = false) {
|
|
|
858
896
|
react: '^18.2.0',
|
|
859
897
|
'react-dom': '^18.2.0',
|
|
860
898
|
tailwindcss: '^3.4.1',
|
|
899
|
+
uniweb: '^0.2.0',
|
|
861
900
|
vite: '^5.1.0',
|
|
862
901
|
'vite-plugin-svgr': '^4.2.0',
|
|
863
902
|
},
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub fetcher - downloads templates from GitHub repositories
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { pipeline } from 'node:stream/promises'
|
|
6
|
+
import { createGunzip } from 'node:zlib'
|
|
7
|
+
import { mkdtemp, rm } from 'node:fs/promises'
|
|
8
|
+
import { tmpdir } from 'node:os'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
import * as tar from 'tar'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Fetch a template from a GitHub repository
|
|
14
|
+
*
|
|
15
|
+
* @param {string} owner - Repository owner
|
|
16
|
+
* @param {string} repo - Repository name
|
|
17
|
+
* @param {Object} options - Fetch options
|
|
18
|
+
* @param {string} options.ref - Branch, tag, or commit (default: HEAD)
|
|
19
|
+
* @param {Function} options.onProgress - Progress callback
|
|
20
|
+
* @returns {Promise<Object>} { tempDir, ref }
|
|
21
|
+
*/
|
|
22
|
+
export async function fetchGitHubTemplate(owner, repo, options = {}) {
|
|
23
|
+
const { ref = 'HEAD', onProgress } = options
|
|
24
|
+
|
|
25
|
+
const displayRef = ref === 'HEAD' ? 'latest' : ref
|
|
26
|
+
onProgress?.(`Fetching ${owner}/${repo}@${displayRef} from GitHub...`)
|
|
27
|
+
|
|
28
|
+
// GitHub provides tarballs without requiring git
|
|
29
|
+
const tarballUrl = `https://api.github.com/repos/${owner}/${repo}/tarball/${ref}`
|
|
30
|
+
|
|
31
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'uniweb-template-'))
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetchWithRetry(tarballUrl, {
|
|
35
|
+
headers: {
|
|
36
|
+
'Accept': 'application/vnd.github+json',
|
|
37
|
+
'User-Agent': 'uniweb-cli',
|
|
38
|
+
// Support private repos if GITHUB_TOKEN is set
|
|
39
|
+
...(process.env.GITHUB_TOKEN && {
|
|
40
|
+
'Authorization': `Bearer ${process.env.GITHUB_TOKEN}`
|
|
41
|
+
}),
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
if (response.status === 404) {
|
|
47
|
+
throw new Error(`Repository not found: ${owner}/${repo}`)
|
|
48
|
+
}
|
|
49
|
+
if (response.status === 403) {
|
|
50
|
+
const remaining = response.headers.get('x-ratelimit-remaining')
|
|
51
|
+
if (remaining === '0') {
|
|
52
|
+
throw new Error(
|
|
53
|
+
'GitHub API rate limit exceeded. Set GITHUB_TOKEN environment variable for higher limits.'
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
throw new Error(`GitHub API error: ${response.status}`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
onProgress?.(`Downloading and extracting...`)
|
|
61
|
+
|
|
62
|
+
await pipeline(
|
|
63
|
+
response.body,
|
|
64
|
+
createGunzip(),
|
|
65
|
+
tar.extract({ cwd: tempDir, strip: 1 }) // strip 'owner-repo-sha/' prefix
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
onProgress?.(`Extracted to ${tempDir}`)
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
tempDir,
|
|
72
|
+
ref: displayRef,
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
// Clean up on error
|
|
76
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {})
|
|
77
|
+
throw err
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Fetch with retry and timeout
|
|
83
|
+
*/
|
|
84
|
+
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
|
|
85
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
86
|
+
try {
|
|
87
|
+
const response = await fetch(url, {
|
|
88
|
+
...options,
|
|
89
|
+
signal: AbortSignal.timeout(60000), // 60s timeout for GitHub (can be slow)
|
|
90
|
+
})
|
|
91
|
+
return response
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (attempt === maxRetries) throw err
|
|
94
|
+
const delay = Math.min(1000 * Math.pow(2, attempt), 10000)
|
|
95
|
+
await new Promise(r => setTimeout(r, delay))
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* npm package fetcher - downloads and extracts templates from npm registry
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { pipeline } from 'node:stream/promises'
|
|
6
|
+
import { createGunzip } from 'node:zlib'
|
|
7
|
+
import { mkdtemp, rm } from 'node:fs/promises'
|
|
8
|
+
import { tmpdir } from 'node:os'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
import * as tar from 'tar'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Fetch a template from npm registry
|
|
14
|
+
*
|
|
15
|
+
* @param {string} packageName - npm package name (e.g., '@uniweb/template-marketing')
|
|
16
|
+
* @param {Object} options - Fetch options
|
|
17
|
+
* @param {Function} options.onProgress - Progress callback
|
|
18
|
+
* @returns {Promise<Object>} { tempDir, version, metadata }
|
|
19
|
+
*/
|
|
20
|
+
export async function fetchNpmTemplate(packageName, options = {}) {
|
|
21
|
+
const { onProgress } = options
|
|
22
|
+
|
|
23
|
+
onProgress?.(`Fetching package info for ${packageName}...`)
|
|
24
|
+
|
|
25
|
+
// Query npm registry for package metadata
|
|
26
|
+
const registryUrl = `https://registry.npmjs.org/${encodeURIComponent(packageName).replace('%40', '@')}`
|
|
27
|
+
|
|
28
|
+
const metaResponse = await fetchWithRetry(registryUrl)
|
|
29
|
+
if (!metaResponse.ok) {
|
|
30
|
+
if (metaResponse.status === 404) {
|
|
31
|
+
throw new Error(`Package not found: ${packageName}`)
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`npm registry error: ${metaResponse.status}`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const pkgMeta = await metaResponse.json()
|
|
37
|
+
const version = pkgMeta['dist-tags']?.latest
|
|
38
|
+
if (!version) {
|
|
39
|
+
throw new Error(`No published versions found for ${packageName}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const versionMeta = pkgMeta.versions[version]
|
|
43
|
+
const tarballUrl = versionMeta?.dist?.tarball
|
|
44
|
+
if (!tarballUrl) {
|
|
45
|
+
throw new Error(`No tarball URL found for ${packageName}@${version}`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
onProgress?.(`Downloading ${packageName}@${version}...`)
|
|
49
|
+
|
|
50
|
+
// Download and extract tarball
|
|
51
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'uniweb-template-'))
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const tarballResponse = await fetchWithRetry(tarballUrl)
|
|
55
|
+
if (!tarballResponse.ok) {
|
|
56
|
+
throw new Error(`Failed to download tarball: ${tarballResponse.status}`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await pipeline(
|
|
60
|
+
tarballResponse.body,
|
|
61
|
+
createGunzip(),
|
|
62
|
+
tar.extract({ cwd: tempDir, strip: 1 }) // strip 'package/' prefix
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
onProgress?.(`Extracted to ${tempDir}`)
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
tempDir,
|
|
69
|
+
version,
|
|
70
|
+
metadata: versionMeta,
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
// Clean up on error
|
|
74
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {})
|
|
75
|
+
throw err
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Fetch with retry and timeout
|
|
81
|
+
*/
|
|
82
|
+
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
|
|
83
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
84
|
+
try {
|
|
85
|
+
const response = await fetch(url, {
|
|
86
|
+
...options,
|
|
87
|
+
signal: AbortSignal.timeout(30000), // 30s timeout
|
|
88
|
+
})
|
|
89
|
+
return response
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (attempt === maxRetries) throw err
|
|
92
|
+
const delay = Math.min(1000 * Math.pow(2, attempt), 10000)
|
|
93
|
+
await new Promise(r => setTimeout(r, delay))
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template resolution and application for the CLI
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { rm } from 'node:fs/promises'
|
|
6
|
+
import { existsSync } from 'node:fs'
|
|
7
|
+
import { join, dirname } from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
|
|
10
|
+
import { parseTemplateId, getTemplateDisplayName, BUILTIN_TEMPLATES, OFFICIAL_TEMPLATES } from './resolver.js'
|
|
11
|
+
import { fetchNpmTemplate } from './fetchers/npm.js'
|
|
12
|
+
import { fetchGitHubTemplate } from './fetchers/github.js'
|
|
13
|
+
|
|
14
|
+
// Try to import from @uniweb/templates if available
|
|
15
|
+
let templatesPackage = null
|
|
16
|
+
try {
|
|
17
|
+
templatesPackage = await import('@uniweb/templates')
|
|
18
|
+
} catch {
|
|
19
|
+
// @uniweb/templates not installed - official templates won't be available locally
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve a template identifier and return the template path
|
|
24
|
+
*
|
|
25
|
+
* @param {string} identifier - Template identifier
|
|
26
|
+
* @param {Object} options - Resolution options
|
|
27
|
+
* @param {Function} options.onProgress - Progress callback
|
|
28
|
+
* @returns {Promise<Object>} { type, path, metadata, cleanup }
|
|
29
|
+
*/
|
|
30
|
+
export async function resolveTemplate(identifier, options = {}) {
|
|
31
|
+
const { onProgress } = options
|
|
32
|
+
const parsed = parseTemplateId(identifier)
|
|
33
|
+
|
|
34
|
+
switch (parsed.type) {
|
|
35
|
+
case 'builtin':
|
|
36
|
+
return {
|
|
37
|
+
type: 'builtin',
|
|
38
|
+
name: parsed.name,
|
|
39
|
+
// Built-in templates are handled programmatically by the CLI
|
|
40
|
+
// No path or cleanup needed
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
case 'official':
|
|
44
|
+
return resolveOfficialTemplate(parsed.name, options)
|
|
45
|
+
|
|
46
|
+
case 'npm':
|
|
47
|
+
return resolveNpmTemplate(parsed.package, options)
|
|
48
|
+
|
|
49
|
+
case 'github':
|
|
50
|
+
return resolveGitHubTemplate(parsed, options)
|
|
51
|
+
|
|
52
|
+
default:
|
|
53
|
+
throw new Error(`Unknown template type: ${parsed.type}`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve an official template from @uniweb/templates
|
|
59
|
+
*/
|
|
60
|
+
async function resolveOfficialTemplate(name, options = {}) {
|
|
61
|
+
const { onProgress } = options
|
|
62
|
+
|
|
63
|
+
if (!templatesPackage) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Official template "${name}" requires @uniweb/templates package.\n` +
|
|
66
|
+
`Install it with: npm install @uniweb/templates`
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!templatesPackage.hasTemplate(name)) {
|
|
71
|
+
const available = await templatesPackage.listBuiltinTemplates()
|
|
72
|
+
const names = available.map(t => t.id).join(', ')
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Official template "${name}" not found.\n` +
|
|
75
|
+
`Available templates: ${names || 'none'}`
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const templatePath = templatesPackage.getTemplatePath(name)
|
|
80
|
+
|
|
81
|
+
onProgress?.(`Using official template: ${name}`)
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
type: 'official',
|
|
85
|
+
name,
|
|
86
|
+
path: templatePath,
|
|
87
|
+
cleanup: async () => {}, // Nothing to clean up
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolve a template from npm
|
|
93
|
+
*/
|
|
94
|
+
async function resolveNpmTemplate(packageName, options = {}) {
|
|
95
|
+
const { onProgress } = options
|
|
96
|
+
|
|
97
|
+
onProgress?.(`Resolving npm template: ${packageName}`)
|
|
98
|
+
|
|
99
|
+
const { tempDir, version, metadata } = await fetchNpmTemplate(packageName, { onProgress })
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
type: 'npm',
|
|
103
|
+
package: packageName,
|
|
104
|
+
version,
|
|
105
|
+
path: tempDir,
|
|
106
|
+
cleanup: async () => {
|
|
107
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {})
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resolve a template from GitHub
|
|
114
|
+
*/
|
|
115
|
+
async function resolveGitHubTemplate(parsed, options = {}) {
|
|
116
|
+
const { onProgress } = options
|
|
117
|
+
const { owner, repo, ref } = parsed
|
|
118
|
+
|
|
119
|
+
onProgress?.(`Resolving GitHub template: ${owner}/${repo}`)
|
|
120
|
+
|
|
121
|
+
const { tempDir } = await fetchGitHubTemplate(owner, repo, { ref, onProgress })
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
type: 'github',
|
|
125
|
+
owner,
|
|
126
|
+
repo,
|
|
127
|
+
ref,
|
|
128
|
+
path: tempDir,
|
|
129
|
+
cleanup: async () => {
|
|
130
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {})
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Apply an external template to a target directory
|
|
137
|
+
*
|
|
138
|
+
* @param {Object} resolved - Resolved template from resolveTemplate()
|
|
139
|
+
* @param {string} targetPath - Target directory
|
|
140
|
+
* @param {Object} data - Template variables
|
|
141
|
+
* @param {Object} options - Apply options
|
|
142
|
+
*/
|
|
143
|
+
export async function applyExternalTemplate(resolved, targetPath, data, options = {}) {
|
|
144
|
+
const { onProgress, onWarning } = options
|
|
145
|
+
|
|
146
|
+
if (!templatesPackage) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
'External template application requires @uniweb/templates package.\n' +
|
|
149
|
+
'Install it with: npm install @uniweb/templates'
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const metadata = await templatesPackage.applyTemplate(
|
|
155
|
+
resolved.path,
|
|
156
|
+
targetPath,
|
|
157
|
+
data,
|
|
158
|
+
{ onProgress, onWarning }
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return metadata
|
|
162
|
+
} finally {
|
|
163
|
+
// Clean up temp directory if there is one
|
|
164
|
+
if (resolved.cleanup) {
|
|
165
|
+
await resolved.cleanup()
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* List all available templates
|
|
172
|
+
*/
|
|
173
|
+
export async function listAvailableTemplates() {
|
|
174
|
+
const templates = []
|
|
175
|
+
|
|
176
|
+
// Built-in templates
|
|
177
|
+
for (const name of BUILTIN_TEMPLATES) {
|
|
178
|
+
templates.push({
|
|
179
|
+
type: 'builtin',
|
|
180
|
+
id: name,
|
|
181
|
+
name: name.charAt(0).toUpperCase() + name.slice(1),
|
|
182
|
+
description: name === 'single'
|
|
183
|
+
? 'One site + one foundation'
|
|
184
|
+
: 'Multiple sites and foundations',
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Official templates from @uniweb/templates
|
|
189
|
+
if (templatesPackage) {
|
|
190
|
+
try {
|
|
191
|
+
const official = await templatesPackage.listBuiltinTemplates()
|
|
192
|
+
for (const t of official) {
|
|
193
|
+
templates.push({
|
|
194
|
+
type: 'official',
|
|
195
|
+
id: t.id,
|
|
196
|
+
name: t.name,
|
|
197
|
+
description: t.description,
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
// Ignore errors listing official templates
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return templates
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Re-export for convenience
|
|
209
|
+
export { parseTemplateId, getTemplateDisplayName, BUILTIN_TEMPLATES, OFFICIAL_TEMPLATES }
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template resolver - parses template identifiers and determines source type
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Built-in templates that are generated programmatically
|
|
6
|
+
export const BUILTIN_TEMPLATES = ['single', 'multi']
|
|
7
|
+
|
|
8
|
+
// Official templates from @uniweb/templates package
|
|
9
|
+
export const OFFICIAL_TEMPLATES = ['marketing', 'docs', 'learning']
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse a template identifier and determine its source type
|
|
13
|
+
*
|
|
14
|
+
* @param {string} identifier - Template identifier (e.g., 'single', 'marketing', 'github:user/repo')
|
|
15
|
+
* @returns {Object} Parsed template info
|
|
16
|
+
*/
|
|
17
|
+
export function parseTemplateId(identifier) {
|
|
18
|
+
if (!identifier || typeof identifier !== 'string') {
|
|
19
|
+
throw new Error('Template identifier is required')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
identifier = identifier.trim()
|
|
23
|
+
|
|
24
|
+
// Built-in templates
|
|
25
|
+
if (BUILTIN_TEMPLATES.includes(identifier)) {
|
|
26
|
+
return {
|
|
27
|
+
type: 'builtin',
|
|
28
|
+
name: identifier,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Official templates from @uniweb/templates
|
|
33
|
+
if (OFFICIAL_TEMPLATES.includes(identifier)) {
|
|
34
|
+
return {
|
|
35
|
+
type: 'official',
|
|
36
|
+
name: identifier,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// GitHub shorthand: github:user/repo or github:user/repo#ref
|
|
41
|
+
if (identifier.startsWith('github:')) {
|
|
42
|
+
const rest = identifier.slice(7) // Remove 'github:'
|
|
43
|
+
return parseGitHubIdentifier(rest)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// GitHub URL: https://github.com/user/repo
|
|
47
|
+
if (identifier.startsWith('https://github.com/') || identifier.startsWith('http://github.com/')) {
|
|
48
|
+
const url = new URL(identifier)
|
|
49
|
+
const pathParts = url.pathname.split('/').filter(Boolean)
|
|
50
|
+
if (pathParts.length >= 2) {
|
|
51
|
+
const [owner, repo] = pathParts
|
|
52
|
+
// Check for tree/branch in URL
|
|
53
|
+
const treeIndex = pathParts.indexOf('tree')
|
|
54
|
+
const ref = treeIndex >= 0 && pathParts[treeIndex + 1] ? pathParts[treeIndex + 1] : undefined
|
|
55
|
+
return {
|
|
56
|
+
type: 'github',
|
|
57
|
+
owner,
|
|
58
|
+
repo: repo.replace(/\.git$/, ''),
|
|
59
|
+
ref,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`Invalid GitHub URL: ${identifier}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Scoped npm package: @scope/package-name
|
|
66
|
+
if (identifier.startsWith('@')) {
|
|
67
|
+
return {
|
|
68
|
+
type: 'npm',
|
|
69
|
+
package: identifier,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Unscoped name - assume it's an npm package with @uniweb/template- prefix
|
|
74
|
+
// This allows users to type `uniweb create foo --template blog` for @uniweb/template-blog
|
|
75
|
+
return {
|
|
76
|
+
type: 'npm',
|
|
77
|
+
package: `@uniweb/template-${identifier}`,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Parse GitHub identifier: user/repo or user/repo#ref
|
|
83
|
+
*/
|
|
84
|
+
function parseGitHubIdentifier(identifier) {
|
|
85
|
+
const [repoPath, ref] = identifier.split('#')
|
|
86
|
+
const [owner, repo] = repoPath.split('/')
|
|
87
|
+
|
|
88
|
+
if (!owner || !repo) {
|
|
89
|
+
throw new Error(`Invalid GitHub identifier: ${identifier}. Expected format: user/repo or user/repo#ref`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
type: 'github',
|
|
94
|
+
owner,
|
|
95
|
+
repo: repo.replace(/\.git$/, ''),
|
|
96
|
+
ref: ref || undefined,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get a display name for a template identifier
|
|
102
|
+
*/
|
|
103
|
+
export function getTemplateDisplayName(parsed) {
|
|
104
|
+
switch (parsed.type) {
|
|
105
|
+
case 'builtin':
|
|
106
|
+
return `Built-in: ${parsed.name}`
|
|
107
|
+
case 'official':
|
|
108
|
+
return `Official: ${parsed.name}`
|
|
109
|
+
case 'npm':
|
|
110
|
+
return parsed.package
|
|
111
|
+
case 'github':
|
|
112
|
+
return `${parsed.owner}/${parsed.repo}${parsed.ref ? `#${parsed.ref}` : ''}`
|
|
113
|
+
default:
|
|
114
|
+
return 'Unknown'
|
|
115
|
+
}
|
|
116
|
+
}
|