Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/expose-trailing-slash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"emdash": minor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[needs fixing] This changeset only lists emdash, but the PR also changes two other published packages: @emdash-cms/cloudflare and @emdash-cms/sandbox-workerd. Their sandbox runners/wrappers now forward trailingSlash into generated worker code (e.g. WrapperOptions.site and the runner siteInfo fields are widened), which is a real public-package behavior change.

CONTRIBUTING.md says a changeset is required for every published package whose behavior changes; otherwise the change won't trigger a release.

Add the other affected packages to this changeset so the coordinated release is captured:

Suggested change
"emdash": minor
---
"emdash": minor
"@emdash-cms/cloudflare": minor
"@emdash-cms/sandbox-workerd": minor
---
Expose the host Astro project's `trailingSlash` config to plugins via `ctx.site.trailingSlash`, so plugins that build absolute URLs (sitemaps, canonical, hreflang) can match the site's routing policy. Available to in-process and sandboxed plugins alike.

"@emdash-cms/cloudflare": minor
"@emdash-cms/sandbox-workerd": minor
---

Expose the host Astro project's `trailingSlash` config to plugins via `ctx.site.trailingSlash`, so plugins that build absolute URLs (sitemaps, canonical, hreflang) can match the site's routing policy. Available to in-process and sandboxed plugins alike.
21 changes: 18 additions & 3 deletions packages/cloudflare/src/sandbox/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,12 @@ export class CloudflareSandboxRunner implements SandboxRunner {
private plugins = new Map<string, CloudflareSandboxedPlugin>();
private options: SandboxOptions;
private resolvedLimits: ResolvedLimits;
private siteInfo?: { name: string; url: string; locale: string };
private siteInfo?: {
name: string;
url: string;
locale: string;
trailingSlash?: "always" | "never" | "ignore";
};

constructor(options: SandboxOptions) {
this.options = options;
Expand Down Expand Up @@ -203,15 +208,25 @@ class CloudflareSandboxedPlugin implements SandboxedPluginInstance {
private code: string;
private wrapperCode: string | null = null;
private limits: ResolvedLimits;
private siteInfo?: { name: string; url: string; locale: string };
private siteInfo?: {
name: string;
url: string;
locale: string;
trailingSlash?: "always" | "never" | "ignore";
};

constructor(
manifest: PluginManifest,
code: string,
loader: WorkerLoader,
createBridge: (opts: { props: PluginBridgeProps }) => PluginBridgeBinding,
limits: ResolvedLimits,
siteInfo?: { name: string; url: string; locale: string },
siteInfo?: {
name: string;
url: string;
locale: string;
trailingSlash?: "always" | "never" | "ignore";
},
) {
this.id = `${manifest.id}:${manifest.version}`;
this.manifest = manifest;
Expand Down
7 changes: 6 additions & 1 deletion packages/cloudflare/src/sandbox/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ const COMMENT_CLOSE_RE = /\*\//g;
*/
export interface WrapperOptions {
/** Site info to inject into the context (no RPC needed) */
site?: { name: string; url: string; locale: string };
site?: {
name: string;
url: string;
locale: string;
trailingSlash?: "always" | "never" | "ignore";
};
}

export function generatePluginWrapper(manifest: PluginManifest, options?: WrapperOptions): string {
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/astro/integration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,9 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
// EmDashHead must not access Astro.csp when the host has disabled
// Astro's built-in CSP runtime; Astro logs a warning for that access.
serializableConfig.astroCspEnabled = Boolean(astroConfig.security.csp);
// Expose Astro's trailingSlash routing policy so plugins can build
// URLs (sitemap/canonical/hreflang) that match what the site serves.
serializableConfig.trailingSlash = astroConfig.trailingSlash;
// Extract i18n config from Astro config
// Astro locales can be strings OR { path, codes } objects — normalize to paths
if (astroConfig.i18n) {
Expand Down
41 changes: 36 additions & 5 deletions packages/core/src/emdash-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,12 @@ export interface EmDashRuntimeParts {
db: Kysely<Database>;
getDb?: () => Kysely<Database>;
storage?: Storage;
siteInfo?: { siteName?: string; siteUrl?: string; locale?: string };
siteInfo?: {
siteName?: string;
siteUrl?: string;
locale?: string;
trailingSlash?: "always" | "never" | "ignore";
};
};
runtimeDeps: RuntimeDependencies;
pipelineRef: { current: HookPipeline };
Expand Down Expand Up @@ -534,7 +539,12 @@ export class EmDashRuntime {
db: Kysely<Database>;
getDb?: () => Kysely<Database>;
storage?: Storage;
siteInfo?: { siteName?: string; siteUrl?: string; locale?: string };
siteInfo?: {
siteName?: string;
siteUrl?: string;
locale?: string;
trailingSlash?: "always" | "never" | "ignore";
};
};
/** Dependencies needed for exclusive hook resolution */
private runtimeDeps: RuntimeDependencies;
Expand Down Expand Up @@ -1106,7 +1116,14 @@ export class EmDashRuntime {
const storage = EmDashRuntime.getStorage(deps);

let pluginStates: Map<string, string> = new Map();
let siteInfo: { siteName?: string; siteUrl?: string; locale?: string } | undefined;
let siteInfo:
| {
siteName?: string;
siteUrl?: string;
locale?: string;
trailingSlash?: "always" | "never" | "ignore";
}
| undefined;
// "Already set up" by default so a read failure (e.g. tables absent on a
// pre-migration db) skips seeding rather than seeding a half-built db.
let seedGate = { collectionCount: 1, setupDone: true };
Expand Down Expand Up @@ -1149,6 +1166,10 @@ export class EmDashRuntime {
siteName: siteOpts.get("emdash:site_title") ?? undefined,
siteUrl: siteOpts.get("emdash:site_url") ?? undefined,
locale: siteOpts.get("emdash:locale") ?? undefined,
// trailingSlash is a build-time Astro routing decision, not a
// user-editable setting, so it comes from the Astro config
// (virtual:emdash/config), not the options table.
trailingSlash: virtualConfig?.trailingSlash,
};
};

Expand Down Expand Up @@ -1831,7 +1852,12 @@ export class EmDashRuntime {
deps: RuntimeDependencies,
db: Kysely<Database>,
mediaStorage?: Storage | null,
siteInfo?: { siteName?: string; siteUrl?: string; locale?: string },
siteInfo?: {
siteName?: string;
siteUrl?: string;
locale?: string;
trailingSlash?: "always" | "never" | "ignore";
},
): Promise<Map<string, SandboxedPluginInstance>> {
// Return cached plugins if already loaded
if (sandboxedPluginCache.size > 0) {
Expand Down Expand Up @@ -1946,7 +1972,12 @@ export class EmDashRuntime {
storage: Storage,
deps: RuntimeDependencies,
cache: Map<string, SandboxedPluginInstance>,
siteInfo?: { siteName?: string; siteUrl?: string; locale?: string },
siteInfo?: {
siteName?: string;
siteUrl?: string;
locale?: string;
trailingSlash?: "always" | "never" | "ignore";
},
): Promise<void> {
// Ensure sandbox runner exists with media storage wired up.
// (storage here is the media Storage adapter from the runtime.)
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/plugins/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -882,6 +882,8 @@ export interface SiteInfoOptions {
siteUrl?: string;
/** Site locale from options table */
locale?: string;
/** Astro's `trailingSlash` config (from `virtual:emdash/config`). */
trailingSlash?: "always" | "never" | "ignore";
}

/**
Expand All @@ -897,6 +899,7 @@ export function createSiteInfo(options: SiteInfoOptions): SiteInfo {
name: options.siteName ?? "",
url: (options.siteUrl ?? "").replace(TRAILING_SLASH_RE, ""), // strip trailing slash
locale: options.locale ?? "en",
trailingSlash: options.trailingSlash ?? "ignore", // Astro's default
};
}

Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/plugins/sandbox/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,12 @@ export interface SandboxOptions {
/** Default resource limits */
limits?: ResourceLimits;
/** Site info for plugin context (injected into wrapper at generation time) */
siteInfo?: { name: string; url: string; locale: string };
siteInfo?: {
name: string;
url: string;
locale: string;
trailingSlash?: "always" | "never" | "ignore";
};
/** Email send callback, wired from the EmailPipeline by the runtime */
emailSend?: SandboxEmailSendCallback;
/**
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/plugins/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,14 @@ export interface SiteInfo {
url: string;
/** Site locale (from settings, defaults to "en") */
locale: string;
/**
* Astro's `trailingSlash` routing policy, from the host's Astro config.
* Plugins that build absolute URLs (sitemap, canonical, hreflang) should
* honor this so the URLs they emit match what the site serves. `createSiteInfo`
* always populates it (defaulting to `"ignore"`, Astro's default); it is
* optional on the type so pre-existing `SiteInfo` construction stays valid.
*/
trailingSlash?: "always" | "never" | "ignore";
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/virtual-modules.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ declare module "virtual:emdash/config" {
/** Public origin from astro.config.mjs, origin-normalized at startup. */
siteUrl?: string;
astroCspEnabled?: boolean;
trailingSlash?: "always" | "never" | "ignore";
}

const config: VirtualConfig;
Expand Down
3 changes: 3 additions & 0 deletions packages/core/tests/integration/plugins/capabilities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -999,11 +999,13 @@ describe("Capability Enforcement Integration (v2)", () => {
siteName: "My Site",
siteUrl: "https://example.com/",
locale: "fr",
trailingSlash: "never",
});

expect(info.name).toBe("My Site");
expect(info.url).toBe("https://example.com"); // trailing slash stripped
expect(info.locale).toBe("fr");
expect(info.trailingSlash).toBe("never");
});

it("uses defaults for missing values", () => {
Expand All @@ -1012,6 +1014,7 @@ describe("Capability Enforcement Integration (v2)", () => {
expect(info.name).toBe("");
expect(info.url).toBe("");
expect(info.locale).toBe("en");
expect(info.trailingSlash).toBe("ignore"); // Astro's default
});

it("strips trailing slash from URL", () => {
Expand Down
1 change: 1 addition & 0 deletions packages/core/tests/integration/runtime/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ describe("EmDashRuntime.create — cold boot", () => {
name: "Example Site",
url: "https://example.com",
locale: "nl",
trailingSlash: "ignore",
},
}),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ describe("EmDashRuntime.handlePluginApiRoute site context", () => {
name: "Example Site",
url: "https://example.com",
locale: "nl",
trailingSlash: "ignore",
},
url: "https://example.com/checkout/success",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ describe("createSandboxRunnerOptions", () => {
name: "Example Site",
url: "https://example.com",
locale: "nl",
trailingSlash: "ignore",
},
});
});
Expand All @@ -33,6 +34,6 @@ describe("createSandboxRunnerOptions", () => {

expect(options.db).toBe(db);
expect(options.mediaStorage).toBe(mediaStorage);
expect(options.siteInfo).toEqual({ name: "", url: "", locale: "en" });
expect(options.siteInfo).toEqual({ name: "", url: "", locale: "en", trailingSlash: "ignore" });
});
});
7 changes: 6 additions & 1 deletion packages/workerd/src/sandbox/dev-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ const EMDASH_SHIM = "export const definePlugin = (d) => d;\n";
*/
export class MiniflareDevRunner implements SandboxRunner {
private options: SandboxOptions;
private siteInfo?: { name: string; url: string; locale: string };
private siteInfo?: {
name: string;
url: string;
locale: string;
trailingSlash?: "always" | "never" | "ignore";
};
private emailSendCallback: SandboxEmailSendCallback | null = null;

/** Miniflare instance (lazily created) */
Expand Down
7 changes: 6 additions & 1 deletion packages/workerd/src/sandbox/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,12 @@ function isTokenClaims(value: unknown): value is PluginTokenClaims {
export class WorkerdSandboxRunner implements SandboxRunner {
private options: SandboxOptions;
private limits: ResolvedLimits;
private siteInfo?: { name: string; url: string; locale: string };
private siteInfo?: {
name: string;
url: string;
locale: string;
trailingSlash?: "always" | "never" | "ignore";
};

/** Loaded plugins indexed by pluginId (manifest.id:manifest.version) */
private plugins = new Map<string, LoadedPlugin>();
Expand Down
7 changes: 6 additions & 1 deletion packages/workerd/src/sandbox/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ const NEWLINE_RE = /[\n\r]/g;
const COMMENT_CLOSE_RE = /\*\//g;

export interface WrapperOptions {
site?: { name: string; url: string; locale: string };
site?: {
name: string;
url: string;
locale: string;
trailingSlash?: "always" | "never" | "ignore";
};
/** URL of the Node backing service (e.g., http://127.0.0.1:18787) */
backingServiceUrl: string;
/** Auth token the plugin sends on outbound bridge calls to Node */
Expand Down
Loading