@tanstack/cta-engine 0.48.0 → 0.49.1
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 +12 -0
- package/dist/attribution.js +162 -0
- package/dist/custom-add-ons/shared.js +5 -4
- package/dist/file-helpers.js +14 -4
- package/dist/frameworks.js +3 -3
- package/dist/index.js +3 -2
- package/dist/template-file.js +6 -1
- package/dist/types/attribution.d.ts +12 -0
- package/dist/types/custom-add-ons/add-on.d.ts +4 -8
- package/dist/types/custom-add-ons/shared.d.ts +1 -8
- package/dist/types/environment.d.ts +9 -8
- package/dist/types/file-helpers.d.ts +5 -0
- package/dist/types/index.d.ts +6 -3
- package/dist/types/types.d.ts +72 -0
- package/dist/types.js +20 -0
- package/package.json +1 -1
- package/src/attribution.ts +245 -0
- package/src/custom-add-ons/shared.ts +5 -4
- package/src/environment.ts +13 -9
- package/src/file-helpers.ts +14 -4
- package/src/frameworks.ts +8 -4
- package/src/index.ts +13 -1
- package/src/template-file.ts +14 -3
- package/src/types.ts +54 -0
- package/tests/add-to-app.test.ts +21 -21
- package/tests/create-app.test.ts +3 -3
- package/tests/custom-add-ons/shared.test.ts +2 -2
- package/tests/file-helper.test.ts +1 -1
- package/tests/filename-processing.test.ts +17 -17
- package/tests/frameworks.test.ts +3 -3
- package/tests/template-context.test.ts +15 -15
- package/tests/template-file.test.ts +12 -12
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AddOn,
|
|
3
|
+
AttributedFile,
|
|
4
|
+
DependencyAttribution,
|
|
5
|
+
FileProvenance,
|
|
6
|
+
Framework,
|
|
7
|
+
Integration,
|
|
8
|
+
IntegrationWithSource,
|
|
9
|
+
LineAttribution,
|
|
10
|
+
Starter,
|
|
11
|
+
} from './types.js'
|
|
12
|
+
|
|
13
|
+
export interface AttributionInput {
|
|
14
|
+
framework: Framework
|
|
15
|
+
chosenAddOns: Array<AddOn>
|
|
16
|
+
starter?: Starter
|
|
17
|
+
files: Record<string, string>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AttributionOutput {
|
|
21
|
+
attributedFiles: Record<string, AttributedFile>
|
|
22
|
+
dependencies: Array<DependencyAttribution>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type Source = { sourceId: string; sourceName: string }
|
|
26
|
+
|
|
27
|
+
// A pattern to search for in file content, with its source add-on
|
|
28
|
+
interface Injection {
|
|
29
|
+
matches: (line: string) => boolean
|
|
30
|
+
appliesTo: (filePath: string) => boolean
|
|
31
|
+
source: Source
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizePath(path: string): string {
|
|
35
|
+
let p = path.startsWith('./') ? path.slice(2) : path
|
|
36
|
+
p = p.replace(/\.ejs$/, '').replace(/_dot_/g, '.')
|
|
37
|
+
const match = p.match(/^(.+\/)?__([^_]+)__(.+)$/)
|
|
38
|
+
return match ? (match[1] || '') + match[3] : p
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function getFileProvenance(
|
|
42
|
+
filePath: string,
|
|
43
|
+
framework: Framework,
|
|
44
|
+
addOns: Array<AddOn>,
|
|
45
|
+
starter?: Starter,
|
|
46
|
+
): Promise<FileProvenance | null> {
|
|
47
|
+
const target = filePath.startsWith('./') ? filePath.slice(2) : filePath
|
|
48
|
+
|
|
49
|
+
if (starter) {
|
|
50
|
+
const files = await starter.getFiles()
|
|
51
|
+
if (files.some((f: string) => normalizePath(f) === target)) {
|
|
52
|
+
return {
|
|
53
|
+
source: 'starter',
|
|
54
|
+
sourceId: starter.id,
|
|
55
|
+
sourceName: starter.name,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Order add-ons by type then phase (matches writeFiles order), check in reverse
|
|
61
|
+
const typeOrder = ['add-on', 'example', 'toolchain', 'deployment']
|
|
62
|
+
const phaseOrder = ['setup', 'add-on', 'example']
|
|
63
|
+
const ordered = typeOrder.flatMap((type) =>
|
|
64
|
+
phaseOrder.flatMap((phase) =>
|
|
65
|
+
addOns.filter((a) => a.phase === phase && a.type === type),
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
for (let i = ordered.length - 1; i >= 0; i--) {
|
|
70
|
+
const files = await ordered[i].getFiles()
|
|
71
|
+
if (files.some((f: string) => normalizePath(f) === target)) {
|
|
72
|
+
return {
|
|
73
|
+
source: 'add-on',
|
|
74
|
+
sourceId: ordered[i].id,
|
|
75
|
+
sourceName: ordered[i].name,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const frameworkFiles = await framework.getFiles()
|
|
81
|
+
if (frameworkFiles.some((f: string) => normalizePath(f) === target)) {
|
|
82
|
+
return {
|
|
83
|
+
source: 'framework',
|
|
84
|
+
sourceId: framework.id,
|
|
85
|
+
sourceName: framework.name,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Build injection patterns from integrations (for source files)
|
|
93
|
+
function integrationInjections(int: IntegrationWithSource): Array<Injection> {
|
|
94
|
+
const source = { sourceId: int._sourceId, sourceName: int._sourceName }
|
|
95
|
+
const injections: Array<Injection> = []
|
|
96
|
+
|
|
97
|
+
const appliesTo = (path: string) => {
|
|
98
|
+
if (int.type === 'vite-plugin') return path.includes('vite.config')
|
|
99
|
+
if (
|
|
100
|
+
int.type === 'provider' ||
|
|
101
|
+
int.type === 'root-provider' ||
|
|
102
|
+
int.type === 'devtools'
|
|
103
|
+
) {
|
|
104
|
+
return path.includes('__root') || path.includes('root.tsx')
|
|
105
|
+
}
|
|
106
|
+
return false
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (int.import) {
|
|
110
|
+
const prefix = int.import.split(' from ')[0]
|
|
111
|
+
injections.push({
|
|
112
|
+
matches: (line) => line.includes(prefix),
|
|
113
|
+
appliesTo,
|
|
114
|
+
source,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const code = int.code || int.jsName
|
|
119
|
+
if (code) {
|
|
120
|
+
injections.push({
|
|
121
|
+
matches: (line) => line.includes(code),
|
|
122
|
+
appliesTo,
|
|
123
|
+
source,
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return injections
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Build injection pattern from a dependency (for package.json)
|
|
131
|
+
function dependencyInjection(dep: DependencyAttribution): Injection {
|
|
132
|
+
return {
|
|
133
|
+
matches: (line) => line.includes(`"${dep.name}"`),
|
|
134
|
+
appliesTo: (path) => path.endsWith('package.json'),
|
|
135
|
+
source: { sourceId: dep.sourceId, sourceName: dep.sourceName },
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function computeAttribution(
|
|
140
|
+
input: AttributionInput,
|
|
141
|
+
): Promise<AttributionOutput> {
|
|
142
|
+
const { framework, chosenAddOns, starter, files } = input
|
|
143
|
+
|
|
144
|
+
// Collect integrations tagged with source
|
|
145
|
+
const integrations: Array<IntegrationWithSource> = chosenAddOns.flatMap(
|
|
146
|
+
(addOn) =>
|
|
147
|
+
(addOn.integrations || []).map((int: Integration) => ({
|
|
148
|
+
...int,
|
|
149
|
+
_sourceId: addOn.id,
|
|
150
|
+
_sourceName: addOn.name,
|
|
151
|
+
})),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
// Collect dependencies from add-ons (from packageAdditions or packageTemplate)
|
|
155
|
+
const dependencies: Array<DependencyAttribution> = chosenAddOns.flatMap(
|
|
156
|
+
(addOn) => {
|
|
157
|
+
const result: Array<DependencyAttribution> = []
|
|
158
|
+
const source = { sourceId: addOn.id, sourceName: addOn.name }
|
|
159
|
+
|
|
160
|
+
const addDeps = (
|
|
161
|
+
deps: Record<string, unknown> | undefined,
|
|
162
|
+
type: 'dependency' | 'devDependency',
|
|
163
|
+
) => {
|
|
164
|
+
if (!deps) return
|
|
165
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
166
|
+
if (typeof version === 'string') {
|
|
167
|
+
result.push({ name, version, type, ...source })
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// From static package.json
|
|
173
|
+
addDeps(addOn.packageAdditions?.dependencies, 'dependency')
|
|
174
|
+
addDeps(addOn.packageAdditions?.devDependencies, 'devDependency')
|
|
175
|
+
|
|
176
|
+
// From package.json.ejs template (strip EJS tags and parse)
|
|
177
|
+
if (addOn.packageTemplate) {
|
|
178
|
+
try {
|
|
179
|
+
const tmpl = JSON.parse(
|
|
180
|
+
addOn.packageTemplate.replace(/"[^"]*<%[^%]*%>[^"]*"/g, '""'),
|
|
181
|
+
)
|
|
182
|
+
addDeps(tmpl.dependencies, 'dependency')
|
|
183
|
+
addDeps(tmpl.devDependencies, 'devDependency')
|
|
184
|
+
} catch {}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return result
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
// Build unified injection patterns from both integrations and dependencies
|
|
192
|
+
const injections: Array<Injection> = [
|
|
193
|
+
...integrations.flatMap(integrationInjections),
|
|
194
|
+
...dependencies.map(dependencyInjection),
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
const attributedFiles: Record<string, AttributedFile> = {}
|
|
198
|
+
|
|
199
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
200
|
+
const provenance = await getFileProvenance(
|
|
201
|
+
filePath,
|
|
202
|
+
framework,
|
|
203
|
+
chosenAddOns,
|
|
204
|
+
starter,
|
|
205
|
+
)
|
|
206
|
+
if (!provenance) continue
|
|
207
|
+
|
|
208
|
+
const lines = content.split('\n')
|
|
209
|
+
const relevant = injections.filter((inj) => inj.appliesTo(filePath))
|
|
210
|
+
|
|
211
|
+
// Find injected lines
|
|
212
|
+
const injectedLines = new Map<number, Source>()
|
|
213
|
+
for (const inj of relevant) {
|
|
214
|
+
lines.forEach((line, i) => {
|
|
215
|
+
if (inj.matches(line) && !injectedLines.has(i + 1)) {
|
|
216
|
+
injectedLines.set(i + 1, inj.source)
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
attributedFiles[filePath] = {
|
|
222
|
+
content,
|
|
223
|
+
provenance,
|
|
224
|
+
lineAttributions: lines.map((_, i): LineAttribution => {
|
|
225
|
+
const lineNum = i + 1
|
|
226
|
+
const inj = injectedLines.get(lineNum)
|
|
227
|
+
return inj
|
|
228
|
+
? {
|
|
229
|
+
line: lineNum,
|
|
230
|
+
sourceId: inj.sourceId,
|
|
231
|
+
sourceName: inj.sourceName,
|
|
232
|
+
type: 'injected',
|
|
233
|
+
}
|
|
234
|
+
: {
|
|
235
|
+
line: lineNum,
|
|
236
|
+
sourceId: provenance.sourceId,
|
|
237
|
+
sourceName: provenance.sourceName,
|
|
238
|
+
type: 'original',
|
|
239
|
+
}
|
|
240
|
+
}),
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return { attributedFiles, dependencies }
|
|
245
|
+
}
|
|
@@ -5,7 +5,7 @@ import { createMemoryEnvironment } from '../environment.js'
|
|
|
5
5
|
import { finalizeAddOns, populateAddOnOptionsDefaults } from '../add-ons.js'
|
|
6
6
|
import { getFrameworkById } from '../frameworks.js'
|
|
7
7
|
import { readConfigFileFromEnvironment } from '../config-file.js'
|
|
8
|
-
import { readFileHelper } from '../file-helpers.js'
|
|
8
|
+
import { readFileHelper, toCleanPath } from '../file-helpers.js'
|
|
9
9
|
import { loadStarter } from '../custom-add-ons/starter.js'
|
|
10
10
|
|
|
11
11
|
import type { Environment, Options, SerializedOptions } from '../types.js'
|
|
@@ -117,9 +117,10 @@ export async function runCreateApp(options: Required<Options>) {
|
|
|
117
117
|
})
|
|
118
118
|
|
|
119
119
|
output.files = Object.fromEntries(
|
|
120
|
-
Object.entries(output.files).map(([key, value]) =>
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
Object.entries(output.files).map(([key, value]) => [
|
|
121
|
+
toCleanPath(key, targetDir),
|
|
122
|
+
value,
|
|
123
|
+
]),
|
|
123
124
|
)
|
|
124
125
|
|
|
125
126
|
return output
|
package/src/environment.ts
CHANGED
|
@@ -21,6 +21,12 @@ import {
|
|
|
21
21
|
|
|
22
22
|
import type { Environment } from './types.js'
|
|
23
23
|
|
|
24
|
+
export interface MemoryEnvironmentOutput {
|
|
25
|
+
files: Record<string, string>
|
|
26
|
+
deletedFiles: Array<string>
|
|
27
|
+
commands: Array<{ command: string; args: Array<string> }>
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
export function createDefaultEnvironment(): Environment {
|
|
25
31
|
let errors: Array<string> = []
|
|
26
32
|
return {
|
|
@@ -46,7 +52,12 @@ export function createDefaultEnvironment(): Environment {
|
|
|
46
52
|
await mkdir(dirname(path), { recursive: true })
|
|
47
53
|
return writeFile(path, getBinaryFile(base64Contents) as string)
|
|
48
54
|
},
|
|
49
|
-
execute: async (
|
|
55
|
+
execute: async (
|
|
56
|
+
command: string,
|
|
57
|
+
args: Array<string>,
|
|
58
|
+
cwd: string,
|
|
59
|
+
options?: { inherit?: boolean },
|
|
60
|
+
) => {
|
|
50
61
|
try {
|
|
51
62
|
if (options?.inherit) {
|
|
52
63
|
// For commands that should show output directly to the user
|
|
@@ -106,14 +117,7 @@ export function createDefaultEnvironment(): Environment {
|
|
|
106
117
|
export function createMemoryEnvironment(returnPathsRelativeTo: string = '') {
|
|
107
118
|
const environment = createDefaultEnvironment()
|
|
108
119
|
|
|
109
|
-
const output: {
|
|
110
|
-
files: Record<string, string>
|
|
111
|
-
deletedFiles: Array<string>
|
|
112
|
-
commands: Array<{
|
|
113
|
-
command: string
|
|
114
|
-
args: Array<string>
|
|
115
|
-
}>
|
|
116
|
-
} = {
|
|
120
|
+
const output: MemoryEnvironmentOutput = {
|
|
117
121
|
files: {},
|
|
118
122
|
commands: [],
|
|
119
123
|
deletedFiles: [],
|
package/src/file-helpers.ts
CHANGED
|
@@ -36,6 +36,16 @@ export function getBinaryFile(content: string): string | null {
|
|
|
36
36
|
return null
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Convert an absolute path to a clean relative path by removing a base directory.
|
|
41
|
+
* Returns a path without leading ./ or / prefix.
|
|
42
|
+
*/
|
|
43
|
+
export function toCleanPath(absolutePath: string, baseDir: string): string {
|
|
44
|
+
let cleanPath = absolutePath.replace(baseDir, '')
|
|
45
|
+
if (cleanPath.startsWith('/')) cleanPath = cleanPath.slice(1)
|
|
46
|
+
return cleanPath
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
export function relativePath(
|
|
40
50
|
from: string,
|
|
41
51
|
to: string,
|
|
@@ -122,7 +132,7 @@ async function recursivelyGatherFilesHelper(
|
|
|
122
132
|
)
|
|
123
133
|
} else {
|
|
124
134
|
const filePath = resolve(path, file.name)
|
|
125
|
-
files[filePath
|
|
135
|
+
files[toCleanPath(filePath, basePath)] = await readFileHelper(filePath)
|
|
126
136
|
}
|
|
127
137
|
}
|
|
128
138
|
}
|
|
@@ -159,7 +169,7 @@ async function recursivelyGatherFilesFromEnvironmentHelper(
|
|
|
159
169
|
)
|
|
160
170
|
} else {
|
|
161
171
|
const filePath = resolve(path, file)
|
|
162
|
-
files[filePath
|
|
172
|
+
files[toCleanPath(filePath, basePath)] =
|
|
163
173
|
await environment.readFile(filePath)
|
|
164
174
|
}
|
|
165
175
|
}
|
|
@@ -232,7 +242,7 @@ export function cleanUpFiles(
|
|
|
232
242
|
) {
|
|
233
243
|
return Object.keys(files).reduce<Record<string, string>>((acc, file) => {
|
|
234
244
|
if (basename(file) !== '.cta.json') {
|
|
235
|
-
acc[targetDir ? file
|
|
245
|
+
acc[targetDir ? toCleanPath(file, targetDir) : file] = files[file]
|
|
236
246
|
}
|
|
237
247
|
return acc
|
|
238
248
|
}, {})
|
|
@@ -241,7 +251,7 @@ export function cleanUpFiles(
|
|
|
241
251
|
export function cleanUpFileArray(files: Array<string>, targetDir?: string) {
|
|
242
252
|
return files.reduce<Array<string>>((acc, file) => {
|
|
243
253
|
if (basename(file) !== '.cta.json') {
|
|
244
|
-
acc.push(targetDir ? file
|
|
254
|
+
acc.push(targetDir ? toCleanPath(file, targetDir) : file)
|
|
245
255
|
}
|
|
246
256
|
return acc
|
|
247
257
|
}, [])
|
package/src/frameworks.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
findFilesRecursively,
|
|
6
6
|
isDirectory,
|
|
7
7
|
readFileHelper,
|
|
8
|
+
toCleanPath,
|
|
8
9
|
} from './file-helpers.js'
|
|
9
10
|
|
|
10
11
|
import type { AddOn, Framework, FrameworkDefinition } from './types.js'
|
|
@@ -20,7 +21,7 @@ export function scanProjectDirectory(
|
|
|
20
21
|
|
|
21
22
|
const files = Object.keys(absolutePaths).reduce(
|
|
22
23
|
(acc, path) => {
|
|
23
|
-
acc[path
|
|
24
|
+
acc[toCleanPath(path, baseDirectory)] = absolutePaths[path]
|
|
24
25
|
return acc
|
|
25
26
|
},
|
|
26
27
|
{} as Record<string, string>,
|
|
@@ -59,13 +60,16 @@ export function scanAddOnDirectories(addOnsDirectories: Array<string>) {
|
|
|
59
60
|
|
|
60
61
|
let packageAdditions: Record<string, any> = {}
|
|
61
62
|
let packageTemplate: string | undefined = undefined
|
|
62
|
-
|
|
63
|
+
|
|
63
64
|
if (existsSync(resolve(addOnsBase, dir, 'package.json'))) {
|
|
64
65
|
packageAdditions = JSON.parse(
|
|
65
66
|
readFileSync(resolve(addOnsBase, dir, 'package.json'), 'utf-8'),
|
|
66
67
|
)
|
|
67
68
|
} else if (existsSync(resolve(addOnsBase, dir, 'package.json.ejs'))) {
|
|
68
|
-
packageTemplate = readFileSync(
|
|
69
|
+
packageTemplate = readFileSync(
|
|
70
|
+
resolve(addOnsBase, dir, 'package.json.ejs'),
|
|
71
|
+
'utf-8',
|
|
72
|
+
)
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
let readme: string | undefined
|
|
@@ -88,7 +92,7 @@ export function scanAddOnDirectories(addOnsDirectories: Array<string>) {
|
|
|
88
92
|
}
|
|
89
93
|
const files: Record<string, string> = {}
|
|
90
94
|
for (const file of Object.keys(absoluteFiles)) {
|
|
91
|
-
files[file
|
|
95
|
+
files[toCleanPath(file, assetsDir)] = readFileHelper(file)
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
const getFiles = () => {
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
export { createApp } from './create-app.js'
|
|
2
|
+
export { computeAttribution } from './attribution.js'
|
|
2
3
|
export { addToApp } from './add-to-app.js'
|
|
3
4
|
|
|
4
|
-
export {
|
|
5
|
+
export {
|
|
6
|
+
finalizeAddOns,
|
|
7
|
+
getAllAddOns,
|
|
8
|
+
populateAddOnOptionsDefaults,
|
|
9
|
+
} from './add-ons.js'
|
|
5
10
|
|
|
6
11
|
export { loadRemoteAddOn } from './custom-add-ons/add-on.js'
|
|
7
12
|
export { loadStarter } from './custom-add-ons/starter.js'
|
|
@@ -41,6 +46,7 @@ export {
|
|
|
41
46
|
getBinaryFile,
|
|
42
47
|
recursivelyGatherFiles,
|
|
43
48
|
relativePath,
|
|
49
|
+
toCleanPath,
|
|
44
50
|
} from './file-helpers.js'
|
|
45
51
|
|
|
46
52
|
export { formatCommand, handleSpecialURL } from './utils.js'
|
|
@@ -85,6 +91,12 @@ export type {
|
|
|
85
91
|
SerializedOptions,
|
|
86
92
|
Starter,
|
|
87
93
|
StarterCompiled,
|
|
94
|
+
LineAttribution,
|
|
95
|
+
FileProvenance,
|
|
96
|
+
AttributedFile,
|
|
97
|
+
DependencyAttribution,
|
|
88
98
|
} from './types.js'
|
|
99
|
+
export type { AttributionInput, AttributionOutput } from './attribution.js'
|
|
100
|
+
export type { MemoryEnvironmentOutput } from './environment.js'
|
|
89
101
|
export type { PersistedOptions } from './config-file.js'
|
|
90
102
|
export type { PackageManager } from './package-manager.js'
|
package/src/template-file.ts
CHANGED
|
@@ -9,7 +9,13 @@ import {
|
|
|
9
9
|
} from './package-manager.js'
|
|
10
10
|
import { relativePath } from './file-helpers.js'
|
|
11
11
|
|
|
12
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
AddOn,
|
|
14
|
+
Environment,
|
|
15
|
+
Integration,
|
|
16
|
+
IntegrationWithSource,
|
|
17
|
+
Options,
|
|
18
|
+
} from './types.js'
|
|
13
19
|
|
|
14
20
|
function convertDotFilesAndPaths(path: string) {
|
|
15
21
|
return path
|
|
@@ -50,11 +56,16 @@ export function createTemplateFile(environment: Environment, options: Options) {
|
|
|
50
56
|
}
|
|
51
57
|
}
|
|
52
58
|
|
|
53
|
-
|
|
59
|
+
// Collect integrations and tag them with source add-on for attribution
|
|
60
|
+
const integrations: Array<IntegrationWithSource> = []
|
|
54
61
|
for (const addOn of options.chosenAddOns) {
|
|
55
62
|
if (addOn.integrations) {
|
|
56
63
|
for (const integration of addOn.integrations) {
|
|
57
|
-
integrations.push(
|
|
64
|
+
integrations.push({
|
|
65
|
+
...integration,
|
|
66
|
+
_sourceId: addOn.id,
|
|
67
|
+
_sourceName: addOn.name,
|
|
68
|
+
})
|
|
58
69
|
}
|
|
59
70
|
}
|
|
60
71
|
}
|
package/src/types.ts
CHANGED
|
@@ -39,6 +39,26 @@ export const AddOnBaseSchema = z.object({
|
|
|
39
39
|
warning: z.string().optional(),
|
|
40
40
|
tailwind: z.boolean().optional().default(true),
|
|
41
41
|
type: z.enum(['add-on', 'example', 'starter', 'toolchain', 'deployment']),
|
|
42
|
+
category: z
|
|
43
|
+
.enum([
|
|
44
|
+
'tanstack',
|
|
45
|
+
'database',
|
|
46
|
+
'orm',
|
|
47
|
+
'auth',
|
|
48
|
+
'deploy',
|
|
49
|
+
'styling',
|
|
50
|
+
'monitoring',
|
|
51
|
+
'cms',
|
|
52
|
+
'api',
|
|
53
|
+
'i18n',
|
|
54
|
+
'tooling',
|
|
55
|
+
'other',
|
|
56
|
+
])
|
|
57
|
+
.optional(),
|
|
58
|
+
exclusive: z
|
|
59
|
+
.array(z.enum(['orm', 'auth', 'deploy', 'database', 'linter']))
|
|
60
|
+
.optional(),
|
|
61
|
+
color: z.string().optional(),
|
|
42
62
|
priority: z.number().optional(),
|
|
43
63
|
command: z
|
|
44
64
|
.object({
|
|
@@ -256,3 +276,37 @@ type UIEnvironment = {
|
|
|
256
276
|
}
|
|
257
277
|
|
|
258
278
|
export type Environment = ProjectEnvironment & FileEnvironment & UIEnvironment
|
|
279
|
+
|
|
280
|
+
// Attribution tracking types for file provenance
|
|
281
|
+
export interface LineAttribution {
|
|
282
|
+
line: number
|
|
283
|
+
sourceId: string
|
|
284
|
+
sourceName: string
|
|
285
|
+
type: 'original' | 'injected'
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export interface FileProvenance {
|
|
289
|
+
source: 'framework' | 'add-on' | 'starter'
|
|
290
|
+
sourceId: string
|
|
291
|
+
sourceName: string
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export interface AttributedFile {
|
|
295
|
+
content: string
|
|
296
|
+
provenance: FileProvenance
|
|
297
|
+
lineAttributions: Array<LineAttribution>
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export interface DependencyAttribution {
|
|
301
|
+
name: string
|
|
302
|
+
version: string
|
|
303
|
+
type: 'dependency' | 'devDependency'
|
|
304
|
+
sourceId: string
|
|
305
|
+
sourceName: string
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Integration with source add-on tracking (used in templates and attribution)
|
|
309
|
+
export type IntegrationWithSource = Integration & {
|
|
310
|
+
_sourceId: string
|
|
311
|
+
_sourceName: string
|
|
312
|
+
}
|
package/tests/add-to-app.test.ts
CHANGED
|
@@ -34,7 +34,7 @@ const fakeCTAJSON: PersistedOptions = {
|
|
|
34
34
|
|
|
35
35
|
beforeEach(() => {
|
|
36
36
|
const fakeFiles = {
|
|
37
|
-
'
|
|
37
|
+
'package.json': JSON.stringify({
|
|
38
38
|
name: 'test',
|
|
39
39
|
version: '1.0.0',
|
|
40
40
|
dependencies: {},
|
|
@@ -71,7 +71,7 @@ beforeEach(() => {
|
|
|
71
71
|
},
|
|
72
72
|
},
|
|
73
73
|
dependsOn: [],
|
|
74
|
-
getFiles: () => Promise.resolve(['
|
|
74
|
+
getFiles: () => Promise.resolve(['jack.txt']),
|
|
75
75
|
getFileContents: () => Promise.resolve('foo'),
|
|
76
76
|
getDeletedFiles: () => Promise.resolve([]),
|
|
77
77
|
},
|
|
@@ -152,8 +152,8 @@ describe('writeFiles', () => {
|
|
|
152
152
|
'/foo',
|
|
153
153
|
{
|
|
154
154
|
files: {
|
|
155
|
-
'
|
|
156
|
-
'
|
|
155
|
+
'bar.txt': 'baz',
|
|
156
|
+
'blarg.txt': 'blarg',
|
|
157
157
|
},
|
|
158
158
|
deletedFiles: [],
|
|
159
159
|
},
|
|
@@ -177,8 +177,8 @@ describe('writeFiles', () => {
|
|
|
177
177
|
'/foo',
|
|
178
178
|
{
|
|
179
179
|
files: {
|
|
180
|
-
'
|
|
181
|
-
'
|
|
180
|
+
'bar.txt': 'baz',
|
|
181
|
+
'blarg.txt': 'blarg',
|
|
182
182
|
},
|
|
183
183
|
deletedFiles: [],
|
|
184
184
|
},
|
|
@@ -186,9 +186,9 @@ describe('writeFiles', () => {
|
|
|
186
186
|
)
|
|
187
187
|
environment.finishRun()
|
|
188
188
|
expect(output.files).toEqual({
|
|
189
|
-
'
|
|
190
|
-
'
|
|
191
|
-
'
|
|
189
|
+
'blooop.txt': 'blooop',
|
|
190
|
+
'bar.txt': 'baz',
|
|
191
|
+
'blarg.txt': 'blarg',
|
|
192
192
|
})
|
|
193
193
|
})
|
|
194
194
|
|
|
@@ -203,9 +203,9 @@ describe('writeFiles', () => {
|
|
|
203
203
|
'/foo',
|
|
204
204
|
{
|
|
205
205
|
files: {
|
|
206
|
-
'
|
|
207
|
-
'
|
|
208
|
-
'
|
|
206
|
+
'unchanged.jpg': 'base64::foobaz',
|
|
207
|
+
'changing.jpg': 'base64::aGVsbG8=',
|
|
208
|
+
'new.jpg': 'base64::aGVsbG8=',
|
|
209
209
|
},
|
|
210
210
|
deletedFiles: [],
|
|
211
211
|
},
|
|
@@ -214,9 +214,9 @@ describe('writeFiles', () => {
|
|
|
214
214
|
environment.finishRun()
|
|
215
215
|
// It's ok for unchanged.jpg not to be written, because it matches the existing file
|
|
216
216
|
expect(output.files).toEqual({
|
|
217
|
-
'
|
|
218
|
-
'
|
|
219
|
-
'
|
|
217
|
+
'unchanged.jpg': 'base64::foobaz',
|
|
218
|
+
'changing.jpg': 'base64::aGVsbG8=',
|
|
219
|
+
'new.jpg': 'base64::aGVsbG8=',
|
|
220
220
|
})
|
|
221
221
|
})
|
|
222
222
|
|
|
@@ -245,7 +245,7 @@ describe('writeFiles', () => {
|
|
|
245
245
|
'/foo',
|
|
246
246
|
{
|
|
247
247
|
files: {
|
|
248
|
-
'
|
|
248
|
+
'package.json': JSON.stringify(
|
|
249
249
|
{
|
|
250
250
|
scripts: {
|
|
251
251
|
test: 'echo "test"',
|
|
@@ -264,7 +264,7 @@ describe('writeFiles', () => {
|
|
|
264
264
|
)
|
|
265
265
|
environment.finishRun()
|
|
266
266
|
expect(output.files).toEqual({
|
|
267
|
-
'
|
|
267
|
+
'package.json': JSON.stringify(
|
|
268
268
|
{
|
|
269
269
|
name: 'test',
|
|
270
270
|
version: '1.0.0',
|
|
@@ -291,11 +291,11 @@ describe('writeFiles', () => {
|
|
|
291
291
|
await writeFiles(
|
|
292
292
|
environment,
|
|
293
293
|
'/foo',
|
|
294
|
-
{ files: {}, deletedFiles: ['
|
|
294
|
+
{ files: {}, deletedFiles: ['bloop.txt'] },
|
|
295
295
|
true,
|
|
296
296
|
)
|
|
297
297
|
environment.finishRun()
|
|
298
|
-
expect(output.deletedFiles).toEqual(['
|
|
298
|
+
expect(output.deletedFiles).toEqual(['bloop.txt'])
|
|
299
299
|
})
|
|
300
300
|
})
|
|
301
301
|
|
|
@@ -338,8 +338,8 @@ describe('addToApp', () => {
|
|
|
338
338
|
})
|
|
339
339
|
environment.finishRun()
|
|
340
340
|
expect(output.files).toEqual({
|
|
341
|
-
'
|
|
342
|
-
'
|
|
341
|
+
'jack.txt': 'foo',
|
|
342
|
+
'package.json': JSON.stringify(
|
|
343
343
|
{
|
|
344
344
|
name: 'test',
|
|
345
345
|
version: '1.0.0',
|
package/tests/create-app.test.ts
CHANGED
|
@@ -35,7 +35,7 @@ const simpleOptions = {
|
|
|
35
35
|
},
|
|
36
36
|
},
|
|
37
37
|
},
|
|
38
|
-
getFiles: () => ['
|
|
38
|
+
getFiles: () => ['src/test.txt'],
|
|
39
39
|
getFileContents: () => 'Hello',
|
|
40
40
|
getDeletedFiles: () => [],
|
|
41
41
|
},
|
|
@@ -78,7 +78,7 @@ describe('createApp', () => {
|
|
|
78
78
|
command: 'echo',
|
|
79
79
|
args: ['Hello'],
|
|
80
80
|
},
|
|
81
|
-
getFiles: () => ['
|
|
81
|
+
getFiles: () => ['src/test2.txt'],
|
|
82
82
|
getFileContents: () => 'Hello-2',
|
|
83
83
|
getDeletedFiles: () => [],
|
|
84
84
|
} as unknown as AddOn,
|
|
@@ -106,7 +106,7 @@ describe('createApp', () => {
|
|
|
106
106
|
dependencies: {},
|
|
107
107
|
devDependencies: {},
|
|
108
108
|
},
|
|
109
|
-
getFiles: () => ['
|
|
109
|
+
getFiles: () => ['src/test2.txt', 'public/foo.jpg'],
|
|
110
110
|
getFileContents: () => 'base64::aGVsbG8=',
|
|
111
111
|
getDeletedFiles: () => [],
|
|
112
112
|
} as unknown as AddOn,
|