ecopages 0.2.0-alpha.39 → 0.2.0-alpha.41

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  The official CLI for the Ecopages framework.
4
4
 
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`.
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.
6
6
 
7
7
  ## Quick Start
8
8
 
package/bin/cli.js CHANGED
@@ -1,13 +1,162 @@
1
1
  #!/usr/bin/env node
2
- import { defineCommand, runMain } from "citty";
3
2
  import { downloadTemplate } from "giget";
4
3
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
5
4
  import { spawn } from "node:child_process";
6
5
  import { join } from "node:path";
6
+ import { parseArgs } from "node:util";
7
7
  import { Logger } from "@ecopages/logger";
8
- import { createLaunchPlan, launchPlanRequiresExistingEntryFile } from "./launch-plan.js";
8
+ import { createLaunchPlan } from "./launch-plan.js";
9
9
  const logger = new Logger("[ecopages:cli]", { debug: process.env.ECOPAGES_LOGGER_DEBUG === "true" });
10
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");
71
+ }
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");
87
+ }
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");
103
+ }
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");
115
+ }
116
+ function parseCommandArguments(rawArgs, options) {
117
+ return parseArgs({
118
+ args: rawArgs,
119
+ options,
120
+ allowPositionals: true,
121
+ strict: true
122
+ });
123
+ }
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
+ };
159
+ }
11
160
  function runLaunchPlan(launchPlan) {
12
161
  if (Object.keys(launchPlan.envOverrides).length > 0) {
13
162
  logger.debug(`Environment overrides: ${JSON.stringify(launchPlan.envOverrides)}`);
@@ -40,185 +189,125 @@ async function runEntryCommand(args, options = {}, entryFile = "app.ts") {
40
189
  logger.error(message);
41
190
  process.exit(1);
42
191
  }
43
- if (launchPlanRequiresExistingEntryFile(launchPlan) && !existsSync(entryFile)) {
192
+ if (!existsSync(entryFile)) {
44
193
  logger.error(`Error: Entry file "${entryFile}" not found in the current directory.`);
45
194
  process.exit(1);
46
195
  }
47
196
  runLaunchPlan(launchPlan);
48
197
  }
49
- const serverArgs = {
50
- entry: {
51
- type: "positional",
52
- description: "Entry file",
53
- default: "app.ts"
54
- },
55
- port: {
56
- type: "string",
57
- alias: ["p"],
58
- description: "Override ECOPAGES_PORT"
59
- },
60
- hostname: {
61
- type: "string",
62
- alias: ["n"],
63
- description: "Override ECOPAGES_HOSTNAME"
64
- },
65
- "base-url": {
66
- type: "string",
67
- alias: ["b"],
68
- description: "Override ECOPAGES_BASE_URL"
69
- },
70
- debug: {
71
- type: "boolean",
72
- alias: ["d"],
73
- description: "Enable debug logging (ECOPAGES_LOGGER_DEBUG=true)"
74
- },
75
- "react-fast-refresh": {
76
- type: "boolean",
77
- alias: ["r"],
78
- description: "Enable React Fast Refresh for HMR"
79
- },
80
- runtime: {
81
- type: "string",
82
- description: "Force a specific runtime (bun or node)"
198
+ async function runInitCommand(rawArgs) {
199
+ const parsed = parseInitCommandArgs(rawArgs);
200
+ if (parsed.help) {
201
+ return;
83
202
  }
84
- };
85
- const initCommand = defineCommand({
86
- meta: {
87
- name: "init",
88
- description: "Initialize a new project from a template"
89
- },
90
- args: {
91
- dir: {
92
- type: "positional",
93
- description: "Target directory name",
94
- required: true
95
- },
96
- template: {
97
- type: "string",
98
- description: "Template name from ecopages/examples/",
99
- default: "starter-jsx"
100
- },
101
- repo: {
102
- type: "string",
103
- description: "GitHub repo (user/repo)",
104
- default: "ecopages/ecopages"
105
- }
106
- },
107
- async run({ args }) {
108
- const { dir, template, repo } = args;
109
- if (existsSync(dir)) {
110
- logger.error(`Target directory already exists: ${dir}`);
111
- process.exit(1);
112
- }
113
- logger.info(`Creating target directory '${dir}'...`);
114
- try {
115
- await downloadTemplate(`github:${repo}/examples/${template}`, {
116
- dir,
117
- force: true
118
- });
119
- const pkgPath = join(dir, "package.json");
120
- if (existsSync(pkgPath)) {
121
- const projectPkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
122
- projectPkg.name = dir;
123
- writeFileSync(pkgPath, JSON.stringify(projectPkg, null, 2) + "\n");
124
- logger.info(`Renamed project to '${dir}'`);
125
- }
126
- logger.info("Project initialized! Run `bun install && bun dev` to start.");
127
- } catch (err) {
128
- logger.error(`Failed to fetch template: ${err.message}`);
129
- process.exit(1);
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}'`);
130
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);
131
226
  }
132
- });
133
- const devCommand = defineCommand({
134
- meta: {
135
- name: "dev",
136
- description: "Start the development server"
137
- },
138
- args: serverArgs,
139
- async run({ args }) {
140
- await runEntryCommand(["--dev"], { ...args, nodeEnv: "development" }, args.entry);
141
- }
142
- });
143
- const devWatchCommand = defineCommand({
144
- meta: {
145
- name: "dev:watch",
146
- description: "Start the development server with watch mode (restarts on file changes)"
147
- },
148
- args: serverArgs,
149
- async run({ args }) {
150
- await runEntryCommand(["--dev"], { ...args, watch: true, nodeEnv: "development" }, args.entry);
151
- }
152
- });
153
- const devHotCommand = defineCommand({
154
- meta: {
155
- name: "dev:hot",
156
- description: "Start the development server with hot reload (HMR without restart)"
157
- },
158
- args: serverArgs,
159
- async run({ args }) {
160
- await runEntryCommand(["--dev"], { ...args, hot: true, nodeEnv: "development" }, args.entry);
161
- }
162
- });
163
- const buildCommand = defineCommand({
164
- meta: {
165
- name: "build",
166
- description: "Build the project for production"
167
- },
168
- args: {
169
- entry: {
170
- type: "positional",
171
- description: "Entry file",
172
- default: "app.ts"
173
- },
174
- runtime: {
175
- type: "string",
176
- description: "Force a specific runtime (bun or node)"
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}\`.`);
177
301
  }
178
- },
179
- async run({ args }) {
180
- await runEntryCommand(["--build"], { nodeEnv: "production", ...args }, args.entry);
181
- }
182
- });
183
- const startCommand = defineCommand({
184
- meta: {
185
- name: "start",
186
- description: "Start the production server"
187
- },
188
- args: serverArgs,
189
- async run({ args }) {
190
- await runEntryCommand([], { ...args, nodeEnv: "production" }, args.entry);
191
- }
192
- });
193
- const previewCommand = defineCommand({
194
- meta: {
195
- name: "preview",
196
- description: "Preview the production build"
197
- },
198
- args: serverArgs,
199
- async run({ args }) {
200
- await runEntryCommand(["--preview"], { ...args, nodeEnv: "production" }, args.entry);
201
- }
202
- });
203
- const mainCommand = defineCommand({
204
- meta: {
205
- name: "ecopages",
206
- version: pkg.version,
207
- description: "Ecopages CLI utilities"
208
- },
209
- subCommands: {
210
- init: initCommand,
211
- dev: devCommand,
212
- "dev:watch": devWatchCommand,
213
- "dev:hot": devHotCommand,
214
- build: buildCommand,
215
- start: startCommand,
216
- preview: previewCommand
217
- }
218
- });
302
+ } catch (error) {
303
+ const message = error instanceof Error ? error.message : String(error);
304
+ logger.error(message);
305
+ process.exit(1);
306
+ }
307
+ }
219
308
  if (!process.env.VITEST) {
220
- runMain(mainCommand);
309
+ runCli();
221
310
  }
222
311
  export {
223
- mainCommand
312
+ runCli
224
313
  };
@@ -1,6 +1,13 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  const require2 = createRequire(import.meta.url);
4
+ function buildNodeEnvFileArgs(nodeEnv) {
5
+ const envFiles = [".env", ".env.local"];
6
+ if (nodeEnv) {
7
+ envFiles.push(`.env.${nodeEnv}`, `.env.${nodeEnv}.local`);
8
+ }
9
+ return envFiles.filter((envFile) => existsSync(envFile)).map((envFile) => `--env-file=${envFile}`);
10
+ }
4
11
  function buildEnvOverrides(options) {
5
12
  const env = {};
6
13
  if (options.port) env.ECOPAGES_PORT = String(options.port);
@@ -37,14 +44,6 @@ function buildBunArgs(args, options, entryFile, hasConfig) {
37
44
  }
38
45
  return bunArgs;
39
46
  }
40
- function buildNodeArgs(args, options, entryFile, _hasConfig) {
41
- const nodeArgs = [];
42
- nodeArgs.push(entryFile, ...args);
43
- if (options.reactFastRefresh) {
44
- nodeArgs.push("--react-fast-refresh");
45
- }
46
- return nodeArgs;
47
- }
48
47
  function resolveTsxCliPath() {
49
48
  try {
50
49
  return require2.resolve("tsx/cli");
@@ -55,39 +54,32 @@ function resolveTsxCliPath() {
55
54
  }
56
55
  }
57
56
  async function createLaunchPlan(args, options = {}, entryFile = "app.ts") {
58
- const hasConfig = existsSync("eco.config.ts");
59
57
  const envOverrides = buildEnvOverrides(options);
60
58
  const runtime = detectRuntime(options);
61
59
  const env = { ...process.env, ...envOverrides };
62
60
  if (runtime === "node") {
63
61
  const tsxCliPath = resolveTsxCliPath();
62
+ const nodeArgs = [entryFile, ...args];
64
63
  return {
65
64
  runtime,
66
- executionStrategy: "direct-runtime",
67
65
  command: process.execPath,
68
- commandArgs: [tsxCliPath, ...buildNodeArgs(args, options, entryFile, hasConfig)],
66
+ commandArgs: [...buildNodeEnvFileArgs(options.nodeEnv), tsxCliPath, ...nodeArgs],
69
67
  envOverrides,
70
68
  env
71
69
  };
72
70
  }
73
71
  return {
74
72
  runtime,
75
- executionStrategy: "direct-runtime",
76
73
  command: "bun",
77
- commandArgs: buildBunArgs(args, options, entryFile, hasConfig),
74
+ commandArgs: buildBunArgs(args, options, entryFile, existsSync("eco.config.ts")),
78
75
  envOverrides,
79
76
  env
80
77
  };
81
78
  }
82
- function launchPlanRequiresExistingEntryFile(launchPlan) {
83
- return launchPlan.executionStrategy !== "config-only-bootstrap";
84
- }
85
79
  export {
86
80
  buildBunArgs,
87
81
  buildEnvOverrides,
88
- buildNodeArgs,
89
82
  createLaunchPlan,
90
83
  detectRuntime,
91
- launchPlanRequiresExistingEntryFile,
92
84
  resolveTsxCliPath
93
85
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ecopages",
3
- "version": "0.2.0-alpha.39",
3
+ "version": "0.2.0-alpha.41",
4
4
  "description": "CLI utilities for Ecopages",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,9 +29,8 @@
29
29
  "ecopages": "bin/cli.js"
30
30
  },
31
31
  "dependencies": {
32
- "@ecopages/core": "0.2.0-alpha.39",
32
+ "@ecopages/core": "0.2.0-alpha.41",
33
33
  "@ecopages/logger": "^0.2.3",
34
- "citty": "^0.1.6",
35
34
  "giget": "^2.0.0",
36
35
  "tsx": "^4.22.0"
37
36
  },