@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.
Files changed (83) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/edge-add-ons.js +106 -0
  3. package/dist/edge-config-file.js +15 -0
  4. package/dist/edge-create-app.js +438 -0
  5. package/dist/edge-environment.js +141 -0
  6. package/dist/edge-file-helpers.js +88 -0
  7. package/dist/edge-frameworks.js +33 -0
  8. package/dist/edge-package-json.js +146 -0
  9. package/dist/edge-path.js +62 -0
  10. package/dist/edge-render.js +31 -0
  11. package/dist/edge-template-file.js +141 -0
  12. package/dist/edge.js +7 -0
  13. package/dist/frameworks/react/add-ons/storybook/info.json +5 -10
  14. package/dist/generated/create-manifest.js +4683 -0
  15. package/dist/manifest-types.js +1 -0
  16. package/dist/manifest.js +1 -0
  17. package/dist/types/custom-add-ons/add-on.d.ts +5 -3
  18. package/dist/types/edge-add-ons.d.ts +5 -0
  19. package/dist/types/edge-config-file.d.ts +8 -0
  20. package/dist/types/edge-create-app.d.ts +2 -0
  21. package/dist/types/edge-environment.d.ts +19 -0
  22. package/dist/types/edge-file-helpers.d.ts +7 -0
  23. package/dist/types/edge-frameworks.d.ts +7 -0
  24. package/dist/types/edge-package-json.d.ts +3 -0
  25. package/dist/types/edge-path.d.ts +5 -0
  26. package/dist/types/edge-render.d.ts +1 -0
  27. package/dist/types/edge-template-file.d.ts +2 -0
  28. package/dist/types/edge.d.ts +9 -0
  29. package/dist/types/generated/create-manifest.d.ts +36 -0
  30. package/dist/types/manifest-types.d.ts +4 -0
  31. package/dist/types/manifest.d.ts +1 -0
  32. package/dist/types/types.d.ts +96 -56
  33. package/dist/types.js +5 -3
  34. package/package.json +25 -5
  35. package/scripts/generate-manifest.mjs +407 -0
  36. package/src/edge-add-ons.ts +138 -0
  37. package/src/edge-config-file.ts +35 -0
  38. package/src/edge-create-app.ts +594 -0
  39. package/src/edge-environment.ts +175 -0
  40. package/src/edge-file-helpers.ts +112 -0
  41. package/src/edge-frameworks.ts +54 -0
  42. package/src/edge-package-json.ts +212 -0
  43. package/src/edge-path.ts +77 -0
  44. package/src/edge-render.ts +32 -0
  45. package/src/edge-template-file.ts +204 -0
  46. package/src/edge.ts +43 -0
  47. package/src/frameworks/react/add-ons/storybook/info.json +5 -10
  48. package/src/generated/create-manifest.ts +6490 -0
  49. package/src/manifest-types.ts +8 -0
  50. package/src/manifest.ts +1 -0
  51. package/src/types.ts +5 -3
  52. package/tests/edge-import.test.ts +31 -0
  53. package/tests/edge-manifest.test.ts +168 -0
  54. package/dist/frameworks/react/add-ons/storybook/assets/_dot_storybook/main.ts +0 -17
  55. package/dist/frameworks/react/add-ons/storybook/assets/_dot_storybook/preview.ts +0 -15
  56. package/dist/frameworks/react/add-ons/storybook/assets/src/components/storybook/button.stories.ts +0 -67
  57. package/dist/frameworks/react/add-ons/storybook/assets/src/components/storybook/button.tsx +0 -47
  58. package/dist/frameworks/react/add-ons/storybook/assets/src/components/storybook/dialog.stories.tsx +0 -92
  59. package/dist/frameworks/react/add-ons/storybook/assets/src/components/storybook/dialog.tsx +0 -29
  60. package/dist/frameworks/react/add-ons/storybook/assets/src/components/storybook/index.ts +0 -14
  61. package/dist/frameworks/react/add-ons/storybook/assets/src/components/storybook/input.stories.ts +0 -43
  62. package/dist/frameworks/react/add-ons/storybook/assets/src/components/storybook/input.tsx +0 -39
  63. package/dist/frameworks/react/add-ons/storybook/assets/src/components/storybook/radio-group.stories.ts +0 -53
  64. package/dist/frameworks/react/add-ons/storybook/assets/src/components/storybook/radio-group.tsx +0 -52
  65. package/dist/frameworks/react/add-ons/storybook/assets/src/components/storybook/slider.stories.ts +0 -55
  66. package/dist/frameworks/react/add-ons/storybook/assets/src/components/storybook/slider.tsx +0 -57
  67. package/dist/frameworks/react/add-ons/storybook/assets/src/routes/demo/storybook.tsx +0 -93
  68. package/dist/frameworks/react/add-ons/storybook/package.json +0 -10
  69. package/src/frameworks/react/add-ons/storybook/assets/_dot_storybook/main.ts +0 -17
  70. package/src/frameworks/react/add-ons/storybook/assets/_dot_storybook/preview.ts +0 -15
  71. package/src/frameworks/react/add-ons/storybook/assets/src/components/storybook/button.stories.ts +0 -67
  72. package/src/frameworks/react/add-ons/storybook/assets/src/components/storybook/button.tsx +0 -47
  73. package/src/frameworks/react/add-ons/storybook/assets/src/components/storybook/dialog.stories.tsx +0 -92
  74. package/src/frameworks/react/add-ons/storybook/assets/src/components/storybook/dialog.tsx +0 -29
  75. package/src/frameworks/react/add-ons/storybook/assets/src/components/storybook/index.ts +0 -14
  76. package/src/frameworks/react/add-ons/storybook/assets/src/components/storybook/input.stories.ts +0 -43
  77. package/src/frameworks/react/add-ons/storybook/assets/src/components/storybook/input.tsx +0 -39
  78. package/src/frameworks/react/add-ons/storybook/assets/src/components/storybook/radio-group.stories.ts +0 -53
  79. package/src/frameworks/react/add-ons/storybook/assets/src/components/storybook/radio-group.tsx +0 -52
  80. package/src/frameworks/react/add-ons/storybook/assets/src/components/storybook/slider.stories.ts +0 -55
  81. package/src/frameworks/react/add-ons/storybook/assets/src/components/storybook/slider.tsx +0 -57
  82. package/src/frameworks/react/add-ons/storybook/assets/src/routes/demo/storybook.tsx +0 -93
  83. 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.2",
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 '&amp;'
349
+ case '<':
350
+ return '&lt;'
351
+ case '>':
352
+ return '&gt;'
353
+ case '"':
354
+ return '&#34;'
355
+ case "'":
356
+ return '&#39;'
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
+ }