ecopages 0.2.0-alpha.42 → 0.2.0-alpha.44

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/bin/cli.js CHANGED
@@ -169,7 +169,7 @@ function runLaunchPlan(launchPlan) {
169
169
  });
170
170
  child.on("error", (error) => {
171
171
  if (error && error.code === "ENOENT") {
172
- const hint = launchPlan.runtime === "bun" ? "Install Bun from https://bun.sh to continue." : "Reinstall ecopages and its dependencies so the packaged tsx runtime is available for Node.js launches.";
172
+ const hint = launchPlan.runtime === "bun" ? "Install Bun from https://bun.sh to continue." : "Reinstall ecopages and its dependencies so the Node entry bridge dependencies are available.";
173
173
  logger.error(`Command not found: ${launchPlan.command}. ${hint}`);
174
174
  process.exit(1);
175
175
  }
@@ -182,6 +182,10 @@ function runLaunchPlan(launchPlan) {
182
182
  }
183
183
  async function runEntryCommand(args, options = {}, entryFile = "app.ts") {
184
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
+ }
185
189
  try {
186
190
  launchPlan = await createLaunchPlan(args, options, entryFile);
187
191
  } catch (error) {
@@ -189,10 +193,6 @@ async function runEntryCommand(args, options = {}, entryFile = "app.ts") {
189
193
  logger.error(message);
190
194
  process.exit(1);
191
195
  }
192
- if (!existsSync(entryFile)) {
193
- logger.error(`Error: Entry file "${entryFile}" not found in the current directory.`);
194
- process.exit(1);
195
- }
196
196
  runLaunchPlan(launchPlan);
197
197
  }
198
198
  async function runInitCommand(rawArgs) {
@@ -1,12 +1,12 @@
1
- import { existsSync } from "node:fs";
2
- import { createRequire } from "node:module";
3
- const require2 = createRequire(import.meta.url);
4
- function buildNodeEnvFileArgs(nodeEnv) {
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { parseEnv } from "node:util";
3
+ import { buildNodeEntryBridge } from "./node-entry-bridge.js";
4
+ function getEnvFilePaths(nodeEnv) {
5
5
  const envFiles = [".env", ".env.local"];
6
6
  if (nodeEnv) {
7
7
  envFiles.push(`.env.${nodeEnv}`, `.env.${nodeEnv}.local`);
8
8
  }
9
- return envFiles.filter((envFile) => existsSync(envFile)).map((envFile) => `--env-file=${envFile}`);
9
+ return envFiles.filter((envFile) => existsSync(envFile));
10
10
  }
11
11
  function buildEnvOverrides(options) {
12
12
  const env = {};
@@ -17,6 +17,16 @@ function buildEnvOverrides(options) {
17
17
  if (options.nodeEnv) env.NODE_ENV = options.nodeEnv;
18
18
  return env;
19
19
  }
20
+ function buildLaunchEnv(options) {
21
+ const envOverrides = buildEnvOverrides(options);
22
+ const envFileValues = getEnvFilePaths(options.nodeEnv).reduce((env, envFile) => {
23
+ return { ...env, ...parseEnv(readFileSync(envFile, "utf8")) };
24
+ }, {});
25
+ return {
26
+ envOverrides,
27
+ env: { ...envFileValues, ...process.env, ...envOverrides }
28
+ };
29
+ }
20
30
  function detectRuntime(options = {}) {
21
31
  if (options.runtime === "bun" || options.runtime === "node") {
22
32
  return options.runtime;
@@ -36,7 +46,7 @@ function buildBunArgs(args, options, entryFile, hasConfig) {
36
46
  if (options.hot) bunArgs.push("--hot");
37
47
  bunArgs.push("run");
38
48
  if (hasConfig) {
39
- bunArgs.push("--preload", "./eco.config.js");
49
+ bunArgs.push("--preload", `./eco.config.${"ts"}`);
40
50
  }
41
51
  bunArgs.push(entryFile, ...args);
42
52
  if (options.reactFastRefresh) {
@@ -44,26 +54,15 @@ function buildBunArgs(args, options, entryFile, hasConfig) {
44
54
  }
45
55
  return bunArgs;
46
56
  }
47
- function resolveTsxCliPath() {
48
- try {
49
- return require2.resolve("tsx/cli");
50
- } catch {
51
- throw new Error(
52
- "Unable to resolve the packaged tsx runtime required for Node.js launches. Reinstall ecopages and its dependencies, or use the Bun runtime instead."
53
- );
54
- }
55
- }
56
57
  async function createLaunchPlan(args, options = {}, entryFile = "app.ts") {
57
- const envOverrides = buildEnvOverrides(options);
58
+ const { envOverrides, env } = buildLaunchEnv(options);
58
59
  const runtime = detectRuntime(options);
59
- const env = { ...process.env, ...envOverrides };
60
60
  if (runtime === "node") {
61
- const tsxCliPath = resolveTsxCliPath();
62
- const nodeArgs = [entryFile, ...args];
61
+ const nodeEntryBridge = await buildNodeEntryBridge(entryFile);
63
62
  return {
64
63
  runtime,
65
64
  command: process.execPath,
66
- commandArgs: [...buildNodeEnvFileArgs(options.nodeEnv), tsxCliPath, ...nodeArgs],
65
+ commandArgs: [nodeEntryBridge, ...args],
67
66
  envOverrides,
68
67
  env
69
68
  };
@@ -79,7 +78,7 @@ async function createLaunchPlan(args, options = {}, entryFile = "app.ts") {
79
78
  export {
80
79
  buildBunArgs,
81
80
  buildEnvOverrides,
81
+ buildLaunchEnv,
82
82
  createLaunchPlan,
83
- detectRuntime,
84
- resolveTsxCliPath
83
+ detectRuntime
85
84
  };
@@ -0,0 +1,156 @@
1
+ import { lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync, unlinkSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { build } from "esbuild";
6
+ function getLoader(filePath) {
7
+ const extension = path.extname(filePath).toLowerCase();
8
+ if (extension === ".tsx") return "tsx";
9
+ if (extension === ".jsx") return "jsx";
10
+ if (extension === ".json") return "json";
11
+ return extension === ".ts" || extension === ".mts" || extension === ".cts" ? "ts" : "js";
12
+ }
13
+ function shouldRewriteSource(filePath, rootDir) {
14
+ const normalizedPath = path.resolve(filePath);
15
+ const normalizedRootDir = path.resolve(rootDir);
16
+ return normalizedPath === normalizedRootDir || normalizedPath.startsWith(`${normalizedRootDir}${path.sep}`);
17
+ }
18
+ function rewriteImportMeta(contents, filePath, entryFile) {
19
+ const normalizedPath = path.resolve(filePath);
20
+ const normalizedEntryFile = path.resolve(entryFile);
21
+ return contents.replaceAll("import.meta.env", "process.env").replaceAll("import.meta.main", normalizedPath === normalizedEntryFile ? "true" : "false").replaceAll("import.meta.url", JSON.stringify(pathToFileURL(filePath).href)).replaceAll("import.meta.dirname", JSON.stringify(path.dirname(filePath))).replaceAll("import.meta.filename", JSON.stringify(filePath)).replaceAll("import.meta.dir", JSON.stringify(path.dirname(filePath))).replaceAll("import.meta.path", JSON.stringify(filePath));
22
+ }
23
+ function getPackageNameFromSpecifier(specifier) {
24
+ if (specifier.startsWith("@")) {
25
+ const [scope, name] = specifier.split("/");
26
+ return `${scope}/${name}`;
27
+ }
28
+ return specifier.split("/")[0] ?? specifier;
29
+ }
30
+ function findPackageRoot(resolvedPath) {
31
+ let currentPath = path.dirname(resolvedPath);
32
+ while (true) {
33
+ const packageJsonPath = path.join(currentPath, "package.json");
34
+ try {
35
+ lstatSync(packageJsonPath);
36
+ return currentPath;
37
+ } catch {
38
+ }
39
+ const parentPath = path.dirname(currentPath);
40
+ if (parentPath === currentPath) {
41
+ throw new Error(`Could not find package root for resolved dependency path: ${resolvedPath}`);
42
+ }
43
+ currentPath = parentPath;
44
+ }
45
+ }
46
+ function linkPointsToPackage(linkPath, packageRoot) {
47
+ try {
48
+ return realpathSync(linkPath) === realpathSync(packageRoot);
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+ function removeRuntimePackageLink(linkPath) {
54
+ try {
55
+ const stats = lstatSync(linkPath);
56
+ if (stats.isSymbolicLink()) {
57
+ unlinkSync(linkPath);
58
+ return;
59
+ }
60
+ } catch {
61
+ return;
62
+ }
63
+ rmSync(linkPath, { recursive: true, force: true });
64
+ }
65
+ function ensureRuntimePackageLink(nodeModulesDir, specifier, resolvedPath) {
66
+ const packageName = getPackageNameFromSpecifier(specifier);
67
+ const packageRoot = findPackageRoot(resolvedPath);
68
+ const linkPath = path.join(nodeModulesDir, packageName);
69
+ mkdirSync(path.dirname(linkPath), { recursive: true });
70
+ try {
71
+ lstatSync(linkPath);
72
+ if (linkPointsToPackage(linkPath, packageRoot)) {
73
+ return;
74
+ }
75
+ removeRuntimePackageLink(linkPath);
76
+ } catch {
77
+ }
78
+ symlinkSync(packageRoot, linkPath, "dir");
79
+ }
80
+ function createNodeEntryBridgePlugin({ rootDir, entryFile, runtimeNodeModulesDir }) {
81
+ return {
82
+ name: "ecopages-node-entry-bridge",
83
+ setup(buildContext) {
84
+ buildContext.onResolve({ filter: /^[@A-Za-z0-9][^:]*$/ }, (args) => {
85
+ try {
86
+ const resolvedPath = createRequire(path.join(args.resolveDir || rootDir, "package.json")).resolve(
87
+ args.path
88
+ );
89
+ if (!args.path.startsWith("@ecopages/")) {
90
+ ensureRuntimePackageLink(runtimeNodeModulesDir, args.path, resolvedPath);
91
+ }
92
+ if (resolvedPath.endsWith(".node")) {
93
+ return {
94
+ path: args.path,
95
+ external: true
96
+ };
97
+ }
98
+ if (!args.path.startsWith("@ecopages/")) {
99
+ return {
100
+ path: args.path,
101
+ external: true
102
+ };
103
+ }
104
+ } catch {
105
+ return void 0;
106
+ }
107
+ return void 0;
108
+ });
109
+ buildContext.onResolve({ filter: /\.node$/ }, (args) => ({
110
+ path: path.isAbsolute(args.path) ? args.path : path.resolve(args.resolveDir, args.path),
111
+ external: true
112
+ }));
113
+ buildContext.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, (args) => {
114
+ if (!shouldRewriteSource(args.path, rootDir)) {
115
+ return void 0;
116
+ }
117
+ const contents = rewriteImportMeta(readFileSync(args.path, "utf8"), args.path, entryFile);
118
+ return {
119
+ contents,
120
+ loader: getLoader(args.path),
121
+ resolveDir: path.dirname(args.path)
122
+ };
123
+ });
124
+ }
125
+ };
126
+ }
127
+ async function buildNodeEntryBridge(entryFile, options = {}) {
128
+ const rootDir = path.resolve(options.rootDir ?? process.cwd());
129
+ const absoluteEntryFile = path.resolve(rootDir, entryFile);
130
+ const outdir = path.resolve(rootDir, ".eco", "node-entry");
131
+ const outfile = path.join(outdir, "app-entry.mjs");
132
+ rmSync(outdir, { recursive: true, force: true });
133
+ mkdirSync(outdir, { recursive: true });
134
+ await build({
135
+ absWorkingDir: rootDir,
136
+ entryPoints: [absoluteEntryFile],
137
+ outfile,
138
+ bundle: true,
139
+ format: "esm",
140
+ platform: "node",
141
+ target: "es2022",
142
+ sourcemap: "linked",
143
+ logLevel: "silent",
144
+ plugins: [
145
+ createNodeEntryBridgePlugin({
146
+ rootDir,
147
+ entryFile: absoluteEntryFile,
148
+ runtimeNodeModulesDir: path.join(outdir, "node_modules")
149
+ })
150
+ ]
151
+ });
152
+ return outfile;
153
+ }
154
+ export {
155
+ buildNodeEntryBridge
156
+ };
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "ecopages",
3
- "version": "0.2.0-alpha.42",
3
+ "version": "0.2.0-alpha.44",
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": {
@@ -29,10 +32,10 @@
29
32
  "ecopages": "bin/cli.js"
30
33
  },
31
34
  "dependencies": {
32
- "@ecopages/core": "0.2.0-alpha.42",
35
+ "@ecopages/core": "0.2.0-alpha.44",
33
36
  "@ecopages/logger": "^0.2.3",
34
- "giget": "^2.0.0",
35
- "tsx": "^4.22.0"
37
+ "esbuild": "^0.28.0",
38
+ "giget": "^2.0.0"
36
39
  },
37
40
  "peerDependencies": {
38
41
  "bun-types": "*",