🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@ecopages/core

Package Overview
Dependencies
Maintainers
1
Versions
88
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ecopages/core - npm Package Compare versions

Comparing version
0.2.0-beta.23
to
0.2.0-beta.24
+34
src/build/dev-browser-script-cache.d.ts
import type { EcoPagesAppConfig } from '../types/internal-types.js';
import type { ProcessedAsset } from '../services/assets/asset-processing-service/assets.types.js';
export declare const DEV_BROWSER_SCRIPT_CACHE_DIR = ".browser-script-bundles";
export declare const DEV_BROWSER_SCRIPT_CACHE_FILENAME = ".build-cache.json";
/** Output path restored from the dev disk cache before dependency metadata is merged. */
export type DevBrowserScriptCacheHit = {
filepath: string;
};
/**
* @remarks
* Dev asset caching spans three scopes that should not be conflated:
* - request dedupe (`request-build-dedupe.ts`) — one in-flight build per request key
* - in-memory service cache (`AssetProcessingService`) — per-process reuse within one dev server
* - this disk manifest (`.eco/.browser-script-bundles`) — cross-request reuse of content-script bundles
*
* Disk entries intentionally store only output file paths. Declarative fields such as
* `groupedBundle`, `attributes`, and `packageRole` live on the originating
* `ContentScriptAsset` and are merged via `materializeContentScriptAsset()` on lookup.
*/
export interface DevBrowserScriptCacheEntry {
filepath: string;
builtAt: number;
}
export interface DevBrowserScriptCacheManifest {
invalidationVersion: string;
buildInputsFingerprint: string;
entries: Record<string, DevBrowserScriptCacheEntry>;
}
/** Returns whether dev-only cross-request content-script bundle cache is active. */
export declare function shouldUseDevBrowserScriptCache(): boolean;
/** Looks up one persisted dev content-script output path by dependency cache key. */
export declare function getDevBrowserScriptCacheEntry(appConfig: EcoPagesAppConfig, depKey: string): DevBrowserScriptCacheHit | null;
/** Persists one dev content-script bundle output for cross-request reuse. */
export declare function setDevBrowserScriptCacheEntry(appConfig: EcoPagesAppConfig, depKey: string, asset: ProcessedAsset): void;
import path from "node:path";
import { fileSystem } from "@ecopages/file-system";
import { getAppBrowserBuildPlugins } from "./build-adapter.js";
import { createBuildInputsFingerprint, hashAppConfigFile } from "./build-input-fingerprint.js";
import {
readProductionCacheManifest,
writeProductionCacheManifest,
isProductionCacheManifestCurrent,
matchesProductionCacheFingerprint
} from "./production-build-cache.js";
import { getCorePackageVersion } from "../services/module-loading/route-module-build-manifest.js";
import { resolveInternalExecutionDir } from "../utils/resolve-work-dir.js";
import { isDevelopmentRuntime } from "../utils/runtime.js";
const DEV_BROWSER_SCRIPT_CACHE_DIR = ".browser-script-bundles";
const DEV_BROWSER_SCRIPT_CACHE_FILENAME = ".build-cache.json";
function getDevBrowserScriptCacheDir(appConfig) {
return path.join(resolveInternalExecutionDir(appConfig), DEV_BROWSER_SCRIPT_CACHE_DIR);
}
function getDevBrowserScriptCacheManifestPath(appConfig) {
return path.join(getDevBrowserScriptCacheDir(appConfig), DEV_BROWSER_SCRIPT_CACHE_FILENAME);
}
function createBrowserBuildKey(appConfig) {
try {
const pluginNames = getAppBrowserBuildPlugins(appConfig).map((plugin) => plugin.name).sort().join("|");
return pluginNames || "default";
} catch {
return "default";
}
}
function createDevBrowserScriptCacheInvalidationVersion(appConfig) {
return [getCorePackageVersion(), hashAppConfigFile(appConfig), createBrowserBuildKey(appConfig)].join("::");
}
function readManifest(appConfig) {
const manifest = readProductionCacheManifest(
getDevBrowserScriptCacheManifestPath(appConfig)
);
if (!manifest) {
return void 0;
}
const invalidationVersion = createDevBrowserScriptCacheInvalidationVersion(appConfig);
const buildInputsFingerprint = createBuildInputsFingerprint(appConfig);
if (!isProductionCacheManifestCurrent(manifest, invalidationVersion) || !matchesProductionCacheFingerprint(manifest, buildInputsFingerprint)) {
return void 0;
}
return manifest;
}
function writeManifest(appConfig, manifest) {
writeProductionCacheManifest(getDevBrowserScriptCacheManifestPath(appConfig), manifest);
}
function shouldUseDevBrowserScriptCache() {
return isDevelopmentRuntime();
}
function getDevBrowserScriptCacheEntry(appConfig, depKey) {
if (!shouldUseDevBrowserScriptCache()) {
return null;
}
const manifest = readManifest(appConfig);
const entry = manifest?.entries[depKey];
if (!entry?.filepath || !fileSystem.exists(entry.filepath)) {
return null;
}
return { filepath: entry.filepath };
}
function setDevBrowserScriptCacheEntry(appConfig, depKey, asset) {
if (!shouldUseDevBrowserScriptCache() || !asset.filepath) {
return;
}
const invalidationVersion = createDevBrowserScriptCacheInvalidationVersion(appConfig);
const buildInputsFingerprint = createBuildInputsFingerprint(appConfig);
const existing = readManifest(appConfig);
const manifest = existing ?? {
invalidationVersion,
buildInputsFingerprint,
entries: {}
};
manifest.invalidationVersion = invalidationVersion;
manifest.buildInputsFingerprint = buildInputsFingerprint;
manifest.entries[depKey] = {
filepath: asset.filepath,
builtAt: Date.now()
};
writeManifest(appConfig, manifest);
}
export {
DEV_BROWSER_SCRIPT_CACHE_DIR,
DEV_BROWSER_SCRIPT_CACHE_FILENAME,
getDevBrowserScriptCacheEntry,
setDevBrowserScriptCacheEntry,
shouldUseDevBrowserScriptCache
};
/** Returns whether the current thread is the Lit static-render worker. */
export declare function isLitStaticRenderWorkerThread(): boolean;
function isLitStaticRenderWorkerThread() {
return process.env.ECOPAGES_LIT_STATIC_RENDER_WORKER === "true";
}
export {
isLitStaticRenderWorkerThread
};
import type { BuildResult } from '../build/build-adapter.js';
/**
* Per-request browser build dedupe cache.
*
* @remarks
* `DedupingBuildExecutor` only coalesces concurrent identical builds. Asset
* processing can call the same logical bundle several times in one request
* (page graph, HMR, content scripts) after the first build settles. This cache
* reuses the settled result until the request scope ends.
*/
declare class RequestBuildDedupe {
private readonly storage;
run<T>(fn: () => Promise<T>): Promise<T>;
dedupeBuild(key: string, build: () => Promise<BuildResult>): Promise<BuildResult>;
getCachedCountForTests(): number;
}
export declare const requestBuildDedupe: RequestBuildDedupe;
export {};
import { AsyncLocalStorage } from "node:async_hooks";
class RequestBuildDedupe {
storage = new AsyncLocalStorage();
run(fn) {
if (this.storage.getStore()) {
return fn();
}
return this.storage.run(/* @__PURE__ */ new Map(), fn);
}
dedupeBuild(key, build) {
const store = this.storage.getStore();
if (!store) {
return build();
}
const existing = store.get(key);
if (existing) {
return existing;
}
const buildPromise = build().catch((error) => {
store.delete(key);
throw error;
});
store.set(key, buildPromise);
return buildPromise;
}
getCachedCountForTests() {
return this.storage.getStore()?.size ?? 0;
}
}
const requestBuildDedupe = new RequestBuildDedupe();
export {
requestBuildDedupe
};
export type StartupTracePhase = 'config-ready' | 'setupAppRuntimePlugins' | 'route-registry' | 'server-listen' | 'first-request-ssr';
declare class StartupTrace {
private readonly phaseStarts;
private readonly phases;
private firstRequestPending;
private firstRequestPath;
private firstRequestBundleCount;
private firstRequestClientBytes;
private summaryEmitted;
isEnabled(): boolean;
markPhaseStart(phase: StartupTracePhase): void;
markPhaseEnd(phase: StartupTracePhase): void;
markConfigReady(): void;
markServerListening(): void;
beginServerListen(): void;
recordBrowserBundle(outputs: Array<{
path: string;
}>): void;
traceFirstRequest<T>(request: Request, handler: () => Promise<T>): Promise<T>;
private emitFirstRequestSummary;
/** Resets mutable trace state for unit tests. */
resetForTests(): void;
}
export declare const startupTrace: StartupTrace;
export {};
import { statSync } from "node:fs";
import { fileSystem } from "@ecopages/file-system";
const LOG_PREFIX = "[ecopages:startup-trace]";
function isStartupTraceEnabled() {
return process.env.ECOPAGES_STARTUP_TRACE === "true" || process.env.ECOPAGES_LOGGER_DEBUG === "true";
}
function wallMsSinceProcessStart() {
return Math.round(process.uptime() * 1e3);
}
function writeTraceLine(message) {
process.stderr.write(`${LOG_PREFIX} ${message}
`);
}
class StartupTrace {
phaseStarts = /* @__PURE__ */ new Map();
phases = /* @__PURE__ */ new Map();
firstRequestPending = true;
firstRequestPath;
firstRequestBundleCount = 0;
firstRequestClientBytes = 0;
summaryEmitted = false;
isEnabled() {
return isStartupTraceEnabled();
}
markPhaseStart(phase) {
if (!isStartupTraceEnabled() || this.phases.has(phase)) {
return;
}
this.phaseStarts.set(phase, performance.now());
}
markPhaseEnd(phase) {
if (!isStartupTraceEnabled() || this.phases.has(phase)) {
return;
}
const startedAt = this.phaseStarts.get(phase);
if (startedAt === void 0) {
return;
}
const durationMs = Math.round(performance.now() - startedAt);
const record = {
durationMs,
wallMs: wallMsSinceProcessStart()
};
this.phases.set(phase, record);
writeTraceLine(`phase=${phase} durationMs=${durationMs} wallMs=${record.wallMs}`);
}
markConfigReady() {
if (!isStartupTraceEnabled() || this.phases.has("config-ready")) {
return;
}
const record = {
durationMs: 0,
wallMs: wallMsSinceProcessStart()
};
this.phases.set("config-ready", record);
writeTraceLine(`phase=config-ready wallMs=${record.wallMs}`);
}
markServerListening() {
this.markPhaseEnd("server-listen");
}
beginServerListen() {
this.markPhaseStart("server-listen");
}
recordBrowserBundle(outputs) {
if (!isStartupTraceEnabled() || !this.firstRequestPending) {
return;
}
this.firstRequestBundleCount += 1;
for (const output of outputs) {
try {
if (fileSystem.exists(output.path)) {
this.firstRequestClientBytes += statSync(output.path).size;
}
} catch {
}
}
}
async traceFirstRequest(request, handler) {
if (!isStartupTraceEnabled() || !this.firstRequestPending) {
return handler();
}
this.firstRequestPath = new URL(request.url).pathname;
this.markPhaseStart("first-request-ssr");
try {
return await handler();
} finally {
this.markPhaseEnd("first-request-ssr");
this.emitFirstRequestSummary();
this.firstRequestPending = false;
}
}
emitFirstRequestSummary() {
if (!isStartupTraceEnabled() || this.summaryEmitted) {
return;
}
this.summaryEmitted = true;
writeTraceLine(
[
"summary",
`path=${this.firstRequestPath ?? "unknown"}`,
`bundleCount=${this.firstRequestBundleCount}`,
`clientBundleBytes=${this.firstRequestClientBytes}`,
`wallMs=${wallMsSinceProcessStart()}`
].join(" ")
);
}
/** Resets mutable trace state for unit tests. */
resetForTests() {
this.phaseStarts.clear();
this.phases.clear();
this.firstRequestPending = true;
this.firstRequestPath = void 0;
this.firstRequestBundleCount = 0;
this.firstRequestClientBytes = 0;
this.summaryEmitted = false;
}
}
const startupTrace = new StartupTrace();
export {
startupTrace
};
import type { ProcessedAsset } from './assets.types.js';
/** Applies the public source URL to one processed asset when available. */
export declare function finalizeProcessedAsset(processed: ProcessedAsset, resolveProcessedAssetSrcUrl: (processed: ProcessedAsset) => string | undefined): ProcessedAsset;
function finalizeProcessedAsset(processed, resolveProcessedAssetSrcUrl) {
const srcUrl = resolveProcessedAssetSrcUrl(processed);
return srcUrl ? { ...processed, srcUrl } : processed;
}
export {
finalizeProcessedAsset
};
import type { ContentScriptAsset, ProcessedAsset } from './assets.types.js';
/**
* Builds a processed content-script asset from one dependency declaration and a
* previously emitted output file.
*
* @remarks
* Dev disk cache entries store only output paths. Callers must materialize the
* full processed asset from the originating dependency so HTML and page-browser
* graph assembly receive grouped-bundle metadata and script attributes.
*/
export declare function materializeContentScriptAsset(dep: ContentScriptAsset, filepath: string): ProcessedAsset;
function materializeContentScriptAsset(dep, filepath) {
return {
filepath,
kind: "script",
inline: dep.inline ?? false,
content: dep.inline ? dep.content : void 0,
position: dep.position,
attributes: dep.attributes,
excludeFromHtml: dep.excludeFromHtml,
packageRole: dep.packageRole,
groupedBundle: dep.groupedBundle,
bundledSourceFilepaths: dep.bundledSourceFilepaths
};
}
export {
materializeContentScriptAsset
};
import type { AnyIntegrationPlugin } from '../../../plugins/integration-plugin.js';
import type { EcoPagesAppConfig } from '../../../types/internal-types.js';
/** Resolves the integration plugin that owns one asset-processing batch key. */
export declare function resolveIntegrationPluginForProcessingKey(appConfig: EcoPagesAppConfig, processingKey: string): AnyIntegrationPlugin | undefined;
function resolveIntegrationPluginForProcessingKey(appConfig, processingKey) {
if (!appConfig.integrations?.length) {
return void 0;
}
const exactMatch = appConfig.integrations.find((integration) => integration.name === processingKey);
if (exactMatch) {
return exactMatch;
}
const baseName = processingKey.split(":")[0];
if (!baseName || baseName === processingKey) {
return void 0;
}
return appConfig.integrations.find((integration) => integration.name === baseName);
}
export {
resolveIntegrationPluginForProcessingKey
};
+10
-2
{
"name": "@ecopages/core",
"version": "0.2.0-beta.23",
"version": "0.2.0-beta.24",
"description": "Core package for Ecopages",

@@ -20,3 +20,3 @@ "keywords": [

"dependencies": {
"@ecopages/file-system": "0.2.0-beta.23",
"@ecopages/file-system": "0.2.0-beta.24",
"@ecopages/logger": "^0.2.3",

@@ -211,2 +211,6 @@ "@ecopages/scripts-injector": "^0.1.5",

},
"./build/lit-static-render-worker-context": {
"types": "./src/build/lit-static-render-worker-context.d.ts",
"default": "./src/build/lit-static-render-worker-context.js"
},
"./cache": {

@@ -421,2 +425,6 @@ "types": "./src/cache/index.d.ts",

},
"./build/lit-static-render-worker-context.ts": {
"types": "./src/build/lit-static-render-worker-context.d.ts",
"default": "./src/build/lit-static-render-worker-context.js"
},
"./cache.ts": {

@@ -423,0 +431,0 @@ "types": "./src/cache/index.d.ts",

@@ -13,2 +13,3 @@ import { appLogger } from "../../global/app-logger.js";

import { parseCliArgs } from "../../utils/parse-cli-args.js";
import { startupTrace } from "../../diagnostics/startup-trace.js";
class AbstractApplicationAdapter {

@@ -46,2 +47,3 @@ appConfig;

getAppModuleLoader(this.appConfig);
startupTrace.markConfigReady();
if (options.clearOutput) {

@@ -226,2 +228,3 @@ this.clearDistFolder().catch((error) => {

const normalizedOrigin = origin.replace(/\/$/, "");
startupTrace.markServerListening();
if (!this.onAppStartCallback) {

@@ -228,0 +231,0 @@ this.logServerStarted(normalizedOrigin);

import { appLogger } from "../../global/app-logger.js";
import { SharedApplicationAdapter } from "../shared/application-adapter.js";
import { resolveRuntimeBinding, resolveStaticRuntimeMode } from "../shared/runtime-app-bootstrap.js";
import { startupTrace } from "../../diagnostics/startup-trace.js";
import { createBunServerAdapter } from "./server-adapter.js";

@@ -104,2 +105,3 @@ import { BunRuntimeHost } from "./runtime-host.js";

const runtimeServerOptions = serverOptions;
startupTrace.beginServerListen();
this.server = await this.runtimeHost.start({

@@ -106,0 +108,0 @@ serveOptions: runtimeServerOptions,

@@ -8,2 +8,3 @@ import { appLogger } from "../../global/app-logger.js";

import { hostOwnsDevClient } from "../../dev/dev-client-ownership.js";
import { startupTrace } from "../../diagnostics/startup-trace.js";
class NodeEcopagesApp extends SharedApplicationAdapter {

@@ -91,2 +92,3 @@ serverAdapter;

const serveOptions = this.serverAdapter.getServerOptions();
startupTrace.beginServerListen();
this.server = await this.runtimeHost.start({

@@ -93,0 +95,0 @@ serveOptions,

+17
-9

@@ -5,2 +5,4 @@ import path from "node:path";

import { RouteRegistry } from "../../router/server/route-registry.js";
import { startupTrace } from "../../diagnostics/startup-trace.js";
import { requestBuildDedupe } from "../../diagnostics/request-build-dedupe.js";
import { MemoryCacheStore } from "../../services/cache/memory-cache-store.js";

@@ -75,2 +77,3 @@ import { PageCacheService } from "../../services/cache/page-cache-service.js";

async initSharedRouter() {
startupTrace.markPhaseStart("route-registry");
this.router = new RouteRegistry({

@@ -85,2 +88,3 @@ pagesDir: path.join(this.appConfig.rootDir, this.appConfig.srcDir, this.appConfig.pagesDir),

await this.router.init();
startupTrace.markPhaseEnd("route-registry");
}

@@ -434,11 +438,15 @@ createRouteRegistryPageModuleAdapter() {

async handleSharedRequest(request, context) {
const hmrResponse = this.tryHandleSharedHmrRequest(request, context);
if (hmrResponse) {
return hmrResponse;
}
const apiResponse = await this.tryHandleSharedApiRequest(request, context);
if (apiResponse) {
return apiResponse;
}
return this.routeHandler.handleResponse(request);
return requestBuildDedupe.run(
() => startupTrace.traceFirstRequest(request, async () => {
const hmrResponse = this.tryHandleSharedHmrRequest(request, context);
if (hmrResponse) {
return hmrResponse;
}
const apiResponse = await this.tryHandleSharedApiRequest(request, context);
if (apiResponse) {
return apiResponse;
}
return this.routeHandler.handleResponse(request);
})
);
}

@@ -445,0 +453,0 @@ }

import { mergeBrowserRuntimeManifests } from "./browser-runtime-manifest.js";
import { appLogger } from "../global/app-logger.js";
import { startupTrace } from "../diagnostics/startup-trace.js";
import { isLitStaticRenderWorkerThread } from "./lit-static-render-worker-context.js";
function patchAppRuntime(appConfig, patch) {

@@ -53,7 +55,12 @@ appConfig.runtime = {

async function setupAppRuntimePlugins(options) {
startupTrace.markPhaseStart("setupAppRuntimePlugins");
if (options.appConfig.runtime?.runtimeAssetsPrepared) {
appLogger.debug("Skipped setupAppRuntimePlugins: runtime assets already prepared");
registerRuntimePlugins(options.appConfig, options.onRuntimePlugin);
startupTrace.markPhaseEnd("setupAppRuntimePlugins");
return;
}
if (isLitStaticRenderWorkerThread()) {
appLogger.debug("Lit static-render worker: skipping processor setup (main thread already prepared artifacts)");
}
appLogger.debugTime("setupAppRuntimePlugins");

@@ -64,4 +71,7 @@ try {

}
const skipProcessorSetup = isLitStaticRenderWorkerThread();
for (const processor of options.appConfig.processors.values()) {
await processor.setup();
if (!skipProcessorSetup) {
await processor.setup();
}
if (processor.plugins) {

@@ -84,2 +94,3 @@ for (const plugin of processor.plugins) {

appLogger.debugTimeEnd("setupAppRuntimePlugins");
startupTrace.markPhaseEnd("setupAppRuntimePlugins");
}

@@ -86,0 +97,0 @@ }

@@ -6,2 +6,4 @@ interface EcopagesEnv {

ECOPAGES_LOGGER_DEBUG: 'true' | 'false';
/** When `true`, emits startup phase timings to stderr. Also enabled when `ECOPAGES_LOGGER_DEBUG=true`. */
ECOPAGES_STARTUP_TRACE?: 'true' | 'false';
}

@@ -8,0 +10,0 @@

@@ -201,2 +201,10 @@ import type { EcoBuildPlugin } from '../build/build-types.js';

/**
* Shapes one dependency batch before core asset processing runs.
*
* @remarks
* Integrations use this to assign grouped-build metadata or other batch-level
* policy without teaching core about integration-specific asset graphs.
*/
prepareAssetDependencies?(dependencies: AssetDefinition[]): AssetDefinition[];
/**
* Prepares build-facing contributions before the app build manifest is sealed.

@@ -203,0 +211,0 @@ *

@@ -106,2 +106,5 @@ import path from "node:path";

}
if (this.isHmrEnabled()) {
continue;
}
if (!contribution?.dependencies?.length) {

@@ -134,11 +137,14 @@ continue;

for (const [routeFile, groupedAssetKeys] of groupedAssetKeysByRoute) {
groupedAssetsByRoute.set(
routeFile,
processedGroupedDependencies.filter((asset) => {
if (!asset.groupedBundle) {
return false;
}
return groupedAssetKeys.has(getGroupedBundleAssetKey(asset.groupedBundle));
})
);
const matchedAssets = processedGroupedDependencies.filter((asset) => {
if (!asset.groupedBundle) {
return false;
}
return groupedAssetKeys.has(getGroupedBundleAssetKey(asset.groupedBundle));
});
if (groupedAssetKeys.size > 0 && matchedAssets.length === 0) {
appLogger.warn(
`Grouped page-browser assets for ${routeFile} are missing groupedBundle metadata after processing. Hydration scripts may be omitted from HTML.`
);
}
groupedAssetsByRoute.set(routeFile, matchedAssets);
}

@@ -145,0 +151,0 @@ return {

@@ -6,2 +6,8 @@ export type EcopagesVirtualImport = {

/**
* @remarks
* Content server modules static-import every MDX entry. They must never become
* browser module-script dependencies discovered from page or component sources.
*/
export declare function isBrowserEcopagesVirtualImport(specifier: string): boolean;
/**
* Extracts runtime `ecopages:` virtual-module imports from a component source file.

@@ -8,0 +14,0 @@ *

import { readFileSync } from "node:fs";
import { parseSync } from "oxc-parser";
const CONTENT_SERVER_VIRTUAL_MODULE_PATTERN = /^ecopages:content\/[a-z][a-z0-9-]+\/server$/;
function isBrowserEcopagesVirtualImport(specifier) {
return specifier.startsWith("ecopages:") && !CONTENT_SERVER_VIRTUAL_MODULE_PATTERN.test(specifier);
}
function extractEcopagesVirtualImports(file) {

@@ -21,3 +25,3 @@ let source;

const specifier = node.source?.value ?? "";
if (!specifier.startsWith("ecopages:")) continue;
if (!isBrowserEcopagesVirtualImport(specifier)) continue;
if (found.get(specifier) === null) {

@@ -57,3 +61,4 @@ continue;

export {
extractEcopagesVirtualImports
extractEcopagesVirtualImports,
isBrowserEcopagesVirtualImport
};
import { normalizeModuleDeclarations } from "../../eco/module-dependencies.js";
import { isBrowserEcopagesVirtualImport } from "./ecopages-virtual-imports.js";
function getDeclaredModules(value) {

@@ -25,2 +26,5 @@ if (!Array.isArray(value)) {

for (const declaration of normalizeModuleDeclarations(getDeclaredModules(declaredModules))) {
if (declaration.from.startsWith("ecopages:") && !isBrowserEcopagesVirtualImport(declaration.from)) {
continue;
}
mergeModuleDeclaration(modulesMap, declaration);

@@ -27,0 +31,0 @@ }

@@ -45,2 +45,3 @@ import type { EcoPagesAppConfig, IHmrManager } from '../../../types/internal-types.js';

processDependencies(deps: AssetDefinition[], key: string): Promise<ProcessedAsset[]>;
private prepareDependenciesForProcessing;
/**

@@ -84,2 +85,4 @@ * Processes deduplicated dependencies grouped by processor type.

private getCachedAsset;
private getCachedContentScriptAsset;
private resolveCachedContentScriptFilepath;
/**

@@ -86,0 +89,0 @@ * Stores one processed asset in the dependency cache.

@@ -7,9 +7,16 @@ import path from "node:path";

import {
ensureGroupedContentScriptsBundle,
partitionGroupedContentScriptDependencies,
processGroupedDependencyBundles
} from "./grouped-content-bundles.js";
import { resolveIntegrationPluginForProcessingKey } from "./resolve-integration-plugin.js";
import { isHmrAware } from "./processor.interface.js";
import { ProcessorRegistry } from "./processor.registry.js";
import { processUngroupedDependency } from "./ungrouped-dependency-processing.js";
import { materializeContentScriptAsset } from "./materialize-content-script-asset.js";
import {
getDevBrowserScriptCacheEntry,
setDevBrowserScriptCacheEntry
} from "../../../build/dev-browser-script-cache.js";
import {
ContentScriptProcessor,

@@ -72,6 +79,12 @@ ContentStylesheetProcessor,

const dedupedDeps = deduplicateAssetDependencies(deps);
const results = await this.processDependenciesParallel(dedupedDeps, key);
const preparedDeps = this.prepareDependenciesForProcessing(dedupedDeps, key);
ensureGroupedContentScriptsBundle(preparedDeps);
const results = await this.processDependenciesParallel(preparedDeps);
await this.optimizeDependencies(results);
return results;
}
prepareDependenciesForProcessing(deps, processingKey) {
const plugin = resolveIntegrationPluginForProcessingKey(this.config, processingKey);
return plugin?.prepareAssetDependencies?.(deps) ?? deps;
}
/**

@@ -85,3 +98,3 @@ * Processes deduplicated dependencies grouped by processor type.

*/
async processDependenciesParallel(deps, key) {
async processDependenciesParallel(deps) {
const grouped = this.groupDependenciesByType(deps);

@@ -93,3 +106,2 @@ const groupPromises = Object.entries(grouped).map(async ([, typeDeps]) => {

dep,
key,
depKey: getAssetDependencyKey(dep),

@@ -116,3 +128,2 @@ getCachedAsset: (assetDep, depKey) => this.getCachedAsset(assetDep, depKey),

bundles: Array.from(groupedBundleDeps.values()),
key,
getCachedAsset: (dep, depKey) => this.getCachedAsset(dep, depKey),

@@ -237,2 +248,5 @@ getDependencyKey: getAssetDependencyKey,

}
if (dep.kind === "script" && dep.source === "content") {
return this.getCachedContentScriptAsset(dep, depKey);
}
const cached = this.cache.get(depKey);

@@ -248,2 +262,22 @@ if (!cached) {

}
getCachedContentScriptAsset(dep, depKey) {
const filepath = this.resolveCachedContentScriptFilepath(depKey) ?? getDevBrowserScriptCacheEntry(this.config, depKey)?.filepath;
if (!filepath) {
return null;
}
const materialized = materializeContentScriptAsset(dep, filepath);
this.cache.set(depKey, { asset: materialized });
return materialized;
}
resolveCachedContentScriptFilepath(depKey) {
const cached = this.cache.get(depKey);
if (!cached?.asset.filepath) {
return void 0;
}
if (!fileSystem.exists(cached.asset.filepath)) {
this.cache.delete(depKey);
return void 0;
}
return cached.asset.filepath;
}
/**

@@ -254,2 +288,5 @@ * Stores one processed asset in the dependency cache.

this.cache.set(depKey, { asset });
if (dep.kind === "script" && dep.source === "content") {
setDevBrowserScriptCacheEntry(this.config, depKey, asset);
}
}

@@ -256,0 +293,0 @@ /**

import type { AssetDefinition, ProcessedAsset } from './assets.types.js';
/** Forces grouped content scripts to run through the bundler in production builds. */
export declare function ensureGroupedContentScriptsBundle(dependencies: AssetDefinition[]): void;
/**

@@ -15,3 +17,2 @@ * Splits grouped content-script dependencies from ordinary dependencies so callers can

bundles: AssetDefinition[][];
key: string;
getCachedAsset: (dep: AssetDefinition, depKey: string) => ProcessedAsset | null;

@@ -18,0 +19,0 @@ getDependencyKey: (dep: AssetDefinition) => string;

@@ -0,1 +1,16 @@

import { isDevelopmentRuntime } from "../../../utils/runtime.js";
import { finalizeProcessedAsset } from "./finalize-processed-asset.js";
function ensureGroupedContentScriptsBundle(dependencies) {
if (isDevelopmentRuntime()) {
return;
}
for (const dependency of dependencies) {
if (dependency.kind !== "script" || dependency.source !== "content" || !dependency.groupedBundle?.id) {
continue;
}
if (dependency.bundle === false) {
dependency.bundle = true;
}
}
}
function partitionGroupedContentScriptDependencies(typeDeps) {

@@ -21,3 +36,2 @@ const groupedBundleDeps = /* @__PURE__ */ new Map();

bundles,
key,
getCachedAsset,

@@ -33,3 +47,3 @@ getDependencyKey,

const cached = getCachedAsset(dep, getDependencyKey(dep));
return cached ? { key, ...cached } : null;
return cached ? finalizeProcessedAsset(cached, resolveProcessedAssetSrcUrl) : null;
});

@@ -48,10 +62,5 @@ if (cachedResults.every((result) => result !== null)) {

const depKey = getDependencyKey(dep);
const srcUrl = resolveProcessedAssetSrcUrl(processed);
const processedWithKey = {
key,
...processed,
srcUrl
};
setCachedAsset(dep, depKey, processedWithKey);
return processedWithKey;
const finalized = finalizeProcessedAsset(processed, resolveProcessedAssetSrcUrl);
setCachedAsset(dep, depKey, finalized);
return finalized;
});

@@ -66,4 +75,5 @@ } catch (error) {

export {
ensureGroupedContentScriptsBundle,
partitionGroupedContentScriptDependencies,
processGroupedDependencyBundles
};
export * from './asset.factory.js';
export * from './grouped-content-bundles.js';
export * from './page-package.js';

@@ -3,0 +4,0 @@ export * from './asset-processing.service.js';

export * from "./asset.factory.js";
export * from "./grouped-content-bundles.js";
export * from "./page-package.js";

@@ -3,0 +4,0 @@ export * from "./asset-processing.service.js";

import type { ContentScriptAsset, ProcessedAsset } from '../../assets.types.js';
import { BaseScriptProcessor } from '../base/base-script-processor.js';
export declare class ContentScriptProcessor extends BaseScriptProcessor<ContentScriptAsset> {
private getContentScriptEntryDir;
private getContentScriptEntryPath;
private createBundleConfigHash;
private createContentScriptCacheKey;
private toProcessedAsset;
private removeContentScriptEntry;
processGrouped(deps: ContentScriptAsset[]): Promise<ProcessedAsset[]>;
private getGroupedBundlerOptions;
process(dep: ContentScriptAsset): Promise<ProcessedAsset>;
}
import path from "node:path";
import { fileSystem } from "@ecopages/file-system";
import { shouldUseDevBrowserScriptCache } from "../../../../../build/dev-browser-script-cache.js";
import { BaseScriptProcessor } from "../base/base-script-processor.js";
class ContentScriptProcessor extends BaseScriptProcessor {
getContentScriptEntryDir() {
const dir = path.join(this.appConfig.absolutePaths.workDir, "content-script-entries");
fileSystem.ensureDir(dir);
return dir;
}
getContentScriptEntryPath(contentHash) {
return path.join(this.getContentScriptEntryDir(), `${contentHash}.js`);
}
createBundleConfigHash(dep, shouldBundle) {
return this.generateHash(
JSON.stringify({
bundle: shouldBundle,
minify: shouldBundle && this.isProduction,
opts: dep.bundleOptions
})
);
}
createContentScriptCacheKey(dep, shouldBundle) {
const contentHash = this.generateHash(dep.content);
const configHash = this.createBundleConfigHash(dep, shouldBundle);
return `${this.buildCacheKey(`content-script:${contentHash}`, contentHash, dep)}:${configHash}`;
}
toProcessedAsset(dep, filepath, inlineContent) {
return {
filepath,
content: dep.inline ? inlineContent : void 0,
kind: "script",
position: dep.position,
attributes: dep.attributes,
inline: dep.inline,
excludeFromHtml: dep.excludeFromHtml,
packageRole: dep.packageRole,
groupedBundle: dep.groupedBundle,
bundledSourceFilepaths: dep.bundledSourceFilepaths
};
}
removeContentScriptEntry(contentHash) {
if (shouldUseDevBrowserScriptCache()) {
return;
}
fileSystem.remove(this.getContentScriptEntryPath(contentHash));
}
async processGrouped(deps) {

@@ -13,23 +56,18 @@ if (deps.length === 0) {

}
const tempDir = path.join(
this.appConfig.absolutePaths.distDir,
`grouped-script-entries-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`
);
fileSystem.ensureDir(tempDir);
let tempEntries = [];
try {
const tempEntries = deps.map((dep, index) => {
const entryName = dep.groupedBundle?.entryName ?? dep.name ?? `grouped-script-${index}`;
const tempFilepath = path.join(tempDir, `${entryName}.js`);
tempEntries = deps.map((dep) => {
const contentHash = this.generateHash(dep.content);
const tempFilepath = this.getContentScriptEntryPath(contentHash);
fileSystem.write(tempFilepath, dep.content);
return {
dep,
entryName,
contentHash,
tempFilepath
};
});
const primaryDep = deps[0];
const outputPaths = await this.bundleScripts({
...this.getBundlerOptions(primaryDep),
entries: tempEntries.map(({ entryName, tempFilepath }) => ({
entryName,
...this.getGroupedBundlerOptions(deps),
entries: tempEntries.map(({ dep, contentHash, tempFilepath }) => ({
entryName: dep.groupedBundle?.entryName ?? dep.name ?? contentHash,
entrypoint: tempFilepath

@@ -41,3 +79,4 @@ })),

});
return tempEntries.map(({ dep, entryName }) => {
return tempEntries.map(({ dep, contentHash }) => {
const entryName = dep.groupedBundle?.entryName ?? dep.name ?? contentHash;
const bundledFilePath = outputPaths.get(entryName);

@@ -47,74 +86,61 @@ if (!bundledFilePath) {

}
return {
filepath: bundledFilePath,
content: dep.inline ? fileSystem.readFileSync(bundledFilePath).toString() : void 0,
kind: "script",
position: dep.position,
attributes: dep.attributes,
inline: dep.inline,
excludeFromHtml: dep.excludeFromHtml,
packageRole: dep.packageRole,
groupedBundle: dep.groupedBundle,
bundledSourceFilepaths: dep.bundledSourceFilepaths
};
return this.toProcessedAsset(
dep,
bundledFilePath,
dep.inline ? fileSystem.readFileSync(bundledFilePath).toString() : void 0
);
});
} finally {
fileSystem.remove(tempDir);
for (const { contentHash } of tempEntries) {
this.removeContentScriptEntry(contentHash);
}
}
}
async process(dep) {
const hash = this.generateHash(dep.content);
const filename = dep.name ? `${dep.name}.js` : `script-${hash}.js`;
const shouldBundle = this.shouldBundle(dep);
const filepath = path.join(this.getAssetsDir(), "scripts", filename);
if (!shouldBundle) {
if (!dep.inline) fileSystem.write(filepath, dep.content);
const unbundledProcessedAsset = {
filepath,
content: dep.inline ? dep.content : void 0,
kind: "script",
position: dep.position,
attributes: dep.attributes,
inline: dep.inline,
excludeFromHtml: dep.excludeFromHtml,
packageRole: dep.packageRole,
groupedBundle: dep.groupedBundle,
bundledSourceFilepaths: dep.bundledSourceFilepaths
getGroupedBundlerOptions(deps) {
const primaryDep = deps[0];
const options = this.getBundlerOptions(primaryDep);
if (deps.some((dep) => dep.bundleOptions?.splitting === false)) {
return {
...options,
splitting: false
};
this.writeCacheFile(filename, unbundledProcessedAsset);
return unbundledProcessedAsset;
}
if (dep.content) {
const tempDir = this.appConfig.absolutePaths.distDir;
fileSystem.ensureDir(tempDir);
const tempFileName = path.join(
tempDir,
`${path.parse(filename).name}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp.js`
);
fileSystem.write(tempFileName, dep.content);
const bundledFilePath = await this.bundleScript({
entrypoint: tempFileName,
outdir: this.getAssetsDir(),
minify: this.isProduction,
naming: `${path.parse(filename).name}-[hash].[ext]`,
...this.getBundlerOptions(dep)
});
const processedAsset = {
filepath: bundledFilePath,
content: dep.inline ? fileSystem.readFileSync(bundledFilePath).toString() : void 0,
kind: "script",
position: dep.position,
attributes: dep.attributes,
inline: dep.inline,
excludeFromHtml: dep.excludeFromHtml,
packageRole: dep.packageRole,
groupedBundle: dep.groupedBundle,
bundledSourceFilepaths: dep.bundledSourceFilepaths
};
fileSystem.remove(tempFileName);
this.writeCacheFile(filename, processedAsset);
return processedAsset;
}
throw new Error("No content found for script asset");
return options;
}
async process(dep) {
const shouldBundle = this.shouldBundle(dep);
const cacheKey = this.createContentScriptCacheKey(dep, shouldBundle);
return this.getOrProcess(cacheKey, async () => {
const hash = this.generateHash(dep.content);
const filename = dep.name ? `${dep.name}.js` : `script-${hash}.js`;
const filepath = path.join(this.getAssetsDir(), "scripts", filename);
if (!shouldBundle) {
if (!dep.inline) {
fileSystem.write(filepath, dep.content);
}
return this.toProcessedAsset(dep, filepath, dep.inline ? dep.content : void 0);
}
if (!dep.content) {
throw new Error("No content found for script asset");
}
const entryPath = this.getContentScriptEntryPath(hash);
fileSystem.write(entryPath, dep.content);
try {
const bundledFilePath = await this.bundleScript({
entrypoint: entryPath,
outdir: this.getAssetsDir(),
minify: this.isProduction,
naming: `${path.parse(filename).name}-[hash].[ext]`,
...this.getBundlerOptions(dep)
});
return this.toProcessedAsset(
dep,
bundledFilePath,
dep.inline ? fileSystem.readFileSync(bundledFilePath).toString() : void 0
);
} finally {
this.removeContentScriptEntry(hash);
}
});
}
}

@@ -121,0 +147,0 @@ export {

@@ -5,3 +5,2 @@ import type { AssetProcessor } from './processor.interface.js';

dep: AssetDefinition;
key: string;
depKey: string;

@@ -8,0 +7,0 @@ getCachedAsset: (dep: AssetDefinition, depKey: string) => ProcessedAsset | null;

import { fileSystem } from "@ecopages/file-system";
import { finalizeProcessedAsset } from "./finalize-processed-asset.js";
async function processUngroupedDependency(options) {
const {
dep,
key,
depKey,

@@ -17,3 +17,3 @@ getCachedAsset,

if (cached) {
return { key, ...cached };
return finalizeProcessedAsset(cached, resolveProcessedAssetSrcUrl);
}

@@ -31,10 +31,5 @@ const processor = getProcessor(dep);

const processed = await processor.process(dep);
const srcUrl = resolveProcessedAssetSrcUrl(processed);
const processedWithKey = {
key,
...processed,
srcUrl
};
setCachedAsset(dep, depKey, processedWithKey);
return processedWithKey;
const finalized = finalizeProcessedAsset(processed, resolveProcessedAssetSrcUrl);
setCachedAsset(dep, depKey, finalized);
return finalized;
} catch (error) {

@@ -41,0 +36,0 @@ logProcessingError(dep, error);

@@ -5,2 +5,5 @@ import { getAppBrowserBuildPlugins, getAppTranspileOptions } from "../../build/build-adapter.js";

import { getAppSourceTransforms } from "../../plugins/source-transform.js";
import { startupTrace } from "../../diagnostics/startup-trace.js";
import { requestBuildDedupe } from "../../diagnostics/request-build-dedupe.js";
import { createBuildOptionsDedupeKey } from "../../build/deduping-build-executor.js";
function resolveBrowserBundleExecutor(appConfig, profile, executor) {

@@ -42,3 +45,8 @@ const buildRuntime = requireBuildRuntime(appConfig);

const buildExecutor = resolveBrowserBundleExecutor(this.appConfig, profile, executor);
return await buildExecutor.build(request);
const dedupeKey = createBuildOptionsDedupeKey(request);
return requestBuildDedupe.dedupeBuild(dedupeKey, async () => {
const result = await buildExecutor.build(request);
startupTrace.recordBrowserBundle(result.outputs);
return result;
});
}

@@ -48,3 +56,3 @@ async bundleGroupedEntries(entries, options) {

...options,
entrypoints: entries.map((entry) => entry.entrypoint)
entrypoints: Object.fromEntries(entries.map((entry) => [entry.entryName, entry.entrypoint]))
};

@@ -51,0 +59,0 @@ return this.bundle(request);