From def2fbea84ced82d9b58213b43a6852ac73a95ec Mon Sep 17 00:00:00 2001 From: Justin Francos Date: Mon, 16 Feb 2026 10:37:14 -0500 Subject: [PATCH 01/72] Embedded langs in (#15298) * option for adding additional langs * tests * changeset * cleanup test * cleanup test * cleanup * clarify changeset * Update .changeset/tangy-tables-jog.md Co-authored-by: Emanuele Stoppa * add more details to changeset * add more details to changeset * Update .changeset/tangy-tables-jog.md Co-authored-by: Florian Lefebvre * Update .changeset/tangy-tables-jog.md Co-authored-by: Florian Lefebvre * minor not patch * Apply suggestions from code review Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com> --------- Co-authored-by: Emanuele Stoppa Co-authored-by: Florian Lefebvre Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com> --- .changeset/tangy-tables-jog.md | 25 +++++++++++++++++++ packages/astro/components/Code.astro | 10 ++++++++ .../astro/test/astro-component-code.test.js | 6 +++++ .../src/pages/langs.astro | 12 +++++++++ 4 files changed, 53 insertions(+) create mode 100644 .changeset/tangy-tables-jog.md create mode 100644 packages/astro/test/fixtures/astro-component-code/src/pages/langs.astro diff --git a/.changeset/tangy-tables-jog.md b/.changeset/tangy-tables-jog.md new file mode 100644 index 000000000000..e7284c44b8f1 --- /dev/null +++ b/.changeset/tangy-tables-jog.md @@ -0,0 +1,25 @@ +--- +'astro': minor +--- + +Adds a new optional `embeddedLangs` prop to the `` component to support languages beyond the primary `lang` + +This allows, for example, highlighting `.vue` files with a ` + +` +--- + +``` + +See the [`` component documentation](https://v6.docs.astro.build/en/guides/syntax-highlighting/#code-) for more details. diff --git a/packages/astro/components/Code.astro b/packages/astro/components/Code.astro index bdc71729af5c..a84083cebe8a 100644 --- a/packages/astro/components/Code.astro +++ b/packages/astro/components/Code.astro @@ -16,6 +16,14 @@ interface Props extends Omit, 'lang'> { * @default "plaintext" */ lang?: CodeLanguage; + /** + * Additional languages to load. + * Useful if `code` embeds languages not included by the given `lang` + * For example, TSX in Vue + * + * @default [] + */ + embeddedLangs?: CodeLanguage[]; /** * A metastring to pass to the highlighter. * Allows passing information to transformers: https://shiki.style/guide/transformers#meta @@ -72,6 +80,7 @@ interface Props extends Omit, 'lang'> { const { code, lang = 'plaintext', + embeddedLangs = [], meta, theme = 'github-dark', themes = {}, @@ -101,6 +110,7 @@ const highlighter = await createShikiHighlighter({ ? lang : 'plaintext' : (lang as any), + ...embeddedLangs, ], theme, themes, diff --git a/packages/astro/test/astro-component-code.test.js b/packages/astro/test/astro-component-code.test.js index adb77a33aec3..3c7b7738479b 100644 --- a/packages/astro/test/astro-component-code.test.js +++ b/packages/astro/test/astro-component-code.test.js @@ -121,4 +121,10 @@ describe('', () => { assert.match(codeEl.attr('style'), /background-color:/); assert.equal($('pre').length, 0); }); + + it(' tokenizes TSX', async () => { + const html = await fixture.readFile('/langs/index.html'); + const $ = cheerio.load(html); + assert.ok([...$('.line > span')].some((el) => $(el).text().trim() === 'const')); + }); }); diff --git a/packages/astro/test/fixtures/astro-component-code/src/pages/langs.astro b/packages/astro/test/fixtures/astro-component-code/src/pages/langs.astro new file mode 100644 index 000000000000..ccc0adda5349 --- /dev/null +++ b/packages/astro/test/fixtures/astro-component-code/src/pages/langs.astro @@ -0,0 +1,12 @@ +--- +import {Code} from 'astro:components'; + +const code = ` + +` +--- + From 26bdb87bf465a5878fb64b1d5df6d6921cf1d9b6 Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 24 Mar 2026 11:46:55 +0000 Subject: [PATCH 02/72] chore: enter prelease mode (#16057) --- .changeset/config.json | 2 +- .changeset/pre.json | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 .changeset/pre.json diff --git a/.changeset/config.json b/.changeset/config.json index be86265817aa..902828cfe8b7 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -4,7 +4,7 @@ "commit": false, "linked": [], "access": "public", - "baseBranch": "origin/main", + "baseBranch": "origin/next", "updateInternalDependencies": "patch", "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { "onlyUpdatePeerDependentsWhenOutOfRange": true diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 000000000000..779138f4f6dd --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,36 @@ +{ + "mode": "pre", + "tag": "alpha", + "initialVersions": { + "astro": "6.0.8", + "@astrojs/prism": "4.0.1", + "@astrojs/rss": "4.0.17", + "create-astro": "5.0.3", + "@astrojs/db": "0.20.1", + "@astrojs/alpinejs": "0.5.0", + "@astrojs/cloudflare": "13.1.3", + "@astrojs/markdoc": "1.0.2", + "@astrojs/mdx": "5.0.2", + "@astrojs/netlify": "7.0.4", + "@astrojs/node": "10.0.3", + "@astrojs/partytown": "2.1.5", + "@astrojs/preact": "5.0.2", + "@astrojs/react": "5.0.1", + "@astrojs/sitemap": "3.7.1", + "@astrojs/solid-js": "6.0.1", + "@astrojs/svelte": "8.0.3", + "@astrojs/vercel": "10.0.2", + "@astrojs/vue": "6.0.1", + "@astrojs/internal-helpers": "0.8.0", + "@astrojs/check": "0.9.8", + "@astrojs/language-server": "2.16.6", + "@astrojs/ts-plugin": "1.10.7", + "astro-vscode": "2.16.13", + "@astrojs/yaml2ts": "0.2.3", + "@astrojs/markdown-remark": "7.0.1", + "@astrojs/telemetry": "3.3.0", + "@astrojs/underscore-redirects": "1.0.2", + "@astrojs/upgrade": "0.7.1" + }, + "changesets": [] +} From ee079d4c7f143076b84d663c832911009a077c7f Mon Sep 17 00:00:00 2001 From: Emanuele Stoppa Date: Tue, 21 Apr 2026 13:14:16 +0100 Subject: [PATCH 03/72] chore: merge main into next (#16434) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(cloudflare): exclude starlight from SSR dep optimization (#16151) * [ci] release (#16152) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * refactor(mdx): more unit tests, less integrations (#16158) * fix: stop CSS traversal at page boundaries (#16116) * [ci] format * fix(content): clear stale asset imports on content collection entry update (#16124) * fix(content): clear stale asset imports on content collection entry update * fix lint errors * fix(core): append assetQueryParams to inter-chunk JS imports (#15964) (#16110) * fix(core): append assetQueryParams to inter-chunk JS imports (#15964) * Add changeset for inter-chunk skew protection fix * [ci] format * chore: move unit tests to ts (#16157) * [ci] format * fix(vercel): edge middleware next() drops HTTP method and body (#16170) * fix(vercel): edge middleware next() drops HTTP method and body * fix: conditional and format-sensitive * test: don't use tmp fixtures (#16177) * fix(preact): pre-optimize @preact/signals to prevent dev reload flakiness (#16180) * Preserve head metadata in Cloudflare dev rendering (#16161) * Update dev head metadata for non-runnable pipeline * Refine non-runnable component metadata loading * Load component metadata in non-runnable dev * Remove unused export for virtual component metadata constant * Add docs for virtual component metadata module * fix(cloudflare): ensure HMR works when `prerenderEnvironment` is set to 'node' (#16162) Co-authored-by: Matthew Phillips * Include injected routes when determining whether renderers are needed in SSR builds (#16178) * fix(astro): Fix `isHTMLString` check failing in multi-realm environments (#16142) * fix(container): don't escape slot HTML in renderToString during build * performance issue * oops * apply Erika's suggestion * use `isHTMLString` within `markHTMLString` * simplify test fixtures * format * remove the no longer used `Symbol.toStringTag` from `HTMLString` * rename to `htmlStringSymbol` * update changeset * Apply suggestion from @ematipico --------- Co-authored-by: Emanuele Stoppa * fix(underscore-redirects): respect trailingSlash config in redirects (#16034) Co-authored-by: astrobot-houston * [ci] format * [ci] release (#16159) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Fix periods in URLs with trailing slashes causing 404s in dev server (#16154) * Fix periods in URLs with trailing slashes causing 404s in dev server (#16140) Pages with dots in their filenames (e.g. `hello.world.astro`) were incorrectly treated as file-extension paths, forcing `trailingSlash: 'never'` regardless of user config. Only endpoints with file extensions should force this behavior. * ci: retry flaky e2e tests * ci: retry flaky e2e tests --------- Co-authored-by: Matthew Phillips * docs(language-tools): mention js/ts settings namespace in vscode (#16167) * docs(language-tools): mention js/ts settings namespace in vscode * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Matthew Phillips Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix(e2e): remove bogus Node.js import breaking actions-blog tests (#16183) A spurious import of createLoggerFromFlags from cli/flags.ts was added to the client-side PostComment.tsx component via a sync commit, breaking hydration and causing Comment and Logout tests to fail. * test: increase testing coverage (#16189) * [ci] format * Preserve Cloudflare miniflare instance across dev server config restarts (#16059) * fix: preserve viteServer.restart wrapper chain for Cloudflare adapter * add changeset * fix: use Vite in-place restart for config changes to preserve Cloudflare miniflare instance * use vite.resolveConfig to get a proper ResolvedConfig instead of patching inlineConfig * fix watcher listener accumulation, null-check hot.send, move restartInFlight to finally, add tests * fix port drift on restart by passing current httpServer port to createVite * remove non-actionable CSP dev warning * merge main, fix restart tests to use static fixture dir * fix(astro): remove unused re-exports causing Vite build warning (#16197) * fix(astro): remove unused re-exports causing Vite build warning (#16188) * chore: add changeset --------- Co-authored-by: astrobot-houston * Fix trailingSlash for extensionless endpoints in static builds (#16193) * Unblock smoke tests: exclude astro-og-canvas@0.11.0 from minimumReleaseAge (#16211) * Exclude astro-og-canvas@0.11.0 from minimumReleaseAge * Exclude @types/node@24.12.2 from minimumReleaseAge * feat: erasableSyntaxOnly (#15719) * fix: react 19 ssr aito injection preload link (#16224) * feat: integration test of react 19 auto injection preload link * fix: when created preload in link, remove it and fix it for tag can be read * feat: changeset file * fix: pnpm-lock file * fix: regexp for passing eslint * [ci] format * [ci] release (#16182) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * fix(cloudflare): exclude queue consumers from prerender worker (#16225) * fix(cloudflare): exclude queue consumers from prerender worker config The prerender worker's config callback was spreading the entire entryWorkerConfig, including queues.consumers. When Miniflare sees two workers both registered as consumers of the same queue, it rejects with ERR_MULTIPLE_CONSUMERS. The prerender worker only renders static HTML and has no need for queue consumer registrations. This fix destructures queues from the entry worker config and only preserves queue producers (bindings) in the prerender worker config. Closes #16199 Co-Authored-By: Tadao * chore: update pnpm lockfile Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Tadao Co-authored-by: Claude Opus 4.6 (1M context) * refactor: port tests to ts (#16243) * fix: stream Fragment sync siblings before async children resolve (#16239) * [ci] format * Port 8 unit test files from JavaScript to TypeScript (#16249) Part of the test-to-TypeScript migration (#16241). Ported files: - app/dev-url-construction.test - app/headers.test - app/url-attribute-xss.test - assets/image-layout.test - render/escape.test - render/hydration.test - routing/origin-pathname.test - routing/routing-helpers.test Removed @ts-check directives (redundant in .ts files), converted JSDoc type annotations to native TypeScript where needed, and added minimal type assertions for test mocks. typecheck:tests and both test:unit:ts / test:unit:js pass. * chore: adapt code to upstream deprecation (#16192) * chore: adapt code to upstream deprecation Signed-off-by: Alexander Niebuhr <45965090+alexanderniebuhr@users.noreply.github.com> * fix remove ununsed import Signed-off-by: Alexander Niebuhr <45965090+alexanderniebuhr@users.noreply.github.com> * Apply suggestion from @alexanderniebuhr * Apply suggestion from @alexanderniebuhr * Apply suggestion from @alexanderniebuhr * Apply suggestion from @alexanderniebuhr --------- Signed-off-by: Alexander Niebuhr <45965090+alexanderniebuhr@users.noreply.github.com> * refactor: migrate blog template to use new astro font loading api (#16128) Co-authored-by: Florian Lefebvre * [ci] format * fix(assets): resolve Picture TDZ error when combined with content render() (#16171) * fix(assets): resolve Picture TDZ error when combined with content render (#16036) Introduce an internal virtual module (virtual:astro-get-image) that exports only getImage and imageConfig without any Astro component references. The content runtime now imports from this narrower module instead of astro:assets, breaking the circular initialization dependency that caused a TDZ ReferenceError when prerendered pages using were bundled in the same chunk as content collection render() calls. * chore: add changeset for Picture TDZ fix * fix: rename virtual module to virtual:astro:get-image Use colon separator (virtual:astro:*) instead of hyphen so the module matches existing optimizeDeps.exclude patterns in both Astro core and the Cloudflare adapter. The hyphenated name was not excluded from Vite's dependency optimizer, causing esbuild to fail resolving it. * fix: add virtual:astro:get-image to dev-only.d.ts and remove ts-expect-error Add type declaration for the virtual:astro:get-image module so TypeScript recognizes the import without needing a @ts-expect-error directive. * Apply suggestion from @alexanderniebuhr --------- Co-authored-by: uni * Revives UnoCSS in dev mode when used with the client router (#16242) * [ci] format * chore: inline dlv dependency (#16259) * chore: inline dlv dependency * format * Create three-kids-camp.md * fix: use `for...of` * [ci] format * chore: upgrade biome (#16246) * [ci] format * [ci] release (#16244) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * test: port 16 routing unit tests to TypeScript (#16266) * [ci] format * refactor(core): rename logger internal types (#16271) Co-authored-by: Princesseuh <3019731+Princesseuh@users.noreply.github.com> * Bump @qwik.dev/partytown to v0.13.2 (#16265) Co-authored-by: Emanuele Stoppa * Consolidate inline script escaping into shared utility (#16303) * Consolidate inline script escaping into shared stringifyForScript utility Unify the two separate script-embedding escape approaches (defineScriptVars and safeJsonStringify) into a single stringifyForScript function in escape.ts. Uses a comprehensive < escape strategy rather than pattern-matching specific tag sequences, covering all ETAGO variants in one pass. * Add changeset * Update changeset description * Fix stringifyForScript to handle undefined values * Update astro-directives test assertion for new escape sequence * Skip actions server-output validation when an adapter is configured (#16202) * Fix .svelte files in node_modules with Cloudflare prerenderEnvironment: node (#16210) * fix(cloudflare,svelte): resolve .svelte files in node_modules with prerenderEnvironment: 'node' Packages that ship .svelte files (e.g. bits-ui) failed with 'Unknown file extension .svelte' when using the Cloudflare adapter with prerenderEnvironment: 'node'. Two issues caused this: 1. The cf-externals plugin set top-level ssr.noExternal = true, which caused vitefu's crawlFrameworkPkgs to return an empty noExternal list (it assumed everything was already noExternal). This only applied to the ssr environment in Vite 6, leaving the prerender environment without the needed noExternal entries. 2. The createNodePrerenderPlugin disabled dep optimization entirely for the prerender environment (noDiscovery: true, include: []). The fix removes ssr.noExternal = true from cf-externals, removes the dep optimization override, and has @astrojs/svelte use crawlFrameworkPkgs to discover svelte packages and add them to resolve.noExternal for all server environments via configEnvironment. * add changeset * fix: use fileURLToPath for cross-platform root path * test: add response body to assertion error for CI debugging * fix: rename fake-svelte-pkg/dist to src to avoid .gitignore exclusion * [ci] format * fix: avoid full-reload in scss modules (#14924) * fix: avoid full-reload in scss modules * fix: improve regex * fix: ci * test: add test for modules * fix(hmr): prevent full-reload for SCSS/CSS module changes SCSS and CSS module file changes were triggering unnecessary full page reloads during development instead of applying HMR updates. This broke the developer experience by losing component state and scroll position on every style edit. Root cause: In the `astro:hmr-reload` Vite plugin, style files (CSS, SCSS, SASS, LESS, etc.) in the SSR module graph were correctly skipped via a regex check, but the handler returned `undefined` instead of an empty array. In Vite 6, returning `undefined` from a `hotUpdate` hook means "I didn't handle this", causing Vite to propagate through the SSR module graph to `.astro` importers. Since `.astro` pages have no HMR boundary, this triggered a full page reload. Changes to `vite-plugin-hmr-reload`: - Extract `isStyleModule()` helper that checks both `mod.file` and `mod.id` (stripping query params like `?inline`, `?used`) against the style extension regex. This correctly identifies all style-related modules including CSS module variants. - Return `[]` (empty array) when only style modules were encountered in the SSR environment. This tells Vite "handled, nothing to update in SSR", preventing the propagation chain that caused full reloads. The client environment handles CSS HMR natively through framework-specific HMR boundaries (Preact, React, Vue, etc.). Changes to e2e test fixtures: - Update the SCSS module HMR test to use a Preact component with `client:load` instead of a pure server-rendered Astro page. This matches the real-world scenario from the original bug report (issue #14869) where Preact + SCSS modules triggered full reloads. CSS module HMR requires a client-side framework to re-render components with updated class name hashes — pure SSR pages cannot hot-update CSS module class names without a full reload. Closes #14869 * fix(hmr): clarify comment about CSS HMR working for all pages The previous comment suggested CSS HMR only worked through framework-specific boundaries. Vite's built-in style update mechanism handles it for all pages, with or without framework components (covered by the scss-external test). Co-Authored-By: Claude Opus 4.6 (1M context) * update pnpm-lock.yaml * chore: add changeset for SCSS/CSS module HMR fix --------- Co-authored-by: Erika <3019731+Princesseuh@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Matthew Phillips Co-authored-by: Matthew Phillips * [ci] release (#16281) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * fix(dev): preserve --port flag after Vite-triggered server restart (#16311) * fix(dev): preserve custom port across Vite-triggered server restarts * chore: add changeset for --port fix --------- Co-authored-by: astrobot-houston * fix: add checkout step back to mergeability check (#15783) * refactor: unit tests that are integration tests (#16280) * refactor: unit tests that are integration tests * refactor(test): move teardown.js to TypeScript * refactor(test): move assets/fonts/utils.js to TypeScript * refactor(test): move i18n/test-helpers.js to TypeScript * refactor(test): move app/test-helpers.js to TypeScript * refactor(test): move routing/test-helpers.js to TypeScript * refactor(test): add TypeScript version of test-utils (JS kept for unconverted consumers) * refactor(test): add TypeScript versions of mocks and build/test-helpers (JS kept for unconverted consumers) * refactor(test): convert Group A batch 1 tests to TypeScript * refactor(test): move action-status.test.js to TypeScript * refactor(test): move actions-app.test.js to TypeScript * refactor(test): move app/csrf.test.js to TypeScript * refactor(test): move app/astro-response.test.js to TypeScript * refactor(test): move app/astro-attrs.test.js to TypeScript * refactor(test): move app/encoded-backslash-bypass.test.js to TypeScript * refactor(test): move app/double-slash-bypass.test.js to TypeScript * refactor(test): move app/error-pages.test.js to TypeScript * refactor(test): move app/locals.test.js to TypeScript * refactor(test): move app/response.test.js to TypeScript * refactor(test): move app/trailing-slash.test.js to TypeScript * refactor(test): move assets/getImage.test.js to TypeScript * refactor(test): move assets/fonts/infra.test.js to TypeScript * refactor(test): move assets/fonts/core.test.js to TypeScript * refactor(test): move noop.test.js to TypeScript * refactor(test): move call-middleware.test.js to TypeScript * refactor(test): move preserve-build-client-dir.test.js to TypeScript * refactor(test): move sequence.test.js to TypeScript * refactor(test): move rendering.test.js to TypeScript * refactor(test): move server-islands.test.js to TypeScript * refactor(test): move sec-fetch.test.js to TypeScript * refactor(test): move middleware-app.test.js to TypeScript * refactor(test): move generate.test.js to TypeScript * refactor(test): move router.test.js to TypeScript * refactor(test): move i18n-middleware.test.js to TypeScript * refactor(test): move i18n-app.test.js to TypeScript * refactor(test): move head-injection-app.test.js to TypeScript * refactor(test): move fallback.test.js to TypeScript * refactor(test): move render-context.test.js to TypeScript * refactor(test): move i18n-routing-static.test.js to TypeScript * refactor(test): move i18n-static-build.test.js to TypeScript * refactor(test): move paginate.test.js to TypeScript * refactor(test): move context-helpers.test.js to TypeScript * refactor(test): move manual-routing.test.js to TypeScript * refactor(test): move html-primitives.test.js to TypeScript * refactor(test): move manual-middleware.test.js to TypeScript * refactor(test): move class-list-and-style.test.js to TypeScript * refactor(test): move create-manifest.test.js to TypeScript * refactor(test): move api.test.js to TypeScript * refactor(test): move manifest.test.js to TypeScript * refactor(test): move hooks.test.js to TypeScript * refactor(test): move route-matching.test.js to TypeScript * refactor(test): move get-params.test.js to TypeScript * refactor(test): move render.test.js to TypeScript * refactor(test): move static-build.test.js to TypeScript * refactor(test): move rewrite-app.test.js to TypeScript * refactor(test): move rewrite-validation.test.js to TypeScript * refactor(test): move params-encoding.test.js to TypeScript * refactor(test): delete old JS helpers and fix remaining .js import references to .ts * refactor(test): update test:unit script for all-TypeScript unit tests * refactor(test): remove dev-hydration test, extract integration-test-helpers for createRequestAndResponse * fix(test): update unit tests for Logger→AstroLogger rename and API changes from main * refactor: address linting * refactor(test): replace fixable any types with proper types across unit tests * fix tests * Surface console output from workerd during Cloudflare prerendering (#16307) * Surface console output from workerd during prerendering * Filter out HTTP request logs from prerender server output * Narrow log filter to only match internal __astro_ request paths * Fix lint: disable no-control-regex for ANSI pattern, use non-capturing group * fix(core): svg via f=svg (#16316) * Fix static asset error responses including immutable cache headers (#16319) * fix(node): prevent cache poisoning from conditional request errors Move immutable cache header from send's 'headers' event to 'stream' event so it is only set on successful responses. The 'headers' event fires before precondition checks (If-Match, If-Unmodified-Since), which meant error responses (412) were sent with Cache-Control: public, max-age=31536000, immutable — allowing an attacker to poison CDN caches with a single malformed If-Match request. Also propagate the real HTTP status from send's error object instead of always returning 500. * add changeset * update changeset wording * Use redirect: manual in Cloudflare image binding transform (#16320) * Use redirect: 'manual' in cloudflare image-binding-transform fetch * Fix unused parameter lint error * test: add unit test for dlv util (#16262) * fix: netrify image validation (#16027) * fix(netlify): enforce validation for remote image dimensions * test(netlify): add regression test for missing image dimensions * fix: astro.config.mjs file * feat: changeset * delete: unnecessary file * fix: refactor to use loadFixture and project test infrastructure * fix: update changeset * fix: changeset explanation and refactor test code * refactor(assets): move verifyOptions export to internal to avoid hydration regressions * [ci] format * Add nightly Semgrep security scanning workflow (#16205) Co-authored-by: Felix Schneider <99918022+trueberryless@users.noreply.github.com> Co-authored-by: Emanuele Stoppa * ci: cron job semgrep (#16336) * Update github-actions (#16146) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * [ci] release (#16314) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * chore: add rule for detecting incorrect imports (#16343) * refactor: migrate react tests to typescript (#16318) * refactor: migrate custom 404 tests to typescript (#16350) * refactor: migrate vue tests to typescript (#16349) * refactor: migrate vue tests to typescript * fix import path * chore: trigger ci * fix(deps): update language tools (#16230) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore: fix lock file (#16352) * refactor: migrate sitemap tests to typescript (#16353) * fix: stabilize client build output filenames across runs (#16348) Co-authored-by: Emanuele Stoppa * changeset: add changeset for telemetry changes (#16257) * refactor: migrate alpinejs tests to typescript (#16354) * fix(core): fix append of assetQueryParams to inter-chunk JS *dynamic* imports (#16258) (#16282) Co-authored-by: Leif Marcus <2342671+leifmarcus@users.noreply.github.com> * refactor: migrate markdoc tests to typescript (#16355) * refactor: migrate astro-rss tests to typescript (#16357) * fix(dev): Passing on allowedDomains configuration. (#16317) Co-authored-by: Emanuele Stoppa * refactor: migrate svelte tests to typescript (#16351) * chore: remove lone fixtures (#16363) * fix(core): clean chunk name (#16367) * fix(core): clean chunk name * linting and tests * refactor: migrate telemetry tests to typescript (#16358) * refactor: migrate telemetry tests to typescript * format * remove types * format * just use any * format * fix typecheck * chore: trigger ci * refactor(astro): migrate error tests to typescript (#16377) * refactor(astro): migrate error tests to typescript * chore: trigger ci * chore: trigger ci * refactor(upgrade): migrate tests to typescript (#16372) * refactor(upgrade): migrate tests to typescript * add ShellFunction type * fix(netlify): correct test describe signature (#16371) * chore: reduce fixtures by merging them (#16364) * feat: tweak issue auto-triage (#16284) * [ci] format * chore: absorb tests into others (#16365) * perf(core): cache crawl result (#16381) * refactor(astro): correct Fixture type signatures in test-utils (#16380) * refactor(astro): correct Fixture type signatures in test-utils * chore: trigger ci * chore: trigger ci * Improves Vue scoped style handling in DEV mode during client router navigation. (#16379) * Improves Vue scoped style handling in DEV mode during client router navigation. * invert test * streamline Vue scoped style detection in DEV mode * [ci] format * refactor(mdx): migrate tests to typescript (#16359) * refactor(vercel): migrate tests to typescript (#16360) * chore(deps): update `@types/node` (#16362) * [ci] release (#16356) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * refactor(node): migrate unit tests to typescript (#16388) * refactor(remark): migrate tests to typescript (#16392) * fix(i18n): respect trailingSlash config in domain routing (#16022) Co-authored-by: ematipico <602478+ematipico@users.noreply.github.com> * refactor(mdx): add SpyIntegrationLogger for test (#16384) * refactor: remove PRERENDER env variable in tests (#16391) * refactor(underscore-redirects): migrate tests to typescript (#16374) * refactor(cloudflare): migrate tests to typescript (#16378) * refactor(astro): migrate css tests to typescript (#16393) * refactor(internal-helpers): migrate tests to typescript (#16401) * refactor(netlify): migrate tests to typescript (#16395) * refactor(astro): migrate ssr tests to typescript (#16394) * refactor(node): migrate tests to typescript (#16376) * refactor(create-astro): migrate tests to typescript (#16400) * refactor(db): migrate tests to typescript (#16403) * refactor(astro): migrate e2e tests to typescript (#16402) * refactor(astro): migrate 39 tests to typescript (#16405) * refactor(astro): migrate 9 tests to typescript (#16410) * [ci] format * refactor(astro): migrate dev tests to typescript (#16408) * refactor(astro): migrate content tests to typescript (#16409) * refactor(astro): migrate 10 tests to typescript (#16411) * refactor(astro): migrate 19 tests to typescript (#16414) * refactor(vercel): remove duplicated test files (#16416) * refactor(astro): migrate core-image tests to typescript (#16413) * refactor(astro): migrate 4 tests to typescript (#16427) --------- Signed-off-by: Alexander Niebuhr <45965090+alexanderniebuhr@users.noreply.github.com> Co-authored-by: Matthew Phillips Co-authored-by: Houston (Bot) <108291165+astrobot-houston@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Martin DONADIEU Co-authored-by: Martin DONADIEU Co-authored-by: dataCenter430 <161712630+dataCenter430@users.noreply.github.com> Co-authored-by: tmimmanuel <14046872+tmimmanuel@users.noreply.github.com> Co-authored-by: tmimmanuel Co-authored-by: BitToby <218712309+bittoby@users.noreply.github.com> Co-authored-by: Rafael Yasuhide Sudo Co-authored-by: Alexander Niebuhr <45965090+alexanderniebuhr@users.noreply.github.com> Co-authored-by: astrobot-houston Co-authored-by: Alexander Niebuhr Co-authored-by: Desel72 Co-authored-by: Misrilal <106655807+Misrilal-Sah@users.noreply.github.com> Co-authored-by: Matthew Phillips Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Schahin Co-authored-by: Florian Lefebvre Co-authored-by: fkatsuhiro <113022468+fkatsuhiro@users.noreply.github.com> Co-authored-by: travisBREAKS <148665997+travisbreaks@users.noreply.github.com> Co-authored-by: Tadao Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: Amar Reddy <20904126+AmarReddy4@users.noreply.github.com> Co-authored-by: Alex Dombroski Co-authored-by: Alex Dombroski Co-authored-by: uni Co-authored-by: Martin Trapp <94928215+martrapp@users.noreply.github.com> Co-authored-by: Martin Trapp Co-authored-by: Roman Co-authored-by: barry <100205797+barry3406@users.noreply.github.com> Co-authored-by: Princesseuh <3019731+Princesseuh@users.noreply.github.com> Co-authored-by: ChrisLaRocque Co-authored-by: Aral Roca Gomez Co-authored-by: Alejandro Romano Co-authored-by: Ciaran Moran Co-authored-by: Fred K. Schott <622227+FredKSchott@users.noreply.github.com> Co-authored-by: Felix Schneider <99918022+trueberryless@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: ocavue Co-authored-by: James Murty Co-authored-by: Leif Marcus <2342671+leifmarcus@users.noreply.github.com> Co-authored-by: Peter Philipp Co-authored-by: Erika Co-authored-by: Maxim Slobodchikov <93232189+maximslo@users.noreply.github.com> Co-authored-by: Mathieu Mafille <81818115+mathieumaf@users.noreply.github.com> --- .agents/skills/triage/comment.md | 28 +- .agents/skills/triage/diagnose.md | 5 + .agents/skills/triage/fix.md | 15 +- .changeset/two-eels-live.md | 5 + .flue/workflows/issue-triage/WORKFLOW.ts | 11 +- .github/scripts/tsconfig.json | 7 + .github/workflows/check-merge.yml | 17 +- .github/workflows/check.yml | 2 +- .github/workflows/ci.yml | 4 +- .github/workflows/congrats.yml | 2 +- .github/workflows/continuous_benchmark.yml | 4 +- .github/workflows/diff-dependencies.yml | 2 +- .github/workflows/format.yml | 2 +- .github/workflows/issue-close-cleanup.yml | 20 + .github/workflows/issue-triage.yml | 2 +- .github/workflows/preview-release.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/scripts.yml | 2 +- .github/workflows/semgrep.yml | 14 + biome.jsonc | 25 +- eslint.config.js | 1 + examples/basics/package.json | 2 +- examples/blog/README.md | 1 + examples/blog/astro.config.mjs | 26 +- examples/blog/package.json | 2 +- .../assets}/fonts/atkinson-bold.woff | Bin .../assets}/fonts/atkinson-regular.woff | Bin examples/blog/src/components/BaseHead.astro | 5 +- examples/blog/src/styles/global.css | 16 +- examples/component/package.json | 2 +- examples/container-with-vitest/package.json | 4 +- examples/framework-alpine/package.json | 2 +- examples/framework-multiple/package.json | 8 +- examples/framework-preact/package.json | 4 +- examples/framework-react/package.json | 4 +- examples/framework-solid/package.json | 2 +- examples/framework-svelte/package.json | 4 +- examples/framework-vue/package.json | 2 +- examples/hackernews/package.json | 4 +- examples/integration/package.json | 2 +- examples/minimal/package.json | 2 +- examples/portfolio/package.json | 2 +- examples/ssr/package.json | 6 +- examples/starlog/package.json | 2 +- examples/toolbar-app/package.json | 4 +- examples/with-markdoc/package.json | 2 +- examples/with-mdx/package.json | 4 +- examples/with-nanostores/package.json | 4 +- examples/with-tailwindcss/package.json | 2 +- examples/with-vitest/package.json | 2 +- knip.js | 4 +- package.json | 5 +- packages/astro-rss/package.json | 3 +- ...ms.test.js => pagesGlobToRssItems.test.ts} | 4 +- .../test/{rss.test.js => rss.test.ts} | 10 +- .../test/{test-utils.js => test-utils.ts} | 14 +- packages/astro-rss/tsconfig.test.json | 12 + packages/astro/CHANGELOG.md | 87 ++ packages/astro/dev-only.d.ts | 11 + ...ions-blog.test.js => actions-blog.test.ts} | 4 +- ...ct-19.test.js => actions-react-19.test.ts} | 4 +- ...ponent.test.js => astro-component.test.ts} | 4 +- ...{astro-envs.test.js => astro-envs.test.ts} | 4 +- ...ut.test.js => client-idle-timeout.test.ts} | 4 +- ...lient-only.test.js => client-only.test.ts} | 4 +- .../e2e/cloudflare-node-prerender-hmr.test.ts | 34 + ...{cloudflare.test.js => cloudflare.test.ts} | 30 +- ...ns.test.js => content-collections.test.ts} | 4 +- ...yles.test.js => core-image-styles.test.ts} | 4 +- ...t-only.test.js => csp-client-only.test.ts} | 4 +- ...nds.test.js => csp-server-islands.test.ts} | 8 +- .../astro/e2e/{css.test.js => css.test.ts} | 4 +- ...st.js => custom-client-directives.test.ts} | 8 +- ...its.test.js => dev-toolbar-audits.test.ts} | 10 +- ...ev-toolbar.test.js => dev-toolbar.test.ts} | 8 +- ...or-cyclic.test.js => error-cyclic.test.ts} | 4 +- ...{error-sass.test.js => error-sass.test.ts} | 4 +- .../e2e/{errors.test.js => errors.test.ts} | 10 +- .../src/components/PostComment.tsx | 1 - .../astro.config.mjs | 9 + .../package.json | 13 + .../src/pages/index.astro | 6 + .../fixtures/hmr}/astro.config.mjs | 1 - packages/astro/e2e/fixtures/hmr/package.json | 4 + .../hmr/src/components/ScssModuleHeading.jsx | 5 + .../hmr/src/pages/scss-external.astro | 11 + .../fixtures/hmr/src/pages/scss-module.astro | 12 + .../hmr/src/styles/scss-external.scss | 3 + .../hmr/src/styles/scss-module.module.scss | 3 + .../view-transitions/src/pages/one.astro | 1 + .../astro/e2e/{hmr.test.js => hmr.test.ts} | 34 +- ...on-race.test.js => hydration-race.test.ts} | 4 +- .../astro/e2e/{i18n.test.js => i18n.test.ts} | 4 +- ...ks.test.js => multiple-frameworks.test.ts} | 16 +- ...t.test.js => namespaced-component.test.ts} | 4 +- ...react.test.js => nested-in-preact.test.ts} | 4 +- ...-react.test.js => nested-in-react.test.ts} | 6 +- ...-solid.test.js => nested-in-solid.test.ts} | 4 +- ...velte.test.js => nested-in-svelte.test.ts} | 4 +- ...d-in-vue.test.js => nested-in-vue.test.ts} | 6 +- ...rsive.test.js => nested-recursive.test.ts} | 6 +- ...d-styles.test.js => nested-styles.test.ts} | 4 +- .../e2e/{pass-js.test.js => pass-js.test.ts} | 4 +- ...est.js => preact-compat-component.test.ts} | 2 +- ...onent.test.js => preact-component.test.ts} | 2 +- ....test.js => preact-lazy-component.test.ts} | 2 +- .../{prefetch.test.js => prefetch.test.ts} | 51 +- ...ponent.test.js => react-component.test.ts} | 4 +- ...t.js => react19-preact-hook-error.test.ts} | 10 +- ...key.test.js => server-islands-key.test.ts} | 8 +- ...islands.test.js => server-islands.test.ts} | 12 +- ...ent-tests.js => shared-component-tests.ts} | 28 +- ...ircular.test.js => solid-circular.test.ts} | 4 +- ...ponent.test.js => solid-component.test.ts} | 2 +- ...-recurse.test.js => solid-recurse.test.ts} | 4 +- ...onent.test.js => svelte-component.test.ts} | 8 +- ...ailwindcss.test.js => tailwindcss.test.ts} | 4 +- .../e2e/{test-utils.js => test-utils.ts} | 82 +- ...solution.test.js => ts-resolution.test.ts} | 14 +- ...tions.test.js => view-transitions.test.ts} | 131 ++- ...s.test.js => vite-virtual-modules.test.ts} | 19 +- ...omponent.test.js => vue-component.test.ts} | 2 +- packages/astro/package.json | 15 +- packages/astro/playwright.config.js | 2 +- packages/astro/playwright.firefox.config.js | 2 +- packages/astro/src/actions/integration.ts | 4 +- packages/astro/src/assets/build/generate.ts | 4 +- packages/astro/src/assets/consts.ts | 4 + packages/astro/src/assets/endpoint/shared.ts | 10 + .../core/compute-font-families-assets.ts | 4 +- .../core/get-or-create-font-family-assets.ts | 4 +- .../src/assets/fonts/vite-plugin-fonts.ts | 4 +- packages/astro/src/assets/internal.ts | 1 + packages/astro/src/assets/utils/index.ts | 4 - .../src/assets/utils/inferSourceFormat.ts | 29 + .../assets/utils/vendor/image-size/README.md | 1 + .../vendor/image-size/utils/bit-reader.ts | 11 +- .../astro/src/assets/vite-plugin-assets.ts | 50 +- packages/astro/src/cli/add/index.ts | 113 +-- .../src/cli/create-key/core/create-key.ts | 4 +- packages/astro/src/cli/docs/core/open-docs.ts | 4 +- packages/astro/src/cli/flags.ts | 8 +- packages/astro/src/cli/info/core/info.ts | 4 +- .../src/cli/info/infra/tinyclip-clipboard.ts | 6 +- .../src/cli/infra/logger-help-display.ts | 6 +- packages/astro/src/cli/install-package.ts | 6 +- packages/astro/src/cli/preferences/index.ts | 2 +- packages/astro/src/container/index.ts | 6 +- packages/astro/src/content/content-layer.ts | 6 +- packages/astro/src/content/instance.ts | 4 +- packages/astro/src/content/loaders/errors.ts | 13 +- packages/astro/src/content/loaders/glob.ts | 8 +- .../astro/src/content/mutable-data-store.ts | 27 + packages/astro/src/content/runtime.ts | 3 +- .../astro/src/content/server-listeners.ts | 4 +- packages/astro/src/content/types-generator.ts | 8 +- packages/astro/src/content/utils.ts | 4 +- .../content/vite-plugin-content-imports.ts | 4 +- packages/astro/src/core/app/base.ts | 17 +- packages/astro/src/core/app/dev/app.ts | 12 +- packages/astro/src/core/app/dev/pipeline.ts | 12 + packages/astro/src/core/app/logging.ts | 8 +- packages/astro/src/core/app/types.ts | 4 +- packages/astro/src/core/base-pipeline.ts | 101 +- packages/astro/src/core/build/generate.ts | 20 +- packages/astro/src/core/build/index.ts | 8 +- packages/astro/src/core/build/page-data.ts | 4 +- packages/astro/src/core/build/pipeline.ts | 11 +- .../astro/src/core/build/plugins/index.ts | 2 + .../build/plugins/plugin-chunk-imports.ts | 60 ++ .../src/core/build/plugins/plugin-css.ts | 14 +- packages/astro/src/core/build/static-build.ts | 26 +- packages/astro/src/core/build/types.ts | 4 +- packages/astro/src/core/build/util.ts | 18 +- packages/astro/src/core/cache/runtime/noop.ts | 6 +- .../core/config/schemas/refined-validators.ts | 251 +++++ .../astro/src/core/config/schemas/refined.ts | 204 +--- packages/astro/src/core/cookies/cookies.ts | 7 +- packages/astro/src/core/create-vite.ts | 102 +- .../astro/src/core/dev/adapter-validation.ts | 6 +- packages/astro/src/core/dev/container.ts | 10 +- packages/astro/src/core/dev/dev.ts | 4 +- packages/astro/src/core/dev/restart.ts | 183 ++-- packages/astro/src/core/logger/console.ts | 11 +- packages/astro/src/core/logger/core.ts | 98 +- packages/astro/src/core/logger/node.ts | 40 +- packages/astro/src/core/logger/vite.ts | 2 +- packages/astro/src/core/messages/runtime.ts | 4 +- .../src/core/preview/static-preview-server.ts | 4 +- packages/astro/src/core/render-context.ts | 83 +- .../astro/src/core/render/params-and-props.ts | 4 +- packages/astro/src/core/render/route-cache.ts | 8 +- packages/astro/src/core/render/slots.ts | 6 +- packages/astro/src/core/request.ts | 4 +- .../astro/src/core/routing/create-manifest.ts | 36 +- packages/astro/src/core/routing/default.ts | 4 +- packages/astro/src/core/routing/dev.ts | 1 - packages/astro/src/core/routing/helpers.ts | 16 +- packages/astro/src/core/routing/prerender.ts | 4 +- packages/astro/src/core/routing/rewrite.ts | 4 +- packages/astro/src/core/sync/index.ts | 8 +- packages/astro/src/env/validators.ts | 2 +- .../src/integrations/features-validation.ts | 12 +- packages/astro/src/integrations/hooks.ts | 32 +- packages/astro/src/manifest/serialized.ts | 1 + packages/astro/src/preferences/dlv.ts | 7 + packages/astro/src/preferences/index.ts | 2 +- packages/astro/src/preferences/store.ts | 9 +- packages/astro/src/prerender/routing.ts | 10 +- packages/astro/src/runtime/server/endpoint.ts | 4 +- packages/astro/src/runtime/server/escape.ts | 21 +- .../src/runtime/server/render/component.ts | 15 +- .../runtime/server/render/server-islands.ts | 25 +- .../astro/src/runtime/server/render/util.ts | 7 +- .../astro/src/runtime/server/transition.ts | 10 +- .../astro/src/transitions/swap-functions.ts | 42 +- packages/astro/src/types/astro.ts | 6 +- packages/astro/src/types/public/config.ts | 6 +- packages/astro/src/vite-plugin-app/app.ts | 10 +- .../vite-plugin-app/createAstroServerApp.ts | 8 +- .../astro/src/vite-plugin-app/pipeline.ts | 41 +- .../src/vite-plugin-astro-server/base.ts | 159 +++- .../src/vite-plugin-astro-server/error.ts | 4 +- .../src/vite-plugin-astro-server/plugin.ts | 126 +-- .../src/vite-plugin-astro-server/sec-fetch.ts | 4 +- .../trailing-slash.ts | 75 +- .../astro/src/vite-plugin-astro/compile.ts | 6 +- packages/astro/src/vite-plugin-astro/hmr.ts | 4 +- packages/astro/src/vite-plugin-astro/index.ts | 4 +- packages/astro/src/vite-plugin-head/index.ts | 67 +- .../astro/src/vite-plugin-hmr-reload/index.ts | 32 +- .../index.ts | 4 +- .../astro/src/vite-plugin-markdown/index.ts | 4 +- .../astro/src/vite-plugin-renderers/index.ts | 5 +- .../astro/src/vite-plugin-routes/index.ts | 4 +- .../test/{0-css.test.js => 0-css.test.ts} | 92 +- packages/astro/test/actions.test.js | 19 + packages/astro/test/api-routes.test.js | 2 +- .../astro/test/asset-query-params.test.js | 72 ++ ...s-dir.test.js => astro-assets-dir.test.ts} | 5 +- ... => astro-assets-prefix-multi-cdn.test.ts} | 24 +- ...ix.test.js => astro-assets-prefix.test.ts} | 26 +- ...ro-assets.test.js => astro-assets.test.ts} | 18 +- ...stro-basic.test.js => astro-basic.test.ts} | 32 +- ...hildren.test.js => astro-children.test.ts} | 4 +- ...only.test.js => astro-client-only.test.ts} | 11 +- ...st.js => astro-component-bundling.test.ts} | 34 +- ...e.test.js => astro-component-code.test.ts} | 6 +- ...-cookies.test.js => astro-cookies.test.ts} | 32 +- ...ing.test.js => astro-css-bundling.test.ts} | 9 +- ...ders.test.js => astro-dev-headers.test.ts} | 12 +- ...-http2.test.js => astro-dev-http2.test.ts} | 6 +- ...tives.test.js => astro-directives.test.ts} | 69 +- ...-doctype.test.js => astro-doctype.test.ts} | 4 +- ...-dynamic.test.js => astro-dynamic.test.ts} | 10 +- ...{astro-envs.test.js => astro-envs.test.ts} | 15 +- ...{astro-expr.test.js => astro-expr.test.ts} | 22 +- .../astro/test/astro-external-files.test.js | 19 - packages/astro/test/astro-fallback.test.js | 21 - packages/astro/test/astro-generator.test.js | 24 - ...test.js => astro-get-static-paths.test.ts} | 18 +- ...ro-global.test.js => astro-global.test.ts} | 15 +- ...{astro-head.test.js => astro-head.test.ts} | 4 +- ...ro-markdown-frontmatter-injection.test.ts} | 33 +- ...test.js => astro-markdown-plugins.test.ts} | 40 +- ...js => astro-markdown-remarkRehype.test.ts} | 6 +- ...i.test.js => astro-markdown-shiki.test.ts} | 12 +- ...url.test.js => astro-markdown-url.test.ts} | 0 ...arkdown.test.js => astro-markdown.test.ts} | 20 +- ...{astro-mode.test.js => astro-mode.test.ts} | 15 +- ...nse.test.js => astro-not-response.test.ts} | 9 +- ...test.js => astro-pageDirectoryUrl.test.ts} | 8 +- ...stro-pages.test.js => astro-pages.test.ts} | 6 +- ...tml.test.js => astro-partial-html.test.ts} | 6 +- ...js => astro-preview-allowed-hosts.test.ts} | 18 +- ....test.js => astro-preview-headers.test.ts} | 6 +- ...ro-public.test.js => astro-public.test.ts} | 23 +- ...-scripts.test.js => astro-scripts.test.ts} | 24 +- .../astro/test/astro-slot-with-client.test.js | 26 - ...ted.test.js => astro-slots-nested.test.ts} | 8 +- ...stro-slots.test.js => astro-slots.test.ts} | 31 +- ...{astro-sync.test.js => astro-sync.test.ts} | 84 +- packages/astro/test/cli.test.js | 144 ++- .../astro/test/client-address-node.test.js | 2 +- packages/astro/test/config-format.test.js | 18 + .../content-collection-picture-render.test.ts | 56 ++ ... => content-collection-references.test.ts} | 55 +- ....js => content-collection-tla-svg.test.ts} | 7 +- ....js => content-collections-render.test.ts} | 17 +- ...ontent-collections-type-inference.test.ts} | 13 +- ...ns.test.js => content-collections.test.ts} | 94 +- .../astro/test/content-frontmatter.test.ts | 47 + ...e.test.js => content-intellisense.test.ts} | 24 +- ...g.test.js => core-image-fs-config.test.ts} | 12 +- ...e.test.js => core-image-infersize.test.ts} | 61 +- ...yout.test.js => core-image-layout.test.ts} | 206 ++-- ...s => core-image-picture-emit-file.test.ts} | 25 +- ...t.js => core-image-remark-imgattr.test.ts} | 48 +- ...st.js => core-image-svg-in-island.test.ts} | 14 +- ...age-svg.test.js => core-image-svg.test.ts} | 97 +- ...ore-image-unconventional-settings.test.ts} | 57 +- ...{core-image.test.js => core-image.test.ts} | 412 ++++---- ...{css-assets.test.js => css-assets.test.ts} | 12 +- ...est.js => css-dangling-references.test.ts} | 4 +- ...tion.test.js => css-deduplication.test.ts} | 10 +- ...ndle.test.js => css-double-bundle.test.ts} | 7 +- ...test.js => css-dynamic-import-dev.test.ts} | 8 +- ...e.test.js => css-import-as-inline.test.ts} | 7 +- ...test.js => css-inline-stylesheets.test.ts} | 42 +- ...plit.test.js => css-no-code-split.test.ts} | 7 +- ...mport.test.js => css-order-import.test.ts} | 37 +- ...ayout.test.js => css-order-layout.test.ts} | 23 +- .../{css-order.test.js => css-order.test.ts} | 35 +- ...4-html.test.js => custom-404-html.test.ts} | 6 +- ... => custom-404-implicit-rerouting.test.ts} | 25 +- ...s => custom-404-injected-from-dep.test.ts} | 5 +- ...ed.test.js => custom-404-injected.test.ts} | 6 +- ...cals.test.js => custom-404-locals.test.ts} | 6 +- ...m-404-md.test.js => custom-404-md.test.ts} | 6 +- ...atic.test.js => custom-404-static.test.ts} | 6 +- packages/astro/test/dev-base.test.ts | 47 + packages/astro/test/dev-container.test.ts | 167 ++++ packages/astro/test/dev-error-pages.test.ts | 69 ++ packages/astro/test/dev-render-chunk.test.ts | 31 + .../astro/test/dev-render-components.test.ts | 77 ++ packages/astro/test/dev-request-url.test.ts | 37 + packages/astro/test/dev-restart.test.ts | 269 ++++++ ...ipts.test.js => dev-route-scripts.test.ts} | 16 +- packages/astro/test/endpoint-response.test.js | 71 ++ packages/astro/test/endpoint-routing.test.js | 46 + packages/astro/test/endpoint-runtime.test.js | 44 + ...or-bad-js.test.js => error-bad-js.test.ts} | 8 +- ...n.test.js => error-build-location.test.ts} | 8 +- .../{error-map.test.js => error-map.test.ts} | 8 +- ...-error.test.js => error-non-error.test.ts} | 8 +- ...upport.test.js => featuresSupport.test.ts} | 14 +- .../test/{fetch.test.js => fetch.test.ts} | 4 +- .../astro.config.mjs | 7 - .../alias-tsconfig-baseurl-only/package.json | 10 - .../src/components/Alias.svelte | 4 - .../src/components/Client.svelte | 2 - .../src/components/Foo.astro | 1 - .../src/components/Style.astro | 2 - .../src/pages/index.astro | 27 - .../src/styles/extra.css | 3 - .../src/styles/main.css | 5 - .../src/utils/constants.js | 3 - .../src/utils/index.js | 1 - .../alias-tsconfig-baseurl-only/tsconfig.json | 7 - .../asset-query-params-chunks/package.json | 8 + .../src/components/CounterA.astro | 6 + .../src/components/CounterB.astro | 6 + .../src/components/DynamicLoader.astro | 6 + .../src/components/shared.js | 18 + .../src/pages/index.astro | 13 + .../src/pages/generator.astro} | 0 .../fixtures/astro-components/package.json | 8 - .../package.json | 8 - .../public/test.html | 0 .../src}/components/Slot.astro | 0 .../src/pages/set-html-children.astro} | 2 +- .../src/pages/set-html-fetch.astro} | 0 .../src/pages/set-html-types.astro} | 0 .../src/components/Client.jsx | 0 .../src/components/Slotted.astro | 0 .../src/components/Thing.jsx | 0 .../src/pages/fallback.astro} | 0 .../src/pages/slot-with-client.astro} | 0 .../test/fixtures/astro-fallback/package.json | 10 - .../public/external-file.js | 0 .../src/pages/external-files.astro} | 0 .../fixtures/astro-sitemap-rss/package.json | 8 - .../astro-sitemap-rss/src/pages/404.astro | 10 - .../src/pages/episode/fazers.md | 13 - .../src/pages/episode/rap-snitch-knishes.md | 13 - .../src/pages/episode/rhymes-like-dimes.md | 14 - .../src/pages/episodes/[...page].astro | 58 -- .../astro-slot-with-client/astro.config.mjs | 8 - .../astro-slot-with-client/package.json | 9 - .../src/components/Card.astro | 0 .../src/pages/unused-slot.astro} | 0 .../test/fixtures/config-host/package.json | 8 - .../fixtures/config-path/config/my-config.mjs | 6 - .../test/fixtures/config-path/package.json | 8 - .../astro.config.mjs | 0 .../package.json | 2 +- .../src/assets/test-image.png | Bin 0 -> 70 bytes .../src/content.config.ts | 16 + .../src/content/blog/post-1.md | 8 + .../src/pages/blog/[...slug].astro | 26 + .../src/pages/index.astro | 12 + .../.gitignore | 1 - .../astro.config.mjs | 12 - .../lockfile-mismatch/content/manifest.json | 1 - .../version-mismatch/content/manifest.json | 1 - .../package.json | 8 - .../src/content.config.ts | 12 - .../src/pages/index.astro | 10 - .../astro.config.mjs | 11 - .../package.json | 8 - .../src/content/docs/one.md | 8 - .../src/content/docs/two.md | 7 - .../src/pages/docs.astro | 17 - .../fixtures/content-frontmatter/package.json | 8 + .../src/content.config.ts | 9 +- .../src/content/posts/blog.md} | 2 - .../content-frontmatter/src/pages/index.astro | 8 + .../package.json | 4 +- .../src/content.config.ts | 2 +- .../fixtures/core-image-base/package.json | 11 - .../core-image-base/src/assets/penguin1.jpg | Bin 11621 -> 0 bytes .../core-image-base/src/assets/penguin2.jpg | Bin 11677 -> 0 bytes .../core-image-base/src/content.config.ts | 18 - .../core-image-base/src/content/blog/one.md | 10 - .../core-image-base/src/pages/alias.astro | 5 - .../src/pages/aliasMarkdown.md | 3 - .../src/pages/blog/[...slug].astro | 32 - .../core-image-base/src/pages/format.astro | 18 - .../core-image-base/src/pages/get-image.astro | 8 - .../core-image-base/src/pages/index.astro | 18 - .../core-image-base/src/pages/post.md | 3 - .../core-image-base/src/pages/quality.astro | 22 - .../fixtures/core-image-base/tsconfig.json | 12 - .../penguin-imported.jpg | Bin 11677 -> 0 bytes .../src/pages/direct.astro | 0 .../core-image-svg-optimized/astro.config.mjs | 15 - .../core-image-svg-optimized/tsconfig.json | 10 - .../src/assets/unoptimized.svg | 0 .../src/pages/optimized.astro | 0 .../core-image/src/pages/outsideProject.astro | 2 +- .../css-inline-stylesheets-2/astro.config.mjs | 5 - .../css-inline-stylesheets-2/package.json | 8 - .../src/components/Button.astro | 86 -- .../src/content.config.ts | 13 - .../src/content/en/endeavour.md | 14 - .../css-inline-stylesheets-2/src/imported.css | 15 - .../src/layouts/Layout.astro | 35 - .../src/pages/endeavour.md | 15 - .../src/pages/index.astro | 20 - .../css-inline-stylesheets-3/package.json | 8 - .../src/components/Button.astro | 86 -- .../src/content/en/endeavour.md | 14 - .../css-inline-stylesheets-3/src/imported.css | 15 - .../src/layouts/Layout.astro | 35 - .../src/pages/index.astro | 17 - .../src/components/Item.astro | 0 .../src/pages/transparent.astro} | 0 .../css-order-transparent/package.json | 7 - .../custom-500-middleware/astro.config.mjs | 4 - .../custom-500-middleware/package.json | 8 - .../custom-500-middleware/src/middleware.js | 4 - .../custom-500-middleware/src/pages/500.astro | 17 - .../src/pages/index.astro | 11 - .../test/fixtures/dev-container/package.json | 8 + .../fixtures/dev-container/public/test.txt | 1 + .../dev-container/src/components/404.astro | 1 + .../dev-container/src/components/test.astro | 1 + .../dev-container/src/pages/index.astro | 9 + .../dev-container/src/pages/page.astro | 1 + .../dev-container/src/pages/test-[slug].astro | 1 + .../fixtures/dev-error-pages/package.json | 8 + .../dev-error-pages/src/pages/404.astro | 1 + .../dev-error-pages/src/pages/500.astro | 1 + .../dev-error-pages/src/pages/index.astro | 1 + .../dev-error-pages/src/pages/throwing.astro | 3 + .../test/fixtures/dev-render/package.json | 8 + .../src/components/BothFlipped.astro | 1 + .../src/components/BothLiteral.astro | 1 + .../src/components/BothSpread.astro | 1 + .../dev-render/src/components/Class.astro | 1 + .../dev-render/src/components/ClassList.astro | 1 + .../src/components/NullComponent.astro | 3 + .../fixtures/dev-render/src/pages/chunk.astro | 4 + .../dev-render/src/pages/class-merge.astro | 12 + .../src/pages/custom-elements.astro | 11 + .../fixtures/dev-render/src/pages/index.astro | 12 + .../dev-render/src/pages/null-component.astro | 4 + .../dev-render/src/pages/sub/index.astro | 1 + .../fixtures/dev-request-url/package.json | 8 + .../src/pages/prerendered.astro | 4 + .../dev-request-url/src/pages/url.astro | 1 + .../fixtures/endpoint-routing/package.json | 8 + .../endpoint-routing/src/pages/headers.ts | 1 + .../endpoint-routing/src/pages/incorrect.ts | 1 + .../src/pages/internal-error.ts | 1 + .../src/pages/multi-headers.js | 10 + .../endpoint-routing/src/pages/not-found.ts | 1 + .../src/pages/response-redirect.ts | 1 + .../endpoint-routing/src/pages/response.ts | 1 + .../endpoint-routing/src/pages/setCookies.js | 8 + .../endpoint-routing/src/pages/streaming.js | 22 + .../astro.config.mjs | 29 - .../package.json | 15 - .../src/pages/index.astro | 0 .../test/fixtures/head-injection/package.json | 8 - .../src/components/Layout.astro | 18 - .../src/components/RegularSlot.astro | 8 - .../src/components/SlotRenderComponent.astro | 12 - .../src/components/SlotRenderLayout.astro | 7 - .../src/components/SlotsRender.astro | 25 - .../src/components/UsesSlotRender.astro | 7 - .../components/with-slot-render2/inner.astro | 10 - .../slots-render-outer.astro | 5 - .../head-injection/src/pages/index.md | 7 - .../with-render-slot-in-head-buffer.astro | 7 - .../src/pages/with-slot-in-render-slot.astro | 24 - .../src/pages/with-slot-in-slot.astro | 11 - .../src/pages/with-slot-render.astro | 9 - .../src/pages/with-slot-render2.astro | 19 - .../astro/test/fixtures/hmr-css/package.json | 8 - .../i18n-css-leak-basic/astro.config.mjs | 14 + .../package.json | 2 +- .../src/components/Header.astro | 9 + .../src/layouts/DocsLayout.astro | 12 + .../src/layouts/SiteLayout.astro | 14 + .../src/pages/docs/index.astro | 7 + .../i18n-css-leak-basic/src/pages/index.astro | 7 + .../i18n-css-leak-basic/src/styles/docs.css | 7 + .../i18n-css-leak-basic/src/styles/site.css | 3 + .../i18n-routing-base/astro.config.mjs | 17 - .../fixtures/i18n-routing-base/package.json | 8 - .../src/pages/en/blog/[id].astro | 18 - .../src/pages/en/start.astro | 8 - .../i18n-routing-base/src/pages/index.astro | 8 - .../src/pages/pt/blog/[id].astro | 18 - .../src/pages/pt/start.astro | 8 - .../src/pages/spanish/blog/[id].astro | 18 - .../src/pages/spanish/start.astro | 12 - .../i18n-routing-dynamic/astro.config.mjs | 13 - .../i18n-routing-dynamic/package.json | 8 - .../src/pages/[language].astro | 11 - .../src/pages/index.astro | 0 .../astro.config.mjs | 20 - .../package.json | 9 - .../src/pages/[slug].astro | 13 - .../src/pages/about.astro | 5 - .../src/pages/es/index.astro | 5 - .../src/pages/index.astro | 5 - .../astro.config.mjs | 17 - .../package.json | 8 - .../src/middleware.js | 24 - .../src/pages/about.astro | 8 - .../src/pages/blog.astro | 8 - .../src/pages/en/blog/[id].astro | 18 - .../src/pages/en/start.astro | 13 - .../src/pages/index.astro | 8 - .../src/pages/pt/blog/[id].astro | 18 - .../src/pages/pt/start.astro | 12 - .../src/pages/spanish/index.astro | 14 - .../i18n-routing-manual/astro.config.mjs | 14 - .../fixtures/i18n-routing-manual/package.json | 8 - .../i18n-routing-manual/src/middleware.js | 20 - .../i18n-routing-manual/src/pages/404.astro | 12 - .../src/pages/en/blog/[id].astro | 18 - .../src/pages/en/blog/index.astro | 8 - .../src/pages/en/index.astro | 8 - .../src/pages/en/start.astro | 8 - .../i18n-routing-manual/src/pages/help.astro | 11 - .../i18n-routing-manual/src/pages/index.astro | 8 - .../src/pages/pt/blog/[id].astro | 18 - .../src/pages/pt/start.astro | 12 - .../src/pages/spanish/index.astro | 14 - .../astro.config.mjs | 17 - .../package.json | 8 - .../src/pages/en/blog/[id].astro | 18 - .../src/pages/en/end.astro | 8 - .../src/pages/en/start.astro | 8 - .../src/pages/index.astro | 8 - .../src/pages/preferred-locale.astro | 13 - .../src/pages/pt/blog/[id].astro | 18 - .../src/pages/pt/start.astro | 8 - .../i18n-routing-subdomain/astro.config.mjs | 28 - .../i18n-routing-subdomain/package.json | 8 - .../src/pages/en/blog/[id].astro | 18 - .../src/pages/en/index.astro | 8 - .../src/pages/en/start.astro | 8 - .../src/pages/index.astro | 19 - .../src/pages/pt/blog/[id].astro | 18 - .../src/pages/pt/start.astro | 8 - .../i18n-server-island/astro.config.mjs | 17 - .../fixtures/i18n-server-island/package.json | 8 - .../src/components/Island.astro | 1 - .../src/pages/en/island.astro | 5 - .../i18n-server-island/src/pages/index.astro | 8 - .../package.json | 8 - .../src/middleware.js | 17 - .../src/pages/index.astro | 1 - .../fixtures/middleware-ssg/astro.config.mjs | 5 - .../test/fixtures/middleware-ssg/package.json | 8 - .../fixtures/middleware-ssg/src/middleware.js | 12 - .../middleware-ssg/src/pages/index.astro | 14 - .../middleware-ssg/src/pages/second.astro | 13 - .../middleware-virtual/astro.config.mjs | 3 - .../fixtures/middleware-virtual/package.json | 8 - .../middleware-virtual/src/middleware.js | 6 - .../middleware-virtual/src/pages/index.astro | 13 - .../fixtures/redirects/src/pages/late.astro | 2 +- .../astro/test/fixtures/set-html/package.json | 8 - .../test/fixtures/ssr-env/astro.config.mjs | 5 - .../astro/test/fixtures/ssr-env/package.json | 10 - .../test/fixtures/ssr-markdown/package.json | 8 - .../src/layouts/Base.astro | 0 .../src/pages/post.md | 0 .../ssr-script/src/pages/dynamic.astro | 11 + .../ssr-script/src/scripts/confetti.js | 3 + .../src/components/Env.jsx | 0 .../src/pages/ssr.astro | 0 .../ssr-split-manifest/astro.config.mjs | 7 - .../fixtures/ssr-split-manifest/package.json | 8 - .../src/pages/[...post].astro | 18 - .../ssr-split-manifest/src/pages/index.astro | 17 - .../ssr-split-manifest/src/pages/lorem.md | 1 - .../src/pages/prerender.astro | 12 - .../ssr-split-manifest/src/pages/zod.astro | 17 - .../ssr-trailing-slash/astro.config.mjs | 9 - .../fixtures/ssr-trailing-slash/package.json | 13 - .../ssr-trailing-slash/src/pages/index.astro | 10 - .../src/pages/fragment-streaming.astro | 28 + .../test/fixtures/unused-slot/package.json | 8 - .../with-endpoint-routes/package.json | 8 - .../with-endpoint-routes/src/astro.png | Bin 2573 -> 0 bytes .../with-endpoint-routes/src/pages/404.astro | 1 - .../src/pages/[slug].json.ts | 13 - .../src/pages/data/[slug].json.ts | 13 - .../src/pages/home.json.ts | 3 - .../src/pages/images/[image].svg.ts | 16 - .../src/pages/images/hex.ts | 6 - .../src/pages/images/static.svg.ts | 12 - .../src/pages/invalid-redirect.json.ts | 11 - .../with-endpoint-routes/src/pages/not-ok.ts | 5 - .../with-subpath-no-trailing-slash/.gitignore | 1 - .../astro.config.mjs | 4 - .../package.json | 8 - .../src/pages/[id].astro | 6 - .../src/pages/another.astro | 1 - .../src/pages/index.astro | 1 - .../fixtures/without-site-config/package.json | 8 - .../without-site-config/src/pages/[id].astro | 6 - .../src/pages/another.astro | 1 - .../src/pages/base-index.astro | 1 - .../src/pages/html-ext/[slug].astro | 6 - .../src/pages/html-ext/[slug].html.astro | 6 - .../without-site-config/src/pages/index.astro | 1 - .../src/pages/redirect.astro | 4 - .../without-site-config/src/pages/te st.astro | 1 - ...343\203\206\343\202\271\343\203\210.astro" | 1 - .../test/{fonts.test.js => fonts.test.ts} | 18 +- ...{fontsource.test.js => fontsource.test.ts} | 6 +- packages/astro/test/hmr-markdown.test.js | 55 +- packages/astro/test/hmr-new-page.test.js | 84 +- packages/astro/test/hmr-slots-render.test.js | 52 +- packages/astro/test/i18n-css-leak.test.js | 40 + .../test/integration-route-setup-hook.test.js | 42 + .../astro/test/integration-test-helpers.js | 60 ++ packages/astro/test/live-loaders.test.js | 6 +- ...age-format.test.js => page-format.test.ts} | 14 +- ...yles.test.js => page-level-styles.test.ts} | 4 +- .../{parallel.test.js => parallel.test.ts} | 10 +- .../{partials.test.js => partials.test.ts} | 8 +- ...t.js => passthrough-image-service.test.ts} | 21 +- .../test/{postcss.test.js => postcss.test.ts} | 8 +- ...est.js => preact-compat-component.test.ts} | 8 +- ...onent.test.js => preact-component.test.ts} | 18 +- ...ict.test.js => prerender-conflict.test.ts} | 54 +- ...se-404.test.js => public-base-404.test.ts} | 9 +- ...-solid.test.js => react-and-solid.test.ts} | 5 +- ...xport.test.js => react-jsx-export.test.ts} | 21 +- .../{redirects.test.js => redirects.test.ts} | 10 +- ...astro-containing-client-component.test.ts} | 4 +- ...{remote-css.test.js => remote-css.test.ts} | 6 +- packages/astro/test/request-signal.test.js | 2 +- ...t.js => reuse-injected-entrypoint.test.ts} | 24 +- .../test/{rewrite.test.js => rewrite.test.ts} | 32 +- ...ir-css.test.js => root-srcdir-css.test.ts} | 6 +- ...oute-guard.test.js => route-guard.test.ts} | 8 +- ....test.js => scoped-style-strategy.test.ts} | 23 +- ...fest.test.js => serializeManifest.test.ts} | 17 +- ...ver-entry.test.js => server-entry.test.ts} | 35 +- ...islands.test.js => server-islands.test.ts} | 33 +- packages/astro/test/set-html.test.js | 57 -- ...ts-preact.test.js => slots-preact.test.ts} | 4 +- ...lots-react.test.js => slots-react.test.ts} | 4 +- ...lots-solid.test.js => slots-solid.test.ts} | 4 +- ...ts-svelte.test.js => slots-svelte.test.ts} | 4 +- .../{slots-vue.test.js => slots-vue.test.ts} | 4 +- .../{sourcemap.test.js => sourcemap.test.ts} | 6 +- ...e.test.js => space-in-folder-name.test.ts} | 7 +- ...pecial-chars-in-component-imports.test.ts} | 19 +- ...st.js => ssr-adapter-build-config.test.ts} | 9 +- ...pi-route.test.js => ssr-api-route.test.ts} | 22 +- ...{ssr-assets.test.js => ssr-assets.test.ts} | 6 +- ...sr-dynamic.test.js => ssr-dynamic.test.ts} | 11 +- ...-array.test.js => ssr-large-array.test.ts} | 5 +- packages/astro/test/ssr-markdown.test.js | 33 - ...artytown.test.js => ssr-partytown.test.ts} | 5 +- ...=> ssr-prerender-get-static-paths.test.ts} | 29 +- ...rerender.test.js => ssr-prerender.test.ts} | 25 +- ...sr-preview.test.js => ssr-preview.test.ts} | 6 +- ...st.js => ssr-renderers-static-vue.test.ts} | 5 +- ...sr-request.test.js => ssr-request.test.ts} | 9 +- ...{ssr-script.test.js => ssr-script.test.ts} | 64 +- packages/astro/test/ssr-scripts.test.js | 26 - .../{ssr-env.test.js => ssr-scripts.test.ts} | 16 +- ...js => static-build-code-component.test.ts} | 4 +- ...d-dir.test.js => static-build-dir.test.ts} | 8 +- ...est.js => static-build-frameworks.test.ts} | 4 +- ....js => static-build-page-dist-url.test.ts} | 4 +- ...s => static-build-page-url-format.test.ts} | 4 +- ...t.js => static-build-vite-plugins.test.ts} | 7 +- packages/astro/test/static-build.test.js | 12 +- packages/astro/test/streaming.test.js | 20 + ...tion.test.js => svg-deduplication.test.ts} | 10 +- packages/astro/test/test-adapter.js | 6 +- packages/astro/test/test-utils.js | 18 +- packages/astro/test/types/schemas.ts | 6 +- packages/astro/test/types/tsconfig.json | 4 +- .../test/units/_temp-fixtures/package.json | 8 - ...ion-error.test.js => action-error.test.ts} | 14 +- ...ction-path.test.js => action-path.test.ts} | 1 - ...n-status.test.js => action-status.test.ts} | 44 +- ...ctions-app.test.js => actions-app.test.ts} | 25 +- ...ns-proxy.test.js => actions-proxy.test.ts} | 37 +- ...ct.test.js => form-data-to-object.test.ts} | 6 +- .../{serialize.test.js => serialize.test.ts} | 11 +- ...stro-attrs.test.js => astro-attrs.test.ts} | 44 +- ...esponse.test.js => astro-response.test.ts} | 17 +- .../units/app/{csrf.test.js => csrf.test.ts} | 22 +- ...n.test.js => dev-url-construction.test.ts} | 24 +- ...ss.test.js => double-slash-bypass.test.ts} | 56 +- ...st.js => encoded-backslash-bypass.test.ts} | 45 +- ...rror-pages.test.js => error-pages.test.ts} | 161 ++-- .../app/{headers.test.js => headers.test.ts} | 0 .../app/{locals.test.js => locals.test.ts} | 64 +- .../units/app/{node.test.js => node.test.ts} | 74 +- .../{response.test.js => response.test.ts} | 28 +- .../app/{test-helpers.js => test-helpers.ts} | 48 +- ...g-slash.test.js => trailing-slash.test.ts} | 96 +- ...-xss.test.js => url-attribute-xss.test.ts} | 1 - .../units/assets/endpoint-svg-reject.test.ts | 61 ++ .../fonts/{core.test.js => core.test.ts} | 26 +- .../assets/fonts/{e2e.test.js => e2e.test.ts} | 31 +- .../fonts/{infra.test.js => infra.test.ts} | 67 +- .../{providers.test.js => providers.test.ts} | 1 - .../astro/test/units/assets/fonts/utils.js | 166 ---- .../fonts/{utils.test.js => utils.test.ts} | 1 - .../astro/test/units/assets/fonts/utils.ts | 135 +++ .../{getImage.test.js => getImage.test.ts} | 22 +- ...ge-layout.test.js => image-layout.test.ts} | 0 ...-service.test.js => image-service.test.ts} | 6 +- .../assets/{remote.test.js => remote.test.ts} | 28 +- .../astro/test/units/assets/utils.test.ts | 270 ++++++ .../{generate.test.js => generate.test.ts} | 74 +- ...t.js => preserve-build-client-dir.test.ts} | 31 +- ...islands.test.js => server-islands.test.ts} | 11 +- ...tic-build.test.js => static-build.test.ts} | 29 +- .../{test-helpers.js => test-helpers.ts} | 107 +-- ...ovider.test.js => memory-provider.test.ts} | 171 ++-- .../cache/{noop.test.js => noop.test.ts} | 18 +- ...atching.test.js => route-matching.test.ts} | 3 +- .../{runtime.test.js => runtime.test.ts} | 9 +- .../cache/{utils.test.js => utils.test.ts} | 2 +- packages/astro/test/units/cli/utils.ts | 6 +- ...ase-path.test.js => css-base-path.test.ts} | 55 +- ...nvalid-css.test.js => invalid-css.test.ts} | 6 +- ...compiler.test.js => rust-compiler.test.ts} | 16 +- ...fig-merge.test.js => config-merge.test.ts} | 8 +- ...resolve.test.js => config-resolve.test.ts} | 0 ...g-server.test.js => config-server.test.ts} | 18 +- ...config.test.js => config-tsconfig.test.ts} | 26 +- ...lidate.test.js => config-validate.test.ts} | 50 +- .../astro/test/units/config/format.test.js | 24 - .../units/config/refined-validators.test.ts | 444 +++++++++ .../content-collections/frontmatter.test.js | 73 -- ...ry-info.test.js => get-entry-info.test.ts} | 0 ...ry-type.test.js => get-entry-type.test.ts} | 0 ...ences.test.js => image-references.test.ts} | 18 +- .../mutable-data-store.test.js | 48 - .../mutable-data-store.test.ts | 153 +++ ...ore-loader.test.js => core-loader.test.ts} | 22 +- ...sforms.test.js => data-transforms.test.ts} | 76 +- ...ile-loader.test.js => file-loader.test.ts} | 36 +- ...lob-loader.test.js => glob-loader.test.ts} | 58 +- ...e-loaders.test.js => live-loaders.test.ts} | 96 +- ...rnings.test.js => loader-warnings.test.ts} | 90 +- ...ing.test.js => markdown-rendering.test.ts} | 92 +- ...tion.test.js => schema-validation.test.ts} | 80 +- ...ence.test.js => store-persistence.test.ts} | 4 +- .../{test-helpers.js => test-helpers.ts} | 61 +- .../{delete.test.js => delete.test.ts} | 10 +- .../cookies/{error.test.js => error.test.ts} | 4 +- .../cookies/{get.test.js => get.test.ts} | 32 +- .../cookies/{has.test.js => has.test.ts} | 0 .../cookies/{merge.test.js => merge.test.ts} | 0 .../cookies/{set.test.js => set.test.ts} | 14 +- .../csp/{common.test.js => common.test.ts} | 11 +- .../{rendering.test.js => rendering.test.ts} | 128 +-- .../csp/{runtime.test.js => runtime.test.ts} | 0 .../astro/test/units/dev/base-rewrite.test.ts | 160 ++++ packages/astro/test/units/dev/base.test.js | 112 --- packages/astro/test/units/dev/dev.test.js | 196 ---- .../astro/test/units/dev/error-pages.test.js | 290 ------ .../astro/test/units/dev/error-pages.test.ts | 98 ++ .../astro/test/units/dev/hydration.test.js | 50 - packages/astro/test/units/dev/restart.test.js | 233 ----- .../{sec-fetch.test.js => sec-fetch.test.ts} | 10 +- .../units/dev/trailing-slash-decision.test.ts | 150 +++ ...idators.test.js => env-validators.test.ts} | 53 +- .../{dev-utils.test.js => dev-utils.test.ts} | 0 .../errors/{errors.test.js => errors.test.ts} | 0 .../test/units/errors/zod-error-map.test.ts | 193 ++++ ...{astro_i18n.test.js => astro_i18n.test.ts} | 445 ++++----- ...nifest.test.js => create-manifest.test.ts} | 10 +- .../{fallback.test.js => fallback.test.ts} | 119 ++- .../{i18n-app.test.js => i18n-app.test.ts} | 130 ++- ...leware.test.js => i18n-middleware.test.ts} | 81 +- ...ic.test.js => i18n-routing-static.test.ts} | 50 +- ...uild.test.js => i18n-static-build.test.ts} | 11 +- ...{i18n-utils.test.js => i18n-utils.test.ts} | 17 +- ...ware.test.js => manual-middleware.test.ts} | 89 +- ...routing.test.js => manual-routing.test.ts} | 61 +- .../i18n/{router.test.js => router.test.ts} | 131 +-- .../astro/test/units/i18n/test-helpers.js | 168 ---- .../astro/test/units/i18n/test-helpers.ts | 119 +++ .../astro/test/units/integrations/api.test.js | 560 ----------- .../astro/test/units/integrations/api.test.ts | 302 ++++++ .../test/units/integrations/hooks.test.ts | 319 +++++++ .../test/units/logger/destination.test.ts | 172 ++++ .../logger/{locale.test.js => locale.test.ts} | 0 .../test/units/manifest/serialized.test.js | 227 +++++ ...leware.test.js => call-middleware.test.ts} | 50 +- .../{locals.test.js => locals.test.ts} | 6 +- ...are-app.test.js => middleware-app.test.ts} | 129 +-- .../{sequence.test.js => sequence.test.ts} | 76 +- .../astro/test/units/{mocks.js => mocks.ts} | 205 ++-- .../astro/test/units/preferences/dlv.test.ts | 20 + ...redirect.test.js => open-redirect.test.ts} | 0 .../{render.test.js => render.test.ts} | 35 +- ...tic-build.test.js => static-build.test.ts} | 54 +- .../{template.test.js => template.test.ts} | 12 +- ...pattern.test.js => remote-pattern.test.ts} | 16 +- .../astro/test/units/render/chunk.test.js | 46 - ...e.test.js => class-list-and-style.test.ts} | 3 +- .../test/units/render/components.test.js | 201 ---- ...elpers.test.js => context-helpers.test.ts} | 21 +- .../render/{escape.test.js => escape.test.ts} | 17 +- ...app.test.js => head-injection-app.test.ts} | 54 +- .../{boundary.test.js => boundary.test.ts} | 0 .../{buffer.test.js => buffer.test.ts} | 18 +- .../{comment.test.js => comment.test.ts} | 0 .../{graph.test.js => graph.test.ts} | 17 +- .../{policy.test.js => policy.test.ts} | 0 .../{resolver.test.js => resolver.test.ts} | 2 +- ...pters.test.js => runtime-adapters.test.ts} | 15 +- .../{runtime.test.js => runtime.test.ts} | 19 +- .../render/{head.test.js => head.test.ts} | 41 +- ...itives.test.js => html-primitives.test.ts} | 105 ++- .../{hydration.test.js => hydration.test.ts} | 7 +- .../{paginate.test.js => paginate.test.ts} | 7 +- ...atching.test.js => queue-batching.test.ts} | 27 +- ...{queue-pool.test.js => queue-pool.test.ts} | 30 +- ...dering.test.js => queue-rendering.test.ts} | 127 +-- ...context.test.js => render-context.test.ts} | 31 +- .../{rendering.test.js => rendering.test.ts} | 45 +- ...-elements.test.js => ssr-elements.test.ts} | 1 - ...ransitions.test.js => transitions.test.ts} | 1 - ...pi-context.test.js => api-context.test.ts} | 8 +- ...ev-routing.test.js => dev-routing.test.ts} | 2 +- ...est.js => dynamic-route-collision.test.ts} | 2 +- .../test/units/routing/endpoints.test.js | 89 -- .../{generator.test.js => generator.test.ts} | 18 +- ...{get-params.test.js => get-params.test.ts} | 9 +- ...e.test.js => getstaticpaths-cache.test.ts} | 42 +- .../{manifest.test.js => manifest.test.ts} | 192 +++- ...thname.test.js => origin-pathname.test.ts} | 0 ...coding.test.js => params-encoding.test.ts} | 8 +- ...tion.test.js => params-validation.test.ts} | 33 +- .../{prerender.test.js => prerender.test.ts} | 0 ...outing.test.js => preview-routing.test.ts} | 5 +- .../units/routing/resolved-pathname.test.js | 91 -- .../units/routing/resolved-pathname.test.ts | 67 ++ ...ewrite-app.test.js => rewrite-app.test.ts} | 65 +- ...ion.test.js => rewrite-validation.test.ts} | 17 +- .../{rewrite.test.js => rewrite.test.ts} | 1 + ...anifest.test.js => route-manifest.test.ts} | 55 +- .../test/units/routing/route-matching.test.js | 298 ------ .../test/units/routing/route-matching.test.ts | 178 ++++ .../units/routing/route-sanitization.test.js | 64 -- .../units/routing/route-sanitization.test.ts | 31 + ...ter-match.test.js => router-match.test.ts} | 2 +- .../units/routing/routing-helpers.test.ts | 32 + ...ority.test.js => routing-priority.test.ts} | 2 +- .../astro/test/units/routing/test-helpers.js | 58 -- .../astro/test/units/routing/test-helpers.ts | 54 ++ .../test/units/routing/trailing-slash.test.js | 277 ------ .../test/units/routing/trailing-slash.test.ts | 213 +++++ ...-routes.test.js => virtual-routes.test.ts} | 2 +- .../test/units/runtime/endpoints.test.js | 80 -- ...tic-paths.test.js => static-paths.test.ts} | 54 +- ...{encryption.test.js => encryption.test.ts} | 0 .../{endpoint.test.js => endpoint.test.ts} | 51 +- ....test.js => server-islands-render.test.ts} | 70 +- ...red-state.test.js => shared-state.test.ts} | 0 ...-session.test.js => astro-session.test.ts} | 189 ++-- .../test/units/{teardown.js => teardown.ts} | 2 +- packages/astro/test/units/test-utils.js | 233 ----- packages/astro/test/units/test-utils.ts | 249 +++++ ...{controller.test.js => controller.test.ts} | 68 +- .../vite-plugin-astro-server/request.test.js | 65 -- .../vite-plugin-astro-server/response.test.js | 125 --- .../{compile.test.js => compile.test.ts} | 56 +- .../{hmr.test.js => hmr.test.ts} | 0 .../{escape.test.js => escape.test.ts} | 13 +- .../{slots.test.js => slots.test.ts} | 2 +- .../{transform.test.js => transform.test.ts} | 20 +- packages/astro/test/unused-slot.test.js | 20 - packages/astro/tsconfig.json | 5 +- packages/astro/tsconfig.test.json | 28 +- packages/create-astro/package.json | 3 +- .../test/{context.test.js => context.test.ts} | 22 +- ...endencies.test.js => dependencies.test.ts} | 59 +- .../test/{git.test.js => git.test.ts} | 28 +- ...egrations.test.js => integrations.test.ts} | 50 +- .../test/{intro.test.js => intro.test.ts} | 16 +- .../test/{next.test.js => next.test.ts} | 16 +- ...est.js => package-name-validation.test.ts} | 0 ...ject-name.test.js => project-name.test.ts} | 91 +- ...ng.test.js => template-processing.test.ts} | 0 .../{template.test.js => template.test.ts} | 43 +- packages/create-astro/test/utils.js | 32 - packages/create-astro/test/utils.ts | 62 ++ .../test/{verify.test.js => verify.test.ts} | 18 +- packages/create-astro/tsconfig.test.json | 16 + packages/db/package.json | 5 +- .../test/{basics.test.js => basics.test.ts} | 20 +- .../{db-in-src.test.js => db-in-src.test.ts} | 6 +- ...andling.test.js => error-handling.test.ts} | 21 +- ...-only.test.js => integration-only.test.ts} | 6 +- ...egrations.test.js => integrations.test.ts} | 6 +- ...l-remote.test.js => libsql-remote.test.ts} | 12 +- ...{local-prod.test.js => local-prod.test.ts} | 10 +- .../test/{no-seed.test.js => no-seed.test.ts} | 6 +- ...ptoken.test.js => ssr-no-apptoken.test.ts} | 8 +- ...c-remote.test.js => static-remote.test.ts} | 10 +- .../db/test/{test-utils.js => test-utils.ts} | 30 +- ...queries.test.js => column-queries.test.ts} | 59 +- .../{db-client.test.js => db-client.test.ts} | 0 ...-queries.test.js => index-queries.test.ts} | 29 +- ...ries.test.js => reference-queries.test.ts} | 20 +- ...emote-info.test.js => remote-info.test.ts} | 2 +- ...-queries.test.js => reset-queries.test.ts} | 10 +- packages/db/tsconfig.test.json | 12 + packages/integrations/alpinejs/package.json | 3 +- .../test/{basics.test.js => basics.test.ts} | 0 .../{directive.test.js => directive.test.ts} | 0 ...t.test.js => plugin-script-import.test.ts} | 0 .../test/{test-utils.js => test-utils.ts} | 21 +- .../integrations/alpinejs/tsconfig.test.json | 17 + packages/integrations/cloudflare/CHANGELOG.md | 44 + packages/integrations/cloudflare/package.json | 6 +- packages/integrations/cloudflare/src/index.ts | 10 +- packages/integrations/cloudflare/src/info.ts | 5 - .../cloudflare/src/prerenderer.ts | 19 +- .../src/utils/image-binding-transform.ts | 9 +- ...-plugin-dev-server-prerender-middleware.ts | 8 - .../integrations/cloudflare/src/wrangler.ts | 14 +- ...orm.test.js => astro-dev-platform.test.ts} | 6 +- .../{astro-env.test.js => astro-env.test.ts} | 12 +- ....test.js => binding-image-service.test.ts} | 33 +- ...address.test.js => client-address.test.ts} | 7 +- ....test.js => compile-image-service.test.ts} | 13 +- ...yfile.test.js => custom-entryfile.test.ts} | 6 +- ...int.test.js => dev-image-endpoint.test.ts} | 7 +- ...test.js => external-image-service.test.ts} | 12 +- ...cts.test.js => external-redirects.test.ts} | 5 +- .../binding-image-service/astro.config.mjs | 3 + .../prerender-node-env/astro.config.mjs | 5 +- .../fake-svelte-pkg/package.json | 11 + .../fake-svelte-pkg/src/FakeComponent.svelte | 1 + .../fake-svelte-pkg/src/index.js | 1 + .../fixtures/prerender-node-env/package.json | 5 +- .../src/components/SvelteWrapper.svelte | 7 + .../prerender-node-env/src/pages/svelte.astro | 15 + .../astro.config.mjs | 7 + .../prerender-queue-consumers/package.json | 9 + .../src/pages/api.ts | 9 + .../src/pages/index.astro | 10 + .../prerender-queue-consumers/wrangler.jsonc | 18 + ...cts.test.js => internal-redirects.test.ts} | 5 +- ...env.test.js => prerender-node-env.test.ts} | 24 +- .../test/prerender-queue-consumers.test.ts | 32 + ...tyles.test.js => prerender-styles.test.ts} | 14 +- ...ors.test.js => prerenderer-errors.test.ts} | 5 +- ...ority.test.js => routing-priority.test.ts} | 12 +- ...ver-entry.test.js => server-entry.test.ts} | 4 +- ...s => server-island-prerender-deps.test.ts} | 6 +- .../{sessions.test.js => sessions.test.ts} | 49 +- ...{sql-import.test.js => sql-import.test.ts} | 11 +- .../{ssr-deps.test.js => ssr-deps.test.ts} | 32 +- .../test/{static.test.js => static.test.ts} | 5 +- ...-deps.test.js => svelte-rune-deps.test.ts} | 7 +- .../test/{_test-utils.js => test-utils.ts} | 24 +- ...eturn.test.js => top-level-return.test.ts} | 32 +- .../{with-base.test.js => with-base.test.ts} | 32 +- ...{with-react.test.js => with-react.test.ts} | 34 +- ...solid-js.test.js => with-solid-js.test.ts} | 7 +- ...ith-svelte.test.js => with-svelte.test.ts} | 7 +- .../{with-vue.test.js => with-vue.test.ts} | 7 +- ...t.js => wrangler-preview-platform.test.ts} | 6 +- .../{wrangler.test.js => wrangler.test.ts} | 0 .../cloudflare/tsconfig.test.json | 12 + packages/integrations/markdoc/package.json | 3 +- ...ns.test.js => content-collections.test.ts} | 19 +- ...nt-layer.test.js => content-layer.test.ts} | 24 +- .../{headings.test.js => headings.test.ts} | 25 +- ...ge-assets.test.js => image-assets.test.ts} | 44 +- ...sets.test.js => propagated-assets.test.ts} | 20 +- ...ents.test.js => render-components.test.ts} | 24 +- ...t.js => render-extends-components.test.ts} | 15 +- ...ender-html.test.js => render-html.test.ts} | 135 ++- ....js => render-indented-components.test.ts} | 15 +- ...trs.test.js => render-table-attrs.test.ts} | 4 +- ....test.js => render-with-transform.test.ts} | 14 +- .../test/{render.test.js => render.test.ts} | 44 +- ...ng.test.js => syntax-highlighting.test.ts} | 84 +- .../{variables.test.js => variables.test.ts} | 6 +- .../integrations/markdoc/tsconfig.test.json | 17 + packages/integrations/mdx/package.json | 3 +- packages/integrations/mdx/src/server.ts | 3 +- packages/integrations/mdx/src/utils.ts | 2 +- .../mdx/src/vite-plugin-mdx-postprocess.ts | 10 +- ...-head-mdx.test.js => css-head-mdx.test.ts} | 4 +- .../astro.config.mjs | 3 +- .../mdx-astro-container-escape}/package.json | 3 +- .../src/components/Div.astro | 1 + .../src/pages/index.astro | 12 + .../src/posts/post.mdx | 9 + .../src/components/component}/Test.mdx | 0 .../components/component}/WithFragment.mdx | 0 .../src/components/slots}/Slotted.astro | 0 .../src/components/slots}/Test.mdx | 0 .../src/content/1.mdx | 0 .../src/layouts/Base.astro | 0 .../src/pages/component}/glob.astro | 2 +- .../src/pages/component/index.astro | 5 + .../src/pages/component/w-fragment.astro | 5 + .../src/pages/frontmatter}/glob.json.js | 0 .../src/pages/frontmatter}/index.mdx | 2 +- .../src/pages/frontmatter}/with-headings.mdx | 2 +- .../src/pages/script-style-raw.mdx} | 0 .../src/pages/slots}/glob.astro | 2 +- .../mdx-basics/src/pages/slots/index.astro | 5 + .../src/pages/static-paths}/[slug].astro | 2 +- .../src/pages/url-export}/pages.json.js | 0 .../src/pages/url-export/test-1.mdx | 1 + .../src/pages/url-export/test-2.mdx | 1 + .../pages/url-export}/with-url-override.mdx | 0 .../mdx-component/src/pages/index.astro | 5 - .../mdx-component/src/pages/w-fragment.astro | 5 - .../mdx-escape/src/components/Em.astro | 7 - .../mdx-escape/src/components/P.astro | 1 - .../mdx-escape/src/components/Title.astro | 1 - .../mdx-escape/src/pages/html-tag.mdx | 5 - .../fixtures/mdx-escape/src/pages/index.mdx | 13 - .../fixtures/mdx-slots/src/pages/index.astro | 5 - .../mdx-url-export/src/pages/test-1.mdx | 1 - .../mdx-url-export/src/pages/test-2.mdx | 1 - ....test.js => invalid-mdx-component.test.ts} | 11 +- .../test/mdx-astro-container-escape.test.ts | 27 + ...> mdx-astro-markdown-remarkRehype.test.ts} | 13 +- .../integrations/mdx/test/mdx-basics.test.ts | 460 +++++++++ .../mdx/test/mdx-component.test.js | 194 ---- ...ayer.test.js => mdx-content-layer.test.ts} | 12 +- .../integrations/mdx/test/mdx-escape.test.js | 32 - ...t.js => mdx-frontmatter-injection.test.ts} | 37 +- .../mdx/test/mdx-frontmatter.test.js | 78 -- ...dings.test.js => mdx-get-headings.test.ts} | 94 +- .../mdx/test/mdx-get-static-paths.test.js | 33 - ...{mdx-images.test.js => mdx-images.test.ts} | 22 +- ...loop.test.js => mdx-infinite-loop.test.ts} | 6 +- .../{mdx-math.test.js => mdx-math.test.ts} | 0 ...amespace.test.js => mdx-namespace.test.ts} | 14 +- ...-optimize.test.js => mdx-optimize.test.ts} | 21 +- .../{mdx-page.test.js => mdx-page.test.ts} | 18 +- .../integrations/mdx/test/mdx-plugins.test.js | 314 ------- .../integrations/mdx/test/mdx-plugins.test.ts | 182 ++++ ....test.js => mdx-plus-react-errors.test.ts} | 15 +- ...s-react.test.js => mdx-plus-react.test.ts} | 16 +- .../mdx/test/mdx-script-style-raw.test.js | 75 -- .../integrations/mdx/test/mdx-slots.test.js | 124 --- ...est.js => mdx-syntax-highlighting.test.ts} | 16 +- .../mdx/test/mdx-url-export.test.js | 28 - ...vars.test.js => mdx-vite-env-vars.test.ts} | 6 +- ...imgattr.test.js => remark-imgattr.test.ts} | 15 +- packages/integrations/mdx/test/test-utils.ts | 44 + .../mdx/test/units/mdx-compilation.test.ts | 274 ++++++ ...test.js => rehype-optimize-static.test.ts} | 16 +- .../mdx/test/units/rehype-plugins.test.ts | 160 ++++ .../mdx/test/units/server.test.ts | 44 + .../integrations/mdx/test/units/utils.test.ts | 176 ++++ .../units/vite-plugin-mdx-postprocess.test.ts | 238 +++++ packages/integrations/mdx/tsconfig.test.json | 13 + packages/integrations/netlify/CHANGELOG.md | 16 + packages/integrations/netlify/package.json | 11 +- .../integrations/netlify/src/image-service.ts | 3 + ...{primitives.test.js => primitives.test.ts} | 9 +- .../netlify/test/functions/cookies.test.js | 65 -- .../netlify/test/functions/cookies.test.ts | 59 ++ .../test/functions/edge-middleware.test.js | 66 -- .../test/functions/edge-middleware.test.ts | 60 ++ .../netlify/test/functions/image-cdn.test.js | 182 ---- .../netlify/test/functions/image-cdn.test.ts | 184 ++++ .../test/functions/include-files.test.js | 184 ---- .../test/functions/include-files.test.ts | 166 ++++ .../netlify/test/functions/redirects.test.js | 69 -- .../netlify/test/functions/redirects.test.ts | 62 ++ .../{sessions.test.js => sessions.test.ts} | 30 +- .../test/functions/skew-protection.test.js | 70 -- .../test/functions/skew-protection.test.ts | 64 ++ .../hosted/{hosted.test.js => hosted.test.ts} | 6 +- .../image-missing-dimension/astro.config.mjs | 12 + .../image-missing-dimension/package.json | 9 + .../src/pages/index.astro | 8 + .../{headers.test.js => headers.test.ts} | 4 +- .../static/image-missing-dimension.test.ts | 23 + .../{redirects.test.js => redirects.test.ts} | 13 +- ...headers.test.js => static-headers.test.ts} | 11 +- .../integrations/netlify/test/test-utils.ts | 37 + .../integrations/netlify/tsconfig.test.json | 13 + packages/integrations/node/CHANGELOG.md | 6 + packages/integrations/node/package.json | 6 +- .../integrations/node/src/serve-static.ts | 31 +- .../{api-route.test.js => api-route.test.ts} | 24 +- .../test/{assets.test.js => assets.test.ts} | 34 +- .../{bad-urls.test.js => bad-urls.test.ts} | 8 +- .../test/{encoded.test.js => encoded.test.ts} | 9 +- .../integrations/node/test/errors.test.js | 65 -- .../integrations/node/test/errors.test.ts | 52 + .../test/{headers.test.js => headers.test.ts} | 15 +- .../test/{image.test.js => image.test.ts} | 10 +- .../test/{locals.test.js => locals.test.ts} | 14 +- ... node-middleware-listener-cleanup.test.ts} | 14 +- ...leware.test.js => node-middleware.test.ts} | 38 +- ...-500.test.js => prerender-404-500.test.ts} | 26 +- .../{prerender.test.js => prerender.test.ts} | 49 +- ...s => prerendered-error-page-fetch.test.ts} | 21 +- ...eaders.test.js => preview-headers.test.ts} | 8 +- ...view-host.test.js => preview-host.test.ts} | 2 +- .../{redirects.test.js => redirects.test.ts} | 9 +- ...erver-host.test.js => server-host.test.ts} | 0 .../{sessions.test.js => sessions.test.ts} | 19 +- ...headers.test.js => static-headers.test.ts} | 28 +- packages/integrations/node/test/test-utils.js | 82 -- packages/integrations/node/test/test-utils.ts | 94 ++ ...g-slash.test.js => trailing-slash.test.ts} | 29 +- ...dir.test.js => resolve-client-dir.test.ts} | 0 ...js => serve-static-path-traversal.test.ts} | 6 +- .../node/test/{url.test.js => url.test.ts} | 34 +- ...s.test.js => well-known-locations.test.ts} | 12 +- packages/integrations/node/tsconfig.test.json | 17 + packages/integrations/partytown/CHANGELOG.md | 6 + packages/integrations/partytown/package.json | 4 +- packages/integrations/preact/CHANGELOG.md | 6 + packages/integrations/preact/package.json | 2 +- packages/integrations/preact/src/index.ts | 2 + packages/integrations/react/CHANGELOG.md | 6 + packages/integrations/react/package.json | 5 +- packages/integrations/react/src/server.ts | 7 + .../react-19-preloads}/astro.config.mjs | 6 +- .../fixtures/react-19-preloads/package.json | 10 + .../src/components/ImageComponent.jsx | 8 + .../react-19-preloads/src/pages/index.astro | 11 + ....test.js => parsed-react-children.test.ts} | 0 .../react/test/react-19-preloads.test.ts | 20 + ...ponent.test.js => react-component.test.ts} | 33 +- .../integrations/react/tsconfig.test.json | 17 + packages/integrations/sitemap/package.json | 3 +- .../{base-path.test.js => base-path.test.ts} | 4 +- ...nks-files.test.js => chunks-files.test.ts} | 22 +- .../test/{config.test.js => config.test.ts} | 4 +- ...tom-pages.test.js => custom-pages.test.ts} | 9 +- ...temaps.test.js => custom-sitemaps.test.ts} | 12 +- ...amic-path.test.js => dynamic-path.test.ts} | 6 +- ...fallback.test.js => i18n-fallback.test.ts} | 9 +- ...{namespaces.test.js => namespaces.test.ts} | 3 +- .../test/{routes.test.js => routes.test.ts} | 9 +- .../test/{smoke.test.js => smoke.test.ts} | 0 .../sitemap/test/{ssr.test.js => ssr.test.ts} | 4 +- ...taticPaths.test.js => staticPaths.test.ts} | 9 +- ...g-slash.test.js => trailing-slash.test.ts} | 4 +- ...temap.test.js => generate-sitemap.test.ts} | 0 .../integrations/sitemap/tsconfig.test.json | 17 + packages/integrations/svelte/CHANGELOG.md | 6 + packages/integrations/svelte/package.json | 8 +- packages/integrations/svelte/src/index.ts | 45 +- ...dering.test.js => async-rendering.test.ts} | 7 +- .../test/{check.test.js => check.test.ts} | 0 ....test.js => conditional-rendering.test.ts} | 6 +- ....test.js => empty-class-attribute.test.ts} | 8 +- ...erics.test.js => extract-generics.test.ts} | 0 .../integrations/svelte/tsconfig.test.json | 17 + packages/integrations/vercel/CHANGELOG.md | 6 + packages/integrations/vercel/package.json | 7 +- packages/integrations/vercel/src/index.ts | 31 +- .../vercel/src/serverless/middleware.ts | 4 +- ...leware.test.js => edge-middleware.test.ts} | 44 +- .../test/fixtures/image/astro.config.mjs | 2 +- .../hosted/{hosted.test.js => hosted.test.ts} | 0 .../test/{image.test.js => image.test.ts} | 25 +- ...ets.test.js => integration-assets.test.ts} | 6 +- .../vercel/test/{isr.test.js => isr.test.ts} | 7 +- ...-duration.test.js => max-duration.test.ts} | 7 +- ...test.js => path-override-security.test.ts} | 12 +- ...est.js => prerendered-error-pages.test.ts} | 9 +- ...s.test.js => redirects-serverless.test.ts} | 7 +- .../{redirects.test.js => redirects.test.ts} | 45 +- ...islands.test.js => server-islands.test.ts} | 9 +- ...r.test.js => serverless-prerender.test.ts} | 14 +- ...=> serverless-with-dynamic-routes.test.ts} | 8 +- ...c-assets.test.js => static-assets.test.ts} | 37 +- ...headers.test.js => static-headers.test.ts} | 10 +- .../test/{static.test.js => static.test.ts} | 7 +- .../{streaming.test.js => streaming.test.ts} | 7 +- .../vercel/test/test-image-service.js | 32 - .../integrations/vercel/test/test-utils.js | 8 - .../integrations/vercel/test/test-utils.ts | 32 + ...nalytics.test.js => web-analytics.test.ts} | 7 +- .../integrations/vercel/tsconfig.test.json | 13 + packages/integrations/vue/package.json | 3 +- ...css.test.js => app-entrypoint-css.test.ts} | 7 +- ...rypoint.test.js => app-entrypoint.test.ts} | 42 +- .../test/{basics.test.js => basics.test.ts} | 9 +- .../vue/test/{check.test.js => check.test.ts} | 0 .../vue/test/{test-utils.js => test-utils.ts} | 13 +- .../vue/test/{toTsx.test.js => toTsx.test.ts} | 0 packages/integrations/vue/tsconfig.test.json | 17 + packages/internal-helpers/package.json | 3 +- ...e-filter.test.js => create-filter.test.ts} | 0 .../test/{path.test.js => path.test.ts} | 5 +- .../test/{request.test.js => request.test.ts} | 2 +- packages/internal-helpers/tsconfig.test.json | 12 + .../language-tools/astro-check/package.json | 2 +- .../language-server/package.json | 2 +- .../language-server/src/check.ts | 12 +- .../src/core/frontmatterHolders.ts | 18 +- .../language-server/src/core/index.ts | 9 +- .../language-server/src/core/svelte.ts | 9 +- .../language-server/src/core/vue.ts | 9 +- .../test/content-intellisense/caching.test.ts | 103 +- .../content-intellisense/completions.test.ts | 82 +- .../content-intellisense/definitions.test.ts | 84 +- .../content-intellisense/diagnostics.test.ts | 174 ++-- .../test/content-intellisense/hover.test.ts | 41 +- .../language-server/test/package.json | 4 +- .../ts-plugin/src/frontmatter.ts | 16 +- .../language-tools/ts-plugin/src/language.ts | 9 +- packages/language-tools/vscode/README.md | 2 +- packages/language-tools/vscode/package.json | 4 +- packages/markdown/remark/package.json | 3 +- ...utolinking.test.js => autolinking.test.ts} | 4 +- .../test/{browser.test.js => browser.test.ts} | 2 +- .../{entities.test.js => entities.test.ts} | 4 +- ...rontmatter.test.js => frontmatter.test.ts} | 18 +- .../{highlight.test.js => highlight.test.ts} | 0 .../test/{plugins.test.js => plugins.test.ts} | 20 +- .../test/{prism.test.js => prism.test.ts} | 0 ....test.js => remark-collect-images.test.ts} | 43 +- .../test/{shiki.test.js => shiki.test.ts} | 74 +- packages/markdown/remark/tsconfig.test.json | 12 + packages/telemetry/CHANGELOG.md | 6 + packages/telemetry/package.json | 7 +- packages/telemetry/src/config.ts | 4 +- packages/telemetry/src/index.ts | 4 +- .../test/{config.test.js => config.test.ts} | 0 .../test/{index.test.js => index.test.ts} | 43 +- packages/telemetry/tsconfig.test.json | 12 + packages/underscore-redirects/CHANGELOG.md | 6 + packages/underscore-redirects/package.json | 5 +- packages/underscore-redirects/src/astro.ts | 52 +- packages/underscore-redirects/src/index.ts | 1 + .../underscore-redirects/test/astro.test.js | 28 - .../underscore-redirects/test/astro.test.ts | 60 ++ .../test/{print.test.js => print.test.ts} | 0 .../test/{weight.test.js => weight.test.ts} | 0 .../underscore-redirects/tsconfig.test.json | 12 + packages/upgrade/package.json | 3 +- .../test/{context.test.js => context.test.ts} | 0 .../test/{install.test.js => install.test.ts} | 26 +- packages/upgrade/test/{utils.js => utils.ts} | 15 +- .../test/{verify.test.js => verify.test.ts} | 0 packages/upgrade/tsconfig.test.json | 12 + pnpm-lock.yaml | 889 ++++++++---------- pnpm-workspace.yaml | 4 + scripts/jsconfig.json | 12 - scripts/testing/github-test-reporter.js | 2 +- scripts/tsconfig.json | 12 + tsconfig.base.json | 2 + 1296 files changed, 18538 insertions(+), 15862 deletions(-) create mode 100644 .changeset/two-eels-live.md create mode 100644 .github/scripts/tsconfig.json create mode 100644 .github/workflows/issue-close-cleanup.yml create mode 100644 .github/workflows/semgrep.yml rename examples/blog/{public => src/assets}/fonts/atkinson-bold.woff (100%) rename examples/blog/{public => src/assets}/fonts/atkinson-regular.woff (100%) rename packages/astro-rss/test/{pagesGlobToRssItems.test.js => pagesGlobToRssItems.test.ts} (95%) rename packages/astro-rss/test/{rss.test.js => rss.test.ts} (98%) rename packages/astro-rss/test/{test-utils.js => test-utils.ts} (81%) create mode 100644 packages/astro-rss/tsconfig.test.json rename packages/astro/e2e/{actions-blog.test.js => actions-blog.test.ts} (98%) rename packages/astro/e2e/{actions-react-19.test.js => actions-react-19.test.ts} (95%) rename packages/astro/e2e/{astro-component.test.js => astro-component.test.ts} (97%) rename packages/astro/e2e/{astro-envs.test.js => astro-envs.test.ts} (92%) rename packages/astro/e2e/{client-idle-timeout.test.js => client-idle-timeout.test.ts} (88%) rename packages/astro/e2e/{client-only.test.js => client-only.test.ts} (97%) create mode 100644 packages/astro/e2e/cloudflare-node-prerender-hmr.test.ts rename packages/astro/e2e/{cloudflare.test.js => cloudflare.test.ts} (86%) rename packages/astro/e2e/{content-collections.test.js => content-collections.test.ts} (88%) rename packages/astro/e2e/{core-image-styles.test.js => core-image-styles.test.ts} (91%) rename packages/astro/e2e/{csp-client-only.test.js => csp-client-only.test.ts} (97%) rename packages/astro/e2e/{csp-server-islands.test.js => csp-server-islands.test.ts} (78%) rename packages/astro/e2e/{css.test.js => css.test.ts} (91%) rename packages/astro/e2e/{custom-client-directives.test.js => custom-client-directives.test.ts} (93%) rename packages/astro/e2e/{dev-toolbar-audits.test.js => dev-toolbar-audits.test.ts} (97%) rename packages/astro/e2e/{dev-toolbar.test.js => dev-toolbar.test.ts} (98%) rename packages/astro/e2e/{error-cyclic.test.js => error-cyclic.test.ts} (83%) rename packages/astro/e2e/{error-sass.test.js => error-sass.test.ts} (83%) rename packages/astro/e2e/{errors.test.js => errors.test.ts} (94%) create mode 100644 packages/astro/e2e/fixtures/cloudflare-node-prerender-hmr/astro.config.mjs create mode 100644 packages/astro/e2e/fixtures/cloudflare-node-prerender-hmr/package.json create mode 100644 packages/astro/e2e/fixtures/cloudflare-node-prerender-hmr/src/pages/index.astro rename packages/astro/{test/fixtures/astro-fallback => e2e/fixtures/hmr}/astro.config.mjs (82%) create mode 100644 packages/astro/e2e/fixtures/hmr/src/components/ScssModuleHeading.jsx create mode 100644 packages/astro/e2e/fixtures/hmr/src/pages/scss-external.astro create mode 100644 packages/astro/e2e/fixtures/hmr/src/pages/scss-module.astro create mode 100644 packages/astro/e2e/fixtures/hmr/src/styles/scss-external.scss create mode 100644 packages/astro/e2e/fixtures/hmr/src/styles/scss-module.module.scss rename packages/astro/e2e/{hmr.test.js => hmr.test.ts} (81%) rename packages/astro/e2e/{hydration-race.test.js => hydration-race.test.ts} (91%) rename packages/astro/e2e/{i18n.test.js => i18n.test.ts} (93%) rename packages/astro/e2e/{multiple-frameworks.test.js => multiple-frameworks.test.ts} (92%) rename packages/astro/e2e/{namespaced-component.test.js => namespaced-component.test.ts} (97%) rename packages/astro/e2e/{nested-in-preact.test.js => nested-in-preact.test.ts} (96%) rename packages/astro/e2e/{nested-in-react.test.js => nested-in-react.test.ts} (96%) rename packages/astro/e2e/{nested-in-solid.test.js => nested-in-solid.test.ts} (96%) rename packages/astro/e2e/{nested-in-svelte.test.js => nested-in-svelte.test.ts} (96%) rename packages/astro/e2e/{nested-in-vue.test.js => nested-in-vue.test.ts} (96%) rename packages/astro/e2e/{nested-recursive.test.js => nested-recursive.test.ts} (94%) rename packages/astro/e2e/{nested-styles.test.js => nested-styles.test.ts} (91%) rename packages/astro/e2e/{pass-js.test.js => pass-js.test.ts} (98%) rename packages/astro/e2e/{preact-compat-component.test.js => preact-compat-component.test.ts} (86%) rename packages/astro/e2e/{preact-component.test.js => preact-component.test.ts} (89%) rename packages/astro/e2e/{preact-lazy-component.test.js => preact-lazy-component.test.ts} (90%) rename packages/astro/e2e/{prefetch.test.js => prefetch.test.ts} (93%) rename packages/astro/e2e/{react-component.test.js => react-component.test.ts} (95%) rename packages/astro/e2e/{react19-preact-hook-error.test.js => react19-preact-hook-error.test.ts} (78%) rename packages/astro/e2e/{server-islands-key.test.js => server-islands-key.test.ts} (77%) rename packages/astro/e2e/{server-islands.test.js => server-islands.test.ts} (92%) rename packages/astro/e2e/{shared-component-tests.js => shared-component-tests.ts} (90%) rename packages/astro/e2e/{solid-circular.test.js => solid-circular.test.ts} (85%) rename packages/astro/e2e/{solid-component.test.js => solid-component.test.ts} (89%) rename packages/astro/e2e/{solid-recurse.test.js => solid-recurse.test.ts} (88%) rename packages/astro/e2e/{svelte-component.test.js => svelte-component.test.ts} (84%) rename packages/astro/e2e/{tailwindcss.test.js => tailwindcss.test.ts} (96%) rename packages/astro/e2e/{test-utils.js => test-utils.ts} (60%) rename packages/astro/e2e/{ts-resolution.test.js => ts-resolution.test.ts} (83%) rename packages/astro/e2e/{view-transitions.test.js => view-transitions.test.ts} (94%) rename packages/astro/e2e/{vite-virtual-modules.test.js => vite-virtual-modules.test.ts} (75%) rename packages/astro/e2e/{vue-component.test.js => vue-component.test.ts} (95%) create mode 100644 packages/astro/src/assets/utils/inferSourceFormat.ts create mode 100644 packages/astro/src/core/build/plugins/plugin-chunk-imports.ts create mode 100644 packages/astro/src/core/config/schemas/refined-validators.ts create mode 100644 packages/astro/src/preferences/dlv.ts rename packages/astro/test/{0-css.test.js => 0-css.test.ts} (87%) rename packages/astro/test/{astro-assets-dir.test.js => astro-assets-dir.test.ts} (89%) rename packages/astro/test/{astro-assets-prefix-multi-cdn.test.js => astro-assets-prefix-multi-cdn.test.ts} (85%) rename packages/astro/test/{astro-assets-prefix.test.js => astro-assets-prefix.test.ts} (89%) rename packages/astro/test/{astro-assets.test.js => astro-assets.test.ts} (78%) rename packages/astro/test/{astro-basic.test.js => astro-basic.test.ts} (87%) rename packages/astro/test/{astro-children.test.js => astro-children.test.ts} (97%) rename packages/astro/test/{astro-client-only.test.js => astro-client-only.test.ts} (93%) rename packages/astro/test/{astro-component-bundling.test.js => astro-component-bundling.test.ts} (69%) rename packages/astro/test/{astro-component-code.test.js => astro-component-code.test.ts} (96%) rename packages/astro/test/{astro-cookies.test.js => astro-cookies.test.ts} (86%) rename packages/astro/test/{astro-css-bundling.test.js => astro-css-bundling.test.ts} (94%) rename packages/astro/test/{astro-dev-headers.test.js => astro-dev-headers.test.ts} (86%) rename packages/astro/test/{astro-dev-http2.test.js => astro-dev-http2.test.ts} (88%) rename packages/astro/test/{astro-directives.test.js => astro-directives.test.ts} (64%) rename packages/astro/test/{astro-doctype.test.js => astro-doctype.test.ts} (96%) rename packages/astro/test/{astro-dynamic.test.js => astro-dynamic.test.ts} (90%) rename packages/astro/test/{astro-envs.test.js => astro-envs.test.ts} (94%) rename packages/astro/test/{astro-expr.test.js => astro-expr.test.ts} (81%) delete mode 100644 packages/astro/test/astro-external-files.test.js delete mode 100644 packages/astro/test/astro-fallback.test.js delete mode 100644 packages/astro/test/astro-generator.test.js rename packages/astro/test/{astro-get-static-paths.test.js => astro-get-static-paths.test.ts} (93%) rename packages/astro/test/{astro-global.test.js => astro-global.test.ts} (97%) rename packages/astro/test/{astro-head.test.js => astro-head.test.ts} (90%) rename packages/astro/test/{astro-markdown-frontmatter-injection.test.js => astro-markdown-frontmatter-injection.test.ts} (55%) rename packages/astro/test/{astro-markdown-plugins.test.js => astro-markdown-plugins.test.ts} (90%) rename packages/astro/test/{astro-markdown-remarkRehype.test.js => astro-markdown-remarkRehype.test.ts} (92%) rename packages/astro/test/{astro-markdown-shiki.test.js => astro-markdown-shiki.test.ts} (83%) rename packages/astro/test/{astro-markdown-url.test.js => astro-markdown-url.test.ts} (100%) rename packages/astro/test/{astro-markdown.test.js => astro-markdown.test.ts} (92%) rename packages/astro/test/{astro-mode.test.js => astro-mode.test.ts} (93%) rename packages/astro/test/{astro-not-response.test.js => astro-not-response.test.ts} (82%) rename packages/astro/test/{astro-pageDirectoryUrl.test.js => astro-pageDirectoryUrl.test.ts} (85%) rename packages/astro/test/{astro-pages.test.js => astro-pages.test.ts} (91%) rename packages/astro/test/{astro-partial-html.test.js => astro-partial-html.test.ts} (91%) rename packages/astro/test/{astro-preview-allowed-hosts.test.js => astro-preview-allowed-hosts.test.ts} (89%) rename packages/astro/test/{astro-preview-headers.test.js => astro-preview-headers.test.ts} (88%) rename packages/astro/test/{astro-public.test.js => astro-public.test.ts} (79%) rename packages/astro/test/{astro-scripts.test.js => astro-scripts.test.ts} (88%) delete mode 100644 packages/astro/test/astro-slot-with-client.test.js rename packages/astro/test/{astro-slots-nested.test.js => astro-slots-nested.test.ts} (92%) rename packages/astro/test/{astro-slots.test.js => astro-slots.test.ts} (87%) rename packages/astro/test/{astro-sync.test.js => astro-sync.test.ts} (81%) create mode 100644 packages/astro/test/config-format.test.js create mode 100644 packages/astro/test/content-collection-picture-render.test.ts rename packages/astro/test/{content-collection-references.test.js => content-collection-references.test.ts} (83%) rename packages/astro/test/{content-collection-tla-svg.test.js => content-collection-tla-svg.test.ts} (90%) rename packages/astro/test/{content-collections-render.test.js => content-collections-render.test.ts} (94%) rename packages/astro/test/{content-collections-type-inference.test.js => content-collections-type-inference.test.ts} (89%) rename packages/astro/test/{content-collections.test.js => content-collections.test.ts} (81%) create mode 100644 packages/astro/test/content-frontmatter.test.ts rename packages/astro/test/{content-intellisense.test.js => content-intellisense.test.ts} (89%) rename packages/astro/test/{core-image-fs-config.test.js => core-image-fs-config.test.ts} (95%) rename packages/astro/test/{core-image-infersize.test.js => core-image-infersize.test.ts} (66%) rename packages/astro/test/{core-image-layout.test.js => core-image-layout.test.ts} (72%) rename packages/astro/test/{core-image-picture-emit-file.test.js => core-image-picture-emit-file.test.ts} (84%) rename packages/astro/test/{core-image-remark-imgattr.test.js => core-image-remark-imgattr.test.ts} (51%) rename packages/astro/test/{core-image-svg-in-island.test.js => core-image-svg-in-island.test.ts} (89%) rename packages/astro/test/{core-image-svg.test.js => core-image-svg.test.ts} (71%) rename packages/astro/test/{core-image-unconventional-settings.test.js => core-image-unconventional-settings.test.ts} (77%) rename packages/astro/test/{core-image.test.js => core-image.test.ts} (79%) rename packages/astro/test/{css-assets.test.js => css-assets.test.ts} (79%) rename packages/astro/test/{css-dangling-references.test.js => css-dangling-references.test.ts} (93%) rename packages/astro/test/{css-deduplication.test.js => css-deduplication.test.ts} (92%) rename packages/astro/test/{css-double-bundle.test.js => css-double-bundle.test.ts} (92%) rename packages/astro/test/{css-dynamic-import-dev.test.js => css-dynamic-import-dev.test.ts} (86%) rename packages/astro/test/{css-import-as-inline.test.js => css-import-as-inline.test.ts} (93%) rename packages/astro/test/{css-inline-stylesheets.test.js => css-inline-stylesheets.test.ts} (89%) rename packages/astro/test/{css-no-code-split.test.js => css-no-code-split.test.ts} (85%) rename packages/astro/test/{css-order-import.test.js => css-order-import.test.ts} (86%) rename packages/astro/test/{css-order-layout.test.js => css-order-layout.test.ts} (77%) rename packages/astro/test/{css-order.test.js => css-order.test.ts} (82%) rename packages/astro/test/{custom-404-html.test.js => custom-404-html.test.ts} (88%) rename packages/astro/test/{custom-404-implicit-rerouting.test.js => custom-404-implicit-rerouting.test.ts} (74%) rename packages/astro/test/{custom-404-injected-from-dep.test.js => custom-404-injected-from-dep.test.ts} (82%) rename packages/astro/test/{custom-404-injected.test.js => custom-404-injected.test.ts} (88%) rename packages/astro/test/{custom-404-locals.test.js => custom-404-locals.test.ts} (91%) rename packages/astro/test/{custom-404-md.test.js => custom-404-md.test.ts} (87%) rename packages/astro/test/{custom-404-static.test.js => custom-404-static.test.ts} (90%) create mode 100644 packages/astro/test/dev-base.test.ts create mode 100644 packages/astro/test/dev-container.test.ts create mode 100644 packages/astro/test/dev-error-pages.test.ts create mode 100644 packages/astro/test/dev-render-chunk.test.ts create mode 100644 packages/astro/test/dev-render-components.test.ts create mode 100644 packages/astro/test/dev-request-url.test.ts create mode 100644 packages/astro/test/dev-restart.test.ts rename packages/astro/test/{dev-route-scripts.test.js => dev-route-scripts.test.ts} (86%) create mode 100644 packages/astro/test/endpoint-response.test.js create mode 100644 packages/astro/test/endpoint-routing.test.js create mode 100644 packages/astro/test/endpoint-runtime.test.js rename packages/astro/test/{error-bad-js.test.js => error-bad-js.test.ts} (88%) rename packages/astro/test/{error-build-location.test.js => error-build-location.test.ts} (70%) rename packages/astro/test/{error-map.test.js => error-map.test.ts} (93%) rename packages/astro/test/{error-non-error.test.js => error-non-error.test.ts} (78%) rename packages/astro/test/{featuresSupport.test.js => featuresSupport.test.ts} (75%) rename packages/astro/test/{fetch.test.js => fetch.test.ts} (94%) delete mode 100644 packages/astro/test/fixtures/alias-tsconfig-baseurl-only/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/alias-tsconfig-baseurl-only/package.json delete mode 100644 packages/astro/test/fixtures/alias-tsconfig-baseurl-only/src/components/Alias.svelte delete mode 100644 packages/astro/test/fixtures/alias-tsconfig-baseurl-only/src/components/Client.svelte delete mode 100644 packages/astro/test/fixtures/alias-tsconfig-baseurl-only/src/components/Foo.astro delete mode 100644 packages/astro/test/fixtures/alias-tsconfig-baseurl-only/src/components/Style.astro delete mode 100644 packages/astro/test/fixtures/alias-tsconfig-baseurl-only/src/pages/index.astro delete mode 100644 packages/astro/test/fixtures/alias-tsconfig-baseurl-only/src/styles/extra.css delete mode 100644 packages/astro/test/fixtures/alias-tsconfig-baseurl-only/src/styles/main.css delete mode 100644 packages/astro/test/fixtures/alias-tsconfig-baseurl-only/src/utils/constants.js delete mode 100644 packages/astro/test/fixtures/alias-tsconfig-baseurl-only/src/utils/index.js delete mode 100644 packages/astro/test/fixtures/alias-tsconfig-baseurl-only/tsconfig.json create mode 100644 packages/astro/test/fixtures/asset-query-params-chunks/package.json create mode 100644 packages/astro/test/fixtures/asset-query-params-chunks/src/components/CounterA.astro create mode 100644 packages/astro/test/fixtures/asset-query-params-chunks/src/components/CounterB.astro create mode 100644 packages/astro/test/fixtures/asset-query-params-chunks/src/components/DynamicLoader.astro create mode 100644 packages/astro/test/fixtures/asset-query-params-chunks/src/components/shared.js create mode 100644 packages/astro/test/fixtures/asset-query-params-chunks/src/pages/index.astro rename packages/astro/test/fixtures/{astro-generator/src/pages/index.astro => astro-basic/src/pages/generator.astro} (100%) delete mode 100644 packages/astro/test/fixtures/astro-components/package.json delete mode 100644 packages/astro/test/fixtures/astro-css-bundling-nested-layouts/package.json rename packages/astro/test/fixtures/{set-html => astro-directives}/public/test.html (100%) rename packages/astro/test/fixtures/{set-html => astro-directives/src}/components/Slot.astro (100%) rename packages/astro/test/fixtures/{set-html/src/pages/children.astro => astro-directives/src/pages/set-html-children.astro} (88%) rename packages/astro/test/fixtures/{set-html/src/pages/fetch.astro => astro-directives/src/pages/set-html-fetch.astro} (100%) rename packages/astro/test/fixtures/{set-html/src/pages/index.astro => astro-directives/src/pages/set-html-types.astro} (100%) rename packages/astro/test/fixtures/{astro-fallback => astro-expr}/src/components/Client.jsx (100%) rename packages/astro/test/fixtures/{astro-slot-with-client => astro-expr}/src/components/Slotted.astro (100%) rename packages/astro/test/fixtures/{astro-slot-with-client => astro-expr}/src/components/Thing.jsx (100%) rename packages/astro/test/fixtures/{astro-fallback/src/pages/index.astro => astro-expr/src/pages/fallback.astro} (100%) rename packages/astro/test/fixtures/{astro-slot-with-client/src/pages/index.astro => astro-expr/src/pages/slot-with-client.astro} (100%) delete mode 100644 packages/astro/test/fixtures/astro-fallback/package.json rename packages/astro/test/fixtures/{astro-external-files => astro-public}/public/external-file.js (100%) rename packages/astro/test/fixtures/{astro-external-files/src/pages/index.astro => astro-public/src/pages/external-files.astro} (100%) delete mode 100644 packages/astro/test/fixtures/astro-sitemap-rss/package.json delete mode 100644 packages/astro/test/fixtures/astro-sitemap-rss/src/pages/404.astro delete mode 100644 packages/astro/test/fixtures/astro-sitemap-rss/src/pages/episode/fazers.md delete mode 100644 packages/astro/test/fixtures/astro-sitemap-rss/src/pages/episode/rap-snitch-knishes.md delete mode 100644 packages/astro/test/fixtures/astro-sitemap-rss/src/pages/episode/rhymes-like-dimes.md delete mode 100644 packages/astro/test/fixtures/astro-sitemap-rss/src/pages/episodes/[...page].astro delete mode 100644 packages/astro/test/fixtures/astro-slot-with-client/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/astro-slot-with-client/package.json rename packages/astro/test/fixtures/{unused-slot => astro-slots}/src/components/Card.astro (100%) rename packages/astro/test/fixtures/{unused-slot/src/pages/index.astro => astro-slots/src/pages/unused-slot.astro} (100%) delete mode 100644 packages/astro/test/fixtures/config-host/package.json delete mode 100644 packages/astro/test/fixtures/config-path/config/my-config.mjs delete mode 100644 packages/astro/test/fixtures/config-path/package.json rename packages/astro/test/fixtures/{css-order-transparent => content-collection-picture-render}/astro.config.mjs (100%) rename packages/astro/test/fixtures/{astro-external-files => content-collection-picture-render}/package.json (64%) create mode 100644 packages/astro/test/fixtures/content-collection-picture-render/src/assets/test-image.png create mode 100644 packages/astro/test/fixtures/content-collection-picture-render/src/content.config.ts create mode 100644 packages/astro/test/fixtures/content-collection-picture-render/src/content/blog/post-1.md create mode 100644 packages/astro/test/fixtures/content-collection-picture-render/src/pages/blog/[...slug].astro create mode 100644 packages/astro/test/fixtures/content-collection-picture-render/src/pages/index.astro delete mode 100644 packages/astro/test/fixtures/content-collections-cache-invalidation/.gitignore delete mode 100644 packages/astro/test/fixtures/content-collections-cache-invalidation/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/content-collections-cache-invalidation/cache/lockfile-mismatch/content/manifest.json delete mode 100644 packages/astro/test/fixtures/content-collections-cache-invalidation/cache/version-mismatch/content/manifest.json delete mode 100644 packages/astro/test/fixtures/content-collections-cache-invalidation/package.json delete mode 100644 packages/astro/test/fixtures/content-collections-cache-invalidation/src/content.config.ts delete mode 100644 packages/astro/test/fixtures/content-collections-cache-invalidation/src/pages/index.astro delete mode 100644 packages/astro/test/fixtures/content-collections-same-contents/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/content-collections-same-contents/package.json delete mode 100644 packages/astro/test/fixtures/content-collections-same-contents/src/content/docs/one.md delete mode 100644 packages/astro/test/fixtures/content-collections-same-contents/src/content/docs/two.md delete mode 100644 packages/astro/test/fixtures/content-collections-same-contents/src/pages/docs.astro create mode 100644 packages/astro/test/fixtures/content-frontmatter/package.json rename packages/astro/test/fixtures/{content-collections-same-contents => content-frontmatter}/src/content.config.ts (63%) rename packages/astro/test/fixtures/{content-collections-cache-invalidation/src/content/blog/one.md => content-frontmatter/src/content/posts/blog.md} (59%) create mode 100644 packages/astro/test/fixtures/content-frontmatter/src/pages/index.astro delete mode 100644 packages/astro/test/fixtures/core-image-base/package.json delete mode 100644 packages/astro/test/fixtures/core-image-base/src/assets/penguin1.jpg delete mode 100644 packages/astro/test/fixtures/core-image-base/src/assets/penguin2.jpg delete mode 100644 packages/astro/test/fixtures/core-image-base/src/content.config.ts delete mode 100644 packages/astro/test/fixtures/core-image-base/src/content/blog/one.md delete mode 100644 packages/astro/test/fixtures/core-image-base/src/pages/alias.astro delete mode 100644 packages/astro/test/fixtures/core-image-base/src/pages/aliasMarkdown.md delete mode 100644 packages/astro/test/fixtures/core-image-base/src/pages/blog/[...slug].astro delete mode 100644 packages/astro/test/fixtures/core-image-base/src/pages/format.astro delete mode 100644 packages/astro/test/fixtures/core-image-base/src/pages/get-image.astro delete mode 100644 packages/astro/test/fixtures/core-image-base/src/pages/index.astro delete mode 100644 packages/astro/test/fixtures/core-image-base/src/pages/post.md delete mode 100644 packages/astro/test/fixtures/core-image-base/src/pages/quality.astro delete mode 100644 packages/astro/test/fixtures/core-image-base/tsconfig.json delete mode 100644 packages/astro/test/fixtures/core-image-fs-config-outside-imported/penguin-imported.jpg rename packages/astro/test/fixtures/{core-image-base => core-image-ssg}/src/pages/direct.astro (100%) delete mode 100644 packages/astro/test/fixtures/core-image-svg-optimized/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/core-image-svg-optimized/tsconfig.json rename packages/astro/test/fixtures/{core-image-svg-optimized => core-image-svg}/src/assets/unoptimized.svg (100%) rename packages/astro/test/fixtures/{core-image-svg-optimized => core-image-svg}/src/pages/optimized.astro (100%) delete mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-2/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-2/package.json delete mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-2/src/components/Button.astro delete mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-2/src/content.config.ts delete mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-2/src/content/en/endeavour.md delete mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-2/src/imported.css delete mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-2/src/layouts/Layout.astro delete mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-2/src/pages/endeavour.md delete mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-2/src/pages/index.astro delete mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-3/package.json delete mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-3/src/components/Button.astro delete mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-3/src/content/en/endeavour.md delete mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-3/src/imported.css delete mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-3/src/layouts/Layout.astro delete mode 100644 packages/astro/test/fixtures/css-inline-stylesheets-3/src/pages/index.astro rename packages/astro/test/fixtures/{css-order-transparent => css-order-layout}/src/components/Item.astro (100%) rename packages/astro/test/fixtures/{css-order-transparent/src/pages/index.astro => css-order-layout/src/pages/transparent.astro} (100%) delete mode 100644 packages/astro/test/fixtures/css-order-transparent/package.json delete mode 100644 packages/astro/test/fixtures/custom-500-middleware/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/custom-500-middleware/package.json delete mode 100644 packages/astro/test/fixtures/custom-500-middleware/src/middleware.js delete mode 100644 packages/astro/test/fixtures/custom-500-middleware/src/pages/500.astro delete mode 100644 packages/astro/test/fixtures/custom-500-middleware/src/pages/index.astro create mode 100644 packages/astro/test/fixtures/dev-container/package.json create mode 100644 packages/astro/test/fixtures/dev-container/public/test.txt create mode 100644 packages/astro/test/fixtures/dev-container/src/components/404.astro create mode 100644 packages/astro/test/fixtures/dev-container/src/components/test.astro create mode 100644 packages/astro/test/fixtures/dev-container/src/pages/index.astro create mode 100644 packages/astro/test/fixtures/dev-container/src/pages/page.astro create mode 100644 packages/astro/test/fixtures/dev-container/src/pages/test-[slug].astro create mode 100644 packages/astro/test/fixtures/dev-error-pages/package.json create mode 100644 packages/astro/test/fixtures/dev-error-pages/src/pages/404.astro create mode 100644 packages/astro/test/fixtures/dev-error-pages/src/pages/500.astro create mode 100644 packages/astro/test/fixtures/dev-error-pages/src/pages/index.astro create mode 100644 packages/astro/test/fixtures/dev-error-pages/src/pages/throwing.astro create mode 100644 packages/astro/test/fixtures/dev-render/package.json create mode 100644 packages/astro/test/fixtures/dev-render/src/components/BothFlipped.astro create mode 100644 packages/astro/test/fixtures/dev-render/src/components/BothLiteral.astro create mode 100644 packages/astro/test/fixtures/dev-render/src/components/BothSpread.astro create mode 100644 packages/astro/test/fixtures/dev-render/src/components/Class.astro create mode 100644 packages/astro/test/fixtures/dev-render/src/components/ClassList.astro create mode 100644 packages/astro/test/fixtures/dev-render/src/components/NullComponent.astro create mode 100644 packages/astro/test/fixtures/dev-render/src/pages/chunk.astro create mode 100644 packages/astro/test/fixtures/dev-render/src/pages/class-merge.astro create mode 100644 packages/astro/test/fixtures/dev-render/src/pages/custom-elements.astro create mode 100644 packages/astro/test/fixtures/dev-render/src/pages/index.astro create mode 100644 packages/astro/test/fixtures/dev-render/src/pages/null-component.astro create mode 100644 packages/astro/test/fixtures/dev-render/src/pages/sub/index.astro create mode 100644 packages/astro/test/fixtures/dev-request-url/package.json create mode 100644 packages/astro/test/fixtures/dev-request-url/src/pages/prerendered.astro create mode 100644 packages/astro/test/fixtures/dev-request-url/src/pages/url.astro create mode 100644 packages/astro/test/fixtures/endpoint-routing/package.json create mode 100644 packages/astro/test/fixtures/endpoint-routing/src/pages/headers.ts create mode 100644 packages/astro/test/fixtures/endpoint-routing/src/pages/incorrect.ts create mode 100644 packages/astro/test/fixtures/endpoint-routing/src/pages/internal-error.ts create mode 100644 packages/astro/test/fixtures/endpoint-routing/src/pages/multi-headers.js create mode 100644 packages/astro/test/fixtures/endpoint-routing/src/pages/not-found.ts create mode 100644 packages/astro/test/fixtures/endpoint-routing/src/pages/response-redirect.ts create mode 100644 packages/astro/test/fixtures/endpoint-routing/src/pages/response.ts create mode 100644 packages/astro/test/fixtures/endpoint-routing/src/pages/setCookies.js create mode 100644 packages/astro/test/fixtures/endpoint-routing/src/pages/streaming.js delete mode 100644 packages/astro/test/fixtures/feature-support-message-suppresion/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/feature-support-message-suppresion/package.json delete mode 100644 packages/astro/test/fixtures/feature-support-message-suppresion/src/pages/index.astro delete mode 100644 packages/astro/test/fixtures/head-injection/package.json delete mode 100644 packages/astro/test/fixtures/head-injection/src/components/Layout.astro delete mode 100644 packages/astro/test/fixtures/head-injection/src/components/RegularSlot.astro delete mode 100644 packages/astro/test/fixtures/head-injection/src/components/SlotRenderComponent.astro delete mode 100644 packages/astro/test/fixtures/head-injection/src/components/SlotRenderLayout.astro delete mode 100644 packages/astro/test/fixtures/head-injection/src/components/SlotsRender.astro delete mode 100644 packages/astro/test/fixtures/head-injection/src/components/UsesSlotRender.astro delete mode 100644 packages/astro/test/fixtures/head-injection/src/components/with-slot-render2/inner.astro delete mode 100644 packages/astro/test/fixtures/head-injection/src/components/with-slot-render2/slots-render-outer.astro delete mode 100644 packages/astro/test/fixtures/head-injection/src/pages/index.md delete mode 100644 packages/astro/test/fixtures/head-injection/src/pages/with-render-slot-in-head-buffer.astro delete mode 100644 packages/astro/test/fixtures/head-injection/src/pages/with-slot-in-render-slot.astro delete mode 100644 packages/astro/test/fixtures/head-injection/src/pages/with-slot-in-slot.astro delete mode 100644 packages/astro/test/fixtures/head-injection/src/pages/with-slot-render.astro delete mode 100644 packages/astro/test/fixtures/head-injection/src/pages/with-slot-render2.astro delete mode 100644 packages/astro/test/fixtures/hmr-css/package.json create mode 100644 packages/astro/test/fixtures/i18n-css-leak-basic/astro.config.mjs rename packages/astro/test/fixtures/{astro-generator => i18n-css-leak-basic}/package.json (71%) create mode 100644 packages/astro/test/fixtures/i18n-css-leak-basic/src/components/Header.astro create mode 100644 packages/astro/test/fixtures/i18n-css-leak-basic/src/layouts/DocsLayout.astro create mode 100644 packages/astro/test/fixtures/i18n-css-leak-basic/src/layouts/SiteLayout.astro create mode 100644 packages/astro/test/fixtures/i18n-css-leak-basic/src/pages/docs/index.astro create mode 100644 packages/astro/test/fixtures/i18n-css-leak-basic/src/pages/index.astro create mode 100644 packages/astro/test/fixtures/i18n-css-leak-basic/src/styles/docs.css create mode 100644 packages/astro/test/fixtures/i18n-css-leak-basic/src/styles/site.css delete mode 100644 packages/astro/test/fixtures/i18n-routing-base/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/i18n-routing-base/package.json delete mode 100644 packages/astro/test/fixtures/i18n-routing-base/src/pages/en/blog/[id].astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-base/src/pages/en/start.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-base/src/pages/index.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-base/src/pages/pt/blog/[id].astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-base/src/pages/pt/start.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-base/src/pages/spanish/blog/[id].astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-base/src/pages/spanish/start.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-dynamic/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/i18n-routing-dynamic/package.json delete mode 100644 packages/astro/test/fixtures/i18n-routing-dynamic/src/pages/[language].astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-dynamic/src/pages/index.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-fallback-rewrite-hybrid/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/i18n-routing-fallback-rewrite-hybrid/package.json delete mode 100644 packages/astro/test/fixtures/i18n-routing-fallback-rewrite-hybrid/src/pages/[slug].astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-fallback-rewrite-hybrid/src/pages/about.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-fallback-rewrite-hybrid/src/pages/es/index.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-fallback-rewrite-hybrid/src/pages/index.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual-with-default-middleware/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual-with-default-middleware/package.json delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual-with-default-middleware/src/middleware.js delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual-with-default-middleware/src/pages/about.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual-with-default-middleware/src/pages/blog.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual-with-default-middleware/src/pages/en/blog/[id].astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual-with-default-middleware/src/pages/en/start.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual-with-default-middleware/src/pages/index.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual-with-default-middleware/src/pages/pt/blog/[id].astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual-with-default-middleware/src/pages/pt/start.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual-with-default-middleware/src/pages/spanish/index.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual/package.json delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual/src/middleware.js delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual/src/pages/404.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual/src/pages/en/blog/[id].astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual/src/pages/en/blog/index.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual/src/pages/en/index.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual/src/pages/en/start.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual/src/pages/help.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual/src/pages/index.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual/src/pages/pt/blog/[id].astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual/src/pages/pt/start.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-manual/src/pages/spanish/index.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-redirect-preferred-language/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/i18n-routing-redirect-preferred-language/package.json delete mode 100644 packages/astro/test/fixtures/i18n-routing-redirect-preferred-language/src/pages/en/blog/[id].astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-redirect-preferred-language/src/pages/en/end.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-redirect-preferred-language/src/pages/en/start.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-redirect-preferred-language/src/pages/index.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-redirect-preferred-language/src/pages/preferred-locale.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-redirect-preferred-language/src/pages/pt/blog/[id].astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-redirect-preferred-language/src/pages/pt/start.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-subdomain/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/i18n-routing-subdomain/package.json delete mode 100644 packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/blog/[id].astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/index.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/en/start.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/index.astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/pt/blog/[id].astro delete mode 100644 packages/astro/test/fixtures/i18n-routing-subdomain/src/pages/pt/start.astro delete mode 100644 packages/astro/test/fixtures/i18n-server-island/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/i18n-server-island/package.json delete mode 100644 packages/astro/test/fixtures/i18n-server-island/src/components/Island.astro delete mode 100644 packages/astro/test/fixtures/i18n-server-island/src/pages/en/island.astro delete mode 100644 packages/astro/test/fixtures/i18n-server-island/src/pages/index.astro delete mode 100644 packages/astro/test/fixtures/middleware-sequence-request-clone/package.json delete mode 100644 packages/astro/test/fixtures/middleware-sequence-request-clone/src/middleware.js delete mode 100644 packages/astro/test/fixtures/middleware-sequence-request-clone/src/pages/index.astro delete mode 100644 packages/astro/test/fixtures/middleware-ssg/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/middleware-ssg/package.json delete mode 100644 packages/astro/test/fixtures/middleware-ssg/src/middleware.js delete mode 100644 packages/astro/test/fixtures/middleware-ssg/src/pages/index.astro delete mode 100644 packages/astro/test/fixtures/middleware-ssg/src/pages/second.astro delete mode 100644 packages/astro/test/fixtures/middleware-virtual/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/middleware-virtual/package.json delete mode 100644 packages/astro/test/fixtures/middleware-virtual/src/middleware.js delete mode 100644 packages/astro/test/fixtures/middleware-virtual/src/pages/index.astro delete mode 100644 packages/astro/test/fixtures/set-html/package.json delete mode 100644 packages/astro/test/fixtures/ssr-env/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/ssr-env/package.json delete mode 100644 packages/astro/test/fixtures/ssr-markdown/package.json rename packages/astro/test/fixtures/{ssr-markdown => ssr-prerender}/src/layouts/Base.astro (100%) rename packages/astro/test/fixtures/{ssr-markdown => ssr-prerender}/src/pages/post.md (100%) create mode 100644 packages/astro/test/fixtures/ssr-script/src/pages/dynamic.astro create mode 100644 packages/astro/test/fixtures/ssr-script/src/scripts/confetti.js rename packages/astro/test/fixtures/{ssr-env => ssr-scripts}/src/components/Env.jsx (100%) rename packages/astro/test/fixtures/{ssr-env => ssr-scripts}/src/pages/ssr.astro (100%) delete mode 100644 packages/astro/test/fixtures/ssr-split-manifest/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/ssr-split-manifest/package.json delete mode 100644 packages/astro/test/fixtures/ssr-split-manifest/src/pages/[...post].astro delete mode 100644 packages/astro/test/fixtures/ssr-split-manifest/src/pages/index.astro delete mode 100644 packages/astro/test/fixtures/ssr-split-manifest/src/pages/lorem.md delete mode 100644 packages/astro/test/fixtures/ssr-split-manifest/src/pages/prerender.astro delete mode 100644 packages/astro/test/fixtures/ssr-split-manifest/src/pages/zod.astro delete mode 100644 packages/astro/test/fixtures/ssr-trailing-slash/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/ssr-trailing-slash/package.json delete mode 100644 packages/astro/test/fixtures/ssr-trailing-slash/src/pages/index.astro create mode 100644 packages/astro/test/fixtures/streaming/src/pages/fragment-streaming.astro delete mode 100644 packages/astro/test/fixtures/unused-slot/package.json delete mode 100644 packages/astro/test/fixtures/with-endpoint-routes/package.json delete mode 100644 packages/astro/test/fixtures/with-endpoint-routes/src/astro.png delete mode 100644 packages/astro/test/fixtures/with-endpoint-routes/src/pages/404.astro delete mode 100644 packages/astro/test/fixtures/with-endpoint-routes/src/pages/[slug].json.ts delete mode 100644 packages/astro/test/fixtures/with-endpoint-routes/src/pages/data/[slug].json.ts delete mode 100644 packages/astro/test/fixtures/with-endpoint-routes/src/pages/home.json.ts delete mode 100644 packages/astro/test/fixtures/with-endpoint-routes/src/pages/images/[image].svg.ts delete mode 100644 packages/astro/test/fixtures/with-endpoint-routes/src/pages/images/hex.ts delete mode 100644 packages/astro/test/fixtures/with-endpoint-routes/src/pages/images/static.svg.ts delete mode 100644 packages/astro/test/fixtures/with-endpoint-routes/src/pages/invalid-redirect.json.ts delete mode 100644 packages/astro/test/fixtures/with-endpoint-routes/src/pages/not-ok.ts delete mode 100644 packages/astro/test/fixtures/with-subpath-no-trailing-slash/.gitignore delete mode 100644 packages/astro/test/fixtures/with-subpath-no-trailing-slash/astro.config.mjs delete mode 100644 packages/astro/test/fixtures/with-subpath-no-trailing-slash/package.json delete mode 100644 packages/astro/test/fixtures/with-subpath-no-trailing-slash/src/pages/[id].astro delete mode 100644 packages/astro/test/fixtures/with-subpath-no-trailing-slash/src/pages/another.astro delete mode 100644 packages/astro/test/fixtures/with-subpath-no-trailing-slash/src/pages/index.astro delete mode 100644 packages/astro/test/fixtures/without-site-config/package.json delete mode 100644 packages/astro/test/fixtures/without-site-config/src/pages/[id].astro delete mode 100644 packages/astro/test/fixtures/without-site-config/src/pages/another.astro delete mode 100644 packages/astro/test/fixtures/without-site-config/src/pages/base-index.astro delete mode 100644 packages/astro/test/fixtures/without-site-config/src/pages/html-ext/[slug].astro delete mode 100644 packages/astro/test/fixtures/without-site-config/src/pages/html-ext/[slug].html.astro delete mode 100644 packages/astro/test/fixtures/without-site-config/src/pages/index.astro delete mode 100644 packages/astro/test/fixtures/without-site-config/src/pages/redirect.astro delete mode 100644 packages/astro/test/fixtures/without-site-config/src/pages/te st.astro delete mode 100644 "packages/astro/test/fixtures/without-site-config/src/pages/\343\203\206\343\202\271\343\203\210.astro" rename packages/astro/test/{fonts.test.js => fonts.test.ts} (96%) rename packages/astro/test/{fontsource.test.js => fontsource.test.ts} (80%) create mode 100644 packages/astro/test/i18n-css-leak.test.js create mode 100644 packages/astro/test/integration-route-setup-hook.test.js create mode 100644 packages/astro/test/integration-test-helpers.js rename packages/astro/test/{page-format.test.js => page-format.test.ts} (91%) rename packages/astro/test/{page-level-styles.test.js => page-level-styles.test.ts} (91%) rename packages/astro/test/{parallel.test.js => parallel.test.ts} (88%) rename packages/astro/test/{partials.test.js => partials.test.ts} (87%) rename packages/astro/test/{passthrough-image-service.test.js => passthrough-image-service.test.ts} (87%) rename packages/astro/test/{postcss.test.js => postcss.test.ts} (92%) rename packages/astro/test/{preact-compat-component.test.js => preact-compat-component.test.ts} (83%) rename packages/astro/test/{preact-component.test.js => preact-component.test.ts} (93%) rename packages/astro/test/{prerender-conflict.test.js => prerender-conflict.test.ts} (76%) rename packages/astro/test/{public-base-404.test.js => public-base-404.test.ts} (91%) rename packages/astro/test/{react-and-solid.test.js => react-and-solid.test.ts} (84%) rename packages/astro/test/{react-jsx-export.test.js => react-jsx-export.test.ts} (71%) rename packages/astro/test/{redirects.test.js => redirects.test.ts} (89%) rename packages/astro/test/{reexport-astro-containing-client-component.test.js => reexport-astro-containing-client-component.test.ts} (88%) rename packages/astro/test/{remote-css.test.js => remote-css.test.ts} (84%) rename packages/astro/test/{reuse-injected-entrypoint.test.js => reuse-injected-entrypoint.test.ts} (90%) rename packages/astro/test/{rewrite.test.js => rewrite.test.ts} (91%) rename packages/astro/test/{root-srcdir-css.test.js => root-srcdir-css.test.ts} (83%) rename packages/astro/test/{route-guard.test.js => route-guard.test.ts} (95%) rename packages/astro/test/{scoped-style-strategy.test.js => scoped-style-strategy.test.ts} (86%) rename packages/astro/test/{serializeManifest.test.js => serializeManifest.test.ts} (95%) rename packages/astro/test/{server-entry.test.js => server-entry.test.ts} (81%) rename packages/astro/test/{server-islands.test.js => server-islands.test.ts} (94%) delete mode 100644 packages/astro/test/set-html.test.js rename packages/astro/test/{slots-preact.test.js => slots-preact.test.ts} (95%) rename packages/astro/test/{slots-react.test.js => slots-react.test.ts} (97%) rename packages/astro/test/{slots-solid.test.js => slots-solid.test.ts} (95%) rename packages/astro/test/{slots-svelte.test.js => slots-svelte.test.ts} (95%) rename packages/astro/test/{slots-vue.test.js => slots-vue.test.ts} (95%) rename packages/astro/test/{sourcemap.test.js => sourcemap.test.ts} (82%) rename packages/astro/test/{space-in-folder-name.test.js => space-in-folder-name.test.ts} (84%) rename packages/astro/test/{special-chars-in-component-imports.test.js => special-chars-in-component-imports.test.ts} (88%) rename packages/astro/test/{ssr-adapter-build-config.test.js => ssr-adapter-build-config.test.ts} (91%) rename packages/astro/test/{ssr-api-route.test.js => ssr-api-route.test.ts} (95%) rename packages/astro/test/{ssr-assets.test.js => ssr-assets.test.ts} (83%) rename packages/astro/test/{ssr-dynamic.test.js => ssr-dynamic.test.ts} (92%) rename packages/astro/test/{ssr-large-array.test.js => ssr-large-array.test.ts} (90%) delete mode 100644 packages/astro/test/ssr-markdown.test.js rename packages/astro/test/{ssr-partytown.test.js => ssr-partytown.test.ts} (91%) rename packages/astro/test/{ssr-prerender-get-static-paths.test.js => ssr-prerender-get-static-paths.test.ts} (91%) rename packages/astro/test/{ssr-prerender.test.js => ssr-prerender.test.ts} (88%) rename packages/astro/test/{ssr-preview.test.js => ssr-preview.test.ts} (75%) rename packages/astro/test/{ssr-renderers-static-vue.test.js => ssr-renderers-static-vue.test.ts} (88%) rename packages/astro/test/{ssr-request.test.js => ssr-request.test.ts} (94%) rename packages/astro/test/{ssr-script.test.js => ssr-script.test.ts} (75%) delete mode 100644 packages/astro/test/ssr-scripts.test.js rename packages/astro/test/{ssr-env.test.js => ssr-scripts.test.ts} (61%) rename packages/astro/test/{static-build-code-component.test.js => static-build-code-component.test.ts} (86%) rename packages/astro/test/{static-build-dir.test.js => static-build-dir.test.ts} (86%) rename packages/astro/test/{static-build-frameworks.test.js => static-build-frameworks.test.ts} (92%) rename packages/astro/test/{static-build-page-dist-url.test.js => static-build-page-dist-url.test.ts} (91%) rename packages/astro/test/{static-build-page-url-format.test.js => static-build-page-url-format.test.ts} (87%) rename packages/astro/test/{static-build-vite-plugins.test.js => static-build-vite-plugins.test.ts} (92%) rename packages/astro/test/{svg-deduplication.test.js => svg-deduplication.test.ts} (94%) delete mode 100644 packages/astro/test/units/_temp-fixtures/package.json rename packages/astro/test/units/actions/{action-error.test.js => action-error.test.ts} (90%) rename packages/astro/test/units/actions/{action-path.test.js => action-path.test.ts} (99%) rename packages/astro/test/units/actions/{action-status.test.js => action-status.test.ts} (58%) rename packages/astro/test/units/actions/{actions-app.test.js => actions-app.test.ts} (95%) rename packages/astro/test/units/actions/{actions-proxy.test.js => actions-proxy.test.ts} (84%) rename packages/astro/test/units/actions/{form-data-to-object.test.js => form-data-to-object.test.ts} (98%) rename packages/astro/test/units/actions/{serialize.test.js => serialize.test.ts} (95%) rename packages/astro/test/units/app/{astro-attrs.test.js => astro-attrs.test.ts} (95%) rename packages/astro/test/units/app/{astro-response.test.js => astro-response.test.ts} (90%) rename packages/astro/test/units/app/{csrf.test.js => csrf.test.ts} (96%) rename packages/astro/test/units/app/{dev-url-construction.test.js => dev-url-construction.test.ts} (92%) rename packages/astro/test/units/app/{double-slash-bypass.test.js => double-slash-bypass.test.ts} (77%) rename packages/astro/test/units/app/{encoded-backslash-bypass.test.js => encoded-backslash-bypass.test.ts} (78%) rename packages/astro/test/units/app/{error-pages.test.js => error-pages.test.ts} (80%) rename packages/astro/test/units/app/{headers.test.js => headers.test.ts} (100%) rename packages/astro/test/units/app/{locals.test.js => locals.test.ts} (77%) rename packages/astro/test/units/app/{node.test.js => node.test.ts} (92%) rename packages/astro/test/units/app/{response.test.js => response.test.ts} (87%) rename packages/astro/test/units/app/{test-helpers.js => test-helpers.ts} (64%) rename packages/astro/test/units/app/{trailing-slash.test.js => trailing-slash.test.ts} (87%) rename packages/astro/test/units/app/{url-attribute-xss.test.js => url-attribute-xss.test.ts} (98%) create mode 100644 packages/astro/test/units/assets/endpoint-svg-reject.test.ts rename packages/astro/test/units/assets/fonts/{core.test.js => core.test.ts} (98%) rename packages/astro/test/units/assets/fonts/{e2e.test.js => e2e.test.ts} (94%) rename packages/astro/test/units/assets/fonts/{infra.test.js => infra.test.ts} (93%) rename packages/astro/test/units/assets/fonts/{providers.test.js => providers.test.ts} (99%) delete mode 100644 packages/astro/test/units/assets/fonts/utils.js rename packages/astro/test/units/assets/fonts/{utils.test.js => utils.test.ts} (99%) create mode 100644 packages/astro/test/units/assets/fonts/utils.ts rename packages/astro/test/units/assets/{getImage.test.js => getImage.test.ts} (95%) rename packages/astro/test/units/assets/{image-layout.test.js => image-layout.test.ts} (100%) rename packages/astro/test/units/assets/{image-service.test.js => image-service.test.ts} (96%) rename packages/astro/test/units/assets/{remote.test.js => remote.test.ts} (84%) create mode 100644 packages/astro/test/units/assets/utils.test.ts rename packages/astro/test/units/build/{generate.test.js => generate.test.ts} (82%) rename packages/astro/test/units/build/{preserve-build-client-dir.test.js => preserve-build-client-dir.test.ts} (63%) rename packages/astro/test/units/build/{server-islands.test.js => server-islands.test.ts} (96%) rename packages/astro/test/units/build/{static-build.test.js => static-build.test.ts} (65%) rename packages/astro/test/units/build/{test-helpers.js => test-helpers.ts} (76%) rename packages/astro/test/units/cache/{memory-provider.test.js => memory-provider.test.ts} (81%) rename packages/astro/test/units/cache/{noop.test.js => noop.test.ts} (89%) rename packages/astro/test/units/cache/{route-matching.test.js => route-matching.test.ts} (97%) rename packages/astro/test/units/cache/{runtime.test.js => runtime.test.ts} (96%) rename packages/astro/test/units/cache/{utils.test.js => utils.test.ts} (99%) rename packages/astro/test/units/compile/{css-base-path.test.js => css-base-path.test.ts} (92%) rename packages/astro/test/units/compile/{invalid-css.test.js => invalid-css.test.ts} (82%) rename packages/astro/test/units/compile/{rust-compiler.test.js => rust-compiler.test.ts} (91%) rename packages/astro/test/units/config/{config-merge.test.js => config-merge.test.ts} (56%) rename packages/astro/test/units/config/{config-resolve.test.js => config-resolve.test.ts} (100%) rename packages/astro/test/units/config/{config-server.test.js => config-server.test.ts} (83%) rename packages/astro/test/units/config/{config-tsconfig.test.js => config-tsconfig.test.ts} (73%) rename packages/astro/test/units/config/{config-validate.test.js => config-validate.test.ts} (94%) delete mode 100644 packages/astro/test/units/config/format.test.js create mode 100644 packages/astro/test/units/config/refined-validators.test.ts delete mode 100644 packages/astro/test/units/content-collections/frontmatter.test.js rename packages/astro/test/units/content-collections/{get-entry-info.test.js => get-entry-info.test.ts} (100%) rename packages/astro/test/units/content-collections/{get-entry-type.test.js => get-entry-type.test.ts} (100%) rename packages/astro/test/units/content-collections/{image-references.test.js => image-references.test.ts} (87%) delete mode 100644 packages/astro/test/units/content-collections/mutable-data-store.test.js create mode 100644 packages/astro/test/units/content-collections/mutable-data-store.test.ts rename packages/astro/test/units/content-layer/{core-loader.test.js => core-loader.test.ts} (93%) rename packages/astro/test/units/content-layer/{data-transforms.test.js => data-transforms.test.ts} (87%) rename packages/astro/test/units/content-layer/{file-loader.test.js => file-loader.test.ts} (91%) rename packages/astro/test/units/content-layer/{glob-loader.test.js => glob-loader.test.ts} (87%) rename packages/astro/test/units/content-layer/{live-loaders.test.js => live-loaders.test.ts} (87%) rename packages/astro/test/units/content-layer/{loader-warnings.test.js => loader-warnings.test.ts} (88%) rename packages/astro/test/units/content-layer/{markdown-rendering.test.js => markdown-rendering.test.ts} (88%) rename packages/astro/test/units/content-layer/{schema-validation.test.js => schema-validation.test.ts} (89%) rename packages/astro/test/units/content-layer/{store-persistence.test.js => store-persistence.test.ts} (98%) rename packages/astro/test/units/content-layer/{test-helpers.js => test-helpers.ts} (51%) rename packages/astro/test/units/cookies/{delete.test.js => delete.test.ts} (95%) rename packages/astro/test/units/cookies/{error.test.js => error.test.ts} (86%) rename packages/astro/test/units/cookies/{get.test.js => get.test.ts} (85%) rename packages/astro/test/units/cookies/{has.test.js => has.test.ts} (100%) rename packages/astro/test/units/cookies/{merge.test.js => merge.test.ts} (100%) rename packages/astro/test/units/cookies/{set.test.js => set.test.ts} (92%) rename packages/astro/test/units/csp/{common.test.js => common.test.ts} (82%) rename packages/astro/test/units/csp/{rendering.test.js => rendering.test.ts} (83%) rename packages/astro/test/units/csp/{runtime.test.js => runtime.test.ts} (100%) create mode 100644 packages/astro/test/units/dev/base-rewrite.test.ts delete mode 100644 packages/astro/test/units/dev/base.test.js delete mode 100644 packages/astro/test/units/dev/dev.test.js delete mode 100644 packages/astro/test/units/dev/error-pages.test.js create mode 100644 packages/astro/test/units/dev/error-pages.test.ts delete mode 100644 packages/astro/test/units/dev/hydration.test.js delete mode 100644 packages/astro/test/units/dev/restart.test.js rename packages/astro/test/units/dev/{sec-fetch.test.js => sec-fetch.test.ts} (95%) create mode 100644 packages/astro/test/units/dev/trailing-slash-decision.test.ts rename packages/astro/test/units/env/{env-validators.test.js => env-validators.test.ts} (92%) rename packages/astro/test/units/errors/{dev-utils.test.js => dev-utils.test.ts} (100%) rename packages/astro/test/units/errors/{errors.test.js => errors.test.ts} (100%) create mode 100644 packages/astro/test/units/errors/zod-error-map.test.ts rename packages/astro/test/units/i18n/{astro_i18n.test.js => astro_i18n.test.ts} (81%) rename packages/astro/test/units/i18n/{create-manifest.test.js => create-manifest.test.ts} (93%) rename packages/astro/test/units/i18n/{fallback.test.js => fallback.test.ts} (72%) rename packages/astro/test/units/i18n/{i18n-app.test.js => i18n-app.test.ts} (79%) rename packages/astro/test/units/i18n/{i18n-middleware.test.js => i18n-middleware.test.ts} (73%) rename packages/astro/test/units/i18n/{i18n-routing-static.test.js => i18n-routing-static.test.ts} (94%) rename packages/astro/test/units/i18n/{i18n-static-build.test.js => i18n-static-build.test.ts} (96%) rename packages/astro/test/units/i18n/{i18n-utils.test.js => i18n-utils.test.ts} (87%) rename packages/astro/test/units/i18n/{manual-middleware.test.js => manual-middleware.test.ts} (88%) rename packages/astro/test/units/i18n/{manual-routing.test.js => manual-routing.test.ts} (96%) rename packages/astro/test/units/i18n/{router.test.js => router.test.ts} (73%) delete mode 100644 packages/astro/test/units/i18n/test-helpers.js create mode 100644 packages/astro/test/units/i18n/test-helpers.ts delete mode 100644 packages/astro/test/units/integrations/api.test.js create mode 100644 packages/astro/test/units/integrations/api.test.ts create mode 100644 packages/astro/test/units/integrations/hooks.test.ts create mode 100644 packages/astro/test/units/logger/destination.test.ts rename packages/astro/test/units/logger/{locale.test.js => locale.test.ts} (100%) create mode 100644 packages/astro/test/units/manifest/serialized.test.js rename packages/astro/test/units/middleware/{call-middleware.test.js => call-middleware.test.ts} (80%) rename packages/astro/test/units/middleware/{locals.test.js => locals.test.ts} (95%) rename packages/astro/test/units/middleware/{middleware-app.test.js => middleware-app.test.ts} (88%) rename packages/astro/test/units/middleware/{sequence.test.js => sequence.test.ts} (66%) rename packages/astro/test/units/{mocks.js => mocks.ts} (57%) create mode 100644 packages/astro/test/units/preferences/dlv.test.ts rename packages/astro/test/units/redirects/{open-redirect.test.js => open-redirect.test.ts} (100%) rename packages/astro/test/units/redirects/{render.test.js => render.test.ts} (84%) rename packages/astro/test/units/redirects/{static-build.test.js => static-build.test.ts} (88%) rename packages/astro/test/units/redirects/{template.test.js => template.test.ts} (93%) rename packages/astro/test/units/{remote-pattern.test.js => remote-pattern.test.ts} (91%) delete mode 100644 packages/astro/test/units/render/chunk.test.js rename packages/astro/test/units/render/{class-list-and-style.test.js => class-list-and-style.test.ts} (99%) delete mode 100644 packages/astro/test/units/render/components.test.js rename packages/astro/test/units/render/{context-helpers.test.js => context-helpers.test.ts} (74%) rename packages/astro/test/units/render/{escape.test.js => escape.test.ts} (90%) rename packages/astro/test/units/render/{head-injection-app.test.js => head-injection-app.test.ts} (75%) rename packages/astro/test/units/render/head-propagation/{boundary.test.js => boundary.test.ts} (100%) rename packages/astro/test/units/render/head-propagation/{buffer.test.js => buffer.test.ts} (80%) rename packages/astro/test/units/render/head-propagation/{comment.test.js => comment.test.ts} (100%) rename packages/astro/test/units/render/head-propagation/{graph.test.js => graph.test.ts} (80%) rename packages/astro/test/units/render/head-propagation/{policy.test.js => policy.test.ts} (100%) rename packages/astro/test/units/render/head-propagation/{resolver.test.js => resolver.test.ts} (98%) rename packages/astro/test/units/render/head-propagation/{runtime-adapters.test.js => runtime-adapters.test.ts} (75%) rename packages/astro/test/units/render/head-propagation/{runtime.test.js => runtime.test.ts} (72%) rename packages/astro/test/units/render/{head.test.js => head.test.ts} (78%) rename packages/astro/test/units/render/{html-primitives.test.js => html-primitives.test.ts} (78%) rename packages/astro/test/units/render/{hydration.test.js => hydration.test.ts} (96%) rename packages/astro/test/units/render/{paginate.test.js => paginate.test.ts} (97%) rename packages/astro/test/units/render/{queue-batching.test.js => queue-batching.test.ts} (84%) rename packages/astro/test/units/render/{queue-pool.test.js => queue-pool.test.ts} (90%) rename packages/astro/test/units/render/{queue-rendering.test.js => queue-rendering.test.ts} (69%) rename packages/astro/test/units/render/{render-context.test.js => render-context.test.ts} (79%) rename packages/astro/test/units/render/{rendering.test.js => rendering.test.ts} (84%) rename packages/astro/test/units/render/{ssr-elements.test.js => ssr-elements.test.ts} (99%) rename packages/astro/test/units/render/{transitions.test.js => transitions.test.ts} (99%) rename packages/astro/test/units/routing/{api-context.test.js => api-context.test.ts} (91%) rename packages/astro/test/units/routing/{dev-routing.test.js => dev-routing.test.ts} (99%) rename packages/astro/test/units/routing/{dynamic-route-collision.test.js => dynamic-route-collision.test.ts} (99%) delete mode 100644 packages/astro/test/units/routing/endpoints.test.js rename packages/astro/test/units/routing/{generator.test.js => generator.test.ts} (89%) rename packages/astro/test/units/routing/{get-params.test.js => get-params.test.ts} (96%) rename packages/astro/test/units/routing/{getstaticpaths-cache.test.js => getstaticpaths-cache.test.ts} (77%) rename packages/astro/test/units/routing/{manifest.test.js => manifest.test.ts} (72%) rename packages/astro/test/units/routing/{origin-pathname.test.js => origin-pathname.test.ts} (100%) rename packages/astro/test/units/routing/{params-encoding.test.js => params-encoding.test.ts} (86%) rename packages/astro/test/units/routing/{params-validation.test.js => params-validation.test.ts} (70%) rename packages/astro/test/units/routing/{prerender.test.js => prerender.test.ts} (100%) rename packages/astro/test/units/routing/{preview-routing.test.js => preview-routing.test.ts} (93%) delete mode 100644 packages/astro/test/units/routing/resolved-pathname.test.js create mode 100644 packages/astro/test/units/routing/resolved-pathname.test.ts rename packages/astro/test/units/routing/{rewrite-app.test.js => rewrite-app.test.ts} (89%) rename packages/astro/test/units/routing/{rewrite-validation.test.js => rewrite-validation.test.ts} (86%) rename packages/astro/test/units/routing/{rewrite.test.js => rewrite.test.ts} (99%) rename packages/astro/test/units/routing/{route-manifest.test.js => route-manifest.test.ts} (85%) delete mode 100644 packages/astro/test/units/routing/route-matching.test.js create mode 100644 packages/astro/test/units/routing/route-matching.test.ts delete mode 100644 packages/astro/test/units/routing/route-sanitization.test.js create mode 100644 packages/astro/test/units/routing/route-sanitization.test.ts rename packages/astro/test/units/routing/{router-match.test.js => router-match.test.ts} (99%) create mode 100644 packages/astro/test/units/routing/routing-helpers.test.ts rename packages/astro/test/units/routing/{routing-priority.test.js => routing-priority.test.ts} (99%) delete mode 100644 packages/astro/test/units/routing/test-helpers.js create mode 100644 packages/astro/test/units/routing/test-helpers.ts delete mode 100644 packages/astro/test/units/routing/trailing-slash.test.js create mode 100644 packages/astro/test/units/routing/trailing-slash.test.ts rename packages/astro/test/units/routing/{virtual-routes.test.js => virtual-routes.test.ts} (92%) delete mode 100644 packages/astro/test/units/runtime/endpoints.test.js rename packages/astro/test/units/runtime/{static-paths.test.js => static-paths.test.ts} (84%) rename packages/astro/test/units/server-islands/{encryption.test.js => encryption.test.ts} (100%) rename packages/astro/test/units/server-islands/{endpoint.test.js => endpoint.test.ts} (84%) rename packages/astro/test/units/server-islands/{server-islands-render.test.js => server-islands-render.test.ts} (86%) rename packages/astro/test/units/server-islands/{shared-state.test.js => shared-state.test.ts} (100%) rename packages/astro/test/units/sessions/{astro-session.test.js => astro-session.test.ts} (71%) rename packages/astro/test/units/{teardown.js => teardown.ts} (89%) delete mode 100644 packages/astro/test/units/test-utils.js create mode 100644 packages/astro/test/units/test-utils.ts rename packages/astro/test/units/vite-plugin-astro-server/{controller.test.js => controller.test.ts} (61%) delete mode 100644 packages/astro/test/units/vite-plugin-astro-server/request.test.js delete mode 100644 packages/astro/test/units/vite-plugin-astro-server/response.test.js rename packages/astro/test/units/vite-plugin-astro/{compile.test.js => compile.test.ts} (62%) rename packages/astro/test/units/vite-plugin-astro/{hmr.test.js => hmr.test.ts} (100%) rename packages/astro/test/units/vite-plugin-html/{escape.test.js => escape.test.ts} (93%) rename packages/astro/test/units/vite-plugin-html/{slots.test.js => slots.test.ts} (98%) rename packages/astro/test/units/vite-plugin-html/{transform.test.js => transform.test.ts} (90%) delete mode 100644 packages/astro/test/unused-slot.test.js rename packages/create-astro/test/{context.test.js => context.test.ts} (83%) rename packages/create-astro/test/{dependencies.test.js => dependencies.test.ts} (73%) rename packages/create-astro/test/{git.test.js => git.test.ts} (70%) rename packages/create-astro/test/{integrations.test.js => integrations.test.ts} (78%) rename packages/create-astro/test/{intro.test.js => intro.test.ts} (57%) rename packages/create-astro/test/{next.test.js => next.test.ts} (59%) rename packages/create-astro/test/{package-name-validation.test.js => package-name-validation.test.ts} (100%) rename packages/create-astro/test/{project-name.test.js => project-name.test.ts} (58%) rename packages/create-astro/test/{template-processing.test.js => template-processing.test.ts} (100%) rename packages/create-astro/test/{template.test.js => template.test.ts} (53%) delete mode 100644 packages/create-astro/test/utils.js create mode 100644 packages/create-astro/test/utils.ts rename packages/create-astro/test/{verify.test.js => verify.test.ts} (63%) create mode 100644 packages/create-astro/tsconfig.test.json rename packages/db/test/{basics.test.js => basics.test.ts} (92%) rename packages/db/test/{db-in-src.test.js => db-in-src.test.ts} (86%) rename packages/db/test/{error-handling.test.js => error-handling.test.ts} (82%) rename packages/db/test/{integration-only.test.js => integration-only.test.ts} (89%) rename packages/db/test/{integrations.test.js => integrations.test.ts} (92%) rename packages/db/test/{libsql-remote.test.js => libsql-remote.test.ts} (91%) rename packages/db/test/{local-prod.test.js => local-prod.test.ts} (93%) rename packages/db/test/{no-seed.test.js => no-seed.test.ts} (88%) rename packages/db/test/{ssr-no-apptoken.test.js => ssr-no-apptoken.test.ts} (81%) rename packages/db/test/{static-remote.test.js => static-remote.test.ts} (84%) rename packages/db/test/{test-utils.js => test-utils.ts} (72%) rename packages/db/test/unit/{column-queries.test.js => column-queries.test.ts} (91%) rename packages/db/test/unit/{db-client.test.js => db-client.test.ts} (100%) rename packages/db/test/unit/{index-queries.test.js => index-queries.test.ts} (92%) rename packages/db/test/unit/{reference-queries.test.js => reference-queries.test.ts} (92%) rename packages/db/test/unit/{remote-info.test.js => remote-info.test.ts} (92%) rename packages/db/test/unit/{reset-queries.test.js => reset-queries.test.ts} (82%) create mode 100644 packages/db/tsconfig.test.json rename packages/integrations/alpinejs/test/{basics.test.js => basics.test.ts} (100%) rename packages/integrations/alpinejs/test/{directive.test.js => directive.test.ts} (100%) rename packages/integrations/alpinejs/test/{plugin-script-import.test.js => plugin-script-import.test.ts} (100%) rename packages/integrations/alpinejs/test/{test-utils.js => test-utils.ts} (74%) create mode 100644 packages/integrations/alpinejs/tsconfig.test.json delete mode 100644 packages/integrations/cloudflare/src/info.ts rename packages/integrations/cloudflare/test/{astro-dev-platform.test.js => astro-dev-platform.test.ts} (94%) rename packages/integrations/cloudflare/test/{astro-env.test.js => astro-env.test.ts} (93%) rename packages/integrations/cloudflare/test/{binding-image-service.test.js => binding-image-service.test.ts} (59%) rename packages/integrations/cloudflare/test/{client-address.test.js => client-address.test.ts} (96%) rename packages/integrations/cloudflare/test/{compile-image-service.test.js => compile-image-service.test.ts} (90%) rename packages/integrations/cloudflare/test/{custom-entryfile.test.js => custom-entryfile.test.ts} (88%) rename packages/integrations/cloudflare/test/{dev-image-endpoint.test.js => dev-image-endpoint.test.ts} (92%) rename packages/integrations/cloudflare/test/{external-image-service.test.js => external-image-service.test.ts} (90%) rename packages/integrations/cloudflare/test/{external-redirects.test.js => external-redirects.test.ts} (93%) create mode 100644 packages/integrations/cloudflare/test/fixtures/prerender-node-env/fake-svelte-pkg/package.json create mode 100644 packages/integrations/cloudflare/test/fixtures/prerender-node-env/fake-svelte-pkg/src/FakeComponent.svelte create mode 100644 packages/integrations/cloudflare/test/fixtures/prerender-node-env/fake-svelte-pkg/src/index.js create mode 100644 packages/integrations/cloudflare/test/fixtures/prerender-node-env/src/components/SvelteWrapper.svelte create mode 100644 packages/integrations/cloudflare/test/fixtures/prerender-node-env/src/pages/svelte.astro create mode 100644 packages/integrations/cloudflare/test/fixtures/prerender-queue-consumers/astro.config.mjs create mode 100644 packages/integrations/cloudflare/test/fixtures/prerender-queue-consumers/package.json create mode 100644 packages/integrations/cloudflare/test/fixtures/prerender-queue-consumers/src/pages/api.ts create mode 100644 packages/integrations/cloudflare/test/fixtures/prerender-queue-consumers/src/pages/index.astro create mode 100644 packages/integrations/cloudflare/test/fixtures/prerender-queue-consumers/wrangler.jsonc rename packages/integrations/cloudflare/test/{internal-redirects.test.js => internal-redirects.test.ts} (92%) rename packages/integrations/cloudflare/test/{prerender-node-env.test.js => prerender-node-env.test.ts} (84%) create mode 100644 packages/integrations/cloudflare/test/prerender-queue-consumers.test.ts rename packages/integrations/cloudflare/test/{prerender-styles.test.js => prerender-styles.test.ts} (93%) rename packages/integrations/cloudflare/test/{prerenderer-errors.test.js => prerenderer-errors.test.ts} (89%) rename packages/integrations/cloudflare/test/{routing-priority.test.js => routing-priority.test.ts} (96%) rename packages/integrations/cloudflare/test/{server-entry.test.js => server-entry.test.ts} (86%) rename packages/integrations/cloudflare/test/{server-island-prerender-deps.test.js => server-island-prerender-deps.test.ts} (93%) rename packages/integrations/cloudflare/test/{sessions.test.js => sessions.test.ts} (76%) rename packages/integrations/cloudflare/test/{sql-import.test.js => sql-import.test.ts} (86%) rename packages/integrations/cloudflare/test/{ssr-deps.test.js => ssr-deps.test.ts} (74%) rename packages/integrations/cloudflare/test/{static.test.js => static.test.ts} (87%) rename packages/integrations/cloudflare/test/{svelte-rune-deps.test.js => svelte-rune-deps.test.ts} (92%) rename packages/integrations/cloudflare/test/{_test-utils.js => test-utils.ts} (62%) rename packages/integrations/cloudflare/test/{top-level-return.test.js => top-level-return.test.ts} (76%) rename packages/integrations/cloudflare/test/{with-base.test.js => with-base.test.ts} (64%) rename packages/integrations/cloudflare/test/{with-react.test.js => with-react.test.ts} (76%) rename packages/integrations/cloudflare/test/{with-solid-js.test.js => with-solid-js.test.ts} (82%) rename packages/integrations/cloudflare/test/{with-svelte.test.js => with-svelte.test.ts} (82%) rename packages/integrations/cloudflare/test/{with-vue.test.js => with-vue.test.ts} (81%) rename packages/integrations/cloudflare/test/{wrangler-preview-platform.test.js => wrangler-preview-platform.test.ts} (90%) rename packages/integrations/cloudflare/test/{wrangler.test.js => wrangler.test.ts} (100%) create mode 100644 packages/integrations/cloudflare/tsconfig.test.json rename packages/integrations/markdoc/test/{content-collections.test.js => content-collections.test.ts} (83%) rename packages/integrations/markdoc/test/{content-layer.test.js => content-layer.test.ts} (75%) rename packages/integrations/markdoc/test/{headings.test.js => headings.test.ts} (91%) rename packages/integrations/markdoc/test/{image-assets.test.js => image-assets.test.ts} (71%) rename packages/integrations/markdoc/test/{propagated-assets.test.js => propagated-assets.test.ts} (75%) rename packages/integrations/markdoc/test/{render-components.test.js => render-components.test.ts} (76%) rename packages/integrations/markdoc/test/{render-extends-components.test.js => render-extends-components.test.ts} (79%) rename packages/integrations/markdoc/test/{render-html.test.js => render-html.test.ts} (80%) rename packages/integrations/markdoc/test/{render-indented-components.test.js => render-indented-components.test.ts} (78%) rename packages/integrations/markdoc/test/{render-table-attrs.test.js => render-table-attrs.test.ts} (92%) rename packages/integrations/markdoc/test/{render-with-transform.test.js => render-with-transform.test.ts} (79%) rename packages/integrations/markdoc/test/{render.test.js => render.test.ts} (82%) rename packages/integrations/markdoc/test/{syntax-highlighting.test.js => syntax-highlighting.test.ts} (54%) rename packages/integrations/markdoc/test/{variables.test.js => variables.test.ts} (91%) create mode 100644 packages/integrations/markdoc/tsconfig.test.json rename packages/integrations/mdx/test/{css-head-mdx.test.js => css-head-mdx.test.ts} (97%) rename packages/{astro/test/fixtures/css-inline-stylesheets-3 => integrations/mdx/test/fixtures/mdx-astro-container-escape}/astro.config.mjs (58%) rename packages/{astro/test/fixtures/core-image-svg-optimized => integrations/mdx/test/fixtures/mdx-astro-container-escape}/package.json (63%) create mode 100644 packages/integrations/mdx/test/fixtures/mdx-astro-container-escape/src/components/Div.astro create mode 100644 packages/integrations/mdx/test/fixtures/mdx-astro-container-escape/src/pages/index.astro create mode 100644 packages/integrations/mdx/test/fixtures/mdx-astro-container-escape/src/posts/post.mdx rename packages/integrations/mdx/test/fixtures/{mdx-component/src/components => mdx-basics/src/components/component}/Test.mdx (100%) rename packages/integrations/mdx/test/fixtures/{mdx-component/src/components => mdx-basics/src/components/component}/WithFragment.mdx (100%) rename packages/integrations/mdx/test/fixtures/{mdx-slots/src/components => mdx-basics/src/components/slots}/Slotted.astro (100%) rename packages/integrations/mdx/test/fixtures/{mdx-slots/src/components => mdx-basics/src/components/slots}/Test.mdx (100%) rename packages/integrations/mdx/test/fixtures/{mdx-get-static-paths => mdx-basics}/src/content/1.mdx (100%) rename packages/integrations/mdx/test/fixtures/{mdx-frontmatter => mdx-basics}/src/layouts/Base.astro (100%) rename packages/integrations/mdx/test/fixtures/{mdx-component/src/pages => mdx-basics/src/pages/component}/glob.astro (76%) create mode 100644 packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/component/index.astro create mode 100644 packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/component/w-fragment.astro rename packages/integrations/mdx/test/fixtures/{mdx-frontmatter/src/pages => mdx-basics/src/pages/frontmatter}/glob.json.js (100%) rename packages/integrations/mdx/test/fixtures/{mdx-frontmatter/src/pages => mdx-basics/src/pages/frontmatter}/index.mdx (87%) rename packages/integrations/mdx/test/fixtures/{mdx-frontmatter/src/pages => mdx-basics/src/pages/frontmatter}/with-headings.mdx (50%) rename packages/integrations/mdx/test/fixtures/{mdx-script-style-raw/src/pages/index.mdx => mdx-basics/src/pages/script-style-raw.mdx} (100%) rename packages/integrations/mdx/test/fixtures/{mdx-slots/src/pages => mdx-basics/src/pages/slots}/glob.astro (63%) create mode 100644 packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/slots/index.astro rename packages/integrations/mdx/test/fixtures/{mdx-get-static-paths/src/pages => mdx-basics/src/pages/static-paths}/[slug].astro (87%) rename packages/integrations/mdx/test/fixtures/{mdx-url-export/src/pages => mdx-basics/src/pages/url-export}/pages.json.js (100%) create mode 100644 packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/url-export/test-1.mdx create mode 100644 packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/url-export/test-2.mdx rename packages/integrations/mdx/test/fixtures/{mdx-url-export/src/pages => mdx-basics/src/pages/url-export}/with-url-override.mdx (100%) delete mode 100644 packages/integrations/mdx/test/fixtures/mdx-component/src/pages/index.astro delete mode 100644 packages/integrations/mdx/test/fixtures/mdx-component/src/pages/w-fragment.astro delete mode 100644 packages/integrations/mdx/test/fixtures/mdx-escape/src/components/Em.astro delete mode 100644 packages/integrations/mdx/test/fixtures/mdx-escape/src/components/P.astro delete mode 100644 packages/integrations/mdx/test/fixtures/mdx-escape/src/components/Title.astro delete mode 100644 packages/integrations/mdx/test/fixtures/mdx-escape/src/pages/html-tag.mdx delete mode 100644 packages/integrations/mdx/test/fixtures/mdx-escape/src/pages/index.mdx delete mode 100644 packages/integrations/mdx/test/fixtures/mdx-slots/src/pages/index.astro delete mode 100644 packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/test-1.mdx delete mode 100644 packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/test-2.mdx rename packages/integrations/mdx/test/{invalid-mdx-component.test.js => invalid-mdx-component.test.ts} (76%) create mode 100644 packages/integrations/mdx/test/mdx-astro-container-escape.test.ts rename packages/integrations/mdx/test/{mdx-astro-markdown-remarkRehype.test.js => mdx-astro-markdown-remarkRehype.test.ts} (79%) create mode 100644 packages/integrations/mdx/test/mdx-basics.test.ts delete mode 100644 packages/integrations/mdx/test/mdx-component.test.js rename packages/integrations/mdx/test/{mdx-content-layer.test.js => mdx-content-layer.test.ts} (74%) delete mode 100644 packages/integrations/mdx/test/mdx-escape.test.js rename packages/integrations/mdx/test/{mdx-frontmatter-injection.test.js => mdx-frontmatter-injection.test.ts} (59%) delete mode 100644 packages/integrations/mdx/test/mdx-frontmatter.test.js rename packages/integrations/mdx/test/{mdx-get-headings.test.js => mdx-get-headings.test.ts} (69%) delete mode 100644 packages/integrations/mdx/test/mdx-get-static-paths.test.js rename packages/integrations/mdx/test/{mdx-images.test.js => mdx-images.test.ts} (77%) rename packages/integrations/mdx/test/{mdx-infinite-loop.test.js => mdx-infinite-loop.test.ts} (82%) rename packages/integrations/mdx/test/{mdx-math.test.js => mdx-math.test.ts} (100%) rename packages/integrations/mdx/test/{mdx-namespace.test.js => mdx-namespace.test.ts} (82%) rename packages/integrations/mdx/test/{mdx-optimize.test.js => mdx-optimize.test.ts} (82%) rename packages/integrations/mdx/test/{mdx-page.test.js => mdx-page.test.ts} (85%) delete mode 100644 packages/integrations/mdx/test/mdx-plugins.test.js create mode 100644 packages/integrations/mdx/test/mdx-plugins.test.ts rename packages/integrations/mdx/test/{mdx-plus-react-errors.test.js => mdx-plus-react-errors.test.ts} (67%) rename packages/integrations/mdx/test/{mdx-plus-react.test.js => mdx-plus-react.test.ts} (82%) delete mode 100644 packages/integrations/mdx/test/mdx-script-style-raw.test.js delete mode 100644 packages/integrations/mdx/test/mdx-slots.test.js rename packages/integrations/mdx/test/{mdx-syntax-highlighting.test.js => mdx-syntax-highlighting.test.ts} (92%) delete mode 100644 packages/integrations/mdx/test/mdx-url-export.test.js rename packages/integrations/mdx/test/{mdx-vite-env-vars.test.js => mdx-vite-env-vars.test.ts} (95%) rename packages/integrations/mdx/test/{remark-imgattr.test.js => remark-imgattr.test.ts} (75%) create mode 100644 packages/integrations/mdx/test/test-utils.ts create mode 100644 packages/integrations/mdx/test/units/mdx-compilation.test.ts rename packages/integrations/mdx/test/units/{rehype-optimize-static.test.js => rehype-optimize-static.test.ts} (81%) create mode 100644 packages/integrations/mdx/test/units/rehype-plugins.test.ts create mode 100644 packages/integrations/mdx/test/units/server.test.ts create mode 100644 packages/integrations/mdx/test/units/utils.test.ts create mode 100644 packages/integrations/mdx/test/units/vite-plugin-mdx-postprocess.test.ts create mode 100644 packages/integrations/mdx/tsconfig.test.json rename packages/integrations/netlify/test/development/{primitives.test.js => primitives.test.ts} (93%) delete mode 100644 packages/integrations/netlify/test/functions/cookies.test.js create mode 100644 packages/integrations/netlify/test/functions/cookies.test.ts delete mode 100644 packages/integrations/netlify/test/functions/edge-middleware.test.js create mode 100644 packages/integrations/netlify/test/functions/edge-middleware.test.ts delete mode 100644 packages/integrations/netlify/test/functions/image-cdn.test.js create mode 100644 packages/integrations/netlify/test/functions/image-cdn.test.ts delete mode 100644 packages/integrations/netlify/test/functions/include-files.test.js create mode 100644 packages/integrations/netlify/test/functions/include-files.test.ts delete mode 100644 packages/integrations/netlify/test/functions/redirects.test.js create mode 100644 packages/integrations/netlify/test/functions/redirects.test.ts rename packages/integrations/netlify/test/functions/{sessions.test.js => sessions.test.ts} (83%) delete mode 100644 packages/integrations/netlify/test/functions/skew-protection.test.js create mode 100644 packages/integrations/netlify/test/functions/skew-protection.test.ts rename packages/integrations/netlify/test/hosted/{hosted.test.js => hosted.test.ts} (74%) create mode 100644 packages/integrations/netlify/test/static/fixtures/image-missing-dimension/astro.config.mjs create mode 100644 packages/integrations/netlify/test/static/fixtures/image-missing-dimension/package.json create mode 100644 packages/integrations/netlify/test/static/fixtures/image-missing-dimension/src/pages/index.astro rename packages/integrations/netlify/test/static/{headers.test.js => headers.test.ts} (87%) create mode 100644 packages/integrations/netlify/test/static/image-missing-dimension.test.ts rename packages/integrations/netlify/test/static/{redirects.test.js => redirects.test.ts} (67%) rename packages/integrations/netlify/test/static/{static-headers.test.js => static-headers.test.ts} (78%) create mode 100644 packages/integrations/netlify/test/test-utils.ts create mode 100644 packages/integrations/netlify/tsconfig.test.json rename packages/integrations/node/test/{api-route.test.js => api-route.test.ts} (85%) rename packages/integrations/node/test/{assets.test.js => assets.test.ts} (50%) rename packages/integrations/node/test/{bad-urls.test.js => bad-urls.test.ts} (88%) rename packages/integrations/node/test/{encoded.test.js => encoded.test.ts} (74%) delete mode 100644 packages/integrations/node/test/errors.test.js create mode 100644 packages/integrations/node/test/errors.test.ts rename packages/integrations/node/test/{headers.test.js => headers.test.ts} (92%) rename packages/integrations/node/test/{image.test.js => image.test.ts} (96%) rename packages/integrations/node/test/{locals.test.js => locals.test.ts} (78%) rename packages/integrations/node/test/{node-middleware-listener-cleanup.test.js => node-middleware-listener-cleanup.test.ts} (82%) rename packages/integrations/node/test/{node-middleware.test.js => node-middleware.test.ts} (91%) rename packages/integrations/node/test/{prerender-404-500.test.js => prerender-404-500.test.ts} (92%) rename packages/integrations/node/test/{prerender.test.js => prerender.test.ts} (94%) rename packages/integrations/node/test/{prerendered-error-page-fetch.test.js => prerendered-error-page-fetch.test.ts} (77%) rename packages/integrations/node/test/{preview-headers.test.js => preview-headers.test.ts} (80%) rename packages/integrations/node/test/{preview-host.test.js => preview-host.test.ts} (97%) rename packages/integrations/node/test/{redirects.test.js => redirects.test.ts} (91%) rename packages/integrations/node/test/{server-host.test.js => server-host.test.ts} (100%) rename packages/integrations/node/test/{sessions.test.js => sessions.test.ts} (93%) rename packages/integrations/node/test/{static-headers.test.js => static-headers.test.ts} (82%) delete mode 100644 packages/integrations/node/test/test-utils.js create mode 100644 packages/integrations/node/test/test-utils.ts rename packages/integrations/node/test/{trailing-slash.test.js => trailing-slash.test.ts} (95%) rename packages/integrations/node/test/units/{resolve-client-dir.test.js => resolve-client-dir.test.ts} (100%) rename packages/integrations/node/test/units/{serve-static-path-traversal.test.js => serve-static-path-traversal.test.ts} (94%) rename packages/integrations/node/test/{url.test.js => url.test.ts} (82%) rename packages/integrations/node/test/{well-known-locations.test.js => well-known-locations.test.ts} (84%) create mode 100644 packages/integrations/node/tsconfig.test.json rename packages/{astro/test/fixtures/middleware-sequence-request-clone => integrations/react/test/fixtures/react-19-preloads}/astro.config.mjs (55%) create mode 100644 packages/integrations/react/test/fixtures/react-19-preloads/package.json create mode 100644 packages/integrations/react/test/fixtures/react-19-preloads/src/components/ImageComponent.jsx create mode 100644 packages/integrations/react/test/fixtures/react-19-preloads/src/pages/index.astro rename packages/integrations/react/test/{parsed-react-children.test.js => parsed-react-children.test.ts} (100%) create mode 100644 packages/integrations/react/test/react-19-preloads.test.ts rename packages/integrations/react/test/{react-component.test.js => react-component.test.ts} (88%) create mode 100644 packages/integrations/react/tsconfig.test.json rename packages/integrations/sitemap/test/{base-path.test.js => base-path.test.ts} (94%) rename packages/integrations/sitemap/test/{chunks-files.test.js => chunks-files.test.ts} (71%) rename packages/integrations/sitemap/test/{config.test.js => config.test.ts} (97%) rename packages/integrations/sitemap/test/{custom-pages.test.js => custom-pages.test.ts} (78%) rename packages/integrations/sitemap/test/{custom-sitemaps.test.js => custom-sitemaps.test.ts} (75%) rename packages/integrations/sitemap/test/{dynamic-path.test.js => dynamic-path.test.ts} (78%) rename packages/integrations/sitemap/test/{i18n-fallback.test.js => i18n-fallback.test.ts} (80%) rename packages/integrations/sitemap/test/{namespaces.test.js => namespaces.test.ts} (96%) rename packages/integrations/sitemap/test/{routes.test.js => routes.test.ts} (77%) rename packages/integrations/sitemap/test/{smoke.test.js => smoke.test.ts} (100%) rename packages/integrations/sitemap/test/{ssr.test.js => ssr.test.ts} (86%) rename packages/integrations/sitemap/test/{staticPaths.test.js => staticPaths.test.ts} (88%) rename packages/integrations/sitemap/test/{trailing-slash.test.js => trailing-slash.test.ts} (97%) rename packages/integrations/sitemap/test/units/{generate-sitemap.test.js => generate-sitemap.test.ts} (100%) create mode 100644 packages/integrations/sitemap/tsconfig.test.json rename packages/integrations/svelte/test/{async-rendering.test.js => async-rendering.test.ts} (87%) rename packages/integrations/svelte/test/{check.test.js => check.test.ts} (100%) rename packages/integrations/svelte/test/{conditional-rendering.test.js => conditional-rendering.test.ts} (94%) rename packages/integrations/svelte/test/{empty-class-attribute.test.js => empty-class-attribute.test.ts} (93%) rename packages/integrations/svelte/test/{extract-generics.test.js => extract-generics.test.ts} (100%) create mode 100644 packages/integrations/svelte/tsconfig.test.json rename packages/integrations/vercel/test/{edge-middleware.test.js => edge-middleware.test.ts} (67%) rename packages/integrations/vercel/test/hosted/{hosted.test.js => hosted.test.ts} (100%) rename packages/integrations/vercel/test/{image.test.js => image.test.ts} (88%) rename packages/integrations/vercel/test/{integration-assets.test.js => integration-assets.test.ts} (90%) rename packages/integrations/vercel/test/{isr.test.js => isr.test.ts} (91%) rename packages/integrations/vercel/test/{max-duration.test.js => max-duration.test.ts} (77%) rename packages/integrations/vercel/test/{path-override-security.test.js => path-override-security.test.ts} (83%) rename packages/integrations/vercel/test/{prerendered-error-pages.test.js => prerendered-error-pages.test.ts} (66%) rename packages/integrations/vercel/test/{redirects-serverless.test.js => redirects-serverless.test.ts} (79%) rename packages/integrations/vercel/test/{redirects.test.js => redirects.test.ts} (68%) rename packages/integrations/vercel/test/{server-islands.test.js => server-islands.test.ts} (70%) rename packages/integrations/vercel/test/{serverless-prerender.test.js => serverless-prerender.test.ts} (82%) rename packages/integrations/vercel/test/{serverless-with-dynamic-routes.test.js => serverless-with-dynamic-routes.test.ts} (75%) rename packages/integrations/vercel/test/{static-assets.test.js => static-assets.test.ts} (74%) rename packages/integrations/vercel/test/{static-headers.test.js => static-headers.test.ts} (70%) rename packages/integrations/vercel/test/{static.test.js => static.test.ts} (79%) rename packages/integrations/vercel/test/{streaming.test.js => streaming.test.ts} (77%) delete mode 100644 packages/integrations/vercel/test/test-image-service.js delete mode 100644 packages/integrations/vercel/test/test-utils.js create mode 100644 packages/integrations/vercel/test/test-utils.ts rename packages/integrations/vercel/test/{web-analytics.test.js => web-analytics.test.ts} (83%) create mode 100644 packages/integrations/vercel/tsconfig.test.json rename packages/integrations/vue/test/{app-entrypoint-css.test.js => app-entrypoint-css.test.ts} (93%) rename packages/integrations/vue/test/{app-entrypoint.test.js => app-entrypoint.test.ts} (83%) rename packages/integrations/vue/test/{basics.test.js => basics.test.ts} (87%) rename packages/integrations/vue/test/{check.test.js => check.test.ts} (100%) rename packages/integrations/vue/test/{test-utils.js => test-utils.ts} (61%) rename packages/integrations/vue/test/{toTsx.test.js => toTsx.test.ts} (100%) create mode 100644 packages/integrations/vue/tsconfig.test.json rename packages/internal-helpers/test/{create-filter.test.js => create-filter.test.ts} (100%) rename packages/internal-helpers/test/{path.test.js => path.test.ts} (99%) rename packages/internal-helpers/test/{request.test.js => request.test.ts} (99%) create mode 100644 packages/internal-helpers/tsconfig.test.json rename packages/markdown/remark/test/{autolinking.test.js => autolinking.test.ts} (91%) rename packages/markdown/remark/test/{browser.test.js => browser.test.ts} (92%) rename packages/markdown/remark/test/{entities.test.js => entities.test.ts} (80%) rename packages/markdown/remark/test/{frontmatter.test.js => frontmatter.test.ts} (93%) rename packages/markdown/remark/test/{highlight.test.js => highlight.test.ts} (100%) rename packages/markdown/remark/test/{plugins.test.js => plugins.test.ts} (62%) rename packages/markdown/remark/test/{prism.test.js => prism.test.ts} (100%) rename packages/markdown/remark/test/{remark-collect-images.test.js => remark-collect-images.test.ts} (81%) rename packages/markdown/remark/test/{shiki.test.js => shiki.test.ts} (78%) create mode 100644 packages/markdown/remark/tsconfig.test.json rename packages/telemetry/test/{config.test.js => config.test.ts} (100%) rename packages/telemetry/test/{index.test.js => index.test.ts} (72%) create mode 100644 packages/telemetry/tsconfig.test.json delete mode 100644 packages/underscore-redirects/test/astro.test.js create mode 100644 packages/underscore-redirects/test/astro.test.ts rename packages/underscore-redirects/test/{print.test.js => print.test.ts} (100%) rename packages/underscore-redirects/test/{weight.test.js => weight.test.ts} (100%) create mode 100644 packages/underscore-redirects/tsconfig.test.json rename packages/upgrade/test/{context.test.js => context.test.ts} (100%) rename packages/upgrade/test/{install.test.js => install.test.ts} (94%) rename packages/upgrade/test/{utils.js => utils.ts} (68%) rename packages/upgrade/test/{verify.test.js => verify.test.ts} (100%) create mode 100644 packages/upgrade/tsconfig.test.json delete mode 100644 scripts/jsconfig.json create mode 100644 scripts/tsconfig.json diff --git a/.agents/skills/triage/comment.md b/.agents/skills/triage/comment.md index 6dc3d5ea1574..9697dc2316b4 100644 --- a/.agents/skills/triage/comment.md +++ b/.agents/skills/triage/comment.md @@ -35,24 +35,38 @@ Generate and return a GitHub comment following the template below. The **Fix** line in the template has three possible forms. Choose the one that matches the triage outcome: -1. **You created a fix:** Use `I was able to fix this issue.` and include the suggested fix link. +1. **You created a fix:** Use `I found a potential fix for this issue.` and include the suggested fix link. Avoid claiming certainty, even if the fix passes tests, frame it as a suggestion that needs human review. 2. **The issue is already fixed on main** (e.g. the user is on an older major version and the bug doesn't reproduce on current main): Use `This issue has already been fixed.` and tell the user how to get the fix (e.g. upgrade). Link the relevant upgrade guide if applicable: [v6](https://docs.astro.build/en/guides/upgrade-to/v6/), [v5](https://docs.astro.build/en/guides/upgrade-to/v5/). -3. **You could not find or create a fix:** Use `I was unable to find a fix for this issue.` and give guidance or a best guess at where the fix might be. +3. **Low-confidence or no fix:** Use `I wasn't able to find a fix, but I identified some areas that may be relevant.` and list the files/code paths that seem related. Frame this as a jumping-off point for a human, not a diagnosis. If a failing test was added, mention it. +4. **No leads at all:** Use `I was unable to determine the cause of this issue.` This should be rare, only use it when you genuinely have nothing useful to point to. ### "Priority" Instructions The **Priority** line communicates the severity of this issue to maintainers. Its goal is to answer the question: **"How bad is it?"** -Select exactly ONE priority label from the `priorityLabels` arg. Use the label descriptions to guide your decision, combined with the triage report's root cause and impact analysis. Render it in bold, with the `- ` prefix removed, like this: `**Priorty P2: Has Workaround.** Then, follow it with 1-2 sentences explaining _why_ you chose that priority. Answer: "who is likely to be affected and under what conditions?". If you are unsure, use your best judgment based on the label descriptions and the triage findings. +Select exactly ONE priority label from the `priorityLabels` arg. Use the label descriptions to guide your decision, combined with the triage report's root cause and impact analysis. Render it in bold, with the `- ` prefix removed, like this: `**Priority P2: Has Workaround.**` Then, follow it with 1-2 sentences explaining _why_ you chose that priority. Answer: "who is likely to be affected and under what conditions?". If you are unsure, use your best judgment based on the label descriptions and the triage findings. + +**Priority calibration — err on the side of lower priority:** + +- **Experimental/unstable features** should almost never be higher than P3. Users of experimental features accept instability. (e.g. a broken option in `experimental.fonts`) +- **Niche adapter/integration combos** (e.g. MDX + Svelte + Cloudflare) are typically P3 or lower unless they affect a core workflow. +- **P4 vs P5** — the key question is breadth: how many typical Astro users would hit this in a standard workflow? (e.g. P4: wrong output for a common routing pattern; P5: `astro build` crashes for most projects) +- **P2: Has Workaround vs P2: Nice to Have** — pick based on whether something behaves unexpectedly (but circumventable) vs. simply a convenience gap (e.g. Has Workaround: unexpected behavior with a way to restructure around it; Nice to Have: cosmetic issue in an error message). If there is no workaround at all, consider P3 or higher instead. +- **When selecting between similar labels**, always refer to their descriptions in `priorityLabels` to make the final call. +- **When in doubt, go lower.** A P3 that gets bumped up by a maintainer is much better than a P5 that causes false alarm. ### Template +The comment must start with an at-a-glance summary, followed by short explanations, then the full report in a collapsible section. Keep the top section scannable, a maintainer should understand the status in under 5 seconds and be able to quickly jump into fixing the issue. + ```markdown -**[I was able to reproduce this issue. / I was unable to reproduce this issue.]** [2-3 sentences describing the root cause, result, and key observations.] +- **Reproduced:** [Yes / No / Skipped — reason] +- **Exploration:** [Yes / No / Partial / Already fixed on main] [If `branchName` is non-null: — [View branch](https://github.com/withastro/astro/compare/{branchName}?expand=1)] +- **Priority:** [See "Priority" Instructions above. Keep to one line explaining why this priority was chosen, who is likely to be affected, and under what conditions (this section should answer the question: "how bad is it?")] -**[See "Fix" Instructions above.]** [1-2 sentences describing the solution, where/when it was already fixed, or guidance on where a fix might be.] [If `branchName` is non-null: [View Suggested Fix](https://github.com/withastro/astro/compare/{branchName}?expand=1)] +[2-3 sentences describing the root cause or key observations, or where/when it was already fixed. Be specific about what's happening and where in the codebase.] -**[See "Priority" Instructions above.]** [1-2 sentences explaining why this priority was chosen, who is likely to be affected, and under what conditions (this section should answer the question: "how bad is it?")] +**[See "Fix" Instructions above.]** [1-2 sentences describing the fix in more detail: what was changed, guidance on where a fix might be, or relevant code areas.]
Full Triage Report @@ -61,7 +75,7 @@ Select exactly ONE priority label from the `priorityLabels` arg. Use the label d
-_This report was made by an LLM. Mistakes happen, check important info._ +_This report was made by an LLM. The analysis may be wrong, and the potential fix might not work, but is intended as a starting point for exploring the issue._ ``` ## Result diff --git a/.agents/skills/triage/diagnose.md b/.agents/skills/triage/diagnose.md index 8bd17de82eae..2a294786cd3d 100644 --- a/.agents/skills/triage/diagnose.md +++ b/.agents/skills/triage/diagnose.md @@ -61,6 +61,8 @@ Iterate until you understand: - What data is being passed - Where the logic diverges from expected behavior +Once done, **revert all instrumentation** before moving on. Use `git checkout -- ` to remove your `console.log` additions from `packages/`. Debug logs must not leak into downstream steps. + ## Step 4: Identify Root Cause Once you understand the issue, document: @@ -75,6 +77,9 @@ Consider: - Is this a regression from a recent change? - Does this affect other similar use cases? - Are there edge cases to consider? +- Never suggest removing a user's dependency (adapters, framework integrations, features like MDX or DB) as a fix, those are things the user needs. The fix must work within the user's existing stack and expected feature-set. + +**Tone calibration:** Describe the root cause factually, not dramatically. Avoid language that overstates impact ("critical flaw", "fundamentally broken", "severe vulnerability") unless the evidence genuinely supports it. A missing null check is a missing null check, not a "critical oversight in the rendering pipeline." The diagnosis should help a maintainer understand what's wrong, guiding them towards a fix, not alarm them. ## Step 5: Write Output diff --git a/.agents/skills/triage/fix.md b/.agents/skills/triage/fix.md index 17bb0805ef74..b6ac206090e6 100644 --- a/.agents/skills/triage/fix.md +++ b/.agents/skills/triage/fix.md @@ -35,7 +35,19 @@ Read `report.md` from the `triageDir` directory to understand: - The suggested approach - Any edge cases to consider -**Skip if prerequisites unmet:** Check `report.md`: If bug not reproduced/skipped OR diagnosis confidence is `low`/`null` OR no root cause found → append "FIX SKIPPED: [reason]" to `report.md` and return `fixed: false`. +**Skip if prerequisites unmet:** Check `report.md`: If bug was not reproduced or was skipped → append "FIX SKIPPED: Not reproduced" to `report.md` and return `fixed: false`. Do NOT attempt a fix based on guesswork when you cannot reproduce or diagnose the issue. + +**Low-confidence path:** If diagnosis confidence is `low` or `null`, or no clear root cause was found → do NOT attempt a code fix. Instead: + +1. Identify the most likely area(s) of the codebase related to the issue (files, functions, code paths). +2. If possible, write a failing test that demonstrates the expected behavior described in the issue. Place it alongside existing tests for that area. +3. If you identified specific code paths, add brief inline comments (prefixed `// TRIAGE:`) near the most relevant lines in `packages/` to help the implementor orient quickly. Keep to 2-3 comments max — these are signposts, not a diagnosis. +4. Append to `report.md`: the areas you identified, why they seem relevant, and any failing test or comments you added. +5. Return `fixed: false`. + +This "breadcrumb" approach is more useful to maintainers than a wrong fix. + +**High-confidence path:** If diagnosis confidence is `medium` or `high` and a clear root cause was identified → proceed with implementing a fix as described in the steps below. **Note:** The repo may be messy from previous steps. Check `git status` and either work from the current state or `git reset --hard` to start clean. @@ -55,6 +67,7 @@ Make changes in `packages/` source files. Follow these principles: - Only change what's necessary to fix the bug - Don't refactor unrelated code - Don't add new features +- **Never "fix" an issue by removing a user's dependency.** Removing an adapter (Cloudflare, Netlify, Vercel, etc.), framework integration (Svelte, React, Vue, etc.), or feature (MDX, DB, etc.) is not a fix, these are things the user needs. The fix must work within the user's existing stack or expected feature set. **Consider edge cases:** diff --git a/.changeset/two-eels-live.md b/.changeset/two-eels-live.md new file mode 100644 index 000000000000..f1bd94acdf99 --- /dev/null +++ b/.changeset/two-eels-live.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes an issue where i18n domains would return 404 when `trailingSlash` is set to `never`. diff --git a/.flue/workflows/issue-triage/WORKFLOW.ts b/.flue/workflows/issue-triage/WORKFLOW.ts index 8502c78626a5..8193ba26f224 100644 --- a/.flue/workflows/issue-triage/WORKFLOW.ts +++ b/.flue/workflows/issue-triage/WORKFLOW.ts @@ -241,19 +241,22 @@ export default async function triage( const triageResult = await runTriagePipeline(flue, issueNumber, issueDetails); let isPushed = false; - // If a successful fix was created, push the fix up to a new branch on GitHub. + // Push the fix branch if there are meaningful changes (fix, failing test, etc.). // The comment we post below will reference that branch, then a maintainer can choose to: // - checkout that branch locally, using the fix as a starting point // - create a PR from that branch entirely in the GH UI // - ignore it completely - if (triageResult.fixed) { + { const diff = await flue.shell('git diff main --stat'); if (diff.stdout.trim()) { const status = await flue.shell('git status --porcelain'); if (status.stdout.trim()) { await flue.shell('git add -A'); + const defaultMessage = triageResult.fixed + ? 'fix(auto-triage): automated fix' + : 'test(auto-triage): failing test and investigation notes'; await flue.shell( - `git commit -m ${JSON.stringify(triageResult.commitMessage ?? 'fix(auto-triage): automated fix')}`, + `git commit -m ${JSON.stringify(triageResult.commitMessage ?? defaultMessage)}`, ); } const pushResult = await flue.shell(`git push -f origin ${branch}`); @@ -274,7 +277,7 @@ export default async function triage( result: v.pipe( v.string(), v.description( - 'Return only the GitHub comment body generated from the template, following the included template directly. This returned comment must start with "**I was able to reproduce this issue.**" or "**I was unable to reproduce this issue.**"', + 'Return only the GitHub comment body generated from the template, following the included template directly. This returned comment must start with the bullet-point summary (- **Reproduced:** ...)', ), ), }); diff --git a/.github/scripts/tsconfig.json b/.github/scripts/tsconfig.json new file mode 100644 index 000000000000..2529f53ec0ef --- /dev/null +++ b/.github/scripts/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "allowJs": true + } +} diff --git a/.github/workflows/check-merge.yml b/.github/workflows/check-merge.yml index ebf67c0a2b6a..42db71d7f64d 100644 --- a/.github/workflows/check-merge.yml +++ b/.github/workflows/check-merge.yml @@ -39,10 +39,23 @@ jobs: with: files: | .changeset/**/*.md - + # Intentionally ran after the changed-files step so the github API is used to identify + # changed files rather than a local git diff, this is more reliable for pull requests + # originating from a forked repository. + - name: Checkout files + id: checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: steps.changed-files.outputs.any_changed == 'true' + with: + ref: ${{ github.event.pull_request.head.sha }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + fetch-depth: 1 + persist-credentials: false + sparse-checkout: | + .changeset - name: Check if any changesets contain minor or major changes id: check - if: steps.blocked.outputs.result != 'true' + if: steps.changed-files.outputs.any_changed == 'true' env: ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} run: | diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 234dc416b8f1..3d1d59355f70 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -36,7 +36,7 @@ jobs: - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: - node-version: 24.14.0 + node-version: 24.14.1 cache: "pnpm" - name: Install dependencies diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95038d18cf40..632c12e85ba8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,7 +124,7 @@ jobs: - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: - node-version: 24.14.0 + node-version: 24.14.1 cache: "pnpm" - name: Install dependencies @@ -141,7 +141,7 @@ jobs: run: pnpm run publint - name: Type-check test files - run: pnpm -C packages/astro run typecheck:tests + run: pnpm run typecheck:tests test: name: 'Test (${{ matrix.TEST_SUITE.name }}): ${{ matrix.os }} (node@${{ matrix.NODE_VERSION }})' diff --git a/.github/workflows/congrats.yml b/.github/workflows/congrats.yml index 4052bdac8776..6ad87c9ce9a1 100644 --- a/.github/workflows/congrats.yml +++ b/.github/workflows/congrats.yml @@ -9,7 +9,7 @@ jobs: congrats: name: congratsbot if: ${{ github.repository_owner == 'withastro' && github.event.head_commit.message != '[ci] format' }} - uses: withastro/automation/.github/workflows/congratsbot.yml@main + uses: withastro/automation/.github/workflows/congratsbot.yml@a5bd0c5748c4d56e687cdd558064f9ee8adfb1f2 # main with: EMOJIS: '🎉,🎊,🧑‍🚀,🥳,🙌,🚀,👏,<:houston_golden:1068575433647456447>,<:astrocoin:894990669515489301>,<:astro_pride:1130501345326157854>' secrets: diff --git a/.github/workflows/continuous_benchmark.yml b/.github/workflows/continuous_benchmark.yml index 6eae291930a4..3e3f2e89a12a 100644 --- a/.github/workflows/continuous_benchmark.yml +++ b/.github/workflows/continuous_benchmark.yml @@ -41,7 +41,7 @@ jobs: - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: - node-version: 24.14.0 + node-version: 24.14.1 cache: "pnpm" - name: Install dependencies @@ -56,7 +56,7 @@ jobs: timeout-minutes: 15 - name: Run the benchmarks - uses: CodSpeedHQ/action@1c8ae4843586d3ba879736b7f6b7b0c990757fab # v4.12.1 + uses: CodSpeedHQ/action@db35df748deb45fdef0960669f57d627c1956c30 # v4.13.1 timeout-minutes: 30 with: working-directory: ./benchmark diff --git a/.github/workflows/diff-dependencies.yml b/.github/workflows/diff-dependencies.yml index d48c503d2465..8809e4d3c55d 100644 --- a/.github/workflows/diff-dependencies.yml +++ b/.github/workflows/diff-dependencies.yml @@ -20,7 +20,7 @@ jobs: fetch-depth: 0 # allows the diff action to access git history - name: Create Diff - uses: e18e/action-dependency-diff@d995338f3b229fe7b2cd82048df5da930f70c7c3 # v1.4.4 + uses: e18e/action-dependency-diff@5d3c6ac2ad2de2eaca1dc120c5accfd9590764b6 # v1.5.1 with: # We’re using this package primarily to track size changes, not as worried about duplicates duplicate-threshold: 100 diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 9faaa1886608..656ac1b6dbe7 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -9,7 +9,7 @@ on: jobs: prettier: if: github.repository_owner == 'withastro' - uses: withastro/automation/.github/workflows/format.yml@main + uses: withastro/automation/.github/workflows/format.yml@a5bd0c5748c4d56e687cdd558064f9ee8adfb1f2 # main with: command: "format" secrets: inherit diff --git a/.github/workflows/issue-close-cleanup.yml b/.github/workflows/issue-close-cleanup.yml new file mode 100644 index 000000000000..d623fea6f428 --- /dev/null +++ b/.github/workflows/issue-close-cleanup.yml @@ -0,0 +1,20 @@ +name: "Issue: Close Cleanup" + +on: + issues: + types: [closed] + +jobs: + cleanup-branch: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Delete triage fix branch + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BRANCH="flue/fix-${{ github.event.issue.number }}" + # Delete the branch if it exists; ignore errors if it doesn't + gh api "repos/${{ github.repository }}/git/refs/heads/${BRANCH}" \ + -X DELETE 2>/dev/null && echo "Deleted branch ${BRANCH}" || echo "No branch ${BRANCH} to clean up" diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index ca57bdde12da..096b5ceb9e94 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -72,7 +72,7 @@ jobs: - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: - node-version: 24.14.0 + node-version: 24.14.1 cache: pnpm - name: Clone Astro Compiler (for debugging) diff --git a/.github/workflows/preview-release.yml b/.github/workflows/preview-release.yml index 2506fafbde12..3992a0e7d598 100644 --- a/.github/workflows/preview-release.yml +++ b/.github/workflows/preview-release.yml @@ -48,7 +48,7 @@ jobs: - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: - node-version: 24.14.0 + node-version: 24.14.1 cache: "pnpm" - name: Install dependencies diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index af1d80d65d5e..dd35b0baa3d0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,7 +36,7 @@ jobs: - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: - node-version: 24.14.0 + node-version: 24.14.1 cache: "pnpm" - name: Install dependencies diff --git a/.github/workflows/scripts.yml b/.github/workflows/scripts.yml index 01ad219dd8cb..68ed1dd565e6 100644 --- a/.github/workflows/scripts.yml +++ b/.github/workflows/scripts.yml @@ -38,7 +38,7 @@ jobs: - name: Setup Node uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: - node-version: 24.14.0 + node-version: 24.14.1 cache: "pnpm" - name: Install dependencies diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 000000000000..26794f96f54b --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,14 @@ +name: Semgrep OSS scan +on: + pull_request: {} + schedule: + - cron: '0 0 * * 6' +jobs: + semgrep: + name: semgrep-oss + runs-on: ubuntu-latest + container: + image: semgrep/semgrep@sha256:500acf49f5e5785aa89af609b983f0427ac8cd08f7e34146277df6cffb002759 # v1.157.0 + steps: + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + - run: semgrep scan --config=auto diff --git a/biome.jsonc b/biome.jsonc index c661d7f0e49e..63a65b9fdd6d 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.4.2/schema.json", + "$schema": "https://biomejs.dev/schemas/2.4.10/schema.json", "files": { "includes": [ "**", @@ -46,7 +46,8 @@ // Enforce separate type imports for type-only imports to avoid bundling unneeded code "useImportType": "error", "useExportType": "error", - "useNumberNamespace": "warn" + "useNumberNamespace": "warn", + "noInferrableTypes": "error" }, "suspicious": { // This one is specific to catch `console.log`. The rest of logs are permitted @@ -187,6 +188,26 @@ } } } + }, + { + "includes": ["**/astro/test/**"], + "linter": { + "rules": { + "style": { + "noRestrictedImports": { + "level": "error", + "options": { + "patterns": [ + { + "group": ["**/src/**"], + "message": "The test should not import the source code. Import the code from the dist/ folder instead." + } + ] + } + } + } + } + } } ] } diff --git a/eslint.config.js b/eslint.config.js index 55aa79531465..8686de7d256f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -65,6 +65,7 @@ export default [ '@typescript-eslint/consistent-indexed-object-style': 'off', '@typescript-eslint/consistent-type-definitions': 'off', '@typescript-eslint/dot-notation': 'off', + '@typescript-eslint/no-inferrable-types': 'off', '@typescript-eslint/no-base-to-string': 'off', '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-floating-promises': 'off', diff --git a/examples/basics/package.json b/examples/basics/package.json index 998a586ddf46..907071afa56f 100644 --- a/examples/basics/package.json +++ b/examples/basics/package.json @@ -13,6 +13,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^6.1.2" + "astro": "^6.1.8" } } diff --git a/examples/blog/README.md b/examples/blog/README.md index 4307d60ba3c9..c5b756145819 100644 --- a/examples/blog/README.md +++ b/examples/blog/README.md @@ -36,6 +36,7 @@ Inside of your Astro project, you'll see the following folders and files: ```text ├── public/ ├── src/ +│   ├── assets/ │   ├── components/ │   ├── content/ │   ├── layouts/ diff --git a/examples/blog/astro.config.mjs b/examples/blog/astro.config.mjs index 0dbd924c3929..ea43603de26f 100644 --- a/examples/blog/astro.config.mjs +++ b/examples/blog/astro.config.mjs @@ -2,10 +2,34 @@ import mdx from '@astrojs/mdx'; import sitemap from '@astrojs/sitemap'; -import { defineConfig } from 'astro/config'; +import { defineConfig, fontProviders } from 'astro/config'; // https://astro.build/config export default defineConfig({ site: 'https://example.com', integrations: [mdx(), sitemap()], + fonts: [ + { + provider: fontProviders.local(), + name: 'Atkinson', + cssVariable: '--font-atkinson', + fallbacks: ['sans-serif'], + options: { + variants: [ + { + src: ['./src/assets/fonts/atkinson-regular.woff'], + weight: 400, + style: 'normal', + display: 'swap', + }, + { + src: ['./src/assets/fonts/atkinson-bold.woff'], + weight: 700, + style: 'normal', + display: 'swap', + }, + ], + }, + }, + ], }); diff --git a/examples/blog/package.json b/examples/blog/package.json index d777f6265bc6..a8fea9d8fb85 100644 --- a/examples/blog/package.json +++ b/examples/blog/package.json @@ -16,7 +16,7 @@ "@astrojs/mdx": "^5.0.3", "@astrojs/rss": "^4.0.18", "@astrojs/sitemap": "^3.7.2", - "astro": "^6.1.2", + "astro": "^6.1.8", "sharp": "^0.34.3" } } diff --git a/examples/blog/public/fonts/atkinson-bold.woff b/examples/blog/src/assets/fonts/atkinson-bold.woff similarity index 100% rename from examples/blog/public/fonts/atkinson-bold.woff rename to examples/blog/src/assets/fonts/atkinson-bold.woff diff --git a/examples/blog/public/fonts/atkinson-regular.woff b/examples/blog/src/assets/fonts/atkinson-regular.woff similarity index 100% rename from examples/blog/public/fonts/atkinson-regular.woff rename to examples/blog/src/assets/fonts/atkinson-regular.woff diff --git a/examples/blog/src/components/BaseHead.astro b/examples/blog/src/components/BaseHead.astro index 4a4384c4fd22..12fe4fa15712 100644 --- a/examples/blog/src/components/BaseHead.astro +++ b/examples/blog/src/components/BaseHead.astro @@ -5,6 +5,7 @@ import '../styles/global.css'; import type { ImageMetadata } from 'astro'; import FallbackImage from '../assets/blog-placeholder-1.jpg'; import { SITE_TITLE } from '../consts'; +import { Font } from 'astro:assets'; interface Props { title: string; @@ -31,9 +32,7 @@ const { title, description, image = FallbackImage } = Astro.props; /> - - - + diff --git a/examples/blog/src/styles/global.css b/examples/blog/src/styles/global.css index bd6f8ced4fd9..8d0e05ff446e 100644 --- a/examples/blog/src/styles/global.css +++ b/examples/blog/src/styles/global.css @@ -16,22 +16,8 @@ 0 2px 6px rgba(var(--gray), 25%), 0 8px 24px rgba(var(--gray), 33%), 0 16px 32px rgba(var(--gray), 33%); } -@font-face { - font-family: "Atkinson"; - src: url("/fonts/atkinson-regular.woff") format("woff"); - font-weight: 400; - font-style: normal; - font-display: swap; -} -@font-face { - font-family: "Atkinson"; - src: url("/fonts/atkinson-bold.woff") format("woff"); - font-weight: 700; - font-style: normal; - font-display: swap; -} body { - font-family: "Atkinson", sans-serif; + font-family: var(--font-atkinson); margin: 0; padding: 0; text-align: left; diff --git a/examples/component/package.json b/examples/component/package.json index ab320513f56d..e1c48e6524b9 100644 --- a/examples/component/package.json +++ b/examples/component/package.json @@ -18,7 +18,7 @@ ], "scripts": {}, "devDependencies": { - "astro": "^6.1.2" + "astro": "^6.1.8" }, "peerDependencies": { "astro": "^5.0.0 || ^6.0.0" diff --git a/examples/container-with-vitest/package.json b/examples/container-with-vitest/package.json index 1b2688d88ffd..753d919f01f5 100644 --- a/examples/container-with-vitest/package.json +++ b/examples/container-with-vitest/package.json @@ -14,8 +14,8 @@ "test": "vitest run" }, "dependencies": { - "@astrojs/react": "^5.0.2", - "astro": "^6.1.2", + "@astrojs/react": "^5.0.3", + "astro": "^6.1.8", "react": "^18.3.1", "react-dom": "^18.3.1", "vitest": "^4.1.0" diff --git a/examples/framework-alpine/package.json b/examples/framework-alpine/package.json index 930666f3c7e6..eb9e8ea3a349 100644 --- a/examples/framework-alpine/package.json +++ b/examples/framework-alpine/package.json @@ -16,6 +16,6 @@ "@astrojs/alpinejs": "^0.5.0", "@types/alpinejs": "^3.13.11", "alpinejs": "^3.15.8", - "astro": "^6.1.2" + "astro": "^6.1.8" } } diff --git a/examples/framework-multiple/package.json b/examples/framework-multiple/package.json index cd06f32217ff..bb6603311063 100644 --- a/examples/framework-multiple/package.json +++ b/examples/framework-multiple/package.json @@ -13,14 +13,14 @@ "astro": "astro" }, "dependencies": { - "@astrojs/preact": "^5.1.0", - "@astrojs/react": "^5.0.2", + "@astrojs/preact": "^5.1.1", + "@astrojs/react": "^5.0.3", "@astrojs/solid-js": "^6.0.1", - "@astrojs/svelte": "^8.0.4", + "@astrojs/svelte": "^8.0.5", "@astrojs/vue": "^6.0.1", "@types/react": "^18.3.28", "@types/react-dom": "^18.3.7", - "astro": "^6.1.2", + "astro": "^6.1.8", "preact": "^10.28.4", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/examples/framework-preact/package.json b/examples/framework-preact/package.json index ce4f08ecc17c..2a4301079abd 100644 --- a/examples/framework-preact/package.json +++ b/examples/framework-preact/package.json @@ -13,9 +13,9 @@ "astro": "astro" }, "dependencies": { - "@astrojs/preact": "^5.1.0", + "@astrojs/preact": "^5.1.1", "@preact/signals": "^2.8.1", - "astro": "^6.1.2", + "astro": "^6.1.8", "preact": "^10.28.4" } } diff --git a/examples/framework-react/package.json b/examples/framework-react/package.json index ac51b152c884..6f5b9b86a35d 100644 --- a/examples/framework-react/package.json +++ b/examples/framework-react/package.json @@ -13,10 +13,10 @@ "astro": "astro" }, "dependencies": { - "@astrojs/react": "^5.0.2", + "@astrojs/react": "^5.0.3", "@types/react": "^18.3.28", "@types/react-dom": "^18.3.7", - "astro": "^6.1.2", + "astro": "^6.1.8", "react": "^18.3.1", "react-dom": "^18.3.1" } diff --git a/examples/framework-solid/package.json b/examples/framework-solid/package.json index 645463f9afa5..5a9546bc706c 100644 --- a/examples/framework-solid/package.json +++ b/examples/framework-solid/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@astrojs/solid-js": "^6.0.1", - "astro": "^6.1.2", + "astro": "^6.1.8", "solid-js": "^1.9.11" } } diff --git a/examples/framework-svelte/package.json b/examples/framework-svelte/package.json index 33e4d0617014..c39df182b1f8 100644 --- a/examples/framework-svelte/package.json +++ b/examples/framework-svelte/package.json @@ -13,8 +13,8 @@ "astro": "astro" }, "dependencies": { - "@astrojs/svelte": "^8.0.4", - "astro": "^6.1.2", + "@astrojs/svelte": "^8.0.5", + "astro": "^6.1.8", "svelte": "^5.53.5" } } diff --git a/examples/framework-vue/package.json b/examples/framework-vue/package.json index 048e90cacb16..8f191e8e6bf2 100644 --- a/examples/framework-vue/package.json +++ b/examples/framework-vue/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@astrojs/vue": "^6.0.1", - "astro": "^6.1.2", + "astro": "^6.1.8", "vue": "^3.5.29" } } diff --git a/examples/hackernews/package.json b/examples/hackernews/package.json index 607ab44ef833..ff006a06999b 100644 --- a/examples/hackernews/package.json +++ b/examples/hackernews/package.json @@ -13,7 +13,7 @@ "astro": "astro" }, "dependencies": { - "@astrojs/node": "^10.0.4", - "astro": "^6.1.2" + "@astrojs/node": "^10.0.5", + "astro": "^6.1.8" } } diff --git a/examples/integration/package.json b/examples/integration/package.json index cb25deee7185..4213dbd61fd7 100644 --- a/examples/integration/package.json +++ b/examples/integration/package.json @@ -18,7 +18,7 @@ ], "scripts": {}, "devDependencies": { - "astro": "^6.1.2" + "astro": "^6.1.8" }, "peerDependencies": { "astro": "^4.0.0" diff --git a/examples/minimal/package.json b/examples/minimal/package.json index d0be1e2325f8..e2f04d513dac 100644 --- a/examples/minimal/package.json +++ b/examples/minimal/package.json @@ -13,6 +13,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^6.1.2" + "astro": "^6.1.8" } } diff --git a/examples/portfolio/package.json b/examples/portfolio/package.json index 0c2e3e808b32..2c002652b07e 100644 --- a/examples/portfolio/package.json +++ b/examples/portfolio/package.json @@ -13,6 +13,6 @@ "astro": "astro" }, "dependencies": { - "astro": "^6.1.2" + "astro": "^6.1.8" } } diff --git a/examples/ssr/package.json b/examples/ssr/package.json index 2bc758bda901..64468234388e 100644 --- a/examples/ssr/package.json +++ b/examples/ssr/package.json @@ -14,9 +14,9 @@ "server": "node dist/server/entry.mjs" }, "dependencies": { - "@astrojs/node": "^10.0.4", - "@astrojs/svelte": "^8.0.4", - "astro": "^6.1.2", + "@astrojs/node": "^10.0.5", + "@astrojs/svelte": "^8.0.5", + "astro": "^6.1.8", "svelte": "^5.53.5" } } diff --git a/examples/starlog/package.json b/examples/starlog/package.json index 2dc25c568649..0741f3fd322b 100644 --- a/examples/starlog/package.json +++ b/examples/starlog/package.json @@ -9,7 +9,7 @@ "astro": "astro" }, "dependencies": { - "astro": "^6.1.2", + "astro": "^6.1.8", "sass": "^1.97.3", "sharp": "^0.34.3" }, diff --git a/examples/toolbar-app/package.json b/examples/toolbar-app/package.json index e2198b271c7e..91e31d37d94d 100644 --- a/examples/toolbar-app/package.json +++ b/examples/toolbar-app/package.json @@ -15,8 +15,8 @@ "./app": "./dist/app.js" }, "devDependencies": { - "@types/node": "^18.17.8", - "astro": "^6.1.2" + "@types/node": "^22.10.6", + "astro": "^6.1.8" }, "engines": { "node": ">=22.12.0" diff --git a/examples/with-markdoc/package.json b/examples/with-markdoc/package.json index de5c74aecbf2..21ab77221ffe 100644 --- a/examples/with-markdoc/package.json +++ b/examples/with-markdoc/package.json @@ -14,6 +14,6 @@ }, "dependencies": { "@astrojs/markdoc": "^1.0.3", - "astro": "^6.1.2" + "astro": "^6.1.8" } } diff --git a/examples/with-mdx/package.json b/examples/with-mdx/package.json index d8846698635c..55473c164b95 100644 --- a/examples/with-mdx/package.json +++ b/examples/with-mdx/package.json @@ -14,8 +14,8 @@ }, "dependencies": { "@astrojs/mdx": "^5.0.3", - "@astrojs/preact": "^5.1.0", - "astro": "^6.1.2", + "@astrojs/preact": "^5.1.1", + "astro": "^6.1.8", "preact": "^10.28.4" } } diff --git a/examples/with-nanostores/package.json b/examples/with-nanostores/package.json index 9e64c71a4e0f..51d4d470daef 100644 --- a/examples/with-nanostores/package.json +++ b/examples/with-nanostores/package.json @@ -13,9 +13,9 @@ "astro": "astro" }, "dependencies": { - "@astrojs/preact": "^5.1.0", + "@astrojs/preact": "^5.1.1", "@nanostores/preact": "^1.0.0", - "astro": "^6.1.2", + "astro": "^6.1.8", "nanostores": "^1.1.1", "preact": "^10.28.4" } diff --git a/examples/with-tailwindcss/package.json b/examples/with-tailwindcss/package.json index aa5b0e9a6fe5..74c775b2f087 100644 --- a/examples/with-tailwindcss/package.json +++ b/examples/with-tailwindcss/package.json @@ -16,7 +16,7 @@ "@astrojs/mdx": "^5.0.3", "@tailwindcss/vite": "^4.2.1", "@types/canvas-confetti": "^1.9.0", - "astro": "^6.1.2", + "astro": "^6.1.8", "canvas-confetti": "^1.9.4", "tailwindcss": "^4.2.1" } diff --git a/examples/with-vitest/package.json b/examples/with-vitest/package.json index 2ac5b27629d7..e44ed3480a7c 100644 --- a/examples/with-vitest/package.json +++ b/examples/with-vitest/package.json @@ -14,7 +14,7 @@ "test": "vitest" }, "dependencies": { - "astro": "^6.1.2", + "astro": "^6.1.8", "vitest": "^4.1.0" } } diff --git a/knip.js b/knip.js index 70702de01bc7..5e28a0378e0a 100644 --- a/knip.js +++ b/knip.js @@ -1,5 +1,5 @@ // @ts-check -const testEntry = 'test/**/*.test.js'; +const testEntry = 'test/**/*.test.{js,ts}'; /** @type {import('knip').KnipConfig} */ export default { @@ -33,7 +33,7 @@ export default { testEntry, 'test/types/**/*', 'e2e/**/*.test.js', - 'test/units/teardown.js', + 'test/units/teardown.ts', // Can't detect this file when using inside a vite plugin 'src/vite-plugin-app/createAstroServerApp.ts', ], diff --git a/package.json b/package.json index 8943e7dee106..c2ae5c16c30b 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "test:e2e": "cd packages/astro && pnpm playwright install firefox && pnpm run test:e2e", "test:e2e:match": "cd packages/astro && pnpm playwright install firefox && pnpm run test:e2e:match", "test:e2e:hosts": "turbo run test:hosted", + "typecheck:tests": "pnpm -r typecheck:tests", "benchmark": "astro-benchmark", "lint": "biome lint && knip && eslint . --report-unused-disable-directives-severity=warn --concurrency=auto", "lint:ci": "knip && pnpm run eslint:ci", @@ -62,12 +63,12 @@ }, "devDependencies": { "@astrojs/check": "^0.9.5", - "@biomejs/biome": "2.4.2", + "@biomejs/biome": "2.4.10", "@changesets/changelog-github": "^0.5.2", "@changesets/cli": "^2.29.8", "@flue/cli": "^0.0.47", "@flue/client": "^0.0.29", - "@types/node": "^18.19.115", + "@types/node": "^22.10.6", "bgproc": "^0.2.0", "esbuild": "0.25.5", "eslint": "^9.39.3", diff --git a/packages/astro-rss/package.json b/packages/astro-rss/package.json index 715b8b4e8c67..ef76cd050187 100644 --- a/packages/astro-rss/package.json +++ b/packages/astro-rss/package.json @@ -24,7 +24,8 @@ "build": "astro-scripts build \"src/**/*.ts\" && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"", - "test": "astro-scripts test \"test/**/*.test.js\"" + "test": "astro-scripts test \"test/**/*.test.ts\"", + "typecheck:tests": "tsc --build tsconfig.test.json" }, "devDependencies": { "@types/xml2js": "^0.4.14", diff --git a/packages/astro-rss/test/pagesGlobToRssItems.test.js b/packages/astro-rss/test/pagesGlobToRssItems.test.ts similarity index 95% rename from packages/astro-rss/test/pagesGlobToRssItems.test.js rename to packages/astro-rss/test/pagesGlobToRssItems.test.ts index 36613c96c89e..19e897e8648b 100644 --- a/packages/astro-rss/test/pagesGlobToRssItems.test.js +++ b/packages/astro-rss/test/pagesGlobToRssItems.test.ts @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { pagesGlobToRssItems } from '../dist/index.js'; -import { phpFeedItem, web1FeedItem } from './test-utils.js'; +import { phpFeedItem, web1FeedItem } from './test-utils.ts'; describe('pagesGlobToRssItems', () => { it('should generate on valid result', async () => { @@ -48,7 +48,7 @@ describe('pagesGlobToRssItems', () => { ]; assert.deepEqual( - items.sort((a, b) => a.pubDate - b.pubDate), + items.sort((a, b) => a.pubDate!.getTime() - b.pubDate!.getTime()), expected, ); }); diff --git a/packages/astro-rss/test/rss.test.js b/packages/astro-rss/test/rss.test.ts similarity index 98% rename from packages/astro-rss/test/rss.test.js rename to packages/astro-rss/test/rss.test.ts index a52caf45c361..402548b78eec 100644 --- a/packages/astro-rss/test/rss.test.js +++ b/packages/astro-rss/test/rss.test.ts @@ -15,7 +15,7 @@ import { web1FeedItem, web1FeedItemWithAllData, web1FeedItemWithContent, -} from './test-utils.js'; +} from './test-utils.ts'; // note: I spent 30 minutes looking for a nice node-based snapshot tool // ...and I gave up. Enjoy big strings! @@ -37,7 +37,7 @@ const validXmlWithXSLStylesheet = `<![CDATA[${title}]]>${site}/`; -function assertXmlDeepEqual(a, b) { +function assertXmlDeepEqual(a: string, b: string) { const parsedA = parseXmlString(a); const parsedB = parseXmlString(b); @@ -266,7 +266,7 @@ describe('getRssString', () => { category: z.string().optional(), }); } catch (e) { - error = e.message; + error = (e as Error).message; } assert.equal(error, null); }); @@ -280,7 +280,7 @@ describe('getRssString', () => { items: [ { title: 'Title', - pubDate: new Date().toISOString(), + pubDate: new Date(), description: 'Description', link: '/link', enclosure: { @@ -293,7 +293,7 @@ describe('getRssString', () => { site, }); } catch (e) { - error = e.message; + error = (e as Error).message; } assert.equal(error, null); diff --git a/packages/astro-rss/test/test-utils.js b/packages/astro-rss/test/test-utils.ts similarity index 81% rename from packages/astro-rss/test/test-utils.js rename to packages/astro-rss/test/test-utils.ts index d3ee8ca336c7..4cb276ed1bdf 100644 --- a/packages/astro-rss/test/test-utils.js +++ b/packages/astro-rss/test/test-utils.ts @@ -12,7 +12,7 @@ export const phpFeedItemWithoutDate = { }; export const phpFeedItem = { ...phpFeedItemWithoutDate, - pubDate: '1994-05-03', + pubDate: new Date('1994-05-03'), }; export const phpFeedItemWithContent = { ...phpFeedItem, @@ -27,7 +27,7 @@ export const web1FeedItem = { // Should support empty string as a URL (possible for homepage route) link: '', title: 'Web 1.0', - pubDate: '1997-05-03', + pubDate: new Date('1997-05-03'), description: 'Web 1.0 is the term used for the earliest version of the Internet as it emerged from its origins with Defense Advanced Research Projects Agency (DARPA) and became, for the first time, a global network representing the future of digital communications.', }; @@ -57,12 +57,12 @@ const parser = new xml2js.Parser({ trim: true }); * * Utility function to parse an XML string into an object using `xml2js`. * - * @param {string} xmlString - Stringified XML to parse. - * @return {{ err: Error, result: any }} Represents an option containing the parsed XML string or an Error. + * @param xmlString - Stringified XML to parse. + * @return Represents an option containing the parsed XML string or an Error. */ -export function parseXmlString(xmlString) { - let res; - parser.parseString(xmlString, (err, result) => { +export function parseXmlString(xmlString: string): { err: Error | null; result: unknown } { + let res!: { err: Error | null; result: unknown }; + parser.parseString(xmlString, (err: Error | null, result: unknown) => { res = { err, result }; }); return res; diff --git a/packages/astro-rss/tsconfig.test.json b/packages/astro-rss/tsconfig.test.json new file mode 100644 index 000000000000..7d6bc4428b35 --- /dev/null +++ b/packages/astro-rss/tsconfig.test.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["test/**/*.ts"], + "exclude": ["test/fixtures/**"], + "compilerOptions": { + "allowJs": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "rewriteRelativeImportExtensions": true + }, + "references": [{ "path": "../astro/tsconfig.test.json" }] +} diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md index 9106cfd12e50..3dff13a8e42c 100644 --- a/packages/astro/CHANGELOG.md +++ b/packages/astro/CHANGELOG.md @@ -1,5 +1,92 @@ # astro +## 6.1.8 + +### Patch Changes + +- [#16367](https://github.com/withastro/astro/pull/16367) [`a6866a7`](https://github.com/withastro/astro/commit/a6866a7ef086627f8f8237274361d8acc2f85121) Thanks [@ematipico](https://github.com/ematipico)! - Fixes an issue where build output files could contain special characters (`!`, `~`, `{`, `}`) in their names, causing deploy failures on platforms like Netlify. + +- [#16381](https://github.com/withastro/astro/pull/16381) [`217c5b3`](https://github.com/withastro/astro/commit/217c5b3b937f0aee7e59280e8a10cf2bd4237605) Thanks [@ematipico](https://github.com/ematipico)! - Slightly improved the performance of the dev server by caching the internal crawling of the dependencies of a project. + +- [#16348](https://github.com/withastro/astro/pull/16348) [`7d26cd7`](https://github.com/withastro/astro/commit/7d26cd77bc1b33cee81f0e7b408dc2d170be1bdd) Thanks [@ocavue](https://github.com/ocavue)! - Fixes a bug where emitted assets during a client build would contain always fresh, new hashes in their name. Now the build should be more stable. + +- [#16317](https://github.com/withastro/astro/pull/16317) [`d012bfe`](https://github.com/withastro/astro/commit/d012bfeadb5b33f9ab1175191d59357d629c327e) Thanks [@das-peter](https://github.com/das-peter)! - Fixes a bug where `allowedDomains` weren't correctly propagated when using the development server. + +- [#16379](https://github.com/withastro/astro/pull/16379) [`5a84551`](https://github.com/withastro/astro/commit/5a845514114ae21ca9820e98b56cce33c0cf579b) Thanks [@martrapp](https://github.com/martrapp)! - Improves Vue scoped style handling in DEV mode during client router navigation. + +- [#16317](https://github.com/withastro/astro/pull/16317) [`d012bfe`](https://github.com/withastro/astro/commit/d012bfeadb5b33f9ab1175191d59357d629c327e) Thanks [@das-peter](https://github.com/das-peter)! - Adds tests to verify settings are properly propagated when using the development server. + +- [#16282](https://github.com/withastro/astro/pull/16282) [`5b0fdaa`](https://github.com/withastro/astro/commit/5b0fdaa8ba3dc17f4b93d9847c3255150b0aeab2) Thanks [@jmurty](https://github.com/jmurty)! - Fixes build errors on platforms with skew protection enabled (e.g. Vercel, Netlify) for inter-chunk Javascript using dynamic imports + +- Updated dependencies [[`e0b240e`](https://github.com/withastro/astro/commit/e0b240edea4db632138def3a9003b4b12e12f765)]: + - @astrojs/telemetry@3.3.1 + +## 6.1.7 + +### Patch Changes + +- [#16027](https://github.com/withastro/astro/pull/16027) [`c62516b`](https://github.com/withastro/astro/commit/c62516bbbf8fdf95d38293440d28221c048c41f0) Thanks [@fkatsuhiro](https://github.com/fkatsuhiro)! - Fixes a bug where remote image dimensions were not validated during static builds on Netlify. + +- [#16311](https://github.com/withastro/astro/pull/16311) [`94048f2`](https://github.com/withastro/astro/commit/94048f27c30f47ae0e01f90231e0496ed80595f7) Thanks [@Arecsu](https://github.com/Arecsu)! - Fixes `--port` flag being ignored after a Vite-triggered server restart (e.g. when a `.env` file changes) + +- [#16316](https://github.com/withastro/astro/pull/16316) [`0fcd04c`](https://github.com/withastro/astro/commit/0fcd04cc985002b56c9e2d36bcb68da0d3f08d5f) Thanks [@ematipico](https://github.com/ematipico)! - Fixes the `/_image` endpoint accepting an arbitrary `f=svg` query parameter and serving non-SVG content as `image/svg+xml`. The endpoint now validates that the source is actually SVG before honoring `f=svg`, matching the same guard already enforced on the `` component path. + +## 6.1.6 + +### Patch Changes + +- [#16202](https://github.com/withastro/astro/pull/16202) [`b5c2fba`](https://github.com/withastro/astro/commit/b5c2fba8bf2bc315db94e525f12f7661dd357822) Thanks [@matthewp](https://github.com/matthewp)! - Fixes Actions failing with `ActionsWithoutServerOutputError` when using `output: 'static'` with an adapter + +- [#16303](https://github.com/withastro/astro/pull/16303) [`b06eabf`](https://github.com/withastro/astro/commit/b06eabf01afda713066feb803bbc4c89af634aaf) Thanks [@matthewp](https://github.com/matthewp)! - Improves handling of special characters in inline `` variants (case-insensitive, + * whitespace, or self-closing forms) or ` + +

I should appear before the promise resolves

+ {promise.then(() =>

I appear after the promise resolves

)} +
+ + +

Bare sync sibling (always worked)

+ {promise.then(() =>

Bare async sibling

)} + + diff --git a/packages/astro/test/fixtures/unused-slot/package.json b/packages/astro/test/fixtures/unused-slot/package.json deleted file mode 100644 index b30e1f40e39d..000000000000 --- a/packages/astro/test/fixtures/unused-slot/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@test/unused-slot", - "version": "0.0.0", - "private": true, - "dependencies": { - "astro": "workspace:*" - } -} diff --git a/packages/astro/test/fixtures/with-endpoint-routes/package.json b/packages/astro/test/fixtures/with-endpoint-routes/package.json deleted file mode 100644 index fe4f12c99376..000000000000 --- a/packages/astro/test/fixtures/with-endpoint-routes/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@test/with-endpoint-routes", - "version": "0.0.0", - "private": true, - "dependencies": { - "astro": "workspace:*" - } -} diff --git a/packages/astro/test/fixtures/with-endpoint-routes/src/astro.png b/packages/astro/test/fixtures/with-endpoint-routes/src/astro.png deleted file mode 100644 index 36889e8f77b092d4362b11fdf308f385be086619..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2573 zcmV+o3i9=dP)1X7zP0tLqJjn0vUaMiM+hS2Lc&>eTfDE83zIy z2LTyGKvM_;8bUu%ML|;u0vibf8wdg$e|?I6eTqatQoOvxe}0NZKvM|<90~&*2?HEP zK~xF?9Dsg{3j-Yr106;{RDgYp2LT#JK~)O^9YsJ?fqslfK~;Z!i@m(W3 z1tLsBS`q~$5(FcIe~!Mr#uEi35(FeoLt9KjTN4E(69goLe~*QKk4iyUzr4nUe~|Uk z#qz9>_0z`eWiIv7#_x(>>}4(Tt&#AWgz%Y!?~GvVXD#ezE%BU&>sK`Hc3A9aEbM12 z>}D+QiC^@|z44xh>sT}ESTyTbGwf(B@t}$Bds^&TGwfM3>t-zLWi9JuE$@h4?ucIX z(Z%)A#PrR<@uZ9Kql@l-TkBjg>svAFTQcifGV5A1>sd1ESu^y=z3gNz>|-wMV=wQ9 zUGIfm?}T0KVK3}qFzjG3>|ZeKUoq@nG3;G2>|HYKTr%uiGVEJ3^vS;KV=n7rF6?42 z>tQeJU@z-mFza40>s>MQ%D(JoF6?43?~P#WWG(AqF6>}0>tHbJT`=x_TJo`!>|!qK zVlV7uFYba|^v=TasgL#5$nJ(+_0-3Xlcov)00%uuL_t(|0qnsA0l**t072ZLf7`hv znDTc5-v$5x0000W)k!P8FmoxzO_X&LKVJkNHgT%O4U$`Bm;Jo>66Uc?C$){Vlm(Il zK@0G(tYIr52mP#z8xDj`vC8&F}9a3)~aAHZ}#EkeK($m%$5E(>n+5k2Pt;E>v<2Epm z;x;U_H6~-|h8jj{Hv-O#d~iz4%d(8iICkLFNwr}q%?W9bahXQ2w=oT!4B*6u`&c<^ zEQgHX*hdY_hTR9Ct>Fb=q_g_~wcbc28Z+`wTX`j!bECN!$g7&saMfP@N>M6hS`rikFTW* zsiKc2vm{zkf_Z!bfEM0IkVXYjv;zZ}3C{WXhE!mp!x?9HEI!E^`T>UAj#0QEm9&9{ zL?#c{hM{I}#81O)$Oj;%n{uO$NyFxm136jb2sgC_#H0q6vJUe|+F*$;ZwPf8g5UFi z{6aFYWGp^ASuLRDA$5dlG{N#1Ay37Ie69g0&q9cMlSZCLYCbInCs^!l2-2W!qbE|4 z-kTqLiKKlv5pD?H2T(E)0o+|k6Ha8D#f8vN280ZI!YDQJIG>Tyd5(aNv6NC6g=suP z0q=wx;tY|RC=6gGJZ(Zj#yVOBZdD(Fj2Gz$6B+wg!;!lpkvIb~q^2i@>Zp>MV_y^H&ZxWaGav10Y~K&sl4&wA9ijF+MjT zHRowim{_d!?=>qewbF9HIo^ht&t-B?VPd!2?X~9nxyN#Qd(`|y#_(xSeZtoXq3U&H~UbgSP{W=g?ri9fboKN|UbsX%hBxHkp zOis0Sh7e=~Y;ip-z+3q%XN~!Tapc*+6nv_(mVhlm7=_U)#H_fJzc9s?rpWhSte`De z5_&5YwBghJ&=fJXV?N`~x5JGjBO5!I=ASYlVFiI5`zVxQ1+-2MAFmny5VHI~j;H91 zQBXlHOmo2rg8B9j!#3}Bfo|~6cXtGIjJC`-`+i)-=`1h^`XKh8L+u&4WBxE3Rw|BE zK(8e(=!n&x`R4SDSQU{ZiAcn$%lULpd(U&Mt^)t3MA9rDZON(h0?7}*Fwk0Tk| zuvlVQp5#SRkPR&9g!LRjPsw%+dxu!`qdVIE!@Tt<~Vd)X65+&9rE9}A9ppPY_vX#T-vz89G8#}d3uLjOea;{ z$mCrLWknS8MYyU250B5@Az;S+L;di82g8kIFi#{IZ(c(cS=~IMi3X3N$ey0BA5Rze zWv_M&yB}snyrKb~`?u*$#zLrBbQYYOEs=$cBccrRekqqtOH>%oeN7 zZg)7HE|=Tm^2xNW&E?&y0s$j-ZLla5ir-%h zl**N8C0>o!67^IgZMWNv$k<`k&Q`t8m&ZkZI}V@NAaKy zq+_;kBNm?n5EI3v4UIs+(o%M9Oi&xq1puMYv0W-WE;}rm`F11{=u?7Tw z6=S$O&ZP|&io*!lx!3^X+c>sukN`g`UI$Lxx53x|5sW)8p5Text from pages/404.astro diff --git a/packages/astro/test/fixtures/with-endpoint-routes/src/pages/[slug].json.ts b/packages/astro/test/fixtures/with-endpoint-routes/src/pages/[slug].json.ts deleted file mode 100644 index 3c1408300f25..000000000000 --- a/packages/astro/test/fixtures/with-endpoint-routes/src/pages/[slug].json.ts +++ /dev/null @@ -1,13 +0,0 @@ -export async function getStaticPaths() { - return [ - { params: { slug: 'thing1' } }, - { params: { slug: 'thing2' } } - ]; -} - -export async function GET({ params }) { - return Response.json({ - slug: params.slug, - title: '[slug]' - }); -} diff --git a/packages/astro/test/fixtures/with-endpoint-routes/src/pages/data/[slug].json.ts b/packages/astro/test/fixtures/with-endpoint-routes/src/pages/data/[slug].json.ts deleted file mode 100644 index 26cd9b065c34..000000000000 --- a/packages/astro/test/fixtures/with-endpoint-routes/src/pages/data/[slug].json.ts +++ /dev/null @@ -1,13 +0,0 @@ -export async function getStaticPaths() { - return [ - { params: { slug: 'thing3' } }, - { params: { slug: 'thing4' } } - ]; -} - -export async function GET({ params }) { - return Response.json({ - slug: params.slug, - title: 'data [slug]' - }); -} diff --git a/packages/astro/test/fixtures/with-endpoint-routes/src/pages/home.json.ts b/packages/astro/test/fixtures/with-endpoint-routes/src/pages/home.json.ts deleted file mode 100644 index 2bee50a8e328..000000000000 --- a/packages/astro/test/fixtures/with-endpoint-routes/src/pages/home.json.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function GET() { - return Response.json({ title: 'home' }); -} diff --git a/packages/astro/test/fixtures/with-endpoint-routes/src/pages/images/[image].svg.ts b/packages/astro/test/fixtures/with-endpoint-routes/src/pages/images/[image].svg.ts deleted file mode 100644 index e80063105532..000000000000 --- a/packages/astro/test/fixtures/with-endpoint-routes/src/pages/images/[image].svg.ts +++ /dev/null @@ -1,16 +0,0 @@ -export async function getStaticPaths() { - return [{ params: { image: "1" } }, { params: { image: "2" } }]; -} - -export async function GET({ params }) { - return new Response( - ` - ${params.image} -`, - { - headers: { - 'content-type': 'image/svg+xml', - }, - } - ); -} diff --git a/packages/astro/test/fixtures/with-endpoint-routes/src/pages/images/hex.ts b/packages/astro/test/fixtures/with-endpoint-routes/src/pages/images/hex.ts deleted file mode 100644 index c258c3091c2d..000000000000 --- a/packages/astro/test/fixtures/with-endpoint-routes/src/pages/images/hex.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { readFile } from 'node:fs/promises'; - -export async function GET() { - const buffer = await readFile(new URL('../../astro.png', import.meta.url)); - return new Response(buffer.buffer); -} diff --git a/packages/astro/test/fixtures/with-endpoint-routes/src/pages/images/static.svg.ts b/packages/astro/test/fixtures/with-endpoint-routes/src/pages/images/static.svg.ts deleted file mode 100644 index 423f258f8c17..000000000000 --- a/packages/astro/test/fixtures/with-endpoint-routes/src/pages/images/static.svg.ts +++ /dev/null @@ -1,12 +0,0 @@ -export function GET() { - return new Response( - ` - Static SVG -`, - { - headers: { - 'content-type': 'image/svg+xml', - }, - } - ); -} diff --git a/packages/astro/test/fixtures/with-endpoint-routes/src/pages/invalid-redirect.json.ts b/packages/astro/test/fixtures/with-endpoint-routes/src/pages/invalid-redirect.json.ts deleted file mode 100644 index 9e8e40580ea8..000000000000 --- a/packages/astro/test/fixtures/with-endpoint-routes/src/pages/invalid-redirect.json.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const GET = () => { - return new Response( - undefined, - { - status: 301, - headers: { - Location: 'https://example.com', - } - } - ); -}; diff --git a/packages/astro/test/fixtures/with-endpoint-routes/src/pages/not-ok.ts b/packages/astro/test/fixtures/with-endpoint-routes/src/pages/not-ok.ts deleted file mode 100644 index c1a8aff913cc..000000000000 --- a/packages/astro/test/fixtures/with-endpoint-routes/src/pages/not-ok.ts +++ /dev/null @@ -1,5 +0,0 @@ -export async function GET() { - return new Response("Text from pages/not-ok.ts", { - status: 404, - }); -} diff --git a/packages/astro/test/fixtures/with-subpath-no-trailing-slash/.gitignore b/packages/astro/test/fixtures/with-subpath-no-trailing-slash/.gitignore deleted file mode 100644 index 8a085be49804..000000000000 --- a/packages/astro/test/fixtures/with-subpath-no-trailing-slash/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/dist-* diff --git a/packages/astro/test/fixtures/with-subpath-no-trailing-slash/astro.config.mjs b/packages/astro/test/fixtures/with-subpath-no-trailing-slash/astro.config.mjs deleted file mode 100644 index 227d24574d05..000000000000 --- a/packages/astro/test/fixtures/with-subpath-no-trailing-slash/astro.config.mjs +++ /dev/null @@ -1,4 +0,0 @@ - -export default { - site: 'http://example.com', -} diff --git a/packages/astro/test/fixtures/with-subpath-no-trailing-slash/package.json b/packages/astro/test/fixtures/with-subpath-no-trailing-slash/package.json deleted file mode 100644 index 199b3c1528d4..000000000000 --- a/packages/astro/test/fixtures/with-subpath-no-trailing-slash/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@test/with-subpath-no-trailing-slash", - "version": "0.0.0", - "private": true, - "dependencies": { - "astro": "workspace:*" - } -} diff --git a/packages/astro/test/fixtures/with-subpath-no-trailing-slash/src/pages/[id].astro b/packages/astro/test/fixtures/with-subpath-no-trailing-slash/src/pages/[id].astro deleted file mode 100644 index b5dbc43074c7..000000000000 --- a/packages/astro/test/fixtures/with-subpath-no-trailing-slash/src/pages/[id].astro +++ /dev/null @@ -1,6 +0,0 @@ ---- -export function getStaticPaths() { - return [{ params: { id: '1' } }]; -} ---- -

Post #1

diff --git a/packages/astro/test/fixtures/with-subpath-no-trailing-slash/src/pages/another.astro b/packages/astro/test/fixtures/with-subpath-no-trailing-slash/src/pages/another.astro deleted file mode 100644 index d0563f41456c..000000000000 --- a/packages/astro/test/fixtures/with-subpath-no-trailing-slash/src/pages/another.astro +++ /dev/null @@ -1 +0,0 @@ -
another page
\ No newline at end of file diff --git a/packages/astro/test/fixtures/with-subpath-no-trailing-slash/src/pages/index.astro b/packages/astro/test/fixtures/with-subpath-no-trailing-slash/src/pages/index.astro deleted file mode 100644 index 42e6a5177169..000000000000 --- a/packages/astro/test/fixtures/with-subpath-no-trailing-slash/src/pages/index.astro +++ /dev/null @@ -1 +0,0 @@ -
testing
\ No newline at end of file diff --git a/packages/astro/test/fixtures/without-site-config/package.json b/packages/astro/test/fixtures/without-site-config/package.json deleted file mode 100644 index 473b7a34bccb..000000000000 --- a/packages/astro/test/fixtures/without-site-config/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "@test/without-site-config", - "version": "0.0.0", - "private": true, - "dependencies": { - "astro": "workspace:*" - } -} diff --git a/packages/astro/test/fixtures/without-site-config/src/pages/[id].astro b/packages/astro/test/fixtures/without-site-config/src/pages/[id].astro deleted file mode 100644 index b5dbc43074c7..000000000000 --- a/packages/astro/test/fixtures/without-site-config/src/pages/[id].astro +++ /dev/null @@ -1,6 +0,0 @@ ---- -export function getStaticPaths() { - return [{ params: { id: '1' } }]; -} ---- -

Post #1

diff --git a/packages/astro/test/fixtures/without-site-config/src/pages/another.astro b/packages/astro/test/fixtures/without-site-config/src/pages/another.astro deleted file mode 100644 index d0563f41456c..000000000000 --- a/packages/astro/test/fixtures/without-site-config/src/pages/another.astro +++ /dev/null @@ -1 +0,0 @@ -
another page
\ No newline at end of file diff --git a/packages/astro/test/fixtures/without-site-config/src/pages/base-index.astro b/packages/astro/test/fixtures/without-site-config/src/pages/base-index.astro deleted file mode 100644 index 76e198f3da49..000000000000 --- a/packages/astro/test/fixtures/without-site-config/src/pages/base-index.astro +++ /dev/null @@ -1 +0,0 @@ -
testing
diff --git a/packages/astro/test/fixtures/without-site-config/src/pages/html-ext/[slug].astro b/packages/astro/test/fixtures/without-site-config/src/pages/html-ext/[slug].astro deleted file mode 100644 index 599fd0f26e9b..000000000000 --- a/packages/astro/test/fixtures/without-site-config/src/pages/html-ext/[slug].astro +++ /dev/null @@ -1,6 +0,0 @@ ---- -export function getStaticPaths() { - return [{ params: { slug: '1' } }]; -} ---- -

none: {Astro.params.slug}

diff --git a/packages/astro/test/fixtures/without-site-config/src/pages/html-ext/[slug].html.astro b/packages/astro/test/fixtures/without-site-config/src/pages/html-ext/[slug].html.astro deleted file mode 100644 index 79ab2d434b38..000000000000 --- a/packages/astro/test/fixtures/without-site-config/src/pages/html-ext/[slug].html.astro +++ /dev/null @@ -1,6 +0,0 @@ ---- -export function getStaticPaths() { - return [{ params: { slug: '1' } }]; -} ---- -

html: {Astro.params.slug}

diff --git a/packages/astro/test/fixtures/without-site-config/src/pages/index.astro b/packages/astro/test/fixtures/without-site-config/src/pages/index.astro deleted file mode 100644 index 42e6a5177169..000000000000 --- a/packages/astro/test/fixtures/without-site-config/src/pages/index.astro +++ /dev/null @@ -1 +0,0 @@ -
testing
\ No newline at end of file diff --git a/packages/astro/test/fixtures/without-site-config/src/pages/redirect.astro b/packages/astro/test/fixtures/without-site-config/src/pages/redirect.astro deleted file mode 100644 index 4b640e5b5df8..000000000000 --- a/packages/astro/test/fixtures/without-site-config/src/pages/redirect.astro +++ /dev/null @@ -1,4 +0,0 @@ ---- -const anotherURL = new URL('./another/', Astro.url); -return Response.redirect(anotherURL.toString()); ---- diff --git a/packages/astro/test/fixtures/without-site-config/src/pages/te st.astro b/packages/astro/test/fixtures/without-site-config/src/pages/te st.astro deleted file mode 100644 index 42e6a5177169..000000000000 --- a/packages/astro/test/fixtures/without-site-config/src/pages/te st.astro +++ /dev/null @@ -1 +0,0 @@ -
testing
\ No newline at end of file diff --git "a/packages/astro/test/fixtures/without-site-config/src/pages/\343\203\206\343\202\271\343\203\210.astro" "b/packages/astro/test/fixtures/without-site-config/src/pages/\343\203\206\343\202\271\343\203\210.astro" deleted file mode 100644 index 42e6a5177169..000000000000 --- "a/packages/astro/test/fixtures/without-site-config/src/pages/\343\203\206\343\202\271\343\203\210.astro" +++ /dev/null @@ -1 +0,0 @@ -
testing
\ No newline at end of file diff --git a/packages/astro/test/fonts.test.js b/packages/astro/test/fonts.test.ts similarity index 96% rename from packages/astro/test/fonts.test.js rename to packages/astro/test/fonts.test.ts index 0f1598ad0f53..0fe844ccc42f 100644 --- a/packages/astro/test/fonts.test.js +++ b/packages/astro/test/fonts.test.ts @@ -1,4 +1,3 @@ -// @ts-check import assert from 'node:assert/strict'; import { existsSync } from 'node:fs'; import { readdir } from 'node:fs/promises'; @@ -6,15 +5,13 @@ import { after, before, describe, it } from 'node:test'; import { fontProviders } from 'astro/config'; import * as cheerio from 'cheerio'; import testAdapter from './test-adapter.js'; -import { loadFixture } from './test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from './test-utils.js'; describe('astro fonts', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; describe('dev', () => { - /** @type {import('./test-utils.js').DevServer} */ - let devServer; + let devServer: DevServer; describe('shared', () => { before(async () => { @@ -159,7 +156,7 @@ describe('astro fonts', () => { }, ], }); - await fixture.build({}); + await fixture.build(); }); it('Includes styles', async () => { @@ -221,7 +218,7 @@ describe('astro fonts', () => { }, ], }); - await fixture.build({}); + await fixture.build(); }); it('works', async () => { @@ -236,8 +233,7 @@ describe('astro fonts', () => { }); describe('ssr', () => { - /** @type {(url: string) => Promise} */ - let fixtureFetch; + let fixtureFetch: (url: string) => Promise; before(async () => { fixture = await loadFixture({ @@ -253,7 +249,7 @@ describe('astro fonts', () => { }, ], }); - await fixture.build({}); + await fixture.build(); const app = await fixture.loadTestAdapterApp(); fixtureFetch = async (url) => { const request = new Request(`http://example.com${url}`); diff --git a/packages/astro/test/fontsource.test.js b/packages/astro/test/fontsource.test.ts similarity index 80% rename from packages/astro/test/fontsource.test.js rename to packages/astro/test/fontsource.test.ts index 51c7a69d6ae8..b89b063fd5a9 100644 --- a/packages/astro/test/fontsource.test.js +++ b/packages/astro/test/fontsource.test.ts @@ -1,10 +1,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('@fontsource/* packages', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/fontsource-package/' }); @@ -14,7 +14,7 @@ describe('@fontsource/* packages', () => { it('can be imported in frontmatter', async () => { const html = await fixture.readFile('/index.html'); const $ = cheerio.load(html); - const assetPath = $('link').attr('href'); + const assetPath = $('link').attr('href')!; const css = await fixture.readFile(assetPath); assert.equal(css.includes('Montserrat'), true); }); diff --git a/packages/astro/test/hmr-markdown.test.js b/packages/astro/test/hmr-markdown.test.js index 62760295ac7f..417e2be7f302 100644 --- a/packages/astro/test/hmr-markdown.test.js +++ b/packages/astro/test/hmr-markdown.test.js @@ -23,32 +23,31 @@ describe('HMR: Markdown updates', () => { await devServer.stop(); }); - it( - 'should update HTML when markdown changes', - { skip: isWindows, todo: 'HMR tests hang on Windows' }, - async () => { - let response = await fixture.fetch('/'); - assert.equal(response.status, 200); - let html = await response.text(); - assert.ok(html.includes('Original content')); - - response = await fixture.fetch('/blog/post'); - assert.equal(response.status, 200); - html = await response.text(); - assert.ok(html.includes('Original content')); - - await fixture.editFile(markdownPath, UPDATED_CONTENT); - await fixture.onNextDataStoreChange(); - - response = await fixture.fetch('/'); - assert.equal(response.status, 200); - html = await response.text(); - assert.ok(html.includes('Updated content')); - - response = await fixture.fetch('/blog/post'); - assert.equal(response.status, 200); - html = await response.text(); - assert.ok(html.includes('Updated content')); - }, - ); + it('should update HTML when markdown changes', { + skip: isWindows, + todo: 'HMR tests hang on Windows', + }, async () => { + let response = await fixture.fetch('/'); + assert.equal(response.status, 200); + let html = await response.text(); + assert.ok(html.includes('Original content')); + + response = await fixture.fetch('/blog/post'); + assert.equal(response.status, 200); + html = await response.text(); + assert.ok(html.includes('Original content')); + + await fixture.editFile(markdownPath, UPDATED_CONTENT); + await fixture.onNextDataStoreChange(); + + response = await fixture.fetch('/'); + assert.equal(response.status, 200); + html = await response.text(); + assert.ok(html.includes('Updated content')); + + response = await fixture.fetch('/blog/post'); + assert.equal(response.status, 200); + html = await response.text(); + assert.ok(html.includes('Updated content')); + }); }); diff --git a/packages/astro/test/hmr-new-page.test.js b/packages/astro/test/hmr-new-page.test.js index 8f0b04a5be25..d4ad8a09f335 100644 --- a/packages/astro/test/hmr-new-page.test.js +++ b/packages/astro/test/hmr-new-page.test.js @@ -50,52 +50,50 @@ describe('HMR: New page detection', () => { assert.equal(response.status, 404); }); - it( - 'should detect a new page without server restart', - { skip: isWindows, todo: 'I hangs on windows' }, - async () => { - // 1. Verify the page doesn't exist yet - let response = await fixture.fetch('/new-page'); - assert.equal(response.status, 404, 'Page should not exist initially'); - - // 2. Create the new page file - await fs.promises.writeFile(newPagePath, NEW_PAGE_CONTENT, 'utf-8'); + it('should detect a new page without server restart', { + skip: isWindows, + todo: 'I hangs on windows', + }, async () => { + // 1. Verify the page doesn't exist yet + let response = await fixture.fetch('/new-page'); + assert.equal(response.status, 404, 'Page should not exist initially'); + + // 2. Create the new page file + await fs.promises.writeFile(newPagePath, NEW_PAGE_CONTENT, 'utf-8'); + + // 3. Wait for HMR to process the change + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // 4. Verify the new page is now accessible + response = await fixture.fetch('/new-page'); + assert.equal(response.status, 200, 'Page should be accessible after creation'); + + const html = await response.text(); + assert.ok(html.includes('New Page Created via HMR'), 'Page content should match'); + }); - // 3. Wait for HMR to process the change + it('should detect page removal without server restart', { + skip: isWindows, + todo: 'I hangs on windows', + }, async () => { + // Ensure the page exists first (from previous test or create it) + if (!fs.existsSync(newPagePath)) { + await fs.promises.writeFile(newPagePath, NEW_PAGE_CONTENT, 'utf-8'); await new Promise((resolve) => setTimeout(resolve, 2000)); + } - // 4. Verify the new page is now accessible - response = await fixture.fetch('/new-page'); - assert.equal(response.status, 200, 'Page should be accessible after creation'); - - const html = await response.text(); - assert.ok(html.includes('New Page Created via HMR'), 'Page content should match'); - }, - ); - - it( - 'should detect page removal without server restart', - { skip: isWindows, todo: 'I hangs on windows' }, - async () => { - // Ensure the page exists first (from previous test or create it) - if (!fs.existsSync(newPagePath)) { - await fs.promises.writeFile(newPagePath, NEW_PAGE_CONTENT, 'utf-8'); - await new Promise((resolve) => setTimeout(resolve, 2000)); - } - - // 1. Verify the page is accessible - let response = await fixture.fetch('/new-page'); - assert.equal(response.status, 200, 'Page should exist before deletion'); - - // 2. Delete the page file - await fs.promises.unlink(newPagePath); + // 1. Verify the page is accessible + let response = await fixture.fetch('/new-page'); + assert.equal(response.status, 200, 'Page should exist before deletion'); - // 3. Wait for HMR to process the change - await new Promise((resolve) => setTimeout(resolve, 2000)); + // 2. Delete the page file + await fs.promises.unlink(newPagePath); - // 4. Verify the page now returns 404 - response = await fixture.fetch('/new-page'); - assert.equal(response.status, 404, 'Page should return 404 after deletion'); - }, - ); + // 3. Wait for HMR to process the change + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // 4. Verify the page now returns 404 + response = await fixture.fetch('/new-page'); + assert.equal(response.status, 404, 'Page should return 404 after deletion'); + }); }); diff --git a/packages/astro/test/hmr-slots-render.test.js b/packages/astro/test/hmr-slots-render.test.js index 1fa462e28b0a..8852296554f1 100644 --- a/packages/astro/test/hmr-slots-render.test.js +++ b/packages/astro/test/hmr-slots-render.test.js @@ -37,35 +37,33 @@ describe('HMR: slots.render with callback args after style change', () => { ); } - it( - 'should render after style change in the slot-render component', - { skip: isWindows }, - async () => { - // Initial fetch - verify correct rendering - let res = await fixture.fetch('/'); - assert.equal(res.status, 200); - verifyRendering(cheerio.load(await res.text()), 'initial'); + it('should render after style change in the slot-render component', { + skip: isWindows, + }, async () => { + // Initial fetch - verify correct rendering + let res = await fixture.fetch('/'); + assert.equal(res.status, 200); + verifyRendering(cheerio.load(await res.text()), 'initial'); - // Style-only edit (triggers HMR style-only path) - await fixture.editFile('/src/components/Each.astro', (c) => - c.replace('font-size: 0.5rem;', 'font-size: 1rem;'), - ); - await new Promise((r) => setTimeout(r, 500)); + // Style-only edit (triggers HMR style-only path) + await fixture.editFile('/src/components/Each.astro', (c) => + c.replace('font-size: 0.5rem;', 'font-size: 1rem;'), + ); + await new Promise((r) => setTimeout(r, 500)); - // Page refresh after HMR - must still render correctly - res = await fixture.fetch('/'); - assert.equal(res.status, 200); - verifyRendering(cheerio.load(await res.text()), 'after style change'); + // Page refresh after HMR - must still render correctly + res = await fixture.fetch('/'); + assert.equal(res.status, 200); + verifyRendering(cheerio.load(await res.text()), 'after style change'); - // Second style edit + refresh - await fixture.editFile('/src/components/Each.astro', (c) => - c.replace('font-size: 1rem;', 'font-size: 2rem;'), - ); - await new Promise((r) => setTimeout(r, 500)); + // Second style edit + refresh + await fixture.editFile('/src/components/Each.astro', (c) => + c.replace('font-size: 1rem;', 'font-size: 2rem;'), + ); + await new Promise((r) => setTimeout(r, 500)); - res = await fixture.fetch('/'); - assert.equal(res.status, 200); - verifyRendering(cheerio.load(await res.text()), 'after 2nd style change'); - }, - ); + res = await fixture.fetch('/'); + assert.equal(res.status, 200); + verifyRendering(cheerio.load(await res.text()), 'after 2nd style change'); + }); }); diff --git a/packages/astro/test/i18n-css-leak.test.js b/packages/astro/test/i18n-css-leak.test.js new file mode 100644 index 000000000000..839d9b946140 --- /dev/null +++ b/packages/astro/test/i18n-css-leak.test.js @@ -0,0 +1,40 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import { load as cheerioLoad } from 'cheerio'; +import { loadFixture } from './test-utils.js'; + +describe('CSS graph boundaries with astro:i18n', () => { + let fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/i18n-css-leak-basic/', + build: { inlineStylesheets: 'never' }, + }); + await fixture.build(); + }); + + async function getPageCss(pathname) { + const html = await fixture.readFile(pathname); + const $ = cheerioLoad(html); + const hrefs = $('link[rel=stylesheet]') + .map((_index, el) => $(el).attr('href')) + .get(); + const stylesheets = await Promise.all(hrefs.map((href) => fixture.readFile(href))); + return stylesheets.join('\n'); + } + + it('does not attach docs-only CSS to unrelated pages', async () => { + const css = await getPageCss('/index.html'); + assert.match(css, /background:#fff/); + assert.doesNotMatch(css, /background:#000/); + assert.doesNotMatch(css, /color:red/); + }); + + it('keeps docs-only CSS on the docs page', async () => { + const css = await getPageCss('/docs/index.html'); + assert.match(css, /background:#000/); + assert.match(css, /color:red/); + assert.doesNotMatch(css, /background:#fff/); + }); +}); diff --git a/packages/astro/test/integration-route-setup-hook.test.js b/packages/astro/test/integration-route-setup-hook.test.js new file mode 100644 index 000000000000..f9a5f0d08c6c --- /dev/null +++ b/packages/astro/test/integration-route-setup-hook.test.js @@ -0,0 +1,42 @@ +import * as assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { loadFixture } from './test-utils.js'; + +describe('Routes setup hook', () => { + it('should work in dev', async () => { + let routes = []; + const fixture = await loadFixture({ + root: './fixtures/dev-render/', + integrations: [ + { + name: 'test', + hooks: { + 'astro:route:setup': (params) => { + routes.push({ + component: params.route.component, + prerender: params.route.prerender, + }); + }, + }, + }, + ], + }); + const devServer = await fixture.startDevServer(); + + try { + // The hook should have been called for each route during startup. + // Filter to just the project pages we know about. + const projectRoutes = routes + .filter((r) => r.component.startsWith('src/pages/')) + .sort((a, b) => a.component.localeCompare(b.component)); + + assert.ok(projectRoutes.length > 0, 'Should have collected routes'); + // All routes in a static project should be prerendered by default + for (const route of projectRoutes) { + assert.equal(route.prerender, true, `${route.component} should be prerendered`); + } + } finally { + await devServer.stop(); + } + }); +}); diff --git a/packages/astro/test/integration-test-helpers.js b/packages/astro/test/integration-test-helpers.js new file mode 100644 index 000000000000..0f0e881e7c61 --- /dev/null +++ b/packages/astro/test/integration-test-helpers.js @@ -0,0 +1,60 @@ +/** + * Lightweight helpers for integration tests that need mock HTTP + * request/response objects. Extracted from units/test-utils.ts so + * that JS integration tests don't cross-import from the TS unit-test + * helpers. + */ +import { EventEmitter } from 'node:events'; +import httpMocks from 'node-mocks-http'; + +export function createRequestAndResponse(reqOptions = {}) { + const req = httpMocks.createRequest(reqOptions); + req.headers.host ||= 'localhost'; + + const res = httpMocks.createResponse({ + eventEmitter: EventEmitter, + req, + }); + + const done = toPromise(res); + + const text = async () => { + let chunks = await done; + return buffersToString(chunks); + }; + + const json = async () => { + const raw = await text(); + return JSON.parse(raw); + }; + + return { req, res, done, json, text }; +} + +function toPromise(res) { + return new Promise((resolve) => { + const write = res.write; + res.write = function (data, encoding) { + if (ArrayBuffer.isView(data) && !Buffer.isBuffer(data)) { + data = Buffer.from(data.buffer); + } + if (typeof data === 'string') { + data = Buffer.from(data); + } + return write.call(this, data, encoding); + }; + res.on('end', () => { + let chunks = res._getChunks(); + resolve(chunks); + }); + }); +} + +function buffersToString(buffers) { + let decoder = new TextDecoder(); + let str = ''; + for (const buffer of buffers) { + str += decoder.decode(buffer); + } + return str; +} diff --git a/packages/astro/test/live-loaders.test.js b/packages/astro/test/live-loaders.test.js index 4e217117ba6e..0659c17e7e0e 100644 --- a/packages/astro/test/live-loaders.test.js +++ b/packages/astro/test/live-loaders.test.js @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import { Writable } from 'node:stream'; import { after, before, describe, it } from 'node:test'; -import { Logger } from '../dist/core/logger/core.js'; +import { AstroLogger } from '../dist/core/logger/core.js'; import testAdapter from './test-adapter.js'; import { loadFixture } from './test-utils.js'; @@ -20,9 +20,9 @@ describe('Live content collections', () => { const logs = []; before(async () => { devServer = await fixture.startDevServer({ - logger: new Logger({ + logger: new AstroLogger({ level: 'info', - dest: new Writable({ + destination: new Writable({ objectMode: true, write(event, _, callback) { logs.push(event); diff --git a/packages/astro/test/page-format.test.js b/packages/astro/test/page-format.test.ts similarity index 91% rename from packages/astro/test/page-format.test.js rename to packages/astro/test/page-format.test.ts index 125b90eacf74..ecaf3928c058 100644 --- a/packages/astro/test/page-format.test.js +++ b/packages/astro/test/page-format.test.ts @@ -1,12 +1,11 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('build.format', () => { describe('directory', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/page-format/', @@ -27,8 +26,7 @@ describe('build.format', () => { }); describe('file', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/page-format/', @@ -64,8 +62,7 @@ describe('build.format', () => { }); describe('preserve - i18n', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ base: '/test', @@ -91,8 +88,7 @@ describe('build.format', () => { }); describe('preserve - i18n', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ base: '/test', diff --git a/packages/astro/test/page-level-styles.test.js b/packages/astro/test/page-level-styles.test.ts similarity index 91% rename from packages/astro/test/page-level-styles.test.js rename to packages/astro/test/page-level-styles.test.ts index eb4ba032a79e..d6d139042077 100644 --- a/packages/astro/test/page-level-styles.test.js +++ b/packages/astro/test/page-level-styles.test.ts @@ -1,11 +1,11 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { load as cheerioLoad } from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; // Asset bundling describe('Page-level styles', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ diff --git a/packages/astro/test/parallel.test.js b/packages/astro/test/parallel.test.ts similarity index 88% rename from packages/astro/test/parallel.test.js rename to packages/astro/test/parallel.test.ts index fadb089b5123..30de1fd1b652 100644 --- a/packages/astro/test/parallel.test.js +++ b/packages/astro/test/parallel.test.ts @@ -1,10 +1,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('Component parallelization', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -17,10 +17,8 @@ describe('Component parallelization', () => { let html = await fixture.readFile('/index.html'); let $ = cheerio.load(html); - const startTimes = Array.from($('.start')).map((element) => Number(element.children[0].data)); - const finishTimes = Array.from($('.finished')).map((element) => - Number(element.children[0].data), - ); + const startTimes = Array.from($('.start')).map((element) => Number($(element).text())); + const finishTimes = Array.from($('.finished')).map((element) => Number($(element).text())); const renderStartWithin = Math.max(...startTimes) - Math.min(...startTimes); assert.equal( diff --git a/packages/astro/test/partials.test.js b/packages/astro/test/partials.test.ts similarity index 87% rename from packages/astro/test/partials.test.js rename to packages/astro/test/partials.test.ts index 10ce8f6c519d..8eb3104394f3 100644 --- a/packages/astro/test/partials.test.js +++ b/packages/astro/test/partials.test.ts @@ -1,11 +1,10 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from './test-utils.js'; describe('Partials', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -14,8 +13,7 @@ describe('Partials', () => { }); describe('dev', () => { - /** @type {import('./test-utils.js').DevServer} */ - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); diff --git a/packages/astro/test/passthrough-image-service.test.js b/packages/astro/test/passthrough-image-service.test.ts similarity index 87% rename from packages/astro/test/passthrough-image-service.test.js rename to packages/astro/test/passthrough-image-service.test.ts index 89258b53f59c..fa40c1c684e3 100644 --- a/packages/astro/test/passthrough-image-service.test.js +++ b/packages/astro/test/passthrough-image-service.test.ts @@ -1,11 +1,10 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from './test-utils.js'; describe('passthroughImageService', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -14,8 +13,8 @@ describe('passthroughImageService', () => { }); describe('dev', () => { - let $; - let devServer; + let $: cheerio.CheerioAPI; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); @@ -35,20 +34,20 @@ describe('passthroughImageService', () => { it('serves SVG logo with correct content type', async () => { const $img = $('#logo img'); - const src = $img.attr('src'); + const src = $img.attr('src')!; const response = await fixture.fetch(src); const contentType = response.headers.get('content-type'); assert.ok( - contentType.includes('image/svg+xml'), + contentType!.includes('image/svg+xml'), `Expected SVG content type, got: ${contentType}`, ); }); }); describe('build', () => { - let $; + let $: cheerio.CheerioAPI; before(async () => { await fixture.build(); @@ -77,7 +76,7 @@ describe('passthroughImageService', () => { it('preserves original format', () => { const $img = $('#image img'); - const src = $img.attr('src'); + const src = $img.attr('src')!; assert.ok(src.endsWith('.jpg'), `Should preserve jpg format, got: ${src}`); }); }); @@ -95,7 +94,7 @@ describe('passthroughImageService', () => { it('preserves original format', () => { const $img = $('#picture img'); - const src = $img.attr('src'); + const src = $img.attr('src')!; assert.ok(src.endsWith('.jpg'), `Should preserve jpg format, got: ${src}`); }); }); @@ -108,7 +107,7 @@ describe('passthroughImageService', () => { it('preserves SVG format', () => { const $img = $('#logo img'); - const src = $img.attr('src'); + const src = $img.attr('src')!; assert.ok(src.endsWith('.svg'), `Should preserve svg format, got: ${src}`); }); }); diff --git a/packages/astro/test/postcss.test.js b/packages/astro/test/postcss.test.ts similarity index 92% rename from packages/astro/test/postcss.test.js rename to packages/astro/test/postcss.test.ts index 145db478f4e6..eac80fb58c50 100644 --- a/packages/astro/test/postcss.test.js +++ b/packages/astro/test/postcss.test.ts @@ -2,11 +2,11 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; import eol from 'eol'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('PostCSS', () => { - let fixture; - let bundledCSS; + let fixture: Fixture; + let bundledCSS: string; before( async () => { fixture = await loadFixture({ @@ -19,7 +19,7 @@ describe('PostCSS', () => { // get bundled CSS (will be hashed, hence DOM query) const html = await fixture.readFile('/index.html'); const $ = cheerio.load(html); - const bundledCSSHREF = $('link[rel=stylesheet][href^=/_astro/]').attr('href'); + const bundledCSSHREF = $('link[rel=stylesheet][href^=/_astro/]').attr('href')!; bundledCSS = (await fixture.readFile(bundledCSSHREF.replace(/^\/?/, '/'))) .replace(/\s/g, '') .replace('/n', ''); diff --git a/packages/astro/test/preact-compat-component.test.js b/packages/astro/test/preact-compat-component.test.ts similarity index 83% rename from packages/astro/test/preact-compat-component.test.js rename to packages/astro/test/preact-compat-component.test.ts index c0a639dfbc66..a7af288c3c53 100644 --- a/packages/astro/test/preact-compat-component.test.js +++ b/packages/astro/test/preact-compat-component.test.ts @@ -1,11 +1,10 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from './test-utils.js'; describe('Preact compat component', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -14,8 +13,7 @@ describe('Preact compat component', () => { }); describe('Development', () => { - /** @type {import('./test-utils.js').DevServer} */ - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); diff --git a/packages/astro/test/preact-component.test.js b/packages/astro/test/preact-component.test.ts similarity index 93% rename from packages/astro/test/preact-component.test.js rename to packages/astro/test/preact-component.test.ts index c668e1fe60fd..2bd5b866ccec 100644 --- a/packages/astro/test/preact-component.test.js +++ b/packages/astro/test/preact-component.test.ts @@ -1,11 +1,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('Preact component', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -90,14 +89,17 @@ describe('Preact component', () => { // Grab the imports const exp = /import\("(.+?)"\)/g; - let match, componentUrl; + let match: RegExpExecArray | null; + let componentUrl: string | undefined; while ((match = exp.exec(html))) { if (match[1].includes('PragmaComment.js')) { componentUrl = match[1]; break; } } + // @ts-expect-error: this test is currently skipped and its type isn't worth fixing right now const component = await fixture.fetch(componentUrl).then((res) => res.text()); + // @ts-expect-error: this test is currently skipped and its type isn't worth fixing right now const jsxRuntime = component.imports.filter((i) => i.specifier.includes('jsx-runtime')); // test 1: preact/jsx-runtime is used for the component @@ -109,8 +111,8 @@ describe('Preact component', () => { const $ = cheerio.load(html); assert.equal($('.preact-signal').length, 2); - const sigs1Raw = $($('astro-island')[0]).attr('data-preact-signals'); - const sigs2Raw = $($('astro-island')[1]).attr('data-preact-signals'); + const sigs1Raw = $($('astro-island')[0]).attr('data-preact-signals')!; + const sigs2Raw = $($('astro-island')[1]).attr('data-preact-signals')!; assert.notEqual(sigs1Raw, undefined); assert.notEqual(sigs2Raw, undefined); @@ -128,7 +130,7 @@ describe('Preact component', () => { const element = $('.preact-signal-array'); assert.equal(element.length, 1); - const sigs1Raw = $($('astro-island')[2]).attr('data-preact-signals'); + const sigs1Raw = $($('astro-island')[2]).attr('data-preact-signals')!; const sigs1 = JSON.parse(sigs1Raw); @@ -150,7 +152,7 @@ describe('Preact component', () => { const element = $('.preact-signal-object'); assert.equal(element.length, 1); - const sigs1Raw = $($('astro-island')[3]).attr('data-preact-signals'); + const sigs1Raw = $($('astro-island')[3]).attr('data-preact-signals')!; const sigs1 = JSON.parse(sigs1Raw); diff --git a/packages/astro/test/prerender-conflict.test.js b/packages/astro/test/prerender-conflict.test.ts similarity index 76% rename from packages/astro/test/prerender-conflict.test.js rename to packages/astro/test/prerender-conflict.test.ts index 0199f0d0f821..ff738c138224 100644 --- a/packages/astro/test/prerender-conflict.test.js +++ b/packages/astro/test/prerender-conflict.test.ts @@ -1,7 +1,7 @@ import { strict as assert } from 'node:assert'; import { before, describe, it } from 'node:test'; -import { Logger } from '../dist/core/logger/core.js'; -import { loadFixture } from './test-utils.js'; +import { type AstroLogMessage, AstroLogger } from '../dist/core/logger/core.js'; +import { type Fixture, loadFixture } from './test-utils.js'; /** * Dynamic vs dynamic duplication should warn by default and succeed. @@ -11,23 +11,26 @@ import { loadFixture } from './test-utils.js'; describe('Prerender conflicts', () => { describe('dynamic vs dynamic', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/prerender-conflict-dynamic-dynamic/' }); }); it('warns by default and succeeds', async () => { - const logs = []; - await fixture.build({ - logger: new Logger({ - level: 'warn', - dest: { - write(chunk) { - logs.push(chunk); - }, + const logs: AstroLogMessage[] = []; + const logger = new AstroLogger({ + level: 'warn', + destination: { + write(chunk: AstroLogMessage) { + logs.push(chunk); + return true; }, - }), + }, + }); + await fixture.build({ + // @ts-expect-error: logger is an internal API + logger, }); const relevantLogs = logs @@ -44,7 +47,7 @@ describe('Prerender conflicts', () => { }); it('fails when prerenderConflictBehavior is set to error', async () => { - let err; + let err: unknown; try { await fixture.build({ prerenderConflictBehavior: 'error' }); } catch (e) { @@ -59,23 +62,26 @@ describe('Prerender conflicts', () => { }); describe('static vs dynamic', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/prerender-conflict-static-dynamic/' }); }); it('warns by default and succeeds', async () => { - const logs = []; - await fixture.build({ - logger: new Logger({ - level: 'warn', - dest: { - write(chunk) { - logs.push(chunk); - }, + const logs: AstroLogMessage[] = []; + const logger = new AstroLogger({ + level: 'warn', + destination: { + write(chunk: AstroLogMessage) { + logs.push(chunk); + return true; }, - }), + }, + }); + await fixture.build({ + // @ts-expect-error: logger is an internal API + logger, }); const relevantLogs = logs @@ -92,7 +98,7 @@ describe('Prerender conflicts', () => { }); it('fails when prerenderConflictBehavior is set to error', async () => { - let err; + let err: unknown; try { await fixture.build({ prerenderConflictBehavior: 'error' }); } catch (e) { diff --git a/packages/astro/test/public-base-404.test.js b/packages/astro/test/public-base-404.test.ts similarity index 91% rename from packages/astro/test/public-base-404.test.js rename to packages/astro/test/public-base-404.test.ts index 12c5ce14c6b4..c075898b38fa 100644 --- a/packages/astro/test/public-base-404.test.js +++ b/packages/astro/test/public-base-404.test.ts @@ -1,13 +1,12 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from './test-utils.js'; describe('Public dev with base', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let $; - let devServer; + let fixture: Fixture; + let $: cheerio.CheerioAPI; + let devServer: DevServer; before(async () => { fixture = await loadFixture({ diff --git a/packages/astro/test/react-and-solid.test.js b/packages/astro/test/react-and-solid.test.ts similarity index 84% rename from packages/astro/test/react-and-solid.test.js rename to packages/astro/test/react-and-solid.test.ts index 59126166fe75..8c963181b440 100644 --- a/packages/astro/test/react-and-solid.test.js +++ b/packages/astro/test/react-and-solid.test.ts @@ -1,11 +1,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('Solid app with some React components', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/react-and-solid/' }); diff --git a/packages/astro/test/react-jsx-export.test.js b/packages/astro/test/react-jsx-export.test.ts similarity index 71% rename from packages/astro/test/react-jsx-export.test.js rename to packages/astro/test/react-jsx-export.test.ts index 66bc073cca9b..f5da3d28cf2c 100644 --- a/packages/astro/test/react-jsx-export.test.js +++ b/packages/astro/test/react-jsx-export.test.ts @@ -1,11 +1,12 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type AstroLogMessage, AstroLogger } from '../dist/core/logger/core.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('react-jsx-export', () => { - let fixture; - let logs = []; + let fixture: Fixture; + const logs: AstroLogMessage[] = []; const ids = [ 'anonymous_arrow_default_export', @@ -25,18 +26,22 @@ describe('react-jsx-export', () => { const reactInvalidHookWarning = 'Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons'; before(async () => { - const logging = { - dest: { + const logger = new AstroLogger({ + destination: { write(chunk) { logs.push(chunk); + return true; }, }, level: 'warn', - }; + }); fixture = await loadFixture({ root: './fixtures/react-jsx-export/', }); - await fixture.build({ logging }); + await fixture.build({ + // @ts-expect-error: `logger` is @internal in AstroInlineConfig so it's stripped from dist types + logger, + }); }); it('Can load all JSX components', async () => { @@ -50,7 +55,7 @@ describe('react-jsx-export', () => { it('Cannot output React Invalid Hook warning', async () => { assert.equal( - logs.every((log) => log.message.indexOf(reactInvalidHookWarning) === -1), + logs.every((log) => !log.message.includes(reactInvalidHookWarning)), true, ); }); diff --git a/packages/astro/test/redirects.test.js b/packages/astro/test/redirects.test.ts similarity index 89% rename from packages/astro/test/redirects.test.js rename to packages/astro/test/redirects.test.ts index f8ac1b20c138..910f9fb72732 100644 --- a/packages/astro/test/redirects.test.js +++ b/packages/astro/test/redirects.test.ts @@ -1,16 +1,14 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from './test-utils.js'; describe('Astro.redirect output: "static"', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; describe('dev', () => { - /** @type {import('./test-utils.js').DevServer} */ - let devServer; + let devServer: DevServer; before(async () => { - process.env.STATIC_MODE = true; + process.env.STATIC_MODE = 'true'; fixture = await loadFixture({ root: './fixtures/redirects/', output: 'static', diff --git a/packages/astro/test/reexport-astro-containing-client-component.test.js b/packages/astro/test/reexport-astro-containing-client-component.test.ts similarity index 88% rename from packages/astro/test/reexport-astro-containing-client-component.test.js rename to packages/astro/test/reexport-astro-containing-client-component.test.ts index af290621092e..2391d5f36e41 100644 --- a/packages/astro/test/reexport-astro-containing-client-component.test.js +++ b/packages/astro/test/reexport-astro-containing-client-component.test.ts @@ -1,10 +1,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('Re-exported astro components with client components', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/reexport-astro-containing-client-component/' }); diff --git a/packages/astro/test/remote-css.test.js b/packages/astro/test/remote-css.test.ts similarity index 84% rename from packages/astro/test/remote-css.test.js rename to packages/astro/test/remote-css.test.ts index b6b0b03a1ad1..f5868cce86fe 100644 --- a/packages/astro/test/remote-css.test.js +++ b/packages/astro/test/remote-css.test.ts @@ -1,10 +1,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('Remote CSS', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -19,7 +19,7 @@ describe('Remote CSS', () => { const html = await fixture.readFile('/index.html'); const $ = cheerio.load(html); - const relPath = $('link').attr('href'); + const relPath = $('link').attr('href')!; const css = await fixture.readFile(relPath); assert.match(css, /https:\/\/unpkg.com\/open-props/); diff --git a/packages/astro/test/request-signal.test.js b/packages/astro/test/request-signal.test.js index a04ad2a7e426..66f16ba11918 100644 --- a/packages/astro/test/request-signal.test.js +++ b/packages/astro/test/request-signal.test.js @@ -3,7 +3,7 @@ import { EventEmitter } from 'node:events'; import { after, before, describe, it } from 'node:test'; import { setTimeout as delay } from 'node:timers/promises'; import { loadFixture } from './test-utils.js'; -import { createRequestAndResponse } from './units/test-utils.js'; +import { createRequestAndResponse } from './integration-test-helpers.js'; const createMockSocket = () => { const socket = new EventEmitter(); diff --git a/packages/astro/test/reuse-injected-entrypoint.test.js b/packages/astro/test/reuse-injected-entrypoint.test.ts similarity index 90% rename from packages/astro/test/reuse-injected-entrypoint.test.js rename to packages/astro/test/reuse-injected-entrypoint.test.ts index d0b004a455da..e6b8690cfead 100644 --- a/packages/astro/test/reuse-injected-entrypoint.test.js +++ b/packages/astro/test/reuse-injected-entrypoint.test.ts @@ -1,9 +1,19 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { load as cheerioLoad } from 'cheerio'; -import { loadFixture } from './test-utils.js'; - -const routes = [ +import { type DevServer, type Fixture, loadFixture } from './test-utils.js'; + +type Route = { + description: string; + url: string; + h1?: string; + p?: string; + scriptContent?: string; + htmlMatch?: string; + fourOhFour?: boolean; +}; + +const routes: Route[] = [ { description: 'matches / to index.astro', url: '/', @@ -53,13 +63,13 @@ const routes = [ }, ]; -function appendForwardSlash(path) { +function appendForwardSlash(path: string) { return path.endsWith('/') ? path : path + '/'; } describe('Reuse injected entrypoint', () => { describe('build', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -107,8 +117,8 @@ describe('Reuse injected entrypoint', () => { }); describe('dev', () => { - let fixture; - let devServer; + let fixture: Fixture; + let devServer: DevServer; before(async () => { fixture = await loadFixture({ diff --git a/packages/astro/test/rewrite.test.js b/packages/astro/test/rewrite.test.ts similarity index 91% rename from packages/astro/test/rewrite.test.js rename to packages/astro/test/rewrite.test.ts index 1dfadd8e5af7..77dc89a8bf57 100644 --- a/packages/astro/test/rewrite.test.js +++ b/packages/astro/test/rewrite.test.ts @@ -1,12 +1,11 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { load as cheerioLoad } from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from './test-utils.js'; describe('Dev rewrite, trailing slash -> never', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let devServer; + let fixture: Fixture; + let devServer: DevServer; before(async () => { fixture = await loadFixture({ @@ -28,9 +27,8 @@ describe('Dev rewrite, trailing slash -> never', () => { }); describe('Dev rewrite, trailing slash -> never, with base', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let devServer; + let fixture: Fixture; + let devServer: DevServer; before(async () => { fixture = await loadFixture({ @@ -63,9 +61,8 @@ describe('Dev rewrite, trailing slash -> never, with base', () => { }); describe('Dev rewrite, hybrid/server', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let devServer; + let fixture: Fixture; + let devServer: DevServer; before(async () => { fixture = await loadFixture({ @@ -110,9 +107,8 @@ describe('Dev rewrite, hybrid/server', () => { }); describe('Dev rewrite URL contains base and has no trailing slash', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let devServer; + let fixture: Fixture; + let devServer: DevServer; before(async () => { fixture = await loadFixture({ @@ -176,9 +172,8 @@ describe('SSR route', () => { }); describe('Runtime error, default 500', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let devServer; + let fixture: Fixture; + let devServer: DevServer; before(async () => { fixture = await loadFixture({ @@ -200,9 +195,8 @@ describe('Runtime error, default 500', () => { }); describe('Runtime error in dev, custom 500', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let devServer; + let fixture: Fixture; + let devServer: DevServer; before(async () => { fixture = await loadFixture({ diff --git a/packages/astro/test/root-srcdir-css.test.js b/packages/astro/test/root-srcdir-css.test.ts similarity index 83% rename from packages/astro/test/root-srcdir-css.test.js rename to packages/astro/test/root-srcdir-css.test.ts index 581cce7a3814..30b36760cf4c 100644 --- a/packages/astro/test/root-srcdir-css.test.js +++ b/packages/astro/test/root-srcdir-css.test.ts @@ -1,10 +1,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('srcDir', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -19,7 +19,7 @@ describe('srcDir', () => { const html = await fixture.readFile('/index.html'); const $ = cheerio.load(html); - const relPath = $('link').attr('href'); + const relPath = $('link').attr('href')!; const css = await fixture.readFile(relPath); assert.match(css, /body\{color:green\}/); }); diff --git a/packages/astro/test/route-guard.test.js b/packages/astro/test/route-guard.test.ts similarity index 95% rename from packages/astro/test/route-guard.test.js rename to packages/astro/test/route-guard.test.ts index 6c2bd10d2352..69b96645ed78 100644 --- a/packages/astro/test/route-guard.test.js +++ b/packages/astro/test/route-guard.test.ts @@ -1,12 +1,10 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from './test-utils.js'; describe('Route Guard - Dev Server', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - /** @type {import('./test-utils').DevServer} */ - let devServer; + let fixture: Fixture; + let devServer: DevServer; before(async () => { fixture = await loadFixture({ root: './fixtures/route-guard/' }); diff --git a/packages/astro/test/scoped-style-strategy.test.js b/packages/astro/test/scoped-style-strategy.test.ts similarity index 86% rename from packages/astro/test/scoped-style-strategy.test.js rename to packages/astro/test/scoped-style-strategy.test.ts index c1a76aabbf22..0b6c748e4a48 100644 --- a/packages/astro/test/scoped-style-strategy.test.js +++ b/packages/astro/test/scoped-style-strategy.test.ts @@ -1,13 +1,12 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('scopedStyleStrategy', () => { describe('scopedStyleStrategy: "where"', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let stylesheet; + let fixture: Fixture; + let stylesheet: string; before(async () => { fixture = await loadFixture({ @@ -21,7 +20,7 @@ describe('scopedStyleStrategy', () => { const html = await fixture.readFile('/index.html'); const $ = cheerio.load(html); const $link = $('link[rel=stylesheet]'); - const href = $link.attr('href'); + const href = $link.attr('href')!; stylesheet = await fixture.readFile(href); }); @@ -35,9 +34,8 @@ describe('scopedStyleStrategy', () => { }); describe('scopedStyleStrategy: "class"', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let stylesheet; + let fixture: Fixture; + let stylesheet: string; before(async () => { fixture = await loadFixture({ @@ -51,7 +49,7 @@ describe('scopedStyleStrategy', () => { const html = await fixture.readFile('/index.html'); const $ = cheerio.load(html); const $link = $('link[rel=stylesheet]'); - const href = $link.attr('href'); + const href = $link.attr('href')!; stylesheet = await fixture.readFile(href); }); @@ -65,9 +63,8 @@ describe('scopedStyleStrategy', () => { }); describe('default', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let stylesheet; + let fixture: Fixture; + let stylesheet: string; before(async () => { fixture = await loadFixture({ @@ -80,7 +77,7 @@ describe('scopedStyleStrategy', () => { const html = await fixture.readFile('/index.html'); const $ = cheerio.load(html); const $link = $('link[rel=stylesheet]'); - const href = $link.attr('href'); + const href = $link.attr('href')!; stylesheet = await fixture.readFile(href); }); diff --git a/packages/astro/test/serializeManifest.test.js b/packages/astro/test/serializeManifest.test.ts similarity index 95% rename from packages/astro/test/serializeManifest.test.js rename to packages/astro/test/serializeManifest.test.ts index 2a36352ede93..96085367880c 100644 --- a/packages/astro/test/serializeManifest.test.js +++ b/packages/astro/test/serializeManifest.test.ts @@ -4,12 +4,11 @@ import * as cheerio from 'cheerio'; import { ServerOnlyModule } from '../dist/core/errors/errors-data.js'; import { AstroError } from '../dist/core/errors/index.js'; import testAdapter from './test-adapter.js'; -import { loadFixture } from './test-utils.js'; +import { type App, type DevServer, type Fixture, loadFixture } from './test-utils.js'; describe('astro:config/client', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let devServer; + let fixture: Fixture; + let devServer: DevServer; describe('in dev', async () => { before(async () => { @@ -90,8 +89,7 @@ describe('astro:config/client', () => { }); describe('astro:config/client in a client script', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; describe('when build', () => { before(async () => { @@ -110,10 +108,9 @@ describe('astro:config/client in a client script', () => { }); describe('astro:config/server', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let devServer; - let app; + let fixture: Fixture; + let devServer: DevServer; + let app: App; describe('when build', () => { before(async () => { diff --git a/packages/astro/test/server-entry.test.js b/packages/astro/test/server-entry.test.ts similarity index 81% rename from packages/astro/test/server-entry.test.js rename to packages/astro/test/server-entry.test.ts index 102ab25c0e90..bf8b492590a2 100644 --- a/packages/astro/test/server-entry.test.js +++ b/packages/astro/test/server-entry.test.ts @@ -1,17 +1,26 @@ -// @ts-check - import { describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; +import { type App, type Fixture, loadFixture } from './test-utils.js'; import testAdapter, { selfTestAdapter } from './test-adapter.js'; import assert from 'node:assert/strict'; -import fakeAdapter from './fixtures/server-entry/fake-adapter/index.js'; import { existsSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; +import type { AstroIntegration } from 'astro'; + +type FakeAdapter = ( + options: + | { type: 'rollupInput'; shape: 'string' | 'object' | 'array' } + | { type: 'serverEntrypoint' }, +) => AstroIntegration; + +const fakeAdapter: FakeAdapter = await (async () => { + const importPath: string = './fixtures/server-entry/fake-adapter/index.js'; + const mod = await import(importPath); + return mod.default; +})(); describe('Server entry', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let app; + let fixture: Fixture; + let app: App; it('should load the custom entry when using legacy entrypoint', async () => { fixture = await loadFixture({ @@ -23,7 +32,7 @@ describe('Server entry', () => { }, }); - await fixture.build({}); + await fixture.build(); app = await fixture.loadTestAdapterApp(false); const request = new Request('http://example.com/'); @@ -41,7 +50,7 @@ describe('Server entry', () => { }, }); - await fixture.build({}); + await fixture.build(); app = await fixture.loadSelfAdapterApp(false); const request = new Request('http://example.com/'); @@ -59,7 +68,7 @@ describe('Server entry', () => { }, }); - await fixture.build({}); + await fixture.build(); assert.ok(existsSync(fileURLToPath(new URL('server/custom.mjs', fixture.config.outDir)))); }); @@ -74,7 +83,7 @@ describe('Server entry', () => { }, }); - await fixture.build({}); + await fixture.build(); assert.ok(existsSync(fileURLToPath(new URL('server/custom.mjs', fixture.config.outDir)))); }); @@ -89,7 +98,7 @@ describe('Server entry', () => { }, }); - await fixture.build({}); + await fixture.build(); assert.ok(existsSync(fileURLToPath(new URL('server/custom.mjs', fixture.config.outDir)))); }); @@ -104,7 +113,7 @@ describe('Server entry', () => { }, }); - await fixture.build({}); + await fixture.build(); assert.ok(existsSync(fileURLToPath(new URL('server/custom.mjs', fixture.config.outDir)))); }); diff --git a/packages/astro/test/server-islands.test.js b/packages/astro/test/server-islands.test.ts similarity index 94% rename from packages/astro/test/server-islands.test.js rename to packages/astro/test/server-islands.test.ts index 9ffcf5b941a7..15baf2d5801d 100644 --- a/packages/astro/test/server-islands.test.js +++ b/packages/astro/test/server-islands.test.ts @@ -5,10 +5,10 @@ import * as cheerio from 'cheerio'; import { encryptString } from '../dist/core/encryption.js'; import testAdapter from './test-adapter.js'; -import { loadFixture } from './test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from './test-utils.js'; // Helper to create encryption key from test key string -async function createKeyFromString(keyString) { +async function createKeyFromString(keyString: string) { const binaryString = atob(keyString); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { @@ -30,8 +30,7 @@ async function getEncryptedComponentExport( describe('Server islands', () => { describe('SSR', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/server-islands/ssr', @@ -43,7 +42,7 @@ describe('Server islands', () => { }); describe('dev', () => { - let devServer; + let devServer: DevServer; before(async () => { process.env.ASTRO_KEY = 'eKBaVEuI7YjfanEXHuJe/pwZKKt3LkAHeMxvTU7aR0M='; @@ -167,7 +166,7 @@ describe('Server islands', () => { const res = await fixture.fetch('/test'); assert.equal(res.status, 200); const html = await res.text(); - const fetchMatch = html.match(/fetch\('\/_server-islands\/Island\?[^']*p=([^&']*)/); + const fetchMatch = /fetch\('\/_server-islands\/Island\?[^']*p=([^&']*)/.exec(html)!; assert.equal(fetchMatch.length, 2, 'should include props in the query string'); assert.equal(fetchMatch[1], '', 'should not include encrypted empty props'); }); @@ -176,7 +175,7 @@ describe('Server islands', () => { const res = await fixture.fetch('/fragment'); assert.equal(res.status, 200); const html = await res.text(); - const fetchMatch = html.match(/fetch\('\/_server-islands\/Island\?[^']*p=([^&']*)/); + const fetchMatch = /fetch\('\/_server-islands\/Island\?[^']*p=([^&']*)/.exec(html)!; assert.equal(fetchMatch.length, 2, 'should include props in the query string'); assert.equal(fetchMatch[1], '', 'should not include encrypted empty props'); }); @@ -185,7 +184,7 @@ describe('Server islands', () => { const res = await fixture.fetch('/fragment'); assert.equal(res.status, 200); const html = await res.text(); - const fetchMatch = html.match(/fetch\('\/_server-islands\/Island\?[^']*p=([^&']*)/); + const fetchMatch = /fetch\('\/_server-islands\/Island\?[^']*p=([^&']*)/.exec(html)!; assert.equal(fetchMatch.length, 2, 'should include props in the query string'); assert.equal(fetchMatch[1], '', 'should not include encrypted empty props'); }); @@ -195,7 +194,7 @@ describe('Server islands', () => { assert.equal(res.status, 200); const html = await res.text(); // Extract the island fetch URL from the page - const urlMatch = html.match(/fetch\('(\/_server-islands\/Wrapper\?[^']+)'/); + const urlMatch = /fetch\('(\/_server-islands\/Wrapper\?[^']+)'/.exec(html)!; assert.ok(urlMatch, 'should have a server island fetch URL'); const islandRes = await fixture.fetch(urlMatch[1]); assert.equal(islandRes.status, 200); @@ -345,7 +344,7 @@ describe('Server islands', () => { const res = await app.render(request); assert.equal(res.status, 200); const html = await res.text(); - const fetchMatch = html.match(/fetch\('\/_server-islands\/Island\?[^']*p=([^&']*)/); + const fetchMatch = /fetch\('\/_server-islands\/Island\?[^']*p=([^&']*)/.exec(html)!; assert.equal(fetchMatch.length, 2, 'should include props in the query string'); assert.equal(fetchMatch[1], '', 'should not include encrypted empty props'); }); @@ -353,8 +352,7 @@ describe('Server islands', () => { }); describe('Hybrid mode', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/server-islands/hybrid', @@ -384,7 +382,7 @@ describe('Server islands', () => { const $ = cheerio.load(html); const serverIslandScript = $('script').filter((_, el) => - $(el).html().trim().startsWith('async function replaceServerIsland'), + ($(el).html() ?? '').trim().startsWith('async function replaceServerIsland'), ); assert.equal( serverIslandScript.length, @@ -411,7 +409,7 @@ describe('Server islands', () => { const res = await devFixture.fetch('/'); assert.equal(res.status, 200); const html = await res.text(); - const fetchMatch = /fetch\('(\/_server-islands\/Island[^']*)/.exec(html); + const fetchMatch = /fetch\('(\/_server-islands\/Island[^']*)/.exec(html)!; assert.ok(fetchMatch, 'should have a server island fetch URL'); const islandRes = await devFixture.fetch(fetchMatch[1]); assert.equal( @@ -425,7 +423,7 @@ describe('Server islands', () => { }); describe('with no adapter', () => { - let devServer; + let devServer: DevServer; it('Errors during the build', async () => { try { @@ -434,7 +432,10 @@ describe('Server islands', () => { }); assert.equal(true, false, 'should not have succeeded'); } catch (err) { - assert.equal(err.title, 'Cannot use Server Islands without an adapter.'); + assert.equal( + (err as { title: string }).title, + 'Cannot use Server Islands without an adapter.', + ); } }); diff --git a/packages/astro/test/set-html.test.js b/packages/astro/test/set-html.test.js deleted file mode 100644 index 04655d80cb6c..000000000000 --- a/packages/astro/test/set-html.test.js +++ /dev/null @@ -1,57 +0,0 @@ -import assert from 'node:assert/strict'; -import { after, before, describe, it } from 'node:test'; -import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; - -describe('set:html', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/set-html/', - }); - }); - - describe('Development', () => { - /** @type {import('./test-utils').DevServer} */ - let devServer; - - before(async () => { - devServer = await fixture.startDevServer(); - globalThis.TEST_FETCH = (fetch, url, init) => { - return fetch(fixture.resolveUrl(url), init); - }; - }); - - after(async () => { - await devServer.stop(); - }); - - it('can take a fetch()', async () => { - let res = await fixture.fetch('/fetch'); - assert.equal(res.status, 200); - let html = await res.text(); - const $ = cheerio.load(html); - assert.equal($('#fetched-html').length, 1); - assert.equal($('#fetched-html').text(), 'works'); - }); - it('test Fragment when Fragment is as a slot', async () => { - let res = await fixture.fetch('/children'); - assert.equal(res.status, 200); - let html = await res.text(); - assert.equal(html.includes('Test'), true); - }); - }); - - describe('Build', () => { - before(async () => { - await fixture.build(); - }); - - it('test Fragment when Fragment is as a slot', async () => { - let res = await fixture.readFile('/children/index.html'); - assert.equal(res.includes('Test'), true); - }); - }); -}); diff --git a/packages/astro/test/slots-preact.test.js b/packages/astro/test/slots-preact.test.ts similarity index 95% rename from packages/astro/test/slots-preact.test.js rename to packages/astro/test/slots-preact.test.ts index b486a27a777c..930f6181408f 100644 --- a/packages/astro/test/slots-preact.test.js +++ b/packages/astro/test/slots-preact.test.ts @@ -1,10 +1,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('Slots: Preact', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/slots-preact/' }); diff --git a/packages/astro/test/slots-react.test.js b/packages/astro/test/slots-react.test.ts similarity index 97% rename from packages/astro/test/slots-react.test.js rename to packages/astro/test/slots-react.test.ts index 64069dcef28a..5ae58d2e78b4 100644 --- a/packages/astro/test/slots-react.test.js +++ b/packages/astro/test/slots-react.test.ts @@ -1,10 +1,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('Slots: React', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/slots-react/' }); diff --git a/packages/astro/test/slots-solid.test.js b/packages/astro/test/slots-solid.test.ts similarity index 95% rename from packages/astro/test/slots-solid.test.js rename to packages/astro/test/slots-solid.test.ts index 68e64fe62214..a39317a975d8 100644 --- a/packages/astro/test/slots-solid.test.js +++ b/packages/astro/test/slots-solid.test.ts @@ -1,10 +1,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('Slots: Solid', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/slots-solid/' }); diff --git a/packages/astro/test/slots-svelte.test.js b/packages/astro/test/slots-svelte.test.ts similarity index 95% rename from packages/astro/test/slots-svelte.test.js rename to packages/astro/test/slots-svelte.test.ts index e45c6d414c37..6b3b1cce60ad 100644 --- a/packages/astro/test/slots-svelte.test.js +++ b/packages/astro/test/slots-svelte.test.ts @@ -1,10 +1,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('Slots: Svelte', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/slots-svelte/' }); diff --git a/packages/astro/test/slots-vue.test.js b/packages/astro/test/slots-vue.test.ts similarity index 95% rename from packages/astro/test/slots-vue.test.js rename to packages/astro/test/slots-vue.test.ts index 8edee824118c..1e3bb9e12dab 100644 --- a/packages/astro/test/slots-vue.test.js +++ b/packages/astro/test/slots-vue.test.ts @@ -1,10 +1,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('Slots: Vue', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/slots-vue/' }); diff --git a/packages/astro/test/sourcemap.test.js b/packages/astro/test/sourcemap.test.ts similarity index 82% rename from packages/astro/test/sourcemap.test.js rename to packages/astro/test/sourcemap.test.ts index 7040655a35df..3cc23fe81286 100644 --- a/packages/astro/test/sourcemap.test.js +++ b/packages/astro/test/sourcemap.test.ts @@ -1,9 +1,9 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('Sourcemap', async () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/sourcemap/' }); @@ -12,7 +12,7 @@ describe('Sourcemap', async () => { it('Builds sourcemap', async () => { const dir = await fixture.readdir('./_astro'); - const counterMap = dir.find((file) => file.match(/^Counter\.[\w-]+\.js\.map$/)); + const counterMap = dir.find((file) => /^Counter\.[\w-]+\.js\.map$/.exec(file)); assert.ok(counterMap); }); diff --git a/packages/astro/test/space-in-folder-name.test.js b/packages/astro/test/space-in-folder-name.test.ts similarity index 84% rename from packages/astro/test/space-in-folder-name.test.js rename to packages/astro/test/space-in-folder-name.test.ts index 3de7ec13b72d..cfb5642bd8fd 100644 --- a/packages/astro/test/space-in-folder-name.test.js +++ b/packages/astro/test/space-in-folder-name.test.ts @@ -1,10 +1,10 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from './test-utils.js'; describe('Projects with a space in the folder name', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -13,8 +13,7 @@ describe('Projects with a space in the folder name', () => { }); describe('dev', () => { - /** @type {import('./test-utils').Fixture} */ - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); diff --git a/packages/astro/test/special-chars-in-component-imports.test.js b/packages/astro/test/special-chars-in-component-imports.test.ts similarity index 88% rename from packages/astro/test/special-chars-in-component-imports.test.js rename to packages/astro/test/special-chars-in-component-imports.test.ts index 64301741adff..6f6a80eb5245 100644 --- a/packages/astro/test/special-chars-in-component-imports.test.js +++ b/packages/astro/test/special-chars-in-component-imports.test.ts @@ -1,11 +1,10 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { load as cheerioLoad } from 'cheerio'; -import { isWindows, loadFixture } from './test-utils.js'; +import { type DevServer, type Fixture, isWindows, loadFixture } from './test-utils.js'; describe('Special chars in component import paths', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; const componentIds = [ 'caret', @@ -33,6 +32,18 @@ describe('Special chars in component import paths', () => { assert.equal(html.includes(''), true); }); + it('Output JS filenames do not contain unsafe characters', async () => { + const files = await fixture.readdir('/_astro'); + const jsFiles = files.filter((f) => f.endsWith('.js')); + for (const file of jsFiles) { + assert.equal( + /[!~#{}<>]/.test(file), + false, + `File "${file}" contains unsafe characters that break some hosting platforms`, + ); + } + }); + it('Special chars in imports work from .astro files', async () => { const html = await fixture.readFile('/index.html'); const $ = cheerioLoad(html); @@ -79,7 +90,7 @@ describe('Special chars in component import paths', () => { if (isWindows) return; describe('dev', () => { - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); diff --git a/packages/astro/test/ssr-adapter-build-config.test.js b/packages/astro/test/ssr-adapter-build-config.test.ts similarity index 91% rename from packages/astro/test/ssr-adapter-build-config.test.js rename to packages/astro/test/ssr-adapter-build-config.test.ts index c37adb43e778..3ff4d1d2398c 100644 --- a/packages/astro/test/ssr-adapter-build-config.test.js +++ b/packages/astro/test/ssr-adapter-build-config.test.ts @@ -1,11 +1,11 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; +import type { Plugin } from 'vite'; import { viteID } from '../dist/core/util.js'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('Integration buildConfig hook', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -23,6 +23,7 @@ describe('Integration buildConfig hook', () => { vite: { plugins: [ { + name: 'my-ssr-plugin', resolveId: { filter: { id: /^(astro\/app|@my-ssr)$/, @@ -42,7 +43,7 @@ describe('Integration buildConfig hook', () => { return `import { App } from 'astro/app';export function createExports(manifest) { return { manifest, createApp: () => new App(manifest) }; }`; }, }, - }, + } satisfies Plugin, ], }, }); diff --git a/packages/astro/test/ssr-api-route.test.js b/packages/astro/test/ssr-api-route.test.ts similarity index 95% rename from packages/astro/test/ssr-api-route.test.js rename to packages/astro/test/ssr-api-route.test.ts index 24e10b188c4c..c30c57709396 100644 --- a/packages/astro/test/ssr-api-route.test.js +++ b/packages/astro/test/ssr-api-route.test.ts @@ -1,11 +1,18 @@ import assert from 'node:assert/strict'; +import fs from 'node:fs'; import net from 'node:net'; import { after, before, describe, it } from 'node:test'; import testAdapter from './test-adapter.js'; -import { loadFixture } from './test-utils.js'; +import { + type App, + type AstroInlineConfig, + type DevServer, + type Fixture, + loadFixture, +} from './test-utils.js'; describe('API routes in SSR', () => { - const config = { + const config: AstroInlineConfig = { root: './fixtures/ssr-api-route/', output: 'server', site: 'https://mysite.dev/subsite/', @@ -20,8 +27,7 @@ describe('API routes in SSR', () => { }; describe('Build', () => { - /** @type {import('./test-utils.js').App} */ - let app; + let app: App; before(async () => { const fixture = await loadFixture(config); await fixture.build(); @@ -94,10 +100,8 @@ describe('API routes in SSR', () => { }); describe('Dev', () => { - /** @type {import('./test-utils.js').DevServer} */ - let devServer; - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let devServer: DevServer; + let fixture: Fixture; before(async () => { fixture = await loadFixture(config); devServer = await fixture.startDevServer(); @@ -145,7 +149,7 @@ describe('API routes in SSR', () => { }); it('Can set multiple headers of the same type', async () => { - const response = await new Promise((resolve) => { + const response = await new Promise((resolve) => { let { port } = devServer.address; let host = 'localhost'; let socket = new net.Socket(); diff --git a/packages/astro/test/ssr-assets.test.js b/packages/astro/test/ssr-assets.test.ts similarity index 83% rename from packages/astro/test/ssr-assets.test.js rename to packages/astro/test/ssr-assets.test.ts index d56ad1686b32..d20331a70f02 100644 --- a/packages/astro/test/ssr-assets.test.js +++ b/packages/astro/test/ssr-assets.test.ts @@ -1,11 +1,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import testAdapter from './test-adapter.js'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('SSR Assets', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -20,7 +19,6 @@ describe('SSR Assets', () => { it('Do not have to implement getStaticPaths', async () => { const app = await fixture.loadTestAdapterApp(); - /** @type {Set} */ const assets = app.manifest.assets; assert.equal(assets.size, 1); assert.equal(Array.from(assets)[0].endsWith('.css'), true); diff --git a/packages/astro/test/ssr-dynamic.test.js b/packages/astro/test/ssr-dynamic.test.ts similarity index 92% rename from packages/astro/test/ssr-dynamic.test.js rename to packages/astro/test/ssr-dynamic.test.ts index dc9037bb64c3..c7f54cfb0a2d 100644 --- a/packages/astro/test/ssr-dynamic.test.js +++ b/packages/astro/test/ssr-dynamic.test.ts @@ -5,11 +5,10 @@ import { before, describe, it } from 'node:test'; import { fileURLToPath } from 'node:url'; import { load as cheerioLoad } from 'cheerio'; import testAdapter from './test-adapter.js'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('Dynamic pages in SSR', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { const root = './fixtures/ssr-dynamic/'; @@ -47,13 +46,13 @@ describe('Dynamic pages in SSR', () => { await fixture.build(); }); - async function matchRoute(path) { + async function matchRoute(path: string) { const app = await fixture.loadTestAdapterApp(); const request = new Request('https://example.com' + path); return app.match(request); } - async function fetchHTML(path) { + async function fetchHTML(path: string) { const app = await fixture.loadTestAdapterApp(); const request = new Request('http://example.com' + path); const response = await app.render(request); @@ -61,7 +60,7 @@ describe('Dynamic pages in SSR', () => { return html; } - async function fetchJSON(path) { + async function fetchJSON(path: string) { const app = await fixture.loadTestAdapterApp(); const request = new Request('http://example.com' + path); const response = await app.render(request); diff --git a/packages/astro/test/ssr-large-array.test.js b/packages/astro/test/ssr-large-array.test.ts similarity index 90% rename from packages/astro/test/ssr-large-array.test.js rename to packages/astro/test/ssr-large-array.test.ts index f8602446704f..490aad3da41c 100644 --- a/packages/astro/test/ssr-large-array.test.js +++ b/packages/astro/test/ssr-large-array.test.ts @@ -2,11 +2,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; import testAdapter from './test-adapter.js'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('SSR with Large Array and client rendering', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ diff --git a/packages/astro/test/ssr-markdown.test.js b/packages/astro/test/ssr-markdown.test.js deleted file mode 100644 index 286891790316..000000000000 --- a/packages/astro/test/ssr-markdown.test.js +++ /dev/null @@ -1,33 +0,0 @@ -import assert from 'node:assert/strict'; -import { before, describe, it } from 'node:test'; -import { load as cheerioLoad } from 'cheerio'; -import testAdapter from './test-adapter.js'; -import { loadFixture } from './test-utils.js'; - -describe('Markdown pages in SSR', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/ssr-markdown/', - output: 'server', - adapter: testAdapter(), - }); - await fixture.build(); - }); - - async function fetchHTML(path) { - const app = await fixture.loadTestAdapterApp(); - const request = new Request('http://example.com' + path); - const response = await app.render(request); - const html = await response.text(); - return html; - } - - it('Renders markdown pages correctly', async () => { - const html = await fetchHTML('/post'); - const $ = cheerioLoad(html); - assert.equal($('#subheading').text(), 'Subheading'); - }); -}); diff --git a/packages/astro/test/ssr-partytown.test.js b/packages/astro/test/ssr-partytown.test.ts similarity index 91% rename from packages/astro/test/ssr-partytown.test.js rename to packages/astro/test/ssr-partytown.test.ts index 4910f3e51ba4..091f072075a4 100644 --- a/packages/astro/test/ssr-partytown.test.js +++ b/packages/astro/test/ssr-partytown.test.ts @@ -2,11 +2,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { load as cheerioLoad } from 'cheerio'; import testAdapter from './test-adapter.js'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('Using the Partytown integration in SSR', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ diff --git a/packages/astro/test/ssr-prerender-get-static-paths.test.js b/packages/astro/test/ssr-prerender-get-static-paths.test.ts similarity index 91% rename from packages/astro/test/ssr-prerender-get-static-paths.test.js rename to packages/astro/test/ssr-prerender-get-static-paths.test.ts index e34b65aba9ba..2708b5477376 100644 --- a/packages/astro/test/ssr-prerender-get-static-paths.test.js +++ b/packages/astro/test/ssr-prerender-get-static-paths.test.ts @@ -1,12 +1,12 @@ import assert from 'node:assert/strict'; import { after, afterEach, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; +import type { Plugin } from 'vite'; import testAdapter from './test-adapter.js'; -import { loadFixture } from './test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from './test-utils.js'; describe('Prerender', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; describe('output: "server"', () => { describe('getStaticPaths - build calls', () => { before(async () => { @@ -26,7 +26,7 @@ describe('Prerender', () => { afterEach(() => { // reset the flag used by [...calledTwiceTest].astro between each test - globalThis.isCalledOnce = false; + (globalThis as any).isCalledOnce = false; }); it('is only called once during build', () => { @@ -44,16 +44,16 @@ describe('Prerender', () => { }); describe('getStaticPaths - dev calls', () => { - let devServer; + let devServer: DevServer; before(async () => { - globalThis.isCalledOnce = false; + (globalThis as any).isCalledOnce = false; devServer = await fixture.startDevServer(); }); afterEach(() => { // reset the flag used by [...calledTwiceTest].astro between each test - globalThis.isCalledOnce = false; + (globalThis as any).isCalledOnce = false; }); after(async () => { @@ -145,7 +145,7 @@ describe('Prerender', () => { afterEach(() => { // reset the flag used by [...calledTwiceTest].astro between each test - globalThis.isCalledOnce = false; + (globalThis as any).isCalledOnce = false; }); it('is only called once during build', () => { @@ -163,16 +163,16 @@ describe('Prerender', () => { }); describe('getStaticPaths - dev calls', () => { - let devServer; + let devServer: DevServer; before(async () => { - globalThis.isCalledOnce = false; + (globalThis as any).isCalledOnce = false; devServer = await fixture.startDevServer(); }); afterEach(() => { // reset the flag used by [...calledTwiceTest].astro between each test - globalThis.isCalledOnce = false; + (globalThis as any).isCalledOnce = false; }); after(async () => { @@ -238,11 +238,9 @@ describe('Prerender', () => { }); }); -/** @returns {import('vite').Plugin} */ -function vitePluginRemovePrerenderExport() { +function vitePluginRemovePrerenderExport(): Plugin { const EXTENSIONS = ['.astro', '.ts']; - /** @type {import('vite').Plugin} */ - const plugin = { + const plugin: Plugin = { name: 'remove-prerender-export', transform(code, id) { if (!EXTENSIONS.some((ext) => id.endsWith(ext))) return; @@ -252,6 +250,7 @@ function vitePluginRemovePrerenderExport() { return { name: 'remove-prerender-export-injector', configResolved(resolved) { + // @ts-expect-error: `resolved.plugins` is typed as `ReadonlyArray` resolved.plugins.unshift(plugin); }, }; diff --git a/packages/astro/test/ssr-prerender.test.js b/packages/astro/test/ssr-prerender.test.ts similarity index 88% rename from packages/astro/test/ssr-prerender.test.js rename to packages/astro/test/ssr-prerender.test.ts index 837e420d3e91..ab6677f768fb 100644 --- a/packages/astro/test/ssr-prerender.test.js +++ b/packages/astro/test/ssr-prerender.test.ts @@ -1,12 +1,12 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; +import type { AstroIntegration } from 'astro'; import * as cheerio from 'cheerio'; import testAdapter from './test-adapter.js'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('SSR: prerender', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -30,7 +30,6 @@ describe('SSR: prerender', () => { it('includes prerendered pages in the asset manifest', async () => { const app = await fixture.loadTestAdapterApp(); - /** @type {Set} */ const assets = app.manifest.assets; assert.equal(assets.has('/static/index.html'), true); }); @@ -96,6 +95,15 @@ describe('SSR: prerender', () => { assert.equal($('p').text().includes('not give 404'), true); }); }); + + it('Renders markdown pages correctly', async () => { + const app = await fixture.loadTestAdapterApp(); + const request = new Request('http://example.com/post'); + const response = await app.render(request); + const html = await response.text(); + const $ = cheerio.load(html); + assert.equal($('#subheading').text(), 'Subheading'); + }); }); // NOTE: This test doesn't make sense as it relies on the fact that on the client build, @@ -103,19 +111,18 @@ describe('SSR: prerender', () => { // is not always guaranteed to run. If we want to support this feature, we may want to only allow // editing `route.prerender` on the `astro:build:done` hook. describe.skip('Integrations can hook into the prerendering decision', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; - const testIntegration = { + const testIntegration: AstroIntegration = { name: 'test prerendering integration', hooks: { ['astro:build:setup']({ pages, target }) { if (target !== 'client') return; // this page has `export const prerender = true` - pages.get('src/pages/static.astro').route.prerender = false; + pages.get('src/pages/static.astro')!.route.prerender = false; // this page does not - pages.get('src/pages/not-prerendered.astro').route.prerender = true; + pages.get('src/pages/not-prerendered.astro')!.route.prerender = true; }, }, }; diff --git a/packages/astro/test/ssr-preview.test.js b/packages/astro/test/ssr-preview.test.ts similarity index 75% rename from packages/astro/test/ssr-preview.test.js rename to packages/astro/test/ssr-preview.test.ts index f8202d464413..8a1d4511865c 100644 --- a/packages/astro/test/ssr-preview.test.js +++ b/packages/astro/test/ssr-preview.test.ts @@ -1,10 +1,9 @@ import { before, describe, it } from 'node:test'; import testAdapter from './test-adapter.js'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('SSR Preview', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -16,7 +15,6 @@ describe('SSR Preview', () => { }); it('preview server works', async () => { - /** @type {import('./test-utils').PreviewServer} */ const previewServer = await fixture.preview(); await previewServer.stop(); }); diff --git a/packages/astro/test/ssr-renderers-static-vue.test.js b/packages/astro/test/ssr-renderers-static-vue.test.ts similarity index 88% rename from packages/astro/test/ssr-renderers-static-vue.test.js rename to packages/astro/test/ssr-renderers-static-vue.test.ts index a712c0633a38..5196108d1f4b 100644 --- a/packages/astro/test/ssr-renderers-static-vue.test.js +++ b/packages/astro/test/ssr-renderers-static-vue.test.ts @@ -1,11 +1,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import testAdapter from './test-adapter.js'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('SSR renderers with static framework pages', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ diff --git a/packages/astro/test/ssr-request.test.js b/packages/astro/test/ssr-request.test.ts similarity index 94% rename from packages/astro/test/ssr-request.test.js rename to packages/astro/test/ssr-request.test.ts index 537592f3a7d0..04f4fb496876 100644 --- a/packages/astro/test/ssr-request.test.js +++ b/packages/astro/test/ssr-request.test.ts @@ -2,11 +2,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { load as cheerioLoad } from 'cheerio'; import testAdapter from './test-adapter.js'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('Using Astro.request in SSR', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -56,7 +55,7 @@ describe('Using Astro.request in SSR', () => { const html = await response.text(); const $ = cheerioLoad(html); - const linkHref = $('link').attr('href'); + const linkHref = $('link').attr('href')!; assert.equal(linkHref.startsWith('/subpath/'), true); request = new Request('http://example.com' + linkHref); @@ -76,7 +75,7 @@ describe('Using Astro.request in SSR', () => { const $ = cheerioLoad(html); for (const el of $('script')) { - const scriptSrc = $(el).attr('src'); + const scriptSrc = $(el).attr('src')!; assert.equal(scriptSrc.startsWith('/subpath/'), true); request = new Request('http://example.com' + scriptSrc); response = await app.render(request); diff --git a/packages/astro/test/ssr-script.test.js b/packages/astro/test/ssr-script.test.ts similarity index 75% rename from packages/astro/test/ssr-script.test.js rename to packages/astro/test/ssr-script.test.ts index 755c5061f526..66ad376506c5 100644 --- a/packages/astro/test/ssr-script.test.js +++ b/packages/astro/test/ssr-script.test.ts @@ -2,9 +2,9 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { load as cheerioLoad } from 'cheerio'; import testAdapter from './test-adapter.js'; -import { loadFixture } from './test-utils.js'; +import { type AstroInlineConfig, type Fixture, loadFixture } from './test-utils.js'; -async function fetchHTML(fixture, path) { +async function fetchHTML(fixture: Fixture, path: string) { const app = await fixture.loadTestAdapterApp(); const request = new Request('http://example.com' + path); const response = await app.render(request); @@ -12,16 +12,14 @@ async function fetchHTML(fixture, path) { return html; } -/** @type {import('./test-utils.js').AstroInlineConfig} */ -const defaultFixtureOptions = { +const defaultFixtureOptions: AstroInlineConfig = { root: './fixtures/ssr-script/', output: 'server', adapter: testAdapter(), }; describe('Inline scripts in SSR', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; describe('without base path', () => { before(async () => { @@ -37,6 +35,45 @@ describe('Inline scripts in SSR', () => { const $ = cheerioLoad(html); assert.equal($('script').length, 1); }); + + it('server output filenames do not contain unsafe characters', async () => { + const files = await fixture.glob('server/**/*.{js,mjs}'); + for (const file of files) { + assert.equal( + /[!~#{}<>]/.test(file), + false, + `File "${file}" contains characters that break hosting platforms like Netlify`, + ); + } + }); + }); + + describe('with assetQueryParams', () => { + before(async () => { + fixture = await loadFixture({ + ...defaultFixtureOptions, + outDir: './dist/inline-scripts-with-asset-query-params', + adapter: testAdapter({ + extendAdapter: { + client: { + assetQueryParams: new URLSearchParams({ dpl: 'test123' }), + }, + }, + }), + }); + await fixture.build(); + }); + + it('client output filenames do not contain hash placeholders or unsafe characters', async () => { + const files = await fixture.glob('client/**/*.{js,mjs}'); + for (const file of files) { + assert.equal( + /[!~{}]/.test(file), + false, + `File "${file}" contains unsafe characters (likely unresolved hash placeholders)`, + ); + } + }); }); describe('with base path', () => { @@ -60,8 +97,7 @@ describe('Inline scripts in SSR', () => { }); describe('External scripts in SSR', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; describe('without base path', () => { before(async () => { @@ -80,7 +116,7 @@ describe('External scripts in SSR', () => { it('script has correct path', async () => { const html = await fetchHTML(fixture, '/'); const $ = cheerioLoad(html); - assert.match($('script').attr('src'), /^\/_astro\/.*\.js$/); + assert.match($('script').attr('src')!, /^\/_astro\/.*\.js$/); }); }); @@ -102,7 +138,7 @@ describe('External scripts in SSR', () => { it('script has correct path', async () => { const html = await fetchHTML(fixture, '/hello/'); const $ = cheerioLoad(html); - assert.match($('script').attr('src'), /^\/hello\/_astro\/.*\.js$/); + assert.match($('script').attr('src')!, /^\/hello\/_astro\/.*\.js$/); }); }); @@ -126,7 +162,7 @@ describe('External scripts in SSR', () => { it('script has correct path', async () => { const html = await fetchHTML(fixture, '/'); const $ = cheerioLoad(html); - assert.match($('script').attr('src'), /^https:\/\/cdn\.example\.com\/_astro\/.*\.js$/); + assert.match($('script').attr('src')!, /^https:\/\/cdn\.example\.com\/_astro\/.*\.js$/); }); }); @@ -160,7 +196,7 @@ describe('External scripts in SSR', () => { it('script has correct path', async () => { const html = await fetchHTML(fixture, '/'); const $ = cheerioLoad(html); - assert.match($('script').attr('src'), /^\/assets\/entry\..{8}\.mjs$/); + assert.match($('script').attr('src')!, /^\/assets\/entry\..{8}\.mjs$/); }); }); @@ -195,7 +231,7 @@ describe('External scripts in SSR', () => { it('script has correct path', async () => { const html = await fetchHTML(fixture, '/hello/'); const $ = cheerioLoad(html); - assert.match($('script').attr('src'), /^\/hello\/assets\/entry\..{8}\.mjs$/); + assert.match($('script').attr('src')!, /^\/hello\/assets\/entry\..{8}\.mjs$/); }); }); @@ -233,7 +269,7 @@ describe('External scripts in SSR', () => { const html = await fetchHTML(fixture, '/'); const $ = cheerioLoad(html); assert.match( - $('script').attr('src'), + $('script').attr('src')!, /^https:\/\/cdn\.example\.com\/assets\/entry\..{8}\.mjs$/, ); }); diff --git a/packages/astro/test/ssr-scripts.test.js b/packages/astro/test/ssr-scripts.test.js deleted file mode 100644 index 5afa2a25bc50..000000000000 --- a/packages/astro/test/ssr-scripts.test.js +++ /dev/null @@ -1,26 +0,0 @@ -import assert from 'node:assert/strict'; -import { before, describe, it } from 'node:test'; -import testAdapter from './test-adapter.js'; -import { loadFixture } from './test-utils.js'; - -describe('SSR Hydrated component scripts', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/ssr-scripts/', - output: 'server', - adapter: testAdapter(), - }); - await fixture.build(); - }); - - it('Are included in the manifest.assets so that an adapter can know to serve static', async () => { - const app = await fixture.loadTestAdapterApp(); - - /** @type {Set} */ - const assets = app.manifest.assets; - assert.ok(assets.size > 0); - }); -}); diff --git a/packages/astro/test/ssr-env.test.js b/packages/astro/test/ssr-scripts.test.ts similarity index 61% rename from packages/astro/test/ssr-env.test.js rename to packages/astro/test/ssr-scripts.test.ts index 7da190736768..e36039cdfb66 100644 --- a/packages/astro/test/ssr-env.test.js +++ b/packages/astro/test/ssr-scripts.test.ts @@ -2,21 +2,27 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; import testAdapter from './test-adapter.js'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; -describe('SSR Environment Variables', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; +describe('SSR Hydrated component scripts', () => { + let fixture: Fixture; before(async () => { fixture = await loadFixture({ - root: './fixtures/ssr-env/', + root: './fixtures/ssr-scripts/', output: 'server', adapter: testAdapter(), }); await fixture.build(); }); + it('Are included in the manifest.assets so that an adapter can know to serve static', async () => { + const app = await fixture.loadTestAdapterApp(); + + const assets = app.manifest.assets; + assert.ok(assets.size > 0); + }); + it('import.meta.env.SSR is true', async () => { const app = await fixture.loadTestAdapterApp(); const request = new Request('http://example.com/ssr'); diff --git a/packages/astro/test/static-build-code-component.test.js b/packages/astro/test/static-build-code-component.test.ts similarity index 86% rename from packages/astro/test/static-build-code-component.test.js rename to packages/astro/test/static-build-code-component.test.ts index d23d4115b229..8ba2c950c5d0 100644 --- a/packages/astro/test/static-build-code-component.test.js +++ b/packages/astro/test/static-build-code-component.test.ts @@ -1,10 +1,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe('Code component inside static build', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ diff --git a/packages/astro/test/static-build-dir.test.js b/packages/astro/test/static-build-dir.test.ts similarity index 86% rename from packages/astro/test/static-build-dir.test.js rename to packages/astro/test/static-build-dir.test.ts index 1b99315a2597..f56ae9fe2005 100644 --- a/packages/astro/test/static-build-dir.test.js +++ b/packages/astro/test/static-build-dir.test.ts @@ -3,10 +3,8 @@ import { before, describe, it } from 'node:test'; import { loadFixture } from './test-utils.js'; describe('Static build: dir takes the URL path to the output directory', () => { - /** @type {URL} */ - let checkDir; - /** @type {URL} */ - let checkGeneratedDir; + let checkDir: URL; + let checkGeneratedDir: URL; before(async () => { const fixture = await loadFixture({ root: './fixtures/static-build-dir/', @@ -27,7 +25,7 @@ describe('Static build: dir takes the URL path to the output directory', () => { await fixture.build(); }); it('dir takes the URL path to the output directory', async () => { - const removeTrailingSlash = (str) => str.replace(/\/$/, ''); + const removeTrailingSlash = (str: string) => str.replace(/\/$/, ''); assert.equal( removeTrailingSlash(checkDir.toString()), removeTrailingSlash(new URL('./fixtures/static-build-dir/dist', import.meta.url).toString()), diff --git a/packages/astro/test/static-build-frameworks.test.js b/packages/astro/test/static-build-frameworks.test.ts similarity index 92% rename from packages/astro/test/static-build-frameworks.test.js rename to packages/astro/test/static-build-frameworks.test.ts index ed362b7c28cb..ddc425d0dead 100644 --- a/packages/astro/test/static-build-frameworks.test.js +++ b/packages/astro/test/static-build-frameworks.test.ts @@ -1,14 +1,14 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { isWindows, loadFixture } from './test-utils.js'; +import { type Fixture, isWindows, loadFixture } from './test-utils.js'; describe('Static build - frameworks', () => { if (isWindows) { return; } - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ diff --git a/packages/astro/test/static-build-page-dist-url.test.js b/packages/astro/test/static-build-page-dist-url.test.ts similarity index 91% rename from packages/astro/test/static-build-page-dist-url.test.js rename to packages/astro/test/static-build-page-dist-url.test.ts index 4ebd9b5d327e..2f74e1d4038d 100644 --- a/packages/astro/test/static-build-page-dist-url.test.js +++ b/packages/astro/test/static-build-page-dist-url.test.ts @@ -3,10 +3,8 @@ import { before, describe, it } from 'node:test'; import { loadFixture } from './test-utils.js'; describe('Static build: pages routes have distURL', () => { - /** @type {Map} */ - let assets; + let assets: Map; before(async () => { - /** @type {import('./test-utils').Fixture} */ const fixture = await loadFixture({ root: './fixtures/astro pages/', integrations: [ diff --git a/packages/astro/test/static-build-page-url-format.test.js b/packages/astro/test/static-build-page-url-format.test.ts similarity index 87% rename from packages/astro/test/static-build-page-url-format.test.js rename to packages/astro/test/static-build-page-url-format.test.ts index 2755b3ffb3cb..5351c06346f4 100644 --- a/packages/astro/test/static-build-page-url-format.test.js +++ b/packages/astro/test/static-build-page-url-format.test.ts @@ -1,9 +1,9 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.js'; describe("Static build - format: 'file'", () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ diff --git a/packages/astro/test/static-build-vite-plugins.test.js b/packages/astro/test/static-build-vite-plugins.test.ts similarity index 92% rename from packages/astro/test/static-build-vite-plugins.test.js rename to packages/astro/test/static-build-vite-plugins.test.ts index 23a801ee9639..05fda88c7daf 100644 --- a/packages/astro/test/static-build-vite-plugins.test.js +++ b/packages/astro/test/static-build-vite-plugins.test.ts @@ -3,10 +3,8 @@ import { before, describe, it } from 'node:test'; import { loadFixture } from './test-utils.js'; describe('Static build: vite plugins included when required', () => { - /** @type {Map} */ - const pluginsCalled = new Map(); - /** @type {Map} */ - const expectedPluginResult = new Map([ + const pluginsCalled = new Map(); + const expectedPluginResult = new Map([ ['prepare-no-apply-plugin', true], ['prepare-serve-plugin', false], ['prepare-apply-fn-plugin', true], @@ -14,7 +12,6 @@ describe('Static build: vite plugins included when required', () => { ['prepare-build-plugin', true], ]); before(async () => { - /** @type {import('./test-utils').Fixture} */ const fixture = await loadFixture({ root: './fixtures/astro pages/', integrations: [ diff --git a/packages/astro/test/static-build.test.js b/packages/astro/test/static-build.test.js index e94c0af65bee..370dcd6449ee 100644 --- a/packages/astro/test/static-build.test.js +++ b/packages/astro/test/static-build.test.js @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { load as cheerioLoad } from 'cheerio'; -import { Logger } from '../dist/core/logger/core.js'; +import { AstroLogger } from '../dist/core/logger/core.js'; import { loadFixture } from './test-utils.js'; function addLeadingSlash(path) { @@ -14,19 +14,19 @@ function removeBasePath(path) { } /** - * @typedef {import('../src/core/logger/core').LogMessage} LogMessage + * @typedef {import('../src/core/logger/core').AstroLogMessage} AstroLogMessage */ describe('Static build', () => { /** @type {import('./test-utils').Fixture} */ let fixture; - /** @type {LogMessage[]} */ + /** @type {AstroLogMessage[]} */ let logs = []; before(async () => { - /** @type {import('../src/core/logger/core').Logger} */ - const logger = new Logger({ - dest: { + /** @type {import('../src/core/logger/core').AstroLogger} */ + const logger = new AstroLogger({ + destination: { write(chunk) { logs.push(chunk); }, diff --git a/packages/astro/test/streaming.test.js b/packages/astro/test/streaming.test.js index ad71119d1e61..b4e2639f5b3e 100644 --- a/packages/astro/test/streaming.test.js +++ b/packages/astro/test/streaming.test.js @@ -87,6 +87,26 @@ describe('Streaming', () => { const text = await response.text(); assert.equal(text, ''); }); + + it('sync sibling inside Fragment streams before async child resolves', async () => { + const app = await fixture.loadTestAdapterApp(); + const request = new Request('http://example.com/fragment-streaming'); + const response = await app.render(request); + + const chunks = []; + for await (const bytes of streamAsyncIterator(response.body)) { + chunks.push(decoder.decode(bytes)); + } + + const syncChunkIndex = chunks.findIndex((c) => c.includes('sync-in-fragment')); + const asyncChunkIndex = chunks.findIndex((c) => c.includes('async-in-fragment')); + assert.ok(syncChunkIndex !== -1, 'sync-in-fragment present in output'); + assert.ok(asyncChunkIndex !== -1, 'async-in-fragment present in output'); + assert.ok( + syncChunkIndex < asyncChunkIndex, + `sync content (chunk ${syncChunkIndex}) should stream before async content (chunk ${asyncChunkIndex})`, + ); + }); }); }); diff --git a/packages/astro/test/svg-deduplication.test.js b/packages/astro/test/svg-deduplication.test.ts similarity index 94% rename from packages/astro/test/svg-deduplication.test.js rename to packages/astro/test/svg-deduplication.test.ts index a3e69dfa3343..c52581e0c508 100644 --- a/packages/astro/test/svg-deduplication.test.js +++ b/packages/astro/test/svg-deduplication.test.ts @@ -2,11 +2,10 @@ import assert from 'node:assert/strict'; import { readdir } from 'node:fs/promises'; import { after, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from './test-utils.js'; describe('SVG Deduplication', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; describe('build', () => { before(async () => { @@ -21,7 +20,7 @@ describe('SVG Deduplication', () => { const distDir = new URL('./fixtures/svg-deduplication/dist/', import.meta.url); const assetsDir = new URL('./_astro/', distDir); - let svgFiles = []; + let svgFiles: string[] = []; try { const files = await readdir(assetsDir); svgFiles = files.filter((file) => file.endsWith('.svg')); @@ -73,8 +72,7 @@ describe('SVG Deduplication', () => { }); describe('dev', () => { - /** @type {import('./test-utils').DevServer} */ - let devServer; + let devServer: DevServer; before(async () => { fixture = await loadFixture({ diff --git a/packages/astro/test/test-adapter.js b/packages/astro/test/test-adapter.js index 31ddb6959328..1bf72be6a376 100644 --- a/packages/astro/test/test-adapter.js +++ b/packages/astro/test/test-adapter.js @@ -11,13 +11,13 @@ import { viteID } from '../dist/core/util.js'; * * @param {{ * provideAddress?: boolean; - * extendAdapter?: AstroAdapter; + * extendAdapter?: Partial; * setMiddlewareEntryPoint?: (middlewareEntryPoint: MiddlewareEntryPoint) => void; - * env: Record; + * env?: Record; * }} param0 * @returns {AstroIntegration} */ -export default function ({ +export default function testAdapter({ provideAddress = true, staticHeaders = false, extendAdapter, diff --git a/packages/astro/test/test-utils.js b/packages/astro/test/test-utils.js index 6a6930a7f9e5..2a480ea14e36 100644 --- a/packages/astro/test/test-utils.js +++ b/packages/astro/test/test-utils.js @@ -27,23 +27,32 @@ process.env.ASTRO_TELEMETRY_DISABLED = true; * @typedef {import('../src/cli/check/index').CheckPayload} CheckPayload * @typedef {import('http').IncomingMessage} NodeRequest * @typedef {import('http').ServerResponse} NodeResponse + * `RequestHandler` is defined in `@astrojs/node` so we cannot import it directly. + * See https://github.com/withastro/astro/blob/astro@6.0.0/packages/integrations/node/src/types.ts#L44-L50 + * @typedef {(req: NodeRequest, res: NodeResponse, next?: (err?: unknown) => void, locals?: object) => void | Promise} RequestHandler * + * `startServer` is defined in `@astrojs/node` so we cannot import it directly. + * See https://github.com/withastro/astro/blob/astro@6.0.0/packages/integrations/node/src/server.ts#L21 + * @typedef {PreviewServer & { server: import('http').Server }} AdapterServer + * @typedef {() => ({server: AdapterServer, stop: Promise})} AdapterStartServer * * @typedef {Object} Fixture - * @property {typeof build} build + * @property {(extraInlineConfig?: Parameters[0], options?: Parameters[1]) => Promise} build * @property {(url: string) => string} resolveUrl * @property {(path: string) => Promise} pathExists * @property {(url: string, opts?: Parameters[1]) => Promise} fetch * @property {(path: string) => Promise} readFile + * @property {(path: string) => Promise} readBuffer * @property {(path: string, updater: (content: string) => string, waitForNextWrite = true) => Promise<() => void>} editFile * @property {(path: string) => Promise} readdir * @property {(pattern: string) => Promise} glob * @property {(inlineConfig?: Parameters[0]) => ReturnType} startDevServer - * @property {typeof preview} preview + * @property {(extraInlineConfig?: Parameters[0]) => Promise} preview * @property {() => Promise} clean * @property {(streaming?: boolean) => Promise} loadTestAdapterApp * @property {(streaming?: boolean) => Promise} loadSelfAdapterApp - * @property {() => Promise<(req: NodeRequest, res: NodeResponse) => void>} loadNodeAdapterHandler + * @property {() => Promise<{ handler: RequestHandler; startServer: AdapterStartServer }>} loadAdapterEntryModule + * @property {() => Promise} loadNodeAdapterHandler * @property {(timeout?: number) => Promise} onNextDataStoreChange * @property {typeof check} check * @property {typeof sync} sync @@ -244,6 +253,9 @@ export async function loadFixture(inlineConfig) { new URL(filePath.replace(/^\//, ''), config.outDir), encoding === undefined ? 'utf8' : encoding, ), + readBuffer: (filePath) => { + return fs.promises.readFile(new URL(filePath.replace(/^\//, ''), config.outDir)); + }, readdir: (fp) => fs.promises.readdir(new URL(fp.replace(/^\//, ''), config.outDir)), glob: (p) => glob(p, { diff --git a/packages/astro/test/types/schemas.ts b/packages/astro/test/types/schemas.ts index 5059f763b167..3f84ba2a947a 100644 --- a/packages/astro/test/types/schemas.ts +++ b/packages/astro/test/types/schemas.ts @@ -1,9 +1,9 @@ import { describe, it } from 'node:test'; import { expectTypeOf } from 'expect-type'; import type * as z from 'zod/v4'; -import { type FontProviderSchema, FontFamilySchema } from '../../src/assets/fonts/config.js'; -import type { FontProvider, FontFamily } from '../../src/assets/fonts/types.js'; -import type { CacheSchema, RouteRulesSchema } from '../../src/core/cache/config.js'; +import { type FontProviderSchema, FontFamilySchema } from '../../dist/assets/fonts/config.js'; +import type { FontProvider, FontFamily } from '../../dist/assets/fonts/types.js'; +import type { CacheSchema, RouteRulesSchema } from '../../dist/core/cache/config.js'; import type { CacheProviderConfig, RouteRules } from '../../dist/core/cache/types.js'; import type { SessionDriverConfigSchema } from '../../dist/core/session/config.js'; import type { SessionDriverConfig } from '../../dist/core/session/types.js'; diff --git a/packages/astro/test/types/tsconfig.json b/packages/astro/test/types/tsconfig.json index cc320a61e407..249c1cc68986 100644 --- a/packages/astro/test/types/tsconfig.json +++ b/packages/astro/test/types/tsconfig.json @@ -1,8 +1,8 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { + "composite": true, "allowJs": true, - "emitDeclarationOnly": false, - "noEmit": true + "emitDeclarationOnly": false } } diff --git a/packages/astro/test/units/_temp-fixtures/package.json b/packages/astro/test/units/_temp-fixtures/package.json deleted file mode 100644 index 3ecea0bfe38d..000000000000 --- a/packages/astro/test/units/_temp-fixtures/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "astro-temp-fixtures", - "description": "This directory contains nested directories of dynamically created unit test fixtures. The deps here can be used by them", - "dependencies": { - "@astrojs/mdx": "workspace:*", - "astro": "workspace:*" - } -} diff --git a/packages/astro/test/units/actions/action-error.test.js b/packages/astro/test/units/actions/action-error.test.ts similarity index 90% rename from packages/astro/test/units/actions/action-error.test.js rename to packages/astro/test/units/actions/action-error.test.ts index 5e506a3ddb0b..adb609f560a8 100644 --- a/packages/astro/test/units/actions/action-error.test.js +++ b/packages/astro/test/units/actions/action-error.test.ts @@ -1,4 +1,3 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { @@ -8,6 +7,7 @@ import { isActionError, isInputError, } from '../../../dist/actions/runtime/client.js'; +import type { ActionErrorCode } from '../../../dist/actions/runtime/types.js'; describe('ActionError', () => { it('sets code, status, and message from constructor', () => { @@ -42,7 +42,7 @@ describe('ActionError', () => { describe('ActionError.codeToStatus', () => { it('maps all known codes to correct HTTP status', () => { - for (const [code, status] of Object.entries(codeToStatusMap)) { + for (const [code, status] of Object.entries(codeToStatusMap) as [ActionErrorCode, number][]) { assert.equal(ActionError.codeToStatus(code), status, `Expected ${code} to map to ${status}`); } }); @@ -95,7 +95,7 @@ describe('ActionInputError', () => { { code: 'invalid_type', message: 'Expected string', path: ['name'] }, { code: 'too_small', message: 'Too short', path: ['name'] }, { code: 'invalid_type', message: 'Required', path: ['email'] }, - ]; + ] as unknown as ConstructorParameters[0]; const error = new ActionInputError(issues); assert.equal(error.code, 'BAD_REQUEST'); assert.equal(error.status, 400); @@ -111,7 +111,9 @@ describe('ActionInputError', () => { }); it('handles issues without paths', () => { - const issues = [{ code: 'custom', message: 'Something wrong', path: [] }]; + const issues = [ + { code: 'custom', message: 'Something wrong', path: [] }, + ] as unknown as ConstructorParameters[0]; const error = new ActionInputError(issues); assert.deepEqual(error.fields, {}); }); @@ -146,7 +148,9 @@ describe('isActionError', () => { describe('isInputError', () => { it('returns true for ActionInputError instances', () => { - const issues = [{ code: 'invalid_type', message: 'bad', path: ['x'] }]; + const issues = [ + { code: 'invalid_type', message: 'bad', path: ['x'] }, + ] as unknown as ConstructorParameters[0]; assert.equal(isInputError(new ActionInputError(issues)), true); }); diff --git a/packages/astro/test/units/actions/action-path.test.js b/packages/astro/test/units/actions/action-path.test.ts similarity index 99% rename from packages/astro/test/units/actions/action-path.test.js rename to packages/astro/test/units/actions/action-path.test.ts index 701cb375ba16..5e9c57133c3c 100644 --- a/packages/astro/test/units/actions/action-path.test.js +++ b/packages/astro/test/units/actions/action-path.test.ts @@ -1,4 +1,3 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { diff --git a/packages/astro/test/units/actions/action-status.test.js b/packages/astro/test/units/actions/action-status.test.ts similarity index 58% rename from packages/astro/test/units/actions/action-status.test.js rename to packages/astro/test/units/actions/action-status.test.ts index 802ab83b01f4..1167c927752e 100644 --- a/packages/astro/test/units/actions/action-status.test.js +++ b/packages/astro/test/units/actions/action-status.test.ts @@ -1,43 +1,26 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { createComponent, render } from '../../../dist/runtime/server/index.js'; import { serializeActionResult } from '../../../dist/actions/runtime/server.js'; -import { createTestApp, createPage } from '../mocks.js'; +import { ActionError } from '../../../dist/actions/runtime/client.js'; +import type { ActionErrorCode } from '../../../dist/actions/runtime/types.js'; +import { createTestApp, createPage } from '../mocks.ts'; -// Build locals with an _actionPayload to simulate an action having run. -// Mirrors the shape of ActionsLocals from src/actions/runtime/types.ts. -// We use serializeActionResult from the server runtime to produce -// properly-formatted payloads that deserializeActionResult can parse. - -// Minimal ActionError-compatible object — ActionError class is not exported from -// the dist client bundle so we construct the shape it expects directly. -function makeActionError(code, message = 'test error') { - const codeToStatus = { - BAD_REQUEST: 400, - UNPROCESSABLE_CONTENT: 422, - NOT_FOUND: 404, - UNAUTHORIZED: 401, - FORBIDDEN: 403, - INTERNAL_SERVER_ERROR: 500, - }; - return { type: 'AstroActionError', code, message, status: codeToStatus[code] ?? 500 }; -} - -function makeLocalsWithError(code) { - const actionResult = serializeActionResult({ error: makeActionError(code), data: undefined }); +function makeLocalsWithError(code: ActionErrorCode) { + const error = new ActionError({ code }); + const actionResult = serializeActionResult({ error, data: undefined }); return { _actionPayload: { actionName: 'testAction', actionResult } }; } -function makeLocalsWithData(data = null) { +function makeLocalsWithData(data: unknown = null) { const actionResult = serializeActionResult({ data, error: undefined }); return { _actionPayload: { actionName: 'testAction', actionResult } }; } describe('action result status computation', () => { it('uses default status when no action payload is present', async () => { - let capturedStatus; - const page = createComponent((result, props, slots) => { + let capturedStatus: number | undefined; + const page = createComponent((result: any, props: any, slots: any) => { const Astro = result.createAstro(props, slots); capturedStatus = Astro.response.status; return render`

ok

`; @@ -50,8 +33,8 @@ describe('action result status computation', () => { }); it('uses the error status code when an action error result is in locals', async () => { - let capturedStatus; - const page = createComponent((result, props, slots) => { + let capturedStatus: number | undefined; + const page = createComponent((result: any, props: any, slots: any) => { const Astro = result.createAstro(props, slots); capturedStatus = Astro.response.status; return render`

ok

`; @@ -60,15 +43,14 @@ describe('action result status computation', () => { const app = createTestApp([createPage(page, { route: '/test', prerender: false })]); const request = new Request('http://example.com/test'); - // Simulate middleware having set the action payload on locals await app.render(request, { locals: makeLocalsWithError('UNPROCESSABLE_CONTENT') }); assert.equal(capturedStatus, 422); }); it('uses default status for a successful action data result', async () => { - let capturedStatus; - const page = createComponent((result, props, slots) => { + let capturedStatus: number | undefined; + const page = createComponent((result: any, props: any, slots: any) => { const Astro = result.createAstro(props, slots); capturedStatus = Astro.response.status; return render`

ok

`; diff --git a/packages/astro/test/units/actions/actions-app.test.js b/packages/astro/test/units/actions/actions-app.test.ts similarity index 95% rename from packages/astro/test/units/actions/actions-app.test.js rename to packages/astro/test/units/actions/actions-app.test.ts index d70eaaaecde4..2e13925c953a 100644 --- a/packages/astro/test/units/actions/actions-app.test.js +++ b/packages/astro/test/units/actions/actions-app.test.ts @@ -1,4 +1,3 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import * as devalue from 'devalue'; @@ -6,12 +5,13 @@ import { z } from 'zod'; import { defineAction } from '../../../dist/actions/runtime/server.js'; import { ActionError } from '../../../dist/actions/runtime/client.js'; import { createComponent, render } from '../../../dist/runtime/server/index.js'; -import { createTestApp, createPage, createRouteData } from '../mocks.js'; -import { spreadPart, staticPart } from '../routing/test-helpers.js'; +import { createTestApp, createPage, createRouteData } from '../mocks.ts'; +import { spreadPart, staticPart } from '../routing/test-helpers.ts'; +import type { RouteData } from '../../../dist/types/public/internal.js'; const noopPage = createComponent(() => render``); -const actionRouteData = createRouteData({ +const actionRouteData: RouteData = createRouteData({ route: '/_actions/[...path]', type: 'endpoint', component: 'astro/actions/runtime/entrypoints/route.js', @@ -19,22 +19,19 @@ const actionRouteData = createRouteData({ pathname: undefined, }); -/** - * Creates an App wired up with action handlers at `/_actions/[...path]`. - * - * @param {Record} serverActions - The `server` export from an actions file - * @param {object} [options] - * @param {number} [options.actionBodySizeLimit] - */ -function createActionsApp(serverActions, options = {}) { +function createActionsApp( + serverActions: Record>, + options: { actionBodySizeLimit?: number } = {}, +) { return createTestApp( [ createPage(noopPage, { route: '/test' }), { routeData: actionRouteData, - module: async () => ({ + // The action entrypoint isn't a page component, but App routes it by matching. + module: (async () => ({ page: () => import('../../../dist/actions/runtime/entrypoints/route.js'), - }), + })) as any, }, ], { diff --git a/packages/astro/test/units/actions/actions-proxy.test.js b/packages/astro/test/units/actions/actions-proxy.test.ts similarity index 84% rename from packages/astro/test/units/actions/actions-proxy.test.js rename to packages/astro/test/units/actions/actions-proxy.test.ts index 7b6f0917e2a7..a8fa5c4fabd7 100644 --- a/packages/astro/test/units/actions/actions-proxy.test.js +++ b/packages/astro/test/units/actions/actions-proxy.test.ts @@ -1,19 +1,26 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; +import type { APIContext } from '../../../dist/types/public/context.js'; +import type { SafeResult } from '../../../dist/actions/runtime/types.js'; import { createActionsProxy, ActionError } from '../../../dist/actions/runtime/client.js'; -/** - * Creates a proxy with a spy handleAction that records calls and returns a configurable result. - * @param {object} [opts] - * @param {import('../../../dist/actions/runtime/client.js').SafeResult} [opts.result] - */ -function setup(opts = {}) { - const result = opts.result ?? { data: 'ok', error: undefined }; - /** @type {{ param: any; path: string; context: any }[]} */ - const calls = []; - - const handleAction = async (param, path, context) => { +// #region Helpers + +interface SetupOptions { + result?: SafeResult; +} + +interface CallRecord { + param: unknown; + path: string; + context: APIContext | undefined; +} + +function setup(opts: SetupOptions = {}) { + const result: SafeResult = opts.result ?? { data: 'ok', error: undefined }; + const calls: CallRecord[] = []; + + const handleAction = async (param: unknown, path: string, context: APIContext | undefined) => { calls.push({ param, path, context }); return result; }; @@ -22,6 +29,10 @@ function setup(opts = {}) { return { proxy, calls }; } +// #endregion + +// #region Tests + describe('createActionsProxy', () => { describe('path building', () => { it('builds a top-level path from property access', async () => { @@ -121,3 +132,5 @@ describe('createActionsProxy', () => { }); }); }); + +// #endregion diff --git a/packages/astro/test/units/actions/form-data-to-object.test.js b/packages/astro/test/units/actions/form-data-to-object.test.ts similarity index 98% rename from packages/astro/test/units/actions/form-data-to-object.test.js rename to packages/astro/test/units/actions/form-data-to-object.test.ts index c3a2978615f9..71163a67080d 100644 --- a/packages/astro/test/units/actions/form-data-to-object.test.js +++ b/packages/astro/test/units/actions/form-data-to-object.test.ts @@ -40,14 +40,14 @@ describe('formDataToObject', () => { }); const res = formDataToObject(formData, input); - assert.ok(isNaN(res.age)); + assert.ok(isNaN(res.age as number)); }); it('should handle boolean checks', () => { const formData = new FormData(); formData.set('isCool', 'yes'); - formData.set('isTrue', true); - formData.set('isFalse', false); + formData.set('isTrue', String(true)); + formData.set('isFalse', String(false)); formData.set('falseString', 'false'); const input = z.object({ diff --git a/packages/astro/test/units/actions/serialize.test.js b/packages/astro/test/units/actions/serialize.test.ts similarity index 95% rename from packages/astro/test/units/actions/serialize.test.js rename to packages/astro/test/units/actions/serialize.test.ts index 853835379d68..3d7d5146a861 100644 --- a/packages/astro/test/units/actions/serialize.test.js +++ b/packages/astro/test/units/actions/serialize.test.ts @@ -1,4 +1,3 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import * as devalue from 'devalue'; @@ -8,6 +7,7 @@ import { ActionInputError, deserializeActionResult, } from '../../../dist/actions/runtime/client.js'; +import type { ActionErrorCode } from '../../../dist/actions/runtime/types.js'; describe('serializeActionResult', () => { describe('data results', () => { @@ -89,7 +89,8 @@ describe('serializeActionResult', () => { const result = serializeActionResult({ data: undefined, error: undefined }); assert.equal(result.type, 'empty'); assert.equal(result.status, 204); - assert.equal(result.body, undefined); + // The 'empty' variant has no body field — verify it's absent at runtime + assert.equal('body' in result ? result.body : undefined, undefined); }); }); @@ -110,7 +111,7 @@ describe('serializeActionResult', () => { it('serializes an ActionInputError with issues and fields', () => { const issues = [ { code: 'invalid_type', expected: 'string', message: 'Required', path: ['comment'] }, - ]; + ] as unknown as ConstructorParameters[0]; const error = new ActionInputError(issues); const result = serializeActionResult({ data: undefined, error }); assert.equal(result.type, 'error'); @@ -125,7 +126,7 @@ describe('serializeActionResult', () => { }); it('uses correct status for different error codes', () => { - const codes = [ + const codes: [ActionErrorCode, number][] = [ ['BAD_REQUEST', 400], ['NOT_FOUND', 404], ['INTERNAL_SERVER_ERROR', 500], @@ -183,7 +184,7 @@ describe('deserializeActionResult', () => { it('deserializes an ActionInputError result', () => { const issues = [ { code: 'invalid_type', expected: 'string', message: 'Required', path: ['name'] }, - ]; + ] as unknown as ConstructorParameters[0]; const serialized = serializeActionResult({ data: undefined, error: new ActionInputError(issues), diff --git a/packages/astro/test/units/app/astro-attrs.test.js b/packages/astro/test/units/app/astro-attrs.test.ts similarity index 95% rename from packages/astro/test/units/app/astro-attrs.test.js rename to packages/astro/test/units/app/astro-attrs.test.ts index e6f85d877a6c..8e50a9a84339 100644 --- a/packages/astro/test/units/app/astro-attrs.test.js +++ b/packages/astro/test/units/app/astro-attrs.test.ts @@ -1,4 +1,3 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { App } from '../../../dist/core/app/app.js'; @@ -10,7 +9,7 @@ import { addAttribute, } from '../../../dist/runtime/server/index.js'; import * as cheerio from 'cheerio'; -import { createManifest, createRouteInfo } from './test-helpers.js'; +import { createManifest, createRouteInfo } from './test-helpers.ts'; const attributesRouteData = { route: '/attributes', @@ -20,11 +19,11 @@ const attributesRouteData = { distURL: [], pattern: /^\/attributes\/?$/, segments: [[{ content: 'attributes', dynamic: false, spread: false }]], - type: 'page', + type: 'page' as const, prerender: false, fallbackRoutes: [], isIndex: false, - origin: 'project', + origin: 'project' as const, }; const attributesNamespacedRouteData = { @@ -35,11 +34,11 @@ const attributesNamespacedRouteData = { distURL: [], pattern: /^\/namespaced\/?$/, segments: [[{ content: 'namespaced', dynamic: false, spread: false }]], - type: 'page', + type: 'page' as const, prerender: false, fallbackRoutes: [], isIndex: false, - origin: 'project', + origin: 'project' as const, }; const attributesNamespacedComponentRouteData = { @@ -50,11 +49,11 @@ const attributesNamespacedComponentRouteData = { distURL: [], pattern: /^\/namespaced-component\/?$/, segments: [[{ content: 'namespaced-component', dynamic: false, spread: false }]], - type: 'page', + type: 'page' as const, prerender: false, fallbackRoutes: [], isIndex: false, - origin: 'project', + origin: 'project' as const, }; const attributesPage = createComponent(() => { @@ -120,7 +119,7 @@ const attributesNamespacedPage = createComponent(() => { `; }); -const namespacedSpanComponent = createComponent((result, props, slots) => { +const namespacedSpanComponent = createComponent((result: any, props: any, slots: any) => { const Astro = result.createAstro(props, slots); return render` @@ -128,10 +127,12 @@ const namespacedSpanComponent = createComponent((result, props, slots) => { `; }); -const attributesNamespacedComponentPage = createComponent((result) => { - return render`${renderComponent(result, 'NamespacedSpan', namespacedSpanComponent, { +const attributesNamespacedComponentPage = createComponent((result: any) => { + const onClick: (e: unknown) => void = // biome-ignore lint/suspicious/noConsole: allowed - 'on:click': /** @type {(e: unknown) => void} */ (event) => console.log(event), + (event) => console.log(event); + return render`${renderComponent(result, 'NamespacedSpan', namespacedSpanComponent, { + 'on:click': onClick, })}`; }); @@ -164,16 +165,20 @@ const pageMap = new Map([ const app = new App( createManifest({ - // @ts-expect-error routes prop is not yet type-defined routes: [ createRouteInfo(attributesRouteData), createRouteInfo(attributesNamespacedRouteData), createRouteInfo(attributesNamespacedComponentRouteData), ], - pageMap, - }), + pageMap: pageMap as any, + }) as any, ); +interface TestAttribute { + attribute: string; + value: string | undefined; +} + describe('Attributes', async () => { it('Passes attributes to elements as expected', async () => { const request = new Request('http://example.com/attributes'); @@ -181,14 +186,7 @@ describe('Attributes', async () => { const html = await response.text(); const $ = cheerio.load(html); - /** - * @typedef {Object} TestAttribute - * @property {string} attribute - * @property {string | undefined} value - */ - - /** @type {Record} */ - const attrs = { + const attrs: Record = { 'download-true': { attribute: 'download', value: '' }, 'download-false': { attribute: 'download', value: undefined }, 'download-undefined': { attribute: 'download', value: undefined }, diff --git a/packages/astro/test/units/app/astro-response.test.js b/packages/astro/test/units/app/astro-response.test.ts similarity index 90% rename from packages/astro/test/units/app/astro-response.test.js rename to packages/astro/test/units/app/astro-response.test.ts index 0e619bb9e937..35df5a539d8e 100644 --- a/packages/astro/test/units/app/astro-response.test.js +++ b/packages/astro/test/units/app/astro-response.test.ts @@ -1,9 +1,8 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { App } from '../../../dist/core/app/app.js'; import { createComponent, render } from '../../../dist/runtime/server/index.js'; -import { createManifest, createRouteInfo } from './test-helpers.js'; +import { createManifest, createRouteInfo } from './test-helpers.ts'; const notFoundRouteData = { route: '/not-found', @@ -13,11 +12,11 @@ const notFoundRouteData = { distURL: [], pattern: /^\/not-found\/?$/, segments: [[{ content: 'not-found', dynamic: false, spread: false }]], - type: 'page', + type: 'page' as const, prerender: false, fallbackRoutes: [], isIndex: false, - origin: 'project', + origin: 'project' as const, }; const notFoundCustomRouteData = { @@ -28,11 +27,11 @@ const notFoundCustomRouteData = { distURL: [], pattern: /^\/not-found-custom\/?$/, segments: [[{ content: 'not-found-custom', dynamic: false, spread: false }]], - type: 'page', + type: 'page' as const, prerender: false, fallbackRoutes: [], isIndex: false, - origin: 'project', + origin: 'project' as const, }; const notFoundPage = createComponent(() => { @@ -42,7 +41,7 @@ const notFoundPage = createComponent(() => { }); }); -const notFoundCustomPage = createComponent((result, props, slots) => { +const notFoundCustomPage = createComponent((result: any, props: any, slots: any) => { const Astro = result.createAstro(props, slots); Astro.response.status = 404; return render`
Custom 404
`; @@ -70,8 +69,8 @@ const pageMap = new Map([ const app = new App( createManifest({ routes: [createRouteInfo(notFoundRouteData), createRouteInfo(notFoundCustomRouteData)], - pageMap, - }), + pageMap: pageMap as any, + }) as any, ); describe('Returning responses', () => { diff --git a/packages/astro/test/units/app/csrf.test.js b/packages/astro/test/units/app/csrf.test.ts similarity index 96% rename from packages/astro/test/units/app/csrf.test.js rename to packages/astro/test/units/app/csrf.test.ts index db70762e29ce..ba00e57b78a7 100644 --- a/packages/astro/test/units/app/csrf.test.js +++ b/packages/astro/test/units/app/csrf.test.ts @@ -1,4 +1,3 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { @@ -6,7 +5,7 @@ import { createOriginCheckMiddleware, } from '../../../dist/core/app/middlewares.js'; import { callMiddleware } from '../../../dist/core/middleware/callMiddleware.js'; -import { createMockAPIContext, createResponseFunction } from '../mocks.js'; +import { createMockAPIContext, createResponseFunction } from '../mocks.ts'; describe('CSRF - hasFormLikeHeader', () => { it('returns true for multipart/form-data', () => { @@ -53,14 +52,17 @@ describe('CSRF - createOriginCheckMiddleware', () => { const middleware = createOriginCheckMiddleware(); const responseFn = createResponseFunction('ok'); - /** - * @param {object} opts - * @param {string} opts.method - * @param {string} opts.url - * @param {Record} [opts.headers] - * @param {boolean} [opts.isPrerendered] - */ - function callCSRF({ method, url, headers = {}, isPrerendered = false }) { + function callCSRF({ + method, + url, + headers = {}, + isPrerendered = false, + }: { + method: string; + url: string; + headers?: Record; + isPrerendered?: boolean; + }) { const request = new Request(url, { method, headers }); const ctx = createMockAPIContext({ request, url: new URL(url), isPrerendered }); return callMiddleware(middleware, ctx, responseFn); diff --git a/packages/astro/test/units/app/dev-url-construction.test.js b/packages/astro/test/units/app/dev-url-construction.test.ts similarity index 92% rename from packages/astro/test/units/app/dev-url-construction.test.js rename to packages/astro/test/units/app/dev-url-construction.test.ts index c273991369c8..ab752b98e2ce 100644 --- a/packages/astro/test/units/app/dev-url-construction.test.js +++ b/packages/astro/test/units/app/dev-url-construction.test.ts @@ -1,22 +1,22 @@ import * as assert from 'node:assert/strict'; import { describe, it } from 'node:test'; +import type { SSRManifest } from '../../../dist/core/app/types.js'; import { getFirstForwardedValue, validateForwardedHeaders, } from '../../../dist/core/app/validate-headers.js'; -/** - * Mirrors the URL construction logic in AstroServerApp.handleRequest so that - * the protocol and host derivation can be exercised in isolation. - * - * @param {object} opts - * @param {Record} opts.headers - Incoming request headers - * @param {boolean} [opts.isHttps=false] - Whether Vite itself is running TLS - * @param {import('../../../dist/core/app/types.js').SSRManifest['allowedDomains']} [opts.allowedDomains] - * @param {string} [opts.requestUrl='/'] - * @returns {URL} - */ -function buildDevUrl({ headers, isHttps = false, allowedDomains, requestUrl = '/' }) { +function buildDevUrl({ + headers, + isHttps = false, + allowedDomains, + requestUrl = '/', +}: { + headers: Record; + isHttps?: boolean; + allowedDomains?: SSRManifest['allowedDomains']; + requestUrl?: string; +}): URL { const validated = validateForwardedHeaders( getFirstForwardedValue(headers['x-forwarded-proto']), getFirstForwardedValue(headers['x-forwarded-host']), diff --git a/packages/astro/test/units/app/double-slash-bypass.test.js b/packages/astro/test/units/app/double-slash-bypass.test.ts similarity index 77% rename from packages/astro/test/units/app/double-slash-bypass.test.js rename to packages/astro/test/units/app/double-slash-bypass.test.ts index f5defd12b491..3e73de93c750 100644 --- a/packages/astro/test/units/app/double-slash-bypass.test.js +++ b/packages/astro/test/units/app/double-slash-bypass.test.ts @@ -1,10 +1,10 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; +import type { MiddlewareHandler } from '../../../dist/types/public/common.js'; import { App } from '../../../dist/core/app/app.js'; import { parseRoute } from '../../../dist/core/routing/parse-route.js'; import { createComponent, render } from '../../../dist/runtime/server/index.js'; -import { createManifest } from './test-helpers.js'; +import { createManifest, createRouteInfo } from './test-helpers.ts'; /** * Security tests for double-slash URL prefix middleware authorization bypass. @@ -21,12 +21,10 @@ import { createManifest } from './test-helpers.js'; * CWE-285: Improper Authorization */ -const routeOptions = /** @type {Parameters[1]} */ ( - /** @type {any} */ ({ - config: { base: '/', trailingSlash: 'ignore' }, - pageExtensions: [], - }) -); +const routeOptions: Parameters[1] = { + config: { base: '/', trailingSlash: 'ignore' }, + pageExtensions: [], +} as any; const adminRouteData = parseRoute('admin', routeOptions, { component: 'src/pages/admin.astro', @@ -40,15 +38,15 @@ const publicRouteData = parseRoute('index.astro', routeOptions, { component: 'src/pages/index.astro', }); -const adminPage = createComponent(() => { +const adminPage = createComponent((_result: any, _props: any, _slots: any) => { return render`

Admin Panel

`; }); -const dashboardPage = createComponent(() => { +const dashboardPage = createComponent((_result: any, _props: any, _slots: any) => { return render`

Dashboard

`; }); -const publicPage = createComponent(() => { +const publicPage = createComponent((_result: any, _props: any, _slots: any) => { return render`

Public

`; }); @@ -82,36 +80,30 @@ const pageMap = new Map([ /** * Middleware that blocks access to /admin and /dashboard routes, * as recommended in the official Astro authentication docs. - * @returns {() => Promise<{onRequest: import('../../../dist/types/public/common.js').MiddlewareHandler}>} */ function createAuthMiddleware() { - return async () => ({ - onRequest: /** @type {import('../../../dist/types/public/common.js').MiddlewareHandler} */ ( - async (context, next) => { - const protectedPaths = ['/admin', '/dashboard']; - if (protectedPaths.some((p) => context.url.pathname.startsWith(p))) { - return new Response('Forbidden', { status: 403 }); - } - return next(); + return (async () => ({ + onRequest: (async (context, next) => { + const protectedPaths = ['/admin', '/dashboard']; + if (protectedPaths.some((p) => context.url.pathname.startsWith(p))) { + return new Response('Forbidden', { status: 403 }); } - ), - }); + return next(); + }) satisfies MiddlewareHandler, + })) as () => Promise<{ onRequest: MiddlewareHandler }>; } -/** - * @param {ReturnType} middleware - */ -function createApp(middleware) { +function createApp(middleware: ReturnType) { return new App( createManifest({ routes: [ - { routeData: adminRouteData }, - { routeData: dashboardRouteData }, - { routeData: publicRouteData }, + createRouteInfo(adminRouteData), + createRouteInfo(dashboardRouteData), + createRouteInfo(publicRouteData), ], - pageMap, - middleware, - }), + pageMap: pageMap as any, + middleware: middleware as any, + }) as any, ); } diff --git a/packages/astro/test/units/app/encoded-backslash-bypass.test.js b/packages/astro/test/units/app/encoded-backslash-bypass.test.ts similarity index 78% rename from packages/astro/test/units/app/encoded-backslash-bypass.test.js rename to packages/astro/test/units/app/encoded-backslash-bypass.test.ts index fcf6c3e56d82..cbb5a5dfb458 100644 --- a/packages/astro/test/units/app/encoded-backslash-bypass.test.js +++ b/packages/astro/test/units/app/encoded-backslash-bypass.test.ts @@ -1,10 +1,10 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { App } from '../../../dist/core/app/app.js'; import { parseRoute } from '../../../dist/core/routing/parse-route.js'; import { createComponent, render } from '../../../dist/runtime/server/index.js'; -import { createManifest } from './test-helpers.js'; +import type { MiddlewareHandler } from '../../../dist/types/public/common.js'; +import { createManifest, createRouteInfo } from './test-helpers.ts'; /** * Tests that encoded backslash characters (%5C) in URL paths do not cause @@ -15,12 +15,10 @@ import { createManifest } from './test-helpers.js'; * The middleware then sees a different path than what the router matched. */ -const routeOptions = /** @type {Parameters[1]} */ ( - /** @type {any} */ ({ - config: { base: '/', trailingSlash: 'ignore' }, - pageExtensions: [], - }) -); +const routeOptions: Parameters[1] = { + config: { base: '/', trailingSlash: 'ignore' }, + pageExtensions: [], +} as any; // Dynamic route: /users/[slug] const userSlugRouteData = parseRoute('users/[slug]', routeOptions, { @@ -31,7 +29,7 @@ const publicRouteData = parseRoute('index.astro', routeOptions, { component: 'src/pages/index.astro', }); -const page = createComponent(() => { +const page = createComponent((_result: any, _props: any, _slots: any) => { return render`

Page

`; }); @@ -50,25 +48,22 @@ const pageMap = new Map([ * Middleware that blocks access to /users/admin path, * simulating authorization checks on dynamic routes. */ -const middleware = - /** @type {() => Promise<{onRequest: import('../../../dist/types/public/common.js').MiddlewareHandler}>} */ ( - async () => ({ - onRequest: async (context, next) => { - const pathname = context.url.pathname; - if (pathname === '/users/admin' || pathname.startsWith('/users/admin/')) { - return new Response('Forbidden', { status: 403 }); - } - return next(); - }, - }) - ); +const middleware = (async () => ({ + onRequest: (async (context, next) => { + const pathname = context.url.pathname; + if (pathname === '/users/admin' || pathname.startsWith('/users/admin/')) { + return new Response('Forbidden', { status: 403 }); + } + return next(); + }) satisfies MiddlewareHandler, +})) as () => Promise<{ onRequest: MiddlewareHandler }>; const app = new App( createManifest({ - routes: [{ routeData: userSlugRouteData }, { routeData: publicRouteData }], - pageMap, - middleware, - }), + routes: [createRouteInfo(userSlugRouteData), createRouteInfo(publicRouteData)], + pageMap: pageMap as any, + middleware: middleware as any, + }) as any, ); describe('URL normalization: encoded backslash handling in pathname', () => { diff --git a/packages/astro/test/units/app/error-pages.test.js b/packages/astro/test/units/app/error-pages.test.ts similarity index 80% rename from packages/astro/test/units/app/error-pages.test.js rename to packages/astro/test/units/app/error-pages.test.ts index 0249f47ffe41..6adb8358dba0 100644 --- a/packages/astro/test/units/app/error-pages.test.js +++ b/packages/astro/test/units/app/error-pages.test.ts @@ -1,13 +1,22 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { App } from '../../../dist/core/app/app.js'; +import type { RouteData } from '../../../dist/types/public/internal.js'; +import type { SSRManifest } from '../../../dist/core/app/types.js'; import { createComponent, maybeRenderHead, render } from '../../../dist/runtime/server/index.js'; -import { createManifest } from './test-helpers.js'; +import { createManifest } from './test-helpers.ts'; + +function makeRouteData(partial: Omit): RouteData { + return partial as RouteData; +} + +function makeApp(opts: Record): App { + return new App(createManifest(opts as any) as unknown as SSRManifest); +} describe('App render error pages', () => { it('preserves headers and body for 500 responses from routes', async () => { - const routeData = { + const routeData = makeRouteData({ route: '/[...slug]', component: 'src/pages/[...slug].astro', params: ['...slug'], @@ -20,7 +29,7 @@ describe('App render error pages', () => { fallbackRoutes: [], isIndex: false, origin: 'project', - }; + }); const pageMap = new Map([ [ @@ -39,7 +48,7 @@ describe('App render error pages', () => { ], ]); - const app = new App(createManifest({ routes: [{ routeData }], pageMap })); + const app = makeApp({ routes: [{ routeData }], pageMap }); const request = new Request('http://example.com/any'); const response = await app.render(request, { routeData }); @@ -53,7 +62,7 @@ describe('App render error pages', () => { }); it('renders the 404 page when an API route lacks a handler for the request method', async () => { - const apiRouteData = { + const apiRouteData = makeRouteData({ route: '/api/route', component: 'src/pages/api/route.js', params: [], @@ -69,9 +78,9 @@ describe('App render error pages', () => { fallbackRoutes: [], isIndex: false, origin: 'project', - }; + }); - const notFoundRouteData = { + const notFoundRouteData = makeRouteData({ route: '/404', component: 'src/pages/404.astro', params: [], @@ -84,13 +93,13 @@ describe('App render error pages', () => { fallbackRoutes: [], isIndex: false, origin: 'project', - }; + }); - const notFoundPage = createComponent((_result) => { + const notFoundPage = createComponent((_result: any, _props: any, _slots: any) => { return render`

Something went horribly wrong!

`; }); - const pageMap = new Map([ + const pageMap = new Map([ [ apiRouteData.component, async () => ({ @@ -109,12 +118,10 @@ describe('App render error pages', () => { ], ]); - const app = new App( - createManifest({ - routes: [{ routeData: apiRouteData }, { routeData: notFoundRouteData }], - pageMap, - }), - ); + const app = makeApp({ + routes: [{ routeData: apiRouteData }, { routeData: notFoundRouteData }], + pageMap, + }); const request = new Request('http://example.com/api/route', { method: 'PUT' }); const response = await app.render(request, { routeData: apiRouteData }); @@ -123,7 +130,7 @@ describe('App render error pages', () => { }); it('renders the 404 page when a route does not match', async () => { - const notFoundRouteData = { + const notFoundRouteData = makeRouteData({ route: '/404', component: 'src/pages/404.astro', params: [], @@ -136,7 +143,7 @@ describe('App render error pages', () => { fallbackRoutes: [], isIndex: false, origin: 'project', - }; + }); const notFoundPage = createComponent(() => { return render`

Something went horribly wrong!

`; @@ -153,7 +160,7 @@ describe('App render error pages', () => { ], ]); - const app = new App(createManifest({ routes: [{ routeData: notFoundRouteData }], pageMap })); + const app = makeApp({ routes: [{ routeData: notFoundRouteData }], pageMap }); const request = new Request('http://example.com/some/fake/route'); const response = await app.render(request); @@ -162,7 +169,7 @@ describe('App render error pages', () => { }); it('renders the 404 page when a route does not match and routeData is provided', async () => { - const notFoundRouteData = { + const notFoundRouteData = makeRouteData({ route: '/404', component: 'src/pages/404.astro', params: [], @@ -175,7 +182,7 @@ describe('App render error pages', () => { fallbackRoutes: [], isIndex: false, origin: 'project', - }; + }); const notFoundPage = createComponent(() => { return render`

Something went horribly wrong!

`; @@ -192,7 +199,7 @@ describe('App render error pages', () => { ], ]); - const app = new App(createManifest({ routes: [{ routeData: notFoundRouteData }], pageMap })); + const app = makeApp({ routes: [{ routeData: notFoundRouteData }], pageMap }); const request = new Request('http://example.com/some/fake/route'); const routeData = app.match(request); const response = await app.render(request, { routeData }); @@ -202,7 +209,7 @@ describe('App render error pages', () => { }); it('renders the 404 page with imports when a matching route returns 404', async () => { - const blogRouteData = { + const blogRouteData = makeRouteData({ route: '/blog/[...ssrPath]', component: 'src/pages/blog/[...ssrPath].astro', params: ['...ssrPath'], @@ -218,9 +225,9 @@ describe('App render error pages', () => { fallbackRoutes: [], isIndex: false, origin: 'project', - }; + }); - const notFoundRouteData = { + const notFoundRouteData = makeRouteData({ route: '/404', component: 'src/pages/404.astro', params: [], @@ -233,10 +240,10 @@ describe('App render error pages', () => { fallbackRoutes: [], isIndex: false, origin: 'project', - }; + }); - const notFoundPage = createComponent((result) => { - return render`${maybeRenderHead(result)}

Something went horribly wrong!

`; + const notFoundPage = createComponent((result: any, _props: any, _slots: any) => { + return render`${(maybeRenderHead as any)(result)}

Something went horribly wrong!

`; }); const pageMap = new Map([ @@ -259,18 +266,16 @@ describe('App render error pages', () => { ], ]); - const app = new App( - createManifest({ - routes: [ - { routeData: blogRouteData }, - { - routeData: notFoundRouteData, - styles: [{ type: 'external', src: '/main.css' }], - }, - ], - pageMap, - }), - ); + const app = makeApp({ + routes: [ + { routeData: blogRouteData }, + { + routeData: notFoundRouteData, + styles: [{ type: 'external', src: '/main.css' }], + }, + ], + pageMap, + }); const request = new Request('http://example.com/blog/fake/route'); const routeData = app.match(request); const response = await app.render(request, { routeData }); @@ -282,7 +287,7 @@ describe('App render error pages', () => { }); it('renders the 500 page when a route throws an error', async () => { - const errorRouteData = { + const errorRouteData = makeRouteData({ route: '/causes-error', component: 'src/pages/causes-error.astro', params: [], @@ -295,9 +300,9 @@ describe('App render error pages', () => { fallbackRoutes: [], isIndex: false, origin: 'project', - }; + }); - const internalErrorRouteData = { + const internalErrorRouteData = makeRouteData({ route: '/500', component: 'src/pages/500.astro', params: [], @@ -310,7 +315,7 @@ describe('App render error pages', () => { fallbackRoutes: [], isIndex: false, origin: 'project', - }; + }); const internalErrorPage = createComponent(() => { return render`

This is an error page

`; @@ -337,12 +342,10 @@ describe('App render error pages', () => { ], ]); - const app = new App( - createManifest({ - routes: [{ routeData: errorRouteData }, { routeData: internalErrorRouteData }], - pageMap, - }), - ); + const app = makeApp({ + routes: [{ routeData: errorRouteData }, { routeData: internalErrorRouteData }], + pageMap, + }); const request = new Request('http://example.com/causes-error'); const response = await app.render(request, { routeData: errorRouteData }); @@ -351,7 +354,7 @@ describe('App render error pages', () => { }); it('renders the 404 page when an API route lacks a handler in production', async () => { - const apiRouteData = { + const apiRouteData = makeRouteData({ route: '/api/route', component: 'src/pages/api/route.js', params: [], @@ -367,9 +370,9 @@ describe('App render error pages', () => { fallbackRoutes: [], isIndex: false, origin: 'project', - }; + }); - const notFoundRouteData = { + const notFoundRouteData = makeRouteData({ route: '/404', component: 'src/pages/404.astro', params: [], @@ -382,13 +385,13 @@ describe('App render error pages', () => { fallbackRoutes: [], isIndex: false, origin: 'project', - }; + }); - const notFoundPage = createComponent((result) => { - return render`${maybeRenderHead(result)}

Something went horribly wrong!

`; + const notFoundPage = createComponent((result: any, _props: any, _slots: any) => { + return render`${(maybeRenderHead as any)(result)}

Something went horribly wrong!

`; }); - const pageMap = new Map([ + const pageMap = new Map([ [ apiRouteData.component, async () => ({ @@ -407,12 +410,10 @@ describe('App render error pages', () => { ], ]); - const app = new App( - createManifest({ - routes: [{ routeData: apiRouteData }, { routeData: notFoundRouteData }], - pageMap, - }), - ); + const app = makeApp({ + routes: [{ routeData: apiRouteData }, { routeData: notFoundRouteData }], + pageMap, + }); const request = new Request('http://example.com/api/route', { method: 'PUT' }); const response = await app.render(request); @@ -421,7 +422,7 @@ describe('App render error pages', () => { }); it('renders the 404 page when a route does not match with trailingSlash always', async () => { - const notFoundRouteData = { + const notFoundRouteData = makeRouteData({ route: '/404', component: 'src/pages/404.astro', params: [], @@ -434,7 +435,7 @@ describe('App render error pages', () => { fallbackRoutes: [], isIndex: false, origin: 'project', - }; + }); const notFoundPage = createComponent(() => { return render`

Something went horribly wrong!

`; @@ -451,13 +452,11 @@ describe('App render error pages', () => { ], ]); - const app = new App( - createManifest({ - routes: [{ routeData: notFoundRouteData }], - pageMap, - trailingSlash: 'always', - }), - ); + const app = makeApp({ + routes: [{ routeData: notFoundRouteData }], + pageMap, + trailingSlash: 'always', + }); const request = new Request('http://example.com/ajksalscla/'); const response = await app.render(request); @@ -466,7 +465,7 @@ describe('App render error pages', () => { }); it('renders the 404 page when a route does not match with trailingSlash always and routeData', async () => { - const notFoundRouteData = { + const notFoundRouteData = makeRouteData({ route: '/404', component: 'src/pages/404.astro', params: [], @@ -479,7 +478,7 @@ describe('App render error pages', () => { fallbackRoutes: [], isIndex: false, origin: 'project', - }; + }); const notFoundPage = createComponent(() => { return render`

Something went horribly wrong!

`; @@ -496,13 +495,11 @@ describe('App render error pages', () => { ], ]); - const app = new App( - createManifest({ - routes: [{ routeData: notFoundRouteData }], - pageMap, - trailingSlash: 'always', - }), - ); + const app = makeApp({ + routes: [{ routeData: notFoundRouteData }], + pageMap, + trailingSlash: 'always', + }); const request = new Request('http://example.com/ajksalscla/'); const routeData = app.match(request); const response = await app.render(request, { routeData }); diff --git a/packages/astro/test/units/app/headers.test.js b/packages/astro/test/units/app/headers.test.ts similarity index 100% rename from packages/astro/test/units/app/headers.test.js rename to packages/astro/test/units/app/headers.test.ts diff --git a/packages/astro/test/units/app/locals.test.js b/packages/astro/test/units/app/locals.test.ts similarity index 77% rename from packages/astro/test/units/app/locals.test.js rename to packages/astro/test/units/app/locals.test.ts index 907b4ac74d0b..49f0e6bc91b7 100644 --- a/packages/astro/test/units/app/locals.test.js +++ b/packages/astro/test/units/app/locals.test.ts @@ -1,10 +1,20 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { App } from '../../../dist/core/app/app.js'; +import type { SSRManifest } from '../../../dist/core/app/types.js'; +import type { RouteData } from '../../../dist/types/public/internal.js'; import { createComponent, render } from '../../../dist/runtime/server/index.js'; -import { createManifest } from './test-helpers.js'; +import { createManifest } from './test-helpers.ts'; -const fooRouteData = { +function makeRouteData(partial: Omit): RouteData { + return partial as RouteData; +} + +function makeApp(opts: Record): App { + return new App(createManifest(opts as any) as unknown as SSRManifest); +} + +const fooRouteData = makeRouteData({ route: '/foo', component: 'src/pages/foo.astro', params: [], @@ -17,9 +27,9 @@ const fooRouteData = { fallbackRoutes: [], isIndex: false, origin: 'project', -}; +}); -const apiRouteData = { +const apiRouteData = makeRouteData({ route: '/api', component: 'src/pages/api.js', params: [], @@ -32,9 +42,9 @@ const apiRouteData = { fallbackRoutes: [], isIndex: false, origin: 'project', -}; +}); -const errorRouteData = { +const errorRouteData = makeRouteData({ route: '/go-to-error-page', component: 'src/pages/go-to-error-page.astro', params: [], @@ -47,9 +57,9 @@ const errorRouteData = { fallbackRoutes: [], isIndex: false, origin: 'project', -}; +}); -const notFoundRouteData = { +const notFoundRouteData = makeRouteData({ route: '/404', component: 'src/pages/404.astro', params: [], @@ -62,9 +72,9 @@ const notFoundRouteData = { fallbackRoutes: [], isIndex: false, origin: 'project', -}; +}); -const internalErrorRouteData = { +const internalErrorRouteData = makeRouteData({ route: '/500', component: 'src/pages/500.astro', params: [], @@ -77,24 +87,24 @@ const internalErrorRouteData = { fallbackRoutes: [], isIndex: false, origin: 'project', -}; +}); -const fooPage = createComponent((result, props, slots) => { +const fooPage = createComponent((result: any, props: any, slots: any) => { const Astro = result.createAstro(props, slots); return render`

${Astro.locals.foo}

`; }); -const notFoundPage = createComponent((result, props, slots) => { +const notFoundPage = createComponent((result: any, props: any, slots: any) => { const Astro = result.createAstro(props, slots); return render`

${Astro.locals.foo}

`; }); -const internalErrorPage = createComponent((result, props, slots) => { +const internalErrorPage = createComponent((result: any, props: any, slots: any) => { const Astro = result.createAstro(props, slots); return render`

${Astro.locals.foo}

`; }); -const pageMap = new Map([ +const pageMap = new Map([ [ fooRouteData.component, async () => ({ @@ -107,7 +117,7 @@ const pageMap = new Map([ apiRouteData.component, async () => ({ page: async () => ({ - GET: async ({ locals }) => + GET: async ({ locals }: { locals: Record }) => new Response(JSON.stringify({ ...locals }), { headers: { 'Content-Type': 'application/json', @@ -144,18 +154,16 @@ const pageMap = new Map([ ], ]); -const app = new App( - createManifest({ - routes: [ - { routeData: fooRouteData }, - { routeData: apiRouteData }, - { routeData: errorRouteData }, - { routeData: notFoundRouteData }, - { routeData: internalErrorRouteData }, - ], - pageMap, - }), -); +const app = makeApp({ + routes: [ + { routeData: fooRouteData }, + { routeData: apiRouteData }, + { routeData: errorRouteData }, + { routeData: notFoundRouteData }, + { routeData: internalErrorRouteData }, + ], + pageMap, +}); describe('SSR Astro.locals from server', () => { it('Can access Astro.locals in page', async () => { diff --git a/packages/astro/test/units/app/node.test.js b/packages/astro/test/units/app/node.test.ts similarity index 92% rename from packages/astro/test/units/app/node.test.js rename to packages/astro/test/units/app/node.test.ts index e820cd8e84a7..8a3eefd408cf 100644 --- a/packages/astro/test/units/app/node.test.js +++ b/packages/astro/test/units/app/node.test.ts @@ -3,7 +3,9 @@ import { EventEmitter } from 'node:events'; import { describe, it } from 'node:test'; import { createRequest, writeResponse } from '../../../dist/core/app/node.js'; -const mockNodeRequest = { +// Minimal mock satisfying the subset of IncomingMessage used by createRequest. +// We intentionally omit the full IncomingMessage interface members not exercised here. +const mockNodeRequest: any = { url: '/', method: 'GET', headers: { @@ -29,7 +31,7 @@ describe('node', () => { }, { allowedDomains: [{ hostname: 'example.com' }] }, ); - assert.equal(result[Symbol.for('astro.clientAddress')], '1.1.1.1'); + assert.equal((result as any)[Symbol.for('astro.clientAddress')], '1.1.1.1'); }); it('parses client IP from multi-value x-forwarded-for header', () => { @@ -43,7 +45,7 @@ describe('node', () => { }, { allowedDomains: [{ hostname: 'example.com' }] }, ); - assert.equal(result[Symbol.for('astro.clientAddress')], '1.1.1.1'); + assert.equal((result as any)[Symbol.for('astro.clientAddress')], '1.1.1.1'); }); it('parses client IP from multi-value x-forwarded-for header with spaces', () => { @@ -57,7 +59,7 @@ describe('node', () => { }, { allowedDomains: [{ hostname: 'example.com' }] }, ); - assert.equal(result[Symbol.for('astro.clientAddress')], '1.1.1.1'); + assert.equal((result as any)[Symbol.for('astro.clientAddress')], '1.1.1.1'); }); it('fallbacks to remoteAddress when no x-forwarded-for header is present', () => { @@ -70,7 +72,7 @@ describe('node', () => { }, { allowedDomains: [{ hostname: 'example.com' }] }, ); - assert.equal(result[Symbol.for('astro.clientAddress')], '2.2.2.2'); + assert.equal((result as any)[Symbol.for('astro.clientAddress')], '2.2.2.2'); }); it('ignores x-forwarded-for when no allowedDomains is configured (default)', () => { @@ -83,7 +85,7 @@ describe('node', () => { }); // Without allowedDomains, x-forwarded-for should NOT be trusted // Falls back to socket remoteAddress - assert.equal(result[Symbol.for('astro.clientAddress')], '2.2.2.2'); + assert.equal((result as any)[Symbol.for('astro.clientAddress')], '2.2.2.2'); }); it('ignores x-forwarded-for when allowedDomains is empty', () => { @@ -98,7 +100,7 @@ describe('node', () => { { allowedDomains: [] }, ); // Empty allowedDomains means no proxy trust, use socket address - assert.equal(result[Symbol.for('astro.clientAddress')], '2.2.2.2'); + assert.equal((result as any)[Symbol.for('astro.clientAddress')], '2.2.2.2'); }); it('trusts x-forwarded-for when host matches allowedDomains', () => { @@ -113,7 +115,7 @@ describe('node', () => { { allowedDomains: [{ hostname: 'example.com' }] }, ); // Host matches allowedDomains, so x-forwarded-for is trusted - assert.equal(result[Symbol.for('astro.clientAddress')], '1.1.1.1'); + assert.equal((result as any)[Symbol.for('astro.clientAddress')], '1.1.1.1'); }); it('ignores x-forwarded-for when host does not match allowedDomains', () => { @@ -128,7 +130,7 @@ describe('node', () => { { allowedDomains: [{ hostname: 'example.com' }] }, ); // Host does not match allowedDomains, so x-forwarded-for is NOT trusted - assert.equal(result[Symbol.for('astro.clientAddress')], '2.2.2.2'); + assert.equal((result as any)[Symbol.for('astro.clientAddress')], '2.2.2.2'); }); it('trusts x-forwarded-for when x-forwarded-host matches allowedDomains', () => { @@ -143,7 +145,7 @@ describe('node', () => { { allowedDomains: [{ hostname: 'example.com' }] }, ); // X-Forwarded-Host validated against allowedDomains, so XFF is trusted - assert.equal(result[Symbol.for('astro.clientAddress')], '1.1.1.1'); + assert.equal((result as any)[Symbol.for('astro.clientAddress')], '1.1.1.1'); }); it('trusts multi-value x-forwarded-for when host matches allowedDomains', () => { @@ -157,7 +159,7 @@ describe('node', () => { }, { allowedDomains: [{ hostname: 'example.com' }] }, ); - assert.equal(result[Symbol.for('astro.clientAddress')], '1.1.1.1'); + assert.equal((result as any)[Symbol.for('astro.clientAddress')], '1.1.1.1'); }); it('falls back to remoteAddress when host matches allowedDomains but no x-forwarded-for', () => { @@ -170,7 +172,7 @@ describe('node', () => { }, { allowedDomains: [{ hostname: 'example.com' }] }, ); - assert.equal(result[Symbol.for('astro.clientAddress')], '2.2.2.2'); + assert.equal((result as any)[Symbol.for('astro.clientAddress')], '2.2.2.2'); }); it('prevents IP spoofing: attacker cannot override clientAddress without allowedDomains', () => { @@ -183,7 +185,7 @@ describe('node', () => { }, }); // Without allowedDomains, the spoofed IP must be ignored - assert.equal(result[Symbol.for('astro.clientAddress')], '2.2.2.2'); + assert.equal((result as any)[Symbol.for('astro.clientAddress')], '2.2.2.2'); }); it('prevents IP spoofing: attacker cannot override clientAddress when host does not match', () => { @@ -199,7 +201,7 @@ describe('node', () => { { allowedDomains: [{ hostname: 'example.com' }] }, ); // Host doesn't match allowedDomains, so XFF is not trusted - assert.equal(result[Symbol.for('astro.clientAddress')], '2.2.2.2'); + assert.equal((result as any)[Symbol.for('astro.clientAddress')], '2.2.2.2'); }); }); @@ -860,7 +862,7 @@ describe('node', () => { const { Readable } = await import('node:stream'); // Create a stream that produces data exceeding the limit const limit = 1024; // 1KB limit - const chunks = []; + const chunks: Buffer[] = []; // Create 2KB of data (exceeds 1KB limit) for (let i = 0; i < 4; i++) { chunks.push(Buffer.alloc(512, 0x41)); @@ -882,13 +884,13 @@ describe('node', () => { // The request should be created, but reading the body should fail await assert.rejects( async () => { - const reader = request.body.getReader(); + const reader = request.body!.getReader(); while (true) { const { done } = await reader.read(); if (done) break; } }, - (err) => { + (err: Error) => { assert.ok(err.message.includes('Body size limit exceeded')); return true; }, @@ -914,14 +916,14 @@ describe('node', () => { const request = createRequest(req, { bodySizeLimit: limit }); // Reading the body should succeed - const reader = request.body.getReader(); - const chunks = []; + const reader = request.body!.getReader(); + const readChunks: Uint8Array[] = []; while (true) { const { done, value } = await reader.read(); if (done) break; - chunks.push(value); + readChunks.push(value); } - const totalSize = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0); + const totalSize = readChunks.reduce((sum, chunk) => sum + chunk.byteLength, 0); assert.equal(totalSize, 1024); }); @@ -944,21 +946,21 @@ describe('node', () => { const request = createRequest(req); // Reading the body should succeed without limit - const reader = request.body.getReader(); - const chunks = []; + const reader = request.body!.getReader(); + const readChunks: Uint8Array[] = []; while (true) { const { done, value } = await reader.read(); if (done) break; - chunks.push(value); + readChunks.push(value); } - const totalSize = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0); + const totalSize = readChunks.reduce((sum, chunk) => sum + chunk.byteLength, 0); assert.equal(totalSize, 2048); }); }); describe('abort signal', () => { it('aborts the request.signal when the underlying socket closes', () => { - const socket = new EventEmitter(); + const socket: any = new EventEmitter(); socket.encrypted = true; socket.remoteAddress = '2.2.2.2'; socket.destroyed = false; @@ -973,7 +975,7 @@ describe('node', () => { }); it('cleans up socket listeners after the response finishes', async () => { - const socket = new EventEmitter(); + const socket: any = new EventEmitter(); socket.encrypted = true; socket.remoteAddress = '2.2.2.2'; socket.destroyed = false; @@ -986,7 +988,7 @@ describe('node', () => { assert.equal(socket.listenerCount('close') > 0, true); const response = new Response('ok'); - const destination = new MockServerResponse(nodeRequest); + const destination = new MockServerResponse(nodeRequest) as any; await writeResponse(response, destination); assert.equal(result.signal.aborted, false); @@ -996,7 +998,13 @@ describe('node', () => { }); class MockServerResponse extends EventEmitter { - constructor(req) { + req: any; + statusCode: number; + statusMessage: string | undefined; + headers: Record; + body: unknown[]; + + constructor(req: any) { super(); this.req = req; this.statusCode = 200; @@ -1005,21 +1013,21 @@ class MockServerResponse extends EventEmitter { this.body = []; } - writeHead(status, headers) { + writeHead(status: number, headers: Record): void { this.statusCode = status; this.headers = headers; } - write(chunk) { + write(chunk: unknown): boolean { this.body.push(chunk); return true; } - end() { + end(): void { this.emit('finish'); } - destroy() { + destroy(): void { this.emit('close'); } } diff --git a/packages/astro/test/units/app/response.test.js b/packages/astro/test/units/app/response.test.ts similarity index 87% rename from packages/astro/test/units/app/response.test.js rename to packages/astro/test/units/app/response.test.ts index ce972c536f1c..fcd865cab89c 100644 --- a/packages/astro/test/units/app/response.test.js +++ b/packages/astro/test/units/app/response.test.ts @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { App } from '../../../dist/core/app/app.js'; import { createComponent, render } from '../../../dist/runtime/server/index.js'; -import { createManifest } from './test-helpers.js'; +import { createManifest, createRouteInfo } from './test-helpers.ts'; const statusRouteData = { route: '/status-code', @@ -12,11 +12,11 @@ const statusRouteData = { distURL: [], pattern: /^\/status-code\/?$/, segments: [[{ content: 'status-code', dynamic: false, spread: false }]], - type: 'page', + type: 'page' as const, prerender: false, fallbackRoutes: [], isIndex: false, - origin: 'project', + origin: 'project' as const, }; const someHeaderRouteData = { @@ -27,11 +27,11 @@ const someHeaderRouteData = { distURL: [], pattern: /^\/some-header\/?$/, segments: [[{ content: 'some-header', dynamic: false, spread: false }]], - type: 'page', + type: 'page' as const, prerender: false, fallbackRoutes: [], isIndex: false, - origin: 'project', + origin: 'project' as const, }; const notFoundRouteData = { @@ -42,14 +42,14 @@ const notFoundRouteData = { distURL: [], pattern: /^\/404\/?$/, segments: [[{ content: '404', dynamic: false, spread: false }]], - type: 'page', + type: 'page' as const, prerender: false, fallbackRoutes: [], isIndex: false, - origin: 'project', + origin: 'project' as const, }; -const statusPage = createComponent((result, props, slots) => { +const statusPage = createComponent((result: any, props: any, slots: any) => { const Astro = result.createAstro(props, slots); Astro.response.status = 404; Astro.response.statusText = 'Oops'; @@ -57,7 +57,7 @@ const statusPage = createComponent((result, props, slots) => { return render`

Testing

`; }); -const someHeaderPage = createComponent((result, props, slots) => { +const someHeaderPage = createComponent((result: any, props: any, slots: any) => { const Astro = result.createAstro(props, slots); Astro.response.headers.set('One-Two', 'three'); Astro.response.headers.set('Four-Five', 'six'); @@ -99,12 +99,12 @@ const pageMap = new Map([ const app = new App( createManifest({ routes: [ - { routeData: statusRouteData }, - { routeData: someHeaderRouteData }, - { routeData: notFoundRouteData }, + createRouteInfo(statusRouteData), + createRouteInfo(someHeaderRouteData), + createRouteInfo(notFoundRouteData), ], - pageMap, - }), + pageMap: pageMap as any, + }) as any, ); describe('Using Astro.response in SSR', () => { diff --git a/packages/astro/test/units/app/test-helpers.js b/packages/astro/test/units/app/test-helpers.ts similarity index 64% rename from packages/astro/test/units/app/test-helpers.js rename to packages/astro/test/units/app/test-helpers.ts index 59a2d3f8e5bb..bb7b83c2f5b3 100644 --- a/packages/astro/test/units/app/test-helpers.js +++ b/packages/astro/test/units/app/test-helpers.ts @@ -1,16 +1,11 @@ -// @ts-check +import type { + SSRManifest, + SSRManifestI18n, + SSRManifestCSP, + RouteInfo, +} from '../../../dist/core/app/types.js'; +import type { RouteData } from '../../../dist/types/public/internal.js'; -/** - * @param {object} [options] - * @param {any[]} [options.routes] - * @param {Map} [options.pageMap] - * @param {string} [options.base] - * @param {string} [options.trailingSlash] - * @param {Function} [options.middleware] - * @param {Function} [options.actions] - * @param {number} [options.actionBodySizeLimit] - * @param {object} [options.i18n] - */ export function createManifest({ routes, pageMap, @@ -22,23 +17,34 @@ export function createManifest({ i18n = undefined, csp = undefined, serverLike = true, -} = {}) { +}: { + routes?: RouteInfo[]; + pageMap?: SSRManifest['pageMap']; + base?: string; + trailingSlash?: 'always' | 'never' | 'ignore'; + middleware?: SSRManifest['middleware']; + actions?: SSRManifest['actions']; + actionBodySizeLimit?: number; + i18n?: SSRManifestI18n; + csp?: SSRManifestCSP; + serverLike?: boolean; +} = {}): SSRManifest { const rootDir = new URL('file:///astro-test/'); const buildDir = new URL('file:///astro-test/dist/'); - return /** @type {import('../../../dist/core/app/types.js').SSRManifest} */ ({ + return { adapterName: 'test-adapter', routes, site: undefined, base, userAssetsBase: undefined, - trailingSlash: /** @type {'always' | 'never' | 'ignore'} */ (trailingSlash), - buildFormat: /** @type {'directory'} */ ('directory'), + trailingSlash, + buildFormat: 'directory', compressHTML: false, assetsPrefix: undefined, renderers: [], serverLike, - middlewareMode: /** @type {'classic'} */ ('classic'), + middlewareMode: 'classic', clientDirectives: new Map(), entryModules: {}, inlinedScripts: new Map(), @@ -47,7 +53,7 @@ export function createManifest({ pageModule: undefined, pageMap, serverIslandMappings: undefined, - key: Promise.resolve(/** @type {CryptoKey} */ ({})), + key: Promise.resolve({} as CryptoKey), i18n, middleware, actions, @@ -75,14 +81,14 @@ export function createManifest({ placement: undefined, }, internalFetchHeaders: undefined, - logLevel: /** @type {'silent'} */ ('silent'), + logLevel: 'silent', experimentalQueuedRendering: { enabled: false, }, - }); + } as SSRManifest; } -export function createRouteInfo(routeData) { +export function createRouteInfo(routeData: RouteData): RouteInfo { return { routeData, file: routeData.component, diff --git a/packages/astro/test/units/app/trailing-slash.test.js b/packages/astro/test/units/app/trailing-slash.test.ts similarity index 87% rename from packages/astro/test/units/app/trailing-slash.test.js rename to packages/astro/test/units/app/trailing-slash.test.ts index 063c228e1dc3..075954644b1c 100644 --- a/packages/astro/test/units/app/trailing-slash.test.js +++ b/packages/astro/test/units/app/trailing-slash.test.ts @@ -1,14 +1,16 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { App } from '../../../dist/core/app/app.js'; +import type { SSRManifest } from '../../../dist/core/app/types.js'; +import type { RouteData } from '../../../dist/types/public/internal.js'; import { createComponent, render } from '../../../dist/runtime/server/index.js'; -import { createManifest } from './test-helpers.js'; +import { createManifest } from './test-helpers.ts'; -function escapeRoute(route) { +function escapeRoute(route: string): string { return route.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } -function createRouteData(route) { +function makeRouteData(route: string): RouteData { const segments = route .split('/') .filter(Boolean) @@ -27,22 +29,26 @@ function createRouteData(route) { fallbackRoutes: [], isIndex: false, origin: 'project', - }; + } as RouteData; } -const okPage = createComponent(() => { +function makeApp(opts: Record): App { + return new App(createManifest(opts as any) as unknown as SSRManifest); +} + +const okPage = createComponent((_result: any, _props: any, _slots: any) => { return render`

Ok

`; }); -const notFoundPage = createComponent(() => { +const notFoundPage = createComponent((_result: any, _props: any, _slots: any) => { return render`

Not Found

`; }); -const anotherRouteData = createRouteData('/another'); -const subPathRouteData = createRouteData('/sub/path'); -const dotPathRouteData = createRouteData('/dot.in.directory/path'); -const notFoundRouteData = { - ...createRouteData('/404'), +const anotherRouteData = makeRouteData('/another'); +const subPathRouteData = makeRouteData('/sub/path'); +const dotPathRouteData = makeRouteData('/dot.in.directory/path'); +const notFoundRouteData: RouteData = { + ...makeRouteData('/404'), component: 'src/pages/404.astro', }; @@ -83,18 +89,16 @@ const pageMap = new Map([ describe('Redirecting trailing slashes in SSR', () => { describe('trailingSlash: always', () => { - const app = new App( - createManifest({ - trailingSlash: 'always', - routes: [ - { routeData: anotherRouteData }, - { routeData: subPathRouteData }, - { routeData: dotPathRouteData }, - { routeData: notFoundRouteData }, - ], - pageMap, - }), - ); + const app = makeApp({ + trailingSlash: 'always', + routes: [ + { routeData: anotherRouteData }, + { routeData: subPathRouteData }, + { routeData: dotPathRouteData }, + { routeData: notFoundRouteData }, + ], + pageMap, + }); it('Redirects to add a trailing slash', async () => { const request = new Request('http://example.com/another'); @@ -198,17 +202,15 @@ describe('Redirecting trailing slashes in SSR', () => { }); describe('trailingSlash: never', () => { - const app = new App( - createManifest({ - trailingSlash: 'never', - routes: [ - { routeData: anotherRouteData }, - { routeData: subPathRouteData }, - { routeData: notFoundRouteData }, - ], - pageMap, - }), - ); + const app = makeApp({ + trailingSlash: 'never', + routes: [ + { routeData: anotherRouteData }, + { routeData: subPathRouteData }, + { routeData: notFoundRouteData }, + ], + pageMap, + }); it('Redirects to remove a trailing slash', async () => { const request = new Request('http://example.com/another/'); @@ -287,14 +289,12 @@ describe('Redirecting trailing slashes in SSR', () => { }); describe('trailingSlash: never with base path', () => { - const app = new App( - createManifest({ - base: '/mybase', - trailingSlash: 'never', - routes: [{ routeData: anotherRouteData }, { routeData: notFoundRouteData }], - pageMap, - }), - ); + const app = makeApp({ + base: '/mybase', + trailingSlash: 'never', + routes: [{ routeData: anotherRouteData }, { routeData: notFoundRouteData }], + pageMap, + }); it('Redirects to remove a trailing slash on base path', async () => { const request = new Request('http://example.com/mybase/'); @@ -325,13 +325,11 @@ describe('Redirecting trailing slashes in SSR', () => { }); describe('trailingSlash: ignore', () => { - const app = new App( - createManifest({ - trailingSlash: 'ignore', - routes: [{ routeData: anotherRouteData }, { routeData: notFoundRouteData }], - pageMap, - }), - ); + const app = makeApp({ + trailingSlash: 'ignore', + routes: [{ routeData: anotherRouteData }, { routeData: notFoundRouteData }], + pageMap, + }); it('Redirects to collapse multiple trailing slashes', async () => { const request = new Request('http://example.com/another///'); diff --git a/packages/astro/test/units/app/url-attribute-xss.test.js b/packages/astro/test/units/app/url-attribute-xss.test.ts similarity index 98% rename from packages/astro/test/units/app/url-attribute-xss.test.js rename to packages/astro/test/units/app/url-attribute-xss.test.ts index 56aa4d401c90..3afd18d4a1f3 100644 --- a/packages/astro/test/units/app/url-attribute-xss.test.js +++ b/packages/astro/test/units/app/url-attribute-xss.test.ts @@ -1,4 +1,3 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { addAttribute } from '../../../dist/runtime/server/render/util.js'; diff --git a/packages/astro/test/units/assets/endpoint-svg-reject.test.ts b/packages/astro/test/units/assets/endpoint-svg-reject.test.ts new file mode 100644 index 000000000000..f4f60c79e365 --- /dev/null +++ b/packages/astro/test/units/assets/endpoint-svg-reject.test.ts @@ -0,0 +1,61 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { inferSourceFormat } from '../../../dist/assets/utils/inferSourceFormat.js'; + +describe('inferSourceFormat', () => { + it('detects svg from file extension', () => { + assert.equal(inferSourceFormat('/images/logo.svg'), 'svg'); + }); + + it('detects svg from remote URL', () => { + assert.equal(inferSourceFormat('https://example.com/icon.svg'), 'svg'); + }); + + it('detects svg from remote URL with query string', () => { + assert.equal(inferSourceFormat('https://example.com/icon.svg?v=2'), 'svg'); + }); + + it('detects svg from remote URL with hash', () => { + assert.equal(inferSourceFormat('https://example.com/icon.svg#fragment'), 'svg'); + }); + + it('detects svg from data: URI with image/svg+xml', () => { + assert.equal(inferSourceFormat('data:image/svg+xml;base64,PHN2Zz4='), 'svg'); + }); + + it('detects png from file extension', () => { + assert.equal(inferSourceFormat('/images/photo.png'), 'png'); + }); + + it('detects jpg from file extension', () => { + assert.equal(inferSourceFormat('/images/photo.jpg'), 'jpg'); + }); + + it('detects webp from remote URL', () => { + assert.equal(inferSourceFormat('https://cdn.example.com/img.webp'), 'webp'); + }); + + it('detects png from data: URI', () => { + assert.equal(inferSourceFormat('data:image/png;base64,iVBOR'), 'png'); + }); + + it('returns undefined for extensionless path', () => { + assert.equal(inferSourceFormat('/images/photo'), undefined); + }); + + it('is case-insensitive for extensions', () => { + assert.equal(inferSourceFormat('/images/logo.SVG'), 'svg'); + assert.equal(inferSourceFormat('/images/photo.PNG'), 'png'); + }); + + it('handles paths with multiple dots', () => { + assert.equal(inferSourceFormat('/images/my.photo.jpg'), 'jpg'); + }); + + it('detects format from Astro internal query string paths', () => { + assert.equal( + inferSourceFormat('/src/assets/penguin.jpg?origWidth=207&origHeight=243&origFormat=jpg'), + 'jpg', + ); + }); +}); diff --git a/packages/astro/test/units/assets/fonts/core.test.js b/packages/astro/test/units/assets/fonts/core.test.ts similarity index 98% rename from packages/astro/test/units/assets/fonts/core.test.js rename to packages/astro/test/units/assets/fonts/core.test.ts index 4b45fb2ab8e0..d90820fb3345 100644 --- a/packages/astro/test/units/assets/fonts/core.test.js +++ b/packages/astro/test/units/assets/fonts/core.test.ts @@ -1,4 +1,3 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { collectComponentData } from '../../../../dist/assets/fonts/core/collect-component-data.js'; @@ -10,14 +9,19 @@ import { filterPreloads } from '../../../../dist/assets/fonts/core/filter-preloa import { getOrCreateFontFamilyAssets } from '../../../../dist/assets/fonts/core/get-or-create-font-family-assets.js'; import { optimizeFallbacks } from '../../../../dist/assets/fonts/core/optimize-fallbacks.js'; import { resolveFamily } from '../../../../dist/assets/fonts/core/resolve-family.js'; -import { SpyLogger } from '../../test-utils.js'; +import type { SystemFallbacksProvider } from '../../../../dist/assets/fonts/definitions.js'; +import type { + FontFamilyAssetsByUniqueKey, + ResolvedFontFamily, +} from '../../../../dist/assets/fonts/types.js'; +import { SpyLogger } from '../../test-utils.ts'; import { FakeFontMetricsResolver, FakeHasher, FakeStringMatcher, markdownBold, PassthroughFontResolver, -} from './utils.js'; +} from './utils.ts'; describe('fonts core', () => { describe('resolveFamily()', () => { @@ -463,8 +467,7 @@ describe('fonts core', () => { describe('getOrCreateFontFamilyAssets()', () => { it('reuses the same object as needed', () => { - /** @type {Array} */ - const families = [ + const families: Array = [ { name: 'Foo', uniqueName: 'Foo-xxx', @@ -496,8 +499,7 @@ describe('fonts core', () => { }, ]; - /** @type {import('../../../../dist/assets/fonts/types.js').FontFamilyAssetsByUniqueKey} */ - const fontFamilyAssetsByUniqueKey = new Map(); + const fontFamilyAssetsByUniqueKey: FontFamilyAssetsByUniqueKey = new Map(); const logger = new SpyLogger(); assert.deepStrictEqual( @@ -546,8 +548,7 @@ describe('fonts core', () => { }); it('logs warnings for conflicting css variables', () => { - /** @type {import('../../../../dist/assets/fonts/types.js').FontFamilyAssetsByUniqueKey} */ - const fontFamilyAssetsByUniqueKey = new Map(); + const fontFamilyAssetsByUniqueKey: FontFamilyAssetsByUniqueKey = new Map(); const logger = new SpyLogger(); getOrCreateFontFamilyAssets({ @@ -668,7 +669,7 @@ describe('fonts core', () => { generate: ({ originalUrl }) => originalUrl, }, fontTypeExtractor: { - extract: (url) => /** @type {any} */ (url.split('.').at(-1)) ?? 'woff', + extract: (url) => (url.split('.').at(-1) as any) ?? 'woff', }, urlResolver: { resolve: (url) => 'resolved:' + url, @@ -737,7 +738,7 @@ describe('fonts core', () => { generate: ({ originalUrl }) => originalUrl, }, fontTypeExtractor: { - extract: (url) => /** @type {any} */ (url.split('.').at(-1)) ?? 'woff', + extract: (url) => (url.split('.').at(-1) as any) ?? 'woff', }, urlResolver: { resolve: (url) => 'resolved:' + url, @@ -1427,8 +1428,7 @@ describe('fonts core', () => { name: 'Test', uniqueName: 'Test-xxx', }; - /** @type {import('../../../../dist/assets/fonts/definitions.js').SystemFallbacksProvider} */ - const systemFallbacksProvider = { + const systemFallbacksProvider: SystemFallbacksProvider = { getLocalFonts: () => ['Arial'], getMetricsForLocalFont: () => ({ ascent: 1854, diff --git a/packages/astro/test/units/assets/fonts/e2e.test.js b/packages/astro/test/units/assets/fonts/e2e.test.ts similarity index 94% rename from packages/astro/test/units/assets/fonts/e2e.test.js rename to packages/astro/test/units/assets/fonts/e2e.test.ts index 7b848f51955a..6f2d272d7aeb 100644 --- a/packages/astro/test/units/assets/fonts/e2e.test.js +++ b/packages/astro/test/units/assets/fonts/e2e.test.ts @@ -1,4 +1,3 @@ -// @ts-check import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { readFile, rm } from 'node:fs/promises'; @@ -27,20 +26,18 @@ import { UnifontFontResolver } from '../../../../dist/assets/fonts/infra/unifont import { UnstorageFsStorage } from '../../../../dist/assets/fonts/infra/unstorage-fs-storage.js'; import { XxhashHasher } from '../../../../dist/assets/fonts/infra/xxhash-hasher.js'; import { fontProviders } from '../../../../dist/assets/fonts/providers/index.js'; -import { Logger } from '../../../../dist/core/logger/core.js'; +import type { FontFamily } from '../../../../dist/assets/fonts/types.js'; +import { AstroLogger } from '../../../../dist/core/logger/core.js'; import { nodeLogDestination } from '../../../../dist/core/logger/node.js'; -/** - * @param {{ fonts: Array }} param0 - */ -async function run({ fonts: _fonts }) { +async function run({ fonts: _fonts }: { fonts: Array }) { const hasher = await XxhashHasher.create(); const resolvedFamilies = _fonts.map((family) => resolveFamily({ family, hasher })); const defaults = DEFAULTS; const { bold } = colors; - const logger = new Logger({ + const logger = new AstroLogger({ level: 'silent', - dest: nodeLogDestination, + destination: nodeLogDestination, }); const stringMatcher = new LevenshteinStringMatcher(); const base = new URL('./data/cache/', import.meta.url); @@ -49,7 +46,7 @@ async function run({ fonts: _fonts }) { const storage = new UnstorageFsStorage({ base }); const root = new URL('./data/fonts/', import.meta.url); const contentResolver = new FsFontFileContentResolver({ - readFileSync: (path) => readFileSync(path, 'utf-8'), + readFileSync: (path: string) => readFileSync(path, 'utf-8'), }); const fontFileIdGenerator = new DevFontFileIdGenerator({ contentResolver, hasher }); const fontTypeExtractor = new NodeFontTypeExtractor(); @@ -131,11 +128,9 @@ describe('Fonts E2E', () => { name: 'Test', cssVariable: '--font-test', provider: fontProviders.local(), - options: /** @type {any} */ ( - /** @type {import('../../../../dist/assets/fonts/providers/local.js').LocalFamilyOptions} */ ({ - variants: [{ src: ['./test.woff2'], weight: '400', style: 'normal' }], - }) - ), + options: { + variants: [{ src: ['./test.woff2'], weight: '400', style: 'normal' }], + } as any, }, ], }); @@ -269,11 +264,9 @@ describe('Fonts E2E', () => { name: 'Test', cssVariable: '--font-test', provider: fontProviders.local(), - options: /** @type {any} */ ( - /** @type {import('../../../../dist/assets/fonts/providers/local.js').LocalFamilyOptions} */ ({ - variants: [{ src: ['./test.woff2'], weight: '400', style: 'normal' }], - }) - ), + options: { + variants: [{ src: ['./test.woff2'], weight: '400', style: 'normal' }], + } as any, }, ], }); diff --git a/packages/astro/test/units/assets/fonts/infra.test.js b/packages/astro/test/units/assets/fonts/infra.test.ts similarity index 93% rename from packages/astro/test/units/assets/fonts/infra.test.js rename to packages/astro/test/units/assets/fonts/infra.test.ts index 51e7aff6377a..9b3874d29794 100644 --- a/packages/astro/test/units/assets/fonts/infra.test.js +++ b/packages/astro/test/units/assets/fonts/infra.test.ts @@ -1,8 +1,8 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { fileURLToPath } from 'node:url'; import { defineFontProvider } from 'unifont'; +import type { InitializedProvider } from 'unifont'; import { BuildFontFileIdGenerator } from '../../../../dist/assets/fonts/infra/build-font-file-id-generator.js'; import { BuildUrlResolver } from '../../../../dist/assets/fonts/infra/build-url-resolver.js'; import { CachedFontFetcher } from '../../../../dist/assets/fonts/infra/cached-font-fetcher.js'; @@ -19,7 +19,8 @@ import { } from '../../../../dist/assets/fonts/infra/minifiable-css-renderer.js'; import { NodeFontTypeExtractor } from '../../../../dist/assets/fonts/infra/node-font-type-extractor.js'; import { UnifontFontResolver } from '../../../../dist/assets/fonts/infra/unifont-font-resolver.js'; -import { FakeHasher, SpyStorage } from './utils.js'; +import type { FontProvider } from '../../../../dist/index.js'; +import { FakeHasher, SpyStorage } from './utils.ts'; describe('fonts infra', () => { describe('MinifiableCssRenderer', () => { @@ -61,17 +62,11 @@ describe('fonts infra', () => { }); describe('CachedFontFetcher', () => { - /** - * - * @param {{ ok: boolean }} param0 - */ - function createReadFileMock({ ok }) { - /** @type {Array} */ - const filesUrls = []; + function createReadFileMock({ ok }: { ok: boolean }) { + const filesUrls: Array = []; return { filesUrls, - /** @type {(url: string) => Promise} */ - readFile: async (url) => { + readFile: async (url: string): Promise => { filesUrls.push(url); if (!ok) { throw 'fs error'; @@ -81,24 +76,17 @@ describe('fonts infra', () => { }; } - /** - * - * @param {{ ok: boolean }} param0 - */ - function createFetchMock({ ok }) { - /** @type {Array} */ - const fetchUrls = []; + function createFetchMock({ ok }: { ok: boolean }) { + const fetchUrls: Array = []; return { fetchUrls, - /** @type {(url: string) => Promise} */ - fetch: async (url) => { + fetch: async (url: string): Promise => { fetchUrls.push(url); - // @ts-expect-error return { ok, status: ok ? 200 : 500, - arrayBuffer: async () => new ArrayBuffer(), - }; + arrayBuffer: async () => new ArrayBuffer(0), + } as unknown as Response; }, }; } @@ -166,16 +154,19 @@ describe('fonts infra', () => { let error = await fontFetcher .fetch({ id: 'abc', url: '/foo/bar', init: undefined }) - .catch((err) => err); + .catch((err: unknown) => err); assert.equal(error instanceof Error, true); - assert.equal(error.cause, 'fs error'); + assert.equal((error as Error).cause, 'fs error'); error = await fontFetcher .fetch({ id: 'abc', url: 'https://example.com', init: undefined }) - .catch((err) => err); + .catch((err: unknown) => err); assert.equal(error instanceof Error, true); - assert.equal(error.cause instanceof Error, true); - assert.equal(error.cause.message.includes('Response was not successful'), true); + assert.equal((error as Error).cause instanceof Error, true); + assert.equal( + ((error as Error).cause as Error).message.includes('Response was not successful'), + true, + ); }); }); @@ -219,8 +210,7 @@ describe('fonts infra', () => { }); it('NodeFontTypeExtractor', () => { - /** @type {Array<[string, false | string]>} */ - const data = [ + const data: Array<[string, false | string]> = [ ['', false], ['.', false], ['test.', false], @@ -338,7 +328,7 @@ describe('fonts infra', () => { const resolver = new BuildFontFileIdGenerator({ hasher: new FakeHasher(), contentResolver: { - resolve: (url) => url, + resolve: (url: string) => url, }, }); assert.equal( @@ -361,7 +351,7 @@ describe('fonts infra', () => { const resolver = new DevFontFileIdGenerator({ hasher: new FakeHasher(), contentResolver: { - resolve: (url) => url, + resolve: (url: string) => url, }, }); assert.equal( @@ -421,12 +411,7 @@ describe('fonts infra', () => { }); describe('UnifontFontResolver', () => { - /** - * @param {string} name - * @param {any} [config] - * @returns {import('../../../../dist/index.js').FontProvider} - * */ - const createProvider = (name, config) => ({ + const createProvider = (name: string, config?: Record): FontProvider => ({ name, config, resolveFont: () => undefined, @@ -597,11 +582,9 @@ describe('fonts infra', () => { listFonts: () => ['a', 'b', 'c'], }; }); - /** @returns {import('../../../../dist/index.js').FontProvider} */ - const astroProvider = () => { + const astroProvider = (): FontProvider => { const provider = unifontProvider(); - /** @type {import('unifont').InitializedProvider | undefined} */ - let initializedProvider; + let initializedProvider: InitializedProvider | undefined; return { name: provider._name, async init(context) { diff --git a/packages/astro/test/units/assets/fonts/providers.test.js b/packages/astro/test/units/assets/fonts/providers.test.ts similarity index 99% rename from packages/astro/test/units/assets/fonts/providers.test.js rename to packages/astro/test/units/assets/fonts/providers.test.ts index 1968f206d2a0..31b1a51fa895 100644 --- a/packages/astro/test/units/assets/fonts/providers.test.js +++ b/packages/astro/test/units/assets/fonts/providers.test.ts @@ -1,4 +1,3 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { fileURLToPath } from 'node:url'; diff --git a/packages/astro/test/units/assets/fonts/utils.js b/packages/astro/test/units/assets/fonts/utils.js deleted file mode 100644 index 9b14fb25eb4f..000000000000 --- a/packages/astro/test/units/assets/fonts/utils.js +++ /dev/null @@ -1,166 +0,0 @@ -// @ts-check - -/** - * @import { Hasher, FontMetricsResolver, Storage, FontResolver, StringMatcher } from '../../../../dist/assets/fonts/definitions' - */ - -/** @implements {Storage} */ -export class SpyStorage { - /** @type {Map} */ - #store = new Map(); - - get store() { - return this.#store; - } - - /** - * @param {string} key - * @returns {Promise} - */ - async getItem(key) { - return this.#store.get(key) ?? null; - } - - /** - * @param {string} key - * @returns {Promise} - */ - async getItemRaw(key) { - return this.#store.get(key) ?? null; - } - - /** - * @param {string} key - * @param {any} value - * @returns {Promise} - */ - async setItemRaw(key, value) { - this.#store.set(key, value); - } - - /** - * @param {string} key - * @param {any} value - * @returns {Promise} - */ - async setItem(key, value) { - this.#store.set(key, value); - } -} - -/** @implements {Hasher} */ -export class FakeHasher { - /** @type {string | undefined} */ - #value; - - /** - * @param {string | undefined} [value=undefined] - */ - constructor(value = undefined) { - this.#value = value; - } - - /** - * @param {string} input - */ - hashString(input) { - return this.#value ?? input; - } - - /** - * @param {any} input - */ - hashObject(input) { - return this.#value ?? JSON.stringify(input); - } -} - -/** @implements {FontMetricsResolver} */ -export class FakeFontMetricsResolver { - async getMetrics() { - return { - ascent: 0, - descent: 0, - lineGap: 0, - unitsPerEm: 0, - xWidthAvg: 0, - }; - } - - /** - * @param {Parameters[0]} input - */ - generateFontFace(input) { - return JSON.stringify(input, null, 2) + `,`; - } -} - -/** - * @param {string} input - */ -export function markdownBold(input) { - return `**${input}**`; -} - -/** @implements {FontResolver} */ -export class PassthroughFontResolver { - /** @type {Map>>} */ - #providers; - - /** - * @private - * @param {Map>>} providers - */ - constructor(providers) { - this.#providers = providers; - } - - /** - * @param {{ families: Array; hasher: Hasher }} param0 - */ - static async create({ families, hasher }) { - /** @type {Map>>} */ - const providers = new Map(); - for (const { provider } of families) { - provider.name = `${provider.name}-${hasher.hashObject(provider.config ?? {})}`; - providers.set(provider.name, /** @type {any} */ (provider)); - } - const storage = new SpyStorage(); - await Promise.all( - Array.from(providers.values()).map(async (provider) => { - await provider.init?.({ storage, root: new URL(import.meta.url) }); - }), - ); - return new PassthroughFontResolver(providers); - } - - /** - * @param {import('../../../../dist/assets/fonts/types.js').ResolveFontOptions> & { provider: import('../../../../dist/index.js').FontProvider; }} param0 - */ - async resolveFont({ provider, ...rest }) { - const res = await this.#providers.get(provider.name)?.resolveFont(rest); - return res?.fonts ?? []; - } - - /** - * @param {{ provider: import('../../../../dist/index.js').FontProvider }} param0 - */ - async listFonts({ provider }) { - return await this.#providers.get(provider.name)?.listFonts?.(); - } -} - -/** @implements {StringMatcher} */ -export class FakeStringMatcher { - /** @type {string} */ - #match; - - /** @param {string} match */ - constructor(match) { - this.#match = match; - } - - getClosestMatch() { - return this.#match; - } -} diff --git a/packages/astro/test/units/assets/fonts/utils.test.js b/packages/astro/test/units/assets/fonts/utils.test.ts similarity index 99% rename from packages/astro/test/units/assets/fonts/utils.test.js rename to packages/astro/test/units/assets/fonts/utils.test.ts index 0e0e7361013c..661cc75d5036 100644 --- a/packages/astro/test/units/assets/fonts/utils.test.js +++ b/packages/astro/test/units/assets/fonts/utils.test.ts @@ -1,4 +1,3 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { diff --git a/packages/astro/test/units/assets/fonts/utils.ts b/packages/astro/test/units/assets/fonts/utils.ts new file mode 100644 index 000000000000..0d6846df2e7f --- /dev/null +++ b/packages/astro/test/units/assets/fonts/utils.ts @@ -0,0 +1,135 @@ +import type { + FontMetricsResolver, + FontResolver, + Hasher, + Storage, + StringMatcher, +} from '../../../../dist/assets/fonts/definitions.js'; +import type { + FontProvider, + ResolvedFontFamily, + ResolveFontOptions, + FontFaceMetrics, + CssProperties, +} from '../../../../dist/assets/fonts/types.js'; + +export class SpyStorage implements Storage { + #store = new Map(); + + get store() { + return this.#store; + } + + async getItem(key: string): Promise { + return this.#store.get(key) ?? null; + } + + async getItemRaw(key: string): Promise { + return (this.#store.get(key) as Buffer) ?? null; + } + + async setItemRaw(key: string, value: Buffer): Promise { + this.#store.set(key, value); + } + + async setItem(key: string, value: unknown): Promise { + this.#store.set(key, value); + } +} + +export class FakeHasher implements Hasher { + #value: string | undefined; + + constructor(value?: string) { + this.#value = value; + } + + hashString(input: string): string { + return this.#value ?? input; + } + + hashObject(input: Record): string { + return this.#value ?? JSON.stringify(input); + } +} + +export class FakeFontMetricsResolver implements FontMetricsResolver { + async getMetrics(): Promise { + return { + ascent: 0, + descent: 0, + lineGap: 0, + unitsPerEm: 0, + xWidthAvg: 0, + }; + } + + generateFontFace(input: { + metrics: FontFaceMetrics; + fallbackMetrics: FontFaceMetrics; + name: string; + font: string; + properties: CssProperties; + }): string { + return JSON.stringify(input, null, 2) + `,`; + } +} + +export function markdownBold(input: string): string { + return `**${input}**`; +} + +export class PassthroughFontResolver implements FontResolver { + #providers: Map>>; + + private constructor(providers: Map>>) { + this.#providers = providers; + } + + static async create({ + families, + hasher, + }: { + families: Array; + hasher: Hasher; + }): Promise { + const providers = new Map>>(); + for (const { provider } of families) { + provider.name = `${provider.name}-${hasher.hashObject((provider.config ?? {}) as Record)}`; + providers.set(provider.name, provider as FontProvider>); + } + const storage = new SpyStorage(); + await Promise.all( + Array.from(providers.values()).map(async (provider) => { + await provider.init?.({ storage, root: new URL(import.meta.url) }); + }), + ); + return new PassthroughFontResolver(providers); + } + + async resolveFont({ + provider, + ...rest + }: ResolveFontOptions> & { + provider: FontProvider; + }): Promise> { + const res = await this.#providers.get(provider.name)?.resolveFont(rest); + return res?.fonts ?? []; + } + + async listFonts({ provider }: { provider: FontProvider }): Promise { + return await this.#providers.get(provider.name)?.listFonts?.(); + } +} + +export class FakeStringMatcher implements StringMatcher { + #match: string; + + constructor(match: string) { + this.#match = match; + } + + getClosestMatch(): string { + return this.#match; + } +} diff --git a/packages/astro/test/units/assets/getImage.test.js b/packages/astro/test/units/assets/getImage.test.ts similarity index 95% rename from packages/astro/test/units/assets/getImage.test.js rename to packages/astro/test/units/assets/getImage.test.ts index 72f5faeddf43..d2b95b490e6f 100644 --- a/packages/astro/test/units/assets/getImage.test.js +++ b/packages/astro/test/units/assets/getImage.test.ts @@ -1,11 +1,11 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; +import type { GetImageResult, UnresolvedImageTransform } from '../../../dist/assets/types.js'; import { getImage } from '../../../dist/assets/internal.js'; -import { installImageService } from '../mocks.js'; +import { installImageService } from '../mocks.ts'; describe('getImage', () => { - /** @type {ReturnType} */ - let imageService; + let imageService: ReturnType; before(() => { imageService = installImageService({ domains: ['example.com', 'images.unsplash.com'] }); @@ -16,7 +16,7 @@ describe('getImage', () => { }); /** Shorthand for calling getImage with the installed service config */ - function renderImage(props) { + function renderImage(props: UnresolvedImageTransform): Promise { return getImage(props, imageService.imageConfig); } @@ -32,7 +32,7 @@ describe('getImage', () => { const widths = result.srcSet.values.map((v) => v.transform.width); assert.ok(widths.includes(800)); assert.equal(widths.at(-1), 1600); - assert.ok(widths.every((w) => w <= 1600)); + assert.ok(widths.every((w) => w! <= 1600)); }); it('has correct sizes attribute', async () => { @@ -330,8 +330,7 @@ describe('getImage', () => { describe('getImage - remotePatterns', () => { describe('hostname pattern', () => { - /** @type {ReturnType} */ - let service; + let service: ReturnType; before(() => { service = installImageService({ @@ -369,8 +368,7 @@ describe('getImage - remotePatterns', () => { }); describe('hostname + pathname pattern', () => { - /** @type {ReturnType} */ - let service; + let service: ReturnType; before(() => { service = installImageService({ @@ -400,8 +398,7 @@ describe('getImage - remotePatterns', () => { }); describe('protocol pattern', () => { - /** @type {ReturnType} */ - let service; + let service: ReturnType; before(() => { service = installImageService({ @@ -431,8 +428,7 @@ describe('getImage - remotePatterns', () => { }); describe('domains takes precedence', () => { - /** @type {ReturnType} */ - let service; + let service: ReturnType; before(() => { service = installImageService({ diff --git a/packages/astro/test/units/assets/image-layout.test.js b/packages/astro/test/units/assets/image-layout.test.ts similarity index 100% rename from packages/astro/test/units/assets/image-layout.test.js rename to packages/astro/test/units/assets/image-layout.test.ts diff --git a/packages/astro/test/units/assets/image-service.test.js b/packages/astro/test/units/assets/image-service.test.ts similarity index 96% rename from packages/astro/test/units/assets/image-service.test.js rename to packages/astro/test/units/assets/image-service.test.ts index 44ffe42dd83c..3881a339ba0a 100644 --- a/packages/astro/test/units/assets/image-service.test.js +++ b/packages/astro/test/units/assets/image-service.test.ts @@ -89,14 +89,14 @@ describe('sharp encoder options', async () => { describe('sharp image service', async () => { const sharpService = (await import('../../../dist/assets/services/sharp.js')).default; - const config = { service: { entrypoint: '', config: {} } }; + const config: any = { service: { entrypoint: '', config: {} } }; - let inputBuffer; + let inputBuffer: Uint8Array; before(async () => { inputBuffer = new Uint8Array(await readFile(FIXTURE_IMAGE)); }); - async function transform(opts) { + async function transform(opts: Record) { const { data } = await sharpService.transform( inputBuffer, { src: 'penguin.jpg', format: 'webp', ...opts }, diff --git a/packages/astro/test/units/assets/remote.test.js b/packages/astro/test/units/assets/remote.test.ts similarity index 84% rename from packages/astro/test/units/assets/remote.test.js rename to packages/astro/test/units/assets/remote.test.ts index 65e1c86b2f85..2d3169559f6a 100644 --- a/packages/astro/test/units/assets/remote.test.js +++ b/packages/astro/test/units/assets/remote.test.ts @@ -1,26 +1,20 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { revalidateRemoteImage } from '../../../dist/assets/build/remote.js'; -/** - * - * @param {number} status - * @param {Record} headerInit - * @param {ArrayBuffer} body - * @returns {() => Promise} - */ -function makeFetchMock(status, headerInit = {}, body = new ArrayBuffer(0)) { +function makeFetchMock( + status: number, + headerInit: Record = {}, + body: ArrayBuffer = new ArrayBuffer(0), +): () => Promise { const headers = new Headers(headerInit); return async () => - /** @type {Response} */ ( - /** @type {unknown} */ ({ - status, - ok: status >= 200 && status < 300, - headers, - arrayBuffer: async () => body, - }) - ); + ({ + status, + ok: status >= 200 && status < 300, + headers, + arrayBuffer: async () => body, + }) as unknown as Response; } describe('revalidateRemoteImage', () => { diff --git a/packages/astro/test/units/assets/utils.test.ts b/packages/astro/test/units/assets/utils.test.ts new file mode 100644 index 000000000000..a5c499ac2b33 --- /dev/null +++ b/packages/astro/test/units/assets/utils.test.ts @@ -0,0 +1,270 @@ +import * as assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { getAssetsPrefix } from '../../../dist/assets/utils/getAssetsPrefix.js'; +import { etag } from '../../../dist/assets/utils/etag.js'; +import { deterministicString } from '../../../dist/assets/utils/deterministic-string.js'; +import { getOrigQueryParams } from '../../../dist/assets/utils/queryParams.js'; +import { createPlaceholderURL, stringifyPlaceholderURL } from '../../../dist/assets/utils/url.js'; +import { isESMImportedImage, isRemoteImage } from '../../../dist/assets/utils/imageKind.js'; +import { dropAttributes } from '../../../dist/assets/runtime.js'; + +// #region getAssetsPrefix +describe('getAssetsPrefix', () => { + it('returns empty string when no prefix configured', () => { + assert.equal(getAssetsPrefix('.css', undefined), ''); + }); + + it('returns the string prefix directly', () => { + assert.equal(getAssetsPrefix('.css', 'https://cdn.example.com'), 'https://cdn.example.com'); + }); + + it('returns per-type prefix for matching extension', () => { + const prefix = { + js: 'https://js.cdn.com', + css: 'https://css.cdn.com', + fallback: 'https://cdn.com', + }; + assert.equal(getAssetsPrefix('.css', prefix), 'https://css.cdn.com'); + assert.equal(getAssetsPrefix('.js', prefix), 'https://js.cdn.com'); + }); + + it('returns fallback for unknown extension', () => { + const prefix = { js: 'https://js.cdn.com', fallback: 'https://cdn.com' }; + assert.equal(getAssetsPrefix('.webp', prefix), 'https://cdn.com'); + }); + + it('strips leading dot from extension when looking up', () => { + const prefix = { mjs: 'https://mjs.cdn.com', fallback: 'https://cdn.com' }; + assert.equal(getAssetsPrefix('.mjs', prefix), 'https://mjs.cdn.com'); + }); +}); +// #endregion + +// #region etag +describe('etag', () => { + it('returns a deterministic hash for the same input', () => { + const a = etag('hello world'); + const b = etag('hello world'); + assert.equal(a, b); + }); + + it('returns different hashes for different inputs', () => { + assert.notEqual(etag('hello'), etag('world')); + }); + + it('wraps in double quotes by default (strong etag)', () => { + const result = etag('test'); + assert.ok(result.startsWith('"')); + assert.ok(result.endsWith('"')); + }); + + it('wraps with W/ prefix for weak etags', () => { + const result = etag('test', true); + assert.ok(result.startsWith('W/"')); + assert.ok(result.endsWith('"')); + }); + + it('produces different output for strong vs weak', () => { + assert.notEqual(etag('test', false), etag('test', true)); + }); +}); +// #endregion + +// #region deterministicString +describe('deterministicString', () => { + it('orders object keys deterministically', () => { + const a = deterministicString({ b: 2, a: 1 }); + const b = deterministicString({ a: 1, b: 2 }); + assert.equal(a, b); + }); + + it('handles nested objects', () => { + const result = deterministicString({ outer: { z: 1, a: 2 } }); + assert.ok(result.includes('"a"')); + assert.ok(result.includes('"z"')); + }); + + it('handles strings', () => { + assert.equal(deterministicString('hello'), '"hello"'); + }); + + it('handles numbers', () => { + assert.equal(deterministicString(42), '42'); + }); + + it('handles booleans', () => { + assert.equal(deterministicString(true), 'true'); + assert.equal(deterministicString(false), 'false'); + }); + + it('handles null and undefined', () => { + assert.equal(deterministicString(null), 'null'); + assert.equal(deterministicString(undefined), 'undefined'); + }); + + it('handles arrays', () => { + const result = deterministicString([1, 'two', 3]); + assert.ok(result.includes('Array')); + }); + + it('handles Date objects', () => { + const d = new Date('2024-01-01T00:00:00Z'); + const result = deterministicString(d); + assert.ok(result.includes('Date')); + assert.ok(result.includes(String(d.getTime()))); + }); + + it('handles Map', () => { + const m = new Map([ + ['b', 2], + ['a', 1], + ]); + const result = deterministicString(m); + assert.ok(result.includes('Map')); + }); + + it('handles Set', () => { + const s = new Set([3, 1, 2]); + const result = deterministicString(s); + assert.ok(result.includes('Set')); + }); + + it('handles RegExp', () => { + const result = deterministicString(/foo/gi); + assert.ok(result.includes('RegExp')); + assert.ok(result.includes('foo')); + }); + + it('handles bigint', () => { + assert.equal(deterministicString(BigInt(42)), '42n'); + }); +}); +// #endregion + +// #region getOrigQueryParams +describe('getOrigQueryParams', () => { + it('returns parsed width, height, format when all present', () => { + const params = new URLSearchParams('origWidth=800&origHeight=600&origFormat=png'); + const result = getOrigQueryParams(params); + assert.deepEqual(result, { width: 800, height: 600, format: 'png' }); + }); + + it('returns undefined when width is missing', () => { + const params = new URLSearchParams('origHeight=600&origFormat=png'); + assert.equal(getOrigQueryParams(params), undefined); + }); + + it('returns undefined when height is missing', () => { + const params = new URLSearchParams('origWidth=800&origFormat=png'); + assert.equal(getOrigQueryParams(params), undefined); + }); + + it('returns undefined when format is missing', () => { + const params = new URLSearchParams('origWidth=800&origHeight=600'); + assert.equal(getOrigQueryParams(params), undefined); + }); + + it('returns undefined for empty params', () => { + assert.equal(getOrigQueryParams(new URLSearchParams()), undefined); + }); +}); +// #endregion + +// #region createPlaceholderURL / stringifyPlaceholderURL +describe('placeholder URL utilities', () => { + it('createPlaceholderURL creates URL from relative path', () => { + const url = createPlaceholderURL('/images/photo.jpg'); + assert.ok(url instanceof URL); + assert.equal(url.pathname, '/images/photo.jpg'); + }); + + it('createPlaceholderURL preserves query params', () => { + const url = createPlaceholderURL('/img.jpg?w=100'); + assert.equal(url.searchParams.get('w'), '100'); + }); + + it('stringifyPlaceholderURL removes placeholder base', () => { + const url = createPlaceholderURL('/images/photo.jpg'); + const str = stringifyPlaceholderURL(url); + assert.equal(str, '/images/photo.jpg'); + assert.ok(!str.includes('astro://')); + }); + + it('roundtrips path with query and hash', () => { + const url = createPlaceholderURL('/img.jpg?w=100#frag'); + const str = stringifyPlaceholderURL(url); + assert.equal(str, '/img.jpg?w=100#frag'); + }); +}); +// #endregion + +// #region isESMImportedImage / isRemoteImage +describe('image kind detection', () => { + it('isESMImportedImage returns true for objects', () => { + assert.equal( + isESMImportedImage({ src: '/img.jpg', width: 100, height: 100, format: 'jpg' }), + true, + ); + }); + + it('isESMImportedImage returns false for strings', () => { + assert.equal(isESMImportedImage('https://example.com/img.jpg'), false); + }); + + it('isRemoteImage returns true for strings', () => { + assert.equal(isRemoteImage('https://example.com/img.jpg'), true); + }); + + it('isRemoteImage returns false for objects', () => { + assert.equal(isRemoteImage({ src: '/img.jpg', width: 100, height: 100, format: 'jpg' }), false); + }); +}); +// #endregion + +// #region dropAttributes +describe('dropAttributes', () => { + it('removes xmlns, xmlns:xlink, and version', () => { + const attrs = { + xmlns: 'http://www.w3.org/2000/svg', + 'xmlns:xlink': 'http://www.w3.org/1999/xlink', + version: '1.1', + viewBox: '0 0 100 100', + fill: 'red', + }; + const result = dropAttributes(attrs); + assert.equal(result.xmlns, undefined); + assert.equal(result['xmlns:xlink'], undefined); + assert.equal(result.version, undefined); + }); + + it('preserves other attributes', () => { + const attrs = { + xmlns: 'http://www.w3.org/2000/svg', + viewBox: '0 0 100 100', + fill: 'red', + class: 'icon', + }; + const result = dropAttributes(attrs); + assert.equal(result.viewBox, '0 0 100 100'); + assert.equal(result.fill, 'red'); + assert.equal(result.class, 'icon'); + }); + + it('handles empty object', () => { + const result = dropAttributes({}); + assert.deepEqual(result, {}); + }); + + it('handles object without any droppable attributes', () => { + const attrs = { viewBox: '0 0 50 50', fill: 'blue' }; + const result = dropAttributes(attrs); + assert.deepEqual(result, { viewBox: '0 0 50 50', fill: 'blue' }); + }); + + it('mutates and returns the same object', () => { + const attrs = { xmlns: 'test', fill: 'red' }; + const result = dropAttributes(attrs); + assert.equal(result, attrs); + }); +}); +// #endregion diff --git a/packages/astro/test/units/build/generate.test.js b/packages/astro/test/units/build/generate.test.ts similarity index 82% rename from packages/astro/test/units/build/generate.test.js rename to packages/astro/test/units/build/generate.test.ts index 1d9df331b17f..734906cace0d 100644 --- a/packages/astro/test/units/build/generate.test.js +++ b/packages/astro/test/units/build/generate.test.ts @@ -1,4 +1,3 @@ -// @ts-check /** * Unit tests for the renderPath() function in src/core/build/generate.ts. * @@ -12,17 +11,18 @@ */ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; +import type { StaticBuildOptions } from '../../../dist/core/build/types.js'; import { renderPath } from '../../../dist/core/build/generate.js'; import { createComponent, render as renderTemplate, renderComponent, } from '../../../dist/runtime/server/index.js'; -import { createMockPrerenderer, createStaticBuildOptions } from './test-helpers.js'; -import { createRouteData } from '../mocks.js'; +import { createMockPrerenderer, createStaticBuildOptions } from './test-helpers.ts'; +import { createRouteData } from '../mocks.ts'; describe('renderPath()', () => { - let options; + let options: StaticBuildOptions; before(async () => { options = await createStaticBuildOptions(); @@ -132,7 +132,7 @@ describe('renderPath()', () => { it('populates routeToHeaders when adapter requests static headers', async () => { const prerenderer = createMockPrerenderer({ '/page': 'Page' }); const route = createRouteData({ route: '/page' }); - const routeToHeaders = new Map(); + const routeToHeaders = new Map(); const adapterOptions = await createStaticBuildOptions({ adapter: { adapterFeatures: { staticHeaders: true } }, }); @@ -153,7 +153,7 @@ describe('renderPath()', () => { it('does NOT populate routeToHeaders when adapter does not request static headers', async () => { const prerenderer = createMockPrerenderer({ '/page': 'Page' }); const route = createRouteData({ route: '/page' }); - const routeToHeaders = new Map(); + const routeToHeaders = new Map(); await renderPath({ prerenderer, @@ -176,8 +176,8 @@ describe('renderPath()', () => { pages: { 'public/index.html': 'public file' }, }); - const warnings = []; - conflictOptions.logger.warn = (_label, msg) => warnings.push(msg); + const warnings: string[] = []; + (conflictOptions.logger as any).warn = (_label: string, msg: string) => warnings.push(msg); const result = await renderPath({ prerenderer, @@ -201,8 +201,8 @@ describe('renderPath()', () => { }; const route = createRouteData({ route: '/boom' }); - const errors = []; - options.logger.error = (_label, msg) => errors.push(msg); + const errors: string[] = []; + (options.logger as any).error = (_label: string, msg: string) => errors.push(msg); await assert.rejects( () => renderPath({ prerenderer, pathname: '/boom', route, options, logger: options.logger }), @@ -211,6 +211,44 @@ describe('renderPath()', () => { assert.ok(errors.length > 0, 'error should be logged before re-throwing'); }); + // Regression: #16185 — extensionless endpoints with trailingSlash: 'always' + // must have a trailing slash in the prerender request URL so that BaseApp.render() + // does not emit a redirect instead of the endpoint's actual response. + it('sends a trailing-slash request URL for extensionless endpoints when trailingSlash is always', async () => { + const endpointOptions = await createStaticBuildOptions({ + inlineConfig: { trailingSlash: 'always' }, + }); + + let capturedUrl: URL | undefined; + const prerenderer = createMockPrerenderer({ '/demo': 'hello' }); + const originalRender = prerenderer.render.bind(prerenderer); + prerenderer.render = async (request: Request, opts: any) => { + capturedUrl = new URL(request.url); + return originalRender(request, opts); + }; + + const route = createRouteData({ + route: '/demo', + type: 'endpoint', + trailingSlash: 'always', + component: 'src/pages/demo.ts', + }); + + await renderPath({ + prerenderer, + pathname: '/demo', + route, + options: endpointOptions, + logger: endpointOptions.logger, + }); + + assert.ok(capturedUrl, 'prerenderer.render should have been called'); + assert.ok( + capturedUrl.pathname.endsWith('/'), + `expected trailing slash in request URL pathname, got "${capturedUrl.pathname}"`, + ); + }); + it('writes the rendered body to the filesystem (integration smoke)', async () => { const html = 'Written to disk'; const prerenderer = createMockPrerenderer({ '/disk-test': html }); @@ -237,14 +275,14 @@ describe('renderPath()', () => { // --------------------------------------------------------------------------- describe('createMockPrerenderer with ComponentInstance', () => { - let options; + let options: StaticBuildOptions; before(async () => { options = await createStaticBuildOptions(); }); it('renders a bare ComponentInstance to HTML via RenderContext', async () => { - const Page = createComponent((_result) => renderTemplate`

Hello from component

`); + const Page = createComponent((_result: any) => renderTemplate`

Hello from component

`); const prerenderer = createMockPrerenderer({ '/': { default: Page } }); const route = createRouteData({ route: '/' }); @@ -270,7 +308,8 @@ describe('createMockPrerenderer with ComponentInstance', () => { it('passes props to a ComponentInstance via the props key', async () => { const Page = createComponent( - (_result, { title }) => renderTemplate`${title}

${title}

`, + (_result: any, { title }: { title: string }) => + renderTemplate`${title}

${title}

`, ); const prerenderer = createMockPrerenderer({ '/blog/hello': { default: Page, props: { title: 'Hello World' } }, @@ -294,10 +333,11 @@ describe('createMockPrerenderer with ComponentInstance', () => { it('renders nested components', async () => { const Inner = createComponent( - (_result, { label }) => renderTemplate`${label}`, + (_result: any, { label }: { label: string }) => + renderTemplate`${label}`, ); const Page = createComponent( - (result) => + (result: any) => renderTemplate`
${renderComponent(result, 'Inner', Inner, { label: 'nested' })}
`, ); const prerenderer = createMockPrerenderer({ '/nested': { default: Page } }); @@ -319,7 +359,7 @@ describe('createMockPrerenderer with ComponentInstance', () => { }); it('falls back to string pages and ComponentInstance pages in the same prerenderer', async () => { - const Component = createComponent((_result) => renderTemplate`

component page

`); + const Component = createComponent((_result: any) => renderTemplate`

component page

`); const prerenderer = createMockPrerenderer({ '/string': '

string page

', '/component': { default: Component }, @@ -356,7 +396,7 @@ describe('createMockPrerenderer with ComponentInstance', () => { route, options, logger: options.logger, - }).catch((e) => e); + }).catch((e: unknown) => e); assert.ok(err instanceof Error, 'should throw an Error'); assert.ok(err.message.includes('/not-registered'), 'error should name the missing pathname'); diff --git a/packages/astro/test/units/build/preserve-build-client-dir.test.js b/packages/astro/test/units/build/preserve-build-client-dir.test.ts similarity index 63% rename from packages/astro/test/units/build/preserve-build-client-dir.test.js rename to packages/astro/test/units/build/preserve-build-client-dir.test.ts index b723a8335fc6..e178398f1134 100644 --- a/packages/astro/test/units/build/preserve-build-client-dir.test.js +++ b/packages/astro/test/units/build/preserve-build-client-dir.test.ts @@ -1,8 +1,10 @@ import * as assert from 'node:assert/strict'; import { describe, it } from 'node:test'; +import type { AstroSettings } from '../../../dist/types/astro.js'; +import type { RouteData } from '../../../dist/types/public/internal.js'; import { getOutFolder } from '../../../dist/core/build/common.js'; import { getClientOutputDirectory } from '../../../dist/prerender/utils.js'; -import { createSettings } from './test-helpers.js'; +import { createSettings } from './test-helpers.ts'; describe('preserveBuildClientDir', () => { const outDir = new URL('file:///project/dist/'); @@ -10,48 +12,57 @@ describe('preserveBuildClientDir', () => { describe('getClientOutputDirectory', () => { it('returns outDir for static builds without preserveBuildClientDir', () => { - const settings = createSettings({ buildOutput: 'static' }); + const settings = createSettings({ buildOutput: 'static' }) as unknown as AstroSettings; const result = getClientOutputDirectory(settings); assert.equal(result.href, outDir.href); }); it('returns client dir for static builds with preserveBuildClientDir', () => { - const settings = createSettings({ buildOutput: 'static', preserveBuildClientDir: true }); + const settings = createSettings({ + buildOutput: 'static', + preserveBuildClientDir: true, + }) as unknown as AstroSettings; const result = getClientOutputDirectory(settings); assert.equal(result.href, clientDir.href); }); it('returns client dir for server builds regardless of preserveBuildClientDir', () => { - const settings = createSettings({ buildOutput: 'server' }); + const settings = createSettings({ buildOutput: 'server' }) as unknown as AstroSettings; const result = getClientOutputDirectory(settings); assert.equal(result.href, clientDir.href); }); }); describe('getOutFolder', () => { - const pageRoute = { type: 'page', isIndex: false }; + const pageRoute = { type: 'page', isIndex: false } as unknown as RouteData; it('outputs to outDir for static builds without preserveBuildClientDir', () => { - const settings = createSettings({ buildOutput: 'static' }); + const settings = createSettings({ buildOutput: 'static' }) as unknown as AstroSettings; const result = getOutFolder(settings, '/about', pageRoute); assert.equal(result.href, new URL('about/', outDir).href); }); it('outputs to client dir for static builds with preserveBuildClientDir', () => { - const settings = createSettings({ buildOutput: 'static', preserveBuildClientDir: true }); + const settings = createSettings({ + buildOutput: 'static', + preserveBuildClientDir: true, + }) as unknown as AstroSettings; const result = getOutFolder(settings, '/about', pageRoute); assert.equal(result.href, new URL('about/', clientDir).href); }); it('outputs to client dir for server builds regardless of preserveBuildClientDir', () => { - const settings = createSettings({ buildOutput: 'server' }); + const settings = createSettings({ buildOutput: 'server' }) as unknown as AstroSettings; const result = getOutFolder(settings, '/about', pageRoute); assert.equal(result.href, new URL('about/', clientDir).href); }); it('outputs root index to client dir with preserveBuildClientDir', () => { - const settings = createSettings({ buildOutput: 'static', preserveBuildClientDir: true }); - const indexRoute = { type: 'page', isIndex: true }; + const settings = createSettings({ + buildOutput: 'static', + preserveBuildClientDir: true, + }) as unknown as AstroSettings; + const indexRoute = { type: 'page', isIndex: true } as unknown as RouteData; const result = getOutFolder(settings, '/', indexRoute); assert.equal(result.href, new URL('./', clientDir).href); }); diff --git a/packages/astro/test/units/build/server-islands.test.js b/packages/astro/test/units/build/server-islands.test.ts similarity index 96% rename from packages/astro/test/units/build/server-islands.test.js rename to packages/astro/test/units/build/server-islands.test.ts index 319ff88a0075..e5b5abb1b37f 100644 --- a/packages/astro/test/units/build/server-islands.test.js +++ b/packages/astro/test/units/build/server-islands.test.ts @@ -3,12 +3,13 @@ import { promises as fs } from 'node:fs'; import path from 'node:path'; import { describe, it } from 'node:test'; import { fileURLToPath, pathToFileURL } from 'node:url'; +import type { Plugin } from 'vite'; import { AstroBuilder } from '../../../dist/core/build/index.js'; import { parseRoute } from '../../../dist/core/routing/parse-route.js'; -import { createBasicSettings, defaultLogger } from '../test-utils.js'; -import { virtualAstroModules } from './test-helpers.js'; +import { createBasicSettings, defaultLogger } from '../test-utils.ts'; +import { virtualAstroModules } from './test-helpers.ts'; -async function readFilesRecursive(dir) { +async function readFilesRecursive(dir: string): Promise { const entries = await fs.readdir(dir, { withFileTypes: true }); const files = await Promise.all( entries.map(async (entry) => { @@ -22,11 +23,11 @@ async function readFilesRecursive(dir) { return files.flat(); } -function forceDoubleQuotedServerIslandPlaceholders() { +function forceDoubleQuotedServerIslandPlaceholders(): Plugin { return { name: 'force-double-quoted-server-island-placeholders', enforce: 'pre', - renderChunk(code) { + renderChunk(code: string) { if (!code.includes("'$$server-islands-map$$'")) { return; } diff --git a/packages/astro/test/units/build/static-build.test.js b/packages/astro/test/units/build/static-build.test.ts similarity index 65% rename from packages/astro/test/units/build/static-build.test.js rename to packages/astro/test/units/build/static-build.test.ts index 487ebf10c6cd..8e1bbdac8446 100644 --- a/packages/astro/test/units/build/static-build.test.js +++ b/packages/astro/test/units/build/static-build.test.ts @@ -1,10 +1,35 @@ import * as assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { makeAstroPageEntryPointFileName } from '../../../dist/core/build/static-build.js'; +import { cleanChunkName } from '../../../dist/core/build/util.js'; +import type { RouteData } from '../../../dist/types/public/internal.js'; describe('astro/src/core/build', () => { + describe('cleanChunkName', () => { + it('passes through safe names unchanged', () => { + assert.equal(cleanChunkName('page'), 'page'); + assert.equal(cleanChunkName('my-component'), 'my-component'); + assert.equal(cleanChunkName('pages/index'), 'pages/index'); + assert.equal(cleanChunkName('chunk_abc123'), 'chunk_abc123'); + }); + + it('replaces ! and ~ characters', () => { + assert.equal(cleanChunkName('page.!{005}'), 'page.__005_'); + assert.equal(cleanChunkName('~something'), '_something'); + }); + + it('replaces other unsafe characters', () => { + assert.equal(cleanChunkName('name@scope'), 'name_scope'); + assert.equal(cleanChunkName('file#hash'), 'file_hash'); + }); + + it('replaces % character', () => { + assert.equal(cleanChunkName('chunk%name'), 'chunk_name'); + }); + }); + describe('makeAstroPageEntryPointFileName', () => { - const routes = [ + const routes: RouteData[] = [ { route: '/', component: 'src/pages/index.astro', @@ -25,7 +50,7 @@ describe('astro/src/core/build', () => { component: 'src/pages/blog/[year]/[...slug].astro', pathname: undefined, }, - ]; + ] as RouteData[]; it('handles local pages', async () => { const input = '@astro-page:src/pages/index@_@astro'; diff --git a/packages/astro/test/units/build/test-helpers.js b/packages/astro/test/units/build/test-helpers.ts similarity index 76% rename from packages/astro/test/units/build/test-helpers.js rename to packages/astro/test/units/build/test-helpers.ts index 52a67ba5962c..232a3d15cba0 100644 --- a/packages/astro/test/units/build/test-helpers.js +++ b/packages/astro/test/units/build/test-helpers.ts @@ -1,26 +1,29 @@ -// @ts-check -import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs'; +import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; +import type { Plugin } from 'vite'; import { RenderContext } from '../../../dist/core/render-context.js'; import { createRoutesList as _createRoutesList } from '../../../dist/core/routing/create-manifest.js'; -import { createBasicPipeline, createBasicSettings, defaultLogger } from '../test-utils.js'; +import type { StaticBuildOptions } from '../../../dist/core/build/types.js'; +import type { Pipeline } from '../../../dist/core/base-pipeline.js'; +import type { RouteData } from '../../../dist/types/public/internal.js'; +import type { AstroInlineConfig } from '../../../dist/types/public/config.js'; +import type { ComponentInstance } from '../../../dist/types/astro.js'; +import { createBasicPipeline, createBasicSettings, defaultLogger } from '../test-utils.ts'; -/** - * @param {object} options - * @param {'static' | 'server'} options.buildOutput - * @param {boolean} [options.preserveBuildClientDir] - * @param {URL} [options.outDir] - * @param {URL} [options.clientDir] - * @param {'directory' | 'file' | 'preserve'} [options.buildFormat] - */ export function createSettings({ buildOutput, preserveBuildClientDir = false, outDir = new URL('file:///project/dist/'), clientDir = new URL('file:///project/dist/client/'), buildFormat = 'directory', +}: { + buildOutput: 'static' | 'server'; + preserveBuildClientDir?: boolean; + outDir?: URL; + clientDir?: URL; + buildFormat?: 'directory' | 'file' | 'preserve'; }) { return { buildOutput, @@ -37,12 +40,9 @@ export function createSettings({ /** * A Vite plugin that provides in-memory .astro source files as virtual modules. * This allows running a full Astro build without any files on disk. - * - * @param {URL} root - The project root URL - * @param {Record} files - Map of relative paths (e.g. 'src/pages/index.astro') to source content */ -export function virtualAstroModules(root, files) { - const virtualFiles = new Map(); +export function virtualAstroModules(root: URL, files: Record): Plugin { + const virtualFiles = new Map(); for (const [relativePath, source] of Object.entries(files)) { const absolute = fileURLToPath(new URL(relativePath, root)); virtualFiles.set(absolute, source); @@ -52,7 +52,7 @@ export function virtualAstroModules(root, files) { name: 'virtual-astro-modules', enforce: 'pre', resolveId: { - handler(id, importer) { + handler(id: string, importer: string | undefined) { if (virtualFiles.has(id)) return id; if (id.startsWith('/')) { const absolute = fileURLToPath(new URL('.' + id, root)); @@ -65,8 +65,8 @@ export function virtualAstroModules(root, files) { }, }, load: { - handler(id) { - if (virtualFiles.has(id)) return { code: virtualFiles.get(id) }; + handler(id: string) { + if (virtualFiles.has(id)) return { code: virtualFiles.get(id)! }; }, }, }; @@ -78,11 +78,8 @@ export function virtualAstroModules(root, files) { * * All page paths are project-relative (e.g. `'src/pages/index.astro'`). * Call `cleanup()` when done to remove the directory. - * - * @param {Record} [initialFiles] - * @returns {URL} */ -function createTmpRootDir(initialFiles = {}) { +function createTmpRootDir(initialFiles: Record = {}): URL { const rootPath = mkdtempSync(join(tmpdir(), 'astro-test-')); for (const [relativePath, content] of Object.entries(initialFiles)) { const absPath = join(rootPath, relativePath); @@ -100,23 +97,13 @@ function createTmpRootDir(initialFiles = {}) { * directory, `createRoutesList` scans them, and `cleanup()` removes them when * the test is done. Without `pages`, an empty options object is returned * (no routes, no disk I/O). - * - * @param {object} [overrides] - * @param {Record} [overrides.pages] - * Map of project-relative paths (e.g. `'src/pages/index.astro'`) to source - * content. Written to a temp directory and scanned to produce `routesList`. - * @param {'static' | 'server'} [overrides.buildOutput] - * @param {object | undefined} [overrides.adapter] - * @param {any} [overrides.inlineConfig] - * Astro inline config overrides (e.g. `i18n`, `base`, `trailingSlash`). - * @returns {Promise} */ export async function createStaticBuildOptions({ pages = {}, - buildOutput = /** @type {'static'} */ ('static'), - adapter = undefined, - inlineConfig = {}, -} = {}) { + buildOutput = 'static' as 'static' | 'server', + adapter = undefined as object | undefined, + inlineConfig = {} as AstroInlineConfig, +} = {}): Promise { const hasPages = Object.keys(pages).length > 0; // Write page sources to a real temp directory so createRoutesList can scan them. @@ -125,7 +112,7 @@ export async function createStaticBuildOptions({ ? createTmpRootDir(pages) : pathToFileURL(mkdtempSync(join(tmpdir(), 'astro-test-')) + '/'); - const resolvedConfig = /** @type {any} */ ({ + const resolvedConfig = { root: rootUrl, srcDir: new URL('src/', rootUrl), outDir: new URL('dist/', rootUrl), @@ -142,9 +129,9 @@ export async function createStaticBuildOptions({ server: new URL('dist/server/', rootUrl), ...(inlineConfig.build ?? {}), }, - }); + }; - let routesList = { routes: [] }; + let routesList: { routes: RouteData[] } = { routes: [] }; if (hasPages) { const settings = await createBasicSettings({ root: fileURLToPath(rootUrl), @@ -154,7 +141,7 @@ export async function createStaticBuildOptions({ routesList = await _createRoutesList({ settings }, defaultLogger); } - const options = /** @type {any} */ ({ + const options = { origin: 'http://localhost:4321', pageNames: [], routesList, @@ -164,11 +151,25 @@ export async function createStaticBuildOptions({ config: resolvedConfig, }, logger: { info() {}, warn() {}, error() {}, debug() {} }, - }); + } as unknown as StaticBuildOptions; return options; } +/** Page value: raw HTML string, string factory, a Response, or a ComponentInstance with optional props. */ +type PageValue = + | string + | (() => string) + | Response + | (ComponentInstance & { props?: Record }); + +/** Minimal shape matching `AstroPrerenderer` from the public integrations API. */ +interface MockPrerenderer { + name: string; + getStaticPaths: () => Promise<{ pathname: string; route: RouteData }[]>; + render: (request: Request, options: { routeData: RouteData }) => Promise; +} + /** * Creates a minimal `AstroPrerenderer` backed by an in-memory map of pathnames * to page definitions. @@ -194,25 +195,23 @@ export async function createStaticBuildOptions({ * pair — the same shape as `PathWithRoute` in the public integrations API. * * @example Basic usage - * ```js + * ```ts * const prerenderer = createMockPrerenderer({ * '/about': 'About', * '/old': new Response(null, { status: 301, headers: { location: '/new' } }), * }); * ``` - * - * @param {Record string) | Response | (import('../../../dist/types/astro.js').ComponentInstance & { props?: Record })>} pages - * @param {{ staticPaths?: import('../../..').PathWithRoute[] }} [options] - * @returns {import('../../..').AstroPrerenderer} */ -export function createMockPrerenderer(pages, options = {}) { +export function createMockPrerenderer( + pages: Record, + options: { staticPaths?: { pathname: string; route: RouteData }[] } = {}, +): MockPrerenderer { const { staticPaths } = options; /** Lazily-created shared pipeline — one per prerenderer instance. */ - let _pipeline = null; + let _pipeline: Pipeline | null = null; - /** @returns {import('../../../dist/core/base-pipeline.js').Pipeline} */ - function getPipeline() { + function getPipeline(): Pipeline { if (!_pipeline) _pipeline = createBasicPipeline(); return _pipeline; } @@ -224,7 +223,7 @@ export function createMockPrerenderer(pages, options = {}) { return staticPaths ?? []; }, - async render(request, { routeData }) { + async render(request: Request, { routeData }: { routeData: RouteData }) { // For static routes routeData.pathname is the canonical key. // For dynamic routes (pathname === undefined), derive it from the // request URL by stripping build-format artifacts (trailing slash, .html). @@ -257,7 +256,9 @@ export function createMockPrerenderer(pages, options = {}) { // ── ComponentInstance: { default: Component, props? } ──────────── // Everything else is treated as a ComponentInstance and rendered via // RenderContext, letting the pipeline handle it naturally. - const { props = {}, ...componentInstance } = /** @type {any} */ (page); + const { props = {}, ...componentInstance } = page as ComponentInstance & { + props?: Record; + }; const ctx = await RenderContext.create({ pipeline: getPipeline(), request, diff --git a/packages/astro/test/units/cache/memory-provider.test.js b/packages/astro/test/units/cache/memory-provider.test.ts similarity index 81% rename from packages/astro/test/units/cache/memory-provider.test.js rename to packages/astro/test/units/cache/memory-provider.test.ts index 952365af187c..3f3b385df16a 100644 --- a/packages/astro/test/units/cache/memory-provider.test.js +++ b/packages/astro/test/units/cache/memory-provider.test.ts @@ -1,35 +1,44 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; +import type { CacheProvider } from '../../../dist/core/cache/types.js'; +import type { MemoryCacheProviderOptions } from '../../../dist/core/cache/memory-provider.js'; import memoryProvider from '../../../dist/core/cache/memory-provider.js'; /** * Helper: create a CacheProvider instance with optional config. */ -function createProvider(config) { +function createProvider(config?: MemoryCacheProviderOptions): CacheProvider { return memoryProvider(config); } /** * Helper: create a minimal Request. */ -function makeRequest(url, headers = {}) { +function makeRequest(url: string, headers: Record = {}): Request { return new Request(url, { headers }); } /** * Helper: create a next() function that returns a Response with cache headers. - * @param {object} opts - * @param {string} [opts.body='ok'] - * @param {number} [opts.status=200] - * @param {number} [opts.maxAge] - * @param {number} [opts.swr] - * @param {string[]} [opts.tags] - * @param {Record} [opts.headers] */ -function makeNext({ body = 'ok', status = 200, maxAge, swr, tags, headers = {} } = {}) { +function makeNext({ + body = 'ok', + status = 200, + maxAge, + swr, + tags, + headers = {}, +}: { + body?: string; + status?: number; + maxAge?: number; + swr?: number; + tags?: string[]; + headers?: Record; +} = {}): () => Promise { return async () => { const h = new Headers(headers); - const parts = []; + const parts: string[] = []; if (maxAge !== undefined) parts.push(`max-age=${maxAge}`); if (swr !== undefined) parts.push(`stale-while-revalidate=${swr}`); if (parts.length > 0) h.set('CDN-Cache-Control', parts.join(', ')); @@ -38,13 +47,13 @@ function makeNext({ body = 'ok', status = 200, maxAge, swr, tags, headers = {} } }; } -// ─── onRequest: basic caching ──────────────────────────────────────────────── +// #region onRequest: basic caching describe('memory-provider onRequest', () => { it('passes through when no cache headers on response', async () => { const provider = createProvider(); const req = makeRequest('http://localhost/page'); - const res = await provider.onRequest({ request: req, url: new URL(req.url) }, makeNext()); + const res = await provider.onRequest!({ request: req, url: new URL(req.url) }, makeNext()); assert.equal(await res.text(), 'ok'); assert.equal(res.headers.has('X-Astro-Cache'), false); }); @@ -52,7 +61,7 @@ describe('memory-provider onRequest', () => { it('returns MISS on first cacheable request', async () => { const provider = createProvider(); const req = makeRequest('http://localhost/page'); - const res = await provider.onRequest( + const res = await provider.onRequest!( { request: req, url: new URL(req.url) }, makeNext({ maxAge: 60 }), ); @@ -66,14 +75,14 @@ describe('memory-provider onRequest', () => { // First request — MISS const req1 = makeRequest(url); - await provider.onRequest( + await provider.onRequest!( { request: req1, url: new URL(req1.url) }, makeNext({ maxAge: 60, body: 'first' }), ); // Second request — HIT const req2 = makeRequest(url); - const res2 = await provider.onRequest( + const res2 = await provider.onRequest!( { request: req2, url: new URL(req2.url) }, makeNext({ maxAge: 60, body: 'second' }), ); @@ -85,7 +94,7 @@ describe('memory-provider onRequest', () => { const provider = createProvider(); const req = new Request('http://localhost/page', { method: 'POST' }); let called = false; - const res = await provider.onRequest({ request: req, url: new URL(req.url) }, async () => { + const res = await provider.onRequest!({ request: req, url: new URL(req.url) }, async () => { called = true; return new Response('posted'); }); @@ -100,7 +109,7 @@ describe('memory-provider onRequest', () => { // First request — has Set-Cookie, should not cache const req1 = makeRequest(url); - await provider.onRequest( + await provider.onRequest!( { request: req1, url: new URL(req1.url) }, makeNext({ maxAge: 60, headers: { 'Set-Cookie': 'session=abc' } }), ); @@ -108,7 +117,7 @@ describe('memory-provider onRequest', () => { // Second request — should be a miss (not cached) const req2 = makeRequest(url); let nextCalled = false; - await provider.onRequest({ request: req2, url: new URL(req2.url) }, async () => { + await provider.onRequest!({ request: req2, url: new URL(req2.url) }, async () => { nextCalled = true; const h = new Headers({ 'CDN-Cache-Control': 'max-age=60' }); return new Response('fresh', { headers: h }); @@ -117,20 +126,22 @@ describe('memory-provider onRequest', () => { }); }); -// ─── onRequest: host-aware keys ────────────────────────────────────────────── +// #endregion + +// #region onRequest: host-aware keys describe('memory-provider host-aware cache keys', () => { it('different hosts produce different cache entries', async () => { const provider = createProvider(); const req1 = makeRequest('http://host-a.com/page'); - await provider.onRequest( + await provider.onRequest!( { request: req1, url: new URL(req1.url) }, makeNext({ maxAge: 60, body: 'host-a' }), ); const req2 = makeRequest('http://host-b.com/page'); - const res2 = await provider.onRequest( + const res2 = await provider.onRequest!( { request: req2, url: new URL(req2.url) }, makeNext({ maxAge: 60, body: 'host-b' }), ); @@ -140,21 +151,23 @@ describe('memory-provider host-aware cache keys', () => { }); }); -// ─── onRequest: query parameter handling ───────────────────────────────────── +// #endregion + +// #region onRequest: query parameter handling describe('memory-provider query parameters', () => { it('sorts query parameters by default (order-independent keys)', async () => { const provider = createProvider(); const req1 = makeRequest('http://localhost/page?b=2&a=1'); - await provider.onRequest( + await provider.onRequest!( { request: req1, url: new URL(req1.url) }, makeNext({ maxAge: 60, body: 'first' }), ); // Same params, different order — should HIT const req2 = makeRequest('http://localhost/page?a=1&b=2'); - const res2 = await provider.onRequest( + const res2 = await provider.onRequest!( { request: req2, url: new URL(req2.url) }, makeNext({ maxAge: 60, body: 'second' }), ); @@ -165,13 +178,13 @@ describe('memory-provider query parameters', () => { const provider = createProvider(); const req1 = makeRequest('http://localhost/page'); - await provider.onRequest( + await provider.onRequest!( { request: req1, url: new URL(req1.url) }, makeNext({ maxAge: 60, body: 'first' }), ); const req2 = makeRequest('http://localhost/page?utm_source=twitter&utm_medium=social'); - const res2 = await provider.onRequest( + const res2 = await provider.onRequest!( { request: req2, url: new URL(req2.url) }, makeNext({ maxAge: 60, body: 'second' }), ); @@ -182,13 +195,13 @@ describe('memory-provider query parameters', () => { const provider = createProvider(); const req1 = makeRequest('http://localhost/page?page=2'); - await provider.onRequest( + await provider.onRequest!( { request: req1, url: new URL(req1.url) }, makeNext({ maxAge: 60, body: 'page-2' }), ); const req2 = makeRequest('http://localhost/page?page=2&fbclid=abc123'); - const res2 = await provider.onRequest( + const res2 = await provider.onRequest!( { request: req2, url: new URL(req2.url) }, makeNext({ maxAge: 60, body: 'should-not-see' }), ); @@ -199,13 +212,13 @@ describe('memory-provider query parameters', () => { const provider = createProvider(); const req1 = makeRequest('http://localhost/page?page=3'); - await provider.onRequest( + await provider.onRequest!( { request: req1, url: new URL(req1.url) }, makeNext({ maxAge: 60, body: 'page-3' }), ); const req2 = makeRequest('http://localhost/page?page=3&gclid=xyz789'); - const res2 = await provider.onRequest( + const res2 = await provider.onRequest!( { request: req2, url: new URL(req2.url) }, makeNext({ maxAge: 60, body: 'should-not-see' }), ); @@ -216,13 +229,13 @@ describe('memory-provider query parameters', () => { const provider = createProvider(); const req1 = makeRequest('http://localhost/page'); - await provider.onRequest( + await provider.onRequest!( { request: req1, url: new URL(req1.url) }, makeNext({ maxAge: 60, body: 'no-params' }), ); const req2 = makeRequest('http://localhost/page?utm_source=twitter&fbclid=abc&gclid=xyz'); - const res2 = await provider.onRequest( + const res2 = await provider.onRequest!( { request: req2, url: new URL(req2.url) }, makeNext({ maxAge: 60, body: 'should-not-see' }), ); @@ -233,13 +246,13 @@ describe('memory-provider query parameters', () => { const provider = createProvider(); const req1 = makeRequest('http://localhost/page?id=1'); - await provider.onRequest( + await provider.onRequest!( { request: req1, url: new URL(req1.url) }, makeNext({ maxAge: 60, body: 'id-1' }), ); const req2 = makeRequest('http://localhost/page?id=2'); - const res2 = await provider.onRequest( + const res2 = await provider.onRequest!( { request: req2, url: new URL(req2.url) }, makeNext({ maxAge: 60, body: 'id-2' }), ); @@ -250,14 +263,14 @@ describe('memory-provider query parameters', () => { const provider = createProvider({ query: { include: ['page'] } }); const req1 = makeRequest('http://localhost/list?page=1&sort=name'); - await provider.onRequest( + await provider.onRequest!( { request: req1, url: new URL(req1.url) }, makeNext({ maxAge: 60, body: 'page-1' }), ); // Different sort but same page — should HIT (sort not in include list) const req2 = makeRequest('http://localhost/list?page=1&sort=date'); - const res2 = await provider.onRequest( + const res2 = await provider.onRequest!( { request: req2, url: new URL(req2.url) }, makeNext({ maxAge: 60, body: 'page-1-date' }), ); @@ -268,13 +281,13 @@ describe('memory-provider query parameters', () => { const provider = createProvider({ query: { include: ['page'] } }); const req1 = makeRequest('http://localhost/list'); - await provider.onRequest( + await provider.onRequest!( { request: req1, url: new URL(req1.url) }, makeNext({ maxAge: 60, body: 'no-params' }), ); const req2 = makeRequest('http://localhost/list?sort=name&filter=active'); - const res2 = await provider.onRequest( + const res2 = await provider.onRequest!( { request: req2, url: new URL(req2.url) }, makeNext({ maxAge: 60, body: 'should-not-see' }), ); @@ -285,13 +298,13 @@ describe('memory-provider query parameters', () => { const provider = createProvider({ query: { exclude: ['session_*'] } }); const req1 = makeRequest('http://localhost/page?id=1'); - await provider.onRequest( + await provider.onRequest!( { request: req1, url: new URL(req1.url) }, makeNext({ maxAge: 60, body: 'first' }), ); const req2 = makeRequest('http://localhost/page?id=1&session_id=abc'); - const res2 = await provider.onRequest( + const res2 = await provider.onRequest!( { request: req2, url: new URL(req2.url) }, makeNext({ maxAge: 60, body: 'second' }), ); @@ -305,7 +318,9 @@ describe('memory-provider query parameters', () => { }); }); -// ─── onRequest: Vary header support ────────────────────────────────────────── +// #endregion + +// #region onRequest: Vary header support describe('memory-provider Vary header', () => { it('caches different entries for different Vary header values', async () => { @@ -314,14 +329,14 @@ describe('memory-provider Vary header', () => { // First request: Accept-Language: en const req1 = makeRequest(url, { 'Accept-Language': 'en' }); - await provider.onRequest( + await provider.onRequest!( { request: req1, url: new URL(req1.url) }, makeNext({ maxAge: 60, body: 'english', headers: { Vary: 'Accept-Language' } }), ); // Second request: Accept-Language: fr — should MISS const req2 = makeRequest(url, { 'Accept-Language': 'fr' }); - const res2 = await provider.onRequest( + const res2 = await provider.onRequest!( { request: req2, url: new URL(req2.url) }, makeNext({ maxAge: 60, body: 'french', headers: { Vary: 'Accept-Language' } }), ); @@ -330,7 +345,7 @@ describe('memory-provider Vary header', () => { // Third request: Accept-Language: en — should HIT from first const req3 = makeRequest(url, { 'Accept-Language': 'en' }); - const res3 = await provider.onRequest( + const res3 = await provider.onRequest!( { request: req3, url: new URL(req3.url) }, makeNext({ maxAge: 60, body: 'should-not-see' }), ); @@ -343,14 +358,14 @@ describe('memory-provider Vary header', () => { const url = 'http://localhost/page'; const req1 = makeRequest(url, { Cookie: 'user=a' }); - await provider.onRequest( + await provider.onRequest!( { request: req1, url: new URL(req1.url) }, makeNext({ maxAge: 60, body: 'first', headers: { Vary: 'Cookie' } }), ); // Different cookie — should still HIT (Cookie is ignored in Vary) const req2 = makeRequest(url, { Cookie: 'user=b' }); - const res2 = await provider.onRequest( + const res2 = await provider.onRequest!( { request: req2, url: new URL(req2.url) }, makeNext({ maxAge: 60, body: 'second' }), ); @@ -358,7 +373,9 @@ describe('memory-provider Vary header', () => { }); }); -// ─── onRequest: LRU eviction ───────────────────────────────────────────────── +// #endregion + +// #region onRequest: LRU eviction describe('memory-provider LRU eviction', () => { it('evicts oldest entry when max is exceeded', async () => { @@ -367,7 +384,7 @@ describe('memory-provider LRU eviction', () => { // Fill cache with 2 entries for (const path of ['/a', '/b']) { const req = makeRequest(`http://localhost${path}`); - await provider.onRequest( + await provider.onRequest!( { request: req, url: new URL(req.url) }, makeNext({ maxAge: 60, body: path }), ); @@ -375,14 +392,14 @@ describe('memory-provider LRU eviction', () => { // Add a third — should evict /a (oldest) const req3 = makeRequest('http://localhost/c'); - await provider.onRequest( + await provider.onRequest!( { request: req3, url: new URL(req3.url) }, makeNext({ maxAge: 60, body: '/c' }), ); // /b should still be cached (HIT) const reqB = makeRequest('http://localhost/b'); - const resB = await provider.onRequest( + const resB = await provider.onRequest!( { request: reqB, url: new URL(reqB.url) }, makeNext({ maxAge: 60, body: '/b-new' }), ); @@ -390,7 +407,7 @@ describe('memory-provider LRU eviction', () => { // /c should still be cached (HIT) const reqC = makeRequest('http://localhost/c'); - const resC = await provider.onRequest( + const resC = await provider.onRequest!( { request: reqC, url: new URL(reqC.url) }, makeNext({ maxAge: 60, body: '/c-new' }), ); @@ -399,7 +416,7 @@ describe('memory-provider LRU eviction', () => { // /a should have been evicted (MISS) — check without caching the result // by using a next() that returns no cache headers const reqA = makeRequest('http://localhost/a'); - const resA = await provider.onRequest( + const resA = await provider.onRequest!( { request: reqA, url: new URL(reqA.url) }, makeNext({ body: '/a-evicted' }), ); @@ -407,7 +424,9 @@ describe('memory-provider LRU eviction', () => { }); }); -// ─── invalidate ────────────────────────────────────────────────────────────── +// #endregion + +// #region invalidate describe('memory-provider invalidate', () => { it('invalidates by tag', async () => { @@ -416,14 +435,14 @@ describe('memory-provider invalidate', () => { // Cache an entry with tags const req1 = makeRequest(url); - await provider.onRequest( + await provider.onRequest!( { request: req1, url: new URL(req1.url) }, makeNext({ maxAge: 60, tags: ['product'] }), ); // Verify cached const req2 = makeRequest(url); - const res2 = await provider.onRequest( + const res2 = await provider.onRequest!( { request: req2, url: new URL(req2.url) }, makeNext({ maxAge: 60 }), ); @@ -434,7 +453,7 @@ describe('memory-provider invalidate', () => { // Should be MISS now const req3 = makeRequest(url); - const res3 = await provider.onRequest( + const res3 = await provider.onRequest!( { request: req3, url: new URL(req3.url) }, makeNext({ maxAge: 60, body: 'fresh' }), ); @@ -447,7 +466,7 @@ describe('memory-provider invalidate', () => { // Cache two entries for (const path of ['/a', '/b']) { const req = makeRequest(`http://localhost${path}`); - await provider.onRequest( + await provider.onRequest!( { request: req, url: new URL(req.url) }, makeNext({ maxAge: 60, body: path }), ); @@ -458,7 +477,7 @@ describe('memory-provider invalidate', () => { // /a should miss const reqA = makeRequest('http://localhost/a'); - const resA = await provider.onRequest( + const resA = await provider.onRequest!( { request: reqA, url: new URL(reqA.url) }, makeNext({ maxAge: 60, body: 'a-new' }), ); @@ -466,7 +485,7 @@ describe('memory-provider invalidate', () => { // /b should still hit const reqB = makeRequest('http://localhost/b'); - const resB = await provider.onRequest( + const resB = await provider.onRequest!( { request: reqB, url: new URL(reqB.url) }, makeNext({ maxAge: 60, body: 'b-new' }), ); @@ -478,7 +497,7 @@ describe('memory-provider invalidate', () => { const url = 'http://localhost/page'; const req1 = makeRequest(url); - await provider.onRequest( + await provider.onRequest!( { request: req1, url: new URL(req1.url) }, makeNext({ maxAge: 60, tags: ['product'] }), ); @@ -486,7 +505,7 @@ describe('memory-provider invalidate', () => { await provider.invalidate({ tags: ['blog'] }); const req2 = makeRequest(url); - const res2 = await provider.onRequest( + const res2 = await provider.onRequest!( { request: req2, url: new URL(req2.url) }, makeNext({ maxAge: 60 }), ); @@ -494,7 +513,9 @@ describe('memory-provider invalidate', () => { }); }); -// ─── onRequest: SWR (stale-while-revalidate) ──────────────────────────────── +// #endregion + +// #region onRequest: SWR (stale-while-revalidate) describe('memory-provider SWR', () => { it('serves STALE and triggers background revalidation', async () => { @@ -505,7 +526,7 @@ describe('memory-provider SWR', () => { // We can't easily manipulate time, so use a very short maxAge. // Instead, seed with maxAge=1, swr=60, then wait briefly. const req1 = makeRequest(url); - await provider.onRequest( + await provider.onRequest!( { request: req1, url: new URL(req1.url) }, makeNext({ maxAge: 1, swr: 60, body: 'stale-body' }), ); @@ -515,7 +536,7 @@ describe('memory-provider SWR', () => { // Second request should get STALE const req2 = makeRequest(url); - const res2 = await provider.onRequest( + const res2 = await provider.onRequest!( { request: req2, url: new URL(req2.url) }, makeNext({ maxAge: 60, swr: 60, body: 'fresh-body' }), ); @@ -527,7 +548,7 @@ describe('memory-provider SWR', () => { // Third request should now get HIT with the fresh content const req3 = makeRequest(url); - const res3 = await provider.onRequest( + const res3 = await provider.onRequest!( { request: req3, url: new URL(req3.url) }, makeNext({ maxAge: 60, body: 'should-not-see' }), ); @@ -536,7 +557,9 @@ describe('memory-provider SWR', () => { }); }); -// ─── response body correctness ─────────────────────────────────────────────── +// #endregion + +// #region response body correctness describe('memory-provider response body', () => { it('serves correct body from cache', async () => { @@ -545,13 +568,13 @@ describe('memory-provider response body', () => { const body = JSON.stringify({ data: [1, 2, 3], nested: { key: 'value' } }); const req1 = makeRequest(url); - await provider.onRequest( + await provider.onRequest!( { request: req1, url: new URL(req1.url) }, makeNext({ maxAge: 60, body }), ); const req2 = makeRequest(url); - const res2 = await provider.onRequest( + const res2 = await provider.onRequest!( { request: req2, url: new URL(req2.url) }, makeNext({ maxAge: 60, body: 'wrong' }), ); @@ -563,13 +586,13 @@ describe('memory-provider response body', () => { const url = 'http://localhost/page'; const req1 = makeRequest(url); - await provider.onRequest( + await provider.onRequest!( { request: req1, url: new URL(req1.url) }, makeNext({ maxAge: 60, status: 201, body: 'created' }), ); const req2 = makeRequest(url); - const res2 = await provider.onRequest( + const res2 = await provider.onRequest!( { request: req2, url: new URL(req2.url) }, makeNext({ maxAge: 60 }), ); @@ -581,7 +604,7 @@ describe('memory-provider response body', () => { const url = 'http://localhost/page'; const req1 = makeRequest(url); - await provider.onRequest( + await provider.onRequest!( { request: req1, url: new URL(req1.url) }, makeNext({ maxAge: 60, @@ -590,7 +613,7 @@ describe('memory-provider response body', () => { ); const req2 = makeRequest(url); - const res2 = await provider.onRequest( + const res2 = await provider.onRequest!( { request: req2, url: new URL(req2.url) }, makeNext({ maxAge: 60 }), ); @@ -598,3 +621,5 @@ describe('memory-provider response body', () => { assert.equal(res2.headers.get('X-Custom'), 'hello'); }); }); + +// #endregion diff --git a/packages/astro/test/units/cache/noop.test.js b/packages/astro/test/units/cache/noop.test.ts similarity index 89% rename from packages/astro/test/units/cache/noop.test.js rename to packages/astro/test/units/cache/noop.test.ts index 0156fc8ba6d5..1cccce2155b3 100644 --- a/packages/astro/test/units/cache/noop.test.js +++ b/packages/astro/test/units/cache/noop.test.ts @@ -2,7 +2,7 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { NoopAstroCache, DisabledAstroCache } from '../../../dist/core/cache/runtime/noop.js'; import { applyCacheHeaders, isCacheActive } from '../../../dist/core/cache/runtime/cache.js'; -import { defaultLogger } from '../test-utils.js'; +import { defaultLogger } from '../test-utils.ts'; describe('NoopAstroCache', () => { it('enabled is false', () => { @@ -12,8 +12,8 @@ describe('NoopAstroCache', () => { it('set() is callable and does nothing', () => { const cache = new NoopAstroCache(); - cache.set({ maxAge: 300, tags: ['a'] }); - cache.set(false); + cache.set(); + cache.set(); // No error thrown }); @@ -24,7 +24,7 @@ describe('NoopAstroCache', () => { it('invalidate() is callable and resolves', async () => { const cache = new NoopAstroCache(); - await cache.invalidate({ tags: 'x' }); + await cache.invalidate(); // No error thrown }); @@ -57,14 +57,14 @@ describe('DisabledAstroCache', () => { it('set() does not throw', () => { const cache = new DisabledAstroCache(defaultLogger); - cache.set({ maxAge: 300 }); - cache.set(false); + cache.set(); + cache.set(); // No error thrown }); it('tags returns empty array', () => { const cache = new DisabledAstroCache(defaultLogger); - cache.set({ tags: ['x'] }); + cache.set(); assert.deepEqual(cache.tags, []); }); @@ -77,8 +77,8 @@ describe('DisabledAstroCache', () => { it('invalidate() throws AstroError with CacheNotEnabled', async () => { const cache = new DisabledAstroCache(defaultLogger); await assert.rejects( - () => cache.invalidate({ tags: 'x' }), - (err) => err.name === 'CacheNotEnabled', + () => cache.invalidate(), + (err: Error) => err.name === 'CacheNotEnabled', ); }); diff --git a/packages/astro/test/units/cache/route-matching.test.js b/packages/astro/test/units/cache/route-matching.test.ts similarity index 97% rename from packages/astro/test/units/cache/route-matching.test.js rename to packages/astro/test/units/cache/route-matching.test.ts index 2eac33d71a01..1cd999b4629e 100644 --- a/packages/astro/test/units/cache/route-matching.test.js +++ b/packages/astro/test/units/cache/route-matching.test.ts @@ -1,5 +1,6 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; +import type { CacheOptions } from '../../../dist/core/cache/types.js'; import { compileCacheRoutes, matchCacheRoute, @@ -8,7 +9,7 @@ import { /** * Helper: compile routes with default base '/' and trailingSlash 'ignore'. */ -function compile(routes) { +function compile(routes: Record) { return compileCacheRoutes(routes, '/', 'ignore'); } diff --git a/packages/astro/test/units/cache/runtime.test.js b/packages/astro/test/units/cache/runtime.test.ts similarity index 96% rename from packages/astro/test/units/cache/runtime.test.js rename to packages/astro/test/units/cache/runtime.test.ts index 55606cefb679..6764a01bc6f5 100644 --- a/packages/astro/test/units/cache/runtime.test.js +++ b/packages/astro/test/units/cache/runtime.test.ts @@ -1,5 +1,6 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; +import type { CacheProvider, InvalidateOptions } from '../../../dist/core/cache/types.js'; import { AstroCache, applyCacheHeaders, @@ -7,7 +8,7 @@ import { } from '../../../dist/core/cache/runtime/cache.js'; // Mock provider -function createMockProvider(overrides = {}) { +function createMockProvider(overrides: Partial = {}): CacheProvider { return { name: 'test-provider', invalidate: async () => {}, @@ -168,7 +169,7 @@ describe('AstroCache - options getter', () => { const cache = new AstroCache(null); cache.set({ maxAge: 300 }); - const options = cache.options; + const options = cache.options as { maxAge?: number }; options.maxAge = 999; assert.equal(cache.options.maxAge, 300); }); @@ -184,7 +185,7 @@ describe('AstroCache - options getter', () => { describe('AstroCache - invalidate()', () => { it('calls provider.invalidate() with correct options', async () => { - let captured; + let captured: InvalidateOptions | undefined; const provider = createMockProvider({ invalidate: async (opts) => { captured = opts; @@ -196,7 +197,7 @@ describe('AstroCache - invalidate()', () => { }); it('extracts tags from LiveDataEntry for invalidate', async () => { - let captured; + let captured: InvalidateOptions | undefined; const provider = createMockProvider({ invalidate: async (opts) => { captured = opts; diff --git a/packages/astro/test/units/cache/utils.test.js b/packages/astro/test/units/cache/utils.test.ts similarity index 99% rename from packages/astro/test/units/cache/utils.test.js rename to packages/astro/test/units/cache/utils.test.ts index 30957bb7fd76..b3d6aeb9f104 100644 --- a/packages/astro/test/units/cache/utils.test.js +++ b/packages/astro/test/units/cache/utils.test.ts @@ -41,7 +41,7 @@ describe('defaultSetHeaders()', () => { it('empty options produces no headers', () => { const headers = defaultSetHeaders({}); - assert.equal([...headers.entries()].length, 0); + assert.equal([...(headers as any).entries()].length, 0); }); it('tags-only produces Cache-Tag but no CDN-Cache-Control', () => { diff --git a/packages/astro/test/units/cli/utils.ts b/packages/astro/test/units/cli/utils.ts index 8b1ec1b7ebe0..201d4a0301fb 100644 --- a/packages/astro/test/units/cli/utils.ts +++ b/packages/astro/test/units/cli/utils.ts @@ -1,5 +1,5 @@ import { AstroIntegrationLogger } from '../../../dist/core/logger/core.js'; -import type { LogOptions } from '../../../dist/core/logger/core.js'; +import type { AstroLogOptions } from '../../../dist/core/logger/core.js'; import type { CloudIde } from '../../../dist/cli/docs/domain/cloud-ide.js'; import type { CloudIdeProvider } from '../../../dist/cli/docs/definitions.js'; import type { AnyCommand } from '../../../dist/cli/domain/command.js'; @@ -207,8 +207,8 @@ export class SpyLogger { this.#logs.push({ type: 'warn', label, message }); } - options: LogOptions = { - dest: { write: () => true }, + options: AstroLogOptions = { + destination: { write: () => true }, level: 'silent', }; diff --git a/packages/astro/test/units/compile/css-base-path.test.js b/packages/astro/test/units/compile/css-base-path.test.ts similarity index 92% rename from packages/astro/test/units/compile/css-base-path.test.js rename to packages/astro/test/units/compile/css-base-path.test.ts index 8e5638fa1f97..5cb41c56d66f 100644 --- a/packages/astro/test/units/compile/css-base-path.test.js +++ b/packages/astro/test/units/compile/css-base-path.test.ts @@ -3,41 +3,34 @@ import { describe, it } from 'node:test'; import { pathToFileURL } from 'node:url'; import { resolveConfig } from 'vite'; import { compileAstro } from '../../../dist/vite-plugin-astro/compile.js'; +import type { AstroConfig } from '../../../dist/types/public/config.js'; +import type { CompileProps } from '../../../dist/core/compile/compile.js'; +import { AstroLogger } from '../../../dist/core/logger/core.js'; +import { nodeLogDestination } from '../../../dist/core/logger/node.js'; -/** - * Compile Astro source with a given base path - * @param {string} source - Astro source code - * @param {string} base - Base path configuration - */ -async function compileWithBase(source, base = '/') { +const logger = new AstroLogger({ destination: nodeLogDestination, level: 'silent' }); + +/** Compile Astro source with a given base path. */ +async function compileWithBase(source: string, base = '/') { const viteConfig = await resolveConfig({ configFile: false }, 'serve'); - const result = await compileAstro({ - compileProps: { - astroConfig: { - root: pathToFileURL('/'), - base, - experimental: {}, - build: { - format: 'directory', - }, - trailingSlash: 'ignore', - }, - viteConfig, - preferences: { - get: () => Promise.resolve(false), - }, - filename: '/src/pages/index.astro', - source, - }, + const props: CompileProps = { + astroConfig: { + root: pathToFileURL('/'), + base, + experimental: {}, + build: { format: 'directory' }, + trailingSlash: 'ignore', + } as AstroConfig, + viteConfig, + toolbarEnabled: false, + filename: '/src/pages/index.astro', + source, + }; + return compileAstro({ + compileProps: props as any, astroFileToCompileMetadata: new Map(), - logger: { - info: () => {}, - warn: () => {}, - error: () => {}, - debug: () => {}, - }, + logger, }); - return result; } describe('CSS Base Path Rewriting', () => { diff --git a/packages/astro/test/units/compile/invalid-css.test.js b/packages/astro/test/units/compile/invalid-css.test.ts similarity index 82% rename from packages/astro/test/units/compile/invalid-css.test.js rename to packages/astro/test/units/compile/invalid-css.test.ts index 73d52e5ec8c1..9c3e0d043381 100644 --- a/packages/astro/test/units/compile/invalid-css.test.js +++ b/packages/astro/test/units/compile/invalid-css.test.ts @@ -4,6 +4,7 @@ import { pathToFileURL } from 'node:url'; import { resolveConfig } from 'vite'; import { compile } from '../../../dist/core/compile/index.js'; import { AggregateError } from '../../../dist/core/errors/index.js'; +import type { AstroConfig } from '../../../dist/types/public/config.js'; describe('astro/src/core/compile', () => { describe('Invalid CSS', () => { @@ -14,8 +15,9 @@ describe('astro/src/core/compile', () => { astroConfig: { root: pathToFileURL('/'), experimental: {}, - }, + } as AstroConfig, viteConfig: await resolveConfig({ configFile: false }, 'serve'), + toolbarEnabled: false, filename: '/src/pages/index.astro', source: ` --- @@ -37,7 +39,7 @@ describe('astro/src/core/compile', () => { } assert.equal(error instanceof AggregateError, true); - assert.equal(error.errors[0].message.includes('expected ")"'), true); + assert.equal((error as AggregateError).errors[0].message.includes('expected ")"'), true); }); }); }); diff --git a/packages/astro/test/units/compile/rust-compiler.test.js b/packages/astro/test/units/compile/rust-compiler.test.ts similarity index 91% rename from packages/astro/test/units/compile/rust-compiler.test.js rename to packages/astro/test/units/compile/rust-compiler.test.ts index aaa5bbe6fe68..0c53e68d7823 100644 --- a/packages/astro/test/units/compile/rust-compiler.test.js +++ b/packages/astro/test/units/compile/rust-compiler.test.ts @@ -3,12 +3,9 @@ import { describe, it } from 'node:test'; import { pathToFileURL } from 'node:url'; import { resolveConfig } from 'vite'; import { compile } from '../../../dist/core/compile/compile-rs.js'; +import type { AstroConfig } from '../../../dist/types/public/config.js'; -/** - * @param {string} source - * @param {object} [configOverrides] - */ -async function compileWithRust(source, configOverrides = {}) { +async function compileWithRust(source: string, configOverrides: Partial = {}) { const viteConfig = await resolveConfig({ configFile: false }, 'serve'); return compile({ astroConfig: { @@ -20,7 +17,7 @@ async function compileWithRust(source, configOverrides = {}) { devToolbar: { enabled: false }, site: undefined, ...configOverrides, - }, + } as AstroConfig, viteConfig, toolbarEnabled: false, filename: '/src/components/index.astro', @@ -130,9 +127,10 @@ console.log('hello'); it('throws a CompilerError on unclosed tags', async () => { await assert.rejects( () => compileWithRust('

Unclosed tag'), - (err) => { - assert.ok(err.message || err.name); - assert.ok(err.message.includes('Unexpected token')); + (err: unknown) => { + const e = err as { message?: string; name?: string }; + assert.ok(e.message || e.name); + assert.ok(e.message?.includes('Unexpected token')); return true; }, ); diff --git a/packages/astro/test/units/config/config-merge.test.js b/packages/astro/test/units/config/config-merge.test.ts similarity index 56% rename from packages/astro/test/units/config/config-merge.test.js rename to packages/astro/test/units/config/config-merge.test.ts index e269b454a0c8..59eba321d2da 100644 --- a/packages/astro/test/units/config/config-merge.test.js +++ b/packages/astro/test/units/config/config-merge.test.ts @@ -6,15 +6,17 @@ describe('mergeConfig', () => { it('keeps server.allowedHosts as boolean', () => { const defaults = { server: { - allowedHosts: [], + // Typed as string[] to match AstroConfig's allowedHosts field + allowedHosts: [] as string[], }, }; + // allowedHosts can also be true (allow all) — cast to satisfy DeepPartial const overrides = { server: { - allowedHosts: true, + allowedHosts: true as boolean | string[], }, }; - const merged = mergeConfig(defaults, overrides); + const merged = mergeConfig(defaults, overrides as typeof defaults); assert.equal(merged.server.allowedHosts, true); }); }); diff --git a/packages/astro/test/units/config/config-resolve.test.js b/packages/astro/test/units/config/config-resolve.test.ts similarity index 100% rename from packages/astro/test/units/config/config-resolve.test.js rename to packages/astro/test/units/config/config-resolve.test.ts diff --git a/packages/astro/test/units/config/config-server.test.js b/packages/astro/test/units/config/config-server.test.ts similarity index 83% rename from packages/astro/test/units/config/config-server.test.js rename to packages/astro/test/units/config/config-server.test.ts index 6f621007c14a..ab9c0d6b83c6 100644 --- a/packages/astro/test/units/config/config-server.test.js +++ b/packages/astro/test/units/config/config-server.test.ts @@ -1,20 +1,12 @@ import * as assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { fileURLToPath } from 'node:url'; -import { flagsToAstroInlineConfig } from '../../../dist/cli/flags.js'; +import { flagsToAstroInlineConfig, type Flags } from '../../../dist/cli/flags.js'; import { resolveConfig } from '../../../dist/core/config/index.js'; -const cwd = fileURLToPath(new URL('../../fixtures/config-host/', import.meta.url)); - describe('config.server', () => { - function resolveConfigWithFlags(flags) { - return resolveConfig( - flagsToAstroInlineConfig({ - root: cwd, - ...flags, - }), - 'dev', - ); + function resolveConfigWithFlags(flags: Partial) { + return resolveConfig(flagsToAstroInlineConfig(flags as Flags), 'dev'); } describe('host', () => { @@ -64,8 +56,8 @@ describe('config.server', () => { config: configFileURL, }); assert.equal(false, true, 'this should not have resolved'); - } catch (err) { - assert.equal(err.message.includes('Unable to resolve'), true); + } catch (err: unknown) { + assert.equal((err as Error).message.includes('Unable to resolve'), true); } }); }); diff --git a/packages/astro/test/units/config/config-tsconfig.test.js b/packages/astro/test/units/config/config-tsconfig.test.ts similarity index 73% rename from packages/astro/test/units/config/config-tsconfig.test.js rename to packages/astro/test/units/config/config-tsconfig.test.ts index 94e9438982fd..e85e51ef7a38 100644 --- a/packages/astro/test/units/config/config-tsconfig.test.js +++ b/packages/astro/test/units/config/config-tsconfig.test.ts @@ -5,29 +5,34 @@ import { describe, it } from 'node:test'; import { fileURLToPath } from 'node:url'; import { toJson } from 'tsconfck'; import { loadTSConfig, updateTSConfigForFramework } from '../../../dist/core/config/index.js'; +import type { frameworkWithTSSettings } from '../../../dist/core/config/tsconfig.js'; const cwd = fileURLToPath(new URL('../../fixtures/tsconfig-handling/', import.meta.url)); +/** Assert that loadTSConfig returned a valid result (not an error string). */ +function assertValidConfig( + config: Awaited>, +): asserts config is Exclude { + assert.ok(typeof config !== 'string', `Expected a valid config but got error: ${config}`); +} + describe('TSConfig handling', () => { describe('tsconfig / jsconfig loading', () => { it('can load tsconfig.json', async () => { const config = await loadTSConfig(cwd); - assert.equal(config !== undefined, true); }); it('can resolve tsconfig.json up directories', async () => { const config = await loadTSConfig(cwd); - - assert.equal(config !== undefined, true); + assertValidConfig(config); assert.equal(config.tsconfigFile, path.join(cwd, 'tsconfig.json')); assert.deepEqual(config.tsconfig.files, ['im-a-test']); }); it('can fall back to jsconfig.json if tsconfig.json does not exist', async () => { const config = await loadTSConfig(path.join(cwd, 'jsconfig')); - - assert.equal(config !== undefined, true); + assertValidConfig(config); assert.equal(config.tsconfigFile, path.join(cwd, 'jsconfig', 'jsconfig.json')); assert.deepEqual(config.tsconfig.files, ['im-a-test-js']); }); @@ -42,6 +47,7 @@ describe('TSConfig handling', () => { it('does not change baseUrl in raw config', async () => { const loadedConfig = await loadTSConfig(path.join(cwd, 'baseUrl')); + assertValidConfig(loadedConfig); const rawConfig = await readFile(path.join(cwd, 'baseUrl', 'tsconfig.json'), 'utf-8') .then(toJson) .then((content) => JSON.parse(content)); @@ -53,15 +59,21 @@ describe('TSConfig handling', () => { describe('tsconfig / jsconfig updates', () => { it('can update a tsconfig with a framework config', async () => { const config = await loadTSConfig(cwd); + assertValidConfig(config); const updatedConfig = updateTSConfigForFramework(config.tsconfig, 'react'); assert.notEqual(config.tsconfig, 'react-jsx'); - assert.equal(updatedConfig.compilerOptions.jsx, 'react-jsx'); + assert.equal(updatedConfig.compilerOptions?.jsx, 'react-jsx'); }); it('produce no changes on invalid frameworks', async () => { const config = await loadTSConfig(cwd); - const updatedConfig = updateTSConfigForFramework(config.tsconfig, 'doesnt-exist'); + assertValidConfig(config); + // 'doesnt-exist' is not a valid frameworkWithTSSettings — cast to test fallback behaviour + const updatedConfig = updateTSConfigForFramework( + config.tsconfig, + 'doesnt-exist' as frameworkWithTSSettings, + ); assert.deepEqual(config.tsconfig, updatedConfig); }); diff --git a/packages/astro/test/units/config/config-validate.test.js b/packages/astro/test/units/config/config-validate.test.ts similarity index 94% rename from packages/astro/test/units/config/config-validate.test.js rename to packages/astro/test/units/config/config-validate.test.ts index 8938c14e166d..d6229bf01e0f 100644 --- a/packages/astro/test/units/config/config-validate.test.js +++ b/packages/astro/test/units/config/config-validate.test.ts @@ -1,4 +1,3 @@ -// @ts-check import * as assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { stripVTControlCharacters } from 'node:util'; @@ -9,11 +8,7 @@ import { validateConfig as _validateConfig } from '../../../dist/core/config/val import { formatConfigErrorMessage } from '../../../dist/core/messages/runtime.js'; import { envField } from '../../../dist/env/config.js'; -/** - * - * @param {any} userConfig - */ -async function validateConfig(userConfig) { +async function validateConfig(userConfig: Record) { return _validateConfig(userConfig, process.cwd(), ''); } @@ -209,27 +204,26 @@ describe('Config Validation', () => { ); }); - it( - 'errors if `i18n.prefixDefaultLocale` is `false` and `i18n.redirectToDefaultLocale` is `true`', - { todo: 'Enable in Astro 6.0', skip: 'Removed validation' }, - async () => { - const configError = await validateConfig({ - i18n: { - defaultLocale: 'en', - locales: ['es', 'en'], - routing: { - prefixDefaultLocale: false, - redirectToDefaultLocale: true, - }, + it('errors if `i18n.prefixDefaultLocale` is `false` and `i18n.redirectToDefaultLocale` is `true`', { + todo: 'Enable in Astro 6.0', + skip: 'Removed validation', + }, async () => { + const configError = await validateConfig({ + i18n: { + defaultLocale: 'en', + locales: ['es', 'en'], + routing: { + prefixDefaultLocale: false, + redirectToDefaultLocale: true, }, - }).catch((err) => err); - assert.equal(configError instanceof z.ZodError, true); - assert.equal( - configError.issues[0].message, - 'The option `i18n.routing.redirectToDefaultLocale` can be used only when `i18n.routing.prefixDefaultLocale` is set to `true`; otherwise, redirects might cause infinite loops. Remove the option `i18n.routing.redirectToDefaultLocale`, or change its value to `false`.', - ); - }, - ); + }, + }).catch((err) => err); + assert.equal(configError instanceof z.ZodError, true); + assert.equal( + configError.issues[0].message, + 'The option `i18n.routing.redirectToDefaultLocale` can be used only when `i18n.routing.prefixDefaultLocale` is set to `true`; otherwise, redirects might cause infinite loops. Remove the option `i18n.routing.redirectToDefaultLocale`, or change its value to `false`.', + ); + }); it('errors if a domains key does not exist', async () => { const configError = await validateConfig({ @@ -544,8 +538,8 @@ describe('Config Validation', () => { ttl: 60 * 60, // 1 hour }, }); - assert.equal(result.session.ttl, 60 * 60); - assert.equal(result.session.driver, undefined); + assert.equal(result.session?.ttl, 60 * 60); + assert.equal(result.session?.driver, undefined); }); }); diff --git a/packages/astro/test/units/config/format.test.js b/packages/astro/test/units/config/format.test.js deleted file mode 100644 index 66938a03a5b1..000000000000 --- a/packages/astro/test/units/config/format.test.js +++ /dev/null @@ -1,24 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; -import { createFixture, runInContainer } from '../test-utils.js'; - -describe('Astro config formats', () => { - it('An mjs config can import TypeScript modules', async () => { - const fixture = await createFixture({ - '/src/pages/index.astro': ``, - '/src/stuff.ts': `export default 'works';`, - '/astro.config.mjs': `\ - import stuff from './src/stuff.ts'; - export default {} - `, - }); - - await runInContainer({ inlineConfig: { root: fixture.path } }, () => { - assert.equal( - true, - true, - 'We were able to get into the container which means the config loaded.', - ); - }); - }); -}); diff --git a/packages/astro/test/units/config/refined-validators.test.ts b/packages/astro/test/units/config/refined-validators.test.ts new file mode 100644 index 000000000000..f2a37e65d82d --- /dev/null +++ b/packages/astro/test/units/config/refined-validators.test.ts @@ -0,0 +1,444 @@ +import * as assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import type { AstroConfig } from '../../../dist/types/public/config.js'; +import { + validateAssetsPrefix, + validateFontsCssVariables, + validateI18nDefaultLocale, + validateI18nDomains, + validateI18nFallback, + validateI18nRedirectToDefaultLocale, + validateOutDirNotInPublicDir, + validateRemotePatterns, +} from '../../../dist/core/config/schemas/refined-validators.js'; + +/** Cast partial test data to a strict Pick type via `unknown`. */ +const build = (v: unknown) => ({ build: v }) as Pick; +const i18n = (v: unknown) => v as NonNullable; +const domains = (v: unknown) => v as Pick; +const font = (v: unknown) => v as NonNullable[number]; + +// #region validateAssetsPrefix +describe('validateAssetsPrefix', () => { + it('returns no issues for a string prefix', () => { + const issues = validateAssetsPrefix(build({ assetsPrefix: 'https://cdn.example.com' })); + assert.equal(issues.length, 0); + }); + + it('returns no issues when assetsPrefix is undefined', () => { + const issues = validateAssetsPrefix(build({})); + assert.equal(issues.length, 0); + }); + + it('returns no issues for an object with fallback', () => { + const issues = validateAssetsPrefix( + build({ assetsPrefix: { css: 'https://css.cdn.com', fallback: 'https://cdn.com' } }), + ); + assert.equal(issues.length, 0); + }); + + it('returns an issue for an object without fallback', () => { + const issues = validateAssetsPrefix(build({ assetsPrefix: { css: 'https://css.cdn.com' } })); + assert.equal(issues.length, 1); + assert.match(issues[0].message, /fallback/i); + assert.deepEqual(issues[0].path, ['build', 'assetsPrefix']); + }); +}); +// #endregion + +// #region validateRemotePatterns +describe('validateRemotePatterns', () => { + it('returns no issues for empty array', () => { + const issues = validateRemotePatterns([]); + assert.equal(issues.length, 0); + }); + + it('returns no issues for valid hostname wildcard at start', () => { + const issues = validateRemotePatterns([{ hostname: '*.example.com' }]); + assert.equal(issues.length, 0); + }); + + it('returns no issues for double-star hostname wildcard at start', () => { + const issues = validateRemotePatterns([{ hostname: '**.example.com' }]); + assert.equal(issues.length, 0); + }); + + it('returns an issue for wildcard in the middle of hostname', () => { + const issues = validateRemotePatterns([{ hostname: 'cdn.*.example.com' }]); + assert.equal(issues.length, 1); + assert.match(issues[0].message, /beginning of the hostname/); + assert.deepEqual(issues[0].path, ['image', 'remotePatterns', 0, 'hostname']); + }); + + it('returns an issue for wildcard at the end of hostname', () => { + const issues = validateRemotePatterns([{ hostname: 'example.*' }]); + assert.equal(issues.length, 1); + assert.match(issues[0].message, /beginning of the hostname/); + }); + + it('returns no issues for valid pathname wildcard at end', () => { + const issues = validateRemotePatterns([{ pathname: '/images/*' }]); + assert.equal(issues.length, 0); + }); + + it('returns no issues for double-star pathname wildcard at end', () => { + const issues = validateRemotePatterns([{ pathname: '/images/**' }]); + assert.equal(issues.length, 0); + }); + + it('returns an issue for wildcard at the start of pathname', () => { + const issues = validateRemotePatterns([{ pathname: '/*/images' }]); + assert.equal(issues.length, 1); + assert.match(issues[0].message, /end of a pathname/); + assert.deepEqual(issues[0].path, ['image', 'remotePatterns', 0, 'pathname']); + }); + + it('returns issues for multiple invalid patterns', () => { + const issues = validateRemotePatterns([ + { hostname: 'cdn.*.example.com' }, + { hostname: '*.valid.com' }, + { pathname: '/*/bad' }, + ]); + assert.equal(issues.length, 2); + }); + + it('returns no issues for patterns without wildcards', () => { + const issues = validateRemotePatterns([{ hostname: 'example.com', pathname: '/images' }]); + assert.equal(issues.length, 0); + }); +}); +// #endregion + +// #region validateI18nRedirectToDefaultLocale +describe('validateI18nRedirectToDefaultLocale', () => { + it('returns no issues when i18n is undefined', () => { + const issues = validateI18nRedirectToDefaultLocale(undefined); + assert.equal(issues.length, 0); + }); + + it('returns no issues when prefixDefaultLocale is true and redirectToDefaultLocale is true', () => { + const issues = validateI18nRedirectToDefaultLocale( + i18n({ + routing: { + prefixDefaultLocale: true, + redirectToDefaultLocale: true, + fallbackType: 'redirect', + }, + }), + ); + assert.equal(issues.length, 0); + }); + + it('returns no issues when prefixDefaultLocale is false and redirectToDefaultLocale is false', () => { + const issues = validateI18nRedirectToDefaultLocale( + i18n({ + routing: { + prefixDefaultLocale: false, + redirectToDefaultLocale: false, + fallbackType: 'redirect', + }, + }), + ); + assert.equal(issues.length, 0); + }); + + it('returns an issue when prefixDefaultLocale is false and redirectToDefaultLocale is true', () => { + const issues = validateI18nRedirectToDefaultLocale( + i18n({ + routing: { + prefixDefaultLocale: false, + redirectToDefaultLocale: true, + fallbackType: 'redirect', + }, + }), + ); + assert.equal(issues.length, 1); + assert.match(issues[0].message, /redirectToDefaultLocale/); + assert.match(issues[0].message, /prefixDefaultLocale/); + assert.deepEqual(issues[0].path, ['i18n', 'routing', 'redirectToDefaultLocale']); + }); + + it('returns no issues when routing is manual', () => { + const issues = validateI18nRedirectToDefaultLocale(i18n({ routing: 'manual' })); + assert.equal(issues.length, 0); + }); +}); +// #endregion + +// #region validateOutDirNotInPublicDir +describe('validateOutDirNotInPublicDir', () => { + it('returns no issues when outDir is outside publicDir', () => { + const issues = validateOutDirNotInPublicDir( + new URL('file:///project/dist/'), + new URL('file:///project/public/'), + ); + assert.equal(issues.length, 0); + }); + + it('returns an issue when outDir equals publicDir', () => { + const issues = validateOutDirNotInPublicDir( + new URL('file:///project/public/'), + new URL('file:///project/public/'), + ); + assert.equal(issues.length, 1); + assert.match(issues[0].message, /outDir/); + assert.match(issues[0].message, /publicDir/); + assert.deepEqual(issues[0].path, ['outDir']); + }); + + it('returns an issue when outDir is inside publicDir', () => { + const issues = validateOutDirNotInPublicDir( + new URL('file:///project/public/dist/'), + new URL('file:///project/public/'), + ); + assert.equal(issues.length, 1); + }); +}); +// #endregion + +// #region validateI18nDefaultLocale +describe('validateI18nDefaultLocale', () => { + it('returns no issues when defaultLocale is in locales', () => { + const issues = validateI18nDefaultLocale({ + defaultLocale: 'en', + locales: ['en', 'fr', 'de'], + }); + assert.equal(issues.length, 0); + }); + + it('returns an issue when defaultLocale is not in locales', () => { + const issues = validateI18nDefaultLocale({ + defaultLocale: 'es', + locales: ['en', 'fr', 'de'], + }); + assert.equal(issues.length, 1); + assert.match(issues[0].message, /es/); + assert.match(issues[0].message, /not present/); + assert.deepEqual(issues[0].path, ['i18n', 'locales']); + }); + + it('handles object locales (uses path property)', () => { + const issues = validateI18nDefaultLocale({ + defaultLocale: 'english', + locales: [{ path: 'english', codes: ['en'] }, 'fr'], + }); + assert.equal(issues.length, 0); + }); + + it('returns an issue when defaultLocale is missing from object locales', () => { + const issues = validateI18nDefaultLocale({ + defaultLocale: 'en', + locales: [{ path: 'english', codes: ['en'] }, 'fr'], + }); + assert.equal(issues.length, 1); + assert.match(issues[0].message, /en/); + }); +}); +// #endregion + +// #region validateI18nFallback +describe('validateI18nFallback', () => { + it('returns no issues when fallback is undefined', () => { + const issues = validateI18nFallback({ + defaultLocale: 'en', + locales: ['en', 'fr'], + }); + assert.equal(issues.length, 0); + }); + + it('returns no issues for valid fallback entries', () => { + const issues = validateI18nFallback({ + defaultLocale: 'en', + locales: ['en', 'fr', 'de'], + fallback: { fr: 'en', de: 'en' }, + }); + assert.equal(issues.length, 0); + }); + + it('returns an issue when fallback key is not in locales', () => { + const issues = validateI18nFallback({ + defaultLocale: 'en', + locales: ['en', 'fr'], + fallback: { es: 'en' }, + }); + assert.ok(issues.some((i) => i.message.includes('es') && i.message.includes('key'))); + }); + + it('returns an issue when fallback value is not in locales', () => { + const issues = validateI18nFallback({ + defaultLocale: 'en', + locales: ['en', 'fr'], + fallback: { fr: 'de' }, + }); + assert.ok(issues.some((i) => i.message.includes('de') && i.message.includes('value'))); + }); + + it('returns an issue when default locale is used as a fallback key', () => { + const issues = validateI18nFallback({ + defaultLocale: 'en', + locales: ['en', 'fr'], + fallback: { en: 'fr' }, + }); + assert.ok(issues.some((i) => i.message.includes('default locale'))); + }); + + it('returns multiple issues for multiple invalid entries', () => { + const issues = validateI18nFallback({ + defaultLocale: 'en', + locales: ['en', 'fr'], + fallback: { es: 'de', en: 'fr' }, + }); + // es not in locales (key issue), de not in locales (value issue), en is default locale + assert.ok(issues.length >= 3); + }); +}); +// #endregion + +// #region validateI18nDomains +describe('validateI18nDomains', () => { + it('returns no issues when i18n is undefined', () => { + const issues = validateI18nDomains(domains({ i18n: undefined })); + assert.equal(issues.length, 0); + }); + + it('returns no issues when domains is undefined', () => { + const issues = validateI18nDomains(domains({ i18n: { locales: ['en'], defaultLocale: 'en' } })); + assert.equal(issues.length, 0); + }); + + it('returns an issue when site is not set', () => { + const issues = validateI18nDomains( + domains({ + site: undefined, + output: 'server', + i18n: { + locales: ['en', 'fr'], + defaultLocale: 'en', + domains: { fr: 'https://fr.example.com' }, + }, + }), + ); + assert.ok(issues.some((i) => i.message.includes('site'))); + }); + + it('returns an issue when output is not server', () => { + const issues = validateI18nDomains( + domains({ + site: 'https://example.com', + output: 'static', + i18n: { + locales: ['en', 'fr'], + defaultLocale: 'en', + domains: { fr: 'https://fr.example.com' }, + }, + }), + ); + assert.ok(issues.some((i) => i.message.includes('output') && i.message.includes('server'))); + }); + + it('returns an issue when domain locale key is not in locales', () => { + const issues = validateI18nDomains( + domains({ + site: 'https://example.com', + output: 'server', + i18n: { + locales: ['en', 'fr'], + defaultLocale: 'en', + domains: { de: 'https://de.example.com' }, + }, + }), + ); + assert.ok(issues.some((i) => i.message.includes('de'))); + }); + + it('returns an issue when domain value is not a URL', () => { + const issues = validateI18nDomains( + domains({ + site: 'https://example.com', + output: 'server', + i18n: { + locales: ['en', 'fr'], + defaultLocale: 'en', + domains: { fr: 'not-a-url' }, + }, + }), + ); + assert.ok(issues.some((i) => i.message.includes('http'))); + }); + + it('returns an issue when domain URL has a pathname', () => { + const issues = validateI18nDomains( + domains({ + site: 'https://example.com', + output: 'server', + i18n: { + locales: ['en', 'fr'], + defaultLocale: 'en', + domains: { fr: 'https://fr.example.com/blog' }, + }, + }), + ); + assert.ok(issues.some((i) => i.message.includes('/blog'))); + }); + + it('returns no issues for valid domain configuration', () => { + const issues = validateI18nDomains( + domains({ + site: 'https://example.com', + output: 'server', + i18n: { + locales: ['en', 'fr'], + defaultLocale: 'en', + domains: { fr: 'https://fr.example.com' }, + }, + }), + ); + assert.equal(issues.length, 0); + }); +}); +// #endregion + +// #region validateFontsCssVariables +describe('validateFontsCssVariables', () => { + it('returns no issues for valid CSS variable names', () => { + const issues = validateFontsCssVariables([ + font({ cssVariable: '--font-body' }), + font({ cssVariable: '--heading-font' }), + ]); + assert.equal(issues.length, 0); + }); + + it('returns an issue when cssVariable does not start with --', () => { + const issues = validateFontsCssVariables([font({ cssVariable: 'font-body' })]); + assert.equal(issues.length, 1); + assert.match(issues[0].message, /cssVariable/); + assert.deepEqual(issues[0].path, ['fonts', 0, 'cssVariable']); + }); + + it('returns an issue when cssVariable contains a space', () => { + const issues = validateFontsCssVariables([font({ cssVariable: '--font body' })]); + assert.equal(issues.length, 1); + }); + + it('returns an issue when cssVariable contains a colon', () => { + const issues = validateFontsCssVariables([font({ cssVariable: '--font:body' })]); + assert.equal(issues.length, 1); + }); + + it('returns issues for multiple invalid entries', () => { + const issues = validateFontsCssVariables([ + font({ cssVariable: '--valid' }), + font({ cssVariable: 'no-prefix' }), + font({ cssVariable: '--has space' }), + ]); + assert.equal(issues.length, 2); + assert.deepEqual(issues[0].path, ['fonts', 1, 'cssVariable']); + assert.deepEqual(issues[1].path, ['fonts', 2, 'cssVariable']); + }); + + it('returns no issues for empty array', () => { + const issues = validateFontsCssVariables([]); + assert.equal(issues.length, 0); + }); +}); +// #endregion diff --git a/packages/astro/test/units/content-collections/frontmatter.test.js b/packages/astro/test/units/content-collections/frontmatter.test.js deleted file mode 100644 index 5db1ede09532..000000000000 --- a/packages/astro/test/units/content-collections/frontmatter.test.js +++ /dev/null @@ -1,73 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; -import { attachContentServerListeners } from '../../../dist/content/index.js'; -import { createFixture, runInContainer } from '../test-utils.js'; - -describe('frontmatter', () => { - async function createContentFixture() { - return await createFixture({ - '/src/content/posts/blog.md': `\ - --- - title: One - --- - `, - '/src/content.config.ts': `\ - import { defineCollection } from 'astro:content'; - import { z } from 'astro/zod'; - import { glob } from 'astro/loaders'; - - const posts = defineCollection({ - loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/posts' }), - schema: z.string() - }); - - export const collections = { - posts - }; - `, - '/src/pages/index.astro': `\ - --- - --- - - Test - -

Test

- - - `, - }); - } - - it('errors in content/ does not crash server', async () => { - const fixture = await createContentFixture(); - - await runInContainer({ inlineConfig: { root: fixture.path } }, async (container) => { - await attachContentServerListeners(container); - - await fixture.writeFile( - '/src/content/posts/blog.md', - ` - --- - title: One - title: two - --- - `, - ); - await new Promise((resolve) => setTimeout(resolve, 100)); - // Note, if we got here, it didn't crash - }); - }); - - it('increases watcher max listeners to avoid startup warnings', async () => { - const fixture = await createContentFixture(); - - await runInContainer({ inlineConfig: { root: fixture.path } }, async (container) => { - const watcher = container.viteServer.watcher; - watcher.setMaxListeners(10); - - await attachContentServerListeners(container); - - assert.equal(watcher.getMaxListeners(), 50); - }); - }); -}); diff --git a/packages/astro/test/units/content-collections/get-entry-info.test.js b/packages/astro/test/units/content-collections/get-entry-info.test.ts similarity index 100% rename from packages/astro/test/units/content-collections/get-entry-info.test.js rename to packages/astro/test/units/content-collections/get-entry-info.test.ts diff --git a/packages/astro/test/units/content-collections/get-entry-type.test.js b/packages/astro/test/units/content-collections/get-entry-type.test.ts similarity index 100% rename from packages/astro/test/units/content-collections/get-entry-type.test.js rename to packages/astro/test/units/content-collections/get-entry-type.test.ts diff --git a/packages/astro/test/units/content-collections/image-references.test.js b/packages/astro/test/units/content-collections/image-references.test.ts similarity index 87% rename from packages/astro/test/units/content-collections/image-references.test.js rename to packages/astro/test/units/content-collections/image-references.test.ts index 84595e0cee58..f72112ede204 100644 --- a/packages/astro/test/units/content-collections/image-references.test.js +++ b/packages/astro/test/units/content-collections/image-references.test.ts @@ -1,18 +1,19 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { updateImageReferencesInData } from '../../../dist/content/runtime.js'; import { imageSrcToImportId } from '../../../dist/assets/utils/resolveImports.js'; +import type { ImageMetadata } from '../../../dist/assets/types.js'; const IMAGE_PREFIX = '__ASTRO_IMAGE_'; const FILE_NAME = 'src/content/blog/post.md'; -function makeImageMap(src, meta) { +function makeImageMap(src: string, meta: ImageMetadata): Map { const id = imageSrcToImportId(src, FILE_NAME); + assert.ok(id, `imageSrcToImportId returned undefined for src="${src}"`); return new Map([[id, meta]]); } -const heroMeta = { +const heroMeta: ImageMetadata = { src: '/_astro/hero.abc123.png', width: 800, height: 600, @@ -76,10 +77,17 @@ describe('updateImageReferencesInData', () => { }); it('resolves multiple different images in the same entry', () => { - const thumbMeta = { src: '/_astro/thumb.xyz.png', width: 100, height: 100, format: 'png' }; + const thumbMeta: ImageMetadata = { + src: '/_astro/thumb.xyz.png', + width: 100, + height: 100, + format: 'png', + }; const heroId = imageSrcToImportId('./hero.png', FILE_NAME); const thumbId = imageSrcToImportId('./thumb.png', FILE_NAME); - const map = new Map([ + assert.ok(heroId); + assert.ok(thumbId); + const map = new Map([ [heroId, heroMeta], [thumbId, thumbMeta], ]); diff --git a/packages/astro/test/units/content-collections/mutable-data-store.test.js b/packages/astro/test/units/content-collections/mutable-data-store.test.js deleted file mode 100644 index 692a74852600..000000000000 --- a/packages/astro/test/units/content-collections/mutable-data-store.test.js +++ /dev/null @@ -1,48 +0,0 @@ -import { describe, it, before, after } from 'node:test'; -import { strict as assert } from 'node:assert'; -import { promises as fs } from 'node:fs'; -import { mkdtemp, rm } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import path from 'node:path'; -import { pathToFileURL } from 'node:url'; -import * as devalue from 'devalue'; -import { MutableDataStore } from '../../../dist/content/mutable-data-store.js'; - -describe('MutableDataStore', () => { - let tmpDir; - - before(async () => { - tmpDir = await mkdtemp(path.join(tmpdir(), 'astro-test-')); - }); - - after(async () => { - try { - await rm(tmpDir, { recursive: true, force: true }); - } catch { - // Ignore cleanup errors - } - }); - - it('reproduces race condition: concurrent writeToDisk() calls lose data', async () => { - const filePath = pathToFileURL(path.join(tmpDir, 'data-store.json')); - const store = await MutableDataStore.fromFile(filePath); - - store.set('c', 'key1', { id: 'key1', data: {} }); - const p1 = store.writeToDisk(); - - store.set('c', 'key2', { id: 'key2', data: {} }); - const p2 = store.writeToDisk(); - - await Promise.all([p1, p2]); - - const raw = await fs.readFile(filePath, 'utf-8'); - const collections = devalue.parse(raw); - const collection = collections.get('c'); - - assert.ok(collection.has('key1'), 'key1 should be present in the written file'); - assert.ok( - collection.has('key2'), - 'key2 should be present in the written file (this will FAIL before the fix)', - ); - }); -}); diff --git a/packages/astro/test/units/content-collections/mutable-data-store.test.ts b/packages/astro/test/units/content-collections/mutable-data-store.test.ts new file mode 100644 index 000000000000..a15b2590aa3d --- /dev/null +++ b/packages/astro/test/units/content-collections/mutable-data-store.test.ts @@ -0,0 +1,153 @@ +import { describe, it, before, after } from 'node:test'; +import { strict as assert } from 'node:assert'; +import { promises as fs } from 'node:fs'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; +import * as devalue from 'devalue'; +import { MutableDataStore } from '../../../dist/content/mutable-data-store.js'; +import { imageSrcToImportId } from '../../../dist/assets/utils/resolveImports.js'; + +describe('MutableDataStore', () => { + let tmpDir: string; + + before(async () => { + tmpDir = await mkdtemp(path.join(tmpdir(), 'astro-test-')); + }); + + after(async () => { + try { + await rm(tmpDir, { recursive: true, force: true }); + } catch { + // Ignore cleanup errors + } + }); + + it('removes stale image asset import after entry image path is updated (issue #16097)', async () => { + const assetsFilePath = path.join(tmpDir, 'content-assets.mjs'); + const entryFilePath = 'src/content/categories/example.json'; + const store = new MutableDataStore(); + const scoped = store.scopedStore('categories'); + + scoped.set({ + id: 'example', + data: {}, + filePath: entryFilePath, + assetImports: ['./images/seed.webp'], + }); + + scoped.set({ + id: 'example', + data: {}, + filePath: entryFilePath, + assetImports: ['./images/non-existing.jpg'], + }); + + scoped.set({ + id: 'example', + data: {}, + filePath: entryFilePath, + assetImports: ['./images/seed.webp'], + }); + + await store.writeAssetImports(assetsFilePath); + + const content = await fs.readFile(assetsFilePath, 'utf-8'); + + const validId = imageSrcToImportId('./images/seed.webp', entryFilePath); + const staleId = imageSrcToImportId('./images/non-existing.jpg', entryFilePath); + + assert.ok(!!validId); + assert.ok( + content.includes(validId), + `content-assets.mjs should reference the valid image import "${validId}"`, + ); + assert.ok( + !content.includes('non-existing.jpg'), + `content-assets.mjs must NOT reference the stale invalid import "${staleId}" after the path is restored`, + ); + }); + + it('removes asset imports when an entry is deleted', async () => { + const assetsFilePath = path.join(tmpDir, 'content-assets-delete.mjs'); + const entryFilePath = 'src/content/categories/deleted.json'; + const store = new MutableDataStore(); + const scoped = store.scopedStore('categories'); + + scoped.set({ + id: 'deleted-entry', + data: {}, + filePath: entryFilePath, + assetImports: ['./images/to-be-removed.webp'], + }); + + await store.writeAssetImports(assetsFilePath); + const contentBefore = await fs.readFile(assetsFilePath, 'utf-8'); + assert.ok( + contentBefore.includes('to-be-removed.webp'), + 'should contain the image before deletion', + ); + + scoped.delete('deleted-entry'); + await store.writeAssetImports(assetsFilePath); + await store.waitUntilSaveComplete(); + + const contentAfter = await fs.readFile(assetsFilePath, 'utf-8'); + assert.ok( + !contentAfter.includes('to-be-removed.webp'), + 'should NOT contain the image after the entry is deleted', + ); + }); + + it('removes asset imports when a collection is cleared', async () => { + const assetsFilePath = path.join(tmpDir, 'content-assets-clear.mjs'); + const entryFilePath = 'src/content/blog/post.json'; + const store = new MutableDataStore(); + const scoped = store.scopedStore('blog'); + + scoped.set({ + id: 'post-1', + data: {}, + filePath: entryFilePath, + assetImports: ['./images/cover.webp'], + }); + + await store.writeAssetImports(assetsFilePath); + const contentBefore = await fs.readFile(assetsFilePath, 'utf-8'); + assert.ok(contentBefore.includes('cover.webp'), 'should contain the image before clear'); + + scoped.clear(); + await store.writeAssetImports(assetsFilePath); + await store.waitUntilSaveComplete(); + + const contentAfter = await fs.readFile(assetsFilePath, 'utf-8'); + assert.ok( + !contentAfter.includes('cover.webp'), + 'should NOT contain the image after the collection is cleared', + ); + }); + + it('reproduces race condition: concurrent writeToDisk() calls lose data', async () => { + const filePath = pathToFileURL(path.join(tmpDir, 'data-store.json')); + const store = await MutableDataStore.fromFile(filePath); + + store.set('c', 'key1', { id: 'key1', data: {} }); + const p1 = store.writeToDisk(); + + store.set('c', 'key2', { id: 'key2', data: {} }); + const p2 = store.writeToDisk(); + + await Promise.all([p1, p2]); + + const raw = await fs.readFile(filePath, 'utf-8'); + const collections = devalue.parse(raw); + const collection = collections.get('c'); + + assert.ok(collection.has('key1'), 'key1 should be present in the written file'); + assert.ok( + collection.has('key2'), + 'key2 should be present in the written file (this will FAIL before the fix)', + ); + }); +}); diff --git a/packages/astro/test/units/content-layer/core-loader.test.js b/packages/astro/test/units/content-layer/core-loader.test.ts similarity index 93% rename from packages/astro/test/units/content-layer/core-loader.test.js rename to packages/astro/test/units/content-layer/core-loader.test.ts index 9c953484b555..2ec54e1c55fa 100644 --- a/packages/astro/test/units/content-layer/core-loader.test.js +++ b/packages/astro/test/units/content-layer/core-loader.test.ts @@ -4,17 +4,17 @@ import { z } from 'zod'; import { defineCollection } from '../../../dist/content/config.js'; import { ContentLayer } from '../../../dist/content/content-layer.js'; import { MutableDataStore } from '../../../dist/content/mutable-data-store.js'; -import { Logger } from '../../../dist/core/logger/core.js'; +import { AstroLogger } from '../../../dist/core/logger/core.js'; -import { createTempDir, createTestConfigObserver, createMinimalSettings } from './test-helpers.js'; +import { createTempDir, createTestConfigObserver, createMinimalSettings } from './test-helpers.ts'; describe('Core Content Layer loader', () => { - let logger; + let logger: any; const root = createTempDir(); before(() => { - logger = new Logger({ - dest: { write: () => true }, + logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); }); @@ -130,7 +130,7 @@ hello // Create a loader that renders markdown const markdownRenderingLoader = { name: 'markdown-rendering-loader', - load: async (context) => { + load: async (context: any) => { const result = await context.renderMarkdown(markdownContent, { fileURL: new URL('test.md', root), }); @@ -179,7 +179,7 @@ hello // Sync content await contentLayer.sync(); - const entry = store.get('increment', 'value'); + const entry: any = store.get('increment', 'value'); assert.ok(entry); assert.ok(entry.data.renderedHtml); assert.ok(entry.data.renderedHtml.includes('

heading 1

')); @@ -195,7 +195,7 @@ hello // Create a loader that returns Date objects const dateLoader = { name: 'date-loader', - load: async (context) => { + load: async (context: any) => { await context.store.set({ id: 'test-date', data: { @@ -228,7 +228,7 @@ hello // Sync content await contentLayer.sync(); - const entry = store.get('dates', 'test-date'); + const entry: any = store.get('dates', 'test-date'); assert.ok(entry); assert.ok(entry.data.created instanceof Date); assert.equal(entry.data.created.toISOString(), now.toISOString()); @@ -241,7 +241,7 @@ hello // Create a loader that uses slug field const slugLoader = { name: 'slug-loader', - load: async (context) => { + load: async (context: any) => { const data = { lastValue: 1, lastUpdated: new Date(), @@ -283,7 +283,7 @@ hello // Sync content await contentLayer.sync(); - const entry = store.get('increment', 'value'); + const entry: any = store.get('increment', 'value'); assert.ok(entry); assert.equal(entry.data.slug, 'slimy'); }); diff --git a/packages/astro/test/units/content-layer/data-transforms.test.js b/packages/astro/test/units/content-layer/data-transforms.test.ts similarity index 87% rename from packages/astro/test/units/content-layer/data-transforms.test.js rename to packages/astro/test/units/content-layer/data-transforms.test.ts index c4b68f818046..2b1e8392b51a 100644 --- a/packages/astro/test/units/content-layer/data-transforms.test.js +++ b/packages/astro/test/units/content-layer/data-transforms.test.ts @@ -5,8 +5,8 @@ import { defineCollection } from '../../../dist/content/config.js'; import { createReference } from '../../../dist/content/runtime.js'; import { ContentLayer } from '../../../dist/content/content-layer.js'; import { MutableDataStore } from '../../../dist/content/mutable-data-store.js'; -import { Logger } from '../../../dist/core/logger/core.js'; -import { createTempDir, createTestConfigObserver, createMinimalSettings } from './test-helpers.js'; +import { AstroLogger } from '../../../dist/core/logger/core.js'; +import { createTempDir, createTestConfigObserver, createMinimalSettings } from './test-helpers.ts'; describe('Content Layer - Data Transforms', () => { const root = createTempDir(); @@ -15,15 +15,15 @@ describe('Content Layer - Data Transforms', () => { it('transforms reference strings to reference objects', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); // Create a loader that returns data with reference strings const dogsLoader = { name: 'dogs-loader', - load: async (context) => { + load: async (context: any) => { const data = { id: 'beagle', name: 'Beagle Dog', @@ -62,7 +62,7 @@ describe('Content Layer - Data Transforms', () => { await contentLayer.sync(); - const result = store.get('dogs', 'beagle'); + const result: any = store.get('dogs', 'beagle'); assert.ok(result); assert.equal(result.data.id, 'beagle'); assert.equal(result.data.name, 'Beagle Dog'); @@ -72,14 +72,14 @@ describe('Content Layer - Data Transforms', () => { it('transforms dates correctly', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); const eventsLoader = { name: 'events-loader', - load: async (context) => { + load: async (context: any) => { const data = { id: 'event1', title: 'Launch Event', @@ -120,7 +120,7 @@ describe('Content Layer - Data Transforms', () => { await contentLayer.sync(); - const result = store.get('events', 'event1'); + const result: any = store.get('events', 'event1'); assert.ok(result); assert.ok(result.data.publishedDate instanceof Date); assert.ok(result.data.eventTime instanceof Date); @@ -131,14 +131,14 @@ describe('Content Layer - Data Transforms', () => { it('applies schema defaults', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); const productsLoader = { name: 'products-loader', - load: async (context) => { + load: async (context: any) => { const data = { id: 'product1', name: 'Basic Product', @@ -179,7 +179,7 @@ describe('Content Layer - Data Transforms', () => { await contentLayer.sync(); - const result = store.get('products', 'product1'); + const result: any = store.get('products', 'product1'); assert.ok(result); assert.equal(result.data.inStock, false); assert.equal(result.data.category, 'uncategorized'); @@ -189,14 +189,14 @@ describe('Content Layer - Data Transforms', () => { it('handles array of references', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); const teamsLoader = { name: 'teams-loader', - load: async (context) => { + load: async (context: any) => { const data = { id: 'team1', name: 'Rocket Team', @@ -235,7 +235,7 @@ describe('Content Layer - Data Transforms', () => { await contentLayer.sync(); - const result = store.get('teams', 'team1'); + const result: any = store.get('teams', 'team1'); assert.ok(result); assert.equal(result.data.members.length, 3); assert.deepEqual(result.data.members[0], { collection: 'people', id: 'john' }); @@ -246,14 +246,14 @@ describe('Content Layer - Data Transforms', () => { it('validates and rejects invalid data', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); const itemsLoader = { name: 'items-loader', - load: async (context) => { + load: async (context: any) => { const data = { id: 'invalid', name: 'Test Item', @@ -271,7 +271,7 @@ describe('Content Layer - Data Transforms', () => { id: 'invalid', data: parsed, }); - } catch (error) { + } catch (error: any) { // Store error info for testing await context.store.set({ id: 'error', @@ -306,11 +306,11 @@ describe('Content Layer - Data Transforms', () => { await contentLayer.sync(); // The invalid entry should not be stored - const invalidEntry = store.get('items', 'invalid'); + const invalidEntry: any = store.get('items', 'invalid'); assert.equal(invalidEntry, undefined); // Check if error was captured - const errorEntry = store.get('items', 'error'); + const errorEntry: any = store.get('items', 'error'); assert.ok(errorEntry); assert.equal(errorEntry.data.hasError, true); assert.ok(errorEntry.data.errorMessage.includes('data does not match collection schema')); @@ -319,14 +319,14 @@ describe('Content Layer - Data Transforms', () => { it('handles nested schemas with mixed transforms', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); const articlesLoader = { name: 'articles-loader', - load: async (context) => { + load: async (context: any) => { const data = { id: 'complex', metadata: { @@ -380,7 +380,7 @@ describe('Content Layer - Data Transforms', () => { await contentLayer.sync(); - const result = store.get('articles', 'complex'); + const result: any = store.get('articles', 'complex'); assert.ok(result); assert.ok(result.data.metadata.created instanceof Date); assert.ok(result.data.metadata.updated instanceof Date); @@ -393,14 +393,14 @@ describe('Content Layer - Data Transforms', () => { it('handles optional fields correctly', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); const minimalProductLoader = { name: 'minimal-product-loader', - load: async (context) => { + load: async (context: any) => { const data = { id: 'minimal', name: 'Minimal Product', @@ -441,7 +441,7 @@ describe('Content Layer - Data Transforms', () => { await contentLayer.sync(); - const result = store.get('products', 'minimal'); + const result: any = store.get('products', 'minimal'); assert.ok(result); assert.equal(result.data.description, undefined); assert.equal(result.data.price, undefined); @@ -451,14 +451,14 @@ describe('Content Layer - Data Transforms', () => { it('transforms reference with default value', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); const itemsLoader = { name: 'items-loader', - load: async (context) => { + load: async (context: any) => { // Load two items - one with category, one without const items = [ { @@ -493,7 +493,7 @@ describe('Content Layer - Data Transforms', () => { schema: z.object({ id: z.string(), name: z.string(), - category: reference('categories').default('general'), + category: (reference as any)('categories').default('general'), }), }), }; @@ -507,10 +507,10 @@ describe('Content Layer - Data Transforms', () => { await contentLayer.sync(); - const result1 = store.get('items', 'item1'); + const result1: any = store.get('items', 'item1'); assert.deepEqual(result1.data.category, { collection: 'categories', id: 'electronics' }); - const result2 = store.get('items', 'item2'); + const result2: any = store.get('items', 'item2'); // The default is applied as a string, not transformed to a reference object assert.equal(result2.data.category, 'general'); }); diff --git a/packages/astro/test/units/content-layer/file-loader.test.js b/packages/astro/test/units/content-layer/file-loader.test.ts similarity index 91% rename from packages/astro/test/units/content-layer/file-loader.test.js rename to packages/astro/test/units/content-layer/file-loader.test.ts index 5f1add6e6a1d..d1d13df21cfb 100644 --- a/packages/astro/test/units/content-layer/file-loader.test.js +++ b/packages/astro/test/units/content-layer/file-loader.test.ts @@ -5,8 +5,8 @@ import { file } from '../../../dist/content/loaders/file.js'; import { defineCollection } from '../../../dist/content/config.js'; import { ContentLayer } from '../../../dist/content/content-layer.js'; import { MutableDataStore } from '../../../dist/content/mutable-data-store.js'; -import { Logger } from '../../../dist/core/logger/core.js'; -import { createTestConfigObserver, createMinimalSettings } from './test-helpers.js'; +import { AstroLogger } from '../../../dist/core/logger/core.js'; +import { createTestConfigObserver, createMinimalSettings } from './test-helpers.ts'; describe('File Loader', () => { const root = new URL('../../fixtures/content-layer/', import.meta.url); @@ -14,8 +14,8 @@ describe('File Loader', () => { it('loads entries from JSON file', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); @@ -57,8 +57,8 @@ describe('File Loader', () => { it('loads entries from YAML file', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); @@ -95,8 +95,8 @@ describe('File Loader', () => { it('loads entries from TOML file', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); @@ -127,8 +127,8 @@ describe('File Loader', () => { it('loads entries from CSV file with custom parser', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); @@ -173,8 +173,8 @@ describe('File Loader', () => { it('loads nested JSON with custom parser', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); @@ -214,8 +214,8 @@ describe('File Loader', () => { it('uses async parser', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); @@ -249,10 +249,10 @@ describe('File Loader', () => { const settings = createMinimalSettings(root); // Create a custom logger to capture warnings - const warnings = []; - const logger = new Logger({ - dest: { - write: (msg) => { + const warnings: string[] = []; + const logger = new AstroLogger({ + destination: { + write: (msg: any) => { if (msg.level === 'warn') { warnings.push(msg.message); } diff --git a/packages/astro/test/units/content-layer/glob-loader.test.js b/packages/astro/test/units/content-layer/glob-loader.test.ts similarity index 87% rename from packages/astro/test/units/content-layer/glob-loader.test.js rename to packages/astro/test/units/content-layer/glob-loader.test.ts index 4fc4892ab3a4..a3114781c4a0 100644 --- a/packages/astro/test/units/content-layer/glob-loader.test.js +++ b/packages/astro/test/units/content-layer/glob-loader.test.ts @@ -4,12 +4,12 @@ import { glob } from '../../../dist/content/loaders/glob.js'; import { defineCollection } from '../../../dist/content/config.js'; import { ContentLayer } from '../../../dist/content/content-layer.js'; import { MutableDataStore } from '../../../dist/content/mutable-data-store.js'; -import { Logger } from '../../../dist/core/logger/core.js'; +import { AstroLogger } from '../../../dist/core/logger/core.js'; import { createTestConfigObserver, createMinimalSettings, createMarkdownEntryType, -} from './test-helpers.js'; +} from './test-helpers.ts'; describe('Glob Loader', () => { const root = new URL('../../fixtures/content-layer/', import.meta.url); @@ -19,8 +19,8 @@ describe('Glob Loader', () => { const settings = createMinimalSettings(root, { contentEntryTypes: [createMarkdownEntryType()], }); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); @@ -47,7 +47,7 @@ describe('Glob Loader', () => { assert.ok(columbia); assert.ok(columbia.body); assert.ok(columbia.body.includes('Space Shuttle Columbia')); - assert.equal(columbia.filePath.replace(/\\/g, '/'), 'src/content/space/columbia.md'); + assert.equal(columbia.filePath!.replace(/\\/g, '/'), 'src/content/space/columbia.md'); }); it('handles negative matches in glob pattern', async () => { @@ -55,8 +55,8 @@ describe('Glob Loader', () => { const settings = createMinimalSettings(root, { contentEntryTypes: [createMarkdownEntryType()], }); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); @@ -91,8 +91,8 @@ describe('Glob Loader', () => { const settings = createMinimalSettings(root, { contentEntryTypes: [createMarkdownEntryType()], }); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); @@ -124,8 +124,8 @@ describe('Glob Loader', () => { const settings = createMinimalSettings(root, { contentEntryTypes: [createMarkdownEntryType()], }); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); @@ -157,11 +157,11 @@ describe('Glob Loader', () => { // Create custom YAML data entry type const yamlEntryType = { extensions: ['.yaml', '.yml'], - getEntryInfo: ({ contents }) => { + getEntryInfo: ({ contents }: any) => { // Simple YAML parser const lines = contents.trim().split('\n'); - const data = {}; - lines.forEach((line) => { + const data: Record = {}; + lines.forEach((line: string) => { const colonIndex = line.indexOf(':'); if (colonIndex > -1) { const key = line.substring(0, colonIndex).trim(); @@ -183,8 +183,8 @@ describe('Glob Loader', () => { }, dataEntryTypes: [yamlEntryType], }); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); @@ -221,11 +221,11 @@ describe('Glob Loader', () => { // Create custom TOML data entry type const tomlEntryType = { extensions: ['.toml'], - getEntryInfo: ({ contents }) => { + getEntryInfo: ({ contents }: any) => { // Simple TOML parser for key-value pairs const lines = contents.trim().split('\n'); - const data = {}; - lines.forEach((line) => { + const data: Record = {}; + lines.forEach((line: string) => { const equalIndex = line.indexOf('='); if (equalIndex > -1) { const key = line.substring(0, equalIndex).trim(); @@ -247,8 +247,8 @@ describe('Glob Loader', () => { }, dataEntryTypes: [tomlEntryType], }); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); @@ -281,10 +281,10 @@ describe('Glob Loader', () => { it('warns about missing directory', async () => { const store = new MutableDataStore(); - const warnings = []; - const logger = new Logger({ - dest: { - write: (msg) => { + const warnings: string[] = []; + const logger = new AstroLogger({ + destination: { + write: (msg: any) => { if (msg.level === 'warn') { warnings.push(msg.message); } @@ -316,10 +316,10 @@ describe('Glob Loader', () => { it('warns about no matching files', async () => { const store = new MutableDataStore(); - const warnings = []; - const logger = new Logger({ - dest: { - write: (msg) => { + const warnings: string[] = []; + const logger = new AstroLogger({ + destination: { + write: (msg: any) => { if (msg.level === 'warn') { warnings.push(msg.message); } diff --git a/packages/astro/test/units/content-layer/live-loaders.test.js b/packages/astro/test/units/content-layer/live-loaders.test.ts similarity index 87% rename from packages/astro/test/units/content-layer/live-loaders.test.js rename to packages/astro/test/units/content-layer/live-loaders.test.ts index 3413cca5cfc8..ca1c6498134c 100644 --- a/packages/astro/test/units/content-layer/live-loaders.test.js +++ b/packages/astro/test/units/content-layer/live-loaders.test.ts @@ -4,8 +4,8 @@ import { z } from 'zod'; import { defineCollection } from '../../../dist/content/config.js'; import { ContentLayer } from '../../../dist/content/content-layer.js'; import { MutableDataStore } from '../../../dist/content/mutable-data-store.js'; -import { Logger } from '../../../dist/core/logger/core.js'; -import { createTempDir, createTestConfigObserver, createMinimalSettings } from './test-helpers.js'; +import { AstroLogger } from '../../../dist/core/logger/core.js'; +import { createTempDir, createTestConfigObserver, createMinimalSettings } from './test-helpers.ts'; describe('Content Layer - Live Loaders', () => { const root = createTempDir(); @@ -13,8 +13,8 @@ describe('Content Layer - Live Loaders', () => { it('loads initial data through sync', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); @@ -38,7 +38,7 @@ describe('Content Layer - Live Loaders', () => { // Create a live loader const testLoader = { name: 'test-loader', - load: async (context) => { + load: async (context: any) => { // Sync loader that loads initial data for (const entry of Object.values(entries)) { const parsed = await context.parseData({ @@ -49,7 +49,7 @@ describe('Content Layer - Live Loaders', () => { await context.store.set({ id: entry.id, data: parsed, - rendered: entry.rendered, + rendered: (entry as any).rendered, }); } }, @@ -79,20 +79,20 @@ describe('Content Layer - Live Loaders', () => { assert.equal(allEntries.length, 3); // Check individual entries - const entry1 = store.get('liveStuff', '123'); + const entry1: any = store.get('liveStuff', '123'); assert.ok(entry1); assert.equal(entry1.data.title, 'Page 123'); assert.equal(entry1.data.age, 10); assert.ok(entry1.rendered); assert.equal(entry1.rendered.html, '

Page 123

This is rendered content.

'); - const entry2 = store.get('liveStuff', '456'); + const entry2: any = store.get('liveStuff', '456'); assert.ok(entry2); assert.equal(entry2.data.title, 'Page 456'); assert.equal(entry2.data.age, 20); assert.ok(!entry2.rendered); // No rendered content for this entry - const entry3 = store.get('liveStuff', '789'); + const entry3: any = store.get('liveStuff', '789'); assert.ok(entry3); assert.equal(entry3.data.title, 'Page 789'); assert.equal(entry3.data.age, 30); @@ -101,8 +101,8 @@ describe('Content Layer - Live Loaders', () => { it('simulates live loader with loadEntry functionality', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); @@ -115,7 +115,7 @@ describe('Content Layer - Live Loaders', () => { // Loader that simulates live loading behavior const liveSimulationLoader = { name: 'live-simulation-loader', - load: async (context) => { + load: async (context: any) => { // Initial load - only load entry 123 const entry = dataSource['123']; const parsed = await context.parseData({ @@ -161,16 +161,16 @@ describe('Content Layer - Live Loaders', () => { await contentLayer.sync(); // Check initial state - const entry123 = store.get('liveSimulation', '123'); + const entry123: any = store.get('liveSimulation', '123'); assert.ok(entry123); assert.equal(entry123.data.title, 'Page 123'); // Entry 456 would not be loaded initially - const entry456 = store.get('liveSimulation', '456'); + const entry456: any = store.get('liveSimulation', '456'); assert.ok(!entry456); // Check metadata - const meta = store.get('liveSimulation', '_meta'); + const meta: any = store.get('liveSimulation', '_meta'); assert.ok(meta); assert.deepEqual(meta.data.availableIds, ['123', '456']); assert.equal(meta.data.supportsLiveLoading, true); @@ -179,15 +179,15 @@ describe('Content Layer - Live Loaders', () => { it('demonstrates dynamic data transformation', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); // Loader that transforms data based on context const transformLoader = { name: 'transform-loader', - load: async (context) => { + load: async (context: any) => { const entries = [ { id: '1', data: { title: 'Entry 1', value: 10, category: 'A' } }, { id: '2', data: { title: 'Entry 2', value: 20, category: 'B' } }, @@ -241,12 +241,12 @@ describe('Content Layer - Live Loaders', () => { await contentLayer.sync(); // Verify transformations - const entry1 = store.get('transformed', '1'); + const entry1: any = store.get('transformed', '1'); assert.ok(entry1); assert.equal(entry1.data.doubled, 20); assert.equal(entry1.data.categoryLabel, 'Category A'); - const entry2 = store.get('transformed', '2'); + const entry2: any = store.get('transformed', '2'); assert.ok(entry2); assert.equal(entry2.data.doubled, 40); assert.equal(entry2.data.categoryLabel, 'Category B'); @@ -255,15 +255,15 @@ describe('Content Layer - Live Loaders', () => { it('handles loader errors gracefully', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); // Loader that simulates error conditions const errorProneLoader = { name: 'error-prone-loader', - load: async (context) => { + load: async (context: any) => { // Add some valid entries await context.store.set({ id: 'valid-1', @@ -280,7 +280,7 @@ describe('Content Layer - Live Loaders', () => { id: 'invalid-1', data: parsed, }); - } catch (error) { + } catch (error: any) { // Store error information await context.store.set({ id: 'error-log', @@ -315,17 +315,17 @@ describe('Content Layer - Live Loaders', () => { await contentLayer.sync(); // Check valid entry - const validEntry = store.get('errorProne', 'valid-1'); + const validEntry: any = store.get('errorProne', 'valid-1'); assert.ok(validEntry); assert.equal(validEntry.data.title, 'Valid Entry 1'); assert.equal(validEntry.data.status, 'ok'); // Check that invalid entry was not stored - const invalidEntry = store.get('errorProne', 'invalid-1'); + const invalidEntry: any = store.get('errorProne', 'invalid-1'); assert.ok(!invalidEntry); // Check error log - const errorLog = store.get('errorProne', 'error-log'); + const errorLog: any = store.get('errorProne', 'error-log'); assert.ok(errorLog); assert.ok(errorLog.data.errorMessage); }); @@ -333,14 +333,14 @@ describe('Content Layer - Live Loaders', () => { it('supports complex rendered content', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); const renderedContentLoader = { name: 'rendered-content-loader', - load: async (context) => { + load: async (context: any) => { const articles = [ { id: 'article-1', @@ -413,7 +413,7 @@ describe('Content Layer - Live Loaders', () => { await contentLayer.sync(); // Check first article - const article1 = store.get('articles', 'article-1'); + const article1: any = store.get('articles', 'article-1'); assert.ok(article1); assert.equal(article1.data.title, 'First Article'); assert.ok(article1.body); @@ -424,7 +424,7 @@ describe('Content Layer - Live Loaders', () => { assert.ok(article1.rendered.metadata.wordCount > 0); // Check second article - const article2 = store.get('articles', 'article-2'); + const article2: any = store.get('articles', 'article-2'); assert.ok(article2); assert.ok(article2.rendered); // Check for code block rendering @@ -434,14 +434,14 @@ describe('Content Layer - Live Loaders', () => { it('demonstrates cache metadata patterns', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); const cacheAwareLoader = { name: 'cache-aware-loader', - load: async (context) => { + load: async (context: any) => { const now = new Date(); const entries = [ { @@ -532,19 +532,19 @@ describe('Content Layer - Live Loaders', () => { await contentLayer.sync(); // Verify static content caching - const staticContent = store.get('cached', 'static-content'); + const staticContent: any = store.get('cached', 'static-content'); assert.ok(staticContent); assert.equal(staticContent.data.cacheInfo.maxAge, 86400 * 30); assert.ok(staticContent.data.cacheInfo.tags.includes('static')); // Verify dynamic content caching - const dynamicContent = store.get('cached', 'dynamic-content'); + const dynamicContent: any = store.get('cached', 'dynamic-content'); assert.ok(dynamicContent); assert.equal(dynamicContent.data.cacheInfo.maxAge, 300); assert.ok(dynamicContent.data.cacheInfo.tags.includes('realtime')); // Verify personalized content caching - const userContent = store.get('cached', 'user-content'); + const userContent: any = store.get('cached', 'user-content'); assert.ok(userContent); assert.equal(userContent.data.cacheInfo.maxAge, 0); assert.ok(userContent.data.cacheInfo.tags.includes('no-cache')); @@ -553,14 +553,14 @@ describe('Content Layer - Live Loaders', () => { it('validates schema during data loading', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); const validationLoader = { name: 'validation-loader', - load: async (context) => { + load: async (context: any) => { const testData = [ // Valid entries { id: 'valid-1', data: { name: 'Alice', age: 30, email: 'alice@example.com' } }, @@ -586,7 +586,7 @@ describe('Content Layer - Live Loaders', () => { data: parsed, }); successCount++; - } catch (_error) { + } catch (_error: any) { errorCount++; // Optionally store validation errors if (item.id.startsWith('invalid')) { @@ -641,27 +641,27 @@ describe('Content Layer - Live Loaders', () => { await contentLayer.sync(); // Check valid entries - const valid1 = store.get('validated', 'valid-1'); + const valid1: any = store.get('validated', 'valid-1'); assert.ok(valid1); assert.equal(valid1.data.name, 'Alice'); assert.equal(valid1.data.age, 30); - const valid2 = store.get('validated', 'valid-2'); + const valid2: any = store.get('validated', 'valid-2'); assert.ok(valid2); assert.equal(valid2.data.name, 'Bob'); // Check that invalid entries were not stored - const invalidAge = store.get('validated', 'invalid-age'); + const invalidAge: any = store.get('validated', 'invalid-age'); assert.ok(!invalidAge); - const invalidEmail = store.get('validated', 'invalid-email'); + const invalidEmail: any = store.get('validated', 'invalid-email'); assert.ok(!invalidEmail); - const missingField = store.get('validated', 'missing-field'); + const missingField: any = store.get('validated', 'missing-field'); assert.ok(!missingField); // Check summary - const summary = store.get('validated', '_validation_summary'); + const summary: any = store.get('validated', '_validation_summary'); assert.ok(summary); assert.equal(summary.data.successCount, 2); // Only valid-1 and valid-2 assert.equal(summary.data.errorCount, 3); // Three invalid entries diff --git a/packages/astro/test/units/content-layer/loader-warnings.test.js b/packages/astro/test/units/content-layer/loader-warnings.test.ts similarity index 88% rename from packages/astro/test/units/content-layer/loader-warnings.test.js rename to packages/astro/test/units/content-layer/loader-warnings.test.ts index 9408486619cd..7be355b1269d 100644 --- a/packages/astro/test/units/content-layer/loader-warnings.test.js +++ b/packages/astro/test/units/content-layer/loader-warnings.test.ts @@ -4,8 +4,8 @@ import { z } from 'zod'; import { defineCollection } from '../../../dist/content/config.js'; import { ContentLayer } from '../../../dist/content/content-layer.js'; import { MutableDataStore } from '../../../dist/content/mutable-data-store.js'; -import { Logger } from '../../../dist/core/logger/core.js'; -import { createTempDir, createTestConfigObserver, createMinimalSettings } from './test-helpers.js'; +import { AstroLogger } from '../../../dist/core/logger/core.js'; +import { createTempDir, createTestConfigObserver, createMinimalSettings } from './test-helpers.ts'; import { Writable } from 'node:stream'; import fs from 'node:fs/promises'; @@ -13,13 +13,13 @@ describe('Content Layer - Loader Warnings', () => { it('warns about missing data in loaders', async () => { const root = createTempDir(); const store = new MutableDataStore(); - const logs = []; + const logs: any[] = []; - const logger = new Logger({ + const logger = new AstroLogger({ level: 'warn', - dest: new Writable({ + destination: new Writable({ objectMode: true, - write(event, _, callback) { + write(event: any, _: any, callback: any) { logs.push(event); callback(); }, @@ -29,7 +29,7 @@ describe('Content Layer - Loader Warnings', () => { // Loader that simulates various warning scenarios const warningLoader = { name: 'warning-loader', - load: async (context) => { + load: async (context: any) => { // Warn about missing directory context.logger.warn('Directory "src/content/non-existent-dir" does not exist'); @@ -90,12 +90,12 @@ describe('Content Layer - Loader Warnings', () => { assert.ok(duplicateWarning, 'Should warn about duplicate ID'); // Verify entries - const validEntry = store.get('warnings', 'valid-1'); + const validEntry: any = store.get('warnings', 'valid-1'); assert.ok(validEntry); assert.equal(validEntry.data.title, 'Valid Entry'); // Duplicate ID should have the second entry's data (overwritten) - const duplicateEntry = store.get('warnings', 'duplicate-id'); + const duplicateEntry: any = store.get('warnings', 'duplicate-id'); assert.ok(duplicateEntry); assert.equal(duplicateEntry.data.title, 'Second Entry'); assert.equal(duplicateEntry.data.value, 2); @@ -104,13 +104,13 @@ describe('Content Layer - Loader Warnings', () => { it('warns about no files found in pattern matching', async () => { const root = createTempDir(); const store = new MutableDataStore(); - const logs = []; + const logs: any[] = []; - const logger = new Logger({ + const logger = new AstroLogger({ level: 'warn', - dest: new Writable({ + destination: new Writable({ objectMode: true, - write(event, _, callback) { + write(event: any, _: any, callback: any) { logs.push(event); callback(); }, @@ -124,7 +124,7 @@ describe('Content Layer - Loader Warnings', () => { // Loader that simulates glob pattern with no matches const emptyPatternLoader = { name: 'empty-pattern-loader', - load: async (context) => { + load: async (context: any) => { // Simulate checking for files and finding none const pattern = '*.mdx'; const base = 'src/content/empty'; @@ -174,7 +174,7 @@ describe('Content Layer - Loader Warnings', () => { assert.ok(noFilesWarning, 'Should warn about no files found'); // Check metadata - const meta = store.get('emptyPattern', '_meta'); + const meta: any = store.get('emptyPattern', '_meta'); assert.ok(meta); assert.equal(meta.data.filesFound, 0); }); @@ -182,13 +182,13 @@ describe('Content Layer - Loader Warnings', () => { it('handles validation errors gracefully', async () => { const root = createTempDir(); const store = new MutableDataStore(); - const logs = []; + const logs: any[] = []; - const logger = new Logger({ + const logger = new AstroLogger({ level: 'error', - dest: new Writable({ + destination: new Writable({ objectMode: true, - write(event, _, callback) { + write(event: any, _: any, callback: any) { logs.push(event); callback(); }, @@ -198,7 +198,7 @@ describe('Content Layer - Loader Warnings', () => { // Loader that produces validation errors const validationErrorLoader = { name: 'validation-error-loader', - load: async (context) => { + load: async (context: any) => { const testData = [ { id: 'item1', name: 'Valid Item', count: 5 }, { id: 'item2', count: 10 }, // Missing required 'name' @@ -221,7 +221,7 @@ describe('Content Layer - Loader Warnings', () => { data: parsed, }); successCount++; - } catch (error) { + } catch (error: any) { errorCount++; context.logger.error(`Validation failed for ${item.id || 'unknown'}: ${error.message}`); } @@ -276,13 +276,13 @@ describe('Content Layer - Loader Warnings', () => { assert.ok(validationErrors.length > 0, 'Should log validation errors'); // Check valid entry - const validEntry = store.get('validated', 'item1'); + const validEntry: any = store.get('validated', 'item1'); assert.ok(validEntry); assert.equal(validEntry.data.name, 'Valid Item'); assert.equal(validEntry.data.count, 5); // Check summary - const summary = store.get('validated', '_summary'); + const summary: any = store.get('validated', '_summary'); assert.ok(summary); assert.ok(summary.data.validationStats.errors > 0); }); @@ -290,13 +290,13 @@ describe('Content Layer - Loader Warnings', () => { it('handles malformed data gracefully', async () => { const root = createTempDir(); const store = new MutableDataStore(); - const logs = []; + const logs: any[] = []; - const logger = new Logger({ + const logger = new AstroLogger({ level: 'error', - dest: new Writable({ + destination: new Writable({ objectMode: true, - write(event, _, callback) { + write(event: any, _: any, callback: any) { logs.push(event); callback(); }, @@ -306,7 +306,7 @@ describe('Content Layer - Loader Warnings', () => { // Loader that simulates processing malformed data const malformedDataLoader = { name: 'malformed-data-loader', - load: async (context) => { + load: async (context: any) => { // Simulate trying to parse malformed JSON const malformedJson = '{ "id": "test", "name": "Missing closing brace"'; @@ -317,7 +317,7 @@ describe('Content Layer - Loader Warnings', () => { id: 'should-not-exist', data, }); - } catch (error) { + } catch (error: any) { context.logger.error(`Failed to parse JSON: ${error.message}`); // Store error info @@ -370,13 +370,13 @@ describe('Content Layer - Loader Warnings', () => { assert.ok(jsonError, 'Should log JSON parse error'); // Check that error was handled - const errorEntry = store.get('malformed', 'parse-error'); + const errorEntry: any = store.get('malformed', 'parse-error'); assert.ok(errorEntry); assert.equal(errorEntry.data.error, 'JSON Parse Error'); assert.ok(errorEntry.data.recovered); // Check that loader continued after error - const validEntry = store.get('malformed', 'valid-after-error'); + const validEntry: any = store.get('malformed', 'valid-after-error'); assert.ok(validEntry); assert.equal(validEntry.data.error, 'None'); }); @@ -384,13 +384,13 @@ describe('Content Layer - Loader Warnings', () => { it('warns about duplicate IDs across multiple entries', async () => { const root = createTempDir(); const store = new MutableDataStore(); - const logs = []; + const logs: any[] = []; - const logger = new Logger({ + const logger = new AstroLogger({ level: 'warn', - dest: new Writable({ + destination: new Writable({ objectMode: true, - write(event, _, callback) { + write(event: any, _: any, callback: any) { logs.push(event); callback(); }, @@ -414,7 +414,7 @@ describe('Content Layer - Loader Warnings', () => { // Loader that processes array data and warns about duplicates const duplicateCheckLoader = { name: 'duplicate-check-loader', - load: async (context) => { + load: async (context: any) => { // Read and parse the file const filePath = new URL('./dogs.json', dataDir); const content = await fs.readFile(filePath, 'utf-8'); @@ -477,7 +477,7 @@ describe('Content Layer - Loader Warnings', () => { const entries = store.values('dogs'); assert.equal(entries.length, 2); // Only 2 unique IDs - const germanShepherd = store.get('dogs', 'german-shepherd'); + const germanShepherd: any = store.get('dogs', 'german-shepherd'); assert.ok(germanShepherd); assert.equal(germanShepherd.data.breed, 'German Shepherd Mix'); // Last one wins assert.equal(germanShepherd.data.size, 'Medium'); @@ -486,13 +486,13 @@ describe('Content Layer - Loader Warnings', () => { it('handles missing required fields with helpful errors', async () => { const root = createTempDir(); const store = new MutableDataStore(); - const logs = []; + const logs: any[] = []; - const logger = new Logger({ + const logger = new AstroLogger({ level: 'error', - dest: new Writable({ + destination: new Writable({ objectMode: true, - write(event, _, callback) { + write(event: any, _: any, callback: any) { logs.push(event); callback(); }, @@ -502,7 +502,7 @@ describe('Content Layer - Loader Warnings', () => { // Loader with strict schema validation const strictSchemaLoader = { name: 'strict-schema-loader', - load: async (context) => { + load: async (context: any) => { const items = [ { id: 'complete', title: 'Complete Item', priority: 'high', tags: ['important'] }, { id: 'missing-title', priority: 'low', tags: [] }, // Missing required title @@ -521,10 +521,10 @@ describe('Content Layer - Loader Warnings', () => { id: item.id, data: parsed, }); - } catch (error) { + } catch (error: any) { // Log detailed validation error const issues = error.errors || []; - const fields = issues.map((issue) => issue.path.join('.')).join(', '); + const fields = issues.map((issue: any) => issue.path.join('.')).join(', '); context.logger.error( `Validation failed for item "${item.id}": Missing or invalid fields: ${fields || error.message}`, ); @@ -562,7 +562,7 @@ describe('Content Layer - Loader Warnings', () => { assert.ok(validationLogs.length >= 2, 'Should have validation errors for invalid items'); // Only complete item should be stored - const completeItem = store.get('strictItems', 'complete'); + const completeItem: any = store.get('strictItems', 'complete'); assert.ok(completeItem); assert.equal(completeItem.data.title, 'Complete Item'); assert.equal(completeItem.data.priority, 'high'); diff --git a/packages/astro/test/units/content-layer/markdown-rendering.test.js b/packages/astro/test/units/content-layer/markdown-rendering.test.ts similarity index 88% rename from packages/astro/test/units/content-layer/markdown-rendering.test.js rename to packages/astro/test/units/content-layer/markdown-rendering.test.ts index 8af4081de312..0bbc45d4d5c2 100644 --- a/packages/astro/test/units/content-layer/markdown-rendering.test.js +++ b/packages/astro/test/units/content-layer/markdown-rendering.test.ts @@ -2,7 +2,7 @@ import { strict as assert } from 'node:assert'; import { describe, it } from 'node:test'; import { ContentLayer } from '../../../dist/content/content-layer.js'; import { MutableDataStore } from '../../../dist/content/mutable-data-store.js'; -import { Logger } from '../../../dist/core/logger/core.js'; +import { AstroLogger } from '../../../dist/core/logger/core.js'; import { defineCollection } from '../../../dist/content/config.js'; import { z } from 'zod'; import { @@ -10,7 +10,7 @@ import { createTestConfigObserver, createMinimalSettings, parseSimpleMarkdownFrontmatter, -} from './test-helpers.js'; +} from './test-helpers.ts'; describe('Content Layer - Markdown Rendering', () => { // Create a real temp directory for tests @@ -22,7 +22,7 @@ describe('Content Layer - Markdown Rendering', () => { // Inline loader with markdown content const markdownLoader = { name: 'test-markdown-loader', - load: async (context) => { + load: async (context: any) => { const posts = [ { id: 'post-1', @@ -83,8 +83,8 @@ Content with [a link](https://astro.build).`, }; const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); @@ -100,7 +100,7 @@ Content with [a link](https://astro.build).`, await contentLayer.sync(); // Verify markdown was processed - const post1 = store.get('posts', 'post-1'); + const post1: any = store.get('posts', 'post-1'); assert.ok(post1); assert.equal(post1.data.title, 'Test Post'); assert.equal(post1.data.description, 'This is a test post'); @@ -109,7 +109,7 @@ Content with [a link](https://astro.build).`, assert.ok(post1.body); assert.ok(post1.body.includes('# Hello World')); - const post2 = store.get('posts', 'post-2'); + const post2: any = store.get('posts', 'post-2'); assert.ok(post2); assert.equal(post2.data.title, 'Another Post'); assert.ok(post2.data.publishedDate instanceof Date); @@ -123,7 +123,7 @@ Content with [a link](https://astro.build).`, // Custom loader that uses renderMarkdown const customMarkdownLoader = { name: 'custom-markdown-loader', - load: async (context) => { + load: async (context: any) => { const markdownContent = `--- title: Rendered Post author: Test Author @@ -168,8 +168,8 @@ This content is processed by the loader using renderMarkdown. const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); @@ -183,7 +183,7 @@ This content is processed by the loader using renderMarkdown. await contentLayer.sync(); // Check that markdown was rendered - const entry = store.get('custom', 'rendered-post'); + const entry: any = store.get('custom', 'rendered-post'); assert.ok(entry); assert.ok(entry.rendered); assert.ok(entry.rendered.html); @@ -201,7 +201,7 @@ This content is processed by the loader using renderMarkdown. const customLoader = { name: 'headings-test-loader', - load: async (context) => { + load: async (context: any) => { const content = `--- title: Headings Test --- @@ -243,8 +243,8 @@ Section 2 content.`; const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); @@ -257,7 +257,7 @@ Section 2 content.`; await contentLayer.sync(); - const entry = store.get('headings', 'headings-test'); + const entry: any = store.get('headings', 'headings-test'); assert.ok(entry); assert.ok(entry.rendered); assert.ok(entry.rendered.metadata); @@ -268,11 +268,11 @@ Section 2 content.`; assert.ok(headings.length >= 4); // Check heading structure - const h1 = headings.find((h) => h.depth === 1); + const h1 = headings.find((h: any) => h.depth === 1); assert.ok(h1); assert.equal(h1.text, 'Main Title'); - const h2s = headings.filter((h) => h.depth === 2); + const h2s = headings.filter((h: any) => h.depth === 2); assert.ok(h2s.length >= 2); }); @@ -281,7 +281,7 @@ Section 2 content.`; const noFrontmatterLoader = { name: 'no-frontmatter-loader', - load: async (context) => { + load: async (context: any) => { const content = `# Just Markdown This file has no frontmatter, just content.`; @@ -304,8 +304,8 @@ This file has no frontmatter, just content.`; }; const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); @@ -318,7 +318,7 @@ This file has no frontmatter, just content.`; await contentLayer.sync(); - const entry = store.get('noFrontmatter', 'plain'); + const entry: any = store.get('noFrontmatter', 'plain'); assert.ok(entry); assert.ok(entry.body); assert.ok(entry.body.includes('# Just Markdown')); @@ -330,7 +330,7 @@ This file has no frontmatter, just content.`; const customLoader = { name: 'code-test-loader', - load: async (context) => { + load: async (context: any) => { const content = `--- title: Code Examples --- @@ -377,8 +377,8 @@ And some inline code: \`const x = 42\`.`; }, }); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); @@ -391,7 +391,7 @@ And some inline code: \`const x = 42\`.`; await contentLayer.sync(); - const entry = store.get('code', 'code-test'); + const entry: any = store.get('code', 'code-test'); assert.ok(entry); assert.ok(entry.rendered); assert.ok(entry.rendered.html); @@ -404,14 +404,14 @@ And some inline code: \`const x = 42\`.`; it('renderMarkdown parses frontmatter correctly through loader', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); const frontmatterTestLoader = { name: 'frontmatter-test-loader', - load: async (context) => { + load: async (context: any) => { const markdownWithFrontmatter = `--- title: Test Post description: A test post for renderMarkdown @@ -470,7 +470,7 @@ More content here.`; await contentLayer.sync(); - const entry = store.get('frontmatterTest', 'frontmatter-test'); + const entry: any = store.get('frontmatterTest', 'frontmatter-test'); assert.ok(entry); assert.equal(entry.data.title, 'Test Post'); assert.equal(entry.data.description, 'A test post for renderMarkdown'); @@ -480,14 +480,14 @@ More content here.`; it('renderMarkdown excludes frontmatter from HTML output through loader', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); const htmlTestLoader = { name: 'html-test-loader', - load: async (context) => { + load: async (context: any) => { const markdownWithFrontmatter = `--- title: Test Post --- @@ -527,7 +527,7 @@ title: Test Post await contentLayer.sync(); - const entry = store.get('htmlTest', 'html-test'); + const entry: any = store.get('htmlTest', 'html-test'); assert.ok(entry); // HTML should not contain frontmatter assert.ok(!entry.data.html.includes('title:')); @@ -541,14 +541,14 @@ title: Test Post it('renderMarkdown extracts headings correctly through loader', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); const headingsTestLoader = { name: 'headings-test-loader', - load: async (context) => { + load: async (context: any) => { const markdown = `# Heading 1 Some text @@ -565,7 +565,7 @@ Even more text }); // Extract heading information - const headings = result.metadata.headings.map((h) => ({ + const headings = result.metadata.headings.map((h: any) => ({ depth: h.depth, text: h.text, })); @@ -604,7 +604,7 @@ Even more text await contentLayer.sync(); - const entry = store.get('headingsTest', 'headings-test'); + const entry: any = store.get('headingsTest', 'headings-test'); assert.ok(entry); assert.equal(entry.data.headingCount, 4); assert.deepEqual(entry.data.headings, [ @@ -618,14 +618,14 @@ Even more text it('renderMarkdown resolves relative image paths through loader', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); const imageTestLoader = { name: 'image-test-loader', - load: async (context) => { + load: async (context: any) => { const markdownWithImage = `# Post with Image ![Local image](./image.png) @@ -667,7 +667,7 @@ Even more text await contentLayer.sync(); - const entry = store.get('imageTest', 'image-test'); + const entry: any = store.get('imageTest', 'image-test'); assert.ok(entry); assert.ok(entry.data.hasImages); assert.equal(entry.data.localImages.length, 1); @@ -678,14 +678,14 @@ Even more text it('renderMarkdown populates combined imagePaths in metadata', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); const imagePathsLoader = { name: 'imagepaths-test-loader', - load: async (context) => { + load: async (context: any) => { const markdownWithImages = `# Post with Images ![Photo](./photo.jpg) @@ -728,7 +728,7 @@ Even more text await contentLayer.sync(); - const entry = store.get('imagePathsTest', 'imagepaths-test'); + const entry: any = store.get('imagePathsTest', 'imagepaths-test'); assert.ok(entry); // imagePaths should be the combined localImagePaths + remoteImagePaths diff --git a/packages/astro/test/units/content-layer/schema-validation.test.js b/packages/astro/test/units/content-layer/schema-validation.test.ts similarity index 89% rename from packages/astro/test/units/content-layer/schema-validation.test.js rename to packages/astro/test/units/content-layer/schema-validation.test.ts index 1b58812bceb3..a45f1dd7e575 100644 --- a/packages/astro/test/units/content-layer/schema-validation.test.js +++ b/packages/astro/test/units/content-layer/schema-validation.test.ts @@ -4,8 +4,8 @@ import { z } from 'zod'; import { defineCollection } from '../../../dist/content/config.js'; import { ContentLayer } from '../../../dist/content/content-layer.js'; import { MutableDataStore } from '../../../dist/content/mutable-data-store.js'; -import { Logger } from '../../../dist/core/logger/core.js'; -import { createTempDir, createTestConfigObserver, createMinimalSettings } from './test-helpers.js'; +import { AstroLogger } from '../../../dist/core/logger/core.js'; +import { createTempDir, createTestConfigObserver, createMinimalSettings } from './test-helpers.ts'; describe('Content Layer - Schema Validation', () => { const root = createTempDir(); @@ -13,15 +13,15 @@ describe('Content Layer - Schema Validation', () => { it('parses and coerces Date objects in schemas', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); // Loader that provides dates in various formats const dateLoader = { name: 'date-loader', - load: async (context) => { + load: async (context: any) => { const entries = [ { id: 'one', @@ -99,17 +99,17 @@ describe('Content Layer - Schema Validation', () => { } // Verify specific date values - const entryOne = store.get('withDates', 'one'); + const entryOne: any = store.get('withDates', 'one'); assert.equal(entryOne.data.publishedAt.toISOString(), '2021-01-01T00:00:00.000Z'); assert.equal(entryOne.data.updatedAt.toISOString(), '2021-01-02T00:00:00.000Z'); assert.equal(entryOne.data.createdAt.toISOString(), '2021-01-03T00:00:00.000Z'); // Check timestamp conversion - const entryTwo = store.get('withDates', 'two'); + const entryTwo: any = store.get('withDates', 'two'); assert.equal(entryTwo.data.createdAt.toISOString(), '2021-01-02T00:00:00.000Z'); // Check date string parsing - just verify it's a valid Date - const entryThree = store.get('withDates', 'three'); + const entryThree: any = store.get('withDates', 'three'); assert.ok(entryThree.data.createdAt instanceof Date); assert.ok(!isNaN(entryThree.data.createdAt.getTime())); }); @@ -117,15 +117,15 @@ describe('Content Layer - Schema Validation', () => { it('handles custom IDs and slugs', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); // Loader that provides entries with custom slugs const customSlugLoader = { name: 'custom-slug-loader', - load: async (context) => { + load: async (context: any) => { const entries = [ { id: 'fancy-one', @@ -183,7 +183,7 @@ describe('Content Layer - Schema Validation', () => { assert.deepEqual(ids, ['excellent-three', 'fancy-one', 'interesting-two']); // Verify data is correct - const fancyOne = store.get('withCustomSlugs', 'fancy-one'); + const fancyOne: any = store.get('withCustomSlugs', 'fancy-one'); assert.equal(fancyOne.data.slug, 'fancy-one'); assert.equal(fancyOne.data.title, 'First Entry'); }); @@ -191,15 +191,15 @@ describe('Content Layer - Schema Validation', () => { it('supports union schemas (discriminated unions)', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); // Loader that provides different types of content const unionLoader = { name: 'union-loader', - load: async (context) => { + load: async (context: any) => { const entries = [ { id: 'post', @@ -272,7 +272,7 @@ describe('Content Layer - Schema Validation', () => { assert.equal(entries.length, 3); // Verify post entry - const post = store.get('withUnionSchema', 'post'); + const post: any = store.get('withUnionSchema', 'post'); assert.deepEqual(post.data, { type: 'post', title: 'My Post', @@ -280,14 +280,14 @@ describe('Content Layer - Schema Validation', () => { }); // Verify newsletter entry - const newsletter = store.get('withUnionSchema', 'newsletter'); + const newsletter: any = store.get('withUnionSchema', 'newsletter'); assert.deepEqual(newsletter.data, { type: 'newsletter', subject: 'My Newsletter', }); // Verify announcement entry - const announcement = store.get('withUnionSchema', 'announcement'); + const announcement: any = store.get('withUnionSchema', 'announcement'); assert.deepEqual(announcement.data, { type: 'announcement', message: 'Important Update', @@ -298,12 +298,12 @@ describe('Content Layer - Schema Validation', () => { it('validates required fields in empty content', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logs = []; + const logs: any[] = []; - const logger = new Logger({ + const logger = new AstroLogger({ level: 'error', - dest: { - write: (event) => { + destination: { + write: (event: any) => { logs.push(event); return true; }, @@ -313,7 +313,7 @@ describe('Content Layer - Schema Validation', () => { // Loader that simulates empty markdown file scenario const emptyContentLoader = { name: 'empty-content-loader', - load: async (context) => { + load: async (context: any) => { // Simulate empty markdown file - no frontmatter data const entries = [ { @@ -342,15 +342,15 @@ describe('Content Layer - Schema Validation', () => { data: parsed, body: entry.body, }); - } catch (error) { + } catch (error: any) { // Log validation error context.logger.error(`Validation failed for ${entry.id}: ${error.message}`); // Check if it's a Zod error with issues if (error.errors) { const requiredFields = error.errors - .filter((issue) => issue.message === 'Required') - .map((issue) => `**${issue.path.join('.')}**: ${issue.message}`); + .filter((issue: any) => issue.message === 'Required') + .map((issue: any) => `**${issue.path.join('.')}**: ${issue.message}`); if (requiredFields.length > 0) { context.logger.error(requiredFields.join(', ')); @@ -402,12 +402,12 @@ describe('Content Layer - Schema Validation', () => { it('validates ID types and rejects invalid IDs', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logs = []; + const logs: any[] = []; - const logger = new Logger({ + const logger = new AstroLogger({ level: 'error', - dest: { - write: (event) => { + destination: { + write: (event: any) => { logs.push(event); return true; }, @@ -417,7 +417,7 @@ describe('Content Layer - Schema Validation', () => { // Loader that provides entries with various ID types const invalidIdLoader = { name: 'invalid-id-loader', - load: async (context) => { + load: async (context: any) => { const entries = [ { id: 'valid-string-id', @@ -455,7 +455,7 @@ describe('Content Layer - Schema Validation', () => { id: entry.id, data: parsed, }); - } catch (error) { + } catch (error: any) { context.logger.error(error.message); } } @@ -496,15 +496,15 @@ describe('Content Layer - Schema Validation', () => { it('handles empty collections gracefully', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); // Loader that returns no entries const emptyLoader = { name: 'empty-loader', - load: async (_context) => { + load: async (_context: any) => { // Simulate an empty directory - no entries to load // Just return without adding anything to the store }, @@ -541,14 +541,14 @@ describe('Content Layer - Schema Validation', () => { it('handles optional fields with defaults', async () => { const store = new MutableDataStore(); const settings = createMinimalSettings(root); - const logger = new Logger({ - dest: { write: () => true }, + const logger = new AstroLogger({ + destination: { write: () => true }, level: 'silent', }); const defaultsLoader = { name: 'defaults-loader', - load: async (context) => { + load: async (context: any) => { const entries = [ { id: 'full-entry', @@ -604,13 +604,13 @@ describe('Content Layer - Schema Validation', () => { await contentLayer.sync(); // Check full entry - const fullEntry = store.get('withDefaults', 'full-entry'); + const fullEntry: any = store.get('withDefaults', 'full-entry'); assert.equal(fullEntry.data.draft, false); assert.deepEqual(fullEntry.data.tags, ['tag1', 'tag2']); assert.equal(fullEntry.data.rating, 5); // Check minimal entry has defaults applied - const minimalEntry = store.get('withDefaults', 'minimal-entry'); + const minimalEntry: any = store.get('withDefaults', 'minimal-entry'); assert.equal(minimalEntry.data.draft, true); // Default value assert.deepEqual(minimalEntry.data.tags, []); // Default value assert.equal(minimalEntry.data.rating, 0); // Default value diff --git a/packages/astro/test/units/content-layer/store-persistence.test.js b/packages/astro/test/units/content-layer/store-persistence.test.ts similarity index 98% rename from packages/astro/test/units/content-layer/store-persistence.test.js rename to packages/astro/test/units/content-layer/store-persistence.test.ts index 303a19f16bb3..2f6caa3ddd7f 100644 --- a/packages/astro/test/units/content-layer/store-persistence.test.js +++ b/packages/astro/test/units/content-layer/store-persistence.test.ts @@ -3,7 +3,7 @@ import { describe, it } from 'node:test'; import { fileURLToPath } from 'node:url'; import fs from 'node:fs/promises'; import { MutableDataStore } from '../../../dist/content/mutable-data-store.js'; -import { createTempDir } from './test-helpers.js'; +import { createTempDir } from './test-helpers.ts'; describe('Content Layer - Store Persistence', () => { it('updates the store on new builds', async () => { @@ -207,7 +207,7 @@ describe('Content Layer - Store Persistence', () => { assert.ok(!store3.get('cats', 'siamese')); // Old entry gone assert.ok(store3.get('cats', 'siamese-cat')); // New entry exists - const updatedPost = store3.get('posts', 'post1'); + const updatedPost: any = store3.get('posts', 'post1'); assert.equal(updatedPost.data.cat.id, 'siamese-cat'); // Reference updated }); }); diff --git a/packages/astro/test/units/content-layer/test-helpers.js b/packages/astro/test/units/content-layer/test-helpers.ts similarity index 51% rename from packages/astro/test/units/content-layer/test-helpers.js rename to packages/astro/test/units/content-layer/test-helpers.ts index 4f85716f2dee..df3beb649756 100644 --- a/packages/astro/test/units/content-layer/test-helpers.js +++ b/packages/astro/test/units/content-layer/test-helpers.ts @@ -5,22 +5,18 @@ import { pathToFileURL } from 'node:url'; /** * Creates a temporary directory for tests - * @param {string} prefix - Optional prefix for the temp directory name - * @returns {URL} The file URL of the created temp directory */ -export function createTempDir(prefix = 'astro-test-') { +export function createTempDir(prefix = 'astro-test-'): URL { const tempDir = mkdtempSync(path.join(tmpdir(), prefix)); return pathToFileURL(tempDir + path.sep); } /** * Creates a test content config observer for unit tests - * @param {Object} collections - The collections configuration - * @returns {Object} A mock content config observer */ -export function createTestConfigObserver(collections) { +export function createTestConfigObserver(collections: Record): any { const contentConfig = { - status: 'loaded', + status: 'loaded' as const, config: { collections, digest: 'test-digest', @@ -30,8 +26,7 @@ export function createTestConfigObserver(collections) { return { get: () => contentConfig, set: () => {}, - subscribe: (fn) => { - // Call immediately with current config + subscribe: (fn: (config: typeof contentConfig) => void) => { fn(contentConfig); return () => {}; }, @@ -40,11 +35,8 @@ export function createTestConfigObserver(collections) { /** * Creates minimal Astro settings for content layer tests - * @param {URL} root - The root URL for the test - * @param {Object} overrides - Optional overrides for specific settings - * @returns {Object} Astro settings object */ -export function createMinimalSettings(root, overrides = {}) { +export function createMinimalSettings(root: URL, overrides: Record = {}): any { const defaultConfig = { root, srcDir: new URL('./src/', root), @@ -53,7 +45,7 @@ export function createMinimalSettings(root, overrides = {}) { experimental: {}, }; - const settings = { + const settings: Record = { config: { ...defaultConfig, ...(overrides.config || {}), @@ -63,7 +55,6 @@ export function createMinimalSettings(root, overrides = {}) { dataEntryTypes: [], }; - // Apply non-config overrides Object.keys(overrides).forEach((key) => { if (key !== 'config') { settings[key] = overrides[key]; @@ -75,62 +66,56 @@ export function createMinimalSettings(root, overrides = {}) { /** * Simple YAML frontmatter parser for markdown files - * @param {string} contents - The file contents - * @param {string} fileUrl - The file URL - * @returns {Object} Parsed frontmatter data, body, and slug */ -export function parseSimpleMarkdownFrontmatter(contents, fileUrl) { +export function parseSimpleMarkdownFrontmatter(contents: string, fileUrl: string | URL) { const lines = contents.split('\n'); - const frontmatterStart = lines.findIndex((l) => l === '---'); - const frontmatterEnd = lines.findIndex((l, i) => i > frontmatterStart && l === '---'); + const frontmatterStart = lines.findIndex((l: string) => l === '---'); + const frontmatterEnd = lines.findIndex( + (l: string, i: number) => i > frontmatterStart && l === '---', + ); if (frontmatterStart === -1 || frontmatterEnd === -1) { - const slug = path.basename(fileUrl.pathname || fileUrl, '.md'); - return { data: {}, body: contents, slug, rawData: {} }; + const pathname = typeof fileUrl === 'string' ? fileUrl : fileUrl.pathname; + const slug = path.basename(pathname, '.md'); + return { data: {} as Record, body: contents, slug, rawData: {} }; } const frontmatterLines = lines.slice(frontmatterStart + 1, frontmatterEnd); const body = lines.slice(frontmatterEnd + 1).join('\n'); - // Parse YAML-like frontmatter - const data = {}; + const data: Record = {}; for (const line of frontmatterLines) { const [key, ...valueParts] = line.split(':'); if (key && valueParts.length) { const value = valueParts.join(':').trim(); if (value.startsWith('[') && value.endsWith(']')) { - // Parse YAML-style arrays const arrayContent = value.slice(1, -1); data[key.trim()] = arrayContent .split(',') - .map((item) => item.trim().replace(/^["']|["']$/g, '')) - .filter((item) => item.length > 0); + .map((item: string) => item.trim().replace(/^["']|["']$/g, '')) + .filter((item: string) => item.length > 0); } else if (/^\d{4}-\d{2}-\d{2}$/.test(value)) { - // Keep dates as strings for schema to parse data[key.trim()] = value; } else { - // Remove quotes if present data[key.trim()] = value.replace(/^["']|["']$/g, ''); } } } - const slug = path.basename(fileUrl.pathname || fileUrl, '.md'); + const pathname = typeof fileUrl === 'string' ? fileUrl : fileUrl.pathname; + const slug = path.basename(pathname, '.md'); return { data, body, slug, rawData: data }; } /** * Creates a markdown entry type configuration - * @param {Function} getEntryInfo - Optional custom getEntryInfo function - * @returns {Object} Entry type configuration for markdown files */ -export function createMarkdownEntryType(getEntryInfo = parseSimpleMarkdownFrontmatter) { +export function createMarkdownEntryType( + getEntryInfo: (contents: string, fileUrl: string | URL) => any = parseSimpleMarkdownFrontmatter, +) { return { extensions: ['.md'], - getEntryInfo: async ({ contents, fileUrl }) => { - if (typeof fileUrl === 'string') { - return getEntryInfo(contents, fileUrl); - } + getEntryInfo: async ({ contents, fileUrl }: { contents: string; fileUrl: string | URL }) => { return getEntryInfo(contents, fileUrl); }, }; diff --git a/packages/astro/test/units/cookies/delete.test.js b/packages/astro/test/units/cookies/delete.test.ts similarity index 95% rename from packages/astro/test/units/cookies/delete.test.js rename to packages/astro/test/units/cookies/delete.test.ts index 0c16c9ed0255..f6a04507c4af 100644 --- a/packages/astro/test/units/cookies/delete.test.js +++ b/packages/astro/test/units/cookies/delete.test.ts @@ -11,7 +11,7 @@ describe('astro/src/core/cookies', () => { }, }); let cookies = new AstroCookies(req); - assert.equal(cookies.get('foo').value, 'bar'); + assert.equal(cookies.get('foo')!.value, 'bar'); cookies.delete('foo'); let headers = Array.from(cookies.headers()); @@ -25,7 +25,7 @@ describe('astro/src/core/cookies', () => { }, }); let cookies = new AstroCookies(req); - assert.equal(cookies.get('foo').value, 'bar'); + assert.equal(cookies.get('foo')!.value, 'bar'); cookies.delete('foo'); assert.equal(cookies.get('foo'), undefined); @@ -55,7 +55,7 @@ describe('astro/src/core/cookies', () => { secure: true, httpOnly: true, sameSite: 'strict', - }); + } as any); let headers = Array.from(cookies.headers()); assert.equal(headers.length, 1); @@ -75,7 +75,7 @@ describe('astro/src/core/cookies', () => { cookies.delete('foo', { expires: new Date(), - }); + } as any); let headers = Array.from(cookies.headers()); assert.equal(headers.length, 1); @@ -89,7 +89,7 @@ describe('astro/src/core/cookies', () => { cookies.delete('foo', { maxAge: 60, - }); + } as any); let headers = Array.from(cookies.headers()); assert.equal(headers.length, 1); diff --git a/packages/astro/test/units/cookies/error.test.js b/packages/astro/test/units/cookies/error.test.ts similarity index 86% rename from packages/astro/test/units/cookies/error.test.js rename to packages/astro/test/units/cookies/error.test.ts index 6a5a3186f88e..53abc941765f 100644 --- a/packages/astro/test/units/cookies/error.test.js +++ b/packages/astro/test/units/cookies/error.test.ts @@ -7,11 +7,11 @@ describe('astro/src/core/cookies', () => { it('Produces an error if the response is already sent', () => { const req = new Request('http://example.com/', {}); const cookies = new AstroCookies(req); - req[Symbol.for('astro.responseSent')] = true; + (req as any)[Symbol.for('astro.responseSent')] = true; try { cookies.set('foo', 'bar'); assert.equal(false, true); - } catch (err) { + } catch (err: any) { assert.equal(err.name, 'ResponseSentError'); } }); diff --git a/packages/astro/test/units/cookies/get.test.js b/packages/astro/test/units/cookies/get.test.ts similarity index 85% rename from packages/astro/test/units/cookies/get.test.js rename to packages/astro/test/units/cookies/get.test.ts index 6fb0b06bd875..c8c2ce0a687d 100644 --- a/packages/astro/test/units/cookies/get.test.js +++ b/packages/astro/test/units/cookies/get.test.ts @@ -2,12 +2,12 @@ import * as assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { AstroCookies } from '../../../dist/core/cookies/index.js'; -const encode = (data) => { +const encode = (data: any) => { const dataSerialized = typeof data === 'string' ? data : JSON.stringify(data); return Buffer.from(dataSerialized).toString('base64'); }; -const decode = (str) => { +const decode = (str: string) => { return Buffer.from(str, 'base64').toString(); }; @@ -20,7 +20,7 @@ describe('astro/src/core/cookies', () => { }, }); const cookies = new AstroCookies(req); - assert.equal(cookies.get('foo').value, 'bar'); + assert.equal(cookies.get('foo')!.value, 'bar'); }); it('gets the cookie value with default decode', () => { @@ -32,7 +32,7 @@ describe('astro/src/core/cookies', () => { }); const cookies = new AstroCookies(req); // by default decodeURIComponent is used on the value - assert.equal(cookies.get('url').value, url); + assert.equal(cookies.get('url')!.value, url); }); it('gets the cookie value with custom decode', () => { @@ -45,14 +45,14 @@ describe('astro/src/core/cookies', () => { const cookies = new AstroCookies(req); assert.ok(cookies.has('url')); - assert.equal(cookies.get('url', { decode }).value, url); - assert.equal(cookies.get('url').value, encode(url)); + assert.equal(cookies.get('url', { decode })!.value, url); + assert.equal(cookies.get('url')!.value, encode(url)); }); it("Returns undefined is the value doesn't exist", () => { const req = new Request('http://example.com/'); let cookies = new AstroCookies(req); - let cookie = cookies.get('foo'); + let cookie = cookies.get('foo')!; assert.equal(cookie, undefined); }); @@ -75,7 +75,7 @@ describe('astro/src/core/cookies', () => { }); let cookies = new AstroCookies(req); // Should return the unparsed value instead of throwing - assert.equal(cookies.get('malformed').value, '0:%'); + assert.equal(cookies.get('malformed')!.value, '0:%'); }); describe('.json()', () => { @@ -87,7 +87,7 @@ describe('astro/src/core/cookies', () => { }); let cookies = new AstroCookies(req); - const json = cookies.get('foo').json(); + const json = cookies.get('foo')!.json(); assert.equal(typeof json, 'object'); assert.equal(json.key, 'value'); }); @@ -102,7 +102,7 @@ describe('astro/src/core/cookies', () => { }); let cookies = new AstroCookies(req); - const value = cookies.get('foo').number(); + const value = cookies.get('foo')!.number(); assert.equal(typeof value, 'number'); assert.equal(value, 22); }); @@ -115,7 +115,7 @@ describe('astro/src/core/cookies', () => { }); let cookies = new AstroCookies(req); - const value = cookies.get('foo').number(); + const value = cookies.get('foo')!.number(); assert.equal(typeof value, 'number'); assert.equal(Number.isNaN(value), true); }); @@ -130,7 +130,7 @@ describe('astro/src/core/cookies', () => { }); let cookies = new AstroCookies(req); - const value = cookies.get('foo').boolean(); + const value = cookies.get('foo')!.boolean(); assert.equal(typeof value, 'boolean'); assert.equal(value, true); }); @@ -143,7 +143,7 @@ describe('astro/src/core/cookies', () => { }); let cookies = new AstroCookies(req); - const value = cookies.get('foo').boolean(); + const value = cookies.get('foo')!.boolean(); assert.equal(typeof value, 'boolean'); assert.equal(value, false); }); @@ -156,7 +156,7 @@ describe('astro/src/core/cookies', () => { }); let cookies = new AstroCookies(req); - const value = cookies.get('foo').boolean(); + const value = cookies.get('foo')!.boolean(); assert.equal(typeof value, 'boolean'); assert.equal(value, true); }); @@ -169,7 +169,7 @@ describe('astro/src/core/cookies', () => { }); let cookies = new AstroCookies(req); - const value = cookies.get('foo').boolean(); + const value = cookies.get('foo')!.boolean(); assert.equal(typeof value, 'boolean'); assert.equal(value, false); }); @@ -182,7 +182,7 @@ describe('astro/src/core/cookies', () => { }); let cookies = new AstroCookies(req); - const value = cookies.get('foo').boolean(); + const value = cookies.get('foo')!.boolean(); assert.equal(typeof value, 'boolean'); assert.equal(value, true); }); diff --git a/packages/astro/test/units/cookies/has.test.js b/packages/astro/test/units/cookies/has.test.ts similarity index 100% rename from packages/astro/test/units/cookies/has.test.js rename to packages/astro/test/units/cookies/has.test.ts diff --git a/packages/astro/test/units/cookies/merge.test.js b/packages/astro/test/units/cookies/merge.test.ts similarity index 100% rename from packages/astro/test/units/cookies/merge.test.js rename to packages/astro/test/units/cookies/merge.test.ts diff --git a/packages/astro/test/units/cookies/set.test.js b/packages/astro/test/units/cookies/set.test.ts similarity index 92% rename from packages/astro/test/units/cookies/set.test.js rename to packages/astro/test/units/cookies/set.test.ts index d5863ef3a638..201574d8fd07 100644 --- a/packages/astro/test/units/cookies/set.test.js +++ b/packages/astro/test/units/cookies/set.test.ts @@ -60,7 +60,7 @@ describe('astro/src/core/cookies', () => { it('Can pass a number', () => { let req = new Request('http://example.com/'); let cookies = new AstroCookies(req); - cookies.set('one', 2); + cookies.set('one', 2 as any); let headers = Array.from(cookies.headers()); assert.equal(headers.length, 1); assert.equal(headers[0], 'one=2'); @@ -69,8 +69,8 @@ describe('astro/src/core/cookies', () => { it('Can pass a boolean', () => { let req = new Request('http://example.com/'); let cookies = new AstroCookies(req); - cookies.set('admin', true); - assert.equal(cookies.get('admin').boolean(), true); + cookies.set('admin', true as any); + assert.equal(cookies.get('admin')!.boolean(), true); let headers = Array.from(cookies.headers()); assert.equal(headers.length, 1); assert.equal(headers[0], 'admin=true'); @@ -80,7 +80,7 @@ describe('astro/src/core/cookies', () => { let req = new Request('http://example.com/'); let cookies = new AstroCookies(req); cookies.set('foo', 'bar'); - let r = cookies.get('foo'); + let r = cookies.get('foo')!; assert.equal(r.value, 'bar'); }); @@ -88,7 +88,7 @@ describe('astro/src/core/cookies', () => { let req = new Request('http://example.com/'); let cookies = new AstroCookies(req); cookies.set('options', { one: 'two', three: 4 }); - let cook = cookies.get('options'); + let cook = cookies.get('options')!; let value = cook.json(); assert.equal(typeof value, 'object'); assert.equal(value.one, 'two'); @@ -103,11 +103,11 @@ describe('astro/src/core/cookies', () => { }, }); let cookies = new AstroCookies(req); - assert.equal(cookies.get('foo').value, 'bar'); + assert.equal(cookies.get('foo')!.value, 'bar'); // Set a new value cookies.set('foo', 'baz'); - assert.equal(cookies.get('foo').value, 'baz'); + assert.equal(cookies.get('foo')!.value, 'baz'); }); }); }); diff --git a/packages/astro/test/units/csp/common.test.js b/packages/astro/test/units/csp/common.test.ts similarity index 82% rename from packages/astro/test/units/csp/common.test.js rename to packages/astro/test/units/csp/common.test.ts index a3e0cc0eb5f3..816e4e3dad5a 100644 --- a/packages/astro/test/units/csp/common.test.js +++ b/packages/astro/test/units/csp/common.test.ts @@ -2,16 +2,7 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { getDirectives } from '../../../dist/core/csp/common.js'; -/** - * - * @param {{ - * csp: import('../../../dist/types/astro.js').AstroSettings['config']['security']['csp']; - * injected: Array - * }} param0 - * @returns {import('../../../dist/types/astro.js').AstroSettings} - */ -function buildSettings({ csp, injected }) { - /** @type {any} */ +function buildSettings({ csp, injected }: { csp: any; injected: string[] }): any { const settings = { config: { security: { csp }, diff --git a/packages/astro/test/units/csp/rendering.test.js b/packages/astro/test/units/csp/rendering.test.ts similarity index 83% rename from packages/astro/test/units/csp/rendering.test.js rename to packages/astro/test/units/csp/rendering.test.ts index 6fe37b85da66..ae6d13c79034 100644 --- a/packages/astro/test/units/csp/rendering.test.js +++ b/packages/astro/test/units/csp/rendering.test.ts @@ -8,51 +8,65 @@ import { render, renderHead, } from '../../../dist/runtime/server/index.js'; -import { createBasicPipeline } from '../test-utils.js'; +import type { SSRManifestCSP } from '../../../dist/types/public/internal.js'; +import type { Pipeline } from '../../../dist/core/render/index.js'; +import { createBasicPipeline } from '../test-utils.ts'; // #region Test Utilities -/** - * Creates a pipeline with CSP configuration - * @param {Partial} cspConfig - */ -function createCspPipeline(cspConfig = {}) { +function createCspPipeline(cspConfig: Partial = {}): Pipeline { const pipeline = createBasicPipeline(); - pipeline.manifest = { - ...pipeline.manifest, - shouldInjectCspMetaTags: true, - csp: { - cspDestination: cspConfig.cspDestination, - algorithm: cspConfig.algorithm || 'SHA-256', - scriptHashes: cspConfig.scriptHashes || [], - scriptResources: cspConfig.scriptResources || [], - styleHashes: cspConfig.styleHashes || [], - styleResources: cspConfig.styleResources || [], - directives: cspConfig.directives || [], - isStrictDynamic: cspConfig.isStrictDynamic || false, + // manifest is readonly, so we use Object.defineProperty to override it for testing + Object.defineProperty(pipeline, 'manifest', { + value: { + ...pipeline.manifest, + shouldInjectCspMetaTags: true, + csp: { + cspDestination: cspConfig.cspDestination, + algorithm: cspConfig.algorithm || 'SHA-256', + scriptHashes: cspConfig.scriptHashes || [], + scriptResources: cspConfig.scriptResources || [], + styleHashes: cspConfig.styleHashes || [], + styleResources: cspConfig.styleResources || [], + directives: cspConfig.directives || [], + isStrictDynamic: cspConfig.isStrictDynamic || false, + }, }, - }; + writable: false, + configurable: true, + }); return pipeline; } -/** - * Renders a page component and returns HTML and headers - * @param {any} PageComponent - * @param {any} pipeline - * @param {boolean} prerender - */ -async function renderPage(PageComponent, pipeline, prerender = true) { +async function renderPage( + PageComponent: ReturnType, + pipeline: Pipeline, + prerender = true, +): Promise<{ html: string; response: Response }> { const PageModule = { default: PageComponent }; const request = new Request('http://localhost/'); const routeData = { - type: 'page', + type: 'page' as const, + route: '/index', pathname: '/index', component: 'src/pages/index.astro', - params: {}, + params: [] as string[], + segments: [] as any[], + pattern: /^\/$/ as RegExp, + distURL: [] as URL[], prerender, + fallbackRoutes: [] as any[], + isIndex: true, + origin: 'project' as const, }; - const renderContext = await RenderContext.create({ pipeline, request, routeData }); + const renderContext = await RenderContext.create({ + pipeline, + request, + routeData, + pathname: '/index', + clientAddress: '127.0.0.1', + }); const response = await renderContext.render(PageModule); const html = await response.text(); @@ -64,10 +78,10 @@ async function renderPage(PageComponent, pipeline, prerender = true) { // #region Reusable Components /** Simple page component */ -const SimplePage = createComponent((result) => { +const SimplePage = createComponent(() => { return render` - ${renderHead(result)} - ${maybeRenderHead(result)}

Test

+ ${renderHead()} + ${maybeRenderHead()}

Test

`; }); @@ -86,7 +100,7 @@ describe('CSP Rendering', () => { const $ = cheerio.load(html); const meta = $('meta[http-equiv="Content-Security-Policy"]'); - const content = meta.attr('content'); + const content = meta.attr('content')!; assert.ok(content.includes('sha256-abc123'), 'Should include first style hash'); assert.ok(content.includes('sha256-def456'), 'Should include second style hash'); @@ -107,7 +121,7 @@ describe('CSP Rendering', () => { const $ = cheerio.load(html); const meta = $('meta[http-equiv="Content-Security-Policy"]'); - const content = meta.attr('content'); + const content = meta.attr('content')!; assert.ok(content.includes('sha256-xyz789'), 'Should include first script hash'); assert.ok(content.includes('sha256-uvw456'), 'Should include second script hash'); @@ -126,7 +140,7 @@ describe('CSP Rendering', () => { const $ = cheerio.load(html); const meta = $('meta[http-equiv="Content-Security-Policy"]'); - const content = meta.attr('content'); + const content = meta.attr('content')!; assert.ok(content.includes('sha512-'), 'Should use sha512 prefix'); assert.ok(content.includes('sha512-longhash123abc'), 'Should include SHA-512 hash'); @@ -142,7 +156,7 @@ describe('CSP Rendering', () => { const $ = cheerio.load(html); const meta = $('meta[http-equiv="Content-Security-Policy"]'); - const content = meta.attr('content'); + const content = meta.attr('content')!; assert.ok(content.includes('sha384-'), 'Should use sha384 prefix'); assert.ok(content.includes('sha384-mediumhash456'), 'Should include SHA-384 hash'); @@ -160,7 +174,7 @@ describe('CSP Rendering', () => { const $ = cheerio.load(html); const meta = $('meta[http-equiv="Content-Security-Policy"]'); - const content = meta.attr('content'); + const content = meta.attr('content')!; assert.ok(content.includes('sha384-hash2'), 'Should include custom style hash 1'); assert.ok(content.includes('sha384-hash4'), 'Should include custom script hash 1'); @@ -179,7 +193,7 @@ describe('CSP Rendering', () => { const $ = cheerio.load(html); const meta = $('meta[http-equiv="Content-Security-Policy"]'); - const content = meta.attr('content'); + const content = meta.attr('content')!; assert.ok( content.includes("img-src 'self' 'https://example.com'"), @@ -201,7 +215,7 @@ describe('CSP Rendering', () => { const $ = cheerio.load(html); const meta = $('meta[http-equiv="Content-Security-Policy"]'); - const content = meta.attr('content'); + const content = meta.attr('content')!; assert.ok(content.includes('upgrade-insecure-requests'), 'Should include upgrade directive'); assert.ok(content.includes('sandbox'), 'Should include sandbox directive'); @@ -224,7 +238,7 @@ describe('CSP Rendering', () => { const $ = cheerio.load(html); const meta = $('meta[http-equiv="Content-Security-Policy"]'); - const content = meta.attr('content'); + const content = meta.attr('content')!; assert.ok( content.includes('script-src https://cdn.example.com https://scripts.cdn.example.com'), @@ -244,7 +258,7 @@ describe('CSP Rendering', () => { scriptResources: ['https://global.cdn.example.com'], }); - const PageWithCspApi = createComponent((result) => { + const PageWithCspApi = createComponent((result: any) => { const Astro = result.createAstro({}, {}); // Use runtime CSP API @@ -254,16 +268,16 @@ describe('CSP Rendering', () => { Astro.csp.insertDirective('img-src https://images.cdn.example.com'); return render` - ${renderHead(result)} - ${maybeRenderHead(result)}

Scripts

- `; + ${renderHead()} + ${maybeRenderHead()}

Scripts

+ `; }); const { html } = await renderPage(PageWithCspApi, pipeline); const $ = cheerio.load(html); const meta = $('meta[http-equiv="Content-Security-Policy"]'); - const content = meta.attr('content'); + const content = meta.attr('content')!; // Check resources are merged and deduplicated assert.ok( @@ -291,7 +305,7 @@ describe('CSP Rendering', () => { styleResources: ['https://global.cdn.example.com'], }); - const PageWithStyleApi = createComponent((result) => { + const PageWithStyleApi = createComponent((result: any) => { const Astro = result.createAstro({}, {}); // Use runtime CSP API for styles @@ -301,16 +315,16 @@ describe('CSP Rendering', () => { Astro.csp.insertDirective('img-src https://images.cdn.example.com'); return render` - ${renderHead(result)} - ${maybeRenderHead(result)}

Styles

- `; + ${renderHead()} + ${maybeRenderHead()}

Styles

+ `; }); const { html } = await renderPage(PageWithStyleApi, pipeline); const $ = cheerio.load(html); const meta = $('meta[http-equiv="Content-Security-Policy"]'); - const content = meta.attr('content'); + const content = meta.attr('content')!; // Check style resources are merged assert.ok( @@ -342,7 +356,7 @@ describe('CSP Rendering', () => { const $ = cheerio.load(html); const meta = $('meta[http-equiv="Content-Security-Policy"]'); - const content = meta.attr('content'); + const content = meta.attr('content')!; assert.ok(content.includes("'strict-dynamic'"), "Should include 'strict-dynamic' keyword"); }); @@ -401,7 +415,7 @@ describe('CSP Rendering', () => { const $ = cheerio.load(html); const meta = $('meta[http-equiv="Content-Security-Policy"]'); - const content = meta.attr('content'); + const content = meta.attr('content')!; assert.equal(content.includes('font-src'), false, 'Should not include font-src directive'); }); @@ -421,7 +435,7 @@ describe('CSP Rendering', () => { const $ = cheerio.load(html); const meta = $('meta[http-equiv="Content-Security-Policy"]'); - const content = meta.attr('content'); + const content = meta.attr('content')!; // Parse CSP content into structured array const parsed = content @@ -443,22 +457,22 @@ describe('CSP Rendering', () => { // Check script-src has both resources and hashes const scriptSrc = parsed.find((p) => p.directive === 'script-src'); assert.ok( - scriptSrc.resources.includes('https://cdn.example.com'), + scriptSrc!.resources.includes('https://cdn.example.com'), 'script-src should include resource', ); assert.ok( - scriptSrc.resources.some((r) => r.includes('sha256-abc123')), + scriptSrc!.resources.some((r) => r.includes('sha256-abc123')), 'script-src should include hash', ); // Check style-src has both resources and hashes const styleSrc = parsed.find((p) => p.directive === 'style-src'); assert.ok( - styleSrc.resources.includes('https://styles.example.com'), + styleSrc!.resources.includes('https://styles.example.com'), 'style-src should include resource', ); assert.ok( - styleSrc.resources.some((r) => r.includes('sha256-def456')), + styleSrc!.resources.some((r) => r.includes('sha256-def456')), 'style-src should include hash', ); }); diff --git a/packages/astro/test/units/csp/runtime.test.js b/packages/astro/test/units/csp/runtime.test.ts similarity index 100% rename from packages/astro/test/units/csp/runtime.test.js rename to packages/astro/test/units/csp/runtime.test.ts diff --git a/packages/astro/test/units/dev/base-rewrite.test.ts b/packages/astro/test/units/dev/base-rewrite.test.ts new file mode 100644 index 000000000000..925f1a49a091 --- /dev/null +++ b/packages/astro/test/units/dev/base-rewrite.test.ts @@ -0,0 +1,160 @@ +import * as assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { + evaluateBaseRewrite, + resolveDevRoot, +} from '../../../dist/vite-plugin-astro-server/base.js'; + +// #region resolveDevRoot +describe('resolveDevRoot', () => { + it('resolves /docs base without site', () => { + const { devRoot, devRootReplacement } = resolveDevRoot('/docs'); + assert.equal(devRoot, '/docs'); + assert.equal(devRootReplacement, ''); + }); + + it('resolves /docs/ base with trailing slash', () => { + const { devRoot, devRootReplacement } = resolveDevRoot('/docs/'); + assert.equal(devRoot, '/docs/'); + assert.equal(devRootReplacement, '/'); + }); + + it('resolves / base (root)', () => { + const { devRoot, devRootReplacement } = resolveDevRoot('/'); + assert.equal(devRoot, '/'); + assert.equal(devRootReplacement, '/'); + }); + + it('resolves empty base as /', () => { + const { devRoot, devRootReplacement } = resolveDevRoot(''); + assert.equal(devRoot, '/'); + assert.equal(devRootReplacement, '/'); + }); + + it('uses site pathname when site is provided', () => { + const { devRoot } = resolveDevRoot('/docs/', 'https://example.com'); + assert.equal(devRoot, '/docs/'); + }); + + it('absolute base overrides site pathname', () => { + // `/app/` is absolute, so the site's `/prefix/` pathname is irrelevant + const { devRoot } = resolveDevRoot('/app/', 'https://example.com/prefix/'); + assert.equal(devRoot, '/app/'); + }); +}); +// #endregion + +// #region evaluateBaseRewrite — rewrite +describe('evaluateBaseRewrite — rewrite', () => { + it('rewrites URL starting with base by stripping base', () => { + const result = evaluateBaseRewrite('/docs/about', '/docs/about', undefined, '/docs/', '/'); + assert.equal(result.action, 'rewrite'); + if (result.action === 'rewrite') { + assert.equal(result.newUrl, '/about'); + } + }); + + it('rewrites root base request to /', () => { + const result = evaluateBaseRewrite('/docs/', '/docs/', undefined, '/docs/', '/'); + assert.equal(result.action, 'rewrite'); + if (result.action === 'rewrite') { + assert.equal(result.newUrl, '/'); + } + }); + + it('preserves query params after rewrite', () => { + const result = evaluateBaseRewrite( + '/docs/page?foo=bar', + '/docs/page', + undefined, + '/docs/', + '/', + ); + assert.equal(result.action, 'rewrite'); + if (result.action === 'rewrite') { + assert.equal(result.newUrl, '/page?foo=bar'); + } + }); + + it('ensures rewritten URL starts with /', () => { + // devRootReplacement is '' (no trailing slash on devRoot), so stripping + // '/docs' from '/docs/about' yields '/about' which already starts with / + const result = evaluateBaseRewrite('/docs/about', '/docs/about', undefined, '/docs', ''); + assert.equal(result.action, 'rewrite'); + if (result.action === 'rewrite') { + assert.ok(result.newUrl.startsWith('/')); + } + }); + + it('rewrites exact base match (no trailing content)', () => { + const result = evaluateBaseRewrite('/docs', '/docs', undefined, '/docs', ''); + assert.equal(result.action, 'rewrite'); + if (result.action === 'rewrite') { + assert.equal(result.newUrl, '/'); + } + }); +}); +// #endregion + +// #region evaluateBaseRewrite — not-found-subpath +describe('evaluateBaseRewrite — not-found-subpath', () => { + it('returns not-found-subpath for / when base is not /', () => { + const result = evaluateBaseRewrite('/', '/', undefined, '/docs/', '/'); + assert.equal(result.action, 'not-found-subpath'); + if (result.action === 'not-found-subpath') { + assert.equal(result.pathname, '/'); + assert.equal(result.devRoot, '/docs/'); + } + }); + + it('returns not-found-subpath for /index.html', () => { + const result = evaluateBaseRewrite('/index.html', '/index.html', undefined, '/docs/', '/'); + assert.equal(result.action, 'not-found-subpath'); + if (result.action === 'not-found-subpath') { + assert.equal(result.pathname, '/index.html'); + } + }); +}); +// #endregion + +// #region evaluateBaseRewrite — not-found (HTML) +describe('evaluateBaseRewrite — not-found', () => { + it('returns not-found for non-base URL with text/html accept', () => { + const result = evaluateBaseRewrite('/other', '/other', 'text/html', '/docs/', '/'); + assert.equal(result.action, 'not-found'); + if (result.action === 'not-found') { + assert.equal(result.pathname, '/other'); + } + }); + + it('returns not-found when accept includes text/html among others', () => { + const result = evaluateBaseRewrite( + '/other', + '/other', + 'text/html, application/xhtml+xml', + '/docs/', + '/', + ); + assert.equal(result.action, 'not-found'); + }); +}); +// #endregion + +// #region evaluateBaseRewrite — check-public +describe('evaluateBaseRewrite — check-public', () => { + it('returns check-public for non-base URL without HTML accept', () => { + const result = evaluateBaseRewrite('/favicon.ico', '/favicon.ico', 'image/*', '/docs/', '/'); + assert.equal(result.action, 'check-public'); + }); + + it('returns check-public when accept header is undefined', () => { + const result = evaluateBaseRewrite('/script.js', '/script.js', undefined, '/docs/', '/'); + assert.equal(result.action, 'check-public'); + }); + + it('returns check-public for non-HTML accept types', () => { + const result = evaluateBaseRewrite('/api/data', '/api/data', 'application/json', '/docs/', '/'); + assert.equal(result.action, 'check-public'); + }); +}); +// #endregion diff --git a/packages/astro/test/units/dev/base.test.js b/packages/astro/test/units/dev/base.test.js deleted file mode 100644 index f230ad563c1b..000000000000 --- a/packages/astro/test/units/dev/base.test.js +++ /dev/null @@ -1,112 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; -import { createFixture, createRequestAndResponse, runInContainer } from '../test-utils.js'; - -describe('base configuration', () => { - describe('with trailingSlash: "never"', () => { - describe('index route', () => { - it('Requests that include a trailing slash 404', async () => { - const fixture = await createFixture({ - '/src/pages/index.astro': `

testing

`, - }); - - await runInContainer( - { - inlineConfig: { - root: fixture.path, - base: '/docs', - trailingSlash: 'never', - }, - }, - async (container) => { - const { req, res, done } = createRequestAndResponse({ - method: 'GET', - url: '/docs/', - }); - container.handle(req, res); - await done; - assert.equal(res.statusCode, 404); - }, - ); - }); - - it('Requests that exclude a trailing slash 200', async () => { - const fixture = await createFixture({ - '/src/pages/index.astro': `

testing

`, - }); - - await runInContainer( - { - fs, - inlineConfig: { - root: fixture.path, - base: '/docs', - trailingSlash: 'never', - }, - }, - async (container) => { - const { req, res, done } = createRequestAndResponse({ - method: 'GET', - url: '/docs', - }); - container.handle(req, res); - await done; - assert.equal(res.statusCode, 200); - }, - ); - }); - }); - - describe('sub route', () => { - it('Requests that include a trailing slash 404', async () => { - const fixture = await createFixture({ - '/src/pages/sub/index.astro': `

testing

`, - }); - - await runInContainer( - { - inlineConfig: { - root: fixture.path, - base: '/docs', - trailingSlash: 'never', - }, - }, - async (container) => { - const { req, res, done } = createRequestAndResponse({ - method: 'GET', - url: '/docs/sub/', - }); - container.handle(req, res); - await done; - assert.equal(res.statusCode, 404); - }, - ); - }); - - it('Requests that exclude a trailing slash 200', async () => { - const fixture = await createFixture({ - '/src/pages/sub/index.astro': `

testing

`, - }); - - await runInContainer( - { - inlineConfig: { - root: fixture.path, - base: '/docs', - trailingSlash: 'never', - }, - }, - async (container) => { - const { req, res, done } = createRequestAndResponse({ - method: 'GET', - url: '/docs/sub', - }); - container.handle(req, res); - await done; - assert.equal(res.statusCode, 200); - }, - ); - }); - }); - }); -}); diff --git a/packages/astro/test/units/dev/dev.test.js b/packages/astro/test/units/dev/dev.test.js deleted file mode 100644 index 8867976feba8..000000000000 --- a/packages/astro/test/units/dev/dev.test.js +++ /dev/null @@ -1,196 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; -import * as cheerio from 'cheerio'; -import { createFixture, createRequestAndResponse, runInContainer } from '../test-utils.js'; - -describe('dev container', () => { - it('can render requests', async () => { - const fixture = await createFixture({ - '/src/pages/index.astro': ` - --- - const name = 'Testing'; - --- - - {name} - -

{name}

- - - `, - }); - - await runInContainer({ inlineConfig: { root: fixture.path } }, async (container) => { - const { req, res, text } = createRequestAndResponse({ - method: 'GET', - url: '/', - }); - container.handle(req, res); - const html = await text(); - const $ = cheerio.load(html); - assert.equal(res.statusCode, 200); - assert.equal($('h1').length, 1); - }); - }); - - it('Allows dynamic segments in injected routes', async () => { - const fixture = await createFixture({ - '/src/components/test.astro': `

{Astro.params.slug}

`, - '/src/pages/test-[slug].astro': `

{Astro.params.slug}

`, - }); - - await runInContainer( - { - inlineConfig: { - root: fixture.path, - output: 'server', - integrations: [ - { - name: '@astrojs/test-integration', - hooks: { - 'astro:config:setup': ({ injectRoute }) => { - injectRoute({ - pattern: '/another-[slug]', - entrypoint: './src/components/test.astro', - }); - }, - }, - }, - ], - }, - }, - async (container) => { - let r = createRequestAndResponse({ - method: 'GET', - url: '/test-one', - }); - container.handle(r.req, r.res); - await r.done; - assert.equal(r.res.statusCode, 200); - - // Try with the injected route - r = createRequestAndResponse({ - method: 'GET', - url: '/another-two', - }); - container.handle(r.req, r.res); - await r.done; - assert.equal(r.res.statusCode, 200); - }, - ); - }); - - it('Serves injected 404 route for any 404', async () => { - const fixture = await createFixture({ - '/src/components/404.astro': `

Custom 404

`, - '/src/pages/page.astro': `

Regular page

`, - }); - - await runInContainer( - { - inlineConfig: { - root: fixture.path, - output: 'server', - integrations: [ - { - name: '@astrojs/test-integration', - hooks: { - 'astro:config:setup': ({ injectRoute }) => { - injectRoute({ - pattern: '/404', - entrypoint: './src/components/404.astro', - }); - }, - }, - }, - ], - }, - }, - async (container) => { - { - // Regular pages are served as expected. - const r = createRequestAndResponse({ method: 'GET', url: '/page' }); - container.handle(r.req, r.res); - await r.done; - const doc = await r.text(); - assert.equal(doc.includes('Regular page'), true); - assert.equal(r.res.statusCode, 200); - } - { - // `/404` serves the custom 404 page as expected. - const r = createRequestAndResponse({ method: 'GET', url: '/404' }); - container.handle(r.req, r.res); - await r.done; - const doc = await r.text(); - assert.equal(doc.includes('Custom 404'), true); - assert.equal(r.res.statusCode, 404); - } - { - // A nonexistent page also serves the custom 404 page. - const r = createRequestAndResponse({ method: 'GET', url: '/other-page' }); - container.handle(r.req, r.res); - await r.done; - const doc = await r.text(); - assert.equal(doc.includes('Custom 404'), true); - assert.equal(r.res.statusCode, 404); - } - }, - ); - }); - - it('items in public/ are not available from root when using a base', async () => { - const fixture = await createFixture({ - '/public/test.txt': `Test`, - }); - - await runInContainer( - { - inlineConfig: { - root: fixture.path, - base: '/sub/', - }, - }, - async (container) => { - // First try the subpath - let r = createRequestAndResponse({ - method: 'GET', - url: '/sub/test.txt', - }); - - container.handle(r.req, r.res); - await r.done; - - assert.equal(r.res.statusCode, 200); - - // Next try the root path - r = createRequestAndResponse({ - method: 'GET', - url: '/test.txt', - }); - - container.handle(r.req, r.res); - await r.done; - - assert.equal(r.res.statusCode, 404); - }, - ); - }); - - it('items in public/ are available from root when not using a base', async () => { - const fixture = await createFixture({ - '/public/test.txt': `Test`, - }); - - await runInContainer({ inlineConfig: { root: fixture.path } }, async (container) => { - // Try the root path - let r = createRequestAndResponse({ - method: 'GET', - url: '/test.txt', - }); - - container.handle(r.req, r.res); - await r.done; - - assert.equal(r.res.statusCode, 200); - }); - }); -}); diff --git a/packages/astro/test/units/dev/error-pages.test.js b/packages/astro/test/units/dev/error-pages.test.js deleted file mode 100644 index 6578f143f98a..000000000000 --- a/packages/astro/test/units/dev/error-pages.test.js +++ /dev/null @@ -1,290 +0,0 @@ -// @ts-check -import assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; -import * as cheerio from 'cheerio'; -import { ensure404Route } from '../../../dist/core/routing/astro-designed-error-pages.js'; -import { createFixture, createRequestAndResponse, runInContainer } from '../test-utils.js'; - -describe('Dev pipeline - error pages', () => { - describe('Custom 404', () => { - it('renders the custom 404.astro page for unmatched routes', async () => { - const fixture = await createFixture({ - '/src/pages/404.astro': `

Custom 404

`, - '/src/pages/index.astro': `

Home

`, - }); - - await runInContainer({ inlineConfig: { root: fixture.path } }, async (container) => { - const r = createRequestAndResponse({ method: 'GET', url: '/does-not-exist' }); - container.handle(r.req, r.res); - await r.done; - - assert.equal(r.res.statusCode, 404); - const html = await r.text(); - const $ = cheerio.load(html); - assert.equal($('h1').text(), 'Custom 404'); - }); - }); - - it('renders the built-in Astro 404 page when no custom 404.astro exists', async () => { - const fixture = await createFixture({ - '/src/pages/index.astro': `

Home

`, - }); - - await runInContainer({ inlineConfig: { root: fixture.path } }, async (container) => { - const r = createRequestAndResponse({ method: 'GET', url: '/does-not-exist' }); - container.handle(r.req, r.res); - await r.done; - - assert.equal(r.res.statusCode, 404); - }); - }); - - it('serves the custom 404 page for the /404 path itself', async () => { - const fixture = await createFixture({ - '/src/pages/404.astro': `

Custom 404

`, - '/src/pages/index.astro': `

Home

`, - }); - - await runInContainer({ inlineConfig: { root: fixture.path } }, async (container) => { - const r = createRequestAndResponse({ method: 'GET', url: '/404' }); - container.handle(r.req, r.res); - await r.done; - - assert.equal(r.res.statusCode, 404); - const html = await r.text(); - const $ = cheerio.load(html); - assert.equal($('h1').text(), 'Custom 404'); - }); - }); - }); - - describe('Custom 500', () => { - it('renders the custom 500.astro page when a route throws', async () => { - const fixture = await createFixture({ - '/src/pages/index.astro': `--- -throw new Error('boom'); ----`, - '/src/pages/500.astro': `

Server Error

`, - }); - - await runInContainer( - { inlineConfig: { root: fixture.path, output: 'server' } }, - async (container) => { - const r = createRequestAndResponse({ method: 'GET', url: '/' }); - container.handle(r.req, r.res); - await r.done; - - assert.equal(r.res.statusCode, 500); - const html = await r.text(); - const $ = cheerio.load(html); - assert.equal($('h1').text(), 'Server Error'); - }, - ); - }); - - it('renders the dev overlay when no custom 500.astro exists and a route throws', async () => { - const fixture = await createFixture({ - '/src/pages/index.astro': `--- -throw new Error('boom'); ----`, - }); - - await runInContainer( - { inlineConfig: { root: fixture.path, output: 'server' } }, - async (container) => { - const r = createRequestAndResponse({ method: 'GET', url: '/' }); - container.handle(r.req, r.res); - await r.done; - - assert.equal(r.res.statusCode, 500); - const html = await r.text(); - // Dev overlay is emitted when DevApp throws (no custom 500 to catch it) - assert.ok(html.includes('/@vite/client')); - }, - ); - }); - - it('renders the custom 500.astro page when an error originates in middleware', async () => { - const fixture = await createFixture({ - '/src/pages/index.astro': `

Home

`, - '/src/pages/500.astro': `

Server Error

`, - '/src/middleware.js': ` -export const onRequest = (_ctx, _next) => { - throw new Error('middleware error'); -}; -`, - }); - - await runInContainer( - { inlineConfig: { root: fixture.path, output: 'server' } }, - async (container) => { - const r = createRequestAndResponse({ method: 'GET', url: '/' }); - container.handle(r.req, r.res); - await r.done; - - assert.equal(r.res.statusCode, 500); - const html = await r.text(); - const $ = cheerio.load(html); - assert.equal($('h1').text(), 'Server Error'); - }, - ); - }); - - it('falls back to the dev overlay when the custom 500.astro itself throws', async () => { - const fixture = await createFixture({ - '/src/pages/index.astro': `--- -throw new Error('page error'); ----`, - '/src/pages/500.astro': `--- -throw new Error('500 page also broken'); ----`, - }); - - await runInContainer( - { inlineConfig: { root: fixture.path, output: 'server' } }, - async (container) => { - const r = createRequestAndResponse({ method: 'GET', url: '/' }); - container.handle(r.req, r.res); - await r.done; - - assert.equal(r.res.statusCode, 500); - const html = await r.text(); - // Escalated to dev overlay after custom 500 also threw - assert.ok(html.includes('/@vite/client')); - }, - ); - }); - - it('re-throws AstroError MiddlewareNoDataOrNextCalled immediately without rendering a 500 page', async () => { - // Middleware that neither calls next() nor returns a Response triggers - // MiddlewareNoDataOrNextCalled. DevApp re-throws this class of AstroError - // immediately rather than attempting to render the 500 page, because the - // error indicates a programming mistake in the middleware itself. - const fixture = await createFixture({ - '/src/pages/index.astro': `

Home

`, - '/src/pages/500.astro': `

Server Error

`, - '/src/middleware.js': ` -export const onRequest = (_ctx, _next) => { - // intentionally not calling next() and not returning — triggers MiddlewareNoDataOrNextCalled -}; -`, - }); - - await runInContainer( - { inlineConfig: { root: fixture.path, output: 'server' } }, - async (container) => { - const r = createRequestAndResponse({ method: 'GET', url: '/' }); - container.handle(r.req, r.res); - await r.done; - - assert.equal(r.res.statusCode, 500); - const html = await r.text(); - // MiddlewareNoDataOrNextCalled is re-thrown straight to the dev overlay, - // bypassing the custom 500 page entirely. - assert.ok(html.includes('/@vite/client')); - // The custom 500 page should NOT have been rendered. - assert.ok(!html.includes('Server Error')); - }, - ); - }); - }); - - describe('ensure404Route', () => { - it('adds the default /404 route when none exists in the manifest', () => { - /** @type {{ routes: any[] }} */ - const manifest = { routes: [] }; - ensure404Route(manifest); - - const route404 = manifest.routes.find((r) => r.route === '/404'); - assert.ok(route404, 'A /404 route should be added when none exists'); - }); - - it('does not add a duplicate /404 route when one already exists', () => { - /** @type {{ routes: any[] }} */ - const manifest = { - routes: [ - { - route: '/404', - component: 'src/pages/404.astro', - params: [], - pathname: '/404', - distURL: [], - pattern: /^\/404\/?$/, - segments: [[{ content: '404', dynamic: false, spread: false }]], - type: 'page', - prerender: false, - fallbackRoutes: [], - isIndex: false, - origin: 'project', - }, - ], - }; - ensure404Route(manifest); - ensure404Route(manifest); // call twice to verify idempotency - - const count = manifest.routes.filter((r) => r.route === '/404').length; - assert.equal(count, 1, 'There should be exactly one /404 route'); - }); - - it('preserves the user-provided 404 component rather than substituting the default', () => { - const userComponent = 'src/pages/404.astro'; - /** @type {{ routes: any[] }} */ - const manifest = { - routes: [ - { - route: '/404', - component: userComponent, - params: [], - pathname: '/404', - distURL: [], - pattern: /^\/404\/?$/, - segments: [[{ content: '404', dynamic: false, spread: false }]], - type: 'page', - prerender: false, - fallbackRoutes: [], - isIndex: false, - origin: 'project', - }, - ], - }; - ensure404Route(manifest); - - const route404 = manifest.routes.find((r) => r.route === '/404'); - assert.equal( - route404?.component, - userComponent, - 'User-provided 404 component should not be replaced by the default', - ); - }); - - it('does not affect /500 routes', () => { - /** @type {{ routes: any[] }} */ - const manifest = { - routes: [ - { - route: '/500', - component: 'src/pages/500.astro', - params: [], - pathname: '/500', - distURL: [], - pattern: /^\/500\/?$/, - segments: [[{ content: '500', dynamic: false, spread: false }]], - type: 'page', - prerender: false, - fallbackRoutes: [], - isIndex: false, - origin: 'project', - }, - ], - }; - ensure404Route(manifest); - - // /404 should be added (no user 404 exists), /500 should be untouched - const count500 = manifest.routes.filter((r) => r.route === '/500').length; - assert.equal(count500, 1, '/500 route count should remain exactly 1'); - - const has404 = manifest.routes.some((r) => r.route === '/404'); - assert.ok(has404, 'Default /404 should have been added'); - }); - }); -}); diff --git a/packages/astro/test/units/dev/error-pages.test.ts b/packages/astro/test/units/dev/error-pages.test.ts new file mode 100644 index 000000000000..992c1f3c9492 --- /dev/null +++ b/packages/astro/test/units/dev/error-pages.test.ts @@ -0,0 +1,98 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { ensure404Route } from '../../../dist/core/routing/astro-designed-error-pages.js'; + +describe('ensure404Route', () => { + it('adds the default /404 route when none exists in the manifest', () => { + const manifest: any = { routes: [] }; + ensure404Route(manifest); + + const route404 = manifest.routes.find((r: any) => r.route === '/404'); + assert.ok(route404, 'A /404 route should be added when none exists'); + }); + + it('does not add a duplicate /404 route when one already exists', () => { + const manifest: any = { + routes: [ + { + route: '/404', + component: 'src/pages/404.astro', + params: [], + pathname: '/404', + distURL: [], + pattern: /^\/404\/?$/, + segments: [[{ content: '404', dynamic: false, spread: false }]], + type: 'page', + prerender: false, + fallbackRoutes: [], + isIndex: false, + origin: 'project', + }, + ], + }; + ensure404Route(manifest); + ensure404Route(manifest); // call twice to verify idempotency + + const count = manifest.routes.filter((r: any) => r.route === '/404').length; + assert.equal(count, 1, 'There should be exactly one /404 route'); + }); + + it('preserves the user-provided 404 component rather than substituting the default', () => { + const userComponent = 'src/pages/404.astro'; + const manifest: any = { + routes: [ + { + route: '/404', + component: userComponent, + params: [], + pathname: '/404', + distURL: [], + pattern: /^\/404\/?$/, + segments: [[{ content: '404', dynamic: false, spread: false }]], + type: 'page', + prerender: false, + fallbackRoutes: [], + isIndex: false, + origin: 'project', + }, + ], + }; + ensure404Route(manifest); + + const route404 = manifest.routes.find((r: any) => r.route === '/404'); + assert.equal( + route404?.component, + userComponent, + 'User-provided 404 component should not be replaced by the default', + ); + }); + + it('does not affect /500 routes', () => { + const manifest: any = { + routes: [ + { + route: '/500', + component: 'src/pages/500.astro', + params: [], + pathname: '/500', + distURL: [], + pattern: /^\/500\/?$/, + segments: [[{ content: '500', dynamic: false, spread: false }]], + type: 'page', + prerender: false, + fallbackRoutes: [], + isIndex: false, + origin: 'project', + }, + ], + }; + ensure404Route(manifest); + + // /404 should be added (no user 404 exists), /500 should be untouched + const count500 = manifest.routes.filter((r: any) => r.route === '/500').length; + assert.equal(count500, 1, '/500 route count should remain exactly 1'); + + const has404 = manifest.routes.some((r: any) => r.route === '/404'); + assert.ok(has404, 'Default /404 should have been added'); + }); +}); diff --git a/packages/astro/test/units/dev/hydration.test.js b/packages/astro/test/units/dev/hydration.test.js deleted file mode 100644 index 03962a416fa0..000000000000 --- a/packages/astro/test/units/dev/hydration.test.js +++ /dev/null @@ -1,50 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; -import { createFixture, createRequestAndResponse, runInContainer } from '../test-utils.js'; - -describe('hydration', () => { - it( - 'should not crash when reassigning a hydrated component', - { skip: true, todo: "It seems that `components/Client.svelte` isn't found" }, - async () => { - const fixture = await createFixture({ - '/src/pages/index.astro': ` - --- - import Svelte from '../components/Client.svelte'; - const Foo = Svelte; - const Bar = Svelte; - --- - - testing - - - - - - `, - }); - - await runInContainer( - { - inlineConfig: { - root: fixture.path, - logLevel: 'silent', - }, - }, - async (container) => { - const { req, res, done } = createRequestAndResponse({ - method: 'GET', - url: '/', - }); - container.handle(req, res); - await done; - assert.equal( - res.statusCode, - 200, - "We get a 200 because the error occurs in the template, but we didn't crash!", - ); - }, - ); - }, - ); -}); diff --git a/packages/astro/test/units/dev/restart.test.js b/packages/astro/test/units/dev/restart.test.js deleted file mode 100644 index 9d5664cbf246..000000000000 --- a/packages/astro/test/units/dev/restart.test.js +++ /dev/null @@ -1,233 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; -import * as cheerio from 'cheerio'; - -import { - createContainerWithAutomaticRestart, - startContainer, -} from '../../../dist/core/dev/index.js'; -import { createFixture, createRequestAndResponse } from '../test-utils.js'; - -/** @type {import('astro').AstroInlineConfig} */ -const defaultInlineConfig = { - logLevel: 'silent', -}; - -function isStarted(container) { - return !!container.viteServer.httpServer?.listening; -} - -// Checking for restarts may hang if no restarts happen, so set a 20s timeout for each test -describe('dev container restarts', { timeout: 20000 }, () => { - it('Surfaces config errors on restarts', async () => { - const fixture = await createFixture({ - '/src/pages/index.astro': ` - - Test - -

Test

- - - `, - '/astro.config.mjs': ``, - }); - - const restart = await createContainerWithAutomaticRestart({ - inlineConfig: { - ...defaultInlineConfig, - root: fixture.path, - }, - }); - - try { - let r = createRequestAndResponse({ - method: 'GET', - url: '/', - }); - restart.container.handle(r.req, r.res); - let html = await r.text(); - const $ = cheerio.load(html); - assert.equal(r.res.statusCode, 200); - assert.equal($('h1').length, 1); - - // Create an error - let restartComplete = restart.restarted(); - await fixture.writeFile('/astro.config.mjs', 'const foo = bar'); - // TODO: fix this hack - restart.container.viteServer.watcher.emit( - 'change', - fixture.getPath('/astro.config.mjs').replace(/\\/g, '/'), - ); - - // Wait for the restart to finish - let hmrError = await restartComplete; - assert.ok(hmrError instanceof Error); - - // Do it a second time to make sure we are still watching - - restartComplete = restart.restarted(); - await fixture.writeFile('/astro.config.mjs', 'const foo = bar2'); - // TODO: fix this hack - restart.container.viteServer.watcher.emit( - 'change', - fixture.getPath('/astro.config.mjs').replace(/\\/g, '/'), - ); - - hmrError = await restartComplete; - assert.ok(hmrError instanceof Error); - } finally { - await restart.container.close(); - } - }); - - it('Restarts the container if previously started', async () => { - const fixture = await createFixture({ - '/src/pages/index.astro': ` - - Test - -

Test

- - - `, - '/astro.config.mjs': ``, - }); - - const restart = await createContainerWithAutomaticRestart({ - inlineConfig: { - ...defaultInlineConfig, - root: fixture.path, - }, - }); - await startContainer(restart.container); - assert.equal(isStarted(restart.container), true); - - try { - // Trigger a change - let restartComplete = restart.restarted(); - await fixture.writeFile('/astro.config.mjs', ''); - // TODO: fix this hack - restart.container.viteServer.watcher.emit( - 'change', - fixture.getPath('/astro.config.mjs').replace(/\\/g, '/'), - ); - await restartComplete; - - assert.equal(isStarted(restart.container), true); - } finally { - await restart.container.close(); - } - }); - - it('Is able to restart project using Tailwind + astro.config.ts', async () => { - const fixture = await createFixture({ - '/src/pages/index.astro': ``, - '/astro.config.ts': ``, - }); - - const restart = await createContainerWithAutomaticRestart({ - inlineConfig: { - ...defaultInlineConfig, - root: fixture.path, - }, - }); - await startContainer(restart.container); - assert.equal(isStarted(restart.container), true); - - try { - // Trigger a change - let restartComplete = restart.restarted(); - await fixture.writeFile('/astro.config.ts', ''); - // TODO: fix this hack - restart.container.viteServer.watcher.emit( - 'change', - fixture.getPath('/astro.config.mjs').replace(/\\/g, '/'), - ); - await restartComplete; - - assert.equal(isStarted(restart.container), true); - } finally { - await restart.container.close(); - } - }); - - it('Is able to restart project on package.json changes', async () => { - const fixture = await createFixture({ - '/src/pages/index.astro': ``, - }); - - const restart = await createContainerWithAutomaticRestart({ - inlineConfig: { - ...defaultInlineConfig, - root: fixture.path, - }, - }); - await startContainer(restart.container); - assert.equal(isStarted(restart.container), true); - - try { - let restartComplete = restart.restarted(); - await fixture.writeFile('/package.json', `{}`); - // TODO: fix this hack - restart.container.viteServer.watcher.emit( - 'change', - fixture.getPath('/package.json').replace(/\\/g, '/'), - ); - await restartComplete; - } finally { - await restart.container.close(); - } - }); - - it('Is able to restart on viteServer.restart API call', async () => { - const fixture = await createFixture({ - '/src/pages/index.astro': ``, - }); - - const restart = await createContainerWithAutomaticRestart({ - inlineConfig: { - ...defaultInlineConfig, - root: fixture.path, - }, - }); - await startContainer(restart.container); - assert.equal(isStarted(restart.container), true); - - try { - let restartComplete = restart.restarted(); - await restart.container.viteServer.restart(); - await restartComplete; - } finally { - await restart.container.close(); - } - }); - - it('Is able to restart project on .astro/settings.json changes', async () => { - const fixture = await createFixture({ - '/src/pages/index.astro': ``, - '/.astro/settings.json': `{}`, - }); - - const restart = await createContainerWithAutomaticRestart({ - inlineConfig: { - ...defaultInlineConfig, - root: fixture.path, - }, - }); - await startContainer(restart.container); - assert.equal(isStarted(restart.container), true); - - try { - let restartComplete = restart.restarted(); - await fixture.writeFile('/.astro/settings.json', `{ }`); - // TODO: fix this hack - restart.container.viteServer.watcher.emit( - 'change', - fixture.getPath('/.astro/settings.json').replace(/\\/g, '/'), - ); - await restartComplete; - } finally { - await restart.container.close(); - } - }); -}); diff --git a/packages/astro/test/units/dev/sec-fetch.test.js b/packages/astro/test/units/dev/sec-fetch.test.ts similarity index 95% rename from packages/astro/test/units/dev/sec-fetch.test.js rename to packages/astro/test/units/dev/sec-fetch.test.ts index 21de0902c69e..ab92cbd57471 100644 --- a/packages/astro/test/units/dev/sec-fetch.test.js +++ b/packages/astro/test/units/dev/sec-fetch.test.ts @@ -1,13 +1,17 @@ import * as assert from 'node:assert/strict'; import { describe, it } from 'node:test'; +import type { RemotePattern } from '@astrojs/internal-helpers/remote'; import { secFetchMiddleware } from '../../../dist/vite-plugin-astro-server/sec-fetch.js'; -import { createRequestAndResponse, defaultLogger } from '../test-utils.js'; +import { createRequestAndResponse, defaultLogger } from '../test-utils.ts'; /** * Helper to run a request through the secFetchMiddleware and return whether * it was blocked (response ended with 403) or allowed (next() was called). */ -function runMiddleware(headers, allowedDomains) { +function runMiddleware( + headers: Record, + allowedDomains?: Partial[], +): Promise<{ nextCalled: boolean; statusCode: number }> { const middleware = secFetchMiddleware(defaultLogger, allowedDomains); const { req, res, done } = createRequestAndResponse({ method: 'GET', @@ -131,7 +135,7 @@ describe('secFetchMiddleware', () => { }); describe('allowedDomains support', () => { - const allowedDomains = [ + const allowedDomains: Partial[] = [ { hostname: 'myproxy.example.com', protocol: 'https' }, { hostname: '*.ngrok.io', protocol: 'https' }, ]; diff --git a/packages/astro/test/units/dev/trailing-slash-decision.test.ts b/packages/astro/test/units/dev/trailing-slash-decision.test.ts new file mode 100644 index 000000000000..374c4383c81f --- /dev/null +++ b/packages/astro/test/units/dev/trailing-slash-decision.test.ts @@ -0,0 +1,150 @@ +import * as assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { evaluateTrailingSlash } from '../../../dist/vite-plugin-astro-server/trailing-slash.js'; + +// #region internal paths +describe('evaluateTrailingSlash — internal paths', () => { + it('passes through /@vite/client', () => { + const result = evaluateTrailingSlash('/@vite/client', '', 'never'); + assert.deepEqual(result, { action: 'next' }); + }); + + it('passes through /@fs/ paths', () => { + const result = evaluateTrailingSlash('/@fs/project/src/main.ts', '', 'always'); + assert.deepEqual(result, { action: 'next' }); + }); + + it('passes through /@id/ paths', () => { + const result = evaluateTrailingSlash('/@id/module', '', 'never'); + assert.deepEqual(result, { action: 'next' }); + }); +}); +// #endregion + +// #region duplicate trailing slashes +describe('evaluateTrailingSlash — duplicate trailing slashes', () => { + it('redirects /about// to /about/', () => { + const result = evaluateTrailingSlash('/about//', '', 'ignore'); + assert.equal(result.action, 'redirect'); + if (result.action === 'redirect') { + assert.equal(result.status, 301); + assert.equal(result.location, '/about/'); + } + }); + + it('redirects /about/// to /about/', () => { + const result = evaluateTrailingSlash('/about///', '', 'ignore'); + assert.equal(result.action, 'redirect'); + if (result.action === 'redirect') { + assert.equal(result.location, '/about/'); + } + }); + + it('preserves query string in redirect', () => { + const result = evaluateTrailingSlash('/about//', '?foo=bar', 'ignore'); + assert.equal(result.action, 'redirect'); + if (result.action === 'redirect') { + assert.equal(result.location, '/about/?foo=bar'); + } + }); + + it('collapses only trailing slashes, not internal ones', () => { + const result = evaluateTrailingSlash('/blog//post//', '', 'ignore'); + assert.equal(result.action, 'redirect'); + if (result.action === 'redirect') { + // collapseDuplicateTrailingSlashes only collapses trailing slashes + assert.equal(result.location, '/blog//post/'); + } + }); +}); +// #endregion + +// #region trailingSlash: 'never' +describe('evaluateTrailingSlash — trailingSlash: "never"', () => { + it('rejects /about/ (has trailing slash)', () => { + const result = evaluateTrailingSlash('/about/', '', 'never'); + assert.equal(result.action, 'reject'); + if (result.action === 'reject') { + assert.equal(result.status, 404); + assert.equal(result.pathname, '/about/'); + } + }); + + it('passes /about (no trailing slash)', () => { + const result = evaluateTrailingSlash('/about', '', 'never'); + assert.deepEqual(result, { action: 'next' }); + }); + + it('exempts root path / (always allowed)', () => { + const result = evaluateTrailingSlash('/', '', 'never'); + assert.deepEqual(result, { action: 'next' }); + }); + + it('rejects /blog/post/ (nested with trailing slash)', () => { + const result = evaluateTrailingSlash('/blog/post/', '', 'never'); + assert.equal(result.action, 'reject'); + }); +}); +// #endregion + +// #region trailingSlash: 'always' +describe('evaluateTrailingSlash — trailingSlash: "always"', () => { + it('rejects /about (no trailing slash)', () => { + const result = evaluateTrailingSlash('/about', '', 'always'); + assert.equal(result.action, 'reject'); + if (result.action === 'reject') { + assert.equal(result.status, 404); + assert.equal(result.pathname, '/about'); + } + }); + + it('passes /about/ (has trailing slash)', () => { + const result = evaluateTrailingSlash('/about/', '', 'always'); + assert.deepEqual(result, { action: 'next' }); + }); + + it('exempts paths with file extension', () => { + const result = evaluateTrailingSlash('/styles.css', '', 'always'); + assert.deepEqual(result, { action: 'next' }); + }); + + it('exempts .html file extension', () => { + const result = evaluateTrailingSlash('/page.html', '', 'always'); + assert.deepEqual(result, { action: 'next' }); + }); + + it('exempts .js file extension', () => { + const result = evaluateTrailingSlash('/script.js', '', 'always'); + assert.deepEqual(result, { action: 'next' }); + }); + + it('passes root path /', () => { + const result = evaluateTrailingSlash('/', '', 'always'); + assert.deepEqual(result, { action: 'next' }); + }); +}); +// #endregion + +// #region trailingSlash: 'ignore' +describe('evaluateTrailingSlash — trailingSlash: "ignore"', () => { + it('passes /about', () => { + const result = evaluateTrailingSlash('/about', '', 'ignore'); + assert.deepEqual(result, { action: 'next' }); + }); + + it('passes /about/', () => { + const result = evaluateTrailingSlash('/about/', '', 'ignore'); + assert.deepEqual(result, { action: 'next' }); + }); + + it('passes /', () => { + const result = evaluateTrailingSlash('/', '', 'ignore'); + assert.deepEqual(result, { action: 'next' }); + }); + + it('still redirects duplicate slashes', () => { + const result = evaluateTrailingSlash('/about//', '', 'ignore'); + assert.equal(result.action, 'redirect'); + }); +}); +// #endregion diff --git a/packages/astro/test/units/env/env-validators.test.js b/packages/astro/test/units/env/env-validators.test.ts similarity index 92% rename from packages/astro/test/units/env/env-validators.test.js rename to packages/astro/test/units/env/env-validators.test.ts index 1668408a9726..51f436877e0b 100644 --- a/packages/astro/test/units/env/env-validators.test.js +++ b/packages/astro/test/units/env/env-validators.test.ts @@ -6,42 +6,27 @@ import { validateEnvPrefixAgainstSchema, } from '../../../dist/env/validators.js'; -/** - * @typedef {Parameters} Params - */ +type Params = Parameters; const createFixture = () => { - /** - * @type {{ value: Params[1]; options: Params[2] }} input - */ - let input; + let input: { value: Params[0]; options: Params[1] } | undefined; return { - /** - * @param {Params[1]} value - * @param {Params[2]} options - */ - givenInput(value, options) { + givenInput(value: Params[0], options: Params[1]) { input = { value, options }; }, - /** - * @param {import("../../../src/env/validators.js").ValidationResultValue} value - */ - thenResultShouldBeValid(value) { - const result = validateEnvVariable(input.value, input.options); + thenResultShouldBeValid(value: any) { + const result: any = validateEnvVariable(input!.value, input!.options); assert.equal(result.ok, true); assert.equal(result.value, value); input = undefined; }, - /** - * @param {string | Array} providedErrors - */ - thenResultShouldBeInvalid(providedErrors) { - const result = validateEnvVariable(input.value, input.options); + thenResultShouldBeInvalid(providedErrors: string | string[]) { + const result: any = validateEnvVariable(input!.value, input!.options); assert.equal(result.ok, false); const errors = typeof providedErrors === 'string' ? [providedErrors] : providedErrors; assert.equal( - result.errors.every((element) => errors.includes(element)), + result.errors.every((element: string) => errors.includes(element)), true, ); input = undefined; @@ -50,8 +35,7 @@ const createFixture = () => { }; describe('astro:env validators', () => { - /** @type {ReturnType} */ - let fixture; + let fixture: ReturnType; before(() => { fixture = createFixture(); @@ -556,18 +540,11 @@ describe('astro:env validators', () => { }); describe('validateEnvPrefixAgainstSchema', () => { - /** - * Helper to build a minimal config object matching the shape - * validateEnvPrefixAgainstSchema expects. - * - * @param {Record} schema - * @param {string | string[] | undefined} envPrefix - */ - function makeConfig(schema, envPrefix) { - return /** @type {any} */ ({ + function makeConfig(schema: Record, envPrefix?: string | string[]): any { + return { env: { schema }, vite: envPrefix !== undefined ? { envPrefix } : {}, - }); + }; } it('should not throw when schema is empty', () => { @@ -619,7 +596,7 @@ describe('validateEnvPrefixAgainstSchema', () => { ]), ); }, - (err) => { + (err: any) => { assert.equal(err.name, 'EnvPrefixConflictsWithSecret'); assert.equal(err.message.includes('API_SECRET'), true); return true; @@ -637,7 +614,7 @@ describe('validateEnvPrefixAgainstSchema', () => { ), ); }, - (err) => { + (err: any) => { assert.equal(err.name, 'EnvPrefixConflictsWithSecret'); assert.equal(err.message.includes('SECRET_KEY'), true); return true; @@ -659,7 +636,7 @@ describe('validateEnvPrefixAgainstSchema', () => { ), ); }, - (err) => { + (err: any) => { assert.equal(err.name, 'EnvPrefixConflictsWithSecret'); assert.equal(err.message.includes('API_SECRET'), true); assert.equal(err.message.includes('API_KEY'), true); diff --git a/packages/astro/test/units/errors/dev-utils.test.js b/packages/astro/test/units/errors/dev-utils.test.ts similarity index 100% rename from packages/astro/test/units/errors/dev-utils.test.js rename to packages/astro/test/units/errors/dev-utils.test.ts diff --git a/packages/astro/test/units/errors/errors.test.js b/packages/astro/test/units/errors/errors.test.ts similarity index 100% rename from packages/astro/test/units/errors/errors.test.js rename to packages/astro/test/units/errors/errors.test.ts diff --git a/packages/astro/test/units/errors/zod-error-map.test.ts b/packages/astro/test/units/errors/zod-error-map.test.ts new file mode 100644 index 000000000000..622858a24792 --- /dev/null +++ b/packages/astro/test/units/errors/zod-error-map.test.ts @@ -0,0 +1,193 @@ +import * as assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { errorMap } from '../../../dist/core/errors/zod-error-map.js'; + +/** Extract the message string from errorMap's return value. */ +function getMessage(result: ReturnType): string { + if (typeof result === 'string') return result; + if (result && typeof result === 'object' && 'message' in result) return result.message; + throw new Error(`Expected a message, got ${JSON.stringify(result)}`); +} + +// #region invalid_type +describe('errorMap — invalid_type', () => { + it('formats expected vs received message', () => { + const msg = getMessage( + errorMap({ + code: 'invalid_type', + expected: 'string', + input: 42, + path: [], + message: '', + }), + ); + assert.match(msg, /Expected type `"string"`/); + assert.match(msg, /received `"number"`/); + }); + + it('includes bold path prefix for nested paths', () => { + const msg = getMessage( + errorMap({ + code: 'invalid_type', + expected: 'boolean', + input: 'hello', + path: ['config', 'enabled'], + message: '', + }), + ); + assert.match(msg, /\*\*config\.enabled\*\*/); + assert.match(msg, /Expected type `"boolean"`/); + }); + + it('shows "Required" when received is undefined', () => { + const msg = getMessage( + errorMap({ + code: 'invalid_type', + expected: 'string', + input: undefined, + path: ['name'], + message: 'Required', + }), + ); + assert.match(msg, /Required/); + }); + + it('handles root-level path (empty path)', () => { + const msg = getMessage( + errorMap({ + code: 'invalid_type', + expected: 'object', + input: 'bad', + path: [], + message: '', + }), + ); + // No bold prefix when path is empty + assert.ok(!msg.includes('**')); + assert.match(msg, /Expected type `"object"`/); + }); +}); +// #endregion + +// #region invalid_union +describe('errorMap — invalid_union', () => { + it('deduplicates common type errors across union members', () => { + const msg = getMessage( + errorMap({ + code: 'invalid_union', + input: 123, + path: [], + message: '', + errors: [ + [ + { + code: 'invalid_type', + expected: 'string', + received: 'number', + input: 123, + path: ['key'], + message: '', + } as any, + ], + [ + { + code: 'invalid_type', + expected: 'string', + received: 'number', + input: 123, + path: ['key'], + message: '', + } as any, + ], + ], + }), + ); + assert.match(msg, /Did not match union/); + assert.match(msg, /\*\*key\*\*/); + assert.match(msg, /Expected type/); + assert.match(msg, /received/); + }); + + it('shows expected shapes when type errors differ across union members', () => { + const msg = getMessage( + errorMap({ + code: 'invalid_union', + input: { wrong: true }, + path: [], + message: '', + errors: [ + [ + { + code: 'invalid_type', + expected: 'string', + input: { wrong: true }, + path: ['a'], + message: '', + }, + ], + [ + { + code: 'invalid_type', + expected: 'number', + input: { wrong: true }, + path: ['b'], + message: '', + }, + ], + ], + }), + ); + assert.match(msg, /Did not match union/); + assert.match(msg, /Expected type/); + }); + + it('handles nested path for union error', () => { + const msg = getMessage( + errorMap({ + code: 'invalid_union', + input: 'bad', + path: ['items', 0], + message: '', + errors: [ + [ + { + code: 'invalid_type', + expected: 'string', + input: 'bad', + path: ['items', 0, 'type'], + message: '', + }, + ], + ], + }), + ); + assert.match(msg, /\*\*items\.0\*\*/); + }); +}); +// #endregion + +// #region fallback +describe('errorMap — fallback behavior', () => { + it('returns message with path prefix for issues with a message', () => { + const msg = getMessage( + errorMap({ + code: 'custom' as any, + path: ['setting'], + message: 'Invalid value', + input: undefined, + }), + ); + assert.match(msg, /\*\*setting\*\*: Invalid value/); + }); + + it('returns undefined for unknown code without message', () => { + const result = errorMap({ + code: 'custom' as any, + path: [], + input: undefined, + message: undefined as any, + }); + assert.equal(result, undefined); + }); +}); +// #endregion diff --git a/packages/astro/test/units/i18n/astro_i18n.test.js b/packages/astro/test/units/i18n/astro_i18n.test.ts similarity index 81% rename from packages/astro/test/units/i18n/astro_i18n.test.js rename to packages/astro/test/units/i18n/astro_i18n.test.ts index 5714c9cf8629..fd051bf04f83 100644 --- a/packages/astro/test/units/i18n/astro_i18n.test.js +++ b/packages/astro/test/units/i18n/astro_i18n.test.ts @@ -1,5 +1,6 @@ import * as assert from 'node:assert/strict'; import { describe, it } from 'node:test'; +import type { AstroConfig } from '../../../dist/types/public/config.js'; import { toRoutingStrategy } from '../../../dist/core/app/common.js'; import { validateConfig } from '../../../dist/core/config/validate.js'; import { MissingLocale } from '../../../dist/core/errors/errors-data.js'; @@ -12,12 +13,24 @@ import { } from '../../../dist/i18n/index.js'; import { parseLocale } from '../../../dist/i18n/utils.js'; +type I18nRouting = NonNullable['routing']; +type I18nRoutingInput = Partial> | 'manual' | undefined; + +// Helper wrappers that accept partial config objects (matching original JS test behavior). +// The i18n functions require full config types but these tests intentionally pass subsets. +const relativeUrl = (opts: Record) => + getLocaleRelativeUrl(opts as unknown as Parameters[0]); +const relativeUrlList = (opts: Record) => + getLocaleRelativeUrlList(opts as unknown as Parameters[0]); +const absoluteUrl = (opts: Record) => + getLocaleAbsoluteUrl(opts as unknown as Parameters[0]); +const absoluteUrlList = (opts: Record) => + getLocaleAbsoluteUrlList(opts as unknown as Parameters[0]); +const routingStrategy = (routing?: I18nRoutingInput, domains?: Record) => + toRoutingStrategy(routing as I18nRouting, domains); + describe('getLocaleRelativeUrl', () => { it('should correctly return the URL with the base', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { base: '/blog', experimental: { @@ -38,7 +51,7 @@ describe('getLocaleRelativeUrl', () => { // directory format assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'en', base: '/blog/', trailingSlash: 'always', @@ -48,7 +61,7 @@ describe('getLocaleRelativeUrl', () => { '/blog/', ); assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'es', base: '/blog/', ...config.experimental.i18n, @@ -60,7 +73,7 @@ describe('getLocaleRelativeUrl', () => { // file format assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'en', base: '/blog/', ...config.experimental.i18n, @@ -70,7 +83,7 @@ describe('getLocaleRelativeUrl', () => { '/blog/', ); assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'es', base: '/blog/', ...config.experimental.i18n, @@ -81,7 +94,7 @@ describe('getLocaleRelativeUrl', () => { ); assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'it-VA', base: '/blog/', ...config.experimental.i18n, @@ -93,10 +106,6 @@ describe('getLocaleRelativeUrl', () => { }); it('should correctly return the URL without base', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { experimental: { i18n: { @@ -107,7 +116,7 @@ describe('getLocaleRelativeUrl', () => { }; assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'en', base: '/', ...config.experimental.i18n, @@ -117,7 +126,7 @@ describe('getLocaleRelativeUrl', () => { '/', ); assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'es', base: '/', ...config.experimental.i18n, @@ -128,7 +137,7 @@ describe('getLocaleRelativeUrl', () => { ); assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'en', base: '/', ...config.experimental.i18n, @@ -139,7 +148,7 @@ describe('getLocaleRelativeUrl', () => { ); assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'es', base: '/', ...config.experimental.i18n, @@ -150,7 +159,7 @@ describe('getLocaleRelativeUrl', () => { ); assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'en', base: '/', ...config.experimental.i18n, @@ -162,10 +171,6 @@ describe('getLocaleRelativeUrl', () => { }); it('should correctly handle the trailing slash', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { i18n: { defaultLocale: 'en', @@ -181,7 +186,7 @@ describe('getLocaleRelativeUrl', () => { }; // directory format assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'en', base: '/blog', ...config.i18n, @@ -191,7 +196,7 @@ describe('getLocaleRelativeUrl', () => { '/blog', ); assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'es', base: '/blog/', ...config.i18n, @@ -202,7 +207,7 @@ describe('getLocaleRelativeUrl', () => { ); assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'it-VA', base: '/blog/', ...config.i18n, @@ -213,7 +218,7 @@ describe('getLocaleRelativeUrl', () => { ); assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'en', base: '/blog/', ...config.i18n, @@ -225,7 +230,7 @@ describe('getLocaleRelativeUrl', () => { // directory file assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'en', base: '/blog', ...config.i18n, @@ -235,7 +240,7 @@ describe('getLocaleRelativeUrl', () => { '/blog', ); assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'es', base: '/blog/', ...config.i18n, @@ -246,7 +251,7 @@ describe('getLocaleRelativeUrl', () => { ); assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'en', // ignore + file => no trailing slash base: '/blog', @@ -259,10 +264,6 @@ describe('getLocaleRelativeUrl', () => { }); it('should normalize locales by default', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { base: '/blog', i18n: { @@ -272,48 +273,44 @@ describe('getLocaleRelativeUrl', () => { }; assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'en_US', base: '/blog/', ...config.i18n, trailingSlash: 'always', format: 'directory', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(), }), '/blog/en-us/', ); assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'en_US', base: '/blog/', ...config.i18n, trailingSlash: 'always', format: 'directory', normalizeLocale: false, - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(), }), '/blog/en_US/', ); assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'en_AU', base: '/blog/', ...config.i18n, trailingSlash: 'always', format: 'directory', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(), }), '/blog/en-au/', ); }); it('should return the default locale when routing strategy is [pathname-prefix-always]', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { base: '/blog', i18n: { @@ -327,58 +324,54 @@ describe('getLocaleRelativeUrl', () => { // directory format assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'en', base: '/blog/', trailingSlash: 'always', format: 'directory', ...config.i18n, - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), '/blog/en/', ); assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'es', base: '/blog/', ...config.i18n, trailingSlash: 'always', format: 'directory', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), '/blog/es/', ); // file format assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'en', base: '/blog/', ...config.i18n, trailingSlash: 'always', format: 'file', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), '/blog/en/', ); assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'es', base: '/blog/', ...config.i18n, trailingSlash: 'always', format: 'file', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), '/blog/es/', ); }); it('should return the default locale when routing strategy is [pathname-prefix-always-no-redirect]', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { base: '/blog', i18n: { @@ -393,48 +386,48 @@ describe('getLocaleRelativeUrl', () => { // directory format assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'en', base: '/blog/', trailingSlash: 'always', format: 'directory', ...config.i18n, - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), '/blog/en/', ); assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'es', base: '/blog/', ...config.i18n, trailingSlash: 'always', format: 'directory', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), '/blog/es/', ); // file format assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'en', base: '/blog/', ...config.i18n, trailingSlash: 'always', format: 'file', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), '/blog/en/', ); assert.equal( - getLocaleRelativeUrl({ + relativeUrl({ locale: 'es', base: '/blog/', ...config.i18n, trailingSlash: 'always', format: 'file', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), '/blog/es/', ); @@ -443,10 +436,6 @@ describe('getLocaleRelativeUrl', () => { describe('getLocaleRelativeUrlList', () => { it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: never]', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { experimental: { i18n: { @@ -465,7 +454,7 @@ describe('getLocaleRelativeUrlList', () => { }; // directory format assert.deepEqual( - getLocaleRelativeUrlList({ + relativeUrlList({ locale: 'en', base: '/blog', ...config.experimental.i18n, @@ -477,10 +466,6 @@ describe('getLocaleRelativeUrlList', () => { }); it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: always]', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { experimental: { i18n: { @@ -499,7 +484,7 @@ describe('getLocaleRelativeUrlList', () => { }; // directory format assert.deepEqual( - getLocaleRelativeUrlList({ + relativeUrlList({ locale: 'en', base: '/blog/', ...config.experimental.i18n, @@ -511,10 +496,6 @@ describe('getLocaleRelativeUrlList', () => { }); it('should retrieve the correct list of base URL with locales [format: file, trailingSlash: always]', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { experimental: { i18n: { @@ -525,7 +506,7 @@ describe('getLocaleRelativeUrlList', () => { }; // directory format assert.deepEqual( - getLocaleRelativeUrlList({ + relativeUrlList({ locale: 'en', base: '/blog/', ...config.experimental.i18n, @@ -537,10 +518,6 @@ describe('getLocaleRelativeUrlList', () => { }); it('should retrieve the correct list of base URL with locales [format: file, trailingSlash: never]', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { experimental: { i18n: { @@ -551,7 +528,7 @@ describe('getLocaleRelativeUrlList', () => { }; // directory format assert.deepEqual( - getLocaleRelativeUrlList({ + relativeUrlList({ locale: 'en', base: '/blog', ...config.experimental.i18n, @@ -563,10 +540,6 @@ describe('getLocaleRelativeUrlList', () => { }); it('should retrieve the correct list of base URL with locales [format: file, trailingSlash: ignore]', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { experimental: { i18n: { @@ -577,7 +550,7 @@ describe('getLocaleRelativeUrlList', () => { }; // directory format assert.deepEqual( - getLocaleRelativeUrlList({ + relativeUrlList({ locale: 'en', base: '/blog', ...config.experimental.i18n, @@ -589,10 +562,6 @@ describe('getLocaleRelativeUrlList', () => { }); it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: ignore]', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { experimental: { i18n: { @@ -603,7 +572,7 @@ describe('getLocaleRelativeUrlList', () => { }; // directory format assert.deepEqual( - getLocaleRelativeUrlList({ + relativeUrlList({ locale: 'en', base: '/blog/', ...config.experimental.i18n, @@ -615,10 +584,6 @@ describe('getLocaleRelativeUrlList', () => { }); it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: never, routingStrategy: pathname-prefix-always]', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { i18n: { defaultLocale: 'en', @@ -630,23 +595,19 @@ describe('getLocaleRelativeUrlList', () => { }; // directory format assert.deepEqual( - getLocaleRelativeUrlList({ + relativeUrlList({ locale: 'en', base: '/blog', ...config.i18n, trailingSlash: 'never', format: 'directory', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), ['/blog/en', '/blog/en-us', '/blog/es'], ); }); it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: never, routingStrategy: pathname-prefix-always-no-redirect]', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { i18n: { defaultLocale: 'en', @@ -659,13 +620,13 @@ describe('getLocaleRelativeUrlList', () => { }; // directory format assert.deepEqual( - getLocaleRelativeUrlList({ + relativeUrlList({ locale: 'en', base: '/blog', ...config.i18n, trailingSlash: 'never', format: 'directory', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), ['/blog/en', '/blog/en-us', '/blog/es'], ); @@ -675,10 +636,6 @@ describe('getLocaleRelativeUrlList', () => { describe('getLocaleAbsoluteUrl', () => { describe('with [prefix-other-locales]', () => { it('should correctly return the URL with the base', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { base: '/blog', i18n: { @@ -701,7 +658,7 @@ describe('getLocaleAbsoluteUrl', () => { // directory format assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en', base: '/blog/', trailingSlash: 'always', @@ -712,7 +669,7 @@ describe('getLocaleAbsoluteUrl', () => { 'https://example.com/blog/', ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'es', base: '/blog/', ...config.i18n, @@ -724,7 +681,7 @@ describe('getLocaleAbsoluteUrl', () => { ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'es', base: '/blog/', ...config.i18n, @@ -738,7 +695,7 @@ describe('getLocaleAbsoluteUrl', () => { assert.throws( () => - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'ff', base: '/blog/', ...config.i18n, @@ -755,7 +712,7 @@ describe('getLocaleAbsoluteUrl', () => { // file format assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en', base: '/blog/', ...config.i18n, @@ -766,7 +723,7 @@ describe('getLocaleAbsoluteUrl', () => { 'https://example.com/blog/', ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'es', base: '/blog/', ...config.i18n, @@ -778,7 +735,7 @@ describe('getLocaleAbsoluteUrl', () => { ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'it-VA', base: '/blog/', ...config.i18n, @@ -790,7 +747,7 @@ describe('getLocaleAbsoluteUrl', () => { ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'es', base: '/blog/', ...config.i18n, @@ -803,7 +760,7 @@ describe('getLocaleAbsoluteUrl', () => { ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'es', base: '/blog/', prependWith: 'some-name', @@ -819,7 +776,7 @@ describe('getLocaleAbsoluteUrl', () => { // en isn't mapped to a domain assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en', base: '/blog/', prependWith: 'some-name', @@ -836,10 +793,6 @@ describe('getLocaleAbsoluteUrl', () => { }); describe('with [prefix-always]', () => { it('should correctly return the URL with the base', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { base: '/blog', i18n: { @@ -856,59 +809,59 @@ describe('getLocaleAbsoluteUrl', () => { // directory format assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en', base: '/blog/', trailingSlash: 'always', format: 'directory', site: 'https://example.com', ...config.i18n, - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), 'https://example.com/blog/en/', ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'es', base: '/blog/', ...config.i18n, trailingSlash: 'always', format: 'directory', site: 'https://example.com', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), 'https://example.com/blog/es/', ); // file format assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en', base: '/blog/', ...config.i18n, trailingSlash: 'always', format: 'file', site: 'https://example.com', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), 'https://example.com/blog/en/', ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'es', base: '/blog/', ...config.i18n, trailingSlash: 'always', format: 'file', site: 'https://example.com', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), 'https://example.com/blog/es/', ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'es', base: '/blog/', ...config.i18n, @@ -916,13 +869,13 @@ describe('getLocaleAbsoluteUrl', () => { format: 'file', site: 'https://example.com', isBuild: true, - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), 'https://es.example.com/blog/', ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'es', base: '/blog/', prependWith: 'some-name', @@ -932,16 +885,12 @@ describe('getLocaleAbsoluteUrl', () => { site: 'https://example.com', path: 'first-post', isBuild: true, - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), 'https://es.example.com/blog/some-name/first-post/', ); }); it('should correctly return the URL without base', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { i18n: { defaultLocale: 'en', @@ -953,36 +902,32 @@ describe('getLocaleAbsoluteUrl', () => { }; assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en', base: '/', ...config.i18n, trailingSlash: 'always', format: 'directory', site: 'https://example.com', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), 'https://example.com/en/', ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'es', base: '/', ...config.i18n, trailingSlash: 'always', format: 'directory', site: 'https://example.com', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), 'https://example.com/es/', ); }); it('should correctly handle the trailing slash', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { i18n: { defaultLocale: 'en', @@ -994,71 +939,71 @@ describe('getLocaleAbsoluteUrl', () => { }; // directory format assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en', base: '/blog', ...config.i18n, trailingSlash: 'never', format: 'directory', site: 'https://example.com', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), 'https://example.com/blog/en', ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'es', base: '/blog/', ...config.i18n, trailingSlash: 'always', format: 'directory', site: 'https://example.com', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), 'https://example.com/blog/es/', ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en', base: '/blog/', ...config.i18n, trailingSlash: 'ignore', format: 'directory', site: 'https://example.com', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), 'https://example.com/blog/en/', ); // directory file assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en', base: '/blog', ...config.i18n, trailingSlash: 'never', format: 'file', site: 'https://example.com', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), 'https://example.com/blog/en', ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'es', base: '/blog/', ...config.i18n, trailingSlash: 'always', format: 'file', site: 'https://example.com', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), 'https://example.com/blog/es/', ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en', // ignore + file => no trailing slash base: '/blog', @@ -1066,17 +1011,13 @@ describe('getLocaleAbsoluteUrl', () => { trailingSlash: 'ignore', format: 'file', site: 'https://example.com', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), 'https://example.com/blog/en', ); }); it('should normalize locales', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { base: '/blog', experimental: { @@ -1089,7 +1030,7 @@ describe('getLocaleAbsoluteUrl', () => { }; assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en_US', base: '/blog/', ...config.experimental.i18n, @@ -1100,7 +1041,7 @@ describe('getLocaleAbsoluteUrl', () => { ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en_AU', base: '/blog/', ...config.experimental.i18n, @@ -1111,7 +1052,7 @@ describe('getLocaleAbsoluteUrl', () => { ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en_US', base: '/blog/', ...config.experimental.i18n, @@ -1124,10 +1065,6 @@ describe('getLocaleAbsoluteUrl', () => { }); it('should return the default locale when routing strategy is [pathname-prefix-always]', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { base: '/blog', i18n: { @@ -1141,62 +1078,58 @@ describe('getLocaleAbsoluteUrl', () => { // directory format assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en', base: '/blog/', trailingSlash: 'always', site: 'https://example.com', format: 'directory', ...config.i18n, - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), 'https://example.com/blog/en/', ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'es', base: '/blog/', ...config.i18n, site: 'https://example.com', trailingSlash: 'always', format: 'directory', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), 'https://example.com/blog/es/', ); // file format assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en', base: '/blog/', ...config.i18n, site: 'https://example.com', trailingSlash: 'always', format: 'file', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), 'https://example.com/blog/en/', ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'es', base: '/blog/', ...config.i18n, site: 'https://example.com', trailingSlash: 'always', format: 'file', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), 'https://example.com/blog/es/', ); }); it('should return the default locale when routing strategy is [pathname-prefix-always-no-redirect]', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { base: '/blog', i18n: { @@ -1211,52 +1144,52 @@ describe('getLocaleAbsoluteUrl', () => { // directory format assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en', base: '/blog/', trailingSlash: 'always', site: 'https://example.com', format: 'directory', ...config.i18n, - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), 'https://example.com/blog/en/', ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'es', base: '/blog/', ...config.i18n, site: 'https://example.com', trailingSlash: 'always', format: 'directory', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), 'https://example.com/blog/es/', ); // file format assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en', base: '/blog/', ...config.i18n, site: 'https://example.com', trailingSlash: 'always', format: 'file', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), 'https://example.com/blog/en/', ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'es', base: '/blog/', ...config.i18n, site: 'https://example.com', trailingSlash: 'always', format: 'file', - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), }), 'https://example.com/blog/es/', ); @@ -1264,10 +1197,6 @@ describe('getLocaleAbsoluteUrl', () => { }); describe('with [prefix-other-locales]', () => { it('should correctly return the URL without base', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { experimental: { i18n: { @@ -1286,7 +1215,7 @@ describe('getLocaleAbsoluteUrl', () => { }; assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en', base: '/', ...config.experimental.i18n, @@ -1297,7 +1226,7 @@ describe('getLocaleAbsoluteUrl', () => { 'https://example.com/', ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'es', base: '/', ...config.experimental.i18n, @@ -1308,7 +1237,7 @@ describe('getLocaleAbsoluteUrl', () => { 'https://example.com/es/', ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'it-VA', base: '/', ...config.experimental.i18n, @@ -1319,7 +1248,7 @@ describe('getLocaleAbsoluteUrl', () => { 'https://example.com/italiano/', ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en', base: '/', ...config.experimental.i18n, @@ -1330,7 +1259,7 @@ describe('getLocaleAbsoluteUrl', () => { 'https://example.com', ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'es', base: '/', ...config.experimental.i18n, @@ -1341,7 +1270,7 @@ describe('getLocaleAbsoluteUrl', () => { 'https://example.com/es', ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'it-VA', base: '/', ...config.experimental.i18n, @@ -1354,10 +1283,6 @@ describe('getLocaleAbsoluteUrl', () => { }); it('should correctly handle the trailing slash', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { experimental: { i18n: { @@ -1369,7 +1294,7 @@ describe('getLocaleAbsoluteUrl', () => { }; // directory format assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en', base: '/blog', ...config.experimental.i18n, @@ -1380,7 +1305,7 @@ describe('getLocaleAbsoluteUrl', () => { 'https://example.com/blog', ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'es', base: '/blog/', ...config.experimental.i18n, @@ -1392,7 +1317,7 @@ describe('getLocaleAbsoluteUrl', () => { ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en', base: '/blog/', ...config.experimental.i18n, @@ -1405,7 +1330,7 @@ describe('getLocaleAbsoluteUrl', () => { // directory file assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en', base: '/blog', ...config.experimental.i18n, @@ -1416,7 +1341,7 @@ describe('getLocaleAbsoluteUrl', () => { 'https://example.com/blog', ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'es', base: '/blog/', ...config.experimental.i18n, @@ -1428,7 +1353,7 @@ describe('getLocaleAbsoluteUrl', () => { ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en', // ignore + file => no trailing slash base: '/blog', @@ -1442,10 +1367,6 @@ describe('getLocaleAbsoluteUrl', () => { }); it('should normalize locales', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { base: '/blog', experimental: { @@ -1458,7 +1379,7 @@ describe('getLocaleAbsoluteUrl', () => { }; assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en_US', base: '/blog/', ...config.experimental.i18n, @@ -1469,7 +1390,7 @@ describe('getLocaleAbsoluteUrl', () => { ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en_AU', base: '/blog/', ...config.experimental.i18n, @@ -1480,7 +1401,7 @@ describe('getLocaleAbsoluteUrl', () => { ); assert.equal( - getLocaleAbsoluteUrl({ + absoluteUrl({ locale: 'en_US', base: '/blog/', ...config.experimental.i18n, @@ -1496,11 +1417,7 @@ describe('getLocaleAbsoluteUrl', () => { describe('getLocaleAbsoluteUrlList', () => { it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: never]', async () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ - const config = await validateConfig( + const config: AstroConfig = await validateConfig( { trailingSlash: 'never', format: 'directory', @@ -1520,10 +1437,11 @@ describe('getLocaleAbsoluteUrlList', () => { }, }, process.cwd(), + 'build', ); // directory format assert.deepEqual( - getLocaleAbsoluteUrlList({ + absoluteUrlList({ locale: 'en', ...config, ...config.i18n, @@ -1539,11 +1457,7 @@ describe('getLocaleAbsoluteUrlList', () => { }); it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: always]', async () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ - const config = await validateConfig( + const config: AstroConfig = await validateConfig( { trailingSlash: 'always', format: 'directory', @@ -1555,10 +1469,11 @@ describe('getLocaleAbsoluteUrlList', () => { }, }, process.cwd(), + 'build', ); // directory format assert.deepEqual( - getLocaleAbsoluteUrlList({ + absoluteUrlList({ locale: 'en', ...config, ...config.i18n, @@ -1572,11 +1487,7 @@ describe('getLocaleAbsoluteUrlList', () => { }); it('should retrieve the correct list of base URL with locales and path [format: directory, trailingSlash: always]', async () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ - const config = await validateConfig( + const config: AstroConfig = await validateConfig( { format: 'directory', site: 'https://example.com/', @@ -1590,15 +1501,16 @@ describe('getLocaleAbsoluteUrlList', () => { }, }, process.cwd(), + 'build', ); // directory format assert.deepEqual( - getLocaleAbsoluteUrlList({ + absoluteUrlList({ locale: 'en', path: 'download', ...config, - ...config.i18n, - strategy: toRoutingStrategy(config.i18n.routing, {}), + ...config.i18n!, + strategy: routingStrategy(config.i18n!.routing), }), [ 'https://example.com/en/download/', @@ -1609,11 +1521,7 @@ describe('getLocaleAbsoluteUrlList', () => { }); it('should retrieve the correct list of base URL with locales and path [format: directory, trailingSlash: always, domains]', async () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ - const config = await validateConfig( + const config: AstroConfig = await validateConfig( { format: 'directory', output: 'server', @@ -1631,15 +1539,16 @@ describe('getLocaleAbsoluteUrlList', () => { }, }, process.cwd(), + 'build', ); // directory format assert.deepEqual( - getLocaleAbsoluteUrlList({ + absoluteUrlList({ locale: 'en', path: 'download', ...config, - ...config.i18n, - strategy: toRoutingStrategy(config.i18n.routing, {}), + ...config.i18n!, + strategy: routingStrategy(config.i18n!.routing), isBuild: true, }), [ @@ -1651,10 +1560,6 @@ describe('getLocaleAbsoluteUrlList', () => { }); it('should retrieve the correct list of base URL with locales [format: file, trailingSlash: always]', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { i18n: { defaultLocale: 'en', @@ -1671,7 +1576,7 @@ describe('getLocaleAbsoluteUrlList', () => { }; // directory format assert.deepEqual( - getLocaleAbsoluteUrlList({ + absoluteUrlList({ locale: 'en', base: '/blog/', ...config.i18n, @@ -1689,10 +1594,6 @@ describe('getLocaleAbsoluteUrlList', () => { }); it('should retrieve the correct list of base URL with locales [format: file, trailingSlash: never]', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { experimental: { i18n: { @@ -1703,7 +1604,7 @@ describe('getLocaleAbsoluteUrlList', () => { }; // directory format assert.deepEqual( - getLocaleAbsoluteUrlList({ + absoluteUrlList({ locale: 'en', base: '/blog', ...config.experimental.i18n, @@ -1716,10 +1617,6 @@ describe('getLocaleAbsoluteUrlList', () => { }); it('should retrieve the correct list of base URL with locales [format: file, trailingSlash: ignore]', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { experimental: { i18n: { @@ -1730,7 +1627,7 @@ describe('getLocaleAbsoluteUrlList', () => { }; // directory format assert.deepEqual( - getLocaleAbsoluteUrlList({ + absoluteUrlList({ locale: 'en', base: '/blog', ...config.experimental.i18n, @@ -1743,10 +1640,6 @@ describe('getLocaleAbsoluteUrlList', () => { }); it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: ignore]', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { experimental: { i18n: { @@ -1757,7 +1650,7 @@ describe('getLocaleAbsoluteUrlList', () => { }; // directory format assert.deepEqual( - getLocaleAbsoluteUrlList({ + absoluteUrlList({ locale: 'en', base: '/blog/', ...config.experimental.i18n, @@ -1774,10 +1667,6 @@ describe('getLocaleAbsoluteUrlList', () => { }); it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: ignore, routingStrategy: pathname-prefix-always]', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { i18n: { defaultLocale: 'en', @@ -1789,11 +1678,11 @@ describe('getLocaleAbsoluteUrlList', () => { }; // directory format assert.deepEqual( - getLocaleAbsoluteUrlList({ + absoluteUrlList({ locale: 'en', base: '/blog/', ...config.i18n, - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), trailingSlash: 'ignore', format: 'directory', site: 'https://example.com', @@ -1807,10 +1696,6 @@ describe('getLocaleAbsoluteUrlList', () => { }); it('should retrieve the correct list of base URL with locales [format: directory, trailingSlash: ignore, routingStrategy: pathname-prefix-always-no-redirect]', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { i18n: { defaultLocale: 'en', @@ -1823,11 +1708,11 @@ describe('getLocaleAbsoluteUrlList', () => { }; // directory format assert.deepEqual( - getLocaleAbsoluteUrlList({ + absoluteUrlList({ locale: 'en', base: '/blog/', ...config.i18n, - strategy: toRoutingStrategy(config.i18n.routing, {}), + strategy: routingStrategy(config.i18n.routing), trailingSlash: 'ignore', format: 'directory', site: 'https://example.com', @@ -1841,10 +1726,6 @@ describe('getLocaleAbsoluteUrlList', () => { }); it('should retrieve the correct list of base URLs, swapped with the correct domain', () => { - /** - * - * @type {import("../../../dist/@types").AstroUserConfig} - */ const config = { experimental: { i18n: { @@ -1860,7 +1741,7 @@ describe('getLocaleAbsoluteUrlList', () => { }; // directory format assert.deepEqual( - getLocaleAbsoluteUrlList({ + absoluteUrlList({ base: '/blog/', ...config.experimental.i18n, trailingSlash: 'ignore', diff --git a/packages/astro/test/units/i18n/create-manifest.test.js b/packages/astro/test/units/i18n/create-manifest.test.ts similarity index 93% rename from packages/astro/test/units/i18n/create-manifest.test.js rename to packages/astro/test/units/i18n/create-manifest.test.ts index e800a4309e30..8eac50a2f1ef 100644 --- a/packages/astro/test/units/i18n/create-manifest.test.js +++ b/packages/astro/test/units/i18n/create-manifest.test.ts @@ -1,22 +1,22 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { createI18nFallbackRoutes } from '../../../dist/core/routing/create-manifest.js'; -import { createRouteData } from '../mocks.js'; +import type { AstroConfig } from '../../../dist/types/public/config.js'; +import { createRouteData } from '../mocks.ts'; -const BASE_CONFIG = { +const BASE_CONFIG: Pick = { base: '/', trailingSlash: 'ignore', }; -function makeI18n(overrides = {}) { +function makeI18n(overrides: Record = {}): NonNullable { return { defaultLocale: 'en', locales: ['en', 'es'], routing: {}, domains: {}, ...overrides, - }; + } as NonNullable; } describe('createI18nFallbackRoutes — prefix-other-locales, es → en fallback', () => { diff --git a/packages/astro/test/units/i18n/fallback.test.js b/packages/astro/test/units/i18n/fallback.test.ts similarity index 72% rename from packages/astro/test/units/i18n/fallback.test.js rename to packages/astro/test/units/i18n/fallback.test.ts index 01b91856216a..4119b3137cdf 100644 --- a/packages/astro/test/units/i18n/fallback.test.js +++ b/packages/astro/test/units/i18n/fallback.test.ts @@ -1,12 +1,13 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { computeFallbackRoute } from '../../../dist/i18n/fallback.js'; -import { makeFallbackOptions } from './test-helpers.js'; +import type { FallbackRouteResult } from '../../../dist/i18n/fallback.js'; +import { makeFallbackOptions } from './test-helpers.ts'; describe('computeFallbackRoute', () => { describe('when response status is not 404', () => { it('returns none for 200 (success)', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/es/missing', responseStatus: 200, @@ -19,7 +20,7 @@ describe('computeFallbackRoute', () => { }); it('returns none for 301 (redirect)', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/es/redirect', responseStatus: 301, @@ -32,7 +33,7 @@ describe('computeFallbackRoute', () => { }); it('returns none for 302 (temporary redirect)', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/es/redirect', responseStatus: 302, @@ -45,7 +46,7 @@ describe('computeFallbackRoute', () => { }); it('returns none for 403 (forbidden)', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/es/forbidden', responseStatus: 403, @@ -58,7 +59,7 @@ describe('computeFallbackRoute', () => { }); it('returns none for 500 (server error)', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/es/error', responseStatus: 500, @@ -73,7 +74,7 @@ describe('computeFallbackRoute', () => { describe('when no fallback configured', () => { it('returns none for empty fallback object', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/es/missing', responseStatus: 404, @@ -88,7 +89,7 @@ describe('computeFallbackRoute', () => { describe('when locale not in fallback config', () => { it('returns none if current locale has no fallback', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/pt/missing', responseStatus: 404, @@ -103,7 +104,7 @@ describe('computeFallbackRoute', () => { describe('with fallbackType: redirect', () => { it('returns redirect decision for fallback locale', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/es/missing', responseStatus: 404, @@ -115,11 +116,14 @@ describe('computeFallbackRoute', () => { ); assert.equal(result.type, 'redirect'); - assert.equal(result.pathname, '/en/missing'); + assert.equal( + (result as Extract).pathname, + '/en/missing', + ); }); it('removes default locale prefix for prefix-other-locales strategy', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/es/missing', responseStatus: 404, @@ -132,11 +136,14 @@ describe('computeFallbackRoute', () => { ); assert.equal(result.type, 'redirect'); - assert.equal(result.pathname, '/missing'); // No /en/ prefix + assert.equal( + (result as Extract).pathname, + '/missing', + ); // No /en/ prefix }); it('handles base path correctly', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/new-site/es/missing', responseStatus: 404, @@ -149,11 +156,14 @@ describe('computeFallbackRoute', () => { ); assert.equal(result.type, 'redirect'); - assert.equal(result.pathname, '/new-site/en/missing'); + assert.equal( + (result as Extract).pathname, + '/new-site/en/missing', + ); }); it('handles base path with prefix-other-locales strategy', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/new-site/es/missing', responseStatus: 404, @@ -167,11 +177,14 @@ describe('computeFallbackRoute', () => { ); assert.equal(result.type, 'redirect'); - assert.equal(result.pathname, '/new-site/missing'); + assert.equal( + (result as Extract).pathname, + '/new-site/missing', + ); }); it('handles fallback to non-default locale', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/pt/missing', responseStatus: 404, @@ -184,11 +197,14 @@ describe('computeFallbackRoute', () => { ); assert.equal(result.type, 'redirect'); - assert.equal(result.pathname, '/es/missing'); + assert.equal( + (result as Extract).pathname, + '/es/missing', + ); }); it('only triggers for 404 status, not 3xx', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/es/redirect', responseStatus: 301, @@ -202,7 +218,7 @@ describe('computeFallbackRoute', () => { }); it('triggers for 404 status', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/es/notfound', responseStatus: 404, @@ -216,7 +232,7 @@ describe('computeFallbackRoute', () => { }); it('only triggers for 404 status, not 5xx', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/es/error', responseStatus: 500, @@ -232,7 +248,7 @@ describe('computeFallbackRoute', () => { describe('with fallbackType: rewrite', () => { it('returns rewrite decision for fallback locale', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/es/missing', responseStatus: 404, @@ -244,11 +260,14 @@ describe('computeFallbackRoute', () => { ); assert.equal(result.type, 'rewrite'); - assert.equal(result.pathname, '/en/missing'); + assert.equal( + (result as Extract).pathname, + '/en/missing', + ); }); it('removes default locale prefix for prefix-other-locales strategy', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/es/missing', responseStatus: 404, @@ -261,11 +280,14 @@ describe('computeFallbackRoute', () => { ); assert.equal(result.type, 'rewrite'); - assert.equal(result.pathname, '/missing'); + assert.equal( + (result as Extract).pathname, + '/missing', + ); }); it('handles base path correctly', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/new-site/es/missing', responseStatus: 404, @@ -278,11 +300,14 @@ describe('computeFallbackRoute', () => { ); assert.equal(result.type, 'rewrite'); - assert.equal(result.pathname, '/new-site/en/missing'); + assert.equal( + (result as Extract).pathname, + '/new-site/en/missing', + ); }); it('works with dynamic routes', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/es/blog/my-post', responseStatus: 404, @@ -294,11 +319,14 @@ describe('computeFallbackRoute', () => { ); assert.equal(result.type, 'rewrite'); - assert.equal(result.pathname, '/en/blog/my-post'); + assert.equal( + (result as Extract).pathname, + '/en/blog/my-post', + ); }); it('handles deep nested paths', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/es/blog/2024/01/post', responseStatus: 404, @@ -310,13 +338,16 @@ describe('computeFallbackRoute', () => { ); assert.equal(result.type, 'rewrite'); - assert.equal(result.pathname, '/en/blog/2024/01/post'); + assert.equal( + (result as Extract).pathname, + '/en/blog/2024/01/post', + ); }); }); describe('locale extraction from pathname', () => { it('finds locale in first segment', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/es/page', responseStatus: 404, @@ -330,7 +361,7 @@ describe('computeFallbackRoute', () => { }); it('handles paths without locale gracefully', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/page', responseStatus: 404, @@ -344,7 +375,7 @@ describe('computeFallbackRoute', () => { }); it('handles granular locale configurations (object format)', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/spanish/page', responseStatus: 404, @@ -357,13 +388,16 @@ describe('computeFallbackRoute', () => { ); assert.equal(result.type, 'redirect'); - assert.equal(result.pathname, '/en/page'); + assert.equal( + (result as Extract).pathname, + '/en/page', + ); }); }); describe('edge cases', () => { it('handles root path', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/es/', responseStatus: 404, @@ -375,11 +409,11 @@ describe('computeFallbackRoute', () => { ); assert.equal(result.type, 'redirect'); - assert.equal(result.pathname, '/en/'); + assert.equal((result as Extract).pathname, '/en/'); }); it('handles pathname without trailing slash', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/es', responseStatus: 404, @@ -391,11 +425,11 @@ describe('computeFallbackRoute', () => { ); assert.equal(result.type, 'redirect'); - assert.equal(result.pathname, '/en'); + assert.equal((result as Extract).pathname, '/en'); }); it('preserves trailing content after locale replacement', () => { - const result = computeFallbackRoute( + const result: FallbackRouteResult = computeFallbackRoute( makeFallbackOptions({ pathname: '/es/a/b/c/d', responseStatus: 404, @@ -407,7 +441,10 @@ describe('computeFallbackRoute', () => { ); assert.equal(result.type, 'rewrite'); - assert.equal(result.pathname, '/en/a/b/c/d'); + assert.equal( + (result as Extract).pathname, + '/en/a/b/c/d', + ); }); }); }); diff --git a/packages/astro/test/units/i18n/i18n-app.test.js b/packages/astro/test/units/i18n/i18n-app.test.ts similarity index 79% rename from packages/astro/test/units/i18n/i18n-app.test.js rename to packages/astro/test/units/i18n/i18n-app.test.ts index 34df87a42535..8e887dd2b045 100644 --- a/packages/astro/test/units/i18n/i18n-app.test.js +++ b/packages/astro/test/units/i18n/i18n-app.test.ts @@ -1,30 +1,32 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { createComponent, render } from '../../../dist/runtime/server/index.js'; +import type { RoutingStrategies } from '../../../dist/core/app/common.js'; import { createI18nMiddleware } from '../../../dist/i18n/middleware.js'; -import { createTestApp, createPage } from '../mocks.js'; -import { dynamicPart, staticPart } from '../routing/test-helpers.js'; - -/** - * @param {Partial<{ - * defaultLocale: string, - * locales: import('../../../src/types/public/config.js').Locales, - * strategy: string, - * fallbackType: 'redirect' | 'rewrite', - * fallback: Record, - * }>} [overrides] - */ -function makeI18nConfig(overrides = {}) { +import { createComponent, render } from '../../../dist/runtime/server/index.js'; +import type { Locales } from '../../../dist/types/public/config.js'; +import { createPage, createTestApp } from '../mocks.ts'; +import { dynamicPart, spreadPart, staticPart } from '../routing/test-helpers.ts'; + +interface I18nConfigOverrides { + defaultLocale?: string; + locales?: Locales; + strategy?: RoutingStrategies; + fallbackType?: 'redirect' | 'rewrite'; + fallback?: Record; + domains?: Record; + domainLookupTable?: Record; +} + +function makeI18nConfig(overrides: I18nConfigOverrides = {}) { return { defaultLocale: overrides.defaultLocale ?? 'en', - locales: overrides.locales ?? ['en', 'fr', 'es'], - strategy: overrides.strategy ?? 'pathname-prefix-always', - fallbackType: overrides.fallbackType ?? 'rewrite', - fallback: 'fallback' in overrides ? overrides.fallback : {}, - domains: {}, - domainLookupTable: {}, + locales: overrides.locales ?? (['en', 'fr', 'es'] as Locales), + strategy: overrides.strategy ?? ('pathname-prefix-always' as RoutingStrategies), + fallbackType: overrides.fallbackType ?? ('rewrite' as const), + fallback: 'fallback' in overrides ? overrides.fallback : ({} as Record), + domains: overrides.domains ?? ({} as Record), + domainLookupTable: overrides.domainLookupTable ?? ({} as Record), }; } @@ -38,7 +40,7 @@ const notFoundPage = createComponent(() => { }); /** Shorthand for a locale-prefixed catch-all route */ -function localeCatchAll(locale) { +function localeCatchAll(locale: string) { return createPage(localePage, { route: `/${locale}/[...slug]`, segments: [[staticPart(locale)], [dynamicPart('slug')]], @@ -280,6 +282,90 @@ describe('i18n via App - domains-prefix-always', () => { }); }); +describe('i18n via App - domains-prefix-always with trailingSlash: never', () => { + const i18n = makeI18nConfig({ + strategy: 'domains-prefix-always', + locales: ['fi', 'en'], + defaultLocale: 'fi', + domainLookupTable: { + 'https://example.com': 'en', + 'https://example.fi': 'fi', + }, + domains: { + en: 'https://example.com', + fi: 'https://example.fi', + }, + }); + + const middleware = createI18nMiddleware(i18n, '/', 'never', 'directory'); + + /** Like localeCatchAll but with spread param and trailingSlash: never */ + function localeSpreadCatchAll(locale: string) { + return createPage(localePage, { + route: `/${locale}/[...slug]`, + segments: [[staticPart(locale)], [spreadPart('slug')]], + pathname: undefined, + trailingSlash: 'never', + }); + } + + function createDomainApp() { + return createTestApp([localeSpreadCatchAll('fi'), localeSpreadCatchAll('en')], { + i18n, + trailingSlash: 'never', + middleware: () => ({ onRequest: middleware }), + }); + } + + it('root path of en domain-mapped locale returns 200 (not 404)', async () => { + const app = createDomainApp(); + const res = await app.render( + new Request('https://example.com/', { + headers: { 'X-Forwarded-Host': 'example.com', 'X-Forwarded-Proto': 'https' }, + }), + ); + assert.equal(res.status, 200); + const $ = cheerio.load(await res.text()); + assert.equal($('#locale').text(), 'en'); + }); + + it('non-root path of en domain-mapped locale returns 200', async () => { + const app = createDomainApp(); + const res = await app.render( + new Request('https://example.com/about', { + headers: { 'X-Forwarded-Host': 'example.com', 'X-Forwarded-Proto': 'https' }, + }), + ); + assert.equal(res.status, 200); + const $ = cheerio.load(await res.text()); + assert.equal($('#locale').text(), 'en'); + }); + + it('root path of fi domain-mapped locale returns 200 (not 404)', async () => { + const app = createDomainApp(); + const res = await app.render( + new Request('https://example.fi/', { + headers: { 'X-Forwarded-Host': 'example.fi', 'X-Forwarded-Proto': 'https' }, + }), + ); + assert.equal(res.status, 200); + const $ = cheerio.load(await res.text()); + assert.equal($('#locale').text(), 'fi'); + }); + + it('non-root path of fi domain-mapped locale returns 200', async () => { + const app = createDomainApp(); + const res = await app.render( + new Request('https://example.fi/about', { + headers: { 'X-Forwarded-Host': 'example.fi', 'X-Forwarded-Proto': 'https' }, + }), + ); + assert.equal(res.status, 200); + const $ = cheerio.load(await res.text()); + assert.equal($('#locale').text(), 'fi'); + }); +}); + describe('i18n via App - domains-prefix-other-locales', () => { const i18n = makeI18nConfig({ strategy: 'domains-prefix-other-locales', diff --git a/packages/astro/test/units/i18n/i18n-middleware.test.js b/packages/astro/test/units/i18n/i18n-middleware.test.ts similarity index 73% rename from packages/astro/test/units/i18n/i18n-middleware.test.js rename to packages/astro/test/units/i18n/i18n-middleware.test.ts index a9ead1e47778..d8ff04c0b40e 100644 --- a/packages/astro/test/units/i18n/i18n-middleware.test.js +++ b/packages/astro/test/units/i18n/i18n-middleware.test.ts @@ -1,63 +1,75 @@ -// @ts-check import assert from 'node:assert/strict'; import { beforeEach, describe, it } from 'node:test'; +import type { MiddlewareHandler } from 'astro'; +import type { RoutingStrategies } from '../../../dist/core/app/common.js'; +import type { Locales } from '../../../dist/types/public/config.js'; import { createI18nMiddleware } from '../../../dist/i18n/middleware.js'; -import { createMockAPIContext } from '../mocks.js'; +import { createMockAPIContext } from '../mocks.ts'; /** * Creates a "page" response that mimics what the render pipeline returns. * The `X-Astro-Route-Type: page` header is what the i18n middleware reads * to decide whether to apply routing logic. - * - * @param {string} body - * @param {number} [status] - * @param {Record} [extraHeaders] */ -function makePageResponse(body, status = 200, extraHeaders = {}) { +function makePageResponse( + body: string, + status = 200, + extraHeaders: Record = {}, +): Response { return new Response(body, { status, headers: { 'X-Astro-Route-Type': 'page', ...extraHeaders }, }); } +interface I18nManifestOverrides { + defaultLocale?: string; + locales?: Locales; + strategy?: RoutingStrategies; + fallbackType?: 'redirect' | 'rewrite'; + fallback?: Record; + domainLookupTable?: Record; + domains?: Record; +} + /** * Creates a minimal i18n manifest. - * @param {Partial<{ - * defaultLocale: string, - * locales: import('../../../src/types/public/config.js').Locales, - * strategy: import('../../../dist/core/app/common.js').RoutingStrategies, - * fallbackType: 'redirect' | 'rewrite', - * fallback: Record, - * domainLookupTable: Record, - * domains: Record, - * }>} [overrides] */ -function makeI18nManifest(overrides = {}) { +function makeI18nManifest(overrides: I18nManifestOverrides = {}) { return { defaultLocale: overrides.defaultLocale ?? 'en', locales: overrides.locales ?? ['en', 'it'], - strategy: overrides.strategy ?? 'pathname-prefix-always', - fallbackType: overrides.fallbackType ?? 'rewrite', + strategy: overrides.strategy ?? ('pathname-prefix-always' as RoutingStrategies), + fallbackType: overrides.fallbackType ?? ('rewrite' as const), fallback: overrides.fallback ?? {}, domains: overrides.domains ?? {}, domainLookupTable: overrides.domainLookupTable ?? {}, }; } +/** Calls the handler and asserts the result is a Response (not void). */ +async function callHandler( + handler: MiddlewareHandler, + ...args: Parameters +): Promise { + const result = await handler(...args); + assert.ok(result instanceof Response, 'expected handler to return a Response'); + return result; +} + describe('createI18nMiddleware', () => { it('returns a passthrough handler when i18n config is undefined', async () => { const handler = createI18nMiddleware(undefined, '/', 'ignore', 'directory'); const ctx = createMockAPIContext({ url: 'http://localhost/anything' }); const pageResponse = makePageResponse('original'); - const result = await handler(ctx, async () => pageResponse); + const result = await callHandler(handler, ctx, async () => pageResponse); assert.equal(result, pageResponse, 'should return the exact same response object'); }); describe('pathname-prefix-always strategy', () => { - /** @type {import('astro').MiddlewareHandler} */ - let handler; + let handler: MiddlewareHandler; beforeEach(() => { handler = createI18nMiddleware( @@ -72,7 +84,7 @@ describe('createI18nMiddleware', () => { const ctx = createMockAPIContext({ url: 'http://localhost/blog' }); const next = async () => makePageResponse('Blog should not render'); - const result = await handler(ctx, next); + const result = await callHandler(handler, ctx, next); assert.equal(result.status, 404); assert.equal(result.body, null, 'Body should be null so the App reroutes to the 404 page'); @@ -82,7 +94,7 @@ describe('createI18nMiddleware', () => { const ctx = createMockAPIContext({ url: 'http://localhost/en/start' }); const next = async () => makePageResponse('en page'); - const result = await handler(ctx, next); + const result = await callHandler(handler, ctx, next); assert.equal(result.status, 200); assert.equal(await result.text(), 'en page'); @@ -92,7 +104,7 @@ describe('createI18nMiddleware', () => { const ctx = createMockAPIContext({ url: 'http://localhost/' }); const next = async () => makePageResponse('root'); - const result = await handler(ctx, next); + const result = await callHandler(handler, ctx, next); assert.equal(result.status, 302); assert.ok( @@ -103,8 +115,7 @@ describe('createI18nMiddleware', () => { }); describe('pathname-prefix-other-locales strategy', () => { - /** @type {import('astro').MiddlewareHandler} */ - let handler; + let handler: MiddlewareHandler; beforeEach(() => { handler = createI18nMiddleware( @@ -119,7 +130,7 @@ describe('createI18nMiddleware', () => { const ctx = createMockAPIContext({ url: 'http://localhost/blog' }); const next = async () => makePageResponse('en blog'); - const result = await handler(ctx, next); + const result = await callHandler(handler, ctx, next); assert.equal(result.status, 200); }); @@ -128,7 +139,7 @@ describe('createI18nMiddleware', () => { const ctx = createMockAPIContext({ url: 'http://localhost/en/blog' }); const next = async () => makePageResponse('should not be visible'); - const result = await handler(ctx, next); + const result = await callHandler(handler, ctx, next); assert.equal(result.status, 404); }); @@ -149,7 +160,7 @@ describe('createI18nMiddleware', () => { const ctx = createMockAPIContext({ url: 'http://localhost/it/start' }); const next = async () => makePageResponse('no it page', 404); - const result = await handler(ctx, next); + const result = await callHandler(handler, ctx, next); assert.equal(result.status, 302); assert.equal(result.headers.get('Location'), '/en/start'); @@ -168,11 +179,11 @@ describe('createI18nMiddleware', () => { ); const ctx = createMockAPIContext({ url: 'http://localhost/it/start', - rewrite: async (path) => new Response(`rewritten to ${path}`, { status: 200 }), - }); + rewrite: async (_path: string) => new Response(`rewritten to ${_path}`, { status: 200 }), + } as any); const next = async () => makePageResponse('no it page', 404); - const result = await handler(ctx, next); + const result = await callHandler(handler, ctx, next); assert.equal(result.status, 200); assert.equal(await result.text(), 'rewritten to /en/start'); @@ -193,7 +204,7 @@ describe('createI18nMiddleware', () => { headers: { 'X-Astro-Route-Type': 'page', 'X-Astro-Reroute': 'no' }, }); - const result = await handler(ctx, async () => pageResponse); + const result = await callHandler(handler, ctx, async () => pageResponse); assert.equal(result, pageResponse, 'should return the exact same response'); }); @@ -205,7 +216,7 @@ describe('createI18nMiddleware', () => { headers: { 'X-Astro-Route-Type': 'endpoint' }, }); - const result = await handler(ctx, async () => endpointResponse); + const result = await callHandler(handler, ctx, async () => endpointResponse); assert.equal(result, endpointResponse, 'should return the exact same response'); }); diff --git a/packages/astro/test/units/i18n/i18n-routing-static.test.js b/packages/astro/test/units/i18n/i18n-routing-static.test.ts similarity index 94% rename from packages/astro/test/units/i18n/i18n-routing-static.test.js rename to packages/astro/test/units/i18n/i18n-routing-static.test.ts index d61276892b9a..09fc9a5ffe02 100644 --- a/packages/astro/test/units/i18n/i18n-routing-static.test.js +++ b/packages/astro/test/units/i18n/i18n-routing-static.test.ts @@ -1,11 +1,17 @@ -// @ts-check import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; +import type { StaticBuildOptions } from '../../../dist/core/build/types.js'; import { renderPath } from '../../../dist/core/build/generate.js'; -import { createMockAstroSource, createRouteData } from '../mocks.js'; -import { createMockPrerenderer, createStaticBuildOptions } from '../build/test-helpers.js'; - -async function renderAndAssertPath(prerenderer, pathname, route, options, expectedPathSuffix) { +import { createMockAstroSource, createRouteData } from '../mocks.ts'; +import { createMockPrerenderer, createStaticBuildOptions } from '../build/test-helpers.ts'; + +async function renderAndAssertPath( + prerenderer: ReturnType, + pathname: string, + route: Parameters[0]['route'], + options: StaticBuildOptions, + expectedPathSuffix: string, +) { const result = await renderPath({ prerenderer, pathname, @@ -22,9 +28,9 @@ async function renderAndAssertPath(prerenderer, pathname, route, options, expect } describe('[SSG] i18n routing — prefix-always', () => { - let options; + let options: StaticBuildOptions; - const pages = { + const pages: Record = { 'src/pages/index.astro': createMockAstroSource('

I am index

'), 'src/pages/404.astro': createMockAstroSource("

Can't find the page you're looking for.

"), 'src/pages/500.astro': createMockAstroSource('

Unexpected error.

'), @@ -105,9 +111,9 @@ describe('[SSG] i18n routing — prefix-always', () => { }); describe('[SSG] i18n routing — prefix-other-locales', () => { - let options; + let options: StaticBuildOptions; - const pages = { + const pages: Record = { 'src/pages/start.astro': createMockAstroSource('

Start

'), 'src/pages/pt/start.astro': createMockAstroSource('

Oi essa e start

'), }; @@ -164,9 +170,9 @@ describe('[SSG] i18n routing — prefix-other-locales', () => { }); describe('[SSG] i18n routing — pathname-prefix-always, no redirect to default locale', () => { - let options; + let options: StaticBuildOptions; - const pages = { + const pages: Record = { 'src/pages/index.astro': createMockAstroSource('

I am index

'), }; @@ -197,9 +203,9 @@ describe('[SSG] i18n routing — pathname-prefix-always, no redirect to default }); describe('[SSG] i18n routing — fallback (it → en, spanish → en)', () => { - let options; + let options: StaticBuildOptions; - const pages = { + const pages: Record = { 'src/pages/start.astro': createMockAstroSource('

Start

'), 'src/pages/pt/start.astro': createMockAstroSource('

Oi essa e start: pt

'), }; @@ -304,9 +310,9 @@ describe('[SSG] i18n routing — fallback (it → en, spanish → en)', () => { }); describe('[SSG] i18n routing — fallback with prefix-always (it → en)', () => { - let options; + let options: StaticBuildOptions; - const pages = { + const pages: Record = { 'src/pages/en/start.astro': createMockAstroSource('

Start

'), }; @@ -348,9 +354,9 @@ describe('[SSG] i18n routing — fallback with prefix-always (it → en)', () => }); describe('[SSG] i18n routing — fallback rewrite with dynamic routes (es → en)', () => { - let options; + let options: StaticBuildOptions; - const pages = { + const pages: Record = { 'src/pages/index.astro': createMockAstroSource('Index'), 'src/pages/test.astro': createMockAstroSource('test'), }; @@ -417,9 +423,9 @@ describe('[SSG] i18n routing — fallback rewrite with dynamic routes (es → en }); describe('[SSG] i18n routing — fallback rewrite with locale-like filenames (de → en)', () => { - let options; + let options: StaticBuildOptions; - const pages = { + const pages: Record = { 'src/pages/index.astro': createMockAstroSource('Index'), 'src/pages/denmark.astro': createMockAstroSource('Denmark'), 'src/pages/norway.astro': createMockAstroSource('Norway'), @@ -479,7 +485,7 @@ describe('[SSG] i18n routing — fallback rewrite with locale-like filenames (de ['/destinations/norway', '/de/destinations/norway', 'Destination: Norway'], ['/trade/denmark', '/de/trade/denmark', 'Trade: Denmark'], ['/trade/norway', '/de/trade/norway', 'Trade: Norway'], - ]) { + ] as const) { it(`renders ${en} (EN)`, async () => { const route = options.routesList.routes.find((r) => r.route === en); assert.ok(route, `expected route ${en}`); @@ -512,9 +518,9 @@ describe('[SSG] i18n routing — fallback rewrite with locale-like filenames (de }); describe('[SSG] i18n routing — page starting with locale-like segment', () => { - let options; + let options: StaticBuildOptions; - const pages = { + const pages: Record = { 'src/pages/endurance.astro': createMockAstroSource('

Endurance

'), }; diff --git a/packages/astro/test/units/i18n/i18n-static-build.test.js b/packages/astro/test/units/i18n/i18n-static-build.test.ts similarity index 96% rename from packages/astro/test/units/i18n/i18n-static-build.test.js rename to packages/astro/test/units/i18n/i18n-static-build.test.ts index 8f5dd3ec4a01..7f1ebd110989 100644 --- a/packages/astro/test/units/i18n/i18n-static-build.test.js +++ b/packages/astro/test/units/i18n/i18n-static-build.test.ts @@ -1,15 +1,14 @@ -// @ts-check - import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; +import type { StaticBuildOptions } from '../../../dist/core/build/types.js'; import { renderPath } from '../../../dist/core/build/generate.js'; -import { createMockPrerenderer, createStaticBuildOptions } from '../build/test-helpers.js'; -import { createMockAstroSource, createRouteData } from '../mocks.js'; +import { createMockPrerenderer, createStaticBuildOptions } from '../build/test-helpers.ts'; +import { createMockAstroSource, createRouteData } from '../mocks.ts'; // Page sources — mirrors the structure of the deleted fixture. // createStaticBuildOptions writes these into a temp directory and derives // routesList from them using the same config, so routes and settings are in sync. -const pages = { +const pages: Record = { 'src/pages/index.astro': createMockAstroSource('

Index

'), 'src/pages/es/test/item1.astro': createMockAstroSource('

Test Item 1 (ES)

'), 'src/pages/test/item1.astro': createMockAstroSource('

Test Item 1 (EN)

'), @@ -26,7 +25,7 @@ const prerenderer = createMockPrerenderer({ // A single shared options object is sufficient — none of these tests inspect the // written files; they only assert on the `result` returned by renderPath(). -let sharedOpts; +let sharedOpts: StaticBuildOptions; describe('i18n double-prefix prevention', () => { before(async () => { diff --git a/packages/astro/test/units/i18n/i18n-utils.test.js b/packages/astro/test/units/i18n/i18n-utils.test.ts similarity index 87% rename from packages/astro/test/units/i18n/i18n-utils.test.js rename to packages/astro/test/units/i18n/i18n-utils.test.ts index ee31a1531061..852cccae4fc8 100644 --- a/packages/astro/test/units/i18n/i18n-utils.test.js +++ b/packages/astro/test/units/i18n/i18n-utils.test.ts @@ -1,4 +1,3 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { @@ -42,17 +41,17 @@ describe('computeCurrentLocale', () => { }); it('handles object locales with path', () => { - const locales = [{ path: 'spanish', codes: ['es', 'es-ES'] }, 'en']; + const locales = [{ path: 'spanish', codes: ['es', 'es-ES'] as [string, ...string[]] }, 'en']; assert.equal(computeCurrentLocale('/spanish/about', locales, 'en'), 'es'); }); it('handles object locales with codes matching segment', () => { - const locales = [{ path: 'spanish', codes: ['es', 'es-ES'] }, 'en']; + const locales = [{ path: 'spanish', codes: ['es', 'es-ES'] as [string, ...string[]] }, 'en']; assert.equal(computeCurrentLocale('/es/about', locales, 'en'), 'es'); }); it('returns first code for object locale default', () => { - const locales = [{ path: 'english', codes: ['en', 'en-US'] }, 'fr']; + const locales = [{ path: 'english', codes: ['en', 'en-US'] as [string, ...string[]] }, 'fr']; assert.equal(computeCurrentLocale('/about', locales, 'english'), 'en'); }); }); @@ -104,7 +103,7 @@ describe('getPathByLocale', () => { }); it('returns the path for object locales', () => { - const locales = [{ path: 'spanish', codes: ['es', 'es-ES'] }, 'en']; + const locales = [{ path: 'spanish', codes: ['es', 'es-ES'] as [string, ...string[]] }, 'en']; assert.equal(getPathByLocale('es', locales), 'spanish'); }); @@ -119,14 +118,14 @@ describe('getLocaleByPath', () => { }); it('returns the first code for object locales', () => { - const locales = [{ path: 'spanish', codes: ['es', 'es-ES'] }, 'en']; + const locales = [{ path: 'spanish', codes: ['es', 'es-ES'] as [string, ...string[]] }, 'en']; assert.equal(getLocaleByPath('spanish', locales), 'es'); }); }); describe('getAllCodes', () => { it('returns all codes from string and object locales', () => { - const locales = ['en', { path: 'spanish', codes: ['es', 'es-ES'] }]; + const locales = ['en', { path: 'spanish', codes: ['es', 'es-ES'] as [string, ...string[]] }]; assert.deepEqual(getAllCodes(locales), ['en', 'es', 'es-ES']); }); @@ -137,14 +136,14 @@ describe('getAllCodes', () => { describe('toCodes', () => { it('returns first code per locale entry', () => { - const locales = ['en', { path: 'spanish', codes: ['es', 'es-ES'] }]; + const locales = ['en', { path: 'spanish', codes: ['es', 'es-ES'] as [string, ...string[]] }]; assert.deepEqual(toCodes(locales), ['en', 'es']); }); }); describe('toPaths', () => { it('returns path strings for all locales', () => { - const locales = ['en', { path: 'spanish', codes: ['es'] }]; + const locales = ['en', { path: 'spanish', codes: ['es'] as [string, ...string[]] }]; assert.deepEqual(toPaths(locales), ['en', 'spanish']); }); }); diff --git a/packages/astro/test/units/i18n/manual-middleware.test.js b/packages/astro/test/units/i18n/manual-middleware.test.ts similarity index 88% rename from packages/astro/test/units/i18n/manual-middleware.test.js rename to packages/astro/test/units/i18n/manual-middleware.test.ts index 10e7ce163bd2..8c2130c1dcf6 100644 --- a/packages/astro/test/units/i18n/manual-middleware.test.js +++ b/packages/astro/test/units/i18n/manual-middleware.test.ts @@ -1,8 +1,9 @@ import * as assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { requestHasLocale, redirectToDefaultLocale, notFound } from '../../../dist/i18n/index.js'; -import { createManualRoutingContext, createMiddlewarePayload } from './test-helpers.js'; -import { createMockNext } from '../test-utils.js'; +import type { Locales } from '../../../dist/types/public/config.js'; +import { createManualRoutingContext, createMiddlewarePayload } from './test-helpers.ts'; +import { createMockNext } from '../test-utils.ts'; describe('Custom Middleware with Allowlist Pattern', () => { describe('allowlist bypasses i18n routing', () => { @@ -12,13 +13,13 @@ describe('Custom Middleware with Allowlist Pattern', () => { const next = createMockNext(new Response('Help page')); // Middleware logic: if allowlist matches, call next() - let response; + let response: Response | undefined; if (allowList.has(context.url.pathname)) { response = await next(); } assert.ok(next.called); - assert.equal(await response.text(), 'Help page'); + assert.equal(await response!.text(), 'Help page'); }); it('should allow /about if in allowlist', async () => { @@ -26,13 +27,13 @@ describe('Custom Middleware with Allowlist Pattern', () => { const context = createManualRoutingContext({ pathname: '/about' }); const next = createMockNext(new Response('About page')); - let response; + let response: Response | undefined; if (allowList.has(context.url.pathname)) { response = await next(); } assert.ok(next.called); - assert.equal(await response.text(), 'About page'); + assert.equal(await response!.text(), 'About page'); }); it('should not call next() for non-allowlisted paths', async () => { @@ -40,7 +41,7 @@ describe('Custom Middleware with Allowlist Pattern', () => { const context = createManualRoutingContext({ pathname: '/blog' }); const next = createMockNext(); - let response = null; + let response: Response | null = null; if (!allowList.has(context.url.pathname)) { // Path not in allowlist, don't call next response = new Response(null, { status: 404 }); @@ -54,27 +55,27 @@ describe('Custom Middleware with Allowlist Pattern', () => { describe('paths with locales proceed to next()', () => { it('should call next() when requestHasLocale returns true', async () => { - const locales = ['en', 'es']; + const locales: Locales = ['en', 'es']; const hasLocale = requestHasLocale(locales); const context = createManualRoutingContext({ pathname: '/en/blog' }); const next = createMockNext(new Response('Blog page')); - let response; + let response: Response | undefined; if (hasLocale(context)) { response = await next(); } assert.ok(next.called); - assert.equal(await response.text(), 'Blog page'); + assert.equal(await response!.text(), 'Blog page'); }); it('should call next() for /spanish with locale object', async () => { - const locales = ['en', { path: 'spanish', codes: ['es'] }]; + const locales: Locales = ['en', { path: 'spanish', codes: ['es'] }]; const hasLocale = requestHasLocale(locales); const context = createManualRoutingContext({ pathname: '/spanish' }); const next = createMockNext(new Response('Spanish page')); - let response = null; + let response: Response | null = null; if (hasLocale(context)) { response = await next(); } @@ -84,12 +85,12 @@ describe('Custom Middleware with Allowlist Pattern', () => { }); it('should not call next() for paths without locale', async () => { - const locales = ['en', 'es']; + const locales: Locales = ['en', 'es']; const hasLocale = requestHasLocale(locales); const context = createManualRoutingContext({ pathname: '/blog' }); const next = createMockNext(); - let response = null; + let response: Response | null = null; if (hasLocale(context)) { response = await next(); } else { @@ -109,7 +110,7 @@ describe('Custom Middleware with Allowlist Pattern', () => { const context = createManualRoutingContext({ pathname: '/' }); const next = createMockNext(); - let response; + let response: Response; if (context.url.pathname === '/') { response = redirect(context); } else { @@ -134,12 +135,12 @@ describe('Custom Middleware with Allowlist Pattern', () => { describe('unknown paths return 404', () => { it('should return 404 for unknown paths without calling next()', async () => { - const locales = ['en', 'es']; + const locales: Locales = ['en', 'es']; const hasLocale = requestHasLocale(locales); const context = createManualRoutingContext({ pathname: '/unknown' }); const next = createMockNext(); - let response = null; + let response: Response | null = null; if (hasLocale(context)) { response = await next(); } else if (context.url.pathname !== '/') { @@ -152,11 +153,11 @@ describe('Custom Middleware with Allowlist Pattern', () => { }); it('should return 404 for /blog without locale', async () => { - const locales = ['en', 'es']; + const locales: Locales = ['en', 'es']; const hasLocale = requestHasLocale(locales); const context = createManualRoutingContext({ pathname: '/blog' }); - let response = null; + let response: Response | null = null; if (!hasLocale(context) && context.url.pathname !== '/') { response = new Response(null, { status: 404 }); } @@ -173,7 +174,7 @@ describe('Custom Middleware with Allowlist Pattern', () => { const context = createManualRoutingContext({ pathname: '/redirect-me' }); // Middleware logic from fixture - let response = null; + let response: Response | null = null; if (context.url.pathname === '/' || context.url.pathname === '/redirect-me') { response = redirect(context); } @@ -189,13 +190,13 @@ describe('Middleware Flow Control', () => { describe('decision tree execution order', () => { it('should check allowlist first, then locale, then root, then 404', async () => { const allowList = new Set(['/help']); - const locales = ['en', 'es']; + const locales: Locales = ['en', 'es']; const hasLocale = requestHasLocale(locales); const payload = createMiddlewarePayload({ defaultLocale: 'en' }); const redirect = redirectToDefaultLocale(payload); // Test function that mimics the middleware from fixture - async function middleware(pathname) { + async function middleware(pathname: string) { const context = createManualRoutingContext({ pathname }); const next = createMockNext(new Response('Page content')); @@ -240,13 +241,13 @@ describe('Middleware Flow Control', () => { it('should short-circuit on allowlist match', async () => { const allowList = new Set(['/help']); - const locales = ['en', 'es']; + const locales: Locales = ['en', 'es']; const hasLocale = requestHasLocale(locales); const context = createManualRoutingContext({ pathname: '/help' }); const next = createMockNext(new Response('Help page')); // Middleware should return immediately after allowlist check - let response = null; + let response: Response | null = null; if (allowList.has(context.url.pathname)) { response = await next(); } else if (hasLocale(context)) { @@ -259,12 +260,12 @@ describe('Middleware Flow Control', () => { }); it('should short-circuit on locale match', async () => { - const locales = ['en', 'es']; + const locales: Locales = ['en', 'es']; const hasLocale = requestHasLocale(locales); const context = createManualRoutingContext({ pathname: '/en/blog' }); const next = createMockNext(new Response('Blog')); - let response = null; + let response: Response | null = null; if (hasLocale(context)) { response = await next(); } else if (context.url.pathname === '/') { @@ -286,7 +287,7 @@ describe('Middleware Flow Control', () => { let executedNext = false; let executedOther = false; - let response = null; + let response: Response | null = null; if (allowList.has(context.url.pathname)) { executedNext = true; response = await next(); @@ -306,7 +307,7 @@ describe('Middleware Flow Control', () => { const context = createManualRoutingContext({ pathname: '/' }); const next = createMockNext(); - let response; + let response: Response; if (context.url.pathname === '/') { response = redirect(context); // Should return here, not call next() @@ -319,12 +320,12 @@ describe('Middleware Flow Control', () => { }); it('should not call next() when returning 404', async () => { - const locales = ['en', 'es']; + const locales: Locales = ['en', 'es']; const hasLocale = requestHasLocale(locales); const context = createManualRoutingContext({ pathname: '/unknown' }); const next = createMockNext(); - let response = null; + let response: Response | null = null; if (hasLocale(context)) { response = await next(); } else { @@ -340,7 +341,7 @@ describe('Middleware Flow Control', () => { describe('response propagation', () => { it('should propagate response from next() when locale found', async () => { - const locales = ['en', 'es']; + const locales: Locales = ['en', 'es']; const hasLocale = requestHasLocale(locales); const context = createManualRoutingContext({ pathname: '/en/blog' }); const expectedResponse = new Response('Blog content', { @@ -349,13 +350,13 @@ describe('Middleware Flow Control', () => { }); const next = createMockNext(expectedResponse); - let response; + let response: Response | undefined; if (hasLocale(context)) { response = await next(); } assert.equal(response, expectedResponse); - assert.equal(response.headers.get('X-Custom'), 'value'); + assert.equal(response!.headers.get('X-Custom'), 'value'); }); it('should propagate custom response from allowlist route', async () => { @@ -366,13 +367,13 @@ describe('Middleware Flow Control', () => { }); const next = createMockNext(healthResponse); - let response; + let response: Response | undefined; if (allowList.has(context.url.pathname)) { response = await next(); } - assert.equal(response.headers.get('Content-Type'), 'application/json'); - assert.equal(await response.text(), JSON.stringify({ status: 'ok' })); + assert.equal(response!.headers.get('Content-Type'), 'application/json'); + assert.equal(await response!.text(), JSON.stringify({ status: 'ok' })); }); }); }); @@ -382,9 +383,9 @@ describe('Complete Middleware Scenarios', () => { /** * This replicates the exact middleware from the i18n-routing-manual fixture */ - async function fixtureMiddleware(pathname) { + async function fixtureMiddleware(pathname: string): Promise { const allowList = new Set(['/help', '/help/']); - const locales = ['en', 'pt', 'it', { path: 'spanish', codes: ['es', 'es-ar'] }]; + const locales: Locales = ['en', 'pt', 'it', { path: 'spanish', codes: ['es', 'es-ar'] }]; const payload = createMiddlewarePayload({ defaultLocale: 'en', locales, @@ -457,8 +458,8 @@ describe('Complete Middleware Scenarios', () => { }); describe('middleware with base path', () => { - async function middlewareWithBase(pathname, base = '/blog') { - const locales = ['en', 'es']; + async function middlewareWithBase(pathname: string, base = '/blog'): Promise { + const locales: Locales = ['en', 'es']; const payload = createMiddlewarePayload({ base, defaultLocale: 'en', @@ -504,7 +505,7 @@ describe('Complete Middleware Scenarios', () => { const context = createManualRoutingContext({ pathname: '/api/status' }); const next = createMockNext(); - let response; + let response: Response; if (allowList.has(context.url.pathname)) { // Return custom JSON response without calling next() response = new Response(JSON.stringify({ status: 'healthy' }), { @@ -521,12 +522,12 @@ describe('Complete Middleware Scenarios', () => { }); it('should modify response after next() call', async () => { - const locales = ['en']; + const locales: Locales = ['en']; const hasLocale = requestHasLocale(locales); const context = createManualRoutingContext({ pathname: '/en/api' }); const next = createMockNext(new Response('Data')); - let response; + let response: Response | undefined; if (hasLocale(context)) { const originalResponse = await next(); // Add custom header to response from next() @@ -540,7 +541,7 @@ describe('Complete Middleware Scenarios', () => { } assert.ok(next.called); - assert.equal(response.headers.get('X-Custom-Header'), 'added-by-middleware'); + assert.equal(response!.headers.get('X-Custom-Header'), 'added-by-middleware'); }); }); }); diff --git a/packages/astro/test/units/i18n/manual-routing.test.js b/packages/astro/test/units/i18n/manual-routing.test.ts similarity index 96% rename from packages/astro/test/units/i18n/manual-routing.test.js rename to packages/astro/test/units/i18n/manual-routing.test.ts index e8038cd6f2eb..26664b357721 100644 --- a/packages/astro/test/units/i18n/manual-routing.test.js +++ b/packages/astro/test/units/i18n/manual-routing.test.ts @@ -10,7 +10,8 @@ import { redirectToFallback, } from '../../../dist/i18n/index.js'; import { REROUTE_DIRECTIVE_HEADER } from '../../../dist/core/constants.js'; -import { createManualRoutingContext, createMiddlewarePayload } from './test-helpers.js'; +import type { Locales } from '../../../dist/types/public/config.js'; +import { createManualRoutingContext, createMiddlewarePayload } from './test-helpers.ts'; describe('normalizeTheLocale', () => { it('should convert underscores to dashes', () => { @@ -123,24 +124,24 @@ describe('pathHasLocale', () => { describe('object locales - path matching', () => { it('should match locale object by path', () => { - const locales = [{ path: 'spanish', codes: ['es', 'es-ar'] }]; + const locales: Locales = [{ path: 'spanish', codes: ['es', 'es-ar'] }]; assert.equal(pathHasLocale('/spanish', locales), true); }); it('should match locale object in nested path', () => { - const locales = [{ path: 'spanish', codes: ['es'] }]; + const locales: Locales = [{ path: 'spanish', codes: ['es'] }]; assert.equal(pathHasLocale('/spanish/blog', locales), true); assert.equal(pathHasLocale('/spanish/blog/post', locales), true); }); it('should not match locale codes, only path', () => { - const locales = [{ path: 'spanish', codes: ['es', 'es-ar'] }]; + const locales: Locales = [{ path: 'spanish', codes: ['es', 'es-ar'] }]; assert.equal(pathHasLocale('/es', locales), false); assert.equal(pathHasLocale('/es-ar', locales), false); }); it('should match multiple locale objects', () => { - const locales = [ + const locales: Locales = [ { path: 'spanish', codes: ['es'] }, { path: 'portuguese', codes: ['pt'] }, ]; @@ -151,23 +152,23 @@ describe('pathHasLocale', () => { describe('mixed locales', () => { it('should match string locale in mixed array', () => { - const locales = ['en', { path: 'spanish', codes: ['es'] }]; + const locales: Locales = ['en', { path: 'spanish', codes: ['es'] }]; assert.equal(pathHasLocale('/en/blog', locales), true); }); it('should match object locale in mixed array', () => { - const locales = ['en', { path: 'spanish', codes: ['es'] }]; + const locales: Locales = ['en', { path: 'spanish', codes: ['es'] }]; assert.equal(pathHasLocale('/spanish/blog', locales), true); }); it('should not match undefined locale', () => { - const locales = ['en', { path: 'spanish', codes: ['es'] }]; + const locales: Locales = ['en', { path: 'spanish', codes: ['es'] }]; assert.equal(pathHasLocale('/pt', locales), false); assert.equal(pathHasLocale('/fr/blog', locales), false); }); it('should work with complex mixed config', () => { - const locales = [ + const locales: Locales = [ 'en', 'fr', { path: 'spanish', codes: ['es', 'es-ar'] }, @@ -190,7 +191,7 @@ describe('pathHasLocale', () => { }); it('should match locale object path with .html', () => { - const locales = [{ path: 'spanish', codes: ['es'] }]; + const locales: Locales = [{ path: 'spanish', codes: ['es'] }]; assert.equal(pathHasLocale('/spanish.html', locales), true); }); @@ -200,7 +201,7 @@ describe('pathHasLocale', () => { }); it('should strip .html before checking locale', () => { - const locales = [{ path: 'spanish', codes: ['es'] }]; + const locales: Locales = [{ path: 'spanish', codes: ['es'] }]; assert.equal(pathHasLocale('/spanish.html', locales), true); // But not match the code assert.equal(pathHasLocale('/es.html', locales), false); @@ -222,7 +223,7 @@ describe('pathHasLocale', () => { }); it('should handle path with only locale and trailing slash', () => { - const locales = [{ path: 'spanish', codes: ['es'] }]; + const locales: Locales = [{ path: 'spanish', codes: ['es'] }]; assert.equal(pathHasLocale('/spanish/', locales), true); }); @@ -637,7 +638,7 @@ describe('notFound', () => { const response = notFoundFn(context); assert.ok(response instanceof Response); - assert.equal(response.status, 404); + assert.equal(response!.status, 404); }); it('should return 404 for /about with configured locales', () => { @@ -650,7 +651,7 @@ describe('notFound', () => { const response = notFoundFn(context); - assert.equal(response.status, 404); + assert.equal(response!.status, 404); }); it('should set REROUTE_DIRECTIVE_HEADER to no', () => { @@ -663,7 +664,7 @@ describe('notFound', () => { const response = notFoundFn(context); - assert.equal(response.headers.get(REROUTE_DIRECTIVE_HEADER), 'no'); + assert.equal(response!.headers.get(REROUTE_DIRECTIVE_HEADER), 'no'); }); }); @@ -758,8 +759,8 @@ describe('notFound', () => { const response = notFoundFn(context, originalResponse); - assert.equal(response.status, 404); - assert.equal(response.body, originalResponse.body); + assert.equal(response!.status, 404); + assert.equal(response!.body, originalResponse.body); }); it('should copy headers when Response is passed', () => { @@ -776,8 +777,8 @@ describe('notFound', () => { const response = notFoundFn(context, originalResponse); - assert.equal(response.status, 404); - assert.equal(response.headers.get('X-Custom'), 'value'); + assert.equal(response!.status, 404); + assert.equal(response!.headers.get('X-Custom'), 'value'); }); it('should override status to 404 when Response is passed', () => { @@ -791,7 +792,7 @@ describe('notFound', () => { const response = notFoundFn(context, originalResponse); - assert.equal(response.status, 404); + assert.equal(response!.status, 404); }); it('should set REROUTE_DIRECTIVE_HEADER on passed Response', () => { @@ -805,7 +806,7 @@ describe('notFound', () => { const response = notFoundFn(context, originalResponse); - assert.equal(response.headers.get(REROUTE_DIRECTIVE_HEADER), 'no'); + assert.equal(response!.headers.get(REROUTE_DIRECTIVE_HEADER), 'no'); }); it('should return original response when REROUTE_DIRECTIVE_HEADER is no and no fallback', () => { @@ -838,7 +839,7 @@ describe('notFound', () => { const response = notFoundFn(context); - assert.equal(response.status, 404); + assert.equal(response!.status, 404); }); it('should not return original response with fallback when REROUTE_DIRECTIVE_HEADER is no', () => { @@ -857,7 +858,7 @@ describe('notFound', () => { // With fallback defined, it should not return the original assert.notEqual(response, originalResponse); - assert.equal(response.status, 404); + assert.equal(response!.status, 404); }); }); @@ -872,7 +873,7 @@ describe('notFound', () => { const response = notFoundFn(context); - assert.equal(response.status, 404); + assert.equal(response!.status, 404); }); it('should allow locale paths with base', () => { @@ -901,7 +902,7 @@ describe('notFound', () => { const response = notFoundFn(createManualRoutingContext({ pathname: '/site/contact' })); - assert.equal(response.status, 404); + assert.equal(response!.status, 404); }); }); @@ -941,7 +942,7 @@ describe('notFound', () => { const notFoundFn = notFound(payload); assert.equal(notFoundFn(createManualRoutingContext({ pathname: '/en' })), undefined); - assert.equal(notFoundFn(createManualRoutingContext({ pathname: '/es' })).status, 404); + assert.equal(notFoundFn(createManualRoutingContext({ pathname: '/es' }))!.status, 404); }); it('should return null body for 404 without passed Response', () => { @@ -954,7 +955,7 @@ describe('notFound', () => { const response = notFoundFn(context); - assert.equal(response.body, null); + assert.equal(response!.body, null); }); }); }); @@ -1146,7 +1147,7 @@ describe('redirectToFallback', () => { // Mock context.rewrite const context = { ...createManualRoutingContext({ pathname: '/es/blog/post' }), - rewrite: async (path) => { + rewrite: async (path: string) => { return new Response(null, { status: 200, headers: { 'X-Rewrite-Path': path }, @@ -1172,7 +1173,7 @@ describe('redirectToFallback', () => { const context = { ...createManualRoutingContext({ pathname: '/es/search?q=test&lang=es' }), - rewrite: async (path) => { + rewrite: async (path: string) => { return new Response(null, { headers: { 'X-Rewrite-Path': path }, }); @@ -1197,7 +1198,7 @@ describe('redirectToFallback', () => { const context = { ...createManualRoutingContext({ pathname: '/es/about' }), - rewrite: async (path) => { + rewrite: async (path: string) => { return new Response(null, { headers: { 'X-Rewrite-Path': path }, }); diff --git a/packages/astro/test/units/i18n/router.test.js b/packages/astro/test/units/i18n/router.test.ts similarity index 73% rename from packages/astro/test/units/i18n/router.test.js rename to packages/astro/test/units/i18n/router.test.ts index f95743f5c666..f1454eb014b8 100644 --- a/packages/astro/test/units/i18n/router.test.js +++ b/packages/astro/test/units/i18n/router.test.ts @@ -1,11 +1,12 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { I18nRouter } from '../../../dist/i18n/router.js'; -import { makeI18nRouterConfig, makeRouterContext } from './test-helpers.js'; +import type { I18nRouterMatch } from '../../../dist/i18n/router.js'; +import { makeI18nRouterConfig, makeRouterContext } from './test-helpers.ts'; describe('I18nRouter', () => { describe('strategy: pathname-prefix-always', () => { - let router; + let router: I18nRouter; before(() => { const config = makeI18nRouterConfig({ @@ -19,16 +20,16 @@ describe('I18nRouter', () => { it('redirects root path to default locale', () => { const context = makeRouterContext({ currentLocale: undefined }); - const result = router.match('/', context); + const result: I18nRouterMatch = router.match('/', context); assert.equal(result.type, 'redirect'); - assert.equal(result.location, '/en'); + assert.equal((result as Extract).location, '/en'); }); it('returns 404 for paths without locale prefix', () => { const context = makeRouterContext({ currentLocale: undefined }); - const result = router.match('/about', context); + const result: I18nRouterMatch = router.match('/about', context); assert.equal(result.type, 'notFound'); }); @@ -36,7 +37,7 @@ describe('I18nRouter', () => { it('continues for paths with valid locale prefix', () => { const context = makeRouterContext({ currentLocale: 'es' }); - const result = router.match('/es/about', context); + const result: I18nRouterMatch = router.match('/es/about', context); assert.equal(result.type, 'continue'); }); @@ -44,13 +45,13 @@ describe('I18nRouter', () => { it('continues for default locale with prefix', () => { const context = makeRouterContext({ currentLocale: 'en' }); - const result = router.match('/en/about', context); + const result: I18nRouterMatch = router.match('/en/about', context); assert.equal(result.type, 'continue'); }); describe('with base path', () => { - let routerWithBase; + let routerWithBase: I18nRouter; before(() => { const configWithBase = makeI18nRouterConfig({ @@ -65,32 +66,38 @@ describe('I18nRouter', () => { it('handles base path - redirects base root to base + default locale', () => { const context = makeRouterContext({ currentLocale: undefined }); - const result = routerWithBase.match('/new-site/', context); + const result: I18nRouterMatch = routerWithBase.match('/new-site/', context); assert.equal(result.type, 'redirect'); - assert.equal(result.location, '/new-site/en'); + assert.equal( + (result as Extract).location, + '/new-site/en', + ); }); it('handles base path without trailing slash', () => { const context = makeRouterContext({ currentLocale: undefined }); - const result = routerWithBase.match('/new-site', context); + const result: I18nRouterMatch = routerWithBase.match('/new-site', context); assert.equal(result.type, 'redirect'); - assert.equal(result.location, '/new-site/en'); + assert.equal( + (result as Extract).location, + '/new-site/en', + ); }); it('returns 404 for path without locale under base', () => { const context = makeRouterContext({ currentLocale: undefined }); - const result = routerWithBase.match('/new-site/about', context); + const result: I18nRouterMatch = routerWithBase.match('/new-site/about', context); assert.equal(result.type, 'notFound'); }); }); describe('with base "/" (root base path)', () => { - let routerWithSlashBase; + let routerWithSlashBase: I18nRouter; before(() => { const config = makeI18nRouterConfig({ @@ -105,16 +112,16 @@ describe('I18nRouter', () => { it('redirects root to /defaultLocale, not //defaultLocale (#15844)', () => { const context = makeRouterContext({ currentLocale: undefined }); - const result = routerWithSlashBase.match('/', context); + const result: I18nRouterMatch = routerWithSlashBase.match('/', context); assert.equal(result.type, 'redirect'); - assert.equal(result.location, '/en'); + assert.equal((result as Extract).location, '/en'); }); it('continues for paths with valid locale prefix', () => { const context = makeRouterContext({ currentLocale: 'es' }); - const result = routerWithSlashBase.match('/es/about', context); + const result: I18nRouterMatch = routerWithSlashBase.match('/es/about', context); assert.equal(result.type, 'continue'); }); @@ -122,7 +129,7 @@ describe('I18nRouter', () => { it('returns 404 for paths without locale prefix', () => { const context = makeRouterContext({ currentLocale: undefined }); - const result = routerWithSlashBase.match('/about', context); + const result: I18nRouterMatch = routerWithSlashBase.match('/about', context); assert.equal(result.type, 'notFound'); }); @@ -130,7 +137,7 @@ describe('I18nRouter', () => { }); describe('strategy: pathname-prefix-other-locales', () => { - let router; + let router: I18nRouter; before(() => { const config = makeI18nRouterConfig({ @@ -144,16 +151,16 @@ describe('I18nRouter', () => { it('returns 404 with Location header for default locale with prefix', () => { const context = makeRouterContext({ currentLocale: 'en' }); - const result = router.match('/en/about', context); + const result: I18nRouterMatch = router.match('/en/about', context); assert.equal(result.type, 'notFound'); - assert.equal(result.location, '/about'); + assert.equal((result as Extract).location, '/about'); }); it('continues for non-default locale with prefix', () => { const context = makeRouterContext({ currentLocale: 'es' }); - const result = router.match('/es/about', context); + const result: I18nRouterMatch = router.match('/es/about', context); assert.equal(result.type, 'continue'); }); @@ -161,7 +168,7 @@ describe('I18nRouter', () => { it('continues for default locale without prefix', () => { const context = makeRouterContext({ currentLocale: 'en' }); - const result = router.match('/about', context); + const result: I18nRouterMatch = router.match('/about', context); assert.equal(result.type, 'continue'); }); @@ -169,7 +176,7 @@ describe('I18nRouter', () => { it('continues for root path (default locale)', () => { const context = makeRouterContext({ currentLocale: 'en' }); - const result = router.match('/', context); + const result: I18nRouterMatch = router.match('/', context); assert.equal(result.type, 'continue'); }); @@ -177,10 +184,13 @@ describe('I18nRouter', () => { it('handles default locale in middle of path', () => { const context = makeRouterContext({ currentLocale: 'en' }); - const result = router.match('/blog/en/post', context); + const result: I18nRouterMatch = router.match('/blog/en/post', context); assert.equal(result.type, 'notFound'); - assert.equal(result.location, '/blog/post'); + assert.equal( + (result as Extract).location, + '/blog/post', + ); }); it('handles base path with default locale prefix', () => { @@ -193,15 +203,18 @@ describe('I18nRouter', () => { const routerWithBase = new I18nRouter(configWithBase); const context = makeRouterContext({ currentLocale: 'en' }); - const result = routerWithBase.match('/new-site/en/about', context); + const result: I18nRouterMatch = routerWithBase.match('/new-site/en/about', context); assert.equal(result.type, 'notFound'); - assert.equal(result.location, '/new-site/about'); + assert.equal( + (result as Extract).location, + '/new-site/about', + ); }); }); describe('strategy: pathname-prefix-always-no-redirect', () => { - let router; + let router: I18nRouter; before(() => { const config = makeI18nRouterConfig({ @@ -215,7 +228,7 @@ describe('I18nRouter', () => { it('continues for root path (allows serving, no redirect)', () => { const context = makeRouterContext({ currentLocale: undefined }); - const result = router.match('/', context); + const result: I18nRouterMatch = router.match('/', context); assert.equal(result.type, 'continue'); }); @@ -223,7 +236,7 @@ describe('I18nRouter', () => { it('returns 404 for non-root paths without locale prefix', () => { const context = makeRouterContext({ currentLocale: undefined }); - const result = router.match('/about', context); + const result: I18nRouterMatch = router.match('/about', context); assert.equal(result.type, 'notFound'); }); @@ -231,7 +244,7 @@ describe('I18nRouter', () => { it('continues for paths with valid locale prefix', () => { const context = makeRouterContext({ currentLocale: 'es' }); - const result = router.match('/es/about', context); + const result: I18nRouterMatch = router.match('/es/about', context); assert.equal(result.type, 'continue'); }); @@ -246,7 +259,7 @@ describe('I18nRouter', () => { const routerWithBase = new I18nRouter(configWithBase); const context = makeRouterContext({ currentLocale: undefined }); - const result = routerWithBase.match('/new-site', context); + const result: I18nRouterMatch = routerWithBase.match('/new-site', context); assert.equal(result.type, 'continue'); }); @@ -262,14 +275,14 @@ describe('I18nRouter', () => { ); const context = makeRouterContext({ currentLocale: undefined }); - const result = routerWithSlashBase.match('/', context); + const result: I18nRouterMatch = routerWithSlashBase.match('/', context); assert.equal(result.type, 'continue'); }); }); describe('strategy: domains-prefix-always', () => { - let router; + let router: I18nRouter; before(() => { const config = makeI18nRouterConfig({ @@ -291,10 +304,10 @@ describe('I18nRouter', () => { currentDomain: 'en.example.com', }); - const result = router.match('/', context); + const result: I18nRouterMatch = router.match('/', context); assert.equal(result.type, 'redirect'); - assert.equal(result.location, '/en'); + assert.equal((result as Extract).location, '/en'); }); it('continues when locale does not match domain (fallback to pathname logic)', () => { @@ -303,7 +316,7 @@ describe('I18nRouter', () => { currentDomain: 'en.example.com', }); - const result = router.match('/es/about', context); + const result: I18nRouterMatch = router.match('/es/about', context); assert.equal(result.type, 'continue'); }); @@ -314,7 +327,7 @@ describe('I18nRouter', () => { currentDomain: 'en.example.com', }); - const result = router.match('/about', context); + const result: I18nRouterMatch = router.match('/about', context); assert.equal(result.type, 'notFound'); }); @@ -337,15 +350,15 @@ describe('I18nRouter', () => { currentDomain: 'en.example.com', }); - const result = routerWithSlashBase.match('/', context); + const result: I18nRouterMatch = routerWithSlashBase.match('/', context); assert.equal(result.type, 'redirect'); - assert.equal(result.location, '/en'); + assert.equal((result as Extract).location, '/en'); }); }); describe('strategy: domains-prefix-other-locales', () => { - let router; + let router: I18nRouter; before(() => { const config = makeI18nRouterConfig({ @@ -367,10 +380,10 @@ describe('I18nRouter', () => { currentDomain: 'en.example.com', }); - const result = router.match('/en/about', context); + const result: I18nRouterMatch = router.match('/en/about', context); assert.equal(result.type, 'notFound'); - assert.equal(result.location, '/about'); + assert.equal((result as Extract).location, '/about'); }); it('continues for non-default locale when locale matches domain', () => { @@ -379,7 +392,7 @@ describe('I18nRouter', () => { currentDomain: 'es.example.com', }); - const result = router.match('/es/about', context); + const result: I18nRouterMatch = router.match('/es/about', context); assert.equal(result.type, 'continue'); }); @@ -390,14 +403,14 @@ describe('I18nRouter', () => { currentDomain: 'en.example.com', }); - const result = router.match('/es/about', context); + const result: I18nRouterMatch = router.match('/es/about', context); assert.equal(result.type, 'continue'); }); }); describe('strategy: domains-prefix-always-no-redirect', () => { - let router; + let router: I18nRouter; before(() => { const config = makeI18nRouterConfig({ @@ -419,7 +432,7 @@ describe('I18nRouter', () => { currentDomain: 'en.example.com', }); - const result = router.match('/', context); + const result: I18nRouterMatch = router.match('/', context); assert.equal(result.type, 'continue'); }); @@ -430,14 +443,14 @@ describe('I18nRouter', () => { currentDomain: 'en.example.com', }); - const result = router.match('/en/about', context); + const result: I18nRouterMatch = router.match('/en/about', context); assert.equal(result.type, 'continue'); }); }); describe('route filtering - skips i18n processing', () => { - let router; + let router: I18nRouter; before(() => { const config = makeI18nRouterConfig({ @@ -451,7 +464,7 @@ describe('I18nRouter', () => { it('skips 404 pages', () => { const context = makeRouterContext({ currentLocale: undefined }); - const result = router.match('/404', context); + const result: I18nRouterMatch = router.match('/404', context); assert.equal(result.type, 'continue'); }); @@ -459,7 +472,7 @@ describe('I18nRouter', () => { it('skips 500 pages', () => { const context = makeRouterContext({ currentLocale: undefined }); - const result = router.match('/500', context); + const result: I18nRouterMatch = router.match('/500', context); assert.equal(result.type, 'continue'); }); @@ -467,7 +480,7 @@ describe('I18nRouter', () => { it('skips server islands', () => { const context = makeRouterContext({ currentLocale: undefined }); - const result = router.match('/_server-islands/Counter', context); + const result: I18nRouterMatch = router.match('/_server-islands/Counter', context); assert.equal(result.type, 'continue'); }); @@ -478,7 +491,7 @@ describe('I18nRouter', () => { routeType: 'endpoint', }); - const result = router.match('/api/data', context); + const result: I18nRouterMatch = router.match('/api/data', context); assert.equal(result.type, 'continue'); }); @@ -489,7 +502,7 @@ describe('I18nRouter', () => { isReroute: true, }); - const result = router.match('/about', context); + const result: I18nRouterMatch = router.match('/about', context); assert.equal(result.type, 'continue'); }); @@ -500,14 +513,14 @@ describe('I18nRouter', () => { routeType: 'fallback', }); - const result = router.match('/about', context); + const result: I18nRouterMatch = router.match('/about', context); assert.equal(result.type, 'notFound'); }); }); describe('strategy: manual', () => { - let router; + let router: I18nRouter; before(() => { const config = makeI18nRouterConfig({ @@ -521,7 +534,7 @@ describe('I18nRouter', () => { it('always continues (no automatic routing)', () => { const context = makeRouterContext({ currentLocale: undefined }); - const result = router.match('/', context); + const result: I18nRouterMatch = router.match('/', context); assert.equal(result.type, 'continue'); }); @@ -529,7 +542,7 @@ describe('I18nRouter', () => { it('continues for any path', () => { const context = makeRouterContext({ currentLocale: undefined }); - const result = router.match('/any/path', context); + const result: I18nRouterMatch = router.match('/any/path', context); assert.equal(result.type, 'continue'); }); diff --git a/packages/astro/test/units/i18n/test-helpers.js b/packages/astro/test/units/i18n/test-helpers.js deleted file mode 100644 index a6c6d197b82e..000000000000 --- a/packages/astro/test/units/i18n/test-helpers.js +++ /dev/null @@ -1,168 +0,0 @@ -// @ts-check - -/** - * Creates an i18n router config for testing - * @param {object} [options] - * @param {import('../../../dist/core/app/common.js').RoutingStrategies} [options.strategy] - * @param {string} [options.defaultLocale] - * @param {import('../../../src/types/public/config.js').Locales} [options.locales] - * @param {string} [options.base] - * @param {Record} [options.domains] - */ -export function makeI18nRouterConfig({ - strategy = 'pathname-prefix-other-locales', - defaultLocale = 'en', - locales = ['en', 'es', 'pt'], - base = '', - domains, -} = {}) { - return { strategy, defaultLocale, locales, base, domains }; -} - -/** - * Creates router context for testing - * @param {object} [options] - * @param {string | undefined} [options.currentLocale] - * @param {string} [options.currentDomain] - * @param {string} [options.routeType] - * @param {boolean} [options.isReroute] - */ -export function makeRouterContext({ - currentLocale, - currentDomain = 'example.com', - routeType = 'page', - isReroute = false, -} = {}) { - return { currentLocale, currentDomain, routeType, isReroute }; -} - -/** - * Creates fallback options for testing - * @param {object} options - * @param {string} options.pathname - * @param {number} [options.responseStatus] - * @param {string | undefined} [options.currentLocale] - * @param {Record} [options.fallback] - * @param {'redirect' | 'rewrite'} [options.fallbackType] - * @param {import('../../../src/types/public/config.js').Locales} [options.locales] - * @param {string} [options.defaultLocale] - * @param {import('../../../dist/core/app/common.js').RoutingStrategies} [options.strategy] - * @param {string} [options.base] - */ -export function makeFallbackOptions({ - pathname, - responseStatus = 404, - currentLocale, - fallback = {}, - fallbackType = 'redirect', - locales = ['en', 'es', 'pt'], - defaultLocale = 'en', - strategy = 'pathname-prefix-other-locales', - base = '', -}) { - return { - pathname, - responseStatus, - currentLocale, - fallback, - fallbackType, - locales, - defaultLocale, - strategy, - base, - }; -} - -/** - * Creates a minimal mock APIContext for manual routing tests. - * - * This helper creates a mock context object that mimics Astro's APIContext - * with the essential properties needed for testing i18n manual routing functions - * like requestHasLocale, redirectToDefaultLocale, and notFound. - * - * @param {object} [options] - Configuration options for the mock context - * @param {string} [options.pathname='/'] - The pathname for the URL (e.g., '/en/blog') - * @param {string} [options.hostname='localhost'] - The hostname for the URL - * @param {string} [options.method='GET'] - The HTTP method for the request - * @param {string | undefined} [options.currentLocale] - The current locale from the context - * @returns {object} A mock APIContext object with url, request, currentLocale, and redirect method - * - * @example - * const context = createManualRoutingContext({ pathname: '/en/blog' }); - * const hasLocale = requestHasLocale(['en', 'es']); - * hasLocale(context); // true - */ -export function createManualRoutingContext({ - pathname = '/', - hostname = 'localhost', - method = 'GET', - currentLocale = undefined, - ...options -} = {}) { - const url = new URL(`http://${hostname}${pathname}`); - const request = new Request(url.toString(), { method }); - - return { - url, - request, - currentLocale, - redirect(path, status = 302) { - return new Response(null, { - status, - headers: { Location: path }, - }); - }, - ...options, - }; -} - -/** - * Creates a MiddlewarePayload for testing manual routing functions. - * - * This helper creates a payload object that matches the MiddlewarePayload type - * used by i18n manual routing functions like redirectToDefaultLocale and notFound. - * It provides sensible defaults for all required fields. - * - * @param {object} [options] - Configuration options for the middleware payload - * @param {string} [options.base=''] - The base path for the site (e.g., '/blog') - * @param {import('../../../src/types/public/config.js').Locales} [options.locales=['en', 'es']] - Array of locale strings or locale objects - * @param {'always' | 'never' | 'ignore'} [options.trailingSlash='ignore'] - Trailing slash behavior - * @param {'directory' | 'file'} [options.format='directory'] - Build output format - * @param {import('../../../dist/core/app/common.js').RoutingStrategies} [options.strategy='pathname-prefix-other-locales'] - i18n routing strategy - * @param {string} [options.defaultLocale='en'] - The default locale - * @param {Record | undefined} [options.domains] - Domain-to-locale mapping - * @param {Record | undefined} [options.fallback] - Fallback locale configuration - * @param {'redirect' | 'rewrite'} [options.fallbackType='redirect'] - Type of fallback behavior - * @returns {object} A MiddlewarePayload object - * - * @example - * const payload = createMiddlewarePayload({ - * base: '/blog', - * defaultLocale: 'en', - * locales: ['en', 'es', 'pt'] - * }); - * const redirect = redirectToDefaultLocale(payload); - */ -export function createMiddlewarePayload({ - base = '', - locales = ['en', 'es'], - trailingSlash = 'ignore', - format = 'directory', - strategy = 'pathname-prefix-other-locales', - defaultLocale = 'en', - domains = undefined, - fallback = undefined, - fallbackType = 'redirect', -} = {}) { - return { - base, - locales, - trailingSlash, - format, - strategy, - defaultLocale, - domains, - fallback, - fallbackType, - }; -} diff --git a/packages/astro/test/units/i18n/test-helpers.ts b/packages/astro/test/units/i18n/test-helpers.ts new file mode 100644 index 000000000000..fe910ad044d3 --- /dev/null +++ b/packages/astro/test/units/i18n/test-helpers.ts @@ -0,0 +1,119 @@ +import type { RoutingStrategies } from '../../../dist/core/app/common.js'; +import type { Locales } from '../../../dist/types/public/config.js'; +import type { MiddlewarePayload } from '../../../dist/i18n/index.js'; + +export function makeI18nRouterConfig({ + strategy = 'pathname-prefix-other-locales', + defaultLocale = 'en', + locales = ['en', 'es', 'pt'], + base = '', + domains, +}: { + strategy?: RoutingStrategies; + defaultLocale?: string; + locales?: Locales; + base?: string; + domains?: Record; +} = {}) { + return { strategy, defaultLocale, locales, base, domains }; +} + +export function makeRouterContext({ + currentLocale, + currentDomain = 'example.com', + routeType = 'page', + isReroute = false, +}: { + currentLocale?: string; + currentDomain?: string; + routeType?: string; + isReroute?: boolean; +} = {}) { + return { currentLocale, currentDomain, routeType: routeType as 'page' | 'fallback', isReroute }; +} + +export function makeFallbackOptions({ + pathname, + responseStatus = 404, + currentLocale, + fallback = {}, + fallbackType = 'redirect', + locales = ['en', 'es', 'pt'], + defaultLocale = 'en', + strategy = 'pathname-prefix-other-locales', + base = '', +}: { + pathname: string; + responseStatus?: number; + currentLocale?: string; + fallback?: Record; + fallbackType?: 'redirect' | 'rewrite'; + locales?: Locales; + defaultLocale?: string; + strategy?: RoutingStrategies; + base?: string; +}) { + return { + pathname, + responseStatus, + currentLocale, + fallback, + fallbackType, + locales, + defaultLocale, + strategy, + base, + }; +} + +export function createManualRoutingContext({ + pathname = '/', + hostname = 'localhost', + method = 'GET', + currentLocale = undefined as string | undefined, +}: { + pathname?: string; + hostname?: string; + method?: string; + currentLocale?: string; +} = {}) { + const url = new URL(`http://${hostname}${pathname}`); + const request = new Request(url.toString(), { method }); + + // Cast to any — this is a partial mock of APIContext for unit tests + return { + url, + request, + currentLocale, + redirect(path: string, status = 302) { + return new Response(null, { + status, + headers: { Location: path }, + }); + }, + } as any; +} + +export function createMiddlewarePayload({ + base = '', + locales = ['en', 'es'] as Locales, + trailingSlash = 'ignore' as 'always' | 'never' | 'ignore', + format = 'directory' as 'directory' | 'file', + strategy = 'pathname-prefix-other-locales' as RoutingStrategies, + defaultLocale = 'en', + domains = undefined as Record | undefined, + fallback = undefined as Record | undefined, + fallbackType = 'redirect' as 'redirect' | 'rewrite', +}: Partial = {}): MiddlewarePayload { + return { + base, + locales, + trailingSlash, + format, + strategy, + defaultLocale, + domains, + fallback, + fallbackType, + }; +} diff --git a/packages/astro/test/units/integrations/api.test.js b/packages/astro/test/units/integrations/api.test.js deleted file mode 100644 index c625fce65af8..000000000000 --- a/packages/astro/test/units/integrations/api.test.js +++ /dev/null @@ -1,560 +0,0 @@ -import { deepEqual } from 'node:assert'; -import * as assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; -import { validateSupportedFeatures } from '../../../dist/integrations/features-validation.js'; -import { - normalizeCodegenDir, - normalizeInjectedTypeFilename, - runHookBuildSetup, - runHookConfigSetup, -} from '../../../dist/integrations/hooks.js'; -import { createFixture, defaultLogger, runInContainer } from '../test-utils.js'; - -const defaultConfig = { - root: new URL('./', import.meta.url), - srcDir: new URL('src/', import.meta.url), - build: {}, - image: { - remotePatterns: [], - }, - outDir: new URL('./dist/', import.meta.url), - publicDir: new URL('./public/', import.meta.url), - experimental: {}, -}; -const dotAstroDir = new URL('./.astro/', defaultConfig.root); - -describe('Integration API', () => { - it('runHookBuildSetup should work', async () => { - const updatedViteConfig = await runHookBuildSetup({ - config: { - ...defaultConfig, - integrations: [ - { - name: 'test', - hooks: { - 'astro:build:setup'({ updateConfig }) { - updateConfig({ - define: { - foo: 'bar', - }, - }); - }, - }, - }, - ], - }, - vite: {}, - logger: defaultLogger, - pages: new Map(), - target: 'server', - }); - assert.equal(updatedViteConfig.hasOwnProperty('define'), true); - }); - - it('runHookBuildSetup should return updated config', async () => { - let updatedInternalConfig; - const updatedViteConfig = await runHookBuildSetup({ - config: { - ...defaultConfig, - integrations: [ - { - name: 'test', - hooks: { - 'astro:build:setup'({ updateConfig }) { - updatedInternalConfig = updateConfig({ - define: { - foo: 'bar', - }, - }); - }, - }, - }, - ], - }, - vite: {}, - logger: defaultLogger, - pages: new Map(), - target: 'server', - }); - deepEqual(updatedViteConfig, updatedInternalConfig); - }); - - it('runHookConfigSetup can update Astro config', async () => { - const site = 'https://test.com/'; - const updatedSettings = await runHookConfigSetup({ - logger: defaultLogger, - settings: { - config: { - ...defaultConfig, - integrations: [ - { - name: 'test', - hooks: { - 'astro:config:setup': ({ updateConfig }) => { - updateConfig({ site }); - }, - }, - }, - ], - }, - dotAstroDir, - }, - }); - assert.equal(updatedSettings.config.site, site); - }); - - it('runHookConfigSetup runs integrations added by another integration', async () => { - const site = 'https://test.com/'; - const updatedSettings = await runHookConfigSetup({ - logger: defaultLogger, - settings: { - config: { - ...defaultConfig, - integrations: [ - { - name: 'test', - hooks: { - 'astro:config:setup': ({ updateConfig }) => { - updateConfig({ - integrations: [ - { - name: 'dynamically-added', - hooks: { - // eslint-disable-next-line @typescript-eslint/no-shadow - 'astro:config:setup': ({ updateConfig }) => { - updateConfig({ site }); - }, - }, - }, - ], - }); - }, - }, - }, - ], - }, - dotAstroDir, - }, - }); - assert.equal(updatedSettings.config.site, site); - assert.equal(updatedSettings.config.integrations.length, 2); - }); - - describe('Routes resolved hooks', () => { - it.skip( - 'should work in dev', - { todo: "[p2] Understand why routes aren't deep equal anymore" }, - async () => { - let routes = []; - const fixture = await createFixture({ - '/src/pages/about.astro': '', - '/src/actions.ts': 'export const server = {}', - '/src/foo.astro': '', - }); - - await runInContainer( - { - inlineConfig: { - root: fixture.path, - integrations: [ - { - name: 'test', - hooks: { - 'astro:config:setup': (params) => { - params.injectRoute({ - entrypoint: './src/foo.astro', - pattern: '/foo', - }); - }, - 'astro:routes:resolved': (params) => { - routes = params.routes.map((r) => ({ - isPrerendered: r.isPrerendered, - entrypoint: r.entrypoint, - pattern: r.pattern, - params: r.params, - origin: r.origin, - })); - routes.sort((a, b) => a.pattern.localeCompare(b.pattern)); - }, - }, - }, - ], - }, - }, - async (container) => { - assert.equal(routes.length, 6); - assert.deepEqual( - routes, - [ - { - isPrerendered: false, - entrypoint: '_server-islands.astro', - pattern: '/_server-islands/[name]', - params: ['name'], - origin: 'internal', - }, - { - isPrerendered: false, - entrypoint: '../../../../dist/actions/runtime/entrypoints/route.js', - pattern: '/_actions/[...path]', - params: ['...path'], - origin: 'internal', - }, - { - isPrerendered: true, - entrypoint: 'src/pages/about.astro', - pattern: '/about', - params: [], - origin: 'project', - }, - { - isPrerendered: true, - entrypoint: 'src/foo.astro', - pattern: '/foo', - params: [], - origin: 'external', - }, - { - isPrerendered: false, - entrypoint: '../../../../dist/assets/endpoint/dev.js', - pattern: '/_image', - params: [], - origin: 'internal', - }, - { - isPrerendered: false, - entrypoint: 'astro-default-404.astro', - pattern: '/404', - params: [], - origin: 'internal', - }, - ].sort((a, b) => a.pattern.localeCompare(b.pattern)), - ); - - await fixture.writeFile('/src/pages/bar.astro', ''); - container.viteServer.watcher.emit( - 'add', - fixture.getPath('/src/pages/bar.astro').replace(/\\/g, '/'), - ); - await new Promise((r) => setTimeout(r, 100)); - - deepEqual( - routes, - [ - { - isPrerendered: false, - entrypoint: '_server-islands.astro', - pattern: '/_server-islands/[name]', - params: ['name'], - origin: 'internal', - }, - { - isPrerendered: false, - entrypoint: '../../../../dist/actions/runtime/entrypoints/route.js', - pattern: '/_actions/[...path]', - params: ['...path'], - origin: 'internal', - }, - { - isPrerendered: true, - entrypoint: 'src/pages/about.astro', - pattern: '/about', - params: [], - origin: 'project', - }, - { - isPrerendered: true, - entrypoint: 'src/pages/bar.astro', - pattern: '/bar', - params: [], - origin: 'project', - }, - { - isPrerendered: true, - entrypoint: 'src/foo.astro', - pattern: '/foo', - params: [], - origin: 'external', - }, - { - isPrerendered: false, - entrypoint: '../../../../dist/assets/endpoint/dev.js', - pattern: '/_image', - params: [], - origin: 'internal', - }, - { - isPrerendered: false, - entrypoint: 'astro-default-404.astro', - pattern: '/404', - params: [], - origin: 'internal', - }, - ].sort((a, b) => a.pattern.localeCompare(b.pattern)), - ); - - await fixture.writeFile( - '/src/pages/about.astro', - '---\nexport const prerender=false\n', - ); - container.viteServer.watcher.emit( - 'change', - fixture.getPath('/src/pages/about.astro').replace(/\\/g, '/'), - ); - await new Promise((r) => setTimeout(r, 100)); - - deepEqual( - routes, - [ - { - isPrerendered: false, - entrypoint: '_server-islands.astro', - pattern: '/_server-islands/[name]', - params: ['name'], - origin: 'internal', - }, - { - isPrerendered: false, - entrypoint: '../../../../dist/actions/runtime/entrypoints/route.js', - pattern: '/_actions/[...path]', - params: ['...path'], - origin: 'internal', - }, - { - isPrerendered: false, - entrypoint: 'src/pages/about.astro', - pattern: '/about', - params: [], - origin: 'project', - }, - { - isPrerendered: true, - entrypoint: 'src/pages/bar.astro', - pattern: '/bar', - params: [], - origin: 'project', - }, - { - isPrerendered: true, - entrypoint: 'src/foo.astro', - pattern: '/foo', - params: [], - origin: 'external', - }, - { - isPrerendered: false, - entrypoint: '../../../../dist/assets/endpoint/dev.js', - pattern: '/_image', - params: [], - origin: 'internal', - }, - { - isPrerendered: false, - entrypoint: 'astro-default-404.astro', - pattern: '/404', - params: [], - origin: 'internal', - }, - ].sort((a, b) => a.pattern.localeCompare(b.pattern)), - ); - }, - ); - }, - ); - }); - - describe('Routes setup hook', () => { - it('should work in dev', async () => { - let routes = []; - const fixture = await createFixture({ - '/src/pages/no-prerender.astro': '---\nexport const prerender = false\n---', - '/src/pages/prerender.astro': '---\nexport const prerender = true\n---', - '/src/pages/unknown-prerender.astro': '', - }); - - await runInContainer( - { - inlineConfig: { - root: fixture.path, - integrations: [ - { - name: 'test', - hooks: { - 'astro:route:setup': (params) => { - routes.push({ - component: params.route.component, - prerender: params.route.prerender, - }); - }, - }, - }, - ], - }, - }, - async () => { - routes.sort((a, b) => a.component.localeCompare(b.component)); - deepEqual(routes, [ - { - component: 'src/pages/no-prerender.astro', - prerender: false, - }, - { - component: 'src/pages/prerender.astro', - prerender: true, - }, - { - component: 'src/pages/unknown-prerender.astro', - prerender: true, - }, - ]); - }, - ); - }); - }); -}); - -describe('Astro feature map', function () { - it('should support the feature when stable', () => { - let result = validateSupportedFeatures( - 'test', - { - hybridOutput: 'stable', - }, - { - config: { output: 'static' }, - }, - {}, - defaultLogger, - ); - assert.equal(result['hybridOutput'], true); - }); - - it('should not support the feature when not provided', () => { - let result = validateSupportedFeatures( - 'test', - {}, - { - buildOutput: 'server', - config: { output: 'static' }, - }, - defaultLogger, - ); - assert.equal(result['hybridOutput'], false); - }); - - it('should not support the feature when an empty object is provided', () => { - let result = validateSupportedFeatures( - 'test', - {}, - { - buildOutput: 'server', - config: { output: 'static' }, - }, - defaultLogger, - ); - assert.equal(result['hybridOutput'], false); - }); - - describe('static output', function () { - it('should be supported with the correct config', () => { - let result = validateSupportedFeatures( - 'test', - { staticOutput: 'stable' }, - { - config: { output: 'static' }, - }, - defaultLogger, - ); - assert.equal(result['staticOutput'], true); - }); - - it("should not be valid if the config is correct, but the it's unsupported", () => { - let result = validateSupportedFeatures( - 'test', - { staticOutput: 'unsupported' }, - { - buildOutput: 'static', - config: { output: 'static' }, - }, - defaultLogger, - ); - assert.equal(result['staticOutput'], false); - }); - }); - describe('hybrid output', function () { - it('should be supported with the correct config', () => { - let result = validateSupportedFeatures( - 'test', - { hybridOutput: 'stable' }, - { - config: { output: 'static' }, - }, - defaultLogger, - ); - assert.equal(result['hybridOutput'], true); - }); - - it("should not be valid if the config is correct, but the it's unsupported", () => { - let result = validateSupportedFeatures( - 'test', - { - hybridOutput: 'unsupported', - }, - { - buildOutput: 'server', - config: { output: 'static' }, - }, - defaultLogger, - ); - assert.equal(result['hybridOutput'], false); - }); - }); - describe('server output', function () { - it('should be supported with the correct config', () => { - let result = validateSupportedFeatures( - 'test', - { serverOutput: 'stable' }, - { - config: { output: 'server' }, - }, - defaultLogger, - ); - assert.equal(result['serverOutput'], true); - }); - - it("should not be valid if the config is correct, but the it's unsupported", () => { - let result = validateSupportedFeatures( - 'test', - { - serverOutput: 'unsupported', - }, - { - config: { output: 'server' }, - }, - defaultLogger, - ); - assert.equal(result['serverOutput'], false); - }); - }); -}); - -describe('normalizeInjectedTypeFilename', () => { - // invalid filename - assert.throws(() => normalizeInjectedTypeFilename('types', 'integration')); - // valid filename - assert.doesNotThrow(() => normalizeInjectedTypeFilename('types.d.ts', 'integration')); - // filename normalization - assert.equal( - normalizeInjectedTypeFilename('aA1-*/_"~.d.ts', 'integration'), - './integrations/integration/aA1-_____.d.ts', - ); - // integration name normalization - assert.equal( - normalizeInjectedTypeFilename('types.d.ts', 'aA1-*/_"~.'), - './integrations/aA1-_____./types.d.ts', - ); -}); - -describe('normalizeCodegenDir', () => { - assert.equal(normalizeCodegenDir('aA1-*/_"~.'), './integrations/aA1-_____./'); -}); diff --git a/packages/astro/test/units/integrations/api.test.ts b/packages/astro/test/units/integrations/api.test.ts new file mode 100644 index 000000000000..6094d132c222 --- /dev/null +++ b/packages/astro/test/units/integrations/api.test.ts @@ -0,0 +1,302 @@ +import { deepEqual } from 'node:assert'; +import * as assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { validateSupportedFeatures } from '../../../dist/integrations/features-validation.js'; +import { + normalizeCodegenDir, + normalizeInjectedTypeFilename, + runHookBuildSetup, + runHookConfigSetup, +} from '../../../dist/integrations/hooks.js'; +import { defaultLogger } from '../test-utils.ts'; + +import type { AstroConfig } from '../../../dist/types/public/config.js'; +import type { AstroSettings } from '../../../dist/types/astro.js'; + +const defaultConfig: Record = { + root: new URL('./', import.meta.url), + srcDir: new URL('src/', import.meta.url), + build: {}, + image: { + remotePatterns: [], + }, + outDir: new URL('./dist/', import.meta.url), + publicDir: new URL('./public/', import.meta.url), + experimental: {}, +}; + +const dotAstroDir = new URL('./.astro/', defaultConfig.root as URL); + +describe('Integration API', () => { + it('runHookBuildSetup should work', async () => { + const updatedViteConfig = await runHookBuildSetup({ + config: { + ...defaultConfig, + integrations: [ + { + name: 'test', + hooks: { + 'astro:build:setup'({ updateConfig }: { updateConfig: (cfg: object) => object }) { + updateConfig({ + define: { + foo: 'bar', + }, + }); + }, + }, + }, + ], + } as unknown as AstroConfig, + vite: {}, + logger: defaultLogger, + pages: new Map(), + target: 'server', + }); + assert.equal(updatedViteConfig.hasOwnProperty('define'), true); + }); + + it('runHookBuildSetup should return updated config', async () => { + let updatedInternalConfig: unknown; + const updatedViteConfig = await runHookBuildSetup({ + config: { + ...defaultConfig, + integrations: [ + { + name: 'test', + hooks: { + 'astro:build:setup'({ updateConfig }: { updateConfig: (cfg: object) => object }) { + updatedInternalConfig = updateConfig({ + define: { + foo: 'bar', + }, + }); + }, + }, + }, + ], + } as unknown as AstroConfig, + vite: {}, + logger: defaultLogger, + pages: new Map(), + target: 'server', + }); + deepEqual(updatedViteConfig, updatedInternalConfig); + }); + + it('runHookConfigSetup can update Astro config', async () => { + const site = 'https://test.com/'; + const updatedSettings = await runHookConfigSetup({ + logger: defaultLogger, + settings: { + config: { + ...defaultConfig, + integrations: [ + { + name: 'test', + hooks: { + 'astro:config:setup': ({ + updateConfig, + }: { + updateConfig: (cfg: object) => void; + }) => { + updateConfig({ site }); + }, + }, + }, + ], + }, + dotAstroDir, + } as unknown as AstroSettings, + } as Parameters[0]); + assert.equal(updatedSettings.config.site, site); + }); + + it('runHookConfigSetup runs integrations added by another integration', async () => { + const site = 'https://test.com/'; + const updatedSettings = await runHookConfigSetup({ + logger: defaultLogger, + settings: { + config: { + ...defaultConfig, + integrations: [ + { + name: 'test', + hooks: { + 'astro:config:setup': ({ + updateConfig, + }: { + updateConfig: (cfg: object) => void; + }) => { + updateConfig({ + integrations: [ + { + name: 'dynamically-added', + hooks: { + 'astro:config:setup': ({ + updateConfig: innerUpdateConfig, + }: { + updateConfig: (cfg: object) => void; + }) => { + innerUpdateConfig({ site }); + }, + }, + }, + ], + }); + }, + }, + }, + ], + }, + dotAstroDir, + } as unknown as AstroSettings, + } as Parameters[0]); + assert.equal(updatedSettings.config.site, site); + assert.equal(updatedSettings.config.integrations.length, 2); + }); +}); + +describe('Astro feature map', function () { + it('should support the feature when stable', () => { + const result = validateSupportedFeatures( + 'test', + { + hybridOutput: 'stable', + }, + { + config: { output: 'static' }, + } as unknown as AstroSettings, + defaultLogger, + ); + assert.equal(result['hybridOutput'], true); + }); + + it('should not support the feature when not provided', () => { + const result = validateSupportedFeatures( + 'test', + {}, + { + buildOutput: 'server', + config: { output: 'static' }, + } as unknown as AstroSettings, + defaultLogger, + ); + assert.equal(result['hybridOutput'], false); + }); + + it('should not support the feature when an empty object is provided', () => { + const result = validateSupportedFeatures( + 'test', + {}, + { + buildOutput: 'server', + config: { output: 'static' }, + } as unknown as AstroSettings, + defaultLogger, + ); + assert.equal(result['hybridOutput'], false); + }); + + describe('static output', function () { + it('should be supported with the correct config', () => { + const result = validateSupportedFeatures( + 'test', + { staticOutput: 'stable' }, + { + config: { output: 'static' }, + } as unknown as AstroSettings, + defaultLogger, + ); + assert.equal(result['staticOutput'], true); + }); + + it("should not be valid if the config is correct, but the it's unsupported", () => { + const result = validateSupportedFeatures( + 'test', + { staticOutput: 'unsupported' }, + { + buildOutput: 'static', + config: { output: 'static' }, + } as unknown as AstroSettings, + defaultLogger, + ); + assert.equal(result['staticOutput'], false); + }); + }); + describe('hybrid output', function () { + it('should be supported with the correct config', () => { + const result = validateSupportedFeatures( + 'test', + { hybridOutput: 'stable' }, + { + config: { output: 'static' }, + } as unknown as AstroSettings, + defaultLogger, + ); + assert.equal(result['hybridOutput'], true); + }); + + it("should not be valid if the config is correct, but the it's unsupported", () => { + const result = validateSupportedFeatures( + 'test', + { + hybridOutput: 'unsupported', + }, + { + buildOutput: 'server', + config: { output: 'static' }, + } as unknown as AstroSettings, + defaultLogger, + ); + assert.equal(result['hybridOutput'], false); + }); + }); + describe('server output', function () { + it('should be supported with the correct config', () => { + const result = validateSupportedFeatures( + 'test', + { serverOutput: 'stable' }, + { + config: { output: 'server' }, + } as unknown as AstroSettings, + defaultLogger, + ); + assert.equal(result['serverOutput'], true); + }); + + it("should not be valid if the config is correct, but the it's unsupported", () => { + const result = validateSupportedFeatures( + 'test', + { + serverOutput: 'unsupported', + }, + { + config: { output: 'server' }, + } as unknown as AstroSettings, + defaultLogger, + ); + assert.equal(result['serverOutput'], false); + }); + }); +}); + +describe('normalizeInjectedTypeFilename', () => { + // invalid filename + assert.throws(() => normalizeInjectedTypeFilename('types', 'integration')); + // valid filename + assert.doesNotThrow(() => normalizeInjectedTypeFilename('types.d.ts', 'integration')); + // filename normalization + assert.equal( + normalizeInjectedTypeFilename('aA1-*/_"~.d.ts', 'integration'), + './integrations/integration/aA1-_____.d.ts', + ); + // integration name normalization + assert.equal( + normalizeInjectedTypeFilename('types.d.ts', 'aA1-*/_"~.'), + './integrations/aA1-_____./types.d.ts', + ); +}); + +describe('normalizeCodegenDir', () => { + assert.equal(normalizeCodegenDir('aA1-*/_"~.'), './integrations/aA1-_____./'); +}); diff --git a/packages/astro/test/units/integrations/hooks.test.ts b/packages/astro/test/units/integrations/hooks.test.ts new file mode 100644 index 000000000000..43aeb07c6cea --- /dev/null +++ b/packages/astro/test/units/integrations/hooks.test.ts @@ -0,0 +1,319 @@ +import * as assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { + normalizeCodegenDir, + normalizeInjectedTypeFilename, + toIntegrationResolvedRoute, +} from '../../../dist/integrations/hooks.js'; +import { + getAdapterStaticRecommendation, + getSupportMessage, + unwrapSupportKind, +} from '../../../dist/integrations/features-validation.js'; +import { resolveMiddlewareMode } from '../../../dist/integrations/adapter-utils.js'; +import { createRouteData } from '../mocks.ts'; +import { dynamicPart, makeRoute, spreadPart, staticPart } from '../routing/test-helpers.ts'; + +import type { + AdapterSupport, + AstroAdapterFeatures, +} from '../../../dist/types/public/integrations.js'; + +// #region normalizeCodegenDir +describe('normalizeCodegenDir', () => { + it('preserves alphanumeric, dots, and hyphens', () => { + assert.equal(normalizeCodegenDir('my-integration'), './integrations/my-integration/'); + }); + + it('replaces slashes', () => { + assert.equal(normalizeCodegenDir('@scope/plugin'), './integrations/_scope_plugin/'); + }); + + it('replaces spaces and special characters', () => { + assert.equal(normalizeCodegenDir('has space!@#$'), './integrations/has_space____/'); + }); + + it('preserves dots in name', () => { + assert.equal(normalizeCodegenDir('my.integration.v2'), './integrations/my.integration.v2/'); + }); + + it('handles empty string', () => { + assert.equal(normalizeCodegenDir(''), './integrations//'); + }); + + it('replaces unicode characters', () => { + assert.equal(normalizeCodegenDir('cafe\u0301'), './integrations/cafe_/'); + }); +}); +// #endregion + +// #region normalizeInjectedTypeFilename +describe('normalizeInjectedTypeFilename', () => { + it('throws when filename does not end with .d.ts', () => { + assert.throws( + () => normalizeInjectedTypeFilename('types.ts', 'my-integration'), + /does not end with/, + ); + }); + + it('throws for plain filename without extension', () => { + assert.throws( + () => normalizeInjectedTypeFilename('types', 'my-integration'), + /does not end with/, + ); + }); + + it('does not throw for valid .d.ts filename', () => { + assert.doesNotThrow(() => normalizeInjectedTypeFilename('types.d.ts', 'my-integration')); + }); + + it('returns normalized path with integration dir prefix', () => { + assert.equal( + normalizeInjectedTypeFilename('types.d.ts', 'my-integration'), + './integrations/my-integration/types.d.ts', + ); + }); + + it('sanitizes special characters in filename', () => { + assert.equal( + normalizeInjectedTypeFilename('my types!.d.ts', 'my-integration'), + './integrations/my-integration/my_types_.d.ts', + ); + }); + + it('sanitizes special characters in integration name', () => { + assert.equal( + normalizeInjectedTypeFilename('types.d.ts', '@scope/pkg'), + './integrations/_scope_pkg/types.d.ts', + ); + }); + + it('handles both filename and integration name with special chars', () => { + assert.equal( + normalizeInjectedTypeFilename('aA1-*/_"~.d.ts', 'aA1-*/_"~.'), + './integrations/aA1-_____./aA1-_____.d.ts', + ); + }); +}); +// #endregion + +// #region toIntegrationResolvedRoute +describe('toIntegrationResolvedRoute', () => { + it('maps RouteData fields to IntegrationResolvedRoute fields', () => { + const route = makeRoute({ + route: '/blog/[slug]', + segments: [[staticPart('blog')], [dynamicPart('slug')]], + trailingSlash: 'ignore', + pathname: undefined, + }); + const result = toIntegrationResolvedRoute(route, 'ignore'); + + assert.equal(result.isPrerendered, false); + assert.equal(result.entrypoint, route.component); + assert.equal(result.pattern, '/blog/[slug]'); + assert.deepEqual(result.params, ['slug']); + assert.equal(result.origin, 'project'); + assert.equal(result.patternRegex, route.pattern); + assert.deepEqual(result.segments, route.segments); + assert.equal(result.type, 'page'); + assert.equal(result.pathname, undefined); + assert.equal(result.redirect, undefined); + assert.equal(result.redirectRoute, undefined); + assert.deepEqual(result.fallbackRoutes, []); + }); + + it('generate function produces correct path from params', () => { + const route = makeRoute({ + route: '/blog/[slug]', + segments: [[staticPart('blog')], [dynamicPart('slug')]], + trailingSlash: 'ignore', + pathname: undefined, + }); + const result = toIntegrationResolvedRoute(route, 'ignore'); + + assert.equal(result.generate({ slug: 'hello-world' }), '/blog/hello-world'); + }); + + it('handles static routes with pathname', () => { + const route = createRouteData({ route: '/about' }); + const result = toIntegrationResolvedRoute(route, 'ignore'); + + assert.equal(result.pathname, '/about'); + assert.equal(result.pattern, '/about'); + assert.deepEqual(result.params, []); + }); + + it('maps prerendered routes correctly', () => { + const route = createRouteData({ route: '/page', prerender: true }); + const result = toIntegrationResolvedRoute(route, 'ignore'); + assert.equal(result.isPrerendered, true); + }); + + it('recursively maps redirectRoute', () => { + const targetRoute = createRouteData({ route: '/new-blog' }); + const route = createRouteData({ route: '/old-blog', type: 'redirect' }); + route.redirect = '/new-blog'; + route.redirectRoute = targetRoute; + + const result = toIntegrationResolvedRoute(route, 'ignore'); + assert.equal(result.type, 'redirect'); + assert.ok(result.redirectRoute); + assert.equal(result.redirectRoute.pattern, '/new-blog'); + }); + + it('recursively maps fallbackRoutes', () => { + const fallback = createRouteData({ route: '/en/blog' }); + fallback.origin = 'internal'; + const route = createRouteData({ route: '/blog' }); + route.fallbackRoutes = [fallback]; + + const result = toIntegrationResolvedRoute(route, 'ignore'); + assert.equal(result.fallbackRoutes.length, 1); + assert.equal(result.fallbackRoutes[0].pattern, '/en/blog'); + assert.equal(result.fallbackRoutes[0].origin, 'internal'); + }); + + it('applies trailingSlash "always" to generate function', () => { + const route = createRouteData({ route: '/about' }); + const result = toIntegrationResolvedRoute(route, 'always'); + assert.equal(result.generate({}), '/about/'); + }); + + it('applies trailingSlash "never" to generate function', () => { + const route = createRouteData({ route: '/about' }); + const result = toIntegrationResolvedRoute(route, 'never'); + const generated = result.generate({}); + assert.ok(!generated.endsWith('/') || generated === '/'); + }); + + it('handles endpoint route type', () => { + const route = createRouteData({ route: '/api/data', type: 'endpoint' }); + const result = toIntegrationResolvedRoute(route, 'ignore'); + assert.equal(result.type, 'endpoint'); + }); + + it('handles spread params in generate', () => { + const route = makeRoute({ + route: '/blog/[...slug]', + segments: [[staticPart('blog')], [spreadPart('...slug')]], + trailingSlash: 'ignore', + pathname: undefined, + }); + const result = toIntegrationResolvedRoute(route, 'ignore'); + assert.equal(result.generate({ slug: 'a/b/c' }), '/blog/a/b/c'); + }); +}); +// #endregion + +// #region resolveMiddlewareMode +describe('resolveMiddlewareMode', () => { + it('returns "classic" when features is undefined', () => { + assert.equal(resolveMiddlewareMode(undefined), 'classic'); + }); + + it('returns "classic" when features is empty object', () => { + assert.equal(resolveMiddlewareMode({}), 'classic'); + }); + + it('returns the middlewareMode value when explicitly set', () => { + assert.equal(resolveMiddlewareMode({ middlewareMode: 'edge' }), 'edge'); + }); + + it('returns "classic" when middlewareMode is "classic"', () => { + assert.equal(resolveMiddlewareMode({ middlewareMode: 'classic' }), 'classic'); + }); + + it('returns "edge" for deprecated edgeMiddleware: true', () => { + assert.equal(resolveMiddlewareMode({ edgeMiddleware: true } as AstroAdapterFeatures), 'edge'); + }); + + it('returns "classic" for deprecated edgeMiddleware: false', () => { + assert.equal( + resolveMiddlewareMode({ edgeMiddleware: false } as AstroAdapterFeatures), + 'classic', + ); + }); + + it('middlewareMode takes precedence over edgeMiddleware', () => { + assert.equal( + resolveMiddlewareMode({ + middlewareMode: 'classic', + edgeMiddleware: true, + } as AstroAdapterFeatures), + 'classic', + ); + }); +}); +// #endregion + +// #region getAdapterStaticRecommendation +describe('getAdapterStaticRecommendation', () => { + it('returns recommendation for @astrojs/vercel/static', () => { + const result = getAdapterStaticRecommendation('@astrojs/vercel/static'); + assert.ok(result); + assert.ok(result.includes('@astrojs/vercel/serverless')); + }); + + it('returns undefined for unknown adapter', () => { + assert.equal(getAdapterStaticRecommendation('unknown-adapter'), undefined); + }); + + it('returns undefined for empty string', () => { + assert.equal(getAdapterStaticRecommendation(''), undefined); + }); + + it('returns undefined for similar but non-matching adapter name', () => { + assert.equal(getAdapterStaticRecommendation('@astrojs/vercel'), undefined); + }); +}); +// #endregion + +// #region unwrapSupportKind +describe('unwrapSupportKind', () => { + it('returns undefined when supportKind is undefined', () => { + assert.equal(unwrapSupportKind(undefined), undefined); + }); + + it('returns the string directly when supportKind is a string', () => { + assert.equal(unwrapSupportKind('stable'), 'stable'); + }); + + it('returns support from object when supportKind is an object', () => { + assert.equal( + unwrapSupportKind({ support: 'experimental', message: 'Beta feature' }), + 'experimental', + ); + }); + + it('handles all stability levels as strings', () => { + assert.equal(unwrapSupportKind('stable'), 'stable'); + assert.equal(unwrapSupportKind('deprecated'), 'deprecated'); + assert.equal(unwrapSupportKind('unsupported'), 'unsupported'); + assert.equal(unwrapSupportKind('experimental'), 'experimental'); + assert.equal(unwrapSupportKind('limited'), 'limited'); + }); + + it('returns undefined for falsy values', () => { + assert.equal(unwrapSupportKind(undefined), undefined); + }); +}); +// #endregion + +// #region getSupportMessage +describe('getSupportMessage', () => { + it('returns undefined when supportKind is a string', () => { + assert.equal(getSupportMessage('stable'), undefined); + }); + + it('returns the message when supportKind is an object with message', () => { + assert.equal( + getSupportMessage({ support: 'experimental', message: 'Beta feature' }), + 'Beta feature', + ); + }); + + it('returns undefined when supportKind is an object without message', () => { + assert.equal(getSupportMessage({ support: 'stable' } as unknown as AdapterSupport), undefined); + }); +}); +// #endregion diff --git a/packages/astro/test/units/logger/destination.test.ts b/packages/astro/test/units/logger/destination.test.ts new file mode 100644 index 000000000000..f5bd057bbfc8 --- /dev/null +++ b/packages/astro/test/units/logger/destination.test.ts @@ -0,0 +1,172 @@ +import * as assert from 'node:assert/strict'; +import { beforeEach, describe, it } from 'node:test'; +import type { AstroLogMessage, AstroLoggerDestination } from '../../../dist/core/logger/core.js'; +import { AstroLogger } from '../../../dist/core/logger/core.js'; + +let logs: AstroLogMessage[] = []; +let jsonLogs: string[] = []; + +const testDestination: AstroLoggerDestination = { + write(event: AstroLogMessage) { + logs.push(event); + return true; + }, +}; + +const jsonDestination: AstroLoggerDestination = { + write(event: AstroLogMessage) { + if ((event as any)._format === 'json') { + jsonLogs.push(JSON.stringify({ message: event.message, label: event.label })); + } + return true; + }, +}; + +describe('log destination', () => { + beforeEach(() => { + logs = []; + jsonLogs = []; + }); + + describe('event shape', () => { + const logger = new AstroLogger({ + destination: testDestination, + level: 'info', + _format: 'default', + }); + + it('info() pushes an event with level info', () => { + logger.info('build', 'server started'); + assert.equal(logs.length, 1); + assert.equal(logs[0].level, 'info'); + assert.equal(logs[0].label, 'build'); + assert.equal(logs[0].message, 'server started'); + assert.equal(logs[0].newLine, true); + }); + + it('warn() pushes an event with level warn', () => { + logger.warn('build', 'deprecation notice'); + assert.equal(logs.length, 1); + assert.equal(logs[0].level, 'warn'); + assert.equal(logs[0].label, 'build'); + assert.equal(logs[0].message, 'deprecation notice'); + }); + + it('error() pushes an event with level error', () => { + logger.error('build', 'build failed'); + assert.equal(logs.length, 1); + assert.equal(logs[0].level, 'error'); + assert.equal(logs[0].message, 'build failed'); + }); + + it('supports null label', () => { + logger.info(null, 'no label'); + assert.equal(logs[0].label, null); + }); + + it('respects newLine parameter', () => { + logger.info('build', 'no trailing newline', false); + assert.equal(logs[0].newLine, false); + }); + }); + + describe('format propagation', () => { + it('propagates default format to events', () => { + const logger = new AstroLogger({ + destination: testDestination, + level: 'info', + _format: 'default', + }); + logger.info('build', 'test'); + assert.equal((logs[0] as any)._format, 'default'); + }); + + it('propagates json format to events', () => { + const logger = new AstroLogger({ + destination: testDestination, + level: 'info', + _format: 'json', + }); + logger.info('build', 'test'); + assert.equal((logs[0] as any)._format, 'json'); + }); + }); + + describe('json formatting', () => { + const logger = new AstroLogger({ + destination: jsonDestination, + level: 'info', + _format: 'json', + }); + + it('serializes message and label as JSON', () => { + logger.info('build', 'compiled successfully'); + assert.equal(jsonLogs.length, 1); + assert.equal(jsonLogs[0], '{"message":"compiled successfully","label":"build"}'); + }); + + it('serializes null label', () => { + logger.info(null, 'no label message'); + assert.equal(jsonLogs[0], '{"message":"no label message","label":null}'); + }); + + it('only includes message and label', () => { + logger.warn('build', 'a warning'); + assert.equal(jsonLogs[0], '{"message":"a warning","label":"build"}'); + }); + + it('does not write when format is not json', () => { + const defaultLogger = new AstroLogger({ + destination: jsonDestination, + level: 'info', + _format: 'default', + }); + defaultLogger.info('build', 'should not appear'); + assert.equal(jsonLogs.length, 0); + }); + }); + + describe('level filtering', () => { + it('filters out info when level is warn', () => { + const logger = new AstroLogger({ + destination: testDestination, + level: 'warn', + _format: 'default', + }); + logger.info('build', 'should be filtered'); + assert.equal(logs.length, 0); + }); + + it('allows warn when level is warn', () => { + const logger = new AstroLogger({ + destination: testDestination, + level: 'warn', + _format: 'default', + }); + logger.warn('build', 'should pass'); + assert.equal(logs.length, 1); + }); + + it('allows error when level is warn', () => { + const logger = new AstroLogger({ + destination: testDestination, + level: 'warn', + _format: 'default', + }); + logger.error('build', 'should pass'); + assert.equal(logs.length, 1); + }); + + it('filters everything when level is silent', () => { + const logger = new AstroLogger({ + destination: testDestination, + level: 'silent', + _format: 'default', + }); + logger.info('build', 'nope'); + logger.warn('build', 'nope'); + logger.error('build', 'nope'); + assert.equal(logs.length, 0); + }); + }); +}); diff --git a/packages/astro/test/units/logger/locale.test.js b/packages/astro/test/units/logger/locale.test.ts similarity index 100% rename from packages/astro/test/units/logger/locale.test.js rename to packages/astro/test/units/logger/locale.test.ts diff --git a/packages/astro/test/units/manifest/serialized.test.js b/packages/astro/test/units/manifest/serialized.test.js new file mode 100644 index 000000000000..90411ecdf9f1 --- /dev/null +++ b/packages/astro/test/units/manifest/serialized.test.js @@ -0,0 +1,227 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { + serializedManifestPlugin, + SERIALIZED_MANIFEST_RESOLVED_ID, +} from '../../../dist/manifest/serialized.js'; +import { createBasicSettings } from '../test-utils.js'; + +/** + * Invoke the plugin's load handler (as it runs in dev mode) and return the + * parsed SerializedSSRManifest that is embedded in the generated module code. + */ +async function getManifest(settings) { + const plugin = serializedManifestPlugin({ settings, command: 'dev', sync: false }); + const load = plugin.load; + const result = await load.handler.call({}, SERIALIZED_MANIFEST_RESOLVED_ID); + // The generated code contains: _deserializeManifest(()) + const match = /_deserializeManifest\(\((.+)\)\)/s.exec(result.code); + assert.ok(match, 'Could not find manifest JSON in plugin output'); + return JSON.parse(match[1]); +} + +describe('serializedManifestPlugin - dev mode', () => { + describe('allowedDomains', () => { + it('defaults to an empty array when not configured', async () => { + const settings = await createBasicSettings({}); + const manifest = await getManifest(settings); + assert.deepEqual(manifest.allowedDomains, []); + }); + + it('is an empty array when configured as []', async () => { + const settings = await createBasicSettings({ + security: { allowedDomains: [] }, + }); + const manifest = await getManifest(settings); + assert.deepEqual(manifest.allowedDomains, []); + }); + + it('preserves a single hostname pattern', async () => { + const pattern = [{ hostname: 'example.com' }]; + const settings = await createBasicSettings({ + security: { allowedDomains: pattern }, + }); + const manifest = await getManifest(settings); + assert.deepEqual(manifest.allowedDomains, pattern); + }); + + it('preserves multiple patterns with protocol and port', async () => { + const patterns = [ + { hostname: '*.example.com', protocol: 'https' }, + { hostname: 'cdn.example.com', port: '443' }, + ]; + const settings = await createBasicSettings({ + security: { allowedDomains: patterns }, + }); + const manifest = await getManifest(settings); + assert.deepEqual(manifest.allowedDomains, patterns); + }); + }); + + describe('checkOrigin', () => { + it('is false by default', async () => { + const settings = await createBasicSettings({}); + const manifest = await getManifest(settings); + assert.equal(manifest.checkOrigin, false); + }); + + it('is false when checkOrigin=true but buildOutput is not server', async () => { + const settings = await createBasicSettings({ + security: { checkOrigin: true }, + }); + settings.buildOutput = 'static'; + const manifest = await getManifest(settings); + assert.equal(manifest.checkOrigin, false); + }); + + it('is true when checkOrigin=true and buildOutput is server', async () => { + const settings = await createBasicSettings({ + security: { checkOrigin: true }, + }); + settings.buildOutput = 'server'; + const manifest = await getManifest(settings); + assert.equal(manifest.checkOrigin, true); + }); + }); + + describe('actionBodySizeLimit', () => { + it('defaults to 1 MB when not configured', async () => { + const settings = await createBasicSettings({}); + const manifest = await getManifest(settings); + assert.equal(manifest.actionBodySizeLimit, 1024 * 1024); + }); + + it('uses the configured value', async () => { + const settings = await createBasicSettings({ + security: { actionBodySizeLimit: 2097152 }, + }); + const manifest = await getManifest(settings); + assert.equal(manifest.actionBodySizeLimit, 2097152); + }); + }); + + describe('serverIslandBodySizeLimit', () => { + it('defaults to 1 MB when not configured', async () => { + const settings = await createBasicSettings({}); + const manifest = await getManifest(settings); + assert.equal(manifest.serverIslandBodySizeLimit, 1024 * 1024); + }); + + it('uses the configured value', async () => { + const settings = await createBasicSettings({ + security: { serverIslandBodySizeLimit: 512 }, + }); + const manifest = await getManifest(settings); + assert.equal(manifest.serverIslandBodySizeLimit, 512); + }); + }); + + describe('serverLike', () => { + it('is true when buildOutput is server', async () => { + const settings = await createBasicSettings({}); + settings.buildOutput = 'server'; + const manifest = await getManifest(settings); + assert.equal(manifest.serverLike, true); + }); + + it('is false when buildOutput is static', async () => { + const settings = await createBasicSettings({}); + settings.buildOutput = 'static'; + const manifest = await getManifest(settings); + assert.equal(manifest.serverLike, false); + }); + + it('is false when buildOutput is undefined', async () => { + const settings = await createBasicSettings({}); + settings.buildOutput = undefined; + const manifest = await getManifest(settings); + assert.equal(manifest.serverLike, false); + }); + }); + + describe('trailingSlash', () => { + for (const value of ['always', 'never', 'ignore']) { + it(`preserves trailingSlash="${value}"`, async () => { + const settings = await createBasicSettings({ trailingSlash: value }); + const manifest = await getManifest(settings); + assert.equal(manifest.trailingSlash, value); + }); + } + }); + + describe('base', () => { + it('preserves base="/"', async () => { + const settings = await createBasicSettings({ base: '/' }); + const manifest = await getManifest(settings); + assert.equal(manifest.base, '/'); + }); + + it('preserves base="/subpath/"', async () => { + const settings = await createBasicSettings({ base: '/subpath/' }); + const manifest = await getManifest(settings); + assert.equal(manifest.base, '/subpath/'); + }); + }); + + describe('compressHTML', () => { + it('is true by default', async () => { + const settings = await createBasicSettings({}); + const manifest = await getManifest(settings); + assert.equal(manifest.compressHTML, true); + }); + + it('is false when explicitly disabled', async () => { + const settings = await createBasicSettings({ compressHTML: false }); + const manifest = await getManifest(settings); + assert.equal(manifest.compressHTML, false); + }); + }); + + describe('i18n', () => { + it('is undefined when not configured', async () => { + const settings = await createBasicSettings({}); + const manifest = await getManifest(settings); + assert.equal(manifest.i18n, undefined); + }); + + it('includes expected fields when configured', async () => { + const settings = await createBasicSettings({ + i18n: { + defaultLocale: 'en', + locales: ['en', 'fr'], + fallback: { fr: 'en' }, + }, + }); + const manifest = await getManifest(settings); + assert.ok(manifest.i18n, 'i18n should be defined'); + assert.equal(manifest.i18n.defaultLocale, 'en'); + assert.deepEqual(manifest.i18n.locales, ['en', 'fr']); + assert.deepEqual(manifest.i18n.fallback, { fr: 'en' }); + assert.ok('strategy' in manifest.i18n, 'strategy should be present'); + assert.ok('fallbackType' in manifest.i18n, 'fallbackType should be present'); + assert.ok('domainLookupTable' in manifest.i18n, 'domainLookupTable should be present'); + }); + }); + + describe('key', () => { + it('embeds a non-empty encoded key string', async () => { + const settings = await createBasicSettings({}); + const manifest = await getManifest(settings); + assert.ok(typeof manifest.key === 'string' && manifest.key.length > 0); + }); + }); + + describe('directory paths', () => { + it('serializes directory URLs to strings', async () => { + const settings = await createBasicSettings({}); + const manifest = await getManifest(settings); + assert.equal(typeof manifest.rootDir, 'string'); + assert.equal(typeof manifest.srcDir, 'string'); + assert.equal(typeof manifest.outDir, 'string'); + assert.equal(typeof manifest.cacheDir, 'string'); + assert.equal(typeof manifest.publicDir, 'string'); + assert.equal(typeof manifest.buildClientDir, 'string'); + assert.equal(typeof manifest.buildServerDir, 'string'); + }); + }); +}); diff --git a/packages/astro/test/units/middleware/call-middleware.test.js b/packages/astro/test/units/middleware/call-middleware.test.ts similarity index 80% rename from packages/astro/test/units/middleware/call-middleware.test.js rename to packages/astro/test/units/middleware/call-middleware.test.ts index f73d92962bfc..9ebb18d81e75 100644 --- a/packages/astro/test/units/middleware/call-middleware.test.js +++ b/packages/astro/test/units/middleware/call-middleware.test.ts @@ -1,12 +1,12 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it, beforeEach } from 'node:test'; import { callMiddleware } from '../../../dist/core/middleware/callMiddleware.js'; -import { createMockAPIContext, createResponseFunction } from '../mocks.js'; +import { createMockAPIContext, createResponseFunction } from '../mocks.ts'; + +import type { APIContext, MiddlewareHandler } from 'astro'; describe('callMiddleware', () => { - /** @type {import('astro').APIContext} */ - let ctx; + let ctx: APIContext; const defaultResponseFn = createResponseFunction(); beforeEach(() => { @@ -15,7 +15,7 @@ describe('callMiddleware', () => { describe('next() called', () => { it('returns the middleware return value when next() is called and a Response is returned', async () => { - const middleware = async (_ctx, next) => { + const middleware: MiddlewareHandler = async (_ctx, next) => { const response = await next(); return new Response('modified', { status: 200, headers: response.headers }); }; @@ -26,7 +26,7 @@ describe('callMiddleware', () => { }); it('returns the responseFunction result when next() is called but middleware returns undefined', async () => { - const middleware = async (_ctx, next) => { + const middleware: MiddlewareHandler = async (_ctx, next) => { await next(); // deliberately returns undefined }; @@ -37,14 +37,14 @@ describe('callMiddleware', () => { }); it('throws MiddlewareNotAResponse when next() is called but middleware returns a non-Response', async () => { - const middleware = async (_ctx, next) => { + const middleware: MiddlewareHandler = async (_ctx, next) => { await next(); - return 'not a response'; + return 'not a response' as unknown as Response; }; await assert.rejects( () => callMiddleware(middleware, ctx, defaultResponseFn), - (err) => { + (err: Error) => { assert.equal(err.name, 'MiddlewareNotAResponse'); return true; }, @@ -54,7 +54,7 @@ describe('callMiddleware', () => { describe('next() not called', () => { it('returns the Response when middleware short-circuits without calling next()', async () => { - const middleware = async () => { + const middleware: MiddlewareHandler = async () => { return new Response('short-circuit', { status: 200 }); }; @@ -65,7 +65,7 @@ describe('callMiddleware', () => { }); it('returns a 500 Response when middleware short-circuits with an error status', async () => { - const middleware = async () => { + const middleware: MiddlewareHandler = async () => { return new Response(null, { status: 500 }); }; @@ -75,13 +75,13 @@ describe('callMiddleware', () => { }); it('throws MiddlewareNoDataOrNextCalled when middleware returns undefined without calling next()', async () => { - const middleware = async () => { + const middleware: MiddlewareHandler = async () => { // returns undefined, never calls next }; await assert.rejects( () => callMiddleware(middleware, ctx, defaultResponseFn), - (err) => { + (err: Error) => { assert.equal(err.name, 'MiddlewareNoDataOrNextCalled'); return true; }, @@ -89,13 +89,13 @@ describe('callMiddleware', () => { }); it('throws MiddlewareNotAResponse when middleware returns a non-Response without calling next()', async () => { - const middleware = async () => { - return 'not a response'; + const middleware: MiddlewareHandler = async () => { + return 'not a response' as unknown as Response; }; await assert.rejects( () => callMiddleware(middleware, ctx, defaultResponseFn), - (err) => { + (err: Error) => { assert.equal(err.name, 'MiddlewareNotAResponse'); return true; }, @@ -105,12 +105,12 @@ describe('callMiddleware', () => { describe('context mutation', () => { it('locals mutations are visible in the response function', async () => { - const middleware = async (context, next) => { - context.locals.name = 'bar'; + const middleware: MiddlewareHandler = async (context, next) => { + (context.locals as Record).name = 'bar'; return next(); }; - const responseFn = async (apiCtx) => { - return new Response(`name=${apiCtx.locals.name}`); + const responseFn = async (apiCtx: APIContext) => { + return new Response(`name=${(apiCtx.locals as Record).name}`); }; const response = await callMiddleware(middleware, ctx, responseFn); @@ -119,7 +119,7 @@ describe('callMiddleware', () => { }); it('middleware can set response headers after calling next()', async () => { - const middleware = async (_context, next) => { + const middleware: MiddlewareHandler = async (_context, next) => { const response = await next(); response.headers.set('X-Custom', 'value'); return response; @@ -131,7 +131,7 @@ describe('callMiddleware', () => { }); it('middleware can clone the response, modify body, and return a new Response', async () => { - const middleware = async (_context, next) => { + const middleware: MiddlewareHandler = async (_context, next) => { const response = await next(); const cloned = response.clone(); const html = await cloned.text(); @@ -149,7 +149,7 @@ describe('callMiddleware', () => { }); it('middleware can intercept a JSON response, modify it, and return a new Response', async () => { - const middleware = async (_context, next) => { + const middleware: MiddlewareHandler = async (_context, next) => { const response = await next(); const data = await response.json(); data.name = 'REDACTED'; @@ -174,7 +174,7 @@ describe('callMiddleware', () => { describe('synchronous middleware', () => { it('works with a synchronous middleware that calls next()', async () => { - const middleware = (_context, next) => { + const middleware: MiddlewareHandler = (_context, next) => { return next(); }; @@ -184,7 +184,7 @@ describe('callMiddleware', () => { }); it('works with a synchronous middleware that returns a Response', async () => { - const middleware = () => { + const middleware: MiddlewareHandler = () => { return new Response('sync short-circuit'); }; diff --git a/packages/astro/test/units/middleware/locals.test.js b/packages/astro/test/units/middleware/locals.test.ts similarity index 95% rename from packages/astro/test/units/middleware/locals.test.js rename to packages/astro/test/units/middleware/locals.test.ts index eada9afbadbb..1a896a18f4db 100644 --- a/packages/astro/test/units/middleware/locals.test.js +++ b/packages/astro/test/units/middleware/locals.test.ts @@ -1,4 +1,3 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { isLocalsSerializable, trySerializeLocals } from '../../../dist/core/middleware/index.js'; @@ -56,8 +55,9 @@ describe('isLocalsSerializable', () => { it('handles deeply nested objects without stack overflow (iterative implementation)', () => { // Build a 10,000-level deep object — would overflow the call stack with recursion - let deep = /** @type {any} */ ({}); - let current = deep; + type DeepObject = { child?: DeepObject; value?: string }; + const deep: DeepObject = {}; + let current: DeepObject = deep; for (let i = 0; i < 10_000; i++) { current.child = {}; current = current.child; diff --git a/packages/astro/test/units/middleware/middleware-app.test.js b/packages/astro/test/units/middleware/middleware-app.test.ts similarity index 88% rename from packages/astro/test/units/middleware/middleware-app.test.js rename to packages/astro/test/units/middleware/middleware-app.test.ts index 02b5d968d30e..790498eb3994 100644 --- a/packages/astro/test/units/middleware/middleware-app.test.js +++ b/packages/astro/test/units/middleware/middleware-app.test.ts @@ -1,29 +1,36 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { App } from '../../../dist/core/app/app.js'; import { createComponent, render } from '../../../dist/runtime/server/index.js'; -import { createRouteData } from '../mocks.js'; -import { createManifest } from '../app/test-helpers.js'; +import { createRouteData } from '../mocks.ts'; +import { createManifest } from '../app/test-helpers.ts'; + +import type { MiddlewareHandler } from 'astro'; +import type { RouteData } from '../../../dist/types/public/internal.js'; /** * Helper: creates an App with the given middleware and routes. - * @param {object} opts - * @param {import('astro').MiddlewareHandler} opts.onRequest - The middleware handler - * @param {Array<{ routeData: any; component?: any }>} opts.routes - Route definitions - * @param {Map} opts.pageMap - Component map - * @param {string} [opts.base] */ -function createAppWithMiddleware({ onRequest, routes, pageMap, base }) { +function createAppWithMiddleware({ + onRequest, + routes, + pageMap, + base, +}: { + onRequest: MiddlewareHandler; + routes: Array<{ routeData: RouteData; component?: unknown }>; + pageMap: Map Promise>>; + base?: string; +}): App { const manifest = createManifest({ - routes: routes.map((r) => ({ routeData: r.routeData })), - pageMap, + routes: routes.map((r) => ({ routeData: r.routeData })) as any, + pageMap: pageMap as any, base, }); // Override the middleware field — createManifest sets it to undefined, // but the pipeline reads it from manifest.middleware - manifest.middleware = () => ({ onRequest }); - return new App(manifest); + (manifest as any).middleware = () => ({ onRequest }); + return new App(manifest as any); } // ----- Shared route data ----- @@ -46,17 +53,17 @@ const spacesRouteData = createRouteData({ // ----- Shared page components ----- const simplePage = (localKey = 'name') => - createComponent((result, props, slots) => { + createComponent((result: any, props: any, slots: any) => { const Astro = result.createAstro(props, slots); return render`

${Astro.locals[localKey]}

`; }); -const notFoundPage = createComponent((result, props, slots) => { +const notFoundPage = createComponent((result: any, props: any, slots: any) => { const Astro = result.createAstro(props, slots); return render`Error

${Astro.locals.name}

`; }); -const serverErrorPage = createComponent((result, props, slots) => { +const serverErrorPage = createComponent((result: any, props: any, slots: any) => { const Astro = result.createAstro(props, slots); return render`500

${Astro.locals.name}

`; }); @@ -65,7 +72,7 @@ const throwingPage = createComponent(() => { throw new Error('page threw an error'); }); -const cookiePage = createComponent((result, props, slots) => { +const cookiePage = createComponent((result: any, props: any, slots: any) => { const Astro = result.createAstro(props, slots); Astro.cookies.set('from-component', 'component-value'); return render`

cookies

`; @@ -76,8 +83,8 @@ const cookiePage = createComponent((result, props, slots) => { describe('Middleware via App.render()', () => { describe('locals', () => { it('should render locals data set by middleware', async () => { - const onRequest = async (ctx, next) => { - ctx.locals.name = 'bar'; + const onRequest: MiddlewareHandler = async (ctx, next) => { + (ctx.locals as Record).name = 'bar'; return next(); }; const pageMap = new Map([ @@ -96,11 +103,11 @@ describe('Middleware via App.render()', () => { }); it('should change locals data based on URL', async () => { - const onRequest = async (ctx, next) => { + const onRequest: MiddlewareHandler = async (ctx, next) => { if (ctx.url.pathname === '/lorem') { - ctx.locals.name = 'ipsum'; + (ctx.locals as Record).name = 'ipsum'; } else { - ctx.locals.name = 'bar'; + (ctx.locals as Record).name = 'bar'; } return next(); }; @@ -126,17 +133,17 @@ describe('Middleware via App.render()', () => { describe('sequence', () => { it('should call a second middleware in a sequence via manifest', async () => { // We test sequence by making the manifest middleware itself a sequence. - // sequence() is already tested in sequence.test.js; here we verify it works + // sequence() is already tested in sequence.test.ts; here we verify it works // when wired through the App pipeline. const { sequence } = await import('../../../dist/core/middleware/sequence.js'); - const first = async (ctx, next) => { - ctx.locals.name = 'first'; + const first: MiddlewareHandler = async (ctx, next) => { + (ctx.locals as Record).name = 'first'; return next(); }; - const second = async (ctx, next) => { + const second: MiddlewareHandler = async (ctx, next) => { if (ctx.url.pathname === '/second') { - ctx.locals.name = 'second'; + (ctx.locals as Record).name = 'second'; } return next(); }; @@ -162,7 +169,7 @@ describe('Middleware via App.render()', () => { describe('short-circuit responses', () => { it('should successfully create a new response bypassing the page', async () => { - const onRequest = async (ctx, next) => { + const onRequest: MiddlewareHandler = async (ctx, next) => { if (ctx.url.pathname === '/rewrite') { return new Response('New content!!', { status: 200 }); } @@ -187,7 +194,7 @@ describe('Middleware via App.render()', () => { }); it('should return a new response that is a 500', async () => { - const onRequest = async (ctx, next) => { + const onRequest: MiddlewareHandler = async (ctx, next) => { if (ctx.url.pathname === '/broken-500') { return new Response(null, { status: 500 }); } @@ -210,7 +217,7 @@ describe('Middleware via App.render()', () => { }); it('should return 200 if middleware returns a 200 Response for a non-existent route', async () => { - const onRequest = async (ctx, next) => { + const onRequest: MiddlewareHandler = async (ctx, next) => { if (ctx.url.pathname === '/no-route-but-200') { return new Response("It's OK!", { status: 200 }); } @@ -238,7 +245,7 @@ describe('Middleware via App.render()', () => { describe('pass-through middleware', () => { it('should render the page normally if middleware only calls next()', async () => { - const onRequest = async (_ctx, next) => { + const onRequest: MiddlewareHandler = async (_ctx, next) => { return next(); }; const pageMap = new Map([ @@ -262,8 +269,8 @@ describe('Middleware via App.render()', () => { describe('error handling', () => { it('should throw when middleware returns undefined without calling next()', async () => { - const onRequest = async () => { - return undefined; + const onRequest: MiddlewareHandler = async () => { + return undefined as unknown as Response; }; const pageMap = new Map([ [indexRouteData.component, async () => ({ page: async () => ({ default: simplePage() }) })], @@ -280,7 +287,7 @@ describe('Middleware via App.render()', () => { }); it('should render 500.astro when middleware throws an error', async () => { - const onRequest = async (ctx, next) => { + const onRequest: MiddlewareHandler = async (ctx, next) => { if (ctx.url.pathname === '/throw') { throw new Error('middleware error'); } @@ -307,7 +314,7 @@ describe('Middleware via App.render()', () => { describe('redirect', () => { it('should successfully redirect to another page', async () => { - const onRequest = async (ctx, next) => { + const onRequest: MiddlewareHandler = async (ctx, next) => { if (ctx.url.pathname === '/redirect') { return ctx.redirect('/', 302); } @@ -334,7 +341,7 @@ describe('Middleware via App.render()', () => { describe('cookies', () => { it('should allow middleware to set cookies', async () => { - const onRequest = async (ctx, next) => { + const onRequest: MiddlewareHandler = async (ctx, next) => { ctx.cookies.set('foo', 'bar'); return next(); }; @@ -358,7 +365,7 @@ describe('Middleware via App.render()', () => { }); it('should forward cookies set in a component when middleware returns a new response', async () => { - const onRequest = async (_ctx, next) => { + const onRequest: MiddlewareHandler = async (_ctx, next) => { const response = await next(); const html = await response.text(); return new Response(html, { status: 200, headers: response.headers }); @@ -384,7 +391,7 @@ describe('Middleware via App.render()', () => { describe('response modification', () => { it('should be able to clone the response and modify it', async () => { - const onRequest = async (_ctx, next) => { + const onRequest: MiddlewareHandler = async (_ctx, next) => { const response = await next(); const cloned = response.clone(); const html = await cloned.text(); @@ -413,7 +420,7 @@ describe('Middleware via App.render()', () => { describe('API endpoints', () => { it('should correctly work for API endpoints that return a Response object', async () => { - const onRequest = async (_ctx, next) => { + const onRequest: MiddlewareHandler = async (_ctx, next) => { return next(); }; const pageMap = new Map([ @@ -444,7 +451,7 @@ describe('Middleware via App.render()', () => { }); it('should correctly manipulate the response coming from API endpoints', async () => { - const onRequest = async (ctx, next) => { + const onRequest: MiddlewareHandler = async (ctx, next) => { if (ctx.url.pathname === '/api/endpoint') { const response = await next(); const data = await response.json(); @@ -484,8 +491,8 @@ describe('Middleware via App.render()', () => { describe('404 handling', () => { it('should correctly call middleware for 404 routes', async () => { - const onRequest = async (ctx, next) => { - ctx.locals.name = 'bar'; + const onRequest: MiddlewareHandler = async (ctx, next) => { + (ctx.locals as Record).name = 'bar'; return next(); }; const pageMap = new Map([ @@ -513,7 +520,7 @@ describe('Middleware via App.render()', () => { /** * Auth middleware that protects /admin */ - const authMiddleware = async (ctx, next) => { + const authMiddleware: MiddlewareHandler = async (ctx, next) => { if (ctx.url.pathname === '/admin') { const authToken = ctx.request.headers.get('Authorization'); if (!authToken) { @@ -523,7 +530,7 @@ describe('Middleware via App.render()', () => { return next(); }; - function createAuthApp() { + function createAuthApp(): App { const page = simplePage(); const pageMap = new Map([ [adminRouteData.component, async () => ({ page: async () => ({ default: page }) })], @@ -565,7 +572,7 @@ describe('Middleware via App.render()', () => { }); it('should handle requests with spaces in path correctly', async () => { - const onRequest = async (_ctx, next) => { + const onRequest: MiddlewareHandler = async (_ctx, next) => { return next(); }; const spacesPage = createComponent(() => { @@ -589,7 +596,7 @@ describe('Middleware via App.render()', () => { describe('cookies on error pages', () => { it('should preserve cookies set by middleware when returning Response(null, { status: 404 })', async () => { // Middleware sets a cookie and returns 404 with null body (common auth guard pattern) - const onRequest = async (ctx, next) => { + const onRequest: MiddlewareHandler = async (ctx, next) => { ctx.cookies.set('session', 'abc123', { path: '/' }); if (ctx.url.pathname.startsWith('/api/guarded')) { return new Response(null, { status: 404 }); @@ -599,8 +606,8 @@ describe('Middleware via App.render()', () => { const guardedRouteData = createRouteData({ route: '/api/guarded/[...path]', - pathname: undefined, - segments: undefined, + pathname: undefined as unknown as string, + segments: undefined as unknown as undefined, }); // Override for spread route guardedRouteData.params = ['...path']; @@ -643,7 +650,7 @@ describe('Middleware via App.render()', () => { }); it('should preserve cookies set by middleware when returning Response(null, { status: 500 })', async () => { - const onRequest = async (ctx, next) => { + const onRequest: MiddlewareHandler = async (ctx, next) => { ctx.cookies.set('csrf', 'token456', { path: '/' }); if (ctx.url.pathname.startsWith('/api/error')) { return new Response(null, { status: 500 }); @@ -653,8 +660,8 @@ describe('Middleware via App.render()', () => { const errorRouteData = createRouteData({ route: '/api/error/[...path]', - pathname: undefined, - segments: undefined, + pathname: undefined as unknown as string, + segments: undefined as unknown as undefined, }); errorRouteData.params = ['...path']; errorRouteData.pattern = /^\/api\/error(?:\/(.*))?$/; @@ -696,7 +703,7 @@ describe('Middleware via App.render()', () => { }); it('should preserve multiple cookies from sequenced middleware during error page rerouting', async () => { - const onRequest = async (ctx, next) => { + const onRequest: MiddlewareHandler = async (ctx, next) => { ctx.cookies.set('session', 'abc123', { path: '/' }); ctx.cookies.set('csrf', 'token456', { path: '/' }); if (ctx.url.pathname.startsWith('/api/guarded')) { @@ -708,8 +715,8 @@ describe('Middleware via App.render()', () => { const guardedRouteData = createRouteData({ route: '/api/guarded/[...path]', - pathname: undefined, - segments: undefined, + pathname: undefined as unknown as string, + segments: undefined as unknown as undefined, }); guardedRouteData.params = ['...path']; guardedRouteData.pattern = /^\/api\/guarded(?:\/(.*))?$/; @@ -767,7 +774,7 @@ describe('Middleware via App.render()', () => { // Middleware calls next(), then decides to return 404 with a stale Content-Length header. // On re-render for the error page, middleware passes the response through unchanged. let callCount = 0; - const onRequest = async (ctx, next) => { + const onRequest: MiddlewareHandler = async (ctx, next) => { callCount++; const response = await next(); if (callCount === 1 && ctx.url.pathname.startsWith('/api/guarded')) { @@ -781,8 +788,8 @@ describe('Middleware via App.render()', () => { const guardedRouteData = createRouteData({ route: '/api/guarded/[...path]', - pathname: undefined, - segments: undefined, + pathname: undefined as unknown as string, + segments: undefined as unknown as undefined, }); guardedRouteData.params = ['...path']; guardedRouteData.pattern = /^\/api\/guarded(?:\/(.*))?$/; @@ -828,7 +835,7 @@ describe('Middleware via App.render()', () => { it('should not preserve Transfer-Encoding from middleware when rendering 500 error page', async () => { let callCount = 0; - const onRequest = async (ctx, next) => { + const onRequest: MiddlewareHandler = async (ctx, next) => { callCount++; const response = await next(); if (callCount === 1 && ctx.url.pathname.startsWith('/api/error')) { @@ -842,8 +849,8 @@ describe('Middleware via App.render()', () => { const errorRouteData = createRouteData({ route: '/api/error/[...path]', - pathname: undefined, - segments: undefined, + pathname: undefined as unknown as string, + segments: undefined as unknown as undefined, }); errorRouteData.params = ['...path']; errorRouteData.pattern = /^\/api\/error(?:\/(.*))?$/; @@ -890,7 +897,7 @@ describe('Middleware via App.render()', () => { describe('middleware with custom headers', () => { it('should correctly set custom headers in middleware', async () => { - const onRequest = async (_ctx, next) => { + const onRequest: MiddlewareHandler = async (_ctx, next) => { const response = await next(); response.headers.set('X-Custom-Header', 'custom-value'); return response; diff --git a/packages/astro/test/units/middleware/sequence.test.js b/packages/astro/test/units/middleware/sequence.test.ts similarity index 66% rename from packages/astro/test/units/middleware/sequence.test.js rename to packages/astro/test/units/middleware/sequence.test.ts index 452881c1273d..58c1e81bbba9 100644 --- a/packages/astro/test/units/middleware/sequence.test.js +++ b/packages/astro/test/units/middleware/sequence.test.ts @@ -1,13 +1,13 @@ -// @ts-check import assert from 'node:assert/strict'; import { beforeEach, describe, it } from 'node:test'; import { callMiddleware } from '../../../dist/core/middleware/callMiddleware.js'; import { sequence } from '../../../dist/core/middleware/sequence.js'; -import { createMockAPIContext, createResponseFunction } from '../mocks.js'; +import { createMockAPIContext, createResponseFunction } from '../mocks.ts'; + +import type { APIContext, MiddlewareHandler } from 'astro'; describe('sequence', () => { - /** @type {import('astro').APIContext} */ - let globaCtx; + let globaCtx: APIContext; beforeEach(() => { globaCtx = createMockAPIContext(); @@ -23,8 +23,8 @@ describe('sequence', () => { }); it('works with a single handler', async () => { - const handler = async (ctx, next) => { - ctx.locals.touched = true; + const handler: MiddlewareHandler = async (ctx, next) => { + (ctx.locals as Record).touched = true; return next(); }; const combined = sequence(handler); @@ -33,20 +33,20 @@ describe('sequence', () => { const response = await callMiddleware(combined, globaCtx, responseFn); assert.equal(await response.text(), 'single'); - assert.equal(globaCtx.locals.touched, true); + assert.equal((globaCtx.locals as Record).touched, true); }); it('executes handlers in order', async () => { - const order = []; - const handler1 = async (_ctx, next) => { + const order: number[] = []; + const handler1: MiddlewareHandler = async (_ctx, next) => { order.push(1); return next(); }; - const handler2 = async (_ctx, next) => { + const handler2: MiddlewareHandler = async (_ctx, next) => { order.push(2); return next(); }; - const handler3 = async (_ctx, next) => { + const handler3: MiddlewareHandler = async (_ctx, next) => { order.push(3); return next(); }; @@ -59,18 +59,20 @@ describe('sequence', () => { }); it('propagates context mutations across handlers', async () => { - const first = async (ctx, next) => { - ctx.locals.first = 'a'; + const first: MiddlewareHandler = async (ctx, next) => { + (ctx.locals as Record).first = 'a'; return next(); }; - const second = async (ctx, next) => { + const second: MiddlewareHandler = async (ctx, next) => { + const locals = ctx.locals as Record; // should see mutation from first - ctx.locals.second = ctx.locals.first + 'b'; + locals.second = (locals.first as string) + 'b'; return next(); }; const combined = sequence(first, second); - const responseFn = async (apiCtx) => { - return new Response(`${apiCtx.locals.first}-${apiCtx.locals.second}`); + const responseFn = async (apiCtx: APIContext) => { + const locals = apiCtx.locals as Record; + return new Response(`${locals.first}-${locals.second}`); }; const response = await callMiddleware(combined, globaCtx, responseFn); @@ -79,10 +81,10 @@ describe('sequence', () => { }); it('allows the last handler to modify the response from the page', async () => { - const handler1 = async (_ctx, next) => { + const handler1: MiddlewareHandler = async (_ctx, next) => { return next(); }; - const handler2 = async (_ctx, next) => { + const handler2: MiddlewareHandler = async (_ctx, next) => { const response = await next(); const text = await response.text(); return new Response(text.toUpperCase()); @@ -96,11 +98,11 @@ describe('sequence', () => { }); it('supports mixed sync and async handlers', async () => { - const syncHandler = (_ctx, next) => { + const syncHandler: MiddlewareHandler = (_ctx, next) => { return next(); }; - const asyncHandler = async (ctx, next) => { - ctx.locals.async = true; + const asyncHandler: MiddlewareHandler = async (ctx, next) => { + (ctx.locals as Record).async = true; return await next(); }; const combined = sequence(syncHandler, asyncHandler); @@ -109,20 +111,20 @@ describe('sequence', () => { const response = await callMiddleware(combined, globaCtx, responseFn); assert.equal(await response.text(), 'mixed'); - assert.equal(globaCtx.locals.async, true); + assert.equal((globaCtx.locals as Record).async, true); }); it('filters out falsy handlers', async () => { - const order = []; - const handler1 = async (_ctx, next) => { + const order: number[] = []; + const handler1: MiddlewareHandler = async (_ctx, next) => { order.push(1); return next(); }; - const handler2 = async (_ctx, next) => { + const handler2: MiddlewareHandler = async (_ctx, next) => { order.push(2); return next(); }; - const combined = sequence(handler1, null, undefined, handler2); + const combined = sequence(handler1, null as any, undefined as any, handler2); const responseFn = createResponseFunction(); await callMiddleware(combined, globaCtx, responseFn); @@ -131,12 +133,12 @@ describe('sequence', () => { }); it('allows earlier handlers to short-circuit the chain', async () => { - const order = []; - const handler1 = async () => { + const order: number[] = []; + const handler1: MiddlewareHandler = async () => { order.push(1); return new Response('short-circuit'); }; - const handler2 = async (_ctx, next) => { + const handler2: MiddlewareHandler = async (_ctx, next) => { order.push(2); return next(); }; @@ -150,11 +152,11 @@ describe('sequence', () => { }); it('accumulates cookies set by multiple handlers', async () => { - const handler1 = async (ctx, next) => { + const handler1: MiddlewareHandler = async (ctx, next) => { ctx.cookies.set('cookie1', 'value1'); return next(); }; - const handler2 = async (ctx, next) => { + const handler2: MiddlewareHandler = async (ctx, next) => { ctx.cookies.set('cookie2', 'value2'); return next(); }; @@ -168,14 +170,14 @@ describe('sequence', () => { }); it('handles a chain where middle handler returns a redirect', async () => { - const handler1 = async (ctx, next) => { - ctx.locals.beforeRedirect = true; + const handler1: MiddlewareHandler = async (ctx, next) => { + (ctx.locals as Record).beforeRedirect = true; return next(); }; - const handler2 = async (ctx) => { + const handler2: MiddlewareHandler = async (ctx) => { return ctx.redirect('/login'); }; - const handler3 = async (_ctx, next) => { + const handler3: MiddlewareHandler = async (_ctx, next) => { // should never be called return next(); }; @@ -186,6 +188,6 @@ describe('sequence', () => { assert.equal(response.status, 302); assert.equal(response.headers.get('Location'), '/login'); - assert.equal(globaCtx.locals.beforeRedirect, true); + assert.equal((globaCtx.locals as Record).beforeRedirect, true); }); }); diff --git a/packages/astro/test/units/mocks.js b/packages/astro/test/units/mocks.ts similarity index 57% rename from packages/astro/test/units/mocks.js rename to packages/astro/test/units/mocks.ts index 8aaf50cfe89a..870e022a2a5b 100644 --- a/packages/astro/test/units/mocks.js +++ b/packages/astro/test/units/mocks.ts @@ -1,5 +1,5 @@ -import { createBasicPipeline } from './test-utils.js'; -import { makeRoute, staticPart } from './routing/test-helpers.js'; +import { createBasicPipeline } from './test-utils.ts'; +import { makeRoute, staticPart } from './routing/test-helpers.ts'; import { AstroCookies } from '../../dist/core/cookies/index.js'; import { App } from '../../dist/core/app/app.js'; import { baseService } from '../../dist/assets/services/service.js'; @@ -10,7 +10,14 @@ import { renderComponent, spreadAttributes, } from '../../dist/runtime/server/index.js'; -import { createManifest, createRouteInfo } from './app/test-helpers.js'; +import { createManifest, createRouteInfo } from './app/test-helpers.ts'; + +import type { Pipeline } from '../../dist/core/render/index.js'; +import type { RouteData, RoutePart, RouteType } from '../../dist/types/public/internal.js'; +import type { APIContext } from '../../dist/types/public/context.js'; +import type { SSRManifest, RouteInfo } from '../../dist/core/app/types.js'; +import type { AstroComponentFactory } from '../../dist/runtime/server/render/index.js'; +import type { ImageTransform } from '../../dist/assets/types.js'; /** * Mock utilities for unit tests. @@ -20,35 +27,29 @@ import { createManifest, createRouteInfo } from './app/test-helpers.js'; * in their respective directories. */ +interface MockRenderContextOverrides { + request?: Request; + routeData?: Partial; + params?: Record; + pipeline?: Pipeline; + [key: string]: unknown; +} + /** * Creates a minimal RenderContext mock for unit testing redirect functions. * * This is a lightweight mock that provides only what renderRedirect() needs, * without the overhead of creating a full RenderContext instance. - * - * @param {object} overrides - Properties to override - * @param {Request} [overrides.request] - The request object - * @param {object} [overrides.routeData] - Route data including redirect config - * @param {Record} [overrides.params] - Route parameters - * @param {object} [overrides.pipeline] - Pipeline instance - * @returns {object} A mock render context suitable for testing renderRedirect - * - * @example - * const context = createMockRenderContext({ - * request: new Request('http://localhost/source'), - * routeData: { type: 'redirect', redirect: '/target' }, - * params: { slug: 'my-post' } - * }); */ -export function createMockRenderContext(overrides = {}) { +export function createMockRenderContext(overrides: MockRenderContextOverrides = {}) { const pipeline = overrides.pipeline || createBasicPipeline({ manifest: { - rootDir: import.meta.url, + rootDir: new URL(import.meta.url), experimentalQueuedRendering: { enabled: true }, trailingSlash: 'never', - }, + } as unknown as SSRManifest, }); return { @@ -60,22 +61,23 @@ export function createMockRenderContext(overrides = {}) { }; } +interface MockAPIContextOverrides extends Partial> { + url?: string | URL; +} + /** * Creates a mock APIContext suitable for calling middleware directly via `callMiddleware()`. * * All fields can be overridden. The `cookies` field uses the real `AstroCookies` class * by default to avoid mock drift. - * - * @param {Partial & { url?: string | URL }} overrides - * @returns {import('astro').APIContext} */ -export function createMockAPIContext(overrides = {}) { +export function createMockAPIContext(overrides: MockAPIContextOverrides = {}): APIContext { const url = overrides.url instanceof URL ? overrides.url : new URL(overrides.url ?? 'http://localhost/'); const request = overrides.request ?? new Request(url); const cookies = overrides.cookies ?? new AstroCookies(request); - return /** @type {import('astro').APIContext} */ ({ + return { url, request, locals: overrides.locals ?? {}, @@ -98,30 +100,32 @@ export function createMockAPIContext(overrides = {}) { generator: overrides.generator ?? 'astro-test', clientAddress: overrides.clientAddress ?? '127.0.0.1', originPathname: overrides.originPathname ?? url.pathname, - }); + } as APIContext; } /** * Creates a response function compatible with callMiddleware's third argument. * This simulates what "rendering the page" would return. - * - * @param {string} body - The response body - * @param {ResponseInit} [init] - Optional response init (status, headers, etc.) - * @returns {(ctx: import('astro').APIContext, payload?: unknown) => Promise} */ -export function createResponseFunction(body = 'OK', init = {}) { +export function createResponseFunction( + body = 'OK', + init: ResponseInit = {}, +): (_ctx: APIContext, _payload?: unknown) => Promise { return async (_ctx, _payload) => new Response(body, init); } +interface PageResult { + routeData: RouteData; + module: () => Promise<{ page: () => Promise<{ default: AstroComponentFactory }> }>; +} + /** * Converts a component + route config into the shape expected by createTestApp. - * - * @param {Function} component - A component created via `createComponent()` - * @param {object} routeConfig - Fields passed to createRouteData() - * @param {string} routeConfig.route - The route pattern (e.g. '/about', '/[slug]') - * @returns {{ routeData: object, module: Function }} */ -export function createPage(component, routeConfig) { +export function createPage( + component: AstroComponentFactory, + routeConfig: CreateRouteDataOptions, +): PageResult { const routeData = createRouteData(routeConfig); return { routeData, @@ -131,33 +135,25 @@ export function createPage(component, routeConfig) { /** * Creates an App instance with one or more pages. - * - * @param {Array<{ routeData: object, module: Function }>} pages - Pages created via createPage() - * @param {object} [manifestOverrides] - Extra fields passed to createManifest() - * @returns {import('../../dist/core/app/app.js').App} - * - * @example - * const app = createTestApp([ - * createPage(myComponent, { route: '/about' }), - * createPage(indexComponent, { route: '/', isIndex: true }), - * ]); - * const response = await app.render(new Request('http://example.com/about')); */ -export function createTestApp(pages, manifestOverrides = {}) { - const routes = []; - const pageMap = new Map(); +export function createTestApp( + pages: PageResult[], + manifestOverrides: Record = {}, +): App { + const routes: RouteInfo[] = []; + const pageMap = new Map Promise>>(); for (const { routeData, module } of pages) { - routes.push(createRouteInfo(routeData)); + routes.push(createRouteInfo(routeData) as RouteInfo); pageMap.set(routeData.component, module); } - return new App( - createManifest({ - routes, - pageMap, - ...manifestOverrides, - }), - ); + const manifest = createManifest({ + routes, + pageMap: pageMap as unknown as ReturnType['pageMap'], + ...manifestOverrides, + }); + + return new App(manifest as unknown as SSRManifest); } /** @@ -166,28 +162,22 @@ export function createTestApp(pages, manifestOverrides = {}) { * * Equivalent to: `{Astro.props.class}` */ -export const spreadPropsSpan = createComponent((result, props, slots) => { - const Astro = result.createAstro(props, slots); - return render`${Astro.props.class ?? ''}`; -}); +export const spreadPropsSpan: AstroComponentFactory = createComponent( + (result: any, props: any, slots: any) => { + const Astro = result.createAstro(props, slots); + return render`${Astro.props.class ?? ''}`; + }, +); /** * Creates a page component that renders the given child component once for each * props object in the array. - * - * @param {Function} childComponent - The component to render - * @param {Record[]} propsArray - Array of props objects - * @returns {Function} A page component - * - * @example - * const page = createMultiChildPage(spreadPropsSpan, [ - * { 'class:list': ['foo', 'bar'] }, - * { style: { color: 'red' } }, - * ]); - * const app = createTestApp([createPage(page, { route: '/test' })]); */ -export function createMultiChildPage(childComponent, propsArray) { - return createComponent((result) => { +export function createMultiChildPage( + childComponent: AstroComponentFactory, + propsArray: Record[], +): AstroComponentFactory { + return createComponent((result: any) => { const renders = propsArray.map( (props) => render`${renderComponent(result, 'Child', childComponent, props)}`, ); @@ -195,24 +185,25 @@ export function createMultiChildPage(childComponent, propsArray) { }); } +interface CreateRouteDataOptions { + route: string; + type?: RouteType; + component?: string; + prerender?: boolean; + isIndex?: boolean; + pathname?: string; + segments?: RoutePart[][]; + trailingSlash?: 'always' | 'never' | 'ignore'; +} + /** * Convenience wrapper around `makeRoute` from routing test-helpers. * Auto-generates segments from the route string for simple static routes, * while using the real `getPattern()` for regex generation. - * - * @param {object} overrides - * @param {string} overrides.route - The route pattern (e.g. '/foo', '/api/endpoint') - * @param {'page' | 'endpoint' | 'redirect' | 'fallback'} [overrides.type] - * @param {string} [overrides.component] - * @param {boolean} [overrides.prerender] - * @param {boolean} [overrides.isIndex] - * @param {string} [overrides.pathname] - * @param {import('../../dist/types/public/internal.js').RoutePart[][]} [overrides.segments] - * @param {'always' | 'never' | 'ignore'} [overrides.trailingSlash] */ -export function createRouteData(overrides) { +export function createRouteData(overrides: CreateRouteDataOptions): RouteData { const route = overrides.route; - const segments = + const segments: RoutePart[][] = overrides.segments ?? (route === '/' ? [[]] @@ -239,7 +230,13 @@ export function createRouteData(overrides) { */ const unitTestImageService = { ...baseService, - getURL(options, imageConfig) { + getURL( + options: ImageTransform, + imageConfig: { + domains: string[]; + remotePatterns: { hostname?: string; pathname?: string; protocol?: string; port?: string }[]; + }, + ) { const src = typeof options.src === 'string' ? options.src : options.src.src; // Replicate baseService's allowlist check without import.meta.env.BASE_URL if (typeof options.src === 'string' && !isRemoteAllowed(options.src, imageConfig)) { @@ -256,23 +253,31 @@ const unitTestImageService = { }, }; +interface ImageServiceOverrides { + domains?: string[]; + remotePatterns?: { hostname?: string; pathname?: string; protocol?: string; port?: string }[]; +} + /** * Installs the unit test image service on globalThis so that getImage() * can resolve it without the virtual:image-service Vite module. * Returns the imageConfig object to pass to getImage(), and a cleanup function. * * Use the cleanup function inside the after testing hook. - * - * @param {object} [overrides] - * @param {string[]} [overrides.domains] - * @param {object[]} [overrides.remotePatterns] - * @returns {{ imageConfig: object, cleanup: () => void }} */ -export function installImageService(overrides = {}) { - globalThis.astroAsset = { imageService: unitTestImageService }; +export function installImageService(overrides: ImageServiceOverrides = {}): { + imageConfig: { + service: { entrypoint: string; config: Record }; + domains: string[]; + remotePatterns: { hostname?: string; pathname?: string; protocol?: string; port?: string }[]; + endpoint: { route: string }; + }; + cleanup: () => void; +} { + (globalThis as any).astroAsset = { imageService: unitTestImageService }; const imageConfig = { - service: { entrypoint: 'test', config: {} }, + service: { entrypoint: 'test', config: {} as Record }, domains: overrides.domains ?? [], remotePatterns: overrides.remotePatterns ?? [], endpoint: { route: '/_image' }, @@ -281,16 +286,14 @@ export function installImageService(overrides = {}) { return { imageConfig, cleanup() { - globalThis.astroAsset = undefined; + (globalThis as any).astroAsset = undefined; }, }; } /** * Creates a small Astro source component with an empty frontmatter - * @param html - * @returns {string} */ -export function createMockAstroSource(html) { +export function createMockAstroSource(html: string): string { return `---\n---\n${html}`; } diff --git a/packages/astro/test/units/preferences/dlv.test.ts b/packages/astro/test/units/preferences/dlv.test.ts new file mode 100644 index 000000000000..508199d50873 --- /dev/null +++ b/packages/astro/test/units/preferences/dlv.test.ts @@ -0,0 +1,20 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import dlv from '../../../dist/preferences/dlv.js'; + +describe('dlv', () => { + it('returns correct value', () => { + const data = { a: { b: { c: 42 } } }; + assert.equal(dlv(data, 'a.b.c'), 42); + }); + + it('returns undefined for missing keys', () => { + const data = { a: { b: 2 } }; + assert.equal(dlv(data, 'a.c'), undefined); + }); + + it('returns undefined for missing keys in the middle of the path', () => { + const data = { a: { b: 2 } }; + assert.equal(dlv(data, 'a.c.z'), undefined); + }); +}); diff --git a/packages/astro/test/units/redirects/open-redirect.test.js b/packages/astro/test/units/redirects/open-redirect.test.ts similarity index 100% rename from packages/astro/test/units/redirects/open-redirect.test.js rename to packages/astro/test/units/redirects/open-redirect.test.ts diff --git a/packages/astro/test/units/redirects/render.test.js b/packages/astro/test/units/redirects/render.test.ts similarity index 84% rename from packages/astro/test/units/redirects/render.test.js rename to packages/astro/test/units/redirects/render.test.ts index 0b2785b4d4b6..6ef6eff75c75 100644 --- a/packages/astro/test/units/redirects/render.test.js +++ b/packages/astro/test/units/redirects/render.test.ts @@ -6,7 +6,10 @@ import { renderRedirect, resolveRedirectTarget, } from '../../../dist/core/redirects/render.js'; -import { createMockRenderContext } from '../mocks.js'; +import { createMockRenderContext } from '../mocks.ts'; + +import type { RenderContext } from '../../../dist/core/render-context.js'; +import type { RouteData } from '../../../dist/types/public/internal.js'; describe('redirects/render', () => { describe('redirectIsExternal', () => { @@ -48,7 +51,7 @@ describe('redirects/render', () => { }, }); - const response = await renderRedirect(renderContext); + const response = await renderRedirect(renderContext as unknown as RenderContext); assert.equal(response.status, 301); assert.equal(response.headers.get('location'), '/target'); @@ -63,7 +66,7 @@ describe('redirects/render', () => { }, }); - const response = await renderRedirect(renderContext); + const response = await renderRedirect(renderContext as unknown as RenderContext); assert.equal(response.status, 308); assert.equal(response.headers.get('location'), '/target'); @@ -76,11 +79,11 @@ describe('redirects/render', () => { redirect: { destination: '/target', status: 302 }, redirectRoute: { segments: [[{ content: 'target', dynamic: false, spread: false }]], - }, + } as unknown as RouteData, }, }); - const response = await renderRedirect(renderContext); + const response = await renderRedirect(renderContext as unknown as RenderContext); assert.equal(response.status, 302); }); @@ -93,7 +96,7 @@ describe('redirects/render', () => { }, }); - const response = await renderRedirect(renderContext); + const response = await renderRedirect(renderContext as unknown as RenderContext); assert.equal(response.headers.get('location'), '/target%20with%20spaces'); }); @@ -106,7 +109,7 @@ describe('redirects/render', () => { }, }); - const response = await renderRedirect(renderContext); + const response = await renderRedirect(renderContext as unknown as RenderContext); assert.equal(response.status, 301); // External redirects use Response.redirect which sets the Location header differently @@ -122,7 +125,7 @@ describe('redirects/render', () => { params: { slug: 'my-post' }, }); - const response = await renderRedirect(renderContext); + const response = await renderRedirect(renderContext as unknown as RenderContext); assert.equal(response.headers.get('location'), '/articles/my-post'); }); @@ -136,7 +139,7 @@ describe('redirects/render', () => { params: { param1: 'foo', param2: 'bar' }, }); - const response = await renderRedirect(renderContext); + const response = await renderRedirect(renderContext as unknown as RenderContext); assert.equal(response.headers.get('location'), '/new/foo/bar'); }); @@ -150,7 +153,7 @@ describe('redirects/render', () => { params: { rest: 'a/b/c' }, }); - const response = await renderRedirect(renderContext); + const response = await renderRedirect(renderContext as unknown as RenderContext); assert.equal(response.headers.get('location'), '/new/a/b/c'); }); @@ -164,7 +167,7 @@ describe('redirects/render', () => { params: { city: 'Las Vegas\u2019' }, }); - const response = await renderRedirect(renderContext); + const response = await renderRedirect(renderContext as unknown as RenderContext); assert.equal(response.headers.get('location'), '/new/Las%20Vegas%E2%80%99'); }); @@ -177,11 +180,11 @@ describe('redirects/render', () => { redirectRoute: { segments: [[{ content: 'target', dynamic: false, spread: false }]], pathname: '/target', - }, + } as unknown as RouteData, }, }); - const response = await renderRedirect(renderContext); + const response = await renderRedirect(renderContext as unknown as RenderContext); assert.equal(response.headers.get('location'), '/target'); }); @@ -194,7 +197,7 @@ describe('redirects/render', () => { }, }); - const response = await renderRedirect(renderContext); + const response = await renderRedirect(renderContext as unknown as RenderContext); assert.equal(response.headers.get('location'), '/'); }); @@ -211,7 +214,7 @@ describe('computeRedirectStatus', () => { }); it('returns the explicit status when redirectRoute is defined and redirect is an object', () => { - const redirectRoute = /** @type {any} */ ({}); + const redirectRoute = {} as RouteData; assert.equal( computeRedirectStatus('GET', { status: 302, destination: '/dest' }, redirectRoute), 302, @@ -219,7 +222,7 @@ describe('computeRedirectStatus', () => { }); it('falls back to method-based status when redirect is a string even with redirectRoute', () => { - const redirectRoute = /** @type {any} */ ({}); + const redirectRoute = {} as RouteData; assert.equal(computeRedirectStatus('POST', '/dest', redirectRoute), 308); }); }); diff --git a/packages/astro/test/units/redirects/static-build.test.js b/packages/astro/test/units/redirects/static-build.test.ts similarity index 88% rename from packages/astro/test/units/redirects/static-build.test.js rename to packages/astro/test/units/redirects/static-build.test.ts index 9e00e1848c07..9462215280f7 100644 --- a/packages/astro/test/units/redirects/static-build.test.js +++ b/packages/astro/test/units/redirects/static-build.test.ts @@ -1,16 +1,19 @@ -// @ts-check import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { renderPath } from '../../../dist/core/build/generate.js'; -import { createMockPrerenderer, createStaticBuildOptions } from '../build/test-helpers.js'; -import { createTestApp, createPage } from '../mocks.js'; +import { createMockPrerenderer, createStaticBuildOptions } from '../build/test-helpers.ts'; +import { createTestApp, createPage } from '../mocks.ts'; import { createComponent, render, renderComponent } from '../../../dist/runtime/server/index.js'; +import type { StaticBuildOptions } from '../../../dist/core/build/types.js'; +import type { RouteData } from '../../../dist/types/public/internal.js'; +import type { MiddlewareHandler } from '../../../dist/types/public/common.js'; + // Minimal target page for redirect destination routes const TARGET_PAGE = '---\n---\n

Target

'; describe('static redirects — meta refresh output', () => { - let options; + let options: StaticBuildOptions; before(async () => { options = await createStaticBuildOptions({ @@ -37,7 +40,7 @@ describe('static redirects — meta refresh output', () => { }); it('includes http-equiv refresh and target URL in redirect HTML', async () => { - const route = options.routesList.routes.find( + const route = (options.routesList as { routes: RouteData[] }).routes.find( (r) => r.route === '/one' && r.type === 'redirect', ); assert.ok(route, 'expected /one redirect route'); @@ -59,7 +62,7 @@ describe('static redirects — meta refresh output', () => { }); it('generates redirect HTML for a 302 redirect', async () => { - const route = options.routesList.routes.find( + const route = (options.routesList as { routes: RouteData[] }).routes.find( (r) => r.route === '/three' && r.type === 'redirect', ); assert.ok(route, 'expected /three redirect route'); @@ -81,7 +84,7 @@ describe('static redirects — meta refresh output', () => { }); it('generates redirect HTML for an external destination', async () => { - const route = options.routesList.routes.find( + const route = (options.routesList as { routes: RouteData[] }).routes.find( (r) => r.route === '/external/redirect' && r.type === 'redirect', ); assert.ok(route, 'expected /external/redirect route'); @@ -106,7 +109,7 @@ describe('static redirects — meta refresh output', () => { }); it('generates redirect HTML for a relative destination', async () => { - const route = options.routesList.routes.find( + const route = (options.routesList as { routes: RouteData[] }).routes.find( (r) => r.route === '/relative/redirect' && r.type === 'redirect', ); assert.ok(route, 'expected /relative/redirect route'); @@ -131,7 +134,7 @@ describe('static redirects — meta refresh output', () => { }); it('generates redirect HTML for a dynamic slug redirect', async () => { - const route = options.routesList.routes.find( + const route = (options.routesList as { routes: RouteData[] }).routes.find( (r) => r.route === '/blog/[...slug]' && r.type === 'redirect', ); assert.ok(route, 'expected /blog/[...slug] redirect route'); @@ -153,7 +156,7 @@ describe('static redirects — meta refresh output', () => { }); it('falls back to spread rule for multi-segment dynamic paths', async () => { - const route = options.routesList.routes.find( + const route = (options.routesList as { routes: RouteData[] }).routes.find( (r) => r.route === '/more/old/[...spread]' && r.type === 'redirect', ); assert.ok(route, 'expected /more/old/[...spread] redirect route'); @@ -179,7 +182,7 @@ describe('static redirects — meta refresh output', () => { }); describe('static redirects — config.build.redirects = false suppresses redirect pages', () => { - let options; + let options: StaticBuildOptions; before(async () => { options = await createStaticBuildOptions({ @@ -192,7 +195,7 @@ describe('static redirects — config.build.redirects = false suppresses redirec }); it('returns null for a redirect route when build.redirects is false', async () => { - const route = options.routesList.routes.find( + const route = (options.routesList as { routes: RouteData[] }).routes.find( (r) => r.route === '/one' && r.type === 'redirect', ); assert.ok(route, 'expected /one redirect route'); @@ -213,7 +216,7 @@ describe('static redirects — config.build.redirects = false suppresses redirec }); describe('static redirects — site config does not affect redirect URL', () => { - let options; + let options: StaticBuildOptions; before(async () => { options = await createStaticBuildOptions({ @@ -226,7 +229,7 @@ describe('static redirects — site config does not affect redirect URL', () => }); it('uses relative URL in redirect HTML even when site is set', async () => { - const route = options.routesList.routes.find( + const route = (options.routesList as { routes: RouteData[] }).routes.find( (r) => r.route === '/one' && r.type === 'redirect', ); assert.ok(route, 'expected /one redirect route'); @@ -250,8 +253,10 @@ describe('static redirects — site config does not affect redirect URL', () => describe('static redirects — middleware-generated redirect', () => { it('renders redirect HTML for a page that returns a redirect via middleware', async () => { - const indexPage = createComponent((_result, _props, _slots) => render`

Index

`); - const middleware = async (ctx, next) => { + const indexPage = createComponent( + (_result: any, _props: any, _slots: any) => render`

Index

`, + ); + const middleware: MiddlewareHandler = async (ctx, next) => { if (new URL(ctx.request.url).pathname === '/middleware-redirect/') { return new Response(null, { status: 301, headers: { Location: '/test' } }); } @@ -264,7 +269,7 @@ describe('static redirects — middleware-generated redirect', () => { const response = await app.render(new Request('http://example.com/middleware-redirect/'), { routeData: undefined, - }); + } as any); assert.equal(response.status, 301); assert.equal(response.headers.get('Location'), '/test'); }); @@ -292,7 +297,7 @@ describe('static redirects — invalid redirect destination throws', () => { }, }, }), - (err) => { + (err: Error & { name: string }) => { // Should NOT be the misleading getStaticPaths error assert.ok(!err.message.includes('getStaticPaths()')); // Should be our new clear error message @@ -309,7 +314,7 @@ describe('Astro.redirect() in a page component — build.redirects = false', () // /secret calls Astro.redirect('/login') in frontmatter. // build.redirects=false suppresses config-level redirect routes but must NOT // suppress pages that explicitly return a redirect response via Astro.redirect(). - const secretPage = createComponent((result, props, slots) => { + const secretPage = createComponent((result: any, props: any, slots: any) => { const Astro = result.createAstro(props, slots); return Astro.redirect('/login'); }); @@ -325,7 +330,7 @@ describe('Astro.redirect() in a page component — build.redirects = false', () describe('Astro.redirect() — site config does not inject absolute URL', () => { it('uses relative URL in Location header even when site is set', async () => { // The site config should not cause redirect URLs to become absolute. - const secretPage = createComponent((result, props, slots) => { + const secretPage = createComponent((result: any, props: any, slots: any) => { const Astro = result.createAstro(props, slots); return Astro.redirect('/login'); }); @@ -333,7 +338,7 @@ describe('Astro.redirect() — site config does not inject absolute URL', () => const app = createTestApp([createPage(secretPage, { route: '/secret' })]); const response = await app.render(new Request('http://example.com/secret/')); - const location = response.headers.get('location'); + const location = response.headers.get('location')!; assert.ok(!location.includes('https://example.com'), 'should not use absolute URL'); assert.equal(location, '/login'); }); @@ -354,13 +359,13 @@ describe('output: "server"', () => { it('Warns when used inside a component', async () => { // A child component calls Astro.redirect() after the parent has already // started streaming HTML — the same pattern as late.astro + redirect.astro. - const redirectChild = createComponent((result, props, slots) => { + const redirectChild = createComponent((result: any, props: any, slots: any) => { const Astro = result.createAstro(props, slots); return Astro.redirect('/login'); }); const latePage = createComponent( - (result) => + (result: any) => render`

Testing

${renderComponent(result, 'Redirect', redirectChild, {})}`, ); @@ -371,7 +376,8 @@ describe('output: "server"', () => { try { await response.text(); assert.equal(false, true); - } catch (e) { + } catch (e: unknown) { + assert.ok(e instanceof Error); assert.equal( e.message, 'The response has already been sent to the browser and cannot be altered.', diff --git a/packages/astro/test/units/redirects/template.test.js b/packages/astro/test/units/redirects/template.test.ts similarity index 93% rename from packages/astro/test/units/redirects/template.test.js rename to packages/astro/test/units/redirects/template.test.ts index 26da2255a157..05a4786bca7b 100644 --- a/packages/astro/test/units/redirects/template.test.js +++ b/packages/astro/test/units/redirects/template.test.ts @@ -38,8 +38,8 @@ describe('redirects/template', () => { const link = $('body a'); assert.equal(link.length, 1); assert.equal(link.attr('href'), '/new-page'); - assert.ok(link.html().includes('Redirecting')); - assert.ok(link.html().includes('/new-page')); + assert.ok(link.html()?.includes('Redirecting')); + assert.ok(link.html()?.includes('/new-page')); }); it('uses 2 second delay for 302 redirects', () => { @@ -88,8 +88,8 @@ describe('redirects/template', () => { const $ = cheerio.load(html); const bodyText = $('body').html(); - assert.ok(bodyText.includes('from /old')); - assert.ok(bodyText.includes('to /new')); + assert.ok(bodyText?.includes('from /old')); + assert.ok(bodyText?.includes('to /new')); }); it('omits "from" text when not provided', () => { @@ -101,8 +101,8 @@ describe('redirects/template', () => { const $ = cheerio.load(html); const bodyText = $('body').html(); - assert.ok(!bodyText.includes('from ')); - assert.ok(bodyText.includes('to /new')); + assert.ok(!bodyText?.includes('from ')); + assert.ok(bodyText?.includes('to /new')); }); it('handles special characters in URLs', () => { diff --git a/packages/astro/test/units/remote-pattern.test.js b/packages/astro/test/units/remote-pattern.test.ts similarity index 91% rename from packages/astro/test/units/remote-pattern.test.js rename to packages/astro/test/units/remote-pattern.test.ts index 7c9f7c74850a..8cf73f69ac2a 100644 --- a/packages/astro/test/units/remote-pattern.test.js +++ b/packages/astro/test/units/remote-pattern.test.ts @@ -145,26 +145,26 @@ describe('remote-pattern', () => { describe('remote is allowed', () => { it('allows remote URLs based on patterns', async () => { const patterns = { - domains: [], + domains: [] as string[], remotePatterns: [ { - protocol: 'https', + protocol: 'https' as const, hostname: '**.astro.build', pathname: '/en/**', }, { - protocol: 'http', + protocol: 'http' as const, hostname: 'preview.docs.astro.build', port: '8080', }, ], }; - assert.equal(isRemoteAllowed(url1, patterns), true); - assert.equal(isRemoteAllowed(url2, patterns), true); - assert.equal(isRemoteAllowed(url3, patterns), false); - assert.equal(isRemoteAllowed(url4, patterns), false); - assert.equal(isRemoteAllowed(url5, patterns), false); + assert.equal(isRemoteAllowed(url1.href, patterns), true); + assert.equal(isRemoteAllowed(url2.href, patterns), true); + assert.equal(isRemoteAllowed(url3.href, patterns), false); + assert.equal(isRemoteAllowed(url4.href, patterns), false); + assert.equal(isRemoteAllowed(url5.href, patterns), false); }); }); }); diff --git a/packages/astro/test/units/render/chunk.test.js b/packages/astro/test/units/render/chunk.test.js deleted file mode 100644 index 57ab743261a1..000000000000 --- a/packages/astro/test/units/render/chunk.test.js +++ /dev/null @@ -1,46 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; -import * as cheerio from 'cheerio'; -import { createFixture, createRequestAndResponse, runInContainer } from '../test-utils.js'; - -describe('core/render chunk', () => { - it('does not throw on user object with type', async () => { - const fixture = await createFixture({ - '/src/pages/index.astro': `\ - --- - const value = { type: 'foobar' } - --- -
{value}
- `, - }); - - await runInContainer( - { - inlineConfig: { - root: fixture.path, - logLevel: 'silent', - integrations: [], - }, - }, - async (container) => { - const { req, res, done, text } = createRequestAndResponse({ - method: 'GET', - url: '/', - }); - container.handle(req, res); - - await done; - try { - const html = await text(); - const $ = cheerio.load(html); - const target = $('#chunk'); - - assert.ok(target); - assert.equal(target.text(), '[object Object]'); - } catch { - assert.fail(); - } - }, - ); - }); -}); diff --git a/packages/astro/test/units/render/class-list-and-style.test.js b/packages/astro/test/units/render/class-list-and-style.test.ts similarity index 99% rename from packages/astro/test/units/render/class-list-and-style.test.js rename to packages/astro/test/units/render/class-list-and-style.test.ts index f59c21a744bf..0a4ada6ce71f 100644 --- a/packages/astro/test/units/render/class-list-and-style.test.js +++ b/packages/astro/test/units/render/class-list-and-style.test.ts @@ -1,10 +1,9 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import * as cheerio from 'cheerio'; import { addAttribute } from '../../../dist/runtime/server/index.js'; import { toStyleString } from '../../../dist/runtime/server/render/util.js'; -import { createTestApp, createPage, createMultiChildPage, spreadPropsSpan } from '../mocks.js'; +import { createTestApp, createPage, createMultiChildPage, spreadPropsSpan } from '../mocks.ts'; describe('class:list', () => { it('handles a plain string', () => { diff --git a/packages/astro/test/units/render/components.test.js b/packages/astro/test/units/render/components.test.js deleted file mode 100644 index 3d274702710a..000000000000 --- a/packages/astro/test/units/render/components.test.js +++ /dev/null @@ -1,201 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; -import * as cheerio from 'cheerio'; -import { createFixture, createRequestAndResponse, runInContainer } from '../test-utils.js'; - -describe('core/render components', () => { - it('should sanitize dynamic tags', async () => { - const fixture = await createFixture({ - '/src/pages/index.astro': ` - --- - const TagA = 'p style=color:red;' - const TagB = 'p>' - --- - - testing - - - - - - `, - }); - - await runInContainer( - { - inlineConfig: { - root: fixture.path, - logLevel: 'silent', - integrations: [], - }, - }, - async (container) => { - const { req, res, done, text } = createRequestAndResponse({ - method: 'GET', - url: '/', - }); - container.handle(req, res); - - await done; - const html = await text(); - const $ = cheerio.load(html); - const target = $('#target'); - - assert.ok(target); - assert.equal(target.attr('id'), 'target'); - assert.equal(typeof target.attr('style'), 'undefined'); - - assert.equal($('#pwnd').length, 0); - }, - ); - }); - - it('should merge `class` and `class:list`', async () => { - const fixture = await createFixture({ - '/src/pages/index.astro': ` - --- - import Class from '../components/Class.astro'; - import ClassList from '../components/ClassList.astro'; - import BothLiteral from '../components/BothLiteral.astro'; - import BothFlipped from '../components/BothFlipped.astro'; - import BothSpread from '../components/BothSpread.astro'; - --- - - - - - - `, - '/src/components/Class.astro': `
`,
-			'/src/components/ClassList.astro': `
`,
-			'/src/components/BothLiteral.astro': `
`,
-			'/src/components/BothFlipped.astro': `
`,
-			'/src/components/BothSpread.astro': `
`,
-		});
-
-		await runInContainer(
-			{
-				inlineConfig: {
-					root: fixture.path,
-					logLevel: 'silent',
-					integrations: [],
-				},
-			},
-			async (container) => {
-				const { req, res, done, text } = createRequestAndResponse({
-					method: 'GET',
-					url: '/',
-				});
-				container.handle(req, res);
-
-				await done;
-				const html = await text();
-				const $ = cheerio.load(html);
-
-				const check = (name) => JSON.parse($(name).text() || '{}');
-
-				const Class = check('#class');
-				const ClassList = check('#class-list');
-				const BothLiteral = check('#both-literal');
-				const BothFlipped = check('#both-flipped');
-				const BothSpread = check('#both-spread');
-
-				assert.deepEqual(Class, { class: 'red blue' }, '#class');
-				assert.deepEqual(ClassList, { class: 'red blue' }, '#class-list');
-				assert.deepEqual(BothLiteral, { class: 'red blue' }, '#both-literal');
-				assert.deepEqual(BothFlipped, { class: 'red blue' }, '#both-flipped');
-				assert.deepEqual(BothSpread, { class: 'red blue' }, '#both-spread');
-			},
-		);
-	});
-
-	it('should render component with `null` response', async () => {
-		const fixture = await createFixture({
-			'/src/pages/index.astro': `
-				---
-				import NullComponent from '../components/NullComponent.astro';
-				---
-				
-			`,
-			'/src/components/NullComponent.astro': `
-				---
-				return null;
-				---
-			`,
-		});
-
-		await runInContainer(
-			{
-				inlineConfig: {
-					root: fixture.path,
-					logLevel: 'silent',
-				},
-			},
-			async (container) => {
-				const { req, res, done, text } = createRequestAndResponse({
-					method: 'GET',
-					url: '/',
-				});
-				container.handle(req, res);
-
-				await done;
-				const html = await text();
-				const $ = cheerio.load(html);
-
-				assert.equal($('body').text(), '');
-				assert.equal(res.statusCode, 200);
-			},
-		);
-	});
-
-	it('should render custom element attributes as strings instead of boolean attributes', async () => {
-		const fixture = await createFixture({
-			'/src/pages/index.astro': `
-				---
-				const selectedColor = "blue";
-				const autoplay = 2000;
-				---
-				
-					Custom Element Attributes Test
-					
-						
-						
-						Test with autoplay prop working
-					
-				
-			`,
-		});
-
-		await runInContainer(
-			{
-				inlineConfig: {
-					root: fixture.path,
-					logLevel: 'silent',
-					integrations: [],
-				},
-			},
-			async (container) => {
-				const { req, res, done, text } = createRequestAndResponse({
-					method: 'GET',
-					url: '/',
-				});
-				container.handle(req, res);
-
-				await done;
-				const html = await text();
-
-				// Extract test data - following same pattern as class merging test
-				const hasSelectedBlue = html.includes('selected="blue"');
-				const hasAutoplay2000 = html.includes('autoplay="2000"');
-				const hasBooleanSelected = html.includes('');
-				const hasBooleanAutoplay = html.includes('');
-
-				// Test custom elements render string attributes correctly
-				assert.ok(hasSelectedBlue, 'selected="blue"');
-				assert.ok(hasAutoplay2000, 'autoplay="2000"');
-				assert.ok(!hasBooleanSelected, 'no boolean selected');
-				assert.ok(!hasBooleanAutoplay, 'no boolean autoplay');
-			},
-		);
-	});
-});
diff --git a/packages/astro/test/units/render/context-helpers.test.js b/packages/astro/test/units/render/context-helpers.test.ts
similarity index 74%
rename from packages/astro/test/units/render/context-helpers.test.js
rename to packages/astro/test/units/render/context-helpers.test.ts
index bfddaa8fcba0..2dae5dc8291f 100644
--- a/packages/astro/test/units/render/context-helpers.test.js
+++ b/packages/astro/test/units/render/context-helpers.test.ts
@@ -1,10 +1,13 @@
-// @ts-check
 import assert from 'node:assert/strict';
 import { describe, it } from 'node:test';
 import { createComponent, render } from '../../../dist/runtime/server/index.js';
-import { createTestApp, createPage } from '../mocks.js';
+import type { AstroComponentFactory } from '../../../dist/runtime/server/render/index.js';
+import { createTestApp, createPage } from '../mocks.ts';
 
-async function renderAndCapture(page, manifestOverrides = {}) {
+async function renderAndCapture(
+	page: AstroComponentFactory,
+	manifestOverrides: Record = {},
+) {
 	const app = createTestApp(
 		[createPage(page, { route: '/test', prerender: false })],
 		manifestOverrides,
@@ -15,8 +18,8 @@ async function renderAndCapture(page, manifestOverrides = {}) {
 
 describe('Astro.session getter', () => {
 	it('returns undefined when no session driver is configured', async () => {
-		let sessionValue = 'not-called';
-		const page = createComponent((result, props, slots) => {
+		let sessionValue: unknown = 'not-called';
+		const page = createComponent((result: any, props: any, slots: any) => {
 			const Astro = result.createAstro(props, slots);
 			sessionValue = Astro.session;
 			return render`

done

`; @@ -30,8 +33,8 @@ describe('Astro.session getter', () => { describe('Astro.csp getter', () => { it('returns undefined when CSP is not configured in the manifest', async () => { - let cspValue = 'not-called'; - const page = createComponent((result, props, slots) => { + let cspValue: unknown = 'not-called'; + const page = createComponent((result: any, props: any, slots: any) => { const Astro = result.createAstro(props, slots); cspValue = Astro.csp; return render`

done

`; @@ -43,8 +46,8 @@ describe('Astro.csp getter', () => { }); it('returns an object with insert* methods when CSP is configured', async () => { - let cspValue; - const page = createComponent((result, props, slots) => { + let cspValue: Record | undefined; + const page = createComponent((result: any, props: any, slots: any) => { const Astro = result.createAstro(props, slots); cspValue = Astro.csp; return render`

done

`; diff --git a/packages/astro/test/units/render/escape.test.js b/packages/astro/test/units/render/escape.test.ts similarity index 90% rename from packages/astro/test/units/render/escape.test.js rename to packages/astro/test/units/render/escape.test.ts index e38af171cc18..19e8402a01ac 100644 --- a/packages/astro/test/units/render/escape.test.js +++ b/packages/astro/test/units/render/escape.test.ts @@ -1,4 +1,3 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { @@ -86,8 +85,8 @@ describe('unescapeHTML', () => { yield '
  • 1
  • '; yield '
  • 2
  • '; } - const result = unescapeHTML(gen()); - const chunks = []; + const result = unescapeHTML(gen()) as AsyncIterable; + const chunks: string[] = []; for await (const chunk of result) { chunks.push(String(chunk)); } @@ -100,8 +99,8 @@ describe('unescapeHTML', () => { yield '
  • a
  • '; yield '
  • b
  • '; } - const result = unescapeHTML(gen()); - const chunks = []; + const result = unescapeHTML(gen()) as AsyncIterable; + const chunks: string[] = []; for await (const chunk of result) { chunks.push(String(chunk)); } @@ -110,8 +109,8 @@ describe('unescapeHTML', () => { it('can take a Response', async () => { const response = new Response('

    hello

    ', { headers: { 'content-type': 'text/html' } }); - const result = unescapeHTML(response); - const chunks = []; + const result = unescapeHTML(response) as AsyncIterable; + const chunks: string[] = []; const dec = new TextDecoder(); for await (const chunk of result) { chunks.push(chunk instanceof Uint8Array ? dec.decode(chunk) : String(chunk)); @@ -126,8 +125,8 @@ describe('unescapeHTML', () => { controller.close(); }, }); - const result = unescapeHTML(stream); - const chunks = []; + const result = unescapeHTML(stream) as AsyncIterable; + const chunks: string[] = []; for await (const chunk of result) { chunks.push(String(chunk)); } diff --git a/packages/astro/test/units/render/head-injection-app.test.js b/packages/astro/test/units/render/head-injection-app.test.ts similarity index 75% rename from packages/astro/test/units/render/head-injection-app.test.js rename to packages/astro/test/units/render/head-injection-app.test.ts index e718b57874f2..daacf7972a4f 100644 --- a/packages/astro/test/units/render/head-injection-app.test.js +++ b/packages/astro/test/units/render/head-injection-app.test.ts @@ -5,21 +5,28 @@ import { RenderContext } from '../../../dist/core/render-context.js'; import { createComponent, createHeadAndContent, - maybeRenderHead, + maybeRenderHead as _maybeRenderHead, render, renderComponent, - renderHead, + renderHead as _renderHead, renderSlot, renderSlotToString, renderUniqueStylesheet, unescapeHTML, } from '../../../dist/runtime/server/index.js'; -import { createBasicPipeline } from '../test-utils.js'; +import type { AstroComponentFactory } from '../../../dist/runtime/server/render/index.js'; +import type { Pipeline } from '../../../dist/core/render/index.js'; +import { createBasicPipeline } from '../test-utils.ts'; -const createAstroModule = (AstroComponent) => ({ default: AstroComponent }); +// The public types for renderHead/maybeRenderHead declare zero params, +// but the runtime implementation accepts a result argument. +const renderHead = _renderHead as (result: any) => any; +const maybeRenderHead = _maybeRenderHead as (result: any) => any; + +const createAstroModule = (AstroComponent: AstroComponentFactory) => ({ default: AstroComponent }); describe('head injection app-level rendering', () => { - let pipeline; + let pipeline: Pipeline; before(async () => { pipeline = createBasicPipeline(); @@ -30,7 +37,7 @@ describe('head injection app-level rendering', () => { }); }); - async function renderPage(Component) { + async function renderPage(Component: AstroComponentFactory) { const request = new Request('http://example.com/'); const routeData = { type: 'page', @@ -38,7 +45,7 @@ describe('head injection app-level rendering', () => { component: 'src/pages/index.astro', params: {}, }; - const renderContext = await RenderContext.create({ pipeline, request, routeData }); + const renderContext = await RenderContext.create({ pipeline, request, routeData } as any); const response = await renderContext.render(createAstroModule(Component)); return cheerio.load(await response.text()); } @@ -46,13 +53,13 @@ describe('head injection app-level rendering', () => { it('injects propagated head from component created in page scope', async () => { const Other = createComponent(() => render`
    Other
    `); const HeadEntry = createComponent({ - factory(result, props, slots) { + factory(result: any, props: any, slots: any) { const link = renderUniqueStylesheet(result, { type: 'external', src: '/some/fake/styles.css', }); return createHeadAndContent( - unescapeHTML(link), + unescapeHTML(link) as unknown as string, render`${renderComponent(result, 'Other', Other, props, slots)}`, ); }, @@ -60,7 +67,7 @@ describe('head injection app-level rendering', () => { }); const Wrapper = createComponent( - (result) => + (result: any) => render`${renderHead(result)}${renderComponent(result, 'HeadEntry', HeadEntry, {}, {})}`, ); @@ -73,13 +80,13 @@ describe('head injection app-level rendering', () => { it('injects propagated head through nested layout components', async () => { const Other = createComponent(() => render`
    Other
    `); const HeadEntry = createComponent({ - factory(result, props, slots) { + factory(result: any, props: any, slots: any) { const link = renderUniqueStylesheet(result, { type: 'external', src: '/some/fake/styles.css', }); return createHeadAndContent( - unescapeHTML(link), + unescapeHTML(link) as unknown as string, render`${renderComponent(result, 'Other', Other, props, slots)}`, ); }, @@ -87,21 +94,21 @@ describe('head injection app-level rendering', () => { }); const Content = createComponent( - (result) => render`${renderComponent(result, 'HeadEntry', HeadEntry, {}, {})}`, + (result: any) => render`${renderComponent(result, 'HeadEntry', HeadEntry, {}, {})}`, ); Content.propagation = 'in-tree'; const Inner = createComponent( - (result) => render`${renderComponent(result, 'Content', Content, {}, {})}`, + (result: any) => render`${renderComponent(result, 'Content', Content, {}, {})}`, ); Inner.propagation = 'in-tree'; const Layout = createComponent({ - async factory(result, _props, slots) { + async factory(result: any, _props: any, slots: any) { const slotted = await renderSlotToString(result, slots.default); return render`Normal head stuff${renderHead(result)}${unescapeHTML(slotted)}`; }, }); const Page = createComponent( - (result) => + (result: any) => render`${renderComponent(result, 'Layout', Layout, {}, { default: () => render`${renderComponent(result, 'Inner', Inner, {}, {})}` })}`, ); @@ -113,25 +120,28 @@ describe('head injection app-level rendering', () => { it('supports slot rendering during head buffering without style bleed', async () => { const SlottedContent = createComponent({ - factory(result) { + factory(result: any) { const link = renderUniqueStylesheet(result, { type: 'external', src: '/styles/from-slot.css', }); - return createHeadAndContent(unescapeHTML(link), render`

    Paragraph.

    `); + return createHeadAndContent( + unescapeHTML(link) as unknown as string, + render`

    Paragraph.

    `, + ); }, propagation: 'self', }); const SlotRenderComponent = createComponent({ - async factory(result, _props, slots) { + async factory(result: any, _props: any, slots: any) { const html = await renderSlotToString(result, slots.default); const ownLink = renderUniqueStylesheet(result, { type: 'external', src: '/styles/slot-render.css', }); return createHeadAndContent( - ownLink, + ownLink!, render`
    ${unescapeHTML(html)}
    `, ); }, @@ -139,11 +149,11 @@ describe('head injection app-level rendering', () => { }); const Layout = createComponent( - (result, _props, slots) => + (result: any, _props: any, slots: any) => render`${maybeRenderHead(result)}${renderSlot(result, slots.default)}`, ); const Page = createComponent( - (result) => + (result: any) => render`${renderComponent( result, 'Layout', diff --git a/packages/astro/test/units/render/head-propagation/boundary.test.js b/packages/astro/test/units/render/head-propagation/boundary.test.ts similarity index 100% rename from packages/astro/test/units/render/head-propagation/boundary.test.js rename to packages/astro/test/units/render/head-propagation/boundary.test.ts diff --git a/packages/astro/test/units/render/head-propagation/buffer.test.js b/packages/astro/test/units/render/head-propagation/buffer.test.ts similarity index 80% rename from packages/astro/test/units/render/head-propagation/buffer.test.js rename to packages/astro/test/units/render/head-propagation/buffer.test.ts index 4ae667f50ab4..6d1807b0ff0a 100644 --- a/packages/astro/test/units/render/head-propagation/buffer.test.js +++ b/packages/astro/test/units/render/head-propagation/buffer.test.ts @@ -1,28 +1,30 @@ import * as assert from 'node:assert/strict'; import { describe, it } from 'node:test'; +import type { HeadPropagator } from '../../../../dist/core/head-propagation/buffer.js'; import { collectPropagatedHeadParts } from '../../../../dist/core/head-propagation/buffer.js'; +import type { SSRResult } from '../../../../dist/types/public/internal.js'; const headAndContentSym = Symbol.for('astro.headAndContent'); -function createHeadAndContentLike(head) { +function createHeadAndContentLike(head: string) { return { [headAndContentSym]: true, head, }; } -function isHeadAndContent(value) { +function isHeadAndContent(value: unknown): value is { head: string } { return typeof value === 'object' && value !== null && headAndContentSym in value; } -function createResult() { - return {}; +function createResult(): SSRResult { + return {} as SSRResult; } describe('head propagation buffer', () => { it('returns empty head parts when no propagators exist', async () => { const collected = await collectPropagatedHeadParts({ - propagators: new Set(), + propagators: new Set(), result: createResult(), isHeadAndContent, }); @@ -30,7 +32,7 @@ describe('head propagation buffer', () => { }); it('collects non-empty head strings from propagators', async () => { - const propagators = new Set([ + const propagators = new Set([ { init: () => createHeadAndContentLike('') }, { init: () => createHeadAndContentLike('') }, ]); @@ -48,7 +50,7 @@ describe('head propagation buffer', () => { }); it('skips non-head-and-content values and empty heads', async () => { - const propagators = new Set([ + const propagators = new Set([ { init: () => 'value' }, { init: () => createHeadAndContentLike('') }, { init: () => createHeadAndContentLike('') }, @@ -64,7 +66,7 @@ describe('head propagation buffer', () => { }); it('processes propagators added while iterating', async () => { - const propagators = new Set(); + const propagators = new Set(); propagators.add({ init() { propagators.add({ diff --git a/packages/astro/test/units/render/head-propagation/comment.test.js b/packages/astro/test/units/render/head-propagation/comment.test.ts similarity index 100% rename from packages/astro/test/units/render/head-propagation/comment.test.js rename to packages/astro/test/units/render/head-propagation/comment.test.ts diff --git a/packages/astro/test/units/render/head-propagation/graph.test.js b/packages/astro/test/units/render/head-propagation/graph.test.ts similarity index 80% rename from packages/astro/test/units/render/head-propagation/graph.test.js rename to packages/astro/test/units/render/head-propagation/graph.test.ts index b5079aa6ac9c..e1d80b15bfa1 100644 --- a/packages/astro/test/units/render/head-propagation/graph.test.js +++ b/packages/astro/test/units/render/head-propagation/graph.test.ts @@ -1,5 +1,6 @@ import * as assert from 'node:assert/strict'; import { describe, it } from 'node:test'; +import type { ImporterGraph } from '../../../../dist/core/head-propagation/graph.js'; import { buildImporterGraphFromModuleInfo, computeInTreeAncestors, @@ -7,10 +8,10 @@ import { describe('head propagation graph', () => { it('computes in-tree ancestors for a linear chain', () => { - const importerGraph = new Map([ + const importerGraph: ImporterGraph = new Map([ ['leaf', new Set(['parent'])], ['parent', new Set(['page'])], - ['page', new Set()], + ['page', new Set()], ]); const result = computeInTreeAncestors({ seeds: ['leaf'], @@ -20,11 +21,11 @@ describe('head propagation graph', () => { }); it('supports multiple seeds and cycles', () => { - const importerGraph = new Map([ + const importerGraph: ImporterGraph = new Map([ ['a', new Set(['b'])], ['b', new Set(['a', 'page'])], ['c', new Set(['page'])], - ['page', new Set()], + ['page', new Set()], ]); const result = computeInTreeAncestors({ seeds: ['a', 'c'], @@ -37,21 +38,21 @@ describe('head propagation graph', () => { }); it('stops traversal at boundary predicate', () => { - const importerGraph = new Map([ + const importerGraph: ImporterGraph = new Map([ ['leaf', new Set(['boundary'])], ['boundary', new Set(['page'])], - ['page', new Set()], + ['page', new Set()], ]); const result = computeInTreeAncestors({ seeds: ['leaf'], importerGraph, - stopAt: (id) => id === 'boundary', + stopAt: (id: string) => id === 'boundary', }); assert.deepEqual(Array.from(result), ['leaf']); }); it('builds importer graph from module info provider', () => { - const provider = (id) => { + const provider = (id: string) => { if (id === 'a') return { importers: ['page'], dynamicImporters: [] }; if (id === 'b') return { importers: [], dynamicImporters: ['page'] }; if (id === 'page') return { importers: [], dynamicImporters: [] }; diff --git a/packages/astro/test/units/render/head-propagation/policy.test.js b/packages/astro/test/units/render/head-propagation/policy.test.ts similarity index 100% rename from packages/astro/test/units/render/head-propagation/policy.test.js rename to packages/astro/test/units/render/head-propagation/policy.test.ts diff --git a/packages/astro/test/units/render/head-propagation/resolver.test.js b/packages/astro/test/units/render/head-propagation/resolver.test.ts similarity index 98% rename from packages/astro/test/units/render/head-propagation/resolver.test.js rename to packages/astro/test/units/render/head-propagation/resolver.test.ts index 947f4f85971f..84d62d737d40 100644 --- a/packages/astro/test/units/render/head-propagation/resolver.test.js +++ b/packages/astro/test/units/render/head-propagation/resolver.test.ts @@ -35,7 +35,7 @@ describe('head propagation resolver', () => { }); it('getPropagationHint reads from SSR result metadata', () => { - const result = { + const result: any = { componentMetadata: new Map([['/src/Comp.astro', { propagation: 'in-tree' }]]), }; const hint = getPropagationHint(result, { diff --git a/packages/astro/test/units/render/head-propagation/runtime-adapters.test.js b/packages/astro/test/units/render/head-propagation/runtime-adapters.test.ts similarity index 75% rename from packages/astro/test/units/render/head-propagation/runtime-adapters.test.js rename to packages/astro/test/units/render/head-propagation/runtime-adapters.test.ts index f1fea6a5ad5f..ecd03a07fd47 100644 --- a/packages/astro/test/units/render/head-propagation/runtime-adapters.test.js +++ b/packages/astro/test/units/render/head-propagation/runtime-adapters.test.ts @@ -2,19 +2,20 @@ import * as assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { createAstroComponentInstance } from '../../../../dist/runtime/server/render/astro/instance.js'; import { bufferHeadContent } from '../../../../dist/runtime/server/render/astro/render.js'; +import type { SSRResult } from '../../../../dist/types/public/internal.js'; const headAndContentSym = Symbol.for('astro.headAndContent'); function createResult() { return { clientDirectives: new Map(), - componentMetadata: new Map(), + componentMetadata: new Map(), partial: false, _metadata: { hasRenderedHead: false, headInTree: false, propagators: new Set(), - extraHead: [], + extraHead: [] as string[], }, }; } @@ -28,12 +29,12 @@ describe('head propagation runtime adapters', () => { }); createAstroComponentInstance( - result, + result as unknown as SSRResult, 'Comp', - Object.assign(() => null, { + Object.assign((() => null) as () => null, { moduleId: '/src/Comp.astro', - propagation: 'none', - }), + propagation: 'none' as const, + }) as unknown as Parameters[2], {}, {}, ); @@ -52,7 +53,7 @@ describe('head propagation runtime adapters', () => { }, }); - await bufferHeadContent(result); + await bufferHeadContent(result as unknown as SSRResult); assert.deepEqual(result._metadata.extraHead, [ '', ]); diff --git a/packages/astro/test/units/render/head-propagation/runtime.test.js b/packages/astro/test/units/render/head-propagation/runtime.test.ts similarity index 72% rename from packages/astro/test/units/render/head-propagation/runtime.test.js rename to packages/astro/test/units/render/head-propagation/runtime.test.ts index 9a4f302fdd75..df9067f7b0e1 100644 --- a/packages/astro/test/units/render/head-propagation/runtime.test.js +++ b/packages/astro/test/units/render/head-propagation/runtime.test.ts @@ -1,5 +1,6 @@ import * as assert from 'node:assert/strict'; import { describe, it } from 'node:test'; +import type { SSRResult } from '../../../../dist/types/public/internal.js'; import { bufferPropagatedHead, getInstructionRenderState, @@ -17,7 +18,7 @@ function createResult() { hasRenderedHead: false, headInTree: false, propagators: new Set(), - extraHead: [], + extraHead: [] as string[], }, }; } @@ -25,10 +26,18 @@ function createResult() { describe('head propagation runtime facade', () => { it('registers only propagating components', () => { const result = createResult(); - registerIfPropagating(result, { propagation: 'none' }, { init: () => null }); + registerIfPropagating( + result as unknown as SSRResult, + { propagation: 'none' } as Parameters[1], + { init: () => null }, + ); assert.equal(result._metadata.propagators.size, 0); - registerIfPropagating(result, { propagation: 'self' }, { init: () => null }); + registerIfPropagating( + result as unknown as SSRResult, + { propagation: 'self' } as Parameters[1], + { init: () => null }, + ); assert.equal(result._metadata.propagators.size, 1); }); @@ -43,13 +52,13 @@ describe('head propagation runtime facade', () => { }, }); - await bufferPropagatedHead(result); + await bufferPropagatedHead(result as unknown as SSRResult); assert.deepEqual(result._metadata.extraHead, ['']); }); it('exposes render state and evaluates instruction policy', () => { const result = createResult(); - const state = getInstructionRenderState(result); + const state = getInstructionRenderState(result as unknown as SSRResult); assert.deepEqual(state, { hasRenderedHead: false, headInTree: false, diff --git a/packages/astro/test/units/render/head.test.js b/packages/astro/test/units/render/head.test.ts similarity index 78% rename from packages/astro/test/units/render/head.test.js rename to packages/astro/test/units/render/head.test.ts index a289661f24cf..87f935bec1ab 100644 --- a/packages/astro/test/units/render/head.test.js +++ b/packages/astro/test/units/render/head.test.ts @@ -5,19 +5,26 @@ import { RenderContext } from '../../../dist/core/render-context.js'; import { createComponent, Fragment, - maybeRenderHead, + maybeRenderHead as _maybeRenderHead, render, renderComponent, - renderHead, + renderHead as _renderHead, renderSlot, } from '../../../dist/runtime/server/index.js'; -import { createBasicPipeline } from '../test-utils.js'; +import type { AstroComponentFactory } from '../../../dist/runtime/server/render/index.js'; +import type { Pipeline } from '../../../dist/core/render/index.js'; +import { createBasicPipeline } from '../test-utils.ts'; -const createAstroModule = (AstroComponent) => ({ default: AstroComponent }); +// The public types for renderHead/maybeRenderHead declare zero params, +// but the runtime implementation accepts a result argument. +const renderHead = _renderHead as (result: any) => any; +const maybeRenderHead = _maybeRenderHead as (result: any) => any; + +const createAstroModule = (AstroComponent: AstroComponentFactory) => ({ default: AstroComponent }); describe('core/render', () => { describe('Injected head contents', () => { - let pipeline; + let pipeline: Pipeline; before(async () => { pipeline = createBasicPipeline(); pipeline.headElements = () => ({ @@ -30,7 +37,7 @@ describe('core/render', () => { }); it('Multi-level layouts and head injection, with explicit head', async () => { - const BaseLayout = createComponent((result, _props, slots) => { + const BaseLayout = createComponent((result: any, _props: any, slots: any) => { return render` ${renderSlot(result, slots['head'])} @@ -43,7 +50,7 @@ describe('core/render', () => { `; }); - const PageLayout = createComponent((result, _props, slots) => { + const PageLayout = createComponent((result: any, _props: any, slots: any) => { return render`${renderComponent( result, 'Layout', @@ -72,7 +79,7 @@ describe('core/render', () => { `; }); - const Page = createComponent((result) => { + const Page = createComponent((result: any) => { return render`${renderComponent( result, 'PageLayout', @@ -103,7 +110,7 @@ describe('core/render', () => { component: 'src/pages/index.astro', params: {}, }; - const renderContext = await RenderContext.create({ pipeline, request, routeData }); + const renderContext = await RenderContext.create({ pipeline, request, routeData } as any); const response = await renderContext.render(PageModule); const html = await response.text(); @@ -114,7 +121,7 @@ describe('core/render', () => { }); it('Multi-level layouts and head injection, without explicit head', async () => { - const BaseLayout = createComponent((result, _props, slots) => { + const BaseLayout = createComponent((result: any, _props: any, slots: any) => { return render` ${renderSlot(result, slots['head'])} ${maybeRenderHead(result)} @@ -124,7 +131,7 @@ describe('core/render', () => { `; }); - const PageLayout = createComponent((result, _props, slots) => { + const PageLayout = createComponent((result: any, _props: any, slots: any) => { return render`${renderComponent( result, 'Layout', @@ -153,7 +160,7 @@ describe('core/render', () => { `; }); - const Page = createComponent((result) => { + const Page = createComponent((result: any) => { return render`${renderComponent( result, 'PageLayout', @@ -184,7 +191,7 @@ describe('core/render', () => { component: 'src/pages/index.astro', params: {}, }; - const renderContext = await RenderContext.create({ pipeline, request, routeData }); + const renderContext = await RenderContext.create({ pipeline, request, routeData } as any); const response = await renderContext.render(PageModule); const html = await response.text(); @@ -195,11 +202,11 @@ describe('core/render', () => { }); it('Multi-level layouts and head injection, without any content in layouts', async () => { - const BaseLayout = createComponent((result, _props, slots) => { + const BaseLayout = createComponent((result: any, _props: any, slots: any) => { return render`${renderSlot(result, slots['default'])}`; }); - const PageLayout = createComponent((result, _props, slots) => { + const PageLayout = createComponent((result: any, _props: any, slots: any) => { return render`${renderComponent( result, 'Layout', @@ -212,7 +219,7 @@ describe('core/render', () => { `; }); - const Page = createComponent((result) => { + const Page = createComponent((result: any) => { return render`${renderComponent( result, 'PageLayout', @@ -232,7 +239,7 @@ describe('core/render', () => { component: 'src/pages/index.astro', params: {}, }; - const renderContext = await RenderContext.create({ pipeline, request, routeData }); + const renderContext = await RenderContext.create({ pipeline, request, routeData } as any); const response = await renderContext.render(PageModule); const html = await response.text(); diff --git a/packages/astro/test/units/render/html-primitives.test.js b/packages/astro/test/units/render/html-primitives.test.ts similarity index 78% rename from packages/astro/test/units/render/html-primitives.test.js rename to packages/astro/test/units/render/html-primitives.test.ts index 2de5f10cdf4d..cea841009124 100644 --- a/packages/astro/test/units/render/html-primitives.test.js +++ b/packages/astro/test/units/render/html-primitives.test.ts @@ -1,4 +1,3 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import * as cheerio from 'cheerio'; @@ -13,12 +12,13 @@ import { } from '../../../dist/runtime/server/render/util.js'; import { createComponent, + Fragment, render as renderTemplate, renderComponent, renderSlot, unescapeHTML, } from '../../../dist/runtime/server/index.js'; -import { createTestApp, createPage } from '../mocks.js'; +import { createTestApp, createPage } from '../mocks.ts'; describe('toAttributeString', () => { it('escapes & to &', () => { @@ -107,7 +107,31 @@ describe('defineScriptVars', () => { it('sanitizes to prevent XSS injection', () => { const result = String(defineScriptVars({ evil: '' })); assert.ok(!result.includes(''), 'should not contain literal '); - assert.ok(result.includes('\\x3C/script>'), 'should escape the closing tag'); + assert.ok(result.includes('\\u003c/script>'), 'should escape the closing tag'); + }); + + it('sanitizes case-insensitive variants', () => { + for (const tag of ['', '', '']) { + const result = String(defineScriptVars({ evil: tag })); + assert.ok(!result.includes(tag), `should not contain literal ${tag}`); + } + }); + + it('sanitizes with trailing whitespace before >', () => { + for (const tag of ['', '', '']) { + const result = String(defineScriptVars({ evil: tag })); + assert.ok(!result.includes(tag), `should not contain literal ${JSON.stringify(tag)}`); + } + }); + + it('sanitizes self-closing ', () => { + const result = String(defineScriptVars({ evil: '' })); + assert.ok(!result.includes(''), 'should not contain literal '); + }); + + it('handles undefined values without throwing', () => { + const result = String(defineScriptVars({ undef: undefined })); + assert.ok(result.includes('const undef = undefined;')); }); it('converts keys with spaces to valid JS identifiers', () => { @@ -224,7 +248,7 @@ describe('Supports void elements whose name is a string (#2062)', async () => { // Mirrors Input.astro: a component that picks between input/select/textarea // based on the `type` prop, demonstrating that void element detection works // when the tag name is a runtime string value, not a literal. - const Input = createComponent((result, props, slots) => { + const Input = createComponent((result: any, props: any, slots: any) => { const Astro = result.createAstro(props, slots); const { type: initialType, ...rest } = Astro.props; const isSelect = /^select$/i.test(initialType); @@ -242,7 +266,7 @@ describe('Supports void elements whose name is a string (#2062)', async () => { }); const inputPage = createComponent( - (result) => renderTemplate` + (result: any) => renderTemplate` ${renderComponent(result, 'Input', Input, {})} ${renderComponent(result, 'Input', Input, { type: 'password' })} ${renderComponent(result, 'Input', Input, { type: 'text' })} @@ -320,42 +344,95 @@ describe('Allows using the Fragment element', async () => { const $ = cheerio.load(await response.text()); assert.equal($('#one').length, 1); }); + + it('streams sync siblings before async children resolve (issue #13283)', async () => { + // A deferred promise simulates a slow async child inside the Fragment. + let resolveAsync: () => void; + const asyncChild = new Promise((resolve) => { + resolveAsync = resolve; + }); + + const DEFAULT_RESULT = { clientDirectives: new Map() }; + + // Build a Fragment whose default slot contains a sync

    followed by an async

    . + const renderInstance = renderComponent( + DEFAULT_RESULT as any, + 'Fragment', + Fragment, + {}, + { + default: (_result: any) => + renderTemplate`

    sync

    ${asyncChild.then( + () => renderTemplate`

    async

    `, + )}`, + }, + ); + + // Collect chunks as they are written so we can inspect ordering. + const chunks: string[] = []; + const destination = { + write(chunk: unknown) { + chunks.push(String(chunk)); + }, + }; + + // Start rendering — do NOT await yet so we can inspect mid-flight state. + const instance = await Promise.resolve(renderInstance); + const renderPromise = (instance as any).render(destination); + + // Yield to the microtask queue so the sync portion can flush. + await Promise.resolve(); + + // The sync

    must have been written before the async promise resolved. + const syncFlushed = chunks.join('').includes('sync'); + assert.ok(syncFlushed, 'sync sibling should stream before async child resolves'); + + // Now resolve the async child and finish rendering. + resolveAsync!(); + await renderPromise; + + const html = chunks.join(''); + assert.ok(html.includes('sync'), 'sync content present in final output'); + assert.ok(html.includes('async'), 'async content present in final output'); + // Sync must appear before async in the output. + assert.ok(html.indexOf('sync') < html.indexOf('async'), 'sync appears before async in output'); + }); }); describe('renders the components top-down', async () => { it('renders sibling components in document order', async () => { // Mirrors order.astro + OrderA/B/Last.astro using globalThis to track render order - globalThis.__ASTRO_TEST_ORDER__ = []; + (globalThis as any).__ASTRO_TEST_ORDER__ = []; - const OrderA = createComponent((result, _p, slots) => { - globalThis.__ASTRO_TEST_ORDER__.push('A'); + const OrderA = createComponent((result: any, _p: any, slots: any) => { + (globalThis as any).__ASTRO_TEST_ORDER__.push('A'); return renderTemplate`

    A

    ${renderSlot(result, slots.default)}`; }); - const OrderB = createComponent((result, _p, slots) => { - globalThis.__ASTRO_TEST_ORDER__.push('B'); + const OrderB = createComponent((result: any, _p: any, slots: any) => { + (globalThis as any).__ASTRO_TEST_ORDER__.push('B'); return renderTemplate`

    B

    ${renderSlot(result, slots.default)}`; }); const OrderLast = createComponent( () => - renderTemplate`

    Rendered order: ${() => (globalThis.__ASTRO_TEST_ORDER__ ?? []).join(', ')}

    `, + renderTemplate`

    Rendered order: ${() => ((globalThis as any).__ASTRO_TEST_ORDER__ ?? []).join(', ')}

    `, ); const page = createComponent( - (result) => + (result: any) => renderTemplate`${renderComponent( result, 'OrderA', OrderA, {}, { - default: (result2) => + default: (result2: any) => renderTemplate`${renderComponent( result2, 'OrderB', OrderB, {}, { - default: (result3) => + default: (result3: any) => renderTemplate`${renderComponent(result3, 'OrderLast', OrderLast, {})}`, }, )}`, diff --git a/packages/astro/test/units/render/hydration.test.js b/packages/astro/test/units/render/hydration.test.ts similarity index 96% rename from packages/astro/test/units/render/hydration.test.js rename to packages/astro/test/units/render/hydration.test.ts index 5b11e90a9564..afc22e94978d 100644 --- a/packages/astro/test/units/render/hydration.test.js +++ b/packages/astro/test/units/render/hydration.test.ts @@ -1,4 +1,3 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { extractDirectives } from '../../../dist/runtime/server/hydration.js'; @@ -138,9 +137,9 @@ describe('extractDirectives', () => { it('throws for an invalid hydration directive', () => { assert.throws( () => extractDirectives({ 'client:unknown': '' }, clientDirectives), - (err) => { - assert.ok(err.message.includes('invalid hydration directive')); - assert.ok(err.message.includes('client:unknown')); + (err: unknown) => { + assert.ok((err as Error).message.includes('invalid hydration directive')); + assert.ok((err as Error).message.includes('client:unknown')); return true; }, ); diff --git a/packages/astro/test/units/render/paginate.test.js b/packages/astro/test/units/render/paginate.test.ts similarity index 97% rename from packages/astro/test/units/render/paginate.test.js rename to packages/astro/test/units/render/paginate.test.ts index ff74c301bfac..d8b73eb49d1e 100644 --- a/packages/astro/test/units/render/paginate.test.js +++ b/packages/astro/test/units/render/paginate.test.ts @@ -1,15 +1,13 @@ -// @ts-check import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { generatePaginateFunction } from '../../../dist/core/render/paginate.js'; -import { createRouteData } from '../mocks.js'; +import { createRouteData } from '../mocks.ts'; const items = Array.from({ length: 25 }, (_, i) => `item-${i + 1}`); describe('Pagination — optional root page (spread route)', () => { const route = createRouteData({ route: '/posts/optional-root-page/[...page]', - params: ['...page'], segments: [ [{ content: 'posts', dynamic: false, spread: false }], [{ content: 'optional-root-page', dynamic: false, spread: false }], @@ -49,7 +47,6 @@ describe('Pagination — named root page (non-spread route)', () => { // Non-spread route => page 1 has page param "1" (always included) const route = createRouteData({ route: '/posts/named-root-page/[page]', - params: ['page'], segments: [ [{ content: 'posts', dynamic: false, spread: false }], [{ content: 'named-root-page', dynamic: false, spread: false }], @@ -82,7 +79,6 @@ describe('Pagination — multiple params (color + page)', () => { // Each color has its own set of pages; base='/blog', trailingSlash='never' const route = createRouteData({ route: '/posts/[color]/[page]', - params: ['color', 'page'], segments: [ [{ content: 'posts', dynamic: false, spread: false }], [{ content: 'color', dynamic: true, spread: false }], @@ -141,7 +137,6 @@ describe('Pagination — root spread, correct prev URL — Migrated from astro-p // 4 items, pageSize 1 → 4 pages; root spread means page 1 has no number in URL. const route = createRouteData({ route: '/[...page]', - params: ['...page'], segments: [[{ content: '...page', dynamic: true, spread: true }]], }); const paginate = generatePaginateFunction(route, '/blog', 'ignore'); diff --git a/packages/astro/test/units/render/queue-batching.test.js b/packages/astro/test/units/render/queue-batching.test.ts similarity index 84% rename from packages/astro/test/units/render/queue-batching.test.js rename to packages/astro/test/units/render/queue-batching.test.ts index db56cf242198..8f50afa445f2 100644 --- a/packages/astro/test/units/render/queue-batching.test.js +++ b/packages/astro/test/units/render/queue-batching.test.ts @@ -4,6 +4,7 @@ import { buildRenderQueue } from '../../../dist/runtime/server/render/queue/buil import { renderQueue } from '../../../dist/runtime/server/render/queue/renderer.js'; import { NodePool } from '../../../dist/runtime/server/render/queue/pool.js'; import { markHTMLString } from '../../../dist/runtime/server/index.js'; +import type { RenderDestination } from '../../../dist/runtime/server/render/common.js'; // Mock SSRResult for testing function createMockResult() { @@ -13,7 +14,7 @@ function createMockResult() { hasRenderedHead: false, hasDirectives: new Set(), headInTree: false, - extraHead: [], + extraHead: [] as string[], propagators: new Set(), }, styles: new Set(), @@ -23,7 +24,7 @@ function createMockResult() { } // Create a NodePool for testing -function createMockPool() { +function createMockPool(): NodePool { return new NodePool(1000); } @@ -33,7 +34,7 @@ describe('Queue batching optimization', () => { const pool = createMockPool(); const items = ['Hello', ' ', 'world', '!']; - const queue = await buildRenderQueue(items, result, pool); + const queue = await buildRenderQueue(items, result as any, pool); // All text nodes should be in the queue assert.equal(queue.nodes.length, 4); @@ -45,7 +46,7 @@ describe('Queue batching optimization', () => { // When rendered, they should be batched into one write let writeCount = 0; let output = ''; - const destination = { + const destination: RenderDestination = { write(chunk) { writeCount++; output += String(chunk); @@ -64,11 +65,11 @@ describe('Queue batching optimization', () => { const items = [markHTMLString('
    '), markHTMLString('content'), markHTMLString('
    ')]; - const queue = await buildRenderQueue(items, result, pool); + const queue = await buildRenderQueue(items, result as any, pool); let writeCount = 0; let output = ''; - const destination = { + const destination: RenderDestination = { write(chunk) { writeCount++; output += String(chunk); @@ -88,18 +89,18 @@ describe('Queue batching optimization', () => { // Create a simple component const componentInstance = { - render(dest) { + render(dest: RenderDestination) { dest.write('

    Component

    '); }, }; const items = ['before', componentInstance, 'after']; - const queue = await buildRenderQueue(items, result, pool); + const queue = await buildRenderQueue(items, result as any, pool); let writeCount = 0; let output = ''; - const destination = { + const destination: RenderDestination = { write(chunk) { writeCount++; output += String(chunk); @@ -120,12 +121,12 @@ describe('Queue batching optimization', () => { // Create a large array of text items (simulating a list) const items = Array.from({ length: 1000 }, (_, i) => `Item ${i + 1}`); - const queue = await buildRenderQueue(items, result, pool); + const queue = await buildRenderQueue(items, result as any, pool); assert.equal(queue.nodes.length, 1000); let writeCount = 0; - const destination = { + const destination: RenderDestination = { write() { writeCount++; }, @@ -149,11 +150,11 @@ describe('Queue batching optimization', () => { markHTMLString('Italic'), ]; - const queue = await buildRenderQueue(items, result, pool); + const queue = await buildRenderQueue(items, result as any, pool); let writeCount = 0; let output = ''; - const destination = { + const destination: RenderDestination = { write(chunk) { writeCount++; output += String(chunk); diff --git a/packages/astro/test/units/render/queue-pool.test.js b/packages/astro/test/units/render/queue-pool.test.ts similarity index 90% rename from packages/astro/test/units/render/queue-pool.test.js rename to packages/astro/test/units/render/queue-pool.test.ts index a0b9a6aed168..8308c4d7fe8d 100644 --- a/packages/astro/test/units/render/queue-pool.test.js +++ b/packages/astro/test/units/render/queue-pool.test.ts @@ -1,6 +1,12 @@ import { describe, it } from 'node:test'; import { strictEqual, notStrictEqual } from 'node:assert'; import { NodePool } from '../../../dist/runtime/server/render/queue/pool.js'; +import type { + TextNode, + HtmlStringNode, + ComponentNode, + InstructionNode, +} from '../../../dist/runtime/server/render/queue/types.js'; describe('NodePool', () => { it('should acquire a new node when pool is empty', () => { @@ -15,7 +21,7 @@ describe('NodePool', () => { const pool = new NodePool(); // Acquire and set up a text node - const node1 = pool.acquire('text'); + const node1 = pool.acquire('text') as TextNode; node1.content = 'Hello'; // Release it back to the pool @@ -36,13 +42,13 @@ describe('NodePool', () => { const pool = new NodePool(); // Acquire and release a text node - const node1 = pool.acquire('text'); + const node1 = pool.acquire('text') as TextNode; node1.content = 'Hello'; pool.release(node1); strictEqual(pool.size(), 1); // Acquire an html-string node - should NOT reuse the text node - const node2 = pool.acquire('html-string'); + const node2 = pool.acquire('html-string') as HtmlStringNode; strictEqual(node2.type, 'html-string'); strictEqual(node2.html, ''); @@ -159,7 +165,7 @@ describe('NodePool', () => { const pool = new NodePool(); // Test all four node types for identity reuse - const types = ['text', 'html-string', 'component', 'instruction']; + const types = ['text', 'html-string', 'component', 'instruction'] as const; for (const type of types) { const original = pool.acquire(type); @@ -173,20 +179,20 @@ describe('NodePool', () => { const pool = new NodePool(); // Component node - instance should be cleared - const compNode = pool.acquire('component'); - compNode.instance = { render: () => {} }; // Simulate a component instance + const compNode = pool.acquire('component') as ComponentNode; + compNode.instance = { render: () => {} } as any; // Simulate a component instance pool.release(compNode); - const reusedComp = pool.acquire('component'); + const reusedComp = pool.acquire('component') as ComponentNode; strictEqual(reusedComp, compNode); // Same object strictEqual(reusedComp.instance, undefined); // Instance cleared on release // Instruction node - instruction should be cleared - const instrNode = pool.acquire('instruction'); - instrNode.instruction = { type: 'head' }; // Simulate an instruction + const instrNode = pool.acquire('instruction') as InstructionNode; + instrNode.instruction = { type: 'head' } as any; // Simulate an instruction pool.release(instrNode); - const reusedInstr = pool.acquire('instruction'); + const reusedInstr = pool.acquire('instruction') as InstructionNode; strictEqual(reusedInstr, instrNode); // Same object strictEqual(reusedInstr.instruction, undefined); // Instruction cleared on release }); @@ -242,12 +248,12 @@ describe('NodePool', () => { const pool = new NodePool(); // Release a text node - const node = pool.acquire('text'); + const node = pool.acquire('text') as TextNode; node.content = 'old content'; pool.release(node); // Acquire with content parameter - content should be set on the reused node - const reused = pool.acquire('text', 'new content'); + const reused = pool.acquire('text', 'new content') as TextNode; strictEqual(reused, node); // Same object strictEqual(reused.content, 'new content'); }); diff --git a/packages/astro/test/units/render/queue-rendering.test.js b/packages/astro/test/units/render/queue-rendering.test.ts similarity index 69% rename from packages/astro/test/units/render/queue-rendering.test.js rename to packages/astro/test/units/render/queue-rendering.test.ts index 724812396a7f..f4d21b64ee59 100644 --- a/packages/astro/test/units/render/queue-rendering.test.js +++ b/packages/astro/test/units/render/queue-rendering.test.ts @@ -4,6 +4,14 @@ import { buildRenderQueue } from '../../../dist/runtime/server/render/queue/buil import { renderQueue } from '../../../dist/runtime/server/render/queue/renderer.js'; import { NodePool } from '../../../dist/runtime/server/render/queue/pool.js'; import { renderPage } from '../../../dist/runtime/server/render/page.js'; +import type { RenderDestination } from '../../../dist/runtime/server/render/common.js'; +import type { QueueNode, TextNode } from '../../../dist/runtime/server/render/queue/types.js'; + +/** Type-safe accessor for text node content */ +function textContent(node: QueueNode): string { + assert.equal(node.type, 'text'); + return (node as TextNode).content; +} /** * Tests for the queue-based rendering engine @@ -21,9 +29,9 @@ describe('Queue-based rendering engine', () => { hasDirectives: new Set(), hasRenderedServerIslandRuntime: false, headInTree: false, - extraHead: [], - extraStyleHashes: [], - extraScriptHashes: [], + extraHead: [] as string[], + extraStyleHashes: [] as string[], + extraScriptHashes: [] as string[], propagators: new Set(), }, styles: new Set(), @@ -36,7 +44,7 @@ describe('Queue-based rendering engine', () => { } // Create a NodePool for testing - function createMockPool() { + function createMockPool(): NodePool { return new NodePool(1000); } @@ -44,48 +52,49 @@ describe('Queue-based rendering engine', () => { it('should handle simple text nodes', async () => { const result = createMockResult(); const pool = createMockPool(); - const queue = await buildRenderQueue('Hello, World!', result, pool); + const queue = await buildRenderQueue('Hello, World!', result as any, pool); assert.ok(queue.nodes.length > 0); assert.equal(queue.nodes[0].type, 'text'); - assert.equal(queue.nodes[0].content, 'Hello, World!'); + assert.equal(textContent(queue.nodes[0]), 'Hello, World!'); }); it('should handle numbers', async () => { const result = createMockResult(); const pool = createMockPool(); - const queue = await buildRenderQueue(42, result, pool); + const queue = await buildRenderQueue(42, result as any, pool); assert.ok(queue.nodes.length > 0); assert.equal(queue.nodes[0].type, 'text'); - assert.equal(queue.nodes[0].content, '42'); + assert.equal(textContent(queue.nodes[0]), '42'); }); it('should handle booleans', async () => { const result = createMockResult(); const pool = createMockPool(); - const queue = await buildRenderQueue(true, result, pool); + const queue = await buildRenderQueue(true, result as any, pool); assert.ok(queue.nodes.length > 0); assert.equal(queue.nodes[0].type, 'text'); - assert.equal(queue.nodes[0].content, 'true'); + assert.equal(textContent(queue.nodes[0]), 'true'); }); it('should handle arrays', async () => { const result = createMockResult(); const pool = createMockPool(); - const queue = await buildRenderQueue(['Hello', ' ', 'World'], result, pool); + const queue = await buildRenderQueue(['Hello', ' ', 'World'], result as any, pool); assert.equal(queue.nodes.length, 3); - assert.equal(queue.nodes[0].content, 'Hello'); - assert.equal(queue.nodes[1].content, ' '); - assert.equal(queue.nodes[2].content, 'World'); + assert.equal(textContent(queue.nodes[0]), 'Hello'); + assert.equal(textContent(queue.nodes[1]), ' '); + assert.equal(textContent(queue.nodes[2]), 'World'); }); it('should handle null and undefined (skip them)', async () => { const result = createMockResult(); - const nullQueue = await buildRenderQueue(null, result); - const undefinedQueue = await buildRenderQueue(undefined, result); + const pool = createMockPool(); + const nullQueue = await buildRenderQueue(null, result as any, pool); + const undefinedQueue = await buildRenderQueue(undefined, result as any, pool); assert.equal(nullQueue.nodes.length, 0); assert.equal(undefinedQueue.nodes.length, 0); @@ -94,33 +103,33 @@ describe('Queue-based rendering engine', () => { it('should skip false but render 0', async () => { const result = createMockResult(); const pool = createMockPool(); - const falseQueue = await buildRenderQueue(false, result, pool); - const zeroQueue = await buildRenderQueue(0, result, pool); + const falseQueue = await buildRenderQueue(false, result as any, pool); + const zeroQueue = await buildRenderQueue(0, result as any, pool); assert.equal(falseQueue.nodes.length, 0); assert.equal(zeroQueue.nodes.length, 1); - assert.equal(zeroQueue.nodes[0].content, '0'); + assert.equal(textContent(zeroQueue.nodes[0]), '0'); }); it('should handle promises', async () => { const result = createMockResult(); const promise = Promise.resolve('Resolved value'); const pool = createMockPool(); - const queue = await buildRenderQueue(promise, result, pool); + const queue = await buildRenderQueue(promise, result as any, pool); assert.equal(queue.nodes.length, 1); - assert.equal(queue.nodes[0].content, 'Resolved value'); + assert.equal(textContent(queue.nodes[0]), 'Resolved value'); }); it('should handle nested arrays', async () => { const result = createMockResult(); const pool = createMockPool(); - const queue = await buildRenderQueue([['Nested', ' '], 'Array'], result, pool); + const queue = await buildRenderQueue([['Nested', ' '], 'Array'], result as any, pool); assert.equal(queue.nodes.length, 3); - assert.equal(queue.nodes[0].content, 'Nested'); - assert.equal(queue.nodes[1].content, ' '); - assert.equal(queue.nodes[2].content, 'Array'); + assert.equal(textContent(queue.nodes[0]), 'Nested'); + assert.equal(textContent(queue.nodes[1]), ' '); + assert.equal(textContent(queue.nodes[2]), 'Array'); }); it('should handle async iterables', async () => { @@ -133,46 +142,46 @@ describe('Queue-based rendering engine', () => { } const pool = createMockPool(); - const queue = await buildRenderQueue(asyncGen(), result, pool); + const queue = await buildRenderQueue(asyncGen(), result as any, pool); assert.equal(queue.nodes.length, 3); - assert.equal(queue.nodes[0].content, 'First'); - assert.equal(queue.nodes[1].content, 'Second'); - assert.equal(queue.nodes[2].content, 'Third'); + assert.equal(textContent(queue.nodes[0]), 'First'); + assert.equal(textContent(queue.nodes[1]), 'Second'); + assert.equal(textContent(queue.nodes[2]), 'Third'); }); it('should track parent relationships', async () => { const result = createMockResult(); const nestedArray = [['child1', 'child2'], 'sibling']; const pool = createMockPool(); - const queue = await buildRenderQueue(nestedArray, result, pool); + const queue = await buildRenderQueue(nestedArray, result as any, pool); // Verify correct node structure assert.equal(queue.nodes.length, 3); - assert.equal(queue.nodes[0].content, 'child1'); - assert.equal(queue.nodes[1].content, 'child2'); - assert.equal(queue.nodes[2].content, 'sibling'); + assert.equal(textContent(queue.nodes[0]), 'child1'); + assert.equal(textContent(queue.nodes[1]), 'child2'); + assert.equal(textContent(queue.nodes[2]), 'sibling'); }); it('should maintain correct rendering order', async () => { const result = createMockResult(); const pool = createMockPool(); - const queue = await buildRenderQueue(['A', 'B', 'C'], result, pool); + const queue = await buildRenderQueue(['A', 'B', 'C'], result as any, pool); - assert.equal(queue.nodes[0].content, 'A'); - assert.equal(queue.nodes[1].content, 'B'); - assert.equal(queue.nodes[2].content, 'C'); + assert.equal(textContent(queue.nodes[0]), 'A'); + assert.equal(textContent(queue.nodes[1]), 'B'); + assert.equal(textContent(queue.nodes[2]), 'C'); }); it('should handle sync iterables (Set)', async () => { const result = createMockResult(); const set = new Set(['One', 'Two', 'Three']); const pool = createMockPool(); - const queue = await buildRenderQueue(set, result, pool); + const queue = await buildRenderQueue(set, result as any, pool); assert.equal(queue.nodes.length, 3); // Set iteration order is insertion order - const contents = queue.nodes.map((n) => n.content); + const contents = queue.nodes.map((n) => textContent(n)); assert.ok(contents.includes('One')); assert.ok(contents.includes('Two')); assert.ok(contents.includes('Three')); @@ -183,10 +192,10 @@ describe('Queue-based rendering engine', () => { it('should render simple text to string', async () => { const result = createMockResult(); const pool = createMockPool(); - const queue = await buildRenderQueue('Test content', result, pool); + const queue = await buildRenderQueue('Test content', result as any, pool); let output = ''; - const destination = { + const destination: RenderDestination = { write(chunk) { output += String(chunk); }, @@ -199,10 +208,10 @@ describe('Queue-based rendering engine', () => { it('should render array to concatenated string', async () => { const result = createMockResult(); const pool = createMockPool(); - const queue = await buildRenderQueue(['Hello', ' ', 'World'], result, pool); + const queue = await buildRenderQueue(['Hello', ' ', 'World'], result as any, pool); let output = ''; - const destination = { + const destination: RenderDestination = { write(chunk) { output += String(chunk); }, @@ -215,10 +224,10 @@ describe('Queue-based rendering engine', () => { it('should escape HTML in text nodes', async () => { const result = createMockResult(); const pool = createMockPool(); - const queue = await buildRenderQueue('', result, pool); + const queue = await buildRenderQueue('', result as any, pool); let output = ''; - const destination = { + const destination: RenderDestination = { write(chunk) { output += String(chunk); }, @@ -232,10 +241,10 @@ describe('Queue-based rendering engine', () => { it('should handle empty queue', async () => { const result = createMockResult(); const pool = createMockPool(); - const queue = await buildRenderQueue(null, result, pool); + const queue = await buildRenderQueue(null, result as any, pool); let output = ''; - const destination = { + const destination: RenderDestination = { write(chunk) { output += String(chunk); }, @@ -248,10 +257,10 @@ describe('Queue-based rendering engine', () => { it('should render numbers correctly', async () => { const result = createMockResult(); const pool = createMockPool(); - const queue = await buildRenderQueue([1, 2, 3], result, pool); + const queue = await buildRenderQueue([1, 2, 3], result as any, pool); let output = ''; - const destination = { + const destination: RenderDestination = { write(chunk) { output += String(chunk); }, @@ -279,9 +288,9 @@ describe('renderPage() with queuedRendering and .html pages', () => { hasDirectives: new Set(), hasRenderedServerIslandRuntime: false, headInTree: false, - extraHead: [], - extraStyleHashes: [], - extraScriptHashes: [], + extraHead: [] as string[], + extraStyleHashes: [] as string[], + extraScriptHashes: [] as string[], propagators: new Set(), }, styles: new Set(), @@ -303,15 +312,15 @@ describe('renderPage() with queuedRendering and .html pages', () => { it('does not escape HTML tags when rendering a .html page component', async () => { // Simulate the component factory generated by vite-plugin-html for a .html file. // These return a plain string and have `astro:html = true`. - const htmlPageFactory = function render(_props) { + const htmlPageFactory = function render(_props: Record) { return '\n \n'; }; - htmlPageFactory['astro:html'] = true; - htmlPageFactory.moduleId = 'src/pages/admin/index.html'; + (htmlPageFactory as any)['astro:html'] = true; + (htmlPageFactory as any).moduleId = 'src/pages/admin/index.html'; const result = createMockResultWithQueue(); - const response = await renderPage(result, htmlPageFactory, {}, null, false); + const response = await renderPage(result as any, htmlPageFactory as any, {}, null, false); const html = await response.text(); // The raw '; }; // No astro:html flag set — this is the default for non-.html components - regularFactory.moduleId = 'src/pages/regular.astro'; + (regularFactory as any).moduleId = 'src/pages/regular.astro'; const result = createMockResultWithQueue(); - const response = await renderPage(result, regularFactory, {}, null, false); + const response = await renderPage(result as any, regularFactory as any, {}, null, false); const html = await response.text(); assert.ok(!html.includes(''; const expected = ''; - const result = await testEscapeTransform(input, expected); + const result = await testEscapeTransform(input); assert.equal(result, expected); }); @@ -132,14 +131,14 @@ describe('vite-plugin-html: escape transformer', () => { it('preserves content without template literal characters', async () => { const input = '
    Hello world!
    '; - const result = await testEscapeTransform(input, input); + const result = await testEscapeTransform(input); assert.equal(result, input); }); it('handles empty attributes correctly', async () => { const input = '
    '; const expected = '
    '; - const result = await testEscapeTransform(input, expected); + const result = await testEscapeTransform(input); assert.equal(result, expected); }); }); diff --git a/packages/astro/test/units/vite-plugin-html/slots.test.js b/packages/astro/test/units/vite-plugin-html/slots.test.ts similarity index 98% rename from packages/astro/test/units/vite-plugin-html/slots.test.js rename to packages/astro/test/units/vite-plugin-html/slots.test.ts index 0b6694992ae6..829fd5511b7a 100644 --- a/packages/astro/test/units/vite-plugin-html/slots.test.js +++ b/packages/astro/test/units/vite-plugin-html/slots.test.ts @@ -6,7 +6,7 @@ import { VFile } from 'vfile'; import rehypeSlots, { SLOT_PREFIX } from '../../../dist/vite-plugin-html/transform/slots.js'; describe('vite-plugin-html: slot transformer', () => { - async function testSlotTransform(html) { + async function testSlotTransform(html: string) { const s = new MagicString(html); const processor = rehype().data('settings', { fragment: true }).use(rehypeSlots, { s }); diff --git a/packages/astro/test/units/vite-plugin-html/transform.test.js b/packages/astro/test/units/vite-plugin-html/transform.test.ts similarity index 90% rename from packages/astro/test/units/vite-plugin-html/transform.test.js rename to packages/astro/test/units/vite-plugin-html/transform.test.ts index e6cbf6be88c4..df5cc9078e3e 100644 --- a/packages/astro/test/units/vite-plugin-html/transform.test.js +++ b/packages/astro/test/units/vite-plugin-html/transform.test.ts @@ -59,17 +59,15 @@ describe('vite-plugin-html: transform integration', () => { assert.doesNotMatch(result.code, /\$\{___SLOTS___\["default"\]/); }); - it( - 'handles complex escaping in attributes', - { skip: 'There is a bug in replaceAttribute with multiple attributes' }, - async () => { - const code = '
    '; - const result = await transform(code, 'test.html'); - - assert.match(result.code, /data-value="\\\$\{foo\}"/); - assert.match(result.code, /data-template="\\`\\\$\{bar\}\\`"/); - }, - ); + it('handles complex escaping in attributes', { + skip: 'There is a bug in replaceAttribute with multiple attributes', + }, async () => { + const code = '
    '; + const result = await transform(code, 'test.html'); + + assert.match(result.code, /data-value="\\\$\{foo\}"/); + assert.match(result.code, /data-template="\\`\\\$\{bar\}\\`"/); + }); it('transforms empty HTML', async () => { const code = ''; diff --git a/packages/astro/test/unused-slot.test.js b/packages/astro/test/unused-slot.test.js deleted file mode 100644 index 38d4b860ba06..000000000000 --- a/packages/astro/test/unused-slot.test.js +++ /dev/null @@ -1,20 +0,0 @@ -import assert from 'node:assert/strict'; -import { before, describe, it } from 'node:test'; -import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; - -describe('Unused slot', () => { - let fixture; - - before(async () => { - fixture = await loadFixture({ root: './fixtures/unused-slot/' }); - await fixture.build(); - }); - - it('is able to build with the slot missing', async () => { - let html = await fixture.readFile('/index.html'); - let $ = cheerio.load(html); - // No children, slot rendered as empty - assert.equal($('body p').children().length, 0); - }); -}); diff --git a/packages/astro/tsconfig.json b/packages/astro/tsconfig.json index b12a70b882d0..473fa36b5f04 100644 --- a/packages/astro/tsconfig.json +++ b/packages/astro/tsconfig.json @@ -3,10 +3,13 @@ "include": ["src", "dev-only.d.ts"], "exclude": ["dist"], "compilerOptions": { + "composite": true, "rootDir": "./src", "allowJs": true, "declarationDir": "./dist", "outDir": "./dist", - "jsx": "preserve" + "jsx": "preserve", + "tsBuildInfoFile": "${configDir}/dist/._cache/tsconfig/tsbuildinfo.json", + "erasableSyntaxOnly": true } } diff --git a/packages/astro/tsconfig.test.json b/packages/astro/tsconfig.test.json index 65a0ab0b0c44..9750a44694a4 100644 --- a/packages/astro/tsconfig.test.json +++ b/packages/astro/tsconfig.test.json @@ -1,12 +1,32 @@ { "extends": "../../tsconfig.base.json", - "include": ["test/units/**/*.ts"], - "exclude": ["test/units/_temp-fixtures/**", "test/fixtures/**"], + "include": [ + "test/units/**/*.ts", + "test/*.ts", + "e2e/*.ts", + "test/test-adapter.js", + "test/test-image-service.js", + "test/test-plugins.js", + "test/test-prerenderer.js", + "test/test-remote-image-service.js", + "test/test-utils.js", + "package.json" + ], + "exclude": ["test/units/_temp-fixtures/**", "test/fixtures/**", "e2e/fixtures/**"], "compilerOptions": { - "noEmit": true, + "types": ["vite/client", "node"], + "composite": true, "allowJs": true, "noUnusedLocals": false, "noUnusedParameters": false, "rewriteRelativeImportExtensions": true - } + }, + "references": [ + { + "path": "./tsconfig.json" + }, + { + "path": "../../scripts/tsconfig.json" + } + ] } diff --git a/packages/create-astro/package.json b/packages/create-astro/package.json index 1f43630eaf1e..596eeed7417b 100644 --- a/packages/create-astro/package.json +++ b/packages/create-astro/package.json @@ -22,7 +22,8 @@ "build": "astro-scripts build \"src/index.ts\" --bundle && tsc", "build:ci": "astro-scripts build \"src/index.ts\" --bundle", "dev": "astro-scripts dev \"src/**/*.ts\"", - "test": "astro-scripts test \"test/**/*.test.js\"" + "test": "astro-scripts test \"test/**/*.test.ts\"", + "typecheck:tests": "tsc --build tsconfig.test.json" }, "files": [ "dist", diff --git a/packages/create-astro/test/context.test.js b/packages/create-astro/test/context.test.ts similarity index 83% rename from packages/create-astro/test/context.test.js rename to packages/create-astro/test/context.test.ts index 7836d4410914..fb689a2f47b5 100644 --- a/packages/create-astro/test/context.test.js +++ b/packages/create-astro/test/context.test.ts @@ -73,14 +73,20 @@ describe('context', () => { }); it('--add with --no-install conflicts', async () => { - const exitCode = await new Promise((resolve) => { - const originalExit = process.exit; - process.exit = (code) => { - process.exit = originalExit; - resolve(code); - }; - getContext(['--add', 'cloudflare', '--no-install']); - }); + const originalExit = process.exit; + let exitCode: number | string | null | undefined; + const patchedExit: typeof originalExit = (code) => { + exitCode = code; + throw code; + }; + process.exit = patchedExit; + try { + await getContext(['--add', 'cloudflare', '--no-install']); + } catch { + // expected: patchedExit throws to unwind getContext + } finally { + process.exit = originalExit; + } assert.equal(exitCode, 1); }); }); diff --git a/packages/create-astro/test/dependencies.test.js b/packages/create-astro/test/dependencies.test.ts similarity index 73% rename from packages/create-astro/test/dependencies.test.js rename to packages/create-astro/test/dependencies.test.ts index bde585130b37..5285abba6bcd 100644 --- a/packages/create-astro/test/dependencies.test.js +++ b/packages/create-astro/test/dependencies.test.ts @@ -1,18 +1,19 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { dependencies } from '../dist/index.js'; -import { setup } from './utils.js'; +import { type DependenciesContext, mockPrompt, setup } from './utils.ts'; describe('dependencies', () => { const fixture = setup(); it('--yes', async () => { - const context = { + const context: DependenciesContext = { cwd: '', yes: true, packageManager: 'npm', dryRun: true, - prompt: () => ({ deps: true }), + prompt: mockPrompt({ deps: true }), + tasks: [], }; await dependencies(context); @@ -21,13 +22,14 @@ describe('dependencies', () => { }); it('--yes with third-party template warns', async () => { - const context = { + const context: DependenciesContext = { cwd: '', yes: true, template: 'github:someone/starter', packageManager: 'npm', dryRun: true, - prompt: () => ({ deps: true }), + prompt: mockPrompt({ deps: true }), + tasks: [], }; await dependencies(context); @@ -36,13 +38,14 @@ describe('dependencies', () => { }); it('starlight templates do not warn', async () => { - const context = { + const context: DependenciesContext = { cwd: '', yes: true, template: 'starlight/tailwind', packageManager: 'npm', dryRun: true, - prompt: () => ({ deps: true }), + prompt: mockPrompt({ deps: true }), + tasks: [], }; await dependencies(context); @@ -51,13 +54,14 @@ describe('dependencies', () => { }); it('starlight-prefixed third-party templates warn', async () => { - const context = { + const context: DependenciesContext = { cwd: '', yes: true, template: 'starlightevil/foo', packageManager: 'npm', dryRun: true, - prompt: () => ({ deps: true }), + prompt: mockPrompt({ deps: true }), + tasks: [], }; await dependencies(context); @@ -66,13 +70,14 @@ describe('dependencies', () => { }); it('warns without --yes when install is enabled', async () => { - const context = { + const context: DependenciesContext = { cwd: '', install: true, template: 'github:someone/starter', packageManager: 'npm', dryRun: true, - prompt: () => ({ deps: true }), + prompt: mockPrompt({ deps: true }), + tasks: [], }; await dependencies(context); @@ -81,12 +86,13 @@ describe('dependencies', () => { }); it('prompt yes', async () => { - const context = { + const context: DependenciesContext = { cwd: '', packageManager: 'npm', dryRun: true, - prompt: () => ({ deps: true }), + prompt: mockPrompt({ deps: true }), install: undefined, + tasks: [], }; await dependencies(context); @@ -96,12 +102,13 @@ describe('dependencies', () => { }); it('prompt no', async () => { - const context = { + const context: DependenciesContext = { cwd: '', packageManager: 'npm', dryRun: true, - prompt: () => ({ deps: false }), + prompt: mockPrompt({ deps: false }), install: undefined, + tasks: [], }; await dependencies(context); @@ -111,12 +118,13 @@ describe('dependencies', () => { }); it('--install', async () => { - const context = { + const context: DependenciesContext = { cwd: '', install: true, packageManager: 'npm', dryRun: true, - prompt: () => ({ deps: false }), + prompt: mockPrompt({ deps: false }), + tasks: [], }; await dependencies(context); assert.ok(fixture.hasMessage('Skipping dependency installation')); @@ -124,12 +132,13 @@ describe('dependencies', () => { }); it('--no-install ', async () => { - const context = { + const context: DependenciesContext = { cwd: '', install: false, packageManager: 'npm', dryRun: true, - prompt: () => ({ deps: false }), + prompt: mockPrompt({ deps: false }), + tasks: [], }; await dependencies(context); @@ -140,17 +149,20 @@ describe('dependencies', () => { describe('--add', async () => { it('fails for non-supported integration', async () => { - let context = { + let context: DependenciesContext = { cwd: '', add: ['foo '], dryRun: true, - prompt: () => ({ deps: false }), + prompt: mockPrompt({ deps: false }), + packageManager: 'npm', + tasks: [], }; try { await dependencies(context); assert.fail('The function should throw an error'); } catch (error) { + assert.ok(error instanceof Error); assert.ok( error.message.includes('Invalid package name "foo "'), `Expected error about invalid package name, got: ${error.message}`, @@ -160,13 +172,16 @@ describe('dependencies', () => { cwd: '', add: ['react', 'bar lorem'], dryRun: true, - prompt: () => ({ deps: false }), + prompt: mockPrompt({ deps: false }), + packageManager: 'npm', + tasks: [], }; try { await dependencies(context); assert.fail('The function should throw an error'); } catch (error) { + assert.ok(error instanceof Error); assert.ok( error.message.includes('Invalid package name "bar lorem"'), `Expected error about invalid package name, got: ${error.message}`, diff --git a/packages/create-astro/test/git.test.js b/packages/create-astro/test/git.test.ts similarity index 70% rename from packages/create-astro/test/git.test.js rename to packages/create-astro/test/git.test.ts index 85854f0d59b4..f2ceb6dfb727 100644 --- a/packages/create-astro/test/git.test.js +++ b/packages/create-astro/test/git.test.ts @@ -4,26 +4,41 @@ import { mkdir, writeFile } from 'node:fs/promises'; import { after, before, describe, it } from 'node:test'; import { git } from '../dist/index.js'; -import { setup } from './utils.js'; +import { type GitContext, mockPrompt, setup } from './utils.ts'; describe('git', () => { const fixture = setup(); it('none', async () => { - const context = { cwd: '', dryRun: true, prompt: () => ({ git: false }) }; + const context: GitContext = { + cwd: '', + dryRun: true, + prompt: mockPrompt({ git: false }), + tasks: [], + }; await git(context); assert.ok(fixture.hasMessage('Skipping Git initialization')); }); it('yes (--dry-run)', async () => { - const context = { cwd: '', dryRun: true, prompt: () => ({ git: true }) }; + const context: GitContext = { + cwd: '', + dryRun: true, + prompt: mockPrompt({ git: true }), + tasks: [], + }; await git(context); assert.ok(fixture.hasMessage('Skipping Git initialization')); }); it('no (--dry-run)', async () => { - const context = { cwd: '', dryRun: true, prompt: () => ({ git: false }) }; + const context: GitContext = { + cwd: '', + dryRun: true, + prompt: mockPrompt({ git: false }), + tasks: [], + }; await git(context); assert.ok(fixture.hasMessage('Skipping Git initialization')); @@ -40,11 +55,12 @@ describe('git initialized', () => { }); it('already initialized', async () => { - const context = { + const context: GitContext = { git: true, cwd: './test/fixtures/not-empty', dryRun: false, - prompt: () => ({ git: false }), + prompt: mockPrompt({ git: false }), + tasks: [], }; await git(context); diff --git a/packages/create-astro/test/integrations.test.js b/packages/create-astro/test/integrations.test.ts similarity index 78% rename from packages/create-astro/test/integrations.test.js rename to packages/create-astro/test/integrations.test.ts index 4db46965ea65..a1ccf434dfa8 100644 --- a/packages/create-astro/test/integrations.test.js +++ b/packages/create-astro/test/integrations.test.ts @@ -1,18 +1,20 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { dependencies } from '../dist/index.js'; -import { setup } from './utils.js'; +import { type DependenciesContext, mockPrompt, setup } from './utils.ts'; describe('integrations', () => { const fixture = setup(); it('--add node', async () => { - const context = { + const context: DependenciesContext = { cwd: '', yes: true, packageManager: 'npm', dryRun: true, add: ['node'], + prompt: mockPrompt({}), + tasks: [], }; await dependencies(context); @@ -21,12 +23,14 @@ describe('integrations', () => { }); it('--add node --add react', async () => { - const context = { + const context: DependenciesContext = { cwd: '', yes: true, packageManager: 'npm', dryRun: true, add: ['node', 'react'], + prompt: mockPrompt({}), + tasks: [], }; await dependencies(context); @@ -37,12 +41,14 @@ describe('integrations', () => { }); it('--add node,react', async () => { - const context = { + const context: DependenciesContext = { cwd: '', yes: true, packageManager: 'npm', dryRun: true, add: ['node,react'], + prompt: mockPrompt({}), + tasks: [], }; await dependencies(context); @@ -53,11 +59,13 @@ describe('integrations', () => { }); it('-y', async () => { - const context = { + const context: DependenciesContext = { cwd: '', yes: true, packageManager: 'npm', dryRun: true, + prompt: mockPrompt({}), + tasks: [], }; await dependencies(context); assert.ok(fixture.hasMessage('--dry-run Skipping dependency installation')); @@ -65,12 +73,14 @@ describe('integrations', () => { describe('Security: Command injection protection', () => { it('blocks semicolon command injection', async () => { - const context = { + const context: DependenciesContext = { cwd: '', yes: true, packageManager: 'npm', dryRun: true, add: ['react;whoami'], + prompt: mockPrompt({}), + tasks: [], }; await assert.rejects( @@ -81,12 +91,14 @@ describe('integrations', () => { }); it('blocks command substitution with $()', async () => { - const context = { + const context: DependenciesContext = { cwd: '', yes: true, packageManager: 'npm', dryRun: true, add: ['react$(whoami)'], + prompt: mockPrompt({}), + tasks: [], }; await assert.rejects( @@ -97,12 +109,14 @@ describe('integrations', () => { }); it('blocks command substitution with backticks', async () => { - const context = { + const context: DependenciesContext = { cwd: '', yes: true, packageManager: 'npm', dryRun: true, add: ['react`whoami`'], + prompt: mockPrompt({}), + tasks: [], }; await assert.rejects( @@ -113,12 +127,14 @@ describe('integrations', () => { }); it('blocks pipe operators', async () => { - const context = { + const context: DependenciesContext = { cwd: '', yes: true, packageManager: 'npm', dryRun: true, add: ['react|whoami'], + prompt: mockPrompt({}), + tasks: [], }; await assert.rejects( @@ -129,12 +145,14 @@ describe('integrations', () => { }); it('blocks ampersand operators', async () => { - const context = { + const context: DependenciesContext = { cwd: '', yes: true, packageManager: 'npm', dryRun: true, add: ['react&&whoami'], + prompt: mockPrompt({}), + tasks: [], }; await assert.rejects( @@ -145,12 +163,14 @@ describe('integrations', () => { }); it('blocks redirect operators', async () => { - const context = { + const context: DependenciesContext = { cwd: '', yes: true, packageManager: 'npm', dryRun: true, add: ['react>file'], + prompt: mockPrompt({}), + tasks: [], }; await assert.rejects( @@ -161,12 +181,14 @@ describe('integrations', () => { }); it('allows scoped packages', async () => { - const context = { + const context: DependenciesContext = { cwd: '', yes: true, packageManager: 'npm', dryRun: true, add: ['@astrojs/tailwind'], + prompt: mockPrompt({}), + tasks: [], }; await dependencies(context); @@ -178,12 +200,14 @@ describe('integrations', () => { }); it('allows valid package names', async () => { - const context = { + const context: DependenciesContext = { cwd: '', yes: true, packageManager: 'npm', dryRun: true, add: ['my-package', 'package_2.0'], + prompt: mockPrompt({}), + tasks: [], }; await dependencies(context); diff --git a/packages/create-astro/test/intro.test.js b/packages/create-astro/test/intro.test.ts similarity index 57% rename from packages/create-astro/test/intro.test.js rename to packages/create-astro/test/intro.test.ts index d042dad7fc6b..8e0da3755927 100644 --- a/packages/create-astro/test/intro.test.js +++ b/packages/create-astro/test/intro.test.ts @@ -1,18 +1,28 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { intro } from '../dist/index.js'; -import { setup } from './utils.js'; +import { type IntroContext, setup } from './utils.ts'; describe('intro', () => { const fixture = setup(); it('no arguments', async () => { - await intro({ skipHouston: false, version: '0.0.0', username: 'user' }); + const context: IntroContext = { + skipHouston: false, + version: Promise.resolve('0.0.0'), + username: Promise.resolve('user'), + }; + await intro(context); assert.ok(fixture.hasMessage('Houston:')); assert.ok(fixture.hasMessage('Welcome to astro v0.0.0')); }); it('--skip-houston', async () => { - await intro({ skipHouston: true, version: '0.0.0', username: 'user' }); + const context: IntroContext = { + skipHouston: true, + version: Promise.resolve('0.0.0'), + username: Promise.resolve('user'), + }; + await intro(context); assert.equal(fixture.length(), 1); assert.ok(!fixture.hasMessage('Houston:')); assert.ok(fixture.hasMessage('Launch sequence initiated')); diff --git a/packages/create-astro/test/next.test.js b/packages/create-astro/test/next.test.ts similarity index 59% rename from packages/create-astro/test/next.test.js rename to packages/create-astro/test/next.test.ts index 5b9b22b30632..1f948b78e214 100644 --- a/packages/create-astro/test/next.test.js +++ b/packages/create-astro/test/next.test.ts @@ -1,20 +1,30 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { next } from '../dist/index.js'; -import { setup } from './utils.js'; +import { type NextContext, setup } from './utils.ts'; describe('next steps', () => { const fixture = setup(); it('no arguments', async () => { - await next({ skipHouston: false, cwd: './it/fixtures/not-empty', packageManager: 'npm' }); + const context: NextContext = { + skipHouston: false, + cwd: './it/fixtures/not-empty', + packageManager: 'npm', + }; + await next(context); assert.ok(fixture.hasMessage('Liftoff confirmed.')); assert.ok(fixture.hasMessage('npm run dev')); assert.ok(fixture.hasMessage('Good luck out there, astronaut!')); }); it('--skip-houston', async () => { - await next({ skipHouston: true, cwd: './it/fixtures/not-empty', packageManager: 'npm' }); + const context: NextContext = { + skipHouston: true, + cwd: './it/fixtures/not-empty', + packageManager: 'npm', + }; + await next(context); assert.ok(!fixture.hasMessage('Good luck out there, astronaut!')); }); }); diff --git a/packages/create-astro/test/package-name-validation.test.js b/packages/create-astro/test/package-name-validation.test.ts similarity index 100% rename from packages/create-astro/test/package-name-validation.test.js rename to packages/create-astro/test/package-name-validation.test.ts diff --git a/packages/create-astro/test/project-name.test.js b/packages/create-astro/test/project-name.test.ts similarity index 58% rename from packages/create-astro/test/project-name.test.js rename to packages/create-astro/test/project-name.test.ts index 0aebd1c79268..f4de3aef34bd 100644 --- a/packages/create-astro/test/project-name.test.js +++ b/packages/create-astro/test/project-name.test.ts @@ -1,37 +1,53 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { projectName } from '../dist/index.js'; -import { setup } from './utils.js'; +import { mockExit, mockPrompt, type ProjectNameContext, setup } from './utils.ts'; describe('project name', async () => { const fixture = setup(); it('pass in name', async () => { - const context = { projectName: '', cwd: './foo/bar/baz', prompt: () => {} }; + const context: ProjectNameContext = { + projectName: '', + cwd: './foo/bar/baz', + prompt: mockPrompt({}), + exit: mockExit, + }; await projectName(context); assert.equal(context.cwd, './foo/bar/baz'); assert.equal(context.projectName, 'baz'); }); it('dot', async () => { - const context = { projectName: '', cwd: '.', prompt: () => ({ name: 'foobar' }) }; + const context: ProjectNameContext = { + projectName: '', + cwd: '.', + prompt: mockPrompt({ name: 'foobar' }), + exit: mockExit, + }; await projectName(context); assert.ok(fixture.hasMessage('"." is not empty!')); assert.equal(context.projectName, 'foobar'); }); it('dot slash', async () => { - const context = { projectName: '', cwd: './', prompt: () => ({ name: 'foobar' }) }; + const context: ProjectNameContext = { + projectName: '', + cwd: './', + prompt: mockPrompt({ name: 'foobar' }), + exit: mockExit, + }; await projectName(context); assert.ok(fixture.hasMessage('"./" is not empty!')); assert.equal(context.projectName, 'foobar'); }); it('empty', async () => { - const context = { + const context: ProjectNameContext = { projectName: '', cwd: './test/fixtures/empty', - prompt: () => ({ name: 'foobar' }), + prompt: mockPrompt({ name: 'foobar' }), + exit: mockExit, }; await projectName(context); assert.ok(!fixture.hasMessage('"./test/fixtures/empty" is not empty!')); @@ -39,10 +55,11 @@ describe('project name', async () => { }); it('not empty', async () => { - const context = { + const context: ProjectNameContext = { projectName: '', cwd: './test/fixtures/not-empty', - prompt: () => ({ name: 'foobar' }), + prompt: mockPrompt({ name: 'foobar' }), + exit: mockExit, }; await projectName(context); assert.ok(fixture.hasMessage('"./test/fixtures/not-empty" is not empty!')); @@ -50,73 +67,107 @@ describe('project name', async () => { }); it('basic', async () => { - const context = { projectName: '', cwd: '', prompt: () => ({ name: 'foobar' }) }; + const context: ProjectNameContext = { + projectName: '', + cwd: '', + prompt: mockPrompt({ name: 'foobar' }), + exit: mockExit, + }; await projectName(context); assert.equal(context.cwd, 'foobar'); assert.equal(context.projectName, 'foobar'); }); it('head and tail blank spaces should be trimmed', async () => { - const context = { projectName: '', cwd: '', prompt: () => ({ name: ' foobar ' }) }; + const context: ProjectNameContext = { + projectName: '', + cwd: '', + prompt: mockPrompt({ name: ' foobar ' }), + exit: mockExit, + }; await projectName(context); assert.equal(context.cwd, 'foobar'); assert.equal(context.projectName, 'foobar'); }); it('normalize', async () => { - const context = { projectName: '', cwd: '', prompt: () => ({ name: 'Invalid Name' }) }; + const context: ProjectNameContext = { + projectName: '', + cwd: '', + prompt: mockPrompt({ name: 'Invalid Name' }), + exit: mockExit, + }; await projectName(context); assert.equal(context.cwd, 'Invalid Name'); assert.equal(context.projectName, 'invalid-name'); }); it('remove leading/trailing dashes', async () => { - const context = { projectName: '', cwd: '', prompt: () => ({ name: '(invalid)' }) }; + const context: ProjectNameContext = { + projectName: '', + cwd: '', + prompt: mockPrompt({ name: '(invalid)' }), + exit: mockExit, + }; await projectName(context); assert.equal(context.projectName, 'invalid'); }); it('handles scoped packages', async () => { - const context = { projectName: '', cwd: '', prompt: () => ({ name: '@astro/site' }) }; + const context: ProjectNameContext = { + projectName: '', + cwd: '', + prompt: mockPrompt({ name: '@astro/site' }), + exit: mockExit, + }; await projectName(context); assert.equal(context.cwd, '@astro/site'); assert.equal(context.projectName, '@astro/site'); }); it('--yes', async () => { - const context = { projectName: '', cwd: './foo/bar/baz', yes: true, prompt: () => {} }; + const context: ProjectNameContext = { + projectName: '', + cwd: './foo/bar/baz', + yes: true, + prompt: mockPrompt({}), + exit: mockExit, + }; await projectName(context); assert.equal(context.projectName, 'baz'); }); it('dry run with name', async () => { - const context = { + const context: ProjectNameContext = { projectName: '', cwd: './foo/bar/baz', dryRun: true, - prompt: () => {}, + prompt: mockPrompt({}), + exit: mockExit, }; await projectName(context); assert.equal(context.projectName, 'baz'); }); it('dry run with dot', async () => { - const context = { + const context: ProjectNameContext = { projectName: '', cwd: '.', dryRun: true, - prompt: () => ({ name: 'foobar' }), + prompt: mockPrompt({ name: 'foobar' }), + exit: mockExit, }; await projectName(context); assert.equal(context.projectName, 'foobar'); }); it('dry run with empty', async () => { - const context = { + const context: ProjectNameContext = { projectName: '', cwd: './test/fixtures/empty', dryRun: true, - prompt: () => ({ name: 'foobar' }), + prompt: mockPrompt({ name: 'foobar' }), + exit: mockExit, }; await projectName(context); assert.equal(context.projectName, 'empty'); diff --git a/packages/create-astro/test/template-processing.test.js b/packages/create-astro/test/template-processing.test.ts similarity index 100% rename from packages/create-astro/test/template-processing.test.js rename to packages/create-astro/test/template-processing.test.ts diff --git a/packages/create-astro/test/template.test.js b/packages/create-astro/test/template.test.ts similarity index 53% rename from packages/create-astro/test/template.test.js rename to packages/create-astro/test/template.test.ts index 821ac9c2e9ac..5f934d5bec6b 100644 --- a/packages/create-astro/test/template.test.js +++ b/packages/create-astro/test/template.test.ts @@ -1,38 +1,69 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { template } from '../dist/index.js'; -import { setup } from './utils.js'; +import { mockExit, mockPrompt, setup, type TemplateContext } from './utils.ts'; describe('template', async () => { const fixture = setup(); it('none', async () => { - const context = { template: '', cwd: '', dryRun: true, prompt: () => ({ template: 'blog' }) }; + const context: TemplateContext = { + template: '', + dryRun: true, + prompt: mockPrompt({ template: 'blog' }), + exit: mockExit, + tasks: [], + }; await template(context); assert.ok(fixture.hasMessage('Skipping template copying')); assert.equal(context.template, 'blog'); }); it('minimal (--dry-run)', async () => { - const context = { template: 'minimal', cwd: '', dryRun: true, prompt: () => {} }; + const context: TemplateContext = { + template: 'minimal', + dryRun: true, + prompt: mockPrompt({}), + exit: mockExit, + tasks: [], + }; await template(context); assert.ok(fixture.hasMessage('Using minimal as project template')); }); it('basics (--dry-run)', async () => { - const context = { template: 'basics', cwd: '', dryRun: true, prompt: () => {} }; + const context: TemplateContext = { + template: 'basics', + dryRun: true, + prompt: mockPrompt({}), + exit: mockExit, + tasks: [], + }; await template(context); assert.ok(fixture.hasMessage('Using basics as project template')); }); it('blog (--dry-run)', async () => { - const context = { template: 'blog', cwd: '', dryRun: true, prompt: () => {} }; + const context: TemplateContext = { + template: 'blog', + dryRun: true, + prompt: mockPrompt({}), + exit: mockExit, + tasks: [], + }; await template(context); assert.ok(fixture.hasMessage('Using blog as project template')); }); it('minimal (--yes)', async () => { - const context = { template: 'minimal', cwd: '', dryRun: true, yes: true, prompt: () => {} }; + const context: TemplateContext = { + template: 'minimal', + dryRun: true, + yes: true, + prompt: mockPrompt({}), + exit: mockExit, + tasks: [], + }; await template(context); assert.ok(fixture.hasMessage('Using minimal as project template')); }); diff --git a/packages/create-astro/test/utils.js b/packages/create-astro/test/utils.js deleted file mode 100644 index 20063ec53255..000000000000 --- a/packages/create-astro/test/utils.js +++ /dev/null @@ -1,32 +0,0 @@ -import { before, beforeEach } from 'node:test'; -import { stripVTControlCharacters } from 'node:util'; -import { setStdout } from '../dist/index.js'; - -export function setup() { - const ctx = { messages: [] }; - before(() => { - setStdout( - Object.assign({}, process.stdout, { - write(buf) { - ctx.messages.push(stripVTControlCharacters(String(buf)).trim()); - return true; - }, - }), - ); - }); - beforeEach(() => { - ctx.messages = []; - }); - - return { - messages() { - return ctx.messages; - }, - length() { - return ctx.messages.length; - }, - hasMessage(content) { - return !!ctx.messages.find((msg) => msg.includes(content)); - }, - }; -} diff --git a/packages/create-astro/test/utils.ts b/packages/create-astro/test/utils.ts new file mode 100644 index 000000000000..8946819a00ef --- /dev/null +++ b/packages/create-astro/test/utils.ts @@ -0,0 +1,62 @@ +import { before, beforeEach } from 'node:test'; +import { stripVTControlCharacters } from 'node:util'; +import { setStdout } from '../dist/index.js'; +import type { Context } from '../src/actions/context.ts'; +import type { + dependencies, + git, + intro, + next, + projectName, + template, + verify, +} from '../dist/index.js'; + +export type { Context }; +export type DependenciesContext = Parameters[0]; +export type GitContext = Parameters[0]; +export type IntroContext = Parameters[0]; +export type NextContext = Parameters[0]; +export type ProjectNameContext = Parameters[0]; +export type TemplateContext = Parameters[0]; +export type VerifyContext = Parameters[0]; + +export function mockPrompt(answers: Record): Context['prompt'] { + const fn = async (q: { name: string }) => { + return { [q.name]: answers[q.name] }; + }; + return fn as unknown as Context['prompt']; +} + +export const mockExit: Context['exit'] = (code) => { + throw code; +}; + +export function setup() { + const ctx: { messages: string[] } = { messages: [] }; + before(() => { + setStdout( + Object.assign({}, process.stdout, { + write(buf: Uint8Array | string) { + ctx.messages.push(stripVTControlCharacters(String(buf)).trim()); + return true; + }, + }), + ); + }); + beforeEach(() => { + ctx.messages = []; + }); + + return { + messages() { + return ctx.messages; + }, + length() { + return ctx.messages.length; + }, + hasMessage(content: string) { + return !!ctx.messages.find((msg) => msg.includes(content)); + }, + }; +} diff --git a/packages/create-astro/test/verify.test.js b/packages/create-astro/test/verify.test.ts similarity index 63% rename from packages/create-astro/test/verify.test.js rename to packages/create-astro/test/verify.test.ts index ff335014517c..a1447e2ab41e 100644 --- a/packages/create-astro/test/verify.test.js +++ b/packages/create-astro/test/verify.test.ts @@ -1,22 +1,24 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { verify } from '../dist/index.js'; -import { setup } from './utils.js'; +import { mockExit, setup, type VerifyContext } from './utils.ts'; describe('verify', async () => { const fixture = setup(); - const exit = (code) => { - throw code; - }; + const baseContext = { + version: Promise.resolve('0.0.0'), + ref: 'latest', + exit: mockExit, + } satisfies Partial; it('basics', async () => { - const context = { template: 'basics', exit }; + const context: VerifyContext = { ...baseContext, template: 'basics' }; await verify(context); assert.equal(fixture.messages().length, 0, 'Did not expect `verify` to log any messages'); }); it('missing', async () => { - const context = { template: 'missing', exit }; + const context: VerifyContext = { ...baseContext, template: 'missing' }; let err = null; try { await verify(context); @@ -28,13 +30,13 @@ describe('verify', async () => { }); it('starlight', async () => { - const context = { template: 'starlight', exit }; + const context: VerifyContext = { ...baseContext, template: 'starlight' }; await verify(context); assert.equal(fixture.messages().length, 0, 'Did not expect `verify` to log any messages'); }); it('starlight/tailwind', async () => { - const context = { template: 'starlight/tailwind', exit }; + const context: VerifyContext = { ...baseContext, template: 'starlight/tailwind' }; await verify(context); assert.equal(fixture.messages().length, 0, 'Did not expect `verify` to log any messages'); }); diff --git a/packages/create-astro/tsconfig.test.json b/packages/create-astro/tsconfig.test.json new file mode 100644 index 000000000000..cff674d824b9 --- /dev/null +++ b/packages/create-astro/tsconfig.test.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["test/**/*.ts"], + "exclude": ["test/fixtures/**"], + "compilerOptions": { + "allowJs": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "rewriteRelativeImportExtensions": true + }, + "references": [ + { + "path": "../astro/tsconfig.test.json" + } + ] +} diff --git a/packages/db/package.json b/packages/db/package.json index 5def3f051e9c..71509decb282 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -68,8 +68,9 @@ "build:ci": "astro-scripts build \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"", "test": "pnpm run test:integration && pnpm run test:types", - "test:integration": "astro-scripts test \"test/**/*.test.js\"", - "test:types": "tsc --project test/types/tsconfig.json" + "test:integration": "astro-scripts test \"test/**/*.test.ts\"", + "test:types": "tsc --project test/types/tsconfig.json", + "typecheck:tests": "tsc --build tsconfig.test.json" }, "dependencies": { "@clack/prompts": "^1.0.1", diff --git a/packages/db/test/basics.test.js b/packages/db/test/basics.test.ts similarity index 92% rename from packages/db/test/basics.test.js rename to packages/db/test/basics.test.ts index 6186af174703..e0d9f945c091 100644 --- a/packages/db/test/basics.test.js +++ b/packages/db/test/basics.test.ts @@ -2,12 +2,12 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { load as cheerioLoad } from 'cheerio'; import testAdapter from '../../astro/test/test-adapter.js'; -import { loadFixture } from '../../astro/test/test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from '../../astro/test/test-utils.js'; import { resolveDbAppToken } from '../dist/core/utils.js'; -import { clearEnvironment, setupRemoteDb } from './test-utils.js'; +import { clearEnvironment, type RemoteDbServer, setupRemoteDb } from './test-utils.ts'; describe('astro:db', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: new URL('./fixtures/basics/', import.meta.url), @@ -17,7 +17,7 @@ describe('astro:db', () => { }); describe('development', () => { - let devServer; + let devServer: DevServer; before(async () => { clearEnvironment(); @@ -96,8 +96,8 @@ describe('astro:db', () => { }); describe('development --remote', () => { - let devServer; - let remoteDbServer; + let devServer: DevServer; + let remoteDbServer: RemoteDbServer; before(async () => { clearEnvironment(); @@ -178,7 +178,7 @@ describe('astro:db', () => { }); describe('build --remote', () => { - let remoteDbServer; + let remoteDbServer: RemoteDbServer; before(async () => { clearEnvironment(); @@ -219,13 +219,11 @@ describe('astro:db', () => { describe('Precedence for --db-app-token and ASTRO_DB_APP_TOKEN handled correctly', () => { it('prefers --db-app-token over `ASTRO_DB_APP_TOKEN`', () => { - const flags = /** @type {any} */ ({ _: [], dbAppToken: 'from-flag' }); - assert.equal(resolveDbAppToken(flags, 'from-env'), 'from-flag'); + assert.equal(resolveDbAppToken({ _: [], dbAppToken: 'from-flag' }, 'from-env'), 'from-flag'); }); it('falls back to ASTRO_DB_APP_TOKEN if no flags set', () => { - const flags = /** @type {any} */ ({ _: [] }); - assert.equal(resolveDbAppToken(flags, 'from-env'), 'from-env'); + assert.equal(resolveDbAppToken({ _: [] }, 'from-env'), 'from-env'); }); }); }); diff --git a/packages/db/test/db-in-src.test.js b/packages/db/test/db-in-src.test.ts similarity index 86% rename from packages/db/test/db-in-src.test.js rename to packages/db/test/db-in-src.test.ts index 5e29b73724b3..f224b0fca936 100644 --- a/packages/db/test/db-in-src.test.js +++ b/packages/db/test/db-in-src.test.ts @@ -2,10 +2,10 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { load as cheerioLoad } from 'cheerio'; import testAdapter from '../../astro/test/test-adapter.js'; -import { loadFixture } from '../../astro/test/test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from '../../astro/test/test-utils.js'; describe('astro:db', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: new URL('./fixtures/db-in-src/', import.meta.url), @@ -16,7 +16,7 @@ describe('astro:db', () => { }); describe('development: db/ folder inside srcDir', () => { - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); diff --git a/packages/db/test/error-handling.test.js b/packages/db/test/error-handling.test.ts similarity index 82% rename from packages/db/test/error-handling.test.js rename to packages/db/test/error-handling.test.ts index 8dc6e1e89b13..07c634a9562a 100644 --- a/packages/db/test/error-handling.test.js +++ b/packages/db/test/error-handling.test.ts @@ -1,13 +1,13 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; -import { loadFixture } from '../../astro/test/test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from '../../astro/test/test-utils.js'; import { cli } from '../dist/core/cli/index.js'; -import { setupRemoteDb } from './test-utils.js'; +import { type RemoteDbServer, setupRemoteDb } from './test-utils.ts'; const foreignKeyConstraintError = 'LibsqlError: SQLITE_CONSTRAINT: FOREIGN KEY constraint failed'; describe('astro:db - error handling', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: new URL('./fixtures/error-handling/', import.meta.url), @@ -17,20 +17,19 @@ describe('astro:db - error handling', () => { it('Errors on invalid --db-app-token input', async () => { const originalExit = process.exit; const originalError = console.error; - /** @type {string[]} */ - const errorMessages = []; - console.error = (...args) => { + const errorMessages: string[] = []; + console.error = (...args: unknown[]) => { errorMessages.push(args.map(String).join(' ')); }; - process.exit = (code) => { + process.exit = ((code?: number) => { throw new Error(`EXIT_${code}`); - }; + }) as typeof process.exit; try { await cli({ config: fixture.config, flags: { - _: [undefined, 'astro', 'db', 'verify'], + _: ['', 'astro', 'db', 'verify'], dbAppToken: true, }, }); @@ -48,7 +47,7 @@ describe('astro:db - error handling', () => { }); describe('development', () => { - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); @@ -68,7 +67,7 @@ describe('astro:db - error handling', () => { }); describe('build --remote', () => { - let remoteDbServer; + let remoteDbServer: RemoteDbServer; before(async () => { remoteDbServer = await setupRemoteDb(fixture.config); diff --git a/packages/db/test/integration-only.test.js b/packages/db/test/integration-only.test.ts similarity index 89% rename from packages/db/test/integration-only.test.js rename to packages/db/test/integration-only.test.ts index b95d7d141a68..009b6e48fc7b 100644 --- a/packages/db/test/integration-only.test.js +++ b/packages/db/test/integration-only.test.ts @@ -1,10 +1,10 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { load as cheerioLoad } from 'cheerio'; -import { loadFixture } from '../../astro/test/test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from '../../astro/test/test-utils.js'; describe('astro:db with only integrations, no user db config', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: new URL('./fixtures/integration-only/', import.meta.url), @@ -12,7 +12,7 @@ describe('astro:db with only integrations, no user db config', () => { }); describe('development', () => { - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); }); diff --git a/packages/db/test/integrations.test.js b/packages/db/test/integrations.test.ts similarity index 92% rename from packages/db/test/integrations.test.js rename to packages/db/test/integrations.test.ts index b05b28d6a670..fcb9b1120000 100644 --- a/packages/db/test/integrations.test.js +++ b/packages/db/test/integrations.test.ts @@ -1,10 +1,10 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { load as cheerioLoad } from 'cheerio'; -import { loadFixture } from '../../astro/test/test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from '../../astro/test/test-utils.js'; describe('astro:db with integrations', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: new URL('./fixtures/integrations/', import.meta.url), @@ -12,7 +12,7 @@ describe('astro:db with integrations', () => { }); describe('development', () => { - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); diff --git a/packages/db/test/libsql-remote.test.js b/packages/db/test/libsql-remote.test.ts similarity index 91% rename from packages/db/test/libsql-remote.test.js rename to packages/db/test/libsql-remote.test.ts index d009cba1db84..7cf42b856a1c 100644 --- a/packages/db/test/libsql-remote.test.js +++ b/packages/db/test/libsql-remote.test.ts @@ -4,12 +4,12 @@ import { relative } from 'node:path'; import { after, before, describe, it } from 'node:test'; import { fileURLToPath } from 'node:url'; import testAdapter from '../../astro/test/test-adapter.js'; -import { loadFixture } from '../../astro/test/test-utils.js'; -import { clearEnvironment, initializeRemoteDb } from './test-utils.js'; +import { type Fixture, loadFixture } from '../../astro/test/test-utils.js'; +import { clearEnvironment, initializeRemoteDb } from './test-utils.ts'; describe('astro:db local database', () => { describe('build --remote with local libSQL file (absolute path)', () => { - let fixture; + let fixture: Fixture; before(async () => { clearEnvironment(); @@ -17,7 +17,7 @@ describe('astro:db local database', () => { // Remove the file if it exists to avoid conflict between test runs await rm(absoluteFileUrl, { force: true }); - process.env.ASTRO_INTERNAL_TEST_REMOTE = true; + process.env.ASTRO_INTERNAL_TEST_REMOTE = 'true'; process.env.ASTRO_DB_REMOTE_URL = absoluteFileUrl.toString(); const root = new URL('./fixtures/libsql-remote/', import.meta.url); @@ -46,7 +46,7 @@ describe('astro:db local database', () => { }); describe('build --remote with local libSQL file (relative path)', () => { - let fixture; + let fixture: Fixture; before(async () => { clearEnvironment(); @@ -56,7 +56,7 @@ describe('astro:db local database', () => { // Remove the file if it exists to avoid conflict between test runs await rm(prodDbPath, { force: true }); - process.env.ASTRO_INTERNAL_TEST_REMOTE = true; + process.env.ASTRO_INTERNAL_TEST_REMOTE = 'true'; process.env.ASTRO_DB_REMOTE_URL = `file:${prodDbPath}`; const root = new URL('./fixtures/libsql-remote/', import.meta.url); diff --git a/packages/db/test/local-prod.test.js b/packages/db/test/local-prod.test.ts similarity index 93% rename from packages/db/test/local-prod.test.js rename to packages/db/test/local-prod.test.ts index 9b84f09a2420..aa0d1c6d3f9e 100644 --- a/packages/db/test/local-prod.test.js +++ b/packages/db/test/local-prod.test.ts @@ -3,11 +3,11 @@ import { relative } from 'node:path'; import { after, before, describe, it } from 'node:test'; import { fileURLToPath } from 'node:url'; import testAdapter from '../../astro/test/test-adapter.js'; -import { loadFixture } from '../../astro/test/test-utils.js'; +import { type Fixture, loadFixture } from '../../astro/test/test-utils.js'; describe('astro:db local database', () => { describe('build (not remote) with DATABASE_FILE env (file URL)', () => { - let fixture; + let fixture: Fixture; const prodDbPath = new URL('./fixtures/basics/dist/astro.db', import.meta.url).toString(); before(async () => { process.env.ASTRO_DATABASE_FILE = prodDbPath; @@ -34,7 +34,7 @@ describe('astro:db local database', () => { }); describe('build (not remote) with DATABASE_FILE env (relative file path)', () => { - let fixture; + let fixture: Fixture; const absoluteFileUrl = new URL('./fixtures/basics/dist/astro.db', import.meta.url); const prodDbPath = relative(process.cwd(), fileURLToPath(absoluteFileUrl)); @@ -72,7 +72,7 @@ describe('astro:db local database', () => { output: 'server', adapter: testAdapter(), }); - let buildError = null; + let buildError: unknown = null; try { await fixture.build(); } catch (err) { @@ -92,7 +92,7 @@ describe('astro:db local database', () => { }); delete process.env.ASTRO_DATABASE_FILE; - let buildError = null; + let buildError: unknown = null; try { await fixture2.build(); } catch (err) { diff --git a/packages/db/test/no-seed.test.js b/packages/db/test/no-seed.test.ts similarity index 88% rename from packages/db/test/no-seed.test.js rename to packages/db/test/no-seed.test.ts index 0583521760d4..983b7ddcca86 100644 --- a/packages/db/test/no-seed.test.js +++ b/packages/db/test/no-seed.test.ts @@ -1,10 +1,10 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { load as cheerioLoad } from 'cheerio'; -import { loadFixture } from '../../astro/test/test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from '../../astro/test/test-utils.js'; describe('astro:db with no seed file', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: new URL('./fixtures/no-seed/', import.meta.url), @@ -12,7 +12,7 @@ describe('astro:db with no seed file', () => { }); describe('development', () => { - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); }); diff --git a/packages/db/test/ssr-no-apptoken.test.js b/packages/db/test/ssr-no-apptoken.test.ts similarity index 81% rename from packages/db/test/ssr-no-apptoken.test.js rename to packages/db/test/ssr-no-apptoken.test.ts index 87817d3eca95..76594bc06484 100644 --- a/packages/db/test/ssr-no-apptoken.test.js +++ b/packages/db/test/ssr-no-apptoken.test.ts @@ -1,12 +1,12 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import testAdapter from '../../astro/test/test-adapter.js'; -import { loadFixture } from '../../astro/test/test-utils.js'; -import { setupRemoteDb } from './test-utils.js'; +import { type Fixture, loadFixture } from '../../astro/test/test-utils.js'; +import { type RemoteDbServer, setupRemoteDb } from './test-utils.ts'; describe('missing app token', () => { - let fixture; - let remoteDbServer; + let fixture: Fixture; + let remoteDbServer: RemoteDbServer; before(async () => { fixture = await loadFixture({ root: new URL('./fixtures/no-apptoken/', import.meta.url), diff --git a/packages/db/test/static-remote.test.js b/packages/db/test/static-remote.test.ts similarity index 84% rename from packages/db/test/static-remote.test.js rename to packages/db/test/static-remote.test.ts index 34d662688229..929efba9a9a2 100644 --- a/packages/db/test/static-remote.test.js +++ b/packages/db/test/static-remote.test.ts @@ -1,11 +1,11 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { load as cheerioLoad } from 'cheerio'; -import { loadFixture } from '../../astro/test/test-utils.js'; -import { clearEnvironment, setupRemoteDb } from './test-utils.js'; +import { type Fixture, loadFixture } from '../../astro/test/test-utils.js'; +import { clearEnvironment, type RemoteDbServer, setupRemoteDb } from './test-utils.ts'; describe('astro:db', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: new URL('./fixtures/static-remote/', import.meta.url), @@ -14,7 +14,7 @@ describe('astro:db', () => { }); describe('static build --remote', () => { - let remoteDbServer; + let remoteDbServer: RemoteDbServer; before(async () => { remoteDbServer = await setupRemoteDb(fixture.config); @@ -41,7 +41,7 @@ describe('astro:db', () => { }); describe('static build --remote with custom LibSQL', () => { - let remoteDbServer; + let remoteDbServer: RemoteDbServer | undefined; before(async () => { clearEnvironment(); diff --git a/packages/db/test/test-utils.js b/packages/db/test/test-utils.ts similarity index 72% rename from packages/db/test/test-utils.js rename to packages/db/test/test-utils.ts index 7e1a6623debd..ef880a745858 100644 --- a/packages/db/test/test-utils.js +++ b/packages/db/test/test-utils.ts @@ -1,15 +1,19 @@ import { mkdir, unlink } from 'node:fs/promises'; +import type { AstroConfig } from 'astro'; import { createClient } from '@libsql/client'; import { cli } from '../dist/core/cli/index.js'; import { resolveDbConfig } from '../dist/core/load-file.js'; import { getCreateIndexQueries, getCreateTableQuery } from '../dist/core/queries.js'; +import type { DBTable, ResolvedDBTable } from '../dist/core/types.js'; const isWindows = process.platform === 'win32'; -/** - * @param {import('astro').AstroConfig} astroConfig - */ -export async function setupRemoteDb(astroConfig, options = {}) { +export type RemoteDbServer = { stop: () => Promise }; + +export async function setupRemoteDb( + astroConfig: AstroConfig, + options: { useDbAppTokenFlag?: boolean } = {}, +): Promise { const url = isWindows ? new URL(`./.astro/${Date.now()}.db`, astroConfig.root) : new URL(`./${Date.now()}.db`, astroConfig.root); @@ -18,19 +22,19 @@ export async function setupRemoteDb(astroConfig, options = {}) { if (!options.useDbAppTokenFlag) { process.env.ASTRO_DB_APP_TOKEN = token; } - process.env.ASTRO_INTERNAL_TEST_REMOTE = true; + process.env.ASTRO_INTERNAL_TEST_REMOTE = 'true'; if (isWindows) { await mkdir(new URL('.', url), { recursive: true }); } const dbClient = createClient({ - url, + url: url.toString(), authToken: token, }); const { dbConfig } = await resolveDbConfig(astroConfig); - const setupQueries = []; + const setupQueries: string[] = []; for (const [name, table] of Object.entries(dbConfig?.tables ?? {})) { const createQuery = getCreateTableQuery(name, table); const indexQueries = getCreateIndexQueries(name, table); @@ -47,7 +51,7 @@ export async function setupRemoteDb(astroConfig, options = {}) { await cli({ config: astroConfig, flags: { - _: [undefined, 'astro', 'db', 'execute', 'db/seed.ts'], + _: ['', 'astro', 'db', 'execute', 'db/seed.ts'], remote: true, ...(options.useDbAppTokenFlag ? { dbAppToken: token } : {}), }, @@ -66,18 +70,18 @@ export async function setupRemoteDb(astroConfig, options = {}) { }; } -export async function initializeRemoteDb(astroConfig) { +export async function initializeRemoteDb(astroConfig: AstroConfig) { await cli({ config: astroConfig, flags: { - _: [undefined, 'astro', 'db', 'push'], + _: ['', 'astro', 'db', 'push'], remote: true, }, }); await cli({ config: astroConfig, flags: { - _: [undefined, 'astro', 'db', 'execute', 'db/seed.ts'], + _: ['', 'astro', 'db', 'execute', 'db/seed.ts'], remote: true, }, }); @@ -94,3 +98,7 @@ export function clearEnvironment() { } } } + +export function asResolved(table: DBTable): ResolvedDBTable { + return table as ResolvedDBTable; +} diff --git a/packages/db/test/unit/column-queries.test.js b/packages/db/test/unit/column-queries.test.ts similarity index 91% rename from packages/db/test/unit/column-queries.test.js rename to packages/db/test/unit/column-queries.test.ts index 8f1518ca37eb..14e6d9177b3d 100644 --- a/packages/db/test/unit/column-queries.test.js +++ b/packages/db/test/unit/column-queries.test.ts @@ -6,7 +6,9 @@ import { } from '../../dist/core/cli/migration-queries.js'; import { MIGRATION_VERSION } from '../../dist/core/consts.js'; import { tableSchema } from '../../dist/core/schemas.js'; +import type { DBTable, ResolvedDBTable } from '../../dist/core/types.js'; import { column, defineTable, NOW } from '../../dist/runtime/virtual.js'; +import { asResolved } from '../test-utils.ts'; const TABLE_NAME = 'Users'; @@ -23,33 +25,42 @@ const userInitial = tableSchema.parse( }), ); -function userChangeQueries(oldTable, newTable) { +function userChangeQueries(oldTable: DBTable, newTable: DBTable) { return getTableChangeQueries({ tableName: TABLE_NAME, - oldTable, - newTable, + oldTable: asResolved(oldTable), + newTable: asResolved(newTable), }); } -function configChangeQueries(oldTables, newTables) { +function configChangeQueries( + oldTables: Record, + newTables: Record, +) { return getMigrationQueries({ - oldSnapshot: { schema: oldTables, version: MIGRATION_VERSION }, - newSnapshot: { schema: newTables, version: MIGRATION_VERSION }, + oldSnapshot: { + schema: oldTables, + version: MIGRATION_VERSION, + }, + newSnapshot: { + schema: newTables, + version: MIGRATION_VERSION, + }, }); } describe('column queries', () => { describe('getMigrationQueries', () => { it('should be empty when tables are the same', async () => { - const oldTables = { [TABLE_NAME]: userInitial }; - const newTables = { [TABLE_NAME]: userInitial }; + const oldTables = { [TABLE_NAME]: asResolved(userInitial) }; + const newTables = { [TABLE_NAME]: asResolved(userInitial) }; const { queries } = await configChangeQueries(oldTables, newTables); assert.deepEqual(queries, []); }); it('should create table for new tables', async () => { const oldTables = {}; - const newTables = { [TABLE_NAME]: userInitial }; + const newTables = { [TABLE_NAME]: asResolved(userInitial) }; const { queries } = await configChangeQueries(oldTables, newTables); assert.deepEqual(queries, [ `CREATE TABLE "${TABLE_NAME}" (_id INTEGER PRIMARY KEY, "name" text NOT NULL, "age" integer NOT NULL, "email" text NOT NULL UNIQUE, "mi" text)`, @@ -57,7 +68,7 @@ describe('column queries', () => { }); it('should drop table for removed tables', async () => { - const oldTables = { [TABLE_NAME]: userInitial }; + const oldTables = { [TABLE_NAME]: asResolved(userInitial) }; const newTables = {}; const { queries } = await configChangeQueries(oldTables, newTables); assert.deepEqual(queries, [`DROP TABLE "${TABLE_NAME}"`]); @@ -65,15 +76,15 @@ describe('column queries', () => { it('should error if possible table rename is detected', async () => { const rename = 'Peeps'; - const oldTables = { [TABLE_NAME]: userInitial }; - const newTables = { [rename]: userInitial }; - let error = null; + const oldTables = { [TABLE_NAME]: asResolved(userInitial) }; + const newTables = { [rename]: asResolved(userInitial) }; + let error: string | null = null; try { await configChangeQueries(oldTables, newTables); } catch (e) { - error = e.message; + error = (e as Error).message; } - assert.match(error, /Potential table rename detected/); + assert.match(error!, /Potential table rename detected/); }); it('should error if possible column rename is detected', async () => { @@ -87,13 +98,16 @@ describe('column queries', () => { title2: column.text(), }, }); - let error = null; + let error: string | null = null; try { - await configChangeQueries({ [TABLE_NAME]: blogInitial }, { [TABLE_NAME]: blogFinal }); + await configChangeQueries( + { [TABLE_NAME]: asResolved(blogInitial) }, + { [TABLE_NAME]: asResolved(blogFinal) }, + ); } catch (e) { - error = e.message; + error = (e as Error).message; } - assert.match(error, /Potential column rename detected/); + assert.match(error!, /Potential column rename detected/); }); }); @@ -264,7 +278,7 @@ describe('column queries', () => { `CREATE TABLE \"${tempTableName}\" (\"name\" text NOT NULL, \"age\" integer NOT NULL, \"email\" text NOT NULL UNIQUE, \"mi\" text, \"id\" integer PRIMARY KEY)`, `INSERT INTO \"${tempTableName}\" (\"name\", \"age\", \"email\", \"mi\") SELECT \"name\", \"age\", \"email\", \"mi\" FROM \"Users\"`, 'DROP TABLE "Users"', - `ALTER TABLE "${tempTableName}" RENAME TO "Users"`, + `ALTER TABLE \"${tempTableName}\" RENAME TO \"Users\"`, ]); }); @@ -285,7 +299,7 @@ describe('column queries', () => { `CREATE TABLE \"${tempTableName}\" (_id INTEGER PRIMARY KEY, \"name\" text NOT NULL, \"age\" integer NOT NULL, \"email\" text NOT NULL UNIQUE, \"mi\" text)`, `INSERT INTO \"${tempTableName}\" (\"name\", \"age\", \"email\", \"mi\") SELECT \"name\", \"age\", \"email\", \"mi\" FROM \"Users\"`, 'DROP TABLE "Users"', - `ALTER TABLE "${tempTableName}" RENAME TO "Users"`, + `ALTER TABLE \"${tempTableName}\" RENAME TO \"Users\"`, ]); }); @@ -490,7 +504,6 @@ describe('column queries', () => { }); }); -/** @param {string} query */ -function getTempTableName(query) { +function getTempTableName(query: string) { return /Users_[a-z\d]+/.exec(query)?.[0]; } diff --git a/packages/db/test/unit/db-client.test.js b/packages/db/test/unit/db-client.test.ts similarity index 100% rename from packages/db/test/unit/db-client.test.js rename to packages/db/test/unit/db-client.test.ts diff --git a/packages/db/test/unit/index-queries.test.js b/packages/db/test/unit/index-queries.test.ts similarity index 92% rename from packages/db/test/unit/index-queries.test.js rename to packages/db/test/unit/index-queries.test.ts index 4b4722baa7e9..f9179f09e6ab 100644 --- a/packages/db/test/unit/index-queries.test.js +++ b/packages/db/test/unit/index-queries.test.ts @@ -2,7 +2,9 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { getTableChangeQueries } from '../../dist/core/cli/migration-queries.js'; import { dbConfigSchema, tableSchema } from '../../dist/core/schemas.js'; +import type { DBTable } from '../../dist/core/types.js'; import { column } from '../../dist/runtime/virtual.js'; +import { asResolved } from '../test-utils.ts'; const userInitial = tableSchema.parse({ columns: { @@ -201,8 +203,7 @@ describe('index queries', () => { describe('legacy object config', () => { it('adds indexes', async () => { - /** @type {import('../../dist/core/types.js').DBTable} */ - const userFinal = { + const userFinal: DBTable = { ...userInitial, indexes: { nameIdx: { on: ['name'], unique: false }, @@ -212,8 +213,8 @@ describe('index queries', () => { const { queries } = await getTableChangeQueries({ tableName: 'user', - oldTable: userInitial, - newTable: userFinal, + oldTable: asResolved(userInitial), + newTable: asResolved(userFinal), }); assert.deepEqual(queries, [ @@ -223,8 +224,7 @@ describe('index queries', () => { }); it('drops indexes', async () => { - /** @type {import('../../dist/core/types.js').DBTable} */ - const initial = { + const initial: DBTable = { ...userInitial, indexes: { nameIdx: { on: ['name'], unique: false }, @@ -232,24 +232,22 @@ describe('index queries', () => { }, }; - /** @type {import('../../dist/core/types.js').DBTable} */ - const final = { + const final: DBTable = { ...userInitial, indexes: {}, }; const { queries } = await getTableChangeQueries({ tableName: 'user', - oldTable: initial, - newTable: final, + oldTable: asResolved(initial), + newTable: asResolved(final), }); assert.deepEqual(queries, ['DROP INDEX "nameIdx"', 'DROP INDEX "emailIdx"']); }); it('drops and recreates modified indexes', async () => { - /** @type {import('../../dist/core/types.js').DBTable} */ - const initial = { + const initial: DBTable = { ...userInitial, indexes: { nameIdx: { on: ['name'], unique: false }, @@ -257,8 +255,7 @@ describe('index queries', () => { }, }; - /** @type {import('../../dist/core/types.js').DBTable} */ - const final = { + const final: DBTable = { ...userInitial, indexes: { nameIdx: { on: ['name'], unique: true }, @@ -268,8 +265,8 @@ describe('index queries', () => { const { queries } = await getTableChangeQueries({ tableName: 'user', - oldTable: initial, - newTable: final, + oldTable: asResolved(initial), + newTable: asResolved(final), }); assert.deepEqual(queries, [ diff --git a/packages/db/test/unit/reference-queries.test.js b/packages/db/test/unit/reference-queries.test.ts similarity index 92% rename from packages/db/test/unit/reference-queries.test.js rename to packages/db/test/unit/reference-queries.test.ts index 04f5f84aaba1..11b10bfe1010 100644 --- a/packages/db/test/unit/reference-queries.test.js +++ b/packages/db/test/unit/reference-queries.test.ts @@ -2,7 +2,9 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { getTableChangeQueries } from '../../dist/core/cli/migration-queries.js'; import { tablesSchema } from '../../dist/core/schemas.js'; +import type { DBTable } from '../../dist/core/types.js'; import { column, defineTable } from '../../dist/runtime/virtual.js'; +import { asResolved } from '../test-utils.ts'; const BaseUser = defineTable({ columns: { @@ -23,13 +25,8 @@ const BaseSentBox = defineTable({ }, }); -/** - * @typedef {import('../../dist/core/types.js').DBTable} DBTable - * @param {{ User: DBTable, SentBox: DBTable }} params - * @returns - */ function resolveReferences( - { User = BaseUser, SentBox = BaseSentBox } = { + { User = BaseUser, SentBox = BaseSentBox }: { User?: DBTable; SentBox?: DBTable } = { User: BaseUser, SentBox: BaseSentBox, }, @@ -37,11 +34,11 @@ function resolveReferences( return tablesSchema.parse({ User, SentBox }); } -function userChangeQueries(oldTable, newTable) { +function userChangeQueries(oldTable: DBTable, newTable: DBTable) { return getTableChangeQueries({ tableName: 'User', - oldTable, - newTable, + oldTable: asResolved(oldTable), + newTable: asResolved(newTable), }); } @@ -139,7 +136,7 @@ describe('reference queries', () => { }), }); - const expected = (tempTableName) => [ + const expected = (tempTableName: string | undefined) => [ `CREATE TABLE \"${tempTableName}\" (_id INTEGER PRIMARY KEY, \"to\" integer NOT NULL, \"toName\" text NOT NULL, \"subject\" text NOT NULL, \"body\" text NOT NULL, FOREIGN KEY (\"to\", \"toName\") REFERENCES \"User\"(\"id\", \"name\"))`, `INSERT INTO \"${tempTableName}\" (\"_id\", \"to\", \"toName\", \"subject\", \"body\") SELECT \"_id\", \"to\", \"toName\", \"subject\", \"body\" FROM \"User\"`, 'DROP TABLE "User"', @@ -163,7 +160,6 @@ describe('reference queries', () => { }); }); -/** @param {string} query */ -function getTempTableName(query) { +function getTempTableName(query: string) { return /User_[a-z\d]+/.exec(query)?.[0]; } diff --git a/packages/db/test/unit/remote-info.test.js b/packages/db/test/unit/remote-info.test.ts similarity index 92% rename from packages/db/test/unit/remote-info.test.js rename to packages/db/test/unit/remote-info.test.ts index 4940f2e4a283..55b5babd3a7e 100644 --- a/packages/db/test/unit/remote-info.test.js +++ b/packages/db/test/unit/remote-info.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert'; import test, { beforeEach, describe } from 'node:test'; import { getRemoteDatabaseInfo } from '../../dist/core/utils.js'; -import { clearEnvironment } from '../test-utils.js'; +import { clearEnvironment } from '../test-utils.ts'; describe('RemoteDatabaseInfo', () => { beforeEach(() => { diff --git a/packages/db/test/unit/reset-queries.test.js b/packages/db/test/unit/reset-queries.test.ts similarity index 82% rename from packages/db/test/unit/reset-queries.test.js rename to packages/db/test/unit/reset-queries.test.ts index 9fb99f91e53f..2235f5672e62 100644 --- a/packages/db/test/unit/reset-queries.test.js +++ b/packages/db/test/unit/reset-queries.test.ts @@ -3,7 +3,9 @@ import { describe, it } from 'node:test'; import { getMigrationQueries } from '../../dist/core/cli/migration-queries.js'; import { MIGRATION_VERSION } from '../../dist/core/consts.js'; import { tableSchema } from '../../dist/core/schemas.js'; +import type { ResolvedDBTable } from '../../dist/core/types.js'; import { column, defineTable } from '../../dist/runtime/virtual.js'; +import { asResolved } from '../test-utils.ts'; const TABLE_NAME = 'Users'; @@ -23,8 +25,8 @@ const userInitial = tableSchema.parse( describe('force reset', () => { describe('getMigrationQueries', () => { it('should drop table and create new version', async () => { - const oldTables = { [TABLE_NAME]: userInitial }; - const newTables = { [TABLE_NAME]: userInitial }; + const oldTables = { [TABLE_NAME]: asResolved(userInitial) }; + const newTables = { [TABLE_NAME]: asResolved(userInitial) }; const { queries } = await getMigrationQueries({ oldSnapshot: { schema: oldTables, version: MIGRATION_VERSION }, newSnapshot: { schema: newTables, version: MIGRATION_VERSION }, @@ -38,8 +40,8 @@ describe('force reset', () => { }); it('should not drop table when previous snapshot did not have it', async () => { - const oldTables = {}; - const newTables = { [TABLE_NAME]: userInitial }; + const oldTables: Record = {}; + const newTables = { [TABLE_NAME]: asResolved(userInitial) }; const { queries } = await getMigrationQueries({ oldSnapshot: { schema: oldTables, version: MIGRATION_VERSION }, newSnapshot: { schema: newTables, version: MIGRATION_VERSION }, diff --git a/packages/db/tsconfig.test.json b/packages/db/tsconfig.test.json new file mode 100644 index 000000000000..7d6bc4428b35 --- /dev/null +++ b/packages/db/tsconfig.test.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["test/**/*.ts"], + "exclude": ["test/fixtures/**"], + "compilerOptions": { + "allowJs": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "rewriteRelativeImportExtensions": true + }, + "references": [{ "path": "../astro/tsconfig.test.json" }] +} diff --git a/packages/integrations/alpinejs/package.json b/packages/integrations/alpinejs/package.json index 4e0f4843709d..55f505440206 100644 --- a/packages/integrations/alpinejs/package.json +++ b/packages/integrations/alpinejs/package.json @@ -31,7 +31,8 @@ "build": "astro-scripts build \"src/**/*.ts\" && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"", - "test:e2e": "playwright test" + "test:e2e": "playwright test", + "typecheck:tests": "tsc --build tsconfig.test.json" }, "peerDependencies": { "@types/alpinejs": "^3.0.0", diff --git a/packages/integrations/alpinejs/test/basics.test.js b/packages/integrations/alpinejs/test/basics.test.ts similarity index 100% rename from packages/integrations/alpinejs/test/basics.test.js rename to packages/integrations/alpinejs/test/basics.test.ts diff --git a/packages/integrations/alpinejs/test/directive.test.js b/packages/integrations/alpinejs/test/directive.test.ts similarity index 100% rename from packages/integrations/alpinejs/test/directive.test.js rename to packages/integrations/alpinejs/test/directive.test.ts diff --git a/packages/integrations/alpinejs/test/plugin-script-import.test.js b/packages/integrations/alpinejs/test/plugin-script-import.test.ts similarity index 100% rename from packages/integrations/alpinejs/test/plugin-script-import.test.js rename to packages/integrations/alpinejs/test/plugin-script-import.test.ts diff --git a/packages/integrations/alpinejs/test/test-utils.js b/packages/integrations/alpinejs/test/test-utils.ts similarity index 74% rename from packages/integrations/alpinejs/test/test-utils.js rename to packages/integrations/alpinejs/test/test-utils.ts index f18bf6add250..16c57ff68553 100644 --- a/packages/integrations/alpinejs/test/test-utils.js +++ b/packages/integrations/alpinejs/test/test-utils.ts @@ -2,7 +2,12 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { test as testBase } from '@playwright/test'; -import { loadFixture as baseLoadFixture } from '../../../astro/test/test-utils.js'; +import { + loadFixture as baseLoadFixture, + type Fixture, + type AstroInlineConfig, + type DevServer, +} from '../../../astro/test/test-utils.js'; // Get all test files in directory, assign unique port for each of them so they don't conflict const testFiles = await fs.readdir(new URL('.', import.meta.url)); @@ -14,7 +19,7 @@ for (let i = 0; i < testFiles.length; i++) { } } -function loadFixture(inlineConfig) { +function loadFixture(inlineConfig: AstroInlineConfig) { if (!inlineConfig?.root) throw new Error("Must provide { root: './fixtures/...' }"); // resolve the relative root (i.e. "./fixtures/tailwindcss") to a full filepath @@ -23,15 +28,15 @@ function loadFixture(inlineConfig) { ...inlineConfig, root: fileURLToPath(new URL(inlineConfig.root, import.meta.url)), server: { - port: testFileToPort.get(path.basename(inlineConfig.root)), + port: testFileToPort.get(path.basename(String(inlineConfig.root))), }, }); } -function testFactory(inlineConfig) { - let fixture; +function testFactory(inlineConfig: AstroInlineConfig) { + let fixture: Fixture; - const test = testBase.extend({ + const test = testBase.extend<{ astro: Fixture }>({ // biome-ignore lint/correctness/noEmptyPattern: playwright needs this astro: async ({}, use) => { fixture = fixture || (await loadFixture(inlineConfig)); @@ -46,10 +51,10 @@ function testFactory(inlineConfig) { return test; } -export function prepareTestFactory(opts) { +export function prepareTestFactory(opts: AstroInlineConfig) { const test = testFactory(opts); - let devServer; + let devServer: DevServer; test.beforeAll(async ({ astro }) => { devServer = await astro.startDevServer(); diff --git a/packages/integrations/alpinejs/tsconfig.test.json b/packages/integrations/alpinejs/tsconfig.test.json new file mode 100644 index 000000000000..27c89c5fe7a7 --- /dev/null +++ b/packages/integrations/alpinejs/tsconfig.test.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["test/**/*.ts"], + "exclude": ["test/fixtures/**"], + "compilerOptions": { + "noEmit": true, + "allowJs": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "rewriteRelativeImportExtensions": true + }, + "references": [ + { + "path": "../../astro/tsconfig.test.json" + } + ] +} diff --git a/packages/integrations/cloudflare/CHANGELOG.md b/packages/integrations/cloudflare/CHANGELOG.md index f30009913c86..4ffa6c503f3d 100644 --- a/packages/integrations/cloudflare/CHANGELOG.md +++ b/packages/integrations/cloudflare/CHANGELOG.md @@ -1,5 +1,49 @@ # @astrojs/cloudflare +## 13.1.10 + +### Patch Changes + +- [#16320](https://github.com/withastro/astro/pull/16320) [`a43eb4b`](https://github.com/withastro/astro/commit/a43eb4b40b4f81530e3c9b5e2959495900320433) Thanks [@matthewp](https://github.com/matthewp)! - Uses `redirect: 'manual'` for remote image fetches in the Cloudflare binding image transform, consistent with all other image fetch paths + +- [#16307](https://github.com/withastro/astro/pull/16307) [`a81dd3e`](https://github.com/withastro/astro/commit/a81dd3e7932f18b4c10c04378416324f0fea00f2) Thanks [@matthewp](https://github.com/matthewp)! - Surfaces `console.log` and `console.warn` output from workerd during prerendering + +- Updated dependencies []: + - @astrojs/underscore-redirects@1.0.3 + +## 13.1.9 + +### Patch Changes + +- [#16210](https://github.com/withastro/astro/pull/16210) [`e030bd0`](https://github.com/withastro/astro/commit/e030bd058457505b605ef573cfc71239baa963f0) Thanks [@matthewp](https://github.com/matthewp)! - Fixes `.svelte` files in `node_modules` failing with `Unknown file extension ".svelte"` when using the Cloudflare adapter with `prerenderEnvironment: 'node'` + +- Updated dependencies []: + - @astrojs/underscore-redirects@1.0.3 + +## 13.1.8 + +### Patch Changes + +- [#16225](https://github.com/withastro/astro/pull/16225) [`756e7be`](https://github.com/withastro/astro/commit/756e7be510a315516f6aa1647c93d11e8b43f5a9) Thanks [@travisbreaks](https://github.com/travisbreaks)! - Fixes `ERR_MULTIPLE_CONSUMERS` error when using Cloudflare Queues with prerendered pages. The prerender worker config callback now excludes `queues.consumers` from the entry worker config, since the prerender worker only renders static HTML and should not register as a queue consumer. Queue producers (bindings) are preserved. + +- [#16192](https://github.com/withastro/astro/pull/16192) [`79d86b8`](https://github.com/withastro/astro/commit/79d86b88ef199d6a2195584ec53b225c6a9df5f9) Thanks [@alexanderniebuhr](https://github.com/alexanderniebuhr)! - Removes an unused function re-export from the `/info` package path + +- Updated dependencies []: + - @astrojs/underscore-redirects@1.0.3 + +## 13.1.7 + +### Patch Changes + +- Updated dependencies [[`814406d`](https://github.com/withastro/astro/commit/814406de7dc3ea014b47d2d886d55c45e4e1c034)]: + - @astrojs/underscore-redirects@1.0.3 + +## 13.1.6 + +### Patch Changes + +- [#16151](https://github.com/withastro/astro/pull/16151) [`4978165`](https://github.com/withastro/astro/commit/4978165af4ca4c672edad904d7b6c85fc3647dd9) Thanks [@matthewp](https://github.com/matthewp)! - Fixes a dev-mode crash loop in the Cloudflare adapter when using Starlight by excluding `@astrojs/starlight` from SSR dependency optimization + ## 13.1.5 ### Patch Changes diff --git a/packages/integrations/cloudflare/package.json b/packages/integrations/cloudflare/package.json index 688cff786a63..8225537f50c5 100644 --- a/packages/integrations/cloudflare/package.json +++ b/packages/integrations/cloudflare/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/cloudflare", "description": "Deploy your site to Cloudflare Workers", - "version": "13.1.5", + "version": "13.1.10", "type": "module", "types": "./dist/index.d.ts", "author": "withastro", @@ -19,7 +19,6 @@ "homepage": "https://docs.astro.build/en/guides/integrations-guide/cloudflare/", "exports": { ".": "./dist/index.js", - "./info": "./dist/info.js", "./entrypoints/server": "./dist/entrypoints/server.js", "./entrypoints/preview": "./dist/entrypoints/preview.js", "./entrypoints/server.js": "./dist/entrypoints/server.js", @@ -39,7 +38,8 @@ "dev": "astro-scripts dev \"src/**/*.ts\"", "build": "astro-scripts build \"src/**/*.ts\" --clean-dts && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\"", - "test": "astro-scripts test --force-exit \"test/**/*.test.js\"" + "test": "astro-scripts test --force-exit \"test/**/*.test.ts\"", + "typecheck:tests": "tsc --build tsconfig.test.json" }, "dependencies": { "@astrojs/internal-helpers": "workspace:*", diff --git a/packages/integrations/cloudflare/src/index.ts b/packages/integrations/cloudflare/src/index.ts index efb3619e59cc..1e7de2a2cfcc 100644 --- a/packages/integrations/cloudflare/src/index.ts +++ b/packages/integrations/cloudflare/src/index.ts @@ -181,11 +181,15 @@ export default function createIntegration({ experimental: { prerenderWorker: { config(_, { entryWorkerConfig }) { + const { queues, ...restWorkerConfig } = entryWorkerConfig; return { - ...entryWorkerConfig, + ...restWorkerConfig, name: 'prerender', + ...(queues?.producers?.length && { + queues: { producers: queues.producers }, + }), ...(needsImagesBinding && - !entryWorkerConfig.images && { + !restWorkerConfig.images && { images: { binding: imagesBindingName }, }), }; @@ -271,6 +275,7 @@ export default function createIntegration({ 'virtual:astro:*', 'virtual:astro-cloudflare:*', 'virtual:@astrojs/*', + '@astrojs/starlight', ], esbuildOptions: { // Suppress Vite's `createRequire(import.meta.url)` banner to work around @@ -305,7 +310,6 @@ export default function createIntegration({ if (conf.ssr) { // Cloudflare does not support externalizing modules in server environments conf.ssr.external = undefined; - conf.ssr.noExternal = true; } }, }, diff --git a/packages/integrations/cloudflare/src/info.ts b/packages/integrations/cloudflare/src/info.ts deleted file mode 100644 index 26b1a053ee7b..000000000000 --- a/packages/integrations/cloudflare/src/info.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Re-exports utilities for use by astro add CLI. - * This provides a resolvable path from the user's project. - */ -export { getLocalWorkerdCompatibilityDate } from '@cloudflare/vite-plugin'; diff --git a/packages/integrations/cloudflare/src/prerenderer.ts b/packages/integrations/cloudflare/src/prerenderer.ts index f55a8a9a036e..f95c38eb4b2d 100644 --- a/packages/integrations/cloudflare/src/prerenderer.ts +++ b/packages/integrations/cloudflare/src/prerenderer.ts @@ -4,7 +4,7 @@ import type { AssetsGlobalStaticImagesList, PathWithRoute, } from 'astro'; -import { preview, type PreviewServer as VitePreviewServer } from 'vite'; +import { preview, createLogger, type PreviewServer as VitePreviewServer } from 'vite'; import { fileURLToPath } from 'node:url'; import { mkdir } from 'node:fs/promises'; import { cloudflare as cfVitePlugin, type PluginConfig } from '@cloudflare/vite-plugin'; @@ -53,6 +53,21 @@ export function createCloudflarePrerenderer({ // Ensure client dir exists (CF plugin expects it for assets) await mkdir(clientDir, { recursive: true }); + // Create a custom logger that filters out internal HTTP request logs (e.g. "POST /__astro_prerender 200 OK") + // from the Cloudflare vite plugin while still allowing user console.log output to pass through. + // We strip ANSI codes before testing because the Cloudflare vite plugin wraps messages in color codes. + const defaultLogger = createLogger('info'); + // eslint-disable-next-line no-control-regex + const ansiRe = /\x1b\[[0-9;]*m/g; + const astroRequestLogRe = /^(?:GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+\/__astro_/; + const customLogger: ReturnType = { + ...defaultLogger, + info(msg, opts) { + if (astroRequestLogRe.test(msg.replace(ansiRe, ''))) return; + defaultLogger.info(msg, opts); + }, + }; + previewServer = await preview({ configFile: false, base, @@ -61,7 +76,7 @@ export function createCloudflarePrerenderer({ outDir: fileURLToPath(serverDir), }, root: fileURLToPath(root), - logLevel: 'error', + customLogger, preview: { host: 'localhost', port: 0, // Let the OS pick a free port diff --git a/packages/integrations/cloudflare/src/utils/image-binding-transform.ts b/packages/integrations/cloudflare/src/utils/image-binding-transform.ts index d1d0c2db555e..d4f3311afbea 100644 --- a/packages/integrations/cloudflare/src/utils/image-binding-transform.ts +++ b/packages/integrations/cloudflare/src/utils/image-binding-transform.ts @@ -25,7 +25,14 @@ export async function transform( } const imageSrc = new URL(href, url.origin); - const content = await (isRemotePath(href) ? fetch(imageSrc) : assets.fetch(imageSrc)); + const content = await (isRemotePath(href) + ? fetch(imageSrc, { redirect: 'manual' }) + : assets.fetch(imageSrc)); + + if (content.status >= 300 && content.status < 400) { + return new Response('Not Found', { status: 404 }); + } + if (!content.body) { return new Response(null, { status: 404 }); } diff --git a/packages/integrations/cloudflare/src/vite-plugin-dev-server-prerender-middleware.ts b/packages/integrations/cloudflare/src/vite-plugin-dev-server-prerender-middleware.ts index 25fc90ba6d13..5bcd6651e998 100644 --- a/packages/integrations/cloudflare/src/vite-plugin-dev-server-prerender-middleware.ts +++ b/packages/integrations/cloudflare/src/vite-plugin-dev-server-prerender-middleware.ts @@ -18,14 +18,6 @@ export function createNodePrerenderPlugin(): vite.Plugin { }; }, - // Disable dep optimization for the `prerender` environment so dependencies - // are loaded via native import() with correct import.meta.url semantics. - configEnvironment(environmentName) { - if (environmentName === 'prerender') { - return { optimizeDeps: { noDiscovery: true, include: [] } }; - } - }, - configureServer(server) { (server as any)[devPrerenderMiddlewareSymbol] = true; }, diff --git a/packages/integrations/cloudflare/src/wrangler.ts b/packages/integrations/cloudflare/src/wrangler.ts index 96b1d6803fbc..2c9f115cfea3 100644 --- a/packages/integrations/cloudflare/src/wrangler.ts +++ b/packages/integrations/cloudflare/src/wrangler.ts @@ -1,13 +1,13 @@ -import type { PluginConfig } from '@cloudflare/vite-plugin'; +import type { PluginConfig, WorkerConfig } from '@cloudflare/vite-plugin'; export const DEFAULT_SESSION_KV_BINDING_NAME = 'SESSION'; export const DEFAULT_IMAGES_BINDING_NAME = 'IMAGES'; export const DEFAULT_ASSETS_BINDING_NAME = 'ASSETS'; interface CloudflareConfigOptions { - sessionKVBindingName: string | undefined; + sessionKVBindingName?: string | undefined; needsSessionKVBinding?: boolean; - imagesBindingName: string | false | undefined; + imagesBindingName?: string | false | undefined; } /** @@ -15,8 +15,8 @@ interface CloudflareConfigOptions { * Sets the main entrypoint and adds bindings for auto-provisioning. */ export function cloudflareConfigCustomizer( - options: CloudflareConfigOptions, -): PluginConfig['config'] { + options?: CloudflareConfigOptions, +): (config: Partial) => Partial { const sessionKVBindingName = options?.sessionKVBindingName ?? DEFAULT_SESSION_KV_BINDING_NAME; const needsSessionKVBinding = options?.needsSessionKVBinding ?? true; const imagesBindingName = @@ -24,7 +24,7 @@ export function cloudflareConfigCustomizer( ? undefined : (options?.imagesBindingName ?? DEFAULT_IMAGES_BINDING_NAME); - return (config) => { + const customizer = (config: Partial): Partial => { const hasSessionBinding = config.kv_namespaces?.some( (kv) => kv.binding === sessionKVBindingName, ); @@ -54,4 +54,6 @@ export function cloudflareConfigCustomizer( }, }; }; + + return customizer satisfies PluginConfig['config']; } diff --git a/packages/integrations/cloudflare/test/astro-dev-platform.test.js b/packages/integrations/cloudflare/test/astro-dev-platform.test.ts similarity index 94% rename from packages/integrations/cloudflare/test/astro-dev-platform.test.js rename to packages/integrations/cloudflare/test/astro-dev-platform.test.ts index 779d2a1b2a25..62440398df03 100644 --- a/packages/integrations/cloudflare/test/astro-dev-platform.test.js +++ b/packages/integrations/cloudflare/test/astro-dev-platform.test.ts @@ -1,11 +1,11 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './_test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from './test-utils.ts'; describe('AstroDevPlatform', () => { - let fixture; - let devServer; + let fixture: Fixture; + let devServer: DevServer; before(async () => { fixture = await loadFixture({ root: './fixtures/astro-dev-platform/', diff --git a/packages/integrations/cloudflare/test/astro-env.test.js b/packages/integrations/cloudflare/test/astro-env.test.ts similarity index 93% rename from packages/integrations/cloudflare/test/astro-env.test.js rename to packages/integrations/cloudflare/test/astro-env.test.ts index 6259b453ac87..9a961e399e01 100644 --- a/packages/integrations/cloudflare/test/astro-env.test.js +++ b/packages/integrations/cloudflare/test/astro-env.test.ts @@ -1,13 +1,12 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './_test-utils.js'; +import { type DevServer, type Fixture, loadFixture, type PreviewServer } from './test-utils.ts'; describe('astro:env', () => { describe('ssr', () => { - let fixture; - let previewServer; - + let fixture: Fixture; + let previewServer: PreviewServer; before(async () => { process.env.API_URL = 'https://google.de'; process.env.PORT = '4322'; @@ -64,9 +63,8 @@ describe('astro:env', () => { }); describe('dev', () => { - let devServer; - let fixture; - + let devServer: DevServer; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/astro-env/', diff --git a/packages/integrations/cloudflare/test/binding-image-service.test.js b/packages/integrations/cloudflare/test/binding-image-service.test.ts similarity index 59% rename from packages/integrations/cloudflare/test/binding-image-service.test.js rename to packages/integrations/cloudflare/test/binding-image-service.test.ts index a5504620471d..09b7868188bc 100644 --- a/packages/integrations/cloudflare/test/binding-image-service.test.js +++ b/packages/integrations/cloudflare/test/binding-image-service.test.ts @@ -1,12 +1,32 @@ import * as assert from 'node:assert/strict'; +import { createServer, type Server } from 'node:http'; import { after, before, describe, it } from 'node:test'; -import { loadFixture } from './_test-utils.js'; +import { type Fixture, loadFixture, type PreviewServer } from './test-utils.ts'; describe('BindingImageService', () => { - let fixture; - let previewServer; + let fixture: Fixture; + let previewServer: PreviewServer; + let redirectServer: Server; + let redirectServerPort: number; before(async () => { + // Start a local HTTP server that always responds with a 302 redirect. + // Used to test that the image transform endpoint does not follow redirects. + redirectServer = createServer((_req, res) => { + res.writeHead(302, { Location: 'http://example.com/secret' }); + res.end(); + }); + await new Promise((resolve) => { + redirectServer.listen(0, () => { + const address = redirectServer.address(); + if (typeof address === 'string' || !address) { + throw new TypeError('Unexpected address for testing'); + } + redirectServerPort = address.port; + resolve(); + }); + }); + fixture = await loadFixture({ root: './fixtures/binding-image-service/', }); @@ -16,6 +36,7 @@ describe('BindingImageService', () => { after(async () => { await previewServer.stop(); + await new Promise((resolve) => redirectServer.close(resolve)); }); it('returns 403 for missing href parameter', async () => { @@ -52,4 +73,10 @@ describe('BindingImageService', () => { assert.equal(res.status, 200); assert.equal(res.headers.get('content-type'), 'image/avif'); }); + + it('does not follow redirects for remote images', async () => { + const href = `http://localhost:${redirectServerPort}/image.jpg`; + const res = await fixture.fetch(`/_image?href=${encodeURIComponent(href)}&f=webp`); + assert.equal(res.status, 404); + }); }); diff --git a/packages/integrations/cloudflare/test/client-address.test.js b/packages/integrations/cloudflare/test/client-address.test.ts similarity index 96% rename from packages/integrations/cloudflare/test/client-address.test.js rename to packages/integrations/cloudflare/test/client-address.test.ts index e905afd61c07..534b089adf61 100644 --- a/packages/integrations/cloudflare/test/client-address.test.js +++ b/packages/integrations/cloudflare/test/client-address.test.ts @@ -1,7 +1,7 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './_test-utils.js'; +import { type Fixture, loadFixture, type PreviewServer } from './test-utils.ts'; /** * Tests that the Cloudflare adapter correctly extracts and validates @@ -13,9 +13,8 @@ import { loadFixture } from './_test-utils.js'; * Regression test for: https://github.com/withastro/astro-security/issues/69 */ describe('Cloudflare clientAddress', () => { - let fixture; - let previewServer; - + let fixture: Fixture; + let previewServer: PreviewServer; before(async () => { fixture = await loadFixture({ root: './fixtures/client-address/', diff --git a/packages/integrations/cloudflare/test/compile-image-service.test.js b/packages/integrations/cloudflare/test/compile-image-service.test.ts similarity index 90% rename from packages/integrations/cloudflare/test/compile-image-service.test.js rename to packages/integrations/cloudflare/test/compile-image-service.test.ts index 93ee3b9227f2..62652020a73f 100644 --- a/packages/integrations/cloudflare/test/compile-image-service.test.js +++ b/packages/integrations/cloudflare/test/compile-image-service.test.ts @@ -1,11 +1,10 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './_test-utils.js'; +import { type DevServer, type Fixture, loadFixture, type PreviewServer } from './test-utils.ts'; describe('CompileImageService', () => { - let fixture; - + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/compile-image-service/', @@ -13,8 +12,7 @@ describe('CompileImageService', () => { }); describe('dev', () => { - let devServer; - + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); }); @@ -28,7 +26,7 @@ describe('CompileImageService', () => { it('returns 200 for local images via /_image endpoint', async () => { const html = await fixture.fetch('/blog/post').then((res) => res.text()); const $ = cheerio.load(html); - const src = $('img').attr('src'); + const src = $('img').attr('src')!; assert.ok( src.startsWith('/_image'), `Expected image src to route through /_image, got: ${src}`, @@ -39,8 +37,7 @@ describe('CompileImageService', () => { }); describe('preview', () => { - let previewServer; - + let previewServer: PreviewServer; before(async () => { await fixture.build(); previewServer = await fixture.preview(); diff --git a/packages/integrations/cloudflare/test/custom-entryfile.test.js b/packages/integrations/cloudflare/test/custom-entryfile.test.ts similarity index 88% rename from packages/integrations/cloudflare/test/custom-entryfile.test.js rename to packages/integrations/cloudflare/test/custom-entryfile.test.ts index 630c4e00016e..79d8e4fa6bf6 100644 --- a/packages/integrations/cloudflare/test/custom-entryfile.test.js +++ b/packages/integrations/cloudflare/test/custom-entryfile.test.ts @@ -2,11 +2,11 @@ import * as assert from 'node:assert/strict'; import { existsSync } from 'node:fs'; import { after, before, describe, it } from 'node:test'; import { fileURLToPath } from 'node:url'; -import { loadFixture } from './_test-utils.js'; +import { type Fixture, loadFixture, type PreviewServer } from './test-utils.ts'; describe('Custom entry file', () => { - let fixture; - let previewServer; + let fixture: Fixture; + let previewServer: PreviewServer; const root = new URL('./fixtures/custom-entryfile/', import.meta.url); before(async () => { diff --git a/packages/integrations/cloudflare/test/dev-image-endpoint.test.js b/packages/integrations/cloudflare/test/dev-image-endpoint.test.ts similarity index 92% rename from packages/integrations/cloudflare/test/dev-image-endpoint.test.js rename to packages/integrations/cloudflare/test/dev-image-endpoint.test.ts index bd186ec8aead..ad33f34d647c 100644 --- a/packages/integrations/cloudflare/test/dev-image-endpoint.test.js +++ b/packages/integrations/cloudflare/test/dev-image-endpoint.test.ts @@ -1,11 +1,10 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; -import { loadFixture } from './_test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from './test-utils.ts'; describe('Dev image endpoint', () => { - let fixture; - let devServer; - + let fixture: Fixture; + let devServer: DevServer; before(async () => { fixture = await loadFixture({ root: './fixtures/dev-image-endpoint/', diff --git a/packages/integrations/cloudflare/test/external-image-service.test.js b/packages/integrations/cloudflare/test/external-image-service.test.ts similarity index 90% rename from packages/integrations/cloudflare/test/external-image-service.test.js rename to packages/integrations/cloudflare/test/external-image-service.test.ts index 8662df507130..eafe69275b64 100644 --- a/packages/integrations/cloudflare/test/external-image-service.test.js +++ b/packages/integrations/cloudflare/test/external-image-service.test.ts @@ -3,13 +3,12 @@ import { readFileSync } from 'node:fs'; import { after, before, describe, it } from 'node:test'; import { fileURLToPath } from 'node:url'; import { glob } from 'tinyglobby'; -import { loadFixture } from './_test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from './test-utils.ts'; const root = new URL('./fixtures/external-image-service/', import.meta.url); describe('ExternalImageService', () => { - let fixture; - + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/external-image-service/', @@ -24,9 +23,7 @@ describe('ExternalImageService', () => { it('has correct image service', async () => { const files = await glob('**/image-service*', { cwd: fileURLToPath(new URL('dist/server', root)), - filesOnly: true, absolute: true, - flush: true, }); // the image service seems to be bundled inside the entry point const outFileToCheck = readFileSync(files[0], 'utf-8'); @@ -35,9 +32,8 @@ describe('ExternalImageService', () => { }); describe('ExternalImageService dev mode', () => { - let fixture; - let devServer; - + let fixture: Fixture; + let devServer: DevServer; before(async () => { fixture = await loadFixture({ root: './fixtures/external-image-service/', diff --git a/packages/integrations/cloudflare/test/external-redirects.test.js b/packages/integrations/cloudflare/test/external-redirects.test.ts similarity index 93% rename from packages/integrations/cloudflare/test/external-redirects.test.js rename to packages/integrations/cloudflare/test/external-redirects.test.ts index 33e841c0c0fe..e2a8343dfa92 100644 --- a/packages/integrations/cloudflare/test/external-redirects.test.js +++ b/packages/integrations/cloudflare/test/external-redirects.test.ts @@ -1,12 +1,11 @@ import { describe, it } from 'node:test'; -import { loadFixture } from './_test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.ts'; import assert from 'node:assert/strict'; import { existsSync, readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; describe('External Redirects', () => { - let fixture; - + let fixture: Fixture; it('should not attempt to prerender external redirect destinations', async () => { fixture = await loadFixture({ root: './fixtures/external-redirects', diff --git a/packages/integrations/cloudflare/test/fixtures/binding-image-service/astro.config.mjs b/packages/integrations/cloudflare/test/fixtures/binding-image-service/astro.config.mjs index 398d7db71019..421833f961d2 100644 --- a/packages/integrations/cloudflare/test/fixtures/binding-image-service/astro.config.mjs +++ b/packages/integrations/cloudflare/test/fixtures/binding-image-service/astro.config.mjs @@ -5,5 +5,8 @@ export default defineConfig({ adapter: cloudflare({ imageService: 'cloudflare-binding', }), + image: { + domains: ['localhost'], + }, output: 'server', }); diff --git a/packages/integrations/cloudflare/test/fixtures/prerender-node-env/astro.config.mjs b/packages/integrations/cloudflare/test/fixtures/prerender-node-env/astro.config.mjs index 86dbfb924824..cb8168367f51 100644 --- a/packages/integrations/cloudflare/test/fixtures/prerender-node-env/astro.config.mjs +++ b/packages/integrations/cloudflare/test/fixtures/prerender-node-env/astro.config.mjs @@ -1,3 +1,6 @@ import { defineConfig } from 'astro/config'; +import svelte from '@astrojs/svelte'; -export default defineConfig({}); +export default defineConfig({ + integrations: [svelte()], +}); diff --git a/packages/integrations/cloudflare/test/fixtures/prerender-node-env/fake-svelte-pkg/package.json b/packages/integrations/cloudflare/test/fixtures/prerender-node-env/fake-svelte-pkg/package.json new file mode 100644 index 000000000000..5613dd151cde --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/prerender-node-env/fake-svelte-pkg/package.json @@ -0,0 +1,11 @@ +{ + "name": "fake-svelte-pkg", + "version": "1.0.0", + "type": "module", + "exports": { + ".": "./src/index.js" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } +} diff --git a/packages/integrations/cloudflare/test/fixtures/prerender-node-env/fake-svelte-pkg/src/FakeComponent.svelte b/packages/integrations/cloudflare/test/fixtures/prerender-node-env/fake-svelte-pkg/src/FakeComponent.svelte new file mode 100644 index 000000000000..ec2d63936fd4 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/prerender-node-env/fake-svelte-pkg/src/FakeComponent.svelte @@ -0,0 +1 @@ +

    Hello from fake svelte component

    diff --git a/packages/integrations/cloudflare/test/fixtures/prerender-node-env/fake-svelte-pkg/src/index.js b/packages/integrations/cloudflare/test/fixtures/prerender-node-env/fake-svelte-pkg/src/index.js new file mode 100644 index 000000000000..42c5b2af5f7b --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/prerender-node-env/fake-svelte-pkg/src/index.js @@ -0,0 +1 @@ +export { default as FakeComponent } from './FakeComponent.svelte'; diff --git a/packages/integrations/cloudflare/test/fixtures/prerender-node-env/package.json b/packages/integrations/cloudflare/test/fixtures/prerender-node-env/package.json index 19e3ed1bd820..abda43484202 100644 --- a/packages/integrations/cloudflare/test/fixtures/prerender-node-env/package.json +++ b/packages/integrations/cloudflare/test/fixtures/prerender-node-env/package.json @@ -4,6 +4,9 @@ "private": true, "dependencies": { "@astrojs/cloudflare": "workspace:*", - "astro": "workspace:*" + "@astrojs/svelte": "workspace:*", + "astro": "workspace:*", + "svelte": "^5.0.0", + "fake-svelte-pkg": "file:./fake-svelte-pkg" } } diff --git a/packages/integrations/cloudflare/test/fixtures/prerender-node-env/src/components/SvelteWrapper.svelte b/packages/integrations/cloudflare/test/fixtures/prerender-node-env/src/components/SvelteWrapper.svelte new file mode 100644 index 000000000000..7df170a7e13a --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/prerender-node-env/src/components/SvelteWrapper.svelte @@ -0,0 +1,7 @@ + + +
    + +
    diff --git a/packages/integrations/cloudflare/test/fixtures/prerender-node-env/src/pages/svelte.astro b/packages/integrations/cloudflare/test/fixtures/prerender-node-env/src/pages/svelte.astro new file mode 100644 index 000000000000..9ef1fc29ad91 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/prerender-node-env/src/pages/svelte.astro @@ -0,0 +1,15 @@ +--- +export const prerender = true; + +import SvelteWrapper from '../components/SvelteWrapper.svelte'; +--- + + + + + Svelte Prerender Test + + + + + diff --git a/packages/integrations/cloudflare/test/fixtures/prerender-queue-consumers/astro.config.mjs b/packages/integrations/cloudflare/test/fixtures/prerender-queue-consumers/astro.config.mjs new file mode 100644 index 000000000000..339f0e2a49c0 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/prerender-queue-consumers/astro.config.mjs @@ -0,0 +1,7 @@ +import cloudflare from '@astrojs/cloudflare'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + adapter: cloudflare(), + output: 'server', +}); diff --git a/packages/integrations/cloudflare/test/fixtures/prerender-queue-consumers/package.json b/packages/integrations/cloudflare/test/fixtures/prerender-queue-consumers/package.json new file mode 100644 index 000000000000..5e57f22f1754 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/prerender-queue-consumers/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/astro-cloudflare-prerender-queue-consumers", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/cloudflare": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/cloudflare/test/fixtures/prerender-queue-consumers/src/pages/api.ts b/packages/integrations/cloudflare/test/fixtures/prerender-queue-consumers/src/pages/api.ts new file mode 100644 index 000000000000..3060e9427491 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/prerender-queue-consumers/src/pages/api.ts @@ -0,0 +1,9 @@ +import type { APIRoute } from 'astro'; + +export const prerender = false; + +export const GET: APIRoute = async () => { + return new Response(JSON.stringify({ ok: true }), { + headers: { 'Content-Type': 'application/json' }, + }); +}; diff --git a/packages/integrations/cloudflare/test/fixtures/prerender-queue-consumers/src/pages/index.astro b/packages/integrations/cloudflare/test/fixtures/prerender-queue-consumers/src/pages/index.astro new file mode 100644 index 000000000000..55e12f5dd94a --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/prerender-queue-consumers/src/pages/index.astro @@ -0,0 +1,10 @@ +--- +// This page is prerendered by default (output: 'server' with no opt-out) +// Actually, in output: 'server' mode, pages are server-rendered by default. +// We explicitly mark this as prerendered. +export const prerender = true; +--- + +Prerendered +

    Prerendered Page

    + diff --git a/packages/integrations/cloudflare/test/fixtures/prerender-queue-consumers/wrangler.jsonc b/packages/integrations/cloudflare/test/fixtures/prerender-queue-consumers/wrangler.jsonc new file mode 100644 index 000000000000..6ec9e7179b12 --- /dev/null +++ b/packages/integrations/cloudflare/test/fixtures/prerender-queue-consumers/wrangler.jsonc @@ -0,0 +1,18 @@ +{ + "name": "prerender-queue-consumers", + "main": "@astrojs/cloudflare/entrypoints/server", + "compatibility_date": "2026-01-28", + "queues": { + "consumers": [ + { + "queue": "my-queue" + } + ], + "producers": [ + { + "binding": "MY_QUEUE", + "queue": "my-queue" + } + ] + } +} diff --git a/packages/integrations/cloudflare/test/internal-redirects.test.js b/packages/integrations/cloudflare/test/internal-redirects.test.ts similarity index 92% rename from packages/integrations/cloudflare/test/internal-redirects.test.js rename to packages/integrations/cloudflare/test/internal-redirects.test.ts index 84ae0d251b6b..0964ae221387 100644 --- a/packages/integrations/cloudflare/test/internal-redirects.test.js +++ b/packages/integrations/cloudflare/test/internal-redirects.test.ts @@ -1,12 +1,11 @@ import { describe, it } from 'node:test'; -import { loadFixture } from './_test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.ts'; import assert from 'node:assert/strict'; import { existsSync, readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; describe('Internal Redirects', () => { - let fixture; - + let fixture: Fixture; it('should not create a prerendered file for internal redirects', async () => { fixture = await loadFixture({ root: './fixtures/internal-redirects', diff --git a/packages/integrations/cloudflare/test/prerender-node-env.test.js b/packages/integrations/cloudflare/test/prerender-node-env.test.ts similarity index 84% rename from packages/integrations/cloudflare/test/prerender-node-env.test.js rename to packages/integrations/cloudflare/test/prerender-node-env.test.ts index aa4645ff0e05..61e5dfa03318 100644 --- a/packages/integrations/cloudflare/test/prerender-node-env.test.js +++ b/packages/integrations/cloudflare/test/prerender-node-env.test.ts @@ -1,13 +1,11 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import cloudflare from '../dist/index.js'; -import { loadFixture } from './_test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from './test-utils.ts'; describe('prerenderEnvironment: node', () => { - /** @type {import('../../../astro/test/test-utils').Fixture} */ - let fixture; - let devServer; - + let fixture: Fixture; + let devServer: DevServer; before(async () => { fixture = await loadFixture({ root: new URL('./fixtures/prerender-node-env/', import.meta.url).toString(), @@ -39,6 +37,20 @@ describe('prerenderEnvironment: node', () => { ); }); + it('renders svelte component that imports .svelte files from node_modules', async () => { + const res = await fixture.fetch('/svelte'); + assert.equal(res.status, 200); + const html = await res.text(); + assert.ok( + html.includes('id="svelte-wrapper"'), + 'Expected the prerendered page to contain the svelte wrapper', + ); + assert.ok( + html.includes('Hello from fake svelte component'), + 'Expected the fake svelte component to be rendered', + ); + }); + it('includes styles in prerendered page', async () => { const res = await fixture.fetch('/'); const html = await res.text(); @@ -57,7 +69,7 @@ describe('prerenderEnvironment: node', () => { 'Expected fallback content in prerendered HTML', ); - const islandUrlMatch = html.match(/fetch\('(\/_server-islands\/[^']+)'/); + const islandUrlMatch = /fetch\('(\/_server-islands\/[^']+)'/.exec(html); assert.ok(islandUrlMatch, 'Expected prerendered HTML to include a server island fetch URL'); const islandRes = await fixture.fetch(islandUrlMatch[1]); diff --git a/packages/integrations/cloudflare/test/prerender-queue-consumers.test.ts b/packages/integrations/cloudflare/test/prerender-queue-consumers.test.ts new file mode 100644 index 000000000000..0518b194caad --- /dev/null +++ b/packages/integrations/cloudflare/test/prerender-queue-consumers.test.ts @@ -0,0 +1,32 @@ +import * as assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import { type Fixture, loadFixture, type PreviewServer } from './test-utils.ts'; + +describe('Prerender with queue consumers', () => { + let fixture: Fixture; + let previewServer: PreviewServer; + before(async () => { + fixture = await loadFixture({ + root: './fixtures/prerender-queue-consumers/', + }); + await fixture.build(); + previewServer = await fixture.preview(); + }); + + after(async () => { + previewServer.stop(); + }); + + it('builds and previews without ERR_MULTIPLE_CONSUMERS', async () => { + // The prerendered page should be accessible + const res = await fixture.fetch('/'); + const html = await res.text(); + assert.ok(html.includes('Prerendered Page')); + }); + + it('serves the SSR endpoint', async () => { + const res = await fixture.fetch('/api'); + const json = await res.json(); + assert.deepEqual(json, { ok: true }); + }); +}); diff --git a/packages/integrations/cloudflare/test/prerender-styles.test.js b/packages/integrations/cloudflare/test/prerender-styles.test.ts similarity index 93% rename from packages/integrations/cloudflare/test/prerender-styles.test.js rename to packages/integrations/cloudflare/test/prerender-styles.test.ts index 77df3fb8b270..eab3bee011fb 100644 --- a/packages/integrations/cloudflare/test/prerender-styles.test.js +++ b/packages/integrations/cloudflare/test/prerender-styles.test.ts @@ -1,13 +1,11 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import cloudflare from '../dist/index.js'; -import { loadFixture } from './_test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from './test-utils.ts'; describe('Prerendered page styles', () => { - /** @type {import('../../../astro/test/test-utils').Fixture} */ - let fixture; - let devServer; - + let fixture: Fixture; + let devServer: DevServer | undefined; before(async () => { fixture = await loadFixture({ root: new URL('./fixtures/prerender-styles/', import.meta.url).toString(), @@ -62,10 +60,8 @@ describe('Prerendered page styles', () => { }); describe('Styles from Astro components imported in MDX content collections', () => { - /** @type {import('../../../astro/test/test-utils').Fixture} */ - let fixture; - let devServer; - + let fixture: Fixture; + let devServer: DevServer | undefined; before(async () => { fixture = await loadFixture({ root: new URL('./fixtures/prerender-styles/', import.meta.url).toString(), diff --git a/packages/integrations/cloudflare/test/prerenderer-errors.test.js b/packages/integrations/cloudflare/test/prerenderer-errors.test.ts similarity index 89% rename from packages/integrations/cloudflare/test/prerenderer-errors.test.js rename to packages/integrations/cloudflare/test/prerenderer-errors.test.ts index 329b08eaaa88..e90c2152343c 100644 --- a/packages/integrations/cloudflare/test/prerenderer-errors.test.js +++ b/packages/integrations/cloudflare/test/prerenderer-errors.test.ts @@ -1,11 +1,10 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { type Fixture, loadFixture } from '../../../astro/test/test-utils.js'; import cloudflare from '../dist/index.js'; describe('Cloudflare prerenderer errors', () => { - let fixture; - + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: new URL('./fixtures/prerenderer-errors/', import.meta.url).toString(), diff --git a/packages/integrations/cloudflare/test/routing-priority.test.js b/packages/integrations/cloudflare/test/routing-priority.test.ts similarity index 96% rename from packages/integrations/cloudflare/test/routing-priority.test.js rename to packages/integrations/cloudflare/test/routing-priority.test.ts index 36935d8a0ef0..68a13639d437 100644 --- a/packages/integrations/cloudflare/test/routing-priority.test.js +++ b/packages/integrations/cloudflare/test/routing-priority.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { load as cheerioLoad } from 'cheerio'; -import { loadFixture } from './_test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from './test-utils.ts'; const routes = [ { @@ -129,14 +129,13 @@ const routes = [ }, ]; -function appendForwardSlash(path) { +function appendForwardSlash(path: string) { return path.endsWith('/') ? path : path + '/'; } describe('Routing priority', () => { describe('build', () => { - let fixture; - + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/routing-priority/', @@ -174,9 +173,8 @@ describe('Routing priority', () => { }); describe('dev', () => { - let fixture; - let devServer; - + let fixture: Fixture; + let devServer: DevServer; before(async () => { fixture = await loadFixture({ root: './fixtures/routing-priority/', diff --git a/packages/integrations/cloudflare/test/server-entry.test.js b/packages/integrations/cloudflare/test/server-entry.test.ts similarity index 86% rename from packages/integrations/cloudflare/test/server-entry.test.js rename to packages/integrations/cloudflare/test/server-entry.test.ts index 6c4c8e34ee9b..0e662f4a8836 100644 --- a/packages/integrations/cloudflare/test/server-entry.test.js +++ b/packages/integrations/cloudflare/test/server-entry.test.ts @@ -1,11 +1,11 @@ import { describe, it } from 'node:test'; -import { loadFixture } from './_test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.ts'; import assert from 'node:assert/strict'; import { existsSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; describe('Server entry', () => { - let fixture; + let fixture: Fixture; it('should load the custom entry when using legacy entrypoint', async () => { fixture = await loadFixture({ root: './fixtures/server-entry', diff --git a/packages/integrations/cloudflare/test/server-island-prerender-deps.test.js b/packages/integrations/cloudflare/test/server-island-prerender-deps.test.ts similarity index 93% rename from packages/integrations/cloudflare/test/server-island-prerender-deps.test.js rename to packages/integrations/cloudflare/test/server-island-prerender-deps.test.ts index b541f13f8908..b099ebfb4b27 100644 --- a/packages/integrations/cloudflare/test/server-island-prerender-deps.test.js +++ b/packages/integrations/cloudflare/test/server-island-prerender-deps.test.ts @@ -3,11 +3,11 @@ import { promises as fs } from 'node:fs'; import path from 'node:path'; import { describe, it } from 'node:test'; import { fileURLToPath } from 'node:url'; -import { loadFixture } from './_test-utils.js'; +import { loadFixture } from './test-utils.ts'; -async function readFilesRecursive(dir) { +async function readFilesRecursive(dir: string): Promise { const entries = await fs.readdir(dir, { withFileTypes: true }); - const files = await Promise.all( + const files: string[][] = await Promise.all( entries.map(async (entry) => { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { diff --git a/packages/integrations/cloudflare/test/sessions.test.js b/packages/integrations/cloudflare/test/sessions.test.ts similarity index 76% rename from packages/integrations/cloudflare/test/sessions.test.js rename to packages/integrations/cloudflare/test/sessions.test.ts index cd2a3de04fcc..d651a4810026 100644 --- a/packages/integrations/cloudflare/test/sessions.test.js +++ b/packages/integrations/cloudflare/test/sessions.test.ts @@ -2,12 +2,12 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import * as devalue from 'devalue'; import cloudflare from '../dist/index.js'; -import { loadFixture } from './_test-utils.js'; +import { type Fixture, loadFixture, type PreviewServer } from './test-utils.ts'; +import type { AstroInlineConfig } from 'astro'; describe('sessions', () => { - let fixture; - let previewServer; - + let fixture: Fixture; + let previewServer: PreviewServer; before(async () => { fixture = await loadFixture({ root: './fixtures/sessions/', @@ -22,7 +22,7 @@ describe('sessions', () => { it('can regenerate session cookies upon request', async () => { const firstResponse = await fixture.fetch('/regenerate', { method: 'GET' }); - const firstHeaders = firstResponse.headers.get('set-cookie').split(','); + const firstHeaders = firstResponse.headers.get('set-cookie')!.split(','); const firstSessionId = firstHeaders[0].split(';')[0].split('=')[1]; const secondResponse = await fixture.fetch('/regenerate', { @@ -31,7 +31,7 @@ describe('sessions', () => { cookie: `astro-session=${firstSessionId}`, }, }); - const secondHeaders = secondResponse.headers.get('set-cookie').split(','); + const secondHeaders = secondResponse.headers.get('set-cookie')!.split(','); const secondSessionId = secondHeaders[0].split(';')[0].split('=')[1]; assert.notEqual(firstSessionId, secondSessionId); }); @@ -41,7 +41,7 @@ describe('sessions', () => { const firstValue = await firstResponse.json(); assert.equal(firstValue.previousValue, 'none'); - const firstHeaders = firstResponse.headers.get('set-cookie').split(','); + const firstHeaders = firstResponse.headers.get('set-cookie')!.split(','); const firstSessionId = firstHeaders[0].split(';')[0].split('=')[1]; const secondResponse = await fixture.fetch('/update', { method: 'GET', @@ -63,7 +63,7 @@ describe('sessions', () => { }); assert.equal(firstResponse.ok, true); - const firstHeaders = firstResponse.headers.get('set-cookie').split(','); + const firstHeaders = firstResponse.headers.get('set-cookie')!.split(','); const firstSessionId = firstHeaders[0].split(';')[0].split('=')[1]; const data = devalue.parse(await firstResponse.text()); @@ -85,8 +85,7 @@ describe('sessions', () => { }); describe('sessions with custom binding name', () => { - let fixture; - + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/sessions/', @@ -97,13 +96,9 @@ describe('sessions with custom binding name', () => { }); it('can build with custom session binding name', async () => { - await assert.doesNotReject( - async () => { - await fixture.build(); - }, - undefined, - 'Building with custom session binding name should not throw an error', - ); + await assert.doesNotReject(async () => { + await fixture.build(); + }, 'Building with custom session binding name should not throw an error'); }); }); @@ -113,15 +108,19 @@ describe('session wrangler config', () => { root: './fixtures/static/', }); - await fixture.build({ + const config: AstroInlineConfig = { session: { + // @ts-expect-error: the default type of the TDriver in AstroUserConfig must be changed so that this can pass driver: { entrypoint: 'unstorage/drivers/null', }, }, - }); + }; + await fixture.build(config); - const wrangler = JSON.parse(await fixture.readFile('/server/wrangler.json')); + const wrangler = JSON.parse(await fixture.readFile('/server/wrangler.json')) as { + kv_namespaces?: Array<{ binding: string }>; + }; assert.equal( wrangler.kv_namespaces?.some(({ binding }) => binding === 'SESSION'), false, @@ -133,15 +132,19 @@ describe('session wrangler config', () => { root: './fixtures/static/', }); - await fixture.build({ + const config: AstroInlineConfig = { session: { + // @ts-expect-error: the default type of the TDriver in AstroUserConfig must be changed so that this can pass driver: { entrypoint: 'unstorage/drivers/cloudflare-kv-binding', }, }, - }); + }; + await fixture.build(config); - const wrangler = JSON.parse(await fixture.readFile('/server/wrangler.json')); + const wrangler = JSON.parse(await fixture.readFile('/server/wrangler.json')) as { + kv_namespaces?: Array<{ binding: string }>; + }; assert.deepEqual(wrangler.kv_namespaces, [{ binding: 'SESSION' }]); }); }); diff --git a/packages/integrations/cloudflare/test/sql-import.test.js b/packages/integrations/cloudflare/test/sql-import.test.ts similarity index 86% rename from packages/integrations/cloudflare/test/sql-import.test.js rename to packages/integrations/cloudflare/test/sql-import.test.ts index 29d6528db27b..fc72a668a3bb 100644 --- a/packages/integrations/cloudflare/test/sql-import.test.js +++ b/packages/integrations/cloudflare/test/sql-import.test.ts @@ -1,11 +1,10 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './_test-utils.js'; +import { type DevServer, type Fixture, loadFixture, type PreviewServer } from './test-utils.ts'; describe('SQL Import', () => { - let fixture; - + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/sql-import/', @@ -13,8 +12,7 @@ describe('SQL Import', () => { }); describe('dev', () => { - let devServer; - + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); }); @@ -33,8 +31,7 @@ describe('SQL Import', () => { }); describe('build', () => { - let previewServer; - + let previewServer: PreviewServer; before(async () => { await fixture.build(); previewServer = await fixture.preview(); diff --git a/packages/integrations/cloudflare/test/ssr-deps.test.js b/packages/integrations/cloudflare/test/ssr-deps.test.ts similarity index 74% rename from packages/integrations/cloudflare/test/ssr-deps.test.js rename to packages/integrations/cloudflare/test/ssr-deps.test.ts index 92d2973e2f01..1087b7b3fe16 100644 --- a/packages/integrations/cloudflare/test/ssr-deps.test.js +++ b/packages/integrations/cloudflare/test/ssr-deps.test.ts @@ -3,13 +3,13 @@ import { rmSync } from 'node:fs'; import { Writable } from 'node:stream'; import { after, before, describe, it } from 'node:test'; import { fileURLToPath } from 'node:url'; -import { Logger } from '../../../astro/dist/core/logger/core.js'; -import { loadFixture } from './_test-utils.js'; +import { AstroLogger } from '../../../astro/dist/core/logger/core.js'; +import { type DevServer, type Fixture, loadFixture } from './test-utils.ts'; describe('SSR dependencies', () => { - let fixture; - let devServer; - const logs = []; + let fixture: Fixture; + let devServer: DevServer; + const logs: Array<{ message?: string }> = []; before(async () => { fixture = await loadFixture({ @@ -20,18 +20,20 @@ describe('SSR dependencies', () => { const viteCacheDir = new URL('./node_modules/.vite/', fixture.config.root); rmSync(fileURLToPath(viteCacheDir), { recursive: true, force: true }); - devServer = await fixture.startDevServer({ - logger: new Logger({ - level: 'info', - dest: new Writable({ - objectMode: true, - write(event, _, callback) { - logs.push(event); - callback(); - }, - }), + const logger = new AstroLogger({ + level: 'info', + destination: new Writable({ + objectMode: true, + write(event, _, callback) { + logs.push(event); + callback(); + }, }), }); + devServer = await fixture.startDevServer({ + // @ts-expect-error: logger is internal API + logger, + }); }); after(async () => { diff --git a/packages/integrations/cloudflare/test/static.test.js b/packages/integrations/cloudflare/test/static.test.ts similarity index 87% rename from packages/integrations/cloudflare/test/static.test.js rename to packages/integrations/cloudflare/test/static.test.ts index cc7def3167a7..216730fc93b0 100644 --- a/packages/integrations/cloudflare/test/static.test.js +++ b/packages/integrations/cloudflare/test/static.test.ts @@ -1,12 +1,11 @@ import { describe, it } from 'node:test'; -import { loadFixture } from './_test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.ts'; import assert from 'node:assert/strict'; import { existsSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; describe('Static output', () => { - let fixture; - + let fixture: Fixture; it('should not output a _worker.js directory for fully static sites', async () => { fixture = await loadFixture({ root: './fixtures/static', diff --git a/packages/integrations/cloudflare/test/svelte-rune-deps.test.js b/packages/integrations/cloudflare/test/svelte-rune-deps.test.ts similarity index 92% rename from packages/integrations/cloudflare/test/svelte-rune-deps.test.js rename to packages/integrations/cloudflare/test/svelte-rune-deps.test.ts index b5430440153f..c28cdcb1df93 100644 --- a/packages/integrations/cloudflare/test/svelte-rune-deps.test.js +++ b/packages/integrations/cloudflare/test/svelte-rune-deps.test.ts @@ -2,12 +2,11 @@ import * as assert from 'node:assert/strict'; import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; import { after, before, describe, it } from 'node:test'; import { fileURLToPath } from 'node:url'; -import { loadFixture } from './_test-utils.js'; +import { type DevServer, type Fixture, loadFixture } from './test-utils.ts'; describe('Svelte rune dependencies', () => { - let fixture; - let devServer; - + let fixture: Fixture; + let devServer: DevServer; before(async () => { fixture = await loadFixture({ root: './fixtures/svelte-rune-deps/', diff --git a/packages/integrations/cloudflare/test/_test-utils.js b/packages/integrations/cloudflare/test/test-utils.ts similarity index 62% rename from packages/integrations/cloudflare/test/_test-utils.js rename to packages/integrations/cloudflare/test/test-utils.ts index 2859a680c2d1..8cd5dafda333 100644 --- a/packages/integrations/cloudflare/test/_test-utils.js +++ b/packages/integrations/cloudflare/test/test-utils.ts @@ -1,28 +1,32 @@ -import { loadFixture as baseLoadFixture } from '../../../astro/test/test-utils.js'; +import type { DevServer } from '../../../astro/src/core/dev/dev.js'; +import type { PreviewServer } from '../../../astro/src/types/public/preview.js'; +import { + type AstroInlineConfig, + type Fixture, + loadFixture as baseLoadFixture, +} from '../../../astro/test/test-utils.js'; -/** - * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture - */ -export async function loadFixture(inlineConfig) { +export type { AstroInlineConfig, DevServer, Fixture, PreviewServer }; + +export async function loadFixture(inlineConfig: AstroInlineConfig): Promise { if (!inlineConfig?.root) throw new Error("Must provide { root: './fixtures/...' }"); // resolve the relative root (i.e. "./fixtures/tailwindcss") to a full filepath // without this, the main `loadFixture` helper will resolve relative to `packages/astro/test` const fixture = await baseLoadFixture({ ...inlineConfig, - root: new URL(inlineConfig.root, import.meta.url).toString(), + root: new URL(inlineConfig.root as string, import.meta.url).toString(), }); // For unknown reasons, the error below could raise during testing. We add a retry mechanism to handle it. // Some further investigation is needed to understand the root cause. // // Unable to build fixture for the attempt 1: Error: There is a new version of the pre-bundle for "/astro/packages/integrations/cloudflare/test/fixtures/with-svelte/node_modules/.vite/deps_ssr/svelte_server.js?v=9924cddf", a page reload is going to ask for it. - const buildWithRetry = async function (...args) { - let err; + const buildWithRetry: Fixture['build'] = async (...args) => { + let err: unknown; for (let attempt = 1; attempt <= 3; attempt++) { try { - const result = await fixture.build(...args); - return result; + return await fixture.build(...args); } catch (error) { console.error(`Unable to build fixture for the attempt ${attempt}:`, error); err = error; diff --git a/packages/integrations/cloudflare/test/top-level-return.test.js b/packages/integrations/cloudflare/test/top-level-return.test.ts similarity index 76% rename from packages/integrations/cloudflare/test/top-level-return.test.js rename to packages/integrations/cloudflare/test/top-level-return.test.ts index c801eef880bc..d5a9132d3690 100644 --- a/packages/integrations/cloudflare/test/top-level-return.test.js +++ b/packages/integrations/cloudflare/test/top-level-return.test.ts @@ -1,15 +1,14 @@ import { rmSync } from 'node:fs'; import { describe, before, it } from 'node:test'; import { Writable } from 'node:stream'; -import { loadFixture } from './_test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.ts'; import assert from 'node:assert/strict'; import { fileURLToPath } from 'node:url'; -import { Logger } from '../../../astro/dist/core/logger/core.js'; +import { AstroLogger } from '../../../astro/dist/core/logger/core.js'; describe('Top-level Return', () => { - /** @type {import('../../../astro/test/test-utils').Fixture} */ - let fixture; - const logs = []; + let fixture: Fixture; + const logs: Array<{ message?: string }> = []; before(async () => { fixture = await loadFixture({ @@ -21,18 +20,21 @@ describe('Top-level Return', () => { rmSync(fileURLToPath(viteCacheDir), { recursive: true, force: true }); + const logger = new AstroLogger({ + level: 'error', + destination: new Writable({ + objectMode: true, + write(event, _, callback) { + logs.push(event); + callback(); + }, + }), + }); + await fixture.build({ vite: { logLevel: 'error' }, - logger: new Logger({ - level: 'error', - dest: new Writable({ - objectMode: true, - write(event, _, callback) { - logs.push(event); - callback(); - }, - }), - }), + // @ts-expect-error: logger is internal API + logger, }); }); diff --git a/packages/integrations/cloudflare/test/with-base.test.js b/packages/integrations/cloudflare/test/with-base.test.ts similarity index 64% rename from packages/integrations/cloudflare/test/with-base.test.js rename to packages/integrations/cloudflare/test/with-base.test.ts index 998d630dfc96..738c2cd3bcd7 100644 --- a/packages/integrations/cloudflare/test/with-base.test.js +++ b/packages/integrations/cloudflare/test/with-base.test.ts @@ -2,13 +2,13 @@ import * as assert from 'node:assert/strict'; import { rmSync } from 'node:fs'; import { Writable } from 'node:stream'; import { after, before, describe, it } from 'node:test'; -import { loadFixture } from './_test-utils.js'; -import { Logger } from '../../../astro/dist/core/logger/core.js'; +import { type Fixture, loadFixture } from './test-utils.ts'; +import { AstroLogger } from '../../../astro/dist/core/logger/core.js'; import { fileURLToPath } from 'node:url'; describe('base', () => { - let fixture; - const logs = []; + let fixture: Fixture; + const logs: Array<{ message?: string }> = []; before(async () => { fixture = await loadFixture({ @@ -20,19 +20,21 @@ describe('base', () => { rmSync(fileURLToPath(viteCacheDir), { recursive: true, force: true }); - await fixture.build({ - vite: { logLevel: 'debug' }, - logger: new Logger({ - level: 'debug', - dest: new Writable({ - objectMode: true, - write(event, _, callback) { - logs.push(event); - callback(); - }, - }), + const logger = new AstroLogger({ + level: 'debug', + destination: new Writable({ + objectMode: true, + write(event, _, callback) { + logs.push(event); + callback(); + }, }), }); + await fixture.build({ + vite: { logLevel: 'info' }, + // @ts-expect-error: logger is internal API + logger, + }); }); after(async () => { diff --git a/packages/integrations/cloudflare/test/with-react.test.js b/packages/integrations/cloudflare/test/with-react.test.ts similarity index 76% rename from packages/integrations/cloudflare/test/with-react.test.js rename to packages/integrations/cloudflare/test/with-react.test.ts index e57388c36897..4a91a6c796bb 100644 --- a/packages/integrations/cloudflare/test/with-react.test.js +++ b/packages/integrations/cloudflare/test/with-react.test.ts @@ -3,14 +3,14 @@ import { rmSync } from 'node:fs'; import { Writable } from 'node:stream'; import { after, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './_test-utils.js'; -import { Logger } from '../../../astro/dist/core/logger/core.js'; +import { type Fixture, loadFixture, type PreviewServer } from './test-utils.ts'; +import { AstroLogger } from '../../../astro/dist/core/logger/core.js'; import { fileURLToPath } from 'node:url'; describe('React', () => { - let fixture; - let previewServer; - const logs = []; + let fixture: Fixture; + let previewServer: PreviewServer; + const logs: Array<{ message?: string }> = []; before(async () => { fixture = await loadFixture({ @@ -22,19 +22,21 @@ describe('React', () => { rmSync(fileURLToPath(viteCacheDir), { recursive: true, force: true }); - await fixture.build({ - vite: { logLevel: 'debug' }, - logger: new Logger({ - level: 'debug', - dest: new Writable({ - objectMode: true, - write(event, _, callback) { - logs.push(event); - callback(); - }, - }), + const logger = new AstroLogger({ + level: 'debug', + destination: new Writable({ + objectMode: true, + write(event, _, callback) { + logs.push(event); + callback(); + }, }), }); + await fixture.build({ + vite: { logLevel: 'info' }, + // @ts-expect-error: logger is internal API + logger, + }); previewServer = await fixture.preview(); }); diff --git a/packages/integrations/cloudflare/test/with-solid-js.test.js b/packages/integrations/cloudflare/test/with-solid-js.test.ts similarity index 82% rename from packages/integrations/cloudflare/test/with-solid-js.test.js rename to packages/integrations/cloudflare/test/with-solid-js.test.ts index fc8a27cc4713..b407af4b4c8a 100644 --- a/packages/integrations/cloudflare/test/with-solid-js.test.js +++ b/packages/integrations/cloudflare/test/with-solid-js.test.ts @@ -1,12 +1,11 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './_test-utils.js'; +import { type Fixture, loadFixture, type PreviewServer } from './test-utils.ts'; describe('SolidJS', () => { - let fixture; - let previewServer; - + let fixture: Fixture; + let previewServer: PreviewServer; before(async () => { fixture = await loadFixture({ root: './fixtures/with-solid-js/', diff --git a/packages/integrations/cloudflare/test/with-svelte.test.js b/packages/integrations/cloudflare/test/with-svelte.test.ts similarity index 82% rename from packages/integrations/cloudflare/test/with-svelte.test.js rename to packages/integrations/cloudflare/test/with-svelte.test.ts index 7b654a7dcda2..f359aec41b3e 100644 --- a/packages/integrations/cloudflare/test/with-svelte.test.js +++ b/packages/integrations/cloudflare/test/with-svelte.test.ts @@ -1,12 +1,11 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './_test-utils.js'; +import { type Fixture, loadFixture, type PreviewServer } from './test-utils.ts'; describe('Svelte', () => { - let fixture; - let previewServer; - + let fixture: Fixture; + let previewServer: PreviewServer; before(async () => { fixture = await loadFixture({ root: './fixtures/with-svelte/', diff --git a/packages/integrations/cloudflare/test/with-vue.test.js b/packages/integrations/cloudflare/test/with-vue.test.ts similarity index 81% rename from packages/integrations/cloudflare/test/with-vue.test.js rename to packages/integrations/cloudflare/test/with-vue.test.ts index 3c8136f36616..75b856fe0fb7 100644 --- a/packages/integrations/cloudflare/test/with-vue.test.js +++ b/packages/integrations/cloudflare/test/with-vue.test.ts @@ -1,12 +1,11 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './_test-utils.js'; +import { type Fixture, loadFixture, type PreviewServer } from './test-utils.ts'; describe('Vue', () => { - let fixture; - let previewServer; - + let fixture: Fixture; + let previewServer: PreviewServer; before(async () => { fixture = await loadFixture({ root: './fixtures/with-vue/', diff --git a/packages/integrations/cloudflare/test/wrangler-preview-platform.test.js b/packages/integrations/cloudflare/test/wrangler-preview-platform.test.ts similarity index 90% rename from packages/integrations/cloudflare/test/wrangler-preview-platform.test.js rename to packages/integrations/cloudflare/test/wrangler-preview-platform.test.ts index 9bb1150aaa46..1d0dfaad90c3 100644 --- a/packages/integrations/cloudflare/test/wrangler-preview-platform.test.js +++ b/packages/integrations/cloudflare/test/wrangler-preview-platform.test.ts @@ -1,11 +1,11 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './_test-utils.js'; +import { type Fixture, loadFixture, type PreviewServer } from './test-utils.ts'; describe('WranglerPreviewPlatform', () => { - let fixture; - let previewServer; + let fixture: Fixture; + let previewServer: PreviewServer; before(async () => { fixture = await loadFixture({ root: './fixtures/wrangler-preview-platform/', diff --git a/packages/integrations/cloudflare/test/wrangler.test.js b/packages/integrations/cloudflare/test/wrangler.test.ts similarity index 100% rename from packages/integrations/cloudflare/test/wrangler.test.js rename to packages/integrations/cloudflare/test/wrangler.test.ts diff --git a/packages/integrations/cloudflare/tsconfig.test.json b/packages/integrations/cloudflare/tsconfig.test.json new file mode 100644 index 000000000000..fa4f11e4ae8b --- /dev/null +++ b/packages/integrations/cloudflare/tsconfig.test.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["test/**/*.ts"], + "exclude": ["test/fixtures/**"], + "compilerOptions": { + "allowJs": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "rewriteRelativeImportExtensions": true + }, + "references": [{ "path": "../../astro/tsconfig.test.json" }] +} diff --git a/packages/integrations/markdoc/package.json b/packages/integrations/markdoc/package.json index 6c8f16da46c3..21d6f611e1e5 100644 --- a/packages/integrations/markdoc/package.json +++ b/packages/integrations/markdoc/package.json @@ -59,7 +59,8 @@ "build": "astro-scripts build \"src/**/*.ts\" && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"", - "test": "astro-scripts test --timeout 100000 \"test/**/*.test.js\"" + "test": "astro-scripts test --timeout 100000 \"test/**/*.test.ts\"", + "typecheck:tests": "tsc --build tsconfig.test.json" }, "dependencies": { "@astrojs/internal-helpers": "workspace:*", diff --git a/packages/integrations/markdoc/test/content-collections.test.js b/packages/integrations/markdoc/test/content-collections.test.ts similarity index 83% rename from packages/integrations/markdoc/test/content-collections.test.js rename to packages/integrations/markdoc/test/content-collections.test.ts index d9e88c868e65..265db0997991 100644 --- a/packages/integrations/markdoc/test/content-collections.test.js +++ b/packages/integrations/markdoc/test/content-collections.test.ts @@ -1,10 +1,15 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { parse as parseDevalue } from 'devalue'; -import { fixLineEndings, loadFixture } from '../../../astro/test/test-utils.js'; +import { + fixLineEndings, + loadFixture, + type Fixture, + type DevServer, +} from '../../../astro/test/test-utils.js'; import markdoc from '../dist/index.js'; -function formatPost(post) { +function formatPost(post: T): T { return { ...post, body: fixLineEndings(post.body), @@ -13,10 +18,10 @@ function formatPost(post) { const root = new URL('./fixtures/content-collections/', import.meta.url); -const sortById = (a, b) => a.id.localeCompare(b.id); +const sortById = (a: { id: string }, b: { id: string }) => a.id.localeCompare(b.id); describe('Markdoc - Content Collections', () => { - let baseFixture; + let baseFixture: Fixture; before(async () => { baseFixture = await loadFixture({ @@ -26,7 +31,7 @@ describe('Markdoc - Content Collections', () => { }); describe('dev', () => { - let devServer; + let devServer: DevServer; before(async () => { devServer = await baseFixture.startDevServer(); @@ -48,7 +53,7 @@ describe('Markdoc - Content Collections', () => { assert.notEqual(posts, null); assert.deepEqual( - posts.sort(sortById).map((post) => formatPost(post)), + posts.sort(sortById).map((post: { id: string; body: string }) => formatPost(post)), [post1Entry, post2Entry, post3Entry], ); }); @@ -70,7 +75,7 @@ describe('Markdoc - Content Collections', () => { const posts = parseDevalue(res); assert.notEqual(posts, null); assert.deepEqual( - posts.sort(sortById).map((post) => formatPost(post)), + posts.sort(sortById).map((post: { id: string; body: string }) => formatPost(post)), [post1Entry, post2Entry, post3Entry], ); }); diff --git a/packages/integrations/markdoc/test/content-layer.test.js b/packages/integrations/markdoc/test/content-layer.test.ts similarity index 75% rename from packages/integrations/markdoc/test/content-layer.test.js rename to packages/integrations/markdoc/test/content-layer.test.ts index 2c2af3150d8f..d92d28a06d5c 100644 --- a/packages/integrations/markdoc/test/content-layer.test.js +++ b/packages/integrations/markdoc/test/content-layer.test.ts @@ -1,12 +1,12 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { parseHTML } from 'linkedom'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture, type DevServer } from '../../../astro/test/test-utils.js'; const root = new URL('./fixtures/content-layer/', import.meta.url); describe('Markdoc - Content Layer', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -15,7 +15,7 @@ describe('Markdoc - Content Layer', () => { }); describe('dev', () => { - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); @@ -59,32 +59,30 @@ describe('Markdoc - Content Layer', () => { }); }); -/** @param {string} html */ -function renderComponentsChecks(html) { +function renderComponentsChecks(html: string) { const { document } = parseHTML(html); const h2 = document.querySelector('h2'); - assert.equal(h2.textContent, 'Post with components'); + assert.equal(h2!.textContent, 'Post with components'); // Renders custom shortcode component const marquee = document.querySelector('marquee'); assert.notEqual(marquee, null); - assert.equal(marquee.hasAttribute('data-custom-marquee'), true); + assert.equal(marquee!.hasAttribute('data-custom-marquee'), true); // Renders Astro Code component const pre = document.querySelector('pre'); assert.notEqual(pre, null); - assert.ok(pre.classList.contains('github-dark')); - assert.ok(pre.classList.contains('astro-code')); + assert.ok(pre!.classList.contains('github-dark')); + assert.ok(pre!.classList.contains('astro-code')); } -/** @param {string} html */ -function renderComponentsInsidePartialsChecks(html) { +function renderComponentsInsidePartialsChecks(html: string) { const { document } = parseHTML(html); // renders Counter.tsx const button = document.querySelector('#counter'); - assert.equal(button.textContent, '1'); + assert.equal(button!.textContent, '1'); // renders DeeplyNested.astro const deeplyNested = document.querySelector('#deeply-nested'); - assert.equal(deeplyNested.textContent, 'Deeply nested partial'); + assert.equal(deeplyNested!.textContent, 'Deeply nested partial'); } diff --git a/packages/integrations/markdoc/test/headings.test.js b/packages/integrations/markdoc/test/headings.test.ts similarity index 91% rename from packages/integrations/markdoc/test/headings.test.js rename to packages/integrations/markdoc/test/headings.test.ts index b39fb9485b12..689dcd65d6ed 100644 --- a/packages/integrations/markdoc/test/headings.test.js +++ b/packages/integrations/markdoc/test/headings.test.ts @@ -1,23 +1,23 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { parseHTML } from 'linkedom'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture, type DevServer } from '../../../astro/test/test-utils.js'; -async function getFixture(name) { +async function getFixture(name: string) { return await loadFixture({ root: new URL(`./fixtures/${name}/`, import.meta.url), }); } describe('Markdoc - Headings', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await getFixture('headings'); }); describe('dev', () => { - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); @@ -90,14 +90,14 @@ describe('Markdoc - Headings', () => { }); describe('Markdoc - Headings with custom Astro renderer', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await getFixture('headings-custom'); }); describe('dev', () => { - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); @@ -202,28 +202,25 @@ const depthToHeadingMap = { }, }; -/** @param {Document} document */ -function idTest(document) { +function idTest(document: Document) { for (const [depth, info] of Object.entries(depthToHeadingMap)) { assert.equal(document.querySelector(`h${depth}`)?.getAttribute('id'), info.slug); } } -/** @param {Document} document */ -function tocTest(document) { +function tocTest(document: Document) { const toc = document.querySelector('[data-toc] > ul'); - assert.equal(toc.children.length, Object.keys(depthToHeadingMap).length); + assert.equal(toc!.children.length, Object.keys(depthToHeadingMap).length); for (const [depth, info] of Object.entries(depthToHeadingMap)) { - const linkEl = toc.querySelector(`a[href="#${info.slug}"]`); + const linkEl = toc!.querySelector(`a[href="#${info.slug}"]`); assert.ok(linkEl); assert.equal(linkEl.getAttribute('data-depth'), depth); assert.equal(linkEl.textContent.trim(), info.text); } } -/** @param {Document} document */ -function astroComponentTest(document) { +function astroComponentTest(document: Document) { const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6'); for (const heading of headings) { diff --git a/packages/integrations/markdoc/test/image-assets.test.js b/packages/integrations/markdoc/test/image-assets.test.ts similarity index 71% rename from packages/integrations/markdoc/test/image-assets.test.js rename to packages/integrations/markdoc/test/image-assets.test.ts index 17fd944134d9..9d125c96d1a4 100644 --- a/packages/integrations/markdoc/test/image-assets.test.js +++ b/packages/integrations/markdoc/test/image-assets.test.ts @@ -1,20 +1,20 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { parseHTML } from 'linkedom'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture, type DevServer } from '../../../astro/test/test-utils.js'; const imageAssetsFixture = new URL('./fixtures/image-assets/', import.meta.url); const imageAssetsCustomFixture = new URL('./fixtures/image-assets-custom/', import.meta.url); describe('Markdoc - Image assets', () => { - const configurations = [ + const configurations: [URL, string][] = [ [imageAssetsFixture, 'Standard default image node rendering'], [imageAssetsCustomFixture, 'Custom default image node component'], ]; for (const [root, description] of configurations) { describe(description, () => { - let baseFixture; + let baseFixture: Fixture; before(async () => { baseFixture = await loadFixture({ @@ -23,7 +23,7 @@ describe('Markdoc - Image assets', () => { }); describe('dev', () => { - let devServer; + let devServer: DevServer; before(async () => { devServer = await baseFixture.startDevServer(); @@ -37,7 +37,10 @@ describe('Markdoc - Image assets', () => { const res = await baseFixture.fetch('/'); const html = await res.text(); const { document } = parseHTML(html); - assert.equal(document.querySelector('#public > img')?.src, '/favicon.svg'); + assert.equal( + document.querySelector('#public > img')?.src, + '/favicon.svg', + ); }); it('transforms relative image paths to optimized path', async () => { @@ -45,7 +48,7 @@ describe('Markdoc - Image assets', () => { const html = await res.text(); const { document } = parseHTML(html); assert.match( - document.querySelector('#relative > img')?.src, + document.querySelector('#relative > img')!.src, /\/_image\?href=.*%2Fsrc%2Fassets%2Frelative%2Foar.jpg%3ForigWidth%3D420%26origHeight%3D630%26origFormat%3Djpg&w=420&h=630&f=webp/, ); }); @@ -55,7 +58,7 @@ describe('Markdoc - Image assets', () => { const html = await res.text(); const { document } = parseHTML(html); assert.match( - document.querySelector('#alias > img')?.src, + document.querySelector('#alias > img')!.src, /\/_image\?href=.*%2Fsrc%2Fassets%2Falias%2Fcityscape.jpg%3ForigWidth%3D420%26origHeight%3D280%26origFormat%3Djpg&w=420&h=280&f=webp/, ); }); @@ -64,7 +67,10 @@ describe('Markdoc - Image assets', () => { const res = await baseFixture.fetch('/'); const html = await res.text(); const { document } = parseHTML(html); - assert.equal(document.querySelector('#component > img')?.className, 'custom-styles'); + assert.equal( + document.querySelector('#component > img')?.className, + 'custom-styles', + ); }); }); @@ -76,20 +82,26 @@ describe('Markdoc - Image assets', () => { it('uses public/ image paths unchanged', async () => { const html = await baseFixture.readFile('/index.html'); const { document } = parseHTML(html); - assert.equal(document.querySelector('#public > img')?.src, '/favicon.svg'); + assert.equal( + document.querySelector('#public > img')?.src, + '/favicon.svg', + ); }); it('transforms relative image paths to optimized path', async () => { const html = await baseFixture.readFile('/index.html'); const { document } = parseHTML(html); - assert.match(document.querySelector('#relative > img')?.src, /^\/_astro\/oar.*\.webp$/); + assert.match( + document.querySelector('#relative > img')!.src, + /^\/_astro\/oar.*\.webp$/, + ); }); it('transforms aliased image paths to optimized path', async () => { const html = await baseFixture.readFile('/index.html'); const { document } = parseHTML(html); assert.match( - document.querySelector('#alias > img')?.src, + document.querySelector('#alias > img')!.src, /^\/_astro\/cityscape.*\.webp$/, ); }); @@ -97,8 +109,14 @@ describe('Markdoc - Image assets', () => { it('passes images inside image tags to configured image component', async () => { const html = await baseFixture.readFile('/index.html'); const { document } = parseHTML(html); - assert.equal(document.querySelector('#component > img')?.className, 'custom-styles'); - assert.match(document.querySelector('#component > img')?.src, /^\/_astro\/oar.*\.webp$/); + assert.equal( + document.querySelector('#component > img')?.className, + 'custom-styles', + ); + assert.match( + document.querySelector('#component > img')!.src, + /^\/_astro\/oar.*\.webp$/, + ); }); }); }); diff --git a/packages/integrations/markdoc/test/propagated-assets.test.js b/packages/integrations/markdoc/test/propagated-assets.test.ts similarity index 75% rename from packages/integrations/markdoc/test/propagated-assets.test.js rename to packages/integrations/markdoc/test/propagated-assets.test.ts index a0768448f1d9..afe4e817e5d9 100644 --- a/packages/integrations/markdoc/test/propagated-assets.test.js +++ b/packages/integrations/markdoc/test/propagated-assets.test.ts @@ -1,11 +1,11 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { parseHTML } from 'linkedom'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture, type DevServer } from '../../../astro/test/test-utils.js'; describe('Markdoc - propagated assets', () => { - let fixture; - let devServer; + let fixture: Fixture; + let devServer: DevServer; before(async () => { fixture = await loadFixture({ root: new URL('./fixtures/propagated-assets/', import.meta.url), @@ -18,10 +18,8 @@ describe('Markdoc - propagated assets', () => { for (const mode of modes) { describe(mode, () => { - /** @type {Document} */ - let stylesDocument; - /** @type {Document} */ - let scriptsDocument; + let stylesDocument: Document; + let scriptsDocument: Document; before(async () => { if (mode === 'prod') { @@ -44,11 +42,11 @@ describe('Markdoc - propagated assets', () => { it('Bundles styles', async () => { let styleContents; if (mode === 'dev') { - const styles = stylesDocument.querySelectorAll('style'); + const styles = stylesDocument.querySelectorAll('style'); assert.equal(styles.length, 1); styleContents = styles[0].textContent; } else { - const links = stylesDocument.querySelectorAll('link[rel="stylesheet"]'); + const links = stylesDocument.querySelectorAll('link[rel="stylesheet"]'); assert.equal(links.length, 1); styleContents = await fixture.readFile(links[0].href); } @@ -57,10 +55,10 @@ describe('Markdoc - propagated assets', () => { it('[fails] Does not bleed styles to other page', async () => { if (mode === 'dev') { - const styles = scriptsDocument.querySelectorAll('style'); + const styles = scriptsDocument.querySelectorAll('style'); assert.equal(styles.length, 0); } else { - const links = scriptsDocument.querySelectorAll('link[rel="stylesheet"]'); + const links = scriptsDocument.querySelectorAll('link[rel="stylesheet"]'); assert.equal(links.length, 0); } }); diff --git a/packages/integrations/markdoc/test/render-components.test.js b/packages/integrations/markdoc/test/render-components.test.ts similarity index 76% rename from packages/integrations/markdoc/test/render-components.test.js rename to packages/integrations/markdoc/test/render-components.test.ts index e8ddec90976a..74d49c4de798 100644 --- a/packages/integrations/markdoc/test/render-components.test.js +++ b/packages/integrations/markdoc/test/render-components.test.ts @@ -1,12 +1,12 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { parseHTML } from 'linkedom'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture, type DevServer } from '../../../astro/test/test-utils.js'; const root = new URL('./fixtures/render-with-components/', import.meta.url); describe('Markdoc - render components', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -15,7 +15,7 @@ describe('Markdoc - render components', () => { }); describe('dev', () => { - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); @@ -59,36 +59,34 @@ describe('Markdoc - render components', () => { }); }); -/** @param {string} html */ -function renderComponentsChecks(html) { +function renderComponentsChecks(html: string) { const { document } = parseHTML(html); const h2 = document.querySelector('h2'); - assert.equal(h2.textContent, 'Post with components'); + assert.equal(h2!.textContent, 'Post with components'); // Renders custom shortcode component const marquee = document.querySelector('marquee'); assert.notEqual(marquee, null); - assert.equal(marquee.hasAttribute('data-custom-marquee'), true); + assert.equal(marquee!.hasAttribute('data-custom-marquee'), true); // Renders Astro Code component const pre = document.querySelector('pre'); assert.notEqual(pre, null); - assert.equal(pre.className, 'astro-code github-dark'); + assert.equal(pre!.className, 'astro-code github-dark'); // Renders 2nd Astro Code component inside if tag const pre2 = document.querySelectorAll('pre')[1]; assert.notEqual(pre2, null); - assert.equal(pre2.className, 'astro-code github-dark'); + assert.equal(pre2!.className, 'astro-code github-dark'); } -/** @param {string} html */ -function renderComponentsInsidePartialsChecks(html) { +function renderComponentsInsidePartialsChecks(html: string) { const { document } = parseHTML(html); // renders Counter.tsx const button = document.querySelector('#counter'); - assert.equal(button.textContent, '1'); + assert.equal(button!.textContent, '1'); // renders DeeplyNested.astro const deeplyNested = document.querySelector('#deeply-nested'); - assert.equal(deeplyNested.textContent, 'Deeply nested partial'); + assert.equal(deeplyNested!.textContent, 'Deeply nested partial'); } diff --git a/packages/integrations/markdoc/test/render-extends-components.test.js b/packages/integrations/markdoc/test/render-extends-components.test.ts similarity index 79% rename from packages/integrations/markdoc/test/render-extends-components.test.js rename to packages/integrations/markdoc/test/render-extends-components.test.ts index f5f1454c8e1b..7ae4a3349117 100644 --- a/packages/integrations/markdoc/test/render-extends-components.test.js +++ b/packages/integrations/markdoc/test/render-extends-components.test.ts @@ -1,12 +1,12 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { parseHTML } from 'linkedom'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture, type DevServer } from '../../../astro/test/test-utils.js'; const root = new URL('./fixtures/render-with-extends-components/', import.meta.url); describe('Markdoc - render components defined in `extends`', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -15,7 +15,7 @@ describe('Markdoc - render components defined in `extends`', () => { }); describe('dev', () => { - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); @@ -46,21 +46,20 @@ describe('Markdoc - render components defined in `extends`', () => { }); }); -/** @param {string} html */ -function renderComponentsChecks(html) { +function renderComponentsChecks(html: string) { const { document } = parseHTML(html); const h2 = document.querySelector('h2'); - assert.equal(h2.textContent, 'Post with components'); + assert.equal(h2!.textContent, 'Post with components'); // Renders custom shortcode component const marquee = document.querySelector('marquee'); assert.notEqual(marquee, null); - assert.equal(marquee.hasAttribute('data-custom-marquee'), true); + assert.equal(marquee!.hasAttribute('data-custom-marquee'), true); // Renders Astro Code component const pre = document.querySelector('pre'); assert.notEqual(pre, null); - assert.equal(pre.className, 'astro-code github-dark'); + assert.equal(pre!.className, 'astro-code github-dark'); // Renders 2nd Astro Code component inside if tag const pre2 = document.querySelectorAll('pre')[1]; diff --git a/packages/integrations/markdoc/test/render-html.test.js b/packages/integrations/markdoc/test/render-html.test.ts similarity index 80% rename from packages/integrations/markdoc/test/render-html.test.js rename to packages/integrations/markdoc/test/render-html.test.ts index bb5135cccb12..204f35e8fdf1 100644 --- a/packages/integrations/markdoc/test/render-html.test.js +++ b/packages/integrations/markdoc/test/render-html.test.ts @@ -1,23 +1,23 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { parseHTML } from 'linkedom'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture, type DevServer } from '../../../astro/test/test-utils.js'; -async function getFixture(name) { +async function getFixture(name: string) { return await loadFixture({ root: new URL(`./fixtures/${name}/`, import.meta.url), }); } describe('Markdoc - render html', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await getFixture('render-html'); }); describe('dev', () => { - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); @@ -100,27 +100,26 @@ describe('Markdoc - render html', () => { }); }); -/** @param {string} html */ -function renderSimpleChecks(html) { +function renderSimpleChecks(html: string) { const { document } = parseHTML(html); - const h2 = document.querySelector('h2'); + const h2 = document.querySelector('h2')!; assert.equal(h2.textContent, 'Simple post header'); - const spanInsideH2 = document.querySelector('h2 > span'); + const spanInsideH2 = document.querySelector('h2 > span')!; assert.equal(spanInsideH2.textContent, 'post'); assert.equal(spanInsideH2.className, 'inside-h2'); assert.equal(spanInsideH2.style.color, 'fuscia'); - const p1 = document.querySelector('article > p:nth-of-type(1)'); + const p1 = document.querySelector('article > p:nth-of-type(1)')!; assert.equal(p1.children.length, 1); assert.equal(p1.textContent, 'This is a simple Markdoc post.'); - const p2 = document.querySelector('article > p:nth-of-type(2)'); + const p2 = document.querySelector('article > p:nth-of-type(2)')!; assert.equal(p2.children.length, 0); assert.equal(p2.textContent, 'This is a paragraph!'); - const p3 = document.querySelector('article > p:nth-of-type(3)'); + const p3 = document.querySelector('article > p:nth-of-type(3)')!; assert.equal(p3.children.length, 1); assert.equal(p3.textContent, 'This is a span inside a paragraph!'); @@ -130,64 +129,60 @@ function renderSimpleChecks(html) { assert.ok(video.hasAttribute('muted'), 'The video element should have the muted attribute'); } -/** @param {string} html */ -function renderNestedHTMLChecks(html) { +function renderNestedHTMLChecks(html: string) { const { document } = parseHTML(html); - const p1 = document.querySelector('p:nth-of-type(1)'); + const p1 = document.querySelector('p:nth-of-type(1)')!; assert.equal(p1.id, 'p1'); assert.equal(p1.textContent, 'before inner after'); assert.equal(p1.children.length, 1); - const p1Span1 = p1.querySelector('span'); + const p1Span1 = p1.querySelector('span')!; assert.equal(p1Span1.textContent, 'inner'); assert.equal(p1Span1.id, 'inner1'); assert.equal(p1Span1.className, 'inner-class'); assert.equal(p1Span1.style.color, 'hotpink'); - const p2 = document.querySelector('p:nth-of-type(2)'); + const p2 = document.querySelector('p:nth-of-type(2)')!; assert.equal(p2.id, 'p2'); assert.equal(p2.textContent, '\n before\n inner\n after\n'); assert.equal(p2.children.length, 1); - const divL1 = document.querySelector('div:nth-of-type(1)'); + const divL1 = document.querySelector('div:nth-of-type(1)')!; assert.equal(divL1.id, 'div-l1'); assert.equal(divL1.children.length, 2); - const divL2_1 = divL1.querySelector('div:nth-of-type(1)'); + const divL2_1 = divL1.querySelector('div:nth-of-type(1)')!; assert.equal(divL2_1.id, 'div-l2-1'); assert.equal(divL2_1.children.length, 1); - const p3 = divL2_1.querySelector('p:nth-of-type(1)'); + const p3 = divL2_1.querySelector('p:nth-of-type(1)')!; assert.equal(p3.id, 'p3'); assert.equal(p3.textContent, 'before inner after'); assert.equal(p3.children.length, 1); - const divL2_2 = divL1.querySelector('div:nth-of-type(2)'); + const divL2_2 = divL1.querySelector('div:nth-of-type(2)')!; assert.equal(divL2_2.id, 'div-l2-2'); assert.equal(divL2_2.children.length, 2); - const p4 = divL2_2.querySelector('p:nth-of-type(1)'); + const p4 = divL2_2.querySelector('p:nth-of-type(1)')!; assert.equal(p4.id, 'p4'); assert.equal(p4.textContent, 'before inner after'); assert.equal(p4.children.length, 1); - const p5 = divL2_2.querySelector('p:nth-of-type(2)'); + const p5 = divL2_2.querySelector('p:nth-of-type(2)')!; assert.equal(p5.id, 'p5'); assert.equal(p5.textContent, 'before inner after'); assert.equal(p5.children.length, 1); } -/** - * - * @param {string} html */ -function renderRandomlyCasedHTMLAttributesChecks(html) { +function renderRandomlyCasedHTMLAttributesChecks(html: string) { const { document } = parseHTML(html); - const td1 = document.querySelector('#td1'); - const td2 = document.querySelector('#td1'); - const td3 = document.querySelector('#td1'); - const td4 = document.querySelector('#td1'); + const td1 = document.querySelector('#td1')!; + const td2 = document.querySelector('#td1')!; + const td3 = document.querySelector('#td1')!; + const td4 = document.querySelector('#td1')!; // all four 's which had randomly cased variants of colspan/rowspan should all be rendered lowercased at this point @@ -204,98 +199,94 @@ function renderRandomlyCasedHTMLAttributesChecks(html) { assert.equal(td4.getAttribute('rowspan'), '2'); } -/** - * @param {string} html - */ -function renderHTMLWithinPartialChecks(html) { +function renderHTMLWithinPartialChecks(html: string) { const { document } = parseHTML(html); - const li = document.querySelector('ul > li#partial'); + const li = document.querySelector('ul > li#partial')!; assert.equal(li.textContent, 'List item'); } /** * Asserts that the rendered HTML tags with interleaved Markdoc tags (both block and inline) rendered in the expected nested graph of elements - * - * @param {string} html */ -function renderComponentsHTMLChecks(html) { + */ +function renderComponentsHTMLChecks(html: string) { const { document } = parseHTML(html); - const naturalP1 = document.querySelector('article > p:nth-of-type(1)'); + const naturalP1 = document.querySelector('article > p:nth-of-type(1)')!; assert.equal(naturalP1.textContent, 'This is an inline mark in regular Markdown markup.'); assert.equal(naturalP1.children.length, 1); - const p1 = document.querySelector('article > p:nth-of-type(2)'); + const p1 = document.querySelector('article > p:nth-of-type(2)')!; assert.equal(p1.id, 'p1'); assert.equal(p1.textContent, 'This is an inline mark under some HTML'); assert.equal(p1.children.length, 1); - assertInlineMark(p1.children[0]); + assertInlineMark(p1.children[0] as HTMLElement); - const div1p1 = document.querySelector('article > #div1 > p:nth-of-type(1)'); + const div1p1 = document.querySelector('article > #div1 > p:nth-of-type(1)')!; assert.equal(div1p1.id, 'div1-p1'); assert.equal(div1p1.textContent, 'This is an inline mark under some HTML'); assert.equal(div1p1.children.length, 1); - assertInlineMark(div1p1.children[0]); + assertInlineMark(div1p1.children[0] as HTMLElement); - const div1p2 = document.querySelector('article > #div1 > p:nth-of-type(2)'); + const div1p2 = document.querySelector('article > #div1 > p:nth-of-type(2)')!; assert.equal(div1p2.id, 'div1-p2'); assert.equal(div1p2.textContent, 'This is an inline mark under some HTML'); assert.equal(div1p2.children.length, 1); - const div1p2span1 = div1p2.querySelector('span'); + const div1p2span1 = div1p2.querySelector('span')!; assert.equal(div1p2span1.id, 'div1-p2-span1'); assert.equal(div1p2span1.textContent, 'inline mark'); assert.equal(div1p2span1.children.length, 1); - assertInlineMark(div1p2span1.children[0]); + assertInlineMark(div1p2span1.children[0] as HTMLElement); - const aside1 = document.querySelector('article > aside:nth-of-type(1)'); - const aside1Title = aside1.querySelector('p.title'); + const aside1 = document.querySelector('article > aside:nth-of-type(1)')!; + const aside1Title = aside1.querySelector('p.title')!; assert.equal(aside1Title.textContent.trim(), 'Aside One'); - const aside1Section = aside1.querySelector('section'); - const aside1SectionP1 = aside1Section.querySelector('p:nth-of-type(1)'); + const aside1Section = aside1.querySelector('section')!; + const aside1SectionP1 = aside1Section.querySelector('p:nth-of-type(1)')!; assert.equal( aside1SectionP1.textContent, "I'm a Markdown paragraph inside a top-level aside tag", ); - const aside1H2_1 = aside1Section.querySelector('h2:nth-of-type(1)'); + const aside1H2_1 = aside1Section.querySelector('h2:nth-of-type(1)')!; assert.equal(aside1H2_1.id, 'im-an-h2-via-markdown-markup'); // automatic slug assert.equal(aside1H2_1.textContent, "I'm an H2 via Markdown markup"); - const aside1H2_2 = aside1Section.querySelector('h2:nth-of-type(2)'); + const aside1H2_2 = aside1Section.querySelector('h2:nth-of-type(2)')!; assert.equal(aside1H2_2.id, 'h-two'); assert.equal(aside1H2_2.textContent, "I'm an H2 via HTML markup"); - const aside1SectionP2 = aside1Section.querySelector('p:nth-of-type(2)'); + const aside1SectionP2 = aside1Section.querySelector('p:nth-of-type(2)')!; assert.equal(aside1SectionP2.textContent, 'Markdown bold vs HTML bold'); assert.equal(aside1SectionP2.children.length, 2); - const aside1SectionP2Strong1 = aside1SectionP2.querySelector('strong:nth-of-type(1)'); + const aside1SectionP2Strong1 = aside1SectionP2.querySelector('strong:nth-of-type(1)')!; assert.equal(aside1SectionP2Strong1.textContent, 'Markdown bold'); - const aside1SectionP2Strong2 = aside1SectionP2.querySelector('strong:nth-of-type(2)'); + const aside1SectionP2Strong2 = aside1SectionP2.querySelector('strong:nth-of-type(2)')!; assert.equal(aside1SectionP2Strong2.textContent, 'HTML bold'); - const article = document.querySelector('article'); + const article = document.querySelector('article')!; assert.equal(article.textContent.includes('RENDERED'), true); assert.notEqual(article.textContent.includes('NOT RENDERED'), true); - const section1 = document.querySelector('article > #section1'); - const section1div1 = section1.querySelector('#div1'); - const section1Aside1 = section1div1.querySelector('aside:nth-of-type(1)'); - const section1Aside1Title = section1Aside1.querySelector('p.title'); + const section1 = document.querySelector('article > #section1')!; + const section1div1 = section1.querySelector('#div1')!; + const section1Aside1 = section1div1.querySelector('aside:nth-of-type(1)')!; + const section1Aside1Title = section1Aside1.querySelector('p.title')!; assert.equal(section1Aside1Title.textContent.trim(), 'Nested un-indented Aside'); - const section1Aside1Section = section1Aside1.querySelector('section'); - const section1Aside1SectionP1 = section1Aside1Section.querySelector('p:nth-of-type(1)'); + const section1Aside1Section = section1Aside1.querySelector('section')!; + const section1Aside1SectionP1 = section1Aside1Section.querySelector('p:nth-of-type(1)')!; assert.equal(section1Aside1SectionP1.textContent, 'regular Markdown markup'); - const section1Aside1SectionP4 = section1Aside1Section.querySelector('p:nth-of-type(2)'); + const section1Aside1SectionP4 = section1Aside1Section.querySelector('p:nth-of-type(2)')!; assert.equal(section1Aside1SectionP4.textContent, 'nested inline mark content'); assert.equal(section1Aside1SectionP4.children.length, 1); - assertInlineMark(section1Aside1SectionP4.children[0]); + assertInlineMark(section1Aside1SectionP4.children[0] as HTMLElement); - const section1div2 = section1.querySelector('#div2'); - const section1Aside2 = section1div2.querySelector('aside:nth-of-type(1)'); - const section1Aside2Title = section1Aside2.querySelector('p.title'); + const section1div2 = section1.querySelector('#div2')!; + const section1Aside2 = section1div2.querySelector('aside:nth-of-type(1)')!; + const section1Aside2Title = section1Aside2.querySelector('p.title')!; assert.equal(section1Aside2Title.textContent.trim(), 'Nested indented Aside 💀'); - const section1Aside2Section = section1Aside2.querySelector('section'); - const section1Aside2SectionP1 = section1Aside2Section.querySelector('p:nth-of-type(1)'); + const section1Aside2Section = section1Aside2.querySelector('section')!; + const section1Aside2SectionP1 = section1Aside2Section.querySelector('p:nth-of-type(1)')!; assert.equal(section1Aside2SectionP1.textContent, 'regular Markdown markup'); - const section1Aside1SectionP5 = section1Aside2Section.querySelector('p:nth-of-type(2)'); + const section1Aside1SectionP5 = section1Aside2Section.querySelector('p:nth-of-type(2)')!; assert.equal(section1Aside1SectionP5.id, 'p5'); assert.equal(section1Aside1SectionP5.children.length, 1); const section1Aside1SectionP5Span1 = section1Aside1SectionP5.children[0]; @@ -305,9 +296,7 @@ function renderComponentsHTMLChecks(html) { assert.equal(section1Aside1SectionP5Span1Span1.textContent, ' mark'); } -/** @param {HTMLElement | null | undefined} el */ - -function assertInlineMark(el) { +function assertInlineMark(el: HTMLElement | null | undefined) { assert.ok(el); assert.equal(el.children.length, 0); assert.equal(el.textContent, 'inline mark'); diff --git a/packages/integrations/markdoc/test/render-indented-components.test.js b/packages/integrations/markdoc/test/render-indented-components.test.ts similarity index 78% rename from packages/integrations/markdoc/test/render-indented-components.test.js rename to packages/integrations/markdoc/test/render-indented-components.test.ts index ac47e72f9116..2440bb9b23a3 100644 --- a/packages/integrations/markdoc/test/render-indented-components.test.js +++ b/packages/integrations/markdoc/test/render-indented-components.test.ts @@ -1,12 +1,12 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { parseHTML } from 'linkedom'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture, type DevServer } from '../../../astro/test/test-utils.js'; const root = new URL('./fixtures/render-with-indented-components/', import.meta.url); describe('Markdoc - render indented components', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -15,7 +15,7 @@ describe('Markdoc - render indented components', () => { }); describe('dev', () => { - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); @@ -46,11 +46,10 @@ describe('Markdoc - render indented components', () => { }); }); -/** @param {string} html */ -function renderIndentedComponentsChecks(html) { +function renderIndentedComponentsChecks(html: string) { const { document } = parseHTML(html); const h2 = document.querySelector('h2'); - assert.equal(h2.textContent, 'Post with indented components'); + assert.equal(h2!.textContent, 'Post with indented components'); // Renders custom shortcode components const marquees = document.querySelectorAll('marquee'); @@ -58,10 +57,10 @@ function renderIndentedComponentsChecks(html) { // Renders h3 const h3 = document.querySelector('h3'); - assert.equal(h3.textContent, 'I am an h3!'); + assert.equal(h3!.textContent, 'I am an h3!'); // Renders Astro Code component const pre = document.querySelector('pre'); assert.notEqual(pre, null); - assert.equal(pre.className, 'astro-code github-dark'); + assert.equal(pre!.className, 'astro-code github-dark'); } diff --git a/packages/integrations/markdoc/test/render-table-attrs.test.js b/packages/integrations/markdoc/test/render-table-attrs.test.ts similarity index 92% rename from packages/integrations/markdoc/test/render-table-attrs.test.js rename to packages/integrations/markdoc/test/render-table-attrs.test.ts index b1dd7e3f83ed..472a6c526524 100644 --- a/packages/integrations/markdoc/test/render-table-attrs.test.js +++ b/packages/integrations/markdoc/test/render-table-attrs.test.ts @@ -23,7 +23,7 @@ describe('Markdoc - table attributes', () => { assert.equal(th.textContent, 'Feature'); const td = document.querySelector('td'); - assert.equal(td.textContent, 'Custom attributes'); + assert.equal(td!.textContent, 'Custom attributes'); }); }); @@ -41,7 +41,7 @@ describe('Markdoc - table attributes', () => { assert.equal(th.textContent, 'Feature'); const td = document.querySelector('td'); - assert.equal(td.textContent, 'Custom attributes'); + assert.equal(td!.textContent, 'Custom attributes'); await server.stop(); }); diff --git a/packages/integrations/markdoc/test/render-with-transform.test.js b/packages/integrations/markdoc/test/render-with-transform.test.ts similarity index 79% rename from packages/integrations/markdoc/test/render-with-transform.test.js rename to packages/integrations/markdoc/test/render-with-transform.test.ts index fdf068455b1b..ad2adda991c4 100644 --- a/packages/integrations/markdoc/test/render-with-transform.test.js +++ b/packages/integrations/markdoc/test/render-with-transform.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { parseHTML } from 'linkedom'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture, type DevServer } from '../../../astro/test/test-utils.js'; const root = new URL('./fixtures/render-with-transform/', import.meta.url); @@ -12,14 +12,14 @@ const root = new URL('./fixtures/render-with-transform/', import.meta.url); * a custom `render` component, the `render` should win over the built-in `transform()`. */ describe('Markdoc - render with transform override', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root }); }); describe('dev', () => { - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); @@ -48,7 +48,7 @@ describe('Markdoc - render with transform override', () => { }); }); -function assertCustomFenceRendered(html) { +function assertCustomFenceRendered(html: string) { const { document } = parseHTML(html); // The custom component should render a div with data-custom-fence @@ -60,10 +60,10 @@ function assertCustomFenceRendered(html) { ); // Verify it has the language attribute - assert.equal(customFence.getAttribute('data-language'), 'js', 'Expected data-language="js"'); + assert.equal(customFence!.getAttribute('data-language'), 'js', 'Expected data-language="js"'); // The content should be inside a pre > code - const code = customFence.querySelector('pre code'); + const code = customFence!.querySelector('pre code'); assert.notEqual(code, null, 'Expected pre > code inside custom fence'); - assert.ok(code.textContent.includes('hello'), 'Expected code content to include "hello"'); + assert.ok(code!.textContent.includes('hello'), 'Expected code content to include "hello"'); } diff --git a/packages/integrations/markdoc/test/render.test.js b/packages/integrations/markdoc/test/render.test.ts similarity index 82% rename from packages/integrations/markdoc/test/render.test.js rename to packages/integrations/markdoc/test/render.test.ts index 4c9293288bb9..47ae8f097546 100644 --- a/packages/integrations/markdoc/test/render.test.js +++ b/packages/integrations/markdoc/test/render.test.ts @@ -3,7 +3,7 @@ import { describe, it } from 'node:test'; import { parseHTML } from 'linkedom'; import { loadFixture } from '../../../astro/test/test-utils.js'; -async function getFixture(name) { +async function getFixture(name: string) { return await loadFixture({ root: new URL(`./fixtures/${name}/`, import.meta.url), }); @@ -129,20 +129,16 @@ describe('Markdoc - render', () => { }); }); -/** - * @param {string} html - */ -function renderNullChecks(html) { +function renderNullChecks(html: string) { const { document } = parseHTML(html); const h2 = document.querySelector('h2'); - assert.equal(h2.textContent, 'Post with render null'); - assert.equal(h2.parentElement?.tagName, 'BODY'); + assert.equal(h2!.textContent, 'Post with render null'); + assert.equal(h2!.parentElement?.tagName, 'BODY'); const divWrapper = document.querySelector('.div-wrapper'); - assert.equal(divWrapper.textContent, "I'm inside a div wrapper"); + assert.equal(divWrapper!.textContent, "I'm inside a div wrapper"); } -/** @param {string} html */ -function renderPartialsChecks(html) { +function renderPartialsChecks(html: string) { const { document } = parseHTML(html); const top = document.querySelector('#top'); assert.ok(top); @@ -152,11 +148,10 @@ function renderPartialsChecks(html) { assert.ok(configured); } -/** @param {string} html */ -function renderConfigChecks(html) { +function renderConfigChecks(html: string) { const { document } = parseHTML(html); const h2 = document.querySelector('h2'); - assert.equal(h2.textContent, 'Post with config'); + assert.equal(h2!.textContent, 'Post with config'); const textContent = html; assert.notEqual(textContent.includes('Hello'), true); @@ -167,33 +162,28 @@ function renderConfigChecks(html) { assert.equal(runtimeVariable?.textContent?.trim(), 'working!'); } -/** @param {string} html */ -function renderSimpleChecks(html) { +function renderSimpleChecks(html: string) { const { document } = parseHTML(html); const h2 = document.querySelector('h2'); - assert.equal(h2.textContent, 'Simple post'); + assert.equal(h2!.textContent, 'Simple post'); const p = document.querySelector('p'); - assert.equal(p.textContent, 'This is a simple Markdoc post.'); + assert.equal(p!.textContent, 'This is a simple Markdoc post.'); } -/** @param {string} html */ -function renderWithRootFolderContainingSpace(html) { +function renderWithRootFolderContainingSpace(html: string) { const { document } = parseHTML(html); const h2 = document.querySelector('h2'); - assert.equal(h2.textContent, 'Simple post with root folder containing a space'); - const p = document.querySelector('p'); + assert.equal(h2!.textContent, 'Simple post with root folder containing a space'); + const p = document.querySelector('p')!; assert.equal(p.textContent, 'This is a simple Markdoc post with root folder containing a space.'); } -/** - * @param {string} html - */ -function renderTypographerChecks(html) { +function renderTypographerChecks(html: string) { const { document } = parseHTML(html); const h2 = document.querySelector('h2'); - assert.equal(h2.textContent, 'Typographer’s post'); + assert.equal(h2!.textContent, 'Typographer’s post'); - const p = document.querySelector('p'); + const p = document.querySelector('p')!; assert.equal(p.textContent, 'This is a post to test the “typographer” option.'); } diff --git a/packages/integrations/markdoc/test/syntax-highlighting.test.js b/packages/integrations/markdoc/test/syntax-highlighting.test.ts similarity index 54% rename from packages/integrations/markdoc/test/syntax-highlighting.test.js rename to packages/integrations/markdoc/test/syntax-highlighting.test.ts index 6ea841ae1249..c06c5a4dd1a6 100644 --- a/packages/integrations/markdoc/test/syntax-highlighting.test.js +++ b/packages/integrations/markdoc/test/syntax-highlighting.test.ts @@ -1,6 +1,6 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import Markdoc from '@markdoc/markdoc'; +import Markdoc, { type Tag } from '@markdoc/markdoc'; import { isHTMLString } from 'astro/runtime/server/index.js'; import { parseHTML } from 'linkedom'; import prism from '../dist/extensions/prism.js'; @@ -23,49 +23,45 @@ describe('Markdoc - syntax highlighting', () => { describe('shiki', () => { it('transforms with defaults', async () => { const ast = Markdoc.parse(entry); - const content = await Markdoc.transform(ast, await getConfigExtendingShiki()); + // @ts-expect-error AstroMarkdocConfig is incompatible with Markdoc.Config + const config: Markdoc.Config = await getConfigExtendingShiki(); + const content = (await Markdoc.transform(ast, config)) as Tag; assert.equal(content.children.length, 2); for (const codeBlock of content.children) { assert.equal(isHTMLString(codeBlock), true); - const pre = parsePreTag(codeBlock); + const pre = parsePreTag(codeBlock as string); assert.equal(pre.classList.contains('astro-code'), true); assert.equal(pre.classList.contains('github-dark'), true); } }); it('transforms with `theme` property', async () => { const ast = Markdoc.parse(entry); - const content = await Markdoc.transform( - ast, - await getConfigExtendingShiki({ - theme: 'dracula', - }), - ); + // @ts-expect-error AstroMarkdocConfig is incompatible with Markdoc.Config + const config: Markdoc.Config = await getConfigExtendingShiki({ theme: 'dracula' }); + const content = (await Markdoc.transform(ast, config)) as Tag; assert.equal(content.children.length, 2); for (const codeBlock of content.children) { assert.equal(isHTMLString(codeBlock), true); - const pre = parsePreTag(codeBlock); + const pre = parsePreTag(codeBlock as string); assert.equal(pre.classList.contains('astro-code'), true); assert.equal(pre.classList.contains('dracula'), true); } }); it('transforms with `wrap` property', async () => { const ast = Markdoc.parse(entry); - const content = await Markdoc.transform( - ast, - await getConfigExtendingShiki({ - wrap: true, - }), - ); + // @ts-expect-error AstroMarkdocConfig is incompatible with Markdoc.Config + const config: Markdoc.Config = await getConfigExtendingShiki({ wrap: true }); + const content = (await Markdoc.transform(ast, config)) as Tag; assert.equal(content.children.length, 2); for (const codeBlock of content.children) { assert.equal(isHTMLString(codeBlock), true); - const pre = parsePreTag(codeBlock); - assert.equal(pre.getAttribute('style').includes('white-space: pre-wrap'), true); - assert.equal(pre.getAttribute('style').includes('word-wrap: break-word'), true); + const pre = parsePreTag(codeBlock as string); + assert.equal(pre.getAttribute('style')!.includes('white-space: pre-wrap'), true); + assert.equal(pre.getAttribute('style')!.includes('word-wrap: break-word'), true); } }); it('transform within if tags', async () => { @@ -78,14 +74,17 @@ const hello = "yes"; \`\`\` {% /if %}`); - const content = await Markdoc.transform(ast, await getConfigExtendingShiki()); + // @ts-expect-error AstroMarkdocConfig is incompatible with Markdoc.Config + const config: Markdoc.Config = await getConfigExtendingShiki(); + const content = (await Markdoc.transform(ast, config)) as Tag; assert.equal(content.children.length, 1); - assert.equal(content.children[0].length, 2); - const pTag = content.children[0][0]; + const innerChildren = content.children[0] as unknown as Tag[]; + assert.equal(innerChildren.length, 2); + const pTag = innerChildren[0] as Tag; assert.equal(pTag.name, 'p'); - const codeBlock = content.children[0][1]; + const codeBlock = innerChildren[1]; assert.equal(isHTMLString(codeBlock), true); - const pre = parsePreTag(codeBlock); + const pre = parsePreTag(codeBlock as unknown as string); assert.equal(pre.classList.contains('astro-code'), true); assert.equal(pre.classList.contains('github-dark'), true); }); @@ -94,10 +93,14 @@ const hello = "yes"; describe('prism', () => { it('transforms', async () => { const ast = Markdoc.parse(entry); - const config = await setupConfig({ - extends: [prism()], - }); - const content = await Markdoc.transform(ast, config); + // @ts-expect-error AstroMarkdocConfig is incompatible with Markdoc.Config + const config: Markdoc.Config = await setupConfig( + { + extends: [prism()], + }, + undefined, + ); + const content = (await Markdoc.transform(ast, config)) as Tag; assert.equal(content.children.length, 2); const [tsBlock, cssBlock] = content.children; @@ -105,30 +108,25 @@ const hello = "yes"; assert.equal(isHTMLString(tsBlock), true); assert.equal(isHTMLString(cssBlock), true); - const preTs = parsePreTag(tsBlock); + const preTs = parsePreTag(tsBlock as string); assert.equal(preTs.classList.contains('language-ts'), true); - const preCss = parsePreTag(cssBlock); + const preCss = parsePreTag(cssBlock as string); assert.equal(preCss.classList.contains('language-css'), true); }); }); }); -/** - * @param {import('astro').ShikiConfig} config - * @returns {import('../src/config.js').AstroMarkdocConfig} - */ -async function getConfigExtendingShiki(config) { - return await setupConfig({ - extends: [shiki(config)], - }); +async function getConfigExtendingShiki(config?: Parameters[0]) { + return await setupConfig( + { + extends: [shiki(config)], + }, + undefined, + ); } -/** - * @param {string} html - * @returns {HTMLPreElement} - */ -function parsePreTag(html) { +function parsePreTag(html: string): HTMLPreElement { const { document } = parseHTML(html); const pre = document.querySelector('pre'); assert.ok(pre); diff --git a/packages/integrations/markdoc/test/variables.test.js b/packages/integrations/markdoc/test/variables.test.ts similarity index 91% rename from packages/integrations/markdoc/test/variables.test.js rename to packages/integrations/markdoc/test/variables.test.ts index 2225f19c8d86..073124fc8db2 100644 --- a/packages/integrations/markdoc/test/variables.test.js +++ b/packages/integrations/markdoc/test/variables.test.ts @@ -1,13 +1,13 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { parseHTML } from 'linkedom'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture, type DevServer } from '../../../astro/test/test-utils.js'; import markdoc from '../dist/index.js'; const root = new URL('./fixtures/variables/', import.meta.url); describe('Markdoc - Variables', () => { - let baseFixture; + let baseFixture: Fixture; before(async () => { baseFixture = await loadFixture({ @@ -17,7 +17,7 @@ describe('Markdoc - Variables', () => { }); describe('dev', () => { - let devServer; + let devServer: DevServer; before(async () => { devServer = await baseFixture.startDevServer(); diff --git a/packages/integrations/markdoc/tsconfig.test.json b/packages/integrations/markdoc/tsconfig.test.json new file mode 100644 index 000000000000..27c89c5fe7a7 --- /dev/null +++ b/packages/integrations/markdoc/tsconfig.test.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["test/**/*.ts"], + "exclude": ["test/fixtures/**"], + "compilerOptions": { + "noEmit": true, + "allowJs": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "rewriteRelativeImportExtensions": true + }, + "references": [ + { + "path": "../../astro/tsconfig.test.json" + } + ] +} diff --git a/packages/integrations/mdx/package.json b/packages/integrations/mdx/package.json index 95fe5f8d5aab..bfae15c3f5a6 100644 --- a/packages/integrations/mdx/package.json +++ b/packages/integrations/mdx/package.json @@ -31,7 +31,8 @@ "build": "astro-scripts build \"src/**/*.ts\" && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"", - "test": "astro-scripts test --timeout 70000 \"test/**/*.test.js\"" + "test": "astro-scripts test --timeout 70000 \"test/**/*.test.ts\"", + "typecheck:tests": "tsc --build tsconfig.test.json" }, "dependencies": { "@astrojs/markdown-remark": "workspace:*", diff --git a/packages/integrations/mdx/src/server.ts b/packages/integrations/mdx/src/server.ts index 0d728d7a3b53..0671fd55442f 100644 --- a/packages/integrations/mdx/src/server.ts +++ b/packages/integrations/mdx/src/server.ts @@ -3,7 +3,8 @@ import { AstroError } from 'astro/errors'; import { AstroJSX, jsx } from 'astro/jsx-runtime'; import { renderJSX } from 'astro/runtime/server/index.js'; -const slotName = (str: string) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase()); +export const slotName = (str: string) => + str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase()); // NOTE: In practice, MDX components are always tagged with `__astro_tag_component__`, so the right renderer // is used directly, and this check is not often used to return true. diff --git a/packages/integrations/mdx/src/utils.ts b/packages/integrations/mdx/src/utils.ts index 2fe729f8cad9..b2e798212d34 100644 --- a/packages/integrations/mdx/src/utils.ts +++ b/packages/integrations/mdx/src/utils.ts @@ -6,7 +6,7 @@ import type { MdxjsEsm } from 'mdast-util-mdx'; import colors from 'piccolore'; import type { PluggableList } from 'unified'; -function appendForwardSlash(path: string) { +export function appendForwardSlash(path: string) { return path.endsWith('/') ? path : path + '/'; } diff --git a/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts b/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts index 6ef792ffbcd1..c401f25e5abf 100644 --- a/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts +++ b/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts @@ -45,7 +45,7 @@ export function vitePluginMdxPostprocess(astroConfig: AstroConfig): Plugin { /** * Inject `Fragment` identifier import if not already present. */ -function injectUnderscoreFragmentImport(code: string, imports: readonly ImportSpecifier[]) { +export function injectUnderscoreFragmentImport(code: string, imports: readonly ImportSpecifier[]) { if (!isSpecifierImported(code, imports, underscoreFragmentImportRegex, 'astro/jsx-runtime')) { code += `\nimport { Fragment as _Fragment } from 'astro/jsx-runtime';`; } @@ -55,7 +55,7 @@ function injectUnderscoreFragmentImport(code: string, imports: readonly ImportSp /** * Inject MDX metadata as exports of the module. */ -function injectMetadataExports( +export function injectMetadataExports( code: string, exports: readonly ExportSpecifier[], fileInfo: FileInfo, @@ -73,7 +73,7 @@ function injectMetadataExports( * Transforms the `MDXContent` default export as `Content`, which wraps `MDXContent` and * passes additional `components` props. */ -function transformContentExport(code: string, exports: readonly ExportSpecifier[]) { +export function transformContentExport(code: string, exports: readonly ExportSpecifier[]) { if (exports.find(({ n }) => n === 'Content')) return code; // If have `export const components`, pass that as props to `Content` as fallback @@ -105,7 +105,7 @@ export default Content;`; /** * Add properties to the `Content` export. */ -function annotateContentExport( +export function annotateContentExport( code: string, id: string, ssr: boolean, @@ -139,7 +139,7 @@ function annotateContentExport( /** * Check whether the `specifierRegex` matches for an import of `source` in the `code`. */ -function isSpecifierImported( +export function isSpecifierImported( code: string, imports: readonly ImportSpecifier[], specifierRegex: RegExp, diff --git a/packages/integrations/mdx/test/css-head-mdx.test.js b/packages/integrations/mdx/test/css-head-mdx.test.ts similarity index 97% rename from packages/integrations/mdx/test/css-head-mdx.test.js rename to packages/integrations/mdx/test/css-head-mdx.test.ts index 81f1ee49170e..94ae75527b85 100644 --- a/packages/integrations/mdx/test/css-head-mdx.test.js +++ b/packages/integrations/mdx/test/css-head-mdx.test.ts @@ -3,10 +3,10 @@ import { before, describe, it } from 'node:test'; import mdx from '@astrojs/mdx'; import * as cheerio from 'cheerio'; import { parseHTML } from 'linkedom'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture } from '../../../astro/test/test-utils.js'; describe('Head injection w/ MDX', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ diff --git a/packages/astro/test/fixtures/css-inline-stylesheets-3/astro.config.mjs b/packages/integrations/mdx/test/fixtures/mdx-astro-container-escape/astro.config.mjs similarity index 58% rename from packages/astro/test/fixtures/css-inline-stylesheets-3/astro.config.mjs rename to packages/integrations/mdx/test/fixtures/mdx-astro-container-escape/astro.config.mjs index afdd192283fa..2d0b541506a3 100644 --- a/packages/astro/test/fixtures/css-inline-stylesheets-3/astro.config.mjs +++ b/packages/integrations/mdx/test/fixtures/mdx-astro-container-escape/astro.config.mjs @@ -1,5 +1,6 @@ +import mdx from '@astrojs/mdx'; import { defineConfig } from 'astro/config'; export default defineConfig({ - + integrations: [mdx()], }); diff --git a/packages/astro/test/fixtures/core-image-svg-optimized/package.json b/packages/integrations/mdx/test/fixtures/mdx-astro-container-escape/package.json similarity index 63% rename from packages/astro/test/fixtures/core-image-svg-optimized/package.json rename to packages/integrations/mdx/test/fixtures/mdx-astro-container-escape/package.json index 4557b402af29..a7ec46b27d66 100644 --- a/packages/astro/test/fixtures/core-image-svg-optimized/package.json +++ b/packages/integrations/mdx/test/fixtures/mdx-astro-container-escape/package.json @@ -1,8 +1,9 @@ { - "name": "@test/core-image-svg-optimized", + "name": "@test/mdx-astro-container-escape", "version": "0.0.0", "private": true, "dependencies": { + "@astrojs/mdx": "workspace:*", "astro": "workspace:*" }, "scripts": { diff --git a/packages/integrations/mdx/test/fixtures/mdx-astro-container-escape/src/components/Div.astro b/packages/integrations/mdx/test/fixtures/mdx-astro-container-escape/src/components/Div.astro new file mode 100644 index 000000000000..61945625ae84 --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/mdx-astro-container-escape/src/components/Div.astro @@ -0,0 +1 @@ +
    diff --git a/packages/integrations/mdx/test/fixtures/mdx-astro-container-escape/src/pages/index.astro b/packages/integrations/mdx/test/fixtures/mdx-astro-container-escape/src/pages/index.astro new file mode 100644 index 000000000000..ad97478445be --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/mdx-astro-container-escape/src/pages/index.astro @@ -0,0 +1,12 @@ +--- +import { experimental_AstroContainer } from "astro/container"; +import { loadRenderers } from "astro:container"; +import { getContainerRenderer } from "@astrojs/mdx"; +import { Content } from '../posts/post.mdx' + +const renderers = await loadRenderers([getContainerRenderer()]); +const contentContainer = await experimental_AstroContainer.create({ renderers }); +const html = await contentContainer.renderToString(Content); +--- + + diff --git a/packages/integrations/mdx/test/fixtures/mdx-astro-container-escape/src/posts/post.mdx b/packages/integrations/mdx/test/fixtures/mdx-astro-container-escape/src/posts/post.mdx new file mode 100644 index 000000000000..33ebb46a05d6 --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/mdx-astro-container-escape/src/posts/post.mdx @@ -0,0 +1,9 @@ +--- +title: Example +--- + +import Div from '../components/Div.astro' + +
    + Hello, World! +
    diff --git a/packages/integrations/mdx/test/fixtures/mdx-component/src/components/Test.mdx b/packages/integrations/mdx/test/fixtures/mdx-basics/src/components/component/Test.mdx similarity index 100% rename from packages/integrations/mdx/test/fixtures/mdx-component/src/components/Test.mdx rename to packages/integrations/mdx/test/fixtures/mdx-basics/src/components/component/Test.mdx diff --git a/packages/integrations/mdx/test/fixtures/mdx-component/src/components/WithFragment.mdx b/packages/integrations/mdx/test/fixtures/mdx-basics/src/components/component/WithFragment.mdx similarity index 100% rename from packages/integrations/mdx/test/fixtures/mdx-component/src/components/WithFragment.mdx rename to packages/integrations/mdx/test/fixtures/mdx-basics/src/components/component/WithFragment.mdx diff --git a/packages/integrations/mdx/test/fixtures/mdx-slots/src/components/Slotted.astro b/packages/integrations/mdx/test/fixtures/mdx-basics/src/components/slots/Slotted.astro similarity index 100% rename from packages/integrations/mdx/test/fixtures/mdx-slots/src/components/Slotted.astro rename to packages/integrations/mdx/test/fixtures/mdx-basics/src/components/slots/Slotted.astro diff --git a/packages/integrations/mdx/test/fixtures/mdx-slots/src/components/Test.mdx b/packages/integrations/mdx/test/fixtures/mdx-basics/src/components/slots/Test.mdx similarity index 100% rename from packages/integrations/mdx/test/fixtures/mdx-slots/src/components/Test.mdx rename to packages/integrations/mdx/test/fixtures/mdx-basics/src/components/slots/Test.mdx diff --git a/packages/integrations/mdx/test/fixtures/mdx-get-static-paths/src/content/1.mdx b/packages/integrations/mdx/test/fixtures/mdx-basics/src/content/1.mdx similarity index 100% rename from packages/integrations/mdx/test/fixtures/mdx-get-static-paths/src/content/1.mdx rename to packages/integrations/mdx/test/fixtures/mdx-basics/src/content/1.mdx diff --git a/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/layouts/Base.astro b/packages/integrations/mdx/test/fixtures/mdx-basics/src/layouts/Base.astro similarity index 100% rename from packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/layouts/Base.astro rename to packages/integrations/mdx/test/fixtures/mdx-basics/src/layouts/Base.astro diff --git a/packages/integrations/mdx/test/fixtures/mdx-component/src/pages/glob.astro b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/component/glob.astro similarity index 76% rename from packages/integrations/mdx/test/fixtures/mdx-component/src/pages/glob.astro rename to packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/component/glob.astro index b18f65fd383a..62e96523db49 100644 --- a/packages/integrations/mdx/test/fixtures/mdx-component/src/pages/glob.astro +++ b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/component/glob.astro @@ -1,6 +1,6 @@ --- import { parse } from 'node:path'; -const components = Object.values(import.meta.glob('../components/*.mdx', { eager: true })); +const components = Object.values(import.meta.glob('../../components/component/*.mdx', { eager: true })); ---
    diff --git a/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/component/index.astro b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/component/index.astro new file mode 100644 index 000000000000..b91c608eb5d4 --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/component/index.astro @@ -0,0 +1,5 @@ +--- +import Test from '../../components/component/Test.mdx'; +--- + + diff --git a/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/component/w-fragment.astro b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/component/w-fragment.astro new file mode 100644 index 000000000000..3a8ca98240be --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/component/w-fragment.astro @@ -0,0 +1,5 @@ +--- +import WithFragment from '../../components/component/WithFragment.mdx'; +--- + + diff --git a/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/glob.json.js b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/frontmatter/glob.json.js similarity index 100% rename from packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/glob.json.js rename to packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/frontmatter/glob.json.js diff --git a/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/index.mdx b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/frontmatter/index.mdx similarity index 87% rename from packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/index.mdx rename to packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/frontmatter/index.mdx index e6f9c8f4a689..a5f12f5af94d 100644 --- a/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/index.mdx +++ b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/frontmatter/index.mdx @@ -1,6 +1,6 @@ --- title: 'Using YAML frontmatter' -layout: '../layouts/Base.astro' +layout: '../../layouts/Base.astro' illThrowIfIDontExist: "Oh no, that's scary!" --- diff --git a/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/with-headings.mdx b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/frontmatter/with-headings.mdx similarity index 50% rename from packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/with-headings.mdx rename to packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/frontmatter/with-headings.mdx index cc4db9582f83..9fa414968938 100644 --- a/packages/integrations/mdx/test/fixtures/mdx-frontmatter/src/pages/with-headings.mdx +++ b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/frontmatter/with-headings.mdx @@ -1,5 +1,5 @@ --- -layout: '../layouts/Base.astro' +layout: '../../layouts/Base.astro' --- ## Section 1 diff --git a/packages/integrations/mdx/test/fixtures/mdx-script-style-raw/src/pages/index.mdx b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/script-style-raw.mdx similarity index 100% rename from packages/integrations/mdx/test/fixtures/mdx-script-style-raw/src/pages/index.mdx rename to packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/script-style-raw.mdx diff --git a/packages/integrations/mdx/test/fixtures/mdx-slots/src/pages/glob.astro b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/slots/glob.astro similarity index 63% rename from packages/integrations/mdx/test/fixtures/mdx-slots/src/pages/glob.astro rename to packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/slots/glob.astro index 2bd8e613c113..74a9f043d5bb 100644 --- a/packages/integrations/mdx/test/fixtures/mdx-slots/src/pages/glob.astro +++ b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/slots/glob.astro @@ -1,5 +1,5 @@ --- -const components = Object.values(import.meta.glob('../components/*.mdx', { eager: true })); +const components = Object.values(import.meta.glob('../../components/slots/*.mdx', { eager: true })); ---
    diff --git a/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/slots/index.astro b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/slots/index.astro new file mode 100644 index 000000000000..0817e6a673aa --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/slots/index.astro @@ -0,0 +1,5 @@ +--- +import Test from '../../components/slots/Test.mdx'; +--- + + diff --git a/packages/integrations/mdx/test/fixtures/mdx-get-static-paths/src/pages/[slug].astro b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/static-paths/[slug].astro similarity index 87% rename from packages/integrations/mdx/test/fixtures/mdx-get-static-paths/src/pages/[slug].astro rename to packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/static-paths/[slug].astro index 01fc3a2573f6..4e5e6b464ec6 100644 --- a/packages/integrations/mdx/test/fixtures/mdx-get-static-paths/src/pages/[slug].astro +++ b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/static-paths/[slug].astro @@ -1,6 +1,6 @@ --- export const getStaticPaths = async () => { - const content = Object.values(import.meta.glob('../content/*.mdx', { eager: true })); + const content = Object.values(import.meta.glob('../../content/*.mdx', { eager: true })); return content .filter((page) => !page.frontmatter.draft) // skip drafts diff --git a/packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/pages.json.js b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/url-export/pages.json.js similarity index 100% rename from packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/pages.json.js rename to packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/url-export/pages.json.js diff --git a/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/url-export/test-1.mdx b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/url-export/test-1.mdx new file mode 100644 index 000000000000..68ac2a064e03 --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/url-export/test-1.mdx @@ -0,0 +1 @@ +# I'm a page with a url of "/url-export/test-1!" diff --git a/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/url-export/test-2.mdx b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/url-export/test-2.mdx new file mode 100644 index 000000000000..745ffee0d953 --- /dev/null +++ b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/url-export/test-2.mdx @@ -0,0 +1 @@ +# I'm a page with a url of "/url-export/test-2!" diff --git a/packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/with-url-override.mdx b/packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/url-export/with-url-override.mdx similarity index 100% rename from packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/with-url-override.mdx rename to packages/integrations/mdx/test/fixtures/mdx-basics/src/pages/url-export/with-url-override.mdx diff --git a/packages/integrations/mdx/test/fixtures/mdx-component/src/pages/index.astro b/packages/integrations/mdx/test/fixtures/mdx-component/src/pages/index.astro deleted file mode 100644 index ed5ae98a3487..000000000000 --- a/packages/integrations/mdx/test/fixtures/mdx-component/src/pages/index.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import Test from '../components/Test.mdx'; ---- - - diff --git a/packages/integrations/mdx/test/fixtures/mdx-component/src/pages/w-fragment.astro b/packages/integrations/mdx/test/fixtures/mdx-component/src/pages/w-fragment.astro deleted file mode 100644 index d394413f0903..000000000000 --- a/packages/integrations/mdx/test/fixtures/mdx-component/src/pages/w-fragment.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import WithFragment from '../components/WithFragment.mdx'; ---- - - diff --git a/packages/integrations/mdx/test/fixtures/mdx-escape/src/components/Em.astro b/packages/integrations/mdx/test/fixtures/mdx-escape/src/components/Em.astro deleted file mode 100644 index 8166c0586b60..000000000000 --- a/packages/integrations/mdx/test/fixtures/mdx-escape/src/components/Em.astro +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/packages/integrations/mdx/test/fixtures/mdx-escape/src/components/P.astro b/packages/integrations/mdx/test/fixtures/mdx-escape/src/components/P.astro deleted file mode 100644 index e29ac6d8f0d3..000000000000 --- a/packages/integrations/mdx/test/fixtures/mdx-escape/src/components/P.astro +++ /dev/null @@ -1 +0,0 @@ -

    diff --git a/packages/integrations/mdx/test/fixtures/mdx-escape/src/components/Title.astro b/packages/integrations/mdx/test/fixtures/mdx-escape/src/components/Title.astro deleted file mode 100644 index 333ec04a2c3f..000000000000 --- a/packages/integrations/mdx/test/fixtures/mdx-escape/src/components/Title.astro +++ /dev/null @@ -1 +0,0 @@ -

    diff --git a/packages/integrations/mdx/test/fixtures/mdx-escape/src/pages/html-tag.mdx b/packages/integrations/mdx/test/fixtures/mdx-escape/src/pages/html-tag.mdx deleted file mode 100644 index e668c0dc7b1e..000000000000 --- a/packages/integrations/mdx/test/fixtures/mdx-escape/src/pages/html-tag.mdx +++ /dev/null @@ -1,5 +0,0 @@ -import P from '../components/P.astro'; -import Em from '../components/Em.astro'; - -

    Render Me

    -

    Me

    diff --git a/packages/integrations/mdx/test/fixtures/mdx-escape/src/pages/index.mdx b/packages/integrations/mdx/test/fixtures/mdx-escape/src/pages/index.mdx deleted file mode 100644 index d1c6cec9d9ec..000000000000 --- a/packages/integrations/mdx/test/fixtures/mdx-escape/src/pages/index.mdx +++ /dev/null @@ -1,13 +0,0 @@ -import P from '../components/P.astro'; -import Em from '../components/Em.astro'; -import Title from '../components/Title.astro'; - -export const components = { p: P, em: Em, h1: Title }; - -# Hello _there_ - -# _there_ - -Hello _there_ - -_there_ diff --git a/packages/integrations/mdx/test/fixtures/mdx-slots/src/pages/index.astro b/packages/integrations/mdx/test/fixtures/mdx-slots/src/pages/index.astro deleted file mode 100644 index ed5ae98a3487..000000000000 --- a/packages/integrations/mdx/test/fixtures/mdx-slots/src/pages/index.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import Test from '../components/Test.mdx'; ---- - - diff --git a/packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/test-1.mdx b/packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/test-1.mdx deleted file mode 100644 index c9b984787ff2..000000000000 --- a/packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/test-1.mdx +++ /dev/null @@ -1 +0,0 @@ -# I'm a page with a url of "/test-1!" diff --git a/packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/test-2.mdx b/packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/test-2.mdx deleted file mode 100644 index 360f72fc351a..000000000000 --- a/packages/integrations/mdx/test/fixtures/mdx-url-export/src/pages/test-2.mdx +++ /dev/null @@ -1 +0,0 @@ -# I'm a page with a url of "/test-2!" diff --git a/packages/integrations/mdx/test/invalid-mdx-component.test.js b/packages/integrations/mdx/test/invalid-mdx-component.test.ts similarity index 76% rename from packages/integrations/mdx/test/invalid-mdx-component.test.js rename to packages/integrations/mdx/test/invalid-mdx-component.test.ts index b8152e89c718..e6bfb248576c 100644 --- a/packages/integrations/mdx/test/invalid-mdx-component.test.js +++ b/packages/integrations/mdx/test/invalid-mdx-component.test.ts @@ -1,12 +1,12 @@ import * as assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture } from '../../../astro/test/test-utils.js'; import mdx from '../dist/index.js'; const FIXTURE_ROOT = new URL('./fixtures/invalid-mdx-component/', import.meta.url); describe('MDX component with runtime error', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -16,22 +16,21 @@ describe('MDX component with runtime error', () => { }); describe('build', () => { - /** @type {Error | null} */ - let error; + let error: Error | null = null; before(async () => { error = null; try { await fixture.build(); } catch (e) { - error = e; + error = e as Error; } }); it('Throws the right error', async () => { assert.ok(error); assert.match( - error?.hint, + (error as Error & { hint?: string })?.hint ?? '', /This issue often occurs when your MDX component encounters runtime errors/, ); }); diff --git a/packages/integrations/mdx/test/mdx-astro-container-escape.test.ts b/packages/integrations/mdx/test/mdx-astro-container-escape.test.ts new file mode 100644 index 000000000000..106a10cdac79 --- /dev/null +++ b/packages/integrations/mdx/test/mdx-astro-container-escape.test.ts @@ -0,0 +1,27 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import * as cheerio from 'cheerio'; +import { loadFixture, type Fixture } from '../../../astro/test/test-utils.js'; + +describe('MDX Component & Astro Container escape issue', () => { + let fixture: Fixture; + + before(async () => { + fixture = await loadFixture({ + root: new URL('./fixtures/mdx-astro-container-escape/', import.meta.url), + }); + }); + + describe('build', () => { + before(async () => { + await fixture.build(); + }); + + it('should render elements inside component without escaping', async () => { + const html = await fixture.readFile('/index.html'); + const $ = cheerio.load(html); + + assert.equal($('.div').text().includes('

    '), false); + }); + }); +}); diff --git a/packages/integrations/mdx/test/mdx-astro-markdown-remarkRehype.test.js b/packages/integrations/mdx/test/mdx-astro-markdown-remarkRehype.test.ts similarity index 79% rename from packages/integrations/mdx/test/mdx-astro-markdown-remarkRehype.test.js rename to packages/integrations/mdx/test/mdx-astro-markdown-remarkRehype.test.ts index f8cb3c98ff9a..b63c049c5252 100644 --- a/packages/integrations/mdx/test/mdx-astro-markdown-remarkRehype.test.js +++ b/packages/integrations/mdx/test/mdx-astro-markdown-remarkRehype.test.ts @@ -21,9 +21,9 @@ describe('MDX with Astro Markdown remark-rehype config', () => { const html = await fixture.readFile('/index.html'); const { document } = parseHTML(html); - assert.equal(document.querySelector('#footnote-label').textContent, 'Catatan kaki'); + assert.equal(document.querySelector('#footnote-label')!.textContent, 'Catatan kaki'); assert.equal( - document.querySelector('.data-footnote-backref').getAttribute('aria-label'), + document.querySelector('.data-footnote-backref')!.getAttribute('aria-label'), 'Kembali ke konten', ); }); @@ -50,9 +50,9 @@ describe('MDX with Astro Markdown remark-rehype config', () => { const html = await fixture.readFile('/index.html'); const { document } = parseHTML(html); - assert.equal(document.querySelector('#footnote-label').textContent, 'Catatan kaki'); + assert.equal(document.querySelector('#footnote-label')!.textContent, 'Catatan kaki'); assert.equal( - document.querySelector('.data-footnote-backref').getAttribute('aria-label'), + document.querySelector('.data-footnote-backref')!.getAttribute('aria-label'), 'Kembali ke konten', ); }); @@ -62,7 +62,6 @@ describe('MDX with Astro Markdown remark-rehype config', () => { root: new URL('./fixtures/mdx-astro-markdown-remarkRehype/', import.meta.url), integrations: [ mdx({ - extendPlugins: 'astroDefaults', remarkRehype: { footnoteLabel: 'Catatan kaki', }, @@ -79,9 +78,9 @@ describe('MDX with Astro Markdown remark-rehype config', () => { const html = await fixture.readFile('/index.html'); const { document } = parseHTML(html); - assert.equal(document.querySelector('#footnote-label').textContent, 'Catatan kaki'); + assert.equal(document.querySelector('#footnote-label')!.textContent, 'Catatan kaki'); assert.equal( - document.querySelector('.data-footnote-backref').getAttribute('aria-label'), + document.querySelector('.data-footnote-backref')!.getAttribute('aria-label'), 'Back to reference 1', ); }); diff --git a/packages/integrations/mdx/test/mdx-basics.test.ts b/packages/integrations/mdx/test/mdx-basics.test.ts new file mode 100644 index 000000000000..064c2bb0062a --- /dev/null +++ b/packages/integrations/mdx/test/mdx-basics.test.ts @@ -0,0 +1,460 @@ +import * as assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import mdx from '@astrojs/mdx'; +import * as cheerio from 'cheerio'; +import { parseHTML } from 'linkedom'; +import { loadFixture, type Fixture, type DevServer } from '../../../astro/test/test-utils.js'; + +// Merged fixture: combines mdx-component, mdx-slots, mdx-frontmatter, +// mdx-url-export, mdx-get-static-paths, and mdx-script-style-raw. +// All use the same config: integrations: [mdx()], sharing one build and one dev server. +const FIXTURE_ROOT = new URL('./fixtures/mdx-basics/', import.meta.url); + +describe('MDX basics (merged fixture)', () => { + let fixture: Fixture; + + before(async () => { + fixture = await loadFixture({ + root: FIXTURE_ROOT, + integrations: [mdx()], + }); + }); + + describe('build', () => { + before(async () => { + await fixture.build(); + }); + + // --- MDX Component tests (was mdx-component.test.js) --- + + describe('component', () => { + it('supports top-level imports', async () => { + const html = await fixture.readFile('/component/index.html'); + const { document } = parseHTML(html); + + const h1 = document.querySelector('h1')!; + const foo = document.querySelector('#foo')!; + + assert.equal(h1.textContent, 'Hello component!'); + assert.equal(foo.textContent, 'bar'); + }); + + it('supports glob imports - ', async () => { + const html = await fixture.readFile('/component/glob/index.html'); + const { document } = parseHTML(html); + + const h1 = document.querySelector('[data-default-export] h1')!; + const foo = document.querySelector('[data-default-export] #foo')!; + + assert.equal(h1.textContent, 'Hello component!'); + assert.equal(foo.textContent, 'bar'); + }); + + it('supports glob imports - ', async () => { + const html = await fixture.readFile('/component/glob/index.html'); + const { document } = parseHTML(html); + + const h1 = document.querySelector('[data-content-export] h1')!; + const foo = document.querySelector('[data-content-export] #foo')!; + + assert.equal(h1.textContent, 'Hello component!'); + assert.equal(foo.textContent, 'bar'); + }); + + describe('with ', () => { + it('supports top-level imports', async () => { + const html = await fixture.readFile('/component/w-fragment/index.html'); + const { document } = parseHTML(html); + + const h1 = document.querySelector('h1')!; + const p = document.querySelector('p')!; + + assert.equal(h1.textContent, 'MDX containing '); + assert.equal(p.textContent, 'bar'); + }); + + it('supports glob imports - ', async () => { + const html = await fixture.readFile('/component/glob/index.html'); + const { document } = parseHTML(html); + + const h = document.querySelector( + '[data-default-export] [data-file="WithFragment.mdx"] h1', + )!; + const p = document.querySelector( + '[data-default-export] [data-file="WithFragment.mdx"] p', + )!; + + assert.equal(h.textContent, 'MDX containing '); + assert.equal(p.textContent, 'bar'); + }); + + it('supports glob imports - ', async () => { + const html = await fixture.readFile('/component/glob/index.html'); + const { document } = parseHTML(html); + + const h = document.querySelector( + '[data-content-export] [data-file="WithFragment.mdx"] h1', + )!; + const p = document.querySelector( + '[data-content-export] [data-file="WithFragment.mdx"] p', + )!; + + assert.equal(h.textContent, 'MDX containing '); + assert.equal(p.textContent, 'bar'); + }); + }); + }); + + // --- MDX Slots tests (was mdx-slots.test.js) --- + + describe('slots', () => { + it('supports top-level imports', async () => { + const html = await fixture.readFile('/slots/index.html'); + const { document } = parseHTML(html); + + const h1 = document.querySelector('h1')!; + const defaultSlot = document.querySelector('[data-default-slot]')!; + const namedSlot = document.querySelector('[data-named-slot]')!; + + assert.equal(h1.textContent, 'Hello slotted component!'); + assert.equal(defaultSlot.textContent, 'Default content.'); + assert.equal(namedSlot.textContent, 'Content for named slot.'); + }); + + it('supports glob imports - ', async () => { + const html = await fixture.readFile('/slots/glob/index.html'); + const { document } = parseHTML(html); + + const h1 = document.querySelector('[data-default-export] h1')!; + const defaultSlot = document.querySelector('[data-default-export] [data-default-slot]')!; + const namedSlot = document.querySelector('[data-default-export] [data-named-slot]')!; + + assert.equal(h1.textContent, 'Hello slotted component!'); + assert.equal(defaultSlot.textContent, 'Default content.'); + assert.equal(namedSlot.textContent, 'Content for named slot.'); + }); + + it('supports glob imports - ', async () => { + const html = await fixture.readFile('/slots/glob/index.html'); + const { document } = parseHTML(html); + + const h1 = document.querySelector('[data-content-export] h1')!; + const defaultSlot = document.querySelector('[data-content-export] [data-default-slot]')!; + const namedSlot = document.querySelector('[data-content-export] [data-named-slot]')!; + + assert.equal(h1.textContent, 'Hello slotted component!'); + assert.equal(defaultSlot.textContent, 'Default content.'); + assert.equal(namedSlot.textContent, 'Content for named slot.'); + }); + }); + + // --- MDX Frontmatter tests (was mdx-frontmatter.test.js) --- + + describe('frontmatter', () => { + it('builds when "frontmatter.property" is in JSX expression', async () => { + assert.equal(true, true); + }); + + it('extracts frontmatter to "frontmatter" export', async () => { + const { titles } = JSON.parse(await fixture.readFile('/frontmatter/glob.json')); + assert.equal(titles.includes('Using YAML frontmatter'), true); + }); + + it('renders layout from "layout" frontmatter property', async () => { + const html = await fixture.readFile('/frontmatter/index.html'); + const { document } = parseHTML(html); + + const layoutParagraph = document.querySelector('[data-layout-rendered]'); + + assert.notEqual(layoutParagraph, null); + }); + + it('passes frontmatter to layout via "content" and "frontmatter" props', async () => { + const html = await fixture.readFile('/frontmatter/index.html'); + const { document } = parseHTML(html); + + const contentTitle = document.querySelector('[data-content-title]')!; + const frontmatterTitle = document.querySelector('[data-frontmatter-title]')!; + + assert.equal(contentTitle.textContent, 'Using YAML frontmatter'); + assert.equal(frontmatterTitle.textContent, 'Using YAML frontmatter'); + }); + + it('passes headings to layout via "headings" prop', async () => { + const html = await fixture.readFile('/frontmatter/with-headings/index.html'); + const { document } = parseHTML(html); + + const headingSlugs = [...document.querySelectorAll('[data-headings] > li')].map( + (el) => el.textContent, + ); + + assert.equal(headingSlugs.length > 0, true); + assert.equal(headingSlugs.includes('section-1'), true); + assert.equal(headingSlugs.includes('section-2'), true); + }); + + it('passes "file" and "url" to layout', async () => { + const html = await fixture.readFile('/frontmatter/with-headings/index.html'); + const { document } = parseHTML(html); + + const frontmatterFile = document.querySelector('[data-frontmatter-file]')?.textContent; + const frontmatterUrl = document.querySelector('[data-frontmatter-url]')?.textContent; + const file = document.querySelector('[data-file]')?.textContent; + const url = document.querySelector('[data-url]')?.textContent; + + assert.equal( + frontmatterFile?.endsWith('with-headings.mdx'), + true, + '"file" prop does not end with correct path or is undefined', + ); + assert.equal(frontmatterUrl, '/frontmatter/with-headings'); + assert.equal(file, frontmatterFile); + assert.equal(url, frontmatterUrl); + }); + }); + + // --- MDX URL Export tests (was mdx-url-export.test.js) --- + + describe('url export', () => { + it('generates correct urls in glob result', async () => { + const { urls } = JSON.parse(await fixture.readFile('/url-export/pages.json')); + assert.equal(urls.includes('/url-export/test-1'), true); + assert.equal(urls.includes('/url-export/test-2'), true); + }); + + it('respects "export url" overrides in glob result', async () => { + const { urls } = JSON.parse(await fixture.readFile('/url-export/pages.json')); + assert.equal(urls.includes('/AH!'), true); + }); + }); + + // --- getStaticPaths tests (was mdx-get-static-paths.test.js) --- + + describe('getStaticPaths', () => { + it('Provides file and url', async () => { + const html = await fixture.readFile('/static-paths/one/index.html'); + + const $ = cheerio.load(html); + assert.equal($('p').text(), 'First mdx file'); + assert.equal($('#one').text(), 'hello', 'Frontmatter included'); + assert.equal($('#url').text(), 'src/content/1.mdx', 'url is included'); + assert.equal( + $('#file').text().includes('fixtures/mdx-basics/src/content/1.mdx'), + true, + 'file is included', + ); + }); + }); + + // --- MDX script/style raw tests (was mdx-script-style-raw.test.js, build part) --- + + describe('script-style-raw', () => { + it('works with raw script and style strings', async () => { + const html = await fixture.readFile('/script-style-raw/index.html'); + const { document } = parseHTML(html); + + const scriptContent = document.getElementById('test-script')!.innerHTML; + assert.equal( + scriptContent.includes("console.log('raw script')"), + true, + 'script should not be html-escaped', + ); + + const styleContent = document.getElementById('test-style')!.innerHTML; + assert.equal( + styleContent.includes('h1[id="script-style-raw"]'), + true, + 'style should not be html-escaped', + ); + }); + }); + }); + + describe('dev', () => { + let devServer: DevServer; + + before(async () => { + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + // --- MDX Component dev tests --- + + describe('component', () => { + it('supports top-level imports', async () => { + const res = await fixture.fetch('/component'); + assert.equal(res.status, 200); + + const html = await res.text(); + const { document } = parseHTML(html); + + const h1 = document.querySelector('h1')!; + const foo = document.querySelector('#foo')!; + + assert.equal(h1.textContent, 'Hello component!'); + assert.equal(foo.textContent, 'bar'); + }); + + it('supports glob imports - ', async () => { + const res = await fixture.fetch('/component/glob'); + assert.equal(res.status, 200); + + const html = await res.text(); + const { document } = parseHTML(html); + + const h1 = document.querySelector('[data-default-export] h1')!; + const foo = document.querySelector('[data-default-export] #foo')!; + + assert.equal(h1.textContent, 'Hello component!'); + assert.equal(foo.textContent, 'bar'); + }); + + it('supports glob imports - ', async () => { + const res = await fixture.fetch('/component/glob'); + assert.equal(res.status, 200); + + const html = await res.text(); + const { document } = parseHTML(html); + + const h1 = document.querySelector('[data-content-export] h1')!; + const foo = document.querySelector('[data-content-export] #foo')!; + + assert.equal(h1.textContent, 'Hello component!'); + assert.equal(foo.textContent, 'bar'); + }); + + describe('with ', () => { + it('supports top-level imports', async () => { + const res = await fixture.fetch('/component/w-fragment'); + assert.equal(res.status, 200); + + const html = await res.text(); + const { document } = parseHTML(html); + + const h1 = document.querySelector('h1')!; + const p = document.querySelector('p')!; + + assert.equal(h1.textContent, 'MDX containing '); + assert.equal(p.textContent, 'bar'); + }); + + it('supports glob imports - ', async () => { + const res = await fixture.fetch('/component/glob'); + assert.equal(res.status, 200); + + const html = await res.text(); + const { document } = parseHTML(html); + + const h = document.querySelector( + '[data-default-export] [data-file="WithFragment.mdx"] h1', + )!; + const p = document.querySelector( + '[data-default-export] [data-file="WithFragment.mdx"] p', + )!; + + assert.equal(h.textContent, 'MDX containing '); + assert.equal(p.textContent, 'bar'); + }); + + it('supports glob imports - ', async () => { + const res = await fixture.fetch('/component/glob'); + assert.equal(res.status, 200); + + const html = await res.text(); + const { document } = parseHTML(html); + + const h = document.querySelector( + '[data-content-export] [data-file="WithFragment.mdx"] h1', + )!; + const p = document.querySelector( + '[data-content-export] [data-file="WithFragment.mdx"] p', + )!; + + assert.equal(h.textContent, 'MDX containing '); + assert.equal(p.textContent, 'bar'); + }); + }); + }); + + // --- MDX Slots dev tests --- + + describe('slots', () => { + it('supports top-level imports', async () => { + const res = await fixture.fetch('/slots'); + assert.equal(res.status, 200); + + const html = await res.text(); + const { document } = parseHTML(html); + + const h1 = document.querySelector('h1')!; + const defaultSlot = document.querySelector('[data-default-slot]')!; + const namedSlot = document.querySelector('[data-named-slot]')!; + + assert.equal(h1.textContent, 'Hello slotted component!'); + assert.equal(defaultSlot.textContent, 'Default content.'); + assert.equal(namedSlot.textContent, 'Content for named slot.'); + }); + + it('supports glob imports - ', async () => { + const res = await fixture.fetch('/slots/glob'); + assert.equal(res.status, 200); + + const html = await res.text(); + const { document } = parseHTML(html); + + const h1 = document.querySelector('[data-default-export] h1')!; + const defaultSlot = document.querySelector('[data-default-export] [data-default-slot]')!; + const namedSlot = document.querySelector('[data-default-export] [data-named-slot]')!; + + assert.equal(h1.textContent, 'Hello slotted component!'); + assert.equal(defaultSlot.textContent, 'Default content.'); + assert.equal(namedSlot.textContent, 'Content for named slot.'); + }); + + it('supports glob imports - ', async () => { + const res = await fixture.fetch('/slots/glob'); + assert.equal(res.status, 200); + + const html = await res.text(); + const { document } = parseHTML(html); + + const h1 = document.querySelector('[data-content-export] h1')!; + const defaultSlot = document.querySelector('[data-content-export] [data-default-slot]')!; + const namedSlot = document.querySelector('[data-content-export] [data-named-slot]')!; + + assert.equal(h1.textContent, 'Hello slotted component!'); + assert.equal(defaultSlot.textContent, 'Default content.'); + assert.equal(namedSlot.textContent, 'Content for named slot.'); + }); + }); + + // --- MDX script/style raw dev tests --- + + describe('script-style-raw', () => { + it('works with raw script and style strings', async () => { + const res = await fixture.fetch('/script-style-raw'); + assert.equal(res.status, 200); + + const html = await res.text(); + const { document } = parseHTML(html); + + const scriptContent = document.getElementById('test-script')!.innerHTML; + assert.equal( + scriptContent.includes("console.log('raw script')"), + true, + 'script should not be html-escaped', + ); + + const styleContent = document.getElementById('test-style')!.innerHTML; + assert.equal( + styleContent.includes('h1[id="script-style-raw"]'), + true, + 'style should not be html-escaped', + ); + }); + }); + }); +}); diff --git a/packages/integrations/mdx/test/mdx-component.test.js b/packages/integrations/mdx/test/mdx-component.test.js deleted file mode 100644 index 895d83008244..000000000000 --- a/packages/integrations/mdx/test/mdx-component.test.js +++ /dev/null @@ -1,194 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { after, before, describe, it } from 'node:test'; -import mdx from '@astrojs/mdx'; -import { parseHTML } from 'linkedom'; -import { loadFixture } from '../../../astro/test/test-utils.js'; - -describe('MDX Component', () => { - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: new URL('./fixtures/mdx-component/', import.meta.url), - integrations: [mdx()], - }); - }); - - describe('build', () => { - before(async () => { - await fixture.build(); - }); - - it('supports top-level imports', async () => { - const html = await fixture.readFile('/index.html'); - const { document } = parseHTML(html); - - const h1 = document.querySelector('h1'); - const foo = document.querySelector('#foo'); - - assert.equal(h1.textContent, 'Hello component!'); - assert.equal(foo.textContent, 'bar'); - }); - - it('supports glob imports - ', async () => { - const html = await fixture.readFile('/glob/index.html'); - const { document } = parseHTML(html); - - const h1 = document.querySelector('[data-default-export] h1'); - const foo = document.querySelector('[data-default-export] #foo'); - - assert.equal(h1.textContent, 'Hello component!'); - assert.equal(foo.textContent, 'bar'); - }); - - it('supports glob imports - ', async () => { - const html = await fixture.readFile('/glob/index.html'); - const { document } = parseHTML(html); - - const h1 = document.querySelector('[data-content-export] h1'); - const foo = document.querySelector('[data-content-export] #foo'); - - assert.equal(h1.textContent, 'Hello component!'); - assert.equal(foo.textContent, 'bar'); - }); - - describe('with ', () => { - it('supports top-level imports', async () => { - const html = await fixture.readFile('/w-fragment/index.html'); - const { document } = parseHTML(html); - - const h1 = document.querySelector('h1'); - const p = document.querySelector('p'); - - assert.equal(h1.textContent, 'MDX containing '); - assert.equal(p.textContent, 'bar'); - }); - - it('supports glob imports - ', async () => { - const html = await fixture.readFile('/glob/index.html'); - const { document } = parseHTML(html); - - const h = document.querySelector('[data-default-export] [data-file="WithFragment.mdx"] h1'); - const p = document.querySelector('[data-default-export] [data-file="WithFragment.mdx"] p'); - - assert.equal(h.textContent, 'MDX containing '); - assert.equal(p.textContent, 'bar'); - }); - - it('supports glob imports - ', async () => { - const html = await fixture.readFile('/glob/index.html'); - const { document } = parseHTML(html); - - const h = document.querySelector('[data-content-export] [data-file="WithFragment.mdx"] h1'); - const p = document.querySelector('[data-content-export] [data-file="WithFragment.mdx"] p'); - - assert.equal(h.textContent, 'MDX containing '); - assert.equal(p.textContent, 'bar'); - }); - }); - }); - - describe('dev', () => { - let devServer; - - before(async () => { - devServer = await fixture.startDevServer(); - }); - - after(async () => { - await devServer.stop(); - }); - - it('supports top-level imports', async () => { - const res = await fixture.fetch('/'); - - assert.equal(res.status, 200); - - const html = await res.text(); - const { document } = parseHTML(html); - - const h1 = document.querySelector('h1'); - const foo = document.querySelector('#foo'); - - assert.equal(h1.textContent, 'Hello component!'); - assert.equal(foo.textContent, 'bar'); - }); - - it('supports glob imports - ', async () => { - const res = await fixture.fetch('/glob'); - - assert.equal(res.status, 200); - - const html = await res.text(); - const { document } = parseHTML(html); - - const h1 = document.querySelector('[data-default-export] h1'); - const foo = document.querySelector('[data-default-export] #foo'); - - assert.equal(h1.textContent, 'Hello component!'); - assert.equal(foo.textContent, 'bar'); - }); - - it('supports glob imports - ', async () => { - const res = await fixture.fetch('/glob'); - - assert.equal(res.status, 200); - - const html = await res.text(); - const { document } = parseHTML(html); - - const h1 = document.querySelector('[data-content-export] h1'); - const foo = document.querySelector('[data-content-export] #foo'); - - assert.equal(h1.textContent, 'Hello component!'); - assert.equal(foo.textContent, 'bar'); - }); - - describe('with ', () => { - it('supports top-level imports', async () => { - const res = await fixture.fetch('/w-fragment'); - - assert.equal(res.status, 200); - - const html = await res.text(); - const { document } = parseHTML(html); - - const h1 = document.querySelector('h1'); - const p = document.querySelector('p'); - - assert.equal(h1.textContent, 'MDX containing '); - assert.equal(p.textContent, 'bar'); - }); - - it('supports glob imports - ', async () => { - const res = await fixture.fetch('/glob'); - - assert.equal(res.status, 200); - - const html = await res.text(); - const { document } = parseHTML(html); - - const h = document.querySelector('[data-default-export] [data-file="WithFragment.mdx"] h1'); - const p = document.querySelector('[data-default-export] [data-file="WithFragment.mdx"] p'); - - assert.equal(h.textContent, 'MDX containing '); - assert.equal(p.textContent, 'bar'); - }); - - it('supports glob imports - ', async () => { - const res = await fixture.fetch('/glob'); - - assert.equal(res.status, 200); - - const html = await res.text(); - const { document } = parseHTML(html); - - const h = document.querySelector('[data-content-export] [data-file="WithFragment.mdx"] h1'); - const p = document.querySelector('[data-content-export] [data-file="WithFragment.mdx"] p'); - - assert.equal(h.textContent, 'MDX containing '); - assert.equal(p.textContent, 'bar'); - }); - }); - }); -}); diff --git a/packages/integrations/mdx/test/mdx-content-layer.test.js b/packages/integrations/mdx/test/mdx-content-layer.test.ts similarity index 74% rename from packages/integrations/mdx/test/mdx-content-layer.test.js rename to packages/integrations/mdx/test/mdx-content-layer.test.ts index c6998b26f725..887699750839 100644 --- a/packages/integrations/mdx/test/mdx-content-layer.test.js +++ b/packages/integrations/mdx/test/mdx-content-layer.test.ts @@ -1,12 +1,11 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture, type DevServer } from '../../../astro/test/test-utils.js'; describe('Content Layer MDX rendering dev', () => { - /** @type {import("../../../astro/test/test-utils.js").Fixture} */ - let fixture; + let fixture: Fixture; - let devServer; + let devServer: DevServer; before(async () => { fixture = await loadFixture({ root: new URL('./fixtures/content-layer/', import.meta.url), @@ -19,7 +18,7 @@ describe('Content Layer MDX rendering dev', () => { }); it('Render an MDX file', async () => { - const html = await fixture.fetch('/reptiles/iguana').then((r) => r.text()); + const html = await fixture.fetch('/reptiles/iguana').then((r: Response) => r.text()); assert.match(html, /Iguana/); assert.match(html, /This is a rendered entry/); @@ -27,8 +26,7 @@ describe('Content Layer MDX rendering dev', () => { }); describe('Content Layer MDX rendering build', () => { - /** @type {import("../../../astro/test/test-utils.js").Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: new URL('./fixtures/content-layer/', import.meta.url), diff --git a/packages/integrations/mdx/test/mdx-escape.test.js b/packages/integrations/mdx/test/mdx-escape.test.js deleted file mode 100644 index 9770128384d1..000000000000 --- a/packages/integrations/mdx/test/mdx-escape.test.js +++ /dev/null @@ -1,32 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { before, describe, it } from 'node:test'; -import mdx from '@astrojs/mdx'; -import { parseHTML } from 'linkedom'; -import { loadFixture } from '../../../astro/test/test-utils.js'; - -const FIXTURE_ROOT = new URL('./fixtures/mdx-escape/', import.meta.url); - -describe('MDX frontmatter', () => { - let fixture; - before(async () => { - fixture = await loadFixture({ - root: FIXTURE_ROOT, - integrations: [mdx()], - }); - await fixture.build(); - }); - - it('does not have unescaped HTML at top-level', async () => { - const html = await fixture.readFile('/index.html'); - const { document } = parseHTML(html); - - assert.equal(document.body.textContent.includes(' { - const html = await fixture.readFile('/html-tag/index.html'); - const { document } = parseHTML(html); - - assert.equal(document.body.textContent.includes(' { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -15,28 +22,30 @@ describe('MDX frontmatter injection', () => { await fixture.build(); }); + const readFrontmatterByPage = async (): Promise => { + return JSON.parse(await fixture.readFile('/glob.json')) as FrontmatterEntry[]; + }; + it('remark supports custom vfile data - get title', async () => { - const frontmatterByPage = JSON.parse(await fixture.readFile('/glob.json')); - const titles = frontmatterByPage.map((frontmatter = {}) => frontmatter.title); + const frontmatterByPage = await readFrontmatterByPage(); + const titles = frontmatterByPage.map((frontmatter) => frontmatter.title); assert.equal(titles.includes('Page 1'), true); assert.equal(titles.includes('Page 2'), true); }); it('rehype supports custom vfile data - reading time', async () => { - const frontmatterByPage = JSON.parse(await fixture.readFile('/glob.json')); - const readingTimes = frontmatterByPage.map( - (frontmatter = {}) => frontmatter.injectedReadingTime, - ); + const frontmatterByPage = await readFrontmatterByPage(); + const readingTimes = frontmatterByPage.map((frontmatter) => frontmatter.injectedReadingTime); assert.equal(readingTimes.length > 0, true); - for (let readingTime of readingTimes) { + for (const readingTime of readingTimes) { assert.notEqual(readingTime, null); - assert.match(readingTime.text, /^\d+ min read/); + assert.match(readingTime!.text, /^\d+ min read/); } }); it('allow user frontmatter mutation', async () => { - const frontmatterByPage = JSON.parse(await fixture.readFile('/glob.json')); - const descriptions = frontmatterByPage.map((frontmatter = {}) => frontmatter.description); + const frontmatterByPage = await readFrontmatterByPage(); + const descriptions = frontmatterByPage.map((frontmatter) => frontmatter.description); assert.equal( descriptions.includes('Processed by remarkDescription plugin: Page 1 description'), true, @@ -51,8 +60,8 @@ describe('MDX frontmatter injection', () => { const html1 = await fixture.readFile('/page-1/index.html'); const html2 = await fixture.readFile('/page-2/index.html'); - const title1 = parseHTML(html1).document.querySelector('title'); - const title2 = parseHTML(html2).document.querySelector('title'); + const title1 = parseHTML(html1).document.querySelector('title')!; + const title2 = parseHTML(html2).document.querySelector('title')!; assert.equal(title1.innerHTML, 'Page 1'); assert.equal(title2.innerHTML, 'Page 2'); diff --git a/packages/integrations/mdx/test/mdx-frontmatter.test.js b/packages/integrations/mdx/test/mdx-frontmatter.test.js deleted file mode 100644 index 5f7398bac800..000000000000 --- a/packages/integrations/mdx/test/mdx-frontmatter.test.js +++ /dev/null @@ -1,78 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { before, describe, it } from 'node:test'; -import mdx from '@astrojs/mdx'; -import { parseHTML } from 'linkedom'; -import { loadFixture } from '../../../astro/test/test-utils.js'; - -const FIXTURE_ROOT = new URL('./fixtures/mdx-frontmatter/', import.meta.url); - -describe('MDX frontmatter', () => { - let fixture; - before(async () => { - fixture = await loadFixture({ - root: FIXTURE_ROOT, - integrations: [mdx()], - }); - await fixture.build(); - }); - it('builds when "frontmatter.property" is in JSX expression', async () => { - assert.equal(true, true); - }); - - it('extracts frontmatter to "frontmatter" export', async () => { - const { titles } = JSON.parse(await fixture.readFile('/glob.json')); - assert.equal(titles.includes('Using YAML frontmatter'), true); - }); - - it('renders layout from "layout" frontmatter property', async () => { - const html = await fixture.readFile('/index.html'); - const { document } = parseHTML(html); - - const layoutParagraph = document.querySelector('[data-layout-rendered]'); - - assert.notEqual(layoutParagraph, null); - }); - - it('passes frontmatter to layout via "content" and "frontmatter" props', async () => { - const html = await fixture.readFile('/index.html'); - const { document } = parseHTML(html); - - const contentTitle = document.querySelector('[data-content-title]'); - const frontmatterTitle = document.querySelector('[data-frontmatter-title]'); - - assert.equal(contentTitle.textContent, 'Using YAML frontmatter'); - assert.equal(frontmatterTitle.textContent, 'Using YAML frontmatter'); - }); - - it('passes headings to layout via "headings" prop', async () => { - const html = await fixture.readFile('/with-headings/index.html'); - const { document } = parseHTML(html); - - const headingSlugs = [...document.querySelectorAll('[data-headings] > li')].map( - (el) => el.textContent, - ); - - assert.equal(headingSlugs.length > 0, true); - assert.equal(headingSlugs.includes('section-1'), true); - assert.equal(headingSlugs.includes('section-2'), true); - }); - - it('passes "file" and "url" to layout', async () => { - const html = await fixture.readFile('/with-headings/index.html'); - const { document } = parseHTML(html); - - const frontmatterFile = document.querySelector('[data-frontmatter-file]')?.textContent; - const frontmatterUrl = document.querySelector('[data-frontmatter-url]')?.textContent; - const file = document.querySelector('[data-file]')?.textContent; - const url = document.querySelector('[data-url]')?.textContent; - - assert.equal( - frontmatterFile?.endsWith('with-headings.mdx'), - true, - '"file" prop does not end with correct path or is undefined', - ); - assert.equal(frontmatterUrl, '/with-headings'); - assert.equal(file, frontmatterFile); - assert.equal(url, frontmatterUrl); - }); -}); diff --git a/packages/integrations/mdx/test/mdx-get-headings.test.js b/packages/integrations/mdx/test/mdx-get-headings.test.ts similarity index 69% rename from packages/integrations/mdx/test/mdx-get-headings.test.js rename to packages/integrations/mdx/test/mdx-get-headings.test.ts index 2776cfc7c3d6..f889c5140153 100644 --- a/packages/integrations/mdx/test/mdx-get-headings.test.js +++ b/packages/integrations/mdx/test/mdx-get-headings.test.ts @@ -4,10 +4,10 @@ import { rehypeHeadingIds } from '@astrojs/markdown-remark'; import mdx from '@astrojs/mdx'; import { parseHTML } from 'linkedom'; import { visit } from 'unist-util-visit'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture } from '../../../astro/test/test-utils.js'; describe('MDX getHeadings', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -22,9 +22,9 @@ describe('MDX getHeadings', () => { const html = await fixture.readFile('/test/index.html'); const { document } = parseHTML(html); - const h2Ids = document.querySelectorAll('h2').map((el) => el?.id); - const h3Ids = document.querySelectorAll('h3').map((el) => el?.id); - assert.equal(document.querySelector('h1').id, 'heading-test'); + const h2Ids = Array.from(document.querySelectorAll('h2')).map((el) => el?.id); + const h3Ids = Array.from(document.querySelectorAll('h3')).map((el) => el?.id); + assert.equal(document.querySelector('h1')!.id, 'heading-test'); assert.equal(h2Ids.includes('section-1'), true); assert.equal(h2Ids.includes('section-2'), true); assert.equal(h3Ids.includes('subsection-1'), true); @@ -63,10 +63,44 @@ describe('MDX getHeadings', () => { ]), ); }); + + // These tests use the same config (integrations: [mdx()]) and share the build above + describe('with frontmatter', () => { + it('adds anchor IDs to headings', async () => { + const html = await fixture.readFile('/test-with-frontmatter/index.html'); + const { document } = parseHTML(html); + + const h3Ids = Array.from(document.querySelectorAll('h3')).map((el) => el?.id); + + assert.equal(document.querySelector('h1')!.id, 'the-frontmatter-title'); + assert.equal(document.querySelector('h2')!.id, 'frontmattertitle'); + assert.equal(h3Ids.includes('keyword-2'), true); + assert.equal(h3Ids.includes('tag-1'), true); + assert.equal(document.querySelector('h4')!.id, 'item-2'); + assert.equal(document.querySelector('h5')!.id, 'nested-item-3'); + assert.equal(document.querySelector('h6')!.id, 'frontmatterunknown'); + }); + + it('generates correct getHeadings() export', async () => { + const { headingsByPage } = JSON.parse(await fixture.readFile('/pages.json')); + assert.equal( + JSON.stringify(headingsByPage['./test-with-frontmatter.mdx']), + JSON.stringify([ + { depth: 1, slug: 'the-frontmatter-title', text: 'The Frontmatter Title' }, + { depth: 2, slug: 'frontmattertitle', text: 'frontmatter.title' }, + { depth: 3, slug: 'keyword-2', text: 'Keyword 2' }, + { depth: 3, slug: 'tag-1', text: 'Tag 1' }, + { depth: 4, slug: 'item-2', text: 'Item 2' }, + { depth: 5, slug: 'nested-item-3', text: 'Nested Item 3' }, + { depth: 6, slug: 'frontmatterunknown', text: 'frontmatter.unknown' }, + ]), + ); + }); + }); }); describe('MDX heading IDs can be customized by user plugins', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -98,7 +132,7 @@ describe('MDX heading IDs can be customized by user plugins', () => { assert.equal(h1?.textContent, 'Heading test'); assert.equal(h1?.getAttribute('id'), '0'); - const headingIDs = document.querySelectorAll('h1,h2,h3').map((el) => el.id); + const headingIDs = Array.from(document.querySelectorAll('h1,h2,h3')).map((el) => el.id); assert.equal( JSON.stringify(headingIDs), JSON.stringify(Array.from({ length: headingIDs.length }, (_, idx) => String(idx))), @@ -123,7 +157,7 @@ describe('MDX heading IDs can be customized by user plugins', () => { }); describe('MDX heading IDs can be injected before user plugins', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -157,47 +191,3 @@ describe('MDX heading IDs can be injected before user plugins', () => { assert.equal(h1?.id, 'heading-test'); }); }); - -describe('MDX headings with frontmatter', () => { - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: new URL('./fixtures/mdx-get-headings/', import.meta.url), - integrations: [mdx()], - }); - - await fixture.build(); - }); - - it('adds anchor IDs to headings', async () => { - const html = await fixture.readFile('/test-with-frontmatter/index.html'); - const { document } = parseHTML(html); - - const h3Ids = document.querySelectorAll('h3').map((el) => el?.id); - - assert.equal(document.querySelector('h1').id, 'the-frontmatter-title'); - assert.equal(document.querySelector('h2').id, 'frontmattertitle'); - assert.equal(h3Ids.includes('keyword-2'), true); - assert.equal(h3Ids.includes('tag-1'), true); - assert.equal(document.querySelector('h4').id, 'item-2'); - assert.equal(document.querySelector('h5').id, 'nested-item-3'); - assert.equal(document.querySelector('h6').id, 'frontmatterunknown'); - }); - - it('generates correct getHeadings() export', async () => { - const { headingsByPage } = JSON.parse(await fixture.readFile('/pages.json')); - assert.equal( - JSON.stringify(headingsByPage['./test-with-frontmatter.mdx']), - JSON.stringify([ - { depth: 1, slug: 'the-frontmatter-title', text: 'The Frontmatter Title' }, - { depth: 2, slug: 'frontmattertitle', text: 'frontmatter.title' }, - { depth: 3, slug: 'keyword-2', text: 'Keyword 2' }, - { depth: 3, slug: 'tag-1', text: 'Tag 1' }, - { depth: 4, slug: 'item-2', text: 'Item 2' }, - { depth: 5, slug: 'nested-item-3', text: 'Nested Item 3' }, - { depth: 6, slug: 'frontmatterunknown', text: 'frontmatter.unknown' }, - ]), - ); - }); -}); diff --git a/packages/integrations/mdx/test/mdx-get-static-paths.test.js b/packages/integrations/mdx/test/mdx-get-static-paths.test.js deleted file mode 100644 index 74959ccd13bf..000000000000 --- a/packages/integrations/mdx/test/mdx-get-static-paths.test.js +++ /dev/null @@ -1,33 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { before, describe, it } from 'node:test'; -import mdx from '@astrojs/mdx'; -import * as cheerio from 'cheerio'; -import { loadFixture } from '../../../astro/test/test-utils.js'; - -const FIXTURE_ROOT = new URL('./fixtures/mdx-get-static-paths', import.meta.url); - -describe('getStaticPaths', () => { - /** @type {import('astro/test/test-utils').Fixture} */ - let fixture; - before(async () => { - fixture = await loadFixture({ - root: FIXTURE_ROOT, - integrations: [mdx()], - }); - await fixture.build(); - }); - - it('Provides file and url', async () => { - const html = await fixture.readFile('/one/index.html'); - - const $ = cheerio.load(html); - assert.equal($('p').text(), 'First mdx file'); - assert.equal($('#one').text(), 'hello', 'Frontmatter included'); - assert.equal($('#url').text(), 'src/content/1.mdx', 'url is included'); - assert.equal( - $('#file').text().includes('fixtures/mdx-get-static-paths/src/content/1.mdx'), - true, - 'file is included', - ); - }); -}); diff --git a/packages/integrations/mdx/test/mdx-images.test.js b/packages/integrations/mdx/test/mdx-images.test.ts similarity index 77% rename from packages/integrations/mdx/test/mdx-images.test.js rename to packages/integrations/mdx/test/mdx-images.test.ts index 543b9021ebe2..304dcc163fa6 100644 --- a/packages/integrations/mdx/test/mdx-images.test.js +++ b/packages/integrations/mdx/test/mdx-images.test.ts @@ -1,13 +1,13 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { parseHTML } from 'linkedom'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture, type DevServer } from '../../../astro/test/test-utils.js'; const imageTestRoutes = ['with-components', 'esm-import', 'content-collection']; describe('MDX Page', () => { - let devServer; - let fixture; + let devServer: DevServer; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -31,17 +31,17 @@ describe('MDX Page', () => { const imgs = document.getElementsByTagName('img'); assert.equal(imgs.length, 6); // Image using a relative path - assert.equal(imgs.item(0).src.startsWith('/_image'), true); + assert.equal(imgs.item(0)!.src.startsWith('/_image'), true); // Image using an aliased path - assert.equal(imgs.item(1).src.startsWith('/_image'), true); + assert.equal(imgs.item(1)!.src.startsWith('/_image'), true); // Image with title - assert.equal(imgs.item(2).title, 'Houston title'); + assert.equal(imgs.item(2)!.title, 'Houston title'); // Image with spaces in the path - assert.equal(imgs.item(3).src.startsWith('/_image'), true); + assert.equal(imgs.item(3)!.src.startsWith('/_image'), true); // Image using a relative path with no slashes - assert.equal(imgs.item(4).src.startsWith('/_image'), true); + assert.equal(imgs.item(4)!.src.startsWith('/_image'), true); // Image using a relative path with nested directory - assert.equal(imgs.item(5).src.startsWith('/_image'), true); + assert.equal(imgs.item(5)!.src.startsWith('/_image'), true); }); for (const route of imageTestRoutes) { @@ -55,11 +55,11 @@ describe('MDX Page', () => { const imgs = document.getElementsByTagName('img'); assert.equal(imgs.length, 2); - const assetsImg = imgs.item(0); + const assetsImg = imgs.item(0)!; assert.equal(assetsImg.src.startsWith('/_image'), true); assert.equal(assetsImg.hasAttribute('data-my-image'), true); - const publicImg = imgs.item(1); + const publicImg = imgs.item(1)!; assert.equal(publicImg.src, '/favicon.svg'); assert.equal(publicImg.hasAttribute('data-my-image'), true); }); diff --git a/packages/integrations/mdx/test/mdx-infinite-loop.test.js b/packages/integrations/mdx/test/mdx-infinite-loop.test.ts similarity index 82% rename from packages/integrations/mdx/test/mdx-infinite-loop.test.js rename to packages/integrations/mdx/test/mdx-infinite-loop.test.ts index a4c78fcfff1e..383c89d9a52c 100644 --- a/packages/integrations/mdx/test/mdx-infinite-loop.test.js +++ b/packages/integrations/mdx/test/mdx-infinite-loop.test.ts @@ -1,10 +1,10 @@ import * as assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import mdx from '@astrojs/mdx'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture } from '../../../astro/test/test-utils.js'; describe('MDX Infinite Loop', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -14,7 +14,7 @@ describe('MDX Infinite Loop', () => { }); describe('build', () => { - let err; + let err: unknown; before(async () => { try { await fixture.build(); diff --git a/packages/integrations/mdx/test/mdx-math.test.js b/packages/integrations/mdx/test/mdx-math.test.ts similarity index 100% rename from packages/integrations/mdx/test/mdx-math.test.js rename to packages/integrations/mdx/test/mdx-math.test.ts diff --git a/packages/integrations/mdx/test/mdx-namespace.test.js b/packages/integrations/mdx/test/mdx-namespace.test.ts similarity index 82% rename from packages/integrations/mdx/test/mdx-namespace.test.js rename to packages/integrations/mdx/test/mdx-namespace.test.ts index 13137e40e98c..3f7cd47bb375 100644 --- a/packages/integrations/mdx/test/mdx-namespace.test.js +++ b/packages/integrations/mdx/test/mdx-namespace.test.ts @@ -1,10 +1,10 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { parseHTML } from 'linkedom'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture, type DevServer } from '../../../astro/test/test-utils.js'; describe('MDX Namespace', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -22,7 +22,7 @@ describe('MDX Namespace', () => { const { document } = parseHTML(html); const island = document.querySelector('astro-island'); - const component = document.querySelector('#component'); + const component = document.querySelector('#component')!; assert.notEqual(island, undefined); assert.equal(component.textContent, 'Hello world'); @@ -33,7 +33,7 @@ describe('MDX Namespace', () => { const { document } = parseHTML(html); const island = document.querySelector('astro-island'); - const component = document.querySelector('#component'); + const component = document.querySelector('#component')!; assert.notEqual(island, undefined); assert.equal(component.textContent, 'Hello world'); @@ -41,7 +41,7 @@ describe('MDX Namespace', () => { }); describe('dev', () => { - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); @@ -60,7 +60,7 @@ describe('MDX Namespace', () => { const { document } = parseHTML(html); const island = document.querySelector('astro-island'); - const component = document.querySelector('#component'); + const component = document.querySelector('#component')!; assert.notEqual(island, undefined); assert.equal(component.textContent, 'Hello world'); @@ -75,7 +75,7 @@ describe('MDX Namespace', () => { const { document } = parseHTML(html); const island = document.querySelector('astro-island'); - const component = document.querySelector('#component'); + const component = document.querySelector('#component')!; assert.notEqual(island, undefined); assert.equal(component.textContent, 'Hello world'); diff --git a/packages/integrations/mdx/test/mdx-optimize.test.js b/packages/integrations/mdx/test/mdx-optimize.test.ts similarity index 82% rename from packages/integrations/mdx/test/mdx-optimize.test.js rename to packages/integrations/mdx/test/mdx-optimize.test.ts index bc66a5732c91..3f79cf3742ee 100644 --- a/packages/integrations/mdx/test/mdx-optimize.test.js +++ b/packages/integrations/mdx/test/mdx-optimize.test.ts @@ -1,13 +1,12 @@ import * as assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { parseHTML } from 'linkedom'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture } from '../../../astro/test/test-utils.js'; const FIXTURE_ROOT = new URL('./fixtures/mdx-optimize/', import.meta.url); describe('MDX optimize', () => { - /** @type {import('../../../astro/test/test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: FIXTURE_ROOT, @@ -19,17 +18,17 @@ describe('MDX optimize', () => { const html = await fixture.readFile('/index.html'); const { document } = parseHTML(html); - assert.equal(document.querySelector('h1').textContent.includes('MDX page'), true); + assert.equal(document.querySelector('h1')!.textContent.includes('MDX page'), true); assert.equal( - document.querySelector('p').textContent.includes('I once heard a very inspirational quote:'), + document.querySelector('p')!.textContent.includes('I once heard a very inspirational quote:'), true, ); - const blockquote = document.querySelector('blockquote.custom-blockquote'); + const blockquote = document.querySelector('blockquote.custom-blockquote')!; assert.notEqual(blockquote, null); assert.equal(blockquote.textContent.includes('I like pancakes'), true); - const code = document.querySelector('pre.astro-code'); + const code = document.querySelector('pre.astro-code')!; assert.notEqual(code, null); assert.equal(code.textContent.includes(`const pancakes = 'yummy'`), true); }); @@ -38,13 +37,13 @@ describe('MDX optimize', () => { const html = await fixture.readFile('/import/index.html'); const { document } = parseHTML(html); - assert.equal(document.querySelector('h1').textContent.includes('Astro page'), true); + assert.equal(document.querySelector('h1')!.textContent.includes('Astro page'), true); assert.equal( - document.querySelector('p').textContent.includes('I once heard a very inspirational quote:'), + document.querySelector('p')!.textContent.includes('I once heard a very inspirational quote:'), true, ); - const blockquote = document.querySelector('blockquote.custom-blockquote'); + const blockquote = document.querySelector('blockquote.custom-blockquote')!; assert.notEqual(blockquote, null); assert.equal(blockquote.textContent.includes('I like pancakes'), true); }); @@ -90,7 +89,7 @@ describe('MDX optimize', () => { assert.doesNotMatch(html, /set:html=/); assert.equal( - document.getElementById('injected-root-hast').textContent, + document.getElementById('injected-root-hast')!.textContent, 'Injected root hast from rehype plugin', ); }); diff --git a/packages/integrations/mdx/test/mdx-page.test.js b/packages/integrations/mdx/test/mdx-page.test.ts similarity index 85% rename from packages/integrations/mdx/test/mdx-page.test.js rename to packages/integrations/mdx/test/mdx-page.test.ts index 0327bcaf0a82..6237f2185701 100644 --- a/packages/integrations/mdx/test/mdx-page.test.js +++ b/packages/integrations/mdx/test/mdx-page.test.ts @@ -2,10 +2,10 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; import { parseHTML } from 'linkedom'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture, type DevServer } from '../../../astro/test/test-utils.js'; describe('MDX Page', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -24,7 +24,7 @@ describe('MDX Page', () => { const html = await fixture.readFile('/index.html'); const { document } = parseHTML(html); - const h1 = document.querySelector('h1'); + const h1 = document.querySelector('h1')!; assert.equal(h1.textContent, 'Hello page!'); }); @@ -59,13 +59,13 @@ describe('MDX Page', () => { const html = await fixture.readFile('/index.html'); const { document } = parseHTML(html); - const keyTest = document.querySelector('#key-test'); + const keyTest = document.querySelector('#key-test')!; assert.equal(keyTest.textContent, 'oranges'); }); }); describe('dev', () => { - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); @@ -83,7 +83,7 @@ describe('MDX Page', () => { const html = await res.text(); const { document } = parseHTML(html); - const h1 = document.querySelector('h1'); + const h1 = document.querySelector('h1')!; assert.equal(h1.textContent, 'Hello page!'); }); @@ -94,7 +94,7 @@ describe('MDX Page', () => { const html = await res.text(); const $ = cheerio.load(html); assert.equal($('h1').text(), '我的第一篇博客文章'); - assert.doesNotMatch(res.headers.get('content-type'), /charset=utf-8/); + assert.doesNotMatch(res.headers.get('content-type') ?? '', /charset=utf-8/); assert.match(html, / { const res = await fixture.fetch('/chinese-encoding-layout-frontmatter/'); assert.equal(res.status, 200); const html = await res.text(); - assert.doesNotMatch(res.headers.get('content-type'), /charset=utf-8/); + assert.doesNotMatch(res.headers.get('content-type') ?? '', /charset=utf-8/); assert.doesNotMatch(html, / { const res = await fixture.fetch('/chinese-encoding-layout-manual/'); assert.equal(res.status, 200); const html = await res.text(); - assert.doesNotMatch(res.headers.get('content-type'), /charset=utf-8/); + assert.doesNotMatch(res.headers.get('content-type') ?? '', /charset=utf-8/); assert.doesNotMatch(html, / { - it('supports custom remark plugins - TOC', async () => { - const fixture = await buildFixture({ - integrations: [ - mdx({ - remarkPlugins: [remarkToc], - }), - ], - }); - - const html = await fixture.readFile(FILE); - const { document } = parseHTML(html); - - assert.notEqual(selectTocLink(document), null); - }); - - it('Applies GFM by default', async () => { - const fixture = await buildFixture({ - integrations: [mdx()], - }); - - const html = await fixture.readFile(FILE); - const { document } = parseHTML(html); - - assert.notEqual(selectGfmLink(document), null); - }); - - it('Applies SmartyPants by default', async () => { - const fixture = await buildFixture({ - integrations: [mdx()], - }); - - const html = await fixture.readFile(FILE); - const { document } = parseHTML(html); - - const quote = selectSmartypantsQuote(document); - assert.notEqual(quote, null); - assert.equal(quote.textContent.includes('“Smartypants” is — awesome'), true); - }); - - it('supports custom rehype plugins', async () => { - const fixture = await buildFixture({ - integrations: [ - mdx({ - rehypePlugins: [rehypeExamplePlugin], - }), - ], - }); - const html = await fixture.readFile(FILE); - const { document } = parseHTML(html); - - assert.notEqual(selectRehypeExample(document), null); - }); - - it('supports custom rehype plugins from integrations', async () => { - const fixture = await buildFixture({ - integrations: [ - mdx(), - { - name: 'test', - hooks: { - 'astro:config:setup': ({ updateConfig }) => { - updateConfig({ - markdown: { - rehypePlugins: [rehypeExamplePlugin], - }, - }); - }, - }, - }, - ], - }); - const html = await fixture.readFile(FILE); - const { document } = parseHTML(html); - - assert.notEqual(selectRehypeExample(document), null); - }); - - it('supports custom rehype plugins with namespaced attributes', async () => { - const fixture = await buildFixture({ - integrations: [ - mdx({ - rehypePlugins: [rehypeSvgPlugin], - }), - ], - }); - const html = await fixture.readFile(FILE); - const { document } = parseHTML(html); - - assert.notEqual(selectRehypeSvg(document), null); - }); - - it('extends markdown config by default', async () => { - const fixture = await buildFixture({ - markdown: { - remarkPlugins: [remarkExamplePlugin], - rehypePlugins: [rehypeExamplePlugin], - }, - integrations: [mdx()], - }); - - const html = await fixture.readFile(FILE); - const { document } = parseHTML(html); - - assert.notEqual(selectRemarkExample(document), null); - assert.notEqual(selectRehypeExample(document), null); - }); - - it('ignores string-based plugins in markdown config', async () => { - const fixture = await buildFixture({ - markdown: { - remarkPlugins: [['remark-toc', {}]], - }, - integrations: [mdx()], - }); - - const html = await fixture.readFile(FILE); - const { document } = parseHTML(html); - - assert.equal(selectTocLink(document), null); - }); - - for (const extendMarkdownConfig of [true, false]) { - describe(`extendMarkdownConfig = ${extendMarkdownConfig}`, () => { - let fixture; - before(async () => { - fixture = await buildFixture({ - markdown: { - remarkPlugins: [remarkToc], - gfm: false, - smartypants: false, - }, - integrations: [ - mdx({ - extendMarkdownConfig, - remarkPlugins: [remarkExamplePlugin], - rehypePlugins: [rehypeExamplePlugin], - }), - ], - }); - }); - - it('Handles MDX plugins', async () => { - const html = await fixture.readFile(FILE); - const { document } = parseHTML(html); - - assert.notEqual(selectRemarkExample(document, 'MDX remark plugins not applied.'), null); - assert.notEqual(selectRehypeExample(document, 'MDX rehype plugins not applied.'), null); - }); - - it('Handles Markdown plugins', async () => { - const html = await fixture.readFile(FILE); - const { document } = parseHTML(html); - - assert.equal( - selectTocLink( - document, - '`remarkToc` plugin applied unexpectedly. Should override Markdown config.', - ), - null, - ); - }); - - it('Handles gfm', async () => { - const html = await fixture.readFile(FILE); - const { document } = parseHTML(html); - - if (extendMarkdownConfig === true) { - assert.equal(selectGfmLink(document), null, 'Does not respect `markdown.gfm` option.'); - } else { - assert.notEqual(selectGfmLink(document), null, 'Respects `markdown.gfm` unexpectedly.'); - } - }); - - it('Handles smartypants', async () => { - const html = await fixture.readFile(FILE); - const { document } = parseHTML(html); - - const quote = selectSmartypantsQuote(document); - - if (extendMarkdownConfig === true) { - assert.equal( - quote.textContent.includes('"Smartypants" is -- awesome'), - true, - 'Does not respect `markdown.smartypants` option.', - ); - } else { - assert.equal( - quote.textContent.includes('“Smartypants” is — awesome'), - true, - 'Respects `markdown.smartypants` unexpectedly.', - ); - } - }); - }); - } - - it('supports custom recma plugins', async () => { - const fixture = await buildFixture({ - integrations: [ - mdx({ - recmaPlugins: [recmaExamplePlugin], - }), - ], - }); - - const html = await fixture.readFile(FILE); - const { document } = parseHTML(html); - - assert.notEqual(selectRecmaExample(document), null); - }); -}); - -async function buildFixture(config) { - const fixture = await loadFixture({ - root: FIXTURE_ROOT, - ...config, - }); - await fixture.build(); - return fixture; -} - -function remarkExamplePlugin() { - return (tree) => { - tree.children.push({ - type: 'html', - value: '

    ', - }); - }; -} - -function rehypeExamplePlugin() { - return (tree) => { - tree.children.push({ - type: 'element', - tagName: 'div', - properties: { 'data-rehype-plugin-works': 'true' }, - }); - }; -} - -function rehypeSvgPlugin() { - return (tree) => { - tree.children.push({ - type: 'element', - tagName: 'svg', - properties: { xmlns: 'http://www.w3.org/2000/svg' }, - children: [ - { - type: 'element', - tagName: 'use', - properties: { xLinkHref: '#icon' }, - }, - ], - }); - }; -} - -function recmaExamplePlugin() { - return (tree) => { - estreeVisit(tree, (node) => { - if ( - node.type === 'VariableDeclarator' && - node.id.name === 'recmaPluginWorking' && - node.init?.type === 'Literal' - ) { - node.init = { - ...(node.init ?? {}), - value: true, - raw: 'true', - }; - } - }); - }; -} - -function selectTocLink(document) { - return document.querySelector('ul a[href="#section-1"]'); -} - -function selectGfmLink(document) { - return document.querySelector('a[href="https://handle-me-gfm.com"]'); -} - -function selectSmartypantsQuote(document) { - return document.querySelector('blockquote'); -} - -function selectRemarkExample(document) { - return document.querySelector('div[data-remark-plugin-works]'); -} - -function selectRehypeExample(document) { - return document.querySelector('div[data-rehype-plugin-works]'); -} - -function selectRehypeSvg(document) { - return document.querySelector('svg > use[xlink\\:href]'); -} - -function selectRecmaExample(document) { - return document.querySelector('div[data-recma-plugin-works]'); -} diff --git a/packages/integrations/mdx/test/mdx-plugins.test.ts b/packages/integrations/mdx/test/mdx-plugins.test.ts new file mode 100644 index 000000000000..42ed69b689ae --- /dev/null +++ b/packages/integrations/mdx/test/mdx-plugins.test.ts @@ -0,0 +1,182 @@ +import * as assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import mdx from '@astrojs/mdx'; +import { parseHTML } from 'linkedom'; +import remarkToc from 'remark-toc'; +import { + loadFixture, + type AstroInlineConfig, + type Fixture, +} from '../../../astro/test/test-utils.js'; +import type { RehypePlugin, RemarkPlugin } from './test-utils.js'; + +const FIXTURE_ROOT = new URL('./fixtures/mdx-plugins/', import.meta.url); +const FILE = '/with-plugins/index.html'; + +describe('MDX plugins - Astro config integration', () => { + it('supports custom rehype plugins from integrations', async () => { + const fixture = await buildFixture({ + integrations: [ + mdx(), + { + name: 'test', + hooks: { + 'astro:config:setup': ({ updateConfig }) => { + updateConfig({ + markdown: { + rehypePlugins: [rehypeExamplePlugin], + }, + }); + }, + }, + }, + ], + }); + const html = await fixture.readFile(FILE); + const { document } = parseHTML(html); + + assert.notEqual(selectRehypeExample(document), null); + }); + + it('extends markdown config by default', async () => { + const fixture = await buildFixture({ + markdown: { + remarkPlugins: [remarkExamplePlugin], + rehypePlugins: [rehypeExamplePlugin], + }, + integrations: [mdx()], + }); + + const html = await fixture.readFile(FILE); + const { document } = parseHTML(html); + + assert.notEqual(selectRemarkExample(document), null); + assert.notEqual(selectRehypeExample(document), null); + }); + + for (const extendMarkdownConfig of [true, false]) { + describe(`extendMarkdownConfig = ${extendMarkdownConfig}`, () => { + let fixture: Fixture; + before(async () => { + fixture = await buildFixture({ + // Use unique outDir to avoid cache pollution between builds with different configs + outDir: `./dist/mdx-plugins-extend-${extendMarkdownConfig}/`, + markdown: { + remarkPlugins: [remarkToc], + gfm: false, + smartypants: false, + }, + integrations: [ + mdx({ + extendMarkdownConfig, + remarkPlugins: [remarkExamplePlugin], + rehypePlugins: [rehypeExamplePlugin], + }), + ], + }); + }); + + it('Handles MDX plugins', async () => { + const html = await fixture.readFile(FILE); + const { document } = parseHTML(html); + + assert.notEqual(selectRemarkExample(document), null, 'MDX remark plugins not applied.'); + assert.notEqual(selectRehypeExample(document), null, 'MDX rehype plugins not applied.'); + }); + + it('Handles Markdown plugins', async () => { + const html = await fixture.readFile(FILE); + const { document } = parseHTML(html); + + assert.equal( + selectTocLink(document), + null, + '`remarkToc` plugin applied unexpectedly. Should override Markdown config.', + ); + }); + + it('Handles gfm', async () => { + const html = await fixture.readFile(FILE); + const { document } = parseHTML(html); + + if (extendMarkdownConfig === true) { + assert.equal(selectGfmLink(document), null, 'Does not respect `markdown.gfm` option.'); + } else { + assert.notEqual(selectGfmLink(document), null, 'Respects `markdown.gfm` unexpectedly.'); + } + }); + + it('Handles smartypants', async () => { + const html = await fixture.readFile(FILE); + const { document } = parseHTML(html); + + const quote = selectSmartypantsQuote(document)!; + + if (extendMarkdownConfig === true) { + // smartypants: false inherited from markdown config — straight quotes and dashes preserved + assert.equal( + quote.textContent.includes('--'), + true, + 'Does not respect `markdown.smartypants` option: dashes should remain as --.', + ); + } else { + // smartypants defaults to ON — converts quotes to curly and -- to em dash + assert.equal( + quote.textContent.includes('\u2014'), + true, + 'Smartypants should be ON when not extending markdown config: -- should become em dash.', + ); + } + }); + }); + } +}); + +async function buildFixture(config: AstroInlineConfig = {}): Promise { + const fixture = await loadFixture({ + root: FIXTURE_ROOT, + ...config, + }); + await fixture.build(); + return fixture; +} + +const remarkExamplePlugin: RemarkPlugin = () => { + return (tree) => { + tree.children.push({ + type: 'html', + value: '
    ', + }); + }; +}; + +const rehypeExamplePlugin: RehypePlugin = () => { + return (tree) => { + tree.children.push({ + type: 'element', + tagName: 'div', + properties: { 'data-rehype-plugin-works': 'true' }, + children: [], + }); + }; +}; + +function selectTocLink(document: Document) { + return document.querySelector('ul a[href="#section-1"]'); +} + +function selectGfmLink(document: Document) { + return document.querySelector('a[href="https://handle-me-gfm.com"]'); +} + +function selectSmartypantsQuote(document: Document) { + return document.querySelector('blockquote'); +} + +function selectRemarkExample(document: Document) { + return document.querySelector('div[data-remark-plugin-works]'); +} + +function selectRehypeExample(document: Document) { + return document.querySelector('div[data-rehype-plugin-works]'); +} diff --git a/packages/integrations/mdx/test/mdx-plus-react-errors.test.js b/packages/integrations/mdx/test/mdx-plus-react-errors.test.ts similarity index 67% rename from packages/integrations/mdx/test/mdx-plus-react-errors.test.js rename to packages/integrations/mdx/test/mdx-plus-react-errors.test.ts index 9d87fa8a0045..67d909f6df1f 100644 --- a/packages/integrations/mdx/test/mdx-plus-react-errors.test.js +++ b/packages/integrations/mdx/test/mdx-plus-react-errors.test.ts @@ -4,30 +4,29 @@ import { loadFixture } from '../../../astro/test/test-utils.js'; function hookError() { const error = console.error; - const errors = []; - console.error = function (...args) { + const errors: unknown[][] = []; + console.error = function (...args: unknown[]) { errors.push(args); }; - return () => { + return (): unknown[][] => { console.error = error; return errors; }; } describe('MDX and React with build errors', () => { - let fixture; - let unhook; + let unhook: (() => unknown[][]) | undefined; it('shows correct error messages on build error', async () => { try { - fixture = await loadFixture({ + const fixture = await loadFixture({ root: new URL('./fixtures/mdx-plus-react-errors/', import.meta.url), }); unhook = hookError(); await fixture.build(); } catch (err) { - assert.equal(err.message, 'a is not defined'); + assert.equal((err as Error).message, 'a is not defined'); } - unhook(); + unhook?.(); }); }); diff --git a/packages/integrations/mdx/test/mdx-plus-react.test.js b/packages/integrations/mdx/test/mdx-plus-react.test.ts similarity index 82% rename from packages/integrations/mdx/test/mdx-plus-react.test.js rename to packages/integrations/mdx/test/mdx-plus-react.test.ts index 87f420fc06ef..7d821eb2302c 100644 --- a/packages/integrations/mdx/test/mdx-plus-react.test.js +++ b/packages/integrations/mdx/test/mdx-plus-react.test.ts @@ -1,23 +1,23 @@ import * as assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { parseHTML } from 'linkedom'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture } from '../../../astro/test/test-utils.js'; function hookError() { const error = console.error; - const errors = []; - console.error = function (...args) { + const errors: unknown[][] = []; + console.error = function (...args: unknown[]) { errors.push(args); }; - return () => { + return (): unknown[][] => { console.error = error; return errors; }; } describe('MDX and React', () => { - let fixture; - let unhook; + let fixture: Fixture; + let unhook: () => unknown[][]; before(async () => { fixture = await loadFixture({ @@ -31,7 +31,7 @@ describe('MDX and React', () => { const html = await fixture.readFile('/index.html'); const { document } = parseHTML(html); - const p = document.querySelector('p'); + const p = document.querySelector('p')!; assert.equal(p.textContent, 'Hello world'); }); @@ -39,7 +39,7 @@ describe('MDX and React', () => { it('mdx renders fine', async () => { const html = await fixture.readFile('/post/index.html'); const { document } = parseHTML(html); - const h = document.querySelector('#testing'); + const h = document.querySelector('#testing')!; assert.equal(h.textContent, 'Testing'); }); diff --git a/packages/integrations/mdx/test/mdx-script-style-raw.test.js b/packages/integrations/mdx/test/mdx-script-style-raw.test.js deleted file mode 100644 index 3b0acefe04b3..000000000000 --- a/packages/integrations/mdx/test/mdx-script-style-raw.test.js +++ /dev/null @@ -1,75 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { after, before, describe, it } from 'node:test'; -import mdx from '@astrojs/mdx'; -import { parseHTML } from 'linkedom'; -import { loadFixture } from '../../../astro/test/test-utils.js'; - -const FIXTURE_ROOT = new URL('./fixtures/mdx-script-style-raw/', import.meta.url); - -describe('MDX script style raw', () => { - describe('dev', () => { - let fixture; - let devServer; - - before(async () => { - fixture = await loadFixture({ - root: FIXTURE_ROOT, - integrations: [mdx()], - }); - devServer = await fixture.startDevServer(); - }); - - after(async () => { - await devServer.stop(); - }); - - it('works with raw script and style strings', async () => { - const res = await fixture.fetch('/index.html'); - assert.equal(res.status, 200); - - const html = await res.text(); - const { document } = parseHTML(html); - - const scriptContent = document.getElementById('test-script').innerHTML; - assert.equal( - scriptContent.includes("console.log('raw script')"), - true, - 'script should not be html-escaped', - ); - - const styleContent = document.getElementById('test-style').innerHTML; - assert.equal( - styleContent.includes('h1[id="script-style-raw"]'), - true, - 'style should not be html-escaped', - ); - }); - }); - - describe('build', () => { - it('works with raw script and style strings', async () => { - const fixture = await loadFixture({ - root: FIXTURE_ROOT, - integrations: [mdx()], - }); - await fixture.build(); - - const html = await fixture.readFile('/index.html'); - const { document } = parseHTML(html); - - const scriptContent = document.getElementById('test-script').innerHTML; - assert.equal( - scriptContent.includes("console.log('raw script')"), - true, - 'script should not be html-escaped', - ); - - const styleContent = document.getElementById('test-style').innerHTML; - assert.equal( - styleContent.includes('h1[id="script-style-raw"]'), - true, - 'style should not be html-escaped', - ); - }); - }); -}); diff --git a/packages/integrations/mdx/test/mdx-slots.test.js b/packages/integrations/mdx/test/mdx-slots.test.js deleted file mode 100644 index f1ee6a2377ec..000000000000 --- a/packages/integrations/mdx/test/mdx-slots.test.js +++ /dev/null @@ -1,124 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { after, before, describe, it } from 'node:test'; -import mdx from '@astrojs/mdx'; -import { parseHTML } from 'linkedom'; -import { loadFixture } from '../../../astro/test/test-utils.js'; - -describe('MDX slots', () => { - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: new URL('./fixtures/mdx-slots/', import.meta.url), - integrations: [mdx()], - }); - }); - - describe('build', () => { - before(async () => { - await fixture.build(); - }); - - it('supports top-level imports', async () => { - const html = await fixture.readFile('/index.html'); - const { document } = parseHTML(html); - - const h1 = document.querySelector('h1'); - const defaultSlot = document.querySelector('[data-default-slot]'); - const namedSlot = document.querySelector('[data-named-slot]'); - - assert.equal(h1.textContent, 'Hello slotted component!'); - assert.equal(defaultSlot.textContent, 'Default content.'); - assert.equal(namedSlot.textContent, 'Content for named slot.'); - }); - - it('supports glob imports - ', async () => { - const html = await fixture.readFile('/glob/index.html'); - const { document } = parseHTML(html); - - const h1 = document.querySelector('[data-default-export] h1'); - const defaultSlot = document.querySelector('[data-default-export] [data-default-slot]'); - const namedSlot = document.querySelector('[data-default-export] [data-named-slot]'); - - assert.equal(h1.textContent, 'Hello slotted component!'); - assert.equal(defaultSlot.textContent, 'Default content.'); - assert.equal(namedSlot.textContent, 'Content for named slot.'); - }); - - it('supports glob imports - ', async () => { - const html = await fixture.readFile('/glob/index.html'); - const { document } = parseHTML(html); - - const h1 = document.querySelector('[data-content-export] h1'); - const defaultSlot = document.querySelector('[data-content-export] [data-default-slot]'); - const namedSlot = document.querySelector('[data-content-export] [data-named-slot]'); - - assert.equal(h1.textContent, 'Hello slotted component!'); - assert.equal(defaultSlot.textContent, 'Default content.'); - assert.equal(namedSlot.textContent, 'Content for named slot.'); - }); - }); - - describe('dev', () => { - let devServer; - - before(async () => { - devServer = await fixture.startDevServer(); - }); - - after(async () => { - await devServer.stop(); - }); - - it('supports top-level imports', async () => { - const res = await fixture.fetch('/'); - - assert.equal(res.status, 200); - - const html = await res.text(); - const { document } = parseHTML(html); - - const h1 = document.querySelector('h1'); - const defaultSlot = document.querySelector('[data-default-slot]'); - const namedSlot = document.querySelector('[data-named-slot]'); - - assert.equal(h1.textContent, 'Hello slotted component!'); - assert.equal(defaultSlot.textContent, 'Default content.'); - assert.equal(namedSlot.textContent, 'Content for named slot.'); - }); - - it('supports glob imports - ', async () => { - const res = await fixture.fetch('/glob'); - - assert.equal(res.status, 200); - - const html = await res.text(); - const { document } = parseHTML(html); - - const h1 = document.querySelector('[data-default-export] h1'); - const defaultSlot = document.querySelector('[data-default-export] [data-default-slot]'); - const namedSlot = document.querySelector('[data-default-export] [data-named-slot]'); - - assert.equal(h1.textContent, 'Hello slotted component!'); - assert.equal(defaultSlot.textContent, 'Default content.'); - assert.equal(namedSlot.textContent, 'Content for named slot.'); - }); - - it('supports glob imports - ', async () => { - const res = await fixture.fetch('/glob'); - - assert.equal(res.status, 200); - - const html = await res.text(); - const { document } = parseHTML(html); - - const h1 = document.querySelector('[data-content-export] h1'); - const defaultSlot = document.querySelector('[data-content-export] [data-default-slot]'); - const namedSlot = document.querySelector('[data-content-export] [data-named-slot]'); - - assert.equal(h1.textContent, 'Hello slotted component!'); - assert.equal(defaultSlot.textContent, 'Default content.'); - assert.equal(namedSlot.textContent, 'Content for named slot.'); - }); - }); -}); diff --git a/packages/integrations/mdx/test/mdx-syntax-highlighting.test.js b/packages/integrations/mdx/test/mdx-syntax-highlighting.test.ts similarity index 92% rename from packages/integrations/mdx/test/mdx-syntax-highlighting.test.js rename to packages/integrations/mdx/test/mdx-syntax-highlighting.test.ts index 2f72d4eb2866..f490ff15babc 100644 --- a/packages/integrations/mdx/test/mdx-syntax-highlighting.test.js +++ b/packages/integrations/mdx/test/mdx-syntax-highlighting.test.ts @@ -24,9 +24,12 @@ describe('MDX syntax highlighting', () => { const html = await fixture.readFile('/index.html'); const { document } = parseHTML(html); - const shikiCodeBlock = document.querySelector('pre.astro-code'); + const shikiCodeBlock = document.querySelector('pre.astro-code')!; assert.notEqual(shikiCodeBlock, null); - assert.equal(shikiCodeBlock.getAttribute('style').includes('background-color:#24292e'), true); + assert.equal( + shikiCodeBlock.getAttribute('style')!.includes('background-color:#24292e'), + true, + ); }); it('respects markdown.shikiConfig.theme', async () => { @@ -45,9 +48,12 @@ describe('MDX syntax highlighting', () => { const html = await fixture.readFile('/index.html'); const { document } = parseHTML(html); - const shikiCodeBlock = document.querySelector('pre.astro-code'); + const shikiCodeBlock = document.querySelector('pre.astro-code')!; assert.notEqual(shikiCodeBlock, null); - assert.equal(shikiCodeBlock.getAttribute('style').includes('background-color:#282A36'), true); + assert.equal( + shikiCodeBlock.getAttribute('style')!.includes('background-color:#282A36'), + true, + ); }); }); @@ -140,7 +146,7 @@ describe('MDX syntax highlighting', () => { [ rehypePrettyCode, { - onVisitHighlightedLine(node) { + onVisitHighlightedLine(node: { properties: Record }) { node.properties.style = 'background-color:#000000'; }, }, diff --git a/packages/integrations/mdx/test/mdx-url-export.test.js b/packages/integrations/mdx/test/mdx-url-export.test.js deleted file mode 100644 index 66a34db75fc4..000000000000 --- a/packages/integrations/mdx/test/mdx-url-export.test.js +++ /dev/null @@ -1,28 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { before, describe, it } from 'node:test'; -import mdx from '@astrojs/mdx'; -import { loadFixture } from '../../../astro/test/test-utils.js'; - -describe('MDX url export', () => { - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: new URL('./fixtures/mdx-url-export/', import.meta.url), - integrations: [mdx()], - }); - - await fixture.build(); - }); - - it('generates correct urls in glob result', async () => { - const { urls } = JSON.parse(await fixture.readFile('/pages.json')); - assert.equal(urls.includes('/test-1'), true); - assert.equal(urls.includes('/test-2'), true); - }); - - it('respects "export url" overrides in glob result', async () => { - const { urls } = JSON.parse(await fixture.readFile('/pages.json')); - assert.equal(urls.includes('/AH!'), true); - }); -}); diff --git a/packages/integrations/mdx/test/mdx-vite-env-vars.test.js b/packages/integrations/mdx/test/mdx-vite-env-vars.test.ts similarity index 95% rename from packages/integrations/mdx/test/mdx-vite-env-vars.test.js rename to packages/integrations/mdx/test/mdx-vite-env-vars.test.ts index 213386ceb712..393c2b639719 100644 --- a/packages/integrations/mdx/test/mdx-vite-env-vars.test.js +++ b/packages/integrations/mdx/test/mdx-vite-env-vars.test.ts @@ -1,10 +1,10 @@ import * as assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { parseHTML } from 'linkedom'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture } from '../../../astro/test/test-utils.js'; describe('MDX - Vite env vars', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: new URL('./fixtures/mdx-vite-env-vars/', import.meta.url), @@ -54,7 +54,7 @@ describe('MDX - Vite env vars', () => { const html = await fixture.readFile('/vite-env-vars/index.html'); const { document } = parseHTML(html); - const dataAttrDump = document.querySelector('[data-env-dump]'); + const dataAttrDump = document.querySelector('[data-env-dump]')!; assert.notEqual(dataAttrDump, null); assert.equal(dataAttrDump.getAttribute('data-env-prod'), 'true'); diff --git a/packages/integrations/mdx/test/remark-imgattr.test.js b/packages/integrations/mdx/test/remark-imgattr.test.ts similarity index 75% rename from packages/integrations/mdx/test/remark-imgattr.test.js rename to packages/integrations/mdx/test/remark-imgattr.test.ts index 067d18e236c6..50528561ed08 100644 --- a/packages/integrations/mdx/test/remark-imgattr.test.js +++ b/packages/integrations/mdx/test/remark-imgattr.test.ts @@ -1,17 +1,15 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture, type DevServer } from '../../../astro/test/test-utils.js'; const FIXTURE_ROOT = new URL('./fixtures/image-remark-imgattr/', import.meta.url); describe('Testing remark plugins for image processing', () => { - /** @type {import('../../../astro/test/test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; describe('start dev server', () => { - /** @type {import('../../../astro/test/test-utils.js').DevServer} */ - let devServer; + let devServer: DevServer; before(async () => { fixture = await loadFixture({ @@ -26,7 +24,7 @@ describe('Testing remark plugins for image processing', () => { }); describe('Test image attributes can be added by remark plugins', () => { - let $; + let $: ReturnType; before(async () => { let res = await fixture.fetch('/'); let html = await res.text(); @@ -42,7 +40,10 @@ describe('Testing remark plugins for image processing', () => { it(' was processed properly', async () => { let $img = $('img'); - assert.equal(new URL($img.attr('src'), 'http://example.com').searchParams.get('w'), '300'); + assert.equal( + new URL($img.attr('src') ?? '', 'http://example.com').searchParams.get('w'), + '300', + ); }); }); }); diff --git a/packages/integrations/mdx/test/test-utils.ts b/packages/integrations/mdx/test/test-utils.ts new file mode 100644 index 000000000000..2f725e8daa22 --- /dev/null +++ b/packages/integrations/mdx/test/test-utils.ts @@ -0,0 +1,44 @@ +import type * as estree from 'estree'; +import type * as hast from 'hast'; +import type * as mdast from 'mdast'; +import type * as unified from 'unified'; +import { + AstroIntegrationLogger, + type AstroLogMessage, +} from '../../../astro/dist/core/logger/core.js'; + +export type RemarkPlugin = unified.Plugin< + PluginParameters, + mdast.Root +>; + +export type RehypePlugin = unified.Plugin< + PluginParameters, + hast.Root +>; + +export type RecmaPlugin = unified.Plugin< + PluginParameters, + estree.Program +>; + +export class SpyIntegrationLogger extends AstroIntegrationLogger { + readonly messages: AstroLogMessage[]; + + constructor() { + const messages: AstroLogMessage[] = []; + super( + { + destination: { + write(chunk): boolean { + messages.push(chunk); + return true; + }, + }, + level: 'warn', + }, + 'test-spy', + ); + this.messages = messages; + } +} diff --git a/packages/integrations/mdx/test/units/mdx-compilation.test.ts b/packages/integrations/mdx/test/units/mdx-compilation.test.ts new file mode 100644 index 000000000000..b723b951cf32 --- /dev/null +++ b/packages/integrations/mdx/test/units/mdx-compilation.test.ts @@ -0,0 +1,274 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { rehypeHeadingIds } from '@astrojs/markdown-remark'; +import { compile as _compile, type CompileOptions, nodeTypes } from '@mdx-js/mdx'; +import { visit as estreeVisit } from 'estree-util-visit'; +import rehypeRaw from 'rehype-raw'; +import remarkGfm from 'remark-gfm'; +import remarkSmartypants from 'remark-smartypants'; +import { visit } from 'unist-util-visit'; +import { ignoreStringPlugins } from '../../dist/utils.js'; +import { + SpyIntegrationLogger, + type RecmaPlugin, + type RehypePlugin, + type RemarkPlugin, +} from '../test-utils.ts'; + +/** + * Compile MDX to JSX string output for inspection. + */ +async function compile(mdxCode: string, options: Readonly = {}) { + const result = await _compile(mdxCode, { + jsx: true, + ...options, + }); + return result.toString(); +} + +/** + * Compile MDX with rehype-raw (like Astro does) and return the JSX output. + */ +async function compileWithRaw( + mdxCode: string, + options: Readonly = {}, +): Promise { + return compile(mdxCode, { + rehypePlugins: [[rehypeRaw, { passThrough: nodeTypes }], ...(options.rehypePlugins || [])], + remarkPlugins: options.remarkPlugins || [], + recmaPlugins: options.recmaPlugins || [], + ...options, + }); +} + +describe('MDX escape handling', () => { + it('wraps escaped HTML in string expressions, not raw JSX', async () => { + // In MDX, \ is escaped and should be rendered as text, not as an HTML element. + // The compiled JSX wraps it in a string expression like {""} + const code = await compile('\\'); + // The output should have the text as a JSX string expression, not as a JSX element + assert.ok(code.includes('{""}'), 'Escaped HTML should be wrapped in JSX string expression'); + // Should NOT have as an actual JSX element (i.e. outside of string) + assert.ok(!code.includes('{"'), 'Should not have as an actual JSX element'); + }); + + it('preserves angle brackets in inline code', async () => { + const code = await compile('` { + const code = await compile('{`
    `}'); + // JSX expression should contain the string + assert.ok(code.includes('
    '), 'Should contain the escaped string'); + }); +}); + +describe('MDX GFM plugin', () => { + it('converts autolinks when GFM is applied', async () => { + const code = await compile('https://handle-me-gfm.com', { + remarkPlugins: [remarkGfm], + }); + assert.ok(code.includes('https://handle-me-gfm.com'), 'Should contain the URL'); + assert.ok(code.includes('href'), 'GFM should create an anchor element'); + }); + + it('does not convert autolinks without GFM', async () => { + const code = await compile('https://handle-me-gfm.com'); + // Without GFM, the URL should just be text, not wrapped in + assert.ok(code.includes('https://handle-me-gfm.com')); + }); +}); + +describe('MDX SmartyPants plugin', () => { + it('converts quotes and dashes when SmartyPants is applied', async () => { + const code = await compile('> "Smartypants" is -- awesome', { + remarkPlugins: [remarkSmartypants], + }); + // SmartyPants converts straight quotes to curly and -- to em dash + assert.ok( + code.includes('\u201C') || code.includes('\u201D') || code.includes('\u2014'), + 'SmartyPants should convert quotes or dashes to typographic equivalents', + ); + }); + + it('does not convert quotes without SmartyPants', async () => { + const code = await compile('> "Smartypants" is -- awesome'); + // Without SmartyPants, double dashes stay as -- (not converted to em dash \u2014) + assert.ok(code.includes('--'), 'Double dashes should remain unconverted'); + assert.ok(!code.includes('\u2014'), 'Em dash should not appear without SmartyPants'); + }); +}); + +describe('MDX remark plugins', () => { + it('supports custom remark plugins that modify the tree', async () => { + /** Remark plugin that appends a div */ + const remarkAddDiv: RemarkPlugin = () => { + return (tree) => { + tree.children.push({ + type: 'html', + value: '
    ', + }); + }; + }; + + const code = await compileWithRaw('# Hello', { + remarkPlugins: [remarkAddDiv], + }); + assert.ok( + code.includes('data-remark-works'), + 'Custom remark plugin output should be in compiled result', + ); + }); +}); + +describe('MDX rehype plugins', () => { + it('supports custom rehype plugins that modify the tree', async () => { + /** Rehype plugin that appends a div */ + const rehypeAddDiv: RehypePlugin = () => { + return (tree) => { + tree.children.push({ + type: 'element', + tagName: 'div', + properties: { 'data-rehype-works': 'true' }, + children: [], + }); + }; + }; + + const code = await compileWithRaw('# Hello', { + rehypePlugins: [rehypeAddDiv], + }); + assert.ok( + code.includes('data-rehype-works'), + 'Custom rehype plugin output should be in compiled result', + ); + }); + + it('supports rehype plugins with namespaced SVG attributes', async () => { + const rehypeSvg: RehypePlugin = () => { + return (tree) => { + tree.children.push({ + type: 'element', + tagName: 'svg', + properties: { xmlns: 'http://www.w3.org/2000/svg' }, + children: [ + { + type: 'element', + tagName: 'use', + properties: { xlinkHref: '#icon' }, + children: [], + }, + ], + }); + }; + }; + + const code = await compileWithRaw('# Hello', { + rehypePlugins: [rehypeSvg], + }); + assert.ok(code.includes('svg'), 'Should contain SVG element'); + }); +}); + +describe('MDX recma plugins', () => { + it('supports custom recma plugins that transform the estree', async () => { + const recmaExample: RecmaPlugin = () => { + return (tree) => { + estreeVisit(tree, (node) => { + if ( + node.type === 'VariableDeclarator' && + node.id.type === 'Identifier' && + node.id.name === 'recmaPluginWorking' && + node.init?.type === 'Literal' + ) { + node.init = { + ...(node.init ?? {}), + value: true, + raw: 'true', + }; + } + }); + }; + }; + + const mdxCode = `export const recmaPluginWorking = false; + +# Hello`; + const code = await compile(mdxCode, { + recmaPlugins: [recmaExample], + }); + // The recma plugin should have changed false to true + assert.ok(code.includes('true'), 'Recma plugin should transform the value'); + }); +}); + +describe('MDX heading IDs', () => { + it('generates heading IDs with rehypeHeadingIds', async () => { + const mdxCode = `# Hello World + +## Section 1 + +### Subsection 1 +`; + const code = await compileWithRaw(mdxCode, { + rehypePlugins: [rehypeHeadingIds], + }); + assert.ok(code.includes('hello-world'), 'Should generate slug for h1'); + assert.ok(code.includes('section-1'), 'Should generate slug for h2'); + assert.ok(code.includes('subsection-1'), 'Should generate slug for h3'); + }); + + it('generates correct slugs for special characters', async () => { + const mdxCode = `# \`\` + +### « Sacrebleu ! » +`; + const code = await compileWithRaw(mdxCode, { + rehypePlugins: [rehypeHeadingIds], + }); + assert.ok(code.includes('picture-'), 'Should generate slug for code in heading'); + assert.ok(code.includes('-sacrebleu--'), 'Should generate slug for special chars'); + }); + + it('allows user plugins to override heading IDs', async () => { + const customIdPlugin: RehypePlugin = () => { + return (tree) => { + let count = 0; + visit(tree, 'element', (node) => { + if (!/^h\d$/.test(node.tagName)) return; + if (!node.properties?.id) { + node.properties = { ...node.properties, id: String(count++) }; + } + }); + }; + }; + + const mdxCode = `# Hello + +## World +`; + const code = await compileWithRaw(mdxCode, { + rehypePlugins: [customIdPlugin], + }); + // MDX JSX output uses id="0" as a JSX attribute + assert.ok(code.includes('id="0"'), 'Custom plugin should set id="0" on first heading'); + assert.ok(code.includes('id="1"'), 'Custom plugin should set id="1" on second heading'); + }); +}); + +describe('MDX string-based plugin filtering', () => { + it('does not apply string-based remark plugins', async () => { + // When a string-based plugin is provided, the ignoreStringPlugins + // function filters it out. We test the filter function directly in utils.test.js. + // Here we verify that only function plugins affect output. + const logger = new SpyIntegrationLogger(); + + const plugins = ['remark-toc', () => (tree: unknown) => tree]; + const filtered = ignoreStringPlugins(plugins, logger); + + assert.equal(filtered.length, 1, 'Should filter out string plugin'); + assert.equal(typeof filtered[0], 'function', 'Should keep function plugin'); + }); +}); diff --git a/packages/integrations/mdx/test/units/rehype-optimize-static.test.js b/packages/integrations/mdx/test/units/rehype-optimize-static.test.ts similarity index 81% rename from packages/integrations/mdx/test/units/rehype-optimize-static.test.js rename to packages/integrations/mdx/test/units/rehype-optimize-static.test.ts index 6121975a405a..f93b8d3f3338 100644 --- a/packages/integrations/mdx/test/units/rehype-optimize-static.test.js +++ b/packages/integrations/mdx/test/units/rehype-optimize-static.test.ts @@ -1,13 +1,9 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import { compile as _compile } from '@mdx-js/mdx'; +import { compile as _compile, type CompileOptions } from '@mdx-js/mdx'; import { rehypeOptimizeStatic } from '../../dist/rehype-optimize-static.js'; -/** - * @param {string} mdxCode - * @param {Readonly} options - */ -async function compile(mdxCode, options) { +async function compile(mdxCode: string, options?: Readonly) { const result = await _compile(mdxCode, { jsx: true, rehypePlugins: [rehypeOptimizeStatic], @@ -20,12 +16,14 @@ async function compile(mdxCode, options) { return dedent(jsx); } -function dedent(str) { +function dedent(str: string) { const lines = str.split('\n'); if (lines.length <= 1) return str; // Get last line indent, and dedent this amount for the other lines - const lastLineIndent = lines[lines.length - 1].match(/^\s*/)[0].length; - return lines.map((line, i) => (i === 0 ? line : line.slice(lastLineIndent))).join('\n'); + const lastLineIndent = /^\s*/.exec(lines[lines.length - 1])![0].length; + return lines + .map((line: string, i: number) => (i === 0 ? line : line.slice(lastLineIndent))) + .join('\n'); } describe('rehype-optimize-static', () => { diff --git a/packages/integrations/mdx/test/units/rehype-plugins.test.ts b/packages/integrations/mdx/test/units/rehype-plugins.test.ts new file mode 100644 index 000000000000..89e65acaa53b --- /dev/null +++ b/packages/integrations/mdx/test/units/rehype-plugins.test.ts @@ -0,0 +1,160 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import type * as hast from 'hast'; +import { VFile } from 'vfile'; +import { rehypeInjectHeadingsExport } from '../../dist/rehype-collect-headings.js'; +import rehypeMetaString from '../../dist/rehype-meta-string.js'; + +describe('rehypeMetaString', () => { + function createCodeNode(meta: string | undefined): hast.Element { + return { + type: 'element', + tagName: 'code', + data: meta != null ? { meta } : undefined, + children: [{ type: 'text', value: 'const x = 1;' }], + position: undefined, + properties: {}, + }; + } + + function createTree(children: hast.RootContent[]): hast.Root { + return { type: 'root', children }; + } + + it('copies data.meta to properties.metastring', () => { + const codeNode = createCodeNode('{1:3}'); + const tree = createTree([ + { + type: 'element', + tagName: 'pre', + properties: {}, + children: [codeNode], + }, + ]); + + const transform = rehypeMetaString(); + transform(tree); + + assert.equal(codeNode.properties!.metastring, '{1:3}'); + }); + + it('does not set metastring when no data.meta', () => { + const codeNode = createCodeNode(undefined); + // Ensure no data property at all + delete codeNode.data; + const tree = createTree([codeNode]); + + const transform = rehypeMetaString(); + transform(tree); + + assert.equal(codeNode.properties!.metastring, undefined); + }); + + it('handles code elements without properties', () => { + const codeNode: hast.Element = { + type: 'element', + tagName: 'code', + data: { meta: 'title="test"' }, + children: [], + position: undefined, + properties: {}, + }; + const tree = createTree([codeNode]); + + const transform = rehypeMetaString(); + transform(tree); + + assert.equal(codeNode.properties!.metastring, 'title="test"'); + }); + + it('ignores non-code elements', () => { + const divNode: hast.Element = { + type: 'element', + tagName: 'div', + properties: {}, + data: { meta: 'should-not-copy' }, + children: [], + }; + const tree = createTree([divNode]); + + const transform = rehypeMetaString(); + transform(tree); + + assert.equal(divNode.properties!.metastring, undefined); + }); +}); + +describe('rehypeInjectHeadingsExport', () => { + it('injects getHeadings export from vfile headings data', () => { + const headings = [ + { depth: 1, slug: 'hello1', text: 'Hello2' }, + { depth: 2, slug: 'world3', text: 'World4' }, + ]; + + const tree: hast.Root = { type: 'root', children: [] }; + const vfile = new VFile({ + data: { + astro: { + headings, + }, + }, + }); + + const transform = rehypeInjectHeadingsExport(); + transform(tree, vfile); + + assert.equal(tree.children.length, 1); + const injectedNode = tree.children[0] as hast.Element & { + data: { estree: { type: string; body: unknown } }; + }; + assert.equal(injectedNode.type, 'mdxjsEsm'); + // The node should contain a getHeadings function with our headings data + assert.ok(injectedNode.data.estree); + assert.equal(injectedNode.data.estree.type, 'Program'); + // The function should contain the injected heading data + const functionBody = JSON.stringify(injectedNode.data.estree.body); + assert.match(functionBody, /hello1/); + assert.match(functionBody, /Hello2/); + assert.match(functionBody, /world3/); + assert.match(functionBody, /World4/); + }); + + it('injects empty array when no headings', () => { + const tree: hast.Root = { type: 'root', children: [] }; + const vfile = new VFile({ + data: { + astro: {}, + }, + }); + + const transform = rehypeInjectHeadingsExport(); + transform(tree, vfile); + + assert.equal(tree.children.length, 1); + const injectedNode = tree.children[0]; + assert.equal(injectedNode.type, 'mdxjsEsm'); + }); + + it('prepends to existing children', () => { + const existingChild: hast.Element = { + type: 'element', + tagName: 'p', + children: [], + position: undefined, + properties: {}, + }; + const tree: hast.Root = { type: 'root', children: [existingChild] }; + const vfile = new VFile({ + data: { + astro: { headings: [] }, + }, + }); + + const transform = rehypeInjectHeadingsExport(); + transform(tree, vfile); + + assert.equal(tree.children.length, 2); + assert.equal(tree.children[0].type, 'mdxjsEsm'); + assert.equal(tree.children[1], existingChild); + }); +}); diff --git a/packages/integrations/mdx/test/units/server.test.ts b/packages/integrations/mdx/test/units/server.test.ts new file mode 100644 index 000000000000..520081a288df --- /dev/null +++ b/packages/integrations/mdx/test/units/server.test.ts @@ -0,0 +1,44 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { slotName } from '../../dist/server.js'; + +describe('server', () => { + describe('slotName', () => { + it('converts kebab-case to camelCase', () => { + assert.equal(slotName('my-slot'), 'mySlot'); + }); + + it('converts snake_case to camelCase', () => { + assert.equal(slotName('my_slot'), 'mySlot'); + }); + + it('handles multiple separators', () => { + assert.equal(slotName('my-long-slot-name'), 'myLongSlotName'); + }); + + it('handles mixed separators', () => { + assert.equal(slotName('my-slot_name'), 'mySlotName'); + }); + + it('trims whitespace', () => { + assert.equal(slotName(' my-slot '), 'mySlot'); + }); + + it('returns simple names unchanged', () => { + assert.equal(slotName('default'), 'default'); + }); + + it('handles single character after separator', () => { + assert.equal(slotName('a-b'), 'aB'); + }); + + it('handles empty string', () => { + assert.equal(slotName(''), ''); + }); + + it('only converts lowercase letters after separators', () => { + // Uppercase letters after separators are not matched by the regex [a-z] + assert.equal(slotName('my-Slot'), 'my-Slot'); + }); + }); +}); diff --git a/packages/integrations/mdx/test/units/utils.test.ts b/packages/integrations/mdx/test/units/utils.test.ts new file mode 100644 index 000000000000..a20db2de1694 --- /dev/null +++ b/packages/integrations/mdx/test/units/utils.test.ts @@ -0,0 +1,176 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import type { AstroConfig } from 'astro'; +import { + appendForwardSlash, + getFileInfo, + ignoreStringPlugins, + jsToTreeNode, +} from '../../dist/utils.js'; +import { SpyIntegrationLogger } from '../test-utils.ts'; + +describe('utils', () => { + describe('appendForwardSlash', () => { + it('appends slash when missing', () => { + assert.equal(appendForwardSlash('/foo'), '/foo/'); + }); + + it('does not double-append slash', () => { + assert.equal(appendForwardSlash('/foo/'), '/foo/'); + }); + + it('handles empty string', () => { + assert.equal(appendForwardSlash(''), '/'); + }); + + it('handles root slash', () => { + assert.equal(appendForwardSlash('/'), '/'); + }); + }); + + describe('getFileInfo', () => { + function mockConfig(overrides: Partial = {}): AstroConfig { + return { + root: new URL('file:///project/'), + base: '/', + site: undefined, + trailingSlash: 'ignore', + ...overrides, + } as AstroConfig; + } + + it('computes fileUrl for pages', () => { + const config = mockConfig(); + const result = getFileInfo('/project/src/pages/test.mdx', config); + assert.equal(result.fileId, '/project/src/pages/test.mdx'); + assert.equal(result.fileUrl, '/test'); + }); + + it('computes fileUrl for nested pages', () => { + const config = mockConfig(); + const result = getFileInfo('/project/src/pages/blog/post.mdx', config); + assert.equal(result.fileUrl, '/blog/post'); + }); + + it('strips index from page URLs', () => { + const config = mockConfig(); + const result = getFileInfo('/project/src/pages/index.mdx', config); + // The regex strips /index.mdx leaving an empty string + assert.equal(result.fileUrl, ''); + }); + + it('strips query strings from fileId', () => { + const config = mockConfig(); + const result = getFileInfo('/project/src/pages/test.mdx?astro&lang=mdx', config); + assert.equal(result.fileId, '/project/src/pages/test.mdx'); + }); + + it('uses relative path for non-page files under root', () => { + const config = mockConfig(); + const result = getFileInfo('/project/src/content/post.mdx', config); + assert.equal(result.fileUrl, 'src/content/post.mdx'); + }); + + it('respects trailingSlash=always', () => { + const config = mockConfig({ trailingSlash: 'always' }); + const result = getFileInfo('/project/src/pages/test.mdx', config); + assert.equal(result.fileUrl, '/test/'); + }); + + it('respects site + base config for pages', () => { + const config = mockConfig({ + site: 'https://example.com', + base: '/blog', + }); + const result = getFileInfo('/project/src/pages/test.mdx', config); + assert.equal(result.fileUrl, '/blog/test'); + }); + + it('handles files outside project root', () => { + const config = mockConfig(); + const result = getFileInfo('/other/path/file.mdx', config); + assert.equal(result.fileId, '/other/path/file.mdx'); + assert.equal(result.fileUrl, '/other/path/file.mdx'); + }); + }); + + describe('jsToTreeNode', () => { + it('parses a simple export statement', () => { + const node = jsToTreeNode('export const x = 1;'); + const estree = node.data!.estree!; + assert.equal(node.type, 'mdxjsEsm'); + assert.equal(estree.type, 'Program'); + assert.equal(estree.sourceType, 'module'); + assert.ok(estree.body.length > 0); + }); + + it('parses an import statement', () => { + const node = jsToTreeNode("import foo from 'bar';"); + assert.equal(node.type, 'mdxjsEsm'); + assert.equal(node.data!.estree!.body[0].type, 'ImportDeclaration'); + }); + + it('parses a function export', () => { + const node = jsToTreeNode('export function getHeadings() { return []; }'); + assert.equal(node.type, 'mdxjsEsm'); + const decl = node.data!.estree!.body[0]; + assert.equal(decl.type, 'ExportNamedDeclaration'); + }); + + it('throws on invalid JS', () => { + assert.throws(() => jsToTreeNode('this is not valid javascript {{{'), { + name: 'SyntaxError', + }); + }); + }); + + describe('ignoreStringPlugins', () => { + it('returns function plugins unchanged', () => { + const plugin1 = () => {}; + const plugin2 = () => {}; + const logger = new SpyIntegrationLogger(); + const result = ignoreStringPlugins([plugin1, plugin2], logger); + assert.equal(result.length, 2); + assert.equal(result[0], plugin1); + assert.equal(result[1], plugin2); + assert.equal(logger.messages.filter((m) => m.level === 'warn').length, 0); + }); + + it('filters out string-based plugins', () => { + const fnPlugin = () => {}; + const logger = new SpyIntegrationLogger(); + const result = ignoreStringPlugins(['remark-toc', fnPlugin], logger); + assert.equal(result.length, 1); + assert.equal(result[0], fnPlugin); + }); + + it('filters out array-based string plugins [string, options]', () => { + const fnPlugin = () => {}; + const logger = new SpyIntegrationLogger(); + const result = ignoreStringPlugins([['remark-toc', {}], fnPlugin], logger); + assert.equal(result.length, 1); + assert.equal(result[0], fnPlugin); + }); + + it('logs warnings for string plugins', () => { + const logger = new SpyIntegrationLogger(); + ignoreStringPlugins(['remark-toc', ['rehype-highlight', {}]], logger); + // One warning per string plugin + one summary warning + assert.equal(logger.messages.filter((m) => m.level === 'warn').length, 3); + }); + + it('returns empty array for all string plugins', () => { + const logger = new SpyIntegrationLogger(); + const result = ignoreStringPlugins(['remark-toc'], logger); + assert.equal(result.length, 0); + }); + + it('handles array-based function plugins [function, options]', () => { + const fnPlugin = () => {}; + const logger = new SpyIntegrationLogger(); + const result = ignoreStringPlugins([[fnPlugin, { option: true }]], logger); + assert.equal(result.length, 1); + assert.equal(logger.messages.filter((m) => m.level === 'warn').length, 0); + }); + }); +}); diff --git a/packages/integrations/mdx/test/units/vite-plugin-mdx-postprocess.test.ts b/packages/integrations/mdx/test/units/vite-plugin-mdx-postprocess.test.ts new file mode 100644 index 000000000000..8cbddd643e5e --- /dev/null +++ b/packages/integrations/mdx/test/units/vite-plugin-mdx-postprocess.test.ts @@ -0,0 +1,238 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { init, parse } from 'es-module-lexer'; +import { + annotateContentExport, + injectMetadataExports, + injectUnderscoreFragmentImport, + isSpecifierImported, + transformContentExport, +} from '../../dist/vite-plugin-mdx-postprocess.js'; + +await init; + +/** + * Helper: parse code with es-module-lexer and return [imports, exports] + */ +function parseCode(code: string) { + return parse(code); +} + +describe('vite-plugin-mdx-postprocess', () => { + describe('injectUnderscoreFragmentImport', () => { + it('injects Fragment import when not present', () => { + const code = `import { jsx } from 'astro/jsx-runtime';`; + const [imports] = parseCode(code); + const result = injectUnderscoreFragmentImport(code, imports); + assert.ok(result.includes("import { Fragment as _Fragment } from 'astro/jsx-runtime'")); + }); + + it('does not inject Fragment import when already present', () => { + const code = `import { jsx, Fragment as _Fragment } from 'astro/jsx-runtime';`; + const [imports] = parseCode(code); + const result = injectUnderscoreFragmentImport(code, imports); + // Should not have a second import + const importCount = (result.match(/Fragment as _Fragment/g) || []).length; + assert.equal(importCount, 1); + }); + + it('does not inject when _Fragment is imported with different spacing', () => { + const code = `import { _Fragment } from 'astro/jsx-runtime';`; + const [imports] = parseCode(code); + const result = injectUnderscoreFragmentImport(code, imports); + // _Fragment is in the import statement, regex should match + assert.ok(result.includes("import { _Fragment } from 'astro/jsx-runtime'")); + // Should not add a second Fragment import + const fragmentImports = result.match(/from 'astro\/jsx-runtime'/g) || []; + assert.equal(fragmentImports.length, 1); + }); + + it('injects Fragment import when import is from a different source', () => { + const code = `import { Fragment as _Fragment } from 'react/jsx-runtime';`; + const [imports] = parseCode(code); + const result = injectUnderscoreFragmentImport(code, imports); + assert.ok(result.includes("import { Fragment as _Fragment } from 'astro/jsx-runtime'")); + }); + }); + + describe('injectMetadataExports', () => { + it('injects url and file exports when not present', () => { + const code = `export const frontmatter = {};`; + const [, exports] = parseCode(code); + const result = injectMetadataExports(code, exports, { + fileUrl: '/test-page', + fileId: '/src/pages/test-page.mdx', + }); + assert.ok(result.includes('export const url = "/test-page"')); + assert.ok(result.includes('export const file = "/src/pages/test-page.mdx"')); + }); + + it('does not inject url export when already present', () => { + const code = `export const url = "/custom";`; + const [, exports] = parseCode(code); + const result = injectMetadataExports(code, exports, { + fileUrl: '/test-page', + fileId: '/src/pages/test-page.mdx', + }); + // Should not add a second url export + const urlExports = (result.match(/export const url/g) || []).length; + assert.equal(urlExports, 1); + // But should still add file + assert.ok(result.includes('export const file = "/src/pages/test-page.mdx"')); + }); + + it('does not inject file export when already present', () => { + const code = `export const file = "/custom.mdx";`; + const [, exports] = parseCode(code); + const result = injectMetadataExports(code, exports, { + fileUrl: '/test-page', + fileId: '/src/pages/test-page.mdx', + }); + const fileExports = (result.match(/export const file/g) || []).length; + assert.equal(fileExports, 1); + // But should still add url + assert.ok(result.includes('export const url = "/test-page"')); + }); + + it('escapes special characters in fileUrl and fileId', () => { + const code = `export const frontmatter = {};`; + const [, exports] = parseCode(code); + const result = injectMetadataExports(code, exports, { + fileUrl: '/path/with "quotes"', + fileId: '/src/pages/with "quotes".mdx', + }); + // JSON.stringify handles escaping + assert.ok(result.includes('export const url = "/path/with \\"quotes\\""')); + assert.ok(result.includes('export const file = "/src/pages/with \\"quotes\\".mdx"')); + }); + }); + + describe('transformContentExport', () => { + it('wraps MDXContent as Content export', () => { + const code = `export default function MDXContent(props) { return jsx("div", {}); }`; + const [, exports] = parseCode(code); + const result = transformContentExport(code, exports); + // Should remove "export default" from MDXContent + assert.ok(result.includes('function MDXContent')); + assert.ok(!result.includes('export default function MDXContent')); + // Should create Content wrapper + assert.ok(result.includes('export const Content')); + assert.ok(result.includes('export default Content')); + // Should pass Fragment + assert.ok(result.includes('Fragment: _Fragment')); + }); + + it('skips transformation when Content export already exists', () => { + const code = `export const Content = () => {};\nexport default function MDXContent(props) { return jsx("div", {}); }`; + const [, exports] = parseCode(code); + const result = transformContentExport(code, exports); + // Should return code unchanged + assert.equal(result, code); + }); + + it('includes components spread when components export exists', () => { + const code = [ + `export const components = { h1: CustomH1 };`, + `export default function MDXContent(props) { return jsx("div", {}); }`, + ].join('\n'); + const [, exports] = parseCode(code); + const result = transformContentExport(code, exports); + assert.ok(result.includes('...components')); + }); + + it('does not include components spread when no components export', () => { + const code = `export default function MDXContent(props) { return jsx("div", {}); }`; + const [, exports] = parseCode(code); + const result = transformContentExport(code, exports); + assert.ok(!result.includes('...components,')); + }); + + it('includes astro-image handling when __usesAstroImage flag is exported', () => { + const code = [ + `export const __usesAstroImage = true;`, + `export default function MDXContent(props) { return jsx("div", {}); }`, + ].join('\n'); + const [, exports] = parseCode(code); + const result = transformContentExport(code, exports); + assert.ok(result.includes('astro-image')); + }); + }); + + describe('annotateContentExport', () => { + it('adds mdx-component symbol', () => { + const code = `export const Content = () => {};`; + const [imports] = parseCode(code); + const result = annotateContentExport(code, '/test.mdx', false, imports); + assert.ok(result.includes("Content[Symbol.for('mdx-component')] = true")); + }); + + it('adds needsHeadRendering symbol', () => { + const code = `export const Content = () => {};`; + const [imports] = parseCode(code); + const result = annotateContentExport(code, '/test.mdx', false, imports); + assert.ok(result.includes("Content[Symbol.for('astro.needsHeadRendering')]")); + }); + + it('adds moduleId', () => { + const code = `export const Content = () => {};`; + const [imports] = parseCode(code); + const result = annotateContentExport(code, '/src/pages/test.mdx', false, imports); + assert.ok(result.includes('Content.moduleId = "/src/pages/test.mdx"')); + }); + + it('adds __astro_tag_component__ import and call in SSR mode', () => { + const code = `export const Content = () => {};`; + const [imports] = parseCode(code); + const result = annotateContentExport(code, '/test.mdx', true, imports); + assert.ok(result.includes('import { __astro_tag_component__ }')); + assert.ok(result.includes("__astro_tag_component__(Content, 'astro:jsx')")); + }); + + it('does not add __astro_tag_component__ in non-SSR mode', () => { + const code = `export const Content = () => {};`; + const [imports] = parseCode(code); + const result = annotateContentExport(code, '/test.mdx', false, imports); + assert.ok(!result.includes('__astro_tag_component__')); + }); + + it('does not duplicate __astro_tag_component__ import when already present', () => { + const code = `import { __astro_tag_component__ } from 'astro/runtime/server/index.js';\nexport const Content = () => {};`; + const [imports] = parseCode(code); + const result = annotateContentExport(code, '/test.mdx', true, imports); + const importCount = ( + result.match(/import.*__astro_tag_component__.*astro\/runtime\/server/g) || [] + ).length; + assert.equal(importCount, 1); + }); + }); + + describe('isSpecifierImported', () => { + it('returns true when specifier matches in correct source', () => { + const code = `import { Fragment as _Fragment } from 'astro/jsx-runtime';`; + const [imports] = parseCode(code); + const regex = /[\s,{]_Fragment[\s,}]/; + assert.equal(isSpecifierImported(code, imports, regex, 'astro/jsx-runtime'), true); + }); + + it('returns false when specifier is from different source', () => { + const code = `import { Fragment as _Fragment } from 'react/jsx-runtime';`; + const [imports] = parseCode(code); + const regex = /[\s,{]_Fragment[\s,}]/; + assert.equal(isSpecifierImported(code, imports, regex, 'astro/jsx-runtime'), false); + }); + + it('returns false when specifier is not imported', () => { + const code = `import { jsx } from 'astro/jsx-runtime';`; + const [imports] = parseCode(code); + const regex = /[\s,{]_Fragment[\s,}]/; + assert.equal(isSpecifierImported(code, imports, regex, 'astro/jsx-runtime'), false); + }); + + it('returns false with no imports', () => { + const code = `const x = 1;`; + const [imports] = parseCode(code); + const regex = /[\s,{]_Fragment[\s,}]/; + assert.equal(isSpecifierImported(code, imports, regex, 'astro/jsx-runtime'), false); + }); + }); +}); diff --git a/packages/integrations/mdx/tsconfig.test.json b/packages/integrations/mdx/tsconfig.test.json new file mode 100644 index 000000000000..853326403557 --- /dev/null +++ b/packages/integrations/mdx/tsconfig.test.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["test/**/*.ts"], + "exclude": ["test/fixtures/**"], + "compilerOptions": { + "allowJs": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "rewriteRelativeImportExtensions": true, + "rootDir": "." + }, + "references": [{ "path": "../../astro/tsconfig.test.json" }] +} diff --git a/packages/integrations/netlify/CHANGELOG.md b/packages/integrations/netlify/CHANGELOG.md index 50b0d84f3b1d..8c5add863ffd 100644 --- a/packages/integrations/netlify/CHANGELOG.md +++ b/packages/integrations/netlify/CHANGELOG.md @@ -1,5 +1,21 @@ # @astrojs/netlify +## 7.0.7 + +### Patch Changes + +- [#16027](https://github.com/withastro/astro/pull/16027) [`c62516b`](https://github.com/withastro/astro/commit/c62516bbbf8fdf95d38293440d28221c048c41f0) Thanks [@fkatsuhiro](https://github.com/fkatsuhiro)! - Fixes a bug where remote image dimensions were not validated during static builds on Netlify. + +- Updated dependencies []: + - @astrojs/underscore-redirects@1.0.3 + +## 7.0.6 + +### Patch Changes + +- Updated dependencies [[`814406d`](https://github.com/withastro/astro/commit/814406de7dc3ea014b47d2d886d55c45e4e1c034)]: + - @astrojs/underscore-redirects@1.0.3 + ## 7.0.5 ### Patch Changes diff --git a/packages/integrations/netlify/package.json b/packages/integrations/netlify/package.json index 4cfd4a79f9bf..1885aa72ac1e 100644 --- a/packages/integrations/netlify/package.json +++ b/packages/integrations/netlify/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/netlify", "description": "Deploy your site to Netlify", - "version": "7.0.5", + "version": "7.0.7", "type": "module", "types": "./dist/index.d.ts", "author": "withastro", @@ -33,10 +33,11 @@ "build": "astro-scripts build \"src/**/*.ts\" && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\"", "test": "pnpm run test-fn && pnpm run test-static && pnpm run test:dev", - "test-fn": "astro-scripts test \"test/functions/*.test.js\"", - "test:dev": "astro-scripts test \"test/development/*.test.js\"", - "test-static": "astro-scripts test \"test/static/*.test.js\"", - "test:hosted": "astro-scripts test \"test/hosted/*.test.js\"" + "test-fn": "astro-scripts test \"test/functions/*.test.ts\"", + "test:dev": "astro-scripts test \"test/development/*.test.ts\"", + "test-static": "astro-scripts test \"test/static/*.test.ts\"", + "test:hosted": "astro-scripts test \"test/hosted/*.test.ts\"", + "typecheck:tests": "tsc --build tsconfig.test.json" }, "dependencies": { "@astrojs/internal-helpers": "workspace:*", diff --git a/packages/integrations/netlify/src/image-service.ts b/packages/integrations/netlify/src/image-service.ts index 91c3596ad25c..f8e93a05abdc 100644 --- a/packages/integrations/netlify/src/image-service.ts +++ b/packages/integrations/netlify/src/image-service.ts @@ -1,5 +1,6 @@ import type { ExternalImageService } from 'astro'; import { baseService } from 'astro/assets'; +import { verifyOptions } from '../../../astro/dist/assets/internal.js'; import { isESMImportedImage } from 'astro/assets/utils'; import { AstroError } from 'astro/errors'; @@ -51,6 +52,8 @@ const service: ExternalImageService = { getHTMLAttributes: baseService.getHTMLAttributes, getSrcSet: baseService.getSrcSet, validateOptions(options) { + verifyOptions(options); + if (options.format && !SUPPORTED_FORMATS.includes(options.format)) { throw new AstroError( `Unsupported image format "${options.format}"`, diff --git a/packages/integrations/netlify/test/development/primitives.test.js b/packages/integrations/netlify/test/development/primitives.test.ts similarity index 93% rename from packages/integrations/netlify/test/development/primitives.test.js rename to packages/integrations/netlify/test/development/primitives.test.ts index 016305e42cae..2bad2bd8ce36 100644 --- a/packages/integrations/netlify/test/development/primitives.test.js +++ b/packages/integrations/netlify/test/development/primitives.test.ts @@ -2,14 +2,13 @@ import assert from 'node:assert/strict'; import { after, afterEach, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from '../../../../astro/test/test-utils.js'; import netlifyAdapter from '../../dist/index.js'; +import { type DevServer, type Fixture, loadFixture } from '../test-utils.ts'; describe('Netlify primitives', () => { describe('Development', () => { - /** @type {import('../../../../astro/test/test-utils').Fixture} */ - let fixture; - let devServer; + let fixture: Fixture; + let devServer: DevServer; before(async () => { fixture = await loadFixture({ root: new URL('./fixtures/primitives/', import.meta.url), @@ -101,7 +100,7 @@ describe('Netlify primitives', () => { } } finally { await cdnDisabledServer.stop(); - process.env.DISABLE_IMAGE_CDN = undefined; + delete process.env.DISABLE_IMAGE_CDN; } }); }); diff --git a/packages/integrations/netlify/test/functions/cookies.test.js b/packages/integrations/netlify/test/functions/cookies.test.js deleted file mode 100644 index 6ef16763ed5d..000000000000 --- a/packages/integrations/netlify/test/functions/cookies.test.js +++ /dev/null @@ -1,65 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { before, describe, it } from 'node:test'; -import { loadFixture } from '../../../../astro/test/test-utils.js'; - -describe( - 'Cookies', - () => { - let fixture; - - before(async () => { - fixture = await loadFixture({ root: new URL('./fixtures/cookies/', import.meta.url) }); - await fixture.build(); - }); - - it('Can set multiple', async () => { - const entryURL = new URL( - './fixtures/cookies/.netlify/v1/functions/ssr/ssr.mjs', - import.meta.url, - ); - const { default: handler } = await import(entryURL); - const resp = await handler( - new Request('http://example.com/login', { method: 'POST', body: '{}' }), - {}, - ); - assert.equal(resp.status, 301); - assert.equal(resp.headers.get('location'), '/'); - assert.deepEqual(resp.headers.getSetCookie(), ['foo=foo; HttpOnly', 'bar=bar; HttpOnly']); - }); - - it('Can set partitioned cookie', async () => { - const entryURL = new URL( - './fixtures/cookies/.netlify/v1/functions/ssr/ssr.mjs', - import.meta.url, - ); - const { default: handler } = await import(entryURL); - const resp = await handler(new Request('http://example.com/partitioned'), {}); - assert.equal(resp.status, 200); - const cookie = resp.headers.getSetCookie()[0]; - assert.ok(cookie.includes('Partitioned'), 'Cookie should include Partitioned attribute'); - }); - - it('renders dynamic 404 page', async () => { - const entryURL = new URL( - './fixtures/cookies/.netlify/v1/functions/ssr/ssr.mjs', - import.meta.url, - ); - const { default: handler } = await import(entryURL); - const resp = await handler( - new Request('http://example.com/nonexistant-page', { - headers: { - 'x-test': 'bar', - }, - }), - {}, - ); - assert.equal(resp.status, 404); - const text = await resp.text(); - assert.equal(text.includes('This is my custom 404 page'), true); - assert.equal(text.includes('x-test: bar'), true); - }); - }, - { - timeout: 120000, - }, -); diff --git a/packages/integrations/netlify/test/functions/cookies.test.ts b/packages/integrations/netlify/test/functions/cookies.test.ts new file mode 100644 index 000000000000..a412dc6537ec --- /dev/null +++ b/packages/integrations/netlify/test/functions/cookies.test.ts @@ -0,0 +1,59 @@ +import * as assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import { type Fixture, loadFixture } from '../test-utils.ts'; + +describe('Cookies', { timeout: 120000 }, () => { + let fixture: Fixture; + + before(async () => { + fixture = await loadFixture({ root: new URL('./fixtures/cookies/', import.meta.url) }); + await fixture.build(); + }); + + it('Can set multiple', async () => { + const entryURL = new URL( + './fixtures/cookies/.netlify/v1/functions/ssr/ssr.mjs', + import.meta.url, + ); + const { default: handler } = await import(entryURL.href); + const resp = await handler( + new Request('http://example.com/login', { method: 'POST', body: '{}' }), + {}, + ); + assert.equal(resp.status, 301); + assert.equal(resp.headers.get('location'), '/'); + assert.deepEqual(resp.headers.getSetCookie(), ['foo=foo; HttpOnly', 'bar=bar; HttpOnly']); + }); + + it('Can set partitioned cookie', async () => { + const entryURL = new URL( + './fixtures/cookies/.netlify/v1/functions/ssr/ssr.mjs', + import.meta.url, + ); + const { default: handler } = await import(entryURL.href); + const resp = await handler(new Request('http://example.com/partitioned'), {}); + assert.equal(resp.status, 200); + const cookie = resp.headers.getSetCookie()[0]!; + assert.ok(cookie.includes('Partitioned'), 'Cookie should include Partitioned attribute'); + }); + + it('renders dynamic 404 page', async () => { + const entryURL = new URL( + './fixtures/cookies/.netlify/v1/functions/ssr/ssr.mjs', + import.meta.url, + ); + const { default: handler } = await import(entryURL.href); + const resp = await handler( + new Request('http://example.com/nonexistant-page', { + headers: { + 'x-test': 'bar', + }, + }), + {}, + ); + assert.equal(resp.status, 404); + const text = await resp.text(); + assert.equal(text.includes('This is my custom 404 page'), true); + assert.equal(text.includes('x-test: bar'), true); + }); +}); diff --git a/packages/integrations/netlify/test/functions/edge-middleware.test.js b/packages/integrations/netlify/test/functions/edge-middleware.test.js deleted file mode 100644 index 6d255f4dc7f9..000000000000 --- a/packages/integrations/netlify/test/functions/edge-middleware.test.js +++ /dev/null @@ -1,66 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { after, before, describe, it } from 'node:test'; -import { loadFixture } from '../../../../astro/test/test-utils.js'; - -describe( - 'Middleware', - () => { - const root = new URL('./fixtures/middleware/', import.meta.url); - - describe('middlewareMode: classic', () => { - let fixture; - before(async () => { - process.env.EDGE_MIDDLEWARE = 'false'; - fixture = await loadFixture({ root }); - await fixture.build(); - }); - - it('emits no edge function', async () => { - assert.equal( - fixture.pathExists('../.netlify/v1/edge-functions/middleware/middleware.mjs'), - false, - ); - }); - - it('applies middleware to static files at build-time', async () => { - // prerendered page has middleware applied at build time - const prerenderedPage = await fixture.readFile('prerender/index.html'); - assert.equal(prerenderedPage.includes('Middleware'), true); - }); - - after(async () => { - process.env.EDGE_MIDDLEWARE = undefined; - await fixture.clean(); - }); - }); - - describe('middlewareMode: edge', () => { - let fixture; - before(async () => { - process.env.EDGE_MIDDLEWARE = 'true'; - fixture = await loadFixture({ root }); - await fixture.build(); - }); - - it('emits an edge function', async () => { - const contents = await fixture.readFile( - '../.netlify/v1/edge-functions/middleware/middleware.mjs', - ); - assert.equal(contents.includes('"Hello world"'), false); - }); - - it.skip('does not apply middleware during prerendering', async () => { - const prerenderedPage = await fixture.readFile('prerender/index.html'); - assert.equal(prerenderedPage.includes(''), true); - }); - - after(async () => { - process.env.EDGE_MIDDLEWARE = undefined; - await fixture.clean(); - }); - }); - }, - { - timeout: 120000, - }, -); diff --git a/packages/integrations/netlify/test/functions/edge-middleware.test.ts b/packages/integrations/netlify/test/functions/edge-middleware.test.ts new file mode 100644 index 000000000000..bc6abbe64877 --- /dev/null +++ b/packages/integrations/netlify/test/functions/edge-middleware.test.ts @@ -0,0 +1,60 @@ +import * as assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import { type Fixture, loadFixture } from '../test-utils.ts'; + +describe('Middleware', { timeout: 120000 }, () => { + const root = new URL('./fixtures/middleware/', import.meta.url); + + describe('middlewareMode: classic', () => { + let fixture: Fixture; + before(async () => { + process.env.EDGE_MIDDLEWARE = 'false'; + fixture = await loadFixture({ root }); + await fixture.build(); + }); + + it('emits no edge function', async () => { + assert.equal( + fixture.pathExists('../.netlify/v1/edge-functions/middleware/middleware.mjs'), + false, + ); + }); + + it('applies middleware to static files at build-time', async () => { + // prerendered page has middleware applied at build time + const prerenderedPage = await fixture.readFile('prerender/index.html'); + assert.equal(prerenderedPage.includes('Middleware'), true); + }); + + after(async () => { + delete process.env.EDGE_MIDDLEWARE; + await fixture.clean(); + }); + }); + + describe('middlewareMode: edge', () => { + let fixture: Fixture; + before(async () => { + process.env.EDGE_MIDDLEWARE = 'true'; + fixture = await loadFixture({ root }); + await fixture.build(); + }); + + it('emits an edge function', async () => { + const contents = await fixture.readFile( + '../.netlify/v1/edge-functions/middleware/middleware.mjs', + ); + assert.equal(contents.includes('"Hello world"'), false); + }); + + it.skip('does not apply middleware during prerendering', async () => { + const prerenderedPage = await fixture.readFile('prerender/index.html'); + assert.equal(prerenderedPage.includes(''), true); + }); + + after(async () => { + delete process.env.EDGE_MIDDLEWARE; + await fixture.clean(); + }); + }); +}); diff --git a/packages/integrations/netlify/test/functions/image-cdn.test.js b/packages/integrations/netlify/test/functions/image-cdn.test.js deleted file mode 100644 index 8d6196817607..000000000000 --- a/packages/integrations/netlify/test/functions/image-cdn.test.js +++ /dev/null @@ -1,182 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { after, before, describe, it } from 'node:test'; -import { remotePatternToRegex } from '@astrojs/netlify'; -import { loadFixture } from '../../../../astro/test/test-utils.js'; -import imageService from '../../dist/image-service.js'; - -describe( - 'Image CDN', - () => { - const root = new URL('./fixtures/middleware/', import.meta.url); - - describe('configuration', () => { - after(() => { - process.env.DISABLE_IMAGE_CDN = undefined; - }); - - it('enables Netlify Image CDN', async () => { - const fixture = await loadFixture({ root }); - await fixture.build(); - - const astronautPage = await fixture.readFile('astronaut/index.html'); - assert.equal(astronautPage.includes(`src="/.netlify/image`), true); - }); - - it('respects image CDN opt-out', async () => { - process.env.DISABLE_IMAGE_CDN = 'true'; - const fixture = await loadFixture({ root }); - await fixture.build(); - - const astronautPage = await fixture.readFile('astronaut/index.html'); - assert.equal(astronautPage.includes(`src="/_astro/astronaut.`), true); - }); - }); - - describe('remote image config', () => { - let regexes; - - before(async () => { - const fixture = await loadFixture({ root }); - await fixture.build(); - - const config = await fixture.readFile('../.netlify/v1/config.json'); - if (config) { - regexes = JSON.parse(config).images.remote_images.map((pattern) => new RegExp(pattern)); - } - }); - - it('generates remote image config patterns', async () => { - assert.equal(regexes?.length, 3); - }); - - it('generates correct config for domains', async () => { - const domain = regexes[0]; - assert.equal(domain.test('https://example.net/image.jpg'), true); - assert.equal( - domain.test('https://www.example.net/image.jpg'), - false, - 'subdomain should not match', - ); - assert.equal(domain.test('http://example.net/image.jpg'), true, 'http should match'); - assert.equal( - domain.test('https://example.net/subdomain/image.jpg'), - true, - 'subpath should match', - ); - const subdomain = regexes[1]; - assert.equal( - subdomain.test('https://secret.example.edu/image.jpg'), - true, - 'should match subdomains', - ); - assert.equal( - subdomain.test('https://secretxexample.edu/image.jpg'), - false, - 'should not use dots in domains as wildcards', - ); - }); - - it('generates correct config for remotePatterns', async () => { - const patterns = regexes[2]; - assert.equal( - patterns.test('https://example.org/images/1.jpg'), - true, - 'should match domain', - ); - assert.equal( - patterns.test('https://www.example.org/images/2.jpg'), - true, - 'www subdomain should match', - ); - assert.equal( - patterns.test('https://www.subdomain.example.org/images/2.jpg'), - false, - 'second level subdomain should not match', - ); - assert.equal( - patterns.test('https://example.org/not-images/2.jpg'), - false, - 'wrong path should not match', - ); - }); - - it('warns when remotepatterns generates an invalid regex', async (t) => { - const logger = { - warn: t.mock.fn(), - }; - const regex = remotePatternToRegex( - { - hostname: '*.examp[le.org', - pathname: '/images/*', - }, - logger, - ); - assert.strictEqual(regex, undefined); - const calls = logger.warn.mock.calls; - assert.strictEqual(calls.length, 1); - assert.equal( - calls[0].arguments[0], - 'Could not generate a valid regex from the remotePattern "{"hostname":"*.examp[le.org","pathname":"/images/*"}". Please check the syntax.', - ); - }); - }); - - describe('fit parameter', () => { - it('includes fit parameter in image URL', () => { - const url = imageService.getURL({ - src: 'images/astronaut.jpg', - width: 300, - height: 400, - fit: 'cover', - format: 'webp', - }); - assert.ok(url.includes('fit=cover'), `Expected fit=cover in URL, got: ${url}`); - }); - - it('maps Astro fit values to Netlify equivalents', () => { - const cases = [ - ['contain', 'contain'], - ['cover', 'cover'], - ['fill', 'fill'], - ['inside', 'contain'], - ['outside', 'cover'], - ['scale-down', 'contain'], - ]; - for (const [astroFit, netlifyFit] of cases) { - const url = imageService.getURL({ - src: 'img.jpg', - width: 100, - height: 100, - fit: astroFit, - }); - assert.ok( - url.includes(`fit=${netlifyFit}`), - `Expected fit=${netlifyFit} for astro fit="${astroFit}", got: ${url}`, - ); - } - }); - - it('omits fit parameter when fit is none or unset', () => { - const withNone = imageService.getURL({ - src: 'img.jpg', - width: 100, - height: 100, - fit: 'none', - }); - assert.ok( - !withNone.includes('fit='), - `Expected no fit param for fit="none", got: ${withNone}`, - ); - - const withoutFit = imageService.getURL({ src: 'img.jpg', width: 100, height: 100 }); - assert.ok( - !withoutFit.includes('fit='), - `Expected no fit param when unset, got: ${withoutFit}`, - ); - }); - }); - }, - { - timeout: 120000, - }, -); diff --git a/packages/integrations/netlify/test/functions/image-cdn.test.ts b/packages/integrations/netlify/test/functions/image-cdn.test.ts new file mode 100644 index 000000000000..709c306ec3c1 --- /dev/null +++ b/packages/integrations/netlify/test/functions/image-cdn.test.ts @@ -0,0 +1,184 @@ +import * as assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import { remotePatternToRegex } from '@astrojs/netlify'; +import imageService from '../../dist/image-service.js'; +import { loadFixture, SpyIntegrationLogger } from '../test-utils.ts'; +import type { ImageTransform } from 'astro'; + +async function getURL(options: ImageTransform) { + return await imageService.getURL( + options, + // @ts-expect-error The second argument is not used in the current + // implementation of `imageService.getURL`, but we need to pass it + // to satisfy the type signature. + {}, + ); +} + +describe('Image CDN', { timeout: 120000 }, () => { + const root = new URL('./fixtures/middleware/', import.meta.url); + + describe('configuration', () => { + after(() => { + delete process.env.DISABLE_IMAGE_CDN; + }); + + it('enables Netlify Image CDN', async () => { + const fixture = await loadFixture({ root }); + await fixture.build(); + + const astronautPage = await fixture.readFile('astronaut/index.html'); + assert.equal(astronautPage.includes(`src="/.netlify/image`), true); + }); + + it('respects image CDN opt-out', async () => { + process.env.DISABLE_IMAGE_CDN = 'true'; + const fixture = await loadFixture({ root }); + await fixture.build(); + + const astronautPage = await fixture.readFile('astronaut/index.html'); + assert.equal(astronautPage.includes(`src="/_astro/astronaut.`), true); + }); + }); + + describe('remote image config', () => { + let regexes: RegExp[]; + + before(async () => { + const fixture = await loadFixture({ root }); + await fixture.build(); + + const config = await fixture.readFile('../.netlify/v1/config.json'); + if (config) { + regexes = JSON.parse(config).images.remote_images.map( + (pattern: string) => new RegExp(pattern), + ); + } + }); + + it('generates remote image config patterns', async () => { + assert.equal(regexes?.length, 3); + }); + + it('generates correct config for domains', async () => { + const domain = regexes[0]!; + assert.equal(domain.test('https://example.net/image.jpg'), true); + assert.equal( + domain.test('https://www.example.net/image.jpg'), + false, + 'subdomain should not match', + ); + assert.equal(domain.test('http://example.net/image.jpg'), true, 'http should match'); + assert.equal( + domain.test('https://example.net/subdomain/image.jpg'), + true, + 'subpath should match', + ); + const subdomain = regexes[1]!; + assert.equal( + subdomain.test('https://secret.example.edu/image.jpg'), + true, + 'should match subdomains', + ); + assert.equal( + subdomain.test('https://secretxexample.edu/image.jpg'), + false, + 'should not use dots in domains as wildcards', + ); + }); + + it('generates correct config for remotePatterns', async () => { + const patterns = regexes[2]!; + assert.equal(patterns.test('https://example.org/images/1.jpg'), true, 'should match domain'); + assert.equal( + patterns.test('https://www.example.org/images/2.jpg'), + true, + 'www subdomain should match', + ); + assert.equal( + patterns.test('https://www.subdomain.example.org/images/2.jpg'), + false, + 'second level subdomain should not match', + ); + assert.equal( + patterns.test('https://example.org/not-images/2.jpg'), + false, + 'wrong path should not match', + ); + }); + + it('warns when remotepatterns generates an invalid regex', async () => { + const logger = new SpyIntegrationLogger(); + const regex = remotePatternToRegex( + { + hostname: '*.examp[le.org', + pathname: '/images/*', + }, + logger, + ); + assert.strictEqual(regex, undefined); + assert.strictEqual(logger.messages.length, 1); + const message = logger.messages[0]; + assert.equal(message.level, 'warn'); + assert.equal( + message.message, + 'Could not generate a valid regex from the remotePattern "{"hostname":"*.examp[le.org","pathname":"/images/*"}". Please check the syntax.', + ); + }); + }); + + describe('fit parameter', () => { + it('includes fit parameter in image URL', async () => { + const url = await getURL({ + src: 'images/astronaut.jpg', + width: 300, + height: 400, + fit: 'cover', + format: 'webp', + }); + assert.ok(url.includes('fit=cover'), `Expected fit=cover in URL, got: ${url}`); + }); + + it('maps Astro fit values to Netlify equivalents', async () => { + const cases = [ + ['contain', 'contain'], + ['cover', 'cover'], + ['fill', 'fill'], + ['inside', 'contain'], + ['outside', 'cover'], + ['scale-down', 'contain'], + ]; + for (const [astroFit, netlifyFit] of cases) { + const url = await getURL({ + src: 'img.jpg', + width: 100, + height: 100, + fit: astroFit, + }); + assert.ok( + url.includes(`fit=${netlifyFit}`), + `Expected fit=${netlifyFit} for astro fit="${astroFit}", got: ${url}`, + ); + } + }); + + it('omits fit parameter when fit is none or unset', async () => { + const withNone = await getURL({ + src: 'img.jpg', + width: 100, + height: 100, + fit: 'none', + }); + assert.ok( + !withNone.includes('fit='), + `Expected no fit param for fit="none", got: ${withNone}`, + ); + + const withoutFit = await getURL({ src: 'img.jpg', width: 100, height: 100 }); + assert.ok( + !withoutFit.includes('fit='), + `Expected no fit param when unset, got: ${withoutFit}`, + ); + }); + }); +}); diff --git a/packages/integrations/netlify/test/functions/include-files.test.js b/packages/integrations/netlify/test/functions/include-files.test.js deleted file mode 100644 index e54e116a78c3..000000000000 --- a/packages/integrations/netlify/test/functions/include-files.test.js +++ /dev/null @@ -1,184 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { existsSync } from 'node:fs'; -import { after, before, describe, it } from 'node:test'; -import netlify from '@astrojs/netlify'; -import * as cheerio from 'cheerio'; -import { globSync } from 'tinyglobby'; -import { loadFixture } from '../../../../astro/test/test-utils.js'; - -describe( - 'Included vite assets files', - () => { - let fixture; - - const root = new URL('./fixtures/includes/', import.meta.url); - const expectedCwd = new URL('.netlify/v1/functions/ssr/packages/integrations/netlify/', root); - - const expectedAssetsInclude = ['./*.json']; - const excludedAssets = ['./files/exclude-asset.json']; - - before(async () => { - fixture = await loadFixture({ - root, - vite: { - assetsInclude: expectedAssetsInclude, - }, - adapter: netlify({ - excludeFiles: excludedAssets, - }), - }); - await fixture.build(); - }); - - it('Emits vite assets files', async () => { - for (const pattern of expectedAssetsInclude) { - const files = globSync(pattern); - for (const file of files) { - assert.ok( - existsSync(new URL(file, expectedCwd)), - `Expected file ${pattern} to exist in build`, - ); - } - } - }); - - it('Does not include vite assets files when excluded', async () => { - for (const file of excludedAssets) { - assert.ok( - !existsSync(new URL(file, expectedCwd)), - `Expected file ${file} to not exist in build`, - ); - } - }); - - after(async () => { - await fixture.clean(); - }); - }, - { - timeout: 120000, - }, -); - -describe( - 'Included files', - () => { - let fixture; - - const root = new URL('./fixtures/includes/', import.meta.url); - const expectedCwd = new URL( - '.netlify/v1/functions/ssr/packages/integrations/netlify/test/functions/fixtures/includes/', - root, - ); - - const expectedFiles = [ - './files/include-this.txt', - './files/also-this.csv', - './files/subdirectory/and-this.csv', - ]; - - before(async () => { - fixture = await loadFixture({ - root, - adapter: netlify({ - includeFiles: expectedFiles, - }), - }); - await fixture.build(); - }); - - it('Emits include files', async () => { - for (const file of expectedFiles) { - assert.ok(existsSync(new URL(file, expectedCwd)), `Expected file ${file} to exist`); - } - }); - - it('Can load included files correctly', async () => { - const entryURL = new URL( - './fixtures/includes/.netlify/v1/functions/ssr/ssr.mjs', - import.meta.url, - ); - const { default: handler } = await import(entryURL); - const resp = await handler(new Request('http://example.com/?file=include-this.txt'), {}); - const html = await resp.text(); - const $ = cheerio.load(html); - assert.equal($('h1').text(), 'hello'); - }); - - it('Includes traced node modules with symlinks', async () => { - const expected = new URL( - '.netlify/v1/functions/ssr/node_modules/.pnpm/cowsay@1.6.0/node_modules/cowsay/cows/happy-whale.cow', - root, - ); - assert.ok(existsSync(expected, 'Expected excluded file to exist in default build')); - }); - - after(async () => { - await fixture.clean(); - }); - }, - { - timeout: 120000, - }, -); - -describe( - 'Excluded files', - () => { - let fixture; - - const root = new URL('./fixtures/includes/', import.meta.url); - const expectedCwd = new URL( - '.netlify/v1/functions/ssr/packages/integrations/netlify/test/functions/fixtures/includes/', - root, - ); - - const includeFiles = ['./files/**/*.txt']; - const excludedTxt = ['./files/subdirectory/not-this.txt', './files/subdirectory/or-this.txt']; - const excludeFiles = [...excludedTxt, '../../../../../../../node_modules/.pnpm/cowsay@*/**']; - - before(async () => { - fixture = await loadFixture({ - root, - adapter: netlify({ - includeFiles: includeFiles, - excludeFiles: excludeFiles, - }), - }); - await fixture.build(); - }); - - it('Excludes traced node modules', async () => { - const expected = new URL( - '.netlify/v1/functions/ssr/node_modules/.pnpm/cowsay@1.6.0/node_modules/cowsay/cows/happy-whale.cow', - root, - ); - assert.ok(!existsSync(expected), 'Expected excluded file to not exist in build'); - }); - - it('Does not include files when excluded', async () => { - for (const pattern of includeFiles) { - const files = globSync(pattern, { ignore: excludedTxt }); - for (const file of files) { - assert.ok( - existsSync(new URL(file, expectedCwd)), - `Expected file ${pattern} to exist in build`, - ); - } - } - for (const file of excludedTxt) { - assert.ok( - !existsSync(new URL(file, expectedCwd)), - `Expected file ${file} to not exist in build`, - ); - } - }); - - after(async () => { - await fixture.clean(); - }); - }, - { - timeout: 120000, - }, -); diff --git a/packages/integrations/netlify/test/functions/include-files.test.ts b/packages/integrations/netlify/test/functions/include-files.test.ts new file mode 100644 index 000000000000..5efdd6227869 --- /dev/null +++ b/packages/integrations/netlify/test/functions/include-files.test.ts @@ -0,0 +1,166 @@ +import * as assert from 'node:assert/strict'; +import { existsSync } from 'node:fs'; +import { after, before, describe, it } from 'node:test'; +import netlify from '@astrojs/netlify'; +import * as cheerio from 'cheerio'; +import { globSync } from 'tinyglobby'; +import { type Fixture, loadFixture } from '../test-utils.ts'; + +describe('Included vite assets files', { timeout: 120000 }, () => { + let fixture: Fixture; + + const root = new URL('./fixtures/includes/', import.meta.url); + const expectedCwd = new URL('.netlify/v1/functions/ssr/packages/integrations/netlify/', root); + + const expectedAssetsInclude = ['./*.json']; + const excludedAssets = ['./files/exclude-asset.json']; + + before(async () => { + fixture = await loadFixture({ + root, + vite: { + assetsInclude: expectedAssetsInclude, + }, + adapter: netlify({ + excludeFiles: excludedAssets, + }), + }); + await fixture.build(); + }); + + it('Emits vite assets files', async () => { + for (const pattern of expectedAssetsInclude) { + const files = globSync(pattern); + for (const file of files) { + assert.ok( + existsSync(new URL(file, expectedCwd)), + `Expected file ${pattern} to exist in build`, + ); + } + } + }); + + it('Does not include vite assets files when excluded', async () => { + for (const file of excludedAssets) { + assert.ok( + !existsSync(new URL(file, expectedCwd)), + `Expected file ${file} to not exist in build`, + ); + } + }); + + after(async () => { + await fixture.clean(); + }); +}); + +describe('Included files', { timeout: 120000 }, () => { + let fixture: Fixture; + + const root = new URL('./fixtures/includes/', import.meta.url); + const expectedCwd = new URL( + '.netlify/v1/functions/ssr/packages/integrations/netlify/test/functions/fixtures/includes/', + root, + ); + + const expectedFiles = [ + './files/include-this.txt', + './files/also-this.csv', + './files/subdirectory/and-this.csv', + ]; + + before(async () => { + fixture = await loadFixture({ + root, + adapter: netlify({ + includeFiles: expectedFiles, + }), + }); + await fixture.build(); + }); + + it('Emits include files', async () => { + for (const file of expectedFiles) { + assert.ok(existsSync(new URL(file, expectedCwd)), `Expected file ${file} to exist`); + } + }); + + it('Can load included files correctly', async () => { + const entryURL = new URL( + './fixtures/includes/.netlify/v1/functions/ssr/ssr.mjs', + import.meta.url, + ); + const { default: handler } = await import(entryURL.href); + const resp = await handler(new Request('http://example.com/?file=include-this.txt'), {}); + const html = await resp.text(); + const $ = cheerio.load(html); + assert.equal($('h1').text(), 'hello'); + }); + + it('Includes traced node modules with symlinks', async () => { + const expected = new URL( + '.netlify/v1/functions/ssr/node_modules/.pnpm/cowsay@1.6.0/node_modules/cowsay/cows/happy-whale.cow', + root, + ); + assert.ok(existsSync(expected), 'Expected excluded file to exist in default build'); + }); + + after(async () => { + await fixture.clean(); + }); +}); + +describe('Excluded files', { timeout: 120000 }, () => { + let fixture: Fixture; + + const root = new URL('./fixtures/includes/', import.meta.url); + const expectedCwd = new URL( + '.netlify/v1/functions/ssr/packages/integrations/netlify/test/functions/fixtures/includes/', + root, + ); + + const includeFiles = ['./files/**/*.txt']; + const excludedTxt = ['./files/subdirectory/not-this.txt', './files/subdirectory/or-this.txt']; + const excludeFiles = [...excludedTxt, '../../../../../../../node_modules/.pnpm/cowsay@*/**']; + + before(async () => { + fixture = await loadFixture({ + root, + adapter: netlify({ + includeFiles: includeFiles, + excludeFiles: excludeFiles, + }), + }); + await fixture.build(); + }); + + it('Excludes traced node modules', async () => { + const expected = new URL( + '.netlify/v1/functions/ssr/node_modules/.pnpm/cowsay@1.6.0/node_modules/cowsay/cows/happy-whale.cow', + root, + ); + assert.ok(!existsSync(expected), 'Expected excluded file to not exist in build'); + }); + + it('Does not include files when excluded', async () => { + for (const pattern of includeFiles) { + const files = globSync(pattern, { ignore: excludedTxt }); + for (const file of files) { + assert.ok( + existsSync(new URL(file, expectedCwd)), + `Expected file ${pattern} to exist in build`, + ); + } + } + for (const file of excludedTxt) { + assert.ok( + !existsSync(new URL(file, expectedCwd)), + `Expected file ${file} to not exist in build`, + ); + } + }); + + after(async () => { + await fixture.clean(); + }); +}); diff --git a/packages/integrations/netlify/test/functions/redirects.test.js b/packages/integrations/netlify/test/functions/redirects.test.js deleted file mode 100644 index 01bddc4c9a28..000000000000 --- a/packages/integrations/netlify/test/functions/redirects.test.js +++ /dev/null @@ -1,69 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { createServer } from 'node:http'; -import { before, describe, it } from 'node:test'; -import { loadFixture } from '../../../../astro/test/test-utils.js'; - -describe( - 'SSR - Redirects', - () => { - let fixture; - - before(async () => { - fixture = await loadFixture({ root: new URL('./fixtures/redirects/', import.meta.url) }); - await fixture.build(); - }); - - it('Creates a redirects file', async () => { - const redirects = await fixture.readFile('./_redirects'); - const parts = redirects.split(/\s+/); - assert.deepEqual(parts, ['', '/other', '/', '301', '']); - // Snapshots are not supported in Node.js test yet (https://github.com/nodejs/node/issues/48260) - assert.equal(redirects, '\n/other / 301\n'); - }); - - it('Does not create .html files', async () => { - let hasErrored = false; - try { - await fixture.readFile('/other/index.html'); - } catch { - hasErrored = true; - } - assert.equal(hasErrored, true, 'this file should not exist'); - }); - - it('renders static 404 page', async () => { - const entryURL = new URL( - './fixtures/redirects/.netlify/v1/functions/ssr/ssr.mjs', - import.meta.url, - ); - const { default: handler } = await import(entryURL); - const resp = await handler(new Request('http://example.com/nonexistant-page'), {}); - assert.equal(resp.status, 404); - assert.equal(resp.headers.get('content-type'), 'text/html; charset=utf-8'); - const text = await resp.text(); - assert.equal(text.includes('This is my static 404 page'), true); - }); - - it('does not pass through 404 request', async () => { - let testServerCalls = 0; - const testServer = createServer((_req, res) => { - testServerCalls++; - res.writeHead(200); - res.end(); - }); - testServer.listen(5678); - const entryURL = new URL( - './fixtures/redirects/.netlify/v1/functions/ssr/ssr.mjs', - import.meta.url, - ); - const { default: handler } = await import(entryURL); - const resp = await handler(new Request('http://localhost:5678/nonexistant-page'), {}); - assert.equal(resp.status, 404); - assert.equal(testServerCalls, 0); - testServer.close(); - }); - }, - { - timeout: 120000, - }, -); diff --git a/packages/integrations/netlify/test/functions/redirects.test.ts b/packages/integrations/netlify/test/functions/redirects.test.ts new file mode 100644 index 000000000000..553885096486 --- /dev/null +++ b/packages/integrations/netlify/test/functions/redirects.test.ts @@ -0,0 +1,62 @@ +import * as assert from 'node:assert/strict'; +import { createServer } from 'node:http'; +import { before, describe, it } from 'node:test'; +import { type Fixture, loadFixture } from '../test-utils.ts'; + +describe('SSR - Redirects', { timeout: 120000 }, () => { + let fixture: Fixture; + + before(async () => { + fixture = await loadFixture({ root: new URL('./fixtures/redirects/', import.meta.url) }); + await fixture.build(); + }); + + it('Creates a redirects file', async () => { + const redirects = await fixture.readFile('./_redirects'); + const parts = redirects.split(/\s+/); + // based on https://github.com/withastro/astro/issues/16030 for the default option `trailingSlash: 'ignore'` both variants should be generated + assert.deepEqual(parts, ['', '/other/', '/', '301', '/other', '/', '301', '']); + }); + + it('Does not create .html files', async () => { + let hasErrored = false; + try { + await fixture.readFile('/other/index.html'); + } catch { + hasErrored = true; + } + assert.equal(hasErrored, true, 'this file should not exist'); + }); + + it('renders static 404 page', async () => { + const entryURL = new URL( + './fixtures/redirects/.netlify/v1/functions/ssr/ssr.mjs', + import.meta.url, + ); + const { default: handler } = await import(entryURL.href); + const resp = await handler(new Request('http://example.com/nonexistant-page'), {}); + assert.equal(resp.status, 404); + assert.equal(resp.headers.get('content-type'), 'text/html; charset=utf-8'); + const text = await resp.text(); + assert.equal(text.includes('This is my static 404 page'), true); + }); + + it('does not pass through 404 request', async () => { + let testServerCalls = 0; + const testServer = createServer((_req, res) => { + testServerCalls++; + res.writeHead(200); + res.end(); + }); + testServer.listen(5678); + const entryURL = new URL( + './fixtures/redirects/.netlify/v1/functions/ssr/ssr.mjs', + import.meta.url, + ); + const { default: handler } = await import(entryURL.href); + const resp = await handler(new Request('http://localhost:5678/nonexistant-page'), {}); + assert.equal(resp.status, 404); + assert.equal(testServerCalls, 0); + testServer.close(); + }); +}); diff --git a/packages/integrations/netlify/test/functions/sessions.test.js b/packages/integrations/netlify/test/functions/sessions.test.ts similarity index 83% rename from packages/integrations/netlify/test/functions/sessions.test.js rename to packages/integrations/netlify/test/functions/sessions.test.ts index 7b41cb129648..99c0283ccb76 100644 --- a/packages/integrations/netlify/test/functions/sessions.test.js +++ b/packages/integrations/netlify/test/functions/sessions.test.ts @@ -1,12 +1,11 @@ -// @ts-check import assert from 'node:assert/strict'; import { mkdir, rm } from 'node:fs/promises'; import { after, before, describe, it } from 'node:test'; import { BlobsServer } from '@netlify/blobs/server'; +import { sessionDrivers } from 'astro/config'; import * as devalue from 'devalue'; -import { loadFixture } from '../../../../astro/test/test-utils.js'; import netlify from '../../dist/index.js'; -import { sessionDrivers } from 'astro/config'; +import { type Fixture, loadFixture } from '../test-utils.ts'; const token = 'mock'; const siteID = '1'; @@ -14,11 +13,9 @@ const dataDir = '.netlify/sessions'; describe('Astro.session', () => { describe('Production', () => { - /** @type {import('../../../../astro/test/test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; - /** @type {BlobsServer} */ - let blobServer; + let blobServer: BlobsServer; before(async () => { process.env.NETLIFY = '1'; await rm(dataDir, { recursive: true, force: true }).catch(() => {}); @@ -30,11 +27,11 @@ describe('Astro.session', () => { }); await blobServer.start(); fixture = await loadFixture({ - // @ts-ignore root: new URL('./fixtures/sessions/', import.meta.url), output: 'server', adapter: netlify(), session: { + // @ts-expect-error: the default type of the TDriver in AstroUserConfig must be changed so that this can pass driver: sessionDrivers.netlifyBlobs({ name: 'test', uncachedEdgeURL: `http://localhost:8971`, @@ -52,24 +49,19 @@ describe('Astro.session', () => { const mod = await import(entryURL.href); handler = mod.default; }); - /** @type {(request: Request, options: {}) => Promise} */ - let handler; + let handler: (request: Request, options: object) => Promise; after(async () => { await blobServer.stop(); delete process.env.NETLIFY; }); - /** - * @param {string} path - * @param {RequestInit} requestInit - */ - function fetchResponse(path, requestInit) { + function fetchResponse(path: string, requestInit: RequestInit) { return handler(new Request(new URL(path, 'http://example.com'), requestInit), {}); } it('can regenerate session cookies upon request', async () => { const firstResponse = await fetchResponse('/regenerate', { method: 'GET' }); const firstHeaders = firstResponse.headers.get('set-cookie')?.split(',') ?? ''; - const firstSessionId = firstHeaders[0].split(';')[0].split('=')[1]; + const firstSessionId = firstHeaders[0]!.split(';')[0]!.split('=')[1]; const secondResponse = await fetchResponse('/regenerate', { method: 'GET', @@ -78,7 +70,7 @@ describe('Astro.session', () => { }, }); const secondHeaders = secondResponse.headers.get('set-cookie')?.split(',') ?? ''; - const secondSessionId = secondHeaders[0].split(';')[0].split('=')[1]; + const secondSessionId = secondHeaders[0]!.split(';')[0]!.split('=')[1]; assert.notEqual(firstSessionId, secondSessionId); }); @@ -88,7 +80,7 @@ describe('Astro.session', () => { assert.equal(firstValue.previousValue, 'none'); const firstHeaders = firstResponse.headers.get('set-cookie')?.split(',') ?? ''; - const firstSessionId = firstHeaders[0].split(';')[0].split('=')[1]; + const firstSessionId = firstHeaders[0]!.split(';')[0]!.split('=')[1]; const secondResponse = await fetchResponse('/update', { method: 'GET', headers: { @@ -110,7 +102,7 @@ describe('Astro.session', () => { assert.equal(firstResponse.ok, true); const firstHeaders = firstResponse.headers.get('set-cookie')?.split(',') ?? ''; - const firstSessionId = firstHeaders[0].split(';')[0].split('=')[1]; + const firstSessionId = firstHeaders[0]!.split(';')[0]!.split('=')[1]; const data = devalue.parse(await firstResponse.text()); assert.equal(data.message, 'Favorite URL set to https://domain.invalid/ from nothing'); diff --git a/packages/integrations/netlify/test/functions/skew-protection.test.js b/packages/integrations/netlify/test/functions/skew-protection.test.js deleted file mode 100644 index ee5f8c840689..000000000000 --- a/packages/integrations/netlify/test/functions/skew-protection.test.js +++ /dev/null @@ -1,70 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { readFile } from 'node:fs/promises'; -import { before, describe, it } from 'node:test'; -import { loadFixture } from '../../../../astro/test/test-utils.js'; - -describe( - 'Skew Protection', - () => { - let fixture; - - before(async () => { - // Set DEPLOY_ID env var for the test - process.env.DEPLOY_ID = 'test-deploy-123'; - - fixture = await loadFixture({ - root: new URL('./fixtures/skew-protection/', import.meta.url), - }); - await fixture.build(); - - // Clean up - delete process.env.DEPLOY_ID; - }); - - it('Server islands inline adapter headers', async () => { - // Render a page with server islands and check the HTML contains inline headers - const entryURL = new URL( - './fixtures/skew-protection/.netlify/v1/functions/ssr/ssr.mjs', - import.meta.url, - ); - const { default: handler } = await import(entryURL); - const resp = await handler(new Request('http://example.com/server-island'), {}); - const html = await resp.text(); - - // Check that the HTML contains the inline headers in the server island script - // Should have something like: const headers = new Headers({"X-Netlify-Deploy-ID":"test-deploy-123"}); - assert.ok( - html.includes('test-deploy-123'), - 'Expected server island HTML to include deploy ID in inline script', - ); - }); - - it('Manifest contains internalFetchHeaders', async () => { - // The manifest is embedded in the build output - // Check the manifest file which contains the serialized manifest - const manifestURL = new URL( - './fixtures/skew-protection/.netlify/build/chunks/', - import.meta.url, - ); - - // Find the manifest file (it has a hash in the name) - const { readdir } = await import('node:fs/promises'); - const files = await readdir(manifestURL); - let found = false; - for (const file of files) { - const contents = await readFile(new URL(file, manifestURL), 'utf-8'); - if (contents.includes('"internalFetchHeaders":{"X-Netlify-Deploy-ID":"test-deploy-123"}')) { - found = true; - break; - } - } - assert.ok( - found, - 'Manifest should include internalFetchHeaders field with the correct deploy ID value', - ); - }); - }, - { - timeout: 120000, - }, -); diff --git a/packages/integrations/netlify/test/functions/skew-protection.test.ts b/packages/integrations/netlify/test/functions/skew-protection.test.ts new file mode 100644 index 000000000000..b92f03cb958b --- /dev/null +++ b/packages/integrations/netlify/test/functions/skew-protection.test.ts @@ -0,0 +1,64 @@ +import * as assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { before, describe, it } from 'node:test'; +import { type Fixture, loadFixture } from '../test-utils.ts'; + +describe('Skew Protection', { timeout: 120000 }, () => { + let fixture: Fixture; + + before(async () => { + // Set DEPLOY_ID env var for the test + process.env.DEPLOY_ID = 'test-deploy-123'; + + fixture = await loadFixture({ + root: new URL('./fixtures/skew-protection/', import.meta.url), + }); + await fixture.build(); + + // Clean up + delete process.env.DEPLOY_ID; + }); + + it('Server islands inline adapter headers', async () => { + // Render a page with server islands and check the HTML contains inline headers + const entryURL = new URL( + './fixtures/skew-protection/.netlify/v1/functions/ssr/ssr.mjs', + import.meta.url, + ); + const { default: handler } = await import(entryURL.href); + const resp = await handler(new Request('http://example.com/server-island'), {}); + const html = await resp.text(); + + // Check that the HTML contains the inline headers in the server island script + // Should have something like: const headers = new Headers({"X-Netlify-Deploy-ID":"test-deploy-123"}); + assert.ok( + html.includes('test-deploy-123'), + 'Expected server island HTML to include deploy ID in inline script', + ); + }); + + it('Manifest contains internalFetchHeaders', async () => { + // The manifest is embedded in the build output + // Check the manifest file which contains the serialized manifest + const manifestURL = new URL( + './fixtures/skew-protection/.netlify/build/chunks/', + import.meta.url, + ); + + // Find the manifest file (it has a hash in the name) + const { readdir } = await import('node:fs/promises'); + const files = await readdir(manifestURL); + let found = false; + for (const file of files) { + const contents = await readFile(new URL(file, manifestURL), 'utf-8'); + if (contents.includes('"internalFetchHeaders":{"X-Netlify-Deploy-ID":"test-deploy-123"}')) { + found = true; + break; + } + } + assert.ok( + found, + 'Manifest should include internalFetchHeaders field with the correct deploy ID value', + ); + }); +}); diff --git a/packages/integrations/netlify/test/hosted/hosted.test.js b/packages/integrations/netlify/test/hosted/hosted.test.ts similarity index 74% rename from packages/integrations/netlify/test/hosted/hosted.test.js rename to packages/integrations/netlify/test/hosted/hosted.test.ts index 3bc9349f960b..3657c17cb7fa 100644 --- a/packages/integrations/netlify/test/hosted/hosted.test.js +++ b/packages/integrations/netlify/test/hosted/hosted.test.ts @@ -20,10 +20,10 @@ describe('Hosted Netlify Tests', () => { }); it('Server returns fresh content', async () => { - const responseOne = await fetch(`${NETLIFY_TEST_URL}/time`).then((res) => res.text()); + const responseOne: string = await fetch(`${NETLIFY_TEST_URL}/time`).then((res) => res.text()); - const responseTwo = await fetch(`${NETLIFY_TEST_URL}/time`).then((res) => res.text()); + const responseTwo: string = await fetch(`${NETLIFY_TEST_URL}/time`).then((res) => res.text()); - assert.notEqual(responseOne.body, responseTwo.body); + assert.notEqual(responseOne, responseTwo); }); }); diff --git a/packages/integrations/netlify/test/static/fixtures/image-missing-dimension/astro.config.mjs b/packages/integrations/netlify/test/static/fixtures/image-missing-dimension/astro.config.mjs new file mode 100644 index 000000000000..6f09867119ee --- /dev/null +++ b/packages/integrations/netlify/test/static/fixtures/image-missing-dimension/astro.config.mjs @@ -0,0 +1,12 @@ +import { defineConfig } from 'astro/config'; +import netlify from '@astrojs/netlify'; + +export default defineConfig({ + adapter: netlify(), + output: 'static', + image: { + service: { + entrypoint: 'astro/assets/services/sharp' + } + } +}); diff --git a/packages/integrations/netlify/test/static/fixtures/image-missing-dimension/package.json b/packages/integrations/netlify/test/static/fixtures/image-missing-dimension/package.json new file mode 100644 index 000000000000..98054c7d11bd --- /dev/null +++ b/packages/integrations/netlify/test/static/fixtures/image-missing-dimension/package.json @@ -0,0 +1,9 @@ +{ + "name": "image-missing-dimention", + "type": "module", + "private": true, + "dependencies": { + "astro": "workspace:*", + "@astrojs/netlify": "workspace:*" + } +} diff --git a/packages/integrations/netlify/test/static/fixtures/image-missing-dimension/src/pages/index.astro b/packages/integrations/netlify/test/static/fixtures/image-missing-dimension/src/pages/index.astro new file mode 100644 index 000000000000..9402dbf6da2d --- /dev/null +++ b/packages/integrations/netlify/test/static/fixtures/image-missing-dimension/src/pages/index.astro @@ -0,0 +1,8 @@ +--- +import { Image } from 'astro:assets'; +--- + +Astro diff --git a/packages/integrations/netlify/test/static/headers.test.js b/packages/integrations/netlify/test/static/headers.test.ts similarity index 87% rename from packages/integrations/netlify/test/static/headers.test.js rename to packages/integrations/netlify/test/static/headers.test.ts index 5c1400098b10..c5316742d352 100644 --- a/packages/integrations/netlify/test/static/headers.test.js +++ b/packages/integrations/netlify/test/static/headers.test.ts @@ -1,9 +1,9 @@ import * as assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { loadFixture } from '../../../../astro/test/test-utils.js'; +import { type Fixture, loadFixture } from '../test-utils.ts'; describe('SSG - headers', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: new URL('./fixtures/redirects/', import.meta.url) }); diff --git a/packages/integrations/netlify/test/static/image-missing-dimension.test.ts b/packages/integrations/netlify/test/static/image-missing-dimension.test.ts new file mode 100644 index 000000000000..3e29122228da --- /dev/null +++ b/packages/integrations/netlify/test/static/image-missing-dimension.test.ts @@ -0,0 +1,23 @@ +import * as assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { loadFixture } from '../test-utils.ts'; + +describe('Image validation when is not size specification in netlify.', () => { + it('throw on missing dimension in static build', async () => { + const fixture = await loadFixture({ + root: new URL('./fixtures/image-missing-dimension/', import.meta.url), + }); + + try { + await fixture.build(); + assert.fail(); + } catch (e) { + // check the error image about missing image dimension + assert.match( + (e as Error).name, + /MissingImageDimension/, + `Build failed but not with the expected "MissingImageDimension"`, + ); + } + }); +}); diff --git a/packages/integrations/netlify/test/static/redirects.test.js b/packages/integrations/netlify/test/static/redirects.test.ts similarity index 67% rename from packages/integrations/netlify/test/static/redirects.test.js rename to packages/integrations/netlify/test/static/redirects.test.ts index cab95483143d..ceafb5856720 100644 --- a/packages/integrations/netlify/test/static/redirects.test.js +++ b/packages/integrations/netlify/test/static/redirects.test.ts @@ -1,9 +1,9 @@ import * as assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { loadFixture } from '../../../../astro/test/test-utils.js'; +import { type Fixture, loadFixture } from '../test-utils.ts'; describe('SSG - Redirects', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: new URL('./fixtures/redirects/', import.meta.url) }); @@ -13,13 +13,22 @@ describe('SSG - Redirects', () => { it('Creates a redirects file', async () => { const redirects = await fixture.readFile('./_redirects'); const parts = redirects.split(/\s+/); + // based on https://github.com/withastro/astro/issues/16030 for the default option `trailingSlash: 'ignore'` both variants should be generated assert.deepEqual(parts, [ '', + '/two/', + '/', + '302', + '/two', '/', '302', + '/other/', + '/', + '301', + '/other', '/', '301', diff --git a/packages/integrations/netlify/test/static/static-headers.test.js b/packages/integrations/netlify/test/static/static-headers.test.ts similarity index 78% rename from packages/integrations/netlify/test/static/static-headers.test.js rename to packages/integrations/netlify/test/static/static-headers.test.ts index a5b7014894b0..297b0ed57dff 100644 --- a/packages/integrations/netlify/test/static/static-headers.test.js +++ b/packages/integrations/netlify/test/static/static-headers.test.ts @@ -1,10 +1,10 @@ import * as assert from 'node:assert/strict'; import { existsSync, readdirSync } from 'node:fs'; import { before, describe, it } from 'node:test'; -import { loadFixture } from '../../../../astro/test/test-utils.js'; +import { type Fixture, loadFixture } from '../test-utils.ts'; describe('Static headers', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: new URL('./fixtures/static-headers/', import.meta.url) }); @@ -22,8 +22,9 @@ describe('Static headers', () => { it('CSP headers are added when CSP is enabled', async () => { const config = await fixture.readFile('../.netlify/v1/config.json'); - const headers = JSON.parse(config).headers; - const index = headers.find((x) => x.for === '/'); + const headers: Array<{ for: string; values: Record }> = + JSON.parse(config).headers; + const index = headers.find((x) => x.for === '/')!; assert.notEqual(index, undefined, 'the index must have CSP headers'); assert.notEqual( @@ -32,7 +33,7 @@ describe('Static headers', () => { 'the index must have CSP headers', ); assert.ok( - index.values['Content-Security-Policy'].includes('script-src'), + index.values['Content-Security-Policy']!.includes('script-src'), 'must contain the script-src directive because of the server island', ); }); diff --git a/packages/integrations/netlify/test/test-utils.ts b/packages/integrations/netlify/test/test-utils.ts new file mode 100644 index 000000000000..3f1f0bbdb101 --- /dev/null +++ b/packages/integrations/netlify/test/test-utils.ts @@ -0,0 +1,37 @@ +import { + type DevServer, + type AstroInlineConfig, + type Fixture, + loadFixture as baseLoadFixture, +} from '../../../astro/test/test-utils.js'; +import { + AstroIntegrationLogger, + type AstroLogMessage, +} from '../../../astro/dist/core/logger/core.js'; + +export type { AstroInlineConfig, DevServer, Fixture }; + +export function loadFixture(config: AstroInlineConfig) { + return baseLoadFixture(config); +} + +export class SpyIntegrationLogger extends AstroIntegrationLogger { + readonly messages: AstroLogMessage[]; + + constructor() { + const messages: AstroLogMessage[] = []; + super( + { + destination: { + write(chunk): boolean { + messages.push(chunk); + return true; + }, + }, + level: 'warn', + }, + 'test-spy', + ); + this.messages = messages; + } +} diff --git a/packages/integrations/netlify/tsconfig.test.json b/packages/integrations/netlify/tsconfig.test.json new file mode 100644 index 000000000000..462c7b7db770 --- /dev/null +++ b/packages/integrations/netlify/tsconfig.test.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["test/**/*.ts"], + "exclude": ["test/**/fixtures/**", "test/hosted/hosted-astro-project/**"], + "compilerOptions": { + "allowJs": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "rewriteRelativeImportExtensions": true, + "rootDir": "." + }, + "references": [{ "path": "../../astro/tsconfig.test.json" }] +} diff --git a/packages/integrations/node/CHANGELOG.md b/packages/integrations/node/CHANGELOG.md index b2392492d98d..9c3d168cc240 100644 --- a/packages/integrations/node/CHANGELOG.md +++ b/packages/integrations/node/CHANGELOG.md @@ -1,5 +1,11 @@ # @astrojs/node +## 10.0.5 + +### Patch Changes + +- [#16319](https://github.com/withastro/astro/pull/16319) [`940afd5`](https://github.com/withastro/astro/commit/940afd53040a14e924606b3218a8619c1e2674ee) Thanks [@matthewp](https://github.com/matthewp)! - Fixes static asset error responses incorrectly including immutable cache headers. Conditional request failures (e.g. `If-Match` mismatch) now return the correct status code without far-future cache directives. + ## 10.0.4 ### Patch Changes diff --git a/packages/integrations/node/package.json b/packages/integrations/node/package.json index bbe5e7dc59b3..e07cf38ff22a 100644 --- a/packages/integrations/node/package.json +++ b/packages/integrations/node/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/node", "description": "Deploy your site to a Node.js server", - "version": "10.0.4", + "version": "10.0.5", "type": "module", "types": "./dist/index.d.ts", "author": "withastro", @@ -30,7 +30,8 @@ "dev": "astro-scripts dev \"src/**/*.ts\"", "build": "astro-scripts build \"src/**/*.ts\" && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\"", - "test": "astro-scripts test \"test/**/*.test.js\"" + "test": "astro-scripts test \"test/**/*.test.ts\"", + "typecheck:tests": "tsc --build tsconfig.test.json" }, "dependencies": { "@astrojs/internal-helpers": "workspace:*", @@ -43,6 +44,7 @@ "devDependencies": { "@fastify/middie": "^9.1.0", "@fastify/static": "^9.0.0", + "@types/express": "^5.0.6", "@types/node": "^22.10.6", "@types/send": "^1.2.1", "@types/server-destroy": "^1.0.4", diff --git a/packages/integrations/node/src/serve-static.ts b/packages/integrations/node/src/serve-static.ts index 2050c6236e71..4a490e3afedc 100644 --- a/packages/integrations/node/src/serve-static.ts +++ b/packages/integrations/node/src/serve-static.ts @@ -130,26 +130,33 @@ export function createStaticHandler( stream.on('error', (err) => { if (forwardError) { - console.error(err.toString()); - res.writeHead(500); - res.end('Internal server error'); + // The `send` library emits errors with a `statusCode` property + // (e.g. 412 for precondition failures from If-Match / If-Unmodified-Since). + // Use the real status when available instead of always returning 500. + const status = 'statusCode' in err ? (err as any).statusCode : 500; + if (status >= 500) { + console.error(err.toString()); + } + res.writeHead(status); + res.end(status >= 500 ? 'Internal server error' : ''); return; } // File not found, forward to the SSR handler ssr(); }); - stream.on('headers', (_res: ServerResponse) => { - // assets in dist/_astro are hashed and should get the immutable header - if (normalizedPathname.startsWith(`/${app.manifest.assetsDir}/`)) { - // This is the "far future" cache header, used for static files whose name includes their digest hash. - // 1 year (31,536,000 seconds) is convention. - // Taken from https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#immutable - _res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); - } - }); stream.on('file', () => { forwardError = true; }); + // The `stream` event fires only when `send` is actually going to stream + // the file content (i.e. after all precondition checks like If-Match and + // If-Unmodified-Since have passed). Setting cache headers here instead of + // in the `headers` event ensures error responses (e.g. 412) are never + // sent with immutable cache headers, which would poison CDN caches. + stream.on('stream', () => { + if (normalizedPathname.startsWith(`/${app.manifest.assetsDir}/`)) { + res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); + } + }); stream.pipe(res); } else { ssr(); diff --git a/packages/integrations/node/test/api-route.test.js b/packages/integrations/node/test/api-route.test.ts similarity index 85% rename from packages/integrations/node/test/api-route.test.js rename to packages/integrations/node/test/api-route.test.ts index 05cdcd637bdf..26e26d0fe8bc 100644 --- a/packages/integrations/node/test/api-route.test.js +++ b/packages/integrations/node/test/api-route.test.ts @@ -1,16 +1,14 @@ import * as assert from 'node:assert/strict'; import crypto from 'node:crypto'; import { after, before, describe, it } from 'node:test'; +import type { PreviewServer } from '../../../astro/src/types/public/preview.js'; import nodejs from '../dist/index.js'; -import { createRequestAndResponse, loadFixture } from './test-utils.js'; +import { createRequestAndResponse, type Fixture, loadFixture } from './test-utils.ts'; describe('API routes', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - /** @type {import('../../../astro/src/types/public/preview.js').PreviewServer} */ - let previewServer; - /** @type {URL} */ - let baseUri; + let fixture: Fixture; + let previewServer: PreviewServer; + let baseUri: URL; before(async () => { fixture = await loadFixture({ @@ -26,7 +24,7 @@ describe('API routes', () => { after(() => previewServer.stop()); it('Can get the request body', async () => { - const { handler } = await import('./fixtures/api-route/dist/server/entry.mjs'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, done } = createRequestAndResponse({ method: 'POST', url: '/recipes', @@ -48,7 +46,7 @@ describe('API routes', () => { }); it('Can get binary data', async () => { - const { handler } = await import('./fixtures/api-route/dist/server/entry.mjs'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, done } = createRequestAndResponse({ method: 'POST', @@ -67,7 +65,7 @@ describe('API routes', () => { }); it('Can post large binary data', async () => { - const { handler } = await import('./fixtures/api-route/dist/server/entry.mjs'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, done } = createRequestAndResponse({ method: 'POST', @@ -76,7 +74,7 @@ describe('API routes', () => { handler(req, res); - let expectedDigest = null; + let expectedDigest: Buffer | null = null; req.once('async_iterator', () => { // Send 256MB of garbage data in 256KB chunks. This should be fast (< 1sec). let remainingBytes = 256 * 1024 * 1024; @@ -96,11 +94,11 @@ describe('API routes', () => { }); const [out] = await done; - assert.deepEqual(new Uint8Array(out.buffer), new Uint8Array(expectedDigest)); + assert.deepEqual(new Uint8Array(out.buffer), new Uint8Array(expectedDigest!)); }); it('Can bail on streaming', async () => { - const { handler } = await import('./fixtures/api-route/dist/server/entry.mjs'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, done } = createRequestAndResponse({ url: '/streaming', }); diff --git a/packages/integrations/node/test/assets.test.js b/packages/integrations/node/test/assets.test.ts similarity index 50% rename from packages/integrations/node/test/assets.test.js rename to packages/integrations/node/test/assets.test.ts index 758026ecca19..e7682ef7b65e 100644 --- a/packages/integrations/node/test/assets.test.js +++ b/packages/integrations/node/test/assets.test.ts @@ -1,14 +1,14 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; +import { fileURLToPath } from 'node:url'; +import type { PreviewServer } from '../../../astro/src/types/public/preview.js'; import * as cheerio from 'cheerio'; import nodejs from '../dist/index.js'; -import { loadFixture } from './test-utils.js'; -import { fileURLToPath } from 'node:url'; +import { type Fixture, loadFixture } from './test-utils.ts'; describe('Assets', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let devPreview; + let fixture: Fixture; + let devPreview: PreviewServer; before(async () => { const root = new URL('./fixtures/image/', import.meta.url); @@ -39,9 +39,31 @@ describe('Assets', () => { const $ = cheerio.load(html); // Fetch the asset - const fileURL = $('a').attr('href'); + const fileURL = $('a').attr('href')!; response = await fixture.fetch(fileURL); cacheControl = response.headers.get('cache-control'); assert.equal(cacheControl, 'public, max-age=31536000, immutable'); }); + + it('Malformed If-Match header should return 412 without immutable cache headers', async () => { + // First, get a valid asset URL from the page + let response = await fixture.fetch('/text-file'); + const html = await response.text(); + const $ = cheerio.load(html); + const fileURL = $('a').attr('href')!; + + // Send a request with a malformed If-Match header that won't match the ETag + response = await fixture.fetch(fileURL, { + headers: { 'If-Match': 'xxx' }, + }); + + // Should return 412 Precondition Failed, not 500 + assert.equal(response.status, 412); + + // Must NOT include the immutable far-future cache header on error responses, + // as that would allow CDN cache poisoning. The `send` library may still set + // its own default `public, max-age=0` which is harmless (not cached). + const cacheControl = response.headers.get('cache-control'); + assert.notEqual(cacheControl, 'public, max-age=31536000, immutable'); + }); }); diff --git a/packages/integrations/node/test/bad-urls.test.js b/packages/integrations/node/test/bad-urls.test.ts similarity index 88% rename from packages/integrations/node/test/bad-urls.test.js rename to packages/integrations/node/test/bad-urls.test.ts index 2e3e3ad28c18..fc45bef3fa4a 100644 --- a/packages/integrations/node/test/bad-urls.test.js +++ b/packages/integrations/node/test/bad-urls.test.ts @@ -1,12 +1,12 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; +import type { PreviewServer } from '../../../astro/src/types/public/preview.js'; import nodejs from '../dist/index.js'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.ts'; describe('Bad URLs', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let devPreview; + let fixture: Fixture; + let devPreview: PreviewServer; before(async () => { fixture = await loadFixture({ diff --git a/packages/integrations/node/test/encoded.test.js b/packages/integrations/node/test/encoded.test.ts similarity index 74% rename from packages/integrations/node/test/encoded.test.js rename to packages/integrations/node/test/encoded.test.ts index 4fc97cf7fd09..657119e6d0b2 100644 --- a/packages/integrations/node/test/encoded.test.js +++ b/packages/integrations/node/test/encoded.test.ts @@ -1,11 +1,10 @@ import * as assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import nodejs from '../dist/index.js'; -import { createRequestAndResponse, loadFixture } from './test-utils.js'; +import { createRequestAndResponse, type Fixture, loadFixture } from './test-utils.ts'; describe('Encoded Pathname', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -17,7 +16,7 @@ describe('Encoded Pathname', () => { }); it('Can get an Astro file', async () => { - const { handler } = await import('./fixtures/encoded/dist/server/entry.mjs'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, text } = createRequestAndResponse({ url: '/什么', }); @@ -30,7 +29,7 @@ describe('Encoded Pathname', () => { }); it('Can get a Markdown file', async () => { - const { handler } = await import('./fixtures/encoded/dist/server/entry.mjs'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, text } = createRequestAndResponse({ url: '/blog/什么', diff --git a/packages/integrations/node/test/errors.test.js b/packages/integrations/node/test/errors.test.js deleted file mode 100644 index 9090b162ee4b..000000000000 --- a/packages/integrations/node/test/errors.test.js +++ /dev/null @@ -1,65 +0,0 @@ -import assert from 'node:assert/strict'; -import { after, before, describe, it } from 'node:test'; -import * as cheerio from 'cheerio'; -import nodejs from '../dist/index.js'; -import { loadFixture } from './test-utils.js'; - -describe('Errors', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; - - before(async () => { - fixture = await loadFixture({ - root: './fixtures/errors/', - output: 'server', - adapter: nodejs({ mode: 'standalone' }), - }); - await fixture.build(); - }); - let devPreview; - - // The two tests that need the server to run are skipped - // before(async () => { - // devPreview = await fixture.preview(); - // }); - after(async () => { - await devPreview?.stop(); - }); - - it( - 'rejected promise in template', - { skip: true, todo: 'Review the response from the in-stream' }, - async () => { - const res = await fixture.fetch('/in-stream'); - const html = await res.text(); - const $ = cheerio.load(html); - - assert.equal($('p').text().trim(), 'Internal server error'); - }, - ); - - it( - 'generator that throws called in template', - { skip: true, todo: 'Review the response from the generator' }, - async () => { - const result = ['

    Astro

    1', 'Internal server error']; - - /** @type {Response} */ - const res = await fixture.fetch('/generator'); - const reader = res.body.getReader(); - const decoder = new TextDecoder(); - const chunk1 = await reader.read(); - const chunk2 = await reader.read(); - const chunk3 = await reader.read(); - assert.equal(chunk1.done, false); - if (chunk2.done) { - assert.equal(decoder.decode(chunk1.value), result.join('')); - } else if (chunk3.done) { - assert.equal(decoder.decode(chunk1.value), result[0]); - assert.equal(decoder.decode(chunk2.value), result[1]); - } else { - throw new Error('The response should take at most 2 chunks.'); - } - }, - ); -}); diff --git a/packages/integrations/node/test/errors.test.ts b/packages/integrations/node/test/errors.test.ts new file mode 100644 index 000000000000..c9b7fb92d8ea --- /dev/null +++ b/packages/integrations/node/test/errors.test.ts @@ -0,0 +1,52 @@ +import assert from 'node:assert/strict'; +import { before, describe, it } from 'node:test'; +import * as cheerio from 'cheerio'; +import nodejs from '../dist/index.js'; +import { type Fixture, loadFixture } from './test-utils.ts'; + +describe('Errors', () => { + let fixture: Fixture; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/errors/', + output: 'server', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + }); + + it('rejected promise in template', { + skip: true, + todo: 'Review the response from the in-stream', + }, async () => { + const res = await fixture.fetch('/in-stream'); + const html = await res.text(); + const $ = cheerio.load(html); + + assert.equal($('p').text().trim(), 'Internal server error'); + }); + + it('generator that throws called in template', { + skip: true, + todo: 'Review the response from the generator', + }, async () => { + const result = ['

    Astro

    1', 'Internal server error']; + + const res: Response = await fixture.fetch('/generator'); + const reader = res.body!.getReader(); + const decoder = new TextDecoder(); + const chunk1 = await reader.read(); + const chunk2 = await reader.read(); + const chunk3 = await reader.read(); + assert.equal(chunk1.done, false); + if (chunk2.done) { + assert.equal(decoder.decode(chunk1.value), result.join('')); + } else if (chunk3.done) { + assert.equal(decoder.decode(chunk1.value), result[0]); + assert.equal(decoder.decode(chunk2.value), result[1]); + } else { + throw new Error('The response should take at most 2 chunks.'); + } + }); +}); diff --git a/packages/integrations/node/test/headers.test.js b/packages/integrations/node/test/headers.test.ts similarity index 92% rename from packages/integrations/node/test/headers.test.js rename to packages/integrations/node/test/headers.test.ts index 15f52a434d25..e9a5faa9c353 100644 --- a/packages/integrations/node/test/headers.test.js +++ b/packages/integrations/node/test/headers.test.ts @@ -1,12 +1,11 @@ import * as assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import nodejs from '../dist/index.js'; -import { createRequestAndResponse, loadFixture } from './test-utils.js'; +import { createRequestAndResponse, type Fixture, loadFixture } from './test-utils.ts'; -describe('Node Adapter Headers', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; +let fixture: Fixture; +describe('Node Adapter Headers', () => { describe('streaming', () => { before(async () => { fixture = await loadFixture({ @@ -131,7 +130,7 @@ describe('Node Adapter Headers', () => { // TODO: needs e2e tests to check real headers it('sends several chunks', async () => { - const { handler } = await import('./fixtures/headers/dist/server/entry.mjs'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, done } = createRequestAndResponse({ method: 'GET', @@ -159,7 +158,7 @@ describe('Node Adapter Headers', () => { // TODO: needs e2e tests to check real headers it('sends a single chunk', async () => { - const { handler } = await import('./fixtures/headers/dist/server/entry.mjs?cachebust=0'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, done } = createRequestAndResponse({ method: 'GET', @@ -176,8 +175,8 @@ describe('Node Adapter Headers', () => { }); }); -async function runTest(url, expectedHeaders) { - const { handler } = await import('./fixtures/headers/dist/server/entry.mjs'); +async function runTest(url: string, expectedHeaders: Record) { + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, done } = createRequestAndResponse({ method: 'GET', diff --git a/packages/integrations/node/test/image.test.js b/packages/integrations/node/test/image.test.ts similarity index 96% rename from packages/integrations/node/test/image.test.js rename to packages/integrations/node/test/image.test.ts index 0426a7605c0f..f6d4389423f5 100644 --- a/packages/integrations/node/test/image.test.js +++ b/packages/integrations/node/test/image.test.ts @@ -1,16 +1,16 @@ import * as assert from 'node:assert/strict'; import { cp, rm } from 'node:fs/promises'; import { after, before, describe, it } from 'node:test'; +import { fileURLToPath } from 'node:url'; +import type { PreviewServer } from '../../../astro/src/types/public/preview.js'; import { inferRemoteSize } from 'astro/assets/utils/inferRemoteSize.js'; import * as cheerio from 'cheerio'; import nodejs from '../dist/index.js'; -import { loadFixture } from './test-utils.js'; -import { fileURLToPath } from 'node:url'; +import { type Fixture, loadFixture } from './test-utils.ts'; describe('Image endpoint', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let devPreview; + let fixture: Fixture; + let devPreview: PreviewServer; before(async () => { const root = new URL('./fixtures/image/', import.meta.url); diff --git a/packages/integrations/node/test/locals.test.js b/packages/integrations/node/test/locals.test.ts similarity index 78% rename from packages/integrations/node/test/locals.test.js rename to packages/integrations/node/test/locals.test.ts index b8e3ed40fc4b..d588d4ac3e69 100644 --- a/packages/integrations/node/test/locals.test.js +++ b/packages/integrations/node/test/locals.test.ts @@ -1,11 +1,10 @@ import * as assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import nodejs from '../dist/index.js'; -import { createRequestAndResponse, loadFixture } from './test-utils.js'; +import { createRequestAndResponse, type Fixture, loadFixture } from './test-utils.ts'; describe('API routes', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -17,7 +16,7 @@ describe('API routes', () => { }); it('Can use locals added by node middleware', async () => { - const { handler } = await import('./fixtures/locals/dist/server/entry.mjs'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, text } = createRequestAndResponse({ url: '/from-node-middleware', }); @@ -33,11 +32,12 @@ describe('API routes', () => { }); it('Throws an error when provided non-objects as locals', async () => { - const { handler } = await import('./fixtures/locals/dist/server/entry.mjs'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, done } = createRequestAndResponse({ url: '/from-node-middleware', }); + // @ts-expect-error - intentionally passing a non-object to test error handling handler(req, res, undefined, 'locals'); req.send(); @@ -46,7 +46,7 @@ describe('API routes', () => { }); it('Can use locals added by astro middleware', async () => { - const { handler } = await import('./fixtures/locals/dist/server/entry.mjs'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, text } = createRequestAndResponse({ url: '/from-astro-middleware', @@ -61,7 +61,7 @@ describe('API routes', () => { }); it('Can access locals in API', async () => { - const { handler } = await import('./fixtures/locals/dist/server/entry.mjs'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, done } = createRequestAndResponse({ method: 'POST', url: '/api', diff --git a/packages/integrations/node/test/node-middleware-listener-cleanup.test.js b/packages/integrations/node/test/node-middleware-listener-cleanup.test.ts similarity index 82% rename from packages/integrations/node/test/node-middleware-listener-cleanup.test.js rename to packages/integrations/node/test/node-middleware-listener-cleanup.test.ts index a0fffe44b953..5684b426a7cd 100644 --- a/packages/integrations/node/test/node-middleware-listener-cleanup.test.js +++ b/packages/integrations/node/test/node-middleware-listener-cleanup.test.ts @@ -3,17 +3,15 @@ import { after, before, describe, it } from 'node:test'; import { fileURLToPath } from 'node:url'; import fastifyMiddie from '@fastify/middie'; import fastifyStatic from '@fastify/static'; -import Fastify from 'fastify'; +import Fastify, { type FastifyInstance } from 'fastify'; import nodejs from '../dist/index.js'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.ts'; describe('Node middleware socket listener cleanup', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let server; + let fixture: Fixture; + let server: FastifyInstance; before(async () => { - process.env.PRERENDER = true; fixture = await loadFixture({ root: './fixtures/node-middleware/', output: 'static', @@ -36,7 +34,6 @@ describe('Node middleware socket listener cleanup', () => { after(async () => { server.close(); await fixture.clean(); - delete process.env.PRERENDER; }); it('should not leak socket listeners when serving static files', async () => { @@ -45,7 +42,7 @@ describe('Node middleware socket listener cleanup', () => { }); let listenerWarningEmitted = false; - const warningListener = (warning) => { + const warningListener = (warning: Error) => { if (warning.name === 'MaxListenersExceededWarning') { listenerWarningEmitted = true; } @@ -56,6 +53,7 @@ describe('Node middleware socket listener cleanup', () => { // Make multiple back-to-back requests to a static page for (let i = 0; i < 30; i++) { const response = await fetch('http://localhost:8890', { + // @ts-expect-error: it seems that Node.js `fetch` doesn't accept `agent` here. Should we use dispatcher instead? https://stackoverflow.com/a/76069981 agent, headers: { Connection: 'keep-alive', diff --git a/packages/integrations/node/test/node-middleware.test.js b/packages/integrations/node/test/node-middleware.test.ts similarity index 91% rename from packages/integrations/node/test/node-middleware.test.js rename to packages/integrations/node/test/node-middleware.test.ts index e1603e34b93e..cef931d083d9 100644 --- a/packages/integrations/node/test/node-middleware.test.js +++ b/packages/integrations/node/test/node-middleware.test.ts @@ -1,3 +1,4 @@ +import type { Server } from 'node:http'; import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { fileURLToPath } from 'node:url'; @@ -5,21 +6,15 @@ import fastifyMiddie from '@fastify/middie'; import fastifyStatic from '@fastify/static'; import * as cheerio from 'cheerio'; import express from 'express'; -import Fastify from 'fastify'; +import Fastify, { type FastifyInstance } from 'fastify'; import nodejs from '../dist/index.js'; -import { loadFixture, waitServerListen } from './test-utils.js'; - -/** - * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture - */ +import { type Fixture, loadFixture, waitServerListen, type AdapterServer } from './test-utils.ts'; describe('behavior from middleware, standalone', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let server; + let fixture: Fixture; + let server: AdapterServer; before(async () => { - process.env.PRERENDER = false; fixture = await loadFixture({ root: './fixtures/node-middleware/', output: 'server', @@ -35,8 +30,6 @@ describe('behavior from middleware, standalone', () => { after(async () => { await server.stop(); await fixture.clean(); - - delete process.env.PRERENDER; }); describe('404', async () => { @@ -55,12 +48,10 @@ describe('behavior from middleware, standalone', () => { }); describe('behavior from middleware, middleware with express', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let server; + let fixture: Fixture; + let server: Server; before(async () => { - process.env.PRERENDER = false; fixture = await loadFixture({ root: './fixtures/node-middleware/', output: 'server', @@ -76,8 +67,6 @@ describe('behavior from middleware, middleware with express', () => { after(async () => { server.close(); await fixture.clean(); - - delete process.env.PRERENDER; }); it('should render the endpoint', async () => { @@ -140,12 +129,10 @@ describe('behavior from middleware, middleware with express', () => { }); describe('behavior from middleware, middleware with fastify', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let server; + let fixture: Fixture; + let server: FastifyInstance; before(async () => { - process.env.PRERENDER = false; fixture = await loadFixture({ root: './fixtures/node-middleware/', output: 'server', @@ -169,8 +156,6 @@ describe('behavior from middleware, middleware with fastify', () => { after(async () => { server.close(); await fixture.clean(); - - delete process.env.PRERENDER; }); it('should render the endpoint', async () => { @@ -225,9 +210,8 @@ describe('behavior from middleware, middleware with fastify', () => { // the Node adapter in middleware mode can identify them as static files and NOT // match them against catch-all routes. describe('middleware with fastify and catch-all route: SSR assets in manifest', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let server; + let fixture: Fixture; + let server: FastifyInstance; before(async () => { fixture = await loadFixture({ diff --git a/packages/integrations/node/test/prerender-404-500.test.js b/packages/integrations/node/test/prerender-404-500.test.ts similarity index 92% rename from packages/integrations/node/test/prerender-404-500.test.js rename to packages/integrations/node/test/prerender-404-500.test.ts index a7e968f0c01a..c2b41fec2d50 100644 --- a/packages/integrations/node/test/prerender-404-500.test.js +++ b/packages/integrations/node/test/prerender-404-500.test.ts @@ -2,21 +2,14 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; import nodejs from '../dist/index.js'; -import { loadFixture, waitServerListen } from './test-utils.js'; - -/** - * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture - */ +import { type Fixture, loadFixture, waitServerListen, type AdapterServer } from './test-utils.ts'; describe('Prerender 404', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let server; + let fixture: Fixture; + let server: AdapterServer; describe('With base', async () => { before(async () => { - process.env.PRERENDER = true; - fixture = await loadFixture({ // inconsequential config that differs between tests // to bust cache and prevent modules and their state @@ -38,7 +31,6 @@ describe('Prerender 404', () => { after(async () => { await server.stop(); await fixture.clean(); - process.env.PRERENDER = undefined; }); it('Can render SSR route', async () => { @@ -102,8 +94,6 @@ describe('Prerender 404', () => { describe('Without base', async () => { before(async () => { - process.env.PRERENDER = true; - fixture = await loadFixture({ // inconsequential config that differs between tests // to bust cache and prevent modules and their state @@ -124,7 +114,6 @@ describe('Prerender 404', () => { after(async () => { await server.stop(); await fixture.clean(); - process.env.PRERENDER = undefined; }); it('Can render SSR route', async () => { @@ -161,13 +150,11 @@ describe('Prerender 404', () => { }); describe('Hybrid 404', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let server; + let fixture: Fixture; + let server: AdapterServer; describe('With base', async () => { before(async () => { - process.env.PRERENDER = false; fixture = await loadFixture({ // inconsequential config that differs between tests // to bust cache and prevent modules and their state @@ -189,7 +176,6 @@ describe('Hybrid 404', () => { after(async () => { await server.stop(); await fixture.clean(); - process.env.PRERENDER = undefined; }); it('Can render SSR route', async () => { @@ -226,7 +212,6 @@ describe('Hybrid 404', () => { describe('Without base', async () => { before(async () => { - process.env.PRERENDER = false; fixture = await loadFixture({ // inconsequential config that differs between tests // to bust cache and prevent modules and their state @@ -247,7 +232,6 @@ describe('Hybrid 404', () => { after(async () => { await server.stop(); await fixture.clean(); - process.env.PRERENDER = undefined; }); it('Can render SSR route', async () => { diff --git a/packages/integrations/node/test/prerender.test.js b/packages/integrations/node/test/prerender.test.ts similarity index 94% rename from packages/integrations/node/test/prerender.test.js rename to packages/integrations/node/test/prerender.test.ts index 0b30deb43903..9a873ab0a3b6 100644 --- a/packages/integrations/node/test/prerender.test.js +++ b/packages/integrations/node/test/prerender.test.ts @@ -2,21 +2,20 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; import nodejs from '../dist/index.js'; -import { loadFixture, waitServerListen } from './test-utils.js'; - -/** - * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture - */ +import { + type Fixture, + loadFixture, + waitServerListen, + type AdapterServer, + type DevServer, +} from './test-utils.ts'; describe('Prerendering', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let server; + let fixture: Fixture; + let server: AdapterServer; describe('With base', async () => { before(async () => { - process.env.PRERENDER = true; - fixture = await loadFixture({ base: '/some-base', root: './fixtures/prerender/', @@ -34,8 +33,6 @@ describe('Prerendering', () => { after(async () => { await server.stop(); await fixture.clean(); - - delete process.env.PRERENDER; }); it('Can render SSR route', async () => { @@ -103,8 +100,6 @@ describe('Prerendering', () => { describe('Without base', async () => { before(async () => { - process.env.PRERENDER = true; - fixture = await loadFixture({ root: './fixtures/prerender/', output: 'server', @@ -121,8 +116,6 @@ describe('Prerendering', () => { after(async () => { await server.stop(); await fixture.clean(); - - delete process.env.PRERENDER; }); it('Can render SSR route', async () => { @@ -180,7 +173,6 @@ describe('Prerendering', () => { describe('Via integration', () => { before(async () => { - process.env.PRERENDER = false; fixture = await loadFixture({ root: './fixtures/prerender/', output: 'server', @@ -209,8 +201,6 @@ describe('Prerendering', () => { after(async () => { await server.stop(); await fixture.clean(); - - delete process.env.PRERENDER; }); it('Can render SSR route', async () => { @@ -234,11 +224,9 @@ describe('Prerendering', () => { }); describe('Dev', () => { - let devServer; + let devServer: DevServer; before(async () => { - process.env.PRERENDER = true; - fixture = await loadFixture({ root: './fixtures/prerender/', output: 'server', @@ -250,8 +238,6 @@ describe('Prerendering', () => { after(async () => { await devServer.stop(); - - delete process.env.PRERENDER; }); it('Can render SSR route', async () => { @@ -275,13 +261,11 @@ describe('Prerendering', () => { }); describe('Hybrid rendering', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let server; + let fixture: Fixture; + let server: AdapterServer; describe('With base', () => { before(async () => { - process.env.PRERENDER = false; fixture = await loadFixture({ base: '/some-base', root: './fixtures/prerender/', @@ -299,8 +283,6 @@ describe('Hybrid rendering', () => { after(async () => { await server.stop(); await fixture.clean(); - - delete process.env.PRERENDER; }); it('Can render SSR route', async () => { @@ -367,7 +349,6 @@ describe('Hybrid rendering', () => { describe('Without base', () => { before(async () => { - process.env.PRERENDER = false; fixture = await loadFixture({ root: './fixtures/prerender/', output: 'static', @@ -384,8 +365,6 @@ describe('Hybrid rendering', () => { after(async () => { await server.stop(); await fixture.clean(); - - delete process.env.PRERENDER; }); it('Can render SSR route', async () => { @@ -443,8 +422,6 @@ describe('Hybrid rendering', () => { describe('Shared modules', () => { before(async () => { - process.env.PRERENDER = false; - fixture = await loadFixture({ root: './fixtures/prerender/', output: 'static', @@ -461,8 +438,6 @@ describe('Hybrid rendering', () => { after(async () => { await server.stop(); await fixture.clean(); - - delete process.env.PRERENDER; }); it('Can render SSR route', async () => { diff --git a/packages/integrations/node/test/prerendered-error-page-fetch.test.js b/packages/integrations/node/test/prerendered-error-page-fetch.test.ts similarity index 77% rename from packages/integrations/node/test/prerendered-error-page-fetch.test.js rename to packages/integrations/node/test/prerendered-error-page-fetch.test.ts index b66482b85595..9fb6c6110f89 100644 --- a/packages/integrations/node/test/prerendered-error-page-fetch.test.js +++ b/packages/integrations/node/test/prerendered-error-page-fetch.test.ts @@ -1,18 +1,14 @@ -// @ts-check import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; +import type { PreviewServer } from '../../../astro/src/types/public/preview.js'; import node from '../dist/index.js'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.ts'; describe('prerenderedErrorPageFetch', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - /** @type {import('astro').PreviewServer} */ - let devPreview; - /** @type {typeof globalThis.fetch} */ - let originalFetch; - /** @type {Array} */ - let urls; + let fixture: Fixture; + let devPreview: PreviewServer; + let originalFetch: typeof globalThis.fetch; + let urls: Array = []; before(async () => { fixture = await loadFixture({ @@ -20,11 +16,10 @@ describe('prerenderedErrorPageFetch', () => { adapter: node({ mode: 'standalone' }), }); await fixture.clean(); - await fixture.build({}); - devPreview = await fixture.preview({}); + await fixture.build(); + devPreview = await fixture.preview(); originalFetch = globalThis.fetch; globalThis.fetch = (...args) => { - urls ??= []; if (typeof args[0] === 'string') { urls.push(args[0]); } diff --git a/packages/integrations/node/test/preview-headers.test.js b/packages/integrations/node/test/preview-headers.test.ts similarity index 80% rename from packages/integrations/node/test/preview-headers.test.js rename to packages/integrations/node/test/preview-headers.test.ts index 3fd9d0508d6e..449d9657b4c3 100644 --- a/packages/integrations/node/test/preview-headers.test.js +++ b/packages/integrations/node/test/preview-headers.test.ts @@ -1,12 +1,12 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; +import type { PreviewServer } from '../../../astro/src/types/public/preview.js'; import nodejs from '../dist/index.js'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.ts'; describe('Astro preview headers', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let devPreview; + let fixture: Fixture; + let devPreview: PreviewServer; const headers = { astro: 'test', }; diff --git a/packages/integrations/node/test/preview-host.test.js b/packages/integrations/node/test/preview-host.test.ts similarity index 97% rename from packages/integrations/node/test/preview-host.test.js rename to packages/integrations/node/test/preview-host.test.ts index fec925e677f3..f601fc235772 100644 --- a/packages/integrations/node/test/preview-host.test.js +++ b/packages/integrations/node/test/preview-host.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import nodejs from '../dist/index.js'; -import { loadFixture } from './test-utils.js'; +import { loadFixture } from './test-utils.ts'; describe('Astro preview host', () => { it('defaults to localhost', async () => { diff --git a/packages/integrations/node/test/redirects.test.js b/packages/integrations/node/test/redirects.test.ts similarity index 91% rename from packages/integrations/node/test/redirects.test.js rename to packages/integrations/node/test/redirects.test.ts index 0688414faa3d..b46a4f2e7da0 100644 --- a/packages/integrations/node/test/redirects.test.js +++ b/packages/integrations/node/test/redirects.test.ts @@ -1,12 +1,11 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import nodejs from '../dist/index.js'; -import { loadFixture, waitServerListen } from './test-utils.js'; +import { type Fixture, loadFixture, waitServerListen, type AdapterServer } from './test-utils.ts'; describe('Redirects', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let server; + let fixture: Fixture; + let server: AdapterServer; before(async () => { fixture = await loadFixture({ @@ -25,7 +24,7 @@ describe('Redirects', () => { await fixture.clean(); }); - function fetchEndpoint(url, options = {}) { + function fetchEndpoint(url: string, options: RequestInit = {}) { return fetch(`http://${server.host}:${server.port}/${url}`, { ...options, redirect: 'manual' }); } diff --git a/packages/integrations/node/test/server-host.test.js b/packages/integrations/node/test/server-host.test.ts similarity index 100% rename from packages/integrations/node/test/server-host.test.js rename to packages/integrations/node/test/server-host.test.ts diff --git a/packages/integrations/node/test/sessions.test.js b/packages/integrations/node/test/sessions.test.ts similarity index 93% rename from packages/integrations/node/test/sessions.test.js rename to packages/integrations/node/test/sessions.test.ts index 5abd5a566c5d..9cee8bae9403 100644 --- a/packages/integrations/node/test/sessions.test.js +++ b/packages/integrations/node/test/sessions.test.ts @@ -1,16 +1,14 @@ -// @ts-check import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; +import type { PreviewServer } from '../../../astro/src/types/public/preview.js'; import * as devalue from 'devalue'; import nodejs from '../dist/index.js'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture, type DevServer } from './test-utils.ts'; describe('Astro.session', () => { describe('Production', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - /** @type {import('../../../astro/src/types/public/preview.js').PreviewServer} */ - let app; + let fixture: Fixture; + let app: PreviewServer; before(async () => { fixture = await loadFixture({ @@ -18,8 +16,8 @@ describe('Astro.session', () => { output: 'server', adapter: nodejs({ mode: 'middleware' }), }); - await fixture.build({}); - app = await fixture.preview({}); + await fixture.build(); + app = await fixture.preview(); }); after(async () => { @@ -95,9 +93,8 @@ describe('Astro.session', () => { }); describe('Development', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let devServer; + let fixture: Fixture; + let devServer: DevServer; before(async () => { fixture = await loadFixture({ root: './fixtures/sessions/', diff --git a/packages/integrations/node/test/static-headers.test.js b/packages/integrations/node/test/static-headers.test.ts similarity index 82% rename from packages/integrations/node/test/static-headers.test.js rename to packages/integrations/node/test/static-headers.test.ts index 3dc3d98f4c37..68bfb82b089b 100644 --- a/packages/integrations/node/test/static-headers.test.js +++ b/packages/integrations/node/test/static-headers.test.ts @@ -1,10 +1,12 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import nodejs from '../dist/index.js'; -import { loadFixture, waitServerListen } from './test-utils.js'; +import { type Fixture, loadFixture, waitServerListen, type AdapterServer } from './test-utils.ts'; + +type StaticHeaderEntry = { pathname: string; headers: Array<{ key: string; value: string }> }; describe('Static headers', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/static-headers' }); @@ -12,11 +14,13 @@ describe('Static headers', () => { }); it('CSP headers are added when CSP is enabled', async () => { - const headers = JSON.parse(await fixture.readFile('../dist/_headers.json')); + const headers: StaticHeaderEntry[] = JSON.parse( + await fixture.readFile('../dist/_headers.json'), + ); const csp = headers - .find((x) => x.pathname === '/') - .headers.find((x) => x.key === 'Content-Security-Policy'); + .find((x) => x.pathname === '/')! + .headers.find((x) => x.key === 'Content-Security-Policy')!; assert.notEqual(csp, undefined, 'the index must have CSP headers'); assert.ok( @@ -29,9 +33,8 @@ describe('Static headers', () => { }); describe('Static headers', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let server; + let fixture: Fixture; + let server: AdapterServer; before(async () => { fixture = await loadFixture({ @@ -55,7 +58,7 @@ describe('Static headers', () => { it('CSP headers are added to the request', async () => { const res = await fetch(`http://${server.host}:${server.port}/`); - const cps = res.headers.get('Content-Security-Policy'); + const cps = res.headers.get('Content-Security-Policy')!; assert.ok( cps.includes('script-src'), 'should contain script-src directive due to server island', @@ -64,7 +67,7 @@ describe('Static headers', () => { it('CSP headers are added to dynamic orute', async () => { const res = await fetch(`http://${server.host}:${server.port}/one`); - const cps = res.headers.get('Content-Security-Policy'); + const cps = res.headers.get('Content-Security-Policy')!; assert.ok( cps.includes('script-src'), 'should contain script-src directive due to server island', @@ -73,9 +76,8 @@ describe('Static headers', () => { }); describe('Static headers with non-root base', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let server; + let fixture: Fixture; + let server: AdapterServer; before(async () => { fixture = await loadFixture({ diff --git a/packages/integrations/node/test/test-utils.js b/packages/integrations/node/test/test-utils.js deleted file mode 100644 index 8553a159e09c..000000000000 --- a/packages/integrations/node/test/test-utils.js +++ /dev/null @@ -1,82 +0,0 @@ -import { EventEmitter } from 'node:events'; -import httpMocks from 'node-mocks-http'; -import { loadFixture as baseLoadFixture } from '../../../astro/test/test-utils.js'; - -process.env.ASTRO_NODE_AUTOSTART = 'disabled'; -process.env.ASTRO_NODE_LOGGING = 'disabled'; -/** - * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture - */ - -export function loadFixture(inlineConfig) { - if (!inlineConfig?.root) throw new Error("Must provide { root: './fixtures/...' }"); - - // resolve the relative root (i.e. "./fixtures/tailwindcss") to a full filepath - // without this, the main `loadFixture` helper will resolve relative to `packages/astro/test` - return baseLoadFixture({ - ...inlineConfig, - root: new URL(inlineConfig.root, import.meta.url).toString(), - }); -} - -export function createRequestAndResponse(reqOptions) { - const req = httpMocks.createRequest(reqOptions); - - const res = httpMocks.createResponse({ - eventEmitter: EventEmitter, - req, - }); - - const done = toPromise(res); - - // Get the response as text - const text = async () => { - const chunks = await done; - return buffersToString(chunks); - }; - - return { req, res, done, text }; -} - -/** @returns {Promise>} */ -function toPromise(res) { - return new Promise((resolve) => { - // node-mocks-http doesn't correctly handle non-Buffer typed arrays, - // so override the write method to fix it. - const write = res.write; - res.write = function (data, encoding) { - if (ArrayBuffer.isView(data) && !Buffer.isBuffer(data)) { - data = Buffer.from(data.buffer); - } - return write.call(this, data, encoding); - }; - res.on('end', () => { - const chunks = res._getChunks(); - resolve(chunks); - }); - }); -} - -function buffersToString(buffers) { - const decoder = new TextDecoder(); - let str = ''; - for (const buffer of buffers) { - str += decoder.decode(buffer); - } - return str; -} - -export function waitServerListen(server) { - return new Promise((resolve, reject) => { - function onListen() { - server.off('error', onError); - resolve(); - } - function onError(error) { - server.off('listening', onListen); - reject(error); - } - server.once('listening', onListen); - server.once('error', onError); - }); -} diff --git a/packages/integrations/node/test/test-utils.ts b/packages/integrations/node/test/test-utils.ts new file mode 100644 index 000000000000..3fea903844cd --- /dev/null +++ b/packages/integrations/node/test/test-utils.ts @@ -0,0 +1,94 @@ +import { EventEmitter } from 'node:events'; +import type { Server, ServerResponse } from 'node:http'; +import * as httpMocks from 'node-mocks-http'; +import { + type AstroInlineConfig, + type Fixture, + type AdapterServer, + type DevServer, + loadFixture as baseLoadFixture, +} from '../../../astro/test/test-utils.js'; +import type * as express from 'express'; + +process.env.ASTRO_NODE_AUTOSTART = 'disabled'; +process.env.ASTRO_NODE_LOGGING = 'disabled'; + +export type { AstroInlineConfig, Fixture, AdapterServer, DevServer }; + +export function loadFixture(inlineConfig: AstroInlineConfig) { + if (!inlineConfig?.root) throw new Error("Must provide { root: './fixtures/...' }"); + + // resolve the relative root (i.e. "./fixtures/tailwindcss") to a full filepath + // without this, the main `loadFixture` helper will resolve relative to `packages/astro/test` + return baseLoadFixture({ + ...inlineConfig, + root: new URL(inlineConfig.root as string, import.meta.url).toString(), + }); +} + +export function createRequestAndResponse(reqOptions?: httpMocks.RequestOptions): { + req: httpMocks.MockRequest; + res: httpMocks.MockResponse; + done: Promise[]>; + text: () => Promise; +} { + const req: httpMocks.MockRequest = + httpMocks.createRequest(reqOptions); + + const res: httpMocks.MockResponse = httpMocks.createResponse({ + eventEmitter: EventEmitter, + req, + }); + + const done: Promise[]> = toPromise(res); + + // Get the response as text + const text: () => Promise = async () => { + const chunks = await done; + return buffersToString(chunks); + }; + + return { req, res, done, text }; +} + +function toPromise(res: httpMocks.MockResponse): Promise> { + return new Promise((resolve) => { + // node-mocks-http doesn't correctly handle non-Buffer typed arrays, + // so override the write method to fix it. + const write = res.write; + res.write = function (data: any, encoding?: any) { + if (ArrayBuffer.isView(data) && !Buffer.isBuffer(data)) { + data = Buffer.from(data.buffer); + } + return write.call(this, data, encoding); + }; + res.on('end', () => { + const chunks = (res as any)._getChunks(); + resolve(chunks); + }); + }); +} + +function buffersToString(buffers: Array) { + const decoder = new TextDecoder(); + let str = ''; + for (const buffer of buffers) { + str += decoder.decode(buffer); + } + return str; +} + +export function waitServerListen(server: Server): Promise { + return new Promise((resolve, reject) => { + function onListen() { + server.off('error', onError); + resolve(); + } + function onError(error: Error) { + server.off('listening', onListen); + reject(error); + } + server.once('listening', onListen); + server.once('error', onError); + }); +} diff --git a/packages/integrations/node/test/trailing-slash.test.js b/packages/integrations/node/test/trailing-slash.test.ts similarity index 95% rename from packages/integrations/node/test/trailing-slash.test.js rename to packages/integrations/node/test/trailing-slash.test.ts index 2f37f4d64df8..ef375823761d 100644 --- a/packages/integrations/node/test/trailing-slash.test.js +++ b/packages/integrations/node/test/trailing-slash.test.ts @@ -2,21 +2,15 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; import nodejs from '../dist/index.js'; -import { loadFixture, waitServerListen } from './test-utils.js'; - -/** - * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture - */ +import { type Fixture, loadFixture, waitServerListen, type AdapterServer } from './test-utils.ts'; describe('Trailing slash', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let server; + let fixture: Fixture; + let server: AdapterServer; describe('Always', async () => { describe('With base', async () => { before(async () => { process.env.ASTRO_NODE_AUTOSTART = 'disabled'; - process.env.PRERENDER = true; fixture = await loadFixture({ root: './fixtures/trailing-slash/', @@ -36,8 +30,6 @@ describe('Trailing slash', () => { after(async () => { await server.stop(); await fixture.clean(); - - delete process.env.PRERENDER; }); it('Can render prerendered base route', async () => { @@ -96,7 +88,6 @@ describe('Trailing slash', () => { describe('Without base', async () => { before(async () => { process.env.ASTRO_NODE_AUTOSTART = 'disabled'; - process.env.PRERENDER = true; fixture = await loadFixture({ root: './fixtures/trailing-slash/', @@ -115,8 +106,6 @@ describe('Trailing slash', () => { after(async () => { await server.stop(); await fixture.clean(); - - delete process.env.PRERENDER; }); it('Can render prerendered base route', async () => { @@ -214,7 +203,6 @@ describe('Trailing slash', () => { describe('With base', async () => { before(async () => { process.env.ASTRO_NODE_AUTOSTART = 'disabled'; - process.env.PRERENDER = true; fixture = await loadFixture({ root: './fixtures/trailing-slash/', @@ -234,8 +222,6 @@ describe('Trailing slash', () => { after(async () => { await server.stop(); await fixture.clean(); - - delete process.env.PRERENDER; }); it('Can render prerendered base route', async () => { @@ -276,7 +262,6 @@ describe('Trailing slash', () => { describe('Without base', async () => { before(async () => { process.env.ASTRO_NODE_AUTOSTART = 'disabled'; - process.env.PRERENDER = true; fixture = await loadFixture({ root: './fixtures/trailing-slash/', @@ -295,8 +280,6 @@ describe('Trailing slash', () => { after(async () => { await server.stop(); await fixture.clean(); - - delete process.env.PRERENDER; }); it('Can render prerendered base route', async () => { @@ -339,7 +322,6 @@ describe('Trailing slash', () => { describe('With base', async () => { before(async () => { process.env.ASTRO_NODE_AUTOSTART = 'disabled'; - process.env.PRERENDER = true; fixture = await loadFixture({ root: './fixtures/trailing-slash/', @@ -359,8 +341,6 @@ describe('Trailing slash', () => { after(async () => { await server.stop(); await fixture.clean(); - - delete process.env.PRERENDER; }); it('Can render prerendered base route', async () => { @@ -419,7 +399,6 @@ describe('Trailing slash', () => { describe('Without base', async () => { before(async () => { process.env.ASTRO_NODE_AUTOSTART = 'disabled'; - process.env.PRERENDER = true; fixture = await loadFixture({ root: './fixtures/trailing-slash/', @@ -438,8 +417,6 @@ describe('Trailing slash', () => { after(async () => { await server.stop(); await fixture.clean(); - - delete process.env.PRERENDER; }); it('Can render prerendered base route', async () => { diff --git a/packages/integrations/node/test/units/resolve-client-dir.test.js b/packages/integrations/node/test/units/resolve-client-dir.test.ts similarity index 100% rename from packages/integrations/node/test/units/resolve-client-dir.test.js rename to packages/integrations/node/test/units/resolve-client-dir.test.ts diff --git a/packages/integrations/node/test/units/serve-static-path-traversal.test.js b/packages/integrations/node/test/units/serve-static-path-traversal.test.ts similarity index 94% rename from packages/integrations/node/test/units/serve-static-path-traversal.test.js rename to packages/integrations/node/test/units/serve-static-path-traversal.test.ts index 6265450330a4..131aa5a2e73c 100644 --- a/packages/integrations/node/test/units/serve-static-path-traversal.test.js +++ b/packages/integrations/node/test/units/serve-static-path-traversal.test.ts @@ -2,12 +2,12 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import { describe, it, before, after } from 'node:test'; +import { after, before, describe, it } from 'node:test'; import { resolveStaticPath } from '../../dist/serve-static.js'; describe('resolveStaticPath', () => { - let tmpRoot; - let clientDir; + let tmpRoot: string; + let clientDir: string; before(() => { tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'astro-test-')); diff --git a/packages/integrations/node/test/url.test.js b/packages/integrations/node/test/url.test.ts similarity index 82% rename from packages/integrations/node/test/url.test.js rename to packages/integrations/node/test/url.test.ts index c3e90ead56f9..2c7b4fbac32f 100644 --- a/packages/integrations/node/test/url.test.js +++ b/packages/integrations/node/test/url.test.ts @@ -1,13 +1,13 @@ import * as assert from 'node:assert/strict'; +import { Socket } from 'node:net'; import { before, describe, it } from 'node:test'; import { TLSSocket } from 'node:tls'; import * as cheerio from 'cheerio'; import nodejs from '../dist/index.js'; -import { createRequestAndResponse, loadFixture } from './test-utils.js'; +import { createRequestAndResponse, type Fixture, loadFixture } from './test-utils.ts'; describe('URL', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -19,7 +19,7 @@ describe('URL', () => { }); it('return http when non-secure', async () => { - const { handler } = await import('./fixtures/url/dist/server/entry.mjs'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, text } = createRequestAndResponse({ url: '/', }); @@ -27,26 +27,28 @@ describe('URL', () => { handler(req, res); req.send(); - const html = await text(); + const html: string = await text(); assert.equal(html.includes('http:'), true); + assert.equal(html.includes('https:'), false); }); it('return https when secure', async () => { - const { handler } = await import('./fixtures/url/dist/server/entry.mjs'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, text } = createRequestAndResponse({ - socket: new TLSSocket(), + socket: new TLSSocket(new Socket()), url: '/', }); handler(req, res); req.send(); - const html = await text(); + const html: string = await text(); + assert.equal(html.includes('http:'), false); assert.equal(html.includes('https:'), true); }); it('return http when the X-Forwarded-Proto header is set to http', async () => { - const { handler } = await import('./fixtures/url/dist/server/entry.mjs'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, text } = createRequestAndResponse({ headers: { 'X-Forwarded-Proto': 'http' }, url: '/', @@ -60,7 +62,7 @@ describe('URL', () => { }); it('return https when the X-Forwarded-Proto header is set to https', async () => { - const { handler } = await import('./fixtures/url/dist/server/entry.mjs'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, text } = createRequestAndResponse({ headers: { 'X-Forwarded-Proto': 'https' }, url: '/', @@ -74,7 +76,7 @@ describe('URL', () => { }); it('includes forwarded host and port in the url', async () => { - const { handler } = await import('./fixtures/url/dist/server/entry.mjs'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, text } = createRequestAndResponse({ headers: { 'X-Forwarded-Proto': 'https', @@ -95,7 +97,7 @@ describe('URL', () => { }); it('accepts port in forwarded host and forwarded port', async () => { - const { handler } = await import('./fixtures/url/dist/server/entry.mjs'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, text } = createRequestAndResponse({ headers: { 'X-Forwarded-Proto': 'https', @@ -115,7 +117,7 @@ describe('URL', () => { }); it('ignores X-Forwarded-Host when no allowedDomains configured', async () => { - const { handler } = await import('./fixtures/url/dist/server/entry.mjs'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, text } = createRequestAndResponse({ headers: { 'X-Forwarded-Proto': 'https', @@ -136,7 +138,7 @@ describe('URL', () => { }); it('rejects port in forwarded host when port not in allowedDomains', async () => { - const { handler } = await import('./fixtures/url/dist/server/entry.mjs'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, text } = createRequestAndResponse({ headers: { 'X-Forwarded-Proto': 'https', @@ -157,7 +159,7 @@ describe('URL', () => { }); it('rejects empty X-Forwarded-Host with allowedDomains configured', async () => { - const { handler } = await import('./fixtures/url/dist/server/entry.mjs'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, text } = createRequestAndResponse({ headers: { 'X-Forwarded-Proto': 'https', @@ -178,7 +180,7 @@ describe('URL', () => { }); it('rejects X-Forwarded-Host with path injection attempt', async () => { - const { handler } = await import('./fixtures/url/dist/server/entry.mjs'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, text } = createRequestAndResponse({ headers: { 'X-Forwarded-Proto': 'https', diff --git a/packages/integrations/node/test/well-known-locations.test.js b/packages/integrations/node/test/well-known-locations.test.ts similarity index 84% rename from packages/integrations/node/test/well-known-locations.test.js rename to packages/integrations/node/test/well-known-locations.test.ts index 57082f28ad6e..a8afdd1519b4 100644 --- a/packages/integrations/node/test/well-known-locations.test.js +++ b/packages/integrations/node/test/well-known-locations.test.ts @@ -1,11 +1,11 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; +import type { PreviewServer } from '../../../astro/src/types/public/preview.js'; import nodejs from '../dist/index.js'; -import { createRequestAndResponse, loadFixture } from './test-utils.js'; +import { createRequestAndResponse, type Fixture, loadFixture } from './test-utils.ts'; describe('test URIs beginning with a dot', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -17,7 +17,7 @@ describe('test URIs beginning with a dot', () => { }); describe('can load well-known URIs', async () => { - let devPreview; + let devPreview: PreviewServer; before(async () => { devPreview = await fixture.preview(); @@ -46,7 +46,7 @@ describe('test URIs beginning with a dot', () => { describe('dotfile access via unnormalized paths', async () => { it('denies dotfile access when path contains .well-known/../ traversal', async () => { - const { handler } = await import('./fixtures/well-known-locations/dist/server/entry.mjs'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, done } = createRequestAndResponse({ method: 'GET', url: '/.well-known/../.hidden-file', @@ -64,7 +64,7 @@ describe('test URIs beginning with a dot', () => { }); it('denies dotfolder file access when path contains .well-known/../ traversal', async () => { - const { handler } = await import('./fixtures/well-known-locations/dist/server/entry.mjs'); + const handler = await fixture.loadNodeAdapterHandler(); const { req, res, done } = createRequestAndResponse({ method: 'GET', url: '/.well-known/../.hidden/file.json', diff --git a/packages/integrations/node/tsconfig.test.json b/packages/integrations/node/tsconfig.test.json new file mode 100644 index 000000000000..27c89c5fe7a7 --- /dev/null +++ b/packages/integrations/node/tsconfig.test.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["test/**/*.ts"], + "exclude": ["test/fixtures/**"], + "compilerOptions": { + "noEmit": true, + "allowJs": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "rewriteRelativeImportExtensions": true + }, + "references": [ + { + "path": "../../astro/tsconfig.test.json" + } + ] +} diff --git a/packages/integrations/partytown/CHANGELOG.md b/packages/integrations/partytown/CHANGELOG.md index ac6e563a923a..f02cf6c2c9cb 100644 --- a/packages/integrations/partytown/CHANGELOG.md +++ b/packages/integrations/partytown/CHANGELOG.md @@ -1,5 +1,11 @@ # @astrojs/partytown +## 2.1.7 + +### Patch Changes + +- [#16265](https://github.com/withastro/astro/pull/16265) [`7fe40bc`](https://github.com/withastro/astro/commit/7fe40bc7381d981dedad16625d89c00e31cd8fd0) Thanks [@ChrisLaRocque](https://github.com/ChrisLaRocque)! - Updates `@qwik.dev/partytown` to 0.13.2 + ## 2.1.6 ### Patch Changes diff --git a/packages/integrations/partytown/package.json b/packages/integrations/partytown/package.json index f49d9c95ade6..69c4baba0447 100644 --- a/packages/integrations/partytown/package.json +++ b/packages/integrations/partytown/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/partytown", "description": "Use Partytown to move scripts into a web worker in your Astro project", - "version": "2.1.6", + "version": "2.1.7", "type": "module", "types": "./dist/index.d.ts", "author": "withastro", @@ -32,7 +32,7 @@ "dev": "astro-scripts dev \"src/**/*.ts\"" }, "dependencies": { - "@qwik.dev/partytown": "^0.11.2", + "@qwik.dev/partytown": "^0.13.2", "mrmime": "^2.0.1" }, "devDependencies": { diff --git a/packages/integrations/preact/CHANGELOG.md b/packages/integrations/preact/CHANGELOG.md index 05d807ee4ff4..e048aeefc3a1 100644 --- a/packages/integrations/preact/CHANGELOG.md +++ b/packages/integrations/preact/CHANGELOG.md @@ -1,5 +1,11 @@ # @astrojs/preact +## 5.1.1 + +### Patch Changes + +- [#16180](https://github.com/withastro/astro/pull/16180) [`1d1448c`](https://github.com/withastro/astro/commit/1d1448c2c0e1a149709ada5d00a74f1cd7c1142b) Thanks [@matthewp](https://github.com/matthewp)! - Pre-optimizes `@preact/signals` and `preact/hooks` in the Vite dep optimizer to prevent late discovery triggering full page reloads during dev + ## 5.1.0 ### Minor Changes diff --git a/packages/integrations/preact/package.json b/packages/integrations/preact/package.json index 79b8ef348017..e2d8464db5d3 100644 --- a/packages/integrations/preact/package.json +++ b/packages/integrations/preact/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/preact", "description": "Use Preact components within Astro", - "version": "5.1.0", + "version": "5.1.1", "type": "module", "types": "./dist/index.d.ts", "author": "withastro", diff --git a/packages/integrations/preact/src/index.ts b/packages/integrations/preact/src/index.ts index 2a3c1d20e9b2..205c696c6e04 100644 --- a/packages/integrations/preact/src/index.ts +++ b/packages/integrations/preact/src/index.ts @@ -126,6 +126,8 @@ function configEnvironmentPlugin(compat: boolean | undefined): Plugin { '@astrojs/preact/client.js', 'preact', 'preact/jsx-runtime', + 'preact/hooks', + '@astrojs/preact > @preact/signals', ]; } diff --git a/packages/integrations/react/CHANGELOG.md b/packages/integrations/react/CHANGELOG.md index 288acdc989fd..5e4ed2d2f34d 100644 --- a/packages/integrations/react/CHANGELOG.md +++ b/packages/integrations/react/CHANGELOG.md @@ -1,5 +1,11 @@ # @astrojs/react +## 5.0.3 + +### Patch Changes + +- [#16224](https://github.com/withastro/astro/pull/16224) [`a2b9eeb`](https://github.com/withastro/astro/commit/a2b9eeb14e300c9b6ce1d6ea423d20f4ef9d92f5) Thanks [@fkatsuhiro](https://github.com/fkatsuhiro)! - Fix React 19 "Float" mechanism injecting into Astro islands instead of the . This PR adds a filter to @astrojs/react to strip these auto-generated resource from the island's HTML output, ensuring valid HTML structure. + ## 5.0.2 ### Patch Changes diff --git a/packages/integrations/react/package.json b/packages/integrations/react/package.json index cc60c6d14cd3..bdd9f52439a0 100644 --- a/packages/integrations/react/package.json +++ b/packages/integrations/react/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/react", "description": "Use React components within Astro", - "version": "5.0.2", + "version": "5.0.3", "type": "module", "types": "./dist/index.d.ts", "author": "withastro", @@ -36,7 +36,8 @@ "build": "astro-scripts build \"src/**/*.ts\" && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"", - "test": "astro-scripts test \"test/**/*.test.js\"" + "test": "astro-scripts test \"test/**/*.test.ts\"", + "typecheck:tests": "tsc --build tsconfig.test.json" }, "dependencies": { "@astrojs/internal-helpers": "workspace:*", diff --git a/packages/integrations/react/src/server.ts b/packages/integrations/react/src/server.ts index 4a610d429586..7da3c4535c93 100644 --- a/packages/integrations/react/src/server.ts +++ b/packages/integrations/react/src/server.ts @@ -129,6 +129,13 @@ async function renderToStaticMarkup( } else { html = await renderToPipeableStreamAsync(vnode, renderOptions); } + // Strip React 19 auto-injected resource hints (preloads, etc.) from island output. + // These should be in , not inside the island. + // See: https://github.com/facebook/react/issues/27910 + html = html.replace( + /]*rel="(?:preload|modulepreload|stylesheet|preconnect|dns-prefetch)"[^>]*>/g, + '', + ); return { html, attrs }; } diff --git a/packages/astro/test/fixtures/middleware-sequence-request-clone/astro.config.mjs b/packages/integrations/react/test/fixtures/react-19-preloads/astro.config.mjs similarity index 55% rename from packages/astro/test/fixtures/middleware-sequence-request-clone/astro.config.mjs rename to packages/integrations/react/test/fixtures/react-19-preloads/astro.config.mjs index 0c1b887d0eec..657f300a70d6 100644 --- a/packages/astro/test/fixtures/middleware-sequence-request-clone/astro.config.mjs +++ b/packages/integrations/react/test/fixtures/react-19-preloads/astro.config.mjs @@ -1,8 +1,6 @@ import { defineConfig } from 'astro/config'; +import react from '@astrojs/react'; -// https://astro.build/config export default defineConfig({ - vite: { - plugins: [], - } + integrations: [react()], }); diff --git a/packages/integrations/react/test/fixtures/react-19-preloads/package.json b/packages/integrations/react/test/fixtures/react-19-preloads/package.json new file mode 100644 index 000000000000..b7c092cfdf12 --- /dev/null +++ b/packages/integrations/react/test/fixtures/react-19-preloads/package.json @@ -0,0 +1,10 @@ +{ + "name": "@fixture/react-19-preloads", + "type": "module", + "dependencies": { + "astro": "latest", + "@astrojs/react": "latest", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } +} diff --git a/packages/integrations/react/test/fixtures/react-19-preloads/src/components/ImageComponent.jsx b/packages/integrations/react/test/fixtures/react-19-preloads/src/components/ImageComponent.jsx new file mode 100644 index 000000000000..881bcc6ba432 --- /dev/null +++ b/packages/integrations/react/test/fixtures/react-19-preloads/src/components/ImageComponent.jsx @@ -0,0 +1,8 @@ +export default function ImageComponent() { + return ( +
    +

    React 19 Island

    + Test +
    + ); +} diff --git a/packages/integrations/react/test/fixtures/react-19-preloads/src/pages/index.astro b/packages/integrations/react/test/fixtures/react-19-preloads/src/pages/index.astro new file mode 100644 index 000000000000..6df24b0504b1 --- /dev/null +++ b/packages/integrations/react/test/fixtures/react-19-preloads/src/pages/index.astro @@ -0,0 +1,11 @@ +--- +import ImageComponent from '../components/ImageComponent'; +--- + + + React 19 Test + + + + + diff --git a/packages/integrations/react/test/parsed-react-children.test.js b/packages/integrations/react/test/parsed-react-children.test.ts similarity index 100% rename from packages/integrations/react/test/parsed-react-children.test.js rename to packages/integrations/react/test/parsed-react-children.test.ts diff --git a/packages/integrations/react/test/react-19-preloads.test.ts b/packages/integrations/react/test/react-19-preloads.test.ts new file mode 100644 index 000000000000..ccca49827066 --- /dev/null +++ b/packages/integrations/react/test/react-19-preloads.test.ts @@ -0,0 +1,20 @@ +import assert from 'node:assert'; +import { test } from 'node:test'; +import { loadFixture } from '../../../astro/test/test-utils.js'; + +test.describe('React 19 SSR integration', () => { + test('should strip preloads to prevent invalid HTML inside astro-islands', async () => { + const fixture = await loadFixture({ + root: new URL('./fixtures/react-19-preloads/', import.meta.url), + }); + await fixture.build(); + + const html = await fixture.readFile('/index.html'); + const islandPattern = /]*>([\s\S]*?)<\/astro-island>/; + const match = islandPattern.exec(html); + const island = match ? match[1] : ''; + + assert.ok(!island.includes('rel="preload"'), 'React 19: preloads should be stripped'); + assert.ok(island.includes(' { before(async () => { @@ -144,8 +149,7 @@ describe('React Components', () => { if (isWindows) return; describe('dev', () => { - /** @type {import('../../../astro/test/test-utils.js').Fixture} */ - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); @@ -156,7 +160,8 @@ describe('React Components', () => { }); it('scripts proxy correctly', async () => { - const html = await fixture.fetch('/').then((res) => res.text()); + const response = await fixture.fetch('/'); + const html: string = await response.text(); const $ = cheerioLoad(html); for (const script of $('script').toArray()) { @@ -168,9 +173,10 @@ describe('React Components', () => { // TODO: move this to separate dev test? it.skip('Throws helpful error message on window SSR', async () => { - const html = await fixture.fetch('/window/index.html'); + const response = await fixture.fetch('/window/index.html'); + const html: string = await response.text(); assert.ok( - (await html.text()).includes( + html.includes( `[/window] The window object is not available during server-side rendering (SSR). Try using \`import.meta.env.SSR\` to write SSR-friendly code. @@ -181,19 +187,26 @@ describe('React Components', () => { // In moving over to Vite, the jsx-runtime import is now obscured. TODO: update the method of finding this. it.skip('uses the new JSX transform', async () => { - const html = await fixture.fetch('/index.html'); + const response = await fixture.fetch('/index.html'); + const html: string = await response.text(); // Grab the imports const exp = /import\("(.+?)"\)/g; - let match, componentUrl; + let match, componentUrl: string | undefined; while ((match = exp.exec(html))) { if (match[1].includes('Research.js')) { componentUrl = match[1]; break; } } + if (!componentUrl) { + throw new Error('Could not find component URL in HTML'); + } + const component = await fixture.readFile(componentUrl); - const jsxRuntime = component.imports.filter((i) => i.specifier.includes('jsx-runtime')); + // @ts-expect-error: error TS2339: Property 'imports' does not exist on type 'string'. + const imports = component.imports; + const jsxRuntime = imports.filter((i: any) => i.specifier.includes('jsx-runtime')); // test 1: react/jsx-runtime is used for the component assert.ok(jsxRuntime); diff --git a/packages/integrations/react/tsconfig.test.json b/packages/integrations/react/tsconfig.test.json new file mode 100644 index 000000000000..27c89c5fe7a7 --- /dev/null +++ b/packages/integrations/react/tsconfig.test.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["test/**/*.ts"], + "exclude": ["test/fixtures/**"], + "compilerOptions": { + "noEmit": true, + "allowJs": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "rewriteRelativeImportExtensions": true + }, + "references": [ + { + "path": "../../astro/tsconfig.test.json" + } + ] +} diff --git a/packages/integrations/sitemap/package.json b/packages/integrations/sitemap/package.json index a0edd9f3ca46..abcc7c8afdb5 100644 --- a/packages/integrations/sitemap/package.json +++ b/packages/integrations/sitemap/package.json @@ -30,7 +30,8 @@ "build": "astro-scripts build \"src/**/*.ts\" && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"", - "test": "astro-scripts test \"test/**/*.test.js\"" + "test": "astro-scripts test \"test/**/*.test.ts\"", + "typecheck:tests": "tsc --build tsconfig.test.json" }, "dependencies": { "sitemap": "^9.0.0", diff --git a/packages/integrations/sitemap/test/base-path.test.js b/packages/integrations/sitemap/test/base-path.test.ts similarity index 94% rename from packages/integrations/sitemap/test/base-path.test.js rename to packages/integrations/sitemap/test/base-path.test.ts index fee031ff4e02..4444801a3670 100644 --- a/packages/integrations/sitemap/test/base-path.test.js +++ b/packages/integrations/sitemap/test/base-path.test.ts @@ -1,10 +1,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { loadFixture, readXML } from './test-utils.js'; +import type { Fixture } from '../../../astro/test/test-utils.js'; describe('URLs with base path', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; describe('using node adapter', () => { before(async () => { diff --git a/packages/integrations/sitemap/test/chunks-files.test.js b/packages/integrations/sitemap/test/chunks-files.test.ts similarity index 71% rename from packages/integrations/sitemap/test/chunks-files.test.js rename to packages/integrations/sitemap/test/chunks-files.test.ts index 0fe34078fb25..025b44c98734 100644 --- a/packages/integrations/sitemap/test/chunks-files.test.js +++ b/packages/integrations/sitemap/test/chunks-files.test.ts @@ -1,15 +1,15 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; +import { EnumChangefreq } from 'sitemap'; import { sitemap } from './fixtures/static/deps.mjs'; import { loadFixture, readXML } from './test-utils.js'; +import type { Fixture } from '../../../astro/test/test-utils.js'; describe('Sitemap with chunked files', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; - /** @type {string[]} */ - let blogUrls; - let glossaryUrls; - let pagesUrls; + let fixture: Fixture; + let blogUrls: string[]; + let glossaryUrls: string[]; + let pagesUrls: string[]; before(async () => { fixture = await loadFixture({ @@ -22,7 +22,8 @@ describe('Sitemap with chunked files', () => { chunks: { blog: (item) => { if (item.url.includes('blog')) { - item.changefreq = 'weekly'; + item.changefreq = EnumChangefreq.WEEKLY; + // @ts-expect-error - a string is expected but the original JS code assigns a Date object here item.lastmod = new Date(); item.priority = 0.9; return item; @@ -30,7 +31,8 @@ describe('Sitemap with chunked files', () => { }, glossary: (item) => { if (item.url.includes('glossary')) { - item.changefreq = 'weekly'; + item.changefreq = EnumChangefreq.WEEKLY; + // @ts-expect-error - a string is expected but the original JS code assigns a Date object here item.lastmod = new Date(); item.priority = 0.9; return item; @@ -41,9 +43,9 @@ describe('Sitemap with chunked files', () => { ], }); await fixture.build(); - const flatMapUrls = async (file) => { + const flatMapUrls = async (file: string) => { const data = await readXML(fixture.readFile(file)); - return data.urlset.url.map((url) => url.loc[0]); + return data.urlset.url.map((url: { loc: string[] }) => url.loc[0]); }; blogUrls = await flatMapUrls('sitemap-blog-0.xml'); glossaryUrls = await flatMapUrls('sitemap-glossary-0.xml'); diff --git a/packages/integrations/sitemap/test/config.test.js b/packages/integrations/sitemap/test/config.test.ts similarity index 97% rename from packages/integrations/sitemap/test/config.test.js rename to packages/integrations/sitemap/test/config.test.ts index f95333876d25..aa55bac77e22 100644 --- a/packages/integrations/sitemap/test/config.test.js +++ b/packages/integrations/sitemap/test/config.test.ts @@ -2,10 +2,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { sitemap } from './fixtures/static/deps.mjs'; import { loadFixture, readXML } from './test-utils.js'; +import type { Fixture } from '../../../astro/test/test-utils.js'; describe('Config', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; describe('Static', () => { before(async () => { diff --git a/packages/integrations/sitemap/test/custom-pages.test.js b/packages/integrations/sitemap/test/custom-pages.test.ts similarity index 78% rename from packages/integrations/sitemap/test/custom-pages.test.js rename to packages/integrations/sitemap/test/custom-pages.test.ts index 45ea60b839ab..370ef67ba3ad 100644 --- a/packages/integrations/sitemap/test/custom-pages.test.js +++ b/packages/integrations/sitemap/test/custom-pages.test.ts @@ -2,12 +2,11 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { sitemap } from './fixtures/static/deps.mjs'; import { loadFixture, readXML } from './test-utils.js'; +import type { Fixture } from '../../../astro/test/test-utils.js'; describe('Sitemap with custom pages', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; - /** @type {string[]} */ - let urls; + let fixture: Fixture; + let urls: string[]; before(async () => { fixture = await loadFixture({ @@ -20,7 +19,7 @@ describe('Sitemap with custom pages', () => { }); await fixture.build(); const data = await readXML(fixture.readFile('/sitemap-0.xml')); - urls = data.urlset.url.map((url) => url.loc[0]); + urls = data.urlset.url.map((url: { loc: string[] }) => url.loc[0]); }); it('includes defined custom pages', async () => { diff --git a/packages/integrations/sitemap/test/custom-sitemaps.test.js b/packages/integrations/sitemap/test/custom-sitemaps.test.ts similarity index 75% rename from packages/integrations/sitemap/test/custom-sitemaps.test.js rename to packages/integrations/sitemap/test/custom-sitemaps.test.ts index 7794e626d157..3c6bcb6f8f4b 100644 --- a/packages/integrations/sitemap/test/custom-sitemaps.test.js +++ b/packages/integrations/sitemap/test/custom-sitemaps.test.ts @@ -2,12 +2,11 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { sitemap } from './fixtures/static/deps.mjs'; import { loadFixture, readXML } from './test-utils.js'; +import type { Fixture } from '../../../astro/test/test-utils.js'; describe('Custom sitemaps', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; - /** @type {{ [key: string]: string }} */ - let sitemaps; + let fixture: Fixture; + let sitemaps: { loc: string; lastmod: string }[]; before(async () => { fixture = await loadFixture({ @@ -21,7 +20,10 @@ describe('Custom sitemaps', () => { }); await fixture.build(); const data = await readXML(fixture.readFile('/sitemap-index.xml')); - sitemaps = data.sitemapindex.sitemap.map((s) => ({ loc: s.loc[0], lastmod: s.lastmod[0] })); + sitemaps = data.sitemapindex.sitemap.map((s: { loc: string[]; lastmod: string[] }) => ({ + loc: s.loc[0], + lastmod: s.lastmod[0], + })); }); it('includes defined custom sitemaps', async () => { diff --git a/packages/integrations/sitemap/test/dynamic-path.test.js b/packages/integrations/sitemap/test/dynamic-path.test.ts similarity index 78% rename from packages/integrations/sitemap/test/dynamic-path.test.js rename to packages/integrations/sitemap/test/dynamic-path.test.ts index eab3b912c1bc..7290bfc6b6d6 100644 --- a/packages/integrations/sitemap/test/dynamic-path.test.js +++ b/packages/integrations/sitemap/test/dynamic-path.test.ts @@ -1,10 +1,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { loadFixture, readXML } from './test-utils.js'; +import type { Fixture } from '../../../astro/test/test-utils.js'; describe('Dynamic with rest parameter', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -15,7 +15,7 @@ describe('Dynamic with rest parameter', () => { it('Should generate correct urls', async () => { const data = await readXML(fixture.readFile('/sitemap-0.xml')); - const urls = data.urlset.url.map((url) => url.loc[0]); + const urls = data.urlset.url.map((url: { loc: string[] }) => url.loc[0]); assert.ok(urls.includes('http://example.com/')); assert.ok(urls.includes('http://example.com/blog/')); diff --git a/packages/integrations/sitemap/test/i18n-fallback.test.js b/packages/integrations/sitemap/test/i18n-fallback.test.ts similarity index 80% rename from packages/integrations/sitemap/test/i18n-fallback.test.js rename to packages/integrations/sitemap/test/i18n-fallback.test.ts index 6d621ca81271..c3e2d0fef35f 100644 --- a/packages/integrations/sitemap/test/i18n-fallback.test.js +++ b/packages/integrations/sitemap/test/i18n-fallback.test.ts @@ -1,12 +1,11 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { loadFixture, readXML } from './test-utils.js'; +import type { Fixture } from '../../../astro/test/test-utils.js'; describe('i18n fallback', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; - /** @type {string[]} */ - let urls; + let fixture: Fixture; + let urls: string[]; before(async () => { fixture = await loadFixture({ @@ -14,7 +13,7 @@ describe('i18n fallback', () => { }); await fixture.build(); const data = await readXML(fixture.readFile('/sitemap-0.xml')); - urls = data.urlset.url.map((url) => url.loc[0]); + urls = data.urlset.url.map((url: { loc: string[] }) => url.loc[0]); }); it('includes default locale pages', async () => { diff --git a/packages/integrations/sitemap/test/namespaces.test.js b/packages/integrations/sitemap/test/namespaces.test.ts similarity index 96% rename from packages/integrations/sitemap/test/namespaces.test.js rename to packages/integrations/sitemap/test/namespaces.test.ts index 79c0c44d4022..3952b6b1b09e 100644 --- a/packages/integrations/sitemap/test/namespaces.test.js +++ b/packages/integrations/sitemap/test/namespaces.test.ts @@ -2,9 +2,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { sitemap } from './fixtures/static/deps.mjs'; import { loadFixture } from './test-utils.js'; +import type { Fixture } from '../../../astro/test/test-utils.js'; describe('Namespaces Configuration', () => { - let fixture; + let fixture: Fixture; describe('Default namespaces', () => { before(async () => { diff --git a/packages/integrations/sitemap/test/routes.test.js b/packages/integrations/sitemap/test/routes.test.ts similarity index 77% rename from packages/integrations/sitemap/test/routes.test.js rename to packages/integrations/sitemap/test/routes.test.ts index 00d6ccde305b..eebeb2589b5d 100644 --- a/packages/integrations/sitemap/test/routes.test.js +++ b/packages/integrations/sitemap/test/routes.test.ts @@ -1,12 +1,11 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { loadFixture, readXML } from './test-utils.js'; +import type { Fixture } from '../../../astro/test/test-utils.js'; describe('routes', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; - /** @type {string[]} */ - let urls; + let fixture: Fixture; + let urls: string[]; before(async () => { fixture = await loadFixture({ @@ -14,7 +13,7 @@ describe('routes', () => { }); await fixture.build(); const data = await readXML(fixture.readFile('/sitemap-0.xml')); - urls = data.urlset.url.map((url) => url.loc[0]); + urls = data.urlset.url.map((url: { loc: string[] }) => url.loc[0]); }); it('does not include endpoints', async () => { diff --git a/packages/integrations/sitemap/test/smoke.test.js b/packages/integrations/sitemap/test/smoke.test.ts similarity index 100% rename from packages/integrations/sitemap/test/smoke.test.js rename to packages/integrations/sitemap/test/smoke.test.ts diff --git a/packages/integrations/sitemap/test/ssr.test.js b/packages/integrations/sitemap/test/ssr.test.ts similarity index 86% rename from packages/integrations/sitemap/test/ssr.test.js rename to packages/integrations/sitemap/test/ssr.test.ts index b5c92698b3ba..022f77a7de05 100644 --- a/packages/integrations/sitemap/test/ssr.test.js +++ b/packages/integrations/sitemap/test/ssr.test.ts @@ -1,10 +1,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { loadFixture, readXML } from './test-utils.js'; +import type { Fixture } from '../../../astro/test/test-utils.js'; describe('SSR support', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ diff --git a/packages/integrations/sitemap/test/staticPaths.test.js b/packages/integrations/sitemap/test/staticPaths.test.ts similarity index 88% rename from packages/integrations/sitemap/test/staticPaths.test.js rename to packages/integrations/sitemap/test/staticPaths.test.ts index 7df9d5cb6ac7..56a892755662 100644 --- a/packages/integrations/sitemap/test/staticPaths.test.js +++ b/packages/integrations/sitemap/test/staticPaths.test.ts @@ -1,12 +1,11 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { loadFixture, readXML } from './test-utils.js'; +import type { Fixture } from '../../../astro/test/test-utils.js'; describe('getStaticPaths support', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; - /** @type {string[]} */ - let urls; + let fixture: Fixture; + let urls: string[]; before(async () => { fixture = await loadFixture({ @@ -16,7 +15,7 @@ describe('getStaticPaths support', () => { await fixture.build(); const data = await readXML(fixture.readFile('/sitemap-0.xml')); - urls = data.urlset.url.map((url) => url.loc[0]); + urls = data.urlset.url.map((url: { loc: string[] }) => url.loc[0]); }); it('requires zero config for getStaticPaths', async () => { diff --git a/packages/integrations/sitemap/test/trailing-slash.test.js b/packages/integrations/sitemap/test/trailing-slash.test.ts similarity index 97% rename from packages/integrations/sitemap/test/trailing-slash.test.js rename to packages/integrations/sitemap/test/trailing-slash.test.ts index 181f0def53d2..229cf66516ad 100644 --- a/packages/integrations/sitemap/test/trailing-slash.test.js +++ b/packages/integrations/sitemap/test/trailing-slash.test.ts @@ -1,10 +1,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { loadFixture, readXML } from './test-utils.js'; +import type { Fixture } from '../../../astro/test/test-utils.js'; describe('Trailing slash', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; describe('trailingSlash: ignore', () => { describe('build.format: directory', () => { diff --git a/packages/integrations/sitemap/test/units/generate-sitemap.test.js b/packages/integrations/sitemap/test/units/generate-sitemap.test.ts similarity index 100% rename from packages/integrations/sitemap/test/units/generate-sitemap.test.js rename to packages/integrations/sitemap/test/units/generate-sitemap.test.ts diff --git a/packages/integrations/sitemap/tsconfig.test.json b/packages/integrations/sitemap/tsconfig.test.json new file mode 100644 index 000000000000..27c89c5fe7a7 --- /dev/null +++ b/packages/integrations/sitemap/tsconfig.test.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["test/**/*.ts"], + "exclude": ["test/fixtures/**"], + "compilerOptions": { + "noEmit": true, + "allowJs": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "rewriteRelativeImportExtensions": true + }, + "references": [ + { + "path": "../../astro/tsconfig.test.json" + } + ] +} diff --git a/packages/integrations/svelte/CHANGELOG.md b/packages/integrations/svelte/CHANGELOG.md index c1d7b51b14bf..9f51ad4aa10c 100644 --- a/packages/integrations/svelte/CHANGELOG.md +++ b/packages/integrations/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # @astrojs/svelte +## 8.0.5 + +### Patch Changes + +- [#16210](https://github.com/withastro/astro/pull/16210) [`e030bd0`](https://github.com/withastro/astro/commit/e030bd058457505b605ef573cfc71239baa963f0) Thanks [@matthewp](https://github.com/matthewp)! - Fixes `.svelte` files in `node_modules` failing with `Unknown file extension ".svelte"` when using the Cloudflare adapter with `prerenderEnvironment: 'node'` + ## 8.0.4 ### Patch Changes diff --git a/packages/integrations/svelte/package.json b/packages/integrations/svelte/package.json index b4ad39589949..509858c5e17f 100644 --- a/packages/integrations/svelte/package.json +++ b/packages/integrations/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@astrojs/svelte", - "version": "8.0.4", + "version": "8.0.5", "description": "Use Svelte components within Astro", "type": "module", "types": "./dist/index.d.ts", @@ -35,12 +35,14 @@ "build": "astro-scripts build \"src/**/*.ts\" && astro-scripts build \"src/editor.cts\" --force-cjs --no-clean-dist && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\" && astro-scripts build \"src/editor.cts\" --force-cjs --no-clean-dist", "dev": "astro-scripts dev \"src/**/*.ts\"", - "test": "astro-scripts test \"test/**/*.test.js\"" + "test": "astro-scripts test \"test/**/*.test.ts\"", + "typecheck:tests": "tsc --build tsconfig.test.json" }, "dependencies": { "@sveltejs/vite-plugin-svelte": "^6.2.4", "svelte2tsx": "^0.7.52", - "vite": "^7.3.1" + "vite": "^7.3.1", + "vitefu": "^1.1.2" }, "devDependencies": { "astro": "workspace:*", diff --git a/packages/integrations/svelte/src/index.ts b/packages/integrations/svelte/src/index.ts index b391cd1c90cb..9a92bd6a354b 100644 --- a/packages/integrations/svelte/src/index.ts +++ b/packages/integrations/svelte/src/index.ts @@ -1,7 +1,9 @@ import type { Options } from '@sveltejs/vite-plugin-svelte'; import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import type { AstroIntegration, AstroRenderer } from 'astro'; +import { fileURLToPath } from 'node:url'; import type { Plugin } from 'vite'; +import { crawlFrameworkPkgs } from 'vitefu'; import { createSvelteOptimizeEsbuildPlugins } from './optimize-esbuild-plugins.js'; function getRenderer(): AstroRenderer { @@ -18,11 +20,20 @@ export default function svelteIntegration(options?: Options): AstroIntegration { return { name: '@astrojs/svelte', hooks: { - 'astro:config:setup': async ({ updateConfig, addRenderer }) => { + 'astro:config:setup': async ({ config, updateConfig, addRenderer }) => { addRenderer(getRenderer()); + + const sveltePackages = await crawlFrameworkPkgs({ + root: fileURLToPath(config.root), + isBuild: false, + isFrameworkPkgByJson(pkgJson) { + return !!pkgJson.peerDependencies?.svelte; + }, + }); + updateConfig({ vite: { - plugins: [svelte(options), configEnvironmentPlugin()], + plugins: [svelte(options), configEnvironmentPlugin(sveltePackages.ssr.noExternal)], }, }); }, @@ -30,16 +41,42 @@ export default function svelteIntegration(options?: Options): AstroIntegration { }; } -function configEnvironmentPlugin(): Plugin { +function configEnvironmentPlugin(svelteNoExternal: string[]): Plugin { return { name: '@astrojs/svelte:config-environment', configEnvironment(environmentName, options) { + const isServer = environmentName !== 'client'; + + if (isServer && svelteNoExternal.length > 0) { + // Add svelte framework packages to noExternal so they go through + // Vite's transform pipeline (Node can't import .svelte files natively). + const result: any = { + resolve: { + noExternal: svelteNoExternal, + }, + }; + + if ( + (environmentName === 'ssr' || environmentName === 'prerender') && + options.optimizeDeps?.noDiscovery === false + ) { + result.optimizeDeps = { + include: ['svelte/server', 'svelte/internal/server'], + exclude: ['@astrojs/svelte/server.js'], + esbuildOptions: { + plugins: createSvelteOptimizeEsbuildPlugins('server'), + }, + }; + } + + return result; + } + if ( environmentName === 'client' || ((environmentName === 'ssr' || environmentName === 'prerender') && options.optimizeDeps?.noDiscovery === false) ) { - const isServer = environmentName !== 'client'; return { optimizeDeps: { include: isServer diff --git a/packages/integrations/svelte/test/async-rendering.test.js b/packages/integrations/svelte/test/async-rendering.test.ts similarity index 87% rename from packages/integrations/svelte/test/async-rendering.test.js rename to packages/integrations/svelte/test/async-rendering.test.ts index bcbb8a3ba4a4..31c0818de647 100644 --- a/packages/integrations/svelte/test/async-rendering.test.js +++ b/packages/integrations/svelte/test/async-rendering.test.ts @@ -1,9 +1,9 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { load as cheerioLoad } from 'cheerio'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture, type DevServer } from '../../../astro/test/test-utils.js'; -let fixture; +let fixture: Fixture; // Svelte made breaking changes to async rendering in a patch. // TODO figure out if we need to change our code or not, might just be an upstream bug. @@ -28,8 +28,7 @@ describe.skip('Async rendering', () => { }); describe('dev', () => { - /** @type {import('../../../astro/test/test-utils.js').Fixture} */ - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); diff --git a/packages/integrations/svelte/test/check.test.js b/packages/integrations/svelte/test/check.test.ts similarity index 100% rename from packages/integrations/svelte/test/check.test.js rename to packages/integrations/svelte/test/check.test.ts diff --git a/packages/integrations/svelte/test/conditional-rendering.test.js b/packages/integrations/svelte/test/conditional-rendering.test.ts similarity index 94% rename from packages/integrations/svelte/test/conditional-rendering.test.js rename to packages/integrations/svelte/test/conditional-rendering.test.ts index 42bc8e6aad1c..9abe1e53f623 100644 --- a/packages/integrations/svelte/test/conditional-rendering.test.js +++ b/packages/integrations/svelte/test/conditional-rendering.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { load as cheerioLoad } from 'cheerio'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture, type DevServer } from '../../../astro/test/test-utils.js'; /** * @see https://github.com/withastro/astro/issues/14252 @@ -11,7 +11,7 @@ import { loadFixture } from '../../../astro/test/test-utils.js'; * the condition is initially false during SSR. */ -let fixture; +let fixture: Fixture; describe('Conditional rendering styles', () => { before(async () => { @@ -60,7 +60,7 @@ describe('Conditional rendering styles', () => { }); describe('dev', () => { - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); diff --git a/packages/integrations/svelte/test/empty-class-attribute.test.js b/packages/integrations/svelte/test/empty-class-attribute.test.ts similarity index 93% rename from packages/integrations/svelte/test/empty-class-attribute.test.js rename to packages/integrations/svelte/test/empty-class-attribute.test.ts index 6ef1124ba65c..5d2748e1ab9e 100644 --- a/packages/integrations/svelte/test/empty-class-attribute.test.js +++ b/packages/integrations/svelte/test/empty-class-attribute.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { load as cheerioLoad } from 'cheerio'; -import { loadFixture } from '../../../astro/test/test-utils.js'; +import { loadFixture, type Fixture, type DevServer } from '../../../astro/test/test-utils.js'; /** * @see https://github.com/withastro/astro/issues/15576 @@ -13,7 +13,7 @@ import { loadFixture } from '../../../astro/test/test-utils.js'; describe('Empty class attribute', () => { describe('build', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -52,8 +52,8 @@ describe('Empty class attribute', () => { }); describe('dev', () => { - let fixture; - let devServer; + let fixture: Fixture; + let devServer: DevServer; before(async () => { fixture = await loadFixture({ diff --git a/packages/integrations/svelte/test/extract-generics.test.js b/packages/integrations/svelte/test/extract-generics.test.ts similarity index 100% rename from packages/integrations/svelte/test/extract-generics.test.js rename to packages/integrations/svelte/test/extract-generics.test.ts diff --git a/packages/integrations/svelte/tsconfig.test.json b/packages/integrations/svelte/tsconfig.test.json new file mode 100644 index 000000000000..27c89c5fe7a7 --- /dev/null +++ b/packages/integrations/svelte/tsconfig.test.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["test/**/*.ts"], + "exclude": ["test/fixtures/**"], + "compilerOptions": { + "noEmit": true, + "allowJs": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "rewriteRelativeImportExtensions": true + }, + "references": [ + { + "path": "../../astro/tsconfig.test.json" + } + ] +} diff --git a/packages/integrations/vercel/CHANGELOG.md b/packages/integrations/vercel/CHANGELOG.md index e82a55da5f29..ae40a28d52f5 100644 --- a/packages/integrations/vercel/CHANGELOG.md +++ b/packages/integrations/vercel/CHANGELOG.md @@ -1,5 +1,11 @@ # @astrojs/vercel +## 10.0.4 + +### Patch Changes + +- [#16170](https://github.com/withastro/astro/pull/16170) [`d0fe1ec`](https://github.com/withastro/astro/commit/d0fe1ec216f8f322392e34ce40378d022e495cef) Thanks [@bittoby](https://github.com/bittoby)! - Fixes edge middleware `next()` dropping the HTTP method and body when forwarding requests to the serverless function, which caused non-GET API routes (POST, PUT, PATCH, DELETE) to return 404 + ## 10.0.3 ### Patch Changes diff --git a/packages/integrations/vercel/package.json b/packages/integrations/vercel/package.json index 2bcf5646cfbb..fe44fbae86cf 100644 --- a/packages/integrations/vercel/package.json +++ b/packages/integrations/vercel/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/vercel", "description": "Deploy your site to Vercel", - "version": "10.0.3", + "version": "10.0.4", "type": "module", "author": "withastro", "license": "MIT", @@ -42,8 +42,9 @@ "dev": "astro-scripts dev \"src/**/*.ts\"", "build": "astro-scripts build \"src/**/*.ts\" && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\"", - "test": "astro-scripts test --timeout 60000 \"test/**/!(hosted).test.js\"", - "test:hosted": "astro-scripts test --timeout 30000 \"test/hosted/*.test.js\"" + "test": "astro-scripts test --timeout 60000 \"test/**/!(hosted).test.ts\"", + "test:hosted": "astro-scripts test --timeout 30000 \"test/hosted/*.test.ts\"", + "typecheck:tests": "tsc --build tsconfig.test.json" }, "dependencies": { "@astrojs/internal-helpers": "workspace:*", diff --git a/packages/integrations/vercel/src/index.ts b/packages/integrations/vercel/src/index.ts index c7672aacdf8c..bb7ae4eacc1d 100644 --- a/packages/integrations/vercel/src/index.ts +++ b/packages/integrations/vercel/src/index.ts @@ -651,16 +651,31 @@ type Runtime = `nodejs${string}.x`; class VercelBuilder { readonly NTF_CACHE = {}; + readonly config: AstroConfig; + readonly excludeFiles: URL[]; + readonly includeFiles: URL[]; + readonly logger: AstroIntegrationLogger; + readonly outDir: URL; + readonly maxDuration: number | undefined; + readonly runtime: string; constructor( - readonly config: AstroConfig, - readonly excludeFiles: URL[], - readonly includeFiles: URL[], - readonly logger: AstroIntegrationLogger, - readonly outDir: URL, - readonly maxDuration?: number, - readonly runtime = getRuntime(process, logger), - ) {} + config: AstroConfig, + excludeFiles: URL[], + includeFiles: URL[], + logger: AstroIntegrationLogger, + outDir: URL, + maxDuration?: number, + runtime = getRuntime(process, logger), + ) { + this.config = config; + this.excludeFiles = excludeFiles; + this.includeFiles = includeFiles; + this.logger = logger; + this.outDir = outDir; + this.maxDuration = maxDuration; + this.runtime = runtime; + } async buildServerlessFolder(entry: URL, functionName: string, root: URL) { const { includeFiles, excludeFiles, logger, NTF_CACHE, runtime, maxDuration } = this; diff --git a/packages/integrations/vercel/src/serverless/middleware.ts b/packages/integrations/vercel/src/serverless/middleware.ts index 0f215ee84aa4..058d78296ba3 100644 --- a/packages/integrations/vercel/src/serverless/middleware.ts +++ b/packages/integrations/vercel/src/serverless/middleware.ts @@ -127,12 +127,14 @@ export default async function middleware(request, context) { const next = async () => { const { vercel, ...locals } = ctx.locals; const response = await fetch(new URL('/${NODE_PATH}', request.url), { + method: request.method, headers: { ...Object.fromEntries(request.headers.entries()), '${ASTRO_MIDDLEWARE_SECRET_HEADER}': '${middlewareSecret}', '${ASTRO_PATH_HEADER}': request.url.replace(origin, ''), '${ASTRO_LOCALS_HEADER}': trySerializeLocals(locals) - } + }, + ...(request.body ? { body: request.body, duplex: 'half' } : {}), }); return new Response(response.body, { status: response.status, diff --git a/packages/integrations/vercel/test/edge-middleware.test.js b/packages/integrations/vercel/test/edge-middleware.test.ts similarity index 67% rename from packages/integrations/vercel/test/edge-middleware.test.js rename to packages/integrations/vercel/test/edge-middleware.test.ts index d6313d483ab6..3785e0cdd2de 100644 --- a/packages/integrations/vercel/test/edge-middleware.test.js +++ b/packages/integrations/vercel/test/edge-middleware.test.ts @@ -1,15 +1,14 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture, getVercelConfig } from './test-utils.ts'; describe('Vercel edge middleware', () => { - /** @type {import('./test-utils.js').Fixture} */ - let build; + let build: Fixture; before(async () => { build = await loadFixture({ root: './fixtures/middleware-with-edge-file/', }); - await build.build(); + await build.build({}); }); it('an edge function is created', async () => { @@ -22,8 +21,7 @@ describe('Vercel edge middleware', () => { }); it('deployment config points to the middleware edge function', async () => { - const contents = await build.readFile('../.vercel/output/config.json'); - const { routes } = JSON.parse(contents); + const { routes } = await getVercelConfig(build); assert.equal( routes.some((route) => route.dest === '_middleware'), true, @@ -35,19 +33,47 @@ describe('Vercel edge middleware', () => { '../.vercel/output/functions/_middleware.func/middleware.mjs', build.config.outDir, ); - const module = await import(entry); + const module = await import(entry.href); const request = new Request('http://example.com/foo'); const response = await module.default(request, {}); assert.equal(response.headers.get('set-cookie'), 'foo=bar'); assert.ok((await response.text()).length, 'Body is included'); }); + it('edge middleware forwards HTTP method and body', async () => { + const entry = new URL( + '../.vercel/output/functions/_middleware.func/middleware.mjs', + build.config.outDir, + ); + const module = await import(entry.href); + + const originalFetch = globalThis.fetch; + let captured: RequestInit | undefined; + globalThis.fetch = async (_url, opts) => { + captured = opts; + return new Response('ok', { status: 200 }); + }; + try { + const request = new Request('http://example.com/api/test', { + method: 'POST', + body: '{"data":"test"}', + headers: { 'Content-Type': 'application/json' }, + }); + await module.default(request, {}); + assert.ok(captured, 'fetch was called'); + assert.equal(captured.method, 'POST', 'forwards the HTTP method'); + assert.ok(captured.body, 'forwards the request body'); + } finally { + globalThis.fetch = originalFetch; + } + }); + // TODO: The path here seems to be inconsistent? it.skip('with edge handle file, should successfully build the middleware', async () => { const fixture = await loadFixture({ root: './fixtures/middleware-with-edge-file/', }); - await fixture.build(); + await fixture.build({}); const _contents = await fixture.readFile( // this is abysmal... '../.vercel/output/functions/render.func/www/withastro/astro/packages/vercel/test/fixtures/middleware-with-edge-file/dist/middleware.mjs', @@ -62,7 +88,7 @@ describe('Vercel edge middleware', () => { const fixture = await loadFixture({ root: './fixtures/middleware-without-edge-file/', }); - await fixture.build(); + await fixture.build({}); const _contents = await fixture.readFile( // this is abysmal... '../.vercel/output/functions/render.func/www/withastro/astro/packages/vercel/test/fixtures/middleware-without-edge-file/dist/middleware.mjs', diff --git a/packages/integrations/vercel/test/fixtures/image/astro.config.mjs b/packages/integrations/vercel/test/fixtures/image/astro.config.mjs index bd3385ad8a79..26998fb03ce4 100644 --- a/packages/integrations/vercel/test/fixtures/image/astro.config.mjs +++ b/packages/integrations/vercel/test/fixtures/image/astro.config.mjs @@ -1,6 +1,6 @@ import vercel from '@astrojs/vercel'; import { defineConfig } from 'astro/config'; -import { testImageService } from '../../test-image-service.js'; +import { testImageService } from '../../../../../astro/test/test-image-service.js'; export default defineConfig({ adapter: vercel({ diff --git a/packages/integrations/vercel/test/hosted/hosted.test.js b/packages/integrations/vercel/test/hosted/hosted.test.ts similarity index 100% rename from packages/integrations/vercel/test/hosted/hosted.test.js rename to packages/integrations/vercel/test/hosted/hosted.test.ts diff --git a/packages/integrations/vercel/test/image.test.js b/packages/integrations/vercel/test/image.test.ts similarity index 88% rename from packages/integrations/vercel/test/image.test.js rename to packages/integrations/vercel/test/image.test.ts index c3ae7b60ff01..ac2125e8ed1d 100644 --- a/packages/integrations/vercel/test/image.test.js +++ b/packages/integrations/vercel/test/image.test.ts @@ -1,17 +1,16 @@ import assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import * as cheerio from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, type DevServer, loadFixture } from './test-utils.ts'; describe('Image', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/image/', }); - await fixture.build(); + await fixture.build({}); }); it('build successful', async () => { @@ -23,7 +22,7 @@ describe('Image', () => { const $ = cheerio.load(html); const img = $('#basic-image img'); - assert.equal(img.attr('src').startsWith('/_vercel/image?url=_astr'), true); + assert.equal(img.attr('src')!.startsWith('/_vercel/image?url=_astr'), true); assert.equal(img.attr('loading'), 'lazy'); assert.equal(img.attr('width'), '225'); }); @@ -33,12 +32,12 @@ describe('Image', () => { const $ = cheerio.load(html); const img = $('#small-source img'); const widths = img - .attr('srcset') + .attr('srcset')! .split(', ') .map((entry) => entry.split(' ')[1]); assert.deepEqual(widths, ['640w'], 'uses valid widths in srcset'); - const url = new URL(img.attr('src'), 'http://localhost'); + const url = new URL(img.attr('src')!, 'http://localhost'); assert.equal(url.searchParams.get('w'), '640', 'uses valid width in src'); assert.equal(img.attr('width'), '225', 'uses requested width in img attribute'); @@ -48,7 +47,7 @@ describe('Image', () => { const html = await fixture.readFile('../.vercel/output/static/index.html'); const $ = cheerio.load(html); const img = $('#densities-test img'); - const srcset = img.attr('srcset'); + const srcset = img.attr('srcset')!; // Extract widths from srcset (format: "url 1x", "url 1.5x", etc) const descriptors = srcset.split(', ').map((entry) => entry.split(' ')[1]); @@ -57,7 +56,7 @@ describe('Image', () => { const urls = srcset.split(', ').map((entry) => entry.split(' ')[0]); const widthsFromUrls = urls.map((url) => { const urlObj = new URL(url, 'http://localhost'); - return Number.parseInt(urlObj.searchParams.get('w'), 10); + return Number.parseInt(urlObj.searchParams.get('w')!, 10); }); // The configured sizes are [640, 750, 828, 1080, 1200, 1920, 2048, 3840] @@ -101,7 +100,7 @@ describe('Image', () => { }); describe('dev', () => { - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); @@ -116,7 +115,7 @@ describe('Image', () => { const $ = cheerio.load(html); const img = $('#basic-image img'); - assert.equal(img.attr('src').startsWith('/_image?href='), true); + assert.equal(img.attr('src')!.startsWith('/_image?href='), true); assert.equal(img.attr('loading'), 'lazy'); assert.equal(img.attr('width'), '225'); }); @@ -125,7 +124,7 @@ describe('Image', () => { const html = await fixture.fetch('/').then((res) => res.text()); const $ = cheerio.load(html); const img = $('#svg img'); - const src = img.attr('src'); + const src = img.attr('src')!; const res = await fixture.fetch(src); assert.equal(res.status, 200); @@ -137,7 +136,7 @@ describe('Image', () => { const $ = cheerio.load(html); const img = $('#responsive img'); const widths = img - .attr('srcset') + .attr('srcset')! .split(', ') .map((entry) => entry.split(' ')[1]); assert.deepEqual(widths, ['640w', '750w', '828w', '1080w', '1200w', '1920w']); diff --git a/packages/integrations/vercel/test/integration-assets.test.js b/packages/integrations/vercel/test/integration-assets.test.ts similarity index 90% rename from packages/integrations/vercel/test/integration-assets.test.js rename to packages/integrations/vercel/test/integration-assets.test.ts index 4bc4a570a486..246b59e8ca99 100644 --- a/packages/integrations/vercel/test/integration-assets.test.js +++ b/packages/integrations/vercel/test/integration-assets.test.ts @@ -1,13 +1,13 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; +import { loadFixture } from './test-utils.ts'; describe('Assets generated by integrations', () => { it('moves static assets generated by integrations to the correct location: static output', async () => { const fixture = await loadFixture({ root: './fixtures/integration-assets/', }); - await fixture.build(); + await fixture.build({}); const sitemap = await fixture.readFile('../.vercel/output/static/sitemap-index.xml'); assert(sitemap.includes('')); }); @@ -17,7 +17,7 @@ describe('Assets generated by integrations', () => { root: './fixtures/integration-assets/', output: 'server', }); - await fixture.build(); + await fixture.build({}); const sitemap = await fixture.readFile('../.vercel/output/static/sitemap-index.xml'); assert(sitemap.includes('')); }); diff --git a/packages/integrations/vercel/test/isr.test.js b/packages/integrations/vercel/test/isr.test.ts similarity index 91% rename from packages/integrations/vercel/test/isr.test.js rename to packages/integrations/vercel/test/isr.test.ts index 0b8e2d70c9be..94edc0505b50 100644 --- a/packages/integrations/vercel/test/isr.test.js +++ b/packages/integrations/vercel/test/isr.test.ts @@ -1,16 +1,15 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.ts'; describe('ISR', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/isr/', }); - await fixture.build(); + await fixture.build({}); }); it('generates expected prerender config', async () => { diff --git a/packages/integrations/vercel/test/max-duration.test.js b/packages/integrations/vercel/test/max-duration.test.ts similarity index 77% rename from packages/integrations/vercel/test/max-duration.test.js rename to packages/integrations/vercel/test/max-duration.test.ts index d5e26fc1a999..ba331e11bf90 100644 --- a/packages/integrations/vercel/test/max-duration.test.js +++ b/packages/integrations/vercel/test/max-duration.test.ts @@ -1,16 +1,15 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.ts'; describe('maxDuration', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/max-duration/', }); - await fixture.build(); + await fixture.build({}); }); it('makes it to vercel function configuration', async () => { diff --git a/packages/integrations/vercel/test/path-override-security.test.js b/packages/integrations/vercel/test/path-override-security.test.ts similarity index 83% rename from packages/integrations/vercel/test/path-override-security.test.js rename to packages/integrations/vercel/test/path-override-security.test.ts index b72fe702ea7d..9e5a5eb62eda 100644 --- a/packages/integrations/vercel/test/path-override-security.test.js +++ b/packages/integrations/vercel/test/path-override-security.test.ts @@ -1,8 +1,8 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.ts'; -async function loadFunctionModule(fixture, functionName) { +async function loadFunctionModule(fixture: Fixture, functionName: string) { const functionConfig = JSON.parse( await fixture.readFile(`../.vercel/output/functions/${functionName}.func/.vc-config.json`), ); @@ -11,20 +11,18 @@ async function loadFunctionModule(fixture, functionName) { fixture.config.outDir, ); - return import(functionEntry); + return import(functionEntry.href); } describe('Vercel serverless path override security', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { - process.env.PRERENDER = true; fixture = await loadFixture({ root: './fixtures/serverless-with-dynamic-routes/', output: 'server', }); - await fixture.build(); + await fixture.build({}); }); it('ignores untrusted x_astro_path query param on _render', async () => { diff --git a/packages/integrations/vercel/test/prerendered-error-pages.test.js b/packages/integrations/vercel/test/prerendered-error-pages.test.ts similarity index 66% rename from packages/integrations/vercel/test/prerendered-error-pages.test.js rename to packages/integrations/vercel/test/prerendered-error-pages.test.ts index 79a1f4aafe0f..dd7d56667813 100644 --- a/packages/integrations/vercel/test/prerendered-error-pages.test.js +++ b/packages/integrations/vercel/test/prerendered-error-pages.test.ts @@ -1,20 +1,19 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture, getVercelConfig } from './test-utils.ts'; describe('prerendered error pages routing', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/prerendered-error-pages/', }); - await fixture.build(); + await fixture.build({}); }); it('falls back to 404.html', async () => { - const deploymentConfig = JSON.parse(await fixture.readFile('../.vercel/output/config.json')); + const deploymentConfig = await getVercelConfig(fixture); assert.deepEqual( deploymentConfig.routes.find((r) => r.status === 404), { diff --git a/packages/integrations/vercel/test/redirects-serverless.test.js b/packages/integrations/vercel/test/redirects-serverless.test.ts similarity index 79% rename from packages/integrations/vercel/test/redirects-serverless.test.js rename to packages/integrations/vercel/test/redirects-serverless.test.ts index 8d7dcf75b403..6536d7f529bd 100644 --- a/packages/integrations/vercel/test/redirects-serverless.test.js +++ b/packages/integrations/vercel/test/redirects-serverless.test.ts @@ -1,10 +1,9 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.ts'; describe('Redirects Serverless', () => { - /** @type {import('astro/test/test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -14,7 +13,7 @@ describe('Redirects Serverless', () => { '/other': '/subpage', }, }); - await fixture.build(); + await fixture.build({}); }); it('does not create .html files', async () => { diff --git a/packages/integrations/vercel/test/redirects.test.js b/packages/integrations/vercel/test/redirects.test.ts similarity index 68% rename from packages/integrations/vercel/test/redirects.test.js rename to packages/integrations/vercel/test/redirects.test.ts index a76ff6ad9ed6..e47c93e5421b 100644 --- a/packages/integrations/vercel/test/redirects.test.js +++ b/packages/integrations/vercel/test/redirects.test.ts @@ -1,10 +1,9 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture, getVercelConfig } from './test-utils.ts'; describe('Redirects', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -25,49 +24,43 @@ describe('Redirects', () => { }, trailingSlash: 'always', }); - await fixture.build(); + await fixture.build({}); }); - async function getConfig() { - const json = await fixture.readFile('../.vercel/output/config.json'); - const config = JSON.parse(json); - return config; - } - it('define static routes', async () => { - const config = await getConfig(); - const oneRoute = config.routes.find((r) => r.src === '^/one$'); - assert.equal(oneRoute.headers.Location, '/'); + const config = await getVercelConfig(fixture); + const oneRoute = config.routes.find((r) => r.src === '^/one$')!; + assert.equal(oneRoute.headers['Location'], '/'); assert.equal(oneRoute.status, 301); - const twoRoute = config.routes.find((r) => r.src === '^/two$'); - assert.equal(twoRoute.headers.Location, '/'); + const twoRoute = config.routes.find((r) => r.src === '^/two$')!; + assert.equal(twoRoute.headers['Location'], '/'); assert.equal(twoRoute.status, 301); - const threeRoute = config.routes.find((r) => r.src === '^/three$'); - assert.equal(threeRoute.headers.Location, '/'); + const threeRoute = config.routes.find((r) => r.src === '^/three$')!; + assert.equal(threeRoute.headers['Location'], '/'); assert.equal(threeRoute.status, 302); - const fourRoute = config.routes.find((r) => r.src === '^/four$'); - assert.equal(fourRoute.headers.Location, 'http://example.com'); + const fourRoute = config.routes.find((r) => r.src === '^/four$')!; + assert.equal(fourRoute.headers['Location'], 'http://example.com'); assert.equal(fourRoute.status, 302); }); it('define redirects for static files', async () => { - const config = await getConfig(); + const config = await getVercelConfig(fixture); - const staticRoute = config.routes.find((r) => r.src === '^/Basic/http-2-0\\.html$'); + const staticRoute = config.routes.find((r) => r.src === '^/Basic/http-2-0\\.html$')!; assert.notEqual(staticRoute, undefined); - assert.equal(staticRoute.headers.Location, '/posts/http2'); + assert.equal(staticRoute.headers['Location'], '/posts/http2'); assert.equal(staticRoute.status, 301); }); it('defines dynamic routes', async () => { - const config = await getConfig(); + const config = await getVercelConfig(fixture); - const blogRoute = config.routes.find((r) => r.src.startsWith('^/blog')); + const blogRoute = config.routes.find((r) => r.src.startsWith('^/blog'))!; assert.notEqual(blogRoute, undefined); - assert.equal(blogRoute.headers.Location.startsWith('/team/articles'), true); + assert.equal(blogRoute.headers['Location'].startsWith('/team/articles'), true); assert.equal(blogRoute.status, 301); }); @@ -79,7 +72,7 @@ describe('Redirects', () => { '/blog/(![...slug]': '/team/articles/[...slug]', }, }); - await assert.rejects(() => fails.build(), { + await assert.rejects(() => fails.build({}), { name: 'AstroUserError', message: 'Error generating redirects: Redirect at index 0 has invalid `source` regular expression "/blog/(!:slug*".', diff --git a/packages/integrations/vercel/test/server-islands.test.js b/packages/integrations/vercel/test/server-islands.test.ts similarity index 70% rename from packages/integrations/vercel/test/server-islands.test.js rename to packages/integrations/vercel/test/server-islands.test.ts index f4f1767075d8..e5787e536720 100644 --- a/packages/integrations/vercel/test/server-islands.test.js +++ b/packages/integrations/vercel/test/server-islands.test.ts @@ -1,20 +1,19 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture, getVercelConfig } from './test-utils.ts'; describe('Server Islands', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/server-islands/', }); - await fixture.build(); + await fixture.build({}); }); it('server islands route is in the config', async () => { - const config = JSON.parse(await fixture.readFile('../.vercel/output/config.json')); + const config = await getVercelConfig(fixture); let found = null; for (const route of config.routes) { if (route.src?.includes('_server-islands')) { diff --git a/packages/integrations/vercel/test/serverless-prerender.test.js b/packages/integrations/vercel/test/serverless-prerender.test.ts similarity index 82% rename from packages/integrations/vercel/test/serverless-prerender.test.js rename to packages/integrations/vercel/test/serverless-prerender.test.ts index 662e74ac39fd..7809129a0a73 100644 --- a/packages/integrations/vercel/test/serverless-prerender.test.js +++ b/packages/integrations/vercel/test/serverless-prerender.test.ts @@ -1,17 +1,15 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.ts'; describe('Serverless prerender', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { - process.env.PRERENDER = true; fixture = await loadFixture({ root: './fixtures/serverless-prerender/', }); - await fixture.build(); + await fixture.build({}); }); it('build successful', async () => { @@ -41,16 +39,14 @@ describe('Serverless prerender', () => { }); describe('Serverless hybrid rendering', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { - process.env.PRERENDER = true; fixture = await loadFixture({ root: './fixtures/serverless-prerender/', output: 'static', }); - await fixture.build(); + await fixture.build({}); }); it('build successful', async () => { diff --git a/packages/integrations/vercel/test/serverless-with-dynamic-routes.test.js b/packages/integrations/vercel/test/serverless-with-dynamic-routes.test.ts similarity index 75% rename from packages/integrations/vercel/test/serverless-with-dynamic-routes.test.js rename to packages/integrations/vercel/test/serverless-with-dynamic-routes.test.ts index b402ad0bb2e6..8c8c6a8fc572 100644 --- a/packages/integrations/vercel/test/serverless-with-dynamic-routes.test.js +++ b/packages/integrations/vercel/test/serverless-with-dynamic-routes.test.ts @@ -1,18 +1,16 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.ts'; describe('Serverless with dynamic routes', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { - process.env.PRERENDER = true; fixture = await loadFixture({ root: './fixtures/serverless-with-dynamic-routes/', output: 'server', }); - await fixture.build(); + await fixture.build({}); }); it('build successful', async () => { diff --git a/packages/integrations/vercel/test/static-assets.test.js b/packages/integrations/vercel/test/static-assets.test.ts similarity index 74% rename from packages/integrations/vercel/test/static-assets.test.js rename to packages/integrations/vercel/test/static-assets.test.ts index 8e8b18c8e769..3d5578f3cb86 100644 --- a/packages/integrations/vercel/test/static-assets.test.js +++ b/packages/integrations/vercel/test/static-assets.test.ts @@ -1,14 +1,26 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; +import { + type Fixture, + type AstroInlineConfig, + loadFixture, + getVercelConfig, +} from './test-utils.ts'; describe('Static Assets', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; const VALID_CACHE_CONTROL = 'public, max-age=31536000, immutable'; - async function build({ adapter, assets, output }) { + async function build({ + adapter, + assets, + output, + }: { + adapter?: AstroInlineConfig['adapter']; + assets?: string; + output?: AstroInlineConfig['output']; + }) { fixture = await loadFixture({ root: './fixtures/static-assets/', output, @@ -17,27 +29,20 @@ describe('Static Assets', () => { assets, }, }); - await fixture.build(); - } - - async function getConfig() { - const json = await fixture.readFile('../.vercel/output/config.json'); - const config = JSON.parse(json); - - return config; + await fixture.build({}); } async function getAssets() { return fixture.config.build.assets; } - async function checkValidCacheControl(assets) { - const config = await getConfig(); + async function checkValidCacheControl(assets?: string) { + const config = await getVercelConfig(fixture); const theAssets = assets ?? (await getAssets()); const route = config.routes.find((r) => r.src === `^/${theAssets}/(.*)$`); - assert.equal(route.headers['cache-control'], VALID_CACHE_CONTROL); - assert.equal(route.continue, true); + assert.equal(route!.headers['cache-control'], VALID_CACHE_CONTROL); + assert.equal(route!.continue, true); } describe('static adapter', () => { diff --git a/packages/integrations/vercel/test/static-headers.test.js b/packages/integrations/vercel/test/static-headers.test.ts similarity index 70% rename from packages/integrations/vercel/test/static-headers.test.js rename to packages/integrations/vercel/test/static-headers.test.ts index 26fee84a4a09..9074353efccd 100644 --- a/packages/integrations/vercel/test/static-headers.test.js +++ b/packages/integrations/vercel/test/static-headers.test.ts @@ -1,22 +1,22 @@ import * as assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture, getVercelConfig } from './test-utils.ts'; describe('Static headers', () => { - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/static-headers', }); - await fixture.build(); + await fixture.build({}); }); it('CSP headers are added when CSP is enabled', async () => { - const config = JSON.parse(await fixture.readFile('../.vercel/output/config.json')); + const config = await getVercelConfig(fixture); const routes = config.routes; - const headers = routes.find((x) => x.src === '/').headers; + const headers = routes.find((x) => x.src === '/')!.headers; assert.ok(headers['content-security-policy'], 'the index must have CSP headers'); assert.ok( diff --git a/packages/integrations/vercel/test/static.test.js b/packages/integrations/vercel/test/static.test.ts similarity index 79% rename from packages/integrations/vercel/test/static.test.js rename to packages/integrations/vercel/test/static.test.ts index 8702cf52d8db..bf86d679046d 100644 --- a/packages/integrations/vercel/test/static.test.js +++ b/packages/integrations/vercel/test/static.test.ts @@ -1,16 +1,15 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.ts'; describe('static routing', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/static/', }); - await fixture.build(); + await fixture.build({}); }); it('falls back to 404.html', async () => { diff --git a/packages/integrations/vercel/test/streaming.test.js b/packages/integrations/vercel/test/streaming.test.ts similarity index 77% rename from packages/integrations/vercel/test/streaming.test.js rename to packages/integrations/vercel/test/streaming.test.ts index 1e4b0f111e05..cd57ec07d272 100644 --- a/packages/integrations/vercel/test/streaming.test.js +++ b/packages/integrations/vercel/test/streaming.test.ts @@ -1,16 +1,15 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.ts'; describe('streaming', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/streaming/', }); - await fixture.build(); + await fixture.build({}); }); it('makes it to vercel function configuration', async () => { diff --git a/packages/integrations/vercel/test/test-image-service.js b/packages/integrations/vercel/test/test-image-service.js deleted file mode 100644 index e3c5b4b6e29c..000000000000 --- a/packages/integrations/vercel/test/test-image-service.js +++ /dev/null @@ -1,32 +0,0 @@ -import { fileURLToPath } from 'node:url'; -import { baseService } from 'astro/assets'; - -/** - * stub image service that returns images as-is without optimization - * @param {{ foo?: string }} [config] - */ -export function testImageService(config = {}) { - return { - entrypoint: fileURLToPath(import.meta.url), - config, - }; -} - -/** @type {import("../dist/@types/astro").LocalImageService} */ -export default { - ...baseService, - propertiesToHash: [...baseService.propertiesToHash, 'data-custom'], - getHTMLAttributes(options, serviceConfig) { - options['data-service'] = 'my-custom-service'; - if (serviceConfig.service.config.foo) { - options['data-service-config'] = serviceConfig.service.config.foo; - } - return baseService.getHTMLAttributes(options); - }, - async transform(buffer, transform) { - return { - data: buffer, - format: transform.format, - }; - }, -}; diff --git a/packages/integrations/vercel/test/test-utils.js b/packages/integrations/vercel/test/test-utils.js deleted file mode 100644 index 8e70e9a1c82a..000000000000 --- a/packages/integrations/vercel/test/test-utils.js +++ /dev/null @@ -1,8 +0,0 @@ -import { loadFixture as baseLoadFixture } from '../../../astro/test/test-utils.js'; - -export function loadFixture(config) { - if (config?.root) { - config.root = new URL(config.root, import.meta.url); - } - return baseLoadFixture(config); -} diff --git a/packages/integrations/vercel/test/test-utils.ts b/packages/integrations/vercel/test/test-utils.ts new file mode 100644 index 000000000000..06cad424e9aa --- /dev/null +++ b/packages/integrations/vercel/test/test-utils.ts @@ -0,0 +1,32 @@ +import { + loadFixture as baseLoadFixture, + type Fixture, + type DevServer, + type AstroInlineConfig, +} from '../../../astro/test/test-utils.js'; + +export type { Fixture, DevServer, AstroInlineConfig }; + +export interface VercelOutputConfig { + version: number; + routes: Array<{ + src: string; + dest: string; + status: number; + headers: Record; + continue: boolean; + handle: string; + }>; +} + +export async function getVercelConfig(fixture: Fixture): Promise { + const json = await fixture.readFile('../.vercel/output/config.json'); + return JSON.parse(json); +} + +export function loadFixture(config: AstroInlineConfig) { + if (config?.root) { + config.root = new URL(config.root as string, import.meta.url).toString(); + } + return baseLoadFixture(config); +} diff --git a/packages/integrations/vercel/test/web-analytics.test.js b/packages/integrations/vercel/test/web-analytics.test.ts similarity index 83% rename from packages/integrations/vercel/test/web-analytics.test.js rename to packages/integrations/vercel/test/web-analytics.test.ts index d5056d0acb90..cd85327b56ff 100644 --- a/packages/integrations/vercel/test/web-analytics.test.js +++ b/packages/integrations/vercel/test/web-analytics.test.ts @@ -1,18 +1,17 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.ts'; describe('Vercel Web Analytics', () => { describe('output: static', () => { - /** @type {import('./test-utils.js').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ root: './fixtures/with-web-analytics-enabled/output-as-static/', output: 'static', }); - await fixture.build(); + await fixture.build({}); }); it('ensures that Vercel Web Analytics is present in the header', async () => { diff --git a/packages/integrations/vercel/tsconfig.test.json b/packages/integrations/vercel/tsconfig.test.json new file mode 100644 index 000000000000..853326403557 --- /dev/null +++ b/packages/integrations/vercel/tsconfig.test.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["test/**/*.ts"], + "exclude": ["test/fixtures/**"], + "compilerOptions": { + "allowJs": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "rewriteRelativeImportExtensions": true, + "rootDir": "." + }, + "references": [{ "path": "../../astro/tsconfig.test.json" }] +} diff --git a/packages/integrations/vue/package.json b/packages/integrations/vue/package.json index 4f4f15c36156..7f790965592e 100644 --- a/packages/integrations/vue/package.json +++ b/packages/integrations/vue/package.json @@ -35,7 +35,8 @@ "build": "astro-scripts build \"src/**/*.ts\" && astro-scripts build \"src/editor.cts\" --force-cjs --no-clean-dist && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\" && astro-scripts build \"src/editor.cts\" --force-cjs --no-clean-dist", "dev": "astro-scripts dev \"src/**/*.ts\"", - "test": "astro-scripts test \"test/**/*.test.js\"" + "test": "astro-scripts test \"test/**/*.test.ts\"", + "typecheck:tests": "tsc --build tsconfig.test.json" }, "dependencies": { "@vitejs/plugin-vue": "^6.0.5", diff --git a/packages/integrations/vue/test/app-entrypoint-css.test.js b/packages/integrations/vue/test/app-entrypoint-css.test.ts similarity index 93% rename from packages/integrations/vue/test/app-entrypoint-css.test.js rename to packages/integrations/vue/test/app-entrypoint-css.test.ts index 878aa8e486c3..d3b1e644cdb6 100644 --- a/packages/integrations/vue/test/app-entrypoint-css.test.js +++ b/packages/integrations/vue/test/app-entrypoint-css.test.ts @@ -1,11 +1,10 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { load as cheerioLoad } from 'cheerio'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, type DevServer, loadFixture } from './test-utils.ts'; describe('App Entrypoint CSS', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -38,7 +37,7 @@ describe('App Entrypoint CSS', () => { }); describe('dev', () => { - let devServer; + let devServer: DevServer; before(async () => { devServer = await fixture.startDevServer(); }); diff --git a/packages/integrations/vue/test/app-entrypoint.test.js b/packages/integrations/vue/test/app-entrypoint.test.ts similarity index 83% rename from packages/integrations/vue/test/app-entrypoint.test.js rename to packages/integrations/vue/test/app-entrypoint.test.ts index c2dd5f5701f3..56823cabbc93 100644 --- a/packages/integrations/vue/test/app-entrypoint.test.js +++ b/packages/integrations/vue/test/app-entrypoint.test.ts @@ -2,11 +2,10 @@ import * as assert from 'node:assert/strict'; import { after, before, describe, it } from 'node:test'; import { load as cheerioLoad } from 'cheerio'; import { parseHTML } from 'linkedom'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, type DevServer, loadFixture } from './test-utils.ts'; describe('App Entrypoint', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -43,9 +42,8 @@ describe('App Entrypoint', () => { }); describe('App Entrypoint no export default (dev)', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; - let devServer; + let fixture: Fixture; + let devServer: DevServer; before(async () => { fixture = await loadFixture({ @@ -61,7 +59,7 @@ describe('App Entrypoint no export default (dev)', () => { it('loads during SSR', async () => { const html = await fixture.fetch('/').then((res) => res.text()); const { document } = parseHTML(html); - const bar = document.querySelector('#foo > #bar'); + const bar = document.querySelector('#foo > #bar')!; assert.notEqual(bar, undefined); assert.equal(bar.textContent, 'works'); }); @@ -76,8 +74,7 @@ describe('App Entrypoint no export default (dev)', () => { }); describe('App Entrypoint no export default', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -89,7 +86,7 @@ describe('App Entrypoint no export default', () => { it('loads during SSR', async () => { const data = await fixture.readFile('/index.html'); const { document } = parseHTML(data); - const bar = document.querySelector('#foo > #bar'); + const bar = document.querySelector('#foo > #bar')!; assert.notEqual(bar, undefined); assert.equal(bar.textContent, 'works'); }); @@ -97,8 +94,8 @@ describe('App Entrypoint no export default', () => { it('component not included in renderer bundle', async () => { const data = await fixture.readFile('/index.html'); const { document } = parseHTML(data); - const island = document.querySelector('astro-island'); - const client = island.getAttribute('renderer-url'); + const island = document.querySelector('astro-island')!; + const client = island.getAttribute('renderer-url')!; assert.notEqual(client, undefined); const js = await fixture.readFile(client); assert.doesNotMatch(js, /\w+\.component\("Bar"/g); @@ -114,8 +111,7 @@ describe('App Entrypoint no export default', () => { }); describe('App Entrypoint relative', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -127,7 +123,7 @@ describe('App Entrypoint relative', () => { it('loads during SSR', async () => { const data = await fixture.readFile('/index.html'); const { document } = parseHTML(data); - const bar = document.querySelector('#foo > #bar'); + const bar = document.querySelector('#foo > #bar')!; assert.notEqual(bar, undefined); assert.equal(bar.textContent, 'works'); }); @@ -135,8 +131,8 @@ describe('App Entrypoint relative', () => { it('component not included in renderer bundle', async () => { const data = await fixture.readFile('/index.html'); const { document } = parseHTML(data); - const island = document.querySelector('astro-island'); - const client = island.getAttribute('renderer-url'); + const island = document.querySelector('astro-island')!; + const client = island.getAttribute('renderer-url')!; assert.notEqual(client, undefined); const js = await fixture.readFile(client); @@ -145,8 +141,7 @@ describe('App Entrypoint relative', () => { }); describe('App Entrypoint /src/absolute', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -158,7 +153,7 @@ describe('App Entrypoint /src/absolute', () => { it('loads during SSR', async () => { const data = await fixture.readFile('/index.html'); const { document } = parseHTML(data); - const bar = document.querySelector('#foo > #bar'); + const bar = document.querySelector('#foo > #bar')!; assert.notEqual(bar, undefined); assert.equal(bar.textContent, 'works'); }); @@ -166,8 +161,8 @@ describe('App Entrypoint /src/absolute', () => { it('component not included in renderer bundle', async () => { const data = await fixture.readFile('/index.html'); const { document } = parseHTML(data); - const island = document.querySelector('astro-island'); - const client = island.getAttribute('renderer-url'); + const island = document.querySelector('astro-island')!; + const client = island.getAttribute('renderer-url')!; assert.notEqual(client, undefined); const js = await fixture.readFile(client); @@ -176,8 +171,7 @@ describe('App Entrypoint /src/absolute', () => { }); describe('App Entrypoint async', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ diff --git a/packages/integrations/vue/test/basics.test.js b/packages/integrations/vue/test/basics.test.ts similarity index 87% rename from packages/integrations/vue/test/basics.test.js rename to packages/integrations/vue/test/basics.test.ts index 84b274b0d40c..e1dd222c8fbc 100644 --- a/packages/integrations/vue/test/basics.test.js +++ b/packages/integrations/vue/test/basics.test.ts @@ -1,11 +1,10 @@ import * as assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { parseHTML } from 'linkedom'; -import { loadFixture } from './test-utils.js'; +import { type Fixture, loadFixture } from './test-utils.ts'; describe('Basics', () => { - /** @type {import('./test-utils').Fixture} */ - let fixture; + let fixture: Fixture; before(async () => { fixture = await loadFixture({ @@ -17,7 +16,7 @@ describe('Basics', () => { it('Slots are added without the slot attribute', async () => { const data = await fixture.readFile('/index.html'); const { document } = parseHTML(data); - const bar = document.querySelector('#foo'); + const bar = document.querySelector('#foo')!; assert.notEqual(bar, undefined); assert.equal(bar.getAttribute('slot'), null); @@ -26,7 +25,7 @@ describe('Basics', () => { it('Can show images from public', async () => { const data = await fixture.readFile('/public/index.html'); const { document } = parseHTML(data); - const img = document.querySelector('img'); + const img = document.querySelector('img')!; assert.notEqual(img, undefined); assert.equal(img.getAttribute('src'), '/light_walrus.avif'); diff --git a/packages/integrations/vue/test/check.test.js b/packages/integrations/vue/test/check.test.ts similarity index 100% rename from packages/integrations/vue/test/check.test.js rename to packages/integrations/vue/test/check.test.ts diff --git a/packages/integrations/vue/test/test-utils.js b/packages/integrations/vue/test/test-utils.ts similarity index 61% rename from packages/integrations/vue/test/test-utils.js rename to packages/integrations/vue/test/test-utils.ts index 512fe28dcba8..7164a3ce730c 100644 --- a/packages/integrations/vue/test/test-utils.js +++ b/packages/integrations/vue/test/test-utils.ts @@ -1,10 +1,13 @@ -import { loadFixture as baseLoadFixture } from '../../../astro/test/test-utils.js'; +import { + loadFixture as baseLoadFixture, + type Fixture, + type DevServer, + type AstroInlineConfig, +} from '../../../astro/test/test-utils.js'; -/** - * @typedef {import('../../../astro/test/test-utils').Fixture} Fixture - */ +export type { Fixture, DevServer }; -export function loadFixture(inlineConfig) { +export function loadFixture(inlineConfig: AstroInlineConfig) { if (!inlineConfig?.root) throw new Error("Must provide { root: './fixtures/...' }"); // resolve the relative root (i.e. "./fixtures/tailwindcss") to a full filepath diff --git a/packages/integrations/vue/test/toTsx.test.js b/packages/integrations/vue/test/toTsx.test.ts similarity index 100% rename from packages/integrations/vue/test/toTsx.test.js rename to packages/integrations/vue/test/toTsx.test.ts diff --git a/packages/integrations/vue/tsconfig.test.json b/packages/integrations/vue/tsconfig.test.json new file mode 100644 index 000000000000..27c89c5fe7a7 --- /dev/null +++ b/packages/integrations/vue/tsconfig.test.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["test/**/*.ts"], + "exclude": ["test/fixtures/**"], + "compilerOptions": { + "noEmit": true, + "allowJs": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "rewriteRelativeImportExtensions": true + }, + "references": [ + { + "path": "../../astro/tsconfig.test.json" + } + ] +} diff --git a/packages/internal-helpers/package.json b/packages/internal-helpers/package.json index 018b9d44cafc..feadc3dd8610 100644 --- a/packages/internal-helpers/package.json +++ b/packages/internal-helpers/package.json @@ -46,7 +46,8 @@ "build": "astro-scripts build \"src/**/*.ts\" && tsc -p tsconfig.json", "build:ci": "astro-scripts build \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"", - "test": "astro-scripts test \"test/**/*.test.js\"" + "test": "astro-scripts test \"test/**/*.test.ts\"", + "typecheck:tests": "tsc -p tsconfig.test.json --noEmit" }, "dependencies": { "picomatch": "^4.0.3" diff --git a/packages/internal-helpers/test/create-filter.test.js b/packages/internal-helpers/test/create-filter.test.ts similarity index 100% rename from packages/internal-helpers/test/create-filter.test.js rename to packages/internal-helpers/test/create-filter.test.ts diff --git a/packages/internal-helpers/test/path.test.js b/packages/internal-helpers/test/path.test.ts similarity index 99% rename from packages/internal-helpers/test/path.test.js rename to packages/internal-helpers/test/path.test.ts index c4359f05a5fd..2981450c08b6 100644 --- a/packages/internal-helpers/test/path.test.js +++ b/packages/internal-helpers/test/path.test.ts @@ -652,7 +652,7 @@ describe('isParentDirectory', () => { }); it('should correctly reject non-parent relationships', () => { - const invalidCases = [ + const invalidCases: Array<[string, string]> = [ // Different directories ['/home', '/usr'], ['/home/user', '/home/otheruser'], @@ -681,8 +681,11 @@ describe('isParentDirectory', () => { ['', '/home'], ['/home', ''], ['', ''], + // @ts-expect-error: expected to handle null/undefined gracefully [null, '/home'], + // @ts-expect-error: expected to handle null/undefined gracefully ['/home', null], + // @ts-expect-error: expected to handle null/undefined gracefully [undefined, '/home'], // Same path (not parent-child) diff --git a/packages/internal-helpers/test/request.test.js b/packages/internal-helpers/test/request.test.ts similarity index 99% rename from packages/internal-helpers/test/request.test.js rename to packages/internal-helpers/test/request.test.ts index 8741dbdddb62..2029367af9b9 100644 --- a/packages/internal-helpers/test/request.test.js +++ b/packages/internal-helpers/test/request.test.ts @@ -137,7 +137,7 @@ describe('getClientIpAddress', () => { /** * Helper to create a minimal Request with given headers. */ - function makeRequest(headers = {}) { + function makeRequest(headers: HeadersInit = {}) { return new Request('https://example.com', { headers }); } diff --git a/packages/internal-helpers/tsconfig.test.json b/packages/internal-helpers/tsconfig.test.json new file mode 100644 index 000000000000..7d6bc4428b35 --- /dev/null +++ b/packages/internal-helpers/tsconfig.test.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["test/**/*.ts"], + "exclude": ["test/fixtures/**"], + "compilerOptions": { + "allowJs": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "rewriteRelativeImportExtensions": true + }, + "references": [{ "path": "../astro/tsconfig.test.json" }] +} diff --git a/packages/language-tools/astro-check/package.json b/packages/language-tools/astro-check/package.json index 6c4377dbfa27..42901ee1ee4e 100644 --- a/packages/language-tools/astro-check/package.json +++ b/packages/language-tools/astro-check/package.json @@ -40,7 +40,7 @@ "@types/node": "^20.9.0", "@types/yargs": "^17.0.35", "astro-scripts": "workspace:*", - "tinyglobby": "^0.2.15", + "tinyglobby": "^0.2.16", "tsx": "^4.21.0" }, "peerDependencies": { diff --git a/packages/language-tools/language-server/package.json b/packages/language-tools/language-server/package.json index 3773a532aa8f..5a06d28c7874 100644 --- a/packages/language-tools/language-server/package.json +++ b/packages/language-tools/language-server/package.json @@ -34,7 +34,7 @@ "@volar/language-server": "~2.4.28", "@volar/language-service": "~2.4.28", "muggle-string": "^0.4.1", - "tinyglobby": "^0.2.15", + "tinyglobby": "^0.2.16", "volar-service-css": "0.0.70", "volar-service-emmet": "0.0.70", "volar-service-html": "0.0.70", diff --git a/packages/language-tools/language-server/src/check.ts b/packages/language-tools/language-server/src/check.ts index d49ac9f1c15c..245c4051e27c 100644 --- a/packages/language-tools/language-server/src/check.ts +++ b/packages/language-tools/language-server/src/check.ts @@ -33,12 +33,18 @@ export interface CheckResult { export class AstroCheck { private ts!: typeof import('typescript'); public linter!: ReturnType<(typeof kit)['createTypeScriptChecker']>; + private readonly workspacePath: string; + private readonly typescriptPath: string | undefined; + private readonly tsconfigPath: string | undefined; constructor( - private readonly workspacePath: string, - private readonly typescriptPath: string | undefined, - private readonly tsconfigPath: string | undefined, + workspacePath: string, + typescriptPath: string | undefined, + tsconfigPath: string | undefined, ) { + this.workspacePath = workspacePath; + this.typescriptPath = typescriptPath; + this.tsconfigPath = tsconfigPath; this.initialize(); } diff --git a/packages/language-tools/language-server/src/core/frontmatterHolders.ts b/packages/language-tools/language-server/src/core/frontmatterHolders.ts index aa627de1ec5e..66292c1e6cab 100644 --- a/packages/language-tools/language-server/src/core/frontmatterHolders.ts +++ b/packages/language-tools/language-server/src/core/frontmatterHolders.ts @@ -95,13 +95,21 @@ export class FrontmatterHolder implements VirtualCode { mappings: CodeMapping[]; embeddedCodes: VirtualCode[]; public hasFrontmatter = false; + public fileName: string; + public languageId: string; + public snapshot: ts.IScriptSnapshot; + public collection: string | undefined; constructor( - public fileName: string, - public languageId: string, - public snapshot: ts.IScriptSnapshot, - public collection: string | undefined, + fileName: string, + languageId: string, + snapshot: ts.IScriptSnapshot, + collection: string | undefined, ) { + this.fileName = fileName; + this.languageId = languageId; + this.snapshot = snapshot; + this.collection = collection; this.mappings = [ { sourceOffsets: [0], @@ -121,8 +129,8 @@ export class FrontmatterHolder implements VirtualCode { this.embeddedCodes = []; this.snapshot = snapshot; - // If the file is not part of a collection, we don't need to do anything if (!this.collection) { + // If the file is not part of a collection, we don't need to do anything return; } diff --git a/packages/language-tools/language-server/src/core/index.ts b/packages/language-tools/language-server/src/core/index.ts index b8ec8699de8a..0d1f18464113 100644 --- a/packages/language-tools/language-server/src/core/index.ts +++ b/packages/language-tools/language-server/src/core/index.ts @@ -149,11 +149,12 @@ export class AstroVirtualCode implements VirtualCode { compilerDiagnostics!: DiagnosticMessage[]; htmlDocument!: HTMLDocument; codegenStacks = []; + public fileName: string; + public snapshot: ts.IScriptSnapshot; - constructor( - public fileName: string, - public snapshot: ts.IScriptSnapshot, - ) { + constructor(fileName: string, snapshot: ts.IScriptSnapshot) { + this.fileName = fileName; + this.snapshot = snapshot; this.mappings = [ { sourceOffsets: [0], diff --git a/packages/language-tools/language-server/src/core/svelte.ts b/packages/language-tools/language-server/src/core/svelte.ts index 1418159a9adb..8d7275216bd2 100644 --- a/packages/language-tools/language-server/src/core/svelte.ts +++ b/packages/language-tools/language-server/src/core/svelte.ts @@ -45,11 +45,12 @@ class SvelteVirtualCode implements VirtualCode { mappings!: Mapping[]; embeddedCodes!: VirtualCode[]; codegenStacks = []; + public fileName: string; + public snapshot: ts.IScriptSnapshot; - constructor( - public fileName: string, - public snapshot: ts.IScriptSnapshot, - ) { + constructor(fileName: string, snapshot: ts.IScriptSnapshot) { + this.fileName = fileName; + this.snapshot = snapshot; this.mappings = []; this.embeddedCodes = []; diff --git a/packages/language-tools/language-server/src/core/vue.ts b/packages/language-tools/language-server/src/core/vue.ts index 3be6edc3e5c9..26beb97a8d52 100644 --- a/packages/language-tools/language-server/src/core/vue.ts +++ b/packages/language-tools/language-server/src/core/vue.ts @@ -45,11 +45,12 @@ class VueVirtualCode implements VirtualCode { mappings!: Mapping[]; embeddedCodes!: VirtualCode[]; codegenStacks = []; + public fileName: string; + public snapshot: ts.IScriptSnapshot; - constructor( - public fileName: string, - public snapshot: ts.IScriptSnapshot, - ) { + constructor(fileName: string, snapshot: ts.IScriptSnapshot) { + this.fileName = fileName; + this.snapshot = snapshot; this.mappings = []; this.embeddedCodes = []; diff --git a/packages/language-tools/language-server/test/content-intellisense/caching.test.ts b/packages/language-tools/language-server/test/content-intellisense/caching.test.ts index 92c9cdd41751..8f3a33efb16b 100644 --- a/packages/language-tools/language-server/test/content-intellisense/caching.test.ts +++ b/packages/language-tools/language-server/test/content-intellisense/caching.test.ts @@ -9,60 +9,55 @@ import { fixtureDir } from '../utils.ts'; const contentSchemaPath = path.resolve(fixtureDir, '.astro', 'collections', 'caching.schema.json'); -describe( - 'Content Intellisense - Caching', - { skip: Number.parseInt(process.versions.node) === 20 }, - async () => { - let languageServer: LanguageServer; - - before(async () => { - languageServer = await getLanguageServer(); - }); - - it('Properly updates the schema when they are updated', async () => { - const document = await languageServer.handle.openTextDocument( - path.join(fixtureDir, 'src', 'content', 'caching', 'caching.md'), - 'markdown', - ); - - const hover = await languageServer.handle.sendHoverRequest( - document.uri, - Position.create(1, 1), - ); - - assert.deepStrictEqual(hover?.contents, { - kind: 'markdown', - value: 'I will be changed', - }); - - fs.writeFileSync( - contentSchemaPath, - fs.readFileSync(contentSchemaPath, 'utf-8').replaceAll('I will be changed', 'I am changed'), - ); - - await languageServer.handle.didChangeWatchedFiles([ - { - uri: URI.file(contentSchemaPath).toString(), - type: 2, - }, - ]); - - const hover2 = await languageServer.handle.sendHoverRequest( - document.uri, - Position.create(1, 1), - ); - - assert.deepStrictEqual(hover2?.contents, { - kind: 'markdown', - value: 'I am changed', - }); +describe('Content Intellisense - Caching', { + skip: Number.parseInt(process.versions.node) === 20, +}, async () => { + let languageServer: LanguageServer; + + before(async () => { + languageServer = await getLanguageServer(); + }); + + it('Properly updates the schema when they are updated', async () => { + const document = await languageServer.handle.openTextDocument( + path.join(fixtureDir, 'src', 'content', 'caching', 'caching.md'), + 'markdown', + ); + + const hover = await languageServer.handle.sendHoverRequest(document.uri, Position.create(1, 1)); + + assert.deepStrictEqual(hover?.contents, { + kind: 'markdown', + value: 'I will be changed', }); - after(async () => { - fs.writeFileSync( - contentSchemaPath, - fs.readFileSync(contentSchemaPath, 'utf-8').replaceAll('I am changed', 'I will be changed'), - ); + fs.writeFileSync( + contentSchemaPath, + fs.readFileSync(contentSchemaPath, 'utf-8').replaceAll('I will be changed', 'I am changed'), + ); + + await languageServer.handle.didChangeWatchedFiles([ + { + uri: URI.file(contentSchemaPath).toString(), + type: 2, + }, + ]); + + const hover2 = await languageServer.handle.sendHoverRequest( + document.uri, + Position.create(1, 1), + ); + + assert.deepStrictEqual(hover2?.contents, { + kind: 'markdown', + value: 'I am changed', }); - }, -); + }); + + after(async () => { + fs.writeFileSync( + contentSchemaPath, + fs.readFileSync(contentSchemaPath, 'utf-8').replaceAll('I am changed', 'I will be changed'), + ); + }); +}); diff --git a/packages/language-tools/language-server/test/content-intellisense/completions.test.ts b/packages/language-tools/language-server/test/content-intellisense/completions.test.ts index e6bbf0d60d94..2725bb4e64cb 100644 --- a/packages/language-tools/language-server/test/content-intellisense/completions.test.ts +++ b/packages/language-tools/language-server/test/content-intellisense/completions.test.ts @@ -5,45 +5,43 @@ import { Position } from '@volar/language-server'; import { getLanguageServer, type LanguageServer } from '../server.ts'; import { fixtureDir } from '../utils.ts'; -describe( - 'Content Intellisense - Completions', - { skip: Number.parseInt(process.versions.node) === 20 }, - async () => { - let languageServer: LanguageServer; - - before(async () => { - languageServer = await getLanguageServer(); - }); - - it('Provide completions for collection properties', async () => { - const document = await languageServer.handle.openTextDocument( - path.join(fixtureDir, 'src', 'content', 'blog', 'completions.md'), - 'markdown', - ); - - const completions = await languageServer.handle.sendCompletionRequest( - document.uri, - Position.create(1, 1), - ); - - // We don't do any mapping ourselves here, so we'll just check if the labels are correct. - const labels = (completions?.items ?? []).map((item) => item.label); - ['title', 'description', 'tags', 'type'].forEach((m) => assert.ok(labels.includes(m))); - }); - - it('Provide completions for collection property values', async () => { - const document = await languageServer.handle.openTextDocument( - path.join(fixtureDir, 'src', 'content', 'blog', 'completions-values.md'), - 'markdown', - ); - - const completions = await languageServer.handle.sendCompletionRequest( - document.uri, - Position.create(1, 7), - ); - - const labels = (completions?.items ?? []).map((item) => item.label); - ['blog'].forEach((m) => assert.ok(labels.includes(m))); - }); - }, -); +describe('Content Intellisense - Completions', { + skip: Number.parseInt(process.versions.node) === 20, +}, async () => { + let languageServer: LanguageServer; + + before(async () => { + languageServer = await getLanguageServer(); + }); + + it('Provide completions for collection properties', async () => { + const document = await languageServer.handle.openTextDocument( + path.join(fixtureDir, 'src', 'content', 'blog', 'completions.md'), + 'markdown', + ); + + const completions = await languageServer.handle.sendCompletionRequest( + document.uri, + Position.create(1, 1), + ); + + // We don't do any mapping ourselves here, so we'll just check if the labels are correct. + const labels = (completions?.items ?? []).map((item) => item.label); + ['title', 'description', 'tags', 'type'].forEach((m) => assert.ok(labels.includes(m))); + }); + + it('Provide completions for collection property values', async () => { + const document = await languageServer.handle.openTextDocument( + path.join(fixtureDir, 'src', 'content', 'blog', 'completions-values.md'), + 'markdown', + ); + + const completions = await languageServer.handle.sendCompletionRequest( + document.uri, + Position.create(1, 7), + ); + + const labels = (completions?.items ?? []).map((item) => item.label); + ['blog'].forEach((m) => assert.ok(labels.includes(m))); + }); +}); diff --git a/packages/language-tools/language-server/test/content-intellisense/definitions.test.ts b/packages/language-tools/language-server/test/content-intellisense/definitions.test.ts index 2772d67573a4..2aff28e99bed 100644 --- a/packages/language-tools/language-server/test/content-intellisense/definitions.test.ts +++ b/packages/language-tools/language-server/test/content-intellisense/definitions.test.ts @@ -6,49 +6,47 @@ import { Position } from '@volar/language-server'; import { getLanguageServer, type LanguageServer } from '../server.ts'; import { fixtureDir } from '../utils.ts'; -describe( - 'Content Intellisense - Go To Everywhere', - { skip: Number.parseInt(process.versions.node) === 20 }, - async () => { - let languageServer: LanguageServer; - - before(async () => { - languageServer = await getLanguageServer(); +describe('Content Intellisense - Go To Everywhere', { + skip: Number.parseInt(process.versions.node) === 20, +}, async () => { + let languageServer: LanguageServer; + + before(async () => { + languageServer = await getLanguageServer(); + }); + + it('Provide definitions for keys', async () => { + const document = await languageServer.handle.openTextDocument( + path.join(fixtureDir, 'src', 'content', 'blog', 'definitions.md'), + 'markdown', + ); + + const definitions = (await languageServer.handle.sendDefinitionRequest( + document.uri, + Position.create(1, 2), + )) as LocationLink[]; + + const targetUris = definitions?.map((definition) => definition.targetUri); + assert.strictEqual( + targetUris.every((uri) => uri.endsWith('config.ts')), + true, + ); + + const { targetRange, targetSelectionRange, originSelectionRange } = definitions[0]; + + assert.deepStrictEqual(targetRange, { + start: { line: 7, character: 2 }, + end: { line: 7, character: 65 }, }); - it('Provide definitions for keys', async () => { - const document = await languageServer.handle.openTextDocument( - path.join(fixtureDir, 'src', 'content', 'blog', 'definitions.md'), - 'markdown', - ); - - const definitions = (await languageServer.handle.sendDefinitionRequest( - document.uri, - Position.create(1, 2), - )) as LocationLink[]; - - const targetUris = definitions?.map((definition) => definition.targetUri); - assert.strictEqual( - targetUris.every((uri) => uri.endsWith('config.ts')), - true, - ); - - const { targetRange, targetSelectionRange, originSelectionRange } = definitions[0]; - - assert.deepStrictEqual(targetRange, { - start: { line: 7, character: 2 }, - end: { line: 7, character: 65 }, - }); - - assert.deepStrictEqual(targetSelectionRange, { - start: { line: 7, character: 2 }, - end: { line: 7, character: 7 }, - }); - - assert.deepStrictEqual(originSelectionRange, { - start: { line: 1, character: 0 }, - end: { line: 1, character: 5 }, - }); + assert.deepStrictEqual(targetSelectionRange, { + start: { line: 7, character: 2 }, + end: { line: 7, character: 7 }, }); - }, -); + + assert.deepStrictEqual(originSelectionRange, { + start: { line: 1, character: 0 }, + end: { line: 1, character: 5 }, + }); + }); +}); diff --git a/packages/language-tools/language-server/test/content-intellisense/diagnostics.test.ts b/packages/language-tools/language-server/test/content-intellisense/diagnostics.test.ts index bea456c12b4b..6ce3904d206f 100644 --- a/packages/language-tools/language-server/test/content-intellisense/diagnostics.test.ts +++ b/packages/language-tools/language-server/test/content-intellisense/diagnostics.test.ts @@ -7,94 +7,92 @@ import { getLanguageServer, type LanguageServer } from '../server.ts'; import { fixtureDir } from '../utils.ts'; // TODO: We can't sync the fixture with these mistakes at all, as such we can't run these tests. -describe.skip( - 'Content Intellisense - Diagnostics', - { skip: Number.parseInt(process.versions.node) === 20 }, - async () => { - let languageServer: LanguageServer; - - before(async () => { - languageServer = await getLanguageServer(); +describe.skip('Content Intellisense - Diagnostics', { + skip: Number.parseInt(process.versions.node) === 20, +}, async () => { + let languageServer: LanguageServer; + + before(async () => { + languageServer = await getLanguageServer(); + }); + + it('Report errors for missing entries in frontmatter', async () => { + const document = await languageServer.handle.openTextDocument( + path.join(fixtureDir, 'src', 'content', 'blog', 'missing_property.md'), + 'markdown', + ); + const diagnostics = (await languageServer.handle.sendDocumentDiagnosticRequest( + document.uri, + )) as FullDocumentDiagnosticReport; + + assert.strictEqual(diagnostics.items.length, 1); + + const firstDiagnostic = diagnostics.items[0]; + + // The data is not super relevant to the test, so we'll throw it out. + delete firstDiagnostic.data; + + assert.deepStrictEqual(firstDiagnostic, { + code: 0, + message: 'Missing property "description".', + range: { + start: Position.create(0, 0), + end: Position.create(2, 3), + }, + severity: DiagnosticSeverity.Error, + source: 'astro', }); - - it('Report errors for missing entries in frontmatter', async () => { - const document = await languageServer.handle.openTextDocument( - path.join(fixtureDir, 'src', 'content', 'blog', 'missing_property.md'), - 'markdown', - ); - const diagnostics = (await languageServer.handle.sendDocumentDiagnosticRequest( - document.uri, - )) as FullDocumentDiagnosticReport; - - assert.strictEqual(diagnostics.items.length, 1); - - const firstDiagnostic = diagnostics.items[0]; - - // The data is not super relevant to the test, so we'll throw it out. - delete firstDiagnostic.data; - - assert.deepStrictEqual(firstDiagnostic, { - code: 0, - message: 'Missing property "description".', - range: { - start: Position.create(0, 0), - end: Position.create(2, 3), - }, - severity: DiagnosticSeverity.Error, - source: 'astro', - }); - }); - - it('Report errors for invalid types in frontmatter', async () => { - const document = await languageServer.handle.openTextDocument( - path.join(fixtureDir, 'src', 'content', 'blog', 'type_error.md'), - 'markdown', - ); - const diagnostics = (await languageServer.handle.sendDocumentDiagnosticRequest( - document.uri, - )) as FullDocumentDiagnosticReport; - - assert.strictEqual(diagnostics.items.length, 1); - - const firstDiagnostic = diagnostics.items[0]; - - delete firstDiagnostic.data; - - assert.deepStrictEqual(firstDiagnostic, { - code: 0, - message: 'Incorrect type. Expected "string".', - range: { - start: Position.create(1, 7), - end: Position.create(1, 8), - }, - severity: DiagnosticSeverity.Error, - source: 'astro', - }); + }); + + it('Report errors for invalid types in frontmatter', async () => { + const document = await languageServer.handle.openTextDocument( + path.join(fixtureDir, 'src', 'content', 'blog', 'type_error.md'), + 'markdown', + ); + const diagnostics = (await languageServer.handle.sendDocumentDiagnosticRequest( + document.uri, + )) as FullDocumentDiagnosticReport; + + assert.strictEqual(diagnostics.items.length, 1); + + const firstDiagnostic = diagnostics.items[0]; + + delete firstDiagnostic.data; + + assert.deepStrictEqual(firstDiagnostic, { + code: 0, + message: 'Incorrect type. Expected "string".', + range: { + start: Position.create(1, 7), + end: Position.create(1, 8), + }, + severity: DiagnosticSeverity.Error, + source: 'astro', }); - - it('Report error for missing frontmatter', async () => { - const document = await languageServer.handle.openTextDocument( - path.join(fixtureDir, 'src', 'content', 'blog', 'no_frontmatter.md'), - 'markdown', - ); - const diagnostics = (await languageServer.handle.sendDocumentDiagnosticRequest( - document.uri, - )) as FullDocumentDiagnosticReport; - - assert.strictEqual(diagnostics.items.length, 1); - - const firstDiagnostic = diagnostics.items[0]; - - delete firstDiagnostic.data; - - assert.deepStrictEqual(firstDiagnostic, { - message: 'Frontmatter is required for this file.', - range: { - start: Position.create(0, 0), - end: Position.create(0, 0), - }, - severity: DiagnosticSeverity.Error, - }); + }); + + it('Report error for missing frontmatter', async () => { + const document = await languageServer.handle.openTextDocument( + path.join(fixtureDir, 'src', 'content', 'blog', 'no_frontmatter.md'), + 'markdown', + ); + const diagnostics = (await languageServer.handle.sendDocumentDiagnosticRequest( + document.uri, + )) as FullDocumentDiagnosticReport; + + assert.strictEqual(diagnostics.items.length, 1); + + const firstDiagnostic = diagnostics.items[0]; + + delete firstDiagnostic.data; + + assert.deepStrictEqual(firstDiagnostic, { + message: 'Frontmatter is required for this file.', + range: { + start: Position.create(0, 0), + end: Position.create(0, 0), + }, + severity: DiagnosticSeverity.Error, }); - }, -); + }); +}); diff --git a/packages/language-tools/language-server/test/content-intellisense/hover.test.ts b/packages/language-tools/language-server/test/content-intellisense/hover.test.ts index 3ceec867555a..d729182dade8 100644 --- a/packages/language-tools/language-server/test/content-intellisense/hover.test.ts +++ b/packages/language-tools/language-server/test/content-intellisense/hover.test.ts @@ -5,31 +5,26 @@ import { Position } from '@volar/language-server'; import { getLanguageServer, type LanguageServer } from '../server.ts'; import { fixtureDir } from '../utils.ts'; -describe( - 'Content Intellisense - Hover', - { skip: Number.parseInt(process.versions.node) === 20 }, - async () => { - let languageServer: LanguageServer; +describe('Content Intellisense - Hover', { + skip: Number.parseInt(process.versions.node) === 20, +}, async () => { + let languageServer: LanguageServer; - before(async () => { - languageServer = await getLanguageServer(); - }); + before(async () => { + languageServer = await getLanguageServer(); + }); - it('Provide hover information for collection properties', async () => { - const document = await languageServer.handle.openTextDocument( - path.join(fixtureDir, 'src', 'content', 'blog', 'hover.md'), - 'markdown', - ); + it('Provide hover information for collection properties', async () => { + const document = await languageServer.handle.openTextDocument( + path.join(fixtureDir, 'src', 'content', 'blog', 'hover.md'), + 'markdown', + ); - const hover = await languageServer.handle.sendHoverRequest( - document.uri, - Position.create(1, 1), - ); + const hover = await languageServer.handle.sendHoverRequest(document.uri, Position.create(1, 1)); - assert.deepStrictEqual(hover?.contents, { - kind: 'markdown', - value: "The blog post's title.", - }); + assert.deepStrictEqual(hover?.contents, { + kind: 'markdown', + value: "The blog post's title.", }); - }, -); + }); +}); diff --git a/packages/language-tools/language-server/test/package.json b/packages/language-tools/language-server/test/package.json index 8dd02c725f41..9489b295a6fc 100644 --- a/packages/language-tools/language-server/test/package.json +++ b/packages/language-tools/language-server/test/package.json @@ -6,9 +6,9 @@ "@astrojs/svelte": "workspace:*", "@astrojs/vue": "workspace:*", "astro": "workspace:*", - "svelte": "^5.54.1" + "svelte": "^5.55.3" }, "devDependencies": { - "tinyglobby": "^0.2.15" + "tinyglobby": "^0.2.16" } } diff --git a/packages/language-tools/ts-plugin/src/frontmatter.ts b/packages/language-tools/ts-plugin/src/frontmatter.ts index 215bfc1fc65b..4fdd5f7ffbde 100644 --- a/packages/language-tools/ts-plugin/src/frontmatter.ts +++ b/packages/language-tools/ts-plugin/src/frontmatter.ts @@ -87,13 +87,21 @@ export class FrontmatterHolder implements VirtualCode { id = 'frontmatter-holder'; mappings: CodeMapping[]; embeddedCodes: VirtualCode[]; + public fileName: string; + public languageId: string; + public snapshot: ts.IScriptSnapshot; + public collection: string | undefined; constructor( - public fileName: string, - public languageId: string, - public snapshot: ts.IScriptSnapshot, - public collection: string | undefined, + fileName: string, + languageId: string, + snapshot: ts.IScriptSnapshot, + collection: string | undefined, ) { + this.fileName = fileName; + this.languageId = languageId; + this.snapshot = snapshot; + this.collection = collection; this.mappings = [ { sourceOffsets: [0], diff --git a/packages/language-tools/ts-plugin/src/language.ts b/packages/language-tools/ts-plugin/src/language.ts index fc643e805980..6f5ba2cf0df7 100644 --- a/packages/language-tools/ts-plugin/src/language.ts +++ b/packages/language-tools/ts-plugin/src/language.ts @@ -44,11 +44,12 @@ export class AstroVirtualCode implements VirtualCode { mappings!: CodeMapping[]; embeddedCodes!: VirtualCode[]; codegenStacks = []; + public fileName: string; + public snapshot: ts.IScriptSnapshot; - constructor( - public fileName: string, - public snapshot: ts.IScriptSnapshot, - ) { + constructor(fileName: string, snapshot: ts.IScriptSnapshot) { + this.fileName = fileName; + this.snapshot = snapshot; this.mappings = [ { sourceOffsets: [0], diff --git a/packages/language-tools/vscode/README.md b/packages/language-tools/vscode/README.md index c0b8933c4b7f..2423adf1ed09 100644 --- a/packages/language-tools/vscode/README.md +++ b/packages/language-tools/vscode/README.md @@ -25,7 +25,7 @@ A TypeScript plugin adding support for importing and exporting Astro components ## Configuration -HTML, CSS and TypeScript settings can be configured through the `html`, `css` and `typescript` namespaces respectively. For example, HTML documentation on hover can be disabled using `'html.hover.documentation': false`. Formatting can be configured through [Prettier's different configuration methods](https://prettier.io/docs/en/configuration.html). +HTML and CSS settings can be configured through the `html` and `css` setting prefixes. TypeScript-related settings appear in the VS Code Settings UI under the **JavaScript and TypeScript (js/ts)** category in recent VS Code versions, but the actual JSON keys use the `typescript.*` namespace (for example, `"typescript.preferences.importModuleSpecifier": "non-relative"`). For example, HTML documentation on hover can be disabled using `"html.hover.documentation": false`. Formatting can be configured through [Prettier's different configuration methods](https://prettier.io/docs/en/configuration.html). ## Troubleshooting diff --git a/packages/language-tools/vscode/package.json b/packages/language-tools/vscode/package.json index 207d7f934a3f..3690e71e38ff 100644 --- a/packages/language-tools/vscode/package.json +++ b/packages/language-tools/vscode/package.json @@ -261,13 +261,13 @@ "js-yaml": "^4.1.1", "kleur": "^4.1.5", "mocha": "^11.7.5", - "ovsx": "^0.10.9", + "ovsx": "^0.10.10", "vscode-languageclient": "^9.0.1", "vscode-tmgrammar-test": "^0.1.3" }, "dependencies": { "@astrojs/compiler": "^2.13.1", - "prettier": "^3.8.1", + "prettier": "^3.8.2", "prettier-plugin-astro": "^0.14.1" } } diff --git a/packages/markdown/remark/package.json b/packages/markdown/remark/package.json index 55b5e17b78ea..b020022483db 100644 --- a/packages/markdown/remark/package.json +++ b/packages/markdown/remark/package.json @@ -34,7 +34,8 @@ "build": "astro-scripts build \"src/**/*.ts\" && tsc -p tsconfig.json", "build:ci": "astro-scripts build \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"", - "test": "astro-scripts test \"test/**/*.test.js\"" + "test": "astro-scripts test \"test/**/*.test.ts\"", + "typecheck:tests": "tsc -p tsconfig.test.json --noEmit" }, "dependencies": { "@astrojs/internal-helpers": "workspace:*", diff --git a/packages/markdown/remark/test/autolinking.test.js b/packages/markdown/remark/test/autolinking.test.ts similarity index 91% rename from packages/markdown/remark/test/autolinking.test.js rename to packages/markdown/remark/test/autolinking.test.ts index 3fd5ad0fcdad..2eb5acf5570e 100644 --- a/packages/markdown/remark/test/autolinking.test.js +++ b/packages/markdown/remark/test/autolinking.test.ts @@ -1,10 +1,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { createMarkdownProcessor } from '../dist/index.js'; +import { createMarkdownProcessor, type MarkdownProcessor } from '../dist/index.js'; describe('autolinking', () => { describe('plain md', () => { - let processor; + let processor: MarkdownProcessor; before(async () => { processor = await createMarkdownProcessor(); diff --git a/packages/markdown/remark/test/browser.test.js b/packages/markdown/remark/test/browser.test.ts similarity index 92% rename from packages/markdown/remark/test/browser.test.js rename to packages/markdown/remark/test/browser.test.ts index 824f6fa0becd..9a7f5b0c3cd8 100644 --- a/packages/markdown/remark/test/browser.test.js +++ b/packages/markdown/remark/test/browser.test.ts @@ -14,7 +14,7 @@ describe('Bundle for browsers', async () => { assert.ok(result.outputFiles.length > 0); } catch (error) { // Capture any esbuild errors and fail the test - assert.fail(error.message); + assert.fail((error as Error).message); } }); }); diff --git a/packages/markdown/remark/test/entities.test.js b/packages/markdown/remark/test/entities.test.ts similarity index 80% rename from packages/markdown/remark/test/entities.test.js rename to packages/markdown/remark/test/entities.test.ts index 3c244c15abb4..1ae4aea90df8 100644 --- a/packages/markdown/remark/test/entities.test.js +++ b/packages/markdown/remark/test/entities.test.ts @@ -1,9 +1,9 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { createMarkdownProcessor } from '../dist/index.js'; +import { createMarkdownProcessor, type MarkdownProcessor } from '../dist/index.js'; describe('entities', async () => { - let processor; + let processor: MarkdownProcessor; before(async () => { processor = await createMarkdownProcessor(); diff --git a/packages/markdown/remark/test/frontmatter.test.js b/packages/markdown/remark/test/frontmatter.test.ts similarity index 93% rename from packages/markdown/remark/test/frontmatter.test.js rename to packages/markdown/remark/test/frontmatter.test.ts index 336245106d1e..ee365d19e4de 100644 --- a/packages/markdown/remark/test/frontmatter.test.js +++ b/packages/markdown/remark/test/frontmatter.test.ts @@ -1,6 +1,12 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import { extractFrontmatter, parseFrontmatter } from '../dist/index.js'; +import { + extractFrontmatter, + parseFrontmatter, + type ParseFrontmatterOptions, +} from '../dist/index.js'; + +type FrontmatterStyle = ParseFrontmatterOptions['frontmatter']; const bom = '\uFEFF'; @@ -157,13 +163,14 @@ describe('parseFrontmatter', () => { it('frontmatter style for YAML', () => { const yaml = `\nfoo: bar\n`; - const parse1 = (style) => parseFrontmatter(`---${yaml}---`, { frontmatter: style }).content; + const parse1 = (style: FrontmatterStyle) => + parseFrontmatter(`---${yaml}---`, { frontmatter: style }).content; assert.deepEqual(parse1('preserve'), `---${yaml}---`); assert.deepEqual(parse1('remove'), ''); assert.deepEqual(parse1('empty-with-spaces'), ` \n \n `); assert.deepEqual(parse1('empty-with-lines'), `\n\n`); - const parse2 = (style) => + const parse2 = (style: FrontmatterStyle) => parseFrontmatter(`\n \n---${yaml}---\n\ncontent`, { frontmatter: style }).content; assert.deepEqual(parse2('preserve'), `\n \n---${yaml}---\n\ncontent`); assert.deepEqual(parse2('remove'), '\n \n\n\ncontent'); @@ -173,13 +180,14 @@ describe('parseFrontmatter', () => { it('frontmatter style for TOML', () => { const toml = `\nfoo = "bar"\n`; - const parse1 = (style) => parseFrontmatter(`+++${toml}+++`, { frontmatter: style }).content; + const parse1 = (style: FrontmatterStyle) => + parseFrontmatter(`+++${toml}+++`, { frontmatter: style }).content; assert.deepEqual(parse1('preserve'), `+++${toml}+++`); assert.deepEqual(parse1('remove'), ''); assert.deepEqual(parse1('empty-with-spaces'), ` \n \n `); assert.deepEqual(parse1('empty-with-lines'), `\n\n`); - const parse2 = (style) => + const parse2 = (style: FrontmatterStyle) => parseFrontmatter(`\n \n+++${toml}+++\n\ncontent`, { frontmatter: style }).content; assert.deepEqual(parse2('preserve'), `\n \n+++${toml}+++\n\ncontent`); assert.deepEqual(parse2('remove'), '\n \n\n\ncontent'); diff --git a/packages/markdown/remark/test/highlight.test.js b/packages/markdown/remark/test/highlight.test.ts similarity index 100% rename from packages/markdown/remark/test/highlight.test.js rename to packages/markdown/remark/test/highlight.test.ts diff --git a/packages/markdown/remark/test/plugins.test.js b/packages/markdown/remark/test/plugins.test.ts similarity index 62% rename from packages/markdown/remark/test/plugins.test.js rename to packages/markdown/remark/test/plugins.test.ts index c52955f83902..9d0249faf813 100644 --- a/packages/markdown/remark/test/plugins.test.js +++ b/packages/markdown/remark/test/plugins.test.ts @@ -1,28 +1,26 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { fileURLToPath } from 'node:url'; -import { createMarkdownProcessor } from '../dist/index.js'; +import type { VFile } from 'vfile'; +import { createMarkdownProcessor, type RemarkPlugin } from '../dist/index.js'; describe('plugins', () => { it('should be able to get file path when passing fileURL', async () => { - let context; + let context: VFile | undefined; + + const collectFile: RemarkPlugin = () => (_tree, file) => { + context = file; + }; const processor = await createMarkdownProcessor({ - remarkPlugins: [ - () => { - const transformer = (_tree, file) => { - context = file; - }; - return transformer; - }, - ], + remarkPlugins: [collectFile], }); await processor.render(`test`, { fileURL: new URL('virtual.md', import.meta.url), }); - assert.ok(typeof context === 'object'); + assert.ok(context); assert.equal(context.path, fileURLToPath(new URL('virtual.md', import.meta.url))); }); }); diff --git a/packages/markdown/remark/test/prism.test.js b/packages/markdown/remark/test/prism.test.ts similarity index 100% rename from packages/markdown/remark/test/prism.test.js rename to packages/markdown/remark/test/prism.test.ts diff --git a/packages/markdown/remark/test/remark-collect-images.test.js b/packages/markdown/remark/test/remark-collect-images.test.ts similarity index 81% rename from packages/markdown/remark/test/remark-collect-images.test.js rename to packages/markdown/remark/test/remark-collect-images.test.ts index c8652642cde5..579a7b7848ef 100644 --- a/packages/markdown/remark/test/remark-collect-images.test.js +++ b/packages/markdown/remark/test/remark-collect-images.test.ts @@ -1,33 +1,36 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { visit } from 'unist-util-visit'; -import { createMarkdownProcessor } from '../dist/index.js'; +import { + createMarkdownProcessor, + type MarkdownProcessor, + type RehypePlugin, +} from '../dist/index.js'; describe('collect images', async () => { - let processor; - let processorWithHastProperties; + let processor: MarkdownProcessor; + let processorWithHastProperties: MarkdownProcessor; before(async () => { processor = await createMarkdownProcessor({ image: { domains: ['example.com'] } }); + + const addImageProps: RehypePlugin = () => (tree) => { + visit(tree, 'element', (node) => { + if (node.tagName === 'img') { + node.properties.className = ['image-class']; + node.properties.htmlFor = 'some-id'; + } + }); + }; + processorWithHastProperties = await createMarkdownProcessor({ - rehypePlugins: [ - () => { - return (tree) => { - visit(tree, 'element', (node) => { - if (node.tagName === 'img') { - node.properties.className = ['image-class']; - node.properties.htmlFor = 'some-id'; - } - }); - }; - }, - ], + rehypePlugins: [addImageProps], }); }); it('should collect inline image paths', async () => { const markdown = `Hello ![inline image url](./img.png)`; - const fileURL = 'file.md'; + const fileURL = new URL('file.md', import.meta.url); const { code, @@ -45,7 +48,7 @@ describe('collect images', async () => { it('should collect allowed remote image paths', async () => { const markdown = `Hello ![inline remote image url](https://example.com/example.png)`; - const fileURL = 'file.md'; + const fileURL = new URL('file.md', import.meta.url); const { code, @@ -62,7 +65,7 @@ describe('collect images', async () => { it('should not collect other remote image paths', async () => { const markdown = `Hello ![inline remote image url](https://google.com/google.png)`; - const fileURL = 'file.md'; + const fileURL = new URL('file.md', import.meta.url); const { code, @@ -79,7 +82,7 @@ describe('collect images', async () => { it('should add image paths from definition', async () => { const markdown = `Hello ![image ref][img-ref] ![remote image ref][remote-img-ref]\n\n[img-ref]: ./img.webp\n[remote-img-ref]: https://example.com/example.jpg`; - const fileURL = 'file.md'; + const fileURL = new URL('file.md', import.meta.url); const { code, metadata } = await processor.render(markdown, { fileURL }); @@ -94,7 +97,7 @@ describe('collect images', async () => { it('should preserve className as HTML class attribute', async () => { const markdown = `Hello ![image with class](./img.png)`; - const fileURL = 'file.md'; + const fileURL = new URL('file.md', import.meta.url); const { code } = await processorWithHastProperties.render(markdown, { fileURL }); diff --git a/packages/markdown/remark/test/shiki.test.js b/packages/markdown/remark/test/shiki.test.ts similarity index 78% rename from packages/markdown/remark/test/shiki.test.js rename to packages/markdown/remark/test/shiki.test.ts index 56ffe77a5015..bc6d0dcb2fe3 100644 --- a/packages/markdown/remark/test/shiki.test.js +++ b/packages/markdown/remark/test/shiki.test.ts @@ -1,6 +1,13 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import { createMarkdownProcessor, createShikiHighlighter } from '../dist/index.js'; +import type { Element } from 'hast'; +import type { LanguageRegistration, ThemeRegistration } from 'shiki'; +import { + createMarkdownProcessor, + createShikiHighlighter, + type ShikiHighlighter, +} from '../dist/index.js'; +// @ts-expect-error: `clearShikiHighlighterCache` is marked `@internal` and stripped from the `.d.ts`, but still exists at runtime. import { clearShikiHighlighterCache } from '../dist/shiki.js'; describe('shiki syntax highlighting', () => { @@ -44,9 +51,10 @@ describe('shiki syntax highlighting', () => { const highlighter = await createShikiHighlighter(); const hast = await highlighter.codeToHast('const foo = "bar";', 'js'); + const root = hast.children[0] as Element; - assert.match(hast.children[0].properties.class, /astro-code github-dark/); - assert.match(hast.children[0].properties.style, /background-color:#24292e;color:#e1e4e8;/); + assert.match(root.properties.class as string, /astro-code github-dark/); + assert.match(root.properties.style as string, /background-color:#24292e;color:#e1e4e8;/); }); it('createShikiHighlighter can reuse the same instance for different languages', async () => { @@ -76,11 +84,16 @@ describe('shiki syntax highlighting', () => { 'bicep', 'blade', 'bsl', - ]; + ] as const; - const highlighters = new Set(); + const highlighters = new Set(); for (const lang of langs) { - highlighters.add(await createShikiHighlighter({ langs: [lang] })); + highlighters.add( + await createShikiHighlighter({ + // @ts-expect-error: `langs` is typed as `LanguageRegistration[]`, but Shiki's `createHighlighter` accepts both strings and objects in the array. + langs: [lang], + }), + ); } // Ensure that we only have one highlighter instance. @@ -113,7 +126,11 @@ describe('shiki syntax highlighting', () => { const highlighter = await createShikiHighlighter(); const html = await highlighter.codeToHtml(`foo`, 'js', { - attributes: { 'data-foo': 'bar', autofocus: true }, + attributes: { + 'data-foo': 'bar', + // @ts-expect-error: Shiki's `codeToHtml` accepts boolean attributes as `string | boolean`, but the types are currently incorrect. + autofocus: true, + }, }); assert.match(html, /data-foo="bar"/); @@ -164,7 +181,7 @@ describe('shiki syntax highlighting', () => { }); const html = await highlighter.codeToHtml(`let test = "some string"`, 'cjs', { - attributes: { 'data-foo': 'bar', autofocus: true }, + attributes: { 'data-foo': 'bar' }, }); assert.match(html, /data-language="cjs"/); @@ -188,12 +205,15 @@ describe('shiki syntax highlighting', () => { clearShikiHighlighterCache(); const theme = 'github-light'; - const highlighter = await createShikiHighlighter({ theme }); + interface ShikiHighlighterInternal extends ShikiHighlighter { + loadLanguage(...langs: unknown[]): Promise; + getLoadedLanguages(): string[]; + } + const highlighter = (await createShikiHighlighter({ theme })) as ShikiHighlighterInternal; - // loadLanguage is an internal method - const loadLanguageArgs = []; - const originalLoadLanguage = highlighter['loadLanguage']; - highlighter['loadLanguage'] = async (...args) => { + const loadLanguageArgs: unknown[] = []; + const originalLoadLanguage = highlighter.loadLanguage; + highlighter.loadLanguage = async (...args: unknown[]) => { loadLanguageArgs.push(...args); return await originalLoadLanguage(...args); }; @@ -202,19 +222,35 @@ describe('shiki syntax highlighting', () => { assert.equal(loadLanguageArgs.length, 0); // Load a new language - const h1 = await createShikiHighlighter({ theme, langs: ['js'] }); + const h1 = await createShikiHighlighter({ + theme, + // @ts-expect-error: `langs` is typed as `LanguageRegistration[]`, but Shiki's `createHighlighter` accepts both strings and objects in the array. + langs: ['js'], + }); assert.equal(loadLanguageArgs.length, 1); // Load the same language again - const h2 = await createShikiHighlighter({ theme, langs: ['js'] }); + const h2 = await createShikiHighlighter({ + theme, + // @ts-expect-error: `langs` is typed as `LanguageRegistration[]`, but Shiki's `createHighlighter` accepts both strings and objects in the array. + langs: ['js'], + }); assert.equal(loadLanguageArgs.length, 1); // Load another language - const h3 = await createShikiHighlighter({ theme, langs: ['ts'] }); + const h3 = await createShikiHighlighter({ + theme, + // @ts-expect-error: `langs` is typed as `LanguageRegistration[]`, but Shiki's `createHighlighter` accepts both strings and objects in the array. + langs: ['ts'], + }); assert.equal(loadLanguageArgs.length, 2); // Load the same language again - const h4 = await createShikiHighlighter({ theme, langs: ['ts'] }); + const h4 = await createShikiHighlighter({ + theme, + // @ts-expect-error: `langs` is typed as `LanguageRegistration[]`, but Shiki's `createHighlighter` accepts both strings and objects in the array. + langs: ['ts'], + }); assert.equal(loadLanguageArgs.length, 2); // All highlighters should be the same instance @@ -251,7 +287,7 @@ describe('shiki syntax highlighting', () => { it('uses a custom (ThemeRegistrationRaw) theme', async () => { // Minimal subset of a custom theme — only the fields Shiki needs to // derive the pre element's background-color and color. - const serendipityMorning = { + const serendipityMorning: ThemeRegistration = { name: 'Serendipity Morning', type: 'light', colors: { @@ -279,7 +315,7 @@ describe('shiki syntax highlighting', () => { // Minimal rinfo grammar — same language used in the langs fixture. // Must be passed as a LanguageRegistration (name + scopeName at top level), // not the { id, grammar } wrapper used by Astro's config layer. - const riLang = { + const riLang: LanguageRegistration = { name: 'rinfo', scopeName: 'source.rinfo', patterns: [{ include: '#lf-rinfo' }], diff --git a/packages/markdown/remark/tsconfig.test.json b/packages/markdown/remark/tsconfig.test.json new file mode 100644 index 000000000000..fa4f11e4ae8b --- /dev/null +++ b/packages/markdown/remark/tsconfig.test.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["test/**/*.ts"], + "exclude": ["test/fixtures/**"], + "compilerOptions": { + "allowJs": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "rewriteRelativeImportExtensions": true + }, + "references": [{ "path": "../../astro/tsconfig.test.json" }] +} diff --git a/packages/telemetry/CHANGELOG.md b/packages/telemetry/CHANGELOG.md index 3824ff52b99f..c9417c1329ea 100644 --- a/packages/telemetry/CHANGELOG.md +++ b/packages/telemetry/CHANGELOG.md @@ -1,5 +1,11 @@ # @astrojs/telemetry +## 3.3.1 + +### Patch Changes + +- [#16257](https://github.com/withastro/astro/pull/16257) [`e0b240e`](https://github.com/withastro/astro/commit/e0b240edea4db632138def3a9003b4b12e12f765) Thanks [@gameroman](https://github.com/gameroman)! - Removed `debug` dependency + ## 3.3.0 ### Minor Changes diff --git a/packages/telemetry/package.json b/packages/telemetry/package.json index dfaa652d6019..daf7277a0a45 100644 --- a/packages/telemetry/package.json +++ b/packages/telemetry/package.json @@ -1,6 +1,6 @@ { "name": "@astrojs/telemetry", - "version": "3.3.0", + "version": "3.3.1", "type": "module", "types": "./dist/index.d.ts", "author": "withastro", @@ -23,7 +23,8 @@ "build": "astro-scripts build \"src/**/*.ts\" && tsc", "build:ci": "astro-scripts build \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"", - "test": "astro-scripts test \"test/**/*.test.js\"" + "test": "astro-scripts test \"test/**/*.test.ts\"", + "typecheck:tests": "tsc --build tsconfig.test.json" }, "files": [ "dist" @@ -38,7 +39,7 @@ }, "devDependencies": { "@types/dlv": "^1.1.5", - "@types/node": "^18.17.8", + "@types/node": "^22.10.6", "@types/which-pm-runs": "^1.0.2", "astro-scripts": "workspace:*" }, diff --git a/packages/telemetry/src/config.ts b/packages/telemetry/src/config.ts index 359b1e11f86a..6ac6f06af17f 100644 --- a/packages/telemetry/src/config.ts +++ b/packages/telemetry/src/config.ts @@ -33,10 +33,12 @@ function getConfigDir(name: string) { } export class GlobalConfig { + private project: ConfigOptions; private dir: string; private file: string; - constructor(private project: ConfigOptions) { + constructor(project: ConfigOptions) { + this.project = project; this.dir = getConfigDir(this.project.name); this.file = path.join(this.dir, 'config.json'); } diff --git a/packages/telemetry/src/index.ts b/packages/telemetry/src/index.ts index c53bf4d73eaf..1941dc7652da 100644 --- a/packages/telemetry/src/index.ts +++ b/packages/telemetry/src/index.ts @@ -25,6 +25,7 @@ interface EventContext extends ProjectInfo { anonymousSessionId: string; } export class AstroTelemetry { + private opts: AstroTelemetryOptions; private _anonymousSessionId: string | undefined; private _anonymousProjectInfo: ProjectInfo | undefined; private config = new GlobalConfig({ name: 'astro' }); @@ -44,7 +45,8 @@ export class AstroTelemetry { return this.env.TELEMETRY_DISABLED; } - constructor(private opts: AstroTelemetryOptions) { + constructor(opts: AstroTelemetryOptions) { + this.opts = opts; // TODO: When the process exits, flush any queued promises // This caused a "cannot exist astro" error when it ran, so it was removed. // process.on('SIGINT', () => this.flush()); diff --git a/packages/telemetry/test/config.test.js b/packages/telemetry/test/config.test.ts similarity index 100% rename from packages/telemetry/test/config.test.js rename to packages/telemetry/test/config.test.ts diff --git a/packages/telemetry/test/index.test.js b/packages/telemetry/test/index.test.ts similarity index 72% rename from packages/telemetry/test/index.test.js rename to packages/telemetry/test/index.test.ts index 2bfc353614de..d2c3f5c95dd3 100644 --- a/packages/telemetry/test/index.test.js +++ b/packages/telemetry/test/index.test.ts @@ -3,19 +3,22 @@ import { after, before, describe, it } from 'node:test'; import { AstroTelemetry } from '../dist/index.js'; function setup() { - const config = new Map(); - const telemetry = new AstroTelemetry({ astroVersion: '0.0.0-test.1', viteVersion: '0.0.0' }); - const logs = []; + const config = new Map(); + const telemetry = new AstroTelemetry({ + astroVersion: '0.0.0-test.1', + viteVersion: '0.0.0', + }); + const logs: unknown[][] = []; // Stub isCI to false so we can test user-facing behavior - telemetry.isCI = false; + telemetry['isCI'] = false; // Stub process.env to properly test in Astro's own CI - telemetry.env = {}; + telemetry['env'] = {}; // Override config so we can inspect it - telemetry.config = config; + telemetry['config'] = config; // Mock the global debug function to capture logs - const originalDebug = globalThis._astroGlobalDebug; - globalThis._astroGlobalDebug = (type, ...args) => { + const originalDebug = (globalThis as any)._astroGlobalDebug; + (globalThis as any)._astroGlobalDebug = (type: string, ...args: unknown[]) => { if (type === 'telemetry') { logs.push(args); } @@ -34,13 +37,13 @@ function setup() { config, logs, cleanup: () => { - globalThis._astroGlobalDebug = originalDebug; + (globalThis as any)._astroGlobalDebug = originalDebug; process.env.DEBUG = oldDebug; }, }; } describe('AstroTelemetry', () => { - let oldCI; + let oldCI: string | undefined; before(() => { oldCI = process.env.CI; // Stub process.env.CI to `false` @@ -60,9 +63,9 @@ describe('AstroTelemetry', () => { const [key] = Array.from(config.keys()); assert.notEqual(key, undefined); assert.equal(config.get(key), false); - assert.equal(telemetry.enabled, false); - assert.equal(telemetry.isDisabled, true); - const result = await telemetry.record(['TEST']); + assert.equal(telemetry['enabled'], false); + assert.equal(telemetry['isDisabled'], true); + const result = await telemetry.record([{ eventName: 'TEST', payload: {} }]); assert.equal(result, undefined); const [log] = logs; assert.notEqual(log, undefined); @@ -75,9 +78,9 @@ describe('AstroTelemetry', () => { const [key] = Array.from(config.keys()); assert.notEqual(key, undefined); assert.equal(config.get(key), true); - assert.equal(telemetry.enabled, true); - assert.equal(telemetry.isDisabled, false); - await telemetry.record(['TEST']); + assert.equal(telemetry['enabled'], true); + assert.equal(telemetry['isDisabled'], false); + await telemetry.record([{ eventName: 'TEST', payload: {} }]); assert.equal(logs.length, 2); cleanup(); }); @@ -87,8 +90,8 @@ describe('AstroTelemetry', () => { const [key] = Array.from(config.keys()); assert.notEqual(key, undefined); assert.equal(config.get(key), false); - assert.equal(telemetry.enabled, false); - assert.equal(telemetry.isDisabled, true); + assert.equal(telemetry['enabled'], false); + assert.equal(telemetry['isDisabled'], true); const [log] = logs; assert.notEqual(log, undefined); assert.match(logs.join(''), /disabled/); @@ -100,8 +103,8 @@ describe('AstroTelemetry', () => { const [key] = Array.from(config.keys()); assert.notEqual(key, undefined); assert.equal(config.get(key), true); - assert.equal(telemetry.enabled, true); - assert.equal(telemetry.isDisabled, false); + assert.equal(telemetry['enabled'], true); + assert.equal(telemetry['isDisabled'], false); const [log] = logs; assert.notEqual(log, undefined); assert.match(logs.join(''), /enabled/); diff --git a/packages/telemetry/tsconfig.test.json b/packages/telemetry/tsconfig.test.json new file mode 100644 index 000000000000..7d6bc4428b35 --- /dev/null +++ b/packages/telemetry/tsconfig.test.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["test/**/*.ts"], + "exclude": ["test/fixtures/**"], + "compilerOptions": { + "allowJs": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "rewriteRelativeImportExtensions": true + }, + "references": [{ "path": "../astro/tsconfig.test.json" }] +} diff --git a/packages/underscore-redirects/CHANGELOG.md b/packages/underscore-redirects/CHANGELOG.md index e04745eabdad..31a2bda5198f 100644 --- a/packages/underscore-redirects/CHANGELOG.md +++ b/packages/underscore-redirects/CHANGELOG.md @@ -1,5 +1,11 @@ # @astrojs/underscore-redirects +## 1.0.3 + +### Patch Changes + +- [#16034](https://github.com/withastro/astro/pull/16034) [`814406d`](https://github.com/withastro/astro/commit/814406de7dc3ea014b47d2d886d55c45e4e1c034) Thanks [@alexanderniebuhr](https://github.com/alexanderniebuhr)! - Fixes generated redirect files to respect Astro’s `trailingSlash` configuration, so redirect routes work with the expected URL format in built output instead of returning a 404 when accessed with a trailing slash. + ## 1.0.2 ### Patch Changes diff --git a/packages/underscore-redirects/package.json b/packages/underscore-redirects/package.json index 6ebb9c468fd3..06b8bedeb06c 100644 --- a/packages/underscore-redirects/package.json +++ b/packages/underscore-redirects/package.json @@ -1,7 +1,7 @@ { "name": "@astrojs/underscore-redirects", "description": "Utilities to generate _redirects files in Astro projects", - "version": "1.0.2", + "version": "1.0.3", "type": "module", "author": "withastro", "license": "MIT", @@ -24,7 +24,8 @@ "build": "astro-scripts build \"src/**/*.ts\" && tsc -p tsconfig.json", "build:ci": "astro-scripts build \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"", - "test": "astro-scripts test \"test/**/*.test.js\"" + "test": "astro-scripts test \"test/**/*.test.ts\"", + "typecheck:tests": "tsc --build tsconfig.test.json" }, "devDependencies": { "astro": "workspace:*", diff --git a/packages/underscore-redirects/src/astro.ts b/packages/underscore-redirects/src/astro.ts index 30ee2ab16037..860a171eedf7 100644 --- a/packages/underscore-redirects/src/astro.ts +++ b/packages/underscore-redirects/src/astro.ts @@ -17,7 +17,7 @@ function getRedirectStatus(route: IntegrationResolvedRoute): ValidRedirectStatus } interface CreateRedirectsFromAstroRoutesParams { - config: Pick; + config: Pick; /** * Maps a `RouteData` to a dynamic target */ @@ -27,6 +27,35 @@ interface CreateRedirectsFromAstroRoutesParams { assets: HookParameters<'astro:build:done'>['assets']; } +/** + * Returns the path(s) to use for a redirect entry based on the trailingSlash config. + * - 'always': ensures the path ends with '/' + * - 'never': ensures the path does not end with '/' + * - 'ignore'(default): returns both with and without trailing slash variants + */ +export function getTrailingSlashPaths( + inputPath: string, + trailingSlash: 'always' | 'never' | 'ignore', +): string[] { + if (inputPath === '/') { + return ['/']; + } + + const hasTrailingSlash = inputPath.endsWith('/'); + const withoutSlash = hasTrailingSlash ? inputPath.slice(0, -1) : inputPath; + const withSlash = hasTrailingSlash ? inputPath : inputPath + '/'; + + switch (trailingSlash) { + case 'always': + return [withSlash]; + case 'never': + return [withoutSlash]; + case 'ignore': + default: + return [withoutSlash, withSlash]; + } +} + /** * Takes a set of routes and creates a Redirects object from them. */ @@ -57,13 +86,20 @@ export function createRedirectsFromAstroRoutes({ // Use `entrypoint` when available to keep trailing slashes in _redirects. const inputPath = route.type === 'redirect' && route.entrypoint ? route.entrypoint : route.pathname; - redirects.add({ - dynamic: false, - input: `${base}${inputPath}`, - target: typeof route.redirect === 'object' ? route.redirect.destination : route.redirect, - status: getRedirectStatus(route), - weight: 2, - }); + + // Generate redirect entries based on trailingSlash config. + const trailingSlash = config.trailingSlash ?? 'ignore'; + const paths = getTrailingSlashPaths(inputPath, trailingSlash); + for (const path of paths) { + redirects.add({ + dynamic: false, + input: `${base}${path}`, + target: + typeof route.redirect === 'object' ? route.redirect.destination : route.redirect, + status: getRedirectStatus(route), + weight: 2, + }); + } continue; } diff --git a/packages/underscore-redirects/src/index.ts b/packages/underscore-redirects/src/index.ts index 8411cc3cabd6..2c6477e7daf7 100644 --- a/packages/underscore-redirects/src/index.ts +++ b/packages/underscore-redirects/src/index.ts @@ -1,6 +1,7 @@ export { createHostedRouteDefinition, createRedirectsFromAstroRoutes, + getTrailingSlashPaths, } from './astro.js'; export { HostRoutes } from './host-route.js'; export { printAsRedirects } from './print.js'; diff --git a/packages/underscore-redirects/test/astro.test.js b/packages/underscore-redirects/test/astro.test.js deleted file mode 100644 index 6a4944dc907a..000000000000 --- a/packages/underscore-redirects/test/astro.test.js +++ /dev/null @@ -1,28 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; -import { createRedirectsFromAstroRoutes } from '../dist/index.js'; - -describe('Astro', () => { - it('Creates a Redirects object from routes', () => { - const routeToDynamicTargetMap = new Map( - Array.from([ - [{ pattern: '/', pathname: '/', segments: [] }, './.adapter/dist/entry.mjs'], - [{ pattern: '/one', pathname: '/one', segments: [] }, './.adapter/dist/entry.mjs'], - ]), - ); - const _redirects = createRedirectsFromAstroRoutes({ - config: { - build: { format: 'directory' }, - }, - routeToDynamicTargetMap, - dir: new URL(import.meta.url), - buildOutput: 'server', - assets: new Map([ - ['/', new URL('./index.html', import.meta.url)], - ['/one', new URL('./one/index.html', import.meta.url)], - ]), - }); - - assert.equal(_redirects.definitions.length, 2); - }); -}); diff --git a/packages/underscore-redirects/test/astro.test.ts b/packages/underscore-redirects/test/astro.test.ts new file mode 100644 index 000000000000..623c7261f502 --- /dev/null +++ b/packages/underscore-redirects/test/astro.test.ts @@ -0,0 +1,60 @@ +import * as assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { createRedirectsFromAstroRoutes, getTrailingSlashPaths } from '../dist/index.js'; +import type { AstroConfig, IntegrationResolvedRoute } from 'astro'; + +describe('Astro', () => { + it('Creates a Redirects object from routes', () => { + const routeToDynamicTargetMap = new Map( + Array.from([ + [createIntegrationRoute('/'), './.adapter/dist/entry.mjs'], + [createIntegrationRoute('/one'), './.adapter/dist/entry.mjs'], + ]), + ); + + const redirects = createRedirectsFromAstroRoutes({ + config: { + build: { format: 'directory' }, + } as AstroConfig, + routeToDynamicTargetMap, + dir: new URL(import.meta.url), + buildOutput: 'server', + assets: new Map([ + ['/', [new URL('./index.html', import.meta.url)]], + ['/one', [new URL('./one/index.html', import.meta.url)]], + ]), + }); + + assert.equal(redirects.definitions.length, 2); + }); + + it('Generates correct paths for root', () => { + assert.deepEqual(getTrailingSlashPaths('/', 'ignore'), ['/']); + assert.deepEqual(getTrailingSlashPaths('/', 'always'), ['/']); + assert.deepEqual(getTrailingSlashPaths('/', 'never'), ['/']); + }); + + it('Generates correct paths for trailingslash ignore', () => { + assert.deepEqual(getTrailingSlashPaths('/path', 'ignore'), ['/path', '/path/']); + assert.deepEqual(getTrailingSlashPaths('/path/', 'ignore'), ['/path', '/path/']); + }); + + it('Generates correct paths for trailingslash always', () => { + assert.deepEqual(getTrailingSlashPaths('/path', 'always'), ['/path/']); + assert.deepEqual(getTrailingSlashPaths('/path/', 'always'), ['/path/']); + }); + + it('Generates correct paths for trailingslash never', () => { + assert.deepEqual(getTrailingSlashPaths('/path', 'never'), ['/path']); + assert.deepEqual(getTrailingSlashPaths('/path/', 'never'), ['/path']); + }); +}); + +function createIntegrationRoute(pattern: string, pathname = pattern): IntegrationResolvedRoute { + const route: Partial = { + pattern, + pathname, + segments: [], + }; + return route as IntegrationResolvedRoute; +} diff --git a/packages/underscore-redirects/test/print.test.js b/packages/underscore-redirects/test/print.test.ts similarity index 100% rename from packages/underscore-redirects/test/print.test.js rename to packages/underscore-redirects/test/print.test.ts diff --git a/packages/underscore-redirects/test/weight.test.js b/packages/underscore-redirects/test/weight.test.ts similarity index 100% rename from packages/underscore-redirects/test/weight.test.js rename to packages/underscore-redirects/test/weight.test.ts diff --git a/packages/underscore-redirects/tsconfig.test.json b/packages/underscore-redirects/tsconfig.test.json new file mode 100644 index 000000000000..7d6bc4428b35 --- /dev/null +++ b/packages/underscore-redirects/tsconfig.test.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["test/**/*.ts"], + "exclude": ["test/fixtures/**"], + "compilerOptions": { + "allowJs": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "rewriteRelativeImportExtensions": true + }, + "references": [{ "path": "../astro/tsconfig.test.json" }] +} diff --git a/packages/upgrade/package.json b/packages/upgrade/package.json index bf1758e4133f..2209c26100f6 100644 --- a/packages/upgrade/package.json +++ b/packages/upgrade/package.json @@ -20,7 +20,8 @@ "build": "astro-scripts build \"src/index.ts\" --bundle && tsc", "build:ci": "astro-scripts build \"src/index.ts\" --bundle", "dev": "astro-scripts dev \"src/**/*.ts\"", - "test": "astro-scripts test \"test/**/*.test.js\"" + "test": "astro-scripts test \"test/**/*.test.ts\"", + "typecheck:tests": "tsc -p tsconfig.test.json" }, "files": [ "dist", diff --git a/packages/upgrade/test/context.test.js b/packages/upgrade/test/context.test.ts similarity index 100% rename from packages/upgrade/test/context.test.js rename to packages/upgrade/test/context.test.ts diff --git a/packages/upgrade/test/install.test.js b/packages/upgrade/test/install.test.ts similarity index 94% rename from packages/upgrade/test/install.test.js rename to packages/upgrade/test/install.test.ts index 23b000758bfc..5a5777b845ae 100644 --- a/packages/upgrade/test/install.test.js +++ b/packages/upgrade/test/install.test.ts @@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'; import { describe, it, mock } from 'node:test'; import { pathToFileURL } from 'node:url'; import { install } from '../dist/index.js'; -import { setup } from './utils.js'; +import { setup, type ShellFunction } from './utils.ts'; const tmpUrl = pathToFileURL(tmpdir()); @@ -70,7 +70,7 @@ describe('install', () => { prompted = true; return { proceed: false }; }, - exit: (code) => { + exit: (code: number) => { exitCode = code; }, packages: [ @@ -100,7 +100,7 @@ describe('install', () => { prompted = true; return { proceed: true }; }, - exit: (code) => { + exit: (code: number) => { exitCode = code; }, packages: [ @@ -130,7 +130,7 @@ describe('install', () => { prompted = true; return { proceed: true }; }, - exit: (code) => { + exit: (code: number) => { exitCode = code; }, packages: [ @@ -172,7 +172,7 @@ describe('install', () => { prompted = true; return { proceed: true }; }, - exit: (code) => { + exit: (code: number) => { exitCode = code; }, packages: [ @@ -215,7 +215,7 @@ describe('install', () => { }); it('npm peer dependency error retry with legacy-peer-deps', async () => { - const mockShell = mock.fn(async () => { + const mockShell = mock.fn(async () => { if (mockShell.mock.callCount() === 0) { // First call fails with peer dependency error throw new Error('npm ERR! peer dependencies conflict'); @@ -230,7 +230,7 @@ describe('install', () => { dryRun: false, cwd: tmpUrl, packageManager: { name: 'npm', agent: 'npm' }, - exit: (code) => { + exit: (code: number) => { exitCode = code; }, packages: [ @@ -259,7 +259,7 @@ describe('install', () => { }); it('npm non-peer dependency error does not retry', async () => { - const mockShell = mock.fn(async () => { + const mockShell = mock.fn(async () => { throw new Error('npm ERR! some other error'); }); @@ -269,7 +269,7 @@ describe('install', () => { dryRun: false, cwd: tmpUrl, packageManager: { name: 'npm', agent: 'npm' }, - exit: (code) => { + exit: (code: number) => { exitCode = code; }, packages: [ @@ -290,7 +290,7 @@ describe('install', () => { }); it('npm peer dependency error retry fails on second attempt', async () => { - const mockShell = mock.fn(async () => { + const mockShell = mock.fn(async () => { // Both calls fail with peer dependency errors throw new Error('npm ERR! peer dependencies conflict'); }); @@ -301,7 +301,7 @@ describe('install', () => { dryRun: false, cwd: tmpUrl, packageManager: { name: 'npm', agent: 'npm' }, - exit: (code) => { + exit: (code: number) => { exitCode = code; }, packages: [ @@ -331,7 +331,7 @@ describe('install', () => { }); it('pnpm peer dependency error does not retry', async () => { - const mockShell = mock.fn(async () => { + const mockShell = mock.fn(async () => { throw new Error('pnpm ERR! peer dependencies conflict'); }); @@ -341,7 +341,7 @@ describe('install', () => { dryRun: false, cwd: tmpUrl, packageManager: { name: 'pnpm', agent: 'pnpm' }, - exit: (code) => { + exit: (code: number) => { exitCode = code; }, packages: [ diff --git a/packages/upgrade/test/utils.js b/packages/upgrade/test/utils.ts similarity index 68% rename from packages/upgrade/test/utils.js rename to packages/upgrade/test/utils.ts index 20063ec53255..b5aa1c25aabc 100644 --- a/packages/upgrade/test/utils.js +++ b/packages/upgrade/test/utils.ts @@ -2,12 +2,21 @@ import { before, beforeEach } from 'node:test'; import { stripVTControlCharacters } from 'node:util'; import { setStdout } from '../dist/index.js'; +export type ShellFunction = ( + command: string, + flags: string[], +) => Promise<{ + stdout: string; + stderr: string; + exitCode: number; +}>; + export function setup() { - const ctx = { messages: [] }; + const ctx: { messages: string[] } = { messages: [] }; before(() => { setStdout( Object.assign({}, process.stdout, { - write(buf) { + write(buf: string | Uint8Array) { ctx.messages.push(stripVTControlCharacters(String(buf)).trim()); return true; }, @@ -25,7 +34,7 @@ export function setup() { length() { return ctx.messages.length; }, - hasMessage(content) { + hasMessage(content: string) { return !!ctx.messages.find((msg) => msg.includes(content)); }, }; diff --git a/packages/upgrade/test/verify.test.js b/packages/upgrade/test/verify.test.ts similarity index 100% rename from packages/upgrade/test/verify.test.js rename to packages/upgrade/test/verify.test.ts diff --git a/packages/upgrade/tsconfig.test.json b/packages/upgrade/tsconfig.test.json new file mode 100644 index 000000000000..7d6bc4428b35 --- /dev/null +++ b/packages/upgrade/tsconfig.test.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["test/**/*.ts"], + "exclude": ["test/fixtures/**"], + "compilerOptions": { + "allowJs": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "rewriteRelativeImportExtensions": true + }, + "references": [{ "path": "../astro/tsconfig.test.json" }] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 127f98557456..79f4ab078724 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,14 +21,14 @@ importers: specifier: ^0.9.5 version: link:packages/language-tools/astro-check '@biomejs/biome': - specifier: 2.4.2 - version: 2.4.2 + specifier: 2.4.10 + version: 2.4.10 '@changesets/changelog-github': specifier: ^0.5.2 version: 0.5.2 '@changesets/cli': specifier: ^2.29.8 - version: 2.29.8(@types/node@18.19.130) + version: 2.29.8(@types/node@22.19.11) '@flue/cli': specifier: ^0.0.47 version: 0.0.47(typescript@5.9.3) @@ -36,8 +36,8 @@ importers: specifier: ^0.0.29 version: 0.0.29(typescript@5.9.3) '@types/node': - specifier: ^18.19.115 - version: 18.19.130 + specifier: ^22.10.6 + version: 22.19.11 bgproc: specifier: ^0.2.0 version: 0.2.0 @@ -52,13 +52,13 @@ importers: version: 3.0.0(eslint@9.39.3(jiti@2.6.1)) knip: specifier: 5.82.1 - version: 5.82.1(@types/node@18.19.130)(typescript@5.9.3) + version: 5.82.1(@types/node@22.19.11)(typescript@5.9.3) only-allow: specifier: ^1.2.2 version: 1.2.2 prettier: specifier: ^3.8.1 - version: 3.8.1 + version: 3.8.2 prettier-plugin-astro: specifier: ^0.14.1 version: 0.14.1 @@ -67,7 +67,7 @@ importers: version: 0.3.17 tinyglobby: specifier: ^0.2.15 - version: 0.2.15 + version: 0.2.16 turbo: specifier: ^2.8.15 version: 2.8.15 @@ -189,7 +189,7 @@ importers: examples/basics: dependencies: astro: - specifier: ^6.1.2 + specifier: ^6.1.8 version: link:../../packages/astro examples/blog: @@ -204,7 +204,7 @@ importers: specifier: ^3.7.2 version: link:../../packages/integrations/sitemap astro: - specifier: ^6.1.2 + specifier: ^6.1.8 version: link:../../packages/astro sharp: specifier: ^0.34.3 @@ -213,16 +213,16 @@ importers: examples/component: devDependencies: astro: - specifier: ^6.1.2 + specifier: ^6.1.8 version: link:../../packages/astro examples/container-with-vitest: dependencies: '@astrojs/react': - specifier: ^5.0.2 + specifier: ^5.0.3 version: link:../../packages/integrations/react astro: - specifier: ^6.1.2 + specifier: ^6.1.8 version: link:../../packages/astro react: specifier: ^18.3.1 @@ -253,22 +253,22 @@ importers: specifier: ^3.15.8 version: 3.15.8 astro: - specifier: ^6.1.2 + specifier: ^6.1.8 version: link:../../packages/astro examples/framework-multiple: dependencies: '@astrojs/preact': - specifier: ^5.1.0 + specifier: ^5.1.1 version: link:../../packages/integrations/preact '@astrojs/react': - specifier: ^5.0.2 + specifier: ^5.0.3 version: link:../../packages/integrations/react '@astrojs/solid-js': specifier: ^6.0.1 version: link:../../packages/integrations/solid '@astrojs/svelte': - specifier: ^8.0.4 + specifier: ^8.0.5 version: link:../../packages/integrations/svelte '@astrojs/vue': specifier: ^6.0.1 @@ -280,7 +280,7 @@ importers: specifier: ^18.3.7 version: 18.3.7(@types/react@18.3.28) astro: - specifier: ^6.1.2 + specifier: ^6.1.8 version: link:../../packages/astro preact: specifier: ^10.28.4 @@ -296,7 +296,7 @@ importers: version: 1.9.11 svelte: specifier: ^5.53.5 - version: 5.54.1 + version: 5.55.3 vue: specifier: ^3.5.29 version: 3.5.30(typescript@5.9.3) @@ -304,13 +304,13 @@ importers: examples/framework-preact: dependencies: '@astrojs/preact': - specifier: ^5.1.0 + specifier: ^5.1.1 version: link:../../packages/integrations/preact '@preact/signals': specifier: ^2.8.1 version: 2.8.2(preact@10.29.0) astro: - specifier: ^6.1.2 + specifier: ^6.1.8 version: link:../../packages/astro preact: specifier: ^10.28.4 @@ -319,7 +319,7 @@ importers: examples/framework-react: dependencies: '@astrojs/react': - specifier: ^5.0.2 + specifier: ^5.0.3 version: link:../../packages/integrations/react '@types/react': specifier: ^18.3.28 @@ -328,7 +328,7 @@ importers: specifier: ^18.3.7 version: 18.3.7(@types/react@18.3.28) astro: - specifier: ^6.1.2 + specifier: ^6.1.8 version: link:../../packages/astro react: specifier: ^18.3.1 @@ -343,7 +343,7 @@ importers: specifier: ^6.0.1 version: link:../../packages/integrations/solid astro: - specifier: ^6.1.2 + specifier: ^6.1.8 version: link:../../packages/astro solid-js: specifier: ^1.9.11 @@ -352,14 +352,14 @@ importers: examples/framework-svelte: dependencies: '@astrojs/svelte': - specifier: ^8.0.4 + specifier: ^8.0.5 version: link:../../packages/integrations/svelte astro: - specifier: ^6.1.2 + specifier: ^6.1.8 version: link:../../packages/astro svelte: specifier: ^5.53.5 - version: 5.54.1 + version: 5.55.3 examples/framework-vue: dependencies: @@ -367,7 +367,7 @@ importers: specifier: ^6.0.1 version: link:../../packages/integrations/vue astro: - specifier: ^6.1.2 + specifier: ^6.1.8 version: link:../../packages/astro vue: specifier: ^3.5.29 @@ -376,49 +376,49 @@ importers: examples/hackernews: dependencies: '@astrojs/node': - specifier: ^10.0.4 + specifier: ^10.0.5 version: link:../../packages/integrations/node astro: - specifier: ^6.1.2 + specifier: ^6.1.8 version: link:../../packages/astro examples/integration: devDependencies: astro: - specifier: ^6.1.2 + specifier: ^6.1.8 version: link:../../packages/astro examples/minimal: dependencies: astro: - specifier: ^6.1.2 + specifier: ^6.1.8 version: link:../../packages/astro examples/portfolio: dependencies: astro: - specifier: ^6.1.2 + specifier: ^6.1.8 version: link:../../packages/astro examples/ssr: dependencies: '@astrojs/node': - specifier: ^10.0.4 + specifier: ^10.0.5 version: link:../../packages/integrations/node '@astrojs/svelte': - specifier: ^8.0.4 + specifier: ^8.0.5 version: link:../../packages/integrations/svelte astro: - specifier: ^6.1.2 + specifier: ^6.1.8 version: link:../../packages/astro svelte: specifier: ^5.53.5 - version: 5.54.1 + version: 5.55.3 examples/starlog: dependencies: astro: - specifier: ^6.1.2 + specifier: ^6.1.8 version: link:../../packages/astro sass: specifier: ^1.97.3 @@ -430,10 +430,10 @@ importers: examples/toolbar-app: devDependencies: '@types/node': - specifier: ^18.17.8 - version: 18.19.130 + specifier: ^22.10.6 + version: 22.19.11 astro: - specifier: ^6.1.2 + specifier: ^6.1.8 version: link:../../packages/astro examples/with-markdoc: @@ -442,7 +442,7 @@ importers: specifier: ^1.0.3 version: link:../../packages/integrations/markdoc astro: - specifier: ^6.1.2 + specifier: ^6.1.8 version: link:../../packages/astro examples/with-mdx: @@ -451,10 +451,10 @@ importers: specifier: ^5.0.3 version: link:../../packages/integrations/mdx '@astrojs/preact': - specifier: ^5.1.0 + specifier: ^5.1.1 version: link:../../packages/integrations/preact astro: - specifier: ^6.1.2 + specifier: ^6.1.8 version: link:../../packages/astro preact: specifier: ^10.28.4 @@ -463,13 +463,13 @@ importers: examples/with-nanostores: dependencies: '@astrojs/preact': - specifier: ^5.1.0 + specifier: ^5.1.1 version: link:../../packages/integrations/preact '@nanostores/preact': specifier: ^1.0.0 version: 1.0.0(nanostores@1.1.1)(preact@10.29.0) astro: - specifier: ^6.1.2 + specifier: ^6.1.8 version: link:../../packages/astro nanostores: specifier: ^1.1.1 @@ -490,7 +490,7 @@ importers: specifier: ^1.9.0 version: 1.9.0 astro: - specifier: ^6.1.2 + specifier: ^6.1.8 version: link:../../packages/astro canvas-confetti: specifier: ^1.9.4 @@ -502,7 +502,7 @@ importers: examples/with-vitest: dependencies: astro: - specifier: ^6.1.2 + specifier: ^6.1.8 version: link:../../packages/astro vitest: specifier: ^4.1.0 @@ -558,9 +558,6 @@ importers: diff: specifier: ^8.0.3 version: 8.0.3 - dlv: - specifier: ^1.1.3 - version: 1.1.3 dset: specifier: ^3.1.4 version: 3.1.4 @@ -641,7 +638,7 @@ importers: version: 1.0.4 tinyglobby: specifier: ^0.2.15 - version: 0.2.15 + version: 0.2.16 tsconfck: specifier: ^3.1.6 version: 3.1.6(typescript@5.9.3) @@ -688,9 +685,6 @@ importers: '@types/aria-query': specifier: ^5.0.4 version: 5.0.4 - '@types/dlv': - specifier: ^1.1.5 - version: 1.1.5 '@types/hast': specifier: ^3.0.4 version: 3.0.4 @@ -703,6 +697,9 @@ importers: '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 + '@types/parse-srcset': + specifier: ^1.0.0 + version: 1.0.0 '@types/picomatch': specifier: ^4.0.2 version: 4.0.2 @@ -725,8 +722,8 @@ importers: specifier: ^1.3.0 version: 1.3.0 fs-fixture: - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.13.0 + version: 2.13.0 mdast-util-mdx: specifier: ^3.0.0 version: 3.0.0 @@ -735,7 +732,7 @@ importers: version: 3.2.0 node-mocks-http: specifier: ^1.17.2 - version: 1.17.2(@types/node@25.2.3) + version: 1.17.2(@types/express@5.0.6)(@types/node@25.2.3) parse-srcset: specifier: ^1.0.2 version: 1.0.2 @@ -940,7 +937,7 @@ importers: version: 1.9.11 svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 vue: specifier: ^3.5.30 version: 3.5.30(typescript@5.9.3) @@ -1015,6 +1012,15 @@ importers: specifier: ^3.5.30 version: 3.5.30(typescript@5.9.3) + packages/astro/e2e/fixtures/cloudflare-node-prerender-hmr: + dependencies: + '@astrojs/cloudflare': + specifier: workspace:* + version: link:../../../../integrations/cloudflare + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/e2e/fixtures/cloudflare/packages/my-lib: {} packages/astro/e2e/fixtures/content-collections: @@ -1138,12 +1144,19 @@ importers: version: 1.9.11 svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 vue: specifier: ^3.5.30 version: 3.5.30(typescript@5.9.3) packages/astro/e2e/fixtures/hmr: + dependencies: + '@astrojs/preact': + specifier: workspace:* + version: link:../../../../integrations/preact + preact: + specifier: ^10.28.2 + version: 10.29.0 devDependencies: astro: specifier: workspace:* @@ -1195,7 +1208,7 @@ importers: version: 1.9.11 svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 vue: specifier: ^3.5.30 version: 3.5.30(typescript@5.9.3) @@ -1251,7 +1264,7 @@ importers: version: 1.9.11 svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 vue: specifier: ^3.5.30 version: 3.5.30(typescript@5.9.3) @@ -1291,7 +1304,7 @@ importers: version: 1.9.11 svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 vue: specifier: ^3.5.30 version: 3.5.30(typescript@5.9.3) @@ -1331,7 +1344,7 @@ importers: version: 1.9.11 svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 vue: specifier: ^3.5.30 version: 3.5.30(typescript@5.9.3) @@ -1371,7 +1384,7 @@ importers: version: 1.9.11 svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 vue: specifier: ^3.5.30 version: 3.5.30(typescript@5.9.3) @@ -1411,7 +1424,7 @@ importers: version: 1.9.11 svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 vue: specifier: ^3.5.30 version: 3.5.30(typescript@5.9.3) @@ -1451,7 +1464,7 @@ importers: version: 1.9.11 svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 vue: specifier: ^3.5.30 version: 3.5.30(typescript@5.9.3) @@ -1669,7 +1682,7 @@ importers: version: link:../../.. svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 packages/astro/e2e/fixtures/tailwindcss: dependencies: @@ -1729,7 +1742,7 @@ importers: version: 1.9.11 svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 vue: specifier: ^3.5.30 version: 3.5.30(typescript@5.9.3) @@ -1873,7 +1886,7 @@ importers: version: 18.3.1(react@18.3.1) svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 vue: specifier: ^3.5.30 version: 3.5.30(typescript@5.9.3) @@ -1894,7 +1907,7 @@ importers: version: link:../../.. svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 packages/astro/test/fixtures/alias-tsconfig: dependencies: @@ -1909,29 +1922,23 @@ importers: version: link:../../.. svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 - packages/astro/test/fixtures/alias-tsconfig-baseurl-only: + packages/astro/test/fixtures/alias-tsconfig-no-baseurl: dependencies: - '@astrojs/svelte': - specifier: workspace:* - version: link:../../../../integrations/svelte astro: specifier: workspace:* version: link:../../.. - svelte: - specifier: ^5.54.0 - version: 5.54.1 - packages/astro/test/fixtures/alias-tsconfig-no-baseurl: + packages/astro/test/fixtures/alias-tsconfig/deps/namespace-package: {} + + packages/astro/test/fixtures/api-routes: dependencies: astro: specifier: workspace:* version: link:../../.. - packages/astro/test/fixtures/alias-tsconfig/deps/namespace-package: {} - - packages/astro/test/fixtures/api-routes: + packages/astro/test/fixtures/asset-query-params-chunks: dependencies: astro: specifier: workspace:* @@ -2052,7 +2059,7 @@ importers: version: 10.29.0 svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 vue: specifier: ^3.5.30 version: 3.5.30(typescript@5.9.3) @@ -2079,7 +2086,7 @@ importers: version: 18.3.1(react@18.3.1) svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 packages/astro/test/fixtures/astro-client-only/pkg: {} @@ -2104,12 +2111,6 @@ importers: specifier: workspace:* version: link:../../.. - packages/astro/test/fixtures/astro-components: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - packages/astro/test/fixtures/astro-cookies: dependencies: astro: @@ -2122,12 +2123,6 @@ importers: specifier: workspace:* version: link:../../.. - packages/astro/test/fixtures/astro-css-bundling-nested-layouts: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - packages/astro/test/fixtures/astro-dev-headers: dependencies: astro: @@ -2171,7 +2166,7 @@ importers: version: 18.3.1(react@18.3.1) svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 packages/astro/test/fixtures/astro-env: dependencies: @@ -2227,30 +2222,6 @@ importers: specifier: ^10.29.0 version: 10.29.0 - packages/astro/test/fixtures/astro-external-files: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - - packages/astro/test/fixtures/astro-fallback: - dependencies: - '@astrojs/preact': - specifier: workspace:* - version: link:../../../../integrations/preact - astro: - specifier: workspace:* - version: link:../../.. - preact: - specifier: ^10.29.0 - version: 10.29.0 - - packages/astro/test/fixtures/astro-generator: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - packages/astro/test/fixtures/astro-get-static-paths: dependencies: astro: @@ -2443,24 +2414,6 @@ importers: specifier: ^4.2.2 version: 4.2.2 - packages/astro/test/fixtures/astro-sitemap-rss: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - - packages/astro/test/fixtures/astro-slot-with-client: - dependencies: - '@astrojs/preact': - specifier: workspace:* - version: link:../../../../integrations/preact - astro: - specifier: workspace:* - version: link:../../.. - preact: - specifier: ^10.29.0 - version: 10.29.0 - packages/astro/test/fixtures/astro-slots: dependencies: astro: @@ -2501,7 +2454,7 @@ importers: version: 1.9.11 svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 vue: specifier: ^3.5.30 version: 3.5.30(typescript@5.9.3) @@ -2618,7 +2571,7 @@ importers: version: 18.3.1(react@18.3.1) svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 packages/astro/test/fixtures/component-library-shared: dependencies: @@ -2633,18 +2586,6 @@ importers: specifier: workspace:* version: link:../../.. - packages/astro/test/fixtures/config-host: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - - packages/astro/test/fixtures/config-path: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - packages/astro/test/fixtures/config-vite: dependencies: astro: @@ -2696,6 +2637,12 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/test/fixtures/content-collection-picture-render: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/test/fixtures/content-collection-references: dependencies: astro: @@ -2726,12 +2673,6 @@ importers: specifier: workspace:* version: link:../../.. - packages/astro/test/fixtures/content-collections-cache-invalidation: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - packages/astro/test/fixtures/content-collections-empty-dir: dependencies: astro: @@ -2762,19 +2703,19 @@ importers: specifier: ^4.3.6 version: 4.3.6 - packages/astro/test/fixtures/content-collections-same-contents: + packages/astro/test/fixtures/content-collections-type-inference: dependencies: astro: specifier: workspace:* version: link:../../.. - packages/astro/test/fixtures/content-collections-type-inference: + packages/astro/test/fixtures/content-collections-with-config-mjs: dependencies: astro: specifier: workspace:* version: link:../../.. - packages/astro/test/fixtures/content-collections-with-config-mjs: + packages/astro/test/fixtures/content-frontmatter: dependencies: astro: specifier: workspace:* @@ -2840,12 +2781,6 @@ importers: specifier: workspace:* version: link:../../.. - packages/astro/test/fixtures/core-image-base: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - packages/astro/test/fixtures/core-image-data-url: dependencies: astro: @@ -2942,12 +2877,6 @@ importers: specifier: ^19.2.4 version: 19.2.4(react@19.2.4) - packages/astro/test/fixtures/core-image-svg-optimized: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - packages/astro/test/fixtures/core-image-unconventional-settings: dependencies: astro: @@ -3002,7 +2931,7 @@ importers: version: link:../../.. svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 packages/astro/test/fixtures/css-deduplication: dependencies: @@ -3049,18 +2978,6 @@ importers: specifier: workspace:* version: link:../../.. - packages/astro/test/fixtures/css-inline-stylesheets-2: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - - packages/astro/test/fixtures/css-inline-stylesheets-3: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - packages/astro/test/fixtures/css-no-code-split: dependencies: astro: @@ -3100,12 +3017,6 @@ importers: specifier: workspace:* version: link:../../.. - packages/astro/test/fixtures/css-order-transparent: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - packages/astro/test/fixtures/custom-404-html: dependencies: astro: @@ -3193,12 +3104,6 @@ importers: specifier: workspace:* version: link:../../.. - packages/astro/test/fixtures/custom-500-middleware: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - packages/astro/test/fixtures/custom-assets-name: dependencies: '@astrojs/node': @@ -3238,6 +3143,30 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/test/fixtures/dev-container: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + + packages/astro/test/fixtures/dev-error-pages: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + + packages/astro/test/fixtures/dev-render: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + + packages/astro/test/fixtures/dev-request-url: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/test/fixtures/dont-delete-me: dependencies: astro: @@ -3256,6 +3185,12 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/test/fixtures/endpoint-routing: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/test/fixtures/entry-file-names: dependencies: '@astrojs/preact': @@ -3292,12 +3227,6 @@ importers: specifier: workspace:* version: link:../../.. - packages/astro/test/fixtures/feature-support-message-suppresion: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - packages/astro/test/fixtures/fetch: dependencies: '@astrojs/preact': @@ -3317,7 +3246,7 @@ importers: version: 10.29.0 svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 vue: specifier: ^3.5.30 version: 3.5.30(typescript@5.9.3) @@ -3352,18 +3281,6 @@ importers: specifier: workspace:* version: link:../../.. - packages/astro/test/fixtures/head-injection: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - - packages/astro/test/fixtures/hmr-css: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - packages/astro/test/fixtures/hmr-markdown: dependencies: astro: @@ -3424,52 +3341,7 @@ importers: specifier: ^10.29.0 version: 10.29.0 - packages/astro/test/fixtures/i18n-routing-base: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - - packages/astro/test/fixtures/i18n-routing-dynamic: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - - packages/astro/test/fixtures/i18n-routing-fallback-rewrite-hybrid: - dependencies: - '@astrojs/node': - specifier: workspace:* - version: link:../../../../integrations/node - astro: - specifier: workspace:* - version: link:../../.. - - packages/astro/test/fixtures/i18n-routing-manual: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - - packages/astro/test/fixtures/i18n-routing-manual-with-default-middleware: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - - packages/astro/test/fixtures/i18n-routing-redirect-preferred-language: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - - packages/astro/test/fixtures/i18n-routing-subdomain: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - - packages/astro/test/fixtures/i18n-server-island: + packages/astro/test/fixtures/i18n-css-leak-basic: dependencies: astro: specifier: workspace:* @@ -3524,7 +3396,7 @@ importers: version: 1.9.11 svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 vue: specifier: ^3.5.30 version: 3.5.30(typescript@5.9.3) @@ -3620,24 +3492,12 @@ importers: specifier: workspace:* version: link:../../.. - packages/astro/test/fixtures/middleware-sequence-request-clone: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - packages/astro/test/fixtures/middleware-sequence-rewrite: dependencies: astro: specifier: workspace:* version: link:../../.. - packages/astro/test/fixtures/middleware-ssg: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - packages/astro/test/fixtures/middleware-tailwind: dependencies: '@tailwindcss/vite': @@ -3650,12 +3510,6 @@ importers: specifier: ^4.2.2 version: 4.2.2 - packages/astro/test/fixtures/middleware-virtual: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - packages/astro/test/fixtures/minification-html: dependencies: astro: @@ -3754,7 +3608,7 @@ importers: version: 1.9.11 svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 vue: specifier: ^3.5.30 version: 3.5.30(typescript@5.9.3) @@ -3981,7 +3835,7 @@ importers: version: link:../../../.. svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 packages/astro/test/fixtures/server-islands/ssr: dependencies: @@ -3996,7 +3850,7 @@ importers: version: link:../../../.. svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 packages/astro/test/fixtures/sessions: dependencies: @@ -4004,12 +3858,6 @@ importers: specifier: workspace:* version: link:../../.. - packages/astro/test/fixtures/set-html: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - packages/astro/test/fixtures/slots-preact: dependencies: '@astrojs/mdx': @@ -4071,7 +3919,7 @@ importers: version: link:../../.. svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 packages/astro/test/fixtures/slots-vue: dependencies: @@ -4172,24 +4020,6 @@ importers: specifier: workspace:* version: link:../../.. - packages/astro/test/fixtures/ssr-env: - dependencies: - '@astrojs/preact': - specifier: workspace:* - version: link:../../../../integrations/preact - astro: - specifier: workspace:* - version: link:../../.. - preact: - specifier: ^10.29.0 - version: 10.29.0 - - packages/astro/test/fixtures/ssr-markdown: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - packages/astro/test/fixtures/ssr-partytown: dependencies: '@astrojs/partytown': @@ -4253,21 +4083,6 @@ importers: specifier: ^10.29.0 version: 10.29.0 - packages/astro/test/fixtures/ssr-split-manifest: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - - packages/astro/test/fixtures/ssr-trailing-slash: - dependencies: - '@astrojs/node': - specifier: workspace:* - version: link:../../../../integrations/node - astro: - specifier: workspace:* - version: link:../../.. - packages/astro/test/fixtures/static-build: dependencies: '@astrojs/preact': @@ -4367,7 +4182,7 @@ importers: version: link:../../.. svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 packages/astro/test/fixtures/svg-deduplication: dependencies: @@ -4399,12 +4214,6 @@ importers: specifier: ^0.8.0 version: 0.8.0(astro@packages+astro) - packages/astro/test/fixtures/unused-slot: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - packages/astro/test/fixtures/url-import-suffix: dependencies: astro: @@ -4466,38 +4275,11 @@ importers: version: link:../../.. svelte: specifier: ^5.54.0 - version: 5.54.1 + version: 5.55.3 vue: specifier: ^3.5.30 version: 3.5.30(typescript@5.9.3) - packages/astro/test/fixtures/with-endpoint-routes: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - - packages/astro/test/fixtures/with-subpath-no-trailing-slash: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - - packages/astro/test/fixtures/without-site-config: - dependencies: - astro: - specifier: workspace:* - version: link:../../.. - - packages/astro/test/units/_temp-fixtures: - dependencies: - '@astrojs/mdx': - specifier: workspace:* - version: link:../../../../integrations/mdx - astro: - specifier: workspace:* - version: link:../../.. - packages/create-astro: dependencies: '@astrojs/cli-kit': @@ -4785,7 +4567,7 @@ importers: version: 0.1.3 tinyglobby: specifier: ^0.2.15 - version: 0.2.15 + version: 0.2.16 vite: specifier: ^7.3.1 version: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3) @@ -4920,6 +4702,26 @@ importers: version: link:../../../../../astro packages/integrations/cloudflare/test/fixtures/prerender-node-env: + dependencies: + '@astrojs/cloudflare': + specifier: workspace:* + version: link:../../.. + '@astrojs/svelte': + specifier: workspace:* + version: link:../../../../svelte + astro: + specifier: workspace:* + version: link:../../../../../astro + fake-svelte-pkg: + specifier: file:./fake-svelte-pkg + version: file:packages/integrations/cloudflare/test/fixtures/prerender-node-env/fake-svelte-pkg(svelte@5.55.3) + svelte: + specifier: ^5.0.0 + version: 5.55.3 + + packages/integrations/cloudflare/test/fixtures/prerender-node-env/fake-svelte-pkg: {} + + packages/integrations/cloudflare/test/fixtures/prerender-queue-consumers: dependencies: '@astrojs/cloudflare': specifier: workspace:* @@ -5048,7 +4850,7 @@ importers: version: link:../../../../../astro svelte: specifier: ^5.53.5 - version: 5.54.1 + version: 5.55.3 packages/integrations/cloudflare/test/fixtures/top-level-return: dependencies: @@ -5099,7 +4901,7 @@ importers: version: 0.34.5 svelte: specifier: ^5.53.5 - version: 5.54.1 + version: 5.55.3 vue: specifier: ^3.5.29 version: 3.5.30(typescript@5.9.3) @@ -5159,7 +4961,7 @@ importers: version: link:../../../../../astro svelte: specifier: ^5.53.5 - version: 5.54.1 + version: 5.55.3 packages/integrations/cloudflare/test/fixtures/with-vue: dependencies: @@ -5550,6 +5352,15 @@ importers: specifier: workspace:* version: link:../../../../../astro + packages/integrations/mdx/test/fixtures/mdx-astro-container-escape: + dependencies: + '@astrojs/mdx': + specifier: workspace:* + version: link:../../.. + astro: + specifier: workspace:* + version: link:../../../../../astro + packages/integrations/mdx/test/fixtures/mdx-frontmatter-injection: dependencies: '@astrojs/mdx': @@ -5710,7 +5521,7 @@ importers: version: 0.27.3 tinyglobby: specifier: ^0.2.15 - version: 0.2.15 + version: 0.2.16 vite: specifier: ^7.3.1 version: 7.3.1(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3) @@ -5804,6 +5615,15 @@ importers: specifier: workspace:* version: link:../../../../../astro + packages/integrations/netlify/test/static/fixtures/image-missing-dimension: + dependencies: + '@astrojs/netlify': + specifier: workspace:* + version: link:../../../.. + astro: + specifier: workspace:* + version: link:../../../../../../astro + packages/integrations/netlify/test/static/fixtures/redirects: dependencies: '@astrojs/netlify': @@ -5834,6 +5654,9 @@ importers: '@fastify/static': specifier: ^9.0.0 version: 9.0.0 + '@types/express': + specifier: ^5.0.6 + version: 5.0.6 '@types/node': specifier: ^22.10.6 version: 22.19.11 @@ -5863,7 +5686,7 @@ importers: version: 5.8.3 node-mocks-http: specifier: ^1.17.2 - version: 1.17.2(@types/node@22.19.11) + version: 1.17.2(@types/express@5.0.6)(@types/node@22.19.11) packages/integrations/node/test/fixtures/api-route: dependencies: @@ -6042,8 +5865,8 @@ importers: packages/integrations/partytown: dependencies: '@qwik.dev/partytown': - specifier: ^0.11.2 - version: 0.11.2 + specifier: ^0.13.2 + version: 0.13.2 mrmime: specifier: ^2.0.1 version: 2.0.1 @@ -6126,6 +5949,21 @@ importers: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + packages/integrations/react/test/fixtures/react-19-preloads: + dependencies: + '@astrojs/react': + specifier: latest + version: link:../../.. + astro: + specifier: latest + version: link:../../../../../astro + react: + specifier: ^19.0.0 + version: 19.2.4 + react-dom: + specifier: ^19.0.0 + version: 19.2.4(react@19.2.4) + packages/integrations/react/test/fixtures/react-component: dependencies: '@astrojs/react': @@ -6249,13 +6087,16 @@ importers: dependencies: '@sveltejs/vite-plugin-svelte': specifier: ^6.2.4 - version: 6.2.4(svelte@5.54.1)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) + version: 6.2.4(svelte@5.55.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) svelte2tsx: specifier: ^0.7.52 - version: 0.7.52(svelte@5.54.1)(typescript@5.9.3) + version: 0.7.52(svelte@5.55.3)(typescript@5.9.3) vite: specifier: ^7.3.1 version: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3) + vitefu: + specifier: ^1.1.2 + version: 1.1.2(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) devDependencies: astro: specifier: workspace:* @@ -6268,7 +6109,7 @@ importers: version: 1.2.0 svelte: specifier: ^5.54.1 - version: 5.54.1 + version: 5.55.3 packages/integrations/svelte/test/fixtures/async-rendering: dependencies: @@ -6280,7 +6121,7 @@ importers: version: link:../../../../../astro svelte: specifier: ^5.54.1 - version: 5.54.1 + version: 5.55.3 packages/integrations/svelte/test/fixtures/conditional-rendering: dependencies: @@ -6292,7 +6133,7 @@ importers: version: link:../../../../../astro svelte: specifier: ^5.54.1 - version: 5.54.1 + version: 5.55.3 packages/integrations/svelte/test/fixtures/empty-class: dependencies: @@ -6304,7 +6145,7 @@ importers: version: link:../../../../../astro svelte: specifier: ^5.17.1 - version: 5.54.1 + version: 5.55.3 packages/integrations/svelte/test/fixtures/prop-types: dependencies: @@ -6316,7 +6157,7 @@ importers: version: link:../../../../../astro svelte: specifier: ^5.54.1 - version: 5.54.1 + version: 5.55.3 packages/integrations/vercel: dependencies: @@ -6325,7 +6166,7 @@ importers: version: link:../../internal-helpers '@vercel/analytics': specifier: ^1.6.1 - version: 1.6.1(react@19.2.4)(svelte@5.54.1)(vue@3.5.30(typescript@5.9.3)) + version: 1.6.1(react@19.2.4)(svelte@5.55.3)(vue@3.5.30(typescript@5.9.3)) '@vercel/functions': specifier: ^3.4.3 version: 3.4.3 @@ -6340,7 +6181,7 @@ importers: version: 0.27.3 tinyglobby: specifier: ^0.2.15 - version: 0.2.15 + version: 0.2.16 devDependencies: astro: specifier: workspace:* @@ -6703,8 +6544,8 @@ importers: specifier: workspace:* version: link:../../../scripts tinyglobby: - specifier: ^0.2.15 - version: 0.2.15 + specifier: ^0.2.16 + version: 0.2.16 tsx: specifier: ^4.21.0 version: 4.21.0 @@ -6736,8 +6577,8 @@ importers: specifier: ^0.4.1 version: 0.4.1 tinyglobby: - specifier: ^0.2.15 - version: 0.2.15 + specifier: ^0.2.16 + version: 0.2.16 volar-service-css: specifier: 0.0.70 version: 0.0.70(@volar/language-service@2.4.28) @@ -6749,7 +6590,7 @@ importers: version: 0.0.70(@volar/language-service@2.4.28) volar-service-prettier: specifier: 0.0.70 - version: 0.0.70(@volar/language-service@2.4.28)(prettier@3.8.1) + version: 0.0.70(@volar/language-service@2.4.28)(prettier@3.8.2) volar-service-typescript: specifier: 0.0.70 version: 0.0.70(@volar/language-service@2.4.28) @@ -6803,12 +6644,12 @@ importers: specifier: workspace:* version: link:../../../astro svelte: - specifier: ^5.54.1 - version: 5.54.1 + specifier: ^5.55.3 + version: 5.55.3 devDependencies: tinyglobby: - specifier: ^0.2.15 - version: 0.2.15 + specifier: ^0.2.16 + version: 0.2.16 packages/language-tools/language-server/test/fixture: devDependencies: @@ -6871,8 +6712,8 @@ importers: specifier: ^2.13.1 version: 2.13.1 prettier: - specifier: ^3.8.1 - version: 3.8.1 + specifier: ^3.8.2 + version: 3.8.2 prettier-plugin-astro: specifier: ^0.14.1 version: 0.14.1 @@ -6923,8 +6764,8 @@ importers: specifier: ^11.7.5 version: 11.7.5 ovsx: - specifier: ^0.10.9 - version: 0.10.9 + specifier: ^0.10.10 + version: 0.10.10 vscode-languageclient: specifier: ^9.0.1 version: 9.0.1 @@ -7061,8 +6902,8 @@ importers: specifier: ^1.1.5 version: 1.1.5 '@types/node': - specifier: ^18.17.8 - version: 18.19.130 + specifier: ^22.10.6 + version: 22.19.11 '@types/which-pm-runs': specifier: ^1.0.2 version: 1.0.2 @@ -7117,7 +6958,7 @@ importers: version: 0.1.3 tinyglobby: specifier: ^0.2.15 - version: 0.2.15 + version: 0.2.16 tsconfck: specifier: ^3.1.6 version: 3.1.6(typescript@5.9.3) @@ -7475,59 +7316,59 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@biomejs/biome@2.4.2': - resolution: {integrity: sha512-vVE/FqLxNLbvYnFDYg3Xfrh1UdFhmPT5i+yPT9GE2nTUgI4rkqo5krw5wK19YHBd7aE7J6r91RRmb8RWwkjy6w==} + '@biomejs/biome@2.4.10': + resolution: {integrity: sha512-xxA3AphFQ1geij4JTHXv4EeSTda1IFn22ye9LdyVPoJU19fNVl0uzfEuhsfQ4Yue/0FaLs2/ccVi4UDiE7R30w==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.4.2': - resolution: {integrity: sha512-3pEcKCP/1POKyaZZhXcxFl3+d9njmeAihZ17k8lL/1vk+6e0Cbf0yPzKItFiT+5Yh6TQA4uKvnlqe0oVZwRxCA==} + '@biomejs/cli-darwin-arm64@2.4.10': + resolution: {integrity: sha512-vuzzI1cWqDVzOMIkYyHbKqp+AkQq4K7k+UCXWpkYcY/HDn1UxdsbsfgtVpa40shem8Kax4TLDLlx8kMAecgqiw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.4.2': - resolution: {integrity: sha512-P7hK1jLVny+0R9UwyGcECxO6sjETxfPyBm/1dmFjnDOHgdDPjPqozByunrwh4xPKld8sxOr5eAsSqal5uKgeBg==} + '@biomejs/cli-darwin-x64@2.4.10': + resolution: {integrity: sha512-14fzASRo+BPotwp7nWULy2W5xeUyFnTaq1V13Etrrxkrih+ez/2QfgFm5Ehtf5vSjtgx/IJycMMpn5kPd5ZNaA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.4.2': - resolution: {integrity: sha512-/x04YK9+7erw6tYEcJv9WXoBHcULI/wMOvNdAyE9S3JStZZ9yJyV67sWAI+90UHuDo/BDhq0d96LDqGlSVv7WA==} + '@biomejs/cli-linux-arm64-musl@2.4.10': + resolution: {integrity: sha512-WrJY6UuiSD/Dh+nwK2qOTu8kdMDlLV3dLMmychIghHPAysWFq1/DGC1pVZx8POE3ZkzKR3PUUnVrtZfMfaJjyQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [musl] - '@biomejs/cli-linux-arm64@2.4.2': - resolution: {integrity: sha512-DI3Mi7GT2zYNgUTDEbSjl3e1KhoP76OjQdm8JpvZYZWtVDRyLd3w8llSr2TWk1z+U3P44kUBWY3X7H9MD1/DGQ==} + '@biomejs/cli-linux-arm64@2.4.10': + resolution: {integrity: sha512-7MH1CMW5uuxQ/s7FLST63qF8B3Hgu2HRdZ7tA1X1+mk+St4JOuIrqdhIBnnyqeyWJNI+Bww7Es5QZ0wIc1Cmkw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] libc: [glibc] - '@biomejs/cli-linux-x64-musl@2.4.2': - resolution: {integrity: sha512-wbBmTkeAoAYbOQ33f6sfKG7pcRSydQiF+dTYOBjJsnXO2mWEOQHllKlC2YVnedqZFERp2WZhFUoO7TNRwnwEHQ==} + '@biomejs/cli-linux-x64-musl@2.4.10': + resolution: {integrity: sha512-kDTi3pI6PBN6CiczsWYOyP2zk0IJI08EWEQyDMQWW221rPaaEz6FvjLhnU07KMzLv8q3qSuoB93ua6inSQ55Tw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [musl] - '@biomejs/cli-linux-x64@2.4.2': - resolution: {integrity: sha512-GK2ErnrKpWFigYP68cXiCHK4RTL4IUWhK92AFS3U28X/nuAL5+hTuy6hyobc8JZRSt+upXt1nXChK+tuHHx4mA==} + '@biomejs/cli-linux-x64@2.4.10': + resolution: {integrity: sha512-tZLvEEi2u9Xu1zAqRjTcpIDGVtldigVvzug2fTuPG0ME/g8/mXpRPcNgLB22bGn6FvLJpHHnqLnwliOu8xjYrg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] libc: [glibc] - '@biomejs/cli-win32-arm64@2.4.2': - resolution: {integrity: sha512-k2uqwLYrNNxnaoiW3RJxoMGnbKda8FuCmtYG3cOtVljs3CzWxaTR+AoXwKGHscC9thax9R4kOrtWqWN0+KdPTw==} + '@biomejs/cli-win32-arm64@2.4.10': + resolution: {integrity: sha512-umwQU6qPzH+ISTf/eHyJ/QoQnJs3V9Vpjz2OjZXe9MVBZ7prgGafMy7yYeRGnlmDAn87AKTF3Q6weLoMGpeqdQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.4.2': - resolution: {integrity: sha512-9ma7C4g8Sq3cBlRJD2yrsHXB1mnnEBdpy7PhvFrylQWQb4PoyCmPucdX7frvsSBQuFtIiKCrolPl/8tCZrKvgQ==} + '@biomejs/cli-win32-x64@2.4.10': + resolution: {integrity: sha512-aW/JU5GuyH4uxMrNYpoC2kjaHlyJGLgIa3XkhPEZI0uKhZhJZU8BuEyJmvgzSPQNGozBwWjC972RaNdcJ9KyJg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -9607,8 +9448,8 @@ packages: resolution: {integrity: sha512-HDVTWq3H0uTXiU0eeSQntcVUTPP3GamzeXI41+x7uU9J65JgWQh3qWZHblR1i0npXfFtF+mxBiU2nJH8znxWnQ==} engines: {node: '>=18'} - '@qwik.dev/partytown@0.11.2': - resolution: {integrity: sha512-795y49CqBiKiwKAD+QBZlzlqEK275hVcazZ7wBPSfgC23L+vWuA7PJmMpgxojOucZHzYi5rAAQ+IP1I3BKVZxw==} + '@qwik.dev/partytown@0.13.2': + resolution: {integrity: sha512-Umls4bSkuzqLVcGvf8OgwIn/OldproSAbaQ/iYGe8VPYBpl2CaOSxabWwkeC72LDFqxVL0b0q8XlI8MuChDyzg==} engines: {node: '>=18.0.0'} hasBin: true @@ -10040,12 +9881,18 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/canvas-confetti@1.9.0': resolution: {integrity: sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==} '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -10061,6 +9908,12 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -10070,6 +9923,9 @@ packages: '@types/http-cache-semantics@4.2.0': resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -10118,9 +9974,6 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@18.19.130': - resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} - '@types/node@20.19.33': resolution: {integrity: sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==} @@ -10136,6 +9989,9 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/parse-srcset@1.0.0': + resolution: {integrity: sha512-5ehc1s3aIQJ05ZVAmYoqDUN8zvlrIZvRUfYP+2bhGNzrY5RQBeJS9JO9jpkVm/vSOW/MkRYLArS/fhXY5cgXPg==} + '@types/picomatch@4.0.2': resolution: {integrity: sha512-qHHxQ+P9PysNEGbALT8f8YOSHW0KJu6l2xU8DYY0fu/EmGxXdVnuTLvFUvBgPJMSqXq29SYHveejeAha+4AYgA==} @@ -10145,6 +10001,12 @@ packages: '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + '@types/qs@6.15.0': + resolution: {integrity: sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react-dom@18.3.7': resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} peerDependencies: @@ -10168,6 +10030,9 @@ packages: '@types/send@1.2.1': resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/server-destroy@1.0.4': resolution: {integrity: sha512-+x8oAQ4Xp1wtDi2Hlmi7gUNXZNVhB5EoSQpi0qEmINdDN5Ab724WLGAalEdT1SudVY/NzMhbfZO7vU+klT0R+A==} @@ -10616,6 +10481,7 @@ packages: '@xmldom/xmldom@0.9.8': resolution: {integrity: sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==} engines: {node: '>=14.6'} + deprecated: this version has critical issues, please update to the latest version abbrev@3.0.1: resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} @@ -11977,6 +11843,11 @@ packages: engines: {node: '>= 10.17.0'} hasBin: true + fake-svelte-pkg@file:packages/integrations/cloudflare/test/fixtures/prerender-node-env/fake-svelte-pkg: + resolution: {directory: packages/integrations/cloudflare/test/fixtures/prerender-node-env/fake-svelte-pkg, type: directory} + peerDependencies: + svelte: ^5.0.0 + fast-decode-uri-component@1.0.1: resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} @@ -12178,8 +12049,8 @@ packages: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} - fs-fixture@2.11.0: - resolution: {integrity: sha512-elzOu5Ru04qPSBT344kngxx1bpq3RbpznEyjTcn+NHI2nvzwDcGt2zde/a6LBmF5SJtgSYBGHAPnel6S1IefeA==} + fs-fixture@2.13.0: + resolution: {integrity: sha512-bqL4EVFNgoA38OnztLfeHn4NZJ32zWnSNA3ALtessYO0WjpL//QuYl1YkYd7j+TY0cLO6cqgoHxPJpfSwKQAPA==} engines: {node: '>=18.0.0'} fs.realpath@1.0.0: @@ -13691,8 +13562,8 @@ packages: outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} - ovsx@0.10.9: - resolution: {integrity: sha512-gY6912U50YzzNdAEFr9IxAqu59pKySXZzJUxzHRzi3/h/fWFdDDFCCXyjik6VL4TmiVKeor1Yv/cg7I3KfOUuQ==} + ovsx@0.10.10: + resolution: {integrity: sha512-/X5J4VLKPUGGaMynW9hgvsGg9jmwsK/3RhODeA2yzdeDbb8PUSNcg5GQ9aPDJW/znlqNvAwQcXAyE+Cq0RRvAQ==} engines: {node: '>= 20'} hasBin: true @@ -14153,8 +14024,8 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - prettier@3.8.1: - resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + prettier@3.8.2: + resolution: {integrity: sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==} engines: {node: '>=14'} hasBin: true @@ -14939,8 +14810,8 @@ packages: svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 typescript: ^4.9.4 || ^5.0.0 - svelte@5.54.1: - resolution: {integrity: sha512-ow8tncN097Ty8U1H+C3bM1xNlsCbnO2UZeN0lWBnv8f3jKho7QTTQ2LWbMXrPQDodLjH91n4kpNnLolyRhVE6A==} + svelte@5.55.3: + resolution: {integrity: sha512-dS1N+i3bA1v+c4UDb750MlN5vCO82G6vxh8HeTsPsTdJ1BLsN1zxSyDlIdBBqUjqZ/BxEwM8UrFf98aaoVnZFQ==} engines: {node: '>=18'} svgo@3.3.3: @@ -15034,8 +14905,8 @@ packages: resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} engines: {node: '>=18'} - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} tinyrainbow@3.0.3: @@ -15229,9 +15100,6 @@ packages: underscore@1.13.7: resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==} - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -16479,39 +16347,39 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@biomejs/biome@2.4.2': + '@biomejs/biome@2.4.10': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.4.2 - '@biomejs/cli-darwin-x64': 2.4.2 - '@biomejs/cli-linux-arm64': 2.4.2 - '@biomejs/cli-linux-arm64-musl': 2.4.2 - '@biomejs/cli-linux-x64': 2.4.2 - '@biomejs/cli-linux-x64-musl': 2.4.2 - '@biomejs/cli-win32-arm64': 2.4.2 - '@biomejs/cli-win32-x64': 2.4.2 + '@biomejs/cli-darwin-arm64': 2.4.10 + '@biomejs/cli-darwin-x64': 2.4.10 + '@biomejs/cli-linux-arm64': 2.4.10 + '@biomejs/cli-linux-arm64-musl': 2.4.10 + '@biomejs/cli-linux-x64': 2.4.10 + '@biomejs/cli-linux-x64-musl': 2.4.10 + '@biomejs/cli-win32-arm64': 2.4.10 + '@biomejs/cli-win32-x64': 2.4.10 - '@biomejs/cli-darwin-arm64@2.4.2': + '@biomejs/cli-darwin-arm64@2.4.10': optional: true - '@biomejs/cli-darwin-x64@2.4.2': + '@biomejs/cli-darwin-x64@2.4.10': optional: true - '@biomejs/cli-linux-arm64-musl@2.4.2': + '@biomejs/cli-linux-arm64-musl@2.4.10': optional: true - '@biomejs/cli-linux-arm64@2.4.2': + '@biomejs/cli-linux-arm64@2.4.10': optional: true - '@biomejs/cli-linux-x64-musl@2.4.2': + '@biomejs/cli-linux-x64-musl@2.4.10': optional: true - '@biomejs/cli-linux-x64@2.4.2': + '@biomejs/cli-linux-x64@2.4.10': optional: true - '@biomejs/cli-win32-arm64@2.4.2': + '@biomejs/cli-win32-arm64@2.4.10': optional: true - '@biomejs/cli-win32-x64@2.4.2': + '@biomejs/cli-win32-x64@2.4.10': optional: true '@bluwy/giget-core@0.1.6': @@ -16559,7 +16427,7 @@ snapshots: transitivePeerDependencies: - encoding - '@changesets/cli@2.29.8(@types/node@18.19.130)': + '@changesets/cli@2.29.8(@types/node@22.19.11)': dependencies: '@changesets/apply-release-plan': 7.0.14 '@changesets/assemble-release-plan': 6.0.9 @@ -16575,7 +16443,7 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@changesets/write': 0.4.0 - '@inquirer/external-editor': 1.0.3(@types/node@18.19.130) + '@inquirer/external-editor': 1.0.3(@types/node@22.19.11) '@manypkg/get-packages': 1.1.3 ansi-colors: 4.1.3 ci-info: 3.9.0 @@ -17670,12 +17538,12 @@ snapshots: '@import-maps/resolve@2.0.0': {} - '@inquirer/external-editor@1.0.3(@types/node@18.19.130)': + '@inquirer/external-editor@1.0.3(@types/node@22.19.11)': dependencies: chardet: 2.1.1 iconv-lite: 0.7.2 optionalDependencies: - '@types/node': 18.19.130 + '@types/node': 22.19.11 '@isaacs/cliui@8.0.2': dependencies: @@ -18546,7 +18414,7 @@ snapshots: '@publint/pack@0.1.4': {} - '@qwik.dev/partytown@0.11.2': + '@qwik.dev/partytown@0.13.2': dependencies: dotenv: 16.6.1 @@ -18801,20 +18669,20 @@ snapshots: dependencies: acorn: 8.16.0 - '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.54.1)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.54.1)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))': + '@sveltejs/vite-plugin-svelte-inspector@5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.54.1)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) + '@sveltejs/vite-plugin-svelte': 6.2.4(svelte@5.55.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) obug: 2.1.1 - svelte: 5.54.1 + svelte: 5.55.3 vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3) - '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.54.1)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))': + '@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.54.1)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.54.1)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) + '@sveltejs/vite-plugin-svelte-inspector': 5.0.2(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.55.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.3)(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.1 - svelte: 5.54.1 + svelte: 5.55.3 vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3) vitefu: 1.1.2(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3)) @@ -18949,6 +18817,11 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 25.2.3 + '@types/canvas-confetti@1.9.0': {} '@types/chai@5.2.3': @@ -18956,6 +18829,10 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/connect@3.4.38': + dependencies: + '@types/node': 25.2.3 + '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -18970,6 +18847,19 @@ snapshots: '@types/estree@1.0.8': {} + '@types/express-serve-static-core@5.1.1': + dependencies: + '@types/node': 25.2.3 + '@types/qs': 6.15.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -18978,6 +18868,8 @@ snapshots: '@types/http-cache-semantics@4.2.0': {} + '@types/http-errors@2.0.5': {} + '@types/istanbul-lib-coverage@2.0.6': {} '@types/js-yaml@4.0.9': {} @@ -19022,10 +18914,6 @@ snapshots: '@types/node@12.20.55': {} - '@types/node@18.19.130': - dependencies: - undici-types: 5.26.5 - '@types/node@20.19.33': dependencies: undici-types: 6.21.0 @@ -19044,12 +18932,18 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/parse-srcset@1.0.0': {} + '@types/picomatch@4.0.2': {} '@types/prismjs@1.26.6': {} '@types/prop-types@15.7.15': {} + '@types/qs@6.15.0': {} + + '@types/range-parser@1.2.7': {} + '@types/react-dom@18.3.7(@types/react@18.3.28)': dependencies: '@types/react': 18.3.28 @@ -19065,17 +18959,22 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 18.19.130 + '@types/node': 25.2.3 '@types/semver@7.7.1': {} '@types/send@1.2.1': dependencies: - '@types/node': 18.19.130 + '@types/node': 25.2.3 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 25.2.3 '@types/server-destroy@1.0.4': dependencies: - '@types/node': 18.19.130 + '@types/node': 25.2.3 '@types/triple-beam@1.3.5': {} @@ -19093,11 +18992,11 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 18.19.130 + '@types/node': 25.2.3 '@types/xml2js@0.4.14': dependencies: - '@types/node': 18.19.130 + '@types/node': 25.2.3 '@types/yargs-parser@21.0.3': {} @@ -19107,7 +19006,7 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 18.19.130 + '@types/node': 25.2.3 optional: true '@typescript-eslint/eslint-plugin@8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3)': @@ -19181,7 +19080,7 @@ snapshots: debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.4 semver: 7.7.4 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -19224,10 +19123,10 @@ snapshots: dependencies: valibot: 1.2.0(typescript@5.9.3) - '@vercel/analytics@1.6.1(react@19.2.4)(svelte@5.54.1)(vue@3.5.30(typescript@5.9.3))': + '@vercel/analytics@1.6.1(react@19.2.4)(svelte@5.55.3)(vue@3.5.30(typescript@5.9.3))': optionalDependencies: react: 19.2.4 - svelte: 5.54.1 + svelte: 5.55.3 vue: 3.5.30(typescript@5.9.3) '@vercel/functions@3.4.3': @@ -21143,6 +21042,10 @@ snapshots: transitivePeerDependencies: - supports-color + fake-svelte-pkg@file:packages/integrations/cloudflare/test/fixtures/prerender-node-env/fake-svelte-pkg(svelte@5.55.3): + dependencies: + svelte: 5.55.3 + fast-decode-uri-component@1.0.1: {} fast-deep-equal@3.1.3: {} @@ -21366,7 +21269,7 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 - fs-fixture@2.11.0: {} + fs-fixture@2.13.0: {} fs.realpath@1.0.0: {} @@ -22113,10 +22016,10 @@ snapshots: kleur@4.1.5: {} - knip@5.82.1(@types/node@18.19.130)(typescript@5.9.3): + knip@5.82.1(@types/node@22.19.11)(typescript@5.9.3): dependencies: '@nodelib/fs.walk': 1.2.8 - '@types/node': 18.19.130 + '@types/node': 22.19.11 fast-glob: 3.3.3 formatly: 0.3.0 jiti: 2.6.1 @@ -23110,7 +23013,7 @@ snapshots: node-mock-http@1.0.4: {} - node-mocks-http@1.17.2(@types/node@22.19.11): + node-mocks-http@1.17.2(@types/express@5.0.6)(@types/node@22.19.11): dependencies: accepts: 1.3.8 content-disposition: 0.5.4 @@ -23123,9 +23026,10 @@ snapshots: range-parser: 1.2.1 type-is: 1.6.18 optionalDependencies: + '@types/express': 5.0.6 '@types/node': 22.19.11 - node-mocks-http@1.17.2(@types/node@25.2.3): + node-mocks-http@1.17.2(@types/express@5.0.6)(@types/node@25.2.3): dependencies: accepts: 1.3.8 content-disposition: 0.5.4 @@ -23138,6 +23042,7 @@ snapshots: range-parser: 1.2.1 type-is: 1.6.18 optionalDependencies: + '@types/express': 5.0.6 '@types/node': 25.2.3 node-releases@2.0.27: {} @@ -23270,7 +23175,7 @@ snapshots: outdent@0.5.0: {} - ovsx@0.10.9: + ovsx@0.10.10: dependencies: '@vscode/vsce': 3.7.1 commander: 6.2.1 @@ -23826,12 +23731,12 @@ snapshots: prettier-plugin-astro@0.14.1: dependencies: '@astrojs/compiler': 2.13.1 - prettier: 3.8.1 + prettier: 3.8.2 sass-formatter: 0.7.9 prettier@2.8.8: {} - prettier@3.8.1: {} + prettier@3.8.2: {} pretty-bytes@5.6.0: {} @@ -24784,14 +24689,14 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte2tsx@0.7.52(svelte@5.54.1)(typescript@5.9.3): + svelte2tsx@0.7.52(svelte@5.55.3)(typescript@5.9.3): dependencies: dedent-js: 1.0.1 scule: 1.3.0 - svelte: 5.54.1 + svelte: 5.55.3 typescript: 5.9.3 - svelte@5.54.1: + svelte@5.55.3: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 @@ -24924,7 +24829,7 @@ snapshots: tinyexec@1.0.4: {} - tinyglobby@0.2.15: + tinyglobby@0.2.16: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 @@ -25088,8 +24993,6 @@ snapshots: underscore@1.13.7: {} - undici-types@5.26.5: {} - undici-types@6.21.0: {} undici-types@7.16.0: {} @@ -25441,7 +25344,7 @@ snapshots: picomatch: 4.0.4 postcss: 8.5.8 rollup: 4.59.1 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 optionalDependencies: '@types/node': 25.2.3 fsevents: 2.3.3 @@ -25458,7 +25361,7 @@ snapshots: picomatch: 4.0.4 postcss: 8.5.8 rollup: 4.59.1 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 optionalDependencies: '@types/node': 22.19.11 fsevents: 2.3.3 @@ -25475,7 +25378,7 @@ snapshots: picomatch: 4.0.4 postcss: 8.5.8 rollup: 4.59.1 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 optionalDependencies: '@types/node': 25.2.3 fsevents: 2.3.3 @@ -25511,7 +25414,7 @@ snapshots: std-env: 4.0.0 tinybench: 2.9.0 tinyexec: 1.0.4 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 tinyrainbow: 3.0.3 vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 @@ -25556,12 +25459,12 @@ snapshots: optionalDependencies: '@volar/language-service': 2.4.28 - volar-service-prettier@0.0.70(@volar/language-service@2.4.28)(prettier@3.8.1): + volar-service-prettier@0.0.70(@volar/language-service@2.4.28)(prettier@3.8.2): dependencies: vscode-uri: 3.1.0 optionalDependencies: '@volar/language-service': 2.4.28 - prettier: 3.8.1 + prettier: 3.8.2 volar-service-typescript-twoslash-queries@0.0.70(@volar/language-service@2.4.28): dependencies: @@ -25808,7 +25711,7 @@ snapshots: '@vscode/l10n': 0.0.18 ajv: 8.18.0 ajv-draft-04: 1.0.0(ajv@8.18.0) - prettier: 3.8.1 + prettier: 3.8.2 request-light: 0.5.8 vscode-json-languageservice: 4.1.8 vscode-languageserver: 9.0.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b0d8f3026bdf..f4591e2626b3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -48,6 +48,10 @@ minimumReleaseAgeExclude: - smol-toml@1.6.1 # Renovate security update: picomatch@4.0.4 - picomatch@4.0.4 + # Smoke test dependency (docs site) + - astro-og-canvas@0.11.0 + # @types/node@24.12.2 published <3 days ago + - '@types/node@24.12.2' peerDependencyRules: allowAny: - 'astro' diff --git a/scripts/jsconfig.json b/scripts/jsconfig.json deleted file mode 100644 index 0caa704a7a8e..000000000000 --- a/scripts/jsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "declaration": true, - "strict": true, - "module": "esnext", - "moduleResolution": "nodenext", - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "target": "esnext" - } -} diff --git a/scripts/testing/github-test-reporter.js b/scripts/testing/github-test-reporter.js index 38f1c813bca0..0351b6fd5d92 100644 --- a/scripts/testing/github-test-reporter.js +++ b/scripts/testing/github-test-reporter.js @@ -20,7 +20,7 @@ export default new Transform({ file: event.data.file, line: event.data.line, column: event.data.column, - isSuite: event.data.details.type !== 'test', + isSuite: event.data.details.type === 'suite', }); break; } diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 000000000000..00ed7dd5d654 --- /dev/null +++ b/scripts/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "allowJs": true + }, + "references": [ + { + "path": "../.github/scripts/tsconfig.json" + } + ] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 7ed082d83639..1600925b31ce 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -13,6 +13,8 @@ "stripInternal": true, "noUnusedLocals": true, "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "outDir": "${configDir}/node_modules/.cache/tsconfig/out", "types": ["node"] } } From cafec4e23365061491103dfce2e889a15cf86f27 Mon Sep 17 00:00:00 2001 From: Chris Swithinbank Date: Wed, 22 Apr 2026 18:34:04 +0200 Subject: [PATCH 04/72] Update to Vite 8 (#15819) --- .changeset/common-pears-drive.md | 17 + .changeset/fix-dev-port-vite-restart.md | 5 + .github/workflows/ci.yml | 2 +- packages/astro/e2e/actions-blog.test.ts | 3 + packages/astro/e2e/astro-component.test.ts | 39 +- packages/astro/e2e/client-only.test.ts | 3 +- packages/astro/e2e/cloudflare.test.ts | 6 +- packages/astro/e2e/csp-client-only.test.ts | 3 +- packages/astro/e2e/errors.test.ts | 5 +- .../e2e/fixtures/client-only/astro.config.mjs | 5 +- .../client-only/src/pages/index.astro | 7 +- .../fixtures/cloudflare/src/pages/index.astro | 3 +- .../e2e/fixtures/errors/astro.config.mjs | 5 +- .../src/pages/astro-client-media-error.astro | 5 +- .../src/pages/astro-hydration-error.astro | 5 +- .../src/pages/svelte-runtime-error.astro | 5 +- .../src/pages/svelte-syntax-error.astro | 5 +- .../multiple-frameworks/astro.config.mjs | 5 +- .../multiple-frameworks/src/pages/index.astro | 7 +- .../nested-in-preact/astro.config.mjs | 15 +- .../nested-in-preact/src/pages/index.astro | 5 +- .../fixtures/nested-in-react/astro.config.mjs | 5 +- .../nested-in-react/src/pages/index.astro | 5 +- .../fixtures/nested-in-solid/astro.config.mjs | 5 +- .../nested-in-solid/src/pages/index.astro | 5 +- .../nested-in-svelte/astro.config.mjs | 5 +- .../fixtures/nested-in-vue/astro.config.mjs | 5 +- .../nested-in-vue/src/pages/index.astro | 5 +- .../nested-recursive/astro.config.mjs | 5 +- .../nested-recursive/src/pages/index.astro | 9 +- .../svelte-component/astro.config.mjs | 5 +- .../view-transitions/astro.config.mjs | 14 +- .../src/pages/client-only-four.astro | 5 +- .../src/pages/client-only-three.astro | 5 +- .../src/pages/island-svelte-one.astro | 5 +- .../src/pages/island-svelte-two.astro | 5 +- packages/astro/e2e/hmr.test.ts | 16 +- .../astro/e2e/multiple-frameworks.test.ts | 6 +- packages/astro/e2e/nested-in-preact.test.ts | 2 +- packages/astro/e2e/nested-in-react.test.ts | 2 +- packages/astro/e2e/nested-in-solid.test.ts | 2 +- packages/astro/e2e/nested-in-svelte.test.ts | 114 ++-- packages/astro/e2e/nested-in-vue.test.ts | 2 +- packages/astro/e2e/nested-recursive.test.ts | 3 +- packages/astro/e2e/react-component.test.ts | 1 + packages/astro/e2e/svelte-component.test.ts | 12 +- packages/astro/e2e/test-utils.ts | 28 +- packages/astro/e2e/view-transitions.test.ts | 17 +- .../astro/e2e/vite-virtual-modules.test.ts | 5 +- packages/astro/package.json | 3 +- packages/astro/playwright.config.js | 1 + packages/astro/src/assets/utils/assets.ts | 14 +- packages/astro/src/assets/utils/node.ts | 6 +- .../astro/src/assets/vite-plugin-assets.ts | 2 +- packages/astro/src/content/runtime-assets.ts | 6 +- packages/astro/src/content/utils.ts | 5 +- .../content/vite-plugin-content-imports.ts | 5 +- ...-rollup-input.ts => add-rolldown-input.ts} | 10 +- packages/astro/src/core/build/graph.ts | 18 +- .../src/core/build/plugins/plugin-analyzer.ts | 2 +- .../build/plugins/plugin-component-entry.ts | 10 +- .../src/core/build/plugins/plugin-css.ts | 19 +- .../core/build/plugins/plugin-internals.ts | 2 +- .../core/build/plugins/plugin-prerender.ts | 2 +- packages/astro/src/core/build/static-build.ts | 96 ++-- packages/astro/src/core/build/util.ts | 12 +- packages/astro/src/core/create-vite.ts | 2 + packages/astro/src/core/errors/dev/utils.ts | 8 +- .../src/core/head-propagation/comment.ts | 13 - .../astro/src/core/head-propagation/hint.ts | 10 + packages/astro/src/core/logger/vite.ts | 4 +- .../astro/src/core/middleware/vite-plugin.ts | 6 +- packages/astro/src/types/public/content.ts | 6 +- .../astro/src/types/public/integrations.ts | 4 +- .../src/vite-plugin-adapter-config/index.ts | 2 +- .../astro/src/vite-plugin-astro/compile-rs.ts | 4 +- .../astro/src/vite-plugin-astro/compile.ts | 26 +- packages/astro/src/vite-plugin-astro/index.ts | 7 +- packages/astro/src/vite-plugin-head/index.ts | 30 +- .../astro/src/vite-plugin-hmr-reload/index.ts | 29 +- .../index.ts | 7 +- packages/astro/src/vite-plugin-pages/util.ts | 2 +- packages/astro/templates/content/module.mjs | 2 +- .../astro/test/asset-query-params.test.js | 7 +- .../test/astro-component-bundling.test.ts | 2 +- .../astro/test/astro-css-bundling.test.ts | 2 +- .../astro/test/config-vite-css-target.test.js | 2 +- .../test/content-collections-render.test.ts | 3 +- .../test/core-image-svg-in-island.test.ts | 1 + ...core-image-unconventional-settings.test.ts | 12 +- packages/astro/test/core-image.test.ts | 4 +- .../astro/test/csp-server-islands.test.js | 2 +- packages/astro/test/csp.test.js | 14 +- packages/astro/test/css-deduplication.test.ts | 2 +- packages/astro/test/css-order.test.ts | 5 +- packages/astro/test/entry-file-names.test.js | 2 +- packages/astro/test/env-public.test.js | 8 +- packages/astro/test/env-secret.test.js | 5 +- .../fixtures/config-vite/astro.config.mjs | 2 +- .../css-deduplication/astro.config.mjs | 7 +- .../css-order-import/src/components/Two.astro | 2 +- .../src/pages/component.astro | 2 +- .../css-order-import/src/styles/One.css | 2 +- .../css-order-transparent/astro.config.mjs | 3 + .../css-order-transparent/package.json | 7 + .../src/components/Item.astro | 17 + .../src/pages/index.astro | 17 + .../custom-assets-name/astro.config.mjs | 4 +- .../entry-file-names/astro.config.mjs | 2 +- .../server-entry/fake-adapter/index.js | 4 +- packages/astro/test/hoisted-imports.test.js | 4 +- packages/astro/test/non-html-pages.test.js | 2 +- packages/astro/test/queue-rendering.test.js | 4 +- .../test/reuse-injected-entrypoint.test.ts | 4 +- packages/astro/test/serializeManifest.test.ts | 7 +- packages/astro/test/server-islands.test.ts | 21 +- packages/astro/test/ssr-script.test.ts | 20 +- packages/astro/test/streaming.test.js | 35 ++ packages/astro/test/test-adapter.js | 2 + packages/astro/test/test-utils.js | 4 +- .../render/head-propagation/comment.test.ts | 31 +- .../units/vite-plugin-astro/compile.test.ts | 13 +- packages/db/package.json | 2 +- packages/integrations/alpinejs/package.json | 2 +- packages/integrations/cloudflare/package.json | 2 +- packages/integrations/cloudflare/src/index.ts | 8 +- .../cloudflare/test/svelte-rune-deps.test.ts | 18 +- .../markdoc/components/Renderer.astro | 2 +- packages/integrations/markdoc/package.json | 2 +- .../markdoc/src/content-entry-type.ts | 6 +- .../markdoc/test/propagated-assets.test.ts | 2 +- packages/integrations/mdx/package.json | 2 +- .../mdx/src/rehype-optimize-static.ts | 2 +- .../integrations/mdx/src/vite-plugin-mdx.ts | 2 +- packages/integrations/netlify/package.json | 2 +- .../test/functions/skew-protection.test.ts | 35 +- packages/integrations/preact/package.json | 2 +- packages/integrations/react/package.json | 2 +- packages/integrations/react/src/index.ts | 14 +- packages/integrations/solid/package.json | 2 +- packages/integrations/svelte/package.json | 2 +- packages/integrations/vercel/package.json | 2 +- packages/integrations/vue/package.json | 2 +- .../language-server/test/setup.js | 4 +- pnpm-lock.yaml | 533 ++++++++++++------ pnpm-workspace.yaml | 3 + 146 files changed, 1141 insertions(+), 627 deletions(-) create mode 100644 .changeset/common-pears-drive.md create mode 100644 .changeset/fix-dev-port-vite-restart.md rename packages/astro/src/core/build/{add-rollup-input.ts => add-rolldown-input.ts} (79%) delete mode 100644 packages/astro/src/core/head-propagation/comment.ts create mode 100644 packages/astro/src/core/head-propagation/hint.ts create mode 100644 packages/astro/test/fixtures/css-order-transparent/astro.config.mjs create mode 100644 packages/astro/test/fixtures/css-order-transparent/package.json create mode 100644 packages/astro/test/fixtures/css-order-transparent/src/components/Item.astro create mode 100644 packages/astro/test/fixtures/css-order-transparent/src/pages/index.astro diff --git a/.changeset/common-pears-drive.md b/.changeset/common-pears-drive.md new file mode 100644 index 000000000000..7f85b20b8d59 --- /dev/null +++ b/.changeset/common-pears-drive.md @@ -0,0 +1,17 @@ +--- +'@astrojs/cloudflare': major +'@astrojs/alpinejs': major +'@astrojs/markdoc': major +'@astrojs/netlify': major +'@astrojs/preact': major +'@astrojs/svelte': major +'@astrojs/vercel': major +'@astrojs/react': major +'@astrojs/solid-js': major +'@astrojs/mdx': major +'@astrojs/vue': major +'astro': major +'@astrojs/db': major +--- + +Upgrade to Vite v8 diff --git a/.changeset/fix-dev-port-vite-restart.md b/.changeset/fix-dev-port-vite-restart.md new file mode 100644 index 000000000000..18990d8de08a --- /dev/null +++ b/.changeset/fix-dev-port-vite-restart.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes `--port` flag being ignored after a Vite-triggered server restart (e.g. when a `.env` file changes) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 632c12e85ba8..750cc19224f8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -332,7 +332,7 @@ jobs: # For a pull_request event merging into the `next` branch (`base_ref`), use the `v6` branch. # NOTE: For a pull_request event, the `ref_name` is something like `/merge` than the branch name. # NOTE: Perhaps docs repo should use a consistent `next` branch in the future. - ref: ${{ (github.ref_name == 'next' || github.base_ref == 'next') && 'v6' || 'main' }} + ref: ${{ (github.ref_name == 'next' || github.base_ref == 'next') && 'main' || 'main' }} - name: Install dependencies run: pnpm install --no-frozen-lockfile diff --git a/packages/astro/e2e/actions-blog.test.ts b/packages/astro/e2e/actions-blog.test.ts index d23281405dca..5a0efc5e070d 100644 --- a/packages/astro/e2e/actions-blog.test.ts +++ b/packages/astro/e2e/actions-blog.test.ts @@ -113,6 +113,7 @@ test.describe('Astro Actions - Blog', () => { await page.goto(astro.resolveUrl('/blog/first-post/?commentPostIdOverride=bogus')); const form = page.getByTestId('client'); + await waitForHydrate(page, form); const authorInput = form.locator('input[name="author"]'); const bodyInput = form.locator('textarea[name="body"]'); await authorInput.fill('Ben'); @@ -130,6 +131,7 @@ test.describe('Astro Actions - Blog', () => { await page.goto(astro.resolveUrl('/blog/first-post/')); const form = page.getByTestId('client'); + await waitForHydrate(page, form); const authorInput = form.locator('input[name="author"]'); const bodyInput = form.locator('textarea[name="body"]'); @@ -170,6 +172,7 @@ test.describe('Astro Actions - Blog', () => { await page.goto(astro.resolveUrl('/apply')); const form = page.getByTestId('apply-form'); + await waitForHydrate(page, form); const nameInput = form.locator('input[name="name"]'); const emailInput = form.locator('input[name="email"]'); diff --git a/packages/astro/e2e/astro-component.test.ts b/packages/astro/e2e/astro-component.test.ts index 360aad439fdb..76f33b801614 100644 --- a/packages/astro/e2e/astro-component.test.ts +++ b/packages/astro/e2e/astro-component.test.ts @@ -1,10 +1,16 @@ -import { expect } from '@playwright/test'; +import { type Page, expect } from '@playwright/test'; import { type DevServer, testFactory } from './test-utils.ts'; const test = testFactory(import.meta.url, { root: './fixtures/astro-component/' }); let devServer: DevServer; +async function waitForViteToSettle(page: Page) { + // Headless Chrome can trigger one immediate follow-up load after the initial Vite connection. + // Wait for that to clear before asserting whether an edit caused a reload. + await page.waitForTimeout(500); +} + test.beforeAll(async ({ astro }) => { devServer = await astro.startDevServer(); }); @@ -16,6 +22,7 @@ test.afterAll(async () => { test.describe('Astro component HMR', () => { test('component styles', async ({ page, astro }) => { await page.goto(astro.resolveUrl('/')); + await waitForViteToSettle(page); const hero = page.locator('section'); await expect(hero, 'hero has background: white').toHaveCSS( @@ -43,6 +50,7 @@ test.describe('Astro component HMR', () => { await page.goto(astro.resolveUrl('/')); await initialLog; + await waitForViteToSettle(page); const el = page.locator('#hoisted-script'); expect(await el.innerText()).toContain('Hoisted success'); @@ -68,6 +76,7 @@ test.describe('Astro component HMR', () => { await page.goto(astro.resolveUrl('/')); await initialLog; + await waitForViteToSettle(page); const updatedLog = page.waitForEvent( 'console', @@ -84,29 +93,23 @@ test.describe('Astro component HMR', () => { test('update linked dep Astro html', async ({ page, astro }) => { await page.goto(astro.resolveUrl('/')); - let h1 = page.locator('#astro-linked-lib'); + await waitForViteToSettle(page); + const h1 = page.locator('#astro-linked-lib'); expect(await h1.textContent()).toBe('astro-linked-lib'); - await Promise.all([ - page.waitForLoadState('networkidle'), - await astro.editFile('../_deps/astro-linked-lib/Component.astro', (content) => - content.replace('>astro-linked-lib<', '>astro-linked-lib-update<'), - ), - ]); - h1 = page.locator('#astro-linked-lib'); - expect(await h1.textContent()).toBe('astro-linked-lib-update'); + await astro.editFile('../_deps/astro-linked-lib/Component.astro', (content) => + content.replace('>astro-linked-lib<', '>astro-linked-lib-update<'), + ); + await expect(h1).toHaveText('astro-linked-lib-update'); }); test('update linked dep Astro style', async ({ page, astro }) => { await page.goto(astro.resolveUrl('/')); - let h1 = page.locator('#astro-linked-lib'); + await waitForViteToSettle(page); + const h1 = page.locator('#astro-linked-lib'); await expect(h1).toHaveCSS('color', 'rgb(255, 0, 0)'); - await Promise.all([ - page.waitForLoadState('networkidle'), - await astro.editFile('../_deps/astro-linked-lib/Component.astro', (content) => - content.replace('color: red', 'color: green'), - ), - ]); - h1 = page.locator('#astro-linked-lib'); + await astro.editFile('../_deps/astro-linked-lib/Component.astro', (content) => + content.replace('color: red', 'color: green'), + ); await expect(h1).toHaveCSS('color', 'rgb(0, 128, 0)'); }); }); diff --git a/packages/astro/e2e/client-only.test.ts b/packages/astro/e2e/client-only.test.ts index 72bbcce86581..27aa78553100 100644 --- a/packages/astro/e2e/client-only.test.ts +++ b/packages/astro/e2e/client-only.test.ts @@ -98,7 +98,8 @@ test.describe('Client only', () => { await expect(count, 'count incremented by 1').toHaveText('1'); }); - test('Svelte counter', async ({ astro, page }) => { + // TODO: Re-enable once Svelte is compatible with Vite v8 + test.skip('Svelte counter', async ({ astro, page }) => { await page.goto(astro.resolveUrl('/')); const counter = await page.locator('#svelte-counter'); diff --git a/packages/astro/e2e/cloudflare.test.ts b/packages/astro/e2e/cloudflare.test.ts index 52de7cb616f0..ea76401fb7d0 100644 --- a/packages/astro/e2e/cloudflare.test.ts +++ b/packages/astro/e2e/cloudflare.test.ts @@ -5,6 +5,7 @@ import { type PreviewServer, createLoggerSpy, testFactory, + warmupDevServer, } from './test-utils.ts'; type LogEntry = { label: string; message: unknown }; @@ -79,7 +80,7 @@ function sharedTests(testRunner: AstroTest, infoLogs: LogEntry[] | null = null) }); testRunner('server island with props', async ({ page, astro }) => { - await page.goto(astro.resolveUrl('/')); + await page.goto(astro.resolveUrl('/?with-island-props=1')); const islandProps = page.locator('#island-props'); await expect(islandProps).toContainText('Aria'); }); @@ -148,10 +149,11 @@ test.describe('Cloudflare', () => { let devServer: DevServer; const infoLogs: LogEntry[] = []; - test.beforeAll(async ({ astro }) => { + test.beforeAll(async ({ astro, browser }) => { const logger = createLoggerSpy({ info: infoLogs }); // @ts-expect-error `logger` is an @internal option stripped from the public type devServer = await astro.startDevServer({ logger }); + await warmupDevServer(browser, astro.resolveUrl('/')); }); test.afterAll(async () => { diff --git a/packages/astro/e2e/csp-client-only.test.ts b/packages/astro/e2e/csp-client-only.test.ts index 09b9396d42cb..ee6e845d552f 100644 --- a/packages/astro/e2e/csp-client-only.test.ts +++ b/packages/astro/e2e/csp-client-only.test.ts @@ -103,7 +103,8 @@ test.describe('CSP Client only', () => { await expect(count, 'count incremented by 1').toHaveText('1'); }); - test('Svelte counter', async ({ astro, page }) => { + // TODO: Re-enable once Svelte is compatible with Vite v8 + test.skip('Svelte counter', async ({ astro, page }) => { await page.goto(astro.resolveUrl('/')); const counter = await page.locator('#svelte-counter'); diff --git a/packages/astro/e2e/errors.test.ts b/packages/astro/e2e/errors.test.ts index 29ac011640c9..707a0f6f214e 100644 --- a/packages/astro/e2e/errors.test.ts +++ b/packages/astro/e2e/errors.test.ts @@ -25,7 +25,7 @@ test.describe('Error display', () => { await page.goto(astro.resolveUrl('/astro-syntax-error'), { waitUntil: 'networkidle' }); const message = (await getErrorOverlayContent(page)).message; - expect(message).toMatch('Unexpected "while"'); + expect(message).toMatch(/Unexpected ("while"|token)/); await Promise.all([ // Wait for page reload @@ -98,7 +98,8 @@ test.describe('Error display', () => { expect(fileLocation).toMatch(/^pages\/astro-sass-error.astro/); }); - test('framework errors recover when fixed', async ({ page, astro }) => { + // TODO: Re-enable once Svelte is compatible with Vite v8 + test.skip('framework errors recover when fixed', async ({ page, astro }) => { await page.goto(astro.resolveUrl('/svelte-syntax-error'), { waitUntil: 'networkidle' }); const message = (await getErrorOverlayContent(page)).message; diff --git a/packages/astro/e2e/fixtures/client-only/astro.config.mjs b/packages/astro/e2e/fixtures/client-only/astro.config.mjs index b837d96a5e5e..01ffd2c6d37d 100644 --- a/packages/astro/e2e/fixtures/client-only/astro.config.mjs +++ b/packages/astro/e2e/fixtures/client-only/astro.config.mjs @@ -1,7 +1,8 @@ import preact from '@astrojs/preact'; import react from '@astrojs/react'; import solid from '@astrojs/solid-js'; -import svelte from '@astrojs/svelte'; +// TODO: Re-enable once Svelte is compatible with Vite v8 +// import svelte from '@astrojs/svelte'; import vue from '@astrojs/vue'; import { defineConfig } from 'astro/config'; @@ -12,7 +13,7 @@ export default defineConfig({ react({ include: ['**/react/*'] }), preact({ include: ['**/preact/*'] }), solid({ include: ['**/solid/*'] }), - svelte(), + // svelte(), vue(), ], }); diff --git a/packages/astro/e2e/fixtures/client-only/src/pages/index.astro b/packages/astro/e2e/fixtures/client-only/src/pages/index.astro index 003636e1777a..0933bdee24b2 100644 --- a/packages/astro/e2e/fixtures/client-only/src/pages/index.astro +++ b/packages/astro/e2e/fixtures/client-only/src/pages/index.astro @@ -2,7 +2,8 @@ import { PreactCounter } from '../components/preact/PreactCounter.jsx'; import * as react from '../components/react/ReactCounter.jsx'; import SolidCounter from '../components/solid/SolidCounter.jsx'; -import SvelteCounter from '../components/svelte/SvelteCounter.svelte'; +// TODO: Re-enable once Svelte is compatible with Vite v8 +// import SvelteCounter from '../components/svelte/SvelteCounter.svelte'; import VueCounter from '../components/vue/VueCounter.vue'; // Full Astro Component Syntax: @@ -37,10 +38,10 @@ import VueCounter from '../components/vue/VueCounter.vue';

    Loading Vue...

    - + diff --git a/packages/astro/e2e/fixtures/cloudflare/src/pages/index.astro b/packages/astro/e2e/fixtures/cloudflare/src/pages/index.astro index 9d4662d0e244..bd7f147c10ce 100644 --- a/packages/astro/e2e/fixtures/cloudflare/src/pages/index.astro +++ b/packages/astro/e2e/fixtures/cloudflare/src/pages/index.astro @@ -19,6 +19,7 @@ const increment = await getEntry('increment', 'value'); const { Content } = await render(increment); const surname = Astro.url.searchParams.get('surname'); +const showIslandProps = surname !== null || Astro.url.searchParams.has('with-island-props'); --- @@ -56,7 +57,7 @@ const surname = Astro.url.searchParams.get('surname');
    - + {showIslandProps ? : null}
    Vue - Svelte +

    client-only-three

    diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/island-svelte-one.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/island-svelte-one.astro index c60dfa9c9d10..094a1cc6a3c3 100644 --- a/packages/astro/e2e/fixtures/view-transitions/src/pages/island-svelte-one.astro +++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/island-svelte-one.astro @@ -1,5 +1,6 @@ --- -import Counter from '../components/SvelteCounter.svelte'; +// TODO: Re-enable once Svelte is compatible with Vite v8 +// import Counter from '../components/SvelteCounter.svelte'; import Layout from '../components/Layout.astro'; export const prerender = false; @@ -7,5 +8,5 @@ export const prerender = false;

    Page 1

    go to 2 - + diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/island-svelte-two.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/island-svelte-two.astro index 63222bacbc39..e2e0de2e63a5 100644 --- a/packages/astro/e2e/fixtures/view-transitions/src/pages/island-svelte-two.astro +++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/island-svelte-two.astro @@ -1,5 +1,6 @@ --- -import Counter from '../components/SvelteCounter.svelte'; +// TODO: Re-enable once Svelte is compatible with Vite v8 +// import Counter from '../components/SvelteCounter.svelte'; import Layout from '../components/Layout.astro'; export const prerender = false; @@ -7,5 +8,5 @@ export const prerender = false;

    Page 2

    go to 1 - +
    diff --git a/packages/astro/e2e/hmr.test.ts b/packages/astro/e2e/hmr.test.ts index 54e586229ed5..b30cce5c6bed 100644 --- a/packages/astro/e2e/hmr.test.ts +++ b/packages/astro/e2e/hmr.test.ts @@ -1,4 +1,4 @@ -import { expect } from '@playwright/test'; +import { type Page, expect } from '@playwright/test'; import { type DevServer, testFactory } from './test-utils.ts'; const test = testFactory(import.meta.url, { @@ -14,6 +14,12 @@ function throwPageShouldNotReload() { throw new Error('Page should not reload in HMR'); } +async function waitForViteToSettle(page: Page) { + // Headless Chrome can trigger one immediate follow-up load after the initial Vite connection. + // Wait for that to clear before asserting whether an edit caused a reload. + await page.waitForTimeout(500); +} + test.beforeAll(async ({ astro }) => { devServer = await astro.startDevServer(); }); @@ -29,6 +35,7 @@ test.afterAll(async () => { test.describe('Scripts with dependencies', () => { test('refresh with HMR', async ({ page, astro }) => { await page.goto(astro.resolveUrl('/script-dep')); + await waitForViteToSettle(page); const h = page.locator('h1'); await expect(h, 'original text set').toHaveText('before'); @@ -44,6 +51,7 @@ test.describe('Scripts with dependencies', () => { test.describe('Styles', () => { test('dependencies cause refresh with HMR', async ({ page, astro }) => { await page.goto(astro.resolveUrl('/css-dep')); + await waitForViteToSettle(page); page.once('load', throwPageShouldNotReload); @@ -57,6 +65,7 @@ test.describe('Styles', () => { test('external CSS refresh with HMR', async ({ page, astro }) => { await page.goto(astro.resolveUrl('/css-external')); + await waitForViteToSettle(page); page.once('load', throwPageShouldNotReload); @@ -72,6 +81,7 @@ test.describe('Styles', () => { test('inline styles refresh with HMR', async ({ page, astro }) => { await page.goto(astro.resolveUrl('/css-inline')); + await waitForViteToSettle(page); page.once('load', throwPageShouldNotReload); @@ -101,6 +111,7 @@ test.describe('Styles', () => { test('SCSS modules refresh with HMR', async ({ page, astro }) => { await page.goto(astro.resolveUrl('/scss-module')); + await waitForViteToSettle(page); page.once('load', throwPageShouldNotReload); @@ -116,6 +127,7 @@ test.describe('Styles', () => { test('added style tag refresh with full-reload', async ({ page, astro }) => { await page.goto(astro.resolveUrl('/css-inline-component')); + await waitForViteToSettle(page); const h = page.locator('h1.title-with-no-color'); await expect(h).toHaveCSS('color', 'rgb(0, 0, 0)'); @@ -129,6 +141,7 @@ test.describe('Styles', () => { test('multiple added style tags refresh with full-reload', async ({ page, astro }) => { await page.goto(astro.resolveUrl('/css-inline-component')); + await waitForViteToSettle(page); const h = page.locator('h1.title-with-color'); await expect(h).toHaveCSS('color', 'rgb(0, 0, 255)'); @@ -143,6 +156,7 @@ test.describe('Styles', () => { test('removed style tag refresh with full-reload', async ({ page, astro }) => { await page.goto(astro.resolveUrl('/css-inline-component')); + await waitForViteToSettle(page); const h = page.locator('h1.title-with-color'); await expect(h).toHaveCSS('color', 'rgb(0, 0, 255)'); diff --git a/packages/astro/e2e/multiple-frameworks.test.ts b/packages/astro/e2e/multiple-frameworks.test.ts index 4929074c2751..b32c4aa8c84a 100644 --- a/packages/astro/e2e/multiple-frameworks.test.ts +++ b/packages/astro/e2e/multiple-frameworks.test.ts @@ -74,7 +74,8 @@ test.skip('Multiple frameworks', () => { await expect(count, 'count incremented by 1').toHaveText('1'); }); - test('Svelte counter', async ({ page, astro }) => { + // TODO: Re-enable once Svelte is compatible with Vite v8 + test.skip('Svelte counter', async ({ page, astro }) => { await page.goto(astro.resolveUrl('/')); const counter = page.locator('#svelte-counter'); @@ -167,7 +168,8 @@ test.skip('Multiple frameworks', () => { await expect(count, 'initial count updated to 5').toHaveText('5'); }); - test('Svelte component', async ({ astro, page }) => { + // TODO: Re-enable once Svelte is compatible with Vite v8 + test.skip('Svelte component', async ({ astro, page }) => { await page.goto(astro.resolveUrl('/')); const count = page.locator('#svelte-counter pre'); diff --git a/packages/astro/e2e/nested-in-preact.test.ts b/packages/astro/e2e/nested-in-preact.test.ts index 9ca90cb81101..f8cc749e5925 100644 --- a/packages/astro/e2e/nested-in-preact.test.ts +++ b/packages/astro/e2e/nested-in-preact.test.ts @@ -82,7 +82,7 @@ test.describe('Nested Frameworks in Preact', () => { await expect(count, 'count incremented by 1').toHaveText('1'); }); - test('Svelte counter', async ({ astro, page }) => { + test.skip('Svelte counter', async ({ astro, page }) => { await page.goto(astro.resolveUrl('/')); const counter = page.locator('#svelte-counter'); diff --git a/packages/astro/e2e/nested-in-react.test.ts b/packages/astro/e2e/nested-in-react.test.ts index 7c189de3318e..d4a233b62095 100644 --- a/packages/astro/e2e/nested-in-react.test.ts +++ b/packages/astro/e2e/nested-in-react.test.ts @@ -98,7 +98,7 @@ test.describe('Nested Frameworks in React', () => { await expect(count, 'count incremented by 1').toHaveText('1'); }); - test('Svelte counter', async ({ astro, page }) => { + test.skip('Svelte counter', async ({ astro, page }) => { await page.goto(astro.resolveUrl('/')); const counter = page.locator('#svelte-counter'); diff --git a/packages/astro/e2e/nested-in-solid.test.ts b/packages/astro/e2e/nested-in-solid.test.ts index 52ffa8229c58..0d3e44c3f1f4 100644 --- a/packages/astro/e2e/nested-in-solid.test.ts +++ b/packages/astro/e2e/nested-in-solid.test.ts @@ -82,7 +82,7 @@ test.describe('Nested Frameworks in Solid', () => { await expect(count, 'count incremented by 1').toHaveText('1'); }); - test('Svelte counter', async ({ astro, page }) => { + test.skip('Svelte counter', async ({ astro, page }) => { await page.goto(astro.resolveUrl('/')); const counter = page.locator('#svelte-counter'); diff --git a/packages/astro/e2e/nested-in-svelte.test.ts b/packages/astro/e2e/nested-in-svelte.test.ts index c57a8479612e..08ba4b459cd1 100644 --- a/packages/astro/e2e/nested-in-svelte.test.ts +++ b/packages/astro/e2e/nested-in-svelte.test.ts @@ -13,89 +13,91 @@ test.afterAll(async () => { await devServer.stop(); }); -test.describe('Nested Frameworks in Svelte', () => { - test('React counter', async ({ astro, page }) => { - await page.goto(astro.resolveUrl('/')); +// TODO: Re-enable once Svelte is compatible with Vite v8 +test.describe + .skip('Nested Frameworks in Svelte', () => { + test('React counter', async ({ astro, page }) => { + await page.goto(astro.resolveUrl('/')); - const counter = page.locator('#react-counter'); - await expect(counter, 'component is visible').toBeVisible(); + const counter = page.locator('#react-counter'); + await expect(counter, 'component is visible').toBeVisible(); - const count = counter.locator('#react-counter-count'); - await expect(count, 'initial count is 0').toHaveText('0'); + const count = counter.locator('#react-counter-count'); + await expect(count, 'initial count is 0').toHaveText('0'); - await waitForHydrate(page, counter); + await waitForHydrate(page, counter); - const increment = counter.locator('#react-counter-increment'); - await increment.click(); + const increment = counter.locator('#react-counter-increment'); + await increment.click(); - await expect(count, 'count incremented by 1').toHaveText('1'); - }); + await expect(count, 'count incremented by 1').toHaveText('1'); + }); - test('Preact counter', async ({ astro, page }) => { - await page.goto(astro.resolveUrl('/')); + test('Preact counter', async ({ astro, page }) => { + await page.goto(astro.resolveUrl('/')); - const counter = page.locator('#preact-counter'); - await expect(counter, 'component is visible').toBeVisible(); + const counter = page.locator('#preact-counter'); + await expect(counter, 'component is visible').toBeVisible(); - const count = counter.locator('#preact-counter-count'); - await expect(count, 'initial count is 0').toHaveText('0'); + const count = counter.locator('#preact-counter-count'); + await expect(count, 'initial count is 0').toHaveText('0'); - await waitForHydrate(page, counter); + await waitForHydrate(page, counter); - const increment = counter.locator('#preact-counter-increment'); - await increment.click(); + const increment = counter.locator('#preact-counter-increment'); + await increment.click(); - await expect(count, 'count incremented by 1').toHaveText('1'); - }); + await expect(count, 'count incremented by 1').toHaveText('1'); + }); - test('Solid counter', async ({ astro, page }) => { - await page.goto(astro.resolveUrl('/')); + test('Solid counter', async ({ astro, page }) => { + await page.goto(astro.resolveUrl('/')); - const counter = page.locator('#solid-counter'); - await expect(counter, 'component is visible').toBeVisible(); + const counter = page.locator('#solid-counter'); + await expect(counter, 'component is visible').toBeVisible(); - const count = counter.locator('#solid-counter-count'); - await expect(count, 'initial count is 0').toHaveText('0'); + const count = counter.locator('#solid-counter-count'); + await expect(count, 'initial count is 0').toHaveText('0'); - await waitForHydrate(page, counter); + await waitForHydrate(page, counter); - const increment = counter.locator('#solid-counter-increment'); - await increment.click(); + const increment = counter.locator('#solid-counter-increment'); + await increment.click(); - await expect(count, 'count incremented by 1').toHaveText('1'); - }); + await expect(count, 'count incremented by 1').toHaveText('1'); + }); - test('Vue counter', async ({ astro, page }) => { - await page.goto(astro.resolveUrl('/')); + test('Vue counter', async ({ astro, page }) => { + await page.goto(astro.resolveUrl('/')); - const counter = page.locator('#vue-counter'); - await expect(counter, 'component is visible').toBeVisible(); + const counter = page.locator('#vue-counter'); + await expect(counter, 'component is visible').toBeVisible(); - const count = counter.locator('#vue-counter-count'); - await expect(count, 'initial count is 0').toHaveText('0'); + const count = counter.locator('#vue-counter-count'); + await expect(count, 'initial count is 0').toHaveText('0'); - await waitForHydrate(page, counter); + await waitForHydrate(page, counter); - const increment = counter.locator('#vue-counter-increment'); - await increment.click(); + const increment = counter.locator('#vue-counter-increment'); + await increment.click(); - await expect(count, 'count incremented by 1').toHaveText('1'); - }); + await expect(count, 'count incremented by 1').toHaveText('1'); + }); - test('Svelte counter', async ({ astro, page }) => { - await page.goto(astro.resolveUrl('/')); + test('Svelte counter', async ({ astro, page }) => { + await page.goto(astro.resolveUrl('/')); - const counter = page.locator('#svelte-counter'); - await expect(counter, 'component is visible').toBeVisible(); + const counter = page.locator('#svelte-counter'); + await expect(counter, 'component is visible').toBeVisible(); - const count = counter.locator('#svelte-counter-count'); - await expect(count, 'initial count is 0').toHaveText('0'); + const count = counter.locator('#svelte-counter-count'); + await expect(count, 'initial count is 0').toHaveText('0'); - await waitForHydrate(page, counter); + await waitForHydrate(page, counter); - const increment = counter.locator('#svelte-counter-increment'); - await increment.click(); + const increment = counter.locator('#svelte-counter-increment'); + await increment.click(); - await expect(count, 'count incremented by 1').toHaveText('1'); + await expect(count, 'count incremented by 1').toHaveText('1'); + }); }); -}); diff --git a/packages/astro/e2e/nested-in-vue.test.ts b/packages/astro/e2e/nested-in-vue.test.ts index 1d9c5da3a62a..748c5f597599 100644 --- a/packages/astro/e2e/nested-in-vue.test.ts +++ b/packages/astro/e2e/nested-in-vue.test.ts @@ -98,7 +98,7 @@ test.describe('Nested Frameworks in Vue', () => { await expect(count, 'count incremented by 1').toHaveText('1'); }); - test('Svelte counter', async ({ astro, page }) => { + test.skip('Svelte counter', async ({ astro, page }) => { await page.goto(astro.resolveUrl('/')); const counter = page.locator('#svelte-counter'); diff --git a/packages/astro/e2e/nested-recursive.test.ts b/packages/astro/e2e/nested-recursive.test.ts index edd66ee2c95b..24f67c9b09d6 100644 --- a/packages/astro/e2e/nested-recursive.test.ts +++ b/packages/astro/e2e/nested-recursive.test.ts @@ -88,7 +88,8 @@ test.describe('Recursive Nested Frameworks', () => { await expect(count, 'count incremented by 1').toHaveText('1'); }); - test('Svelte counter', async ({ astro, page }) => { + // TODO: Re-enable once Svelte is compatible with Vite v8 + test.skip('Svelte counter', async ({ astro, page }) => { await page.goto(astro.resolveUrl('/')); const counter = page.locator('#svelte-counter'); diff --git a/packages/astro/e2e/react-component.test.ts b/packages/astro/e2e/react-component.test.ts index 0bebe7086dd5..cbbcb81b826d 100644 --- a/packages/astro/e2e/react-component.test.ts +++ b/packages/astro/e2e/react-component.test.ts @@ -32,6 +32,7 @@ test.describe('dev', () => { await page.goto(astro.resolveUrl('/')); const suffix = page.locator('#suffix'); + await waitForHydrate(page, suffix); expect(await suffix.textContent()).toBe('suffix toggle false'); await suffix.click(); expect(await suffix.textContent()).toBe('suffix toggle true'); diff --git a/packages/astro/e2e/svelte-component.test.ts b/packages/astro/e2e/svelte-component.test.ts index 9183065fd25c..67f8d5b0bbbe 100644 --- a/packages/astro/e2e/svelte-component.test.ts +++ b/packages/astro/e2e/svelte-component.test.ts @@ -12,7 +12,8 @@ const config = { counterCssFilePath: './src/components/Counter.svelte', }; -test.describe('Svelte components in Astro files', () => { +// TODO: Re-enable once Svelte is compatible with Vite v8 +test.describe.skip('Svelte components in Astro files', () => { createTests({ ...config, pageUrl: '/', @@ -20,7 +21,8 @@ test.describe('Svelte components in Astro files', () => { }); }); -test.describe('Svelte components in MDX files', () => { +// TODO: Re-enable once Svelte is compatible with Vite v8 +test.describe.skip('Svelte components in MDX files', () => { createTests({ ...config, pageUrl: '/mdx/', @@ -28,7 +30,8 @@ test.describe('Svelte components in MDX files', () => { }); }); -test.describe('Svelte components lifecycle', () => { +// TODO: Re-enable once Svelte is compatible with Vite v8 +test.describe.skip('Svelte components lifecycle', () => { test('slot should unmount properly', async ({ page, astro }) => { await page.goto(astro.resolveUrl('/')); @@ -39,7 +42,8 @@ test.describe('Svelte components lifecycle', () => { }); }); -test.describe('Slotting content into svelte components', () => { +// TODO: Re-enable once Svelte is compatible with Vite v8 +test.describe.skip('Slotting content into svelte components', () => { test('should stay after hydration', async ({ page, astro }) => { await page.goto(astro.resolveUrl('/with-slots')); const hydratableElement = page.locator('#hydratable'); diff --git a/packages/astro/e2e/test-utils.ts b/packages/astro/e2e/test-utils.ts index ae484ccec870..90ce49386411 100644 --- a/packages/astro/e2e/test-utils.ts +++ b/packages/astro/e2e/test-utils.ts @@ -1,7 +1,7 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { type Locator, type Page, expect, test as testBase } from '@playwright/test'; +import { type Browser, type Locator, type Page, expect, test as testBase } from '@playwright/test'; import type { AstroLogger } from '../dist/core/logger/core.js'; import { type AstroInlineConfig, @@ -107,6 +107,32 @@ export async function waitForHydrate(page: Page, el: Locator) { /** * Scroll to element manually without making sure the `el` is stable */ +/** + * Warm up the dev server by loading a page and waiting for islands to hydrate. + * This ensures Vite's dep optimizer has finished and avoids reload flakiness. + */ +export async function warmupDevServer(browser: Browser, url: string) { + const page = await browser.newPage(); + await page.goto(url, { waitUntil: 'load' }); + await page.waitForLoadState('networkidle').catch(() => {}); + const islands = page.locator('astro-island'); + const count = await islands.count(); + for (let i = 0; i < count; i++) { + const island = islands.nth(i); + const uid = await island.getAttribute('uid').catch(() => null); + if (uid) { + await page + .waitForFunction( + (selector) => document.querySelector(selector)?.hasAttribute('ssr') === false, + `astro-island[uid="${uid}"]`, + { timeout: 5_000 }, + ) + .catch(() => {}); + } + } + await page.close(); +} + export async function scrollToElement(el: Locator) { await el.evaluate((node) => { node.scrollIntoView({ behavior: 'auto' }); diff --git a/packages/astro/e2e/view-transitions.test.ts b/packages/astro/e2e/view-transitions.test.ts index 121a88266c6c..741eb9d940f8 100644 --- a/packages/astro/e2e/view-transitions.test.ts +++ b/packages/astro/e2e/view-transitions.test.ts @@ -1,5 +1,5 @@ import { type Page, expect } from '@playwright/test'; -import { type DevServer, testFactory, waitForHydrate } from './test-utils.ts'; +import { type DevServer, testFactory, waitForHydrate, warmupDevServer } from './test-utils.ts'; declare global { interface Window { @@ -15,8 +15,9 @@ const test = testFactory(import.meta.url, { root: './fixtures/view-transitions/' let devServer: DevServer; -test.beforeAll(async ({ astro }) => { +test.beforeAll(async ({ astro, browser }) => { devServer = await astro.startDevServer(); + await warmupDevServer(browser, astro.resolveUrl('/one')); }); test.afterAll(async () => { @@ -574,6 +575,8 @@ test.describe('View Transitions', () => { let cnt = page.locator('.counter pre'); await expect(cnt).toHaveText('5'); + const counter = page.locator('.counter'); + await waitForHydrate(page, counter); await page.click('.increment'); await expect(cnt).toHaveText('6'); @@ -596,6 +599,8 @@ test.describe('View Transitions', () => { let cnt = page.locator('.counter pre'); await expect(cnt).toHaveText('A0'); + const counter = page.locator('.counter'); + await waitForHydrate(page, counter); await page.click('.increment'); await expect(cnt).toHaveText('A1'); @@ -615,7 +620,8 @@ test.describe('View Transitions', () => { await expect(cnt).toHaveText('A1'); }); - test('Svelte Islands can persist using transition:persist', async ({ page, astro }) => { + // TODO: Re-enable once Svelte is compatible with Vite v8 + test.skip('Svelte Islands can persist using transition:persist', async ({ page, astro }) => { // Go to page 1 await page.goto(astro.resolveUrl('/island-svelte-one')); let cnt = page.locator('.counter pre'); @@ -639,6 +645,8 @@ test.describe('View Transitions', () => { let cnt = page.locator('.counter pre'); await expect(cnt).toHaveText('AA0'); + const counter = page.locator('.counter'); + await waitForHydrate(page, counter); await page.click('.increment'); await expect(cnt).toHaveText('AA1'); @@ -975,7 +983,8 @@ test.describe('View Transitions', () => { expect(styles.length, 'style count has not changed').toEqual(totalExpectedStyles); }); - test('client:only styles are retained on transition (2/2)', async ({ page, astro }) => { + // TODO: Re-enable once Svelte is compatible with Vite v8 + test.skip('client:only styles are retained on transition (2/2)', async ({ page, astro }) => { const totalExpectedStyles_page_three = 11; const totalExpectedStyles_page_four = 9; diff --git a/packages/astro/e2e/vite-virtual-modules.test.ts b/packages/astro/e2e/vite-virtual-modules.test.ts index ac8e661f2b3b..dcc4253f9746 100644 --- a/packages/astro/e2e/vite-virtual-modules.test.ts +++ b/packages/astro/e2e/vite-virtual-modules.test.ts @@ -1,13 +1,14 @@ import { type Locator, type Page, expect } from '@playwright/test'; -import { type DevServer, testFactory } from './test-utils.ts'; +import { type DevServer, testFactory, warmupDevServer } from './test-utils.ts'; const test = testFactory(import.meta.url, { root: './fixtures/vite-virtual-modules/' }); const VIRTUAL_MODULE_ID = '/@id/__x00__virtual:dynamic.css'; let devServer: DevServer; -test.beforeAll(async ({ astro }) => { +test.beforeAll(async ({ astro, browser }) => { devServer = await astro.startDevServer(); + await warmupDevServer(browser, astro.resolveUrl('/')); }); test.afterAll(async () => { diff --git a/packages/astro/package.json b/packages/astro/package.json index f7f31951a4a7..f241c4554627 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -169,7 +169,7 @@ "unist-util-visit": "^5.1.0", "unstorage": "^1.17.4", "vfile": "^6.0.3", - "vite": "^7.3.1", + "vite": "^8.0.8", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", @@ -204,7 +204,6 @@ "rehype-slug": "^6.0.0", "rehype-toc": "^3.0.2", "remark-code-titles": "^0.1.2", - "rollup": "^4.58.0", "sass": "^1.98.0", "typescript": "^5.9.3", "undici": "^7.22.0", diff --git a/packages/astro/playwright.config.js b/packages/astro/playwright.config.js index 3269269d6fd7..0e70d16e39c1 100644 --- a/packages/astro/playwright.config.js +++ b/packages/astro/playwright.config.js @@ -7,6 +7,7 @@ process.stdout.isTTY = false; export default defineConfig({ testMatch: 'e2e/*.test.ts', + reporter: 'list', timeout: 40_000, expect: { timeout: 6_000, diff --git a/packages/astro/src/assets/utils/assets.ts b/packages/astro/src/assets/utils/assets.ts index a76b01076818..8ac73a06fc5f 100644 --- a/packages/astro/src/assets/utils/assets.ts +++ b/packages/astro/src/assets/utils/assets.ts @@ -1,7 +1,6 @@ -import type { Environment, Rollup } from 'vite'; +import type { Environment, Rolldown } from 'vite'; -type PluginContext = Rollup.PluginContext; -type EmitFileOptions = Parameters[0]; +type EmitFileOptions = Parameters[0]; // WeakMap keyed by Environment objects to track emitted asset handles // Using WeakMap ensures automatic cleanup when environments are garbage collected @@ -32,12 +31,15 @@ export function resetHandles(env: Environment): void { * Use this instead of pluginContext.emitFile for assets that should * be moved from the server/prerender directory to the client directory. * - * Note: The pluginContext is typed as Rollup.PluginContext for compatibility + * Note: The pluginContext is typed as Rolldown.PluginContext for compatibility * with content entry types, but in practice it will always have the `environment` * property when running in Vite. */ -export function emitClientAsset(pluginContext: PluginContext, options: EmitFileOptions): string { - const env = (pluginContext as PluginContext & { environment: Environment }).environment; +export function emitClientAsset( + pluginContext: Rolldown.PluginContext, + options: EmitFileOptions, +): string { + const env = (pluginContext as Rolldown.PluginContext & { environment: Environment }).environment; const handle = pluginContext.emitFile(options); const handles = getHandles(env); diff --git a/packages/astro/src/assets/utils/node.ts b/packages/astro/src/assets/utils/node.ts index 4b27a83ea148..578eafbf5422 100644 --- a/packages/astro/src/assets/utils/node.ts +++ b/packages/astro/src/assets/utils/node.ts @@ -1,7 +1,7 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; -import type * as vite from 'vite'; +import type { Rolldown } from 'vite'; import { generateContentHash } from '../../core/encryption.js'; import { prependForwardSlash, slash } from '../../core/path.js'; import type { ImageMetadata } from '../types.js'; @@ -9,7 +9,7 @@ import { imageMetadata } from './metadata.js'; export { hashTransform, propsToFilename } from './hash.js'; -type FileEmitter = vite.Rollup.EmitFile; +type FileEmitter = (opts: Parameters[0]) => string; type ImageMetadataWithContents = ImageMetadata & { contents?: Buffer }; type SvgCacheKey = { hash: string }; @@ -42,7 +42,7 @@ async function handleSvgDeduplication( if (existing) { // Emit file again with the same filename to get a new handle - // This ensures Rollup knows about this handle while maintaining deduplication on disk + // This ensures Rolldown knows about this handle while maintaining deduplication on disk const handle = fileEmitter({ name: existing.filename, source: fileData, diff --git a/packages/astro/src/assets/vite-plugin-assets.ts b/packages/astro/src/assets/vite-plugin-assets.ts index f1341cc51c42..58d5c122112d 100644 --- a/packages/astro/src/assets/vite-plugin-assets.ts +++ b/packages/astro/src/assets/vite-plugin-assets.ts @@ -55,7 +55,7 @@ const addStaticImageFactory = ( >(); } - // Rollup will copy the file to the output directory, as such this is the path in the output directory, including the asset prefix / base + // Rolldown will copy the file to the output directory, as such this is the path in the output directory, including the asset prefix / base const ESMImportedImageSrc = isESMImportedImage(options.src) ? options.src.src : options.src; const fileExtension = extname(ESMImportedImageSrc); const assetPrefix = getAssetsPrefix(fileExtension, settings.config.build.assetsPrefix); diff --git a/packages/astro/src/content/runtime-assets.ts b/packages/astro/src/content/runtime-assets.ts index 24d7ecf56d4d..4967fd1f0243 100644 --- a/packages/astro/src/content/runtime-assets.ts +++ b/packages/astro/src/content/runtime-assets.ts @@ -1,11 +1,11 @@ -import type { PluginContext } from 'rollup'; +import type { Rolldown } from 'vite'; import * as z from 'zod/v4'; import type { ImageMetadata, OmitBrand } from '../assets/types.js'; import { emitClientAsset } from '../assets/utils/assets.js'; import { emitImageMetadata } from '../assets/utils/node.js'; export function createImage( - pluginContext: PluginContext, + pluginContext: Rolldown.PluginContext, shouldEmitFile: boolean, entryFilePath: string, ) { @@ -15,7 +15,7 @@ export function createImage( const metadata = (await emitImageMetadata( resolvedFilePath, shouldEmitFile - ? (opts: Parameters[0]) => + ? (opts: Parameters[0]) => emitClientAsset(pluginContext, opts) : undefined, )) as OmitBrand; diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts index 6ac7da29f0a9..9139789070ab 100644 --- a/packages/astro/src/content/utils.ts +++ b/packages/astro/src/content/utils.ts @@ -4,8 +4,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url'; import { parseFrontmatter } from '@astrojs/markdown-remark'; import { slug as githubSlug } from 'github-slugger'; import colors from 'piccolore'; -import type { PluginContext } from 'rollup'; -import type { RunnableDevEnvironment } from 'vite'; +import type { RunnableDevEnvironment, Rolldown } from 'vite'; import xxhash from 'xxhash-wasm'; import * as z from 'zod/v4'; import { AstroError, AstroErrorData, errorMap, MarkdownError } from '../core/errors/index.js'; @@ -162,7 +161,7 @@ export async function getEntryData< }, collectionConfig: CollectionConfig, shouldEmitFile: boolean, - pluginContext?: PluginContext, + pluginContext?: Rolldown.PluginContext, ): Promise { let data = entry.unvalidatedData as TOutputData; diff --git a/packages/astro/src/content/vite-plugin-content-imports.ts b/packages/astro/src/content/vite-plugin-content-imports.ts index 3e62807ed9c7..9811e7070c49 100644 --- a/packages/astro/src/content/vite-plugin-content-imports.ts +++ b/packages/astro/src/content/vite-plugin-content-imports.ts @@ -2,8 +2,7 @@ import type fsMod from 'node:fs'; import { extname } from 'node:path'; import { pathToFileURL } from 'node:url'; import * as devalue from 'devalue'; -import type { PluginContext } from 'rollup'; -import type { Plugin, RunnableDevEnvironment } from 'vite'; +import type { Plugin, Rolldown, RunnableDevEnvironment } from 'vite'; import { getProxyCode } from '../assets/utils/proxy.js'; import { AstroError } from '../core/errors/errors.js'; import { AstroErrorData } from '../core/errors/index.js'; @@ -237,7 +236,7 @@ type GetEntryModuleParams = fs: typeof fsMod; fileId: string; contentDir: URL; - pluginContext: PluginContext; + pluginContext: Rolldown.PluginContext; entryConfigByExt: Map; config: AstroConfig; shouldEmitFile: boolean; diff --git a/packages/astro/src/core/build/add-rollup-input.ts b/packages/astro/src/core/build/add-rolldown-input.ts similarity index 79% rename from packages/astro/src/core/build/add-rollup-input.ts rename to packages/astro/src/core/build/add-rolldown-input.ts index 073fb558231c..babfd6499bc4 100644 --- a/packages/astro/src/core/build/add-rollup-input.ts +++ b/packages/astro/src/core/build/add-rolldown-input.ts @@ -1,4 +1,4 @@ -import type { Rollup } from 'vite'; +import type { Rolldown } from 'vite'; function fromEntries(entries: [string, V][]) { const obj: Record = {}; @@ -8,10 +8,10 @@ function fromEntries(entries: [string, V][]) { return obj; } -export function addRollupInput( - inputOptions: Rollup.InputOptions, +export function addRolldownInput( + inputOptions: Rolldown.InputOptions, newInputs: string[], -): Rollup.InputOptions { +): Rolldown.InputOptions { // Add input module ids to existing input option, whether it's a string, array or object // this way you can use multiple html plugins all adding their own inputs if (!inputOptions.input) { @@ -42,5 +42,5 @@ export function addRollupInput( }; } - throw new Error(`Unknown rollup input type. Supported inputs are string, array and object.`); + throw new Error(`Unknown rolldown input type. Supported inputs are string, array and object.`); } diff --git a/packages/astro/src/core/build/graph.ts b/packages/astro/src/core/build/graph.ts index c34c795a406a..09868938ea55 100644 --- a/packages/astro/src/core/build/graph.ts +++ b/packages/astro/src/core/build/graph.ts @@ -1,9 +1,9 @@ -import type { GetModuleInfo, ModuleInfo } from 'rollup'; +import type { Rolldown } from 'vite'; import { VIRTUAL_PAGE_RESOLVED_MODULE_ID } from '../../vite-plugin-pages/const.js'; interface ExtendedModuleInfo { - info: ModuleInfo; + info: Rolldown.ModuleInfo; depth: number; order: number; } @@ -11,7 +11,7 @@ interface ExtendedModuleInfo { // This walks up the dependency graph and yields out each ModuleInfo object. export function getParentExtendedModuleInfos( id: string, - ctx: { getModuleInfo: GetModuleInfo }, + ctx: { getModuleInfo: Rolldown.GetModuleInfo }, until?: (importer: string) => boolean, depth = 0, order = 0, @@ -51,11 +51,11 @@ export function getParentExtendedModuleInfos( export function getParentModuleInfos( id: string, - ctx: { getModuleInfo: GetModuleInfo }, + ctx: { getModuleInfo: Rolldown.GetModuleInfo }, until?: (importer: string) => boolean, seen = new Set(), - accumulated: ModuleInfo[] = [], -): ModuleInfo[] { + accumulated: Rolldown.ModuleInfo[] = [], +): Rolldown.ModuleInfo[] { seen.add(id); const info = ctx.getModuleInfo(id); @@ -77,7 +77,7 @@ export function getParentModuleInfos( // Returns true if a module is a top-level page. We determine this based on whether // it is imported by the top-level virtual module. -export function moduleIsTopLevelPage(info: ModuleInfo): boolean { +export function moduleIsTopLevelPage(info: Rolldown.ModuleInfo): boolean { return ( info.importers[0]?.includes(VIRTUAL_PAGE_RESOLVED_MODULE_ID) || info.dynamicImporters[0]?.includes(VIRTUAL_PAGE_RESOLVED_MODULE_ID) @@ -88,7 +88,7 @@ export function moduleIsTopLevelPage(info: ModuleInfo): boolean { // This could be a .astro page, a .markdown or a .md (or really any file extension for markdown files) page. export function getTopLevelPageModuleInfos( id: string, - ctx: { getModuleInfo: GetModuleInfo }, -): ModuleInfo[] { + ctx: { getModuleInfo: Rolldown.GetModuleInfo }, +): Rolldown.ModuleInfo[] { return getParentModuleInfos(id, ctx).filter(moduleIsTopLevelPage); } diff --git a/packages/astro/src/core/build/plugins/plugin-analyzer.ts b/packages/astro/src/core/build/plugins/plugin-analyzer.ts index 6304908e3629..db58dcddbfeb 100644 --- a/packages/astro/src/core/build/plugins/plugin-analyzer.ts +++ b/packages/astro/src/core/build/plugins/plugin-analyzer.ts @@ -12,7 +12,7 @@ import { ASTRO_VITE_ENVIRONMENT_NAMES } from '../../constants.js'; export function pluginAnalyzer(internals: BuildInternals): VitePlugin { return { - name: '@astro/rollup-plugin-astro-analyzer', + name: '@astro/rolldown-plugin-astro-analyzer', applyToEnvironment(environment) { return ( environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr || diff --git a/packages/astro/src/core/build/plugins/plugin-component-entry.ts b/packages/astro/src/core/build/plugins/plugin-component-entry.ts index 2dda51066c6b..3afcecf3a9f7 100644 --- a/packages/astro/src/core/build/plugins/plugin-component-entry.ts +++ b/packages/astro/src/core/build/plugins/plugin-component-entry.ts @@ -5,7 +5,7 @@ import { ASTRO_VITE_ENVIRONMENT_NAMES } from '../../constants.js'; const astroEntryPrefix = '\0astro-entry:'; /** - * When adding hydrated or client:only components as Rollup inputs, sometimes we're not using all + * When adding hydrated or client:only components as Rolldown inputs, sometimes we're not using all * of the export names, e.g. `import { Counter } from './ManyComponents.jsx'`. This plugin proxies * entries to re-export only the names that the user is using. */ @@ -18,7 +18,7 @@ export function pluginComponentEntry(internals: BuildInternals): VitePlugin { for (const [componentId, exportNames] of componentToExportNames) { // If one of the imports has a dot, it's a namespaced import, e.g. `import * as foo from 'foo'` // and ``, in which case we re-export `foo` entirely and we don't need to handle - // it in this plugin as it's default behaviour from Rollup. + // it in this plugin as it's default behaviour from Rolldown. if (exportNames.some((name) => name.includes('.') || name === '*')) { componentToExportNames.delete(componentId); } else { @@ -43,12 +43,12 @@ export function pluginComponentEntry(internals: BuildInternals): VitePlugin { return environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.client; }, config(config) { - const rollupInput = config.build?.rollupOptions?.input; + const rolldownInput = config.build?.rolldownOptions?.input; // Astro passes an array of inputs by default. Even though other Vite plugins could // change this to an object, it shouldn't happen in practice as our plugin runs first. - if (Array.isArray(rollupInput)) { + if (Array.isArray(rolldownInput)) { // @ts-expect-error input is definitely defined here, but typescript thinks it doesn't - config.build.rollupOptions.input = rollupInput.map((id) => { + config.build.rolldownOptions.input = rolldownInput.map((id) => { if (componentToExportNames.has(id)) { return astroEntryPrefix + id; } else { diff --git a/packages/astro/src/core/build/plugins/plugin-css.ts b/packages/astro/src/core/build/plugins/plugin-css.ts index 6f050eb349db..ad1db69ea822 100644 --- a/packages/astro/src/core/build/plugins/plugin-css.ts +++ b/packages/astro/src/core/build/plugins/plugin-css.ts @@ -1,5 +1,4 @@ -import type { GetModuleInfo } from 'rollup'; -import type { BuildOptions, ResolvedConfig, Plugin as VitePlugin } from 'vite'; +import type { BuildOptions, ResolvedConfig, Plugin as VitePlugin, Rolldown } from 'vite'; import { isCSSRequest } from 'vite'; import { ASTRO_VITE_ENVIRONMENT_NAMES } from '../../constants.js'; import { isPropagatedAssetBoundary } from '../../head-propagation/boundary.js'; @@ -17,26 +16,26 @@ import { shouldInlineAsset } from './util.js'; /***** ASTRO PLUGIN *****/ export function pluginCSS(options: StaticBuildOptions, internals: BuildInternals): VitePlugin[] { - return rollupPluginAstroBuildCSS({ + return rolldownPluginAstroBuildCSS({ buildOptions: options, internals, }); } -/***** ROLLUP SUB-PLUGINS *****/ +/***** ROLLDOWN SUB-PLUGINS *****/ interface PluginOptions { internals: BuildInternals; buildOptions: StaticBuildOptions; } -function isBuildCssBoundary(id: string, ctx: { getModuleInfo: GetModuleInfo }): boolean { +function isBuildCssBoundary(id: string, ctx: { getModuleInfo: Rolldown.GetModuleInfo }): boolean { if (isPropagatedAssetBoundary(id)) return true; const info = ctx.getModuleInfo(id); return info ? moduleIsTopLevelPage(info) : false; } -function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { +function rolldownPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { const { internals, buildOptions } = options; const { settings } = buildOptions; @@ -48,7 +47,7 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { const moduleIdToPropagatedCss: Record> = {}; const cssBuildPlugin: VitePlugin = { - name: 'astro:rollup-plugin-build-css', + name: 'astro:rolldown-plugin-build-css', applyToEnvironment(environment) { return ( @@ -264,7 +263,7 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { }; const singleCssPlugin: VitePlugin = { - name: 'astro:rollup-plugin-single-css', + name: 'astro:rolldown-plugin-single-css', enforce: 'post', applyToEnvironment(environment) { return ( @@ -294,7 +293,7 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { let assetsInlineLimit: NonNullable; const inlineStylesheetsPlugin: VitePlugin = { - name: 'astro:rollup-plugin-inline-stylesheets', + name: 'astro:rolldown-plugin-inline-stylesheets', enforce: 'post', applyToEnvironment(environment) { return ( @@ -449,7 +448,7 @@ function shouldDeleteCSSChunk(allModules: string[], internals: BuildInternals): function* getParentClientOnlys( id: string, - ctx: { getModuleInfo: GetModuleInfo }, + ctx: { getModuleInfo: Rolldown.GetModuleInfo }, internals: BuildInternals, ): Generator { for (const info of getParentModuleInfos(id, ctx)) { diff --git a/packages/astro/src/core/build/plugins/plugin-internals.ts b/packages/astro/src/core/build/plugins/plugin-internals.ts index f4bc265f7152..87fdf32204e6 100644 --- a/packages/astro/src/core/build/plugins/plugin-internals.ts +++ b/packages/astro/src/core/build/plugins/plugin-internals.ts @@ -36,7 +36,7 @@ export function pluginInternals( if (environmentName === ASTRO_VITE_ENVIRONMENT_NAMES.prerender) { return { build: { - rollupOptions: { + rolldownOptions: { // These packages as they're not bundle-friendly. Users with strict package installations // need to manually install these themselves if they use the related features. external: [ diff --git a/packages/astro/src/core/build/plugins/plugin-prerender.ts b/packages/astro/src/core/build/plugins/plugin-prerender.ts index 3bca8e5042c9..7e2fb0b53e37 100644 --- a/packages/astro/src/core/build/plugins/plugin-prerender.ts +++ b/packages/astro/src/core/build/plugins/plugin-prerender.ts @@ -5,7 +5,7 @@ import { ASTRO_VITE_ENVIRONMENT_NAMES } from '../../constants.js'; export function pluginPrerender(_opts: StaticBuildOptions, internals: BuildInternals): VitePlugin { return { - name: 'astro:rollup-plugin-prerender', + name: 'astro:rolldown-plugin-prerender', applyToEnvironment(environment) { return environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr; diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index c0b11f9ad77e..071c2ff6210f 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -32,18 +32,18 @@ import { } from './plugins/plugin-ssr.js'; import { ASTRO_PAGE_EXTENSION_POST_PATTERN } from './plugins/util.js'; import type { StaticBuildOptions } from './types.js'; -import { cleanChunkName, getTimeStat, viteBuildReturnToRollupOutputs } from './util.js'; +import { cleanChunkName, getTimeStat, viteBuildReturnToRolldownOutputs } from './util.js'; import { NOOP_MODULE_ID } from './plugins/plugin-noop.js'; import { ASTRO_VITE_ENVIRONMENT_NAMES } from '../constants.js'; -import type { InputOption } from 'rollup'; +import type { Rolldown } from 'vite'; import { getSSRAssets } from './internal.js'; import { SERVER_ISLAND_MAP_MARKER } from '../server-islands/vite-plugin-server-islands.js'; const PRERENDER_ENTRY_FILENAME_PREFIX = 'prerender-entry'; /** - * Minimal chunk data extracted from RollupOutput for deferred manifest/content injection. - * Allows releasing full RollupOutput objects early to reduce memory usage. + * Minimal chunk data extracted from RolldownOutput for deferred manifest/content injection. + * Allows releasing full RolldownOutput objects early to reduce memory usage. */ export interface ExtractedChunk { fileName: string; @@ -58,11 +58,11 @@ type BuildPostHook = (params: { }) => void | Promise; /** - * Extracts only the chunks that need post-build injection from RollupOutput. - * This allows releasing the full RollupOutput to reduce memory usage. + * Extracts only the chunks that need post-build injection from RolldownOutput. + * This allows releasing the full RolldownOutput to reduce memory usage. */ function extractRelevantChunks( - outputs: vite.Rollup.RollupOutput[], + outputs: vite.Rolldown.RolldownOutput[], prerender: boolean, ): ExtractedChunk[] { const extracted: ExtractedChunk[] = []; @@ -153,9 +153,9 @@ export async function viteBuild(opts: StaticBuildOptions) { * - Components with hydration directives (client:*) * - Client-only components * - Page scripts - * - These discoveries populate `internals.clientInput` which becomes the rollup input + * - These discoveries populate `internals.clientInput` which becomes the rolldown input * - Config is mutated after builder creation to set dynamic inputs - * - If no client scripts exist, uses a "noop" entrypoint to satisfy Rollup's input requirement + * - If no client scripts exist, uses a "noop" entrypoint to satisfy Rolldown's input requirement * - public/ folder is copied during this build * * Returns outputs from each environment for post-build processing (manifest injection, etc). @@ -169,25 +169,25 @@ async function buildEnvironments(opts: StaticBuildOptions, internals: BuildInter const buildPlugins = getAllBuildPlugins(internals, opts); const flatPlugins = buildPlugins.flat().filter(Boolean); const plugins = [...flatPlugins, ...(viteConfig.plugins || [])]; - let currentRollupInput: InputOption | undefined = undefined; + let currentRolldownInput: Rolldown.InputOption | undefined = undefined; let buildPostHooks: BuildPostHook[] = []; plugins.push({ name: 'astro:resolve-input', - // When the rollup input is safe to update, we normalize it to always be an object + // When the rolldown input is safe to update, we normalize it to always be an object // so we can reliably identify which entrypoint corresponds to the adapter enforce: 'post', config(config) { - if (typeof config.build?.rollupOptions?.input === 'string') { - config.build.rollupOptions.input = { index: config.build.rollupOptions.input }; - } else if (Array.isArray(config.build?.rollupOptions?.input)) { - config.build.rollupOptions.input = Object.fromEntries( - config.build.rollupOptions.input.map((v, i) => [`index_${i}`, v]), + if (typeof config.build?.rolldownOptions?.input === 'string') { + config.build.rolldownOptions.input = { index: config.build.rolldownOptions.input }; + } else if (Array.isArray(config.build?.rolldownOptions?.input)) { + config.build.rolldownOptions.input = Object.fromEntries( + config.build.rolldownOptions.input.map((v, i) => [`index_${i}`, v]), ); } }, - // We save the rollup input to be able to check later on + // We save the rolldown input to be able to check later on configResolved(config) { - currentRollupInput = config.build.rollupOptions.input; + currentRolldownInput = config.build.rolldownOptions.input; }, }); // Post plugin for manifest injection, page generation, and cleanup @@ -233,16 +233,16 @@ async function buildEnvironments(opts: StaticBuildOptions, internals: BuildInter }, }); - function isRollupInput(moduleName: string | null): boolean { - if (!currentRollupInput || !moduleName) { + function isRolldownInput(moduleName: string | undefined): boolean { + if (!currentRolldownInput || !moduleName) { return false; } - if (typeof currentRollupInput === 'string') { - return currentRollupInput === moduleName; - } else if (Array.isArray(currentRollupInput)) { - return currentRollupInput.includes(moduleName); + if (typeof currentRolldownInput === 'string') { + return currentRolldownInput === moduleName; + } else if (Array.isArray(currentRolldownInput)) { + return currentRolldownInput.includes(moduleName); } else { - return Object.keys(currentRollupInput).includes(moduleName); + return Object.keys(currentRolldownInput).includes(moduleName); } } @@ -258,8 +258,8 @@ async function buildEnvironments(opts: StaticBuildOptions, internals: BuildInter emptyOutDir: false, copyPublicDir: false, manifest: false, - rollupOptions: { - ...viteConfig.build?.rollupOptions, + rolldownOptions: { + ...viteConfig.build?.rolldownOptions, // Setting as `exports-only` allows us to safely delete inputs that are only used during prerendering preserveEntrySignatures: 'exports-only', ...(legacyAdapter && settings.buildOutput === 'server' @@ -290,7 +290,7 @@ async function buildEnvironments(opts: StaticBuildOptions, internals: BuildInter return [prefix, cleanChunkName(name), suffix].join(''); }, assetFileNames: `${settings.config.build.assets}/[name].[hash][extname]`, - ...viteConfig.build?.rollupOptions?.output, + ...viteConfig.build?.rolldownOptions?.output, entryFileNames(chunkInfo) { if (chunkInfo.facadeModuleId?.startsWith(VIRTUAL_PAGE_RESOLVED_MODULE_ID)) { return makeAstroPageEntryPointFileName( @@ -301,9 +301,9 @@ async function buildEnvironments(opts: StaticBuildOptions, internals: BuildInter } else if ( chunkInfo.facadeModuleId === RESOLVED_LEGACY_SSR_ENTRY_VIRTUAL_MODULE || // This catches the case when the adapter uses `entrypointResolution: 'auto'`. When doing so, - // the adapter must set rollupOptions.input or Astro sets it from `serverEntrypoint`. - isRollupInput(chunkInfo.name) || - isRollupInput(chunkInfo.facadeModuleId) + // the adapter must set rolldownOptions.input or Astro sets it from `serverEntrypoint`. + isRolldownInput(chunkInfo.name) || + isRolldownInput(chunkInfo.facadeModuleId) ) { return opts.settings.config.build.serverEntry; } else { @@ -333,7 +333,7 @@ async function buildEnvironments(opts: StaticBuildOptions, internals: BuildInter extractPrerenderEntryFileName(internals, prerenderOutput); // Extract chunks needing injection, then release output for GC - const prerenderOutputs = viteBuildReturnToRollupOutputs(prerenderOutput); + const prerenderOutputs = viteBuildReturnToRolldownOutputs(prerenderOutput); const prerenderChunks = extractRelevantChunks(prerenderOutputs, true); prerenderOutput = undefined as any; @@ -346,7 +346,7 @@ async function buildEnvironments(opts: StaticBuildOptions, internals: BuildInter ); settings.timer.end('SSR build'); // Extract chunks needing injection, then release output for GC - const ssrOutputs = viteBuildReturnToRollupOutputs(ssrOutput); + const ssrOutputs = viteBuildReturnToRolldownOutputs(ssrOutput); ssrChunks = extractRelevantChunks(ssrOutputs, false); ssrOutput = undefined as any; } @@ -370,12 +370,12 @@ async function buildEnvironments(opts: StaticBuildOptions, internals: BuildInter // So using the noop plugin here which will give us an input that just gets thrown away. internals.clientInput.add(NOOP_MODULE_ID); } - // Sort the client input to ensure deterministic Rollup entry point ordering. + // Sort the client input to ensure deterministic Rolldown entry point ordering. // `internals.clientInput` is a Set whose iteration order depends on async module resolution // timing during prerendering. Without sorting, consecutive builds of the same // source code can produce different output filenames, breaking CDN caching. const sortedClientInput = Array.from(internals.clientInput).sort(); - builder.environments.client.config.build.rollupOptions.input = sortedClientInput; + builder.environments.client.config.build.rolldownOptions.input = sortedClientInput; settings.timer.start('Client build'); await builder.build(builder.environments.client); settings.timer.end('Client build'); @@ -392,7 +392,7 @@ async function buildEnvironments(opts: StaticBuildOptions, internals: BuildInter build: { emitAssets: true, outDir: fileURLToPath(getPrerenderOutputDirectory(settings)), - rollupOptions: { + rolldownOptions: { // Only skip the default prerender entrypoint if an adapter with `entrypointResolution: 'self'` is used // AND provides a custom prerenderer. Otherwise, use the default. ...(!legacyAdapter && settings.prerenderer @@ -401,7 +401,7 @@ async function buildEnvironments(opts: StaticBuildOptions, internals: BuildInter output: { entryFileNames: `${PRERENDER_ENTRY_FILENAME_PREFIX}.[hash].mjs`, format: 'esm', - ...viteConfig.environments?.prerender?.build?.rollupOptions?.output, + ...viteConfig.environments?.prerender?.build?.rolldownOptions?.output, }, }, ssr: true, @@ -415,7 +415,7 @@ async function buildEnvironments(opts: StaticBuildOptions, internals: BuildInter copyPublicDir: true, sourcemap: viteConfig.environments?.client?.build?.sourcemap ?? false, minify: true, - rollupOptions: { + rolldownOptions: { preserveEntrySignatures: 'exports-only', output: { entryFileNames(chunkInfo) { @@ -425,7 +425,7 @@ async function buildEnvironments(opts: StaticBuildOptions, internals: BuildInter return `${settings.config.build.assets}/${cleanChunkName(chunkInfo.name)}.[hash].js`; }, assetFileNames: `${settings.config.build.assets}/[name].[hash][extname]`, - ...viteConfig.environments?.client?.build?.rollupOptions?.output, + ...viteConfig.environments?.client?.build?.rolldownOptions?.output, }, }, }, @@ -433,9 +433,9 @@ async function buildEnvironments(opts: StaticBuildOptions, internals: BuildInter [ASTRO_VITE_ENVIRONMENT_NAMES.ssr]: { build: { outDir: fileURLToPath(getServerOutputDirectory(settings)), - rollupOptions: { + rolldownOptions: { output: { - ...viteConfig.environments?.ssr?.build?.rollupOptions?.output, + ...viteConfig.environments?.ssr?.build?.rolldownOptions?.output, }, }, }, @@ -461,11 +461,11 @@ async function buildEnvironments(opts: StaticBuildOptions, internals: BuildInter */ function getPrerenderEntryFileName( prerenderOutput: - | vite.Rollup.RollupOutput - | vite.Rollup.RollupOutput[] - | vite.Rollup.RollupWatcher, + | vite.Rolldown.RolldownOutput + | vite.Rolldown.RolldownOutput[] + | vite.Rolldown.RolldownWatcher, ): string { - const outputs = viteBuildReturnToRollupOutputs(prerenderOutput); + const outputs = viteBuildReturnToRolldownOutputs(prerenderOutput); for (const output of outputs) { for (const chunk of output.output) { @@ -490,9 +490,9 @@ function getPrerenderEntryFileName( function extractPrerenderEntryFileName( internals: BuildInternals, prerenderOutput: - | vite.Rollup.RollupOutput - | vite.Rollup.RollupOutput[] - | vite.Rollup.RollupWatcher, + | vite.Rolldown.RolldownOutput + | vite.Rolldown.RolldownOutput[] + | vite.Rolldown.RolldownWatcher, ) { internals.prerenderEntryFileName = getPrerenderEntryFileName(prerenderOutput); } diff --git a/packages/astro/src/core/build/util.ts b/packages/astro/src/core/build/util.ts index e668e2a0c982..91701a18b97c 100644 --- a/packages/astro/src/core/build/util.ts +++ b/packages/astro/src/core/build/util.ts @@ -1,4 +1,4 @@ -import type { Rollup } from 'vite'; +import type { Rolldown } from 'vite'; import type { AstroConfig } from '../../types/public/config.js'; import type { ViteBuildReturn } from './types.js'; @@ -33,14 +33,14 @@ export function shouldAppendForwardSlash( /** * Matches any character that is NOT alphanumeric, underscore, dot, hyphen, or forward slash. - * Rollup's built-in `sanitizeFileName` misses characters like `!` and `~` that can leak + * Rolldown's built-in `sanitizeFileName` misses characters like `!` and `~` that can leak * from Vite module IDs into chunk names (e.g. `page.!{005}.js`). */ const UNSAFE_CHUNK_CHAR_RE = /[^\w.\-/]/g; /** * Replaces characters in a chunk name that are not safe for filesystem paths or URLs. - * Characters like `!` and `~` can leak from Vite module IDs into Rollup chunk names + * Characters like `!` and `~` can leak from Vite module IDs into Rolldown chunk names * and break deploys on platforms like Netlify. */ export function cleanChunkName(name: string): string { @@ -63,10 +63,10 @@ function encodeName(name: string): string { return name; } -export function viteBuildReturnToRollupOutputs( +export function viteBuildReturnToRolldownOutputs( viteBuildReturn: ViteBuildReturn, -): Rollup.RollupOutput[] { - const result: Rollup.RollupOutput[] = []; +): Rolldown.RolldownOutput[] { + const result: Rolldown.RolldownOutput[] = []; if (Array.isArray(viteBuildReturn)) { result.push(...viteBuildReturn); } else if ('output' in viteBuildReturn) { diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts index 60bf891af4c3..69b95b9109cf 100644 --- a/packages/astro/src/core/create-vite.ts +++ b/packages/astro/src/core/create-vite.ts @@ -353,6 +353,7 @@ const COMMON_PREFIXES_NOT_ASTRO = [ '@webcomponents/', '@fontsource/', '@postcss-plugins/', + '@rolldown/', '@rollup/', '@astrojs/renderer-', '@types/', @@ -363,6 +364,7 @@ const COMMON_PREFIXES_NOT_ASTRO = [ 'prettier-plugin-', 'remark-', 'rehype-', + 'rolldown-plugin-', 'rollup-plugin-', 'vite-plugin-', ]; diff --git a/packages/astro/src/core/errors/dev/utils.ts b/packages/astro/src/core/errors/dev/utils.ts index 61ad0e371d06..e42e98f8ec24 100644 --- a/packages/astro/src/core/errors/dev/utils.ts +++ b/packages/astro/src/core/errors/dev/utils.ts @@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url'; import { stripVTControlCharacters } from 'node:util'; import { escape } from 'html-escaper'; import colors from 'piccolore'; -import type { ESBuildTransformResult } from 'vite'; +import type { transformWithOxc } from 'vite'; import type { SSRError } from '../../../types/public/internal.js'; import { removeLeadingForwardSlashWindows } from '../../path.js'; import { normalizePath } from '../../viteUtils.js'; @@ -12,7 +12,7 @@ import { AggregateError, type ErrorWithMetadata } from '../errors.js'; import { codeFrame } from '../printer.js'; import { normalizeLF } from '../utils.js'; -type EsbuildMessage = ESBuildTransformResult['warnings'][number]; +type OxcMessage = Awaited>['warnings'][number]; /** * Takes any error-like object and returns a standardized Error + metadata object. @@ -84,8 +84,8 @@ export function collectErrorMetadata(e: any, rootFolder?: URL): ErrorWithMetadat // If we received an array of errors and it's not from us, it's most likely from ESBuild, try to extract info for Vite to display // NOTE: We still need to be defensive here, because it might not necessarily be from ESBuild, it's just fairly likely. if (!AggregateError.is(e) && Array.isArray(e.errors)) { - (e.errors as EsbuildMessage[]).forEach((buildError, i) => { - const { location, pluginName, text } = buildError; + (e.errors as OxcMessage[]).forEach((buildError, i) => { + const { loc: location, plugin: pluginName, message: text } = buildError; // ESBuild can give us a slightly better error message than the one in the error, so let's use it if (text) { diff --git a/packages/astro/src/core/head-propagation/comment.ts b/packages/astro/src/core/head-propagation/comment.ts deleted file mode 100644 index 65510512fb6c..000000000000 --- a/packages/astro/src/core/head-propagation/comment.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Detect this in comments, both in .astro components and in js/ts files. -// Keep behavior aligned with the existing plugin usage. -const HEAD_INJECT_COMMENT_EXP = /(?:^\/\/|\/\/!)\s*astro-head-inject/; - -/** - * Returns true when source contains the `astro-head-inject` marker comment. - * - * @example - * `//! astro-head-inject` in a helper module marks parent importers as `in-tree`. - */ -export function hasHeadInjectComment(source: string): boolean { - return HEAD_INJECT_COMMENT_EXP.test(source); -} diff --git a/packages/astro/src/core/head-propagation/hint.ts b/packages/astro/src/core/head-propagation/hint.ts new file mode 100644 index 000000000000..ee1952815f96 --- /dev/null +++ b/packages/astro/src/core/head-propagation/hint.ts @@ -0,0 +1,10 @@ +// Detect the `"use astro:head-inject"` directive in source code. +// This directive marks a module as needing head propagation (CSS/script injection into ). +const HEAD_PROPAGATION_HINT = '"use astro:head-inject"'; + +/** + * Returns true when source contains the `"use astro:head-inject"` directive. + */ +export function hasHeadPropagationCall(source: string): boolean { + return source.includes(HEAD_PROPAGATION_HINT); +} diff --git a/packages/astro/src/core/logger/vite.ts b/packages/astro/src/core/logger/vite.ts index 24d88e1c2076..bce1eafb1914 100644 --- a/packages/astro/src/core/logger/vite.ts +++ b/packages/astro/src/core/logger/vite.ts @@ -1,6 +1,6 @@ import { fileURLToPath } from 'node:url'; import { stripVTControlCharacters } from 'node:util'; -import type { LogLevel, Rollup, Logger as ViteLogger } from 'vite'; +import type { LogLevel, Rolldown, Logger as ViteLogger } from 'vite'; import { isAstroError } from '../errors/errors.js'; import { serverShortcuts as formatServerShortcuts } from '../messages/runtime.js'; import { type AstroLogger as AstroLogger, isLogLevelEnabled } from './core.js'; @@ -29,7 +29,7 @@ export function createViteLogger( viteLogLevel: LogLevel = 'info', ): ViteLogger { const warnedMessages = new Set(); - const loggedErrors = new WeakSet(); + const loggedErrors = new WeakSet(); const logger: ViteLogger = { hasWarned: false, diff --git a/packages/astro/src/core/middleware/vite-plugin.ts b/packages/astro/src/core/middleware/vite-plugin.ts index 2de16d29232b..d8f4c6f59a2b 100644 --- a/packages/astro/src/core/middleware/vite-plugin.ts +++ b/packages/astro/src/core/middleware/vite-plugin.ts @@ -6,7 +6,7 @@ import { } from 'vite'; import { getServerOutputDirectory } from '../../prerender/utils.js'; import type { AstroSettings } from '../../types/astro.js'; -import { addRollupInput } from '../build/add-rollup-input.js'; +import { addRolldownInput } from '../build/add-rolldown-input.js'; import type { BuildInternals } from '../build/internal.js'; import type { StaticBuildOptions } from '../build/types.js'; import { ASTRO_VITE_ENVIRONMENT_NAMES, MIDDLEWARE_PATH_SEGMENT_NAME } from '../constants.js'; @@ -162,9 +162,9 @@ export function vitePluginMiddlewareBuild( options(options) { if (canSplitMiddleware) { - // Add middleware as a separate rollup input for environments that support multiple entrypoints. + // Add middleware as a separate rolldown input for environments that support multiple entrypoints. // This allows the middleware to be bundled independently. - return addRollupInput(options, [MIDDLEWARE_MODULE_ID]); + return addRolldownInput(options, [MIDDLEWARE_MODULE_ID]); } else { // TODO warn if edge middleware is enabled } diff --git a/packages/astro/src/types/public/content.ts b/packages/astro/src/types/public/content.ts index a4d1631e8cbc..6bff66a75e24 100644 --- a/packages/astro/src/types/public/content.ts +++ b/packages/astro/src/types/public/content.ts @@ -1,9 +1,9 @@ import type { MarkdownHeading } from '@astrojs/markdown-remark'; -import type * as rollup from 'rollup'; import type { DataEntry, RenderedContent } from '../../content/data-store.js'; import type { LiveCollectionError } from '../../content/loaders/errors.js'; import type { AstroComponentFactory } from '../../runtime/server/index.js'; import type { AstroConfig } from './config.js'; +import type { Rolldown } from 'vite'; export interface AstroInstance { file: string; @@ -110,13 +110,13 @@ export interface ContentEntryType { contents: string; }): GetContentEntryInfoReturnType | Promise; getRenderModule?( - this: rollup.PluginContext, + this: Rolldown.PluginContext, params: { contents: string; fileUrl: URL; viteId: string; }, - ): rollup.LoadResult | Promise; + ): Rolldown.LoadResult | Promise; contentModuleTypes?: string; getRenderFunction?(config: AstroConfig): Promise; diff --git a/packages/astro/src/types/public/integrations.ts b/packages/astro/src/types/public/integrations.ts index 838557ee9650..fef3e4f460b1 100644 --- a/packages/astro/src/types/public/integrations.ts +++ b/packages/astro/src/types/public/integrations.ts @@ -153,7 +153,7 @@ interface AdapterExplicitProperties { * or `"explicit"` (default, but deprecated): * * - **`"auto"` (recommended):** You are responsible for providing a valid module as an entrypoint - * using either `serverEntrypoint` or, if you need further customization at the Vite level using `vite.build.rollupOptions.input`. + * using either `serverEntrypoint` or, if you need further customization at the Vite level using `vite.build.rolldownOptions.input`. * - **`"explicit"` (deprecated)**: You must provide the exports required by the host in the server entrypoint * using a `createExports()` function before passing them to `setAdapter()` as an [`exports`](#exports) list. This supports * adapters built using the Astro 5 version of the Adapter API. By default, all adapters will receive this value to allow backwards @@ -188,7 +188,7 @@ interface AdapterAutoProperties { * or `"explicit"` (default, but deprecated): * * - **`"auto"` (recommended):** You are responsible for providing a valid module as an entrypoint - * using either `serverEntrypoint` or, if you need further customization at the Vite level using `vite.build.rollupOptions.input`. + * using either `serverEntrypoint` or, if you need further customization at the Vite level using `vite.build.rolldownOptions.input`. * - **`"explicit"` (deprecated)**: You must provide the exports required by the host in the server entrypoint * using a `createExports()` function before passing them to `setAdapter()` as an [`exports`](#exports) list. This supports * adapters built using the Astro 5 version of the Adapter API. By default, all adapters will receive this value to allow backwards diff --git a/packages/astro/src/vite-plugin-adapter-config/index.ts b/packages/astro/src/vite-plugin-adapter-config/index.ts index 304fb719e1a8..2992c3cecdf3 100644 --- a/packages/astro/src/vite-plugin-adapter-config/index.ts +++ b/packages/astro/src/vite-plugin-adapter-config/index.ts @@ -18,7 +18,7 @@ export function vitePluginAdapterConfig(settings: AstroSettings): VitePlugin { environments: { [ASTRO_VITE_ENVIRONMENT_NAMES.ssr]: { build: { - rollupOptions: { + rolldownOptions: { input: { index: typeof adapter.serverEntrypoint === 'string' diff --git a/packages/astro/src/vite-plugin-astro/compile-rs.ts b/packages/astro/src/vite-plugin-astro/compile-rs.ts index 9eff8a3916e5..ea072e766aa0 100644 --- a/packages/astro/src/vite-plugin-astro/compile-rs.ts +++ b/packages/astro/src/vite-plugin-astro/compile-rs.ts @@ -1,4 +1,4 @@ -import type { SourceMapInput } from 'rollup'; +import type { Rolldown } from 'vite'; import { type CompileProps, type CompileResult, compile } from '../core/compile/compile-rs.js'; import { getFileInfo } from '../vite-plugin-utils/index.js'; import type { CompileMetadata } from './types.js'; @@ -9,7 +9,7 @@ interface CompileAstroOption { } export interface CompileAstroResult extends Omit { - map: SourceMapInput; + map: Rolldown.SourceMapInput; } export async function compileAstro({ diff --git a/packages/astro/src/vite-plugin-astro/compile.ts b/packages/astro/src/vite-plugin-astro/compile.ts index c1c62efbe695..2f8beeb8891f 100644 --- a/packages/astro/src/vite-plugin-astro/compile.ts +++ b/packages/astro/src/vite-plugin-astro/compile.ts @@ -1,11 +1,11 @@ -import { type ESBuildTransformResult, transformWithEsbuild } from 'vite'; +import { transformWithOxc } from 'vite'; import { type CompileProps, type CompileResult, compile } from '../core/compile/index.js'; import type { AstroLogger } from '../core/logger/core.js'; import type { AstroConfig } from '../types/public/config.js'; import { getFileInfo } from '../vite-plugin-utils/index.js'; import type { CompileMetadata } from './types.js'; import { frontmatterRE } from './utils.js'; -import type { SourceMapInput } from 'rollup'; +import type { Rolldown } from 'vite'; interface CompileAstroOption { compileProps: CompileProps; @@ -14,7 +14,7 @@ interface CompileAstroOption { } export interface CompileAstroResult extends Omit { - map: SourceMapInput; + map: Rolldown.SourceMapInput; } interface EnhanceCompilerErrorOptions { @@ -31,21 +31,20 @@ export async function compileAstro({ logger, }: CompileAstroOption): Promise { let transformResult: CompileResult; - let esbuildResult: ESBuildTransformResult; + let oxcResult: Awaited>; try { transformResult = await compile(compileProps); // Compile all TypeScript to JavaScript. // Also, catches invalid JS/TS in the compiled output before returning. - esbuildResult = await transformWithEsbuild(transformResult.code, compileProps.filename, { - ...compileProps.viteConfig.esbuild, - loader: 'ts', - sourcemap: 'external', - tsconfigRaw: { + oxcResult = await transformWithOxc(transformResult.code, compileProps.filename, { + ...compileProps.viteConfig.oxc, + lang: 'ts', + sourcemap: true, + tsconfig: { compilerOptions: { // Ensure client:only imports are treeshaken verbatimModuleSyntax: false, - importsNotUsedAsValues: 'remove', }, }, }); @@ -88,8 +87,8 @@ export async function compileAstro({ return { ...transformResult, - code: esbuildResult.code + SUFFIX, - map: esbuildResult.map, + code: oxcResult.code + SUFFIX, + map: oxcResult.map!, }; } @@ -118,8 +117,7 @@ async function enhanceCompileError({ if (lineText && !frontmatter.includes(lineText)) throw err; try { - await transformWithEsbuild(frontmatter, id, { - loader: 'ts', + await transformWithOxc(frontmatter, id, { target: 'esnext', sourcemap: false, }); diff --git a/packages/astro/src/vite-plugin-astro/index.ts b/packages/astro/src/vite-plugin-astro/index.ts index 95635c8c444a..a7630e8dd959 100644 --- a/packages/astro/src/vite-plugin-astro/index.ts +++ b/packages/astro/src/vite-plugin-astro/index.ts @@ -1,5 +1,4 @@ import type { HydratedComponent } from '@astrojs/compiler/types'; -import type { SourceDescription } from 'rollup'; import type * as vite from 'vite'; import { defaultClientConditions, defaultServerConditions, normalizePath } from 'vite'; import { ASTRO_VITE_ENVIRONMENT_NAMES } from '../core/constants.js'; @@ -198,6 +197,7 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl if (isAstroServerEnvironment(this.environment)) { return { code: `/* client script, empty in SSR: ${id} */`, + moduleType: 'ts', }; } @@ -217,8 +217,9 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl } } - const result: SourceDescription = { + const result: vite.Rolldown.SourceDescription = { code: '', + moduleType: 'ts', meta: { vite: { lang: 'ts', @@ -277,6 +278,7 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl ); } : {};`, + moduleType: 'ts', meta: { vite: { lang: 'ts' } }, }; } @@ -298,6 +300,7 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl return { code: transformResult.code, map: transformResult.map, + moduleType: 'ts', meta: { astro: astroMetadata, vite: { diff --git a/packages/astro/src/vite-plugin-head/index.ts b/packages/astro/src/vite-plugin-head/index.ts index 6d2fe1f366f8..dd30e4cb9180 100644 --- a/packages/astro/src/vite-plugin-head/index.ts +++ b/packages/astro/src/vite-plugin-head/index.ts @@ -1,7 +1,6 @@ -import type { ModuleInfo } from 'rollup'; import type * as vite from 'vite'; import type { DevEnvironment } from 'vite'; -import { hasHeadInjectComment } from '../core/head-propagation/comment.js'; +import { hasHeadPropagationCall } from '../core/head-propagation/hint.js'; import { buildImporterGraphFromModuleInfo, computeInTreeAncestors, @@ -74,7 +73,12 @@ export default function configHeadVitePlugin(): vite.Plugin { function propagateMetadata< P extends keyof PluginMetadata['astro'], V extends PluginMetadata['astro'][P], - >(this: { getModuleInfo(id: string): ModuleInfo | null }, seed: string, prop: P, value: V) { + >( + this: { getModuleInfo(id: string): vite.Rolldown.ModuleInfo | null }, + seed: string, + prop: P, + value: V, + ) { // Example: `HeadEntry -> Layout -> /src/pages/blog.astro` marks both ancestors. const importerGraph = buildImporterGraphFromEnvironment(seed); const allAncestors = computeInTreeAncestors({ @@ -165,8 +169,8 @@ export default function configHeadVitePlugin(): vite.Plugin { propagateMetadata.call(this, id, 'containsHead', true); } - if (hasHeadInjectComment(source)) { - // `// astro-head-inject` and `//! astro-head-inject` opt a module into bubbling. + if (hasHeadPropagationCall(source)) { + // `"use astro:head-inject"` directive opts a module into bubbling. propagateMetadata.call(this, id, 'propagation', 'in-tree'); } @@ -176,6 +180,11 @@ export default function configHeadVitePlugin(): vite.Plugin { } export function astroHeadBuildPlugin(internals: BuildInternals): vite.Plugin { + // Collect module IDs that contain a head propagation marker in their raw source + // (before bundling). This is necessary because Rolldown may strip comments and + // directives when concatenating modules into chunks, so scanning `mod.code` in + // `generateBundle` alone is not reliable. + const headPropagationModuleIds = new Set(); return { name: 'astro:head-metadata-build', applyToEnvironment(environment) { @@ -184,12 +193,17 @@ export function astroHeadBuildPlugin(internals: BuildInternals): vite.Plugin { environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender ); }, + transform(source, id) { + if (hasHeadPropagationCall(source)) { + headPropagationModuleIds.add(id); + } + }, generateBundle(_opts, bundle) { const map: SSRResult['componentMetadata'] = internals.componentMetadata; const moduleIds = new Set(); // Explicit runtime entries (`createComponent({ propagation: 'self' })`). const selfPropagationSeeds = new Set(); - // Comment-driven seeds (`astro-head-inject` marker in source). + // Head propagation hint seeds (`"use astro:head-inject"` directive in source). const commentPropagationSeeds = new Set(); function getOrCreateMetadata(id: string): SSRComponentMetadata { if (map.has(id)) return map.get(id)!; @@ -222,7 +236,9 @@ export function astroHeadBuildPlugin(internals: BuildInternals): vite.Plugin { } // Head propagation (aka bubbling) - if (mod.code && hasHeadInjectComment(mod.code)) { + // Check both post-bundle code and pre-bundle transform results, + // since Rolldown may strip markers (comments, directives) during bundling. + if ((mod.code && hasHeadPropagationCall(mod.code)) || headPropagationModuleIds.has(id)) { commentPropagationSeeds.add(id); } } diff --git a/packages/astro/src/vite-plugin-hmr-reload/index.ts b/packages/astro/src/vite-plugin-hmr-reload/index.ts index c7163aff8d9a..cb3a821da02c 100644 --- a/packages/astro/src/vite-plugin-hmr-reload/index.ts +++ b/packages/astro/src/vite-plugin-hmr-reload/index.ts @@ -35,13 +35,25 @@ export default function hmrReload(): Plugin { const invalidatedModules = new Set(); for (const mod of modules) { if (mod.id == null) continue; + // Style modules must be checked first: CSS/SCSS files imported by client + // components exist in both the client and SSR module graphs. If we checked + // the client module graph first, we'd skip them and never set + // hasSkippedStyleModules, causing Vite to trigger a full page reload. if (isStyleModule(mod)) { hasSkippedStyleModules = true; continue; } - - const clientModule = server.environments.client.moduleGraph.getModuleById(mod.id); - if (clientModule != null) continue; + // .astro files always have a client stub injected by the astro:build plugin + // to prevent them from being bundled for the browser. That stub is not a + // real client module, so we must not skip main .astro module entries even + // if a client module entry exists for them. Virtual sub-modules (e.g. + // CSS virtual modules with query params) do have real client counterparts + // and should still be checked. + const isMainAstroModule = mod.id.endsWith('.astro'); + if (!isMainAstroModule) { + const clientModule = server.environments.client.moduleGraph.getModuleById(mod.id); + if (clientModule != null) continue; + } this.environment.moduleGraph.invalidateModule(mod, invalidatedModules, timestamp, true); hasSsrOnlyModules = true; @@ -60,10 +72,19 @@ export default function hmrReload(): Plugin { } if (hasSsrOnlyModules) { - server.ws.send({ type: 'full-reload' }); + server.environments.client.hot.send({ + type: 'full-reload', + path: '*', + }); return []; } + // When style modules were skipped, return an empty array to prevent Vite's + // default SSR HMR propagation. Without this, Vite would propagate through the + // module graph to .astro importers, find no HMR acceptor, and trigger a + // full page reload. The client environment handles CSS HMR natively via + // Vite's built-in style update mechanism, which works for all pages + // (with or without framework components). // When style modules were skipped, return an empty array to prevent Vite's // default SSR HMR propagation. Without this, Vite would propagate through the // module graph to .astro importers, find no HMR acceptor, and trigger a diff --git a/packages/astro/src/vite-plugin-integrations-container/index.ts b/packages/astro/src/vite-plugin-integrations-container/index.ts index f984dcd9aa7f..eaeaca7c0e5d 100644 --- a/packages/astro/src/vite-plugin-integrations-container/index.ts +++ b/packages/astro/src/vite-plugin-integrations-container/index.ts @@ -1,5 +1,4 @@ -import type { PluginContext } from 'rollup'; -import type { Plugin as VitePlugin } from 'vite'; +import type { Plugin as VitePlugin, Rolldown } from 'vite'; import { normalizePath } from 'vite'; import type { AstroLogger } from '../core/logger/core.js'; import { runHookServerSetup } from '../integrations/hooks.js'; @@ -22,7 +21,7 @@ export default function astroIntegrationsContainerPlugin({ }, async buildStart() { if (settings.injectedRoutes.length === settings.resolvedInjectedRoutes.length) return; - // Ensure the injectedRoutes are all resolved to their final paths through Rollup + // Ensure the injectedRoutes are all resolved to their final paths through Rolldown settings.resolvedInjectedRoutes = await Promise.all( settings.injectedRoutes.map((route) => resolveEntryPoint.call(this, route)), ); @@ -31,7 +30,7 @@ export default function astroIntegrationsContainerPlugin({ } async function resolveEntryPoint( - this: PluginContext, + this: Rolldown.PluginContext, route: InternalInjectedRoute, ): Promise { const resolvedId = await this.resolve(route.entrypoint.toString()) diff --git a/packages/astro/src/vite-plugin-pages/util.ts b/packages/astro/src/vite-plugin-pages/util.ts index 18b211f41d20..9e73ea71523e 100644 --- a/packages/astro/src/vite-plugin-pages/util.ts +++ b/packages/astro/src/vite-plugin-pages/util.ts @@ -5,7 +5,7 @@ import { VIRTUAL_PAGE_MODULE_ID } from './const.js'; const ASTRO_PAGE_EXTENSION_POST_PATTERN = '@_@'; /** - * Prevents Rollup from triggering other plugins in the process by masking the extension (hence the virtual file). + * Prevents Rolldown from triggering other plugins in the process by masking the extension (hence the virtual file). * Inverse function of getComponentFromVirtualModulePageName() below. * @param virtualModulePrefix The prefix used to create the virtual module * @param path Page component path diff --git a/packages/astro/templates/content/module.mjs b/packages/astro/templates/content/module.mjs index d3b317101e8f..8588c401d917 100644 --- a/packages/astro/templates/content/module.mjs +++ b/packages/astro/templates/content/module.mjs @@ -1,4 +1,4 @@ -// astro-head-inject +"use astro:head-inject"; import { createDeprecatedFunction, createGetCollection, diff --git a/packages/astro/test/asset-query-params.test.js b/packages/astro/test/asset-query-params.test.js index d197e2c2bfd6..3c9008e34659 100644 --- a/packages/astro/test/asset-query-params.test.js +++ b/packages/astro/test/asset-query-params.test.js @@ -181,11 +181,12 @@ describe('Asset Query Parameters in Inter-Chunk JS Imports', () => { const code = await fixture.readFile(`/${file}`); // Match static imports: from "./chunk.js", from "./chunk.js" const staticImports = [ - ...code.matchAll(/(from\s*["'])(\.\.?\/[^"']+\.(?:js|mjs)(?:\?[^"']*)?)(["'])/g), + ...code.matchAll(/(from\s*["'`])(\.\.?\/[^"'`]+\.(?:js|mjs)(?:\?[^"'`]*)?)(["'`])/g), ]; - // Match dynamic imports: import("./chunk.js") + // Match dynamic imports: import("./chunk.js") or import(`./chunk.js`) + // Note: Rolldown (Vite 8) emits backtick template literals instead of quotes const dynamicImports = [ - ...code.matchAll(/(import\s*\(\s*["'])(\.\.?\/[^"']+\.(?:js|mjs)(?:\?[^"']*)?)(["'])/g), + ...code.matchAll(/(import\s*\(\s*["'`])(\.\.?\/[^"'`]+\.(?:js|mjs)(?:\?[^"'`]*)?)(["'`])/g), ]; for (const match of staticImports) { foundStaticImport = true; diff --git a/packages/astro/test/astro-component-bundling.test.ts b/packages/astro/test/astro-component-bundling.test.ts index b16a950481c7..87d7dce3eab6 100644 --- a/packages/astro/test/astro-component-bundling.test.ts +++ b/packages/astro/test/astro-component-bundling.test.ts @@ -62,7 +62,7 @@ describe('Component bundling', () => { assert(match, 'Expected a - -` ---- - -``` - -See the [`` component documentation](https://v6.docs.astro.build/en/guides/syntax-highlighting/#code-) for more details. From c30a7789a477e44826c54c8560587d09dc46a229 Mon Sep 17 00:00:00 2001 From: Erika <3019731+Princesseuh@users.noreply.github.com> Date: Wed, 29 Apr 2026 13:59:53 +0200 Subject: [PATCH 08/72] feat: make the Rust compiler the only option (#16462) Co-authored-by: Armand Philippot --- .changeset/rusty-compilers-only.md | 11 ++ .../src/components/AttributedLayout.astro | 2 +- .../src/pages/non-html-anchor.astro | 1 + packages/astro/package.json | 3 +- packages/astro/src/core/compile/compile-rs.ts | 155 ------------------ packages/astro/src/core/compile/compile.ts | 54 +++--- .../astro/src/core/config/schemas/base.ts | 2 - packages/astro/src/types/public/config.ts | 23 --- .../astro/src/vite-plugin-astro/compile-rs.ts | 52 ------ .../astro/src/vite-plugin-astro/compile.ts | 94 +---------- packages/astro/src/vite-plugin-astro/index.ts | 48 ++---- packages/astro/src/vite-plugin-astro/types.ts | 4 +- packages/astro/test/csp.test.ts | 2 +- .../test/units/compile/css-base-path.test.ts | 31 ++-- .../test/units/compile/rust-compiler.test.ts | 143 ---------------- .../units/vite-plugin-astro/compile.test.ts | 19 --- .../test/prerender-node-env.test.ts | 2 +- packages/integrations/node/test/url.test.ts | 12 +- pnpm-lock.yaml | 111 ++++++------- 19 files changed, 143 insertions(+), 626 deletions(-) create mode 100644 .changeset/rusty-compilers-only.md delete mode 100644 packages/astro/src/core/compile/compile-rs.ts delete mode 100644 packages/astro/src/vite-plugin-astro/compile-rs.ts delete mode 100644 packages/astro/test/units/compile/rust-compiler.test.ts diff --git a/.changeset/rusty-compilers-only.md b/.changeset/rusty-compilers-only.md new file mode 100644 index 000000000000..9c5f595727ad --- /dev/null +++ b/.changeset/rusty-compilers-only.md @@ -0,0 +1,11 @@ +--- +'astro': major +--- + +Replaces the Go compiler with a Rust-based version. + +The Rust-based Astro compiler (`@astrojs/compiler-rs`) is now the default compiler. This new compiler is faster and more reliable, leading to faster build times and iteration cycles during development. + +This new compiler is more strict regarding invalid syntax. For example, unclosed HTML tags will now throw an error instead of being ignored. It also does not attempt to correct semantically invalid HTML anymore, instead leaving it to the browser to handle, similar to other tools or `document.write()` in JavaScript. + +The previous Go-based compiler has been removed, along with the `experimental.rustCompiler` flag used to opt into the Rust compiler. If you were setting `experimental.rustCompiler` in your `astro.config.mjs`, you can now remove it. No other action is required. diff --git a/packages/astro/e2e/fixtures/view-transitions/src/components/AttributedLayout.astro b/packages/astro/e2e/fixtures/view-transitions/src/components/AttributedLayout.astro index 0d0e7a4c743b..bde541f70fa6 100644 --- a/packages/astro/e2e/fixtures/view-transitions/src/components/AttributedLayout.astro +++ b/packages/astro/e2e/fixtures/view-transitions/src/components/AttributedLayout.astro @@ -1,6 +1,6 @@ --- import { ClientRouter } from 'astro:transitions'; -import { HTMLAttributes } from 'astro/types'; +import type { HTMLAttributes } from 'astro/types'; interface Props extends HTMLAttributes<'html'> {} --- diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/non-html-anchor.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/non-html-anchor.astro index 8d5ea8d46c76..55922fa9f195 100644 --- a/packages/astro/e2e/fixtures/view-transitions/src/pages/non-html-anchor.astro +++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/non-html-anchor.astro @@ -20,3 +20,4 @@ import Layout from '../components/Layout.astro'; background: #888; } + diff --git a/packages/astro/package.json b/packages/astro/package.json index 41791f1bc04e..7b17b11ace9c 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -120,7 +120,7 @@ "test:integration:ts": "astro-scripts test \"test/*.test.ts\" --strip-types" }, "dependencies": { - "@astrojs/compiler": "^3.0.1", + "@astrojs/compiler-rs": "^0.1.10", "@astrojs/internal-helpers": "workspace:*", "@astrojs/markdown-remark": "workspace:*", "@astrojs/telemetry": "workspace:*", @@ -180,7 +180,6 @@ }, "devDependencies": { "@astrojs/check": "workspace:*", - "@astrojs/compiler-rs": "^0.1.6", "@playwright/test": "1.58.2", "@types/aria-query": "^5.0.4", "@types/hast": "^3.0.4", diff --git a/packages/astro/src/core/compile/compile-rs.ts b/packages/astro/src/core/compile/compile-rs.ts deleted file mode 100644 index c3a296de358d..000000000000 --- a/packages/astro/src/core/compile/compile-rs.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { fileURLToPath } from 'node:url'; -import type { ResolvedConfig } from 'vite'; -import type { AstroConfig } from '../../types/public/config.js'; -import type { AstroError } from '../errors/errors.js'; -import { AggregateError, CompilerError } from '../errors/errors.js'; -import { AstroErrorData } from '../errors/index.js'; -import { normalizePath, resolvePath } from '../viteUtils.js'; -import { createStylePreprocessor, type PartialCompileCssResult } from './style.js'; -import type { CompileCssResult } from './types.js'; - -export interface CompileProps { - astroConfig: AstroConfig; - viteConfig: ResolvedConfig; - toolbarEnabled: boolean; - filename: string; - source: string; -} - -export interface CompileResult { - code: string; - map: string; - scope: string; - css: CompileCssResult[]; - scripts: any[]; - hydratedComponents: any[]; - clientOnlyComponents: any[]; - serverComponents: any[]; - containsHead: boolean; - propagation: boolean; - styleError: string[]; - diagnostics: any[]; -} - -export async function compile({ - astroConfig, - viteConfig, - toolbarEnabled, - filename, - source, -}: CompileProps): Promise { - let preprocessStyles; - let transform; - try { - ({ preprocessStyles, transform } = await import('@astrojs/compiler-rs')); - } catch (err: unknown) { - throw new Error( - `Failed to load @astrojs/compiler-rs. Make sure it is installed and up to date. Original error: ${err}`, - ); - } - - const cssPartialCompileResults: PartialCompileCssResult[] = []; - const cssTransformErrors: AstroError[] = []; - let transformResult: any; - - try { - const preprocessedStyles = await preprocessStyles( - source, - createStylePreprocessor({ - filename, - viteConfig, - astroConfig, - cssPartialCompileResults, - cssTransformErrors, - }), - ); - - transformResult = transform(source, { - compact: astroConfig.compressHTML, - filename, - normalizedFilename: normalizeFilename(filename, astroConfig.root), - sourcemap: 'both', - internalURL: 'astro/compiler-runtime', - // TODO: remove in Astro v7 - astroGlobalArgs: JSON.stringify(astroConfig.site), - scopedStyleStrategy: astroConfig.scopedStyleStrategy, - resultScopedSlot: true, - transitionsAnimationURL: 'astro/components/viewtransitions.css', - annotateSourceFile: - viteConfig.command === 'serve' && - astroConfig.devToolbar && - astroConfig.devToolbar.enabled && - toolbarEnabled, - preprocessedStyles, - resolvePath(specifier) { - return resolvePath(specifier, filename); - }, - }); - } catch (err: any) { - // The compiler should be able to handle errors by itself, however - // for the rare cases where it can't let's directly throw here with as much info as possible - throw new CompilerError({ - ...AstroErrorData.UnknownCompilerError, - message: err.message ?? 'Unknown compiler error', - stack: err.stack, - location: { - file: filename, - }, - }); - } - - handleCompileResultErrors(filename, transformResult, cssTransformErrors); - - return { - ...transformResult, - css: transformResult.css.map((code: string, i: number) => ({ - ...cssPartialCompileResults[i], - code, - })), - }; -} - -function handleCompileResultErrors( - filename: string, - result: any, - cssTransformErrors: AstroError[], -) { - const compilerError = result.diagnostics.find((diag: any) => diag.severity === 'error'); - - if (compilerError) { - throw new CompilerError({ - name: 'CompilerError', - message: compilerError.text, - location: { - line: compilerError.labels[0].line, - column: compilerError.labels[0].column, - file: filename, - }, - hint: compilerError.hint, - }); - } - - switch (cssTransformErrors.length) { - case 0: - break; - case 1: { - throw cssTransformErrors[0]; - } - default: { - throw new AggregateError({ - ...cssTransformErrors[0], - errors: cssTransformErrors, - }); - } - } -} - -function normalizeFilename(filename: string, root: URL) { - const normalizedFilename = normalizePath(filename); - const normalizedRoot = normalizePath(fileURLToPath(root)); - if (normalizedFilename.startsWith(normalizedRoot)) { - return normalizedFilename.slice(normalizedRoot.length - 1); - } else { - return normalizedFilename; - } -} diff --git a/packages/astro/src/core/compile/compile.ts b/packages/astro/src/core/compile/compile.ts index 66b8d4b96608..7d30ac938133 100644 --- a/packages/astro/src/core/compile/compile.ts +++ b/packages/astro/src/core/compile/compile.ts @@ -1,6 +1,9 @@ import { fileURLToPath } from 'node:url'; -import type { TransformResult } from '@astrojs/compiler'; -import { transform } from '@astrojs/compiler'; +import { + preprocessStyles, + transform, + type TransformResult, +} from '@astrojs/compiler-rs'; import type { ResolvedConfig } from 'vite'; import type { AstroConfig } from '../../types/public/config.js'; import type { AstroError } from '../errors/errors.js'; @@ -29,18 +32,23 @@ export async function compile({ filename, source, }: CompileProps): Promise { - // Because `@astrojs/compiler` can't return the dependencies for each style transformed, - // we need to use an external array to track the dependencies whenever preprocessing is called, - // and we'll rebuild the final `css` result after transformation. const cssPartialCompileResults: PartialCompileCssResult[] = []; const cssTransformErrors: AstroError[] = []; let transformResult: TransformResult; try { - // Transform from `.astro` to valid `.ts` - // use `sourcemap: "both"` so that sourcemap is included in the code - // result passed to esbuild, but also available in the catch handler. - transformResult = await transform(source, { + const preprocessedStyles = await preprocessStyles( + source, + createStylePreprocessor({ + filename, + viteConfig, + astroConfig, + cssPartialCompileResults, + cssTransformErrors, + }), + ); + + transformResult = transform(source, { compact: astroConfig.compressHTML, filename, normalizedFilename: normalizeFilename(filename, astroConfig.root), @@ -56,14 +64,8 @@ export async function compile({ astroConfig.devToolbar && astroConfig.devToolbar.enabled && toolbarEnabled, - preprocessStyle: createStylePreprocessor({ - filename, - viteConfig, - astroConfig, - cssPartialCompileResults, - cssTransformErrors, - }), - async resolvePath(specifier) { + preprocessedStyles, + resolvePath(specifier) { return resolvePath(specifier, filename); }, }); @@ -80,7 +82,7 @@ export async function compile({ }); } - handleCompileResultErrors(transformResult, cssTransformErrors); + handleCompileResultErrors(filename, transformResult, cssTransformErrors); return { ...transformResult, @@ -91,19 +93,21 @@ export async function compile({ }; } -function handleCompileResultErrors(result: TransformResult, cssTransformErrors: AstroError[]) { - // TODO: Export the DiagnosticSeverity enum from @astrojs/compiler? - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison - const compilerError = result.diagnostics.find((diag) => diag.severity === 1); +function handleCompileResultErrors( + filename: string, + result: TransformResult, + cssTransformErrors: AstroError[], +) { + const compilerError = result.diagnostics.find((diag) => diag.severity === 'error'); if (compilerError) { throw new CompilerError({ name: 'CompilerError', message: compilerError.text, location: { - line: compilerError.location.line, - column: compilerError.location.column, - file: compilerError.location.file, + line: compilerError.labels[0].line, + column: compilerError.labels[0].column, + file: filename, }, hint: compilerError.hint, }); diff --git a/packages/astro/src/core/config/schemas/base.ts b/packages/astro/src/core/config/schemas/base.ts index f9cc154f3f1e..61ebffd0acf0 100644 --- a/packages/astro/src/core/config/schemas/base.ts +++ b/packages/astro/src/core/config/schemas/base.ts @@ -110,7 +110,6 @@ export const ASTRO_CONFIG_DEFAULTS = { contentIntellisense: false, chromeDevtoolsWorkspace: false, svgo: false, - rustCompiler: false, queuedRendering: { enabled: false, }, @@ -539,7 +538,6 @@ export const AstroConfigSchema = z.object({ .default(ASTRO_CONFIG_DEFAULTS.experimental.svgo), cache: CacheSchema.optional(), routeRules: RouteRulesSchema.optional(), - rustCompiler: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.rustCompiler), queuedRendering: z .object({ enabled: z.boolean().optional().prefault(false), diff --git a/packages/astro/src/types/public/config.ts b/packages/astro/src/types/public/config.ts index 57029eaf177c..82bdcc587793 100644 --- a/packages/astro/src/types/public/config.ts +++ b/packages/astro/src/types/public/config.ts @@ -2974,29 +2974,6 @@ export interface AstroUserConfig< * ``` */ routeRules?: RouteRules; - /* - * @name experimental.rustCompiler - * @type {boolean} - * @default `false` - * @version 6.0.0 - * @description - * - * Enables the experimental Rust-based Astro compiler (`@astrojs/compiler-rs`) as a replacement to the current Go compiler. - * - * This option requires installing the `@astrojs/compiler-rs` package manually in your project. This compiler is a work in progress and may not yet support all features of the current Go compiler, but it should offer improved performance and better error messages. This compiler is more strict than the previous Go compiler regarding invalid syntax. For instance, unclosed HTML tags or missing closing brackets will throw an error instead of being ignored. - * - * ```js - * // astro.config.mjs - * import { defineConfig } from 'astro/config'; - * - * export default defineConfig({ - * experimental: { - * rustCompiler: true, - * }, - * }); - * ``` - */ - rustCompiler?: boolean; /** * @name experimental.queuedRendering diff --git a/packages/astro/src/vite-plugin-astro/compile-rs.ts b/packages/astro/src/vite-plugin-astro/compile-rs.ts deleted file mode 100644 index ea072e766aa0..000000000000 --- a/packages/astro/src/vite-plugin-astro/compile-rs.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { Rolldown } from 'vite'; -import { type CompileProps, type CompileResult, compile } from '../core/compile/compile-rs.js'; -import { getFileInfo } from '../vite-plugin-utils/index.js'; -import type { CompileMetadata } from './types.js'; - -interface CompileAstroOption { - compileProps: CompileProps; - astroFileToCompileMetadata: Map; -} - -export interface CompileAstroResult extends Omit { - map: Rolldown.SourceMapInput; -} - -export async function compileAstro({ - compileProps, - astroFileToCompileMetadata, -}: CompileAstroOption): Promise { - const transformResult = await compile(compileProps); - - const { fileId: file, fileUrl: url } = getFileInfo( - compileProps.filename, - compileProps.astroConfig, - ); - - let SUFFIX = ''; - SUFFIX += `\nconst $$file = ${JSON.stringify(file)};\nconst $$url = ${JSON.stringify( - url, - )};export { $$file as file, $$url as url };\n`; - - // Add HMR handling in dev mode. - if (!compileProps.viteConfig.isProduction) { - let i = 0; - while (i < transformResult.scripts.length) { - SUFFIX += `import "${compileProps.filename}?astro&type=script&index=${i}&lang.ts";`; - i++; - } - } - - // Attach compile metadata to map for use by virtual modules - astroFileToCompileMetadata.set(compileProps.filename, { - originalCode: compileProps.source, - css: transformResult.css, - scripts: transformResult.scripts, - }); - - return { - ...transformResult, - code: transformResult.code + SUFFIX, - map: transformResult.map || null, - }; -} diff --git a/packages/astro/src/vite-plugin-astro/compile.ts b/packages/astro/src/vite-plugin-astro/compile.ts index 2f8beeb8891f..4f07744540a6 100644 --- a/packages/astro/src/vite-plugin-astro/compile.ts +++ b/packages/astro/src/vite-plugin-astro/compile.ts @@ -1,63 +1,22 @@ -import { transformWithOxc } from 'vite'; +import type { Rolldown } from 'vite'; import { type CompileProps, type CompileResult, compile } from '../core/compile/index.js'; -import type { AstroLogger } from '../core/logger/core.js'; -import type { AstroConfig } from '../types/public/config.js'; import { getFileInfo } from '../vite-plugin-utils/index.js'; import type { CompileMetadata } from './types.js'; -import { frontmatterRE } from './utils.js'; -import type { Rolldown } from 'vite'; interface CompileAstroOption { compileProps: CompileProps; astroFileToCompileMetadata: Map; - logger: AstroLogger; } export interface CompileAstroResult extends Omit { map: Rolldown.SourceMapInput; } -interface EnhanceCompilerErrorOptions { - err: Error; - id: string; - source: string; - config: AstroConfig; - logger: AstroLogger; -} - export async function compileAstro({ compileProps, astroFileToCompileMetadata, - logger, }: CompileAstroOption): Promise { - let transformResult: CompileResult; - let oxcResult: Awaited>; - - try { - transformResult = await compile(compileProps); - // Compile all TypeScript to JavaScript. - // Also, catches invalid JS/TS in the compiled output before returning. - oxcResult = await transformWithOxc(transformResult.code, compileProps.filename, { - ...compileProps.viteConfig.oxc, - lang: 'ts', - sourcemap: true, - tsconfig: { - compilerOptions: { - // Ensure client:only imports are treeshaken - verbatimModuleSyntax: false, - }, - }, - }); - } catch (err: any) { - await enhanceCompileError({ - err, - id: compileProps.filename, - source: compileProps.source, - config: compileProps.astroConfig, - logger: logger, - }); - throw err; - } + const transformResult = await compile(compileProps); const { fileId: file, fileUrl: url } = getFileInfo( compileProps.filename, @@ -87,52 +46,7 @@ export async function compileAstro({ return { ...transformResult, - code: oxcResult.code + SUFFIX, - map: oxcResult.map!, + code: transformResult.code + SUFFIX, + map: transformResult.map || null, }; } - -async function enhanceCompileError({ - err, - id, - source, -}: EnhanceCompilerErrorOptions): Promise { - const lineText = (err as any).loc?.lineText; - // Verify frontmatter: a common reason that this plugin fails is that - // the user provided invalid JS/TS in the component frontmatter. - // If the frontmatter is invalid, the `err` object may be a compiler - // panic or some other vague/confusing compiled error message. - // - // Before throwing, it is better to verify the frontmatter here, and - // let esbuild throw a more specific exception if the code is invalid. - // If frontmatter is valid or cannot be parsed, then continue. - const scannedFrontmatter = frontmatterRE.exec(source); - if (scannedFrontmatter) { - // Top-level return is not supported, so replace `return` with throw - const frontmatter = scannedFrontmatter[1] - .replace(/\breturn\s*;/g, 'throw 0;') - .replace(/\breturn\b/g, 'throw '); - - // If frontmatter does not actually include the offending line, skip - if (lineText && !frontmatter.includes(lineText)) throw err; - - try { - await transformWithOxc(frontmatter, id, { - target: 'esnext', - sourcemap: false, - }); - } catch (frontmatterErr: any) { - // Improve the error by replacing the phrase "unexpected end of file" - // with "unexpected end of frontmatter" in the esbuild error message. - if (frontmatterErr?.message) { - frontmatterErr.message = frontmatterErr.message.replace( - 'end of file', - 'end of frontmatter', - ); - } - throw frontmatterErr; - } - } - - throw err; -} diff --git a/packages/astro/src/vite-plugin-astro/index.ts b/packages/astro/src/vite-plugin-astro/index.ts index a7630e8dd959..c72bb2da15bf 100644 --- a/packages/astro/src/vite-plugin-astro/index.ts +++ b/packages/astro/src/vite-plugin-astro/index.ts @@ -1,4 +1,3 @@ -import type { HydratedComponent } from '@astrojs/compiler/types'; import type * as vite from 'vite'; import { defaultClientConditions, defaultServerConditions, normalizePath } from 'vite'; import { ASTRO_VITE_ENVIRONMENT_NAMES } from '../core/constants.js'; @@ -8,10 +7,13 @@ import type { AstroSettings } from '../types/astro.js'; import type { AstroConfig } from '../types/public/config.js'; import { normalizeFilename, specialQueriesRE } from '../vite-plugin-utils/index.js'; import { type CompileAstroResult, compileAstro } from './compile.js'; -import { compileAstro as compileAstroRs } from './compile-rs.js'; import { handleHotUpdate } from './hmr.js'; import { parseAstroRequest } from './query.js'; -import type { PluginMetadata as AstroPluginMetadata, CompileMetadata } from './types.js'; +import type { + AstroComponent, + PluginMetadata as AstroPluginMetadata, + CompileMetadata, +} from './types.js'; import { loadId } from './utils.js'; export { getAstroMetadata } from './metadata.js'; @@ -38,7 +40,7 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl // Variables for determining if an id starts with /src... const srcRootWeb = config.srcDir.pathname.slice(config.root.pathname.length - 1); const isBrowserPath = (path: string) => path.startsWith(srcRootWeb) && srcRootWeb !== '/'; - const notAstroComponent = (component: HydratedComponent) => + const notAstroComponent = (component: AstroComponent) => !component.resolvedPath.endsWith('.astro'); return [ @@ -90,23 +92,15 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl const toolbarEnabled = await settings.preferences.get('devToolbar.enabled'); // Initialize `compile` function to simplify usage later compile = (code, filename) => { - const compileProps = { - astroConfig: config, - viteConfig, - toolbarEnabled, - filename, - source: code, - }; - if (config.experimental.rustCompiler) { - return compileAstroRs({ - compileProps, - astroFileToCompileMetadata, - }); - } return compileAstro({ - compileProps, + compileProps: { + astroConfig: config, + viteConfig, + toolbarEnabled, + filename, + source: code, + }, astroFileToCompileMetadata, - logger, }); }; }, @@ -207,7 +201,7 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl } if (script.type === 'external') { - const src = script.src; + const src = script.src!; if (src.startsWith('/') && !isBrowserPath(src)) { const publicDir = config.publicDir.pathname.replace(/\/$/, '').split('/').pop() + '/'; @@ -229,13 +223,11 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl switch (script.type) { case 'inline': { - const { code, map } = script; - result.code = appendSourceMap(code, map); + result.code = script.code ?? ''; break; } case 'external': { - const { src } = script; - result.code = `import "${src}"`; + result.code = `import "${script.src}"`; break; } } @@ -331,11 +323,3 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl ]; } -function appendSourceMap(content: string, map?: string) { - if (!map) return content; - // The \n here is on purpose inside a template literal because otherwise, in the final built version of this file, the comment would - // start on its own line, and some tools will think it's actually the sourcemap of this file, not of generated code. - return `${content}${'\n//#'} sourceMappingURL=data:application/json;charset=utf-8;base64,${Buffer.from( - map, - ).toString('base64')}`; -} diff --git a/packages/astro/src/vite-plugin-astro/types.ts b/packages/astro/src/vite-plugin-astro/types.ts index 0cb0c70c0799..43f0f9cf7eda 100644 --- a/packages/astro/src/vite-plugin-astro/types.ts +++ b/packages/astro/src/vite-plugin-astro/types.ts @@ -1,4 +1,4 @@ -import type { HoistedScript, TransformResult } from '@astrojs/compiler'; +import type { HoistedScript, TransformResult } from '@astrojs/compiler-rs'; import type { CompileCssResult } from '../core/compile/types.js'; import type { PropagationHint } from '../types/public/internal.js'; @@ -6,6 +6,8 @@ interface PageOptions { prerender?: boolean; } +export type AstroComponent = TransformResult['hydratedComponents'][number]; + export interface PluginMetadata { astro: { hydratedComponents: TransformResult['hydratedComponents']; diff --git a/packages/astro/test/csp.test.ts b/packages/astro/test/csp.test.ts index f9a3943e71e6..23de68c34ae4 100644 --- a/packages/astro/test/csp.test.ts +++ b/packages/astro/test/csp.test.ts @@ -20,7 +20,7 @@ describe('CSP', () => { const styleDigest = await generateCspDigest(styleContent, 'SHA-256'); const meta = $('meta[http-equiv="Content-Security-Policy"]'); - assert.match(meta.attr('content')!, new RegExp(`'${styleDigest}'`)); + assert.ok(meta.attr('content')!.includes(`'${styleDigest}'`)); }); it('should generate hashes and directives for fonts', async () => { diff --git a/packages/astro/test/units/compile/css-base-path.test.ts b/packages/astro/test/units/compile/css-base-path.test.ts index 5cb41c56d66f..5e9ba3ec38f5 100644 --- a/packages/astro/test/units/compile/css-base-path.test.ts +++ b/packages/astro/test/units/compile/css-base-path.test.ts @@ -5,10 +5,6 @@ import { resolveConfig } from 'vite'; import { compileAstro } from '../../../dist/vite-plugin-astro/compile.js'; import type { AstroConfig } from '../../../dist/types/public/config.js'; import type { CompileProps } from '../../../dist/core/compile/compile.js'; -import { AstroLogger } from '../../../dist/core/logger/core.js'; -import { nodeLogDestination } from '../../../dist/core/logger/node.js'; - -const logger = new AstroLogger({ destination: nodeLogDestination, level: 'silent' }); /** Compile Astro source with a given base path. */ async function compileWithBase(source: string, base = '/') { @@ -29,7 +25,6 @@ async function compileWithBase(source: string, base = '/') { return compileAstro({ compileProps: props as any, astroFileToCompileMetadata: new Map(), - logger, }); } @@ -57,7 +52,7 @@ describe('CSS Base Path Rewriting', () => { const result = await compileWithBase(source, '/my-base/'); const css = result.css[0].code; - assert.match(css, /url\(\/my-base\/images\/bg\.png\)/); + assert.match(css, /url\(['"]?\/my-base\/images\/bg\.png['"]?\)/); }); it('should rewrite double-quoted URLs', async () => { @@ -82,7 +77,7 @@ describe('CSS Base Path Rewriting', () => { const result = await compileWithBase(source, '/my-base'); const css = result.css[0].code; - assert.match(css, /url\(\/my-base\/images\/bg\.png\)/); + assert.match(css, /url\(['"]?\/my-base\/images\/bg\.png['"]?\)/); }); it('should handle nested base paths', async () => { @@ -90,7 +85,7 @@ describe('CSS Base Path Rewriting', () => { const result = await compileWithBase(source, '/path/to/app/'); const css = result.css[0].code; - assert.match(css, /url\(\/path\/to\/app\/images\/bg\.png\)/); + assert.match(css, /url\(['"]?\/path\/to\/app\/images\/bg\.png['"]?\)/); }); it('should handle multiple URLs in one declaration', async () => { @@ -109,7 +104,7 @@ describe('CSS Base Path Rewriting', () => { const result = await compileWithBase(source, '/my-base/'); const css = result.css[0].code; - assert.match(css, /url\(\.\/local\.png\)/); + assert.match(css, /url\(['"]?\.\/local\.png['"]?\)/); assert.doesNotMatch(css, /\/my-base/); }); @@ -118,7 +113,7 @@ describe('CSS Base Path Rewriting', () => { const result = await compileWithBase(source, '/my-base/'); const css = result.css[0].code; - assert.match(css, /url\(\.\.\/parent\.png\)/); + assert.match(css, /url\(['"]?\.\.\/parent\.png['"]?\)/); assert.doesNotMatch(css, /\/my-base/); }); @@ -127,7 +122,7 @@ describe('CSS Base Path Rewriting', () => { const result = await compileWithBase(source, '/my-base/'); const css = result.css[0].code; - assert.match(css, /url\(https:\/\/example\.com\/image\.png\)/); + assert.match(css, /url\(['"]?https:\/\/example\.com\/image\.png['"]?\)/); assert.doesNotMatch(css, /\/my-base/); }); @@ -136,7 +131,7 @@ describe('CSS Base Path Rewriting', () => { const result = await compileWithBase(source, '/my-base/'); const css = result.css[0].code; - assert.match(css, /url\(http:\/\/example\.com\/image\.png\)/); + assert.match(css, /url\(['"]?http:\/\/example\.com\/image\.png['"]?\)/); assert.doesNotMatch(css, /\/my-base/); }); @@ -145,7 +140,7 @@ describe('CSS Base Path Rewriting', () => { const result = await compileWithBase(source, '/my-base/'); const css = result.css[0].code; - assert.match(css, /url\(data:image\/svg\+xml/); + assert.match(css, /url\(['"]?data:image\/svg\+xml/); assert.doesNotMatch(css, /\/my-base/); }); @@ -154,7 +149,7 @@ describe('CSS Base Path Rewriting', () => { const result = await compileWithBase(source, '/my-base/'); const css = result.css[0].code; - assert.match(css, /url\(\/\/cdn\.example\.com\/image\.png\)/); + assert.match(css, /url\(['"]?\/\/cdn\.example\.com\/image\.png['"]?\)/); // Should not have /my-base// (double slash would be wrong) assert.doesNotMatch(css, /\/my-base\/\//); }); @@ -171,8 +166,8 @@ describe('CSS Base Path Rewriting', () => { const css = result.css[0].code; // Should NOT add extra slash - assert.match(css, /url\(\/images\/bg\.png\)/); - assert.doesNotMatch(css, /url\(\/\/images/); + assert.match(css, /url\(['"]?\/images\/bg\.png['"]?\)/); + assert.doesNotMatch(css, /url\(['"]?\/\/images/); }); it('should be idempotent (not double-apply base)', async () => { @@ -181,7 +176,7 @@ describe('CSS Base Path Rewriting', () => { const css = result.css[0].code; // Should not become /my-base/my-base/images/bg.png - assert.match(css, /url\(\/my-base\/images\/bg\.png\)/); + assert.match(css, /url\(['"]?\/my-base\/images\/bg\.png['"]?\)/); assert.doesNotMatch(css, /\/my-base\/my-base/); }); @@ -198,7 +193,7 @@ describe('CSS Base Path Rewriting', () => { const result = await compileWithBase(source, ''); const css = result.css[0].code; - assert.match(css, /url\(\/images\/bg\.png\)/); + assert.match(css, /url\(['"]?\/images\/bg\.png['"]?\)/); }); }); diff --git a/packages/astro/test/units/compile/rust-compiler.test.ts b/packages/astro/test/units/compile/rust-compiler.test.ts deleted file mode 100644 index 0c53e68d7823..000000000000 --- a/packages/astro/test/units/compile/rust-compiler.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -import * as assert from 'node:assert/strict'; -import { describe, it } from 'node:test'; -import { pathToFileURL } from 'node:url'; -import { resolveConfig } from 'vite'; -import { compile } from '../../../dist/core/compile/compile-rs.js'; -import type { AstroConfig } from '../../../dist/types/public/config.js'; - -async function compileWithRust(source: string, configOverrides: Partial = {}) { - const viteConfig = await resolveConfig({ configFile: false }, 'serve'); - return compile({ - astroConfig: { - root: pathToFileURL('/'), - base: '/', - experimental: { rustCompiler: true }, - compressHTML: false, - scopedStyleStrategy: 'attribute', - devToolbar: { enabled: false }, - site: undefined, - ...configOverrides, - } as AstroConfig, - viteConfig, - toolbarEnabled: false, - filename: '/src/components/index.astro', - source, - }); -} - -describe('experimental.rustCompiler - core compile', () => { - it('compiles a basic Astro component', async () => { - const result = await compileWithRust('

    Hello World

    '); - assert.ok(result.code); - }); - - it('compiles a component with frontmatter', async () => { - const result = await compileWithRust(`\ ---- -const greeting = 'Hello'; ---- -

    {greeting}

    `); - assert.ok(result.code); - }); - - it('returns a source map', async () => { - const result = await compileWithRust('

    Hello

    '); - assert.ok(result.map); - }); - - it('returns a scope string', async () => { - const result = await compileWithRust('

    Hello

    '); - assert.equal(typeof result.scope, 'string'); - }); - - it('returns populated css array for styled components', async () => { - const result = await compileWithRust(`\ - -

    Hello

    `); - assert.ok(Array.isArray(result.css)); - assert.equal(result.css.length, 1); - assert.ok(result.css[0].code); - }); - - it('returns empty css array for unstyled components', async () => { - const result = await compileWithRust('

    Hello

    '); - assert.ok(Array.isArray(result.css)); - assert.equal(result.css.length, 0); - }); - - it('returns populated scripts array for components with scripts', async () => { - const result = await compileWithRust(`\ - -

    Hello

    `); - assert.ok(Array.isArray(result.scripts)); - assert.equal(result.scripts.length, 1); - }); - - it('returns empty scripts array for components without scripts', async () => { - const result = await compileWithRust('

    Hello

    '); - assert.ok(Array.isArray(result.scripts)); - assert.equal(result.scripts.length, 0); - }); - - it('detects head content', async () => { - const result = await compileWithRust(`\ - - My Page - -

    Hello

    `); - assert.equal(result.containsHead, true); - }); - - it('reports containsHead as false when no head element present', async () => { - const result = await compileWithRust('

    Hello

    '); - assert.equal(result.containsHead, false); - }); - - it('marks global styles with isGlobal', async () => { - const result = await compileWithRust(`\ - -

    Global

    `); - assert.equal(result.css.length, 1); - assert.equal(result.css[0].isGlobal, true); - }); - - it('marks scoped styles with isGlobal false', async () => { - const result = await compileWithRust(`\ - -

    Scoped

    `); - assert.equal(result.css.length, 1); - assert.equal(result.css[0].isGlobal, false); - }); - - it('returns one css entry per style block', async () => { - const result = await compileWithRust(`\ - - -

    Hello

    -

    World

    `); - assert.equal(result.css.length, 2); - }); - - it('throws a CompilerError on unclosed tags', async () => { - await assert.rejects( - () => compileWithRust('

    Unclosed tag'), - (err: unknown) => { - const e = err as { message?: string; name?: string }; - assert.ok(e.message || e.name); - assert.ok(e.message?.includes('Unexpected token')); - return true; - }, - ); - }); - - it('handles empty component without throwing', async () => { - const result = await compileWithRust(''); - assert.ok(result.code !== undefined); - }); -}); diff --git a/packages/astro/test/units/vite-plugin-astro/compile.test.ts b/packages/astro/test/units/vite-plugin-astro/compile.test.ts index 6fcc2a538823..5f8a59e3eee2 100644 --- a/packages/astro/test/units/vite-plugin-astro/compile.test.ts +++ b/packages/astro/test/units/vite-plugin-astro/compile.test.ts @@ -92,25 +92,6 @@ const name = 'world assert.equal(names.includes('file'), true); assert.equal(names.includes('url'), true); }); - - describe('when the code contains syntax that is transformed by esbuild', () => { - const code = `\ ---- -using x = {} ----`; - - it('should not transform the syntax by default', async () => { - const result = await compile(code, '/src/components/index.astro'); - assert.equal(result.code.includes('using x = {}'), true); - }); - - it('should transform the syntax by oxc.target', async () => { - const result = await compile(code, '/src/components/index.astro', { - oxc: { target: 'es2018' }, - }); - assert.equal(result.code.includes('using x = {}'), false, 'Code contains\n' + result.code); - }); - }); }); // #endregion diff --git a/packages/integrations/cloudflare/test/prerender-node-env.test.ts b/packages/integrations/cloudflare/test/prerender-node-env.test.ts index 61e5dfa03318..28b686879ba4 100644 --- a/packages/integrations/cloudflare/test/prerender-node-env.test.ts +++ b/packages/integrations/cloudflare/test/prerender-node-env.test.ts @@ -55,7 +55,7 @@ describe('prerenderEnvironment: node', () => { const res = await fixture.fetch('/'); const html = await res.text(); assert.ok( - html.includes('rebeccapurple'), + html.includes('#639'), 'Expected scoped styles to be included in the prerendered page', ); }); diff --git a/packages/integrations/node/test/url.test.ts b/packages/integrations/node/test/url.test.ts index 2c7b4fbac32f..439ccf5a695f 100644 --- a/packages/integrations/node/test/url.test.ts +++ b/packages/integrations/node/test/url.test.ts @@ -93,7 +93,7 @@ describe('URL', () => { const html = await text(); const $ = cheerio.load(html); - assert.equal($('body').text(), 'https://abc.xyz:444/'); + assert.equal($('body').text().trim(), 'https://abc.xyz:444/'); }); it('accepts port in forwarded host and forwarded port', async () => { @@ -113,7 +113,7 @@ describe('URL', () => { const html = await text(); const $ = cheerio.load(html); - assert.equal($('body').text(), 'https://abc.xyz:444/'); + assert.equal($('body').text().trim(), 'https://abc.xyz:444/'); }); it('ignores X-Forwarded-Host when no allowedDomains configured', async () => { @@ -134,7 +134,7 @@ describe('URL', () => { const $ = cheerio.load(html); // Should use the Host header, not X-Forwarded-Host when allowedDomains is not configured - assert.equal($('body').text(), 'https://legitimate.example.com/'); + assert.equal($('body').text().trim(), 'https://legitimate.example.com/'); }); it('rejects port in forwarded host when port not in allowedDomains', async () => { @@ -155,7 +155,7 @@ describe('URL', () => { const $ = cheerio.load(html); // Port 8080 not in allowedDomains (only 444), so should fall back to Host header - assert.equal($('body').text(), 'https://localhost:3000/'); + assert.equal($('body').text().trim(), 'https://localhost:3000/'); }); it('rejects empty X-Forwarded-Host with allowedDomains configured', async () => { @@ -176,7 +176,7 @@ describe('URL', () => { const $ = cheerio.load(html); // Empty X-Forwarded-Host should be rejected and fall back to Host header - assert.equal($('body').text(), 'https://legitimate.example.com/'); + assert.equal($('body').text().trim(), 'https://legitimate.example.com/'); }); it('rejects X-Forwarded-Host with path injection attempt', async () => { @@ -197,6 +197,6 @@ describe('URL', () => { const $ = cheerio.load(html); // Path injection attempt should be rejected and fall back to Host header - assert.equal($('body').text(), 'https://localhost:3000/'); + assert.equal($('body').text().trim(), 'https://localhost:3000/'); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55aa70064ba6..e153eb68f9c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -514,9 +514,9 @@ importers: packages/astro: dependencies: - '@astrojs/compiler': - specifier: ^3.0.1 - version: 3.0.1 + '@astrojs/compiler-rs': + specifier: ^0.1.10 + version: 0.1.10(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) '@astrojs/internal-helpers': specifier: workspace:* version: link:../internal-helpers @@ -680,9 +680,6 @@ importers: '@astrojs/check': specifier: workspace:* version: link:../language-tools/astro-check - '@astrojs/compiler-rs': - specifier: ^0.1.6 - version: 0.1.6 '@playwright/test': specifier: 1.58.2 version: 1.58.2 @@ -7019,76 +7016,73 @@ packages: resolution: {integrity: sha512-bVzyKzEpIwqjihBU/aUzt1LQckJuHK0agd3/ITdXhPUYculrc6K1/K7H+XG4rwjXtg+ikT3PM05V1MVYWiIvQw==} engines: {node: '>=18.14.1'} - '@astrojs/compiler-binding-darwin-arm64@0.1.6': - resolution: {integrity: sha512-pYCFf5a/Tat+uRJU7xUSK0aw45kxnwAaKyhpnosJFCFhiiG4d/b7U526gaIqdcIZx6PbZ0hPOYDAxUYYfMDFaw==} + '@astrojs/compiler-binding-darwin-arm64@0.1.10': + resolution: {integrity: sha512-zDYwHvXVCm91XUm5xBRPbZK6yx9foM+Ut2qHiL0L37r1daF1bGLhnYjNV/VP35OLH5o/A1j+9uvl1xEX2a3ftw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@astrojs/compiler-binding-darwin-x64@0.1.6': - resolution: {integrity: sha512-GfXy2xBFwg/yCpd8VWqnDfceCnMgT+7HPoPCuJS+lNeRyi76M/4bACMROcoe59R4brAlc7Tb1kO0MED581OjJQ==} + '@astrojs/compiler-binding-darwin-x64@0.1.10': + resolution: {integrity: sha512-EqugPJFuQdG11sG6TtO8uq0njcEyv9y/5gdpBRZJE6aSoM3yWFmTQ/aNU8PWIiz1upZFjdNQ/UUnVNzYd7N7Uw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@astrojs/compiler-binding-linux-arm64-gnu@0.1.6': - resolution: {integrity: sha512-XcLFDccESW7ILeO6fFsV7W+PlLX7FNifN3WZdqdO/DNAIyHj1WahI65UImauf3VfMirwexI05XsOjLpiSUg05g==} + '@astrojs/compiler-binding-linux-arm64-gnu@0.1.10': + resolution: {integrity: sha512-h1UN8yx2vO/0uwnExN/8MNfsDr7OFADVM3Vv0JWeSpmUO6sFOvTsGVAeBBl29up6c4sdT3QJ5rf6sczQsc25dA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@astrojs/compiler-binding-linux-arm64-musl@0.1.6': - resolution: {integrity: sha512-96Mm9qf1xYHW66DjtvTdHJED0rVeh0X/Gt72lzia8RM6nJSXO+3YqiFBcsP3ixrcHJliMaV8s8+iwb5pR9kgeg==} + '@astrojs/compiler-binding-linux-arm64-musl@0.1.10': + resolution: {integrity: sha512-ot1Lksml0FqrrlYa89wGXQM+n290ncj65PZAq8siEWmXsmwoNCYCbqRSwRO6I/cT+PDlmmV0AcFTrp1DEO3PUA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@astrojs/compiler-binding-linux-x64-gnu@0.1.6': - resolution: {integrity: sha512-Eg0N+YYLjIwCgnQvpGUWCs0C7v/dZmcH9hnWAkZYSamSJPxKWDlGC5rVoaBJCtRleAfjmKxHKaCjn9t7E4pyTg==} + '@astrojs/compiler-binding-linux-x64-gnu@0.1.10': + resolution: {integrity: sha512-RGKGCbCvDat+DppnQbiWIhif6ptvkyXMdqOa7NjES4UoqIIUjE3YZfRn8gp3bXUe4ReWv4hGO1TQd2eitONt1g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@astrojs/compiler-binding-linux-x64-musl@0.1.6': - resolution: {integrity: sha512-YnoxAnVcFzlP/Wr4FPVdjh1ecmvH/BWeeNK8UJZN/xd6XXLbj1loG2ckZ7+lA8HpZeyiwU2+IAZ4XjGsIUHI6w==} + '@astrojs/compiler-binding-linux-x64-musl@0.1.10': + resolution: {integrity: sha512-UemMv5Xq9c7trG9Cel4MHAo2wYiCxZ6qIKUnI4NJqBXDtOBqAjcMoqMbw78KYkb9vg2OIewVaJ+YvQrFZs5VBg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@astrojs/compiler-binding-wasm32-wasi@0.1.6': - resolution: {integrity: sha512-yNZEslAC8v/3G5lSA4GNuXGTRR8TonRzv/hCkBg7aN+8wOQF88YZTR8TLGb0sYEXV7tukYAGmmWAnMeM2zTRCQ==} + '@astrojs/compiler-binding-wasm32-wasi@0.1.10': + resolution: {integrity: sha512-SwawjgiYnm7s5neVKRoHIsnGG06vhuFhKg0iMBO6EsnL19xVbUp61V50gVtdgFlsfXalfH6SHuv2wQw8FhFSNg==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@astrojs/compiler-binding-win32-arm64-msvc@0.1.6': - resolution: {integrity: sha512-gnMcXKokX3/LiaFmUdTEX/UwUE3geH5TorQ2QMSPTgCpv/kS3nokflRIYQ/69NEqlk2DQoR3gas002spZTMffw==} + '@astrojs/compiler-binding-win32-arm64-msvc@0.1.10': + resolution: {integrity: sha512-DKGOtbS8AZztvKUUdzvf+bOp7QLudmQiMHSYobSZKFanryQH5SllOMrZsL2pbDAcfBBrf0WsYWp9R+/ga7gGhQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@astrojs/compiler-binding-win32-x64-msvc@0.1.6': - resolution: {integrity: sha512-7jEsJ1TjX1lVd/1fH1CgO/wvmWBztLHekqefPiIJQC6U+ks7ToMV2+J3so0MM/ZCfXpHnPNigcBmHn94YR1+cQ==} + '@astrojs/compiler-binding-win32-x64-msvc@0.1.10': + resolution: {integrity: sha512-de9Y0GPm19G6SPgUk86ycZNuWEw7AA7vLZvsi/+cNR7tr/vOabU931O22L4bjjS138PFWmBz+qYOH9CT6h6BDA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@astrojs/compiler-binding@0.1.6': - resolution: {integrity: sha512-EPQMZBEgqbjye57qcHQehrYQUl47kNySwUAW3U/IYrMPtuiAWAht1Dl1rahGVeWg8PDXCZXRGm+WCDAowMP1VA==} + '@astrojs/compiler-binding@0.1.10': + resolution: {integrity: sha512-XvsWOM1JTjTDETD4qK2rIIHyPSMtBP+t7+Hmgk1kd4/iiUxLPnyLjnwLKX7N5t1MBHNmGiEA39DOslSMtlnfeg==} engines: {node: ^20.19.0 || >=22.12.0} - '@astrojs/compiler-rs@0.1.6': - resolution: {integrity: sha512-8PtjNrpEK4+B5Cip/ODgBFQAaBLPxkLLehoqmNMAV+iPSFGRjRHmwxGqSoB5L8pvk3z4IngCkiizcw+HoKMWWA==} + '@astrojs/compiler-rs@0.1.10': + resolution: {integrity: sha512-Mul2veNSpEzwd1Zk6coIAu9TKyk6RjBJMWaOOrelBBA+ZmcGXhj7MabjowNTOMgvRUU8YHXAHadzoKD0y49uxw==} '@astrojs/compiler@2.13.1': resolution: {integrity: sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg==} - '@astrojs/compiler@3.0.1': - resolution: {integrity: sha512-z97oYbdebO5aoWzuJ/8q5hLK232+17KcLZ7cJ8BCWk6+qNzVxn/gftC0KzMBUTD8WAaBkPpNSQK6PXLnNrZ0CA==} - '@astrojs/solid-js@5.1.3': resolution: {integrity: sha512-KxfYt4y1d7BuSw6EsN1EaPoGYsIES7bEI6AtTbncuabRUUMZs+mOWOeOdmgnwVLj+jbNbhBjUZsqr77eUviZdw==} engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} @@ -16131,55 +16125,62 @@ snapshots: log-update: 5.0.1 sisteransi: 1.0.5 - '@astrojs/compiler-binding-darwin-arm64@0.1.6': + '@astrojs/compiler-binding-darwin-arm64@0.1.10': optional: true - '@astrojs/compiler-binding-darwin-x64@0.1.6': + '@astrojs/compiler-binding-darwin-x64@0.1.10': optional: true - '@astrojs/compiler-binding-linux-arm64-gnu@0.1.6': + '@astrojs/compiler-binding-linux-arm64-gnu@0.1.10': optional: true - '@astrojs/compiler-binding-linux-arm64-musl@0.1.6': + '@astrojs/compiler-binding-linux-arm64-musl@0.1.10': optional: true - '@astrojs/compiler-binding-linux-x64-gnu@0.1.6': + '@astrojs/compiler-binding-linux-x64-gnu@0.1.10': optional: true - '@astrojs/compiler-binding-linux-x64-musl@0.1.6': + '@astrojs/compiler-binding-linux-x64-musl@0.1.10': optional: true - '@astrojs/compiler-binding-wasm32-wasi@0.1.6': + '@astrojs/compiler-binding-wasm32-wasi@0.1.10(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: - '@napi-rs/wasm-runtime': 1.1.1 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' optional: true - '@astrojs/compiler-binding-win32-arm64-msvc@0.1.6': + '@astrojs/compiler-binding-win32-arm64-msvc@0.1.10': optional: true - '@astrojs/compiler-binding-win32-x64-msvc@0.1.6': + '@astrojs/compiler-binding-win32-x64-msvc@0.1.10': optional: true - '@astrojs/compiler-binding@0.1.6': + '@astrojs/compiler-binding@0.1.10(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': optionalDependencies: - '@astrojs/compiler-binding-darwin-arm64': 0.1.6 - '@astrojs/compiler-binding-darwin-x64': 0.1.6 - '@astrojs/compiler-binding-linux-arm64-gnu': 0.1.6 - '@astrojs/compiler-binding-linux-arm64-musl': 0.1.6 - '@astrojs/compiler-binding-linux-x64-gnu': 0.1.6 - '@astrojs/compiler-binding-linux-x64-musl': 0.1.6 - '@astrojs/compiler-binding-wasm32-wasi': 0.1.6 - '@astrojs/compiler-binding-win32-arm64-msvc': 0.1.6 - '@astrojs/compiler-binding-win32-x64-msvc': 0.1.6 + '@astrojs/compiler-binding-darwin-arm64': 0.1.10 + '@astrojs/compiler-binding-darwin-x64': 0.1.10 + '@astrojs/compiler-binding-linux-arm64-gnu': 0.1.10 + '@astrojs/compiler-binding-linux-arm64-musl': 0.1.10 + '@astrojs/compiler-binding-linux-x64-gnu': 0.1.10 + '@astrojs/compiler-binding-linux-x64-musl': 0.1.10 + '@astrojs/compiler-binding-wasm32-wasi': 0.1.10(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@astrojs/compiler-binding-win32-arm64-msvc': 0.1.10 + '@astrojs/compiler-binding-win32-x64-msvc': 0.1.10 + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' - '@astrojs/compiler-rs@0.1.6': + '@astrojs/compiler-rs@0.1.10(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: - '@astrojs/compiler-binding': 0.1.6 + '@astrojs/compiler-binding': 0.1.10(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' '@astrojs/compiler@2.13.1': {} - '@astrojs/compiler@3.0.1': {} - '@astrojs/solid-js@5.1.3(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.98.0)(solid-js@1.9.11)(tsx@4.21.0)(yaml@2.8.3)': dependencies: solid-js: 1.9.11 From 014d4239a119c48dda491950686c797a0fc50941 Mon Sep 17 00:00:00 2001 From: ematipico Date: Thu, 30 Apr 2026 11:55:17 +0100 Subject: [PATCH 09/72] fix tests --- packages/astro/e2e/test-utils.ts | 28 +- .../src/core/build/plugins/plugin-css.ts | 7 +- .../astro/src/core/config/schemas/base.ts | 1 - .../src/toolbar/vite-plugin-dev-toolbar.ts | 26 - packages/astro/test/actions.test.ts | 15 - .../test/astro-script-template-dedup.test.ts | 16 +- .../minification-html-jsx/package.json | 2 +- .../test/units/compile/css-base-path.test.ts | 27 +- .../test/units/compile/rust-compiler.test.ts | 2 +- .../render/head-propagation/comment.test.ts | 26 - pnpm-lock.yaml | 602 +++++++++++++----- 11 files changed, 496 insertions(+), 256 deletions(-) delete mode 100644 packages/astro/test/units/render/head-propagation/comment.test.ts diff --git a/packages/astro/e2e/test-utils.ts b/packages/astro/e2e/test-utils.ts index 3f5de0014c6e..8ab4129df914 100644 --- a/packages/astro/e2e/test-utils.ts +++ b/packages/astro/e2e/test-utils.ts @@ -1,7 +1,7 @@ import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { type Locator, type Page, expect, test as testBase } from '@playwright/test'; +import { type Locator, type Page, expect, test as testBase, type Browser } from '@playwright/test'; import type { AstroLogger } from '../dist/core/logger/core.js'; import { type AstroInlineConfig, @@ -165,3 +165,29 @@ export function createLoggerSpy(options: LoggerSpyOptions = {}): AstroLogger { // @ts-expect-error: TODO: use a real AstroLogger instance instead of this mock return logger as AstroLogger; } + +/** + * Warm up the dev server by loading a page and waiting for islands to hydrate. + * This ensures Vite's dep optimizer has finished and avoids reload flakiness. + */ +export async function warmupDevServer(browser: Browser, url: string) { + const page = await browser.newPage(); + await page.goto(url, { waitUntil: 'load' }); + await page.waitForLoadState('networkidle').catch(() => {}); + const islands = page.locator('astro-island'); + const count = await islands.count(); + for (let i = 0; i < count; i++) { + const island = islands.nth(i); + const uid = await island.getAttribute('uid').catch(() => null); + if (uid) { + await page + .waitForFunction( + (selector) => document.querySelector(selector)?.hasAttribute('ssr') === false, + `astro-island[uid="${uid}"]`, + { timeout: 5_000 }, + ) + .catch(() => {}); + } + } + await page.close(); +} diff --git a/packages/astro/src/core/build/plugins/plugin-css.ts b/packages/astro/src/core/build/plugins/plugin-css.ts index 6f050eb349db..e4dac132ce89 100644 --- a/packages/astro/src/core/build/plugins/plugin-css.ts +++ b/packages/astro/src/core/build/plugins/plugin-css.ts @@ -1,5 +1,4 @@ -import type { GetModuleInfo } from 'rollup'; -import type { BuildOptions, ResolvedConfig, Plugin as VitePlugin } from 'vite'; +import type { BuildOptions, ResolvedConfig, Plugin as VitePlugin, Rolldown } from 'vite'; import { isCSSRequest } from 'vite'; import { ASTRO_VITE_ENVIRONMENT_NAMES } from '../../constants.js'; import { isPropagatedAssetBoundary } from '../../head-propagation/boundary.js'; @@ -30,7 +29,7 @@ interface PluginOptions { buildOptions: StaticBuildOptions; } -function isBuildCssBoundary(id: string, ctx: { getModuleInfo: GetModuleInfo }): boolean { +function isBuildCssBoundary(id: string, ctx: { getModuleInfo: Rolldown.GetModuleInfo }): boolean { if (isPropagatedAssetBoundary(id)) return true; const info = ctx.getModuleInfo(id); return info ? moduleIsTopLevelPage(info) : false; @@ -449,7 +448,7 @@ function shouldDeleteCSSChunk(allModules: string[], internals: BuildInternals): function* getParentClientOnlys( id: string, - ctx: { getModuleInfo: GetModuleInfo }, + ctx: { getModuleInfo: Rolldown.GetModuleInfo }, internals: BuildInternals, ): Generator { for (const info of getParentModuleInfos(id, ctx)) { diff --git a/packages/astro/src/core/config/schemas/base.ts b/packages/astro/src/core/config/schemas/base.ts index fcb26294fd91..68846e8fdfd0 100644 --- a/packages/astro/src/core/config/schemas/base.ts +++ b/packages/astro/src/core/config/schemas/base.ts @@ -109,7 +109,6 @@ export const ASTRO_CONFIG_DEFAULTS = { clientPrerender: false, contentIntellisense: false, chromeDevtoolsWorkspace: false, - rustCompiler: false, queuedRendering: { enabled: false, }, diff --git a/packages/astro/src/toolbar/vite-plugin-dev-toolbar.ts b/packages/astro/src/toolbar/vite-plugin-dev-toolbar.ts index ac33981bb7aa..aaac1d5fcce7 100644 --- a/packages/astro/src/toolbar/vite-plugin-dev-toolbar.ts +++ b/packages/astro/src/toolbar/vite-plugin-dev-toolbar.ts @@ -1,4 +1,3 @@ -import { readFileSync, writeFileSync } from 'node:fs'; import type * as vite from 'vite'; import { telemetry } from '../events/index.js'; import { eventAppToggled } from '../events/toolbar.js'; @@ -22,31 +21,6 @@ export default function astroDevToolbar({ settings, logger }: AstroPluginOptions 'astro > axobject-query', ...(settings.devToolbarApps.length > 0 ? ['astro/toolbar'] : []), ], - esbuildOptions: { - plugins: [ - { - name: 'astro:strip-toolbar-sourcemap', - setup(build) { - // The dev toolbar entrypoint is served via /@id/ which causes - // the browser to mis-resolve the relative sourceMappingURL that - // esbuild adds, producing a bogus 404 request. Strip it after - // esbuild writes the optimized deps to disk. - build.onEnd((result) => { - if (!result.metafile) return; - for (const outputPath of Object.keys(result.metafile.outputs)) { - if (!outputPath.includes('entrypoint') || !outputPath.endsWith('.js')) - continue; - const code = readFileSync(outputPath, 'utf-8'); - const stripped = code.replace(/\/\/# sourceMappingURL=.*$/m, ''); - if (stripped !== code) { - writeFileSync(outputPath, stripped); - } - } - }); - }, - }, - ], - }, }, }; }, diff --git a/packages/astro/test/actions.test.ts b/packages/astro/test/actions.test.ts index e1f941c2f4e1..4a874e05c6f0 100644 --- a/packages/astro/test/actions.test.ts +++ b/packages/astro/test/actions.test.ts @@ -22,21 +22,6 @@ type ActionInputErrorResponse = { fields: Record; }; -// Error response shapes emitted by `serializeActionResult` in -// src/actions/runtime/server.ts. -type ActionErrorResponse = { - type: 'AstroActionError'; - code: string; - message: string; - status: number; -}; - -type ActionInputErrorResponse = { - type: 'AstroActionInputError'; - issues: Array<{ path: Array; message: string; code: string }>; - fields: Record; -}; - describe('Astro Actions', () => { let fixture: Fixture; before(async () => { diff --git a/packages/astro/test/astro-script-template-dedup.test.ts b/packages/astro/test/astro-script-template-dedup.test.ts index 4a13eb12c4b6..41afb4b29302 100644 --- a/packages/astro/test/astro-script-template-dedup.test.ts +++ b/packages/astro/test/astro-script-template-dedup.test.ts @@ -28,8 +28,8 @@ describe('Scripts inside template elements', () => { const $ = cheerio.load(html); // One script inside the