@tanstack/cta-engine 0.48.0 → 0.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @tanstack/cta-engine
2
2
 
3
+ ## 0.49.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Added categories, colors, exclusive tagging, addon file attribution utils, and railway addon ([#303](https://github.com/TanStack/create-tsrouter-app/pull/303))
8
+
3
9
  ## 0.48.0
4
10
 
5
11
  ### Minor Changes
@@ -0,0 +1,162 @@
1
+ function normalizePath(path) {
2
+ let p = path.startsWith('./') ? path.slice(2) : path;
3
+ p = p.replace(/\.ejs$/, '').replace(/_dot_/g, '.');
4
+ const match = p.match(/^(.+\/)?__([^_]+)__(.+)$/);
5
+ return match ? (match[1] || '') + match[3] : p;
6
+ }
7
+ async function getFileProvenance(filePath, framework, addOns, starter) {
8
+ const target = filePath.startsWith('./') ? filePath.slice(2) : filePath;
9
+ if (starter) {
10
+ const files = await starter.getFiles();
11
+ if (files.some((f) => normalizePath(f) === target)) {
12
+ return {
13
+ source: 'starter',
14
+ sourceId: starter.id,
15
+ sourceName: starter.name,
16
+ };
17
+ }
18
+ }
19
+ // Order add-ons by type then phase (matches writeFiles order), check in reverse
20
+ const typeOrder = ['add-on', 'example', 'toolchain', 'deployment'];
21
+ const phaseOrder = ['setup', 'add-on', 'example'];
22
+ const ordered = typeOrder.flatMap((type) => phaseOrder.flatMap((phase) => addOns.filter((a) => a.phase === phase && a.type === type)));
23
+ for (let i = ordered.length - 1; i >= 0; i--) {
24
+ const files = await ordered[i].getFiles();
25
+ if (files.some((f) => normalizePath(f) === target)) {
26
+ return {
27
+ source: 'add-on',
28
+ sourceId: ordered[i].id,
29
+ sourceName: ordered[i].name,
30
+ };
31
+ }
32
+ }
33
+ const frameworkFiles = await framework.getFiles();
34
+ if (frameworkFiles.some((f) => normalizePath(f) === target)) {
35
+ return {
36
+ source: 'framework',
37
+ sourceId: framework.id,
38
+ sourceName: framework.name,
39
+ };
40
+ }
41
+ return null;
42
+ }
43
+ // Build injection patterns from integrations (for source files)
44
+ function integrationInjections(int) {
45
+ const source = { sourceId: int._sourceId, sourceName: int._sourceName };
46
+ const injections = [];
47
+ const appliesTo = (path) => {
48
+ if (int.type === 'vite-plugin')
49
+ return path.includes('vite.config');
50
+ if (int.type === 'provider' ||
51
+ int.type === 'root-provider' ||
52
+ int.type === 'devtools') {
53
+ return path.includes('__root') || path.includes('root.tsx');
54
+ }
55
+ return false;
56
+ };
57
+ if (int.import) {
58
+ const prefix = int.import.split(' from ')[0];
59
+ injections.push({
60
+ matches: (line) => line.includes(prefix),
61
+ appliesTo,
62
+ source,
63
+ });
64
+ }
65
+ const code = int.code || int.jsName;
66
+ if (code) {
67
+ injections.push({
68
+ matches: (line) => line.includes(code),
69
+ appliesTo,
70
+ source,
71
+ });
72
+ }
73
+ return injections;
74
+ }
75
+ // Build injection pattern from a dependency (for package.json)
76
+ function dependencyInjection(dep) {
77
+ return {
78
+ matches: (line) => line.includes(`"${dep.name}"`),
79
+ appliesTo: (path) => path.endsWith('package.json'),
80
+ source: { sourceId: dep.sourceId, sourceName: dep.sourceName },
81
+ };
82
+ }
83
+ export async function computeAttribution(input) {
84
+ const { framework, chosenAddOns, starter, files } = input;
85
+ // Collect integrations tagged with source
86
+ const integrations = chosenAddOns.flatMap((addOn) => (addOn.integrations || []).map((int) => ({
87
+ ...int,
88
+ _sourceId: addOn.id,
89
+ _sourceName: addOn.name,
90
+ })));
91
+ // Collect dependencies from add-ons (from packageAdditions or packageTemplate)
92
+ const dependencies = chosenAddOns.flatMap((addOn) => {
93
+ const result = [];
94
+ const source = { sourceId: addOn.id, sourceName: addOn.name };
95
+ const addDeps = (deps, type) => {
96
+ if (!deps)
97
+ return;
98
+ for (const [name, version] of Object.entries(deps)) {
99
+ if (typeof version === 'string') {
100
+ result.push({ name, version, type, ...source });
101
+ }
102
+ }
103
+ };
104
+ // From static package.json
105
+ addDeps(addOn.packageAdditions?.dependencies, 'dependency');
106
+ addDeps(addOn.packageAdditions?.devDependencies, 'devDependency');
107
+ // From package.json.ejs template (strip EJS tags and parse)
108
+ if (addOn.packageTemplate) {
109
+ try {
110
+ const tmpl = JSON.parse(addOn.packageTemplate.replace(/"[^"]*<%[^%]*%>[^"]*"/g, '""'));
111
+ addDeps(tmpl.dependencies, 'dependency');
112
+ addDeps(tmpl.devDependencies, 'devDependency');
113
+ }
114
+ catch { }
115
+ }
116
+ return result;
117
+ });
118
+ // Build unified injection patterns from both integrations and dependencies
119
+ const injections = [
120
+ ...integrations.flatMap(integrationInjections),
121
+ ...dependencies.map(dependencyInjection),
122
+ ];
123
+ const attributedFiles = {};
124
+ for (const [filePath, content] of Object.entries(files)) {
125
+ const provenance = await getFileProvenance(filePath, framework, chosenAddOns, starter);
126
+ if (!provenance)
127
+ continue;
128
+ const lines = content.split('\n');
129
+ const relevant = injections.filter((inj) => inj.appliesTo(filePath));
130
+ // Find injected lines
131
+ const injectedLines = new Map();
132
+ for (const inj of relevant) {
133
+ lines.forEach((line, i) => {
134
+ if (inj.matches(line) && !injectedLines.has(i + 1)) {
135
+ injectedLines.set(i + 1, inj.source);
136
+ }
137
+ });
138
+ }
139
+ attributedFiles[filePath] = {
140
+ content,
141
+ provenance,
142
+ lineAttributions: lines.map((_, i) => {
143
+ const lineNum = i + 1;
144
+ const inj = injectedLines.get(lineNum);
145
+ return inj
146
+ ? {
147
+ line: lineNum,
148
+ sourceId: inj.sourceId,
149
+ sourceName: inj.sourceName,
150
+ type: 'injected',
151
+ }
152
+ : {
153
+ line: lineNum,
154
+ sourceId: provenance.sourceId,
155
+ sourceName: provenance.sourceName,
156
+ type: 'original',
157
+ };
158
+ }),
159
+ };
160
+ }
161
+ return { attributedFiles, dependencies };
162
+ }
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { createApp } from './create-app.js';
2
+ export { computeAttribution } from './attribution.js';
2
3
  export { addToApp } from './add-to-app.js';
3
- export { finalizeAddOns, getAllAddOns, populateAddOnOptionsDefaults } from './add-ons.js';
4
+ export { finalizeAddOns, getAllAddOns, populateAddOnOptionsDefaults, } from './add-ons.js';
4
5
  export { loadRemoteAddOn } from './custom-add-ons/add-on.js';
5
6
  export { loadStarter } from './custom-add-ons/starter.js';
6
7
  export { createMemoryEnvironment, createDefaultEnvironment, } from './environment.js';
@@ -26,11 +26,16 @@ export function createTemplateFile(environment, options) {
26
26
  this.name = 'IgnoreFileError';
27
27
  }
28
28
  }
29
+ // Collect integrations and tag them with source add-on for attribution
29
30
  const integrations = [];
30
31
  for (const addOn of options.chosenAddOns) {
31
32
  if (addOn.integrations) {
32
33
  for (const integration of addOn.integrations) {
33
- integrations.push(integration);
34
+ integrations.push({
35
+ ...integration,
36
+ _sourceId: addOn.id,
37
+ _sourceName: addOn.name,
38
+ });
34
39
  }
35
40
  }
36
41
  }
@@ -0,0 +1,12 @@
1
+ import type { AddOn, AttributedFile, DependencyAttribution, Framework, Starter } from './types.js';
2
+ export interface AttributionInput {
3
+ framework: Framework;
4
+ chosenAddOns: Array<AddOn>;
5
+ starter?: Starter;
6
+ files: Record<string, string>;
7
+ }
8
+ export interface AttributionOutput {
9
+ attributedFiles: Record<string, AttributedFile>;
10
+ dependencies: Array<DependencyAttribution>;
11
+ }
12
+ export declare function computeAttribution(input: AttributionInput): Promise<AttributionOutput>;
@@ -39,6 +39,9 @@ export declare function generateProject(persistedOptions: PersistedOptions): Pro
39
39
  link?: string | undefined;
40
40
  license?: string | undefined;
41
41
  warning?: string | undefined;
42
+ category?: "other" | "tanstack" | "database" | "orm" | "auth" | "deploy" | "styling" | "monitoring" | "cms" | "api" | "i18n" | "tooling" | undefined;
43
+ exclusive?: ("database" | "orm" | "auth" | "deploy" | "linter")[] | undefined;
44
+ color?: string | undefined;
42
45
  priority?: number | undefined;
43
46
  routes?: {
44
47
  path: string;
@@ -67,14 +70,7 @@ export declare function generateProject(persistedOptions: PersistedOptions): Pro
67
70
  }[] | undefined;
68
71
  readme?: string | undefined;
69
72
  };
70
- output: {
71
- files: Record<string, string>;
72
- deletedFiles: Array<string>;
73
- commands: Array<{
74
- command: string;
75
- args: Array<string>;
76
- }>;
77
- };
73
+ output: import("../environment.js").MemoryEnvironmentOutput;
78
74
  }>;
79
75
  export declare function buildAssetsDirectory(output: {
80
76
  files: Record<string, string>;
@@ -3,13 +3,6 @@ import type { PersistedOptions } from '../config-file.js';
3
3
  export declare function createPackageAdditions(originalPackageJson: Record<string, any>, currentPackageJson: Record<string, any>): Record<string, any>;
4
4
  export declare function createAppOptionsFromPersisted(json: PersistedOptions): Promise<Options>;
5
5
  export declare function createSerializedOptionsFromPersisted(json: PersistedOptions): SerializedOptions;
6
- export declare function runCreateApp(options: Required<Options>): Promise<{
7
- files: Record<string, string>;
8
- deletedFiles: Array<string>;
9
- commands: Array<{
10
- command: string;
11
- args: Array<string>;
12
- }>;
13
- }>;
6
+ export declare function runCreateApp(options: Required<Options>): Promise<import("../environment.js").MemoryEnvironmentOutput>;
14
7
  export declare function compareFilesRecursively(path: string, ignore: (filePath: string) => boolean, original: Record<string, string>, changedFiles: Record<string, string>): Promise<void>;
15
8
  export declare function readCurrentProjectOptions(environment: Environment): Promise<PersistedOptions>;
@@ -1,13 +1,14 @@
1
1
  import type { Environment } from './types.js';
2
+ export interface MemoryEnvironmentOutput {
3
+ files: Record<string, string>;
4
+ deletedFiles: Array<string>;
5
+ commands: Array<{
6
+ command: string;
7
+ args: Array<string>;
8
+ }>;
9
+ }
2
10
  export declare function createDefaultEnvironment(): Environment;
3
11
  export declare function createMemoryEnvironment(returnPathsRelativeTo?: string): {
4
12
  environment: Environment;
5
- output: {
6
- files: Record<string, string>;
7
- deletedFiles: Array<string>;
8
- commands: Array<{
9
- command: string;
10
- args: Array<string>;
11
- }>;
12
- };
13
+ output: MemoryEnvironmentOutput;
13
14
  };
@@ -1,6 +1,7 @@
1
1
  export { createApp } from './create-app.js';
2
+ export { computeAttribution } from './attribution.js';
2
3
  export { addToApp } from './add-to-app.js';
3
- export { finalizeAddOns, getAllAddOns, populateAddOnOptionsDefaults } from './add-ons.js';
4
+ export { finalizeAddOns, getAllAddOns, populateAddOnOptionsDefaults, } from './add-ons.js';
4
5
  export { loadRemoteAddOn } from './custom-add-ons/add-on.js';
5
6
  export { loadStarter } from './custom-add-ons/starter.js';
6
7
  export { createMemoryEnvironment, createDefaultEnvironment, } from './environment.js';
@@ -16,6 +17,8 @@ export { createAppOptionsFromPersisted, createSerializedOptionsFromPersisted, }
16
17
  export { createSerializedOptions } from './options.js';
17
18
  export { getRawRegistry, getRegistry, getRegistryAddOns, getRegistryStarters, } from './registry.js';
18
19
  export { StarterCompiledSchema, StatusEvent, StatusStepType, StopEvent, AddOnCompiledSchema, AddOnInfoSchema, IntegrationSchema, } from './types.js';
19
- export type { AddOn, AddOnOption, AddOnOptions, AddOnSelectOption, AddOnSelection, Environment, FileBundleHandler, Framework, FrameworkDefinition, Options, SerializedOptions, Starter, StarterCompiled, } from './types.js';
20
+ export type { AddOn, AddOnOption, AddOnOptions, AddOnSelectOption, AddOnSelection, Environment, FileBundleHandler, Framework, FrameworkDefinition, Options, SerializedOptions, Starter, StarterCompiled, LineAttribution, FileProvenance, AttributedFile, DependencyAttribution, } from './types.js';
21
+ export type { AttributionInput, AttributionOutput } from './attribution.js';
22
+ export type { MemoryEnvironmentOutput } from './environment.js';
20
23
  export type { PersistedOptions } from './config-file.js';
21
24
  export type { PackageManager } from './package-manager.js';
@@ -114,6 +114,9 @@ export declare const AddOnBaseSchema: z.ZodObject<{
114
114
  warning: z.ZodOptional<z.ZodString>;
115
115
  tailwind: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
116
116
  type: z.ZodEnum<["add-on", "example", "starter", "toolchain", "deployment"]>;
117
+ category: z.ZodOptional<z.ZodEnum<["tanstack", "database", "orm", "auth", "deploy", "styling", "monitoring", "cms", "api", "i18n", "tooling", "other"]>>;
118
+ exclusive: z.ZodOptional<z.ZodArray<z.ZodEnum<["orm", "auth", "deploy", "database", "linter"]>, "many">>;
119
+ color: z.ZodOptional<z.ZodString>;
117
120
  priority: z.ZodOptional<z.ZodNumber>;
118
121
  command: z.ZodOptional<z.ZodObject<{
119
122
  command: z.ZodString;
@@ -222,6 +225,9 @@ export declare const AddOnBaseSchema: z.ZodObject<{
222
225
  link?: string | undefined;
223
226
  license?: string | undefined;
224
227
  warning?: string | undefined;
228
+ category?: "other" | "tanstack" | "database" | "orm" | "auth" | "deploy" | "styling" | "monitoring" | "cms" | "api" | "i18n" | "tooling" | undefined;
229
+ exclusive?: ("database" | "orm" | "auth" | "deploy" | "linter")[] | undefined;
230
+ color?: string | undefined;
225
231
  priority?: number | undefined;
226
232
  routes?: {
227
233
  path: string;
@@ -267,6 +273,9 @@ export declare const AddOnBaseSchema: z.ZodObject<{
267
273
  license?: string | undefined;
268
274
  warning?: string | undefined;
269
275
  tailwind?: boolean | undefined;
276
+ category?: "other" | "tanstack" | "database" | "orm" | "auth" | "deploy" | "styling" | "monitoring" | "cms" | "api" | "i18n" | "tooling" | undefined;
277
+ exclusive?: ("database" | "orm" | "auth" | "deploy" | "linter")[] | undefined;
278
+ color?: string | undefined;
270
279
  priority?: number | undefined;
271
280
  routes?: {
272
281
  path: string;
@@ -297,6 +306,9 @@ export declare const StarterSchema: z.ZodObject<{
297
306
  license: z.ZodOptional<z.ZodString>;
298
307
  warning: z.ZodOptional<z.ZodString>;
299
308
  type: z.ZodEnum<["add-on", "example", "starter", "toolchain", "deployment"]>;
309
+ category: z.ZodOptional<z.ZodEnum<["tanstack", "database", "orm", "auth", "deploy", "styling", "monitoring", "cms", "api", "i18n", "tooling", "other"]>>;
310
+ exclusive: z.ZodOptional<z.ZodArray<z.ZodEnum<["orm", "auth", "deploy", "database", "linter"]>, "many">>;
311
+ color: z.ZodOptional<z.ZodString>;
300
312
  priority: z.ZodOptional<z.ZodNumber>;
301
313
  command: z.ZodOptional<z.ZodObject<{
302
314
  command: z.ZodString;
@@ -414,6 +426,9 @@ export declare const StarterSchema: z.ZodObject<{
414
426
  link?: string | undefined;
415
427
  license?: string | undefined;
416
428
  warning?: string | undefined;
429
+ category?: "other" | "tanstack" | "database" | "orm" | "auth" | "deploy" | "styling" | "monitoring" | "cms" | "api" | "i18n" | "tooling" | undefined;
430
+ exclusive?: ("database" | "orm" | "auth" | "deploy" | "linter")[] | undefined;
431
+ color?: string | undefined;
417
432
  priority?: number | undefined;
418
433
  routes?: {
419
434
  path: string;
@@ -463,6 +478,9 @@ export declare const StarterSchema: z.ZodObject<{
463
478
  link?: string | undefined;
464
479
  license?: string | undefined;
465
480
  warning?: string | undefined;
481
+ category?: "other" | "tanstack" | "database" | "orm" | "auth" | "deploy" | "styling" | "monitoring" | "cms" | "api" | "i18n" | "tooling" | undefined;
482
+ exclusive?: ("database" | "orm" | "auth" | "deploy" | "linter")[] | undefined;
483
+ color?: string | undefined;
466
484
  priority?: number | undefined;
467
485
  routes?: {
468
486
  path: string;
@@ -494,6 +512,9 @@ export declare const StarterCompiledSchema: z.ZodObject<{
494
512
  license: z.ZodOptional<z.ZodString>;
495
513
  warning: z.ZodOptional<z.ZodString>;
496
514
  type: z.ZodEnum<["add-on", "example", "starter", "toolchain", "deployment"]>;
515
+ category: z.ZodOptional<z.ZodEnum<["tanstack", "database", "orm", "auth", "deploy", "styling", "monitoring", "cms", "api", "i18n", "tooling", "other"]>>;
516
+ exclusive: z.ZodOptional<z.ZodArray<z.ZodEnum<["orm", "auth", "deploy", "database", "linter"]>, "many">>;
517
+ color: z.ZodOptional<z.ZodString>;
497
518
  priority: z.ZodOptional<z.ZodNumber>;
498
519
  command: z.ZodOptional<z.ZodObject<{
499
520
  command: z.ZodString;
@@ -616,6 +637,9 @@ export declare const StarterCompiledSchema: z.ZodObject<{
616
637
  link?: string | undefined;
617
638
  license?: string | undefined;
618
639
  warning?: string | undefined;
640
+ category?: "other" | "tanstack" | "database" | "orm" | "auth" | "deploy" | "styling" | "monitoring" | "cms" | "api" | "i18n" | "tooling" | undefined;
641
+ exclusive?: ("database" | "orm" | "auth" | "deploy" | "linter")[] | undefined;
642
+ color?: string | undefined;
619
643
  priority?: number | undefined;
620
644
  routes?: {
621
645
  path: string;
@@ -667,6 +691,9 @@ export declare const StarterCompiledSchema: z.ZodObject<{
667
691
  link?: string | undefined;
668
692
  license?: string | undefined;
669
693
  warning?: string | undefined;
694
+ category?: "other" | "tanstack" | "database" | "orm" | "auth" | "deploy" | "styling" | "monitoring" | "cms" | "api" | "i18n" | "tooling" | undefined;
695
+ exclusive?: ("database" | "orm" | "auth" | "deploy" | "linter")[] | undefined;
696
+ color?: string | undefined;
670
697
  priority?: number | undefined;
671
698
  routes?: {
672
699
  path: string;
@@ -718,6 +745,9 @@ export declare const AddOnInfoSchema: z.ZodObject<{
718
745
  warning: z.ZodOptional<z.ZodString>;
719
746
  tailwind: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
720
747
  type: z.ZodEnum<["add-on", "example", "starter", "toolchain", "deployment"]>;
748
+ category: z.ZodOptional<z.ZodEnum<["tanstack", "database", "orm", "auth", "deploy", "styling", "monitoring", "cms", "api", "i18n", "tooling", "other"]>>;
749
+ exclusive: z.ZodOptional<z.ZodArray<z.ZodEnum<["orm", "auth", "deploy", "database", "linter"]>, "many">>;
750
+ color: z.ZodOptional<z.ZodString>;
721
751
  priority: z.ZodOptional<z.ZodNumber>;
722
752
  command: z.ZodOptional<z.ZodObject<{
723
753
  command: z.ZodString;
@@ -851,6 +881,9 @@ export declare const AddOnInfoSchema: z.ZodObject<{
851
881
  link?: string | undefined;
852
882
  license?: string | undefined;
853
883
  warning?: string | undefined;
884
+ category?: "other" | "tanstack" | "database" | "orm" | "auth" | "deploy" | "styling" | "monitoring" | "cms" | "api" | "i18n" | "tooling" | undefined;
885
+ exclusive?: ("database" | "orm" | "auth" | "deploy" | "linter")[] | undefined;
886
+ color?: string | undefined;
854
887
  priority?: number | undefined;
855
888
  routes?: {
856
889
  path: string;
@@ -906,6 +939,9 @@ export declare const AddOnInfoSchema: z.ZodObject<{
906
939
  license?: string | undefined;
907
940
  warning?: string | undefined;
908
941
  tailwind?: boolean | undefined;
942
+ category?: "other" | "tanstack" | "database" | "orm" | "auth" | "deploy" | "styling" | "monitoring" | "cms" | "api" | "i18n" | "tooling" | undefined;
943
+ exclusive?: ("database" | "orm" | "auth" | "deploy" | "linter")[] | undefined;
944
+ color?: string | undefined;
909
945
  priority?: number | undefined;
910
946
  routes?: {
911
947
  path: string;
@@ -945,6 +981,9 @@ export declare const AddOnCompiledSchema: z.ZodObject<{
945
981
  warning: z.ZodOptional<z.ZodString>;
946
982
  tailwind: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
947
983
  type: z.ZodEnum<["add-on", "example", "starter", "toolchain", "deployment"]>;
984
+ category: z.ZodOptional<z.ZodEnum<["tanstack", "database", "orm", "auth", "deploy", "styling", "monitoring", "cms", "api", "i18n", "tooling", "other"]>>;
985
+ exclusive: z.ZodOptional<z.ZodArray<z.ZodEnum<["orm", "auth", "deploy", "database", "linter"]>, "many">>;
986
+ color: z.ZodOptional<z.ZodString>;
948
987
  priority: z.ZodOptional<z.ZodNumber>;
949
988
  command: z.ZodOptional<z.ZodObject<{
950
989
  command: z.ZodString;
@@ -1084,6 +1123,9 @@ export declare const AddOnCompiledSchema: z.ZodObject<{
1084
1123
  link?: string | undefined;
1085
1124
  license?: string | undefined;
1086
1125
  warning?: string | undefined;
1126
+ category?: "other" | "tanstack" | "database" | "orm" | "auth" | "deploy" | "styling" | "monitoring" | "cms" | "api" | "i18n" | "tooling" | undefined;
1127
+ exclusive?: ("database" | "orm" | "auth" | "deploy" | "linter")[] | undefined;
1128
+ color?: string | undefined;
1087
1129
  priority?: number | undefined;
1088
1130
  routes?: {
1089
1131
  path: string;
@@ -1142,6 +1184,9 @@ export declare const AddOnCompiledSchema: z.ZodObject<{
1142
1184
  license?: string | undefined;
1143
1185
  warning?: string | undefined;
1144
1186
  tailwind?: boolean | undefined;
1187
+ category?: "other" | "tanstack" | "database" | "orm" | "auth" | "deploy" | "styling" | "monitoring" | "cms" | "api" | "i18n" | "tooling" | undefined;
1188
+ exclusive?: ("database" | "orm" | "auth" | "deploy" | "linter")[] | undefined;
1189
+ color?: string | undefined;
1145
1190
  priority?: number | undefined;
1146
1191
  routes?: {
1147
1192
  path: string;
@@ -1279,4 +1324,31 @@ type UIEnvironment = {
1279
1324
  confirm: (message: string) => Promise<boolean>;
1280
1325
  };
1281
1326
  export type Environment = ProjectEnvironment & FileEnvironment & UIEnvironment;
1327
+ export interface LineAttribution {
1328
+ line: number;
1329
+ sourceId: string;
1330
+ sourceName: string;
1331
+ type: 'original' | 'injected';
1332
+ }
1333
+ export interface FileProvenance {
1334
+ source: 'framework' | 'add-on' | 'starter';
1335
+ sourceId: string;
1336
+ sourceName: string;
1337
+ }
1338
+ export interface AttributedFile {
1339
+ content: string;
1340
+ provenance: FileProvenance;
1341
+ lineAttributions: Array<LineAttribution>;
1342
+ }
1343
+ export interface DependencyAttribution {
1344
+ name: string;
1345
+ version: string;
1346
+ type: 'dependency' | 'devDependency';
1347
+ sourceId: string;
1348
+ sourceName: string;
1349
+ }
1350
+ export type IntegrationWithSource = Integration & {
1351
+ _sourceId: string;
1352
+ _sourceName: string;
1353
+ };
1282
1354
  export {};
package/dist/types.js CHANGED
@@ -24,6 +24,26 @@ export const AddOnBaseSchema = z.object({
24
24
  warning: z.string().optional(),
25
25
  tailwind: z.boolean().optional().default(true),
26
26
  type: z.enum(['add-on', 'example', 'starter', 'toolchain', 'deployment']),
27
+ category: z
28
+ .enum([
29
+ 'tanstack',
30
+ 'database',
31
+ 'orm',
32
+ 'auth',
33
+ 'deploy',
34
+ 'styling',
35
+ 'monitoring',
36
+ 'cms',
37
+ 'api',
38
+ 'i18n',
39
+ 'tooling',
40
+ 'other',
41
+ ])
42
+ .optional(),
43
+ exclusive: z
44
+ .array(z.enum(['orm', 'auth', 'deploy', 'database', 'linter']))
45
+ .optional(),
46
+ color: z.string().optional(),
27
47
  priority: z.number().optional(),
28
48
  command: z
29
49
  .object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/cta-engine",
3
- "version": "0.48.0",
3
+ "version": "0.49.0",
4
4
  "description": "Tanstack Application Builder Engine",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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
+ }
@@ -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: [],
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'
@@ -85,6 +90,12 @@ export type {
85
90
  SerializedOptions,
86
91
  Starter,
87
92
  StarterCompiled,
93
+ LineAttribution,
94
+ FileProvenance,
95
+ AttributedFile,
96
+ DependencyAttribution,
88
97
  } from './types.js'
98
+ export type { AttributionInput, AttributionOutput } from './attribution.js'
99
+ export type { MemoryEnvironmentOutput } from './environment.js'
89
100
  export type { PersistedOptions } from './config-file.js'
90
101
  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
+ }