diff --git a/.agents/skills/commit/SKILL.md b/.agents/skills/commit/SKILL.md index a8b1f30c8..4e91282c9 100644 --- a/.agents/skills/commit/SKILL.md +++ b/.agents/skills/commit/SKILL.md @@ -42,8 +42,8 @@ EOF Don't put `Generated with [Claude Code](https://claude.ai/code)`, or `Co-authored-by` trailers at the end of the commit message. Instead, -use the `Assiged-by` trailer to indicate that the commit was generated by -an AI assistant, if necessary. The format of the `Assiged-by` trailer should +use the `Assisted-by` trailer to indicate that the commit was generated by +an AI assistant, if necessary. The format of the `Assisted-by` trailer should be: ~~~~ diff --git a/CHANGES.md b/CHANGES.md index bd1c15f0b..3d07f9c5e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,17 @@ Version 2.1.6 To be released. +### @fedify/astro + + - Restored the npm entrypoint contract for `@fedify/astro` by making the + build emit _dist/\*.js_ and _dist/\*.d.ts_ files that match the published + package metadata again. This fixes package resolution failures caused by + _package.json_ exporting files that did not exist in the npm tarball. + [[#699], [#701]] + +[#699]: https://github.com/fedify-dev/fedify/issues/699 +[#701]: https://github.com/fedify-dev/fedify/pull/701 + ### @fedify/cli - Fixed `fedify lookup` failing to look up URLs on private or localhost diff --git a/packages/astro/package.json b/packages/astro/package.json index 12bd75454..29ca4c860 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -61,6 +61,7 @@ "build": "pnpm --filter @fedify/astro... run build:self", "prepack": "pnpm build", "prepublish": "pnpm build", + "pretest": "pnpm build", "test": "node --experimental-transform-types --test" } } diff --git a/packages/astro/src/package.test.ts b/packages/astro/src/package.test.ts new file mode 100644 index 000000000..43ea977ce --- /dev/null +++ b/packages/astro/src/package.test.ts @@ -0,0 +1,179 @@ +import { deepStrictEqual, strictEqual } from "node:assert/strict"; +import { access, readFile } from "node:fs/promises"; +import { createRequire } from "node:module"; +import { dirname, resolve } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import type { + Federation, + FederationFetchOptions, +} from "@fedify/fedify/federation"; +import type { APIContext } from "astro"; + +const require = createRequire(import.meta.url); +const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); + +type MockFederation = Pick, "fetch">; + +function toFederation( + federation: MockFederation, +): Federation { + return federation as Federation; +} + +function toApiContext(context: Pick): APIContext { + return context as APIContext; +} + +function expectResponse(response: void | Response): Response { + if (!(response instanceof Response)) { + throw new TypeError("Expected middleware to return a Response"); + } + return response; +} + +function expectTarget(actual: unknown, expected: string, key: string): string { + strictEqual(actual, expected, `Expected ${key} to be ${expected}`); + return actual; +} + +async function assertTargetExists(path: string): Promise { + await access(resolve(packageDir, path)); +} + +test("self-reference ESM import exposes working Astro integration API", async () => { + const mod = await import("@fedify/astro"); + + strictEqual(typeof mod.fedifyIntegration, "function"); + strictEqual(typeof mod.fedifyMiddleware, "function"); + + const integration = mod.fedifyIntegration(); + strictEqual(integration.name, "@fedify/astro"); + + let capturedConfig: unknown; + ( + integration.hooks as Record< + "astro:config:setup", + (args: { updateConfig(config: unknown): void }) => void + > + )["astro:config:setup"]({ + updateConfig(config) { + capturedConfig = config; + }, + }); + deepStrictEqual(capturedConfig, { + vite: { + ssr: { + noExternal: ["@fedify/fedify", "@fedify/vocab"], + }, + }, + }); + + let capturedRequest: Request | undefined; + let capturedContextData: unknown; + const middleware = mod.fedifyMiddleware( + toFederation({ + async fetch( + request: Request, + options: FederationFetchOptions, + ) { + capturedRequest = request; + capturedContextData = options.contextData; + if (options.onNotAcceptable == null) { + throw new TypeError("Expected onNotAcceptable to be defined"); + } + return await options.onNotAcceptable(request); + }, + }), + () => "test-context", + ); + + const request = new Request("https://example.com/"); + const response = expectResponse( + await middleware( + toApiContext({ request }), + () => Promise.resolve(new Response("Not found", { status: 404 })), + ), + ); + strictEqual(capturedRequest, request); + strictEqual(capturedContextData, "test-context"); + strictEqual(response.status, 406); + strictEqual(response.headers.get("Vary"), "Accept"); +}); + +test( + "self-reference CommonJS require exposes working Astro middleware API", + { skip: "Deno" in globalThis }, + async () => { + const packageJson = JSON.parse( + await readFile(resolve(packageDir, "package.json"), "utf8"), + ); + const exportMap = packageJson.exports["."]; + const targets = [ + expectTarget(packageJson.main, "./dist/mod.cjs", "package.json main"), + expectTarget(packageJson.module, "./dist/mod.js", "package.json module"), + expectTarget(packageJson.types, "./dist/mod.d.ts", "package.json types"), + expectTarget( + exportMap.require.types, + "./dist/mod.d.cts", + 'package.json exports["."].require.types', + ), + expectTarget( + exportMap.require.default, + "./dist/mod.cjs", + 'package.json exports["."].require.default', + ), + expectTarget( + exportMap.import.types, + "./dist/mod.d.ts", + 'package.json exports["."].import.types', + ), + expectTarget( + exportMap.import.default, + "./dist/mod.js", + 'package.json exports["."].import.default', + ), + ]; + + for (const target of new Set(targets)) { + await assertTargetExists(target); + } + + const mod = require("@fedify/astro") as typeof import("@fedify/astro"); + + strictEqual(typeof mod.fedifyIntegration, "function"); + strictEqual(typeof mod.fedifyMiddleware, "function"); + + let nextCalled = false; + const middleware = mod.fedifyMiddleware( + toFederation({ + async fetch( + _request: Request, + options: FederationFetchOptions, + ) { + if (options.onNotFound == null) { + throw new TypeError("Expected onNotFound to be defined"); + } + return await options.onNotFound( + new Request("https://example.com/actor"), + ); + }, + }), + () => undefined, + ); + + const response = expectResponse( + await middleware( + toApiContext({ request: new Request("https://example.com/inbox") }), + () => { + nextCalled = true; + return Promise.resolve(new Response("Handled by Astro")); + }, + ), + ); + + strictEqual(nextCalled, true); + strictEqual(response.status, 200); + strictEqual(await response.text(), "Handled by Astro"); + }, +); diff --git a/packages/astro/tsdown.config.ts b/packages/astro/tsdown.config.ts index 27a91233e..bf33f512d 100644 --- a/packages/astro/tsdown.config.ts +++ b/packages/astro/tsdown.config.ts @@ -5,4 +5,10 @@ export default defineConfig({ dts: true, format: ["esm", "cjs"], platform: "node", + outExtensions({ format }) { + return { + js: format === "cjs" ? ".cjs" : ".js", + dts: format === "cjs" ? ".d.cts" : ".d.ts", + }; + }, });