@dhis2/create-app 5.3.1 → 5.4.0
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 +1 -1
- package/src/index.js +52 -10
- package/src/utils/isGitTemplateSpecifier.js +162 -0
- package/src/utils/resolveExternalTemplateSource.js +66 -0
- package/src/utils/validateTemplateDirectory.js +26 -0
- package/tests/is-git-template-specifier.js +89 -0
- package/tests/resolve-external-template-source.js +69 -0
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -5,6 +5,7 @@ const { input, select } = require('@inquirer/prompts')
|
|
|
5
5
|
const fg = require('fast-glob')
|
|
6
6
|
const fs = require('fs-extra')
|
|
7
7
|
const { default: getPackageManager } = require('./utils/getPackageManager')
|
|
8
|
+
const resolveExternalTemplateSource = require('./utils/resolveExternalTemplateSource')
|
|
8
9
|
|
|
9
10
|
process.on('uncaughtException', (error) => {
|
|
10
11
|
if (error instanceof Error && error.name === 'ExitPromptError') {
|
|
@@ -45,7 +46,8 @@ const commandHandler = {
|
|
|
45
46
|
alias: ['ts', 'typeScript'],
|
|
46
47
|
},
|
|
47
48
|
template: {
|
|
48
|
-
description:
|
|
49
|
+
description:
|
|
50
|
+
'Which template to use (Basic, With React Router, or GitHub template specifier)',
|
|
49
51
|
type: 'string',
|
|
50
52
|
},
|
|
51
53
|
packageManager: {
|
|
@@ -56,10 +58,16 @@ const commandHandler = {
|
|
|
56
58
|
},
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
const getBuiltInTemplateDirectory = (templateName) => {
|
|
62
|
+
if (templateName === 'basic') {
|
|
63
|
+
return templates.templateWithList
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (templateName === 'react-router') {
|
|
67
|
+
return templates.templateWithReactRouter
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return null
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
const command = {
|
|
@@ -86,7 +94,7 @@ const command = {
|
|
|
86
94
|
typeScript: argv.typescript ?? true,
|
|
87
95
|
packageManager:
|
|
88
96
|
argv.packageManager ?? getPackageManager() ?? 'pnpm',
|
|
89
|
-
|
|
97
|
+
templateSource: argv.template ?? 'basic',
|
|
90
98
|
}
|
|
91
99
|
|
|
92
100
|
if (!useDefauls) {
|
|
@@ -106,17 +114,29 @@ const command = {
|
|
|
106
114
|
if (argv.template === undefined) {
|
|
107
115
|
const template = await select({
|
|
108
116
|
message: 'Select a template',
|
|
109
|
-
default: '
|
|
117
|
+
default: 'basic',
|
|
110
118
|
choices: [
|
|
111
119
|
{ name: 'Basic Template', value: 'basic' },
|
|
112
120
|
{
|
|
113
121
|
name: 'Template with React Router',
|
|
114
122
|
value: 'react-router',
|
|
115
123
|
},
|
|
124
|
+
{
|
|
125
|
+
name: 'Custom template from Git',
|
|
126
|
+
value: 'custom-git',
|
|
127
|
+
},
|
|
116
128
|
],
|
|
117
129
|
})
|
|
118
130
|
|
|
119
|
-
|
|
131
|
+
if (template === 'custom-git') {
|
|
132
|
+
selectedOptions.templateSource = await input({
|
|
133
|
+
message:
|
|
134
|
+
'Enter GitHub template specifier (e.g. owner/repo#main)',
|
|
135
|
+
required: true,
|
|
136
|
+
})
|
|
137
|
+
} else {
|
|
138
|
+
selectedOptions.templateSource = template
|
|
139
|
+
}
|
|
120
140
|
}
|
|
121
141
|
}
|
|
122
142
|
|
|
@@ -158,8 +178,30 @@ const command = {
|
|
|
158
178
|
}
|
|
159
179
|
|
|
160
180
|
reporter.info('Copying template files')
|
|
161
|
-
|
|
162
|
-
|
|
181
|
+
let resolvedExternalTemplate
|
|
182
|
+
try {
|
|
183
|
+
const builtInTemplatePath = getBuiltInTemplateDirectory(
|
|
184
|
+
selectedOptions.templateSource
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if (builtInTemplatePath) {
|
|
188
|
+
fs.copySync(builtInTemplatePath, cwd)
|
|
189
|
+
} else {
|
|
190
|
+
resolvedExternalTemplate = await resolveExternalTemplateSource(
|
|
191
|
+
selectedOptions.templateSource
|
|
192
|
+
)
|
|
193
|
+
fs.copySync(resolvedExternalTemplate.templatePath, cwd)
|
|
194
|
+
}
|
|
195
|
+
} catch (error) {
|
|
196
|
+
reporter.error(
|
|
197
|
+
error instanceof Error ? error.message : String(error)
|
|
198
|
+
)
|
|
199
|
+
process.exit(1)
|
|
200
|
+
} finally {
|
|
201
|
+
if (resolvedExternalTemplate) {
|
|
202
|
+
await resolvedExternalTemplate.cleanup()
|
|
203
|
+
}
|
|
204
|
+
}
|
|
163
205
|
|
|
164
206
|
const paths = {
|
|
165
207
|
base: cwd,
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const githubHosts = new Set(['github.com', 'www.github.com'])
|
|
2
|
+
const ownerPattern = /^[a-zA-Z0-9_.-]+$/
|
|
3
|
+
|
|
4
|
+
const parseRef = (rawTemplateSource, refPart) => {
|
|
5
|
+
if (refPart === undefined) {
|
|
6
|
+
return null
|
|
7
|
+
}
|
|
8
|
+
if (!refPart) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
`Invalid template source "${rawTemplateSource}". Ref cannot be empty after "#".`
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
if (refPart.includes(':')) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`Invalid template source "${rawTemplateSource}". Use "owner/repo" or "owner/repo#ref".`
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return refPart
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const parseGithubUrlSource = (sourceWithoutRef) => {
|
|
23
|
+
const parsedUrl = new URL(sourceWithoutRef)
|
|
24
|
+
if (!githubHosts.has(parsedUrl.host)) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Unsupported template host "${parsedUrl.host}". Only github.com repositories are supported.`
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const pathParts = parsedUrl.pathname.split('/').filter(Boolean).slice(0, 2)
|
|
31
|
+
if (pathParts.length < 2) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Invalid GitHub repository path in "${sourceWithoutRef}". Use "owner/repo".`
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
owner: pathParts[0],
|
|
39
|
+
repo: pathParts[1],
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const parseGithubShorthandSource = (rawTemplateSource, sourceWithoutRef) => {
|
|
44
|
+
const separatorIndex = sourceWithoutRef.indexOf('/')
|
|
45
|
+
const hasSingleSeparator =
|
|
46
|
+
separatorIndex > 0 &&
|
|
47
|
+
separatorIndex === sourceWithoutRef.lastIndexOf('/')
|
|
48
|
+
if (!hasSingleSeparator) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Invalid template source "${rawTemplateSource}". Use "owner/repo" or "owner/repo#ref".`
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const owner = sourceWithoutRef.slice(0, separatorIndex)
|
|
55
|
+
const repo = sourceWithoutRef.slice(separatorIndex + 1)
|
|
56
|
+
if (
|
|
57
|
+
!ownerPattern.test(owner) ||
|
|
58
|
+
!repo ||
|
|
59
|
+
/\s/.test(repo) ||
|
|
60
|
+
repo.includes('/')
|
|
61
|
+
) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Invalid template source "${rawTemplateSource}". Use "owner/repo" or "owner/repo#ref".`
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
owner,
|
|
69
|
+
repo,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const isValidRefPart = (refPart) => {
|
|
74
|
+
if (refPart === undefined) {
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return Boolean(refPart) && !refPart.includes(':')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const isValidShorthandSource = (sourceWithoutRef) => {
|
|
82
|
+
const separatorIndex = sourceWithoutRef.indexOf('/')
|
|
83
|
+
const hasSingleSeparator =
|
|
84
|
+
separatorIndex > 0 &&
|
|
85
|
+
separatorIndex === sourceWithoutRef.lastIndexOf('/')
|
|
86
|
+
if (!hasSingleSeparator) {
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const owner = sourceWithoutRef.slice(0, separatorIndex)
|
|
91
|
+
const repo = sourceWithoutRef.slice(separatorIndex + 1)
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
ownerPattern.test(owner) &&
|
|
95
|
+
Boolean(repo) &&
|
|
96
|
+
!/\s/.test(repo) &&
|
|
97
|
+
!repo.includes('/')
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const parseGitTemplateSpecifier = (templateSource) => {
|
|
102
|
+
const rawTemplateSource = String(templateSource || '').trim()
|
|
103
|
+
if (!rawTemplateSource) {
|
|
104
|
+
throw new Error('Template source cannot be empty.')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const [sourceWithoutRef, refPart, ...rest] = rawTemplateSource.split('#')
|
|
108
|
+
if (rest.length > 0) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Invalid template source "${rawTemplateSource}". Use at most one "#" to specify a ref.`
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const ref = parseRef(rawTemplateSource, refPart)
|
|
115
|
+
const sourceInfo = sourceWithoutRef.startsWith('https://')
|
|
116
|
+
? parseGithubUrlSource(sourceWithoutRef)
|
|
117
|
+
: parseGithubShorthandSource(rawTemplateSource, sourceWithoutRef)
|
|
118
|
+
|
|
119
|
+
const owner = sourceInfo.owner
|
|
120
|
+
let repo = sourceInfo.repo
|
|
121
|
+
|
|
122
|
+
if (repo.endsWith('.git')) {
|
|
123
|
+
repo = repo.slice(0, -4)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!owner || !repo) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Invalid template source "${rawTemplateSource}". Missing GitHub owner or repository name.`
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
owner,
|
|
134
|
+
repo,
|
|
135
|
+
ref,
|
|
136
|
+
repoUrl: `https://github.com/${owner}/${repo}.git`,
|
|
137
|
+
raw: rawTemplateSource,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const isGitTemplateSpecifier = (templateSource) => {
|
|
142
|
+
const rawTemplateSource = String(templateSource || '').trim()
|
|
143
|
+
if (!rawTemplateSource) {
|
|
144
|
+
return false
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (rawTemplateSource.startsWith('https://')) {
|
|
148
|
+
return true
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const [sourceWithoutRef, refPart, ...rest] = rawTemplateSource.split('#')
|
|
152
|
+
if (rest.length > 0 || !isValidRefPart(refPart)) {
|
|
153
|
+
return false
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return isValidShorthandSource(sourceWithoutRef)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = {
|
|
160
|
+
isGitTemplateSpecifier,
|
|
161
|
+
parseGitTemplateSpecifier,
|
|
162
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const os = require('node:os')
|
|
2
|
+
const path = require('node:path')
|
|
3
|
+
const { exec } = require('@dhis2/cli-helpers-engine')
|
|
4
|
+
const fs = require('fs-extra')
|
|
5
|
+
const {
|
|
6
|
+
isGitTemplateSpecifier,
|
|
7
|
+
parseGitTemplateSpecifier,
|
|
8
|
+
} = require('./isGitTemplateSpecifier')
|
|
9
|
+
const validateTemplateDirectory = require('./validateTemplateDirectory')
|
|
10
|
+
|
|
11
|
+
const resolveExternalTemplateSource = async (templateSource) => {
|
|
12
|
+
const normalizedTemplateSource = String(templateSource || '').trim()
|
|
13
|
+
|
|
14
|
+
if (!isGitTemplateSpecifier(normalizedTemplateSource)) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`Unknown template "${normalizedTemplateSource}". Use one of [basic, react-router] or a GitHub template specifier like "owner/repo#ref".`
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const parsedSpecifier = parseGitTemplateSpecifier(normalizedTemplateSource)
|
|
21
|
+
const tempBase = fs.mkdtempSync(
|
|
22
|
+
path.join(os.tmpdir(), 'd2-create-template-source-')
|
|
23
|
+
)
|
|
24
|
+
const clonedRepoPath = path.join(tempBase, 'repo')
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const gitCloneArgs = parsedSpecifier.ref
|
|
28
|
+
? [
|
|
29
|
+
'clone',
|
|
30
|
+
'--depth',
|
|
31
|
+
'1',
|
|
32
|
+
'--branch',
|
|
33
|
+
parsedSpecifier.ref,
|
|
34
|
+
parsedSpecifier.repoUrl,
|
|
35
|
+
clonedRepoPath,
|
|
36
|
+
]
|
|
37
|
+
: ['clone', '--depth', '1', parsedSpecifier.repoUrl, clonedRepoPath]
|
|
38
|
+
|
|
39
|
+
await exec({
|
|
40
|
+
cmd: 'git',
|
|
41
|
+
args: gitCloneArgs,
|
|
42
|
+
pipe: false,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
validateTemplateDirectory(clonedRepoPath, normalizedTemplateSource)
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
templatePath: clonedRepoPath,
|
|
49
|
+
cleanup: async () => {
|
|
50
|
+
fs.removeSync(tempBase)
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
fs.removeSync(tempBase)
|
|
55
|
+
if (error instanceof Error && error.message) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Failed to resolve template "${normalizedTemplateSource}": ${error.message}`
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Failed to resolve template "${normalizedTemplateSource}".`
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = resolveExternalTemplateSource
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const path = require('node:path')
|
|
2
|
+
const fs = require('fs-extra')
|
|
3
|
+
|
|
4
|
+
const validateTemplateDirectory = (templatePath, templateSource) => {
|
|
5
|
+
if (!fs.existsSync(templatePath)) {
|
|
6
|
+
throw new Error(
|
|
7
|
+
`Template path "${templatePath}" from source "${templateSource}" does not exist.`
|
|
8
|
+
)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const stats = fs.statSync(templatePath)
|
|
12
|
+
if (!stats.isDirectory()) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`Template path "${templatePath}" from source "${templateSource}" is not a directory.`
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const packageJsonPath = path.join(templatePath, 'package.json')
|
|
19
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Template source "${templateSource}" is missing "package.json" at "${templatePath}".`
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = validateTemplateDirectory
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const test = require('tape')
|
|
2
|
+
const {
|
|
3
|
+
isGitTemplateSpecifier,
|
|
4
|
+
parseGitTemplateSpecifier,
|
|
5
|
+
} = require('../src/utils/isGitTemplateSpecifier')
|
|
6
|
+
|
|
7
|
+
test('isGitTemplateSpecifier detects supported GitHub patterns', (t) => {
|
|
8
|
+
t.plan(7)
|
|
9
|
+
|
|
10
|
+
t.equal(isGitTemplateSpecifier('basic'), false, 'built-in key is not git')
|
|
11
|
+
t.equal(
|
|
12
|
+
isGitTemplateSpecifier('react-router'),
|
|
13
|
+
false,
|
|
14
|
+
'second built-in key is not git'
|
|
15
|
+
)
|
|
16
|
+
t.equal(
|
|
17
|
+
isGitTemplateSpecifier('owner/repo'),
|
|
18
|
+
true,
|
|
19
|
+
'owner/repo shorthand is git'
|
|
20
|
+
)
|
|
21
|
+
t.equal(
|
|
22
|
+
isGitTemplateSpecifier('owner/repo#main'),
|
|
23
|
+
true,
|
|
24
|
+
'owner/repo#ref shorthand is git'
|
|
25
|
+
)
|
|
26
|
+
t.equal(
|
|
27
|
+
isGitTemplateSpecifier('https://github.com/owner/repo'),
|
|
28
|
+
true,
|
|
29
|
+
'GitHub URL is git'
|
|
30
|
+
)
|
|
31
|
+
t.equal(
|
|
32
|
+
isGitTemplateSpecifier('owner/repo#main:templates/app'),
|
|
33
|
+
false,
|
|
34
|
+
'subdirectory syntax is no longer supported'
|
|
35
|
+
)
|
|
36
|
+
t.equal(isGitTemplateSpecifier(''), false, 'empty source is not git')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('parseGitTemplateSpecifier parses shorthand with ref', (t) => {
|
|
40
|
+
t.plan(5)
|
|
41
|
+
|
|
42
|
+
const parsed = parseGitTemplateSpecifier('owner/repo#main')
|
|
43
|
+
t.equal(parsed.owner, 'owner', 'owner parsed')
|
|
44
|
+
t.equal(parsed.repo, 'repo', 'repo parsed')
|
|
45
|
+
t.equal(parsed.ref, 'main', 'ref parsed')
|
|
46
|
+
t.equal(
|
|
47
|
+
parsed.repoUrl,
|
|
48
|
+
'https://github.com/owner/repo.git',
|
|
49
|
+
'repo URL normalized'
|
|
50
|
+
)
|
|
51
|
+
t.equal(parsed.raw, 'owner/repo#main', 'raw source preserved')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('parseGitTemplateSpecifier parses URL and strips .git suffix', (t) => {
|
|
55
|
+
t.plan(4)
|
|
56
|
+
|
|
57
|
+
const parsed = parseGitTemplateSpecifier(
|
|
58
|
+
'https://github.com/acme/template.git#release'
|
|
59
|
+
)
|
|
60
|
+
t.equal(parsed.owner, 'acme', 'owner parsed from URL')
|
|
61
|
+
t.equal(parsed.repo, 'template', 'repo parsed and .git removed')
|
|
62
|
+
t.equal(parsed.ref, 'release', 'ref parsed from URL')
|
|
63
|
+
t.equal(parsed.raw, 'https://github.com/acme/template.git#release', 'raw')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('parseGitTemplateSpecifier rejects unsupported or malformed inputs', (t) => {
|
|
67
|
+
t.plan(4)
|
|
68
|
+
|
|
69
|
+
t.throws(
|
|
70
|
+
() => parseGitTemplateSpecifier('owner-only'),
|
|
71
|
+
/Invalid template source/,
|
|
72
|
+
'rejects malformed shorthand'
|
|
73
|
+
)
|
|
74
|
+
t.throws(
|
|
75
|
+
() => parseGitTemplateSpecifier('https://gitlab.com/acme/repo'),
|
|
76
|
+
/Only github.com repositories are supported|Unsupported template host/,
|
|
77
|
+
'rejects non-GitHub host'
|
|
78
|
+
)
|
|
79
|
+
t.throws(
|
|
80
|
+
() => parseGitTemplateSpecifier('owner/repo#'),
|
|
81
|
+
/Ref cannot be empty/,
|
|
82
|
+
'rejects empty ref'
|
|
83
|
+
)
|
|
84
|
+
t.throws(
|
|
85
|
+
() => parseGitTemplateSpecifier('owner/repo#main:templates/app'),
|
|
86
|
+
/Invalid template source/,
|
|
87
|
+
'rejects subdirectory syntax'
|
|
88
|
+
)
|
|
89
|
+
})
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const os = require('node:os')
|
|
2
|
+
const path = require('node:path')
|
|
3
|
+
const fs = require('fs-extra')
|
|
4
|
+
const test = require('tape')
|
|
5
|
+
const resolveExternalTemplateSource = require('../src/utils/resolveExternalTemplateSource')
|
|
6
|
+
const validateTemplateDirectory = require('../src/utils/validateTemplateDirectory')
|
|
7
|
+
|
|
8
|
+
const createTempTemplate = () => {
|
|
9
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'd2-create-test-'))
|
|
10
|
+
fs.writeJsonSync(path.join(tempDir, 'package.json'), { name: 'fixture' })
|
|
11
|
+
return tempDir
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
test('validateTemplateDirectory accepts valid template directory', (t) => {
|
|
15
|
+
const tempDir = createTempTemplate()
|
|
16
|
+
t.plan(1)
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
validateTemplateDirectory(tempDir, 'test-source')
|
|
20
|
+
t.pass('valid directory passes')
|
|
21
|
+
} finally {
|
|
22
|
+
fs.removeSync(tempDir)
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('resolveExternalTemplateSource fails for unknown non-git templates', async (t) => {
|
|
27
|
+
t.plan(1)
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await resolveExternalTemplateSource('unknown-template')
|
|
31
|
+
t.fail('should fail')
|
|
32
|
+
} catch (error) {
|
|
33
|
+
t.match(
|
|
34
|
+
String(error.message || error),
|
|
35
|
+
/Unknown template/,
|
|
36
|
+
'returns unknown-template error'
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('resolveExternalTemplateSource fails fast for unsupported git hosts', async (t) => {
|
|
42
|
+
t.plan(1)
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
await resolveExternalTemplateSource('https://gitlab.com/acme/repo')
|
|
46
|
+
t.fail('should fail')
|
|
47
|
+
} catch (error) {
|
|
48
|
+
t.match(
|
|
49
|
+
String(error.message || error),
|
|
50
|
+
/Unsupported template host|Only github.com repositories are supported/,
|
|
51
|
+
'rejects unsupported host before clone'
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('resolveExternalTemplateSource rejects subdirectory syntax', async (t) => {
|
|
57
|
+
t.plan(1)
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await resolveExternalTemplateSource('owner/repo#main:templates/app')
|
|
61
|
+
t.fail('should fail')
|
|
62
|
+
} catch (error) {
|
|
63
|
+
t.match(
|
|
64
|
+
String(error.message || error),
|
|
65
|
+
/Unknown template|Invalid template source/,
|
|
66
|
+
'subdirectory syntax is rejected'
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
})
|