ecopages 0.2.0-rc.1 → 0.2.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,15 +23,15 @@ bun dev
23
23
 
24
24
  ## Commands
25
25
 
26
- | Command | Description | Equivalent (Bun) |
27
- | :-------------------- | :-------------------------------------------------- | :------------------------------ |
28
- | `ecopages init [dir]` | Scaffolds a new project (interactive without `dir`) | N/A |
29
- | `ecopages dev` | Starts the dev server | `bun run [entry] --dev` |
30
- | `ecopages dev:watch` | Dev server + hard restarts on file changes | `bun --watch run [entry] --dev` |
31
- | `ecopages dev:hot` | Dev server + HMR (no hard restarts) | `bun --hot run [entry] --dev` |
32
- | `ecopages build` | Creates a production build | `bun run [entry] --build` |
33
- | `ecopages start` | Starts the production server | `bun run [entry]` |
34
- | `ecopages preview` | Previews the production build locally | `bun run [entry] --preview` |
26
+ | Command | Description | Equivalent (Bun) |
27
+ | :-------------------- | :------------------------------------------------------ | :----------------------------------------------------------- |
28
+ | `ecopages init [dir]` | Scaffolds a new project (interactive without `dir`) | N/A |
29
+ | `ecopages dev` | Starts the dev server (supervises config/.env restarts) | `bun run [entry] --dev` |
30
+ | `ecopages dev:watch` | Dev server + hard restarts on file changes | Bun: `bun --watch run [entry] --dev`; Node: `node --watch …` |
31
+ | `ecopages dev:hot` | Dev server + HMR (no hard restarts) | `bun --hot run [entry] --dev` |
32
+ | `ecopages build` | Creates a production build | `bun run [entry] --build` |
33
+ | `ecopages start` | Starts the production server | `bun run [entry]` |
34
+ | `ecopages preview` | Previews the production build locally | `bun run [entry] --preview` |
35
35
 
36
36
  > [!NOTE]
37
37
  > The entry file defaults to `app.ts`. Override it with `--entry-file`.
@@ -80,6 +80,8 @@ ecopages dev --port 8080 --debug
80
80
  ecopages dev -r
81
81
  ```
82
82
 
83
+ During `ecopages dev`, adding, changing, or removing `eco.config.ts` (or `--config`) and supported project `.env` files restarts the dev server automatically. The CLI supervises that restart and reloads dotenv files. When the preferred port is busy, development startup uses the same fallback flow as preview: TTY sessions show a Clack confirmation, while non-interactive sessions scan automatically only when the port was not pinned with `--port` or `ECOPAGES_PORT`.
84
+
83
85
  ### Debug logging and startup trace
84
86
 
85
87
  Set these in `.env` or on the command line when diagnosing slow dev startup or first page load.
package/bin/cli.js CHANGED
@@ -5,6 +5,10 @@ import { spawn } from 'node:child_process';
5
5
  import { parseArgs } from 'node:util';
6
6
  import { Logger } from '@ecopages/logger';
7
7
  import { createLaunchPlan } from './launch-plan.js';
8
+ import {
9
+ ECOPAGES_DEV_RESTART_EXIT_CODE,
10
+ ECOPAGES_DEV_RESTART_REASON_ENV,
11
+ } from '@ecopages/core/dev/development-restart';
8
12
  import { withBrandBanner } from './brand.js';
9
13
 
10
14
  const logger = new Logger('[ecopages:cli]', { debug: process.env.ECOPAGES_LOGGER_DEBUG === 'true' });
@@ -39,6 +43,10 @@ const sharedServerOptionDefinitions = {
39
43
  type: 'string',
40
44
  short: 'e',
41
45
  },
46
+ config: {
47
+ type: 'string',
48
+ short: 'c',
49
+ },
42
50
  help: {
43
51
  type: 'boolean',
44
52
  short: 'h',
@@ -60,6 +68,7 @@ function getMainHelpText() {
60
68
  '',
61
69
  'Global options:',
62
70
  ' -e, --entry-file <file> Entry file (default: app.ts)',
71
+ ' -c, --config <file> Ecopages config file (default: eco.config.ts)',
63
72
  ' -h, --help Show help',
64
73
  ' --version Show version',
65
74
  ].join('\n');
@@ -79,6 +88,7 @@ function getServerCommandHelpText(commandName, description) {
79
88
  ' -r, --react-fast-refresh Enable React Fast Refresh for Bun HMR',
80
89
  ' --runtime <runtime> Force bun or node',
81
90
  ' -e, --entry-file <file> Entry file (default: app.ts)',
91
+ ' -c, --config <file> Ecopages config file (default: eco.config.ts)',
82
92
  ' -h, --help Show help',
83
93
  ].join('\n');
84
94
  }
@@ -97,6 +107,7 @@ function getBuildCommandHelpText() {
97
107
  ' -r, --react-fast-refresh Enable React Fast Refresh for Bun HMR',
98
108
  ' --runtime <runtime> Force bun or node',
99
109
  ' -e, --entry-file <file> Entry file (default: app.ts)',
110
+ ' -c, --config <file> Ecopages config file (default: eco.config.ts)',
100
111
  ' -h, --help Show help',
101
112
  ].join('\n');
102
113
  }
@@ -135,11 +146,12 @@ function parseServerCommandArgs(rawArgs, commandName, description, mode = 'serve
135
146
  debug: values.debug,
136
147
  reactFastRefresh: values['react-fast-refresh'],
137
148
  runtime: values.runtime,
149
+ configFile: values.config,
138
150
  },
139
151
  };
140
152
  }
141
153
 
142
- function runLaunchPlan(launchPlan) {
154
+ function runLaunchPlan(launchPlan, options = {}) {
143
155
  if (Object.keys(launchPlan.envOverrides).length > 0) {
144
156
  logger.debug(`Environment overrides: ${JSON.stringify(launchPlan.envOverrides)}`);
145
157
  }
@@ -167,6 +179,11 @@ function runLaunchPlan(launchPlan) {
167
179
  });
168
180
 
169
181
  child.on('exit', (code) => {
182
+ if (options.superviseDevRestarts && code === ECOPAGES_DEV_RESTART_EXIT_CODE) {
183
+ options.onDevRestart?.();
184
+ return;
185
+ }
186
+
170
187
  process.exit(code || 0);
171
188
  });
172
189
  }
@@ -179,23 +196,38 @@ function runLaunchPlan(launchPlan) {
179
196
  * @param {string} entryFile - Entry file to run
180
197
  */
181
198
  async function runEntryCommand(args, options = {}, entryFile = 'app.ts', launchMode = 'start') {
182
- let launchPlan;
183
199
  const requiresBuiltBundle = launchMode === 'start';
200
+ const superviseDevRestarts = launchMode === 'dev';
184
201
 
185
202
  if (!requiresBuiltBundle && !existsSync(entryFile)) {
186
203
  logger.error(`Error: Entry file "${entryFile}" not found in the current directory.`);
187
204
  process.exit(1);
188
205
  }
189
206
 
190
- try {
191
- launchPlan = await createLaunchPlan(args, options, entryFile, launchMode);
192
- } catch (error) {
193
- const message = error instanceof Error ? error.message : String(error);
194
- logger.error(message);
195
- process.exit(1);
196
- }
207
+ const launchChild = async (restartReason) => {
208
+ let launchPlan;
209
+ try {
210
+ launchPlan = await createLaunchPlan(args, options, entryFile, launchMode);
211
+ } catch (error) {
212
+ const message = error instanceof Error ? error.message : String(error);
213
+ logger.error(message);
214
+ process.exit(1);
215
+ }
216
+
217
+ if (restartReason) {
218
+ launchPlan.env[ECOPAGES_DEV_RESTART_REASON_ENV] = restartReason;
219
+ launchPlan.envOverrides[ECOPAGES_DEV_RESTART_REASON_ENV] = restartReason;
220
+ }
221
+
222
+ runLaunchPlan(launchPlan, {
223
+ superviseDevRestarts,
224
+ onDevRestart: () => {
225
+ void launchChild('configuration or environment change');
226
+ },
227
+ });
228
+ };
197
229
 
198
- runLaunchPlan(launchPlan);
230
+ await launchChild();
199
231
  }
200
232
 
201
233
  async function runServerCommand(rawArgs, definition) {
@@ -0,0 +1,56 @@
1
+ import { repositoryName, withSubpath } from './git-template-source-shared.js';
2
+
3
+ export function normalizeGithubParts(parts, source) {
4
+ if (parts.length < 2) {
5
+ return null;
6
+ }
7
+
8
+ const [owner, repositoryPart, marker, ref, ...subpath] = parts;
9
+ const repository = `${owner}/${repositoryName(repositoryPart)}`;
10
+ if (marker === 'tree' || marker === 'blob') {
11
+ if (!ref) throw new Error(`Template URL '${source}' is missing a Git ref.`);
12
+ return `github:${withSubpath(repository, subpath)}#${ref}`;
13
+ }
14
+ return `github:${repository}`;
15
+ }
16
+
17
+ export function normalizeGitlabParts(parts) {
18
+ const markerIndex = parts.indexOf('-');
19
+ const treeIndex = parts.findIndex((part) => part === 'tree' || part === 'blob');
20
+ if (markerIndex > 0 && treeIndex > markerIndex && parts[treeIndex + 1]) {
21
+ const repository = parts.slice(0, markerIndex).map(repositoryName).join('/');
22
+ const ref = parts[treeIndex + 1];
23
+ const subpath = parts.slice(treeIndex + 2);
24
+ return `gitlab:${withSubpath(repository, subpath)}#${ref}`;
25
+ }
26
+ if (parts.length >= 2) return `gitlab:${parts.join('/')}`;
27
+ return null;
28
+ }
29
+
30
+ export function normalizeBitbucketParts(parts) {
31
+ if (parts.length < 2) {
32
+ return null;
33
+ }
34
+
35
+ const [workspace, repositoryPart, marker, ref, ...subpath] = parts;
36
+ const repository = `${workspace}/${repositoryName(repositoryPart)}`;
37
+ if (marker === 'src' && ref) {
38
+ return `bitbucket:${withSubpath(repository, subpath)}#${ref}`;
39
+ }
40
+ return `bitbucket:${repository}`;
41
+ }
42
+
43
+ export function normalizeSourcehutParts(parts) {
44
+ if (parts.length < 2) {
45
+ return null;
46
+ }
47
+
48
+ const treeIndex = parts.findIndex((part) => part === 'tree' || part === 'blob');
49
+ if (treeIndex > 1 && parts[treeIndex + 1]) {
50
+ const repository = parts.slice(0, treeIndex).map(repositoryName).join('/');
51
+ const ref = parts[treeIndex + 1];
52
+ const subpath = parts.slice(treeIndex + 2);
53
+ return `sourcehut:${withSubpath(repository, subpath)}#${ref}`;
54
+ }
55
+ return `sourcehut:${parts.join('/')}`;
56
+ }
@@ -0,0 +1,7 @@
1
+ export function repositoryName(value) {
2
+ return value.replace(/\.git$/u, '');
3
+ }
4
+
5
+ export function withSubpath(repository, subpath) {
6
+ return subpath.length > 0 ? `${repository}/${subpath.join('/')}` : repository;
7
+ }
@@ -1,3 +1,10 @@
1
+ import {
2
+ normalizeBitbucketParts,
3
+ normalizeGithubParts,
4
+ normalizeGitlabParts,
5
+ normalizeSourcehutParts,
6
+ } from './git-template-source-providers.js';
7
+
1
8
  const GIT_PROVIDER_PREFIX = /^(github|gitlab|bitbucket|sourcehut):/u;
2
9
 
3
10
  const HOST_PROVIDER = {
@@ -7,12 +14,21 @@ const HOST_PROVIDER = {
7
14
  'git.sr.ht': 'sourcehut',
8
15
  };
9
16
 
10
- function repositoryName(value) {
11
- return value.replace(/\.git$/u, '');
12
- }
17
+ const PROVIDER_NORMALIZERS = {
18
+ github: normalizeGithubParts,
19
+ gitlab: normalizeGitlabParts,
20
+ bitbucket: normalizeBitbucketParts,
21
+ sourcehut: normalizeSourcehutParts,
22
+ };
23
+
24
+ function normalizeProviderUrl(hostProvider, parts, source) {
25
+ const normalize = PROVIDER_NORMALIZERS[hostProvider];
26
+ const normalized = normalize?.(parts, source);
27
+ if (normalized) {
28
+ return normalized;
29
+ }
13
30
 
14
- function withSubpath(repository, subpath) {
15
- return subpath.length > 0 ? `${repository}/${subpath.join('/')}` : repository;
31
+ throw new Error(`Unsupported template URL '${source}'. Include a repository and optional Git ref.`);
16
32
  }
17
33
 
18
34
  /**
@@ -42,47 +58,5 @@ export function normalizeGitSource(source) {
42
58
  .split('/')
43
59
  .filter(Boolean);
44
60
 
45
- if (hostProvider === 'github' && parts.length >= 2) {
46
- const [owner, repositoryPart, marker, ref, ...subpath] = parts;
47
- const repository = `${owner}/${repositoryName(repositoryPart)}`;
48
- if (marker === 'tree' || marker === 'blob') {
49
- if (!ref) throw new Error(`Template URL '${source}' is missing a Git ref.`);
50
- return `github:${withSubpath(repository, subpath)}#${ref}`;
51
- }
52
- return `github:${repository}`;
53
- }
54
-
55
- if (hostProvider === 'gitlab') {
56
- const markerIndex = parts.indexOf('-');
57
- const treeIndex = parts.findIndex((part) => part === 'tree' || part === 'blob');
58
- if (markerIndex > 0 && treeIndex > markerIndex && parts[treeIndex + 1]) {
59
- const repository = parts.slice(0, markerIndex).map(repositoryName).join('/');
60
- const ref = parts[treeIndex + 1];
61
- const subpath = parts.slice(treeIndex + 2);
62
- return `gitlab:${withSubpath(repository, subpath)}#${ref}`;
63
- }
64
- if (parts.length >= 2) return `gitlab:${parts.join('/')}`;
65
- }
66
-
67
- if (hostProvider === 'bitbucket' && parts.length >= 2) {
68
- const [workspace, repositoryPart, marker, ref, ...subpath] = parts;
69
- const repository = `${workspace}/${repositoryName(repositoryPart)}`;
70
- if (marker === 'src' && ref) {
71
- return `bitbucket:${withSubpath(repository, subpath)}#${ref}`;
72
- }
73
- return `bitbucket:${repository}`;
74
- }
75
-
76
- if (hostProvider === 'sourcehut' && parts.length >= 2) {
77
- const treeIndex = parts.findIndex((part) => part === 'tree' || part === 'blob');
78
- if (treeIndex > 1 && parts[treeIndex + 1]) {
79
- const repository = parts.slice(0, treeIndex).map(repositoryName).join('/');
80
- const ref = parts[treeIndex + 1];
81
- const subpath = parts.slice(treeIndex + 2);
82
- return `sourcehut:${withSubpath(repository, subpath)}#${ref}`;
83
- }
84
- return `sourcehut:${parts.join('/')}`;
85
- }
86
-
87
- throw new Error(`Unsupported template URL '${source}'. Include a repository and optional Git ref.`);
61
+ return normalizeProviderUrl(hostProvider, parts, source);
88
62
  }
package/bin/init.js CHANGED
@@ -220,52 +220,114 @@ function isCancelled(value) {
220
220
  return false;
221
221
  }
222
222
 
223
+ async function promptProjectDirectory() {
224
+ const answer = await prompts.text({
225
+ message: 'Project directory:',
226
+ placeholder: 'my-app',
227
+ validate: (value) => (value.trim().length === 0 ? 'Enter a project directory.' : undefined),
228
+ });
229
+ if (isCancelled(answer)) return null;
230
+ return answer.trim();
231
+ }
232
+
233
+ async function promptTemplateSelection() {
234
+ const answer = await prompts.select({
235
+ message: 'Select a template:',
236
+ options: [
237
+ ...officialTemplates.map((template) => ({
238
+ label: template.displayName,
239
+ value: template.id,
240
+ hint: template.description,
241
+ })),
242
+ {
243
+ label: 'Community template',
244
+ value: '__community__',
245
+ hint: 'Use a Git repository or provider source.',
246
+ },
247
+ ],
248
+ });
249
+ if (isCancelled(answer)) return null;
250
+ return answer;
251
+ }
252
+
253
+ async function promptCommunityTemplateSource() {
254
+ const sourceAnswer = await prompts.text({
255
+ message: 'Template source:',
256
+ placeholder: 'github:owner/repository#v1.0.0',
257
+ validate: (value) => (value.trim().length === 0 ? 'Enter a Git template source.' : undefined),
258
+ });
259
+ if (isCancelled(sourceAnswer)) return null;
260
+ return sourceAnswer.trim();
261
+ }
262
+
263
+ async function resolveInteractiveTemplateChoice(templateId, source) {
264
+ if (templateId || source) {
265
+ return { templateId, source };
266
+ }
267
+
268
+ const answer = await promptTemplateSelection();
269
+ if (answer === null) {
270
+ return null;
271
+ }
272
+ if (answer === '__community__') {
273
+ const communitySource = await promptCommunityTemplateSource();
274
+ if (communitySource === null) {
275
+ return null;
276
+ }
277
+ return { templateId, source: communitySource };
278
+ }
279
+ return { templateId: answer, source };
280
+ }
281
+
282
+ async function promptInstallChoice(packageManager, parsed) {
283
+ if (parsed.start) {
284
+ return true;
285
+ }
286
+ if (parsed.install !== undefined) {
287
+ return parsed.install;
288
+ }
289
+ if (!parsed.interactive) {
290
+ return undefined;
291
+ }
292
+
293
+ const answer = await prompts.confirm({
294
+ message: `Install dependencies with ${packageManager}?`,
295
+ initialValue: true,
296
+ });
297
+ if (isCancelled(answer)) return null;
298
+ return answer;
299
+ }
300
+
301
+ async function promptStartChoice(parsed, install) {
302
+ if (parsed.start !== undefined) {
303
+ return parsed.start;
304
+ }
305
+ if (!parsed.interactive || !install) {
306
+ return undefined;
307
+ }
308
+
309
+ const answer = await prompts.confirm({ message: 'Start the development server now?', initialValue: false });
310
+ if (isCancelled(answer)) return null;
311
+ return answer;
312
+ }
313
+
223
314
  async function collectInitAnswers(parsed) {
224
315
  const packageManager = detectPackageManager();
225
316
  let dir = parsed.dir;
226
317
  let templateId = parsed.template;
227
318
  let source = parsed.from;
228
319
 
229
- if (parsed.interactive) {
230
- if (!dir) {
231
- const answer = await prompts.text({
232
- message: 'Project directory:',
233
- placeholder: 'my-app',
234
- validate: (value) => (value.trim().length === 0 ? 'Enter a project directory.' : undefined),
235
- });
236
- if (isCancelled(answer)) return null;
237
- dir = answer.trim();
238
- }
320
+ if (parsed.interactive && !dir) {
321
+ const promptedDir = await promptProjectDirectory();
322
+ if (promptedDir === null) return null;
323
+ dir = promptedDir;
324
+ }
239
325
 
240
- if (!templateId && !source) {
241
- const answer = await prompts.select({
242
- message: 'Select a template:',
243
- options: [
244
- ...officialTemplates.map((template) => ({
245
- label: template.displayName,
246
- value: template.id,
247
- hint: template.description,
248
- })),
249
- {
250
- label: 'Community template',
251
- value: '__community__',
252
- hint: 'Use a Git repository or provider source.',
253
- },
254
- ],
255
- });
256
- if (isCancelled(answer)) return null;
257
- if (answer === '__community__') {
258
- const sourceAnswer = await prompts.text({
259
- message: 'Template source:',
260
- placeholder: 'github:owner/repository#v1.0.0',
261
- validate: (value) => (value.trim().length === 0 ? 'Enter a Git template source.' : undefined),
262
- });
263
- if (isCancelled(sourceAnswer)) return null;
264
- source = sourceAnswer.trim();
265
- } else {
266
- templateId = answer;
267
- }
268
- }
326
+ if (parsed.interactive) {
327
+ const templateChoice = await resolveInteractiveTemplateChoice(templateId, source);
328
+ if (templateChoice === null) return null;
329
+ templateId = templateChoice.templateId;
330
+ source = templateChoice.source;
269
331
  }
270
332
 
271
333
  if (!templateId && !source) templateId = defaultOfficialTemplate.id;
@@ -273,24 +335,13 @@ async function collectInitAnswers(parsed) {
273
335
  prompts.log.step(`Resolved template source: ${getTemplateSource(templateId, source)}`);
274
336
  }
275
337
 
276
- let install = parsed.start ? true : parsed.install;
277
- if (install === undefined && parsed.interactive) {
278
- const answer = await prompts.confirm({
279
- message: `Install dependencies with ${packageManager}?`,
280
- initialValue: true,
281
- });
282
- if (isCancelled(answer)) return null;
283
- install = answer;
284
- }
285
- install ??= false;
338
+ const installAnswer = await promptInstallChoice(packageManager, parsed);
339
+ if (installAnswer === null) return null;
340
+ const install = installAnswer ?? false;
286
341
 
287
- let start = parsed.start;
288
- if (start === undefined && parsed.interactive && install) {
289
- const answer = await prompts.confirm({ message: 'Start the development server now?', initialValue: false });
290
- if (isCancelled(answer)) return null;
291
- start = answer;
292
- }
293
- start ??= false;
342
+ const startAnswer = await promptStartChoice(parsed, install);
343
+ if (startAnswer === null) return null;
344
+ const start = startAnswer ?? false;
294
345
 
295
346
  return { dir, templateId, source, packageManager, install, start };
296
347
  }
@@ -2,6 +2,12 @@ import { existsSync, readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { parseEnv } from 'node:util';
4
4
  import { SERVER_BUNDLE_DIR, SERVER_BUNDLE_FILENAME } from '@ecopages/core/utils/resolve-entry-file';
5
+ import {
6
+ resolveEcoConfigPath,
7
+ resolveEmittedEcoConfigPath,
8
+ assertProductionConfigIdentity,
9
+ } from '@ecopages/core/config';
10
+ import { ECOPAGES_ENTRY_WATCH_ENV, getDevEnvFileNames } from '@ecopages/core/dev/development-restart';
5
11
 
6
12
  const SERVER_BUNDLE_MANIFEST_FILENAME = 'manifest.json';
7
13
 
@@ -9,13 +15,7 @@ const nodeRequirePreload = import.meta.resolve('./node-require-preload.js');
9
15
  const tsxLoader = import.meta.resolve('tsx/esm');
10
16
 
11
17
  function getEnvFilePaths(nodeEnv) {
12
- const envFiles = ['.env', '.env.local'];
13
-
14
- if (nodeEnv) {
15
- envFiles.push(`.env.${nodeEnv}`, `.env.${nodeEnv}.local`);
16
- }
17
-
18
- return envFiles.filter((envFile) => existsSync(envFile));
18
+ return getDevEnvFileNames(nodeEnv).filter((envFile) => existsSync(envFile));
19
19
  }
20
20
 
21
21
  export function buildEnvOverrides(options) {
@@ -26,9 +26,26 @@ export function buildEnvOverrides(options) {
26
26
  if (options.debug) env.ECOPAGES_LOGGER_DEBUG = 'true';
27
27
  if (options.nodeEnv) env.NODE_ENV = options.nodeEnv;
28
28
  if (options.entryFile) env.ECOPAGES_ENTRY_FILE = options.entryFile;
29
+ if (options.configFile) {
30
+ env.ECOPAGES_CONFIG_FILE = path.isAbsolute(options.configFile)
31
+ ? options.configFile
32
+ : path.resolve(process.cwd(), options.configFile);
33
+ }
34
+ if (options.watch) {
35
+ env[ECOPAGES_ENTRY_WATCH_ENV] = '1';
36
+ }
29
37
  return env;
30
38
  }
31
39
 
40
+ export function resolveEcoConfigFilePath(options = {}, cwd = process.cwd(), resolverOptions = {}) {
41
+ return resolveEcoConfigPath({
42
+ configFile: options.configFile,
43
+ cwd,
44
+ required: false,
45
+ preferEmitted: resolverOptions.preferEmitted,
46
+ });
47
+ }
48
+
32
49
  export function buildLaunchEnv(options) {
33
50
  const envOverrides = buildEnvOverrides(options);
34
51
  const envFileValues = getEnvFilePaths(options.nodeEnv).reduce((env, envFile) => {
@@ -59,7 +76,7 @@ export function detectRuntime(options = {}) {
59
76
  return 'node';
60
77
  }
61
78
 
62
- export function buildBunArgs(args, options, entryFile, hasConfig) {
79
+ export function buildBunArgs(args, options, entryFile, configFilePath) {
63
80
  const bunArgs = [];
64
81
 
65
82
  if (options.watch) bunArgs.push('--watch');
@@ -67,8 +84,8 @@ export function buildBunArgs(args, options, entryFile, hasConfig) {
67
84
 
68
85
  bunArgs.push('run');
69
86
 
70
- if (hasConfig) {
71
- bunArgs.push('--preload', `./eco.config.${'ts'}`);
87
+ if (configFilePath) {
88
+ bunArgs.push('--preload', configFilePath);
72
89
  }
73
90
 
74
91
  bunArgs.push(entryFile, ...args);
@@ -157,6 +174,10 @@ export function createLaunchPlan(args, options, entryFile, launchMode) {
157
174
  const { envOverrides, env } = buildLaunchEnv(resolvedOptions);
158
175
  const runtime = detectRuntime(resolvedOptions);
159
176
  const resolvedLaunchMode = inferLaunchMode(args, launchMode);
177
+ const isExplicitConfig = Boolean(resolvedOptions.configFile || process.env.ECOPAGES_CONFIG_FILE);
178
+ const resolvedConfigFile = resolveEcoConfigFilePath(resolvedOptions, process.cwd(), {
179
+ preferEmitted: usesProductionBundle(resolvedLaunchMode) && !isExplicitConfig,
180
+ });
160
181
 
161
182
  const distServerApp = resolveProductionServerEntry(process.cwd());
162
183
  const shouldUseBundle = usesProductionBundle(resolvedLaunchMode);
@@ -167,6 +188,12 @@ export function createLaunchPlan(args, options, entryFile, launchMode) {
167
188
  }
168
189
 
169
190
  if (useBundle) {
191
+ assertProductionConfigIdentity(process.cwd(), resolvedConfigFile, { isExplicitOverride: isExplicitConfig });
192
+ const runtimeConfigFile = resolveEmittedEcoConfigPath(process.cwd()) ?? resolvedConfigFile;
193
+ if (runtimeConfigFile) {
194
+ envOverrides.ECOPAGES_CONFIG_FILE = runtimeConfigFile;
195
+ env.ECOPAGES_CONFIG_FILE = runtimeConfigFile;
196
+ }
170
197
  if (runtime === 'node') {
171
198
  return {
172
199
  runtime,
@@ -187,10 +214,16 @@ export function createLaunchPlan(args, options, entryFile, launchMode) {
187
214
  }
188
215
 
189
216
  if (runtime === 'node') {
217
+ const commandArgs = ['--import', nodeRequirePreload, '--import', tsxLoader];
218
+ if (resolvedOptions.watch) {
219
+ commandArgs.push('--watch');
220
+ }
221
+ commandArgs.push(resolvedEntryFile, ...args);
222
+
190
223
  return {
191
224
  runtime,
192
225
  command: process.execPath,
193
- commandArgs: ['--import', nodeRequirePreload, '--import', tsxLoader, resolvedEntryFile, ...args],
226
+ commandArgs,
194
227
  envOverrides,
195
228
  env,
196
229
  };
@@ -199,7 +232,7 @@ export function createLaunchPlan(args, options, entryFile, launchMode) {
199
232
  return {
200
233
  runtime,
201
234
  command: 'bun',
202
- commandArgs: buildBunArgs(args, resolvedOptions, resolvedEntryFile, existsSync('eco.config.ts')),
235
+ commandArgs: buildBunArgs(args, resolvedOptions, resolvedEntryFile, resolvedConfigFile),
203
236
  envOverrides,
204
237
  env,
205
238
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ecopages",
3
- "version": "0.2.0-rc.1",
3
+ "version": "0.2.0-rc.10",
4
4
  "description": "CLI utilities for Ecopages",
5
5
  "type": "module",
6
6
  "engines": {
@@ -28,12 +28,15 @@
28
28
  "lit",
29
29
  "mdx"
30
30
  ],
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
31
34
  "bin": {
32
35
  "ecopages": "bin/cli.js"
33
36
  },
34
37
  "dependencies": {
35
38
  "@clack/prompts": "^1.7.0",
36
- "@ecopages/core": "0.2.0-rc.1",
39
+ "@ecopages/core": "0.2.0-rc.10",
37
40
  "@ecopages/logger": "^0.2.3",
38
41
  "giget": "^2.0.0",
39
42
  "tsx": "^4.22.3"
@@ -42,4 +45,4 @@
42
45
  "bun-types": "*",
43
46
  "typescript": "^5"
44
47
  }
45
- }
48
+ }