Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/registry-dev-output-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"emdash": patch
---

Fixes `astro build --devOutput` for sites whose `registry` points at a plain-HTTP localhost aggregator. The build no longer fails with "must use HTTPS outside development"; a development-output build now accepts the same localhost registry URLs as `astro dev`. Production builds still reject them.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,8 @@ jobs:
id: node-e2e
if: needs.change-plan.outputs.e2e_node == 'true'
run: pnpm exec playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
env:
EMDASH_E2E_BUILD: production
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: failure() && steps.node-e2e.outcome == 'failure'
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ uploads/
!packages/core/tests/integration/fixture/.emdash/
!e2e/fixture/.emdash/
!e2e/fixture-cloudflare/.emdash/
e2e/fixture*/.emdash/migrations.json
!templates/*/.emdash/seed.json

# macOS-specific files
Expand Down
3 changes: 3 additions & 0 deletions e2e/fixture/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ export default defineConfig({
defaultLocale: "en",
locales: ["en", "fr", "es"],
fallback: { fr: "en", es: "en" },
// The fixture serves locales from its own `[locale]` routes. Automatic
// routing makes production builds add fallback routes that shadow them.
routing: "manual",
},
devToolbar: { enabled: false },
vite: {
Expand Down
3 changes: 2 additions & 1 deletion e2e/fixture/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"astro": "catalog:",
"emdash": "workspace:*",
"react": "catalog:",
"react-dom": "catalog:"
"react-dom": "catalog:",
"sharp": "^0.35.4"
}
}
5 changes: 5 additions & 0 deletions e2e/fixture/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { defineMiddleware } from "astro:middleware";

// Manual i18n routing requires a middleware file; the fixture's `[locale]`
// routes handle locale prefixes themselves.
export const onRequest = defineMiddleware((_context, next) => next());
10 changes: 10 additions & 0 deletions e2e/fixture/src/outer-middleware.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { readFileSync } from "node:fs";

import { defineMiddleware } from "astro:middleware";
import { installRegistryAuthoritativeFixture } from "emdash/internal/testing/registry";
import { getRequestContext } from "emdash/request-context";

// A built server never runs astro.config.mjs, which installs the fixture for
// `astro dev`, so install it here at startup too.
const registryFixturePath = process.env.EMDASH_REGISTRY_FIXTURE;
if (registryFixturePath) {
installRegistryAuthoritativeFixture(JSON.parse(readFileSync(registryFixturePath, "utf8")));
}

const FINALIZER_NONCE = "outer-finalizer";

function preNextState(locals: App.Locals): string {
Expand Down
114 changes: 81 additions & 33 deletions e2e/global-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import { execFile, spawn } from "node:child_process";
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
Expand Down Expand Up @@ -46,6 +46,11 @@ const TARGETS: Record<string, Target> = {
};

const TARGET = TARGETS[process.env.EMDASH_E2E_TARGET ?? "node"] ?? TARGETS.node!;
/**
* `EMDASH_E2E_BUILD=production` builds the fixture with `astro build --devOutput`
* and serves the built output instead of running `astro dev`. Node target only.
*/
const PRODUCTION_BUILD = process.env.EMDASH_E2E_BUILD === "production";
const FIXTURE_DIR = TARGET.fixtureDir;
const CLI_BINARY = resolve(ROOT, "packages/core/dist/cli/index.mjs");
const PORT = 4444;
Expand Down Expand Up @@ -336,7 +341,19 @@ export default async function globalSetup(): Promise<void> {
// the database — source files stay at their real paths so Astro's virtual
// module resolver can find the compile metadata.
const workDir = FIXTURE_DIR;
const tempDataDir = mkdtempSync(join(tmpdir(), "emdash-pw-"));
// A production build bakes the database path into its output, so it needs a
// path that stays the same between runs.
let tempDataDir: string;
if (PRODUCTION_BUILD) {
if (TARGET !== TARGETS.node) {
throw new Error("EMDASH_E2E_BUILD=production only supports the node target");
}
tempDataDir = join(tmpdir(), "emdash-pw-production");
rmSync(tempDataDir, { recursive: true, force: true });
mkdirSync(tempDataDir, { recursive: true });
} else {
tempDataDir = mkdtempSync(join(tmpdir(), "emdash-pw-"));
}
const dbPath = join(tempDataDir, "test.db");
const registryFixturePath = join(tempDataDir, "registry-fixture.json");
writeFileSync(registryFixturePath, JSON.stringify(registryFixture));
Expand All @@ -352,22 +369,49 @@ export default async function globalSetup(): Promise<void> {
rmSync(join(FIXTURE_DIR, ".wrangler"), { recursive: true, force: true });
}

// 2. Start dev server (with marketplace URL injected via env)
// 2. Start the server (with marketplace URL injected via env)
const astroBin = join(fixtureNodeModules, ".bin", "astro");
const server = spawn(astroBin, ["dev", "--port", String(PORT)], {
cwd: workDir,
env: {
...process.env,
const fixtureEnv = {
...process.env,
EMDASH_TEST_DB: `file:${dbPath}`,
EMDASH_MARKETPLACE_URL: marketplaceUrl,
EMDASH_REGISTRY_URL: marketplaceUrl,
EMDASH_REGISTRY_FIXTURE: registryFixturePath,
EMDASH_ENCRYPTION_KEY: "emdash_enc_v1_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
};
let server;
if (PRODUCTION_BUILD) {
Object.assign(fixtureEnv, {
// The build derives `image.remotePatterns` for EmDash media from the site URL.
EMDASH_SITE_URL: baseUrl,
});
console.log("[pw] Building fixture...");
const buildStart = Date.now();
// `--devOutput` keeps `import.meta.env.DEV` true, which the dev-bypass
// endpoints the suite logs in through require.
await execAsync(astroBin, ["build", "--devOutput"], {
cwd: workDir,
env: fixtureEnv,
timeout: 300_000,
});
console.log(`[pw] Fixture built in ${((Date.now() - buildStart) / 1000).toFixed(1)}s.`);
server = spawn(process.execPath, [join(workDir, "dist/server/entry.mjs")], {
cwd: workDir,
env: {
...fixtureEnv,
HOST: "localhost",
PORT: String(PORT),
},
stdio: "pipe",
});
} else {
server = spawn(astroBin, ["dev", "--port", String(PORT)], {
cwd: workDir,
// Keep Astro's agent-mode server in this process so teardown owns it.
ASTRO_DEV_BACKGROUND: "1",
EMDASH_TEST_DB: `file:${dbPath}`,
EMDASH_MARKETPLACE_URL: marketplaceUrl,
EMDASH_REGISTRY_URL: marketplaceUrl,
EMDASH_REGISTRY_FIXTURE: registryFixturePath,
EMDASH_ENCRYPTION_KEY: "emdash_enc_v1_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
},
stdio: "pipe",
});
env: { ...fixtureEnv, ASTRO_DEV_BACKGROUND: "1" },
stdio: "pipe",
});
}

let serverOutput = "";
const appendServerOutput = (data: Buffer) => {
Expand Down Expand Up @@ -411,25 +455,29 @@ export default async function globalSetup(): Promise<void> {
console.log("[pw] Seeding test data...");
const seed = await seedTestData(baseUrl, token);

// 5b. Warm up pages that use emdash/ui (triggers Astro compilation of all
// component virtual modules, avoiding race conditions in tests)
console.log("[pw] Warming up pages...");
for (let attempt = 0; attempt < 3; attempt++) {
try {
const resp = await fetch(`${baseUrl}/posts/post-with-image`);
if (resp.ok) break;
// Retry on compilation errors — Astro may need multiple passes
await new Promise((r) => setTimeout(r, 1000));
} catch {
await new Promise((r) => setTimeout(r, 1000));
// A production build compiles everything ahead of time, so only the dev
// server needs warming up.
if (!PRODUCTION_BUILD) {
// 5b. Warm up pages that use emdash/ui (triggers Astro compilation of all
// component virtual modules, avoiding race conditions in tests)
console.log("[pw] Warming up pages...");
for (let attempt = 0; attempt < 3; attempt++) {
try {
const resp = await fetch(`${baseUrl}/posts/post-with-image`);
if (resp.ok) break;
// Retry on compilation errors — Astro may need multiple passes
await new Promise((r) => setTimeout(r, 1000));
} catch {
await new Promise((r) => setTimeout(r, 1000));
}
}
}

// 5c. Load the admin in a browser. Its client dependencies are only
// discovered when a browser requests them, and the optimizer then forces
// a full reload that would otherwise land inside the first admin test.
console.log("[pw] Warming up admin...");
await warmUpAdmin(baseUrl);
// 5c. Load the admin in a browser. Its client dependencies are only
// discovered when a browser requests them, and the optimizer then forces
// a full reload that would otherwise land inside the first admin test.
console.log("[pw] Warming up admin...");
await warmUpAdmin(baseUrl);
}

// 6. Write server info
const info = {
Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/editor-image-panel.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test, expect } from "../fixtures";

const EXTERNAL_IMAGE_URL = "http://media.example.test/editor-panel.png";
const EXTERNAL_IMAGE_URL = "https://media.example.test/editor-panel.png";

test.describe("Editor image panel", () => {
test.beforeEach(async ({ admin }) => {
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/astro/integration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,10 @@ export function emdash(config: EmDashConfig = {}): AstroIntegration {
astroCommand = command;
usesCloudflareAdapter = astroConfig.adapter?.name === "@astrojs/cloudflare";
normalizeRegistryConfig(registry, {
allowLocalhost: command === "dev" || command === "sync",
// `astro build --devOutput` sets NODE_ENV before loading the config and
// produces a build whose runtime registry check allows localhost.
allowLocalhost:
command === "dev" || command === "sync" || process.env.NODE_ENV === "development",
});
printBanner(logger);
if (command === "dev") {
Expand Down
4 changes: 3 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading