@tanstack/cli 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.cjs +293 -2
- package/dist/bin.mjs +293 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +17 -17
- package/dist/index.d.mts +17 -17
- package/dist/index.mjs +1 -1
- package/dist/{template-BYKvtZrT.cjs → template-B-i2qO8E.cjs} +83 -34
- package/dist/{template-2SxOxCJc.mjs → template-BgEATWaG.mjs} +83 -34
- package/package.json +1 -1
- package/src/api/fetch.test.ts +3 -3
- package/src/api/fetch.ts +65 -37
- package/src/cache/index.ts +89 -0
- package/src/commands/create.ts +1 -1
- package/src/commands/mcp.test.ts +3 -3
- package/src/commands/mcp.ts +5 -1
- package/src/engine/compile-with-addons.test.ts +7 -7
- package/src/engine/compile.test.ts +1 -1
- package/src/engine/compile.ts +6 -6
- package/src/engine/custom-addons/shared.ts +1 -1
- package/src/engine/template.test.ts +1 -1
- package/src/engine/template.ts +1 -1
- package/src/engine/types.ts +6 -4
- package/src/mcp/api.ts +42 -0
- package/src/mcp/tools.ts +323 -0
- package/src/mcp/types.ts +46 -0
- package/src/templates/base.ts +3 -0
|
@@ -5,6 +5,7 @@ import { basename, dirname, extname, join, resolve } from "node:path";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import ignore from "ignore";
|
|
7
7
|
import parseGitignore from "parse-gitignore";
|
|
8
|
+
import { homedir } from "node:os";
|
|
8
9
|
|
|
9
10
|
//#region src/engine/template.ts
|
|
10
11
|
/**
|
|
@@ -1354,6 +1355,7 @@ ${packageManager}${packageManager === "npm" ? " run" : ""} dev
|
|
|
1354
1355
|
- [TanStack Start Documentation](https://tanstack.com/start)
|
|
1355
1356
|
- [TanStack Router Documentation](https://tanstack.com/router)
|
|
1356
1357
|
`;
|
|
1358
|
+
files[".nvmrc"] = "23";
|
|
1357
1359
|
return files;
|
|
1358
1360
|
}
|
|
1359
1361
|
/**
|
|
@@ -1461,8 +1463,8 @@ function buildPackageJson(options, packages) {
|
|
|
1461
1463
|
"@tanstack/react-router": "^1.132.0",
|
|
1462
1464
|
"@tanstack/react-router-devtools": "^1.132.0",
|
|
1463
1465
|
"@tanstack/react-start": "^1.132.0",
|
|
1464
|
-
react: "^19.
|
|
1465
|
-
"react-dom": "^19.
|
|
1466
|
+
react: "^19.2.0",
|
|
1467
|
+
"react-dom": "^19.2.0",
|
|
1466
1468
|
"vite-tsconfig-paths": "^5.1.4",
|
|
1467
1469
|
...hasHeader ? { "lucide-react": "^0.468.0" } : {},
|
|
1468
1470
|
...packages.dependencies
|
|
@@ -1471,8 +1473,8 @@ function buildPackageJson(options, packages) {
|
|
|
1471
1473
|
"@vitejs/plugin-react": "^4.4.1",
|
|
1472
1474
|
vite: "^7.0.0",
|
|
1473
1475
|
...options.typescript ? {
|
|
1474
|
-
"@types/react": "^19.
|
|
1475
|
-
"@types/react-dom": "^19.
|
|
1476
|
+
"@types/react": "^19.2.0",
|
|
1477
|
+
"@types/react-dom": "^19.2.0",
|
|
1476
1478
|
typescript: "^5.7.0"
|
|
1477
1479
|
} : {},
|
|
1478
1480
|
...options.tailwind ? {
|
|
@@ -1896,7 +1898,7 @@ const IntegrationInfoSchema = z.object({
|
|
|
1896
1898
|
requiresTailwind: z.boolean().optional(),
|
|
1897
1899
|
demoRequiresTailwind: z.boolean().optional(),
|
|
1898
1900
|
dependsOn: z.array(z.string()).optional(),
|
|
1899
|
-
|
|
1901
|
+
exclusive: z.array(z.string()).optional(),
|
|
1900
1902
|
partnerId: z.string().optional(),
|
|
1901
1903
|
options: IntegrationOptionsSchema.optional(),
|
|
1902
1904
|
hooks: z.array(HookSchema).optional(),
|
|
@@ -1943,7 +1945,7 @@ const ManifestIntegrationSchema = z.object({
|
|
|
1943
1945
|
category: CategorySchema.optional(),
|
|
1944
1946
|
modes: z.array(RouterModeSchema),
|
|
1945
1947
|
dependsOn: z.array(z.string()).optional(),
|
|
1946
|
-
|
|
1948
|
+
exclusive: z.array(z.string()).optional(),
|
|
1947
1949
|
partnerId: z.string().optional(),
|
|
1948
1950
|
hasOptions: z.boolean().optional(),
|
|
1949
1951
|
link: z.string().optional(),
|
|
@@ -1966,9 +1968,50 @@ const ManifestSchema = z.object({
|
|
|
1966
1968
|
customTemplates: z.array(ManifestCustomTemplateSchema).optional()
|
|
1967
1969
|
});
|
|
1968
1970
|
|
|
1971
|
+
//#endregion
|
|
1972
|
+
//#region src/cache/index.ts
|
|
1973
|
+
const CACHE_DIR = join(homedir(), ".tanstack", "cache");
|
|
1974
|
+
const DEFAULT_TTL_MS = 1440 * 60 * 1e3;
|
|
1975
|
+
function ensureCacheDir() {
|
|
1976
|
+
if (!existsSync(CACHE_DIR)) mkdirSync(CACHE_DIR, { recursive: true });
|
|
1977
|
+
}
|
|
1978
|
+
function getCachePath(key) {
|
|
1979
|
+
return join(CACHE_DIR, `${key.replace(/[^a-zA-Z0-9-_]/g, "_")}.json`);
|
|
1980
|
+
}
|
|
1981
|
+
function getCached(key) {
|
|
1982
|
+
const cachePath = getCachePath(key);
|
|
1983
|
+
if (!existsSync(cachePath)) return null;
|
|
1984
|
+
try {
|
|
1985
|
+
const raw = readFileSync(cachePath, "utf-8");
|
|
1986
|
+
const entry = JSON.parse(raw);
|
|
1987
|
+
if (Date.now() - entry.timestamp > entry.ttl) return null;
|
|
1988
|
+
return entry.data;
|
|
1989
|
+
} catch {
|
|
1990
|
+
return null;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
function setCache(key, data, ttlMs = DEFAULT_TTL_MS) {
|
|
1994
|
+
ensureCacheDir();
|
|
1995
|
+
const cachePath = getCachePath(key);
|
|
1996
|
+
const entry = {
|
|
1997
|
+
data,
|
|
1998
|
+
timestamp: Date.now(),
|
|
1999
|
+
ttl: ttlMs
|
|
2000
|
+
};
|
|
2001
|
+
writeFileSync(cachePath, JSON.stringify(entry, null, 2), "utf-8");
|
|
2002
|
+
}
|
|
2003
|
+
async function fetchWithCache(key, fetcher, ttlMs = DEFAULT_TTL_MS) {
|
|
2004
|
+
const cached = getCached(key);
|
|
2005
|
+
if (cached !== null) return cached;
|
|
2006
|
+
const data = await fetcher();
|
|
2007
|
+
setCache(key, data, ttlMs);
|
|
2008
|
+
return data;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
1969
2011
|
//#endregion
|
|
1970
2012
|
//#region src/api/fetch.ts
|
|
1971
2013
|
const GITHUB_RAW_BASE = "https://raw.githubusercontent.com/TanStack/cli/main/integrations";
|
|
2014
|
+
const CACHE_TTL_MS = 3600 * 1e3;
|
|
1972
2015
|
/**
|
|
1973
2016
|
* Check if a path is a local directory
|
|
1974
2017
|
*/
|
|
@@ -1976,36 +2019,40 @@ function isLocalPath(path) {
|
|
|
1976
2019
|
return path.startsWith("/") || path.startsWith("./") || path.startsWith("..");
|
|
1977
2020
|
}
|
|
1978
2021
|
/**
|
|
1979
|
-
* Fetch the integration manifest from GitHub or local path
|
|
2022
|
+
* Fetch the integration manifest from GitHub or local path (with caching for remote)
|
|
1980
2023
|
*/
|
|
1981
2024
|
async function fetchManifest(baseUrl = GITHUB_RAW_BASE) {
|
|
1982
2025
|
if (isLocalPath(baseUrl)) {
|
|
1983
2026
|
const manifestPath = join(baseUrl, "manifest.json");
|
|
1984
2027
|
if (!existsSync(manifestPath)) throw new Error(`Manifest not found at ${manifestPath}`);
|
|
1985
|
-
const data
|
|
1986
|
-
return ManifestSchema.parse(data
|
|
2028
|
+
const data = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
2029
|
+
return ManifestSchema.parse(data);
|
|
1987
2030
|
}
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
2031
|
+
return fetchWithCache(`manifest_${baseUrl.replace(/[^a-zA-Z0-9]/g, "_")}`, async () => {
|
|
2032
|
+
const url = `${baseUrl}/manifest.json`;
|
|
2033
|
+
const response = await fetch(url);
|
|
2034
|
+
if (!response.ok) throw new Error(`Failed to fetch manifest: ${response.statusText}`);
|
|
2035
|
+
const data = await response.json();
|
|
2036
|
+
return ManifestSchema.parse(data);
|
|
2037
|
+
}, CACHE_TTL_MS);
|
|
1993
2038
|
}
|
|
1994
2039
|
/**
|
|
1995
|
-
* Fetch integration info.json from GitHub or local path
|
|
2040
|
+
* Fetch integration info.json from GitHub or local path (with caching for remote)
|
|
1996
2041
|
*/
|
|
1997
2042
|
async function fetchIntegrationInfo(integrationId, baseUrl = GITHUB_RAW_BASE) {
|
|
1998
2043
|
if (isLocalPath(baseUrl)) {
|
|
1999
2044
|
const infoPath = join(baseUrl, integrationId, "info.json");
|
|
2000
2045
|
if (!existsSync(infoPath)) throw new Error(`Integration info not found at ${infoPath}`);
|
|
2001
|
-
const data
|
|
2002
|
-
return IntegrationInfoSchema.parse(data
|
|
2046
|
+
const data = JSON.parse(readFileSync(infoPath, "utf-8"));
|
|
2047
|
+
return IntegrationInfoSchema.parse(data);
|
|
2003
2048
|
}
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2049
|
+
return fetchWithCache(`integration_info_${integrationId}_${baseUrl.replace(/[^a-zA-Z0-9]/g, "_")}`, async () => {
|
|
2050
|
+
const url = `${baseUrl}/${integrationId}/info.json`;
|
|
2051
|
+
const response = await fetch(url);
|
|
2052
|
+
if (!response.ok) throw new Error(`Failed to fetch integration ${integrationId}: ${response.statusText}`);
|
|
2053
|
+
const data = await response.json();
|
|
2054
|
+
return IntegrationInfoSchema.parse(data);
|
|
2055
|
+
}, CACHE_TTL_MS);
|
|
2009
2056
|
}
|
|
2010
2057
|
/**
|
|
2011
2058
|
* Recursively read all files from a directory
|
|
@@ -2022,21 +2069,23 @@ function readDirRecursive(dir, basePath = "") {
|
|
|
2022
2069
|
return files;
|
|
2023
2070
|
}
|
|
2024
2071
|
/**
|
|
2025
|
-
* Fetch all files for an integration from GitHub or local path
|
|
2072
|
+
* Fetch all files for an integration from GitHub or local path (with caching for remote)
|
|
2026
2073
|
*/
|
|
2027
2074
|
async function fetchIntegrationFiles(integrationId, baseUrl = GITHUB_RAW_BASE) {
|
|
2028
2075
|
if (isLocalPath(baseUrl)) return readDirRecursive(join(baseUrl, integrationId, "assets"));
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2076
|
+
return fetchWithCache(`integration_files_${integrationId}_${baseUrl.replace(/[^a-zA-Z0-9]/g, "_")}`, async () => {
|
|
2077
|
+
const filesUrl = `${baseUrl}/${integrationId}/files.json`;
|
|
2078
|
+
const response = await fetch(filesUrl);
|
|
2079
|
+
if (!response.ok) return {};
|
|
2080
|
+
const fileList = await response.json();
|
|
2081
|
+
const files = {};
|
|
2082
|
+
await Promise.all(fileList.map(async (filePath) => {
|
|
2083
|
+
const fileUrl = `${baseUrl}/${integrationId}/assets/${filePath}`;
|
|
2084
|
+
const fileResponse = await fetch(fileUrl);
|
|
2085
|
+
if (fileResponse.ok) files[filePath] = await fileResponse.text();
|
|
2086
|
+
}));
|
|
2087
|
+
return files;
|
|
2088
|
+
}, CACHE_TTL_MS);
|
|
2040
2089
|
}
|
|
2041
2090
|
/**
|
|
2042
2091
|
* Fetch integration package.json if it exists
|
package/package.json
CHANGED
package/src/api/fetch.test.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { resolve } from 'node:path'
|
|
2
2
|
import { describe, expect, it } from 'vitest'
|
|
3
3
|
import {
|
|
4
|
-
fetchManifest,
|
|
5
4
|
fetchIntegration,
|
|
6
|
-
fetchIntegrations,
|
|
7
|
-
fetchIntegrationInfo,
|
|
8
5
|
fetchIntegrationFiles,
|
|
6
|
+
fetchIntegrationInfo,
|
|
7
|
+
fetchIntegrations,
|
|
8
|
+
fetchManifest,
|
|
9
9
|
} from './fetch.js'
|
|
10
10
|
|
|
11
11
|
const INTEGRATIONS_PATH = resolve(__dirname, '../../../../integrations')
|
package/src/api/fetch.ts
CHANGED
|
@@ -5,11 +5,15 @@ import {
|
|
|
5
5
|
IntegrationInfoSchema,
|
|
6
6
|
ManifestSchema
|
|
7
7
|
} from '../engine/types.js'
|
|
8
|
+
import { fetchWithCache } from '../cache/index.js'
|
|
8
9
|
import type { IntegrationCompiled, IntegrationInfo, Manifest } from '../engine/types.js'
|
|
9
10
|
|
|
10
11
|
const GITHUB_RAW_BASE =
|
|
11
12
|
'https://raw.githubusercontent.com/TanStack/cli/main/integrations'
|
|
12
13
|
|
|
14
|
+
// 1 hour cache TTL for remote fetches
|
|
15
|
+
const CACHE_TTL_MS = 60 * 60 * 1000
|
|
16
|
+
|
|
13
17
|
/**
|
|
14
18
|
* Check if a path is a local directory
|
|
15
19
|
*/
|
|
@@ -18,7 +22,7 @@ function isLocalPath(path: string): boolean {
|
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
|
-
* Fetch the integration manifest from GitHub or local path
|
|
25
|
+
* Fetch the integration manifest from GitHub or local path (with caching for remote)
|
|
22
26
|
*/
|
|
23
27
|
export async function fetchManifest(
|
|
24
28
|
baseUrl: string = GITHUB_RAW_BASE,
|
|
@@ -32,19 +36,27 @@ export async function fetchManifest(
|
|
|
32
36
|
return ManifestSchema.parse(data)
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
const
|
|
36
|
-
const response = await fetch(url)
|
|
39
|
+
const cacheKey = `manifest_${baseUrl.replace(/[^a-zA-Z0-9]/g, '_')}`
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
return fetchWithCache(
|
|
42
|
+
cacheKey,
|
|
43
|
+
async () => {
|
|
44
|
+
const url = `${baseUrl}/manifest.json`
|
|
45
|
+
const response = await fetch(url)
|
|
46
|
+
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new Error(`Failed to fetch manifest: ${response.statusText}`)
|
|
49
|
+
}
|
|
41
50
|
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
const data = await response.json()
|
|
52
|
+
return ManifestSchema.parse(data)
|
|
53
|
+
},
|
|
54
|
+
CACHE_TTL_MS,
|
|
55
|
+
)
|
|
44
56
|
}
|
|
45
57
|
|
|
46
58
|
/**
|
|
47
|
-
* Fetch integration info.json from GitHub or local path
|
|
59
|
+
* Fetch integration info.json from GitHub or local path (with caching for remote)
|
|
48
60
|
*/
|
|
49
61
|
export async function fetchIntegrationInfo(
|
|
50
62
|
integrationId: string,
|
|
@@ -59,15 +71,23 @@ export async function fetchIntegrationInfo(
|
|
|
59
71
|
return IntegrationInfoSchema.parse(data)
|
|
60
72
|
}
|
|
61
73
|
|
|
62
|
-
const
|
|
63
|
-
const response = await fetch(url)
|
|
74
|
+
const cacheKey = `integration_info_${integrationId}_${baseUrl.replace(/[^a-zA-Z0-9]/g, '_')}`
|
|
64
75
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
76
|
+
return fetchWithCache(
|
|
77
|
+
cacheKey,
|
|
78
|
+
async () => {
|
|
79
|
+
const url = `${baseUrl}/${integrationId}/info.json`
|
|
80
|
+
const response = await fetch(url)
|
|
81
|
+
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
throw new Error(`Failed to fetch integration ${integrationId}: ${response.statusText}`)
|
|
84
|
+
}
|
|
68
85
|
|
|
69
|
-
|
|
70
|
-
|
|
86
|
+
const data = await response.json()
|
|
87
|
+
return IntegrationInfoSchema.parse(data)
|
|
88
|
+
},
|
|
89
|
+
CACHE_TTL_MS,
|
|
90
|
+
)
|
|
71
91
|
}
|
|
72
92
|
|
|
73
93
|
/**
|
|
@@ -97,7 +117,7 @@ function readDirRecursive(
|
|
|
97
117
|
}
|
|
98
118
|
|
|
99
119
|
/**
|
|
100
|
-
* Fetch all files for an integration from GitHub or local path
|
|
120
|
+
* Fetch all files for an integration from GitHub or local path (with caching for remote)
|
|
101
121
|
*/
|
|
102
122
|
export async function fetchIntegrationFiles(
|
|
103
123
|
integrationId: string,
|
|
@@ -108,31 +128,39 @@ export async function fetchIntegrationFiles(
|
|
|
108
128
|
return readDirRecursive(assetsPath)
|
|
109
129
|
}
|
|
110
130
|
|
|
111
|
-
|
|
112
|
-
const filesUrl = `${baseUrl}/${integrationId}/files.json`
|
|
113
|
-
const response = await fetch(filesUrl)
|
|
131
|
+
const cacheKey = `integration_files_${integrationId}_${baseUrl.replace(/[^a-zA-Z0-9]/g, '_')}`
|
|
114
132
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
133
|
+
return fetchWithCache(
|
|
134
|
+
cacheKey,
|
|
135
|
+
async () => {
|
|
136
|
+
// First fetch the file list (we'll need a files.json or similar)
|
|
137
|
+
const filesUrl = `${baseUrl}/${integrationId}/files.json`
|
|
138
|
+
const response = await fetch(filesUrl)
|
|
119
139
|
|
|
120
|
-
|
|
121
|
-
|
|
140
|
+
if (!response.ok) {
|
|
141
|
+
// No files.json, return empty
|
|
142
|
+
return {}
|
|
143
|
+
}
|
|
122
144
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
fileList.map(async (filePath) => {
|
|
126
|
-
const fileUrl = `${baseUrl}/${integrationId}/assets/${filePath}`
|
|
127
|
-
const fileResponse = await fetch(fileUrl)
|
|
145
|
+
const fileList: Array<string> = await response.json()
|
|
146
|
+
const files: Record<string, string> = {}
|
|
128
147
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
148
|
+
// Fetch each file
|
|
149
|
+
await Promise.all(
|
|
150
|
+
fileList.map(async (filePath) => {
|
|
151
|
+
const fileUrl = `${baseUrl}/${integrationId}/assets/${filePath}`
|
|
152
|
+
const fileResponse = await fetch(fileUrl)
|
|
134
153
|
|
|
135
|
-
|
|
154
|
+
if (fileResponse.ok) {
|
|
155
|
+
files[filePath] = await fileResponse.text()
|
|
156
|
+
}
|
|
157
|
+
}),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return files
|
|
161
|
+
},
|
|
162
|
+
CACHE_TTL_MS,
|
|
163
|
+
)
|
|
136
164
|
}
|
|
137
165
|
|
|
138
166
|
/**
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
|
|
5
|
+
const CACHE_DIR = join(homedir(), '.tanstack', 'cache')
|
|
6
|
+
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000 // 24 hours
|
|
7
|
+
|
|
8
|
+
interface CacheEntry<T> {
|
|
9
|
+
data: T
|
|
10
|
+
timestamp: number
|
|
11
|
+
ttl: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function ensureCacheDir() {
|
|
15
|
+
if (!existsSync(CACHE_DIR)) {
|
|
16
|
+
mkdirSync(CACHE_DIR, { recursive: true })
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getCachePath(key: string): string {
|
|
21
|
+
// Sanitize key to be filesystem-safe
|
|
22
|
+
const safeKey = key.replace(/[^a-zA-Z0-9-_]/g, '_')
|
|
23
|
+
return join(CACHE_DIR, `${safeKey}.json`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getCached<T>(key: string): T | null {
|
|
27
|
+
const cachePath = getCachePath(key)
|
|
28
|
+
|
|
29
|
+
if (!existsSync(cachePath)) {
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const raw = readFileSync(cachePath, 'utf-8')
|
|
35
|
+
const entry: CacheEntry<T> = JSON.parse(raw)
|
|
36
|
+
|
|
37
|
+
const age = Date.now() - entry.timestamp
|
|
38
|
+
if (age > entry.ttl) {
|
|
39
|
+
return null // Expired
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return entry.data
|
|
43
|
+
} catch {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function setCache<T>(key: string, data: T, ttlMs: number = DEFAULT_TTL_MS): void {
|
|
49
|
+
ensureCacheDir()
|
|
50
|
+
const cachePath = getCachePath(key)
|
|
51
|
+
|
|
52
|
+
const entry: CacheEntry<T> = {
|
|
53
|
+
data,
|
|
54
|
+
timestamp: Date.now(),
|
|
55
|
+
ttl: ttlMs,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
writeFileSync(cachePath, JSON.stringify(entry, null, 2), 'utf-8')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function fetchWithCache<T>(
|
|
62
|
+
key: string,
|
|
63
|
+
fetcher: () => Promise<T>,
|
|
64
|
+
ttlMs: number = DEFAULT_TTL_MS,
|
|
65
|
+
): Promise<T> {
|
|
66
|
+
// Try cache first
|
|
67
|
+
const cached = getCached<T>(key)
|
|
68
|
+
if (cached !== null) {
|
|
69
|
+
return cached
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Fetch fresh data
|
|
73
|
+
const data = await fetcher()
|
|
74
|
+
|
|
75
|
+
// Cache it
|
|
76
|
+
setCache(key, data, ttlMs)
|
|
77
|
+
|
|
78
|
+
return data
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function clearCache(): void {
|
|
82
|
+
if (existsSync(CACHE_DIR)) {
|
|
83
|
+
rmSync(CACHE_DIR, { recursive: true, force: true })
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getCacheDir(): string {
|
|
88
|
+
return CACHE_DIR
|
|
89
|
+
}
|
package/src/commands/create.ts
CHANGED
|
@@ -19,8 +19,8 @@ import { compile } from '../engine/compile.js'
|
|
|
19
19
|
import { writeConfigFile } from '../engine/config-file.js'
|
|
20
20
|
import { loadTemplate } from '../engine/custom-addons/template.js'
|
|
21
21
|
import type {
|
|
22
|
-
IntegrationCompiled,
|
|
23
22
|
CustomTemplateCompiled,
|
|
23
|
+
IntegrationCompiled,
|
|
24
24
|
ManifestIntegration,
|
|
25
25
|
PackageManager,
|
|
26
26
|
RouterMode,
|
package/src/commands/mcp.test.ts
CHANGED
|
@@ -32,7 +32,7 @@ describe('MCP server tools logic', () => {
|
|
|
32
32
|
description: integration.description,
|
|
33
33
|
category: integration.category,
|
|
34
34
|
dependsOn: integration.dependsOn,
|
|
35
|
-
|
|
35
|
+
exclusive: integration.exclusive,
|
|
36
36
|
hasOptions: integration.hasOptions,
|
|
37
37
|
}))
|
|
38
38
|
|
|
@@ -44,7 +44,7 @@ describe('MCP server tools logic', () => {
|
|
|
44
44
|
expect(query!.category).toBe('tanstack')
|
|
45
45
|
})
|
|
46
46
|
|
|
47
|
-
it('should include dependency and
|
|
47
|
+
it('should include dependency and exclusive type info', async () => {
|
|
48
48
|
const manifest = await fetchManifest(INTEGRATIONS_PATH)
|
|
49
49
|
const integrations = manifest.integrations.filter((a) =>
|
|
50
50
|
a.modes.includes('file-router'),
|
|
@@ -54,7 +54,7 @@ describe('MCP server tools logic', () => {
|
|
|
54
54
|
expect(trpc?.dependsOn).toContain('tanstack-query')
|
|
55
55
|
|
|
56
56
|
const clerk = integrations.find((i) => i.id === 'clerk')
|
|
57
|
-
expect(clerk?.
|
|
57
|
+
expect(clerk?.exclusive).toContain('auth')
|
|
58
58
|
})
|
|
59
59
|
})
|
|
60
60
|
|
package/src/commands/mcp.ts
CHANGED
|
@@ -5,6 +5,7 @@ import express from 'express'
|
|
|
5
5
|
import { z } from 'zod'
|
|
6
6
|
import { fetchIntegrations, fetchManifest } from '../api/fetch.js'
|
|
7
7
|
import { compile } from '../engine/compile.js'
|
|
8
|
+
import { registerDocTools } from '../mcp/tools.js'
|
|
8
9
|
import type { RouterMode } from '../engine/types.js'
|
|
9
10
|
|
|
10
11
|
interface McpOptions {
|
|
@@ -33,7 +34,7 @@ function createServer() {
|
|
|
33
34
|
description: integration.description,
|
|
34
35
|
category: integration.category,
|
|
35
36
|
dependsOn: integration.dependsOn,
|
|
36
|
-
|
|
37
|
+
exclusive: integration.exclusive,
|
|
37
38
|
hasOptions: integration.hasOptions,
|
|
38
39
|
}))
|
|
39
40
|
|
|
@@ -58,6 +59,9 @@ function createServer() {
|
|
|
58
59
|
},
|
|
59
60
|
)
|
|
60
61
|
|
|
62
|
+
// Register documentation/ecosystem tools
|
|
63
|
+
registerDocTools(server)
|
|
64
|
+
|
|
61
65
|
server.tool(
|
|
62
66
|
'createTanStackApplication',
|
|
63
67
|
'Create a new TanStack Start application',
|
|
@@ -2,7 +2,7 @@ import { resolve } from 'node:path'
|
|
|
2
2
|
import { beforeAll, describe, expect, it } from 'vitest'
|
|
3
3
|
import { fetchIntegration, fetchIntegrations } from '../api/fetch.js'
|
|
4
4
|
import { compile } from './compile.js'
|
|
5
|
-
import type {
|
|
5
|
+
import type { CompileOptions, IntegrationCompiled } from './types.js'
|
|
6
6
|
|
|
7
7
|
const INTEGRATIONS_PATH = resolve(__dirname, '../../../../integrations')
|
|
8
8
|
|
|
@@ -214,16 +214,16 @@ describe('compile with real integrations', () => {
|
|
|
214
214
|
})
|
|
215
215
|
})
|
|
216
216
|
|
|
217
|
-
describe('Deployment integration (
|
|
218
|
-
let
|
|
217
|
+
describe('Deployment integration (Railway)', () => {
|
|
218
|
+
let railwayIntegration: IntegrationCompiled
|
|
219
219
|
|
|
220
220
|
beforeAll(async () => {
|
|
221
|
-
|
|
221
|
+
railwayIntegration = await fetchIntegration('railway', INTEGRATIONS_PATH)
|
|
222
222
|
})
|
|
223
223
|
|
|
224
|
-
it('should load
|
|
225
|
-
expect(
|
|
226
|
-
expect(
|
|
224
|
+
it('should load railway integration', () => {
|
|
225
|
+
expect(railwayIntegration.id).toBe('railway')
|
|
226
|
+
expect(railwayIntegration.type).toBe('deployment')
|
|
227
227
|
})
|
|
228
228
|
})
|
|
229
229
|
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { compile, compileWithAttribution } from './compile.js'
|
|
3
|
-
import type {
|
|
3
|
+
import type { CompileOptions, CustomTemplateCompiled, IntegrationCompiled } from './types.js'
|
|
4
4
|
|
|
5
5
|
const baseOptions: CompileOptions = {
|
|
6
6
|
projectName: 'test-project',
|
package/src/engine/compile.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { getBaseFiles, getBaseFilesWithAttribution } from '../templates/base.js'
|
|
2
2
|
import { processTemplateFile } from './template.js'
|
|
3
3
|
import type {
|
|
4
|
-
IntegrationCompiled,
|
|
5
|
-
IntegrationPhase,
|
|
6
4
|
AttributedCompileOutput,
|
|
7
5
|
CompileOptions,
|
|
8
6
|
CompileOutput,
|
|
9
7
|
EnvVar,
|
|
8
|
+
IntegrationCompiled,
|
|
9
|
+
IntegrationPhase,
|
|
10
10
|
LineAttribution,
|
|
11
11
|
} from './types.js'
|
|
12
12
|
|
|
@@ -130,8 +130,8 @@ function buildPackageJson(
|
|
|
130
130
|
'@tanstack/react-router': '^1.132.0',
|
|
131
131
|
'@tanstack/react-router-devtools': '^1.132.0',
|
|
132
132
|
'@tanstack/react-start': '^1.132.0',
|
|
133
|
-
react: '^19.
|
|
134
|
-
'react-dom': '^19.
|
|
133
|
+
react: '^19.2.0',
|
|
134
|
+
'react-dom': '^19.2.0',
|
|
135
135
|
'vite-tsconfig-paths': '^5.1.4',
|
|
136
136
|
...(hasHeader ? { 'lucide-react': '^0.468.0' } : {}),
|
|
137
137
|
...packages.dependencies,
|
|
@@ -141,8 +141,8 @@ function buildPackageJson(
|
|
|
141
141
|
vite: '^7.0.0',
|
|
142
142
|
...(options.typescript
|
|
143
143
|
? {
|
|
144
|
-
'@types/react': '^19.
|
|
145
|
-
'@types/react-dom': '^19.
|
|
144
|
+
'@types/react': '^19.2.0',
|
|
145
|
+
'@types/react-dom': '^19.2.0',
|
|
146
146
|
typescript: '^5.7.0',
|
|
147
147
|
}
|
|
148
148
|
: {}),
|
|
@@ -13,7 +13,7 @@ import { readConfigFile } from '../config-file.js'
|
|
|
13
13
|
import { fetchIntegrations } from '../../api/fetch.js'
|
|
14
14
|
|
|
15
15
|
import type { PersistedOptions } from '../config-file.js'
|
|
16
|
-
import type {
|
|
16
|
+
import type { CompileOptions, CompileOutput, IntegrationCompiled } from '../types.js'
|
|
17
17
|
|
|
18
18
|
// Files to always ignore (from Jack's IGNORE_FILES)
|
|
19
19
|
const IGNORE_FILES = [
|