ecopages 0.2.0-alpha.5 → 0.2.0-alpha.50
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 +54 -73
- package/bin/cli.js +303 -258
- package/bin/launch-plan.js +84 -0
- package/bin/node-require-preload.js +3 -0
- package/package.json +8 -9
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 applies runtime-specific launch behavior for each engine.
|
|
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,76 @@ 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
|
-
|
|
73
|
-
|
|
74
|
-
| Package | Description | JSR Link |
|
|
75
|
-
| :------------------------- | :-------------------------------------------------------- | :--------------------------------------------- |
|
|
76
|
-
| `@ecopages/browser-router` | Client-side navigation and view transitions for Ecopages. | [JSR](https://jsr.io/@ecopages/browser-router) |
|
|
66
|
+
## Ecosystem & Plugins
|
|
77
67
|
|
|
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) |
|
|
68
|
+
Ecopages relies on a modular architecture. Core logic and framework integrations are published as `@ecopages/*` packages on [npm](https://www.npmjs.com/org/ecopages).
|
|
87
69
|
|
|
88
|
-
|
|
70
|
+
### Official Packages
|
|
89
71
|
|
|
90
|
-
|
|
72
|
+
| Package | Description |
|
|
73
|
+
| :---------------------------- | :----------------------------------------- |
|
|
74
|
+
| `@ecopages/browser-router` | Client-side navigation & view transitions. |
|
|
75
|
+
| `@ecopages/codemod` | AST migrations for codebase upgrades. |
|
|
76
|
+
| `@ecopages/core` | The foundational SSG engine. |
|
|
77
|
+
| `@ecopages/ecopages-jsx` | Ecopages-owned JSX routes and hydration. |
|
|
78
|
+
| `@ecopages/file-system` | Runtime-agnostic file system utilities. |
|
|
79
|
+
| `@ecopages/image-processor` | Asset pipeline for responsive images. |
|
|
80
|
+
| `@ecopages/kitajs` | Integration for KitaJS. |
|
|
81
|
+
| `@ecopages/lit` | Integration for Lit SSR/Islands. |
|
|
82
|
+
| `@ecopages/mdx` | Integration for standalone MDX routes. |
|
|
83
|
+
| `@ecopages/postcss-processor` | CSS processing pipeline using PostCSS. |
|
|
84
|
+
| `@ecopages/react` | Integration for React 19 SSR/Islands. |
|
|
85
|
+
| `@ecopages/react-router` | SPA routing for React. |
|
|
91
86
|
|
|
92
|
-
|
|
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:
|
|
97
|
-
|
|
98
|
-
```ini
|
|
99
|
-
@jsr:registry=https://npm.jsr.io
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
Then add the packages you need:
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
bun jsr add @ecopages/core @ecopages/kitajs
|
|
106
|
-
```
|
|
87
|
+
Explore all packages at [npmjs.com/org/ecopages](https://www.npmjs.com/org/ecopages).
|
|
107
88
|
|
|
108
89
|
## License
|
|
109
90
|
|
package/bin/cli.js
CHANGED
|
@@ -1,268 +1,313 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
2
|
+
import { downloadTemplate } from "giget";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
import { Logger } from "@ecopages/logger";
|
|
8
|
+
import { createLaunchPlan } from "./launch-plan.js";
|
|
9
|
+
const logger = new Logger("[ecopages:cli]", { debug: process.env.ECOPAGES_LOGGER_DEBUG === "true" });
|
|
10
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
11
|
+
const sharedServerOptionDefinitions = {
|
|
12
|
+
port: {
|
|
13
|
+
type: "string",
|
|
14
|
+
short: "p"
|
|
15
|
+
},
|
|
16
|
+
hostname: {
|
|
17
|
+
type: "string",
|
|
18
|
+
short: "n"
|
|
19
|
+
},
|
|
20
|
+
"base-url": {
|
|
21
|
+
type: "string",
|
|
22
|
+
short: "b"
|
|
23
|
+
},
|
|
24
|
+
debug: {
|
|
25
|
+
type: "boolean",
|
|
26
|
+
short: "d"
|
|
27
|
+
},
|
|
28
|
+
"react-fast-refresh": {
|
|
29
|
+
type: "boolean",
|
|
30
|
+
short: "r"
|
|
31
|
+
},
|
|
32
|
+
runtime: {
|
|
33
|
+
type: "string"
|
|
34
|
+
},
|
|
35
|
+
help: {
|
|
36
|
+
type: "boolean",
|
|
37
|
+
short: "h"
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const initOptionDefinitions = {
|
|
41
|
+
template: {
|
|
42
|
+
type: "string"
|
|
43
|
+
},
|
|
44
|
+
repo: {
|
|
45
|
+
type: "string"
|
|
46
|
+
},
|
|
47
|
+
help: {
|
|
48
|
+
type: "boolean",
|
|
49
|
+
short: "h"
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
function getMainHelpText() {
|
|
53
|
+
return [
|
|
54
|
+
`ecopages ${pkg.version}`,
|
|
55
|
+
"",
|
|
56
|
+
"Usage: ecopages <command> [options]",
|
|
57
|
+
"",
|
|
58
|
+
"Commands:",
|
|
59
|
+
" init <dir> Initialize a new project from a template",
|
|
60
|
+
" dev [entry] Start the development server",
|
|
61
|
+
" dev:watch [entry] Start the development server with watch mode",
|
|
62
|
+
" dev:hot [entry] Start the development server with hot reload",
|
|
63
|
+
" build [entry] Build the project for production",
|
|
64
|
+
" start [entry] Start the production server",
|
|
65
|
+
" preview [entry] Preview the production build",
|
|
66
|
+
"",
|
|
67
|
+
"Global options:",
|
|
68
|
+
" -h, --help Show help",
|
|
69
|
+
" --version Show version"
|
|
70
|
+
].join("\n");
|
|
50
71
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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;
|
|
72
|
+
function getServerCommandHelpText(commandName, description) {
|
|
73
|
+
return [
|
|
74
|
+
`Usage: ecopages ${commandName} [entry] [options]`,
|
|
75
|
+
"",
|
|
76
|
+
description,
|
|
77
|
+
"",
|
|
78
|
+
"Options:",
|
|
79
|
+
" -p, --port <port> Override ECOPAGES_PORT",
|
|
80
|
+
" -n, --hostname <hostname> Override ECOPAGES_HOSTNAME",
|
|
81
|
+
" -b, --base-url <baseUrl> Override ECOPAGES_BASE_URL",
|
|
82
|
+
" -d, --debug Enable debug logging",
|
|
83
|
+
" -r, --react-fast-refresh Enable React Fast Refresh for Bun HMR",
|
|
84
|
+
" --runtime <runtime> Force bun or node",
|
|
85
|
+
" -h, --help Show help"
|
|
86
|
+
].join("\n");
|
|
70
87
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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';
|
|
88
|
+
function getBuildCommandHelpText() {
|
|
89
|
+
return [
|
|
90
|
+
"Usage: ecopages build [entry] [options]",
|
|
91
|
+
"",
|
|
92
|
+
"Build the project for production.",
|
|
93
|
+
"",
|
|
94
|
+
"Options:",
|
|
95
|
+
" -p, --port <port> Override ECOPAGES_PORT",
|
|
96
|
+
" -n, --hostname <hostname> Override ECOPAGES_HOSTNAME",
|
|
97
|
+
" -b, --base-url <baseUrl> Override ECOPAGES_BASE_URL",
|
|
98
|
+
" -d, --debug Enable debug logging",
|
|
99
|
+
" -r, --react-fast-refresh Enable React Fast Refresh for Bun HMR",
|
|
100
|
+
" --runtime <runtime> Force bun or node",
|
|
101
|
+
" -h, --help Show help"
|
|
102
|
+
].join("\n");
|
|
97
103
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
bunArgs.push(entryFile, ...args);
|
|
112
|
-
|
|
113
|
-
if (options.reactFastRefresh) {
|
|
114
|
-
bunArgs.push('--react-fast-refresh');
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return bunArgs;
|
|
104
|
+
function getInitCommandHelpText() {
|
|
105
|
+
return [
|
|
106
|
+
"Usage: ecopages init <dir> [options]",
|
|
107
|
+
"",
|
|
108
|
+
"Initialize a new project from a template.",
|
|
109
|
+
"",
|
|
110
|
+
"Options:",
|
|
111
|
+
" --template <template> Template name from ecopages/examples/",
|
|
112
|
+
" --repo <repo> GitHub repo in user/repo form",
|
|
113
|
+
" -h, --help Show help"
|
|
114
|
+
].join("\n");
|
|
118
115
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (options.reactFastRefresh) {
|
|
128
|
-
tsxArgs.push('--react-fast-refresh');
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return tsxArgs;
|
|
116
|
+
function parseCommandArguments(rawArgs, options) {
|
|
117
|
+
return parseArgs({
|
|
118
|
+
args: rawArgs,
|
|
119
|
+
options,
|
|
120
|
+
allowPositionals: true,
|
|
121
|
+
strict: true
|
|
122
|
+
});
|
|
132
123
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
124
|
+
function parseServerCommandArgs(rawArgs, commandName, description, mode = "server") {
|
|
125
|
+
const { values, positionals } = parseCommandArguments(rawArgs, sharedServerOptionDefinitions);
|
|
126
|
+
if (values.help) {
|
|
127
|
+
console.log(mode === "build" ? getBuildCommandHelpText() : getServerCommandHelpText(commandName, description));
|
|
128
|
+
return { help: true };
|
|
129
|
+
}
|
|
130
|
+
if (positionals.length > 1) {
|
|
131
|
+
throw new Error(`Too many positional arguments provided for \`${commandName}\`.`);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
entry: positionals[0] ?? "app.ts",
|
|
135
|
+
options: {
|
|
136
|
+
port: values.port,
|
|
137
|
+
hostname: values.hostname,
|
|
138
|
+
baseUrl: values["base-url"],
|
|
139
|
+
debug: values.debug,
|
|
140
|
+
reactFastRefresh: values["react-fast-refresh"],
|
|
141
|
+
runtime: values.runtime
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function parseInitCommandArgs(rawArgs) {
|
|
146
|
+
const { values, positionals } = parseCommandArguments(rawArgs, initOptionDefinitions);
|
|
147
|
+
if (values.help) {
|
|
148
|
+
console.log(getInitCommandHelpText());
|
|
149
|
+
return { help: true };
|
|
150
|
+
}
|
|
151
|
+
if (positionals.length !== 1) {
|
|
152
|
+
throw new Error("The `init` command requires exactly one target directory argument.");
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
dir: positionals[0],
|
|
156
|
+
template: values.template ?? "starter-jsx",
|
|
157
|
+
repo: values.repo ?? "ecopages/ecopages"
|
|
158
|
+
};
|
|
158
159
|
}
|
|
159
|
-
|
|
160
160
|
function runLaunchPlan(launchPlan) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
161
|
+
if (Object.keys(launchPlan.envOverrides).length > 0) {
|
|
162
|
+
logger.debug(`Environment overrides: ${JSON.stringify(launchPlan.envOverrides)}`);
|
|
163
|
+
}
|
|
164
|
+
logger.debug(`Runtime: ${launchPlan.runtime}`);
|
|
165
|
+
logger.debug(`Running: ${launchPlan.command} ${launchPlan.commandArgs.join(" ")}`);
|
|
166
|
+
const child = spawn(launchPlan.command, launchPlan.commandArgs, {
|
|
167
|
+
stdio: "inherit",
|
|
168
|
+
env: launchPlan.env
|
|
169
|
+
});
|
|
170
|
+
child.on("error", (error) => {
|
|
171
|
+
if (error && error.code === "ENOENT") {
|
|
172
|
+
const hint = launchPlan.runtime === "bun" ? "Install Bun from https://bun.sh to continue." : "Reinstall Node.js or run with --runtime bun if this app requires Bun.";
|
|
173
|
+
logger.error(`Command not found: ${launchPlan.command}. ${hint}`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
logger.error(`Failed to run command: ${error.message}`);
|
|
177
|
+
process.exit(1);
|
|
178
|
+
});
|
|
179
|
+
child.on("exit", (code) => {
|
|
180
|
+
process.exit(code || 0);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
async function runEntryCommand(args, options = {}, entryFile = "app.ts") {
|
|
184
|
+
let launchPlan;
|
|
185
|
+
if (!existsSync(entryFile)) {
|
|
186
|
+
logger.error(`Error: Entry file "${entryFile}" not found in the current directory.`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
launchPlan = await createLaunchPlan(args, options, entryFile);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
193
|
+
logger.error(message);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
runLaunchPlan(launchPlan);
|
|
197
|
+
}
|
|
198
|
+
async function runInitCommand(rawArgs) {
|
|
199
|
+
const parsed = parseInitCommandArgs(rawArgs);
|
|
200
|
+
if (parsed.help) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const { dir, template, repo } = parsed;
|
|
204
|
+
if (existsSync(dir)) {
|
|
205
|
+
logger.error(`Target directory already exists: ${dir}`);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
logger.info(`Creating target directory '${dir}'...`);
|
|
209
|
+
try {
|
|
210
|
+
await downloadTemplate(`github:${repo}/examples/${template}`, {
|
|
211
|
+
dir,
|
|
212
|
+
force: true
|
|
213
|
+
});
|
|
214
|
+
const pkgPath = join(dir, "package.json");
|
|
215
|
+
if (existsSync(pkgPath)) {
|
|
216
|
+
const projectPkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
217
|
+
projectPkg.name = dir;
|
|
218
|
+
writeFileSync(pkgPath, JSON.stringify(projectPkg, null, 2) + "\n");
|
|
219
|
+
logger.info(`Renamed project to '${dir}'`);
|
|
220
|
+
}
|
|
221
|
+
logger.info("Project initialized! Run `bun install && bun dev` to start.");
|
|
222
|
+
} catch (error) {
|
|
223
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
224
|
+
logger.error(`Failed to fetch template: ${message}`);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async function runServerCommand(rawArgs, definition) {
|
|
229
|
+
const parsed = parseServerCommandArgs(rawArgs, definition.name, definition.description, definition.mode);
|
|
230
|
+
if (parsed.help) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
await runEntryCommand(definition.entryArgs, { ...parsed.options, ...definition.optionOverrides }, parsed.entry);
|
|
234
|
+
}
|
|
235
|
+
async function runCli(rawArgs = process.argv.slice(2)) {
|
|
236
|
+
const [commandName, ...commandArgs] = rawArgs;
|
|
237
|
+
if (!commandName || commandName === "--help" || commandName === "-h") {
|
|
238
|
+
console.log(getMainHelpText());
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (commandName === "--version") {
|
|
242
|
+
console.log(pkg.version);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
switch (commandName) {
|
|
247
|
+
case "init":
|
|
248
|
+
await runInitCommand(commandArgs);
|
|
249
|
+
return;
|
|
250
|
+
case "dev":
|
|
251
|
+
await runServerCommand(commandArgs, {
|
|
252
|
+
name: "dev",
|
|
253
|
+
description: "Start the development server.",
|
|
254
|
+
entryArgs: ["--dev"],
|
|
255
|
+
optionOverrides: { nodeEnv: "development" }
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
case "dev:watch":
|
|
259
|
+
await runServerCommand(commandArgs, {
|
|
260
|
+
name: "dev:watch",
|
|
261
|
+
description: "Start the development server with watch mode.",
|
|
262
|
+
entryArgs: ["--dev"],
|
|
263
|
+
optionOverrides: { watch: true, nodeEnv: "development" }
|
|
264
|
+
});
|
|
265
|
+
return;
|
|
266
|
+
case "dev:hot":
|
|
267
|
+
await runServerCommand(commandArgs, {
|
|
268
|
+
name: "dev:hot",
|
|
269
|
+
description: "Start the development server with hot reload.",
|
|
270
|
+
entryArgs: ["--dev"],
|
|
271
|
+
optionOverrides: { hot: true, nodeEnv: "development" }
|
|
272
|
+
});
|
|
273
|
+
return;
|
|
274
|
+
case "build":
|
|
275
|
+
await runServerCommand(commandArgs, {
|
|
276
|
+
name: "build",
|
|
277
|
+
description: "Build the project for production.",
|
|
278
|
+
entryArgs: ["--build"],
|
|
279
|
+
optionOverrides: { nodeEnv: "production" },
|
|
280
|
+
mode: "build"
|
|
281
|
+
});
|
|
282
|
+
return;
|
|
283
|
+
case "start":
|
|
284
|
+
await runServerCommand(commandArgs, {
|
|
285
|
+
name: "start",
|
|
286
|
+
description: "Start the production server.",
|
|
287
|
+
entryArgs: [],
|
|
288
|
+
optionOverrides: { nodeEnv: "production" }
|
|
289
|
+
});
|
|
290
|
+
return;
|
|
291
|
+
case "preview":
|
|
292
|
+
await runServerCommand(commandArgs, {
|
|
293
|
+
name: "preview",
|
|
294
|
+
description: "Preview the production build.",
|
|
295
|
+
entryArgs: ["--preview"],
|
|
296
|
+
optionOverrides: { nodeEnv: "production" }
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
default:
|
|
300
|
+
throw new Error(`Unknown command \`${commandName}\`.`);
|
|
301
|
+
}
|
|
302
|
+
} catch (error) {
|
|
303
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
304
|
+
logger.error(message);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
190
307
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
* Execute a bun command with the given arguments and options.
|
|
194
|
-
* Automatically detects eco.config.ts and applies preloads.
|
|
195
|
-
* @param {string[]} args - Arguments to pass to the entry file
|
|
196
|
-
* @param {object} options - CLI options (watch, hot, port, hostname, etc.)
|
|
197
|
-
* @param {string} entryFile - Entry file to run
|
|
198
|
-
*/
|
|
199
|
-
function runBunCommand(args, options = {}, entryFile = 'app.ts') {
|
|
200
|
-
if (!existsSync(entryFile)) {
|
|
201
|
-
logger.error(`Error: Entry file "${entryFile}" not found in the current directory.`);
|
|
202
|
-
process.exit(1);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const launchPlan = createLaunchPlan(args, options, entryFile);
|
|
206
|
-
runLaunchPlan(launchPlan);
|
|
308
|
+
if (!process.env.VITEST) {
|
|
309
|
+
runCli();
|
|
207
310
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
* @param {import('commander').Command} cmd - The command to add options to
|
|
212
|
-
* @returns {import('commander').Command} The command with options added
|
|
213
|
-
*/
|
|
214
|
-
const serverOptions = (cmd) =>
|
|
215
|
-
cmd
|
|
216
|
-
.option('-p, --port <port>', 'Override ECOPAGES_PORT')
|
|
217
|
-
.option('-n, --hostname <hostname>', 'Override ECOPAGES_HOSTNAME')
|
|
218
|
-
.option('-b, --base-url <url>', 'Override ECOPAGES_BASE_URL')
|
|
219
|
-
.option('-d, --debug', 'Enable debug logging (ECOPAGES_LOGGER_DEBUG=true)')
|
|
220
|
-
.option('-r, --react-fast-refresh', 'Enable React Fast Refresh for HMR')
|
|
221
|
-
.option('--runtime <runtime>', 'Force a specific runtime (bun or node)');
|
|
222
|
-
|
|
223
|
-
serverOptions(
|
|
224
|
-
program.command('dev').description('Start the development server').argument('[entry]', 'Entry file', 'app.ts'),
|
|
225
|
-
).action((entry, opts) => {
|
|
226
|
-
runBunCommand(['--dev'], { ...opts, nodeEnv: 'development' }, entry);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
serverOptions(
|
|
230
|
-
program
|
|
231
|
-
.command('dev:watch')
|
|
232
|
-
.description('Start the development server with watch mode (restarts on file changes)')
|
|
233
|
-
.argument('[entry]', 'Entry file', 'app.ts'),
|
|
234
|
-
).action((entry, opts) => {
|
|
235
|
-
runBunCommand(['--dev'], { ...opts, watch: true, nodeEnv: 'development' }, entry);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
serverOptions(
|
|
239
|
-
program
|
|
240
|
-
.command('dev:hot')
|
|
241
|
-
.description('Start the development server with hot reload (HMR without restart)')
|
|
242
|
-
.argument('[entry]', 'Entry file', 'app.ts'),
|
|
243
|
-
).action((entry, opts) => {
|
|
244
|
-
runBunCommand(['--dev'], { ...opts, hot: true, nodeEnv: 'development' }, entry);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
program
|
|
248
|
-
.command('build')
|
|
249
|
-
.description('Build the project for production')
|
|
250
|
-
.argument('[entry]', 'Entry file', 'app.ts')
|
|
251
|
-
.option('--runtime <runtime>', 'Force a specific runtime (bun or node)')
|
|
252
|
-
.action((entry, opts) => {
|
|
253
|
-
runBunCommand(['--build'], { nodeEnv: 'production', ...opts }, entry);
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
serverOptions(
|
|
257
|
-
program.command('start').description('Start the production server').argument('[entry]', 'Entry file', 'app.ts'),
|
|
258
|
-
).action((entry, opts) => {
|
|
259
|
-
runBunCommand([], { ...opts, nodeEnv: 'production' }, entry);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
serverOptions(
|
|
263
|
-
program.command('preview').description('Preview the production build').argument('[entry]', 'Entry file', 'app.ts'),
|
|
264
|
-
).action((entry, opts) => {
|
|
265
|
-
runBunCommand(['--preview'], { ...opts, nodeEnv: 'production' }, entry);
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
program.parse();
|
|
311
|
+
export {
|
|
312
|
+
runCli
|
|
313
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { parseEnv } from "node:util";
|
|
3
|
+
const nodeRequirePreload = import.meta.resolve("./node-require-preload.js");
|
|
4
|
+
const tsxLoader = import.meta.resolve("tsx/esm");
|
|
5
|
+
function getEnvFilePaths(nodeEnv) {
|
|
6
|
+
const envFiles = [".env", ".env.local"];
|
|
7
|
+
if (nodeEnv) {
|
|
8
|
+
envFiles.push(`.env.${nodeEnv}`, `.env.${nodeEnv}.local`);
|
|
9
|
+
}
|
|
10
|
+
return envFiles.filter((envFile) => existsSync(envFile));
|
|
11
|
+
}
|
|
12
|
+
function buildEnvOverrides(options) {
|
|
13
|
+
const env = {};
|
|
14
|
+
if (options.port) env.ECOPAGES_PORT = String(options.port);
|
|
15
|
+
if (options.hostname) env.ECOPAGES_HOSTNAME = options.hostname;
|
|
16
|
+
if (options.baseUrl) env.ECOPAGES_BASE_URL = options.baseUrl;
|
|
17
|
+
if (options.debug) env.ECOPAGES_LOGGER_DEBUG = "true";
|
|
18
|
+
if (options.nodeEnv) env.NODE_ENV = options.nodeEnv;
|
|
19
|
+
return env;
|
|
20
|
+
}
|
|
21
|
+
function buildLaunchEnv(options) {
|
|
22
|
+
const envOverrides = buildEnvOverrides(options);
|
|
23
|
+
const envFileValues = getEnvFilePaths(options.nodeEnv).reduce((env, envFile) => {
|
|
24
|
+
return { ...env, ...parseEnv(readFileSync(envFile, "utf8")) };
|
|
25
|
+
}, {});
|
|
26
|
+
return {
|
|
27
|
+
envOverrides,
|
|
28
|
+
env: { ...envFileValues, ...process.env, ...envOverrides }
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function detectRuntime(options = {}) {
|
|
32
|
+
if (options.runtime === "bun" || options.runtime === "node") {
|
|
33
|
+
return options.runtime;
|
|
34
|
+
}
|
|
35
|
+
const userAgent = process.env.npm_config_user_agent || "";
|
|
36
|
+
if (userAgent.startsWith("bun/")) {
|
|
37
|
+
return "bun";
|
|
38
|
+
}
|
|
39
|
+
if (typeof Bun !== "undefined") {
|
|
40
|
+
return "bun";
|
|
41
|
+
}
|
|
42
|
+
return "node";
|
|
43
|
+
}
|
|
44
|
+
function buildBunArgs(args, options, entryFile, hasConfig) {
|
|
45
|
+
const bunArgs = [];
|
|
46
|
+
if (options.watch) bunArgs.push("--watch");
|
|
47
|
+
if (options.hot) bunArgs.push("--hot");
|
|
48
|
+
bunArgs.push("run");
|
|
49
|
+
if (hasConfig) {
|
|
50
|
+
bunArgs.push("--preload", `./eco.config.${"ts"}`);
|
|
51
|
+
}
|
|
52
|
+
bunArgs.push(entryFile, ...args);
|
|
53
|
+
if (options.reactFastRefresh) {
|
|
54
|
+
bunArgs.push("--react-fast-refresh");
|
|
55
|
+
}
|
|
56
|
+
return bunArgs;
|
|
57
|
+
}
|
|
58
|
+
function createLaunchPlan(args, options = {}, entryFile = "app.ts") {
|
|
59
|
+
const { envOverrides, env } = buildLaunchEnv(options);
|
|
60
|
+
const runtime = detectRuntime(options);
|
|
61
|
+
if (runtime === "node") {
|
|
62
|
+
return {
|
|
63
|
+
runtime,
|
|
64
|
+
command: process.execPath,
|
|
65
|
+
commandArgs: ["--import", nodeRequirePreload, "--import", tsxLoader, entryFile, ...args],
|
|
66
|
+
envOverrides,
|
|
67
|
+
env
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
runtime,
|
|
72
|
+
command: "bun",
|
|
73
|
+
commandArgs: buildBunArgs(args, options, entryFile, existsSync("eco.config.ts")),
|
|
74
|
+
envOverrides,
|
|
75
|
+
env
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export {
|
|
79
|
+
buildBunArgs,
|
|
80
|
+
buildEnvOverrides,
|
|
81
|
+
buildLaunchEnv,
|
|
82
|
+
createLaunchPlan,
|
|
83
|
+
detectRuntime
|
|
84
|
+
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ecopages",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.50",
|
|
4
4
|
"description": "CLI utilities for Ecopages",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=24.0.0"
|
|
8
|
+
},
|
|
6
9
|
"license": "MIT",
|
|
7
10
|
"author": "Ecopages Team",
|
|
8
11
|
"repository": {
|
|
@@ -28,15 +31,11 @@
|
|
|
28
31
|
"bin": {
|
|
29
32
|
"ecopages": "bin/cli.js"
|
|
30
33
|
},
|
|
31
|
-
"files": [
|
|
32
|
-
"bin/",
|
|
33
|
-
"css/",
|
|
34
|
-
"README.md"
|
|
35
|
-
],
|
|
36
34
|
"dependencies": {
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
35
|
+
"@ecopages/core": "0.2.0-alpha.50",
|
|
36
|
+
"@ecopages/logger": "^0.2.3",
|
|
37
|
+
"giget": "^2.0.0",
|
|
38
|
+
"tsx": "^4.22.3"
|
|
40
39
|
},
|
|
41
40
|
"peerDependencies": {
|
|
42
41
|
"bun-types": "*",
|