ecopages 0.2.0-beta.30 → 0.2.0-beta.32
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/README.md +2 -1
- package/bin/cli.js +359 -307
- package/bin/launch-plan.js +193 -143
- package/bin/node-require-preload.js +4 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -79,11 +79,12 @@ Trace lines are prefixed with `[ecopages:startup-trace]` and look like:
|
|
|
79
79
|
[ecopages:startup-trace] phase=setupAppRuntimePlugins durationMs=714 wallMs=1999
|
|
80
80
|
[ecopages:startup-trace] phase=route-registry durationMs=1 wallMs=2000
|
|
81
81
|
[ecopages:startup-trace] phase=server-listen durationMs=45 wallMs=2046
|
|
82
|
+
[ecopages:startup-trace] phase=dev-client-transform durationMs=42 wallMs=2100
|
|
82
83
|
[ecopages:startup-trace] phase=first-request-ssr durationMs=5296 wallMs=12495
|
|
83
84
|
[ecopages:startup-trace] summary path=/docs/getting-started/introduction bundleCount=13 clientBundleBytes=858396 wallMs=12496
|
|
84
85
|
```
|
|
85
86
|
|
|
86
|
-
Phases: config ready → runtime plugins → route registry → server listening → first request SSR. The summary includes bundle count and total client JS bytes for that first request.
|
|
87
|
+
Phases: config ready → runtime plugins → route registry → server listening → first request SSR (with per-module `dev-client-transform` on demand for `/assets/__eco_dev__/` modules). The summary includes bundle count and total client JS bytes for that first request.
|
|
87
88
|
|
|
88
89
|
```bash
|
|
89
90
|
# Focused perf trace only
|
package/bin/cli.js
CHANGED
|
@@ -1,335 +1,387 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
|
|
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 { parseArgs } from 'node:util';
|
|
8
|
+
import { Logger } from '@ecopages/logger';
|
|
9
|
+
import { createLaunchPlan } from './launch-plan.js';
|
|
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
|
+
|
|
11
15
|
const sharedServerOptionDefinitions = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
16
|
+
port: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
short: 'p',
|
|
19
|
+
},
|
|
20
|
+
hostname: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
short: 'n',
|
|
23
|
+
},
|
|
24
|
+
'base-url': {
|
|
25
|
+
type: 'string',
|
|
26
|
+
short: 'b',
|
|
27
|
+
},
|
|
28
|
+
debug: {
|
|
29
|
+
type: 'boolean',
|
|
30
|
+
short: 'd',
|
|
31
|
+
},
|
|
32
|
+
'react-fast-refresh': {
|
|
33
|
+
type: 'boolean',
|
|
34
|
+
short: 'r',
|
|
35
|
+
},
|
|
36
|
+
runtime: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
},
|
|
39
|
+
'entry-file': {
|
|
40
|
+
type: 'string',
|
|
41
|
+
short: 'e',
|
|
42
|
+
},
|
|
43
|
+
help: {
|
|
44
|
+
type: 'boolean',
|
|
45
|
+
short: 'h',
|
|
46
|
+
},
|
|
43
47
|
};
|
|
48
|
+
|
|
44
49
|
const initOptionDefinitions = {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
template: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
},
|
|
53
|
+
repo: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
},
|
|
56
|
+
help: {
|
|
57
|
+
type: 'boolean',
|
|
58
|
+
short: 'h',
|
|
59
|
+
},
|
|
55
60
|
};
|
|
61
|
+
|
|
56
62
|
function getMainHelpText() {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
63
|
+
return [
|
|
64
|
+
`ecopages ${pkg.version}`,
|
|
65
|
+
'',
|
|
66
|
+
'Usage: ecopages <command> [options]',
|
|
67
|
+
'',
|
|
68
|
+
'Commands:',
|
|
69
|
+
' init <dir> Initialize a new project from a template',
|
|
70
|
+
' dev Start the development server',
|
|
71
|
+
' dev:watch Start the development server with watch mode',
|
|
72
|
+
' dev:hot Start the development server with hot reload',
|
|
73
|
+
' build Build the project for production',
|
|
74
|
+
' start Start the production server',
|
|
75
|
+
' preview Preview the production build',
|
|
76
|
+
'',
|
|
77
|
+
'Global options:',
|
|
78
|
+
' -e, --entry-file <file> Entry file (default: app.ts)',
|
|
79
|
+
' -h, --help Show help',
|
|
80
|
+
' --version Show version',
|
|
81
|
+
].join('\n');
|
|
76
82
|
}
|
|
83
|
+
|
|
77
84
|
function getServerCommandHelpText(commandName, description) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
85
|
+
return [
|
|
86
|
+
`Usage: ecopages ${commandName} [options]`,
|
|
87
|
+
'',
|
|
88
|
+
description,
|
|
89
|
+
'',
|
|
90
|
+
'Options:',
|
|
91
|
+
' -p, --port <port> Override ECOPAGES_PORT',
|
|
92
|
+
' -n, --hostname <hostname> Override ECOPAGES_HOSTNAME',
|
|
93
|
+
' -b, --base-url <baseUrl> Override ECOPAGES_BASE_URL',
|
|
94
|
+
' -d, --debug Enable debug logging',
|
|
95
|
+
' -r, --react-fast-refresh Enable React Fast Refresh for Bun HMR',
|
|
96
|
+
' --runtime <runtime> Force bun or node',
|
|
97
|
+
' -e, --entry-file <file> Entry file (default: app.ts)',
|
|
98
|
+
' -h, --help Show help',
|
|
99
|
+
].join('\n');
|
|
93
100
|
}
|
|
101
|
+
|
|
94
102
|
function getBuildCommandHelpText() {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
return [
|
|
104
|
+
'Usage: ecopages build [options]',
|
|
105
|
+
'',
|
|
106
|
+
'Build the project for production.',
|
|
107
|
+
'',
|
|
108
|
+
'Options:',
|
|
109
|
+
' -p, --port <port> Override ECOPAGES_PORT',
|
|
110
|
+
' -n, --hostname <hostname> Override ECOPAGES_HOSTNAME',
|
|
111
|
+
' -b, --base-url <baseUrl> Override ECOPAGES_BASE_URL',
|
|
112
|
+
' -d, --debug Enable debug logging',
|
|
113
|
+
' -r, --react-fast-refresh Enable React Fast Refresh for Bun HMR',
|
|
114
|
+
' --runtime <runtime> Force bun or node',
|
|
115
|
+
' -e, --entry-file <file> Entry file (default: app.ts)',
|
|
116
|
+
' -h, --help Show help',
|
|
117
|
+
].join('\n');
|
|
110
118
|
}
|
|
119
|
+
|
|
111
120
|
function getInitCommandHelpText() {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
return [
|
|
122
|
+
'Usage: ecopages init <dir> [options]',
|
|
123
|
+
'',
|
|
124
|
+
'Initialize a new project from a template.',
|
|
125
|
+
'',
|
|
126
|
+
'Options:',
|
|
127
|
+
' --template <template> Template name from ecopages/examples/',
|
|
128
|
+
' --repo <repo> GitHub repo in user/repo form',
|
|
129
|
+
' -h, --help Show help',
|
|
130
|
+
].join('\n');
|
|
122
131
|
}
|
|
132
|
+
|
|
123
133
|
function parseCommandArguments(rawArgs, options) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
134
|
+
return parseArgs({
|
|
135
|
+
args: rawArgs,
|
|
136
|
+
options,
|
|
137
|
+
allowPositionals: true,
|
|
138
|
+
strict: true,
|
|
139
|
+
});
|
|
130
140
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
141
|
+
|
|
142
|
+
function parseServerCommandArgs(rawArgs, commandName, description, mode = 'server') {
|
|
143
|
+
const { values, positionals } = parseCommandArguments(rawArgs, sharedServerOptionDefinitions);
|
|
144
|
+
|
|
145
|
+
if (values.help) {
|
|
146
|
+
console.log(mode === 'build' ? getBuildCommandHelpText() : getServerCommandHelpText(commandName, description));
|
|
147
|
+
return { help: true };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (positionals.length > 0) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Positional entry file arguments are not supported for \`${commandName}\`. Use --entry-file <file> instead.`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const entry = values['entry-file'] ?? 'app.ts';
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
entry,
|
|
160
|
+
options: {
|
|
161
|
+
port: values.port,
|
|
162
|
+
hostname: values.hostname,
|
|
163
|
+
baseUrl: values['base-url'],
|
|
164
|
+
debug: values.debug,
|
|
165
|
+
reactFastRefresh: values['react-fast-refresh'],
|
|
166
|
+
runtime: values.runtime,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
154
169
|
}
|
|
170
|
+
|
|
155
171
|
function parseInitCommandArgs(rawArgs) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
172
|
+
const { values, positionals } = parseCommandArguments(rawArgs, initOptionDefinitions);
|
|
173
|
+
|
|
174
|
+
if (values.help) {
|
|
175
|
+
console.log(getInitCommandHelpText());
|
|
176
|
+
return { help: true };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (positionals.length !== 1) {
|
|
180
|
+
throw new Error('The `init` command requires exactly one target directory argument.');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
dir: positionals[0],
|
|
185
|
+
template: values.template ?? 'starter-jsx',
|
|
186
|
+
repo: values.repo ?? 'ecopages/ecopages',
|
|
187
|
+
};
|
|
169
188
|
}
|
|
189
|
+
|
|
170
190
|
function runLaunchPlan(launchPlan) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
191
|
+
if (Object.keys(launchPlan.envOverrides).length > 0) {
|
|
192
|
+
logger.debug(`Environment overrides: ${JSON.stringify(launchPlan.envOverrides)}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
logger.debug(`Runtime: ${launchPlan.runtime}`);
|
|
196
|
+
logger.debug(`Running: ${launchPlan.command} ${launchPlan.commandArgs.join(' ')}`);
|
|
197
|
+
|
|
198
|
+
const child = spawn(launchPlan.command, launchPlan.commandArgs, {
|
|
199
|
+
stdio: 'inherit',
|
|
200
|
+
env: launchPlan.env,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
child.on('error', (error) => {
|
|
204
|
+
if (error && error.code === 'ENOENT') {
|
|
205
|
+
const hint =
|
|
206
|
+
launchPlan.runtime === 'bun'
|
|
207
|
+
? 'Install Bun from https://bun.sh to continue.'
|
|
208
|
+
: 'Reinstall Node.js or run with --runtime bun if this app requires Bun.';
|
|
209
|
+
logger.error(`Command not found: ${launchPlan.command}. ${hint}`);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
logger.error(`Failed to run command: ${error.message}`);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
child.on('exit', (code) => {
|
|
218
|
+
process.exit(code || 0);
|
|
219
|
+
});
|
|
192
220
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Launch the entry file via the detected or forced runtime.
|
|
224
|
+
* Node uses native Node semantics; Bun uses Bun runtime flags.
|
|
225
|
+
* @param {string[]} args - Arguments to pass to the entry file
|
|
226
|
+
* @param {object} options - CLI options (watch, hot, port, hostname, etc.)
|
|
227
|
+
* @param {string} entryFile - Entry file to run
|
|
228
|
+
*/
|
|
229
|
+
async function runEntryCommand(args, options = {}, entryFile = 'app.ts', launchMode = 'start') {
|
|
230
|
+
let launchPlan;
|
|
231
|
+
const requiresBuiltBundle = launchMode === 'start';
|
|
232
|
+
|
|
233
|
+
if (!requiresBuiltBundle && !existsSync(entryFile)) {
|
|
234
|
+
logger.error(`Error: Entry file "${entryFile}" not found in the current directory.`);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
launchPlan = await createLaunchPlan(args, options, entryFile, launchMode);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
242
|
+
logger.error(message);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
runLaunchPlan(launchPlan);
|
|
208
247
|
}
|
|
248
|
+
|
|
209
249
|
async function runInitCommand(rawArgs) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
250
|
+
const parsed = parseInitCommandArgs(rawArgs);
|
|
251
|
+
|
|
252
|
+
if (parsed.help) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const { dir, template, repo } = parsed;
|
|
257
|
+
|
|
258
|
+
if (existsSync(dir)) {
|
|
259
|
+
logger.error(`Target directory already exists: ${dir}`);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
logger.info(`Creating target directory '${dir}'...`);
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
await downloadTemplate(`github:${repo}/examples/${template}`, {
|
|
267
|
+
dir,
|
|
268
|
+
force: true,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const pkgPath = join(dir, 'package.json');
|
|
272
|
+
if (existsSync(pkgPath)) {
|
|
273
|
+
const projectPkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
274
|
+
projectPkg.name = dir;
|
|
275
|
+
writeFileSync(pkgPath, JSON.stringify(projectPkg, null, 2) + '\n');
|
|
276
|
+
logger.info(`Renamed project to '${dir}'`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
logger.info('Project initialized! Run `bun install && bun dev` to start.');
|
|
280
|
+
} catch (error) {
|
|
281
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
282
|
+
logger.error(`Failed to fetch template: ${message}`);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
238
285
|
}
|
|
286
|
+
|
|
239
287
|
async function runServerCommand(rawArgs, definition) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
288
|
+
const parsed = parseServerCommandArgs(rawArgs, definition.name, definition.description, definition.mode);
|
|
289
|
+
|
|
290
|
+
if (parsed.help) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
await runEntryCommand(
|
|
295
|
+
definition.entryArgs,
|
|
296
|
+
{ ...parsed.options, ...definition.optionOverrides, entryFile: parsed.entry },
|
|
297
|
+
parsed.entry,
|
|
298
|
+
definition.launchMode ?? definition.name,
|
|
299
|
+
);
|
|
250
300
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
301
|
+
|
|
302
|
+
export async function runCli(rawArgs = process.argv.slice(2)) {
|
|
303
|
+
const [commandName, ...commandArgs] = rawArgs;
|
|
304
|
+
|
|
305
|
+
if (!commandName || commandName === '--help' || commandName === '-h') {
|
|
306
|
+
console.log(getMainHelpText());
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (commandName === '--version') {
|
|
311
|
+
console.log(pkg.version);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
switch (commandName) {
|
|
317
|
+
case 'init':
|
|
318
|
+
await runInitCommand(commandArgs);
|
|
319
|
+
return;
|
|
320
|
+
case 'dev':
|
|
321
|
+
await runServerCommand(commandArgs, {
|
|
322
|
+
name: 'dev',
|
|
323
|
+
description: 'Start the development server.',
|
|
324
|
+
entryArgs: ['--dev'],
|
|
325
|
+
launchMode: 'dev',
|
|
326
|
+
optionOverrides: { nodeEnv: 'development' },
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
case 'dev:watch':
|
|
330
|
+
await runServerCommand(commandArgs, {
|
|
331
|
+
name: 'dev:watch',
|
|
332
|
+
description: 'Start the development server with watch mode.',
|
|
333
|
+
entryArgs: ['--dev'],
|
|
334
|
+
launchMode: 'dev',
|
|
335
|
+
optionOverrides: { watch: true, nodeEnv: 'development' },
|
|
336
|
+
});
|
|
337
|
+
return;
|
|
338
|
+
case 'dev:hot':
|
|
339
|
+
await runServerCommand(commandArgs, {
|
|
340
|
+
name: 'dev:hot',
|
|
341
|
+
description: 'Start the development server with hot reload.',
|
|
342
|
+
entryArgs: ['--dev'],
|
|
343
|
+
launchMode: 'dev',
|
|
344
|
+
optionOverrides: { hot: true, nodeEnv: 'development' },
|
|
345
|
+
});
|
|
346
|
+
return;
|
|
347
|
+
case 'build':
|
|
348
|
+
await runServerCommand(commandArgs, {
|
|
349
|
+
name: 'build',
|
|
350
|
+
description: 'Build the project for production.',
|
|
351
|
+
entryArgs: ['--build'],
|
|
352
|
+
launchMode: 'build',
|
|
353
|
+
optionOverrides: { nodeEnv: 'production' },
|
|
354
|
+
mode: 'build',
|
|
355
|
+
});
|
|
356
|
+
return;
|
|
357
|
+
case 'start':
|
|
358
|
+
await runServerCommand(commandArgs, {
|
|
359
|
+
name: 'start',
|
|
360
|
+
description: 'Start the production server.',
|
|
361
|
+
entryArgs: [],
|
|
362
|
+
launchMode: 'start',
|
|
363
|
+
optionOverrides: { nodeEnv: 'production' },
|
|
364
|
+
});
|
|
365
|
+
return;
|
|
366
|
+
case 'preview':
|
|
367
|
+
await runServerCommand(commandArgs, {
|
|
368
|
+
name: 'preview',
|
|
369
|
+
description: 'Preview the production build.',
|
|
370
|
+
entryArgs: ['--preview'],
|
|
371
|
+
launchMode: 'preview',
|
|
372
|
+
optionOverrides: { nodeEnv: 'production' },
|
|
373
|
+
});
|
|
374
|
+
return;
|
|
375
|
+
default:
|
|
376
|
+
throw new Error(`Unknown command \`${commandName}\`.`);
|
|
377
|
+
}
|
|
378
|
+
} catch (error) {
|
|
379
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
380
|
+
logger.error(message);
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
329
383
|
}
|
|
384
|
+
|
|
330
385
|
if (!process.env.VITEST) {
|
|
331
|
-
|
|
386
|
+
runCli();
|
|
332
387
|
}
|
|
333
|
-
export {
|
|
334
|
-
runCli
|
|
335
|
-
};
|
package/bin/launch-plan.js
CHANGED
|
@@ -1,156 +1,206 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from
|
|
2
|
-
import path from
|
|
3
|
-
import { parseEnv } from
|
|
4
|
-
import { SERVER_BUNDLE_DIR, SERVER_BUNDLE_FILENAME } from
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseEnv } from 'node:util';
|
|
4
|
+
import { SERVER_BUNDLE_DIR, SERVER_BUNDLE_FILENAME } from '@ecopages/core/utils/resolve-entry-file';
|
|
5
|
+
|
|
6
|
+
const SERVER_BUNDLE_MANIFEST_FILENAME = 'manifest.json';
|
|
7
|
+
|
|
8
|
+
const nodeRequirePreload = import.meta.resolve('./node-require-preload.js');
|
|
9
|
+
const tsxLoader = import.meta.resolve('tsx/esm');
|
|
10
|
+
|
|
8
11
|
function getEnvFilePaths(nodeEnv) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
const envFiles = ['.env', '.env.local'];
|
|
13
|
+
|
|
14
|
+
if (nodeEnv) {
|
|
15
|
+
envFiles.push(`.env.${nodeEnv}`, `.env.${nodeEnv}.local`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return envFiles.filter((envFile) => existsSync(envFile));
|
|
14
19
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
|
|
21
|
+
export function buildEnvOverrides(options) {
|
|
22
|
+
const env = {};
|
|
23
|
+
if (options.port) env.ECOPAGES_PORT = String(options.port);
|
|
24
|
+
if (options.hostname) env.ECOPAGES_HOSTNAME = options.hostname;
|
|
25
|
+
if (options.baseUrl) env.ECOPAGES_BASE_URL = options.baseUrl;
|
|
26
|
+
if (options.debug) env.ECOPAGES_LOGGER_DEBUG = 'true';
|
|
27
|
+
if (options.nodeEnv) env.NODE_ENV = options.nodeEnv;
|
|
28
|
+
if (options.entryFile) env.ECOPAGES_ENTRY_FILE = options.entryFile;
|
|
29
|
+
return env;
|
|
24
30
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
|
|
32
|
+
export function buildLaunchEnv(options) {
|
|
33
|
+
const envOverrides = buildEnvOverrides(options);
|
|
34
|
+
const envFileValues = getEnvFilePaths(options.nodeEnv).reduce((env, envFile) => {
|
|
35
|
+
return { ...env, ...parseEnv(readFileSync(envFile, 'utf8')) };
|
|
36
|
+
}, {});
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
envOverrides,
|
|
40
|
+
env: { ...envFileValues, ...process.env, ...envOverrides },
|
|
41
|
+
};
|
|
34
42
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
|
|
44
|
+
export function detectRuntime(options = {}) {
|
|
45
|
+
if (options.runtime === 'bun' || options.runtime === 'node') {
|
|
46
|
+
return options.runtime;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const userAgent = process.env.npm_config_user_agent || '';
|
|
50
|
+
|
|
51
|
+
if (userAgent.startsWith('bun/')) {
|
|
52
|
+
return 'bun';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (typeof Bun !== 'undefined') {
|
|
56
|
+
return 'bun';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return 'node';
|
|
47
60
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
|
|
62
|
+
export function buildBunArgs(args, options, entryFile, hasConfig) {
|
|
63
|
+
const bunArgs = [];
|
|
64
|
+
|
|
65
|
+
if (options.watch) bunArgs.push('--watch');
|
|
66
|
+
if (options.hot) bunArgs.push('--hot');
|
|
67
|
+
|
|
68
|
+
bunArgs.push('run');
|
|
69
|
+
|
|
70
|
+
if (hasConfig) {
|
|
71
|
+
bunArgs.push('--preload', `./eco.config.${'ts'}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
bunArgs.push(entryFile, ...args);
|
|
75
|
+
|
|
76
|
+
if (options.reactFastRefresh) {
|
|
77
|
+
bunArgs.push('--react-fast-refresh');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return bunArgs;
|
|
61
81
|
}
|
|
82
|
+
|
|
62
83
|
function usesProductionBundle(launchMode) {
|
|
63
|
-
|
|
84
|
+
return launchMode === 'start';
|
|
64
85
|
}
|
|
86
|
+
|
|
65
87
|
function inferLaunchMode(args, launchMode) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
if (launchMode) {
|
|
89
|
+
return launchMode;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (args.includes('--build')) {
|
|
93
|
+
return 'build';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (args.includes('--preview')) {
|
|
97
|
+
return 'preview';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (args.includes('--dev')) {
|
|
101
|
+
return 'dev';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return 'start';
|
|
79
105
|
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Builds the command and environment needed to launch the app.
|
|
109
|
+
*
|
|
110
|
+
* In `start` mode, the bundled server
|
|
111
|
+
* entry at `dist/{SERVER_BUNDLE_DIR}/{SERVER_BUNDLE_FILENAME}` is used
|
|
112
|
+
* directly. If the bundle is missing, an error is thrown directing the
|
|
113
|
+
* caller to run `ecopages build` first.
|
|
114
|
+
*
|
|
115
|
+
* In `build`, `dev`, and `preview` modes, the source entry file is executed via tsx
|
|
116
|
+
* (Node) or Bun's native runtime with the appropriate loader flags.
|
|
117
|
+
*
|
|
118
|
+
* @param {string[]} args - Arguments forwarded to the entry file.
|
|
119
|
+
* @param {object} options - CLI options (nodeEnv, runtime, port, etc.).
|
|
120
|
+
* @param {string} entryFile - Path to the entry file (default: `app.ts`).
|
|
121
|
+
* @param {'build' | 'dev' | 'preview' | 'start'} launchMode - The CLI command mode.
|
|
122
|
+
* @returns {{ runtime: string, command: string, commandArgs: string[], envOverrides: object, env: object }}
|
|
123
|
+
* @throws {Error} When `launchMode` is `start` and the bundle does not exist.
|
|
124
|
+
*/
|
|
125
|
+
|
|
80
126
|
function resolveProductionServerEntry(cwd = process.cwd()) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
127
|
+
const manifestPath = path.join(cwd, 'dist', SERVER_BUNDLE_DIR, SERVER_BUNDLE_MANIFEST_FILENAME);
|
|
128
|
+
if (existsSync(manifestPath)) {
|
|
129
|
+
try {
|
|
130
|
+
const parsed = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
131
|
+
if (parsed?.serverEntry) {
|
|
132
|
+
const fromManifest = path.isAbsolute(parsed.serverEntry)
|
|
133
|
+
? parsed.serverEntry
|
|
134
|
+
: path.join(path.dirname(manifestPath), parsed.serverEntry);
|
|
135
|
+
if (existsSync(fromManifest)) {
|
|
136
|
+
return fromManifest;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (parsed?.distDir) {
|
|
140
|
+
const fromDistDir = path.join(parsed.distDir, SERVER_BUNDLE_DIR, SERVER_BUNDLE_FILENAME);
|
|
141
|
+
if (existsSync(fromDistDir)) {
|
|
142
|
+
return fromDistDir;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
// fall through to legacy path
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const legacyPath = path.join(cwd, 'dist', SERVER_BUNDLE_DIR, SERVER_BUNDLE_FILENAME);
|
|
151
|
+
return existsSync(legacyPath) ? legacyPath : undefined;
|
|
102
152
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
153
|
+
|
|
154
|
+
export function createLaunchPlan(args, options, entryFile, launchMode) {
|
|
155
|
+
const resolvedOptions = options ?? {};
|
|
156
|
+
const resolvedEntryFile = entryFile ?? 'app.ts';
|
|
157
|
+
const { envOverrides, env } = buildLaunchEnv(resolvedOptions);
|
|
158
|
+
const runtime = detectRuntime(resolvedOptions);
|
|
159
|
+
const resolvedLaunchMode = inferLaunchMode(args, launchMode);
|
|
160
|
+
|
|
161
|
+
const distServerApp = resolveProductionServerEntry(process.cwd());
|
|
162
|
+
const shouldUseBundle = usesProductionBundle(resolvedLaunchMode);
|
|
163
|
+
const useBundle = shouldUseBundle && Boolean(distServerApp);
|
|
164
|
+
|
|
165
|
+
if (shouldUseBundle && !useBundle) {
|
|
166
|
+
throw new Error('No production bundle found. Run `ecopages build` before starting in production mode.');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (useBundle) {
|
|
170
|
+
if (runtime === 'node') {
|
|
171
|
+
return {
|
|
172
|
+
runtime,
|
|
173
|
+
command: process.execPath,
|
|
174
|
+
commandArgs: [distServerApp, ...args],
|
|
175
|
+
envOverrides,
|
|
176
|
+
env,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
runtime,
|
|
182
|
+
command: 'bun',
|
|
183
|
+
commandArgs: ['run', distServerApp, ...args],
|
|
184
|
+
envOverrides,
|
|
185
|
+
env,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (runtime === 'node') {
|
|
190
|
+
return {
|
|
191
|
+
runtime,
|
|
192
|
+
command: process.execPath,
|
|
193
|
+
commandArgs: ['--import', nodeRequirePreload, '--import', tsxLoader, resolvedEntryFile, ...args],
|
|
194
|
+
envOverrides,
|
|
195
|
+
env,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
runtime,
|
|
201
|
+
command: 'bun',
|
|
202
|
+
commandArgs: buildBunArgs(args, resolvedOptions, resolvedEntryFile, existsSync('eco.config.ts')),
|
|
203
|
+
envOverrides,
|
|
204
|
+
env,
|
|
205
|
+
};
|
|
149
206
|
}
|
|
150
|
-
export {
|
|
151
|
-
buildBunArgs,
|
|
152
|
-
buildEnvOverrides,
|
|
153
|
-
buildLaunchEnv,
|
|
154
|
-
createLaunchPlan,
|
|
155
|
-
detectRuntime
|
|
156
|
-
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import path from
|
|
2
|
-
import { createRequire } from
|
|
3
|
-
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
|
|
4
|
+
globalThis.require = createRequire(path.join(process.cwd(), 'package.json'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ecopages",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.32",
|
|
4
4
|
"description": "CLI utilities for Ecopages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"ecopages": "bin/cli.js"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@ecopages/core": "0.2.0-beta.
|
|
35
|
+
"@ecopages/core": "0.2.0-beta.32",
|
|
36
36
|
"@ecopages/logger": "^0.2.3",
|
|
37
37
|
"giget": "^2.0.0",
|
|
38
38
|
"tsx": "^4.22.3"
|