uniweb 1.0.0 → 1.0.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/LICENSE +201 -0
- package/README.md +532 -3
- package/package.json +60 -11
- package/partials/adding-pages.hbs +77 -0
- package/partials/agents.md +3000 -0
- package/partials/ai-assistance.hbs +28 -0
- package/partials/components-docs.hbs +13 -0
- package/partials/config-reference.hbs +66 -0
- package/partials/deployment.hbs +13 -0
- package/partials/learn-more.hbs +6 -0
- package/partials/quick-start.hbs +8 -0
- package/partials/search-docs.hbs +110 -0
- package/src/backend/asset-download.js +222 -0
- package/src/backend/client.js +986 -0
- package/src/backend/foundation-bring-along.js +692 -0
- package/src/backend/payment-handoff.js +362 -0
- package/src/backend/service-request.js +210 -0
- package/src/backend/site-media.js +255 -0
- package/src/backend/site-preview.js +77 -0
- package/src/backend/site-sync.js +2515 -0
- package/src/backend/workspace.js +234 -0
- package/src/commands/add.js +1900 -0
- package/src/commands/build.js +1214 -0
- package/src/commands/clone.js +466 -0
- package/src/commands/content.js +202 -0
- package/src/commands/deploy.js +537 -0
- package/src/commands/dev.js +202 -0
- package/src/commands/docs.js +530 -0
- package/src/commands/doctor.js +1982 -0
- package/src/commands/export.js +116 -0
- package/src/commands/families.js +82 -0
- package/src/commands/forget.js +197 -0
- package/src/commands/handoff.js +21 -0
- package/src/commands/i18n.js +1707 -0
- package/src/commands/inspect.js +375 -0
- package/src/commands/invite.js +24 -0
- package/src/commands/org.js +96 -0
- package/src/commands/publish.js +1087 -0
- package/src/commands/pull.js +1257 -0
- package/src/commands/push.js +663 -0
- package/src/commands/refresh.js +212 -0
- package/src/commands/register.js +1092 -0
- package/src/commands/rename.js +647 -0
- package/src/commands/site.js +358 -0
- package/src/commands/snapshot.js +461 -0
- package/src/commands/starter.js +206 -0
- package/src/commands/status.js +302 -0
- package/src/commands/sync.js +93 -0
- package/src/commands/template.js +107 -0
- package/src/commands/update.js +887 -0
- package/src/commands/validate.js +376 -0
- package/src/framework-index.json +248 -0
- package/src/index.js +2241 -0
- package/src/templates/fetchers/github.js +113 -0
- package/src/templates/fetchers/npm.js +85 -0
- package/src/templates/fetchers/release.js +92 -0
- package/src/templates/index.js +190 -0
- package/src/templates/processor.js +333 -0
- package/src/templates/resolver.js +232 -0
- package/src/templates/validator.js +155 -0
- package/src/utils/agents-stamp.js +110 -0
- package/src/utils/args.js +126 -0
- package/src/utils/asset-upload.js +270 -0
- package/src/utils/bytes.js +30 -0
- package/src/utils/code-upload.js +394 -0
- package/src/utils/config.js +417 -0
- package/src/utils/conformance.js +274 -0
- package/src/utils/dep-survey.js +176 -0
- package/src/utils/destination-prompt.js +146 -0
- package/src/utils/discover.js +75 -0
- package/src/utils/env.js +22 -0
- package/src/utils/exit.js +30 -0
- package/src/utils/fetch-retry.js +109 -0
- package/src/utils/flag-guard.js +246 -0
- package/src/utils/foundation-name.js +179 -0
- package/src/utils/git.js +342 -0
- package/src/utils/host-prompt.js +51 -0
- package/src/utils/install-integrity.js +210 -0
- package/src/utils/interactive.js +118 -0
- package/src/utils/json-file.js +72 -0
- package/src/utils/names.js +91 -0
- package/src/utils/placement.js +151 -0
- package/src/utils/pm.js +165 -0
- package/src/utils/preview.js +20 -0
- package/src/utils/pull-written.js +132 -0
- package/src/utils/records-guard.js +88 -0
- package/src/utils/registry-auth.js +994 -0
- package/src/utils/registry-orgs.js +290 -0
- package/src/utils/scaffold.js +483 -0
- package/src/utils/schemaless-report.js +30 -0
- package/src/utils/semver-precedence.js +80 -0
- package/src/utils/session-file.js +95 -0
- package/src/utils/site-content-refs.js +23 -0
- package/src/utils/site-copies.js +123 -0
- package/src/utils/site-data-upload.js +302 -0
- package/src/utils/site-identity.js +137 -0
- package/src/utils/snapshot-settings.js +112 -0
- package/src/utils/update-check.js +302 -0
- package/src/utils/uwx-read.js +157 -0
- package/src/utils/workspace.js +223 -0
- package/src/versions.js +467 -0
- package/starter/foundation/main.js +50 -0
- package/starter/foundation/sections/Section/index.jsx +95 -0
- package/starter/foundation/sections/Section/meta.js +44 -0
- package/starter/site/pages/about/1-about.md.hbs +13 -0
- package/starter/site/pages/about/page.yml +3 -0
- package/starter/site/pages/home/1-welcome.md.hbs +14 -0
- package/starter/site/pages/home/page.yml +3 -0
- package/templates/foundation/_gitignore +5 -0
- package/templates/foundation/components/.gitkeep +0 -0
- package/templates/foundation/main.js.hbs +33 -0
- package/templates/foundation/package.json.hbs +51 -0
- package/templates/foundation/sections/.gitkeep +0 -0
- package/templates/foundation/styles.css +6 -0
- package/templates/foundation/vite.config.js +3 -0
- package/templates/site/_gitignore +12 -0
- package/templates/site/entry.js +7 -0
- package/templates/site/index.html.hbs +13 -0
- package/templates/site/package.json.hbs +27 -0
- package/templates/site/public/favicon.svg +7 -0
- package/templates/site/site.yml.hbs +91 -0
- package/templates/site/theme.yml +99 -0
- package/templates/site/vite.config.js +7 -0
- package/templates/workspace/AGENTS.md.hbs +2 -0
- package/templates/workspace/README.md.hbs +87 -0
- package/templates/workspace/_gitignore +29 -0
- package/templates/workspace/_vscode/settings.json +6 -0
- package/templates/workspace/package.json.hbs +20 -0
- package/templates/workspace/pnpm-workspace.yaml.hbs +26 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub fetcher — downloads a template from a GitHub repository
|
|
3
|
+
* (`github:owner/repo[#ref]`).
|
|
4
|
+
*
|
|
5
|
+
* ⭐ Without a token it downloads from GitHub's archive host —
|
|
6
|
+
* `codeload.github.com/<owner>/<repo>/tar.gz/<ref>`, `HEAD` naming the default
|
|
7
|
+
* branch — which is not the REST API, so the API's anonymous limit (60 requests an
|
|
8
|
+
* hour per address) does not apply. With GITHUB_TOKEN set it uses the API's
|
|
9
|
+
* tarball endpoint, which a private repository needs and which then counts
|
|
10
|
+
* against the token's own, higher limit. ⛔ Until 2026-10-08 every download went
|
|
11
|
+
* through the API, token or not.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { pipeline } from 'node:stream/promises'
|
|
15
|
+
import { createGunzip } from 'node:zlib'
|
|
16
|
+
import { mkdtemp, rm } from 'node:fs/promises'
|
|
17
|
+
import { tmpdir } from 'node:os'
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
import * as tar from 'tar'
|
|
20
|
+
import { fetchWithRetry as sharedFetchWithRetry } from '../../utils/fetch-retry.js'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Where to download a repository's tarball from, and with which headers.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} owner
|
|
26
|
+
* @param {string} repo
|
|
27
|
+
* @param {string} [ref='HEAD'] - Branch, tag, or commit
|
|
28
|
+
* @param {string|null} [token] - GITHUB_TOKEN, when set
|
|
29
|
+
* @returns {{ url: string, headers: Record<string, string> }}
|
|
30
|
+
*/
|
|
31
|
+
export function githubTarballRequest(owner, repo, ref = 'HEAD', token = null) {
|
|
32
|
+
if (token) {
|
|
33
|
+
return {
|
|
34
|
+
url: `https://api.github.com/repos/${owner}/${repo}/tarball/${ref}`,
|
|
35
|
+
headers: {
|
|
36
|
+
Accept: 'application/vnd.github+json',
|
|
37
|
+
'User-Agent': 'uniweb-cli',
|
|
38
|
+
Authorization: `Bearer ${token}`
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
url: `https://codeload.github.com/${owner}/${repo}/tar.gz/${ref}`,
|
|
44
|
+
headers: { 'User-Agent': 'uniweb-cli' }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Fetch a template from a GitHub repository
|
|
50
|
+
*
|
|
51
|
+
* @param {string} owner - Repository owner
|
|
52
|
+
* @param {string} repo - Repository name
|
|
53
|
+
* @param {Object} options - Fetch options
|
|
54
|
+
* @param {string} options.ref - Branch, tag, or commit (default: HEAD)
|
|
55
|
+
* @param {Function} options.onProgress - Progress callback
|
|
56
|
+
* @returns {Promise<Object>} { tempDir, ref }
|
|
57
|
+
*/
|
|
58
|
+
export async function fetchGitHubTemplate(owner, repo, options = {}) {
|
|
59
|
+
const { ref = 'HEAD', onProgress } = options
|
|
60
|
+
|
|
61
|
+
const displayRef = ref === 'HEAD' ? 'latest' : ref
|
|
62
|
+
onProgress?.(`Fetching ${owner}/${repo}@${displayRef} from GitHub...`)
|
|
63
|
+
|
|
64
|
+
// GitHub provides tarballs without requiring git
|
|
65
|
+
const token = process.env.GITHUB_TOKEN || null
|
|
66
|
+
const { url, headers } = githubTarballRequest(owner, repo, ref, token)
|
|
67
|
+
|
|
68
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'uniweb-template-'))
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetchWithRetry(url, { headers })
|
|
72
|
+
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
if (response.status === 404) {
|
|
75
|
+
const at = ref === 'HEAD' ? '' : ` at ${ref}`
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Repository not found: ${owner}/${repo}${at}` +
|
|
78
|
+
(token ? '' : ' — a private repository needs GITHUB_TOKEN')
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
if (response.status === 403 && response.headers.get('x-ratelimit-remaining') === '0') {
|
|
82
|
+
throw new Error('GitHub API rate limit exceeded for this GITHUB_TOKEN.')
|
|
83
|
+
}
|
|
84
|
+
throw new Error(`GitHub download failed: HTTP ${response.status}`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
onProgress?.(`Downloading and extracting...`)
|
|
88
|
+
|
|
89
|
+
await pipeline(
|
|
90
|
+
response.body,
|
|
91
|
+
createGunzip(),
|
|
92
|
+
tar.extract({ cwd: tempDir, strip: 1 }) // the archive's one top folder: 'repo-ref/' (codeload) or 'owner-repo-sha/' (API)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
onProgress?.(`Extracted to ${tempDir}`)
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
tempDir,
|
|
99
|
+
ref: displayRef
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
// Clean up on error
|
|
103
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {})
|
|
104
|
+
throw err
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Retry + timeout live in one place now (`../../utils/fetch-retry.js`).
|
|
109
|
+
// This file carried its own copy, as did the other two fetchers — three
|
|
110
|
+
// byte-identical implementations, and none reachable from the upload paths
|
|
111
|
+
// that had no retry at all. The wrapper keeps this caller's own timeout.
|
|
112
|
+
const fetchWithRetry = (url, options = {}, maxRetries = 3) =>
|
|
113
|
+
sharedFetchWithRetry(url, { ...options }, { retries: maxRetries, timeoutMs: 60000 })
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
import { fetchWithRetry as sharedFetchWithRetry } from '../../utils/fetch-retry.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Fetch a template from npm registry
|
|
15
|
+
*
|
|
16
|
+
* @param {string} packageName - npm package name (e.g., '@uniweb/template-marketing')
|
|
17
|
+
* @param {Object} options - Fetch options
|
|
18
|
+
* @param {Function} options.onProgress - Progress callback
|
|
19
|
+
* @returns {Promise<Object>} { tempDir, version, metadata }
|
|
20
|
+
*/
|
|
21
|
+
export async function fetchNpmTemplate(packageName, options = {}) {
|
|
22
|
+
const { onProgress } = options
|
|
23
|
+
|
|
24
|
+
onProgress?.(`Fetching package info for ${packageName}...`)
|
|
25
|
+
|
|
26
|
+
// Query npm registry for package metadata
|
|
27
|
+
const registryUrl = `https://registry.npmjs.org/${encodeURIComponent(packageName).replace('%40', '@')}`
|
|
28
|
+
|
|
29
|
+
const metaResponse = await fetchWithRetry(registryUrl)
|
|
30
|
+
if (!metaResponse.ok) {
|
|
31
|
+
if (metaResponse.status === 404) {
|
|
32
|
+
throw new Error(`Package not found: ${packageName}`)
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`npm registry error: ${metaResponse.status}`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const pkgMeta = await metaResponse.json()
|
|
38
|
+
const version = pkgMeta['dist-tags']?.latest
|
|
39
|
+
if (!version) {
|
|
40
|
+
throw new Error(`No published versions found for ${packageName}`)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const versionMeta = pkgMeta.versions[version]
|
|
44
|
+
const tarballUrl = versionMeta?.dist?.tarball
|
|
45
|
+
if (!tarballUrl) {
|
|
46
|
+
throw new Error(`No tarball URL found for ${packageName}@${version}`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
onProgress?.(`Downloading ${packageName}@${version}...`)
|
|
50
|
+
|
|
51
|
+
// Download and extract tarball
|
|
52
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'uniweb-template-'))
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const tarballResponse = await fetchWithRetry(tarballUrl)
|
|
56
|
+
if (!tarballResponse.ok) {
|
|
57
|
+
throw new Error(`Failed to download tarball: ${tarballResponse.status}`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await pipeline(
|
|
61
|
+
tarballResponse.body,
|
|
62
|
+
createGunzip(),
|
|
63
|
+
tar.extract({ cwd: tempDir, strip: 1 }) // strip 'package/' prefix
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
onProgress?.(`Extracted to ${tempDir}`)
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
tempDir,
|
|
70
|
+
version,
|
|
71
|
+
metadata: versionMeta
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// Clean up on error
|
|
75
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {})
|
|
76
|
+
throw err
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Retry + timeout live in one place now (`../../utils/fetch-retry.js`).
|
|
81
|
+
// This file carried its own copy, as did the other two fetchers — three
|
|
82
|
+
// byte-identical implementations, and none reachable from the upload paths
|
|
83
|
+
// that had no retry at all. The wrapper keeps this caller's own timeout.
|
|
84
|
+
const fetchWithRetry = (url, options = {}, maxRetries = 3) =>
|
|
85
|
+
sharedFetchWithRetry(url, { ...options }, { retries: maxRetries, timeoutMs: 30000 })
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release fetcher — downloads an official template from the release of the
|
|
3
|
+
* templates repo this CLI was published with.
|
|
4
|
+
*
|
|
5
|
+
* ⭐ ONE REQUEST, AND NOT TO GITHUB'S API. The release is pinned
|
|
6
|
+
* (templates/resolver.js, OFFICIAL_TEMPLATES_RELEASE) and a release's files sit
|
|
7
|
+
* at fixed URLs, so the template's tarball is downloaded from its release URL
|
|
8
|
+
* directly. The name was checked against this CLI's own roster before anything
|
|
9
|
+
* is fetched, and a tarball the release lacks answers 404.
|
|
10
|
+
*
|
|
11
|
+
* ⛔ Until 2026-10-08 a `create` first asked the REST API for the release, to find
|
|
12
|
+
* its manifest.json, then downloaded the manifest to check the name, then the
|
|
13
|
+
* tarball: three requests, the first counted against the API's anonymous limit of
|
|
14
|
+
* 60 an hour per address — which a shared network or a CI runner can spend, and
|
|
15
|
+
* `create` then failed with "rate limit exceeded".
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { pipeline } from 'node:stream/promises'
|
|
19
|
+
import { createGunzip } from 'node:zlib'
|
|
20
|
+
import { mkdtemp, rm } from 'node:fs/promises'
|
|
21
|
+
import { tmpdir } from 'node:os'
|
|
22
|
+
import { join } from 'node:path'
|
|
23
|
+
import * as tar from 'tar'
|
|
24
|
+
import { fetchWithRetry as sharedFetchWithRetry } from '../../utils/fetch-retry.js'
|
|
25
|
+
|
|
26
|
+
// GitHub repository for official templates
|
|
27
|
+
const TEMPLATES_REPO = 'uniweb/templates'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The URL a release serves one template's tarball at.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} name - Template name (e.g., 'marketing')
|
|
33
|
+
* @param {string} version - The release tag, e.g. `v0.18.6`
|
|
34
|
+
*/
|
|
35
|
+
export function officialTemplateUrl(name, version) {
|
|
36
|
+
return `https://github.com/${TEMPLATES_REPO}/releases/download/${version}/${name}.tar.gz`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Fetch a template from one release of the official templates
|
|
41
|
+
*
|
|
42
|
+
* @param {string} name - Template name (e.g., 'marketing')
|
|
43
|
+
* @param {Object} options - Fetch options
|
|
44
|
+
* @param {string} options.version - The release tag, e.g. `v0.18.6`
|
|
45
|
+
* @param {Function} options.onProgress - Progress callback
|
|
46
|
+
* @returns {Promise<Object>} { tempDir, baseTempDir, version }
|
|
47
|
+
*/
|
|
48
|
+
export async function fetchOfficialTemplate(name, options = {}) {
|
|
49
|
+
const { version, onProgress } = options
|
|
50
|
+
if (!version) throw new Error('fetchOfficialTemplate needs the release tag to download from')
|
|
51
|
+
|
|
52
|
+
onProgress?.(`Downloading ${name} template (${version})...`)
|
|
53
|
+
|
|
54
|
+
const response = await fetchWithRetry(officialTemplateUrl(name, version), {
|
|
55
|
+
headers: { 'User-Agent': 'uniweb-cli' }
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
if (response.status === 404) {
|
|
60
|
+
throw new Error(`Release ${version} of ${TEMPLATES_REPO} has no template "${name}".`)
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`Could not download the ${name} template (HTTP ${response.status}).`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Extract to temp directory
|
|
66
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'uniweb-template-'))
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
onProgress?.('Extracting template...')
|
|
70
|
+
|
|
71
|
+
await pipeline(response.body, createGunzip(), tar.extract({ cwd: tempDir, strip: 0 }))
|
|
72
|
+
|
|
73
|
+
// The tarball holds the template in a folder named after it:
|
|
74
|
+
// marketing.tar.gz extracts to marketing/template.json
|
|
75
|
+
return {
|
|
76
|
+
tempDir: join(tempDir, name),
|
|
77
|
+
baseTempDir: tempDir, // For cleanup
|
|
78
|
+
version
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
// Clean up on error
|
|
82
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {})
|
|
83
|
+
throw err
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Retry + timeout live in one place now (`../../utils/fetch-retry.js`).
|
|
88
|
+
// This file carried its own copy, as did the other two fetchers — three
|
|
89
|
+
// byte-identical implementations, and none reachable from the upload paths
|
|
90
|
+
// that had no retry at all. The wrapper keeps this caller's own timeout.
|
|
91
|
+
const fetchWithRetry = (url, options = {}, maxRetries = 3) =>
|
|
92
|
+
sharedFetchWithRetry(url, { redirect: 'follow', ...options }, { retries: maxRetries, timeoutMs: 60000 })
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template resolution and application for the CLI
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync } from 'node:fs'
|
|
6
|
+
import { readFile, rm } from 'node:fs/promises'
|
|
7
|
+
import { join, resolve } from 'node:path'
|
|
8
|
+
import { homedir } from 'node:os'
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
parseTemplateId,
|
|
12
|
+
getTemplateDisplayName,
|
|
13
|
+
BUILTIN_TEMPLATES,
|
|
14
|
+
OFFICIAL_TEMPLATES,
|
|
15
|
+
OFFICIAL_TEMPLATES_RELEASE,
|
|
16
|
+
buildTemplateChoices,
|
|
17
|
+
templatePickerPageSize
|
|
18
|
+
} from './resolver.js'
|
|
19
|
+
import { fetchNpmTemplate } from './fetchers/npm.js'
|
|
20
|
+
import { fetchGitHubTemplate } from './fetchers/github.js'
|
|
21
|
+
import { fetchOfficialTemplate } from './fetchers/release.js'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a template identifier and return the template path
|
|
25
|
+
*
|
|
26
|
+
* @param {string} identifier - Template identifier
|
|
27
|
+
* @param {Object} options - Resolution options
|
|
28
|
+
* @param {Function} options.onProgress - Progress callback
|
|
29
|
+
* @returns {Promise<Object>} { type, path, metadata, cleanup }
|
|
30
|
+
*/
|
|
31
|
+
export async function resolveTemplate(identifier, options = {}) {
|
|
32
|
+
const { onProgress } = options
|
|
33
|
+
const parsed = parseTemplateId(identifier)
|
|
34
|
+
|
|
35
|
+
switch (parsed.type) {
|
|
36
|
+
case 'builtin':
|
|
37
|
+
return {
|
|
38
|
+
type: 'builtin',
|
|
39
|
+
name: parsed.name
|
|
40
|
+
// Built-in templates are handled programmatically by the CLI
|
|
41
|
+
// No path or cleanup needed
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
case 'official':
|
|
45
|
+
return resolveOfficialTemplate(parsed.name, options)
|
|
46
|
+
|
|
47
|
+
case 'npm':
|
|
48
|
+
return resolveNpmTemplate(parsed.package, options)
|
|
49
|
+
|
|
50
|
+
case 'github':
|
|
51
|
+
return resolveGitHubTemplate(parsed, options)
|
|
52
|
+
|
|
53
|
+
case 'local':
|
|
54
|
+
return resolveLocalTemplate(parsed.path, options)
|
|
55
|
+
|
|
56
|
+
default:
|
|
57
|
+
throw new Error(`Unknown template type: ${parsed.type}`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolve an official template from the release this CLI was published with —
|
|
63
|
+
* never `latest`, so its content matches the package versions the scaffold pins
|
|
64
|
+
* (resolver.js, OFFICIAL_TEMPLATES_RELEASE).
|
|
65
|
+
*/
|
|
66
|
+
async function resolveOfficialTemplate(name, options = {}) {
|
|
67
|
+
const { onProgress } = options
|
|
68
|
+
|
|
69
|
+
if (!OFFICIAL_TEMPLATES_RELEASE) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
"This CLI's framework index names no @uniweb/templates release to download from."
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
const { tempDir, baseTempDir, version } = await fetchOfficialTemplate(name, {
|
|
75
|
+
version: OFFICIAL_TEMPLATES_RELEASE,
|
|
76
|
+
onProgress
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
onProgress?.(`Using official template: ${name} (${version})`)
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
type: 'official',
|
|
83
|
+
name,
|
|
84
|
+
version,
|
|
85
|
+
path: tempDir,
|
|
86
|
+
cleanup: async () => {
|
|
87
|
+
// Clean up the base temp directory (parent of the template)
|
|
88
|
+
await rm(baseTempDir, { recursive: true, force: true }).catch(() => {})
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolve a template from npm
|
|
95
|
+
*/
|
|
96
|
+
async function resolveNpmTemplate(packageName, options = {}) {
|
|
97
|
+
const { onProgress } = options
|
|
98
|
+
|
|
99
|
+
onProgress?.(`Resolving npm template: ${packageName}`)
|
|
100
|
+
|
|
101
|
+
const { tempDir, version, metadata } = await fetchNpmTemplate(packageName, {
|
|
102
|
+
onProgress
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
type: 'npm',
|
|
107
|
+
package: packageName,
|
|
108
|
+
version,
|
|
109
|
+
path: tempDir,
|
|
110
|
+
cleanup: async () => {
|
|
111
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {})
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolve a template from GitHub
|
|
118
|
+
*/
|
|
119
|
+
async function resolveGitHubTemplate(parsed, options = {}) {
|
|
120
|
+
const { onProgress } = options
|
|
121
|
+
const { owner, repo, ref } = parsed
|
|
122
|
+
|
|
123
|
+
onProgress?.(`Resolving GitHub template: ${owner}/${repo}`)
|
|
124
|
+
|
|
125
|
+
const { tempDir } = await fetchGitHubTemplate(owner, repo, {
|
|
126
|
+
ref,
|
|
127
|
+
onProgress
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
type: 'github',
|
|
132
|
+
owner,
|
|
133
|
+
repo,
|
|
134
|
+
ref,
|
|
135
|
+
path: tempDir,
|
|
136
|
+
cleanup: async () => {
|
|
137
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => {})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Resolve a local template from a filesystem path
|
|
144
|
+
*/
|
|
145
|
+
async function resolveLocalTemplate(templatePath, options = {}) {
|
|
146
|
+
const { onProgress } = options
|
|
147
|
+
|
|
148
|
+
// Expand ~ to home directory
|
|
149
|
+
let resolvedPath = templatePath.startsWith('~')
|
|
150
|
+
? templatePath.replace(/^~/, homedir())
|
|
151
|
+
: templatePath
|
|
152
|
+
|
|
153
|
+
// Resolve to absolute path
|
|
154
|
+
resolvedPath = resolve(resolvedPath)
|
|
155
|
+
|
|
156
|
+
if (!existsSync(resolvedPath)) {
|
|
157
|
+
throw new Error(`Local template not found: ${resolvedPath}`)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
onProgress?.(`Using local template: ${resolvedPath}`)
|
|
161
|
+
|
|
162
|
+
// Read template.json for name
|
|
163
|
+
const metaPath = join(resolvedPath, 'template.json')
|
|
164
|
+
let name = templatePath
|
|
165
|
+
if (existsSync(metaPath)) {
|
|
166
|
+
try {
|
|
167
|
+
const meta = JSON.parse(await readFile(metaPath, 'utf-8'))
|
|
168
|
+
name = meta.name || templatePath
|
|
169
|
+
} catch {
|
|
170
|
+
// Use path as name if template.json can't be parsed
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
type: 'local',
|
|
176
|
+
name,
|
|
177
|
+
path: resolvedPath,
|
|
178
|
+
cleanup: null
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Re-export for convenience
|
|
183
|
+
export {
|
|
184
|
+
parseTemplateId,
|
|
185
|
+
getTemplateDisplayName,
|
|
186
|
+
BUILTIN_TEMPLATES,
|
|
187
|
+
OFFICIAL_TEMPLATES,
|
|
188
|
+
buildTemplateChoices,
|
|
189
|
+
templatePickerPageSize
|
|
190
|
+
}
|