@tanstack/create 0.68.2 → 0.68.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/CHANGELOG.md +6 -0
- package/dist/edge-add-ons.js +106 -0
- package/dist/edge-config-file.js +15 -0
- package/dist/edge-create-app.js +438 -0
- package/dist/edge-environment.js +141 -0
- package/dist/edge-file-helpers.js +88 -0
- package/dist/edge-frameworks.js +33 -0
- package/dist/edge-package-json.js +146 -0
- package/dist/edge-path.js +62 -0
- package/dist/edge-render.js +31 -0
- package/dist/edge-template-file.js +141 -0
- package/dist/edge.js +7 -0
- package/dist/frameworks/react/add-ons/storybook/info.json +5 -10
- package/dist/generated/create-manifest.js +4683 -0
- package/dist/manifest-types.js +1 -0
- package/dist/manifest.js +1 -0
- package/dist/types/custom-add-ons/add-on.d.ts +5 -3
- package/dist/types/edge-add-ons.d.ts +5 -0
- package/dist/types/edge-config-file.d.ts +8 -0
- package/dist/types/edge-create-app.d.ts +2 -0
- package/dist/types/edge-environment.d.ts +19 -0
- package/dist/types/edge-file-helpers.d.ts +7 -0
- package/dist/types/edge-frameworks.d.ts +7 -0
- package/dist/types/edge-package-json.d.ts +3 -0
- package/dist/types/edge-path.d.ts +5 -0
- package/dist/types/edge-render.d.ts +1 -0
- package/dist/types/edge-template-file.d.ts +2 -0
- package/dist/types/edge.d.ts +9 -0
- package/dist/types/generated/create-manifest.d.ts +36 -0
- package/dist/types/manifest-types.d.ts +4 -0
- package/dist/types/manifest.d.ts +1 -0
- package/dist/types/types.d.ts +96 -56
- package/dist/types.js +5 -3
- package/package.json +25 -5
- package/scripts/generate-manifest.mjs +407 -0
- package/src/edge-add-ons.ts +138 -0
- package/src/edge-config-file.ts +35 -0
- package/src/edge-create-app.ts +594 -0
- package/src/edge-environment.ts +175 -0
- package/src/edge-file-helpers.ts +112 -0
- package/src/edge-frameworks.ts +54 -0
- package/src/edge-package-json.ts +212 -0
- package/src/edge-path.ts +77 -0
- package/src/edge-render.ts +32 -0
- package/src/edge-template-file.ts +204 -0
- package/src/edge.ts +43 -0
- package/src/frameworks/react/add-ons/storybook/info.json +5 -10
- package/src/generated/create-manifest.ts +6490 -0
- package/src/manifest-types.ts +8 -0
- package/src/manifest.ts +1 -0
- package/src/types.ts +5 -3
- package/tests/edge-import.test.ts +31 -0
- package/tests/edge-manifest.test.ts +168 -0
- package/dist/frameworks/react/add-ons/storybook/assets/_dot_storybook/main.ts +0 -17
- package/dist/frameworks/react/add-ons/storybook/assets/_dot_storybook/preview.ts +0 -15
- package/dist/frameworks/react/add-ons/storybook/assets/src/components/storybook/button.stories.ts +0 -67
- package/dist/frameworks/react/add-ons/storybook/assets/src/components/storybook/button.tsx +0 -47
- package/dist/frameworks/react/add-ons/storybook/assets/src/components/storybook/dialog.stories.tsx +0 -92
- package/dist/frameworks/react/add-ons/storybook/assets/src/components/storybook/dialog.tsx +0 -29
- package/dist/frameworks/react/add-ons/storybook/assets/src/components/storybook/index.ts +0 -14
- package/dist/frameworks/react/add-ons/storybook/assets/src/components/storybook/input.stories.ts +0 -43
- package/dist/frameworks/react/add-ons/storybook/assets/src/components/storybook/input.tsx +0 -39
- package/dist/frameworks/react/add-ons/storybook/assets/src/components/storybook/radio-group.stories.ts +0 -53
- package/dist/frameworks/react/add-ons/storybook/assets/src/components/storybook/radio-group.tsx +0 -52
- package/dist/frameworks/react/add-ons/storybook/assets/src/components/storybook/slider.stories.ts +0 -55
- package/dist/frameworks/react/add-ons/storybook/assets/src/components/storybook/slider.tsx +0 -57
- package/dist/frameworks/react/add-ons/storybook/assets/src/routes/demo/storybook.tsx +0 -93
- package/dist/frameworks/react/add-ons/storybook/package.json +0 -10
- package/src/frameworks/react/add-ons/storybook/assets/_dot_storybook/main.ts +0 -17
- package/src/frameworks/react/add-ons/storybook/assets/_dot_storybook/preview.ts +0 -15
- package/src/frameworks/react/add-ons/storybook/assets/src/components/storybook/button.stories.ts +0 -67
- package/src/frameworks/react/add-ons/storybook/assets/src/components/storybook/button.tsx +0 -47
- package/src/frameworks/react/add-ons/storybook/assets/src/components/storybook/dialog.stories.tsx +0 -92
- package/src/frameworks/react/add-ons/storybook/assets/src/components/storybook/dialog.tsx +0 -29
- package/src/frameworks/react/add-ons/storybook/assets/src/components/storybook/index.ts +0 -14
- package/src/frameworks/react/add-ons/storybook/assets/src/components/storybook/input.stories.ts +0 -43
- package/src/frameworks/react/add-ons/storybook/assets/src/components/storybook/input.tsx +0 -39
- package/src/frameworks/react/add-ons/storybook/assets/src/components/storybook/radio-group.stories.ts +0 -53
- package/src/frameworks/react/add-ons/storybook/assets/src/components/storybook/radio-group.tsx +0 -52
- package/src/frameworks/react/add-ons/storybook/assets/src/components/storybook/slider.stories.ts +0 -55
- package/src/frameworks/react/add-ons/storybook/assets/src/components/storybook/slider.tsx +0 -57
- package/src/frameworks/react/add-ons/storybook/assets/src/routes/demo/storybook.tsx +0 -93
- package/src/frameworks/react/add-ons/storybook/package.json +0 -10
package/package.json
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/create",
|
|
3
|
-
"version": "0.68.
|
|
3
|
+
"version": "0.68.3",
|
|
4
4
|
"description": "TanStack Application Builder Engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/types/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./edge": {
|
|
15
|
+
"types": "./dist/types/edge.d.ts",
|
|
16
|
+
"import": "./dist/edge.js",
|
|
17
|
+
"default": "./dist/edge.js"
|
|
18
|
+
},
|
|
19
|
+
"./manifest": {
|
|
20
|
+
"types": "./dist/types/manifest.d.ts",
|
|
21
|
+
"import": "./dist/manifest.js",
|
|
22
|
+
"default": "./dist/manifest.js"
|
|
23
|
+
},
|
|
24
|
+
"./dist/*": "./dist/*",
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
8
27
|
"repository": {
|
|
9
28
|
"type": "git",
|
|
10
29
|
"url": "git+https://github.com/TanStack/cli.git",
|
|
@@ -46,11 +65,12 @@
|
|
|
46
65
|
"vitest-fetch-mock": "^0.4.5"
|
|
47
66
|
},
|
|
48
67
|
"scripts": {
|
|
49
|
-
"build": "tsc && npm run copy-assets",
|
|
68
|
+
"build": "npm run generate-manifest && tsc && npm run copy-assets",
|
|
69
|
+
"generate-manifest": "node ./scripts/generate-manifest.mjs",
|
|
50
70
|
"copy-assets": "node -e \"const fs=require('fs');const path=require('path');function copyDir(src,dest){if(!fs.existsSync(dest))fs.mkdirSync(dest,{recursive:true});for(const entry of fs.readdirSync(src,{withFileTypes:true})){const srcPath=path.join(src,entry.name);const destPath=path.join(dest,entry.name);if(entry.isDirectory())copyDir(srcPath,destPath);else fs.copyFileSync(srcPath,destPath)}}['react','solid'].forEach(fw=>{['add-ons','toolchains','hosts','examples','project'].forEach(dir=>{const src='src/frameworks/'+fw+'/'+dir;const dest='dist/frameworks/'+fw+'/'+dir;if(fs.existsSync(src))copyDir(src,dest)})})\"",
|
|
51
|
-
"dev": "tsc --watch",
|
|
52
|
-
"test": "eslint ./src && vitest run",
|
|
53
|
-
"test:watch": "vitest",
|
|
71
|
+
"dev": "npm run generate-manifest && tsc --watch",
|
|
72
|
+
"test": "npm run generate-manifest && eslint ./src && vitest run",
|
|
73
|
+
"test:watch": "npm run generate-manifest && vitest",
|
|
54
74
|
"test:coverage": "vitest run --coverage"
|
|
55
75
|
}
|
|
56
76
|
}
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from 'node:fs'
|
|
8
|
+
import { dirname, extname, join, relative, resolve } from 'node:path'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
|
|
11
|
+
const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
12
|
+
const frameworksDir = resolve(packageDir, 'src/frameworks')
|
|
13
|
+
const outputFile = resolve(packageDir, 'src/generated/create-manifest.ts')
|
|
14
|
+
|
|
15
|
+
const binaryExtensions = new Set(['.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico'])
|
|
16
|
+
const templateRenderers = new Map()
|
|
17
|
+
|
|
18
|
+
const frameworkMetadata = {
|
|
19
|
+
react: {
|
|
20
|
+
id: 'react',
|
|
21
|
+
name: 'React',
|
|
22
|
+
description: 'Templates for React',
|
|
23
|
+
version: '0.1.0',
|
|
24
|
+
supportedModes: {
|
|
25
|
+
'file-router': {
|
|
26
|
+
displayName: 'File Router',
|
|
27
|
+
description: 'TanStack Start with file-based routing',
|
|
28
|
+
forceTypescript: true,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
solid: {
|
|
33
|
+
id: 'solid',
|
|
34
|
+
name: 'Solid',
|
|
35
|
+
description: 'Solid templates for Tanstack Router Applications',
|
|
36
|
+
version: '0.1.0',
|
|
37
|
+
supportedModes: {
|
|
38
|
+
'file-router': {
|
|
39
|
+
displayName: 'File Router',
|
|
40
|
+
description: 'TanStack Start with file-based routing',
|
|
41
|
+
forceTypescript: true,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readJson(file) {
|
|
48
|
+
return JSON.parse(readFileSync(file, 'utf8'))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readTemplateFile(file) {
|
|
52
|
+
if (binaryExtensions.has(extname(file))) {
|
|
53
|
+
return `base64::${readFileSync(file).toString('base64')}`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const contents = readFileSync(file, 'utf8').toString()
|
|
57
|
+
if (file.endsWith('.ejs')) {
|
|
58
|
+
registerTemplate(contents)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return contents
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function toCleanPath(file, baseDir) {
|
|
65
|
+
return relative(baseDir, file).replace(/\\/g, '/')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function findFilesRecursively(baseDir) {
|
|
69
|
+
const files = {}
|
|
70
|
+
|
|
71
|
+
if (!existsSync(baseDir)) {
|
|
72
|
+
return files
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function visit(dir) {
|
|
76
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
77
|
+
const file = resolve(dir, entry.name)
|
|
78
|
+
if (entry.isDirectory()) {
|
|
79
|
+
visit(file)
|
|
80
|
+
} else {
|
|
81
|
+
files[toCleanPath(file, baseDir)] = readTemplateFile(file)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
visit(baseDir)
|
|
87
|
+
|
|
88
|
+
return files
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function scanProjectDirectory(frameworkDir) {
|
|
92
|
+
const projectDirectory = join(frameworkDir, 'project')
|
|
93
|
+
const baseDirectory = join(projectDirectory, 'base')
|
|
94
|
+
const basePackagePath = join(baseDirectory, 'package.json')
|
|
95
|
+
const optionalPackagesPath = join(projectDirectory, 'packages.json')
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
base: findFilesRecursively(baseDirectory),
|
|
99
|
+
basePackageJSON: existsSync(basePackagePath) ? readJson(basePackagePath) : {},
|
|
100
|
+
optionalPackages: existsSync(optionalPackagesPath)
|
|
101
|
+
? readJson(optionalPackagesPath)
|
|
102
|
+
: {},
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function scanCatalogDirectory(addOnsBase) {
|
|
107
|
+
if (!existsSync(addOnsBase)) {
|
|
108
|
+
return []
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const addOns = []
|
|
112
|
+
|
|
113
|
+
for (const entry of readdirSync(addOnsBase, { withFileTypes: true })) {
|
|
114
|
+
if (!entry.isDirectory()) {
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const addOnDir = join(addOnsBase, entry.name)
|
|
119
|
+
const info = readJson(join(addOnDir, 'info.json'))
|
|
120
|
+
|
|
121
|
+
let packageAdditions = {}
|
|
122
|
+
let packageTemplate
|
|
123
|
+
const packageJsonPath = join(addOnDir, 'package.json')
|
|
124
|
+
const packageTemplatePath = join(addOnDir, 'package.json.ejs')
|
|
125
|
+
if (existsSync(packageJsonPath)) {
|
|
126
|
+
packageAdditions = readJson(packageJsonPath)
|
|
127
|
+
} else if (existsSync(packageTemplatePath)) {
|
|
128
|
+
packageTemplate = readFileSync(packageTemplatePath, 'utf8')
|
|
129
|
+
registerTemplate(packageTemplate)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let readme
|
|
133
|
+
let readmeIsEjs = false
|
|
134
|
+
const readmePath = join(addOnDir, 'README.md')
|
|
135
|
+
const readmeTemplatePath = join(addOnDir, 'README.md.ejs')
|
|
136
|
+
if (existsSync(readmePath)) {
|
|
137
|
+
readme = readFileSync(readmePath, 'utf8')
|
|
138
|
+
} else if (existsSync(readmeTemplatePath)) {
|
|
139
|
+
readme = readFileSync(readmeTemplatePath, 'utf8')
|
|
140
|
+
registerTemplate(readme)
|
|
141
|
+
readmeIsEjs = true
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let smallLogo
|
|
145
|
+
const smallLogoPath = join(addOnDir, 'small-logo.svg')
|
|
146
|
+
if (existsSync(smallLogoPath)) {
|
|
147
|
+
smallLogo = readFileSync(smallLogoPath, 'utf8')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
addOns.push({
|
|
151
|
+
...info,
|
|
152
|
+
id: entry.name,
|
|
153
|
+
version: info.version ?? '0.0.0',
|
|
154
|
+
packageAdditions,
|
|
155
|
+
packageTemplate,
|
|
156
|
+
readme,
|
|
157
|
+
readmeIsEjs,
|
|
158
|
+
files: findFilesRecursively(join(addOnDir, 'assets')),
|
|
159
|
+
deletedFiles: info.deletedFiles ?? [],
|
|
160
|
+
smallLogo,
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return addOns
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function createFramework(frameworkId) {
|
|
168
|
+
const frameworkDir = join(frameworksDir, frameworkId)
|
|
169
|
+
const project = scanProjectDirectory(frameworkDir)
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
...frameworkMetadata[frameworkId],
|
|
173
|
+
...project,
|
|
174
|
+
addOns: [
|
|
175
|
+
...scanCatalogDirectory(join(frameworkDir, 'add-ons')),
|
|
176
|
+
...scanCatalogDirectory(join(frameworkDir, 'toolchains')),
|
|
177
|
+
...scanCatalogDirectory(join(frameworkDir, 'examples')),
|
|
178
|
+
...scanCatalogDirectory(join(frameworkDir, 'hosts')),
|
|
179
|
+
],
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getTemplateKey(template) {
|
|
184
|
+
let hash = 0x811c9dc5
|
|
185
|
+
for (let i = 0; i < template.length; i++) {
|
|
186
|
+
hash ^= template.charCodeAt(i)
|
|
187
|
+
hash = Math.imul(hash, 0x01000193) >>> 0
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return `${hash.toString(16).padStart(8, '0')}:${template.length}`
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function registerTemplate(template) {
|
|
194
|
+
const key = getTemplateKey(template)
|
|
195
|
+
if (!templateRenderers.has(key)) {
|
|
196
|
+
templateRenderers.set(key, compileTemplate(template))
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function stripSemicolon(code) {
|
|
201
|
+
return code.replace(/;(\s*$)/, '$1')
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function compileTemplate(template) {
|
|
205
|
+
const regex = /<%([=_#-]?|_)?([\s\S]*?)([-_]?%>)/g
|
|
206
|
+
let cursor = 0
|
|
207
|
+
let trimLeadingWhitespace = false
|
|
208
|
+
const lines = []
|
|
209
|
+
|
|
210
|
+
function appendText(value) {
|
|
211
|
+
if (!value) {
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
lines.push(` __append(${JSON.stringify(value)})`)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const match of template.matchAll(regex)) {
|
|
218
|
+
let text = template.slice(cursor, match.index)
|
|
219
|
+
if (trimLeadingWhitespace) {
|
|
220
|
+
text = text.replace(/^\s*\r?\n?/, '')
|
|
221
|
+
trimLeadingWhitespace = false
|
|
222
|
+
}
|
|
223
|
+
if (match[1] === '_') {
|
|
224
|
+
text = text.replace(/\s*$/, '')
|
|
225
|
+
}
|
|
226
|
+
appendText(text)
|
|
227
|
+
|
|
228
|
+
const marker = match[1] || ''
|
|
229
|
+
const code = match[2]
|
|
230
|
+
const close = match[3]
|
|
231
|
+
|
|
232
|
+
if (marker === '=') {
|
|
233
|
+
lines.push(` __append(__escapeXML(${stripSemicolon(code.trim())}))`)
|
|
234
|
+
} else if (marker === '-') {
|
|
235
|
+
lines.push(` __append(${stripSemicolon(code.trim())})`)
|
|
236
|
+
} else if (marker !== '#') {
|
|
237
|
+
lines.push(code)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
trimLeadingWhitespace = close.startsWith('-') || close.startsWith('_')
|
|
241
|
+
cursor = match.index + match[0].length
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let tail = template.slice(cursor)
|
|
245
|
+
if (trimLeadingWhitespace) {
|
|
246
|
+
tail = tail.replace(/^\s*\r?\n?/, '')
|
|
247
|
+
}
|
|
248
|
+
appendText(tail)
|
|
249
|
+
|
|
250
|
+
return lines.join('\n')
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function createTemplateRendererSource() {
|
|
254
|
+
const entries = Array.from(templateRenderers.entries()).sort(([a], [b]) =>
|
|
255
|
+
a.localeCompare(b),
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
const functions = entries
|
|
259
|
+
.map(([key, body]) => {
|
|
260
|
+
const functionName = `__render_${key.replace(/[^a-zA-Z0-9_$]/g, '_')}`
|
|
261
|
+
return `function ${functionName}(context: TemplateRenderContext) {
|
|
262
|
+
const {
|
|
263
|
+
packageManager,
|
|
264
|
+
projectName,
|
|
265
|
+
typescript,
|
|
266
|
+
tailwind,
|
|
267
|
+
js,
|
|
268
|
+
jsx,
|
|
269
|
+
fileRouter,
|
|
270
|
+
codeRouter,
|
|
271
|
+
routerOnly,
|
|
272
|
+
includeExamples,
|
|
273
|
+
addOnEnabled,
|
|
274
|
+
addOnOption,
|
|
275
|
+
addOns,
|
|
276
|
+
integrations,
|
|
277
|
+
routes,
|
|
278
|
+
getPackageManagerAddScript,
|
|
279
|
+
getPackageManagerRunScript,
|
|
280
|
+
getPackageManagerExecuteScript,
|
|
281
|
+
relativePath,
|
|
282
|
+
integrationImportContent,
|
|
283
|
+
integrationImportCode,
|
|
284
|
+
renderTemplate,
|
|
285
|
+
ignoreFile,
|
|
286
|
+
} = context
|
|
287
|
+
let __output = ''
|
|
288
|
+
const __append = (value: unknown) => {
|
|
289
|
+
if (value !== undefined && value !== null) {
|
|
290
|
+
__output += String(value)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
${body}
|
|
294
|
+
return __output
|
|
295
|
+
}`
|
|
296
|
+
})
|
|
297
|
+
.join('\n\n')
|
|
298
|
+
|
|
299
|
+
const mapEntries = entries
|
|
300
|
+
.map(([key]) => {
|
|
301
|
+
const functionName = `__render_${key.replace(/[^a-zA-Z0-9_$]/g, '_')}`
|
|
302
|
+
return ` ${JSON.stringify(key)}: ${functionName},`
|
|
303
|
+
})
|
|
304
|
+
.join('\n')
|
|
305
|
+
|
|
306
|
+
return `type TemplateRecord = Record<string, any>
|
|
307
|
+
type TemplateAddOn = TemplateRecord & {
|
|
308
|
+
integrations?: Array<TemplateRecord>
|
|
309
|
+
routes?: Array<TemplateRecord>
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
type TemplateRenderContext = {
|
|
313
|
+
[key: string]: any
|
|
314
|
+
packageManager: any
|
|
315
|
+
projectName: any
|
|
316
|
+
typescript: any
|
|
317
|
+
tailwind: any
|
|
318
|
+
js: any
|
|
319
|
+
jsx: any
|
|
320
|
+
fileRouter: any
|
|
321
|
+
codeRouter: any
|
|
322
|
+
routerOnly: any
|
|
323
|
+
includeExamples: any
|
|
324
|
+
addOnEnabled: Record<string, any>
|
|
325
|
+
addOnOption: Record<string, any>
|
|
326
|
+
addOns: Array<TemplateAddOn>
|
|
327
|
+
integrations: Array<TemplateRecord>
|
|
328
|
+
routes: Array<TemplateRecord>
|
|
329
|
+
getPackageManagerAddScript: (...args: Array<any>) => string
|
|
330
|
+
getPackageManagerRunScript: (...args: Array<any>) => string
|
|
331
|
+
getPackageManagerExecuteScript: (...args: Array<any>) => string
|
|
332
|
+
relativePath: (...args: Array<any>) => string
|
|
333
|
+
integrationImportContent: (...args: Array<any>) => string
|
|
334
|
+
integrationImportCode: (...args: Array<any>) => string
|
|
335
|
+
renderTemplate: (content: string) => string
|
|
336
|
+
ignoreFile: () => never
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
type TemplateRenderer = (context: TemplateRenderContext) => string | undefined
|
|
340
|
+
|
|
341
|
+
function __escapeXML(value: unknown) {
|
|
342
|
+
if (value === undefined || value === null) {
|
|
343
|
+
return ''
|
|
344
|
+
}
|
|
345
|
+
return String(value).replace(/[&<>'"]/g, (character) => {
|
|
346
|
+
switch (character) {
|
|
347
|
+
case '&':
|
|
348
|
+
return '&'
|
|
349
|
+
case '<':
|
|
350
|
+
return '<'
|
|
351
|
+
case '>':
|
|
352
|
+
return '>'
|
|
353
|
+
case '"':
|
|
354
|
+
return '"'
|
|
355
|
+
case "'":
|
|
356
|
+
return '''
|
|
357
|
+
default:
|
|
358
|
+
return character
|
|
359
|
+
}
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function getManifestTemplateKey(template: string) {
|
|
364
|
+
let hash = 0x811c9dc5
|
|
365
|
+
for (let i = 0; i < template.length; i++) {
|
|
366
|
+
hash ^= template.charCodeAt(i)
|
|
367
|
+
hash = Math.imul(hash, 0x01000193) >>> 0
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return \`\${hash.toString(16).padStart(8, '0')}:\${template.length}\`
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
${functions}
|
|
374
|
+
|
|
375
|
+
const templateRenderers: Record<string, TemplateRenderer> = {
|
|
376
|
+
${mapEntries}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function renderManifestTemplate(
|
|
380
|
+
template: string,
|
|
381
|
+
context: TemplateRenderContext,
|
|
382
|
+
) {
|
|
383
|
+
const key = getManifestTemplateKey(template)
|
|
384
|
+
const renderer = templateRenderers[key]
|
|
385
|
+
if (!renderer) {
|
|
386
|
+
throw new Error(\`Template \${key} was not precompiled into the manifest\`)
|
|
387
|
+
}
|
|
388
|
+
return renderer(context) ?? ''
|
|
389
|
+
}
|
|
390
|
+
`
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const manifest = [createFramework('react'), createFramework('solid')]
|
|
394
|
+
|
|
395
|
+
mkdirSync(dirname(outputFile), { recursive: true })
|
|
396
|
+
writeFileSync(
|
|
397
|
+
outputFile,
|
|
398
|
+
`// Generated by scripts/generate-manifest.mjs. Do not edit by hand.\n` +
|
|
399
|
+
`import type { ManifestFrameworkDefinition } from '../manifest-types.js'\n\n` +
|
|
400
|
+
createTemplateRendererSource() +
|
|
401
|
+
'\n' +
|
|
402
|
+
`export const createManifestFrameworks = (): Array<ManifestFrameworkDefinition> => ${JSON.stringify(
|
|
403
|
+
manifest,
|
|
404
|
+
null,
|
|
405
|
+
2,
|
|
406
|
+
)}\n`,
|
|
407
|
+
)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { AddOnCompiledSchema } from './types.js'
|
|
2
|
+
|
|
3
|
+
import type { AddOn, Framework } from './types.js'
|
|
4
|
+
|
|
5
|
+
export function getAllAddOns(framework: Framework, mode: string): Array<AddOn> {
|
|
6
|
+
return framework
|
|
7
|
+
.getAddOns()
|
|
8
|
+
.filter((a) => a.modes.includes(mode))
|
|
9
|
+
.sort((a, b) => {
|
|
10
|
+
const aPriority = a.priority ?? 0
|
|
11
|
+
const bPriority = b.priority ?? 0
|
|
12
|
+
return bPriority - aPriority
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function finalizeAddOns(
|
|
17
|
+
framework: Framework,
|
|
18
|
+
mode: string,
|
|
19
|
+
chosenAddOnIDs: Array<string>,
|
|
20
|
+
): Promise<Array<AddOn>> {
|
|
21
|
+
const finalAddOnIDs = new Set(chosenAddOnIDs)
|
|
22
|
+
const addOns = getAllAddOns(framework, mode)
|
|
23
|
+
|
|
24
|
+
for (const addOnID of finalAddOnIDs) {
|
|
25
|
+
let addOn: AddOn | undefined
|
|
26
|
+
const localAddOn =
|
|
27
|
+
addOns.find((a) => a.id === addOnID) ??
|
|
28
|
+
addOns.find((a) => a.id.toLowerCase() === addOnID.toLowerCase())
|
|
29
|
+
|
|
30
|
+
if (localAddOn) {
|
|
31
|
+
addOn = localAddOn
|
|
32
|
+
if (localAddOn.id !== addOnID) {
|
|
33
|
+
finalAddOnIDs.delete(addOnID)
|
|
34
|
+
finalAddOnIDs.add(localAddOn.id)
|
|
35
|
+
}
|
|
36
|
+
} else if (addOnID.startsWith('http')) {
|
|
37
|
+
addOn = await loadRemoteAddOn(addOnID)
|
|
38
|
+
addOns.push(addOn)
|
|
39
|
+
} else {
|
|
40
|
+
const suggestion = findClosestAddOn(addOnID, addOns)
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Add-on ${addOnID} not found${suggestion ? `. Did you mean "${suggestion}"?` : ''}`,
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const dependsOn of addOn.dependsOn || []) {
|
|
47
|
+
const dep = addOns.find((a) => a.id === dependsOn)
|
|
48
|
+
if (!dep) {
|
|
49
|
+
throw new Error(`Dependency ${dependsOn} not found`)
|
|
50
|
+
}
|
|
51
|
+
finalAddOnIDs.add(dep.id)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return [...finalAddOnIDs].map((id) => addOns.find((a) => a.id === id)!)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function populateAddOnOptionsDefaults(
|
|
59
|
+
chosenAddOns: Array<AddOn>,
|
|
60
|
+
): Record<string, Record<string, unknown>> {
|
|
61
|
+
const addOnOptions: Record<string, Record<string, unknown>> = {}
|
|
62
|
+
|
|
63
|
+
for (const addOn of chosenAddOns) {
|
|
64
|
+
if (addOn.options) {
|
|
65
|
+
const defaults: Record<string, unknown> = {}
|
|
66
|
+
for (const [optionKey, optionDef] of Object.entries(addOn.options)) {
|
|
67
|
+
defaults[optionKey] = optionDef.default
|
|
68
|
+
}
|
|
69
|
+
addOnOptions[addOn.id] = defaults
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return addOnOptions
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function loadRemoteAddOn(url: string): Promise<AddOn> {
|
|
77
|
+
const response = await fetch(url)
|
|
78
|
+
const jsonContent = await response.json()
|
|
79
|
+
const checked = AddOnCompiledSchema.safeParse(jsonContent)
|
|
80
|
+
|
|
81
|
+
if (!checked.success) {
|
|
82
|
+
throw new Error(`Invalid add-on: ${url}`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const addOn = {
|
|
86
|
+
...checked.data,
|
|
87
|
+
id: url,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
...addOn,
|
|
92
|
+
getFiles: () => Promise.resolve(Object.keys(addOn.files)),
|
|
93
|
+
getFileContents: (path: string) => Promise.resolve(addOn.files[path]),
|
|
94
|
+
getDeletedFiles: () => Promise.resolve(addOn.deletedFiles),
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function findClosestAddOn(
|
|
99
|
+
input: string,
|
|
100
|
+
addOns: Array<AddOn>,
|
|
101
|
+
): string | undefined {
|
|
102
|
+
const inputLower = input.toLowerCase()
|
|
103
|
+
let bestMatch: string | undefined
|
|
104
|
+
let bestDistance = Infinity
|
|
105
|
+
|
|
106
|
+
for (const addOn of addOns) {
|
|
107
|
+
const distance = levenshtein(inputLower, addOn.id.toLowerCase())
|
|
108
|
+
if (distance < bestDistance) {
|
|
109
|
+
bestDistance = distance
|
|
110
|
+
bestMatch = addOn.id
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (bestMatch && bestDistance <= Math.max(Math.floor(input.length / 2), 2)) {
|
|
115
|
+
return bestMatch
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return undefined
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function levenshtein(a: string, b: string): number {
|
|
122
|
+
const m = a.length
|
|
123
|
+
const n = b.length
|
|
124
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j)
|
|
125
|
+
|
|
126
|
+
for (let i = 1; i <= m; i++) {
|
|
127
|
+
const curr = [i]
|
|
128
|
+
for (let j = 1; j <= n; j++) {
|
|
129
|
+
curr[j] =
|
|
130
|
+
a[i - 1] === b[j - 1]
|
|
131
|
+
? prev[j - 1]
|
|
132
|
+
: 1 + Math.min(prev[j], curr[j - 1], prev[j - 1])
|
|
133
|
+
}
|
|
134
|
+
prev = curr
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return prev[n]
|
|
138
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { CONFIG_FILE } from './constants.js'
|
|
2
|
+
import { joinPaths } from './edge-path.js'
|
|
3
|
+
|
|
4
|
+
import type { Environment, Options } from './types.js'
|
|
5
|
+
|
|
6
|
+
export type PersistedOptions = Omit<
|
|
7
|
+
Partial<Options>,
|
|
8
|
+
'addOns' | 'chosenAddOns' | 'framework' | 'starter' | 'targetDir'
|
|
9
|
+
> & {
|
|
10
|
+
framework: string
|
|
11
|
+
version: number
|
|
12
|
+
chosenAddOns: Array<string>
|
|
13
|
+
starter?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createPersistedOptions(options: Options): PersistedOptions {
|
|
17
|
+
const { chosenAddOns, framework, targetDir: _targetDir, ...rest } = options
|
|
18
|
+
return {
|
|
19
|
+
...rest,
|
|
20
|
+
version: 1,
|
|
21
|
+
framework: framework.id,
|
|
22
|
+
chosenAddOns: chosenAddOns.map((addOn) => addOn.id),
|
|
23
|
+
starter: options.starter?.id ?? undefined,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function writeConfigFileToEnvironment(
|
|
28
|
+
environment: Environment,
|
|
29
|
+
options: Options,
|
|
30
|
+
) {
|
|
31
|
+
await environment.writeFile(
|
|
32
|
+
joinPaths(options.targetDir, CONFIG_FILE),
|
|
33
|
+
JSON.stringify(createPersistedOptions(options), null, 2),
|
|
34
|
+
)
|
|
35
|
+
}
|