@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.
@@ -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
- return [key.replace(targetDir, '.'), value]
122
- }),
120
+ Object.entries(output.files).map(([key, value]) => [
121
+ toCleanPath(key, targetDir),
122
+ value,
123
+ ]),
123
124
  )
124
125
 
125
126
  return output
@@ -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 (command: string, args: Array<string>, cwd: string, options?: { inherit?: boolean }) => {
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: [],
@@ -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.replace(basePath, '.')] = await readFileHelper(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.replace(basePath, '.')] =
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.replace(targetDir, '.') : file] = files[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.replace(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.replace(baseDirectory, '.')] = absolutePaths[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(resolve(addOnsBase, dir, 'package.json.ejs'), 'utf-8')
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.replace(assetsDir, '.')] = readFileHelper(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 { finalizeAddOns, getAllAddOns, populateAddOnOptionsDefaults } from './add-ons.js'
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'
@@ -9,7 +9,13 @@ import {
9
9
  } from './package-manager.js'
10
10
  import { relativePath } from './file-helpers.js'
11
11
 
12
- import type { AddOn, Environment, Integration, Options } from './types.js'
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
- const integrations: Array<Required<AddOn>['integrations'][number]> = []
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(integration)
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
+ }
@@ -34,7 +34,7 @@ const fakeCTAJSON: PersistedOptions = {
34
34
 
35
35
  beforeEach(() => {
36
36
  const fakeFiles = {
37
- './package.json': JSON.stringify({
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(['./jack.txt']),
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
- './bar.txt': 'baz',
156
- './blarg.txt': 'blarg',
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
- './bar.txt': 'baz',
181
- './blarg.txt': 'blarg',
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
- './blooop.txt': 'blooop',
190
- './bar.txt': 'baz',
191
- './blarg.txt': 'blarg',
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
- './unchanged.jpg': 'base64::foobaz',
207
- './changing.jpg': 'base64::aGVsbG8=',
208
- './new.jpg': 'base64::aGVsbG8=',
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
- './unchanged.jpg': 'base64::foobaz',
218
- './changing.jpg': 'base64::aGVsbG8=',
219
- './new.jpg': 'base64::aGVsbG8=',
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
- './package.json': JSON.stringify(
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
- './package.json': JSON.stringify(
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: ['./bloop.txt'] },
294
+ { files: {}, deletedFiles: ['bloop.txt'] },
295
295
  true,
296
296
  )
297
297
  environment.finishRun()
298
- expect(output.deletedFiles).toEqual(['./bloop.txt'])
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
- './jack.txt': 'foo',
342
- './package.json': JSON.stringify(
341
+ 'jack.txt': 'foo',
342
+ 'package.json': JSON.stringify(
343
343
  {
344
344
  name: 'test',
345
345
  version: '1.0.0',
@@ -35,7 +35,7 @@ const simpleOptions = {
35
35
  },
36
36
  },
37
37
  },
38
- getFiles: () => ['./src/test.txt'],
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: () => ['./src/test2.txt'],
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: () => ['./src/test2.txt', './public/foo.jpg'],
109
+ getFiles: () => ['src/test2.txt', 'public/foo.jpg'],
110
110
  getFileContents: () => 'base64::aGVsbG8=',
111
111
  getDeletedFiles: () => [],
112
112
  } as unknown as AddOn,