Startseite / Artikel / Eine Astro-App auf Node und Cloudflare Workers: Häufige Konfigurationsfallen

Eine Astro-App auf Node und Cloudflare Workers: Häufige Konfigurationsfallen

Doppelte Astro-Konfigurationen für Node und Worker: React-Deduplizierung, Prisma-Edge-Alias, Vite-Externals, CI-Memory sowie ein Worker-Fetch-Eintrag.

1027 Wörter

Eine Codebasis wird sowohl auf Node (Docker Self-Hosting) als auch auf Cloudflare Workers (Edge) bereitgestellt. Der gemeinsame Codebaum befindet sich neben astro.config.mjs und astro.config.cloudflare.mjs.

Die Einrichtung ist machbar. Um dorthin zu gelangen, waren mehrere gezielte Debugging-Schritte erforderlich, bei denen jeweils eine nur für Cloudflare relevante Einstellung aufgedeckt wurde, die in der Node-Datei nie benötigt wurde. Anfänger-Tutorials dokumentieren solche Unterschiede selten.

Die beiden Konfigurationsdateien sind zu 90 Prozent identisch – und das ist das Problem

Das Erstellen von Unterabteilungen innerhalb einer Konfigurationsdatei erscheint zunächst ordentlich. Es wird jedoch unübersichtlich, wenn man außerdem einen weiteren Adapter, eine weitere externes-Policy, ein weiteres Alias-Map sowie separate Einstellungen für den Speicherverbrauch beim Kompilieren benötigt. Eine lange bedingte Anweisung, die all das abdeckt, liest sich schlechter als zwei separate Dateien.

Der Kompromiss liegt in der Wartung: gemeinsame Einstellungen müssen manuell synchronisiert werden. Die Modulauflösung in React ist ein sensibles Thema, weshalb die Cloudflare-Datei einen Warnkommentar enthält:

// Must mirror astro.config.mjs's React handling. Without dedupe the
// production Rollup client build resolves react-dom's internal react to a
// different chunk than the islands' react, yielding two React instances ->
// "Cannot read properties of null (reading 'useEffect')" when IslandHydrator
// calls createRoot().render() on a hooked component.

Vergessen Sie nicht: Reacts resolve.dedupe sorgt für Korrektheit, aber nicht unbedingt für eine optimale Funktionalität. Doppelte React-Kopien führen bereits beim ersten Hook zu Fehlern, und der Stack zeigt in der Regel auf Ihre Komponente statt auf die Konfiguration des Bundlers.

Prismas generierter Client löst sich nicht für workerd auf

Der Prisma 7 Client nutzt Node-Subpfad-Importe wie #main-entry-point. Eine Rollup-Kompilierung für workerd kann das nicht verarbeiten. Aliasieren Sie das Modul stattdessen direkt zum Edge-Eintrittspunkt:

const PRISMA_CLIENT_DIR = path.dirname(require.resolve('@prisma/client/package.json'));
const PRISMA_EDGE_ENTRY = path.resolve(PRISMA_CLIENT_DIR, '../../.prisma/client/edge.js');
resolve: {
  alias: {
    '.prisma/client/default': PRISMA_EDGE_ENTRY,
  },
}

Es ist wichtig, wie der Pfad ermittelt wird. Ab Prisma 7.8 befinden sich die generierten .prisma/client/-Dateien innerhalb des @prisma/client-Pakets. Unter pnpm wird dies zu einer gehäschten Adresse wie .pnpm/@prisma+client@<hash>/node_modules/.prisma/client/, anstatt zu einem festen node_modules-Verzeichnis. Ein einmal auf einem Laptop eingegebener Pfad funktioniert oft nicht bei einer anderen Layout-Struktur oder Hash-Wert des Stores. Das Verweisen auf @prisma/client/package.json übersteht solche Unterschiede.

Die Liste der externen Abhängigkeiten – und ein bewusst fehlender Eintrag

Alles, was nur für Node benötigt wird, muss außerhalb des workerd-Bundles bleiben. Viele Projekte benötigen nur eine kurze Ausschlussliste:

const NODE_ONLY_EXTERNALS = ['ioredis'];

ioredis wird über eine dynamische import()-Anweisung hinter isCloudflareRuntime() eingebunden, sodass die Workers diesen Codeabschnitt niemals abrufen und seine Externalisierung weiterhin sicher bleibt.

pg befindet sich absichtlich nicht auf dieser Liste. @prisma/adapter-pg lädt pg mittels einer statischen Import-Anweisung ein, und PrismaPg läuft weiterhin auf dem Cloudflare Hyperdrive-Pfad – daher muss der Treiber im Bundle enthalten sein. Wenn nodejs_compat aktiviert ist, nutzt dieser TCP-Client Cloudflare’s Node-Kompatibilitätslayer. Wenn pg stattdessen als extern markiert würde, tritt beim Laden des Workers der Fehler Uncaught Error: No such module „chunks/pg“ auf.

Eine wichtige Regel: betrachten Sie eine Abhängigkeit nur dann als extern, wenn jeder Pfad, der sie importiert, dynamisch und geschützt ist. Ein einziger statischer Import verwandelt ein sauberes Build in einen Absturz nach dem Deploy, den es schwieriger ist zu beheben.

Der Adapter überschreibt Ihre externen Einträge – fügen Sie sie daher erneut hinzu

Dieses Problem war am schwierigsten zu identifizieren. Innerhalb von astro:build:setup zwingt @astrojs/cloudflare vite.ssr.noExternal = true und setzt vite.build.rollupOptions.external auf ['sharp']. Alles, was Sie in ssr.external eingegeben haben, verschwindet bereits vor dem Start von Rollup.

Ein Vite-Plugin mit enforce: 'post', das die externen Einträge wieder zurückschreibt, löst dieses Problem:

{
  name: 'autonnel:cf-extra-externals',
  enforce: 'post',
  config(conf) {
    const existing = conf.build?.rollupOptions?.external;
    if (Array.isArray(existing)) {
      conf.build.rollupOptions.external = [...new Set([...existing, ...NODE_ONLY_EXTERNALS])];
    } else if (typeof existing === 'function') {
      const existingFn = existing;
      conf.build.rollupOptions.external = (id, parentId, isResolved) =>
        NODE_ONLY_EXTERNALS.includes(id) || existingFn(id, parentId, isResolved);
    }
    // ...string / RegExp / undefined branches
  },
}

Es sind mehrere Variablen erforderlich: external kann bereits ein Array, eine Zeichenkette, ein RegExp, eine Funktion oder undefined sein, und zukünftige Adapter-Updates könnten dies erneut ändern. Die Anmerkung neben dem Plugin besagt, dass es verschwinden sollte, sobald @astrojs/cloudflare aufhört, ssr.external zu überschreiben – es handelt sich dabei um einen vorübergehenden Patch für das Verhalten einer bestimmten Version.

Beim Build ist in CI der Speicher ausgegangen, nicht lokal

Durch das Zusammenfassen aller SSR-Einträge in ein einziges workerd-Bundle wurde der Standard-Heap von Node von 2 GB überschritten. Der CI-Server bei Cloudflare stürzte bereits bei etwa 1,99 GB ab, obwohl ein Entwickler-Notebook problemlos arbeitete – das ist eine unangenehme Art von Fehler.

Zwei Vite-Flags beseitigten diesen Druck:

build: {
  sourcemap: false,
  reportCompressedSize: false,
},

Quellkarten beanspruchen viel Speicher, und Worker ignorieren sie. reportCompressedSize erstellt außerdem eine komprimierte Kopie jedes Datensatzes, nur um eine ansprechendere Zusammenfassungstabelle auszugeben. Beides lohnt sich auf diesem Ziel nicht.

Der Worker erledigt zwei Dinge, die Node nicht benötigt

Node stellt Ihnen ein Request-Lebenszyklus-Modell kostenlos zur Verfügung. Bei Workers müssen Sie dieses selbst implementieren:

export default {
  async fetch(request, env, ctx) {
    setRuntimeEnv(env);
    return runWithRequestDb(async () => {
      try {
        return await ssrHandler.fetch(request, env, ctx);
      } finally {
        ctx.waitUntil(disposeRequestDb());
      }
    });
  },
  async scheduled(_event, env) { /* ... */ },
};

setRuntimeEnv(env) existiert, weil es bei Workers kein process.env gibt. Bindings erscheinen als Argumente der Handler, weshalb ein Zugriff auf Konfigurationen einen pro-Request-Brückenmechanismus erfordert. Beim Portieren einer Node-Dienstleistung müssen hier in der Regel viele Dateien angepasst werden; die frühzeitige Einrichtung dieser Brücke ist besser als das Später-Suchen nach verstreuten process.env.FOO-Lesevorgängen.

ctx.waitUntil(disposeRequestDb()) kümmert sich um die Aufräumarbeiten: Der Datenbankklient wird erst nachdem die Antwort gesendet wurde freigegeben. Ein früheres Aufräumen könnte einen Client bereinigen, auf den die Stream-Ausgabe noch angewiesen ist.

Lohnt es sich, zweifache Ziele zu verwenden?

Ja – vorausgesetzt, das zweite Ziel hat eine klare Aufgabe. Workers ist kein kostenloses „überall bereitstellen“-Feature. Es fügt einen weiteren Build-Pipeline mit unterschiedlichen Fehlermodi hinzu, wobei die meisten Probleme beim Bereitstellen auftreten und nicht in den Tests.

Die Arbeit bleibt überschaubar, wenn die Abweichungen begrenzt werden: Zwei Konfigurationen plus ein Eingangsmodul. Der Domain-Code sollte vermeiden, if (isWorkers) zu verwenden, sobald Cache, Speicher und Datenbank bereits über Adapter verfügbar sind. Fehlen diese Verbindungen? Erstellen Sie sie vor dem Hinzufügen des zweiten Laufzeitumfelds. Das Umgekehrte führt dazu, dass Laufzeitprüfungen in Checkout-Flüsse und andere Kerndienste eingebettet werden.