ecopages 0.2.0-alpha.1 → 0.2.0-alpha.11
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 +55 -69
- package/bin/cli.js +198 -206
- package/bin/cli.test.ts +136 -0
- package/bin/launch-plan.js +112 -0
- package/bin/launch-plan.test.ts +160 -0
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
# ecopages
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
> This package is currently in a draft state and is subject to significant changes.
|
|
3
|
+
The official CLI for the Ecopages framework.
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
- **Project scaffolding**: Quickly initialize new Ecopages projects from templates using `bunx ecopages init`
|
|
9
|
-
- **Command utilities**: Namespaced commands that wrap common Bun operations, automatically detecting and applying your `eco.config.ts`
|
|
5
|
+
It provides scaffolding and development commands to streamline your workflow. It prefers Bun when available, falls back to Node otherwise, and automatically detects your `eco.config.ts`.
|
|
10
6
|
|
|
11
7
|
## Quick Start
|
|
12
8
|
|
|
13
|
-
Initialize a new project:
|
|
9
|
+
Initialize a new project from the default template:
|
|
14
10
|
|
|
15
11
|
```bash
|
|
16
12
|
bunx ecopages init my-app
|
|
@@ -19,91 +15,81 @@ bun install
|
|
|
19
15
|
bun dev
|
|
20
16
|
```
|
|
21
17
|
|
|
22
|
-
##
|
|
18
|
+
## Commands
|
|
23
19
|
|
|
24
|
-
|
|
20
|
+
| Command | Description | Equivalent (Bun) |
|
|
21
|
+
| :--------------------------- | :----------------------------------------- | :------------------------------ |
|
|
22
|
+
| `ecopages init <dir>` | Scaffolds a new project | N/A |
|
|
23
|
+
| `ecopages dev [entry]` | Starts the dev server | `bun run [entry] --dev` |
|
|
24
|
+
| `ecopages dev:watch [entry]` | Dev server + hard restarts on file changes | `bun --watch run [entry] --dev` |
|
|
25
|
+
| `ecopages dev:hot [entry]` | Dev server + HMR (no hard restarts) | `bun --hot run [entry] --dev` |
|
|
26
|
+
| `ecopages build [entry]` | Creates a production build | `bun run [entry] --build` |
|
|
27
|
+
| `ecopages start [entry]` | Starts the production server | `bun run [entry]` |
|
|
28
|
+
| `ecopages preview [entry]` | Previews the production build locally | `bun run [entry] --preview` |
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
| `ecopages init <dir>` | Initialize a new Ecopages project | scaffolding tool |
|
|
29
|
-
| `ecopages dev [entry]` | Start the development server | `bun run [entry] --dev` |
|
|
30
|
-
| `ecopages dev:watch [entry]` | Start with watch mode (restarts on file changes) | `bun --watch run [entry] --dev` |
|
|
31
|
-
| `ecopages dev:hot [entry]` | Start with hot reload (HMR without restart) | `bun --hot run [entry] --dev` |
|
|
32
|
-
| `ecopages build [entry]` | Build for production | `bun run [entry] --build` |
|
|
33
|
-
| `ecopages start [entry]` | Start production server | `bun run [entry]` |
|
|
34
|
-
| `ecopages preview [entry]` | Preview production build | `bun run [entry] --preview` |
|
|
30
|
+
> [!NOTE]
|
|
31
|
+
> `[entry]` defaults to `app.ts` if not provided.
|
|
35
32
|
|
|
36
|
-
|
|
33
|
+
## Environment & Runtime Options
|
|
37
34
|
|
|
38
|
-
|
|
35
|
+
Server and build commands accept the following options. They automatically map to the equivalent environment variables for the underlying process:
|
|
39
36
|
|
|
40
|
-
|
|
37
|
+
| Option | Env Var | Description |
|
|
38
|
+
| :------------------------- | :---------------------- | :---------------------------------- |
|
|
39
|
+
| `-p, --port <port>` | `ECOPAGES_PORT` | Server port (default 3000) |
|
|
40
|
+
| `-n, --hostname <host>` | `ECOPAGES_HOSTNAME` | Server hostname |
|
|
41
|
+
| `-b, --base-url <url>` | `ECOPAGES_BASE_URL` | Base URL string |
|
|
42
|
+
| `-d, --debug` | `ECOPAGES_LOGGER_DEBUG` | Enables debug-level logging |
|
|
43
|
+
| `-r, --react-fast-refresh` | | Enables React Fast Refresh |
|
|
44
|
+
| `--runtime <runtime>` | | Force execution via `bun` or `node` |
|
|
41
45
|
|
|
42
|
-
|
|
43
|
-
| :------------------------- | :---------------------- | :---------------------------- |
|
|
44
|
-
| `-p, --port <port>` | `ECOPAGES_PORT` | Server port (default 3000) |
|
|
45
|
-
| `-n, --hostname <host>` | `ECOPAGES_HOSTNAME` | Server hostname |
|
|
46
|
-
| `-b, --base-url <url>` | `ECOPAGES_BASE_URL` | Base URL for the app |
|
|
47
|
-
| `-d, --debug` | `ECOPAGES_LOGGER_DEBUG` | Enable debug logging |
|
|
48
|
-
| `-r, --react-fast-refresh` | - | Enable React Fast Refresh HMR |
|
|
46
|
+
### Runtime Detection
|
|
49
47
|
|
|
50
|
-
|
|
48
|
+
The CLI prefers Bun when the package manager already indicates Bun, when the `Bun` global is available, or when you force it with `--runtime bun`. Otherwise it falls back to Node.
|
|
51
49
|
|
|
52
|
-
|
|
53
|
-
# Start dev server on port 8080 with debug logging
|
|
54
|
-
ecopages dev --port 8080 --debug
|
|
55
|
-
|
|
56
|
-
# Start dev server with React Fast Refresh
|
|
57
|
-
ecopages dev -r
|
|
50
|
+
You can explicitly force the engine using the `--runtime` flag:
|
|
58
51
|
|
|
59
|
-
|
|
60
|
-
ecopages
|
|
52
|
+
```bash
|
|
53
|
+
ecopages build --runtime bun
|
|
61
54
|
```
|
|
62
55
|
|
|
63
|
-
|
|
56
|
+
### Example Usage
|
|
64
57
|
|
|
65
|
-
|
|
58
|
+
```bash
|
|
59
|
+
# Debug dev server on custom port
|
|
60
|
+
ecopages dev --port 8080 --debug
|
|
66
61
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
import { kitajsPlugin } from '@ecopages/kitajs';
|
|
62
|
+
# Dev server with React Fast Refresh enabled
|
|
63
|
+
ecopages dev -r
|
|
70
64
|
```
|
|
71
65
|
|
|
72
|
-
|
|
66
|
+
## Ecosystem & Plugins
|
|
73
67
|
|
|
74
|
-
|
|
75
|
-
| :------------------------- | :-------------------------------------------------------- | :--------------------------------------------- |
|
|
76
|
-
| `@ecopages/browser-router` | Client-side navigation and view transitions for Ecopages. | [JSR](https://jsr.io/@ecopages/browser-router) |
|
|
68
|
+
Ecopages relies on a modular architecture. Core logic and framework integrations are published separately to [JSR](https://jsr.io/@ecopages).
|
|
77
69
|
|
|
78
|
-
|
|
79
|
-
| `@ecopages/file-system` | Runtime-agnostic file system utilities (Bun/Node.js). | [JSR](https://jsr.io/@ecopages/file-system) |
|
|
80
|
-
| `@ecopages/image-processor` | Image processing library for optimized responsive images. | [JSR](https://jsr.io/@ecopages/image-processor) |
|
|
81
|
-
| `@ecopages/kitajs` | KitaJS plugin for Ecopages integration. | [JSR](https://jsr.io/@ecopages/kitajs) |
|
|
82
|
-
| `@ecopages/lit` | Lit plugin for Ecopages integration. | [JSR](https://jsr.io/@ecopages/lit) |
|
|
83
|
-
| `@ecopages/mdx` | MDX plugin for Ecopages integration. | [JSR](https://jsr.io/@ecopages/mdx) |
|
|
84
|
-
| `@ecopages/postcss-processor` | Utility functions for processing CSS with PostCSS. | [JSR](https://jsr.io/@ecopages/postcss-processor) |
|
|
85
|
-
| `@ecopages/react` | React plugin for Ecopages integration. | [JSR](https://jsr.io/@ecopages/react) |
|
|
86
|
-
| `@ecopages/react-router` | Client-side SPA router for Ecopages React apps. | [JSR](https://jsr.io/@ecopages/react-router) |
|
|
87
|
-
|
|
88
|
-
Explore all packages at [jsr.io/@ecopages](https://jsr.io/@ecopages).
|
|
89
|
-
|
|
90
|
-
## Installation
|
|
91
|
-
|
|
92
|
-
```bash
|
|
93
|
-
bun add ecopages
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
To use Ecopages packages in your project, create a `.npmrc` file in the root of your project to configure JSR registry resolution:
|
|
70
|
+
Configure your project to use JSR by adding a `.npmrc` file:
|
|
97
71
|
|
|
98
72
|
```ini
|
|
99
73
|
@jsr:registry=https://npm.jsr.io
|
|
100
74
|
```
|
|
101
75
|
|
|
102
|
-
|
|
76
|
+
### Official Packages
|
|
77
|
+
|
|
78
|
+
| Package | Description |
|
|
79
|
+
| :---------------------------- | :----------------------------------------- |
|
|
80
|
+
| `@ecopages/browser-router` | Client-side navigation & view transitions. |
|
|
81
|
+
| `@ecopages/codemod` | AST migrations for codebase upgrades. |
|
|
82
|
+
| `@ecopages/core` | The foundational SSG engine. |
|
|
83
|
+
| `@ecopages/file-system` | Runtime-agnostic file system utilities. |
|
|
84
|
+
| `@ecopages/image-processor` | Asset pipeline for responsive images. |
|
|
85
|
+
| `@ecopages/kitajs` | Integration for KitaJS. |
|
|
86
|
+
| `@ecopages/lit` | Integration for Lit SSR/Islands. |
|
|
87
|
+
| `@ecopages/mdx` | Integration for standalone MDX routes. |
|
|
88
|
+
| `@ecopages/postcss-processor` | CSS processing pipeline using PostCSS. |
|
|
89
|
+
| `@ecopages/react` | Integration for React 19 SSR/Islands. |
|
|
90
|
+
| `@ecopages/react-router` | SPA routing for React. |
|
|
103
91
|
|
|
104
|
-
|
|
105
|
-
bun jsr add @ecopages/core @ecopages/kitajs
|
|
106
|
-
```
|
|
92
|
+
Explore all packages at [jsr.io/@ecopages](https://jsr.io/@ecopages).
|
|
107
93
|
|
|
108
94
|
## License
|
|
109
95
|
|
package/bin/cli.js
CHANGED
|
@@ -1,169 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { defineCommand, runMain } from 'citty';
|
|
4
|
+
import { downloadTemplate } from 'giget';
|
|
4
5
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
6
|
import { spawn } from 'node:child_process';
|
|
6
7
|
import { join } from 'node:path';
|
|
7
|
-
import tiged from 'tiged';
|
|
8
8
|
import { Logger } from '@ecopages/logger';
|
|
9
|
+
import { createLaunchPlan, launchPlanRequiresExistingEntryFile } from './launch-plan.js';
|
|
9
10
|
|
|
10
|
-
const logger = new Logger('[ecopages:cli]');
|
|
11
|
+
const logger = new Logger('[ecopages:cli]', { debug: process.env.ECOPAGES_LOGGER_DEBUG === 'true' });
|
|
11
12
|
|
|
12
|
-
const program = new Command();
|
|
13
13
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
|
|
14
14
|
|
|
15
|
-
program.name('ecopages').description('Ecopages CLI utilities').version(pkg.version);
|
|
16
|
-
|
|
17
|
-
async function handleInit(dir, opts) {
|
|
18
|
-
const { template, repo } = opts;
|
|
19
|
-
const targetDir = dir;
|
|
20
|
-
|
|
21
|
-
if (existsSync(targetDir)) {
|
|
22
|
-
logger.error(`Target directory already exists: ${targetDir}`);
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
logger.info(`Creating target directory '${targetDir}'...`);
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const emitter = tiged(`${repo}/examples/${template}`, {
|
|
30
|
-
disableCache: true,
|
|
31
|
-
force: true,
|
|
32
|
-
verbose: false,
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
await emitter.clone(targetDir);
|
|
36
|
-
|
|
37
|
-
const pkgPath = join(targetDir, 'package.json');
|
|
38
|
-
if (existsSync(pkgPath)) {
|
|
39
|
-
const projectPkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
40
|
-
projectPkg.name = dir;
|
|
41
|
-
writeFileSync(pkgPath, JSON.stringify(projectPkg, null, 2) + '\n');
|
|
42
|
-
logger.info(`Renamed project to '${dir}'`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
logger.info('Project initialized! Run `bun install && bun dev` to start.');
|
|
46
|
-
} catch (err) {
|
|
47
|
-
logger.error(`Failed to fetch template: ${err.message}`);
|
|
48
|
-
process.exit(1);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
program
|
|
53
|
-
.command('init <dir>')
|
|
54
|
-
.description('Initialize a new project from a template')
|
|
55
|
-
.option('--template <name>', 'Template name from ecopages/examples/', 'starter-jsx')
|
|
56
|
-
.option('--repo <repo>', 'GitHub repo (user/repo)', 'ecopages/ecopages')
|
|
57
|
-
.action(handleInit);
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Build environment variables from CLI options
|
|
61
|
-
*/
|
|
62
|
-
function buildEnvOverrides(options) {
|
|
63
|
-
const env = {};
|
|
64
|
-
if (options.port) env.ECOPAGES_PORT = String(options.port);
|
|
65
|
-
if (options.hostname) env.ECOPAGES_HOSTNAME = options.hostname;
|
|
66
|
-
if (options.baseUrl) env.ECOPAGES_BASE_URL = options.baseUrl;
|
|
67
|
-
if (options.debug) env.ECOPAGES_LOGGER_DEBUG = 'true';
|
|
68
|
-
if (options.nodeEnv) env.NODE_ENV = options.nodeEnv;
|
|
69
|
-
return env;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function detectRuntime(options = {}) {
|
|
73
|
-
if (options.runtime === 'bun' || options.runtime === 'node') {
|
|
74
|
-
return options.runtime;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const userAgent = process.env.npm_config_user_agent || '';
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* If explicitly launched via bun (e.g., `bun run ...`)
|
|
81
|
-
*/
|
|
82
|
-
if (userAgent.startsWith('bun/')) {
|
|
83
|
-
return 'bun';
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Catch when CLI is run directly by Bun without a package manager command
|
|
88
|
-
*/
|
|
89
|
-
if (typeof Bun !== 'undefined') {
|
|
90
|
-
return 'bun';
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Default to node for npm, pnpm, yarn, or direct execution
|
|
95
|
-
*/
|
|
96
|
-
return 'node';
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function buildBunArgs(args, options, entryFile, hasConfig) {
|
|
100
|
-
const bunArgs = [];
|
|
101
|
-
|
|
102
|
-
if (options.watch) bunArgs.push('--watch');
|
|
103
|
-
if (options.hot) bunArgs.push('--hot');
|
|
104
|
-
|
|
105
|
-
bunArgs.push('run');
|
|
106
|
-
|
|
107
|
-
if (hasConfig) {
|
|
108
|
-
bunArgs.push('--preload', 'eco.config.ts');
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
bunArgs.push(entryFile, ...args);
|
|
112
|
-
|
|
113
|
-
if (options.reactFastRefresh) {
|
|
114
|
-
bunArgs.push('--react-fast-refresh');
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return bunArgs;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function buildNodeArgs(args, options, entryFile) {
|
|
121
|
-
const tsxArgs = [];
|
|
122
|
-
|
|
123
|
-
if (options.watch) tsxArgs.push('watch');
|
|
124
|
-
|
|
125
|
-
tsxArgs.push(entryFile, ...args);
|
|
126
|
-
|
|
127
|
-
if (options.reactFastRefresh) {
|
|
128
|
-
tsxArgs.push('--react-fast-refresh');
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return tsxArgs;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function createLaunchPlan(args, options = {}, entryFile = 'app.ts') {
|
|
135
|
-
const hasConfig = existsSync('eco.config.ts');
|
|
136
|
-
const envOverrides = buildEnvOverrides(options);
|
|
137
|
-
const runtime = detectRuntime(options);
|
|
138
|
-
|
|
139
|
-
if (runtime === 'node') {
|
|
140
|
-
return {
|
|
141
|
-
runtime,
|
|
142
|
-
executionStrategy: 'tsx',
|
|
143
|
-
command: 'tsx',
|
|
144
|
-
commandArgs: buildNodeArgs(args, options, entryFile),
|
|
145
|
-
envOverrides,
|
|
146
|
-
env: { ...process.env, ...envOverrides },
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return {
|
|
151
|
-
runtime,
|
|
152
|
-
executionStrategy: 'direct-runtime',
|
|
153
|
-
command: 'bun',
|
|
154
|
-
commandArgs: buildBunArgs(args, options, entryFile, hasConfig),
|
|
155
|
-
envOverrides,
|
|
156
|
-
env: { ...process.env, ...envOverrides },
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
15
|
function runLaunchPlan(launchPlan) {
|
|
161
16
|
if (Object.keys(launchPlan.envOverrides).length > 0) {
|
|
162
|
-
logger.
|
|
17
|
+
logger.debug(`Environment overrides: ${JSON.stringify(launchPlan.envOverrides)}`);
|
|
163
18
|
}
|
|
164
19
|
|
|
165
|
-
logger.
|
|
166
|
-
logger.
|
|
20
|
+
logger.debug(`Runtime: ${launchPlan.runtime}`);
|
|
21
|
+
logger.debug(`Running: ${launchPlan.command} ${launchPlan.commandArgs.join(' ')}`);
|
|
167
22
|
|
|
168
23
|
const child = spawn(launchPlan.command, launchPlan.commandArgs, {
|
|
169
24
|
stdio: 'inherit',
|
|
@@ -173,9 +28,9 @@ function runLaunchPlan(launchPlan) {
|
|
|
173
28
|
child.on('error', (error) => {
|
|
174
29
|
if (error && error.code === 'ENOENT') {
|
|
175
30
|
const hint =
|
|
176
|
-
launchPlan.
|
|
31
|
+
launchPlan.runtime === 'bun'
|
|
177
32
|
? 'Install Bun from https://bun.sh to continue.'
|
|
178
|
-
: '
|
|
33
|
+
: 'Reinstall ecopages and its dependencies so the packaged tsx runtime is available for Node.js launches.';
|
|
179
34
|
logger.error(`Command not found: ${launchPlan.command}. ${hint}`);
|
|
180
35
|
process.exit(1);
|
|
181
36
|
}
|
|
@@ -190,79 +45,216 @@ function runLaunchPlan(launchPlan) {
|
|
|
190
45
|
}
|
|
191
46
|
|
|
192
47
|
/**
|
|
193
|
-
*
|
|
48
|
+
* Launch the entry file via the detected or forced runtime (bun or node).
|
|
194
49
|
* Automatically detects eco.config.ts and applies preloads.
|
|
195
50
|
* @param {string[]} args - Arguments to pass to the entry file
|
|
196
51
|
* @param {object} options - CLI options (watch, hot, port, hostname, etc.)
|
|
197
52
|
* @param {string} entryFile - Entry file to run
|
|
198
53
|
*/
|
|
199
|
-
function
|
|
200
|
-
|
|
54
|
+
async function runEntryCommand(args, options = {}, entryFile = 'app.ts') {
|
|
55
|
+
let launchPlan;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
launchPlan = await createLaunchPlan(args, options, entryFile);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
+
logger.error(message);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (launchPlanRequiresExistingEntryFile(launchPlan) && !existsSync(entryFile)) {
|
|
201
66
|
logger.error(`Error: Entry file "${entryFile}" not found in the current directory.`);
|
|
202
67
|
process.exit(1);
|
|
203
68
|
}
|
|
204
69
|
|
|
205
|
-
const launchPlan = createLaunchPlan(args, options, entryFile);
|
|
206
70
|
runLaunchPlan(launchPlan);
|
|
207
71
|
}
|
|
208
72
|
|
|
209
|
-
/**
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
73
|
+
/** Shared server argument definitions for citty commands. */
|
|
74
|
+
const serverArgs = {
|
|
75
|
+
entry: {
|
|
76
|
+
type: 'positional',
|
|
77
|
+
description: 'Entry file',
|
|
78
|
+
default: 'app.ts',
|
|
79
|
+
},
|
|
80
|
+
port: {
|
|
81
|
+
type: 'string',
|
|
82
|
+
alias: ['p'],
|
|
83
|
+
description: 'Override ECOPAGES_PORT',
|
|
84
|
+
},
|
|
85
|
+
hostname: {
|
|
86
|
+
type: 'string',
|
|
87
|
+
alias: ['n'],
|
|
88
|
+
description: 'Override ECOPAGES_HOSTNAME',
|
|
89
|
+
},
|
|
90
|
+
'base-url': {
|
|
91
|
+
type: 'string',
|
|
92
|
+
alias: ['b'],
|
|
93
|
+
description: 'Override ECOPAGES_BASE_URL',
|
|
94
|
+
},
|
|
95
|
+
debug: {
|
|
96
|
+
type: 'boolean',
|
|
97
|
+
alias: ['d'],
|
|
98
|
+
description: 'Enable debug logging (ECOPAGES_LOGGER_DEBUG=true)',
|
|
99
|
+
},
|
|
100
|
+
'react-fast-refresh': {
|
|
101
|
+
type: 'boolean',
|
|
102
|
+
alias: ['r'],
|
|
103
|
+
description: 'Enable React Fast Refresh for HMR',
|
|
104
|
+
},
|
|
105
|
+
runtime: {
|
|
106
|
+
type: 'string',
|
|
107
|
+
description: 'Force a specific runtime (bun or node)',
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const initCommand = defineCommand({
|
|
112
|
+
meta: {
|
|
113
|
+
name: 'init',
|
|
114
|
+
description: 'Initialize a new project from a template',
|
|
115
|
+
},
|
|
116
|
+
args: {
|
|
117
|
+
dir: {
|
|
118
|
+
type: 'positional',
|
|
119
|
+
description: 'Target directory name',
|
|
120
|
+
required: true,
|
|
121
|
+
},
|
|
122
|
+
template: {
|
|
123
|
+
type: 'string',
|
|
124
|
+
description: 'Template name from ecopages/examples/',
|
|
125
|
+
default: 'starter-jsx',
|
|
126
|
+
},
|
|
127
|
+
repo: {
|
|
128
|
+
type: 'string',
|
|
129
|
+
description: 'GitHub repo (user/repo)',
|
|
130
|
+
default: 'ecopages/ecopages',
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
async run({ args }) {
|
|
134
|
+
const { dir, template, repo } = args;
|
|
135
|
+
|
|
136
|
+
if (existsSync(dir)) {
|
|
137
|
+
logger.error(`Target directory already exists: ${dir}`);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
222
140
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
141
|
+
logger.info(`Creating target directory '${dir}'...`);
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
await downloadTemplate(`github:${repo}/examples/${template}`, {
|
|
145
|
+
dir,
|
|
146
|
+
force: true,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const pkgPath = join(dir, 'package.json');
|
|
150
|
+
if (existsSync(pkgPath)) {
|
|
151
|
+
const projectPkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
152
|
+
projectPkg.name = dir;
|
|
153
|
+
writeFileSync(pkgPath, JSON.stringify(projectPkg, null, 2) + '\n');
|
|
154
|
+
logger.info(`Renamed project to '${dir}'`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
logger.info('Project initialized! Run `bun install && bun dev` to start.');
|
|
158
|
+
} catch (err) {
|
|
159
|
+
logger.error(`Failed to fetch template: ${err.message}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
227
163
|
});
|
|
228
164
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
165
|
+
const devCommand = defineCommand({
|
|
166
|
+
meta: {
|
|
167
|
+
name: 'dev',
|
|
168
|
+
description: 'Start the development server',
|
|
169
|
+
},
|
|
170
|
+
args: serverArgs,
|
|
171
|
+
async run({ args }) {
|
|
172
|
+
await runEntryCommand(['--dev'], { ...args, nodeEnv: 'development' }, args.entry);
|
|
173
|
+
},
|
|
236
174
|
});
|
|
237
175
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
176
|
+
const devWatchCommand = defineCommand({
|
|
177
|
+
meta: {
|
|
178
|
+
name: 'dev:watch',
|
|
179
|
+
description: 'Start the development server with watch mode (restarts on file changes)',
|
|
180
|
+
},
|
|
181
|
+
args: serverArgs,
|
|
182
|
+
async run({ args }) {
|
|
183
|
+
await runEntryCommand(['--dev'], { ...args, watch: true, nodeEnv: 'development' }, args.entry);
|
|
184
|
+
},
|
|
245
185
|
});
|
|
246
186
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
187
|
+
const devHotCommand = defineCommand({
|
|
188
|
+
meta: {
|
|
189
|
+
name: 'dev:hot',
|
|
190
|
+
description: 'Start the development server with hot reload (HMR without restart)',
|
|
191
|
+
},
|
|
192
|
+
args: serverArgs,
|
|
193
|
+
async run({ args }) {
|
|
194
|
+
await runEntryCommand(['--dev'], { ...args, hot: true, nodeEnv: 'development' }, args.entry);
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const buildCommand = defineCommand({
|
|
199
|
+
meta: {
|
|
200
|
+
name: 'build',
|
|
201
|
+
description: 'Build the project for production',
|
|
202
|
+
},
|
|
203
|
+
args: {
|
|
204
|
+
entry: {
|
|
205
|
+
type: 'positional',
|
|
206
|
+
description: 'Entry file',
|
|
207
|
+
default: 'app.ts',
|
|
208
|
+
},
|
|
209
|
+
runtime: {
|
|
210
|
+
type: 'string',
|
|
211
|
+
description: 'Force a specific runtime (bun or node)',
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
async run({ args }) {
|
|
215
|
+
await runEntryCommand(['--build'], { nodeEnv: 'production', ...args }, args.entry);
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const startCommand = defineCommand({
|
|
220
|
+
meta: {
|
|
221
|
+
name: 'start',
|
|
222
|
+
description: 'Start the production server',
|
|
223
|
+
},
|
|
224
|
+
args: serverArgs,
|
|
225
|
+
async run({ args }) {
|
|
226
|
+
await runEntryCommand([], { ...args, nodeEnv: 'production' }, args.entry);
|
|
227
|
+
},
|
|
228
|
+
});
|
|
255
229
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
230
|
+
const previewCommand = defineCommand({
|
|
231
|
+
meta: {
|
|
232
|
+
name: 'preview',
|
|
233
|
+
description: 'Preview the production build',
|
|
234
|
+
},
|
|
235
|
+
args: serverArgs,
|
|
236
|
+
async run({ args }) {
|
|
237
|
+
await runEntryCommand(['--preview'], { ...args, nodeEnv: 'production' }, args.entry);
|
|
238
|
+
},
|
|
260
239
|
});
|
|
261
240
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
241
|
+
export const mainCommand = defineCommand({
|
|
242
|
+
meta: {
|
|
243
|
+
name: 'ecopages',
|
|
244
|
+
version: pkg.version,
|
|
245
|
+
description: 'Ecopages CLI utilities',
|
|
246
|
+
},
|
|
247
|
+
subCommands: {
|
|
248
|
+
init: initCommand,
|
|
249
|
+
dev: devCommand,
|
|
250
|
+
'dev:watch': devWatchCommand,
|
|
251
|
+
'dev:hot': devHotCommand,
|
|
252
|
+
build: buildCommand,
|
|
253
|
+
start: startCommand,
|
|
254
|
+
preview: previewCommand,
|
|
255
|
+
},
|
|
266
256
|
});
|
|
267
257
|
|
|
268
|
-
|
|
258
|
+
if (!process.env.VITEST) {
|
|
259
|
+
runMain(mainCommand);
|
|
260
|
+
}
|
package/bin/cli.test.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { runCommand } from 'citty';
|
|
3
|
+
import { mainCommand } from './cli.js';
|
|
4
|
+
import * as giget from 'giget';
|
|
5
|
+
import * as fs from 'node:fs';
|
|
6
|
+
import * as launchPlan from './launch-plan.js';
|
|
7
|
+
|
|
8
|
+
vi.mock('giget', () => ({
|
|
9
|
+
downloadTemplate: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock('node:fs', async (importOriginal) => {
|
|
13
|
+
const actual = await importOriginal<typeof import('node:fs')>();
|
|
14
|
+
return {
|
|
15
|
+
...actual,
|
|
16
|
+
existsSync: vi.fn((path) => actual.existsSync(path)),
|
|
17
|
+
writeFileSync: vi.fn(),
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
vi.mock('./launch-plan.js', () => ({
|
|
22
|
+
createLaunchPlan: vi.fn(),
|
|
23
|
+
launchPlanRequiresExistingEntryFile: vi.fn(),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock('node:child_process', () => ({
|
|
27
|
+
spawn: vi.fn().mockImplementation(() => ({
|
|
28
|
+
on: vi.fn(),
|
|
29
|
+
})),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock('@ecopages/logger', () => ({
|
|
33
|
+
Logger: class {
|
|
34
|
+
info = vi.fn();
|
|
35
|
+
warn = vi.fn();
|
|
36
|
+
error = vi.fn();
|
|
37
|
+
debug = vi.fn();
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
describe('CLI Commands', () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
vi.clearAllMocks();
|
|
44
|
+
|
|
45
|
+
// Default mocks
|
|
46
|
+
vi.mocked(fs.existsSync).mockReturnValue(false); // pretend no existing dir
|
|
47
|
+
vi.mocked(launchPlan.createLaunchPlan).mockResolvedValue({
|
|
48
|
+
runtime: 'node',
|
|
49
|
+
command: 'node',
|
|
50
|
+
commandArgs: [],
|
|
51
|
+
envOverrides: {},
|
|
52
|
+
env: {},
|
|
53
|
+
} as any);
|
|
54
|
+
vi.mocked(launchPlan.launchPlanRequiresExistingEntryFile).mockReturnValue(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('runs init command with default template and repo', async () => {
|
|
58
|
+
await runCommand(mainCommand, { rawArgs: ['init', 'my-new-project'] });
|
|
59
|
+
expect(giget.downloadTemplate).toHaveBeenCalledWith('github:ecopages/ecopages/examples/starter-jsx', {
|
|
60
|
+
dir: 'my-new-project',
|
|
61
|
+
force: true,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('runs init command with custom template and repo', async () => {
|
|
66
|
+
await runCommand(mainCommand, {
|
|
67
|
+
rawArgs: ['init', 'my-dir', '--template', 'starter-lit', '--repo', 'custom/repo'],
|
|
68
|
+
});
|
|
69
|
+
expect(giget.downloadTemplate).toHaveBeenCalledWith('github:custom/repo/examples/starter-lit', {
|
|
70
|
+
dir: 'my-dir',
|
|
71
|
+
force: true,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('runs dev command and passes defaults to launch plan', async () => {
|
|
76
|
+
await runCommand(mainCommand, { rawArgs: ['dev'] });
|
|
77
|
+
expect(launchPlan.createLaunchPlan).toHaveBeenCalledWith(
|
|
78
|
+
['--dev'],
|
|
79
|
+
expect.objectContaining({ nodeEnv: 'development' }),
|
|
80
|
+
'app.ts',
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('runs dev:hot command', async () => {
|
|
85
|
+
await runCommand(mainCommand, { rawArgs: ['dev:hot'] });
|
|
86
|
+
expect(launchPlan.createLaunchPlan).toHaveBeenCalledWith(
|
|
87
|
+
['--dev'],
|
|
88
|
+
expect.objectContaining({ hot: true, nodeEnv: 'development' }),
|
|
89
|
+
'app.ts',
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('runs dev:watch command', async () => {
|
|
94
|
+
await runCommand(mainCommand, { rawArgs: ['dev:watch'] });
|
|
95
|
+
expect(launchPlan.createLaunchPlan).toHaveBeenCalledWith(
|
|
96
|
+
['--dev'],
|
|
97
|
+
expect.objectContaining({ watch: true, nodeEnv: 'development' }),
|
|
98
|
+
'app.ts',
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('runs build command with custom entry file', async () => {
|
|
103
|
+
await runCommand(mainCommand, { rawArgs: ['build', 'server.ts'] });
|
|
104
|
+
expect(launchPlan.createLaunchPlan).toHaveBeenCalledWith(
|
|
105
|
+
['--build'],
|
|
106
|
+
expect.objectContaining({ nodeEnv: 'production' }),
|
|
107
|
+
'server.ts',
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('passes shared server options like port and hostname correctly', async () => {
|
|
112
|
+
await runCommand(mainCommand, { rawArgs: ['start', '-p', '4000', '--hostname', '0.0.0.0'] });
|
|
113
|
+
expect(launchPlan.createLaunchPlan).toHaveBeenCalledWith(
|
|
114
|
+
[],
|
|
115
|
+
expect.objectContaining({
|
|
116
|
+
nodeEnv: 'production',
|
|
117
|
+
port: '4000',
|
|
118
|
+
hostname: '0.0.0.0',
|
|
119
|
+
}),
|
|
120
|
+
'app.ts',
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('allows overriding base url and debug options', async () => {
|
|
125
|
+
await runCommand(mainCommand, { rawArgs: ['preview', '--base-url', '/my-app/', '-d'] });
|
|
126
|
+
expect(launchPlan.createLaunchPlan).toHaveBeenCalledWith(
|
|
127
|
+
['--preview'],
|
|
128
|
+
expect.objectContaining({
|
|
129
|
+
nodeEnv: 'production',
|
|
130
|
+
'base-url': '/my-app/',
|
|
131
|
+
debug: true,
|
|
132
|
+
}),
|
|
133
|
+
'app.ts',
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
|
|
6
|
+
export function buildEnvOverrides(options) {
|
|
7
|
+
const env = {};
|
|
8
|
+
if (options.port) env.ECOPAGES_PORT = String(options.port);
|
|
9
|
+
if (options.hostname) env.ECOPAGES_HOSTNAME = options.hostname;
|
|
10
|
+
if (options.baseUrl) env.ECOPAGES_BASE_URL = options.baseUrl;
|
|
11
|
+
if (options.debug) env.ECOPAGES_LOGGER_DEBUG = 'true';
|
|
12
|
+
if (options.nodeEnv) env.NODE_ENV = options.nodeEnv;
|
|
13
|
+
return env;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function detectRuntime(options = {}) {
|
|
17
|
+
if (options.runtime === 'bun' || options.runtime === 'node') {
|
|
18
|
+
return options.runtime;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const userAgent = process.env.npm_config_user_agent || '';
|
|
22
|
+
|
|
23
|
+
if (userAgent.startsWith('bun/')) {
|
|
24
|
+
return 'bun';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof Bun !== 'undefined') {
|
|
28
|
+
return 'bun';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return 'node';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function buildBunArgs(args, options, entryFile, hasConfig) {
|
|
35
|
+
const bunArgs = [];
|
|
36
|
+
|
|
37
|
+
if (options.watch) bunArgs.push('--watch');
|
|
38
|
+
if (options.hot) bunArgs.push('--hot');
|
|
39
|
+
|
|
40
|
+
bunArgs.push('run');
|
|
41
|
+
|
|
42
|
+
if (hasConfig) {
|
|
43
|
+
bunArgs.push('--preload', './eco.config.ts');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
bunArgs.push(entryFile, ...args);
|
|
47
|
+
|
|
48
|
+
if (options.reactFastRefresh) {
|
|
49
|
+
bunArgs.push('--react-fast-refresh');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return bunArgs;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function buildNodeArgs(args, options, entryFile, hasConfig) {
|
|
56
|
+
const nodeArgs = [];
|
|
57
|
+
|
|
58
|
+
if (hasConfig) {
|
|
59
|
+
nodeArgs.push('--import', './eco.config.ts');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
nodeArgs.push(entryFile, ...args);
|
|
63
|
+
|
|
64
|
+
if (options.reactFastRefresh) {
|
|
65
|
+
nodeArgs.push('--react-fast-refresh');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return nodeArgs;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function resolveTsxCliPath() {
|
|
72
|
+
try {
|
|
73
|
+
return require.resolve('tsx/cli');
|
|
74
|
+
} catch {
|
|
75
|
+
throw new Error(
|
|
76
|
+
'Unable to resolve the packaged tsx runtime required for Node.js launches. Reinstall ecopages and its dependencies, or use the Bun runtime instead.',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function createLaunchPlan(args, options = {}, entryFile = 'app.ts') {
|
|
82
|
+
const hasConfig = existsSync('eco.config.ts');
|
|
83
|
+
const envOverrides = buildEnvOverrides(options);
|
|
84
|
+
const runtime = detectRuntime(options);
|
|
85
|
+
const env = { ...process.env, ...envOverrides };
|
|
86
|
+
|
|
87
|
+
if (runtime === 'node') {
|
|
88
|
+
const tsxCliPath = resolveTsxCliPath();
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
runtime,
|
|
92
|
+
executionStrategy: 'direct-runtime',
|
|
93
|
+
command: process.execPath,
|
|
94
|
+
commandArgs: [tsxCliPath, ...buildNodeArgs(args, options, entryFile, hasConfig)],
|
|
95
|
+
envOverrides,
|
|
96
|
+
env,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
runtime,
|
|
102
|
+
executionStrategy: 'direct-runtime',
|
|
103
|
+
command: 'bun',
|
|
104
|
+
commandArgs: buildBunArgs(args, options, entryFile, hasConfig),
|
|
105
|
+
envOverrides,
|
|
106
|
+
env,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function launchPlanRequiresExistingEntryFile(launchPlan) {
|
|
111
|
+
return launchPlan.executionStrategy !== 'config-only-bootstrap';
|
|
112
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
buildEnvOverrides,
|
|
8
|
+
buildBunArgs,
|
|
9
|
+
buildNodeArgs,
|
|
10
|
+
createLaunchPlan,
|
|
11
|
+
detectRuntime,
|
|
12
|
+
launchPlanRequiresExistingEntryFile,
|
|
13
|
+
resolveTsxCliPath,
|
|
14
|
+
} from './launch-plan.js';
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
|
|
18
|
+
const originalUserAgent = process.env.npm_config_user_agent;
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
if (originalUserAgent === undefined) {
|
|
22
|
+
delete process.env.npm_config_user_agent;
|
|
23
|
+
} else {
|
|
24
|
+
process.env.npm_config_user_agent = originalUserAgent;
|
|
25
|
+
}
|
|
26
|
+
process.chdir('/Users/andeeplus/github/ecopages');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('launch-plan', () => {
|
|
30
|
+
function writeExperimentalRuntimeConfig(tempDir) {
|
|
31
|
+
fs.writeFileSync(
|
|
32
|
+
path.join(tempDir, 'eco.config.ts'),
|
|
33
|
+
[
|
|
34
|
+
'const rootDir = process.cwd();',
|
|
35
|
+
'export default {',
|
|
36
|
+
'\trootDir,',
|
|
37
|
+
'\tloaders: new Map(),',
|
|
38
|
+
'\tabsolutePaths: {',
|
|
39
|
+
'\t\tconfig: `${rootDir}/eco.config.ts`,',
|
|
40
|
+
'\t\tsrcDir: `${rootDir}/src`,',
|
|
41
|
+
'\t\tdistDir: `${rootDir}/dist`,',
|
|
42
|
+
'\t\tworkDir: `${rootDir}/.eco`,',
|
|
43
|
+
'\t},',
|
|
44
|
+
'\truntime: {},',
|
|
45
|
+
'};',
|
|
46
|
+
].join('\n'),
|
|
47
|
+
'utf8',
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
it('buildEnvOverrides maps CLI options onto environment variables', () => {
|
|
52
|
+
expect(
|
|
53
|
+
buildEnvOverrides({
|
|
54
|
+
port: 4173,
|
|
55
|
+
hostname: '127.0.0.1',
|
|
56
|
+
baseUrl: 'https://example.test',
|
|
57
|
+
debug: true,
|
|
58
|
+
nodeEnv: 'production',
|
|
59
|
+
}),
|
|
60
|
+
).toEqual({
|
|
61
|
+
ECOPAGES_PORT: '4173',
|
|
62
|
+
ECOPAGES_HOSTNAME: '127.0.0.1',
|
|
63
|
+
ECOPAGES_BASE_URL: 'https://example.test',
|
|
64
|
+
ECOPAGES_LOGGER_DEBUG: 'true',
|
|
65
|
+
NODE_ENV: 'production',
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('detectRuntime returns node when Bun is not available', () => {
|
|
70
|
+
process.env.npm_config_user_agent = 'pnpm/10.0.0 npm/? node/v24.0.0 darwin arm64';
|
|
71
|
+
expect(detectRuntime()).toBe('node');
|
|
72
|
+
expect(detectRuntime({ runtime: 'bun' })).toBe('bun');
|
|
73
|
+
expect(detectRuntime({ runtime: 'node' })).toBe('node');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('buildBunArgs preloads eco.config.ts when present', () => {
|
|
77
|
+
expect(buildBunArgs(['--dev'], { hot: true }, 'app.ts', true)).toEqual([
|
|
78
|
+
'--hot',
|
|
79
|
+
'run',
|
|
80
|
+
'--preload',
|
|
81
|
+
'./eco.config.ts',
|
|
82
|
+
'app.ts',
|
|
83
|
+
'--dev',
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('buildNodeArgs imports eco.config.ts when present', () => {
|
|
88
|
+
expect(buildNodeArgs(['--dev'], {}, 'app.ts', true)).toEqual([
|
|
89
|
+
'--import',
|
|
90
|
+
'./eco.config.ts',
|
|
91
|
+
'app.ts',
|
|
92
|
+
'--dev',
|
|
93
|
+
]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('resolveTsxCliPath resolves the packaged tsx cli entry', () => {
|
|
97
|
+
expect(resolveTsxCliPath()).toBe(require.resolve('tsx/cli'));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('createLaunchPlan uses the packaged tsx cli for node runtime', async () => {
|
|
101
|
+
const tempDir = fs.mkdtempSync(path.join(tmpdir(), 'eco-cli-launch-plan-'));
|
|
102
|
+
try {
|
|
103
|
+
process.env.npm_config_user_agent = 'pnpm/10.0.0 npm/? node/v24.0.0 darwin arm64';
|
|
104
|
+
process.chdir(tempDir);
|
|
105
|
+
fs.writeFileSync(path.join(tempDir, 'app.ts'), 'await Promise.resolve();', 'utf8');
|
|
106
|
+
writeExperimentalRuntimeConfig(tempDir);
|
|
107
|
+
|
|
108
|
+
const plan = await createLaunchPlan(['--dev'], { runtime: 'node', nodeEnv: 'development' }, 'app.ts');
|
|
109
|
+
|
|
110
|
+
expect(plan).toMatchObject({
|
|
111
|
+
runtime: 'node',
|
|
112
|
+
executionStrategy: 'direct-runtime',
|
|
113
|
+
command: process.execPath,
|
|
114
|
+
});
|
|
115
|
+
expect(plan.commandArgs).toEqual([
|
|
116
|
+
require.resolve('tsx/cli'),
|
|
117
|
+
'--import',
|
|
118
|
+
'./eco.config.ts',
|
|
119
|
+
'app.ts',
|
|
120
|
+
'--dev',
|
|
121
|
+
]);
|
|
122
|
+
} finally {
|
|
123
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('createLaunchPlan uses bun direct runtime and preloads eco.config.ts', async () => {
|
|
128
|
+
const tempDir = fs.mkdtempSync(path.join(tmpdir(), 'eco-cli-launch-plan-'));
|
|
129
|
+
try {
|
|
130
|
+
process.chdir(tempDir);
|
|
131
|
+
fs.writeFileSync(path.join(tempDir, 'app.ts'), 'await Promise.resolve();', 'utf8');
|
|
132
|
+
writeExperimentalRuntimeConfig(tempDir);
|
|
133
|
+
|
|
134
|
+
const plan = await createLaunchPlan(['--preview'], { runtime: 'bun' }, 'app.ts');
|
|
135
|
+
|
|
136
|
+
expect(plan).toMatchObject({
|
|
137
|
+
runtime: 'bun',
|
|
138
|
+
executionStrategy: 'direct-runtime',
|
|
139
|
+
command: 'bun',
|
|
140
|
+
});
|
|
141
|
+
expect(plan.commandArgs).toEqual(['run', '--preload', './eco.config.ts', 'app.ts', '--preview']);
|
|
142
|
+
} finally {
|
|
143
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('launchPlanRequiresExistingEntryFile requires a concrete entry on every runtime path', async () => {
|
|
148
|
+
const tempDir = fs.mkdtempSync(path.join(tmpdir(), 'eco-cli-launch-plan-'));
|
|
149
|
+
try {
|
|
150
|
+
process.chdir(tempDir);
|
|
151
|
+
fs.writeFileSync(path.join(tempDir, 'app.ts'), 'await Promise.resolve();', 'utf8');
|
|
152
|
+
writeExperimentalRuntimeConfig(tempDir);
|
|
153
|
+
|
|
154
|
+
const bunPlan = await createLaunchPlan(['--dev'], { nodeEnv: 'development' }, 'app.ts');
|
|
155
|
+
expect(launchPlanRequiresExistingEntryFile(bunPlan)).toBe(true);
|
|
156
|
+
} finally {
|
|
157
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ecopages",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.11",
|
|
4
4
|
"description": "CLI utilities for Ecopages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "git+https://github.com/ecopages/ecopages.git",
|
|
11
|
-
"directory": "
|
|
11
|
+
"directory": "packages/ecopages"
|
|
12
12
|
},
|
|
13
13
|
"homepage": "https://github.com/ecopages/ecopages#readme",
|
|
14
14
|
"bugs": {
|
|
@@ -34,9 +34,11 @@
|
|
|
34
34
|
"README.md"
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
37
|
+
"@ecopages/core": "workspace:*",
|
|
38
|
+
"@ecopages/logger": "^0.2.2",
|
|
39
|
+
"citty": "^0.1.6",
|
|
40
|
+
"giget": "^2.0.0",
|
|
41
|
+
"tsx": "^4.21.0"
|
|
40
42
|
},
|
|
41
43
|
"peerDependencies": {
|
|
42
44
|
"bun-types": "*",
|