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 CHANGED
@@ -1,16 +1,12 @@
1
1
  # ecopages
2
2
 
3
- > **DRAFT / EXPERIMENTAL**
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
- `ecopages` is a CLI tool for the Ecopages framework. It provides:
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
- ## CLI Utilities
18
+ ## Commands
23
19
 
24
- ### Commands
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
- | Command | Description | Bun Equivalent |
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
- > **Note:** `[entry]` defaults to `app.ts` if not provided.
33
+ ## Environment & Runtime Options
37
34
 
38
- ### Environment Overrides
35
+ Server and build commands accept the following options. They automatically map to the equivalent environment variables for the underlying process:
39
36
 
40
- All server commands (`dev`, `dev:watch`, `dev:hot`, `start`, `preview`) support the following options:
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
- | Option | Environment Variable | Description |
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
- **Example:**
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
- ```bash
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
- # Start production server with custom hostname
60
- ecopages start --hostname 0.0.0.0 --port 3001
52
+ ```bash
53
+ ecopages build --runtime bun
61
54
  ```
62
55
 
63
- ## Ecopages Packages
56
+ ### Example Usage
64
57
 
65
- The Ecopages ecosystem consists of individual framework packages published to [JSR](https://jsr.io/@ecopages). Import them directly in your project:
58
+ ```bash
59
+ # Debug dev server on custom port
60
+ ecopages dev --port 8080 --debug
66
61
 
67
- ```typescript
68
- import { eco } from '@ecopages/core';
69
- import { kitajsPlugin } from '@ecopages/kitajs';
62
+ # Dev server with React Fast Refresh enabled
63
+ ecopages dev -r
70
64
  ```
71
65
 
72
- ### Available Packages
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
- | `@ecopages/core` | Foundational layer of the Ecopages ecosystem. | [JSR](https://jsr.io/@ecopages/core) |
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
- Explore all packages at [jsr.io/@ecopages](https://jsr.io/@ecopages).
70
+ ### Official Packages
89
71
 
90
- ## Installation
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
- ```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:
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 { Command } from 'commander';
4
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
5
- import { spawn } from 'node:child_process';
6
- import { join } from 'node:path';
7
- import tiged from 'tiged';
8
- import { Logger } from '@ecopages/logger';
9
-
10
- const logger = new Logger('[ecopages:cli]');
11
-
12
- const program = new Command();
13
- const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
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
- }
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
- 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;
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
- 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';
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
- 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;
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
- 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;
116
+ function parseCommandArguments(rawArgs, options) {
117
+ return parseArgs({
118
+ args: rawArgs,
119
+ options,
120
+ allowPositionals: true,
121
+ strict: true
122
+ });
132
123
  }
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
- };
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
- if (Object.keys(launchPlan.envOverrides).length > 0) {
162
- logger.info(`Environment overrides: ${JSON.stringify(launchPlan.envOverrides)}`);
163
- }
164
-
165
- logger.info(`Runtime: ${launchPlan.runtime}`);
166
- logger.info(`Running: ${launchPlan.command} ${launchPlan.commandArgs.join(' ')}`);
167
-
168
- const child = spawn(launchPlan.command, launchPlan.commandArgs, {
169
- stdio: 'inherit',
170
- env: launchPlan.env,
171
- });
172
-
173
- child.on('error', (error) => {
174
- if (error && error.code === 'ENOENT') {
175
- const hint =
176
- launchPlan.command === 'bun'
177
- ? 'Install Bun from https://bun.sh to continue.'
178
- : 'Install tsx (`npm i -g tsx` or add it as a devDependency) to continue.';
179
- logger.error(`Command not found: ${launchPlan.command}. ${hint}`);
180
- process.exit(1);
181
- }
182
-
183
- logger.error(`Failed to run command: ${error.message}`);
184
- process.exit(1);
185
- });
186
-
187
- child.on('exit', (code) => {
188
- process.exit(code || 0);
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
- * Add shared server options to a command.
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
+ };
@@ -0,0 +1,3 @@
1
+ import path from "node:path";
2
+ import { createRequire } from "node:module";
3
+ globalThis.require = createRequire(path.join(process.cwd(), "package.json"));
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "ecopages",
3
- "version": "0.2.0-alpha.5",
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
- "commander": "^12.1.0",
38
- "tiged": "^2.12.7",
39
- "@ecopages/logger": "^0.2.2"
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": "*",