ecopages 0.2.0-rc.0 → 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 +31 -12
- package/bin/brand.js +42 -0
- package/bin/cli.js +45 -97
- package/bin/git-template-source-providers.js +56 -0
- package/bin/git-template-source-shared.js +7 -0
- package/bin/git-template-source.js +62 -0
- package/bin/init.js +392 -0
- package/bin/launch-plan.js +45 -12
- package/package.json +7 -3
- package/templates.json +95 -0
package/README.md
CHANGED
|
@@ -6,10 +6,16 @@ It provides scaffolding and development commands to streamline your workflow. It
|
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
|
9
|
-
Initialize a new project from the
|
|
9
|
+
Initialize a new project from the interactive template picker:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
bunx ecopages init
|
|
12
|
+
bunx ecopages init
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
For a deterministic invocation, select a template explicitly:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bunx ecopages init my-app --template react
|
|
13
19
|
cd my-app
|
|
14
20
|
bun install
|
|
15
21
|
bun dev
|
|
@@ -17,18 +23,29 @@ bun dev
|
|
|
17
23
|
|
|
18
24
|
## Commands
|
|
19
25
|
|
|
20
|
-
| Command
|
|
21
|
-
|
|
|
22
|
-
| `ecopages init
|
|
23
|
-
| `ecopages dev
|
|
24
|
-
| `ecopages dev:watch
|
|
25
|
-
| `ecopages dev:hot
|
|
26
|
-
| `ecopages build
|
|
27
|
-
| `ecopages start
|
|
28
|
-
| `ecopages preview
|
|
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` |
|
|
29
35
|
|
|
30
36
|
> [!NOTE]
|
|
31
|
-
>
|
|
37
|
+
> The entry file defaults to `app.ts`. Override it with `--entry-file`.
|
|
38
|
+
|
|
39
|
+
### Templates
|
|
40
|
+
|
|
41
|
+
Official templates are versioned with the CLI release. Use `--template <id>` for an official template or `--from <source>` for a community Git template. Community sources support giget provider notation and GitHub, GitLab, Bitbucket, and SourceHut repository URLs.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
ecopages init my-site --template jsx
|
|
45
|
+
ecopages init my-site --from github:acme/my-template#v1.0.0
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The official template IDs are `jsx`, `react`, `lit-jsx`, `radiant`, `blog-jsx`, `blog-react`, `docs-starter`, `react-better-auth`, and `llm-wiki`.
|
|
32
49
|
|
|
33
50
|
## Environment & Runtime Options
|
|
34
51
|
|
|
@@ -63,6 +80,8 @@ ecopages dev --port 8080 --debug
|
|
|
63
80
|
ecopages dev -r
|
|
64
81
|
```
|
|
65
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
|
+
|
|
66
85
|
### Debug logging and startup trace
|
|
67
86
|
|
|
68
87
|
Set these in `.env` or on the command line when diagnosing slow dev startup or first page load.
|
package/bin/brand.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Terminal mark rasterized from `assets/brand/logo-on-light.svg`.
|
|
5
|
+
* @remarks Half-blocks keep rows flush; a thin stroke leaves the spiral's inner gaps open.
|
|
6
|
+
*/
|
|
7
|
+
const BRAND_MARK_LINES = [
|
|
8
|
+
' ▄▄▄▄',
|
|
9
|
+
' ▄▄▄▄█▀▀▀▀▄█',
|
|
10
|
+
' ▄▄▀▀▀ ██',
|
|
11
|
+
' ▄█▀ ▄▄▄▄ █',
|
|
12
|
+
'▄█ █▀ ██ █',
|
|
13
|
+
'█▄ ▀█▄ ██ ▄█ ██',
|
|
14
|
+
' █▄ ▀▀██▀▀▀ █',
|
|
15
|
+
' ▀▀█▄ ▀█ ▄█▀',
|
|
16
|
+
' ▄█▀ ▀▀▀▀▀',
|
|
17
|
+
' █▀',
|
|
18
|
+
'▀',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export function formatBrandBanner(version, stream = process.stdout) {
|
|
22
|
+
const markWidth = Math.max(...BRAND_MARK_LINES.map((line) => [...line].length));
|
|
23
|
+
const versionLabel = stream.hasColors?.() ? styleText('dim', version) : version;
|
|
24
|
+
const labels = ['ecopages', versionLabel];
|
|
25
|
+
const labelStart = Math.max(0, Math.floor((BRAND_MARK_LINES.length - labels.length) / 2));
|
|
26
|
+
|
|
27
|
+
return BRAND_MARK_LINES.map((line, index) => {
|
|
28
|
+
const padded = `${line}${' '.repeat(markWidth - [...line].length)}`;
|
|
29
|
+
const label = labels[index - labelStart];
|
|
30
|
+
return label === undefined ? padded : `${padded} ${label}`;
|
|
31
|
+
}).join('\n');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function withBrandBanner(version, text) {
|
|
35
|
+
if (!process.stdout.isTTY) return text;
|
|
36
|
+
return `${formatBrandBanner(version)}\n\n${text}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function printBrandBanner(version) {
|
|
40
|
+
if (!process.stderr.isTTY) return;
|
|
41
|
+
process.stderr.write(`${formatBrandBanner(version, process.stderr)}\n\n`);
|
|
42
|
+
}
|
package/bin/cli.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
4
|
import { spawn } from 'node:child_process';
|
|
6
|
-
import { join } from 'node:path';
|
|
7
5
|
import { parseArgs } from 'node:util';
|
|
8
6
|
import { Logger } from '@ecopages/logger';
|
|
9
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';
|
|
12
|
+
import { withBrandBanner } from './brand.js';
|
|
10
13
|
|
|
11
14
|
const logger = new Logger('[ecopages:cli]', { debug: process.env.ECOPAGES_LOGGER_DEBUG === 'true' });
|
|
12
15
|
|
|
@@ -40,18 +43,9 @@ const sharedServerOptionDefinitions = {
|
|
|
40
43
|
type: 'string',
|
|
41
44
|
short: 'e',
|
|
42
45
|
},
|
|
43
|
-
|
|
44
|
-
type: 'boolean',
|
|
45
|
-
short: 'h',
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const initOptionDefinitions = {
|
|
50
|
-
template: {
|
|
51
|
-
type: 'string',
|
|
52
|
-
},
|
|
53
|
-
repo: {
|
|
46
|
+
config: {
|
|
54
47
|
type: 'string',
|
|
48
|
+
short: 'c',
|
|
55
49
|
},
|
|
56
50
|
help: {
|
|
57
51
|
type: 'boolean',
|
|
@@ -61,12 +55,10 @@ const initOptionDefinitions = {
|
|
|
61
55
|
|
|
62
56
|
function getMainHelpText() {
|
|
63
57
|
return [
|
|
64
|
-
`ecopages ${pkg.version}`,
|
|
65
|
-
'',
|
|
66
58
|
'Usage: ecopages <command> [options]',
|
|
67
59
|
'',
|
|
68
60
|
'Commands:',
|
|
69
|
-
' init
|
|
61
|
+
' init [dir] Initialize a new project from a template',
|
|
70
62
|
' dev Start the development server',
|
|
71
63
|
' dev:watch Start the development server with watch mode',
|
|
72
64
|
' dev:hot Start the development server with hot reload',
|
|
@@ -76,6 +68,7 @@ function getMainHelpText() {
|
|
|
76
68
|
'',
|
|
77
69
|
'Global options:',
|
|
78
70
|
' -e, --entry-file <file> Entry file (default: app.ts)',
|
|
71
|
+
' -c, --config <file> Ecopages config file (default: eco.config.ts)',
|
|
79
72
|
' -h, --help Show help',
|
|
80
73
|
' --version Show version',
|
|
81
74
|
].join('\n');
|
|
@@ -95,6 +88,7 @@ function getServerCommandHelpText(commandName, description) {
|
|
|
95
88
|
' -r, --react-fast-refresh Enable React Fast Refresh for Bun HMR',
|
|
96
89
|
' --runtime <runtime> Force bun or node',
|
|
97
90
|
' -e, --entry-file <file> Entry file (default: app.ts)',
|
|
91
|
+
' -c, --config <file> Ecopages config file (default: eco.config.ts)',
|
|
98
92
|
' -h, --help Show help',
|
|
99
93
|
].join('\n');
|
|
100
94
|
}
|
|
@@ -113,19 +107,7 @@ function getBuildCommandHelpText() {
|
|
|
113
107
|
' -r, --react-fast-refresh Enable React Fast Refresh for Bun HMR',
|
|
114
108
|
' --runtime <runtime> Force bun or node',
|
|
115
109
|
' -e, --entry-file <file> Entry file (default: app.ts)',
|
|
116
|
-
' -
|
|
117
|
-
].join('\n');
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function getInitCommandHelpText() {
|
|
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',
|
|
110
|
+
' -c, --config <file> Ecopages config file (default: eco.config.ts)',
|
|
129
111
|
' -h, --help Show help',
|
|
130
112
|
].join('\n');
|
|
131
113
|
}
|
|
@@ -164,30 +146,12 @@ function parseServerCommandArgs(rawArgs, commandName, description, mode = 'serve
|
|
|
164
146
|
debug: values.debug,
|
|
165
147
|
reactFastRefresh: values['react-fast-refresh'],
|
|
166
148
|
runtime: values.runtime,
|
|
149
|
+
configFile: values.config,
|
|
167
150
|
},
|
|
168
151
|
};
|
|
169
152
|
}
|
|
170
153
|
|
|
171
|
-
function
|
|
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
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function runLaunchPlan(launchPlan) {
|
|
154
|
+
function runLaunchPlan(launchPlan, options = {}) {
|
|
191
155
|
if (Object.keys(launchPlan.envOverrides).length > 0) {
|
|
192
156
|
logger.debug(`Environment overrides: ${JSON.stringify(launchPlan.envOverrides)}`);
|
|
193
157
|
}
|
|
@@ -215,6 +179,11 @@ function runLaunchPlan(launchPlan) {
|
|
|
215
179
|
});
|
|
216
180
|
|
|
217
181
|
child.on('exit', (code) => {
|
|
182
|
+
if (options.superviseDevRestarts && code === ECOPAGES_DEV_RESTART_EXIT_CODE) {
|
|
183
|
+
options.onDevRestart?.();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
218
187
|
process.exit(code || 0);
|
|
219
188
|
});
|
|
220
189
|
}
|
|
@@ -227,61 +196,38 @@ function runLaunchPlan(launchPlan) {
|
|
|
227
196
|
* @param {string} entryFile - Entry file to run
|
|
228
197
|
*/
|
|
229
198
|
async function runEntryCommand(args, options = {}, entryFile = 'app.ts', launchMode = 'start') {
|
|
230
|
-
let launchPlan;
|
|
231
199
|
const requiresBuiltBundle = launchMode === 'start';
|
|
200
|
+
const superviseDevRestarts = launchMode === 'dev';
|
|
232
201
|
|
|
233
202
|
if (!requiresBuiltBundle && !existsSync(entryFile)) {
|
|
234
203
|
logger.error(`Error: Entry file "${entryFile}" not found in the current directory.`);
|
|
235
204
|
process.exit(1);
|
|
236
205
|
}
|
|
237
206
|
|
|
238
|
-
|
|
239
|
-
launchPlan
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
async function runInitCommand(rawArgs) {
|
|
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
|
-
}
|
|
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
|
+
}
|
|
262
216
|
|
|
263
|
-
|
|
217
|
+
if (restartReason) {
|
|
218
|
+
launchPlan.env[ECOPAGES_DEV_RESTART_REASON_ENV] = restartReason;
|
|
219
|
+
launchPlan.envOverrides[ECOPAGES_DEV_RESTART_REASON_ENV] = restartReason;
|
|
220
|
+
}
|
|
264
221
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
222
|
+
runLaunchPlan(launchPlan, {
|
|
223
|
+
superviseDevRestarts,
|
|
224
|
+
onDevRestart: () => {
|
|
225
|
+
void launchChild('configuration or environment change');
|
|
226
|
+
},
|
|
269
227
|
});
|
|
228
|
+
};
|
|
270
229
|
|
|
271
|
-
|
|
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
|
-
}
|
|
230
|
+
await launchChild();
|
|
285
231
|
}
|
|
286
232
|
|
|
287
233
|
async function runServerCommand(rawArgs, definition) {
|
|
@@ -303,7 +249,7 @@ export async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
303
249
|
const [commandName, ...commandArgs] = rawArgs;
|
|
304
250
|
|
|
305
251
|
if (!commandName || commandName === '--help' || commandName === '-h') {
|
|
306
|
-
console.log(getMainHelpText());
|
|
252
|
+
console.log(withBrandBanner(pkg.version, getMainHelpText()));
|
|
307
253
|
return;
|
|
308
254
|
}
|
|
309
255
|
|
|
@@ -314,9 +260,11 @@ export async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
314
260
|
|
|
315
261
|
try {
|
|
316
262
|
switch (commandName) {
|
|
317
|
-
case 'init':
|
|
318
|
-
await
|
|
263
|
+
case 'init': {
|
|
264
|
+
const { runInitCommand } = await import('./init.js');
|
|
265
|
+
await runInitCommand(commandArgs, logger);
|
|
319
266
|
return;
|
|
267
|
+
}
|
|
320
268
|
case 'dev':
|
|
321
269
|
await runServerCommand(commandArgs, {
|
|
322
270
|
name: 'dev',
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeBitbucketParts,
|
|
3
|
+
normalizeGithubParts,
|
|
4
|
+
normalizeGitlabParts,
|
|
5
|
+
normalizeSourcehutParts,
|
|
6
|
+
} from './git-template-source-providers.js';
|
|
7
|
+
|
|
8
|
+
const GIT_PROVIDER_PREFIX = /^(github|gitlab|bitbucket|sourcehut):/u;
|
|
9
|
+
|
|
10
|
+
const HOST_PROVIDER = {
|
|
11
|
+
'github.com': 'github',
|
|
12
|
+
'gitlab.com': 'gitlab',
|
|
13
|
+
'bitbucket.org': 'bitbucket',
|
|
14
|
+
'git.sr.ht': 'sourcehut',
|
|
15
|
+
};
|
|
16
|
+
|
|
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
|
+
}
|
|
30
|
+
|
|
31
|
+
throw new Error(`Unsupported template URL '${source}'. Include a repository and optional Git ref.`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Normalizes a Git provider URL or giget source into a giget template source.
|
|
36
|
+
*/
|
|
37
|
+
export function normalizeGitSource(source) {
|
|
38
|
+
if (GIT_PROVIDER_PREFIX.test(source)) {
|
|
39
|
+
return source;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let url;
|
|
43
|
+
try {
|
|
44
|
+
url = new URL(source);
|
|
45
|
+
} catch {
|
|
46
|
+
throw new Error(`Unsupported template source '${source}'. Use a Git provider URL or giget source.`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const hostProvider = HOST_PROVIDER[url.hostname];
|
|
50
|
+
if (!hostProvider) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Unsupported template host '${url.hostname}'. Supported hosts are GitHub, GitLab, Bitbucket, and SourceHut.`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const parts = url.pathname
|
|
57
|
+
.replace(/^\/+|\/+$/gu, '')
|
|
58
|
+
.split('/')
|
|
59
|
+
.filter(Boolean);
|
|
60
|
+
|
|
61
|
+
return normalizeProviderUrl(hostProvider, parts, source);
|
|
62
|
+
}
|
package/bin/init.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { downloadTemplate } from 'giget';
|
|
2
|
+
import * as prompts from '@clack/prompts';
|
|
3
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { basename, join } from 'node:path';
|
|
6
|
+
import { parseArgs } from 'node:util';
|
|
7
|
+
import { printBrandBanner, withBrandBanner } from './brand.js';
|
|
8
|
+
import { normalizeGitSource } from './git-template-source.js';
|
|
9
|
+
|
|
10
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
11
|
+
const templateManifest = JSON.parse(readFileSync(new URL('../templates.json', import.meta.url), 'utf-8'));
|
|
12
|
+
const officialTemplates = [...templateManifest.templates].sort((left, right) => left.order - right.order);
|
|
13
|
+
const defaultOfficialTemplate = officialTemplates.find((template) => template.default) ?? officialTemplates[0];
|
|
14
|
+
|
|
15
|
+
const initOptionDefinitions = {
|
|
16
|
+
template: {
|
|
17
|
+
type: 'string',
|
|
18
|
+
},
|
|
19
|
+
repo: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
},
|
|
22
|
+
from: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
},
|
|
25
|
+
interactive: {
|
|
26
|
+
type: 'boolean',
|
|
27
|
+
},
|
|
28
|
+
'no-interactive': {
|
|
29
|
+
type: 'boolean',
|
|
30
|
+
},
|
|
31
|
+
install: {
|
|
32
|
+
type: 'boolean',
|
|
33
|
+
},
|
|
34
|
+
'no-install': {
|
|
35
|
+
type: 'boolean',
|
|
36
|
+
},
|
|
37
|
+
start: {
|
|
38
|
+
type: 'boolean',
|
|
39
|
+
},
|
|
40
|
+
'no-start': {
|
|
41
|
+
type: 'boolean',
|
|
42
|
+
},
|
|
43
|
+
help: {
|
|
44
|
+
type: 'boolean',
|
|
45
|
+
short: 'h',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function getInitCommandHelpText() {
|
|
50
|
+
return [
|
|
51
|
+
'Usage: ecopages init [dir] [options]',
|
|
52
|
+
'',
|
|
53
|
+
'Initialize a new project from a template.',
|
|
54
|
+
'',
|
|
55
|
+
'With no directory in a TTY, starts the interactive project creator.',
|
|
56
|
+
'',
|
|
57
|
+
'Options:',
|
|
58
|
+
' --template <id> Official template ID',
|
|
59
|
+
' --from <source> Community Git template source',
|
|
60
|
+
' --interactive Force interactive mode',
|
|
61
|
+
' --no-interactive Disable interactive mode',
|
|
62
|
+
' --install Install dependencies',
|
|
63
|
+
' --no-install Skip dependency installation',
|
|
64
|
+
' --start Install and start the dev server',
|
|
65
|
+
' --no-start Do not start the dev server',
|
|
66
|
+
' -h, --help Show help',
|
|
67
|
+
].join('\n');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseExclusiveBoolean(enable, disable, enableFlag, disableFlag) {
|
|
71
|
+
if (enable && disable) {
|
|
72
|
+
throw new Error(`The \`${enableFlag}\` and \`${disableFlag}\` options cannot be used together.`);
|
|
73
|
+
}
|
|
74
|
+
if (enable) return true;
|
|
75
|
+
if (disable) return false;
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseInitCommandArgs(rawArgs) {
|
|
80
|
+
const { values, positionals } = parseArgs({
|
|
81
|
+
args: rawArgs,
|
|
82
|
+
options: initOptionDefinitions,
|
|
83
|
+
allowPositionals: true,
|
|
84
|
+
strict: true,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (values.help) {
|
|
88
|
+
console.log(withBrandBanner(pkg.version, getInitCommandHelpText()));
|
|
89
|
+
return { help: true };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (positionals.length > 1) {
|
|
93
|
+
throw new Error('The `init` command accepts at most one target directory argument.');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (values.template && values.from) {
|
|
97
|
+
throw new Error('The `--template` and `--from` options cannot be used together.');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (values.repo) {
|
|
101
|
+
throw new Error('The `--repo` option was removed. Use `--from <source>` instead.');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const interactive = parseExclusiveBoolean(
|
|
105
|
+
values.interactive,
|
|
106
|
+
values['no-interactive'],
|
|
107
|
+
'--interactive',
|
|
108
|
+
'--no-interactive',
|
|
109
|
+
);
|
|
110
|
+
const install = parseExclusiveBoolean(values.install, values['no-install'], '--install', '--no-install');
|
|
111
|
+
const start = parseExclusiveBoolean(values.start, values['no-start'], '--start', '--no-start');
|
|
112
|
+
|
|
113
|
+
if (start && install === false) {
|
|
114
|
+
throw new Error('The `--start` option requires dependency installation.');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const dir = positionals[0];
|
|
118
|
+
const useInteractive = interactive ?? (!dir && Boolean(process.stdin.isTTY));
|
|
119
|
+
|
|
120
|
+
if (!dir && !useInteractive) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
'A target directory is required outside a TTY. Run `ecopages init --interactive` to use prompts.',
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
dir,
|
|
128
|
+
template: values.template,
|
|
129
|
+
from: values.from,
|
|
130
|
+
interactive: useInteractive,
|
|
131
|
+
install,
|
|
132
|
+
start,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function findOfficialTemplate(templateId) {
|
|
137
|
+
const template = officialTemplates.find((candidate) => candidate.id === templateId);
|
|
138
|
+
if (!template) {
|
|
139
|
+
const available = officialTemplates.map((candidate) => candidate.id).join(', ');
|
|
140
|
+
throw new Error(`Unknown official template '${templateId}'. Available templates: ${available}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return template;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function detectPackageManager() {
|
|
147
|
+
const userAgent = process.env.npm_config_user_agent || '';
|
|
148
|
+
if (userAgent.startsWith('bun/')) return 'bun';
|
|
149
|
+
if (userAgent.startsWith('pnpm/')) return 'pnpm';
|
|
150
|
+
return 'npm';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function getTemplateSource(templateId, source) {
|
|
154
|
+
if (source) return normalizeGitSource(source);
|
|
155
|
+
const officialTemplate = findOfficialTemplate(templateId);
|
|
156
|
+
return `github:ecopages/ecopages/templates/${officialTemplate.source}#v${pkg.version}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function runChildCommand(command, args, cwd) {
|
|
160
|
+
return new Promise((resolve, reject) => {
|
|
161
|
+
const child = spawn(command, args, { cwd, stdio: 'inherit' });
|
|
162
|
+
child.on('error', reject);
|
|
163
|
+
child.on('exit', (code, signal) => {
|
|
164
|
+
if (signal) {
|
|
165
|
+
reject(new Error(`${command} was terminated by ${signal}.`));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (code !== 0) {
|
|
170
|
+
reject(new Error(`${command} exited with code ${code ?? 1}.`));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
resolve();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function rewriteWorkspaceDependencies(projectPkg) {
|
|
180
|
+
for (const blockName of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
181
|
+
const block = projectPkg[blockName];
|
|
182
|
+
if (!block || typeof block !== 'object') continue;
|
|
183
|
+
for (const [name, version] of Object.entries(block)) {
|
|
184
|
+
if (version === 'workspace:*') block[name] = pkg.version;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function updateProjectManifest(targetDir, packageDirectory, isOfficial) {
|
|
190
|
+
const packagePath = join(targetDir, packageDirectory, 'package.json');
|
|
191
|
+
if (!existsSync(packagePath)) {
|
|
192
|
+
throw new Error(`Template package directory '${packageDirectory}' does not contain a package.json.`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const projectPkg = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
|
196
|
+
projectPkg.name = basename(targetDir);
|
|
197
|
+
if (isOfficial) rewriteWorkspaceDependencies(projectPkg);
|
|
198
|
+
|
|
199
|
+
if (isOfficial) {
|
|
200
|
+
for (const blockName of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
201
|
+
const block = projectPkg[blockName];
|
|
202
|
+
if (!block || typeof block !== 'object') continue;
|
|
203
|
+
if (
|
|
204
|
+
Object.values(block).some((version) => typeof version === 'string' && version.startsWith('workspace:'))
|
|
205
|
+
) {
|
|
206
|
+
throw new Error(`Template package still contains workspace dependencies in ${packagePath}.`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
writeFileSync(packagePath, `${JSON.stringify(projectPkg, null, 2)}\n`);
|
|
212
|
+
return packagePath;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function isCancelled(value) {
|
|
216
|
+
if (prompts.isCancel(value)) {
|
|
217
|
+
prompts.cancel('Project creation cancelled.');
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
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
|
+
|
|
314
|
+
async function collectInitAnswers(parsed) {
|
|
315
|
+
const packageManager = detectPackageManager();
|
|
316
|
+
let dir = parsed.dir;
|
|
317
|
+
let templateId = parsed.template;
|
|
318
|
+
let source = parsed.from;
|
|
319
|
+
|
|
320
|
+
if (parsed.interactive && !dir) {
|
|
321
|
+
const promptedDir = await promptProjectDirectory();
|
|
322
|
+
if (promptedDir === null) return null;
|
|
323
|
+
dir = promptedDir;
|
|
324
|
+
}
|
|
325
|
+
|
|
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;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (!templateId && !source) templateId = defaultOfficialTemplate.id;
|
|
334
|
+
if (parsed.interactive) {
|
|
335
|
+
prompts.log.step(`Resolved template source: ${getTemplateSource(templateId, source)}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const installAnswer = await promptInstallChoice(packageManager, parsed);
|
|
339
|
+
if (installAnswer === null) return null;
|
|
340
|
+
const install = installAnswer ?? false;
|
|
341
|
+
|
|
342
|
+
const startAnswer = await promptStartChoice(parsed, install);
|
|
343
|
+
if (startAnswer === null) return null;
|
|
344
|
+
const start = startAnswer ?? false;
|
|
345
|
+
|
|
346
|
+
return { dir, templateId, source, packageManager, install, start };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export async function runInitCommand(rawArgs, logger) {
|
|
350
|
+
const parsed = parseInitCommandArgs(rawArgs);
|
|
351
|
+
if (parsed.help) return;
|
|
352
|
+
|
|
353
|
+
printBrandBanner(pkg.version);
|
|
354
|
+
const answers = await collectInitAnswers(parsed);
|
|
355
|
+
if (!answers) return;
|
|
356
|
+
|
|
357
|
+
const { dir, templateId, source, packageManager, install, start } = answers;
|
|
358
|
+
if (existsSync(dir)) throw new Error(`Target directory already exists: ${dir}`);
|
|
359
|
+
|
|
360
|
+
const officialTemplate = source ? null : findOfficialTemplate(templateId);
|
|
361
|
+
const templateSource = getTemplateSource(templateId, source);
|
|
362
|
+
const packageDirectory = officialTemplate?.packageDirectory ?? '.';
|
|
363
|
+
let targetCreated = false;
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
if (!parsed.interactive) prompts.log.step(`Resolved template source: ${templateSource}`);
|
|
367
|
+
prompts.log.step(`Downloading ${officialTemplate?.displayName ?? 'community template'}...`);
|
|
368
|
+
targetCreated = true;
|
|
369
|
+
await downloadTemplate(templateSource, { dir, force: true });
|
|
370
|
+
prompts.log.step('Preparing project manifest...');
|
|
371
|
+
updateProjectManifest(dir, packageDirectory, Boolean(officialTemplate));
|
|
372
|
+
prompts.log.step(`Project created in ${dir}.`);
|
|
373
|
+
|
|
374
|
+
const packageCwd = join(dir, packageDirectory);
|
|
375
|
+
if (install) {
|
|
376
|
+
prompts.log.step(`Installing dependencies with ${packageManager}...`);
|
|
377
|
+
await runChildCommand(packageManager, ['install'], packageCwd);
|
|
378
|
+
}
|
|
379
|
+
if (start) {
|
|
380
|
+
prompts.log.step('Starting the development server...');
|
|
381
|
+
await runChildCommand(packageManager, ['run', 'dev'], packageCwd);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const relativeCwd = packageDirectory === '.' ? dir : join(dir, packageDirectory);
|
|
385
|
+
logger.info(
|
|
386
|
+
`Project initialized. Run 'cd "${relativeCwd}" && ${packageManager} install && ${packageManager} run dev'.`,
|
|
387
|
+
);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
if (targetCreated) rmSync(dir, { recursive: true, force: true });
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
}
|
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,11 +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",
|
|
39
|
+
"@ecopages/core": "0.2.0-rc.10",
|
|
36
40
|
"@ecopages/logger": "^0.2.3",
|
|
37
41
|
"giget": "^2.0.0",
|
|
38
42
|
"tsx": "^4.22.3"
|
|
@@ -41,4 +45,4 @@
|
|
|
41
45
|
"bun-types": "*",
|
|
42
46
|
"typescript": "^5"
|
|
43
47
|
}
|
|
44
|
-
}
|
|
48
|
+
}
|
package/templates.json
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"templates": [
|
|
3
|
+
{
|
|
4
|
+
"id": "jsx",
|
|
5
|
+
"displayName": "JSX",
|
|
6
|
+
"description": "A minimal Ecopages JSX application.",
|
|
7
|
+
"category": "integration",
|
|
8
|
+
"order": 10,
|
|
9
|
+
"default": true,
|
|
10
|
+
"source": "jsx",
|
|
11
|
+
"packageDirectory": ".",
|
|
12
|
+
"integrations": ["ecopages-jsx", "mdx"]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "react",
|
|
16
|
+
"displayName": "React",
|
|
17
|
+
"description": "A minimal Ecopages React application.",
|
|
18
|
+
"category": "integration",
|
|
19
|
+
"order": 20,
|
|
20
|
+
"source": "react",
|
|
21
|
+
"packageDirectory": ".",
|
|
22
|
+
"integrations": ["react"]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "lit-jsx",
|
|
26
|
+
"displayName": "Lit JSX",
|
|
27
|
+
"description": "A KitaJS and Lit Ecopages application.",
|
|
28
|
+
"category": "integration",
|
|
29
|
+
"order": 30,
|
|
30
|
+
"source": "lit-jsx",
|
|
31
|
+
"packageDirectory": ".",
|
|
32
|
+
"integrations": ["kitajs", "lit"]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "radiant",
|
|
36
|
+
"displayName": "Radiant UI",
|
|
37
|
+
"description": "An Ecopages JSX application built with Radiant UI components.",
|
|
38
|
+
"category": "integration",
|
|
39
|
+
"order": 35,
|
|
40
|
+
"source": "radiant",
|
|
41
|
+
"packageDirectory": ".",
|
|
42
|
+
"integrations": ["ecopages-jsx", "radiant-ui"]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "blog-jsx",
|
|
46
|
+
"displayName": "Blog JSX",
|
|
47
|
+
"description": "A content-driven blog rendered with Ecopages JSX.",
|
|
48
|
+
"category": "application",
|
|
49
|
+
"order": 40,
|
|
50
|
+
"source": "blog-jsx",
|
|
51
|
+
"packageDirectory": ".",
|
|
52
|
+
"integrations": ["ecopages-jsx", "mdx"]
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"id": "blog-react",
|
|
56
|
+
"displayName": "Blog React",
|
|
57
|
+
"description": "A content-driven blog rendered with React.",
|
|
58
|
+
"category": "application",
|
|
59
|
+
"order": 50,
|
|
60
|
+
"source": "blog-react",
|
|
61
|
+
"packageDirectory": ".",
|
|
62
|
+
"integrations": ["react", "mdx"]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"id": "docs-starter",
|
|
66
|
+
"displayName": "Docs Starter",
|
|
67
|
+
"description": "A documentation site powered by the content processor.",
|
|
68
|
+
"category": "application",
|
|
69
|
+
"order": 60,
|
|
70
|
+
"source": "docs-starter",
|
|
71
|
+
"packageDirectory": ".",
|
|
72
|
+
"integrations": ["ecopages-jsx", "mdx"]
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": "react-better-auth",
|
|
76
|
+
"displayName": "React Better Auth",
|
|
77
|
+
"description": "A React application with Better Auth and Drizzle.",
|
|
78
|
+
"category": "application",
|
|
79
|
+
"order": 70,
|
|
80
|
+
"source": "react-better-auth",
|
|
81
|
+
"packageDirectory": ".",
|
|
82
|
+
"integrations": ["react"]
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"id": "llm-wiki",
|
|
86
|
+
"displayName": "LLM Wiki",
|
|
87
|
+
"description": "A persistent, LLM-maintained knowledge base and site.",
|
|
88
|
+
"category": "application",
|
|
89
|
+
"order": 80,
|
|
90
|
+
"source": "llm-wiki",
|
|
91
|
+
"packageDirectory": "app",
|
|
92
|
+
"integrations": ["ecopages-jsx", "mdx"]
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
}
|