@ecopages/core 0.2.0-beta.6 → 0.2.0-beta.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/core",
3
- "version": "0.2.0-beta.6",
3
+ "version": "0.2.0-beta.7",
4
4
  "description": "Core package for Ecopages",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -17,7 +17,7 @@
17
17
  "directory": "packages/core"
18
18
  },
19
19
  "dependencies": {
20
- "@ecopages/file-system": "0.2.0-beta.6",
20
+ "@ecopages/file-system": "0.2.0-beta.7",
21
21
  "@ecopages/logger": "^0.2.3",
22
22
  "@ecopages/scripts-injector": "^0.1.5",
23
23
  "@oxc-project/runtime": "0.134.0",
@@ -55,7 +55,6 @@ export declare class ServerStaticBuilder {
55
55
  private readonly previewServerFactory;
56
56
  private readonly entryFile;
57
57
  constructor({ appConfig, staticSiteGenerator, serveOptions, apiHandlers, logger, previewServerFactory, entryFile, }: ServerStaticBuilderParams);
58
- private warnApiHandlersUnavailableInStaticMode;
59
58
  private prepareExportDirectory;
60
59
  private refreshRuntimeAssets;
61
60
  /**
@@ -30,21 +30,6 @@ class ServerStaticBuilder {
30
30
  this.previewServerFactory = previewServerFactory ?? StaticContentServer;
31
31
  this.entryFile = resolveEntryFile({ entryFile });
32
32
  }
33
- warnApiHandlersUnavailableInStaticMode() {
34
- if (this.apiHandlers.length === 0) {
35
- return;
36
- }
37
- const uniqueHandlers = Array.from(
38
- new Set(this.apiHandlers.map((handler) => `${handler.method} ${handler.path}`))
39
- );
40
- const visibleHandlers = uniqueHandlers.slice(0, 5).join(", ");
41
- const remainingCount = uniqueHandlers.length - Math.min(uniqueHandlers.length, 5);
42
- const summary = remainingCount > 0 ? `${visibleHandlers}, +${remainingCount} more` : visibleHandlers;
43
- this.logger.warn(
44
- "Registered API endpoints are not available in static build or preview modes because no server runtime is started. They are excluded from the generated output.\n",
45
- `\u27A4 ${summary}`
46
- );
47
- }
48
33
  prepareExportDirectory() {
49
34
  const exportDir = this.appConfig.absolutePaths?.distDir ?? path.join(this.appConfig.rootDir, this.appConfig.distDir);
50
35
  fileSystem.ensureDir(exportDir, true);
@@ -131,7 +116,6 @@ ${errorMessages}`);
131
116
  async build(options, dependencies) {
132
117
  const { preview = false, baseUrl: explicitBaseUrl } = options ?? {};
133
118
  const baseUrl = explicitBaseUrl ?? `http://${this.serveOptions.hostname || DEFAULT_ECOPAGES_HOSTNAME}:${this.serveOptions.port || DEFAULT_ECOPAGES_PORT}`;
134
- this.warnApiHandlersUnavailableInStaticMode();
135
119
  this.prepareExportDirectory();
136
120
  await this.refreshRuntimeAssets();
137
121
  await this.bundleServerEntry();
@@ -1,10 +1,41 @@
1
1
  import { createRequire } from "node:module";
2
+ import { readFileSync } from "node:fs";
2
3
  import path from "node:path";
3
4
  import { collectBrowserRuntimeImportRewriteMap, rewriteBrowserRuntimeImports } from "./browser-runtime-plugin.js";
4
5
  import { createServerSideCssShimPlugin } from "./server-side-css-shim-plugin.js";
5
6
  import { createRolldownPluginBridge } from "./rolldown-plugin-bridge.js";
6
- import { isDeclaredAppPackageImport, normalizeNodeRuntimeBuildOutputs } from "./runtime-build-output-normalizer.js";
7
+ import {
8
+ isDeclaredAppPackageImport,
9
+ isWorkspacePackageImport,
10
+ normalizeNodeRuntimeBuildOutputs
11
+ } from "./runtime-build-output-normalizer.js";
7
12
  const corePackageRequire = createRequire(new URL("../../package.json", import.meta.url));
13
+ let corePackageNames;
14
+ function getCorePackageNames() {
15
+ if (corePackageNames) {
16
+ return corePackageNames;
17
+ }
18
+ const packageJsonPath = new URL("../../package.json", import.meta.url);
19
+ corePackageNames = /* @__PURE__ */ new Set();
20
+ try {
21
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
22
+ for (const field of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
23
+ const entries = packageJson[field];
24
+ if (!entries || typeof entries !== "object") {
25
+ continue;
26
+ }
27
+ for (const name of Object.keys(entries)) {
28
+ corePackageNames.add(name);
29
+ }
30
+ }
31
+ } catch {
32
+ }
33
+ return corePackageNames;
34
+ }
35
+ function isCoreDeclaredPackageImport(specifier) {
36
+ const name = specifier.startsWith("@") ? specifier.split("/").slice(0, 2).join("/") : specifier.split("/")[0] ?? specifier;
37
+ return getCorePackageNames().has(name);
38
+ }
8
39
  function transpileProfileToOptions(profile) {
9
40
  switch (profile) {
10
41
  case "browser-script":
@@ -37,13 +68,21 @@ function getAppRootRequire(cache, contextRoot) {
37
68
  return req;
38
69
  }
39
70
  function shouldBundlePackageImport(id, contextRoot, appRootRequireCache) {
71
+ if (isWorkspacePackageImport(id, contextRoot)) {
72
+ return true;
73
+ }
40
74
  if (isDeclaredAppPackageImport(id, contextRoot)) {
41
75
  const appRootRequire = getAppRootRequire(appRootRequireCache, contextRoot);
42
76
  const appResolvedPath = tryResolveModule(id, appRootRequire);
43
77
  return Boolean(appResolvedPath && /\.(?:[cm]?ts|tsx|jsx)$/u.test(appResolvedPath));
44
78
  }
45
- const coreResolvedPath = tryResolveModule(id, corePackageRequire);
46
- return Boolean(coreResolvedPath && /\.(?:[cm]?ts|tsx|jsx)$/u.test(coreResolvedPath));
79
+ if (isCoreDeclaredPackageImport(id)) {
80
+ const coreResolvedPath = tryResolveModule(id, corePackageRequire);
81
+ if (coreResolvedPath && !/\.(?:[cm]?ts|tsx|jsx)$/u.test(coreResolvedPath)) {
82
+ return false;
83
+ }
84
+ }
85
+ return true;
47
86
  }
48
87
  function createExternalMatcher(options, appRootRequireCache) {
49
88
  const explicitExternals = new Set(options.external ?? []);
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Returns true if the given specifier is a workspace package.
3
+ *
4
+ * Workspace packages are declared with the `workspace:` protocol and are
5
+ * typically source-only, requiring bundling rather than externalization.
6
+ */
7
+ export declare function isWorkspacePackageImport(specifier: string, rootDir: string): boolean;
1
8
  export declare function isDeclaredAppPackageImport(specifier: string, rootDir: string): boolean;
2
9
  export declare function normalizeNodeRuntimeBuildOutputFile(filePath: string, rootDir: string): void;
3
10
  export declare function normalizeNodeRuntimeBuildOutputs(outputPaths: string[], rootDir: string): void;
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  const corePackageRequire = createRequire(new URL("../../package.json", import.meta.url));
6
6
  const appDeclaredPackageCache = /* @__PURE__ */ new Map();
7
+ const appWorkspacePackageCache = /* @__PURE__ */ new Map();
7
8
  const CORE_RUNTIME_BARE_SPECIFIER_PACKAGES = /* @__PURE__ */ new Set(["ws"]);
8
9
  function tryResolveRuntimeImport(specifier, resolver) {
9
10
  try {
@@ -15,6 +16,24 @@ function tryResolveRuntimeImport(specifier, resolver) {
15
16
  function isBareRuntimeImport(specifier) {
16
17
  return !specifier.startsWith(".") && !path.isAbsolute(specifier) && !specifier.startsWith("/") && !specifier.startsWith("node:") && !specifier.startsWith("@/") && !specifier.startsWith("~/") && !specifier.startsWith("#") && !specifier.includes(":");
17
18
  }
19
+ function extractPackageNameFromFileUrl(specifier) {
20
+ if (!specifier.startsWith("file://")) {
21
+ return void 0;
22
+ }
23
+ const lastNodeModulesIndex = specifier.lastIndexOf("/node_modules/");
24
+ if (lastNodeModulesIndex === -1) {
25
+ return void 0;
26
+ }
27
+ const afterNodeModules = specifier.slice(lastNodeModulesIndex + "/node_modules/".length);
28
+ const parts = afterNodeModules.split("/");
29
+ if (parts.length === 0) {
30
+ return void 0;
31
+ }
32
+ if (parts[0]?.startsWith("@") && parts.length > 1) {
33
+ return `${parts[0]}/${parts[1]}`;
34
+ }
35
+ return parts[0];
36
+ }
18
37
  function getPackageNameFromSpecifier(specifier) {
19
38
  if (specifier.startsWith("@")) {
20
39
  return specifier.split("/").slice(0, 2).join("/");
@@ -47,6 +66,37 @@ function getDeclaredAppPackages(rootDir) {
47
66
  appDeclaredPackageCache.set(cacheKey, declaredPackages);
48
67
  return declaredPackages;
49
68
  }
69
+ function getWorkspacePackages(rootDir) {
70
+ const cacheKey = path.resolve(rootDir);
71
+ const cached = appWorkspacePackageCache.get(cacheKey);
72
+ if (cached) {
73
+ return cached;
74
+ }
75
+ const packageJsonPath = path.resolve(rootDir, "package.json");
76
+ const workspacePackages = /* @__PURE__ */ new Set();
77
+ try {
78
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
79
+ for (const field of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
80
+ const entries = packageJson[field];
81
+ if (!entries || typeof entries !== "object") {
82
+ continue;
83
+ }
84
+ for (const [packageName, version] of Object.entries(entries)) {
85
+ if (typeof version === "string" && version.startsWith("workspace:")) {
86
+ workspacePackages.add(packageName);
87
+ }
88
+ }
89
+ }
90
+ } catch {
91
+ appWorkspacePackageCache.set(cacheKey, workspacePackages);
92
+ return workspacePackages;
93
+ }
94
+ appWorkspacePackageCache.set(cacheKey, workspacePackages);
95
+ return workspacePackages;
96
+ }
97
+ function isWorkspacePackageImport(specifier, rootDir) {
98
+ return getWorkspacePackages(rootDir).has(getPackageNameFromSpecifier(specifier));
99
+ }
50
100
  function isDeclaredAppPackageImport(specifier, rootDir) {
51
101
  return getDeclaredAppPackages(rootDir).has(getPackageNameFromSpecifier(specifier));
52
102
  }
@@ -75,6 +125,23 @@ function rewriteRuntimeBuildOutputImports(code, rootDir) {
75
125
  const rewrittenSpecifier = rewriteRuntimeImportSpecifier(specifier, quote, rootDir);
76
126
  return `${prefix}${rewrittenSpecifier ?? `${quote}${specifier}${quote}`}`;
77
127
  };
128
+ const rewriteFileUrl = (prefix, quote, specifier) => {
129
+ const packageName = extractPackageNameFromFileUrl(specifier);
130
+ if (!packageName) {
131
+ return `${prefix}${quote}${specifier}${quote}`;
132
+ }
133
+ if (!isDeclaredInResolutionChain(packageName, rootDir)) {
134
+ return `${prefix}${quote}${specifier}${quote}`;
135
+ }
136
+ const lastNodeModulesIndex = specifier.lastIndexOf("/node_modules/");
137
+ if (lastNodeModulesIndex === -1) {
138
+ return `${prefix}${quote}${specifier}${quote}`;
139
+ }
140
+ const afterNodeModules = specifier.slice(lastNodeModulesIndex + "/node_modules/".length);
141
+ const afterPackage = afterNodeModules.slice(packageName.length);
142
+ const bareSpecifier = packageName + afterPackage;
143
+ return `${prefix}${quote}${bareSpecifier}${quote}`;
144
+ };
78
145
  const withResolvedPackages = code.replace(
79
146
  /(\bfrom\s+)(['"])([^'"\\]+)\2/g,
80
147
  (_match, prefix, quote, specifier) => rewriteSpecifier(prefix, quote, specifier)
@@ -85,7 +152,17 @@ function rewriteRuntimeBuildOutputImports(code, rootDir) {
85
152
  /(\bimport\s*\(\s*)(['"])([^'"\\]+)\2/g,
86
153
  (_match, prefix, quote, specifier) => rewriteSpecifier(prefix, quote, specifier)
87
154
  );
88
- return withResolvedPackages.replace(
155
+ const withBareSpecifiers = withResolvedPackages.replace(
156
+ /(\bfrom\s+)(['"])(file:\/\/[^'"\\]+)\2/g,
157
+ (_match, prefix, quote, specifier) => rewriteFileUrl(prefix, quote, specifier)
158
+ ).replace(
159
+ /(\bimport\s+)(['"])(file:\/\/[^'"\\]+)\2/g,
160
+ (_match, prefix, quote, specifier) => rewriteFileUrl(prefix, quote, specifier)
161
+ ).replace(
162
+ /(\bimport\s*\(\s*)(['"])(file:\/\/[^'"\\]+)\2/g,
163
+ (_match, prefix, quote, specifier) => rewriteFileUrl(prefix, quote, specifier)
164
+ );
165
+ return withBareSpecifiers.replace(
89
166
  /(['"])(@oxc-project\/runtime\/(?:helpers(?:\/esm)?\/[^'"\\]+))\1/g,
90
167
  (match, quote, specifier) => {
91
168
  const resolvedPath = tryResolveRuntimeImport(specifier, corePackageRequire);
@@ -94,6 +171,9 @@ function rewriteRuntimeBuildOutputImports(code, rootDir) {
94
171
  );
95
172
  }
96
173
  function normalizeNodeRuntimeBuildOutputFile(filePath, rootDir) {
174
+ if (process.env.ECOPAGES_LOGGER_DEBUG === "true") {
175
+ console.log(`[normalizeNodeRuntimeBuildOutputFile] Checking ${filePath}`);
176
+ }
97
177
  if (!/\.(?:[cm]?js)$/u.test(filePath)) {
98
178
  return;
99
179
  }
@@ -104,6 +184,9 @@ function normalizeNodeRuntimeBuildOutputFile(filePath, rootDir) {
104
184
  const code = fileSystem.readFileSync(filePath, "utf-8");
105
185
  const rewritten = rewriteRuntimeBuildOutputImports(code, rootDir);
106
186
  if (rewritten !== code) {
187
+ if (process.env.ECOPAGES_LOGGER_DEBUG === "true") {
188
+ console.log(`[normalizeNodeRuntimeBuildOutputFile] Rewriting ${filePath}`);
189
+ }
107
190
  fileSystem.writeFileSync(filePath, rewritten);
108
191
  }
109
192
  }
@@ -114,6 +197,7 @@ function normalizeNodeRuntimeBuildOutputs(outputPaths, rootDir) {
114
197
  }
115
198
  export {
116
199
  isDeclaredAppPackageImport,
200
+ isWorkspacePackageImport,
117
201
  normalizeNodeRuntimeBuildOutputFile,
118
202
  normalizeNodeRuntimeBuildOutputs
119
203
  };
@@ -34,19 +34,7 @@ export declare class StaticSiteGenerator {
34
34
  appConfig: EcoPagesAppConfig;
35
35
  });
36
36
  private getExportDir;
37
- /**
38
- * Logs the standardized warning emitted when a dynamic-cache page is skipped.
39
- */
40
- private warnDynamicPageSkipped;
41
- /**
42
- * Determines whether one filesystem-discovered page should be excluded from
43
- * static generation.
44
- */
45
37
  private shouldSkipStaticPageFile;
46
- /**
47
- * Determines whether one explicit static route view should be excluded from
48
- * static generation.
49
- */
50
38
  private shouldSkipStaticView;
51
39
  /**
52
40
  * Writes the robots.txt file declared by the app config.
@@ -71,7 +59,7 @@ export declare class StaticSiteGenerator {
71
59
  * issuing a request against the running server origin. Render-strategy routes
72
60
  * go through the normal route renderer directly.
73
61
  */
74
- generateStaticPages(router: StaticGenerationRouteSource, baseUrl: string, routeRendererFactory?: StaticPageRouteRendererFactory): Promise<void>;
62
+ generateStaticPages(router: StaticGenerationRouteSource, baseUrl: string, routeRendererFactory?: StaticPageRouteRendererFactory, skipped?: string[]): Promise<void>;
75
63
  private resolveStaticFetchUrl;
76
64
  /**
77
65
  * Executes the full static-generation workflow for one app run.
@@ -21,39 +21,14 @@ class StaticSiteGenerator {
21
21
  getExportDir() {
22
22
  return this.appConfig.absolutePaths?.distDir ?? path.join(this.appConfig.rootDir, this.appConfig.distDir);
23
23
  }
24
- /**
25
- * Logs the standardized warning emitted when a dynamic-cache page is skipped.
26
- */
27
- warnDynamicPageSkipped(filePath) {
28
- appLogger.warn(
29
- "Pages with cache: 'dynamic' are not supported in static generation or preview, so they will be skipped\n",
30
- `\u27A4 ${filePath}`
31
- );
32
- }
33
- /**
34
- * Determines whether one filesystem-discovered page should be excluded from
35
- * static generation.
36
- */
37
24
  async shouldSkipStaticPageFile(filePath, routeRendererFactory) {
38
25
  const module = await routeRendererFactory.getPageRenderer(filePath).loadPageModule(filePath, {
39
26
  cacheScope: "static-page-probe"
40
27
  });
41
- if (module.default?.cache !== "dynamic") {
42
- return false;
43
- }
44
- this.warnDynamicPageSkipped(filePath);
45
- return true;
28
+ return module.default?.cache === "dynamic";
46
29
  }
47
- /**
48
- * Determines whether one explicit static route view should be excluded from
49
- * static generation.
50
- */
51
- shouldSkipStaticView(routePath, view) {
52
- if (view.cache !== "dynamic") {
53
- return false;
54
- }
55
- this.warnDynamicPageSkipped(routePath);
56
- return true;
30
+ shouldSkipStaticView(_routePath, view) {
31
+ return view.cache === "dynamic";
57
32
  }
58
33
  /**
59
34
  * Writes the robots.txt file declared by the app config.
@@ -105,7 +80,7 @@ class StaticSiteGenerator {
105
80
  const integration = this.appConfig.integrations.find((plugin) => plugin.extensions.includes(ext));
106
81
  return integration?.staticBuildStep || "render";
107
82
  }
108
- async createFilesystemStaticContents(route, baseUrl, routeRendererFactory) {
83
+ async createFilesystemStaticContents(route, baseUrl, routeRendererFactory, skipped) {
109
84
  const {
110
85
  templateRoute: { filePath },
111
86
  params
@@ -123,6 +98,7 @@ class StaticSiteGenerator {
123
98
  throw new Error(STATIC_SITE_GENERATOR_ERRORS.ROUTE_RENDERER_FACTORY_REQUIRED);
124
99
  }
125
100
  if (await this.shouldSkipStaticPageFile(filePath, routeRendererFactory)) {
101
+ skipped?.push(filePath);
126
102
  return null;
127
103
  }
128
104
  const renderer = routeRendererFactory.getPageRenderer(filePath);
@@ -147,7 +123,7 @@ class StaticSiteGenerator {
147
123
  * issuing a request against the running server origin. Render-strategy routes
148
124
  * go through the normal route renderer directly.
149
125
  */
150
- async generateStaticPages(router, baseUrl, routeRendererFactory) {
126
+ async generateStaticPages(router, baseUrl, routeRendererFactory, skipped) {
151
127
  const routes = await router.listStaticGenerationRoutes({ runtimeOrigin: baseUrl });
152
128
  appLogger.debug(
153
129
  "Static Pages",
@@ -156,7 +132,12 @@ class StaticSiteGenerator {
156
132
  const directories = this.getDirectories(routes.map((route) => route.requestUrl));
157
133
  for (const route of routes) {
158
134
  try {
159
- const contents = await this.createFilesystemStaticContents(route, baseUrl, routeRendererFactory);
135
+ const contents = await this.createFilesystemStaticContents(
136
+ route,
137
+ baseUrl,
138
+ routeRendererFactory,
139
+ skipped
140
+ );
160
141
  if (contents === null) {
161
142
  continue;
162
143
  }
@@ -189,17 +170,24 @@ class StaticSiteGenerator {
189
170
  routeRendererFactory,
190
171
  staticRoutes
191
172
  }) {
173
+ const skippedDynamicPages = [];
192
174
  this.generateRobotsTxt();
193
- await this.generateStaticPages(router, baseUrl, routeRendererFactory);
175
+ await this.generateStaticPages(router, baseUrl, routeRendererFactory, skippedDynamicPages);
194
176
  if (staticRoutes && staticRoutes.length > 0 && routeRendererFactory) {
195
- await this.generateExplicitStaticPages(staticRoutes, routeRendererFactory);
177
+ await this.generateExplicitStaticPages(staticRoutes, routeRendererFactory, skippedDynamicPages);
178
+ }
179
+ if (skippedDynamicPages.length > 0) {
180
+ appLogger.debug(
181
+ `Skipped ${skippedDynamicPages.length} page(s) with cache: 'dynamic' (not supported in static generation)`,
182
+ skippedDynamicPages
183
+ );
196
184
  }
197
185
  }
198
186
  /**
199
187
  * Generates static pages from explicit static routes registered via app.static().
200
188
  * These routes use eco.page views via loader functions for HMR support.
201
189
  */
202
- async generateExplicitStaticPages(staticRoutes, routeRendererFactory) {
190
+ async generateExplicitStaticPages(staticRoutes, routeRendererFactory, skipped) {
203
191
  appLogger.debug(
204
192
  "Generating explicit static routes",
205
193
  staticRoutes.map((r) => r.path)
@@ -209,6 +197,7 @@ class StaticSiteGenerator {
209
197
  const mod = await route.loader();
210
198
  const view = mod.default;
211
199
  if (this.shouldSkipStaticView(route.path, view)) {
200
+ skipped?.push(route.path);
212
201
  continue;
213
202
  }
214
203
  await this.generateExplicitStaticRoute(route.path, view, routeRendererFactory);