@tanstack/cta-engine 0.10.0-alpha.9 → 0.11.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/dist/add-ons.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
2
+ import { existsSync, readdirSync, statSync } from 'node:fs';
3
3
  import { resolve } from 'node:path';
4
4
  import chalk from 'chalk';
5
5
  import { getTemplatesRoot } from './templates.js';
6
6
  import { DEFAULT_FRAMEWORK } from './constants.js';
7
+ import { readFileHelper } from './file-helper.js';
7
8
  function isDirectory(path) {
8
9
  return statSync(path).isDirectory();
9
10
  }
@@ -15,7 +16,7 @@ function findFilesRecursively(path, files) {
15
16
  findFilesRecursively(filePath, files);
16
17
  }
17
18
  else {
18
- files[filePath] = readFileSync(filePath, 'utf-8').toString();
19
+ files[filePath] = readFileHelper(filePath);
19
20
  }
20
21
  }
21
22
  }
@@ -91,10 +92,9 @@ export async function finalizeAddOns(framework, template, chosenAddOnIDs) {
91
92
  const finalAddOns = [...finalAddOnIDs].map((id) => addOns.find((a) => a.id === id));
92
93
  return finalAddOns;
93
94
  }
94
- export async function listAddOns(options) {
95
- const mode = options.template === 'file-router' ? 'file-router' : 'code-router';
96
- const addOns = await getAllAddOns(options.framework || DEFAULT_FRAMEWORK, mode);
97
- for (const addOn of addOns) {
95
+ export async function listAddOns(options, { forcedMode, forcedAddOns = [], }) {
96
+ const addOns = await getAllAddOns(options.framework || DEFAULT_FRAMEWORK, forcedMode || options.template || 'typescript');
97
+ for (const addOn of addOns.filter((a) => !forcedAddOns.includes(a.id))) {
98
98
  console.log(`${chalk.bold(addOn.id)}: ${addOn.description}`);
99
99
  }
100
100
  }
package/dist/cli.js CHANGED
@@ -9,11 +9,9 @@ import { listAddOns } from './add-ons.js';
9
9
  import { DEFAULT_FRAMEWORK, SUPPORTED_FRAMEWORKS } from './constants.js';
10
10
  // import { initAddOn } from './custom-add-on.js'
11
11
  import { createDefaultEnvironment } from './environment.js';
12
- export function cli({ name, forcedMode, forcedAddOns, }) {
12
+ export function cli({ name, appName, forcedMode, forcedAddOns, }) {
13
13
  const program = new Command();
14
- program
15
- .name('create-tsrouter-app')
16
- .description('CLI to create a new TanStack application');
14
+ program.name(name).description(`CLI to create a new ${appName} application`);
17
15
  // program
18
16
  // .command('add')
19
17
  // .argument('add-on', 'Name of the add-on (or add-ons separated by commas)')
@@ -32,16 +30,25 @@ export function cli({ name, forcedMode, forcedAddOns, }) {
32
30
  // .action(async () => {
33
31
  // await initAddOn('overlay')
34
32
  // })
33
+ program.argument('[project-name]', 'name of the project');
34
+ if (!forcedMode) {
35
+ program.option('--template <type>', 'project template (typescript, javascript, file-router)', (value) => {
36
+ if (value !== 'typescript' &&
37
+ value !== 'javascript' &&
38
+ value !== 'file-router') {
39
+ throw new InvalidArgumentError(`Invalid template: ${value}. Only the following are allowed: typescript, javascript, file-router`);
40
+ }
41
+ return value;
42
+ });
43
+ }
35
44
  program
36
- .argument('[project-name]', 'name of the project')
37
- .option('--no-git', 'do not create a git repository')
38
- .option('--target-dir <path>', 'the directory to create the project in')
39
45
  .option('--framework <type>', 'project framework (solid, react)', (value) => {
40
46
  if (!SUPPORTED_FRAMEWORKS.includes(value)) {
41
47
  throw new InvalidArgumentError(`Invalid framework: ${value}. Only the following are allowed: ${SUPPORTED_FRAMEWORKS.join(', ')}`);
42
48
  }
43
49
  return value;
44
50
  }, DEFAULT_FRAMEWORK)
51
+ // .option('--overlay [url]', 'add an overlay from a URL', false)
45
52
  .option(`--package-manager <${SUPPORTED_PACKAGE_MANAGERS.join('|')}>`, `Explicitly tell the CLI to use this package manager`, (value) => {
46
53
  if (!SUPPORTED_PACKAGE_MANAGERS.includes(value)) {
47
54
  throw new InvalidArgumentError(`Invalid package manager: ${value}. The following are allowed: ${SUPPORTED_PACKAGE_MANAGERS.join(', ')}`);
@@ -63,25 +70,23 @@ export function cli({ name, forcedMode, forcedAddOns, }) {
63
70
  return addOns;
64
71
  })
65
72
  .option('--list-add-ons', 'list all available add-ons', false)
66
- // .option('--overlay [url]', 'add an overlay from a URL', false)
73
+ .option('--no-git', 'do not create a git repository')
74
+ .option('--target-dir <path>', 'the target directory for the application root')
67
75
  .option('--mcp', 'run the MCP server', false)
68
76
  .option('--mcp-sse', 'run the MCP server in SSE mode', false);
69
- if (!forcedMode) {
70
- program.option('--template <type>', 'project template (typescript, javascript, file-router)', (value) => {
71
- if (value !== 'typescript' &&
72
- value !== 'javascript' &&
73
- value !== 'file-router') {
74
- throw new InvalidArgumentError(`Invalid template: ${value}. Only the following are allowed: typescript, javascript, file-router`);
75
- }
76
- return value;
77
- });
78
- }
79
77
  program.action(async (projectName, options) => {
80
78
  if (options.listAddOns) {
81
- await listAddOns(options);
79
+ await listAddOns(options, {
80
+ forcedMode,
81
+ forcedAddOns,
82
+ });
82
83
  }
83
84
  else if (options.mcp || options.mcpSse) {
84
- await runServer(!!options.mcpSse);
85
+ await runServer(!!options.mcpSse, {
86
+ forcedMode,
87
+ forcedAddOns,
88
+ appName,
89
+ });
85
90
  }
86
91
  else {
87
92
  try {
@@ -94,10 +99,10 @@ export function cli({ name, forcedMode, forcedAddOns, }) {
94
99
  }
95
100
  let finalOptions = await normalizeOptions(cliOptions, forcedAddOns);
96
101
  if (finalOptions) {
97
- intro(`Creating a new TanStack app in ${projectName}...`);
102
+ intro(`Creating a new ${appName} app in ${projectName}...`);
98
103
  }
99
104
  else {
100
- intro("Let's configure your TanStack application");
105
+ intro(`Let's configure your ${appName} application`);
101
106
  finalOptions = await promptForOptions(cliOptions, {
102
107
  forcedMode,
103
108
  forcedAddOns,
@@ -107,6 +112,7 @@ export function cli({ name, forcedMode, forcedAddOns, }) {
107
112
  environment: createDefaultEnvironment(),
108
113
  cwd: options.targetDir || undefined,
109
114
  name,
115
+ appName,
110
116
  });
111
117
  }
112
118
  catch (error) {
@@ -8,6 +8,7 @@ import { CODE_ROUTER, FILE_ROUTER } from './constants.js';
8
8
  import { sortObject } from './utils.js';
9
9
  import { writeConfigFile } from './config-file.js';
10
10
  import { packageManagerExecute } from './package-manager.js';
11
+ import { getBinaryFile } from './file-helper.js';
11
12
  function createCopyFiles(environment, targetDir) {
12
13
  return async function copyFiles(templateDir, files,
13
14
  // optionally copy files from a folder to the root
@@ -197,6 +198,11 @@ async function copyAddOnFile(environment, content, target, targetPath, templateF
197
198
  isAppend = true;
198
199
  }
199
200
  const finalTargetPath = resolve(dirname(targetPath), targetFile);
201
+ const binaryContent = getBinaryFile(content);
202
+ if (binaryContent) {
203
+ await environment.writeFile(finalTargetPath, binaryContent);
204
+ return;
205
+ }
200
206
  if (isTemplate) {
201
207
  await templateFile(content, finalTargetPath);
202
208
  }
@@ -209,7 +215,7 @@ async function copyAddOnFile(environment, content, target, targetPath, templateF
209
215
  }
210
216
  }
211
217
  }
212
- export async function createApp(options, { silent = false, environment, cwd, name = 'create-tsrouter-app', }) {
218
+ export async function createApp(options, { silent = false, environment, cwd, appName = 'TanStack', }) {
213
219
  environment.startRun();
214
220
  const templateDirBase = resolve(getTemplatesRoot(), options.framework, 'base');
215
221
  const templateDirRouter = resolve(getTemplatesRoot(), options.framework, options.mode);
@@ -309,7 +315,7 @@ export async function createApp(options, { silent = false, environment, cwd, nam
309
315
  // Copy all the asset files from the addons
310
316
  const s = silent ? null : spinner();
311
317
  for (const type of ['add-on', 'example']) {
312
- for (const phase of ['setup', 'add-on']) {
318
+ for (const phase of ['setup', 'add-on', 'example']) {
313
319
  for (const addOn of options.chosenAddOns.filter((addOn) => addOn.phase === phase && addOn.type === type)) {
314
320
  s?.start(`Setting up ${addOn.name}...`);
315
321
  await runAddOn(addOn);
@@ -480,7 +486,7 @@ ${environment.getErrors().join('\n')}`;
480
486
  if (options.packageManager === 'deno') {
481
487
  startCommand = `deno ${isAddOnEnabled('start') ? 'task dev' : 'start'}`;
482
488
  }
483
- outro(`Your TanStack app is ready in '${basename(targetDir)}'.
489
+ outro(`Your ${appName} app is ready in '${basename(targetDir)}'.
484
490
 
485
491
  Use the following commands to start your app:
486
492
  % cd ${options.projectName}
@@ -6,6 +6,7 @@ import { createMemoryEnvironment } from './environment.js';
6
6
  import { createApp } from './create-app.js';
7
7
  import { readConfigFile } from './config-file.js';
8
8
  import { finalizeAddOns } from './add-ons.js';
9
+ import { readFileHelper } from './file-helper.js';
9
10
  const INFO_FILE = {
10
11
  'add-on': '.add-on/info.json',
11
12
  overlay: 'overlay-info.json',
@@ -98,7 +99,7 @@ async function recursivelyGatherFiles(path, files) {
98
99
  await recursivelyGatherFiles(resolve(path, file.name), files);
99
100
  }
100
101
  else {
101
- files[resolve(path, file.name)] = (await readFile(resolve(path, file.name))).toString();
102
+ files[resolve(path, file.name)] = readFileHelper(resolve(path, file.name));
102
103
  }
103
104
  }
104
105
  }
@@ -0,0 +1,18 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { extname } from 'node:path';
3
+ const BINARY_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico'];
4
+ export function readFileHelper(path) {
5
+ if (BINARY_EXTENSIONS.includes(extname(path))) {
6
+ return `base64::${readFileSync(path).toString('base64')}`;
7
+ }
8
+ else {
9
+ return readFileSync(path, 'utf-8').toString();
10
+ }
11
+ }
12
+ export function getBinaryFile(content) {
13
+ if (content.startsWith('base64::')) {
14
+ const binaryContent = Buffer.from(content.replace('base64::', ''), 'base64');
15
+ return binaryContent;
16
+ }
17
+ return null;
18
+ }
package/dist/mcp.js CHANGED
@@ -6,10 +6,6 @@ import { z } from 'zod';
6
6
  import { createApp } from './create-app.js';
7
7
  import { finalizeAddOns } from './add-ons.js';
8
8
  import { createDefaultEnvironment } from './environment.js';
9
- const server = new McpServer({
10
- name: 'Demo',
11
- version: '1.0.0',
12
- });
13
9
  const tanStackReactAddOns = [
14
10
  {
15
11
  id: 'clerk',
@@ -52,63 +48,6 @@ const tanStackReactAddOns = [
52
48
  description: 'Add an AI chatbot example to the application',
53
49
  },
54
50
  ];
55
- server.tool('listTanStackReactAddOns', {}, () => {
56
- return {
57
- content: [{ type: 'text', text: JSON.stringify(tanStackReactAddOns) }],
58
- };
59
- });
60
- server.tool('createTanStackReactApplication', {
61
- projectName: z
62
- .string()
63
- .describe('The package.json module name of the application (will also be the directory name)'),
64
- cwd: z.string().describe('The directory to create the application in'),
65
- addOns: z
66
- .array(z.enum([
67
- 'clerk',
68
- 'convex',
69
- 'form',
70
- 'netlify',
71
- 'sentry',
72
- 'shadcn',
73
- 'start',
74
- 'store',
75
- 'tanstack-query',
76
- 'tanchat',
77
- ]))
78
- .describe('The IDs of the add-ons to install'),
79
- }, async ({ projectName, addOns, cwd }) => {
80
- try {
81
- process.chdir(cwd);
82
- const chosenAddOns = await finalizeAddOns('react', 'file-router', addOns);
83
- await createApp({
84
- projectName: projectName.replace(/^\//, './'),
85
- framework: 'react',
86
- typescript: true,
87
- tailwind: true,
88
- packageManager: 'pnpm',
89
- toolchain: 'none',
90
- mode: 'file-router',
91
- addOns: true,
92
- chosenAddOns,
93
- git: true,
94
- variableValues: {},
95
- }, {
96
- silent: true,
97
- environment: createDefaultEnvironment(),
98
- name: 'create-tsrouter-app',
99
- });
100
- return {
101
- content: [{ type: 'text', text: 'Application created successfully' }],
102
- };
103
- }
104
- catch (error) {
105
- return {
106
- content: [
107
- { type: 'text', text: `Error creating application: ${error}` },
108
- ],
109
- };
110
- }
111
- });
112
51
  const tanStackSolidAddOns = [
113
52
  {
114
53
  id: 'solid-ui',
@@ -126,6 +65,10 @@ const tanStackSolidAddOns = [
126
65
  id: 'store',
127
66
  description: 'Enable the TanStack Store state management library',
128
67
  },
68
+ {
69
+ id: 'start',
70
+ description: 'Set this if you want a TanStack Start application that supports server functions or APIs',
71
+ },
129
72
  {
130
73
  id: 'tanstack-query',
131
74
  description: 'Enable TanStack Query for data fetching',
@@ -135,63 +78,136 @@ const tanStackSolidAddOns = [
135
78
  description: 'Add an AI chatbot example to the application',
136
79
  },
137
80
  ];
138
- server.tool('listTanStackSolidAddOns', {}, () => {
139
- return {
140
- content: [{ type: 'text', text: JSON.stringify(tanStackSolidAddOns) }],
141
- };
142
- });
143
- server.tool('createTanStackSolidApplication', {
144
- projectName: z
145
- .string()
146
- .describe('The package.json module name of the application (will also be the directory name)'),
147
- cwd: z.string().describe('The directory to create the application in'),
148
- addOns: z
149
- .array(z.enum([
150
- 'solid-ui',
151
- 'form',
152
- 'sentry',
153
- 'store',
154
- 'tanstack-query',
155
- 'tanchat',
156
- ]))
157
- .describe('The IDs of the add-ons to install'),
158
- }, async ({ projectName, addOns, cwd }) => {
159
- try {
160
- process.chdir(cwd);
161
- const chosenAddOns = await finalizeAddOns('solid', 'file-router', addOns);
162
- await createApp({
163
- projectName: projectName.replace(/^\//, './'),
164
- framework: 'solid',
165
- typescript: true,
166
- tailwind: true,
167
- packageManager: 'pnpm',
168
- toolchain: 'none',
169
- mode: 'file-router',
170
- addOns: true,
171
- chosenAddOns,
172
- git: true,
173
- variableValues: {},
174
- }, {
175
- silent: true,
176
- environment: createDefaultEnvironment(),
177
- name: 'create-tsrouter-app',
178
- });
81
+ function createServer({ appName, forcedAddOns = [], name, }) {
82
+ const server = new McpServer({
83
+ name: `${appName} Application Builder`,
84
+ version: '1.0.0',
85
+ });
86
+ server.tool('listTanStackReactAddOns', {}, () => {
179
87
  return {
180
- content: [{ type: 'text', text: 'Application created successfully' }],
88
+ content: [{ type: 'text', text: JSON.stringify(tanStackReactAddOns) }],
181
89
  };
182
- }
183
- catch (error) {
90
+ });
91
+ server.tool('createTanStackReactApplication', {
92
+ projectName: z
93
+ .string()
94
+ .describe('The package.json module name of the application (will also be the directory name)'),
95
+ cwd: z.string().describe('The directory to create the application in'),
96
+ addOns: z
97
+ .array(z.enum([
98
+ 'clerk',
99
+ 'convex',
100
+ 'form',
101
+ 'netlify',
102
+ 'sentry',
103
+ 'shadcn',
104
+ 'start',
105
+ 'store',
106
+ 'tanstack-query',
107
+ 'tanchat',
108
+ ]))
109
+ .describe('The IDs of the add-ons to install'),
110
+ targetDir: z
111
+ .string()
112
+ .describe('The directory to create the application in. Use the absolute path of the directory you want the application to be created in'),
113
+ }, async ({ projectName, addOns, cwd, targetDir }) => {
114
+ try {
115
+ process.chdir(cwd);
116
+ const chosenAddOns = await finalizeAddOns('react', 'file-router', Array.from(new Set([...addOns, ...forcedAddOns])));
117
+ await createApp({
118
+ projectName: projectName.replace(/^\//, './'),
119
+ framework: 'react',
120
+ typescript: true,
121
+ tailwind: true,
122
+ packageManager: 'pnpm',
123
+ toolchain: 'none',
124
+ mode: 'file-router',
125
+ addOns: true,
126
+ chosenAddOns,
127
+ git: true,
128
+ variableValues: {},
129
+ }, {
130
+ silent: true,
131
+ environment: createDefaultEnvironment(),
132
+ name,
133
+ cwd: targetDir,
134
+ });
135
+ return {
136
+ content: [{ type: 'text', text: 'Application created successfully' }],
137
+ };
138
+ }
139
+ catch (error) {
140
+ return {
141
+ content: [
142
+ { type: 'text', text: `Error creating application: ${error}` },
143
+ ],
144
+ };
145
+ }
146
+ });
147
+ server.tool('listTanStackSolidAddOns', {}, () => {
184
148
  return {
185
- content: [
186
- { type: 'text', text: `Error creating application: ${error}` },
187
- ],
149
+ content: [{ type: 'text', text: JSON.stringify(tanStackSolidAddOns) }],
188
150
  };
189
- }
190
- });
191
- export default async function runServer(sse) {
151
+ });
152
+ server.tool('createTanStackSolidApplication', {
153
+ projectName: z
154
+ .string()
155
+ .describe('The package.json module name of the application (will also be the directory name)'),
156
+ cwd: z.string().describe('The directory to create the application in'),
157
+ addOns: z
158
+ .array(z.enum([
159
+ 'solid-ui',
160
+ 'form',
161
+ 'sentry',
162
+ 'store',
163
+ 'tanstack-query',
164
+ 'tanchat',
165
+ ]))
166
+ .describe('The IDs of the add-ons to install'),
167
+ targetDir: z
168
+ .string()
169
+ .describe('The directory to create the application in. Use the absolute path of the directory you want the application to be created in'),
170
+ }, async ({ projectName, addOns, cwd, targetDir }) => {
171
+ try {
172
+ process.chdir(cwd);
173
+ const chosenAddOns = await finalizeAddOns('solid', 'file-router', Array.from(new Set([...addOns, ...forcedAddOns])));
174
+ await createApp({
175
+ projectName: projectName.replace(/^\//, './'),
176
+ framework: 'solid',
177
+ typescript: true,
178
+ tailwind: true,
179
+ packageManager: 'pnpm',
180
+ toolchain: 'none',
181
+ mode: 'file-router',
182
+ addOns: true,
183
+ chosenAddOns,
184
+ git: true,
185
+ variableValues: {},
186
+ }, {
187
+ silent: true,
188
+ environment: createDefaultEnvironment(),
189
+ name,
190
+ cwd: targetDir,
191
+ });
192
+ return {
193
+ content: [{ type: 'text', text: 'Application created successfully' }],
194
+ };
195
+ }
196
+ catch (error) {
197
+ return {
198
+ content: [
199
+ { type: 'text', text: `Error creating application: ${error}` },
200
+ ],
201
+ };
202
+ }
203
+ });
204
+ return server;
205
+ }
206
+ export default async function runServer(sse, { forcedAddOns, appName, name, }) {
207
+ let transport = null;
208
+ const server = createServer({ appName, forcedAddOns, name });
192
209
  if (sse) {
193
210
  const app = express();
194
- let transport = null;
195
211
  app.get('/sse', (req, res) => {
196
212
  transport = new SSEServerTransport('/messages', res);
197
213
  server.connect(transport);
package/dist/options.js CHANGED
@@ -13,9 +13,6 @@ export async function normalizeOptions(cliOptions, forcedAddOns) {
13
13
  if (parseSeparatedArgs.length > 1) {
14
14
  cliOptions.addOns = parseSeparatedArgs;
15
15
  }
16
- if (forcedAddOns) {
17
- cliOptions.addOns = [...cliOptions.addOns, ...forcedAddOns];
18
- }
19
16
  }
20
17
  if (cliOptions.projectName) {
21
18
  let typescript = cliOptions.template === 'typescript' ||
@@ -37,11 +34,17 @@ export async function normalizeOptions(cliOptions, forcedAddOns) {
37
34
  }
38
35
  let addOns = false;
39
36
  let chosenAddOns = [];
40
- if (Array.isArray(cliOptions.addOns) || overlay?.dependsOn) {
37
+ if (Array.isArray(cliOptions.addOns) ||
38
+ overlay?.dependsOn ||
39
+ forcedAddOns) {
41
40
  addOns = true;
42
- let finalAddOns = [...(overlay?.dependsOn || [])];
41
+ let finalAddOns = Array.from(new Set([...(overlay?.dependsOn || []), ...(forcedAddOns || [])]));
43
42
  if (cliOptions.addOns && Array.isArray(cliOptions.addOns)) {
44
- finalAddOns = [...finalAddOns, ...cliOptions.addOns];
43
+ finalAddOns = Array.from(new Set([
44
+ ...(forcedAddOns || []),
45
+ ...finalAddOns,
46
+ ...cliOptions.addOns,
47
+ ]));
45
48
  }
46
49
  chosenAddOns = await finalizeAddOns(cliOptions.framework || DEFAULT_FRAMEWORK, cliOptions.template === 'file-router' ? FILE_ROUTER : CODE_ROUTER, finalAddOns);
47
50
  tailwind = true;
@@ -1,5 +1,8 @@
1
- import type { AddOn, CliOptions, Framework } from './types.js';
1
+ import type { AddOn, CliOptions, Framework, TemplateOptions } from './types.js';
2
2
  export declare function getAllAddOns(framework: Framework, template: string): Promise<Array<AddOn>>;
3
3
  export declare function finalizeAddOns(framework: Framework, template: string, chosenAddOnIDs: Array<string>): Promise<Array<AddOn>>;
4
- export declare function listAddOns(options: CliOptions): Promise<void>;
4
+ export declare function listAddOns(options: CliOptions, { forcedMode, forcedAddOns, }: {
5
+ forcedMode?: TemplateOptions;
6
+ forcedAddOns?: Array<string>;
7
+ }): Promise<void>;
5
8
  export declare function loadRemoteAddOn(url: string): Promise<AddOn>;
@@ -1,5 +1,6 @@
1
- export declare function cli({ name, forcedMode, forcedAddOns, }: {
1
+ export declare function cli({ name, appName, forcedMode, forcedAddOns, }: {
2
2
  name: string;
3
+ appName: string;
3
4
  forcedMode?: 'typescript' | 'javascript' | 'file-router';
4
5
  forcedAddOns?: Array<string>;
5
6
  }): void;
@@ -1,7 +1,8 @@
1
1
  import type { Environment, Options } from './types.js';
2
- export declare function createApp(options: Options, { silent, environment, cwd, name, }: {
2
+ export declare function createApp(options: Options, { silent, environment, cwd, appName, }: {
3
3
  silent?: boolean;
4
4
  environment: Environment;
5
5
  cwd?: string;
6
- name: string;
6
+ name?: string;
7
+ appName?: string;
7
8
  }): Promise<void>;
@@ -0,0 +1,2 @@
1
+ export declare function readFileHelper(path: string): string;
2
+ export declare function getBinaryFile(content: string): string | null;
@@ -1 +1,7 @@
1
- export default function runServer(sse: boolean): Promise<void>;
1
+ import type { TemplateOptions } from './types.js';
2
+ export default function runServer(sse: boolean, { forcedAddOns, appName, name, }: {
3
+ forcedMode?: TemplateOptions;
4
+ forcedAddOns?: Array<string>;
5
+ appName?: string;
6
+ name?: string;
7
+ }): Promise<void>;
@@ -2,6 +2,7 @@ import type { CODE_ROUTER, FILE_ROUTER } from './constants.js';
2
2
  import type { PackageManager } from './package-manager.js';
3
3
  import type { ToolChain } from './toolchain.js';
4
4
  export type Framework = 'solid' | 'react';
5
+ export type TemplateOptions = 'typescript' | 'javascript' | 'file-router';
5
6
  export interface Options {
6
7
  framework: Framework;
7
8
  projectName: string;
@@ -17,7 +18,7 @@ export interface Options {
17
18
  overlay?: AddOn | undefined;
18
19
  }
19
20
  export interface CliOptions {
20
- template?: 'typescript' | 'javascript' | 'file-router';
21
+ template?: TemplateOptions;
21
22
  framework?: Framework;
22
23
  tailwind?: boolean;
23
24
  packageManager?: PackageManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/cta-engine",
3
- "version": "0.10.0-alpha.9",
3
+ "version": "0.11.1",
4
4
  "description": "Tanstack Application Builder Engine",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/add-ons.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import { readFile } from 'node:fs/promises'
2
- import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
2
+ import { existsSync, readdirSync, statSync } from 'node:fs'
3
3
  import { resolve } from 'node:path'
4
4
  import chalk from 'chalk'
5
- import { getTemplatesRoot } from './templates.js'
6
5
 
6
+ import { getTemplatesRoot } from './templates.js'
7
7
  import { DEFAULT_FRAMEWORK } from './constants.js'
8
- import type { AddOn, CliOptions, Framework } from './types.js'
8
+ import { readFileHelper } from './file-helper.js'
9
+
10
+ import type { AddOn, CliOptions, Framework, TemplateOptions } from './types.js'
9
11
 
10
12
  function isDirectory(path: string): boolean {
11
13
  return statSync(path).isDirectory()
@@ -18,7 +20,7 @@ function findFilesRecursively(path: string, files: Record<string, string>) {
18
20
  if (isDirectory(filePath)) {
19
21
  findFilesRecursively(filePath, files)
20
22
  } else {
21
- files[filePath] = readFileSync(filePath, 'utf-8').toString()
23
+ files[filePath] = readFileHelper(filePath)
22
24
  }
23
25
  }
24
26
  }
@@ -121,14 +123,21 @@ export async function finalizeAddOns(
121
123
  return finalAddOns
122
124
  }
123
125
 
124
- export async function listAddOns(options: CliOptions) {
125
- const mode =
126
- options.template === 'file-router' ? 'file-router' : 'code-router'
126
+ export async function listAddOns(
127
+ options: CliOptions,
128
+ {
129
+ forcedMode,
130
+ forcedAddOns = [],
131
+ }: {
132
+ forcedMode?: TemplateOptions
133
+ forcedAddOns?: Array<string>
134
+ },
135
+ ) {
127
136
  const addOns = await getAllAddOns(
128
137
  options.framework || DEFAULT_FRAMEWORK,
129
- mode,
138
+ forcedMode || options.template || 'typescript',
130
139
  )
131
- for (const addOn of addOns) {
140
+ for (const addOn of addOns.filter((a) => !forcedAddOns.includes(a.id))) {
132
141
  console.log(`${chalk.bold(addOn.id)}: ${addOn.description}`)
133
142
  }
134
143
  }
package/src/cli.ts CHANGED
@@ -20,18 +20,18 @@ import type { CliOptions, Framework } from './types.js'
20
20
 
21
21
  export function cli({
22
22
  name,
23
+ appName,
23
24
  forcedMode,
24
25
  forcedAddOns,
25
26
  }: {
26
27
  name: string
28
+ appName: string
27
29
  forcedMode?: 'typescript' | 'javascript' | 'file-router'
28
30
  forcedAddOns?: Array<string>
29
31
  }) {
30
32
  const program = new Command()
31
33
 
32
- program
33
- .name('create-tsrouter-app')
34
- .description('CLI to create a new TanStack application')
34
+ program.name(name).description(`CLI to create a new ${appName} application`)
35
35
 
36
36
  // program
37
37
  // .command('add')
@@ -54,10 +54,27 @@ export function cli({
54
54
  // await initAddOn('overlay')
55
55
  // })
56
56
 
57
+ program.argument('[project-name]', 'name of the project')
58
+
59
+ if (!forcedMode) {
60
+ program.option<'typescript' | 'javascript' | 'file-router'>(
61
+ '--template <type>',
62
+ 'project template (typescript, javascript, file-router)',
63
+ (value) => {
64
+ if (
65
+ value !== 'typescript' &&
66
+ value !== 'javascript' &&
67
+ value !== 'file-router'
68
+ ) {
69
+ throw new InvalidArgumentError(
70
+ `Invalid template: ${value}. Only the following are allowed: typescript, javascript, file-router`,
71
+ )
72
+ }
73
+ return value
74
+ },
75
+ )
76
+ }
57
77
  program
58
- .argument('[project-name]', 'name of the project')
59
- .option('--no-git', 'do not create a git repository')
60
- .option('--target-dir <path>', 'the directory to create the project in')
61
78
  .option<Framework>(
62
79
  '--framework <type>',
63
80
  'project framework (solid, react)',
@@ -73,6 +90,7 @@ export function cli({
73
90
  },
74
91
  DEFAULT_FRAMEWORK,
75
92
  )
93
+ // .option('--overlay [url]', 'add an overlay from a URL', false)
76
94
  .option<PackageManager>(
77
95
  `--package-manager <${SUPPORTED_PACKAGE_MANAGERS.join('|')}>`,
78
96
  `Explicitly tell the CLI to use this package manager`,
@@ -114,34 +132,26 @@ export function cli({
114
132
  },
115
133
  )
116
134
  .option('--list-add-ons', 'list all available add-ons', false)
117
- // .option('--overlay [url]', 'add an overlay from a URL', false)
135
+ .option('--no-git', 'do not create a git repository')
136
+ .option(
137
+ '--target-dir <path>',
138
+ 'the target directory for the application root',
139
+ )
118
140
  .option('--mcp', 'run the MCP server', false)
119
141
  .option('--mcp-sse', 'run the MCP server in SSE mode', false)
120
142
 
121
- if (!forcedMode) {
122
- program.option<'typescript' | 'javascript' | 'file-router'>(
123
- '--template <type>',
124
- 'project template (typescript, javascript, file-router)',
125
- (value) => {
126
- if (
127
- value !== 'typescript' &&
128
- value !== 'javascript' &&
129
- value !== 'file-router'
130
- ) {
131
- throw new InvalidArgumentError(
132
- `Invalid template: ${value}. Only the following are allowed: typescript, javascript, file-router`,
133
- )
134
- }
135
- return value
136
- },
137
- )
138
- }
139
-
140
143
  program.action(async (projectName: string, options: CliOptions) => {
141
144
  if (options.listAddOns) {
142
- await listAddOns(options)
145
+ await listAddOns(options, {
146
+ forcedMode,
147
+ forcedAddOns,
148
+ })
143
149
  } else if (options.mcp || options.mcpSse) {
144
- await runServer(!!options.mcpSse)
150
+ await runServer(!!options.mcpSse, {
151
+ forcedMode,
152
+ forcedAddOns,
153
+ appName,
154
+ })
145
155
  } else {
146
156
  try {
147
157
  const cliOptions = {
@@ -155,9 +165,9 @@ export function cli({
155
165
 
156
166
  let finalOptions = await normalizeOptions(cliOptions, forcedAddOns)
157
167
  if (finalOptions) {
158
- intro(`Creating a new TanStack app in ${projectName}...`)
168
+ intro(`Creating a new ${appName} app in ${projectName}...`)
159
169
  } else {
160
- intro("Let's configure your TanStack application")
170
+ intro(`Let's configure your ${appName} application`)
161
171
  finalOptions = await promptForOptions(cliOptions, {
162
172
  forcedMode,
163
173
  forcedAddOns,
@@ -167,6 +177,7 @@ export function cli({
167
177
  environment: createDefaultEnvironment(),
168
178
  cwd: options.targetDir || undefined,
169
179
  name,
180
+ appName,
170
181
  })
171
182
  } catch (error) {
172
183
  log.error(
package/src/create-app.ts CHANGED
@@ -9,6 +9,7 @@ import { CODE_ROUTER, FILE_ROUTER } from './constants.js'
9
9
  import { sortObject } from './utils.js'
10
10
  import { writeConfigFile } from './config-file.js'
11
11
  import { packageManagerExecute } from './package-manager.js'
12
+ import { getBinaryFile } from './file-helper.js'
12
13
 
13
14
  import type { AddOn, Environment, Options } from './types.js'
14
15
 
@@ -287,6 +288,15 @@ async function copyAddOnFile(
287
288
 
288
289
  const finalTargetPath = resolve(dirname(targetPath), targetFile)
289
290
 
291
+ const binaryContent = getBinaryFile(content)
292
+ if (binaryContent) {
293
+ await environment.writeFile(
294
+ finalTargetPath,
295
+ binaryContent as unknown as string,
296
+ )
297
+ return
298
+ }
299
+
290
300
  if (isTemplate) {
291
301
  await templateFile(content, finalTargetPath)
292
302
  } else {
@@ -304,12 +314,13 @@ export async function createApp(
304
314
  silent = false,
305
315
  environment,
306
316
  cwd,
307
- name = 'create-tsrouter-app',
317
+ appName = 'TanStack',
308
318
  }: {
309
319
  silent?: boolean
310
320
  environment: Environment
311
321
  cwd?: string
312
- name: string
322
+ name?: string
323
+ appName?: string
313
324
  },
314
325
  ) {
315
326
  environment.startRun()
@@ -498,7 +509,7 @@ export async function createApp(
498
509
  // Copy all the asset files from the addons
499
510
  const s = silent ? null : spinner()
500
511
  for (const type of ['add-on', 'example']) {
501
- for (const phase of ['setup', 'add-on']) {
512
+ for (const phase of ['setup', 'add-on', 'example']) {
502
513
  for (const addOn of options.chosenAddOns.filter(
503
514
  (addOn) => addOn.phase === phase && addOn.type === type,
504
515
  )) {
@@ -782,7 +793,7 @@ ${environment.getErrors().join('\n')}`
782
793
  startCommand = `deno ${isAddOnEnabled('start') ? 'task dev' : 'start'}`
783
794
  }
784
795
 
785
- outro(`Your TanStack app is ready in '${basename(targetDir)}'.
796
+ outro(`Your ${appName} app is ready in '${basename(targetDir)}'.
786
797
 
787
798
  Use the following commands to start your app:
788
799
  % cd ${options.projectName}
@@ -7,6 +7,7 @@ import { createMemoryEnvironment } from './environment.js'
7
7
  import { createApp } from './create-app.js'
8
8
  import { readConfigFile } from './config-file.js'
9
9
  import { finalizeAddOns } from './add-ons.js'
10
+ import { readFileHelper } from './file-helper.js'
10
11
 
11
12
  import type { Framework, Options } from './types.js'
12
13
  import type { PersistedOptions } from './config-file.js'
@@ -134,9 +135,7 @@ async function recursivelyGatherFiles(
134
135
  if (file.isDirectory()) {
135
136
  await recursivelyGatherFiles(resolve(path, file.name), files)
136
137
  } else {
137
- files[resolve(path, file.name)] = (
138
- await readFile(resolve(path, file.name))
139
- ).toString()
138
+ files[resolve(path, file.name)] = readFileHelper(resolve(path, file.name))
140
139
  }
141
140
  }
142
141
  }
@@ -0,0 +1,20 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { extname } from 'node:path'
3
+
4
+ const BINARY_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico']
5
+
6
+ export function readFileHelper(path: string): string {
7
+ if (BINARY_EXTENSIONS.includes(extname(path))) {
8
+ return `base64::${readFileSync(path).toString('base64')}`
9
+ } else {
10
+ return readFileSync(path, 'utf-8').toString()
11
+ }
12
+ }
13
+
14
+ export function getBinaryFile(content: string): string | null {
15
+ if (content.startsWith('base64::')) {
16
+ const binaryContent = Buffer.from(content.replace('base64::', ''), 'base64')
17
+ return binaryContent as unknown as string
18
+ }
19
+ return null
20
+ }
package/src/mcp.ts CHANGED
@@ -8,10 +8,7 @@ import { createApp } from './create-app.js'
8
8
  import { finalizeAddOns } from './add-ons.js'
9
9
  import { createDefaultEnvironment } from './environment.js'
10
10
 
11
- const server = new McpServer({
12
- name: 'Demo',
13
- version: '1.0.0',
14
- })
11
+ import type { TemplateOptions } from './types.js'
15
12
 
16
13
  const tanStackReactAddOns = [
17
14
  {
@@ -57,79 +54,6 @@ const tanStackReactAddOns = [
57
54
  },
58
55
  ]
59
56
 
60
- server.tool('listTanStackReactAddOns', {}, () => {
61
- return {
62
- content: [{ type: 'text', text: JSON.stringify(tanStackReactAddOns) }],
63
- }
64
- })
65
-
66
- server.tool(
67
- 'createTanStackReactApplication',
68
- {
69
- projectName: z
70
- .string()
71
- .describe(
72
- 'The package.json module name of the application (will also be the directory name)',
73
- ),
74
- cwd: z.string().describe('The directory to create the application in'),
75
- addOns: z
76
- .array(
77
- z.enum([
78
- 'clerk',
79
- 'convex',
80
- 'form',
81
- 'netlify',
82
- 'sentry',
83
- 'shadcn',
84
- 'start',
85
- 'store',
86
- 'tanstack-query',
87
- 'tanchat',
88
- ]),
89
- )
90
- .describe('The IDs of the add-ons to install'),
91
- },
92
- async ({ projectName, addOns, cwd }) => {
93
- try {
94
- process.chdir(cwd)
95
- const chosenAddOns = await finalizeAddOns(
96
- 'react',
97
- 'file-router',
98
- addOns as unknown as Array<string>,
99
- )
100
- await createApp(
101
- {
102
- projectName: projectName.replace(/^\//, './'),
103
- framework: 'react',
104
- typescript: true,
105
- tailwind: true,
106
- packageManager: 'pnpm',
107
- toolchain: 'none',
108
- mode: 'file-router',
109
- addOns: true,
110
- chosenAddOns,
111
- git: true,
112
- variableValues: {},
113
- },
114
- {
115
- silent: true,
116
- environment: createDefaultEnvironment(),
117
- name: 'create-tsrouter-app',
118
- },
119
- )
120
- return {
121
- content: [{ type: 'text', text: 'Application created successfully' }],
122
- }
123
- } catch (error) {
124
- return {
125
- content: [
126
- { type: 'text', text: `Error creating application: ${error}` },
127
- ],
128
- }
129
- }
130
- },
131
- )
132
-
133
57
  const tanStackSolidAddOns = [
134
58
  {
135
59
  id: 'solid-ui',
@@ -147,6 +71,11 @@ const tanStackSolidAddOns = [
147
71
  id: 'store',
148
72
  description: 'Enable the TanStack Store state management library',
149
73
  },
74
+ {
75
+ id: 'start',
76
+ description:
77
+ 'Set this if you want a TanStack Start application that supports server functions or APIs',
78
+ },
150
79
  {
151
80
  id: 'tanstack-query',
152
81
  description: 'Enable TanStack Query for data fetching',
@@ -157,81 +86,200 @@ const tanStackSolidAddOns = [
157
86
  },
158
87
  ]
159
88
 
160
- server.tool('listTanStackSolidAddOns', {}, () => {
161
- return {
162
- content: [{ type: 'text', text: JSON.stringify(tanStackSolidAddOns) }],
163
- }
164
- })
89
+ function createServer({
90
+ appName,
91
+ forcedAddOns = [],
92
+ name,
93
+ }: {
94
+ appName?: string
95
+ forcedAddOns?: Array<string>
96
+ name?: string
97
+ }) {
98
+ const server = new McpServer({
99
+ name: `${appName} Application Builder`,
100
+ version: '1.0.0',
101
+ })
165
102
 
166
- server.tool(
167
- 'createTanStackSolidApplication',
168
- {
169
- projectName: z
170
- .string()
171
- .describe(
172
- 'The package.json module name of the application (will also be the directory name)',
173
- ),
174
- cwd: z.string().describe('The directory to create the application in'),
175
- addOns: z
176
- .array(
177
- z.enum([
178
- 'solid-ui',
179
- 'form',
180
- 'sentry',
181
- 'store',
182
- 'tanstack-query',
183
- 'tanchat',
184
- ]),
185
- )
186
- .describe('The IDs of the add-ons to install'),
187
- },
188
- async ({ projectName, addOns, cwd }) => {
189
- try {
190
- process.chdir(cwd)
191
- const chosenAddOns = await finalizeAddOns(
192
- 'solid',
193
- 'file-router',
194
- addOns as unknown as Array<string>,
195
- )
196
- await createApp(
197
- {
198
- projectName: projectName.replace(/^\//, './'),
199
- framework: 'solid',
200
- typescript: true,
201
- tailwind: true,
202
- packageManager: 'pnpm',
203
- toolchain: 'none',
204
- mode: 'file-router',
205
- addOns: true,
206
- chosenAddOns,
207
- git: true,
208
- variableValues: {},
209
- },
210
- {
211
- silent: true,
212
- environment: createDefaultEnvironment(),
213
- name: 'create-tsrouter-app',
214
- },
215
- )
216
- return {
217
- content: [{ type: 'text', text: 'Application created successfully' }],
218
- }
219
- } catch (error) {
220
- return {
221
- content: [
222
- { type: 'text', text: `Error creating application: ${error}` },
223
- ],
103
+ server.tool('listTanStackReactAddOns', {}, () => {
104
+ return {
105
+ content: [{ type: 'text', text: JSON.stringify(tanStackReactAddOns) }],
106
+ }
107
+ })
108
+
109
+ server.tool(
110
+ 'createTanStackReactApplication',
111
+ {
112
+ projectName: z
113
+ .string()
114
+ .describe(
115
+ 'The package.json module name of the application (will also be the directory name)',
116
+ ),
117
+ cwd: z.string().describe('The directory to create the application in'),
118
+ addOns: z
119
+ .array(
120
+ z.enum([
121
+ 'clerk',
122
+ 'convex',
123
+ 'form',
124
+ 'netlify',
125
+ 'sentry',
126
+ 'shadcn',
127
+ 'start',
128
+ 'store',
129
+ 'tanstack-query',
130
+ 'tanchat',
131
+ ]),
132
+ )
133
+ .describe('The IDs of the add-ons to install'),
134
+ targetDir: z
135
+ .string()
136
+ .describe(
137
+ 'The directory to create the application in. Use the absolute path of the directory you want the application to be created in',
138
+ ),
139
+ },
140
+ async ({ projectName, addOns, cwd, targetDir }) => {
141
+ try {
142
+ process.chdir(cwd)
143
+ const chosenAddOns = await finalizeAddOns(
144
+ 'react',
145
+ 'file-router',
146
+ Array.from(
147
+ new Set([...(addOns as unknown as Array<string>), ...forcedAddOns]),
148
+ ),
149
+ )
150
+ await createApp(
151
+ {
152
+ projectName: projectName.replace(/^\//, './'),
153
+ framework: 'react',
154
+ typescript: true,
155
+ tailwind: true,
156
+ packageManager: 'pnpm',
157
+ toolchain: 'none',
158
+ mode: 'file-router',
159
+ addOns: true,
160
+ chosenAddOns,
161
+ git: true,
162
+ variableValues: {},
163
+ },
164
+ {
165
+ silent: true,
166
+ environment: createDefaultEnvironment(),
167
+ name,
168
+ cwd: targetDir,
169
+ },
170
+ )
171
+ return {
172
+ content: [{ type: 'text', text: 'Application created successfully' }],
173
+ }
174
+ } catch (error) {
175
+ return {
176
+ content: [
177
+ { type: 'text', text: `Error creating application: ${error}` },
178
+ ],
179
+ }
224
180
  }
181
+ },
182
+ )
183
+
184
+ server.tool('listTanStackSolidAddOns', {}, () => {
185
+ return {
186
+ content: [{ type: 'text', text: JSON.stringify(tanStackSolidAddOns) }],
225
187
  }
188
+ })
189
+
190
+ server.tool(
191
+ 'createTanStackSolidApplication',
192
+ {
193
+ projectName: z
194
+ .string()
195
+ .describe(
196
+ 'The package.json module name of the application (will also be the directory name)',
197
+ ),
198
+ cwd: z.string().describe('The directory to create the application in'),
199
+ addOns: z
200
+ .array(
201
+ z.enum([
202
+ 'solid-ui',
203
+ 'form',
204
+ 'sentry',
205
+ 'store',
206
+ 'tanstack-query',
207
+ 'tanchat',
208
+ ]),
209
+ )
210
+ .describe('The IDs of the add-ons to install'),
211
+ targetDir: z
212
+ .string()
213
+ .describe(
214
+ 'The directory to create the application in. Use the absolute path of the directory you want the application to be created in',
215
+ ),
216
+ },
217
+ async ({ projectName, addOns, cwd, targetDir }) => {
218
+ try {
219
+ process.chdir(cwd)
220
+ const chosenAddOns = await finalizeAddOns(
221
+ 'solid',
222
+ 'file-router',
223
+ Array.from(
224
+ new Set([...(addOns as unknown as Array<string>), ...forcedAddOns]),
225
+ ),
226
+ )
227
+ await createApp(
228
+ {
229
+ projectName: projectName.replace(/^\//, './'),
230
+ framework: 'solid',
231
+ typescript: true,
232
+ tailwind: true,
233
+ packageManager: 'pnpm',
234
+ toolchain: 'none',
235
+ mode: 'file-router',
236
+ addOns: true,
237
+ chosenAddOns,
238
+ git: true,
239
+ variableValues: {},
240
+ },
241
+ {
242
+ silent: true,
243
+ environment: createDefaultEnvironment(),
244
+ name,
245
+ cwd: targetDir,
246
+ },
247
+ )
248
+ return {
249
+ content: [{ type: 'text', text: 'Application created successfully' }],
250
+ }
251
+ } catch (error) {
252
+ return {
253
+ content: [
254
+ { type: 'text', text: `Error creating application: ${error}` },
255
+ ],
256
+ }
257
+ }
258
+ },
259
+ )
260
+
261
+ return server
262
+ }
263
+
264
+ export default async function runServer(
265
+ sse: boolean,
266
+ {
267
+ forcedAddOns,
268
+ appName,
269
+ name,
270
+ }: {
271
+ forcedMode?: TemplateOptions
272
+ forcedAddOns?: Array<string>
273
+ appName?: string
274
+ name?: string
226
275
  },
227
- )
276
+ ) {
277
+ let transport: SSEServerTransport | null = null
228
278
 
229
- export default async function runServer(sse: boolean) {
279
+ const server = createServer({ appName, forcedAddOns, name })
230
280
  if (sse) {
231
281
  const app = express()
232
282
 
233
- let transport: SSEServerTransport | null = null
234
-
235
283
  app.get('/sse', (req, res) => {
236
284
  transport = new SSEServerTransport('/messages', res)
237
285
  server.connect(transport)
package/src/options.ts CHANGED
@@ -31,10 +31,8 @@ export async function normalizeOptions(
31
31
  if (parseSeparatedArgs.length > 1) {
32
32
  cliOptions.addOns = parseSeparatedArgs
33
33
  }
34
- if (forcedAddOns) {
35
- cliOptions.addOns = [...cliOptions.addOns, ...forcedAddOns]
36
- }
37
34
  }
35
+
38
36
  if (cliOptions.projectName) {
39
37
  let typescript =
40
38
  cliOptions.template === 'typescript' ||
@@ -62,11 +60,23 @@ export async function normalizeOptions(
62
60
 
63
61
  let addOns = false
64
62
  let chosenAddOns: Array<AddOn> = []
65
- if (Array.isArray(cliOptions.addOns) || overlay?.dependsOn) {
63
+ if (
64
+ Array.isArray(cliOptions.addOns) ||
65
+ overlay?.dependsOn ||
66
+ forcedAddOns
67
+ ) {
66
68
  addOns = true
67
- let finalAddOns = [...(overlay?.dependsOn || [])]
69
+ let finalAddOns = Array.from(
70
+ new Set([...(overlay?.dependsOn || []), ...(forcedAddOns || [])]),
71
+ )
68
72
  if (cliOptions.addOns && Array.isArray(cliOptions.addOns)) {
69
- finalAddOns = [...finalAddOns, ...cliOptions.addOns]
73
+ finalAddOns = Array.from(
74
+ new Set([
75
+ ...(forcedAddOns || []),
76
+ ...finalAddOns,
77
+ ...cliOptions.addOns,
78
+ ]),
79
+ )
70
80
  }
71
81
  chosenAddOns = await finalizeAddOns(
72
82
  cliOptions.framework || DEFAULT_FRAMEWORK,
package/src/types.ts CHANGED
@@ -4,6 +4,8 @@ import type { ToolChain } from './toolchain.js'
4
4
 
5
5
  export type Framework = 'solid' | 'react'
6
6
 
7
+ export type TemplateOptions = 'typescript' | 'javascript' | 'file-router'
8
+
7
9
  export interface Options {
8
10
  framework: Framework
9
11
  projectName: string
@@ -20,7 +22,7 @@ export interface Options {
20
22
  }
21
23
 
22
24
  export interface CliOptions {
23
- template?: 'typescript' | 'javascript' | 'file-router'
25
+ template?: TemplateOptions
24
26
  framework?: Framework
25
27
  tailwind?: boolean
26
28
  packageManager?: PackageManager
@@ -13,7 +13,7 @@ function getUrl() {
13
13
  if (typeof window !== "undefined") return "";
14
14
  return `http://localhost:${process.env.PORT ?? 3000}`;
15
15
  })();
16
- return base + "/api/trpc";
16
+ return `${base}/api/trpc`;
17
17
  }
18
18
 
19
19
  export const trpcClient = createTRPCClient<TRPCRouter>({
@@ -67,4 +67,4 @@ export function Provider({ children }: { children: React.ReactNode }) {
67
67
  <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
68
68
  )
69
69
  }
70
- <% } %>
70
+ <% } %>