@contentrain/emitter-astro 0.16.1 → 0.17.0

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/README.md CHANGED
@@ -253,6 +253,8 @@ Each path is written percent-encoded, with and without its trailing slash.
253
253
  `*` or a `:` segment is a pattern to a host file; it is left to the meta-refresh
254
254
  fallback, with a warning. A rule that differs from a built page only in letter case is returned too: Netlify matches rules case-insensitively, and macOS and Windows file systems cannot hold `/About/` beside `/about/`, so it would be served over the page. Cloudflare Pages and Vercel files stop at 1,000 rules (2,000 entries with both slash forms, their static limit); the rest are named in `EmitResult.redirects.host_over_limit`, with a warning, and keep only the meta-refresh fallback there. Netlify's file has no limit.
255
255
 
256
+ **Host-only rules.** `input.hostRedirects` takes rules served only by the host's redirect file, never by `astro.config`, so the build writes no meta-refresh page for them. It is for bulk rules a site does not need as pages: one per WordPress attachment page runs to tens of thousands, and each would become an HTML file in `dist`. They pass the checks `redirects` do (a built page, a case twin, a query string, a pattern, a duplicate) — except the file-name one, since a host file matches `/old.php` itself; but never a file the build writes (robots, `llms.txt`, a feed, `sitemap-index.xml`/`sitemap-N.xml`, `404.html`, `/_astro/`, `/styles/`; in either slash form and any letter case), which a host file would shadow — and an address the site's own rules or the feed redirects already hold is theirs. In a Cloudflare or Vercel file they come after both, so at the rule limit they are the ones left out. With no fallback page behind them, a host-only rule the host file cannot hold is not served at all: a pattern, or one past a named host's limit (`options.redirectHost` `cloudflare` or `vercel`), comes back in `EmitResult.redirects.manual` with its reason, with one `hostRedirects:` warning. With no host named, the Netlify file holds every rule, so one past `vercel.json`'s limit stays written and is named in `host_over_limit` with its own warning — not served on Vercel; name the host to have it reported as manual. On a host with no redirect file (a plain static server) host-only rules are not served.
257
+
256
258
 
257
259
  ## Route collisions
258
260
 
@@ -280,3 +282,7 @@ collection: Astro serves one file per path, so one of the two routes does not
280
282
  exist in the built site, and which one survived is an accident of ordering. Fix
281
283
  it by giving one route a distinct pattern, or by expanding the narrower one into
282
284
  literal routes.
285
+
286
+ ## Checking the emitted types
287
+
288
+ The unit suite asserts what the emitter writes; `pnpm --filter @contentrain/emitter-astro test:astro` (after `pnpm build`) checks it the way a migrated site's build does. It emits a site with every kind of page and endpoint — posts, pages, nested category and tag archives with their feeds, an author profile, a list, a static page, the feed, llms.txt, redirects — once with trailing slashes and once without, installs it with npm from its own `package.json`, and runs `astro check`. A pass is a result naming 0 errors. CI and the release workflow run it on every change.
package/dist/index.d.mts CHANGED
@@ -364,6 +364,18 @@ interface EmitInput {
364
364
  * back in `EmitResult.redirects.manual` with the reason, to set up at the host.
365
365
  */
366
366
  redirects?: RawRedirect[];
367
+ /**
368
+ * Rules served only by the host's redirect file (`public/_redirects`,
369
+ * `vercel.json`), never by `astro.config` — so no meta-refresh page is built
370
+ * for them. For bulk rules the site does not need as pages: one per
371
+ * WordPress attachment page runs to tens of thousands, and a static build
372
+ * would write an HTML file for each. They pass the same checks as
373
+ * `redirects`, and the site's own rules and the feed redirects win an
374
+ * address they share. Without a fallback page, a rule the host file cannot
375
+ * hold — a pattern, or past Cloudflare's or Vercel's rule limit, where these
376
+ * go last — is returned in `EmitResult.redirects.manual`.
377
+ */
378
+ hostRedirects?: RawRedirect[];
367
379
  }
368
380
  /**
369
381
  * Pure result: every generated file as path → content. Writing to disk is a
@@ -373,14 +385,20 @@ interface EmitInput {
373
385
  interface EmitResult {
374
386
  files: Record<string, string>;
375
387
  warnings: string[];
376
- /** Present when `EmitInput.redirects` was given: what was written and what was not, with why. */
388
+ /** Present when `EmitInput.redirects` or `hostRedirects` was given. */
377
389
  redirects?: {
378
- written: RawRedirect[];
390
+ /** Rules written: `redirects` to astro.config and the host files, `hostRedirects` to the host files. */written: RawRedirect[]; /** Rules not written, with why — to set up by hand at the host. */
379
391
  manual: Array<{
380
392
  redirect: RawRedirect;
381
393
  reason: string;
382
394
  }>; /** The host redirect files written, e.g. `public/_redirects (netlify)`. Empty: meta-refresh only. */
383
- host_files: string[]; /** Rules (by `from`) left out of a Cloudflare / Vercel file at its rule limit: meta-refresh only there. */
395
+ host_files: string[];
396
+ /**
397
+ * Rules (by `from`) left out of a Cloudflare / Vercel file at its rule
398
+ * limit. A `redirects` rule keeps its meta-refresh page there. A
399
+ * `hostRedirects` rule is here only with no host named (the Netlify file
400
+ * holds it, `vercel.json` does not); past a named host's limit it is in `manual`.
401
+ */
384
402
  host_over_limit: string[];
385
403
  };
386
404
  }
@@ -665,9 +683,13 @@ interface RedirectPlan {
665
683
  declare function builtAddresses(routes: RouteModel[], content: EmitContent): Map<string, string>;
666
684
  /**
667
685
  * Split the rules into what `astro.config` serves and what the host has to.
668
- * `built` is `builtAddresses` of the emitted routes.
686
+ * `built` is `builtAddresses` of the emitted routes. `hostOnly`: the rules go
687
+ * to host files alone (`EmitInput.hostRedirects`), so the checks that hold
688
+ * only for a static redirect page (a `from` naming a file) are left out.
669
689
  */
670
- declare function planRedirects(redirects: RawRedirect$1[], built: Map<string, string>): RedirectPlan;
690
+ declare function planRedirects(redirects: RawRedirect$1[], built: Map<string, string>, options?: {
691
+ hostOnly?: boolean;
692
+ }): RedirectPlan;
671
693
  /** The `redirects` line for astro.config.mjs, or null when there is nothing to write. */
672
694
  declare function astroRedirectsConfig(config: RedirectPlan['config']): string | null;
673
695
  /** Hosts whose own redirect file the emitter can write. */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/css.ts","../src/feed.ts","../src/util.ts","../src/layouts.ts","../src/components.ts","../src/seo.ts","../src/seo-entry.ts","../src/alternates.ts","../src/embed.ts","../src/ui-strings.ts","../src/balance.ts","../src/noindex.ts","../src/redirects.ts","../src/index.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/css.ts","../src/feed.ts","../src/util.ts","../src/layouts.ts","../src/components.ts","../src/seo.ts","../src/seo-entry.ts","../src/alternates.ts","../src/embed.ts","../src/ui-strings.ts","../src/balance.ts","../src/noindex.ts","../src/redirects.ts","../src/index.ts"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -3795,7 +3795,7 @@ function builtAddresses(routes, content) {
3795
3795
  return out;
3796
3796
  }
3797
3797
  /** The decoded path for a literal Astro redirect key, or why `from` cannot be one. */
3798
- function checkFrom(from) {
3798
+ function checkFrom(from, hostOnly = false) {
3799
3799
  if (!from.startsWith("/") || from.startsWith("//")) return { reason: "from is not a site-root path" };
3800
3800
  if (/[?#]/.test(from)) return { reason: "from has a query string or fragment — a static redirect matches the path only" };
3801
3801
  let path;
@@ -3808,9 +3808,24 @@ function checkFrom(from) {
3808
3808
  if (/[[\]]/.test(path)) return { reason: "from contains [ or ], which Astro reads as a route parameter" };
3809
3809
  const segments = path.split("/").filter(Boolean);
3810
3810
  if (segments.some((s) => s === "." || s === "..")) return { reason: "from contains a . or .. segment" };
3811
- if (/\.[a-z0-9]{1,5}$/i.test(segments.at(-1) ?? "")) return { reason: "from ends in a file name — the static build serves it as a directory, not at this address" };
3811
+ if (!hostOnly && /\.[a-z0-9]{1,5}$/i.test(segments.at(-1) ?? "")) return { reason: "from ends in a file name — the static build serves it as a directory, not at this address" };
3812
3812
  return { path };
3813
3813
  }
3814
+ /**
3815
+ * A path the build writes a file at: the emitter's own endpoints (robots,
3816
+ * llms.txt, every feed, @astrojs/sitemap's files), the 404 page, and the
3817
+ * directories of Astro's bundles and the legacy stylesheets. A host file
3818
+ * answers before the filesystem — a forced Netlify rule and every vercel.json
3819
+ * redirect — so a host-only rule here would shadow the file. `builtAddresses`
3820
+ * knows pages only. Compared without a trailing slash (a host file gets both
3821
+ * forms) and in lower case (Netlify matches rules case-insensitively).
3822
+ * WordPress's own `/sitemap.xml` and `/sitemap_index.xml` are not the build's:
3823
+ * a 301 from them is one a migration wants.
3824
+ */
3825
+ function buildOutputFile(path) {
3826
+ const p = (path.replace(/\/+$/, "") || "/").toLowerCase();
3827
+ return p === "/robots.txt" || p === "/llms.txt" || p === "/404.html" || p.endsWith("/feed.xml") || /^\/sitemap-(?:index|\d+)\.xml$/.test(p) || /^\/(?:_astro|styles)(?:\/|$)/.test(p);
3828
+ }
3814
3829
  function checkTo(to) {
3815
3830
  if (hasControlOrSpace(to)) return false;
3816
3831
  if (to.startsWith("/")) return !to.startsWith("//");
@@ -3823,9 +3838,11 @@ function checkTo(to) {
3823
3838
  }
3824
3839
  /**
3825
3840
  * Split the rules into what `astro.config` serves and what the host has to.
3826
- * `built` is `builtAddresses` of the emitted routes.
3841
+ * `built` is `builtAddresses` of the emitted routes. `hostOnly`: the rules go
3842
+ * to host files alone (`EmitInput.hostRedirects`), so the checks that hold
3843
+ * only for a static redirect page (a `from` naming a file) are left out.
3827
3844
  */
3828
- function planRedirects(redirects, built) {
3845
+ function planRedirects(redirects, built, options = {}) {
3829
3846
  const config = {};
3830
3847
  const written = [];
3831
3848
  const manual = [];
@@ -3849,11 +3866,15 @@ function planRedirects(redirects, built) {
3849
3866
  skip(`status ${status} is not one of ${REDIRECT_STATUSES.join("/")}`);
3850
3867
  continue;
3851
3868
  }
3852
- const from = checkFrom(redirect.from);
3869
+ const from = checkFrom(redirect.from, options.hostOnly);
3853
3870
  if ("reason" in from) {
3854
3871
  skip(from.reason);
3855
3872
  continue;
3856
3873
  }
3874
+ if (options.hostOnly && buildOutputFile(from.path)) {
3875
+ skip("from is a file the build writes — the host file would serve the redirect over it");
3876
+ continue;
3877
+ }
3857
3878
  if (!checkTo(redirect.to)) {
3858
3879
  skip("to is not a site-root path or an http(s) URL");
3859
3880
  continue;
@@ -4201,22 +4222,60 @@ function emitAstroProject(input) {
4201
4222
  };
4202
4223
  const hasRedirects = Object.keys(redirectConfig).length > 0;
4203
4224
  add(scaffoldFiles(ir, input.options ?? {}, noindex, hasRedirects ? astroRedirectsConfig(sortedConfig(redirectConfig)) : null, input.runtime));
4225
+ const hostOnlyPlan = input.hostRedirects ? planRedirects(input.hostRedirects, built, { hostOnly: true }) : void 0;
4226
+ const hostOnlyConfig = {};
4227
+ const hostOnlyRule = /* @__PURE__ */ new Map();
4228
+ const hostOnlyManual = [...hostOnlyPlan?.manual ?? []];
4229
+ if (hostOnlyPlan) {
4230
+ const byFrom = new Map(hostOnlyPlan.written.map((rule) => [decodedPath(rule.from), rule]));
4231
+ for (const [from, rule] of Object.entries(hostOnlyPlan.config)) {
4232
+ const redirect = byFrom.get(from);
4233
+ const key = from.replace(/\/+$/, "").toLowerCase();
4234
+ if (claimed.has(key)) {
4235
+ hostOnlyManual.push({
4236
+ redirect,
4237
+ reason: `another rule already redirects ${from}`
4238
+ });
4239
+ continue;
4240
+ }
4241
+ claimed.add(key);
4242
+ hostOnlyConfig[from] = rule;
4243
+ hostOnlyRule.set(from, redirect);
4244
+ }
4245
+ }
4204
4246
  const feedOrder = new Map(feedRules.map((rule, index) => [decodedPath(rule.from), index]));
4205
4247
  const feedKeys = Object.keys(feedPlan.config).toSorted((a, b) => (feedOrder.get(a) ?? 0) - (feedOrder.get(b) ?? 0));
4206
4248
  const hostOrder = {
4207
4249
  ...sortedConfig(sourceConfig),
4208
- ...Object.fromEntries(feedKeys.map((key) => [key, feedPlan.config[key]]))
4250
+ ...Object.fromEntries(feedKeys.map((key) => [key, feedPlan.config[key]])),
4251
+ ...sortedConfig(hostOnlyConfig)
4209
4252
  };
4210
- const hostRedirects = hasRedirects ? hostRedirectFiles(hostOrder, input.options?.redirectHost) : void 0;
4253
+ const hostRedirects = Object.keys(hostOrder).length ? hostRedirectFiles(hostOrder, input.options?.redirectHost) : void 0;
4254
+ const hostOnly = (from) => hostOnlyRule.has(from);
4255
+ for (const from of hostRedirects?.skipped ?? []) if (hostOnly(from)) hostOnlyManual.push({
4256
+ redirect: hostOnlyRule.get(from),
4257
+ reason: "from contains \":\" or \"*\", which host redirect files read as a pattern"
4258
+ });
4259
+ const namedHost = input.options?.redirectHost;
4260
+ const hostOnlyOver = (hostRedirects?.over_limit ?? []).filter(hostOnly);
4261
+ if (namedHost !== void 0) for (const from of hostOnlyOver) hostOnlyManual.push({
4262
+ redirect: hostOnlyRule.get(from),
4263
+ reason: `left out of ${hostRedirects.written.join(", ")} at its rule limit (${HOST_RULE_LIMIT / 2} rules) — a host-only rule has no meta-refresh page to fall back on`
4264
+ });
4265
+ else if (hostOnlyOver.length) warnings.push(`hostRedirects: ${hostOnlyOver.length} host-only rules not written to vercel.json at its rule limit (${HOST_RULE_LIMIT / 2} rules) — public/_redirects (Netlify) has them; on Vercel they are not served. Name the host (options.redirectHost) to have them reported as manual`);
4266
+ const unservedHostOnly = new Set(hostOnlyManual.map((m) => m.redirect));
4267
+ if (hostOnlyManual.length) warnings.push(`hostRedirects: ${hostOnlyManual.length} of ${input.hostRedirects.length} host-only rules not written — set them up at the host (EmitResult.redirects.manual has each with its reason)`);
4211
4268
  if (hostRedirects) {
4212
4269
  add(hostRedirects.files);
4213
- if (hostRedirects.over_limit.length) {
4270
+ const overLimit = hostRedirects.over_limit.filter((from) => !hostOnly(from));
4271
+ const skippedPatterns = hostRedirects.skipped.filter((from) => !hostOnly(from));
4272
+ if (overLimit.length) {
4214
4273
  const limitedFiles = hostRedirects.written.filter((f) => !f.includes("(netlify)")).join(", ");
4215
- const feedOver = hostRedirects.over_limit.filter((from) => from in feedPlan.config).length;
4274
+ const feedOver = overLimit.filter((from) => from in feedPlan.config).length;
4216
4275
  const which = feedOver ? ` (${feedOver} of them feed redirects — /feed/ and archive feeds come after the site's own rules)` : "";
4217
- warnings.push(`redirects: ${hostRedirects.over_limit.length} rules not written to ${limitedFiles}${which} — ${HOST_RULE_LIMIT / 2} rules (each path with and without its slash) fill Cloudflare Pages' 2,000 static and Vercel's 2,048 redirect limit. They are served by the meta-refresh fallback only; move them to the host's dynamic rules for a real status`);
4276
+ warnings.push(`redirects: ${overLimit.length} rules not written to ${limitedFiles}${which} — ${HOST_RULE_LIMIT / 2} rules (each path with and without its slash) fill Cloudflare Pages' 2,000 static and Vercel's 2,048 redirect limit. They are served by the meta-refresh fallback only; move them to the host's dynamic rules for a real status`);
4218
4277
  }
4219
- if (hostRedirects.skipped.length) warnings.push(`redirects: ${hostRedirects.skipped.length} rules contain ":" or "*", which host redirect files read as patterns — served by the meta-refresh fallback only: ${hostRedirects.skipped.join(", ")}`);
4278
+ if (skippedPatterns.length) warnings.push(`redirects: ${skippedPatterns.length} rules contain ":" or "*", which host redirect files read as patterns — served by the meta-refresh fallback only: ${skippedPatterns.join(", ")}`);
4220
4279
  }
4221
4280
  if (input.options?.trailingSlash === false && (input.options.redirectHost ?? "vercel") === "vercel") {
4222
4281
  const vercel = files["vercel.json"] ? JSON.parse(files["vercel.json"]) : {};
@@ -4381,11 +4440,11 @@ function emitAstroProject(input) {
4381
4440
  return {
4382
4441
  files,
4383
4442
  warnings,
4384
- ...redirectPlan ? { redirects: {
4385
- written: redirectPlan.written,
4386
- manual: redirectPlan.manual,
4443
+ ...redirectPlan || hostOnlyPlan ? { redirects: {
4444
+ written: [...redirectPlan?.written ?? [], ...[...hostOnlyRule.values()].filter((rule) => !unservedHostOnly.has(rule))],
4445
+ manual: [...redirectPlan?.manual ?? [], ...hostOnlyManual],
4387
4446
  host_files: hostRedirects?.written ?? [],
4388
- host_over_limit: hostRedirects?.over_limit ?? []
4447
+ host_over_limit: (hostRedirects?.over_limit ?? []).filter((from) => !hostOnly(from) || namedHost === void 0)
4389
4448
  } } : {}
4390
4449
  };
4391
4450
  }