ecopages 0.2.0-rc.1 → 0.2.0-rc.10
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 +11 -9
- package/bin/cli.js +42 -10
- package/bin/git-template-source-providers.js +56 -0
- package/bin/git-template-source-shared.js +7 -0
- package/bin/git-template-source.js +22 -48
- package/bin/init.js +107 -56
- package/bin/launch-plan.js +45 -12
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -23,15 +23,15 @@ bun dev
|
|
|
23
23
|
|
|
24
24
|
## Commands
|
|
25
25
|
|
|
26
|
-
| Command | Description
|
|
27
|
-
| :-------------------- |
|
|
28
|
-
| `ecopages init [dir]` | Scaffolds a new project (interactive without `dir`)
|
|
29
|
-
| `ecopages dev` | Starts the dev server
|
|
30
|
-
| `ecopages dev:watch` | Dev server + hard restarts on file changes
|
|
31
|
-
| `ecopages dev:hot` | Dev server + HMR (no hard restarts)
|
|
32
|
-
| `ecopages build` | Creates a production build
|
|
33
|
-
| `ecopages start` | Starts the production server
|
|
34
|
-
| `ecopages preview` | Previews the production build locally
|
|
26
|
+
| Command | Description | Equivalent (Bun) |
|
|
27
|
+
| :-------------------- | :------------------------------------------------------ | :----------------------------------------------------------- |
|
|
28
|
+
| `ecopages init [dir]` | Scaffolds a new project (interactive without `dir`) | N/A |
|
|
29
|
+
| `ecopages dev` | Starts the dev server (supervises config/.env restarts) | `bun run [entry] --dev` |
|
|
30
|
+
| `ecopages dev:watch` | Dev server + hard restarts on file changes | Bun: `bun --watch run [entry] --dev`; Node: `node --watch …` |
|
|
31
|
+
| `ecopages dev:hot` | Dev server + HMR (no hard restarts) | `bun --hot run [entry] --dev` |
|
|
32
|
+
| `ecopages build` | Creates a production build | `bun run [entry] --build` |
|
|
33
|
+
| `ecopages start` | Starts the production server | `bun run [entry]` |
|
|
34
|
+
| `ecopages preview` | Previews the production build locally | `bun run [entry] --preview` |
|
|
35
35
|
|
|
36
36
|
> [!NOTE]
|
|
37
37
|
> The entry file defaults to `app.ts`. Override it with `--entry-file`.
|
|
@@ -80,6 +80,8 @@ ecopages dev --port 8080 --debug
|
|
|
80
80
|
ecopages dev -r
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
During `ecopages dev`, adding, changing, or removing `eco.config.ts` (or `--config`) and supported project `.env` files restarts the dev server automatically. The CLI supervises that restart and reloads dotenv files. When the preferred port is busy, development startup uses the same fallback flow as preview: TTY sessions show a Clack confirmation, while non-interactive sessions scan automatically only when the port was not pinned with `--port` or `ECOPAGES_PORT`.
|
|
84
|
+
|
|
83
85
|
### Debug logging and startup trace
|
|
84
86
|
|
|
85
87
|
Set these in `.env` or on the command line when diagnosing slow dev startup or first page load.
|
package/bin/cli.js
CHANGED
|
@@ -5,6 +5,10 @@ import { spawn } from 'node:child_process';
|
|
|
5
5
|
import { parseArgs } from 'node:util';
|
|
6
6
|
import { Logger } from '@ecopages/logger';
|
|
7
7
|
import { createLaunchPlan } from './launch-plan.js';
|
|
8
|
+
import {
|
|
9
|
+
ECOPAGES_DEV_RESTART_EXIT_CODE,
|
|
10
|
+
ECOPAGES_DEV_RESTART_REASON_ENV,
|
|
11
|
+
} from '@ecopages/core/dev/development-restart';
|
|
8
12
|
import { withBrandBanner } from './brand.js';
|
|
9
13
|
|
|
10
14
|
const logger = new Logger('[ecopages:cli]', { debug: process.env.ECOPAGES_LOGGER_DEBUG === 'true' });
|
|
@@ -39,6 +43,10 @@ const sharedServerOptionDefinitions = {
|
|
|
39
43
|
type: 'string',
|
|
40
44
|
short: 'e',
|
|
41
45
|
},
|
|
46
|
+
config: {
|
|
47
|
+
type: 'string',
|
|
48
|
+
short: 'c',
|
|
49
|
+
},
|
|
42
50
|
help: {
|
|
43
51
|
type: 'boolean',
|
|
44
52
|
short: 'h',
|
|
@@ -60,6 +68,7 @@ function getMainHelpText() {
|
|
|
60
68
|
'',
|
|
61
69
|
'Global options:',
|
|
62
70
|
' -e, --entry-file <file> Entry file (default: app.ts)',
|
|
71
|
+
' -c, --config <file> Ecopages config file (default: eco.config.ts)',
|
|
63
72
|
' -h, --help Show help',
|
|
64
73
|
' --version Show version',
|
|
65
74
|
].join('\n');
|
|
@@ -79,6 +88,7 @@ function getServerCommandHelpText(commandName, description) {
|
|
|
79
88
|
' -r, --react-fast-refresh Enable React Fast Refresh for Bun HMR',
|
|
80
89
|
' --runtime <runtime> Force bun or node',
|
|
81
90
|
' -e, --entry-file <file> Entry file (default: app.ts)',
|
|
91
|
+
' -c, --config <file> Ecopages config file (default: eco.config.ts)',
|
|
82
92
|
' -h, --help Show help',
|
|
83
93
|
].join('\n');
|
|
84
94
|
}
|
|
@@ -97,6 +107,7 @@ function getBuildCommandHelpText() {
|
|
|
97
107
|
' -r, --react-fast-refresh Enable React Fast Refresh for Bun HMR',
|
|
98
108
|
' --runtime <runtime> Force bun or node',
|
|
99
109
|
' -e, --entry-file <file> Entry file (default: app.ts)',
|
|
110
|
+
' -c, --config <file> Ecopages config file (default: eco.config.ts)',
|
|
100
111
|
' -h, --help Show help',
|
|
101
112
|
].join('\n');
|
|
102
113
|
}
|
|
@@ -135,11 +146,12 @@ function parseServerCommandArgs(rawArgs, commandName, description, mode = 'serve
|
|
|
135
146
|
debug: values.debug,
|
|
136
147
|
reactFastRefresh: values['react-fast-refresh'],
|
|
137
148
|
runtime: values.runtime,
|
|
149
|
+
configFile: values.config,
|
|
138
150
|
},
|
|
139
151
|
};
|
|
140
152
|
}
|
|
141
153
|
|
|
142
|
-
function runLaunchPlan(launchPlan) {
|
|
154
|
+
function runLaunchPlan(launchPlan, options = {}) {
|
|
143
155
|
if (Object.keys(launchPlan.envOverrides).length > 0) {
|
|
144
156
|
logger.debug(`Environment overrides: ${JSON.stringify(launchPlan.envOverrides)}`);
|
|
145
157
|
}
|
|
@@ -167,6 +179,11 @@ function runLaunchPlan(launchPlan) {
|
|
|
167
179
|
});
|
|
168
180
|
|
|
169
181
|
child.on('exit', (code) => {
|
|
182
|
+
if (options.superviseDevRestarts && code === ECOPAGES_DEV_RESTART_EXIT_CODE) {
|
|
183
|
+
options.onDevRestart?.();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
170
187
|
process.exit(code || 0);
|
|
171
188
|
});
|
|
172
189
|
}
|
|
@@ -179,23 +196,38 @@ function runLaunchPlan(launchPlan) {
|
|
|
179
196
|
* @param {string} entryFile - Entry file to run
|
|
180
197
|
*/
|
|
181
198
|
async function runEntryCommand(args, options = {}, entryFile = 'app.ts', launchMode = 'start') {
|
|
182
|
-
let launchPlan;
|
|
183
199
|
const requiresBuiltBundle = launchMode === 'start';
|
|
200
|
+
const superviseDevRestarts = launchMode === 'dev';
|
|
184
201
|
|
|
185
202
|
if (!requiresBuiltBundle && !existsSync(entryFile)) {
|
|
186
203
|
logger.error(`Error: Entry file "${entryFile}" not found in the current directory.`);
|
|
187
204
|
process.exit(1);
|
|
188
205
|
}
|
|
189
206
|
|
|
190
|
-
|
|
191
|
-
launchPlan
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
207
|
+
const launchChild = async (restartReason) => {
|
|
208
|
+
let launchPlan;
|
|
209
|
+
try {
|
|
210
|
+
launchPlan = await createLaunchPlan(args, options, entryFile, launchMode);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
213
|
+
logger.error(message);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (restartReason) {
|
|
218
|
+
launchPlan.env[ECOPAGES_DEV_RESTART_REASON_ENV] = restartReason;
|
|
219
|
+
launchPlan.envOverrides[ECOPAGES_DEV_RESTART_REASON_ENV] = restartReason;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
runLaunchPlan(launchPlan, {
|
|
223
|
+
superviseDevRestarts,
|
|
224
|
+
onDevRestart: () => {
|
|
225
|
+
void launchChild('configuration or environment change');
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
};
|
|
197
229
|
|
|
198
|
-
|
|
230
|
+
await launchChild();
|
|
199
231
|
}
|
|
200
232
|
|
|
201
233
|
async function runServerCommand(rawArgs, definition) {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { repositoryName, withSubpath } from './git-template-source-shared.js';
|
|
2
|
+
|
|
3
|
+
export function normalizeGithubParts(parts, source) {
|
|
4
|
+
if (parts.length < 2) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const [owner, repositoryPart, marker, ref, ...subpath] = parts;
|
|
9
|
+
const repository = `${owner}/${repositoryName(repositoryPart)}`;
|
|
10
|
+
if (marker === 'tree' || marker === 'blob') {
|
|
11
|
+
if (!ref) throw new Error(`Template URL '${source}' is missing a Git ref.`);
|
|
12
|
+
return `github:${withSubpath(repository, subpath)}#${ref}`;
|
|
13
|
+
}
|
|
14
|
+
return `github:${repository}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function normalizeGitlabParts(parts) {
|
|
18
|
+
const markerIndex = parts.indexOf('-');
|
|
19
|
+
const treeIndex = parts.findIndex((part) => part === 'tree' || part === 'blob');
|
|
20
|
+
if (markerIndex > 0 && treeIndex > markerIndex && parts[treeIndex + 1]) {
|
|
21
|
+
const repository = parts.slice(0, markerIndex).map(repositoryName).join('/');
|
|
22
|
+
const ref = parts[treeIndex + 1];
|
|
23
|
+
const subpath = parts.slice(treeIndex + 2);
|
|
24
|
+
return `gitlab:${withSubpath(repository, subpath)}#${ref}`;
|
|
25
|
+
}
|
|
26
|
+
if (parts.length >= 2) return `gitlab:${parts.join('/')}`;
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizeBitbucketParts(parts) {
|
|
31
|
+
if (parts.length < 2) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const [workspace, repositoryPart, marker, ref, ...subpath] = parts;
|
|
36
|
+
const repository = `${workspace}/${repositoryName(repositoryPart)}`;
|
|
37
|
+
if (marker === 'src' && ref) {
|
|
38
|
+
return `bitbucket:${withSubpath(repository, subpath)}#${ref}`;
|
|
39
|
+
}
|
|
40
|
+
return `bitbucket:${repository}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizeSourcehutParts(parts) {
|
|
44
|
+
if (parts.length < 2) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const treeIndex = parts.findIndex((part) => part === 'tree' || part === 'blob');
|
|
49
|
+
if (treeIndex > 1 && parts[treeIndex + 1]) {
|
|
50
|
+
const repository = parts.slice(0, treeIndex).map(repositoryName).join('/');
|
|
51
|
+
const ref = parts[treeIndex + 1];
|
|
52
|
+
const subpath = parts.slice(treeIndex + 2);
|
|
53
|
+
return `sourcehut:${withSubpath(repository, subpath)}#${ref}`;
|
|
54
|
+
}
|
|
55
|
+
return `sourcehut:${parts.join('/')}`;
|
|
56
|
+
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeBitbucketParts,
|
|
3
|
+
normalizeGithubParts,
|
|
4
|
+
normalizeGitlabParts,
|
|
5
|
+
normalizeSourcehutParts,
|
|
6
|
+
} from './git-template-source-providers.js';
|
|
7
|
+
|
|
1
8
|
const GIT_PROVIDER_PREFIX = /^(github|gitlab|bitbucket|sourcehut):/u;
|
|
2
9
|
|
|
3
10
|
const HOST_PROVIDER = {
|
|
@@ -7,12 +14,21 @@ const HOST_PROVIDER = {
|
|
|
7
14
|
'git.sr.ht': 'sourcehut',
|
|
8
15
|
};
|
|
9
16
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
const PROVIDER_NORMALIZERS = {
|
|
18
|
+
github: normalizeGithubParts,
|
|
19
|
+
gitlab: normalizeGitlabParts,
|
|
20
|
+
bitbucket: normalizeBitbucketParts,
|
|
21
|
+
sourcehut: normalizeSourcehutParts,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function normalizeProviderUrl(hostProvider, parts, source) {
|
|
25
|
+
const normalize = PROVIDER_NORMALIZERS[hostProvider];
|
|
26
|
+
const normalized = normalize?.(parts, source);
|
|
27
|
+
if (normalized) {
|
|
28
|
+
return normalized;
|
|
29
|
+
}
|
|
13
30
|
|
|
14
|
-
|
|
15
|
-
return subpath.length > 0 ? `${repository}/${subpath.join('/')}` : repository;
|
|
31
|
+
throw new Error(`Unsupported template URL '${source}'. Include a repository and optional Git ref.`);
|
|
16
32
|
}
|
|
17
33
|
|
|
18
34
|
/**
|
|
@@ -42,47 +58,5 @@ export function normalizeGitSource(source) {
|
|
|
42
58
|
.split('/')
|
|
43
59
|
.filter(Boolean);
|
|
44
60
|
|
|
45
|
-
|
|
46
|
-
const [owner, repositoryPart, marker, ref, ...subpath] = parts;
|
|
47
|
-
const repository = `${owner}/${repositoryName(repositoryPart)}`;
|
|
48
|
-
if (marker === 'tree' || marker === 'blob') {
|
|
49
|
-
if (!ref) throw new Error(`Template URL '${source}' is missing a Git ref.`);
|
|
50
|
-
return `github:${withSubpath(repository, subpath)}#${ref}`;
|
|
51
|
-
}
|
|
52
|
-
return `github:${repository}`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (hostProvider === 'gitlab') {
|
|
56
|
-
const markerIndex = parts.indexOf('-');
|
|
57
|
-
const treeIndex = parts.findIndex((part) => part === 'tree' || part === 'blob');
|
|
58
|
-
if (markerIndex > 0 && treeIndex > markerIndex && parts[treeIndex + 1]) {
|
|
59
|
-
const repository = parts.slice(0, markerIndex).map(repositoryName).join('/');
|
|
60
|
-
const ref = parts[treeIndex + 1];
|
|
61
|
-
const subpath = parts.slice(treeIndex + 2);
|
|
62
|
-
return `gitlab:${withSubpath(repository, subpath)}#${ref}`;
|
|
63
|
-
}
|
|
64
|
-
if (parts.length >= 2) return `gitlab:${parts.join('/')}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (hostProvider === 'bitbucket' && parts.length >= 2) {
|
|
68
|
-
const [workspace, repositoryPart, marker, ref, ...subpath] = parts;
|
|
69
|
-
const repository = `${workspace}/${repositoryName(repositoryPart)}`;
|
|
70
|
-
if (marker === 'src' && ref) {
|
|
71
|
-
return `bitbucket:${withSubpath(repository, subpath)}#${ref}`;
|
|
72
|
-
}
|
|
73
|
-
return `bitbucket:${repository}`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (hostProvider === 'sourcehut' && parts.length >= 2) {
|
|
77
|
-
const treeIndex = parts.findIndex((part) => part === 'tree' || part === 'blob');
|
|
78
|
-
if (treeIndex > 1 && parts[treeIndex + 1]) {
|
|
79
|
-
const repository = parts.slice(0, treeIndex).map(repositoryName).join('/');
|
|
80
|
-
const ref = parts[treeIndex + 1];
|
|
81
|
-
const subpath = parts.slice(treeIndex + 2);
|
|
82
|
-
return `sourcehut:${withSubpath(repository, subpath)}#${ref}`;
|
|
83
|
-
}
|
|
84
|
-
return `sourcehut:${parts.join('/')}`;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
throw new Error(`Unsupported template URL '${source}'. Include a repository and optional Git ref.`);
|
|
61
|
+
return normalizeProviderUrl(hostProvider, parts, source);
|
|
88
62
|
}
|
package/bin/init.js
CHANGED
|
@@ -220,52 +220,114 @@ function isCancelled(value) {
|
|
|
220
220
|
return false;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
async function promptProjectDirectory() {
|
|
224
|
+
const answer = await prompts.text({
|
|
225
|
+
message: 'Project directory:',
|
|
226
|
+
placeholder: 'my-app',
|
|
227
|
+
validate: (value) => (value.trim().length === 0 ? 'Enter a project directory.' : undefined),
|
|
228
|
+
});
|
|
229
|
+
if (isCancelled(answer)) return null;
|
|
230
|
+
return answer.trim();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function promptTemplateSelection() {
|
|
234
|
+
const answer = await prompts.select({
|
|
235
|
+
message: 'Select a template:',
|
|
236
|
+
options: [
|
|
237
|
+
...officialTemplates.map((template) => ({
|
|
238
|
+
label: template.displayName,
|
|
239
|
+
value: template.id,
|
|
240
|
+
hint: template.description,
|
|
241
|
+
})),
|
|
242
|
+
{
|
|
243
|
+
label: 'Community template',
|
|
244
|
+
value: '__community__',
|
|
245
|
+
hint: 'Use a Git repository or provider source.',
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
});
|
|
249
|
+
if (isCancelled(answer)) return null;
|
|
250
|
+
return answer;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function promptCommunityTemplateSource() {
|
|
254
|
+
const sourceAnswer = await prompts.text({
|
|
255
|
+
message: 'Template source:',
|
|
256
|
+
placeholder: 'github:owner/repository#v1.0.0',
|
|
257
|
+
validate: (value) => (value.trim().length === 0 ? 'Enter a Git template source.' : undefined),
|
|
258
|
+
});
|
|
259
|
+
if (isCancelled(sourceAnswer)) return null;
|
|
260
|
+
return sourceAnswer.trim();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function resolveInteractiveTemplateChoice(templateId, source) {
|
|
264
|
+
if (templateId || source) {
|
|
265
|
+
return { templateId, source };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const answer = await promptTemplateSelection();
|
|
269
|
+
if (answer === null) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
if (answer === '__community__') {
|
|
273
|
+
const communitySource = await promptCommunityTemplateSource();
|
|
274
|
+
if (communitySource === null) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
return { templateId, source: communitySource };
|
|
278
|
+
}
|
|
279
|
+
return { templateId: answer, source };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function promptInstallChoice(packageManager, parsed) {
|
|
283
|
+
if (parsed.start) {
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
if (parsed.install !== undefined) {
|
|
287
|
+
return parsed.install;
|
|
288
|
+
}
|
|
289
|
+
if (!parsed.interactive) {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const answer = await prompts.confirm({
|
|
294
|
+
message: `Install dependencies with ${packageManager}?`,
|
|
295
|
+
initialValue: true,
|
|
296
|
+
});
|
|
297
|
+
if (isCancelled(answer)) return null;
|
|
298
|
+
return answer;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function promptStartChoice(parsed, install) {
|
|
302
|
+
if (parsed.start !== undefined) {
|
|
303
|
+
return parsed.start;
|
|
304
|
+
}
|
|
305
|
+
if (!parsed.interactive || !install) {
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const answer = await prompts.confirm({ message: 'Start the development server now?', initialValue: false });
|
|
310
|
+
if (isCancelled(answer)) return null;
|
|
311
|
+
return answer;
|
|
312
|
+
}
|
|
313
|
+
|
|
223
314
|
async function collectInitAnswers(parsed) {
|
|
224
315
|
const packageManager = detectPackageManager();
|
|
225
316
|
let dir = parsed.dir;
|
|
226
317
|
let templateId = parsed.template;
|
|
227
318
|
let source = parsed.from;
|
|
228
319
|
|
|
229
|
-
if (parsed.interactive) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
validate: (value) => (value.trim().length === 0 ? 'Enter a project directory.' : undefined),
|
|
235
|
-
});
|
|
236
|
-
if (isCancelled(answer)) return null;
|
|
237
|
-
dir = answer.trim();
|
|
238
|
-
}
|
|
320
|
+
if (parsed.interactive && !dir) {
|
|
321
|
+
const promptedDir = await promptProjectDirectory();
|
|
322
|
+
if (promptedDir === null) return null;
|
|
323
|
+
dir = promptedDir;
|
|
324
|
+
}
|
|
239
325
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
label: template.displayName,
|
|
246
|
-
value: template.id,
|
|
247
|
-
hint: template.description,
|
|
248
|
-
})),
|
|
249
|
-
{
|
|
250
|
-
label: 'Community template',
|
|
251
|
-
value: '__community__',
|
|
252
|
-
hint: 'Use a Git repository or provider source.',
|
|
253
|
-
},
|
|
254
|
-
],
|
|
255
|
-
});
|
|
256
|
-
if (isCancelled(answer)) return null;
|
|
257
|
-
if (answer === '__community__') {
|
|
258
|
-
const sourceAnswer = await prompts.text({
|
|
259
|
-
message: 'Template source:',
|
|
260
|
-
placeholder: 'github:owner/repository#v1.0.0',
|
|
261
|
-
validate: (value) => (value.trim().length === 0 ? 'Enter a Git template source.' : undefined),
|
|
262
|
-
});
|
|
263
|
-
if (isCancelled(sourceAnswer)) return null;
|
|
264
|
-
source = sourceAnswer.trim();
|
|
265
|
-
} else {
|
|
266
|
-
templateId = answer;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
326
|
+
if (parsed.interactive) {
|
|
327
|
+
const templateChoice = await resolveInteractiveTemplateChoice(templateId, source);
|
|
328
|
+
if (templateChoice === null) return null;
|
|
329
|
+
templateId = templateChoice.templateId;
|
|
330
|
+
source = templateChoice.source;
|
|
269
331
|
}
|
|
270
332
|
|
|
271
333
|
if (!templateId && !source) templateId = defaultOfficialTemplate.id;
|
|
@@ -273,24 +335,13 @@ async function collectInitAnswers(parsed) {
|
|
|
273
335
|
prompts.log.step(`Resolved template source: ${getTemplateSource(templateId, source)}`);
|
|
274
336
|
}
|
|
275
337
|
|
|
276
|
-
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
message: `Install dependencies with ${packageManager}?`,
|
|
280
|
-
initialValue: true,
|
|
281
|
-
});
|
|
282
|
-
if (isCancelled(answer)) return null;
|
|
283
|
-
install = answer;
|
|
284
|
-
}
|
|
285
|
-
install ??= false;
|
|
338
|
+
const installAnswer = await promptInstallChoice(packageManager, parsed);
|
|
339
|
+
if (installAnswer === null) return null;
|
|
340
|
+
const install = installAnswer ?? false;
|
|
286
341
|
|
|
287
|
-
|
|
288
|
-
if (
|
|
289
|
-
|
|
290
|
-
if (isCancelled(answer)) return null;
|
|
291
|
-
start = answer;
|
|
292
|
-
}
|
|
293
|
-
start ??= false;
|
|
342
|
+
const startAnswer = await promptStartChoice(parsed, install);
|
|
343
|
+
if (startAnswer === null) return null;
|
|
344
|
+
const start = startAnswer ?? false;
|
|
294
345
|
|
|
295
346
|
return { dir, templateId, source, packageManager, install, start };
|
|
296
347
|
}
|
package/bin/launch-plan.js
CHANGED
|
@@ -2,6 +2,12 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { parseEnv } from 'node:util';
|
|
4
4
|
import { SERVER_BUNDLE_DIR, SERVER_BUNDLE_FILENAME } from '@ecopages/core/utils/resolve-entry-file';
|
|
5
|
+
import {
|
|
6
|
+
resolveEcoConfigPath,
|
|
7
|
+
resolveEmittedEcoConfigPath,
|
|
8
|
+
assertProductionConfigIdentity,
|
|
9
|
+
} from '@ecopages/core/config';
|
|
10
|
+
import { ECOPAGES_ENTRY_WATCH_ENV, getDevEnvFileNames } from '@ecopages/core/dev/development-restart';
|
|
5
11
|
|
|
6
12
|
const SERVER_BUNDLE_MANIFEST_FILENAME = 'manifest.json';
|
|
7
13
|
|
|
@@ -9,13 +15,7 @@ const nodeRequirePreload = import.meta.resolve('./node-require-preload.js');
|
|
|
9
15
|
const tsxLoader = import.meta.resolve('tsx/esm');
|
|
10
16
|
|
|
11
17
|
function getEnvFilePaths(nodeEnv) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (nodeEnv) {
|
|
15
|
-
envFiles.push(`.env.${nodeEnv}`, `.env.${nodeEnv}.local`);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return envFiles.filter((envFile) => existsSync(envFile));
|
|
18
|
+
return getDevEnvFileNames(nodeEnv).filter((envFile) => existsSync(envFile));
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export function buildEnvOverrides(options) {
|
|
@@ -26,9 +26,26 @@ export function buildEnvOverrides(options) {
|
|
|
26
26
|
if (options.debug) env.ECOPAGES_LOGGER_DEBUG = 'true';
|
|
27
27
|
if (options.nodeEnv) env.NODE_ENV = options.nodeEnv;
|
|
28
28
|
if (options.entryFile) env.ECOPAGES_ENTRY_FILE = options.entryFile;
|
|
29
|
+
if (options.configFile) {
|
|
30
|
+
env.ECOPAGES_CONFIG_FILE = path.isAbsolute(options.configFile)
|
|
31
|
+
? options.configFile
|
|
32
|
+
: path.resolve(process.cwd(), options.configFile);
|
|
33
|
+
}
|
|
34
|
+
if (options.watch) {
|
|
35
|
+
env[ECOPAGES_ENTRY_WATCH_ENV] = '1';
|
|
36
|
+
}
|
|
29
37
|
return env;
|
|
30
38
|
}
|
|
31
39
|
|
|
40
|
+
export function resolveEcoConfigFilePath(options = {}, cwd = process.cwd(), resolverOptions = {}) {
|
|
41
|
+
return resolveEcoConfigPath({
|
|
42
|
+
configFile: options.configFile,
|
|
43
|
+
cwd,
|
|
44
|
+
required: false,
|
|
45
|
+
preferEmitted: resolverOptions.preferEmitted,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
export function buildLaunchEnv(options) {
|
|
33
50
|
const envOverrides = buildEnvOverrides(options);
|
|
34
51
|
const envFileValues = getEnvFilePaths(options.nodeEnv).reduce((env, envFile) => {
|
|
@@ -59,7 +76,7 @@ export function detectRuntime(options = {}) {
|
|
|
59
76
|
return 'node';
|
|
60
77
|
}
|
|
61
78
|
|
|
62
|
-
export function buildBunArgs(args, options, entryFile,
|
|
79
|
+
export function buildBunArgs(args, options, entryFile, configFilePath) {
|
|
63
80
|
const bunArgs = [];
|
|
64
81
|
|
|
65
82
|
if (options.watch) bunArgs.push('--watch');
|
|
@@ -67,8 +84,8 @@ export function buildBunArgs(args, options, entryFile, hasConfig) {
|
|
|
67
84
|
|
|
68
85
|
bunArgs.push('run');
|
|
69
86
|
|
|
70
|
-
if (
|
|
71
|
-
bunArgs.push('--preload',
|
|
87
|
+
if (configFilePath) {
|
|
88
|
+
bunArgs.push('--preload', configFilePath);
|
|
72
89
|
}
|
|
73
90
|
|
|
74
91
|
bunArgs.push(entryFile, ...args);
|
|
@@ -157,6 +174,10 @@ export function createLaunchPlan(args, options, entryFile, launchMode) {
|
|
|
157
174
|
const { envOverrides, env } = buildLaunchEnv(resolvedOptions);
|
|
158
175
|
const runtime = detectRuntime(resolvedOptions);
|
|
159
176
|
const resolvedLaunchMode = inferLaunchMode(args, launchMode);
|
|
177
|
+
const isExplicitConfig = Boolean(resolvedOptions.configFile || process.env.ECOPAGES_CONFIG_FILE);
|
|
178
|
+
const resolvedConfigFile = resolveEcoConfigFilePath(resolvedOptions, process.cwd(), {
|
|
179
|
+
preferEmitted: usesProductionBundle(resolvedLaunchMode) && !isExplicitConfig,
|
|
180
|
+
});
|
|
160
181
|
|
|
161
182
|
const distServerApp = resolveProductionServerEntry(process.cwd());
|
|
162
183
|
const shouldUseBundle = usesProductionBundle(resolvedLaunchMode);
|
|
@@ -167,6 +188,12 @@ export function createLaunchPlan(args, options, entryFile, launchMode) {
|
|
|
167
188
|
}
|
|
168
189
|
|
|
169
190
|
if (useBundle) {
|
|
191
|
+
assertProductionConfigIdentity(process.cwd(), resolvedConfigFile, { isExplicitOverride: isExplicitConfig });
|
|
192
|
+
const runtimeConfigFile = resolveEmittedEcoConfigPath(process.cwd()) ?? resolvedConfigFile;
|
|
193
|
+
if (runtimeConfigFile) {
|
|
194
|
+
envOverrides.ECOPAGES_CONFIG_FILE = runtimeConfigFile;
|
|
195
|
+
env.ECOPAGES_CONFIG_FILE = runtimeConfigFile;
|
|
196
|
+
}
|
|
170
197
|
if (runtime === 'node') {
|
|
171
198
|
return {
|
|
172
199
|
runtime,
|
|
@@ -187,10 +214,16 @@ export function createLaunchPlan(args, options, entryFile, launchMode) {
|
|
|
187
214
|
}
|
|
188
215
|
|
|
189
216
|
if (runtime === 'node') {
|
|
217
|
+
const commandArgs = ['--import', nodeRequirePreload, '--import', tsxLoader];
|
|
218
|
+
if (resolvedOptions.watch) {
|
|
219
|
+
commandArgs.push('--watch');
|
|
220
|
+
}
|
|
221
|
+
commandArgs.push(resolvedEntryFile, ...args);
|
|
222
|
+
|
|
190
223
|
return {
|
|
191
224
|
runtime,
|
|
192
225
|
command: process.execPath,
|
|
193
|
-
commandArgs
|
|
226
|
+
commandArgs,
|
|
194
227
|
envOverrides,
|
|
195
228
|
env,
|
|
196
229
|
};
|
|
@@ -199,7 +232,7 @@ export function createLaunchPlan(args, options, entryFile, launchMode) {
|
|
|
199
232
|
return {
|
|
200
233
|
runtime,
|
|
201
234
|
command: 'bun',
|
|
202
|
-
commandArgs: buildBunArgs(args, resolvedOptions, resolvedEntryFile,
|
|
235
|
+
commandArgs: buildBunArgs(args, resolvedOptions, resolvedEntryFile, resolvedConfigFile),
|
|
203
236
|
envOverrides,
|
|
204
237
|
env,
|
|
205
238
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ecopages",
|
|
3
|
-
"version": "0.2.0-rc.
|
|
3
|
+
"version": "0.2.0-rc.10",
|
|
4
4
|
"description": "CLI utilities for Ecopages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -28,12 +28,15 @@
|
|
|
28
28
|
"lit",
|
|
29
29
|
"mdx"
|
|
30
30
|
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
31
34
|
"bin": {
|
|
32
35
|
"ecopages": "bin/cli.js"
|
|
33
36
|
},
|
|
34
37
|
"dependencies": {
|
|
35
38
|
"@clack/prompts": "^1.7.0",
|
|
36
|
-
"@ecopages/core": "0.2.0-rc.
|
|
39
|
+
"@ecopages/core": "0.2.0-rc.10",
|
|
37
40
|
"@ecopages/logger": "^0.2.3",
|
|
38
41
|
"giget": "^2.0.0",
|
|
39
42
|
"tsx": "^4.22.3"
|
|
@@ -42,4 +45,4 @@
|
|
|
42
45
|
"bun-types": "*",
|
|
43
46
|
"typescript": "^5"
|
|
44
47
|
}
|
|
45
|
-
}
|
|
48
|
+
}
|