ecopages 0.2.0-alpha.29 → 0.2.0-alpha.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/bin/cli.js +201 -237
- package/bin/launch-plan.js +89 -105
- package/package.json +2 -7
- package/bin/cli.test.ts +0 -136
- package/bin/launch-plan.test.ts +0 -161
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `ecopages` are documented here.
|
|
4
|
+
|
|
5
|
+
> **Note:** Changelog tracking begins at version `0.2.0`. Changes prior to this release are not recorded here but are available in the git history.
|
|
6
|
+
|
|
7
|
+
## [UNRELEASED] — TBD
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
- Localized the Node CLI `tsx` runtime dependency so `ecopages` no longer requires a globally installed `tsx` binary.
|
|
12
|
+
|
|
13
|
+
### Refactoring
|
|
14
|
+
|
|
15
|
+
- Simplified CLI runtime startup and removed the thin-host bootstrap path; runtime selection now follows explicit `--runtime` overrides, package-manager hints, and Bun availability.
|
package/bin/cli.js
CHANGED
|
@@ -1,260 +1,224 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const logger = new Logger('[ecopages:cli]', { debug: process.env.ECOPAGES_LOGGER_DEBUG === 'true' });
|
|
12
|
-
|
|
13
|
-
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
14
|
-
|
|
2
|
+
import { defineCommand, runMain } from "citty";
|
|
3
|
+
import { downloadTemplate } from "giget";
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { Logger } from "@ecopages/logger";
|
|
8
|
+
import { createLaunchPlan, launchPlanRequiresExistingEntryFile } from "./launch-plan.js";
|
|
9
|
+
const logger = new Logger("[ecopages:cli]", { debug: process.env.ECOPAGES_LOGGER_DEBUG === "true" });
|
|
10
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
15
11
|
function runLaunchPlan(launchPlan) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
logger.error(`Failed to run command: ${error.message}`);
|
|
39
|
-
process.exit(1);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
child.on('exit', (code) => {
|
|
43
|
-
process.exit(code || 0);
|
|
44
|
-
});
|
|
12
|
+
if (Object.keys(launchPlan.envOverrides).length > 0) {
|
|
13
|
+
logger.debug(`Environment overrides: ${JSON.stringify(launchPlan.envOverrides)}`);
|
|
14
|
+
}
|
|
15
|
+
logger.debug(`Runtime: ${launchPlan.runtime}`);
|
|
16
|
+
logger.debug(`Running: ${launchPlan.command} ${launchPlan.commandArgs.join(" ")}`);
|
|
17
|
+
const child = spawn(launchPlan.command, launchPlan.commandArgs, {
|
|
18
|
+
stdio: "inherit",
|
|
19
|
+
env: launchPlan.env
|
|
20
|
+
});
|
|
21
|
+
child.on("error", (error) => {
|
|
22
|
+
if (error && error.code === "ENOENT") {
|
|
23
|
+
const hint = launchPlan.runtime === "bun" ? "Install Bun from https://bun.sh to continue." : "Reinstall ecopages and its dependencies so the packaged tsx runtime is available for Node.js launches.";
|
|
24
|
+
logger.error(`Command not found: ${launchPlan.command}. ${hint}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
logger.error(`Failed to run command: ${error.message}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
});
|
|
30
|
+
child.on("exit", (code) => {
|
|
31
|
+
process.exit(code || 0);
|
|
32
|
+
});
|
|
45
33
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
-
logger.error(message);
|
|
62
|
-
process.exit(1);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (launchPlanRequiresExistingEntryFile(launchPlan) && !existsSync(entryFile)) {
|
|
66
|
-
logger.error(`Error: Entry file "${entryFile}" not found in the current directory.`);
|
|
67
|
-
process.exit(1);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
runLaunchPlan(launchPlan);
|
|
34
|
+
async function runEntryCommand(args, options = {}, entryFile = "app.ts") {
|
|
35
|
+
let launchPlan;
|
|
36
|
+
try {
|
|
37
|
+
launchPlan = await createLaunchPlan(args, options, entryFile);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
40
|
+
logger.error(message);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
if (launchPlanRequiresExistingEntryFile(launchPlan) && !existsSync(entryFile)) {
|
|
44
|
+
logger.error(`Error: Entry file "${entryFile}" not found in the current directory.`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
runLaunchPlan(launchPlan);
|
|
71
48
|
}
|
|
72
|
-
|
|
73
|
-
/** Shared server argument definitions for citty commands. */
|
|
74
49
|
const serverArgs = {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
50
|
+
entry: {
|
|
51
|
+
type: "positional",
|
|
52
|
+
description: "Entry file",
|
|
53
|
+
default: "app.ts"
|
|
54
|
+
},
|
|
55
|
+
port: {
|
|
56
|
+
type: "string",
|
|
57
|
+
alias: ["p"],
|
|
58
|
+
description: "Override ECOPAGES_PORT"
|
|
59
|
+
},
|
|
60
|
+
hostname: {
|
|
61
|
+
type: "string",
|
|
62
|
+
alias: ["n"],
|
|
63
|
+
description: "Override ECOPAGES_HOSTNAME"
|
|
64
|
+
},
|
|
65
|
+
"base-url": {
|
|
66
|
+
type: "string",
|
|
67
|
+
alias: ["b"],
|
|
68
|
+
description: "Override ECOPAGES_BASE_URL"
|
|
69
|
+
},
|
|
70
|
+
debug: {
|
|
71
|
+
type: "boolean",
|
|
72
|
+
alias: ["d"],
|
|
73
|
+
description: "Enable debug logging (ECOPAGES_LOGGER_DEBUG=true)"
|
|
74
|
+
},
|
|
75
|
+
"react-fast-refresh": {
|
|
76
|
+
type: "boolean",
|
|
77
|
+
alias: ["r"],
|
|
78
|
+
description: "Enable React Fast Refresh for HMR"
|
|
79
|
+
},
|
|
80
|
+
runtime: {
|
|
81
|
+
type: "string",
|
|
82
|
+
description: "Force a specific runtime (bun or node)"
|
|
83
|
+
}
|
|
109
84
|
};
|
|
110
|
-
|
|
111
85
|
const initCommand = defineCommand({
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
} catch (err) {
|
|
159
|
-
logger.error(`Failed to fetch template: ${err.message}`);
|
|
160
|
-
process.exit(1);
|
|
161
|
-
}
|
|
162
|
-
},
|
|
86
|
+
meta: {
|
|
87
|
+
name: "init",
|
|
88
|
+
description: "Initialize a new project from a template"
|
|
89
|
+
},
|
|
90
|
+
args: {
|
|
91
|
+
dir: {
|
|
92
|
+
type: "positional",
|
|
93
|
+
description: "Target directory name",
|
|
94
|
+
required: true
|
|
95
|
+
},
|
|
96
|
+
template: {
|
|
97
|
+
type: "string",
|
|
98
|
+
description: "Template name from ecopages/examples/",
|
|
99
|
+
default: "starter-jsx"
|
|
100
|
+
},
|
|
101
|
+
repo: {
|
|
102
|
+
type: "string",
|
|
103
|
+
description: "GitHub repo (user/repo)",
|
|
104
|
+
default: "ecopages/ecopages"
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
async run({ args }) {
|
|
108
|
+
const { dir, template, repo } = args;
|
|
109
|
+
if (existsSync(dir)) {
|
|
110
|
+
logger.error(`Target directory already exists: ${dir}`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
logger.info(`Creating target directory '${dir}'...`);
|
|
114
|
+
try {
|
|
115
|
+
await downloadTemplate(`github:${repo}/examples/${template}`, {
|
|
116
|
+
dir,
|
|
117
|
+
force: true
|
|
118
|
+
});
|
|
119
|
+
const pkgPath = join(dir, "package.json");
|
|
120
|
+
if (existsSync(pkgPath)) {
|
|
121
|
+
const projectPkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
122
|
+
projectPkg.name = dir;
|
|
123
|
+
writeFileSync(pkgPath, JSON.stringify(projectPkg, null, 2) + "\n");
|
|
124
|
+
logger.info(`Renamed project to '${dir}'`);
|
|
125
|
+
}
|
|
126
|
+
logger.info("Project initialized! Run `bun install && bun dev` to start.");
|
|
127
|
+
} catch (err) {
|
|
128
|
+
logger.error(`Failed to fetch template: ${err.message}`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
163
132
|
});
|
|
164
|
-
|
|
165
133
|
const devCommand = defineCommand({
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
134
|
+
meta: {
|
|
135
|
+
name: "dev",
|
|
136
|
+
description: "Start the development server"
|
|
137
|
+
},
|
|
138
|
+
args: serverArgs,
|
|
139
|
+
async run({ args }) {
|
|
140
|
+
await runEntryCommand(["--dev"], { ...args, nodeEnv: "development" }, args.entry);
|
|
141
|
+
}
|
|
174
142
|
});
|
|
175
|
-
|
|
176
143
|
const devWatchCommand = defineCommand({
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
144
|
+
meta: {
|
|
145
|
+
name: "dev:watch",
|
|
146
|
+
description: "Start the development server with watch mode (restarts on file changes)"
|
|
147
|
+
},
|
|
148
|
+
args: serverArgs,
|
|
149
|
+
async run({ args }) {
|
|
150
|
+
await runEntryCommand(["--dev"], { ...args, watch: true, nodeEnv: "development" }, args.entry);
|
|
151
|
+
}
|
|
185
152
|
});
|
|
186
|
-
|
|
187
153
|
const devHotCommand = defineCommand({
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
154
|
+
meta: {
|
|
155
|
+
name: "dev:hot",
|
|
156
|
+
description: "Start the development server with hot reload (HMR without restart)"
|
|
157
|
+
},
|
|
158
|
+
args: serverArgs,
|
|
159
|
+
async run({ args }) {
|
|
160
|
+
await runEntryCommand(["--dev"], { ...args, hot: true, nodeEnv: "development" }, args.entry);
|
|
161
|
+
}
|
|
196
162
|
});
|
|
197
|
-
|
|
198
163
|
const buildCommand = defineCommand({
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
164
|
+
meta: {
|
|
165
|
+
name: "build",
|
|
166
|
+
description: "Build the project for production"
|
|
167
|
+
},
|
|
168
|
+
args: {
|
|
169
|
+
entry: {
|
|
170
|
+
type: "positional",
|
|
171
|
+
description: "Entry file",
|
|
172
|
+
default: "app.ts"
|
|
173
|
+
},
|
|
174
|
+
runtime: {
|
|
175
|
+
type: "string",
|
|
176
|
+
description: "Force a specific runtime (bun or node)"
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
async run({ args }) {
|
|
180
|
+
await runEntryCommand(["--build"], { nodeEnv: "production", ...args }, args.entry);
|
|
181
|
+
}
|
|
217
182
|
});
|
|
218
|
-
|
|
219
183
|
const startCommand = defineCommand({
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
184
|
+
meta: {
|
|
185
|
+
name: "start",
|
|
186
|
+
description: "Start the production server"
|
|
187
|
+
},
|
|
188
|
+
args: serverArgs,
|
|
189
|
+
async run({ args }) {
|
|
190
|
+
await runEntryCommand([], { ...args, nodeEnv: "production" }, args.entry);
|
|
191
|
+
}
|
|
228
192
|
});
|
|
229
|
-
|
|
230
193
|
const previewCommand = defineCommand({
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
194
|
+
meta: {
|
|
195
|
+
name: "preview",
|
|
196
|
+
description: "Preview the production build"
|
|
197
|
+
},
|
|
198
|
+
args: serverArgs,
|
|
199
|
+
async run({ args }) {
|
|
200
|
+
await runEntryCommand(["--preview"], { ...args, nodeEnv: "production" }, args.entry);
|
|
201
|
+
}
|
|
239
202
|
});
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
},
|
|
203
|
+
const mainCommand = defineCommand({
|
|
204
|
+
meta: {
|
|
205
|
+
name: "ecopages",
|
|
206
|
+
version: pkg.version,
|
|
207
|
+
description: "Ecopages CLI utilities"
|
|
208
|
+
},
|
|
209
|
+
subCommands: {
|
|
210
|
+
init: initCommand,
|
|
211
|
+
dev: devCommand,
|
|
212
|
+
"dev:watch": devWatchCommand,
|
|
213
|
+
"dev:hot": devHotCommand,
|
|
214
|
+
build: buildCommand,
|
|
215
|
+
start: startCommand,
|
|
216
|
+
preview: previewCommand
|
|
217
|
+
}
|
|
256
218
|
});
|
|
257
|
-
|
|
258
219
|
if (!process.env.VITEST) {
|
|
259
|
-
|
|
220
|
+
runMain(mainCommand);
|
|
260
221
|
}
|
|
222
|
+
export {
|
|
223
|
+
mainCommand
|
|
224
|
+
};
|
package/bin/launch-plan.js
CHANGED
|
@@ -1,112 +1,96 @@
|
|
|
1
|
-
import { existsSync } from
|
|
2
|
-
import { createRequire } from
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (options.nodeEnv) env.NODE_ENV = options.nodeEnv;
|
|
13
|
-
return env;
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
const require2 = createRequire(import.meta.url);
|
|
4
|
+
function buildEnvOverrides(options) {
|
|
5
|
+
const env = {};
|
|
6
|
+
if (options.port) env.ECOPAGES_PORT = String(options.port);
|
|
7
|
+
if (options.hostname) env.ECOPAGES_HOSTNAME = options.hostname;
|
|
8
|
+
if (options.baseUrl) env.ECOPAGES_BASE_URL = options.baseUrl;
|
|
9
|
+
if (options.debug) env.ECOPAGES_LOGGER_DEBUG = "true";
|
|
10
|
+
if (options.nodeEnv) env.NODE_ENV = options.nodeEnv;
|
|
11
|
+
return env;
|
|
14
12
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (typeof Bun !== 'undefined') {
|
|
28
|
-
return 'bun';
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return 'node';
|
|
13
|
+
function detectRuntime(options = {}) {
|
|
14
|
+
if (options.runtime === "bun" || options.runtime === "node") {
|
|
15
|
+
return options.runtime;
|
|
16
|
+
}
|
|
17
|
+
const userAgent = process.env.npm_config_user_agent || "";
|
|
18
|
+
if (userAgent.startsWith("bun/")) {
|
|
19
|
+
return "bun";
|
|
20
|
+
}
|
|
21
|
+
if (typeof Bun !== "undefined") {
|
|
22
|
+
return "bun";
|
|
23
|
+
}
|
|
24
|
+
return "node";
|
|
32
25
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
bunArgs.push(entryFile, ...args);
|
|
47
|
-
|
|
48
|
-
if (options.reactFastRefresh) {
|
|
49
|
-
bunArgs.push('--react-fast-refresh');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return bunArgs;
|
|
26
|
+
function buildBunArgs(args, options, entryFile, hasConfig) {
|
|
27
|
+
const bunArgs = [];
|
|
28
|
+
if (options.watch) bunArgs.push("--watch");
|
|
29
|
+
if (options.hot) bunArgs.push("--hot");
|
|
30
|
+
bunArgs.push("run");
|
|
31
|
+
if (hasConfig) {
|
|
32
|
+
bunArgs.push("--preload", "./eco.config.js");
|
|
33
|
+
}
|
|
34
|
+
bunArgs.push(entryFile, ...args);
|
|
35
|
+
if (options.reactFastRefresh) {
|
|
36
|
+
bunArgs.push("--react-fast-refresh");
|
|
37
|
+
}
|
|
38
|
+
return bunArgs;
|
|
53
39
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (options.reactFastRefresh) {
|
|
65
|
-
nodeArgs.push('--react-fast-refresh');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return nodeArgs;
|
|
40
|
+
function buildNodeArgs(args, options, entryFile, hasConfig) {
|
|
41
|
+
const nodeArgs = [];
|
|
42
|
+
if (hasConfig) {
|
|
43
|
+
nodeArgs.push("--import", "./eco.config.js");
|
|
44
|
+
}
|
|
45
|
+
nodeArgs.push(entryFile, ...args);
|
|
46
|
+
if (options.reactFastRefresh) {
|
|
47
|
+
nodeArgs.push("--react-fast-refresh");
|
|
48
|
+
}
|
|
49
|
+
return nodeArgs;
|
|
69
50
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
51
|
+
function resolveTsxCliPath() {
|
|
52
|
+
try {
|
|
53
|
+
return require2.resolve("tsx/cli");
|
|
54
|
+
} catch {
|
|
55
|
+
throw new Error(
|
|
56
|
+
"Unable to resolve the packaged tsx runtime required for Node.js launches. Reinstall ecopages and its dependencies, or use the Bun runtime instead."
|
|
57
|
+
);
|
|
58
|
+
}
|
|
79
59
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
commandArgs: buildBunArgs(args, options, entryFile, hasConfig),
|
|
105
|
-
envOverrides,
|
|
106
|
-
env,
|
|
107
|
-
};
|
|
60
|
+
async function createLaunchPlan(args, options = {}, entryFile = "app.ts") {
|
|
61
|
+
const hasConfig = existsSync("eco.config.ts");
|
|
62
|
+
const envOverrides = buildEnvOverrides(options);
|
|
63
|
+
const runtime = detectRuntime(options);
|
|
64
|
+
const env = { ...process.env, ...envOverrides };
|
|
65
|
+
if (runtime === "node") {
|
|
66
|
+
const tsxCliPath = resolveTsxCliPath();
|
|
67
|
+
return {
|
|
68
|
+
runtime,
|
|
69
|
+
executionStrategy: "direct-runtime",
|
|
70
|
+
command: process.execPath,
|
|
71
|
+
commandArgs: [tsxCliPath, ...buildNodeArgs(args, options, entryFile, hasConfig)],
|
|
72
|
+
envOverrides,
|
|
73
|
+
env
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
runtime,
|
|
78
|
+
executionStrategy: "direct-runtime",
|
|
79
|
+
command: "bun",
|
|
80
|
+
commandArgs: buildBunArgs(args, options, entryFile, hasConfig),
|
|
81
|
+
envOverrides,
|
|
82
|
+
env
|
|
83
|
+
};
|
|
108
84
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return launchPlan.executionStrategy !== 'config-only-bootstrap';
|
|
85
|
+
function launchPlanRequiresExistingEntryFile(launchPlan) {
|
|
86
|
+
return launchPlan.executionStrategy !== "config-only-bootstrap";
|
|
112
87
|
}
|
|
88
|
+
export {
|
|
89
|
+
buildBunArgs,
|
|
90
|
+
buildEnvOverrides,
|
|
91
|
+
buildNodeArgs,
|
|
92
|
+
createLaunchPlan,
|
|
93
|
+
detectRuntime,
|
|
94
|
+
launchPlanRequiresExistingEntryFile,
|
|
95
|
+
resolveTsxCliPath
|
|
96
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ecopages",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.31",
|
|
4
4
|
"description": "CLI utilities for Ecopages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,13 +28,8 @@
|
|
|
28
28
|
"bin": {
|
|
29
29
|
"ecopages": "bin/cli.js"
|
|
30
30
|
},
|
|
31
|
-
"files": [
|
|
32
|
-
"bin/",
|
|
33
|
-
"css/",
|
|
34
|
-
"README.md"
|
|
35
|
-
],
|
|
36
31
|
"dependencies": {
|
|
37
|
-
"@ecopages/core": "
|
|
32
|
+
"@ecopages/core": "0.2.0-alpha.31",
|
|
38
33
|
"@ecopages/logger": "^0.2.2",
|
|
39
34
|
"citty": "^0.1.6",
|
|
40
35
|
"giget": "^2.0.0",
|
package/bin/cli.test.ts
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { runCommand } from 'citty';
|
|
3
|
-
import { mainCommand } from './cli.js';
|
|
4
|
-
import * as giget from 'giget';
|
|
5
|
-
import * as fs from 'node:fs';
|
|
6
|
-
import * as launchPlan from './launch-plan.js';
|
|
7
|
-
|
|
8
|
-
vi.mock('giget', () => ({
|
|
9
|
-
downloadTemplate: vi.fn(),
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
vi.mock('node:fs', async (importOriginal) => {
|
|
13
|
-
const actual = await importOriginal<typeof import('node:fs')>();
|
|
14
|
-
return {
|
|
15
|
-
...actual,
|
|
16
|
-
existsSync: vi.fn((path) => actual.existsSync(path)),
|
|
17
|
-
writeFileSync: vi.fn(),
|
|
18
|
-
};
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
vi.mock('./launch-plan.js', () => ({
|
|
22
|
-
createLaunchPlan: vi.fn(),
|
|
23
|
-
launchPlanRequiresExistingEntryFile: vi.fn(),
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
vi.mock('node:child_process', () => ({
|
|
27
|
-
spawn: vi.fn().mockImplementation(() => ({
|
|
28
|
-
on: vi.fn(),
|
|
29
|
-
})),
|
|
30
|
-
}));
|
|
31
|
-
|
|
32
|
-
vi.mock('@ecopages/logger', () => ({
|
|
33
|
-
Logger: class {
|
|
34
|
-
info = vi.fn();
|
|
35
|
-
warn = vi.fn();
|
|
36
|
-
error = vi.fn();
|
|
37
|
-
debug = vi.fn();
|
|
38
|
-
},
|
|
39
|
-
}));
|
|
40
|
-
|
|
41
|
-
describe('CLI Commands', () => {
|
|
42
|
-
beforeEach(() => {
|
|
43
|
-
vi.clearAllMocks();
|
|
44
|
-
|
|
45
|
-
// Default mocks
|
|
46
|
-
vi.mocked(fs.existsSync).mockReturnValue(false); // pretend no existing dir
|
|
47
|
-
vi.mocked(launchPlan.createLaunchPlan).mockResolvedValue({
|
|
48
|
-
runtime: 'node',
|
|
49
|
-
command: 'node',
|
|
50
|
-
commandArgs: [],
|
|
51
|
-
envOverrides: {},
|
|
52
|
-
env: {},
|
|
53
|
-
} as any);
|
|
54
|
-
vi.mocked(launchPlan.launchPlanRequiresExistingEntryFile).mockReturnValue(false);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('runs init command with default template and repo', async () => {
|
|
58
|
-
await runCommand(mainCommand, { rawArgs: ['init', 'my-new-project'] });
|
|
59
|
-
expect(giget.downloadTemplate).toHaveBeenCalledWith('github:ecopages/ecopages/examples/starter-jsx', {
|
|
60
|
-
dir: 'my-new-project',
|
|
61
|
-
force: true,
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('runs init command with custom template and repo', async () => {
|
|
66
|
-
await runCommand(mainCommand, {
|
|
67
|
-
rawArgs: ['init', 'my-dir', '--template', 'starter-lit', '--repo', 'custom/repo'],
|
|
68
|
-
});
|
|
69
|
-
expect(giget.downloadTemplate).toHaveBeenCalledWith('github:custom/repo/examples/starter-lit', {
|
|
70
|
-
dir: 'my-dir',
|
|
71
|
-
force: true,
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('runs dev command and passes defaults to launch plan', async () => {
|
|
76
|
-
await runCommand(mainCommand, { rawArgs: ['dev'] });
|
|
77
|
-
expect(launchPlan.createLaunchPlan).toHaveBeenCalledWith(
|
|
78
|
-
['--dev'],
|
|
79
|
-
expect.objectContaining({ nodeEnv: 'development' }),
|
|
80
|
-
'app.ts',
|
|
81
|
-
);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('runs dev:hot command', async () => {
|
|
85
|
-
await runCommand(mainCommand, { rawArgs: ['dev:hot'] });
|
|
86
|
-
expect(launchPlan.createLaunchPlan).toHaveBeenCalledWith(
|
|
87
|
-
['--dev'],
|
|
88
|
-
expect.objectContaining({ hot: true, nodeEnv: 'development' }),
|
|
89
|
-
'app.ts',
|
|
90
|
-
);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('runs dev:watch command', async () => {
|
|
94
|
-
await runCommand(mainCommand, { rawArgs: ['dev:watch'] });
|
|
95
|
-
expect(launchPlan.createLaunchPlan).toHaveBeenCalledWith(
|
|
96
|
-
['--dev'],
|
|
97
|
-
expect.objectContaining({ watch: true, nodeEnv: 'development' }),
|
|
98
|
-
'app.ts',
|
|
99
|
-
);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('runs build command with custom entry file', async () => {
|
|
103
|
-
await runCommand(mainCommand, { rawArgs: ['build', 'server.ts'] });
|
|
104
|
-
expect(launchPlan.createLaunchPlan).toHaveBeenCalledWith(
|
|
105
|
-
['--build'],
|
|
106
|
-
expect.objectContaining({ nodeEnv: 'production' }),
|
|
107
|
-
'server.ts',
|
|
108
|
-
);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('passes shared server options like port and hostname correctly', async () => {
|
|
112
|
-
await runCommand(mainCommand, { rawArgs: ['start', '-p', '4000', '--hostname', '0.0.0.0'] });
|
|
113
|
-
expect(launchPlan.createLaunchPlan).toHaveBeenCalledWith(
|
|
114
|
-
[],
|
|
115
|
-
expect.objectContaining({
|
|
116
|
-
nodeEnv: 'production',
|
|
117
|
-
port: '4000',
|
|
118
|
-
hostname: '0.0.0.0',
|
|
119
|
-
}),
|
|
120
|
-
'app.ts',
|
|
121
|
-
);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('allows overriding base url and debug options', async () => {
|
|
125
|
-
await runCommand(mainCommand, { rawArgs: ['preview', '--base-url', '/my-app/', '-d'] });
|
|
126
|
-
expect(launchPlan.createLaunchPlan).toHaveBeenCalledWith(
|
|
127
|
-
['--preview'],
|
|
128
|
-
expect.objectContaining({
|
|
129
|
-
nodeEnv: 'production',
|
|
130
|
-
'base-url': '/my-app/',
|
|
131
|
-
debug: true,
|
|
132
|
-
}),
|
|
133
|
-
'app.ts',
|
|
134
|
-
);
|
|
135
|
-
});
|
|
136
|
-
});
|
package/bin/launch-plan.test.ts
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import { createRequire } from 'node:module';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
-
import {
|
|
7
|
-
buildEnvOverrides,
|
|
8
|
-
buildBunArgs,
|
|
9
|
-
buildNodeArgs,
|
|
10
|
-
createLaunchPlan,
|
|
11
|
-
detectRuntime,
|
|
12
|
-
launchPlanRequiresExistingEntryFile,
|
|
13
|
-
resolveTsxCliPath,
|
|
14
|
-
} from './launch-plan.js';
|
|
15
|
-
|
|
16
|
-
const require = createRequire(import.meta.url);
|
|
17
|
-
|
|
18
|
-
const originalUserAgent = process.env.npm_config_user_agent;
|
|
19
|
-
const originalCwd = process.cwd();
|
|
20
|
-
|
|
21
|
-
afterEach(() => {
|
|
22
|
-
if (originalUserAgent === undefined) {
|
|
23
|
-
delete process.env.npm_config_user_agent;
|
|
24
|
-
} else {
|
|
25
|
-
process.env.npm_config_user_agent = originalUserAgent;
|
|
26
|
-
}
|
|
27
|
-
process.chdir(originalCwd);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe('launch-plan', () => {
|
|
31
|
-
function writeExperimentalRuntimeConfig(tempDir) {
|
|
32
|
-
fs.writeFileSync(
|
|
33
|
-
path.join(tempDir, 'eco.config.ts'),
|
|
34
|
-
[
|
|
35
|
-
'const rootDir = process.cwd();',
|
|
36
|
-
'export default {',
|
|
37
|
-
'\trootDir,',
|
|
38
|
-
'\tloaders: new Map(),',
|
|
39
|
-
'\tabsolutePaths: {',
|
|
40
|
-
'\t\tconfig: `${rootDir}/eco.config.ts`,',
|
|
41
|
-
'\t\tsrcDir: `${rootDir}/src`,',
|
|
42
|
-
'\t\tdistDir: `${rootDir}/dist`,',
|
|
43
|
-
'\t\tworkDir: `${rootDir}/.eco`,',
|
|
44
|
-
'\t},',
|
|
45
|
-
'\truntime: {},',
|
|
46
|
-
'};',
|
|
47
|
-
].join('\n'),
|
|
48
|
-
'utf8',
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
it('buildEnvOverrides maps CLI options onto environment variables', () => {
|
|
53
|
-
expect(
|
|
54
|
-
buildEnvOverrides({
|
|
55
|
-
port: 4173,
|
|
56
|
-
hostname: '127.0.0.1',
|
|
57
|
-
baseUrl: 'https://example.test',
|
|
58
|
-
debug: true,
|
|
59
|
-
nodeEnv: 'production',
|
|
60
|
-
}),
|
|
61
|
-
).toEqual({
|
|
62
|
-
ECOPAGES_PORT: '4173',
|
|
63
|
-
ECOPAGES_HOSTNAME: '127.0.0.1',
|
|
64
|
-
ECOPAGES_BASE_URL: 'https://example.test',
|
|
65
|
-
ECOPAGES_LOGGER_DEBUG: 'true',
|
|
66
|
-
NODE_ENV: 'production',
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('detectRuntime returns node when Bun is not available', () => {
|
|
71
|
-
process.env.npm_config_user_agent = 'pnpm/10.0.0 npm/? node/v24.0.0 darwin arm64';
|
|
72
|
-
expect(detectRuntime()).toBe('node');
|
|
73
|
-
expect(detectRuntime({ runtime: 'bun' })).toBe('bun');
|
|
74
|
-
expect(detectRuntime({ runtime: 'node' })).toBe('node');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('buildBunArgs preloads eco.config.ts when present', () => {
|
|
78
|
-
expect(buildBunArgs(['--dev'], { hot: true }, 'app.ts', true)).toEqual([
|
|
79
|
-
'--hot',
|
|
80
|
-
'run',
|
|
81
|
-
'--preload',
|
|
82
|
-
'./eco.config.ts',
|
|
83
|
-
'app.ts',
|
|
84
|
-
'--dev',
|
|
85
|
-
]);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('buildNodeArgs imports eco.config.ts when present', () => {
|
|
89
|
-
expect(buildNodeArgs(['--dev'], {}, 'app.ts', true)).toEqual([
|
|
90
|
-
'--import',
|
|
91
|
-
'./eco.config.ts',
|
|
92
|
-
'app.ts',
|
|
93
|
-
'--dev',
|
|
94
|
-
]);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('resolveTsxCliPath resolves the packaged tsx cli entry', () => {
|
|
98
|
-
expect(resolveTsxCliPath()).toBe(require.resolve('tsx/cli'));
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('createLaunchPlan uses the packaged tsx cli for node runtime', async () => {
|
|
102
|
-
const tempDir = fs.mkdtempSync(path.join(tmpdir(), 'eco-cli-launch-plan-'));
|
|
103
|
-
try {
|
|
104
|
-
process.env.npm_config_user_agent = 'pnpm/10.0.0 npm/? node/v24.0.0 darwin arm64';
|
|
105
|
-
process.chdir(tempDir);
|
|
106
|
-
fs.writeFileSync(path.join(tempDir, 'app.ts'), 'await Promise.resolve();', 'utf8');
|
|
107
|
-
writeExperimentalRuntimeConfig(tempDir);
|
|
108
|
-
|
|
109
|
-
const plan = await createLaunchPlan(['--dev'], { runtime: 'node', nodeEnv: 'development' }, 'app.ts');
|
|
110
|
-
|
|
111
|
-
expect(plan).toMatchObject({
|
|
112
|
-
runtime: 'node',
|
|
113
|
-
executionStrategy: 'direct-runtime',
|
|
114
|
-
command: process.execPath,
|
|
115
|
-
});
|
|
116
|
-
expect(plan.commandArgs).toEqual([
|
|
117
|
-
require.resolve('tsx/cli'),
|
|
118
|
-
'--import',
|
|
119
|
-
'./eco.config.ts',
|
|
120
|
-
'app.ts',
|
|
121
|
-
'--dev',
|
|
122
|
-
]);
|
|
123
|
-
} finally {
|
|
124
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('createLaunchPlan uses bun direct runtime and preloads eco.config.ts', async () => {
|
|
129
|
-
const tempDir = fs.mkdtempSync(path.join(tmpdir(), 'eco-cli-launch-plan-'));
|
|
130
|
-
try {
|
|
131
|
-
process.chdir(tempDir);
|
|
132
|
-
fs.writeFileSync(path.join(tempDir, 'app.ts'), 'await Promise.resolve();', 'utf8');
|
|
133
|
-
writeExperimentalRuntimeConfig(tempDir);
|
|
134
|
-
|
|
135
|
-
const plan = await createLaunchPlan(['--preview'], { runtime: 'bun' }, 'app.ts');
|
|
136
|
-
|
|
137
|
-
expect(plan).toMatchObject({
|
|
138
|
-
runtime: 'bun',
|
|
139
|
-
executionStrategy: 'direct-runtime',
|
|
140
|
-
command: 'bun',
|
|
141
|
-
});
|
|
142
|
-
expect(plan.commandArgs).toEqual(['run', '--preload', './eco.config.ts', 'app.ts', '--preview']);
|
|
143
|
-
} finally {
|
|
144
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('launchPlanRequiresExistingEntryFile requires a concrete entry on every runtime path', async () => {
|
|
149
|
-
const tempDir = fs.mkdtempSync(path.join(tmpdir(), 'eco-cli-launch-plan-'));
|
|
150
|
-
try {
|
|
151
|
-
process.chdir(tempDir);
|
|
152
|
-
fs.writeFileSync(path.join(tempDir, 'app.ts'), 'await Promise.resolve();', 'utf8');
|
|
153
|
-
writeExperimentalRuntimeConfig(tempDir);
|
|
154
|
-
|
|
155
|
-
const bunPlan = await createLaunchPlan(['--dev'], { nodeEnv: 'development' }, 'app.ts');
|
|
156
|
-
expect(launchPlanRequiresExistingEntryFile(bunPlan)).toBe(true);
|
|
157
|
-
} finally {
|
|
158
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
});
|