capstart 0.1.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/LICENSE +21 -0
- package/README.md +145 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +957 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adrien Villermois
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Capstart CLI
|
|
2
|
+
|
|
3
|
+
Add Capacitor to an existing Next.js or TanStack Start application.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx capstart init ..
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Capstart detects the framework and package manager, configures a static or SPA
|
|
10
|
+
build, installs Capacitor, adds native projects, builds the web application, and
|
|
11
|
+
runs `cap sync`.
|
|
12
|
+
|
|
13
|
+
Installation, build, and Capacitor command output is hidden by default. Capstart
|
|
14
|
+
shows a single setup progress line and only prints a short command summary when
|
|
15
|
+
something fails.
|
|
16
|
+
|
|
17
|
+
Each main installation operation has its own step:
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
◇ Configure Next.js
|
|
21
|
+
◇ Configure Capacitor
|
|
22
|
+
◇ Install Capacitor packages
|
|
23
|
+
◇ Build the web app
|
|
24
|
+
◇ Prepare iOS and Android projects
|
|
25
|
+
◇ Synchronize native projects
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The commands executed inside each step remain hidden.
|
|
29
|
+
|
|
30
|
+
The detected framework is always shown and must be confirmed before Capstart
|
|
31
|
+
changes the project:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
✓ Detected Next.js
|
|
35
|
+
? Use the detected framework Next.js? Yes
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
If the detection is refused, Capstart lets you choose between Next.js and
|
|
39
|
+
TanStack Start.
|
|
40
|
+
|
|
41
|
+
## Supported frameworks
|
|
42
|
+
|
|
43
|
+
- Next.js projects that can use static export
|
|
44
|
+
- TanStack Start projects that can use SPA mode
|
|
45
|
+
|
|
46
|
+
Server-only features must remain hosted remotely and be called from the mobile
|
|
47
|
+
application over HTTP.
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx capstart init [directory] [options]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Examples:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx capstart init .
|
|
59
|
+
npx capstart init ../my-app --app-id com.example.myapp
|
|
60
|
+
npx capstart init . --platforms ios
|
|
61
|
+
npx capstart init . --framework tanstack-start --dry-run
|
|
62
|
+
npx capstart init . --yes
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Useful options:
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
--framework <nextjs|tanstack-start>
|
|
69
|
+
--app-id <id>
|
|
70
|
+
--app-name <name>
|
|
71
|
+
--platforms <ios,android>
|
|
72
|
+
--skip-install
|
|
73
|
+
--skip-build
|
|
74
|
+
--skip-native
|
|
75
|
+
--dry-run
|
|
76
|
+
--yes
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Use `--yes` to accept a single automatically detected framework in CI or other
|
|
80
|
+
non-interactive environments. Use `--framework` to bypass detection
|
|
81
|
+
confirmation and select an adapter explicitly.
|
|
82
|
+
|
|
83
|
+
After a successful interactive initialization, Capstart detects whether GitHub
|
|
84
|
+
CLI is installed and optionally proposes starring
|
|
85
|
+
[AdrienADV/capstart](https://github.com/AdrienADV/capstart). The repository is
|
|
86
|
+
only starred after explicit confirmation, and this step never runs in CI.
|
|
87
|
+
|
|
88
|
+
The final output includes:
|
|
89
|
+
|
|
90
|
+
- the scripts added to `package.json` and a short explanation of each one;
|
|
91
|
+
- recommended Capacitor packages and production guidance at
|
|
92
|
+
[capstart.dev/docs/installation/#3-add-recommended-capacitor-base-plugins](https://capstart.dev/docs/installation/#3-add-recommended-capacitor-base-plugins);
|
|
93
|
+
- an `Important` section explaining which framework server features must remain
|
|
94
|
+
remotely hosted.
|
|
95
|
+
|
|
96
|
+
Example:
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
Ready
|
|
100
|
+
✓ Your base Capacitor setup is ready.
|
|
101
|
+
|
|
102
|
+
Scripts added
|
|
103
|
+
npm run cap:sync
|
|
104
|
+
Build the web app and sync the native projects.
|
|
105
|
+
npm run cap:ios
|
|
106
|
+
Build, sync, and open the iOS project in Xcode.
|
|
107
|
+
npm run cap:android
|
|
108
|
+
Build, sync, and open the Android project in Android Studio.
|
|
109
|
+
|
|
110
|
+
Next steps
|
|
111
|
+
• Review recommended plugins, native configuration, and production setup:
|
|
112
|
+
https://capstart.dev/docs/installation/#3-add-recommended-capacitor-base-plugins
|
|
113
|
+
|
|
114
|
+
Important
|
|
115
|
+
! Next.js request-time features do not run inside the Capacitor app.
|
|
116
|
+
• Replace request-time Server Components and Server Actions with client-side
|
|
117
|
+
calls to API endpoints.
|
|
118
|
+
• Deploy those APIs, API routes, middleware, ISR, and other request-time logic
|
|
119
|
+
on a remote backend.
|
|
120
|
+
• Configure the mobile app with an HTTPS API base URL that is reachable from
|
|
121
|
+
the device.
|
|
122
|
+
• Do not use "localhost" for the backend URL: on a phone or emulator, it
|
|
123
|
+
points to the device itself.
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
After initialization:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
npm run cap:sync
|
|
130
|
+
npm run cap:ios
|
|
131
|
+
npm run cap:android
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The exact package-manager prefix is generated for npm, pnpm, Yarn, or Bun.
|
|
135
|
+
|
|
136
|
+
## Development
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
cd cli
|
|
140
|
+
npm install
|
|
141
|
+
npm run typecheck
|
|
142
|
+
npm test
|
|
143
|
+
npm run build
|
|
144
|
+
node dist/cli.js --help
|
|
145
|
+
```
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,957 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command, InvalidArgumentError } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/adapters/nextjs.ts
|
|
7
|
+
import { writeFile as writeFile2 } from "fs/promises";
|
|
8
|
+
import path3 from "path";
|
|
9
|
+
|
|
10
|
+
// src/core/ast.ts
|
|
11
|
+
import path2 from "path";
|
|
12
|
+
import {
|
|
13
|
+
Node,
|
|
14
|
+
Project,
|
|
15
|
+
SyntaxKind
|
|
16
|
+
} from "ts-morph";
|
|
17
|
+
|
|
18
|
+
// src/core/project.ts
|
|
19
|
+
import { access, readFile, writeFile } from "fs/promises";
|
|
20
|
+
import path from "path";
|
|
21
|
+
var lockfiles = [
|
|
22
|
+
["bun.lock", "bun"],
|
|
23
|
+
["bun.lockb", "bun"],
|
|
24
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
25
|
+
["yarn.lock", "yarn"],
|
|
26
|
+
["package-lock.json", "npm"]
|
|
27
|
+
];
|
|
28
|
+
async function pathExists(filePath) {
|
|
29
|
+
try {
|
|
30
|
+
await access(filePath);
|
|
31
|
+
return true;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function loadProject(directory) {
|
|
37
|
+
const root = path.resolve(directory);
|
|
38
|
+
const packageJsonPath = path.join(root, "package.json");
|
|
39
|
+
if (!await pathExists(packageJsonPath)) {
|
|
40
|
+
throw new Error(`No package.json found in ${root}`);
|
|
41
|
+
}
|
|
42
|
+
const packageJson = JSON.parse(
|
|
43
|
+
await readFile(packageJsonPath, "utf8")
|
|
44
|
+
);
|
|
45
|
+
return {
|
|
46
|
+
root,
|
|
47
|
+
packageJsonPath,
|
|
48
|
+
packageJson,
|
|
49
|
+
packageManager: await detectPackageManager(root, packageJson)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async function savePackageJson(project, dryRun) {
|
|
53
|
+
if (dryRun) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
await writeFile(
|
|
57
|
+
project.packageJsonPath,
|
|
58
|
+
`${JSON.stringify(project.packageJson, null, 2)}
|
|
59
|
+
`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
function hasDependency(project, dependency) {
|
|
63
|
+
return Boolean(
|
|
64
|
+
project.packageJson.dependencies?.[dependency] ?? project.packageJson.devDependencies?.[dependency]
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
function getProjectName(project) {
|
|
68
|
+
const rawName = project.packageJson.name ?? path.basename(project.root);
|
|
69
|
+
return rawName.replace(/^@[^/]+\//, "");
|
|
70
|
+
}
|
|
71
|
+
function createDefaultAppId(project) {
|
|
72
|
+
const slug = getProjectName(project).toLowerCase().replace(/[^a-z0-9]+/g, "").replace(/^[^a-z]+/, "");
|
|
73
|
+
return `com.capstart.${slug || "app"}`;
|
|
74
|
+
}
|
|
75
|
+
async function detectPackageManager(root, packageJson) {
|
|
76
|
+
for (const [lockfile, packageManager] of lockfiles) {
|
|
77
|
+
if (await pathExists(path.join(root, lockfile))) {
|
|
78
|
+
return packageManager;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (typeof packageJson.packageManager === "string") {
|
|
82
|
+
const packageManager = packageJson.packageManager.split("@")[0];
|
|
83
|
+
if (packageManager === "npm" || packageManager === "pnpm" || packageManager === "yarn" || packageManager === "bun") {
|
|
84
|
+
return packageManager;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return "npm";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/core/ast.ts
|
|
91
|
+
async function findConfigFile(root, names) {
|
|
92
|
+
for (const name of names) {
|
|
93
|
+
const filePath = path2.join(root, name);
|
|
94
|
+
if (await pathExists(filePath)) {
|
|
95
|
+
return filePath;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
function loadSourceFile(filePath) {
|
|
101
|
+
const project = new Project({
|
|
102
|
+
skipAddingFilesFromTsConfig: true
|
|
103
|
+
});
|
|
104
|
+
return project.addSourceFileAtPath(filePath);
|
|
105
|
+
}
|
|
106
|
+
function findExportedObject(sourceFile) {
|
|
107
|
+
for (const exportAssignment of sourceFile.getExportAssignments()) {
|
|
108
|
+
const expression = exportAssignment.getExpression();
|
|
109
|
+
const object = resolveObjectExpression(expression);
|
|
110
|
+
if (object) {
|
|
111
|
+
return object;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const binary of sourceFile.getDescendantsOfKind(
|
|
115
|
+
SyntaxKind.BinaryExpression
|
|
116
|
+
)) {
|
|
117
|
+
if (binary.getLeft().getText() === "module.exports") {
|
|
118
|
+
const object = resolveObjectExpression(binary.getRight());
|
|
119
|
+
if (object) {
|
|
120
|
+
return object;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return void 0;
|
|
125
|
+
}
|
|
126
|
+
function setObjectProperty(object, name, initializer) {
|
|
127
|
+
const property = object.getProperty(name);
|
|
128
|
+
if (!property) {
|
|
129
|
+
object.addPropertyAssignment({ name, initializer });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (Node.isPropertyAssignment(property)) {
|
|
133
|
+
property.setInitializer(initializer);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
throw new Error(`Cannot safely update the "${name}" configuration property.`);
|
|
137
|
+
}
|
|
138
|
+
function getOrCreateNestedObject(object, name) {
|
|
139
|
+
const property = object.getProperty(name);
|
|
140
|
+
if (!property) {
|
|
141
|
+
const created = object.addPropertyAssignment({
|
|
142
|
+
name,
|
|
143
|
+
initializer: "{}"
|
|
144
|
+
});
|
|
145
|
+
const initializer = created.getInitializer();
|
|
146
|
+
if (!Node.isObjectLiteralExpression(initializer)) {
|
|
147
|
+
throw new Error(`Could not create the "${name}" configuration property.`);
|
|
148
|
+
}
|
|
149
|
+
return initializer;
|
|
150
|
+
}
|
|
151
|
+
if (Node.isPropertyAssignment(property)) {
|
|
152
|
+
const initializer = property.getInitializer();
|
|
153
|
+
if (Node.isObjectLiteralExpression(initializer)) {
|
|
154
|
+
return initializer;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
throw new Error(`Cannot safely merge the "${name}" configuration property.`);
|
|
158
|
+
}
|
|
159
|
+
function resolveObjectExpression(expression) {
|
|
160
|
+
if (Node.isObjectLiteralExpression(expression)) {
|
|
161
|
+
return expression;
|
|
162
|
+
}
|
|
163
|
+
if (Node.isIdentifier(expression)) {
|
|
164
|
+
const declaration = expression.getDefinitions()[0]?.getDeclarationNode();
|
|
165
|
+
if (Node.isVariableDeclaration(declaration)) {
|
|
166
|
+
const initializer = declaration.getInitializer();
|
|
167
|
+
if (Node.isObjectLiteralExpression(initializer)) {
|
|
168
|
+
return initializer;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return void 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/adapters/nextjs.ts
|
|
176
|
+
var configNames = [
|
|
177
|
+
"next.config.ts",
|
|
178
|
+
"next.config.mts",
|
|
179
|
+
"next.config.mjs",
|
|
180
|
+
"next.config.js",
|
|
181
|
+
"next.config.cjs"
|
|
182
|
+
];
|
|
183
|
+
var nextjsAdapter = {
|
|
184
|
+
id: "nextjs",
|
|
185
|
+
label: "Next.js",
|
|
186
|
+
webDir: "out",
|
|
187
|
+
detect(project) {
|
|
188
|
+
return hasDependency(project, "next");
|
|
189
|
+
},
|
|
190
|
+
async validate(project) {
|
|
191
|
+
const diagnostics = [];
|
|
192
|
+
if (!project.packageJson.scripts?.build) {
|
|
193
|
+
diagnostics.push({
|
|
194
|
+
level: "error",
|
|
195
|
+
message: 'Missing a "build" script in package.json.'
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const configPath = await findConfigFile(project.root, configNames);
|
|
199
|
+
if (configPath) {
|
|
200
|
+
const sourceFile = loadSourceFile(configPath);
|
|
201
|
+
if (!findExportedObject(sourceFile)) {
|
|
202
|
+
diagnostics.push({
|
|
203
|
+
level: "error",
|
|
204
|
+
message: "The existing Next.js config is too dynamic to update safely. Export a config object before running Capstart."
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return diagnostics;
|
|
209
|
+
},
|
|
210
|
+
async configure(project, dryRun) {
|
|
211
|
+
const disclaimers = [
|
|
212
|
+
{
|
|
213
|
+
title: "Next.js request-time features do not run inside the Capacitor app.",
|
|
214
|
+
details: [
|
|
215
|
+
"Replace request-time Server Components and Server Actions with client-side calls to API endpoints.",
|
|
216
|
+
"Deploy those APIs, API routes, middleware, ISR, and other request-time logic on a remote backend.",
|
|
217
|
+
"Configure the mobile app with an HTTPS API base URL that is reachable from the device.",
|
|
218
|
+
'Do not use "localhost" for the backend URL: on a phone or emulator, it points to the device itself.'
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
];
|
|
222
|
+
const configPath = await findConfigFile(project.root, configNames);
|
|
223
|
+
if (!configPath) {
|
|
224
|
+
const newConfigPath = path3.join(project.root, "next.config.mjs");
|
|
225
|
+
if (!dryRun) {
|
|
226
|
+
await writeFile2(
|
|
227
|
+
newConfigPath,
|
|
228
|
+
[
|
|
229
|
+
"/** @type {import('next').NextConfig} */",
|
|
230
|
+
"const nextConfig = {",
|
|
231
|
+
' output: "export",',
|
|
232
|
+
" trailingSlash: true,",
|
|
233
|
+
" images: {",
|
|
234
|
+
" unoptimized: true,",
|
|
235
|
+
" },",
|
|
236
|
+
"};",
|
|
237
|
+
"",
|
|
238
|
+
"export default nextConfig;",
|
|
239
|
+
""
|
|
240
|
+
].join("\n")
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
return { disclaimers };
|
|
244
|
+
}
|
|
245
|
+
const sourceFile = loadSourceFile(configPath);
|
|
246
|
+
const config = findExportedObject(sourceFile);
|
|
247
|
+
if (!config) {
|
|
248
|
+
throw new Error("Could not safely update the existing Next.js config.");
|
|
249
|
+
}
|
|
250
|
+
setObjectProperty(config, "output", '"export"');
|
|
251
|
+
setObjectProperty(config, "trailingSlash", "true");
|
|
252
|
+
const images = getOrCreateNestedObject(config, "images");
|
|
253
|
+
setObjectProperty(images, "unoptimized", "true");
|
|
254
|
+
if (!dryRun) {
|
|
255
|
+
await sourceFile.save();
|
|
256
|
+
}
|
|
257
|
+
return { disclaimers };
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// src/adapters/tanstack-start.ts
|
|
262
|
+
import { Node as Node2, SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
263
|
+
var configNames2 = [
|
|
264
|
+
"vite.config.ts",
|
|
265
|
+
"vite.config.mts",
|
|
266
|
+
"vite.config.mjs",
|
|
267
|
+
"vite.config.js"
|
|
268
|
+
];
|
|
269
|
+
var tanstackStartAdapter = {
|
|
270
|
+
id: "tanstack-start",
|
|
271
|
+
label: "TanStack Start",
|
|
272
|
+
webDir: "dist/client",
|
|
273
|
+
detect(project) {
|
|
274
|
+
return hasDependency(project, "@tanstack/react-start");
|
|
275
|
+
},
|
|
276
|
+
async validate(project) {
|
|
277
|
+
const diagnostics = [];
|
|
278
|
+
if (!project.packageJson.scripts?.build) {
|
|
279
|
+
diagnostics.push({
|
|
280
|
+
level: "error",
|
|
281
|
+
message: 'Missing a "build" script in package.json.'
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const configPath = await findConfigFile(project.root, configNames2);
|
|
285
|
+
if (!configPath) {
|
|
286
|
+
diagnostics.push({
|
|
287
|
+
level: "error",
|
|
288
|
+
message: "Could not find a Vite configuration file."
|
|
289
|
+
});
|
|
290
|
+
return diagnostics;
|
|
291
|
+
}
|
|
292
|
+
const call = findTanstackStartCall(configPath);
|
|
293
|
+
if (!call) {
|
|
294
|
+
diagnostics.push({
|
|
295
|
+
level: "error",
|
|
296
|
+
message: "Could not find tanstackStart() in the Vite configuration file."
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return diagnostics;
|
|
300
|
+
},
|
|
301
|
+
async configure(project, dryRun) {
|
|
302
|
+
const configPath = await findConfigFile(project.root, configNames2);
|
|
303
|
+
if (!configPath) {
|
|
304
|
+
throw new Error("Could not find a Vite configuration file.");
|
|
305
|
+
}
|
|
306
|
+
const sourceFile = loadSourceFile(configPath);
|
|
307
|
+
const call = sourceFile.getDescendantsOfKind(SyntaxKind2.CallExpression).find(
|
|
308
|
+
(candidate) => candidate.getExpression().getText() === "tanstackStart"
|
|
309
|
+
);
|
|
310
|
+
if (!call) {
|
|
311
|
+
throw new Error("Could not find tanstackStart() in the Vite config.");
|
|
312
|
+
}
|
|
313
|
+
let options = call.getArguments()[0];
|
|
314
|
+
if (!options) {
|
|
315
|
+
call.addArgument("{}");
|
|
316
|
+
options = call.getArguments()[0];
|
|
317
|
+
}
|
|
318
|
+
if (!Node2.isObjectLiteralExpression(options)) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
"Cannot safely update tanstackStart() because its options are not an object literal."
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
const spa = getOrCreateNestedObject(options, "spa");
|
|
324
|
+
setObjectProperty(spa, "enabled", "true");
|
|
325
|
+
const prerender = getOrCreateNestedObject(spa, "prerender");
|
|
326
|
+
setObjectProperty(prerender, "outputPath", '"/index.html"');
|
|
327
|
+
if (!dryRun) {
|
|
328
|
+
await sourceFile.save();
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
disclaimers: [
|
|
332
|
+
{
|
|
333
|
+
title: "TanStack Start server features do not run inside the Capacitor app.",
|
|
334
|
+
details: [
|
|
335
|
+
"Move server functions and server routes to a deployed backend.",
|
|
336
|
+
"Configure the mobile app to call an HTTPS API URL that is reachable from the device.",
|
|
337
|
+
'Do not use "localhost" for the backend URL: on a phone or emulator, it points to the device itself.'
|
|
338
|
+
]
|
|
339
|
+
}
|
|
340
|
+
]
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
function findTanstackStartCall(configPath) {
|
|
345
|
+
return loadSourceFile(configPath).getDescendantsOfKind(SyntaxKind2.CallExpression).find((candidate) => candidate.getExpression().getText() === "tanstackStart");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/adapters/index.ts
|
|
349
|
+
var adapters = [nextjsAdapter, tanstackStartAdapter];
|
|
350
|
+
function getAdapter(id) {
|
|
351
|
+
const adapter = adapters.find((candidate) => candidate.id === id);
|
|
352
|
+
if (!adapter) {
|
|
353
|
+
throw new Error(`Unsupported framework: ${id}`);
|
|
354
|
+
}
|
|
355
|
+
return adapter;
|
|
356
|
+
}
|
|
357
|
+
function getAdapters() {
|
|
358
|
+
return adapters;
|
|
359
|
+
}
|
|
360
|
+
function detectAdapters(project) {
|
|
361
|
+
return adapters.filter((adapter) => adapter.detect(project));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// src/capacitor/configure.ts
|
|
365
|
+
import { readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
|
|
366
|
+
import path4 from "path";
|
|
367
|
+
|
|
368
|
+
// src/core/process.ts
|
|
369
|
+
import { spawn } from "child_process";
|
|
370
|
+
var maxErrorLines = 12;
|
|
371
|
+
var maxCapturedOutput = 64e3;
|
|
372
|
+
async function runCommand(command, args, cwd) {
|
|
373
|
+
await new Promise((resolve, reject) => {
|
|
374
|
+
let output = "";
|
|
375
|
+
const child = spawn(command, args, {
|
|
376
|
+
cwd,
|
|
377
|
+
shell: process.platform === "win32",
|
|
378
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
379
|
+
});
|
|
380
|
+
child.on("error", reject);
|
|
381
|
+
child.stdout?.on("data", (chunk) => {
|
|
382
|
+
output = appendOutput(output, chunk.toString());
|
|
383
|
+
});
|
|
384
|
+
child.stderr?.on("data", (chunk) => {
|
|
385
|
+
output = appendOutput(output, chunk.toString());
|
|
386
|
+
});
|
|
387
|
+
child.on("exit", (code) => {
|
|
388
|
+
if (code === 0) {
|
|
389
|
+
resolve();
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
reject(
|
|
393
|
+
new Error(
|
|
394
|
+
[
|
|
395
|
+
`Command failed: ${command} ${args.join(" ")}`,
|
|
396
|
+
getErrorSummary(output)
|
|
397
|
+
].filter(Boolean).join("\n")
|
|
398
|
+
)
|
|
399
|
+
);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
function appendOutput(current, chunk) {
|
|
404
|
+
return `${current}${chunk}`.slice(-maxCapturedOutput);
|
|
405
|
+
}
|
|
406
|
+
function getErrorSummary(output) {
|
|
407
|
+
return output.replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, "").split(/[\r\n]+/).map((line) => line.trim().slice(0, 300)).filter(Boolean).slice(-maxErrorLines).join("\n");
|
|
408
|
+
}
|
|
409
|
+
async function commandExists(command, args = ["--version"]) {
|
|
410
|
+
return new Promise((resolve) => {
|
|
411
|
+
const child = spawn(command, args, {
|
|
412
|
+
shell: process.platform === "win32",
|
|
413
|
+
stdio: "ignore"
|
|
414
|
+
});
|
|
415
|
+
child.on("error", () => resolve(false));
|
|
416
|
+
child.on("exit", (code) => resolve(code === 0));
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
function installCommand(packageManager, packages, dev) {
|
|
420
|
+
const devFlag = dev ? ["-D"] : [];
|
|
421
|
+
switch (packageManager) {
|
|
422
|
+
case "bun":
|
|
423
|
+
return ["bun", ["add", ...devFlag, ...packages]];
|
|
424
|
+
case "pnpm":
|
|
425
|
+
return ["pnpm", ["add", ...devFlag, ...packages]];
|
|
426
|
+
case "yarn":
|
|
427
|
+
return ["yarn", ["add", ...devFlag, ...packages]];
|
|
428
|
+
case "npm":
|
|
429
|
+
return ["npm", ["install", ...dev ? ["--save-dev"] : [], ...packages]];
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function runScriptCommand(packageManager, script) {
|
|
433
|
+
switch (packageManager) {
|
|
434
|
+
case "yarn":
|
|
435
|
+
return ["yarn", [script]];
|
|
436
|
+
case "bun":
|
|
437
|
+
return ["bun", ["run", script]];
|
|
438
|
+
case "pnpm":
|
|
439
|
+
return ["pnpm", ["run", script]];
|
|
440
|
+
case "npm":
|
|
441
|
+
return ["npm", ["run", script]];
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function capacitorCommand(packageManager, args) {
|
|
445
|
+
switch (packageManager) {
|
|
446
|
+
case "bun":
|
|
447
|
+
return ["bunx", ["cap", ...args]];
|
|
448
|
+
case "pnpm":
|
|
449
|
+
return ["pnpm", ["exec", "cap", ...args]];
|
|
450
|
+
case "yarn":
|
|
451
|
+
return ["yarn", ["cap", ...args]];
|
|
452
|
+
case "npm":
|
|
453
|
+
return ["npm", ["exec", "cap", "--", ...args]];
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function packageScriptPrefix(packageManager) {
|
|
457
|
+
switch (packageManager) {
|
|
458
|
+
case "yarn":
|
|
459
|
+
return "yarn";
|
|
460
|
+
case "bun":
|
|
461
|
+
return "bun run";
|
|
462
|
+
case "pnpm":
|
|
463
|
+
return "pnpm run";
|
|
464
|
+
case "npm":
|
|
465
|
+
return "npm run";
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function capacitorScriptPrefix(packageManager) {
|
|
469
|
+
switch (packageManager) {
|
|
470
|
+
case "bun":
|
|
471
|
+
return "bunx cap";
|
|
472
|
+
case "pnpm":
|
|
473
|
+
return "pnpm exec cap";
|
|
474
|
+
case "yarn":
|
|
475
|
+
return "yarn cap";
|
|
476
|
+
case "npm":
|
|
477
|
+
return "npm exec cap --";
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// src/capacitor/configure.ts
|
|
482
|
+
var configNames3 = [
|
|
483
|
+
"capacitor.config.ts",
|
|
484
|
+
"capacitor.config.mts",
|
|
485
|
+
"capacitor.config.js",
|
|
486
|
+
"capacitor.config.mjs",
|
|
487
|
+
"capacitor.config.json"
|
|
488
|
+
];
|
|
489
|
+
async function configureCapacitor(project, options) {
|
|
490
|
+
const configPath = await findConfigFile(project.root, configNames3);
|
|
491
|
+
if (!configPath) {
|
|
492
|
+
if (!options.dryRun) {
|
|
493
|
+
await writeFile3(
|
|
494
|
+
path4.join(project.root, "capacitor.config.ts"),
|
|
495
|
+
[
|
|
496
|
+
'import type { CapacitorConfig } from "@capacitor/cli";',
|
|
497
|
+
"",
|
|
498
|
+
"const config: CapacitorConfig = {",
|
|
499
|
+
` appId: ${JSON.stringify(options.appId)},`,
|
|
500
|
+
` appName: ${JSON.stringify(options.appName)},`,
|
|
501
|
+
` webDir: ${JSON.stringify(options.webDir)},`,
|
|
502
|
+
"};",
|
|
503
|
+
"",
|
|
504
|
+
"export default config;",
|
|
505
|
+
""
|
|
506
|
+
].join("\n")
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
} else if (configPath.endsWith(".json")) {
|
|
510
|
+
const config = JSON.parse(await readFile2(configPath, "utf8"));
|
|
511
|
+
config.webDir = options.webDir;
|
|
512
|
+
if (!options.dryRun) {
|
|
513
|
+
await writeFile3(configPath, `${JSON.stringify(config, null, 2)}
|
|
514
|
+
`);
|
|
515
|
+
}
|
|
516
|
+
} else {
|
|
517
|
+
const sourceFile = loadSourceFile(configPath);
|
|
518
|
+
const config = findExportedObject(sourceFile);
|
|
519
|
+
if (!config) {
|
|
520
|
+
throw new Error(
|
|
521
|
+
`Cannot safely update the existing ${path4.basename(configPath)}.`
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
setObjectProperty(config, "webDir", JSON.stringify(options.webDir));
|
|
525
|
+
if (!options.dryRun) {
|
|
526
|
+
await sourceFile.save();
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
project.packageJson.scripts ??= {};
|
|
530
|
+
const run2 = packageScriptPrefix(project.packageManager);
|
|
531
|
+
const cap = capacitorScriptPrefix(project.packageManager);
|
|
532
|
+
project.packageJson.scripts["cap:sync"] = `${run2} build && ${cap} sync`;
|
|
533
|
+
for (const platform of options.platforms) {
|
|
534
|
+
project.packageJson.scripts[`cap:${platform}`] = `${run2} cap:sync && ${cap} open ${platform}`;
|
|
535
|
+
}
|
|
536
|
+
await savePackageJson(project, options.dryRun);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/capacitor/install.ts
|
|
540
|
+
import path5 from "path";
|
|
541
|
+
async function installCapacitor(project, platforms) {
|
|
542
|
+
const runtimePackages = [
|
|
543
|
+
"@capacitor/core",
|
|
544
|
+
...platforms.map((platform) => `@capacitor/${platform}`)
|
|
545
|
+
];
|
|
546
|
+
await run(project, installCommand(project.packageManager, runtimePackages, false));
|
|
547
|
+
await run(
|
|
548
|
+
project,
|
|
549
|
+
installCommand(project.packageManager, ["@capacitor/cli"], true)
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
async function buildProject(project) {
|
|
553
|
+
await run(project, runScriptCommand(project.packageManager, "build"));
|
|
554
|
+
}
|
|
555
|
+
async function addNativePlatforms(project, platforms) {
|
|
556
|
+
for (const platform of platforms) {
|
|
557
|
+
if (await pathExists(path5.join(project.root, platform))) {
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
await run(project, capacitorCommand(project.packageManager, ["add", platform]));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
async function syncNativeProjects(project) {
|
|
564
|
+
await run(project, capacitorCommand(project.packageManager, ["sync"]));
|
|
565
|
+
}
|
|
566
|
+
async function run(project, [command, args]) {
|
|
567
|
+
await runCommand(command, args, project.root);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// src/core/framework-selection.ts
|
|
571
|
+
import {
|
|
572
|
+
cancel,
|
|
573
|
+
confirm,
|
|
574
|
+
isCancel,
|
|
575
|
+
select
|
|
576
|
+
} from "@clack/prompts";
|
|
577
|
+
var terminalFrameworkPrompts = {
|
|
578
|
+
async confirmDetected(adapter) {
|
|
579
|
+
const answer = await confirm({
|
|
580
|
+
message: `Use the detected framework ${adapter.label}?`,
|
|
581
|
+
initialValue: true
|
|
582
|
+
});
|
|
583
|
+
return unwrapPrompt(answer);
|
|
584
|
+
},
|
|
585
|
+
async selectFramework(adapters2) {
|
|
586
|
+
const answer = await select({
|
|
587
|
+
message: "Which framework should Capstart configure?",
|
|
588
|
+
options: adapters2.map((adapter) => ({
|
|
589
|
+
value: adapter.id,
|
|
590
|
+
label: adapter.label
|
|
591
|
+
}))
|
|
592
|
+
});
|
|
593
|
+
return unwrapPrompt(answer);
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
async function chooseAdapter(options) {
|
|
597
|
+
if (options.requested) {
|
|
598
|
+
return getAdapter(options.requested);
|
|
599
|
+
}
|
|
600
|
+
if (options.detected.length === 1 && options.acceptDetected) {
|
|
601
|
+
return options.detected[0];
|
|
602
|
+
}
|
|
603
|
+
if (!options.interactive) {
|
|
604
|
+
if (options.detected.length === 1) {
|
|
605
|
+
throw new Error(
|
|
606
|
+
`Detected ${options.detected[0].label}, but confirmation requires an interactive terminal. Pass --yes to accept it or --framework to choose explicitly.`
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
throw new Error(
|
|
610
|
+
"Framework selection requires an interactive terminal. Pass --framework nextjs or --framework tanstack-start."
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
const prompts = options.prompts ?? terminalFrameworkPrompts;
|
|
614
|
+
if (options.detected.length === 1 && await prompts.confirmDetected(options.detected[0])) {
|
|
615
|
+
return options.detected[0];
|
|
616
|
+
}
|
|
617
|
+
return getAdapter(await prompts.selectFramework(getAdapters()));
|
|
618
|
+
}
|
|
619
|
+
function unwrapPrompt(value) {
|
|
620
|
+
if (isCancel(value)) {
|
|
621
|
+
cancel("Capstart initialization cancelled.");
|
|
622
|
+
throw new Error("Initialization cancelled.");
|
|
623
|
+
}
|
|
624
|
+
return value;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// src/core/github-star.ts
|
|
628
|
+
import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
|
|
629
|
+
|
|
630
|
+
// src/core/logger.ts
|
|
631
|
+
import pc from "picocolors";
|
|
632
|
+
var logger = {
|
|
633
|
+
info(message) {
|
|
634
|
+
console.log(`${pc.cyan("\u2022")} ${message}`);
|
|
635
|
+
},
|
|
636
|
+
success(message) {
|
|
637
|
+
console.log(`${pc.green("\u2713")} ${message}`);
|
|
638
|
+
},
|
|
639
|
+
warning(message) {
|
|
640
|
+
console.log(`${pc.yellow("!")} ${message}`);
|
|
641
|
+
},
|
|
642
|
+
error(message) {
|
|
643
|
+
console.error(`${pc.red("\u2717")} ${message}`);
|
|
644
|
+
},
|
|
645
|
+
heading(message) {
|
|
646
|
+
console.log(`
|
|
647
|
+
${pc.bold(message)}`);
|
|
648
|
+
},
|
|
649
|
+
link(url) {
|
|
650
|
+
console.log(` ${pc.underline(pc.cyan(url))}`);
|
|
651
|
+
},
|
|
652
|
+
command(command, description) {
|
|
653
|
+
console.log(` ${pc.cyan(command)}
|
|
654
|
+
${pc.dim(description)}`);
|
|
655
|
+
},
|
|
656
|
+
detail(message) {
|
|
657
|
+
const lines = wrapText(message, getContentWidth());
|
|
658
|
+
console.log(
|
|
659
|
+
lines.map(
|
|
660
|
+
(line, index) => index === 0 ? ` ${pc.yellow("\u2022")} ${line}` : ` ${line}`
|
|
661
|
+
).join("\n")
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
function getContentWidth() {
|
|
666
|
+
const terminalWidth = process.stdout.columns ?? 100;
|
|
667
|
+
return Math.max(40, Math.min(terminalWidth - 4, 80));
|
|
668
|
+
}
|
|
669
|
+
function wrapText(message, width) {
|
|
670
|
+
const lines = [];
|
|
671
|
+
let line = "";
|
|
672
|
+
for (const word of message.split(/\s+/)) {
|
|
673
|
+
if (line.length > 0 && line.length + word.length + 1 > width) {
|
|
674
|
+
lines.push(line);
|
|
675
|
+
line = word;
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
line = line.length === 0 ? word : `${line} ${word}`;
|
|
679
|
+
}
|
|
680
|
+
if (line.length > 0) {
|
|
681
|
+
lines.push(line);
|
|
682
|
+
}
|
|
683
|
+
return lines;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// src/core/github-star.ts
|
|
687
|
+
var repository = "AdrienADV/capstart";
|
|
688
|
+
var terminalGithubStarPrompt = {
|
|
689
|
+
async confirmStar() {
|
|
690
|
+
const answer = await confirm2({
|
|
691
|
+
message: "Would you like to star Capstart on GitHub?",
|
|
692
|
+
initialValue: true
|
|
693
|
+
});
|
|
694
|
+
return isCancel2(answer) ? false : answer;
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
async function offerGithubStar(options) {
|
|
698
|
+
if (!options.interactive) {
|
|
699
|
+
return "skipped";
|
|
700
|
+
}
|
|
701
|
+
const isGhInstalled = options.isGhInstalled ?? (() => commandExists("gh", ["--version"]));
|
|
702
|
+
if (!await isGhInstalled()) {
|
|
703
|
+
return "skipped";
|
|
704
|
+
}
|
|
705
|
+
const prompt = options.prompt ?? terminalGithubStarPrompt;
|
|
706
|
+
if (!await prompt.confirmStar()) {
|
|
707
|
+
return "declined";
|
|
708
|
+
}
|
|
709
|
+
const starRepository = options.starRepository ?? (() => runCommand(
|
|
710
|
+
"gh",
|
|
711
|
+
["api", "--method", "PUT", `/user/starred/${repository}`],
|
|
712
|
+
options.cwd
|
|
713
|
+
));
|
|
714
|
+
try {
|
|
715
|
+
await starRepository();
|
|
716
|
+
logger.success(`Starred https://github.com/${repository}`);
|
|
717
|
+
return "starred";
|
|
718
|
+
} catch {
|
|
719
|
+
logger.warning(
|
|
720
|
+
`Could not star ${repository}. Check your GitHub CLI authentication with "gh auth status".`
|
|
721
|
+
);
|
|
722
|
+
return "failed";
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// src/core/progress.ts
|
|
727
|
+
import { spinner } from "@clack/prompts";
|
|
728
|
+
var silentProgress = {
|
|
729
|
+
message() {
|
|
730
|
+
},
|
|
731
|
+
start() {
|
|
732
|
+
},
|
|
733
|
+
stop() {
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
function createProgress(interactive) {
|
|
737
|
+
return interactive ? spinner() : silentProgress;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// src/commands/init.ts
|
|
741
|
+
async function initCommand(options) {
|
|
742
|
+
const project = await loadProject(options.directory);
|
|
743
|
+
logger.heading("Capstart");
|
|
744
|
+
const detected = detectAdapters(project);
|
|
745
|
+
logDetection(detected);
|
|
746
|
+
const adapter = await chooseAdapter({
|
|
747
|
+
acceptDetected: options.yes,
|
|
748
|
+
detected,
|
|
749
|
+
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
750
|
+
requested: options.framework
|
|
751
|
+
});
|
|
752
|
+
if (!detected.includes(adapter)) {
|
|
753
|
+
logger.warning(`${adapter.label} was selected but was not automatically detected.`);
|
|
754
|
+
}
|
|
755
|
+
const diagnostics = await adapter.validate(project);
|
|
756
|
+
for (const warning of diagnostics.filter((item) => item.level === "warning")) {
|
|
757
|
+
logger.warning(warning.message);
|
|
758
|
+
}
|
|
759
|
+
const errors = diagnostics.filter((item) => item.level === "error");
|
|
760
|
+
if (errors.length > 0) {
|
|
761
|
+
throw new Error(errors.map((error) => error.message).join("\n"));
|
|
762
|
+
}
|
|
763
|
+
const appId = options.appId ?? createDefaultAppId(project);
|
|
764
|
+
const appName = options.appName ?? getProjectName(project);
|
|
765
|
+
validateAppId(appId);
|
|
766
|
+
if (options.dryRun) {
|
|
767
|
+
const frameworkResult2 = await adapter.configure(project, true);
|
|
768
|
+
await configureCapacitor(project, {
|
|
769
|
+
appId,
|
|
770
|
+
appName,
|
|
771
|
+
dryRun: true,
|
|
772
|
+
platforms: options.platforms,
|
|
773
|
+
webDir: adapter.webDir
|
|
774
|
+
});
|
|
775
|
+
logger.heading("Planned changes");
|
|
776
|
+
logger.info(`Configure ${adapter.label} for Capacitor`);
|
|
777
|
+
logger.info(`Configure Capacitor with webDir "${adapter.webDir}"`);
|
|
778
|
+
logger.success("Dry run complete. No files were changed.");
|
|
779
|
+
printFinalGuidance(frameworkResult2.disclaimers);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const frameworkResult = await runSetup({
|
|
783
|
+
adapter,
|
|
784
|
+
appId,
|
|
785
|
+
appName,
|
|
786
|
+
options,
|
|
787
|
+
project
|
|
788
|
+
});
|
|
789
|
+
logger.heading("Ready");
|
|
790
|
+
logger.success("Your base Capacitor setup is ready.");
|
|
791
|
+
logger.heading("Scripts added");
|
|
792
|
+
logger.command(
|
|
793
|
+
`${project.packageManager} run cap:sync`,
|
|
794
|
+
"Build the web app and sync the native projects."
|
|
795
|
+
);
|
|
796
|
+
if (options.platforms.includes("ios")) {
|
|
797
|
+
logger.command(
|
|
798
|
+
`${project.packageManager} run cap:ios`,
|
|
799
|
+
"Build, sync, and open the iOS project in Xcode."
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
if (options.platforms.includes("android")) {
|
|
803
|
+
logger.command(
|
|
804
|
+
`${project.packageManager} run cap:android`,
|
|
805
|
+
"Build, sync, and open the Android project in Android Studio."
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
logger.heading("Next steps");
|
|
809
|
+
logger.info(
|
|
810
|
+
"Review recommended plugins, native configuration, and production setup:"
|
|
811
|
+
);
|
|
812
|
+
logger.link(
|
|
813
|
+
"https://capstart.dev/docs/installation/#3-add-recommended-capacitor-base-plugins"
|
|
814
|
+
);
|
|
815
|
+
await offerGithubStar({
|
|
816
|
+
cwd: project.root,
|
|
817
|
+
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
818
|
+
});
|
|
819
|
+
printFinalGuidance(frameworkResult.disclaimers);
|
|
820
|
+
}
|
|
821
|
+
async function runSetup(context) {
|
|
822
|
+
const { adapter, appId, appName, options, project } = context;
|
|
823
|
+
const progress = createProgress(Boolean(process.stdout.isTTY));
|
|
824
|
+
const frameworkResult = await runStep(
|
|
825
|
+
progress,
|
|
826
|
+
`Configure ${adapter.label}`,
|
|
827
|
+
() => adapter.configure(project, false)
|
|
828
|
+
);
|
|
829
|
+
await runStep(
|
|
830
|
+
progress,
|
|
831
|
+
"Configure Capacitor",
|
|
832
|
+
() => configureCapacitor(project, {
|
|
833
|
+
appId,
|
|
834
|
+
appName,
|
|
835
|
+
dryRun: false,
|
|
836
|
+
platforms: options.platforms,
|
|
837
|
+
webDir: adapter.webDir
|
|
838
|
+
})
|
|
839
|
+
);
|
|
840
|
+
if (!options.skipInstall) {
|
|
841
|
+
await runStep(
|
|
842
|
+
progress,
|
|
843
|
+
"Install Capacitor packages",
|
|
844
|
+
() => installCapacitor(project, options.platforms)
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
if (!options.skipBuild) {
|
|
848
|
+
await runStep(progress, "Build the web app", () => buildProject(project));
|
|
849
|
+
}
|
|
850
|
+
if (!options.skipNative) {
|
|
851
|
+
await runStep(
|
|
852
|
+
progress,
|
|
853
|
+
`Prepare ${formatPlatforms(options.platforms)} projects`,
|
|
854
|
+
() => addNativePlatforms(project, options.platforms)
|
|
855
|
+
);
|
|
856
|
+
await runStep(
|
|
857
|
+
progress,
|
|
858
|
+
"Synchronize native projects",
|
|
859
|
+
() => syncNativeProjects(project)
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
return frameworkResult;
|
|
863
|
+
}
|
|
864
|
+
async function runStep(progress, label, action) {
|
|
865
|
+
progress.start(label);
|
|
866
|
+
try {
|
|
867
|
+
const result = await action();
|
|
868
|
+
progress.stop(label);
|
|
869
|
+
return result;
|
|
870
|
+
} catch (error) {
|
|
871
|
+
progress.stop(`${label} failed`);
|
|
872
|
+
throw error;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function formatPlatforms(platforms) {
|
|
876
|
+
return platforms.map((platform) => platform === "ios" ? "iOS" : "Android").join(" and ");
|
|
877
|
+
}
|
|
878
|
+
function logDetection(detected) {
|
|
879
|
+
if (detected.length === 0) {
|
|
880
|
+
logger.warning("No supported framework was automatically detected.");
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
if (detected.length === 1) {
|
|
884
|
+
logger.success(`Detected ${detected[0].label}`);
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
logger.warning(
|
|
888
|
+
`Detected multiple frameworks: ${detected.map((adapter) => adapter.label).join(", ")}`
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
function validateAppId(appId) {
|
|
892
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(appId)) {
|
|
893
|
+
throw new Error(
|
|
894
|
+
`Invalid app id "${appId}". Use reverse-domain notation, for example com.example.app.`
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
function printFinalGuidance(disclaimers) {
|
|
899
|
+
if (disclaimers.length === 0) {
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
logger.heading("Important");
|
|
903
|
+
for (const disclaimer of disclaimers) {
|
|
904
|
+
logger.warning(disclaimer.title);
|
|
905
|
+
for (const detail of disclaimer.details) {
|
|
906
|
+
logger.detail(detail);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// src/cli.ts
|
|
912
|
+
var program = new Command();
|
|
913
|
+
program.name("capstart").description("Add Capacitor to an existing web application.").version("0.1.0");
|
|
914
|
+
program.command("init").description("Configure an existing application for Capacitor.").argument("[directory]", "project directory", ".").option(
|
|
915
|
+
"-f, --framework <framework>",
|
|
916
|
+
"framework adapter: nextjs or tanstack-start",
|
|
917
|
+
parseFramework
|
|
918
|
+
).option("--app-id <id>", "native application id, for example com.example.app").option("--app-name <name>", "native application name").option(
|
|
919
|
+
"--platforms <platforms>",
|
|
920
|
+
"comma-separated native platforms",
|
|
921
|
+
parsePlatforms,
|
|
922
|
+
["ios", "android"]
|
|
923
|
+
).option("--skip-install", "do not install Capacitor packages").option("--skip-build", "do not build the web application").option("--skip-native", "do not add or synchronize native projects").option("--dry-run", "show configuration changes without writing files").option("-y, --yes", "accept the automatically detected framework").action(async (directory, commandOptions) => {
|
|
924
|
+
await initCommand({
|
|
925
|
+
appId: commandOptions.appId,
|
|
926
|
+
appName: commandOptions.appName,
|
|
927
|
+
directory,
|
|
928
|
+
dryRun: commandOptions.dryRun ?? false,
|
|
929
|
+
framework: commandOptions.framework,
|
|
930
|
+
platforms: commandOptions.platforms,
|
|
931
|
+
skipBuild: commandOptions.skipBuild ?? false,
|
|
932
|
+
skipInstall: commandOptions.skipInstall ?? false,
|
|
933
|
+
skipNative: commandOptions.skipNative ?? false,
|
|
934
|
+
yes: commandOptions.yes ?? false
|
|
935
|
+
});
|
|
936
|
+
});
|
|
937
|
+
program.parseAsync().catch((error) => {
|
|
938
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
939
|
+
process.exitCode = 1;
|
|
940
|
+
});
|
|
941
|
+
function parseFramework(value) {
|
|
942
|
+
if (value === "nextjs" || value === "tanstack-start") {
|
|
943
|
+
return value;
|
|
944
|
+
}
|
|
945
|
+
throw new InvalidArgumentError(
|
|
946
|
+
'Framework must be "nextjs" or "tanstack-start".'
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
function parsePlatforms(value) {
|
|
950
|
+
const platforms = value.split(",").map((platform) => platform.trim()).filter(Boolean);
|
|
951
|
+
if (platforms.length === 0 || platforms.some((platform) => platform !== "ios" && platform !== "android")) {
|
|
952
|
+
throw new InvalidArgumentError(
|
|
953
|
+
'Platforms must contain "ios", "android", or both.'
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
return [...new Set(platforms)];
|
|
957
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "capstart",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Add Capacitor to existing web framework projects.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"capstart": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"capacitor",
|
|
15
|
+
"cli",
|
|
16
|
+
"mobile",
|
|
17
|
+
"nextjs",
|
|
18
|
+
"tanstack-start"
|
|
19
|
+
],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/AdrienADV/capstart.git",
|
|
23
|
+
"directory": "cli"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsup src/cli.ts --format esm --dts --clean",
|
|
30
|
+
"dev": "node --import tsx src/cli.ts",
|
|
31
|
+
"test": "node --import tsx --test test/**/*.test.ts",
|
|
32
|
+
"typecheck": "tsc --noEmit"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@clack/prompts": "^1.0.0",
|
|
36
|
+
"commander": "^14.0.2",
|
|
37
|
+
"picocolors": "^1.1.1",
|
|
38
|
+
"ts-morph": "^27.0.2"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^24.10.1",
|
|
42
|
+
"tsup": "^8.5.1",
|
|
43
|
+
"tsx": "^4.21.0",
|
|
44
|
+
"typescript": "^5.9.3"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=20"
|
|
48
|
+
},
|
|
49
|
+
"license": "MIT"
|
|
50
|
+
}
|