Compare commits

...

17 commits

Author SHA1 Message Date
Martin Trapp
068e618cb9 Fix head swap for persisted client:only components (dev mode) 2023-10-04 18:06:32 +02:00
Bjorn Lu
85cc8daff8 Remove unused CSS output files when inlined (#8743) 2023-10-04 18:06:32 +02:00
bluwy
9ed7da1e3b [ci] format 2023-10-04 18:06:32 +02:00
Arsh
d50aaea36d chore: remove undici polyfill (#8729) 2023-10-04 18:06:32 +02:00
bluwy
05833ab44e [ci] format 2023-10-04 18:06:32 +02:00
Chris
6bba4efb3f Fixes: Shiki syntax highlighting adds is:raw attribute to the HTML output (#8715)
Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
2023-10-04 18:06:32 +02:00
Kobe Ruado
a3ebad84a8 Fix markdown rehype plugin example (#8733) 2023-10-04 18:06:31 +02:00
natemoo-re
4e2c65c27c [ci] format 2023-10-04 18:06:31 +02:00
Nate Moore
9c7cd7c9f6 Improve astro info compatability (#8730)
* Improve `astro info` compatability

* Update packages/astro/src/cli/info/index.ts

Co-authored-by: Arsh <69170106+lilnasy@users.noreply.github.com>

* chore: add changeset

* feat(info): add copy to clipboard support on Unix machines with xclip installed

---------

Co-authored-by: Arsh <69170106+lilnasy@users.noreply.github.com>
2023-10-04 18:06:31 +02:00
Florian Lefebvre
49cbbf8d23 feat: expose partytown types (close #8723) (#8740) 2023-10-04 18:06:31 +02:00
Emanuele Stoppa
8eed6cc7ac feat: add provenance to packages (#8737) 2023-10-04 18:06:31 +02:00
Bjorn Lu
cb62f02ea1 Fix tsconfig.json update causing the server to crash (#8736) 2023-10-04 18:06:31 +02:00
Genteure
d125095d09 fix: typo in error deprecation message (#8708)
Co-authored-by: Alexander Niebuhr <alexander@nbhr.io>
2023-10-04 18:06:31 +02:00
Martin Trapp
365adee6f6
Merge branch 'main' into mt/client-only 2023-10-02 18:18:26 +02:00
Martin Trapp
20c1b3b0d0 Fix view transitions with client:only components 2023-10-02 18:02:41 +02:00
Martin Trapp
84d1f05964 Fix view transitions with client:only components 2023-10-02 17:16:00 +02:00
Martin Trapp
363f80011b Fix view transitions with client:only components 2023-10-02 16:41:23 +02:00
50 changed files with 350 additions and 171 deletions

View file

@ -0,0 +1,5 @@
---
'@astrojs/markdown-remark': patch
---
Remove `is:raw` from remark Shiki plugin

View file

@ -0,0 +1,25 @@
---
'@astrojs/cloudflare': patch
'@astrojs/partytown': patch
'@astrojs/alpinejs': patch
'@astrojs/prefetch': patch
'@astrojs/tailwind': patch
'@astrojs/markdoc': patch
'@astrojs/sitemap': patch
'@astrojs/underscore-redirects': patch
'@astrojs/preact': patch
'@astrojs/svelte': patch
'@astrojs/vercel': patch
'@astrojs/react': patch
'@astrojs/solid-js': patch
'@astrojs/node': patch
'@astrojs/lit': patch
'@astrojs/mdx': patch
'@astrojs/vue': patch
'@astrojs/internal-helpers': patch
'@astrojs/markdown-remark': patch
'@astrojs/telemetry': patch
'astro': patch
---
Add provenance statement when publishing the library from CI

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fix view transitions with client:only components

View file

@ -0,0 +1,5 @@
---
'@astrojs/telemetry': patch
---
Removed an unnecessary dependency.

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fix `tsconfig.json` update causing the server to crash

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Remove unused CSS output files when inlined

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Node-based adapters now create less server-side javascript

View file

@ -0,0 +1,5 @@
---
'@astrojs/partytown': patch
---
Expose types for TypeScript users

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Improve `astro info` copy to clipboard compatability

View file

@ -3,7 +3,6 @@
// ISOMORPHIC FILE: NO TOP-LEVEL IMPORT/REQUIRE() ALLOWED // ISOMORPHIC FILE: NO TOP-LEVEL IMPORT/REQUIRE() ALLOWED
// This file has to run as both ESM and CJS on older Node.js versions // This file has to run as both ESM and CJS on older Node.js versions
// Needed for Stackblitz: https://github.com/stackblitz/webcontainer-core/issues/281
const CI_INSTRUCTIONS = { const CI_INSTRUCTIONS = {
NETLIFY: 'https://docs.netlify.com/configure-builds/manage-dependencies/#node-js-and-javascript', NETLIFY: 'https://docs.netlify.com/configure-builds/manage-dependencies/#node-js-and-javascript',
@ -16,15 +15,11 @@ const CI_INSTRUCTIONS = {
const engines = '>=18.14.1'; const engines = '>=18.14.1';
const skipSemverCheckIfAbove = 19; const skipSemverCheckIfAbove = 19;
// HACK (2023-08-18) Stackblitz does not support Node 18 yet, so we'll fake Node 16 support for some time until it's supported
// TODO: Remove when Node 18 is supported on Stackblitz
const isStackblitz = process.env.SHELL === '/bin/jsh' && process.versions.webcontainer != null;
/** `astro *` */ /** `astro *` */
async function main() { async function main() {
const version = process.versions.node; const version = process.versions.node;
// Fast-path for higher Node.js versions // Fast-path for higher Node.js versions
if (!isStackblitz && (parseInt(version) || 0) <= skipSemverCheckIfAbove) { if ((parseInt(version) || 0) <= skipSemverCheckIfAbove) {
try { try {
const semver = await import('semver'); const semver = await import('semver');
if (!semver.satisfies(version, engines)) { if (!semver.satisfies(version, engines)) {

View file

@ -3,6 +3,7 @@ import Layout from '../components/Layout.astro';
import Island from '../components/Island'; import Island from '../components/Island';
--- ---
<Layout> <Layout>
<p id="page-one">Page 1</p>
<a id="click-two" href="/client-only-two">go to page 2</a> <a id="click-two" href="/client-only-two">go to page 2</a>
<div transition:persist="island"> <div transition:persist="island">
<Island client:only count={5}>message here</Island> <Island client:only count={5}>message here</Island>

View file

@ -4,6 +4,7 @@ import Island from '../components/Island';
--- ---
<Layout> <Layout>
<p id="page-two">Page 2</p> <p id="page-two">Page 2</p>
<a id="click-one" href="/client-only-one">go to page 1</a>
<div transition:persist="island"> <div transition:persist="island">
<Island client:only count={5}>message here</Island> <Island client:only count={5}>message here</Island>
</div> </div>

View file

@ -649,9 +649,14 @@ test.describe('View Transitions', () => {
}); });
test('client:only styles are retained on transition', async ({ page, astro }) => { test('client:only styles are retained on transition', async ({ page, astro }) => {
const loads = [];
page.addListener('load', async (p) => {
loads.push(p);
});
const totalExpectedStyles = 7; const totalExpectedStyles = 7;
// Go to page 1 // Go to page 1 (normal load)
await page.goto(astro.resolveUrl('/client-only-one')); await page.goto(astro.resolveUrl('/client-only-one'));
let msg = page.locator('.counter-message'); let msg = page.locator('.counter-message');
await expect(msg).toHaveText('message here'); await expect(msg).toHaveText('message here');
@ -659,13 +664,24 @@ test.describe('View Transitions', () => {
let styles = await page.locator('style').all(); let styles = await page.locator('style').all();
expect(styles.length).toEqual(totalExpectedStyles); expect(styles.length).toEqual(totalExpectedStyles);
// Transition to page 2 (will do a full load)
await page.click('#click-two'); await page.click('#click-two');
let pageTwo = page.locator('#page-two'); let pageTwo = page.locator('#page-two');
await expect(pageTwo, 'should have content').toHaveText('Page 2'); await expect(pageTwo, 'should have content').toHaveText('Page 2');
// Transition to page 1 (will do a full load)
await page.click('#click-one');
let pageOne = page.locator('#page-one');
await expect(pageOne, 'should have content').toHaveText('Page 1');
// Transition to page 1 (real transition, no full load)
await page.click('#click-two');
styles = await page.locator('style').all(); styles = await page.locator('style').all();
expect(styles.length).toEqual(totalExpectedStyles, 'style count has not changed'); expect(styles.length).toEqual(totalExpectedStyles, 'style count has not changed');
expect(loads.length, 'There should only be 1 page load').toEqual(3);
}); });
test('Horizontal scroll position restored on back button', async ({ page, astro }) => { test('Horizontal scroll position restored on back button', async ({ page, astro }) => {

View file

@ -168,7 +168,6 @@
"string-width": "^6.1.0", "string-width": "^6.1.0",
"strip-ansi": "^7.1.0", "strip-ansi": "^7.1.0",
"tsconfig-resolver": "^3.0.1", "tsconfig-resolver": "^3.0.1",
"undici": "^5.23.0",
"unist-util-visit": "^4.1.2", "unist-util-visit": "^4.1.2",
"vfile": "^5.3.7", "vfile": "^5.3.7",
"vite": "^4.4.9", "vite": "^4.4.9",
@ -226,5 +225,8 @@
"engines": { "engines": {
"node": ">=18.14.1", "node": ">=18.14.1",
"npm": ">=6.14.0" "npm": ">=6.14.0"
},
"publishConfig": {
"provenance": true
} }
} }

View file

@ -1168,10 +1168,10 @@ export interface AstroUserConfig {
* Pass [rehype plugins](https://github.com/remarkjs/remark-rehype) to customize how your Markdown's output HTML is processed. You can import and apply the plugin function (recommended), or pass the plugin name as a string. * Pass [rehype plugins](https://github.com/remarkjs/remark-rehype) to customize how your Markdown's output HTML is processed. You can import and apply the plugin function (recommended), or pass the plugin name as a string.
* *
* ```js * ```js
* import rehypeMinifyHtml from 'rehype-minify'; * import { rehypeAccessibleEmojis } from 'rehype-accessible-emojis';
* { * {
* markdown: { * markdown: {
* rehypePlugins: [rehypeMinifyHtml] * rehypePlugins: [rehypeAccessibleEmojis]
* } * }
* } * }
* ``` * ```

View file

@ -41,10 +41,22 @@ export async function printInfo({ flags }: InfoOptions) {
await copyToClipboard(output.trim()); await copyToClipboard(output.trim());
} }
const SUPPORTED_SYSTEM = new Set(['darwin', 'win32']);
async function copyToClipboard(text: string) { async function copyToClipboard(text: string) {
const system = platform(); const system = platform();
if (!SUPPORTED_SYSTEM.has(system)) return; let command = '';
if (system === 'darwin') {
command = 'pbcopy';
} else if (system === 'win32') {
command = 'clip';
} else {
// Unix: check if `xclip` is installed
const output = execSync('which xclip', { encoding: 'utf8' });
if (output[0] !== '/') {
// Did not find a path for xclip, bail out!
return;
}
command = 'xclip -sel clipboard -l 1';
}
console.log(); console.log();
const { shouldCopy } = await prompts({ const { shouldCopy } = await prompts({
@ -54,11 +66,11 @@ async function copyToClipboard(text: string) {
initial: true, initial: true,
}); });
if (!shouldCopy) return; if (!shouldCopy) return;
const command = system === 'darwin' ? 'pbcopy' : 'clip';
try { try {
execSync(`echo ${JSON.stringify(text.trim())} | ${command}`, { execSync(command, {
input: text.trim(),
encoding: 'utf8', encoding: 'utf8',
stdio: 'ignore',
}); });
} catch (e) { } catch (e) {
console.error( console.error(

View file

@ -148,9 +148,15 @@ export const _internal = {
hasContentFlag(modUrl, DATA_FLAG) || hasContentFlag(modUrl, DATA_FLAG) ||
Boolean(getContentRendererByViteId(modUrl, settings)) Boolean(getContentRendererByViteId(modUrl, settings))
) { ) {
const mod = await viteServer.moduleGraph.getModuleByUrl(modUrl); try {
if (mod) { const mod = await viteServer.moduleGraph.getModuleByUrl(modUrl);
viteServer.moduleGraph.invalidateModule(mod); if (mod) {
viteServer.moduleGraph.invalidateModule(mod);
}
} catch (e: any) {
// The server may be closed due to a restart caused by this file change
if (e.code === 'ERR_CLOSED_SERVER') break;
throw e;
} }
} }
} }

View file

@ -200,7 +200,7 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
const inlineConfig = settings.config.build.inlineStylesheets; const inlineConfig = settings.config.build.inlineStylesheets;
const { assetsInlineLimit = 4096 } = settings.config.vite?.build ?? {}; const { assetsInlineLimit = 4096 } = settings.config.vite?.build ?? {};
Object.entries(bundle).forEach(([_, stylesheet]) => { Object.entries(bundle).forEach(([id, stylesheet]) => {
if ( if (
stylesheet.type !== 'asset' || stylesheet.type !== 'asset' ||
stylesheet.name?.endsWith('.css') !== true || stylesheet.name?.endsWith('.css') !== true ||
@ -224,10 +224,15 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
: { type: 'external', src: stylesheet.fileName }; : { type: 'external', src: stylesheet.fileName };
const pages = Array.from(eachPageData(internals)); const pages = Array.from(eachPageData(internals));
let sheetAddedToPage = false;
pages.forEach((pageData) => { pages.forEach((pageData) => {
const orderingInfo = pagesToCss[pageData.moduleSpecifier]?.[stylesheet.fileName]; const orderingInfo = pagesToCss[pageData.moduleSpecifier]?.[stylesheet.fileName];
if (orderingInfo !== undefined) return pageData.styles.push({ ...orderingInfo, sheet }); if (orderingInfo !== undefined) {
pageData.styles.push({ ...orderingInfo, sheet });
sheetAddedToPage = true;
return;
}
const propagatedPaths = pagesToPropagatedCss[pageData.moduleSpecifier]; const propagatedPaths = pagesToPropagatedCss[pageData.moduleSpecifier];
if (propagatedPaths === undefined) return; if (propagatedPaths === undefined) return;
@ -243,8 +248,21 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
pageData.propagatedStyles.set(pageInfoId, new Set()).get(pageInfoId)!; pageData.propagatedStyles.set(pageInfoId, new Set()).get(pageInfoId)!;
propagatedStyles.add(sheet); propagatedStyles.add(sheet);
sheetAddedToPage = true;
}); });
}); });
if (toBeInlined && sheetAddedToPage) {
// CSS is already added to all used pages, we can delete it from the bundle
// and make sure no chunks reference it via `importedCss` (for Vite preloading)
// to avoid duplicate CSS.
delete bundle[id];
for (const chunk of Object.values(bundle)) {
if (chunk.type === 'chunk') {
chunk.viteMetadata?.importedCss?.delete(id);
}
}
}
}); });
}, },
}; };

View file

@ -39,7 +39,6 @@ export function createAPIContext({
props, props,
adapterName, adapterName,
}: CreateAPIContext): APIContext { }: CreateAPIContext): APIContext {
initResponseWithEncoding();
const context = { const context = {
cookies: new AstroCookies(request), cookies: new AstroCookies(request),
request, request,
@ -92,44 +91,28 @@ export function createAPIContext({
type ResponseParameters = ConstructorParameters<typeof Response>; type ResponseParameters = ConstructorParameters<typeof Response>;
export let ResponseWithEncoding: ReturnType<typeof initResponseWithEncoding>; export class ResponseWithEncoding extends Response {
// TODO Remove this after StackBlitz supports Node 18. constructor(body: ResponseParameters[0], init: ResponseParameters[1], encoding?: BufferEncoding) {
let initResponseWithEncoding = () => { // If a body string is given, try to encode it to preserve the behaviour as simple objects.
class LocalResponseWithEncoding extends Response { // We don't do the full handling as simple objects so users can control how headers are set instead.
constructor( if (typeof body === 'string') {
body: ResponseParameters[0], // In NodeJS, we can use Buffer.from which supports all BufferEncoding
init: ResponseParameters[1], if (typeof Buffer !== 'undefined' && Buffer.from) {
encoding?: BufferEncoding body = Buffer.from(body, encoding);
) {
// If a body string is given, try to encode it to preserve the behaviour as simple objects.
// We don't do the full handling as simple objects so users can control how headers are set instead.
if (typeof body === 'string') {
// In NodeJS, we can use Buffer.from which supports all BufferEncoding
if (typeof Buffer !== 'undefined' && Buffer.from) {
body = Buffer.from(body, encoding);
}
// In non-NodeJS, use the web-standard TextEncoder for utf-8 strings
else if (encoding == null || encoding === 'utf8' || encoding === 'utf-8') {
body = encoder.encode(body);
}
} }
// In non-NodeJS, use the web-standard TextEncoder for utf-8 strings
super(body, init); else if (encoding == null || encoding === 'utf8' || encoding === 'utf-8') {
body = encoder.encode(body);
if (encoding) {
this.headers.set('X-Astro-Encoding', encoding);
} }
} }
super(body, init);
if (encoding) {
this.headers.set('X-Astro-Encoding', encoding);
}
} }
}
// Set the module scoped variable.
ResponseWithEncoding = LocalResponseWithEncoding;
// Turn this into a noop.
initResponseWithEncoding = (() => {}) as any;
return LocalResponseWithEncoding;
};
export async function callEndpoint<MiddlewareResult = Response | EndpointOutput>( export async function callEndpoint<MiddlewareResult = Response | EndpointOutput>(
mod: EndpointHandler, mod: EndpointHandler,

View file

@ -1158,7 +1158,7 @@ export const ContentSchemaContainsSlugError = {
/** /**
* @docs * @docs
* @message A collection queried via `getCollection()` does not exist. * @message A collection queried via `getCollection()` does not exist.
* @deprecated Collections that do not exist no longer result in an error. A warning is omitted instead. * @deprecated Collections that do not exist no longer result in an error. A warning is given instead.
* @description * @description
* When querying a collection, ensure a collection directory with the requested name exists under `src/content/`. * When querying a collection, ensure a collection directory with the requested name exists under `src/content/`.
*/ */

View file

@ -1,19 +1,52 @@
import { EventEmitter } from 'node:events'; import { EventEmitter } from 'node:events';
import path from 'node:path';
import type * as vite from 'vite'; import type * as vite from 'vite';
import type { ModuleLoader, ModuleLoaderEventEmitter } from './loader.js'; import type { ModuleLoader, ModuleLoaderEventEmitter } from './loader.js';
export function createViteLoader(viteServer: vite.ViteDevServer): ModuleLoader { export function createViteLoader(viteServer: vite.ViteDevServer): ModuleLoader {
const events = new EventEmitter() as ModuleLoaderEventEmitter; const events = new EventEmitter() as ModuleLoaderEventEmitter;
viteServer.watcher.on('add', (...args) => events.emit('file-add', args)); let isTsconfigUpdated = false;
viteServer.watcher.on('unlink', (...args) => events.emit('file-unlink', args)); function isTsconfigUpdate(filePath: string) {
viteServer.watcher.on('change', (...args) => events.emit('file-change', args)); const result = path.basename(filePath) === 'tsconfig.json';
if (result) isTsconfigUpdated = true;
return result;
}
wrapMethod(viteServer.ws, 'send', (msg) => { // Skip event emit on tsconfig change as Vite restarts the server, and we don't
// want to trigger unnecessary work that will be invalidated shortly.
viteServer.watcher.on('add', (...args) => {
if (!isTsconfigUpdate(args[0])) {
events.emit('file-add', args);
}
});
viteServer.watcher.on('unlink', (...args) => {
if (!isTsconfigUpdate(args[0])) {
events.emit('file-unlink', args);
}
});
viteServer.watcher.on('change', (...args) => {
if (!isTsconfigUpdate(args[0])) {
events.emit('file-change', args);
}
});
const _wsSend = viteServer.ws.send;
viteServer.ws.send = function (...args: any) {
// If the tsconfig changed, Vite will trigger a reload as it invalidates the module.
// However in Astro, the whole server is restarted when the tsconfig changes. If we
// do a restart and reload at the same time, the browser will refetch and the server
// is not ready yet, causing a blank page. Here we block that reload from happening.
if (isTsconfigUpdated) {
isTsconfigUpdated = false;
return;
}
const msg = args[0] as vite.HMRPayload;
if (msg?.type === 'error') { if (msg?.type === 'error') {
events.emit('hmr-error', msg); events.emit('hmr-error', msg);
} }
}); _wsSend.apply(this, args);
};
return { return {
import(src) { import(src) {
@ -56,11 +89,3 @@ export function createViteLoader(viteServer: vite.ViteDevServer): ModuleLoader {
events, events,
}; };
} }
function wrapMethod(object: any, method: string, newFn: (...args: any[]) => void) {
const orig = object[method];
object[method] = function (...args: any[]) {
newFn.apply(this, args);
return orig.apply(this, args);
};
}

View file

@ -1,63 +1,7 @@
import buffer from 'node:buffer';
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import {
ByteLengthQueuingStrategy,
CountQueuingStrategy,
ReadableByteStreamController,
ReadableStream,
ReadableStreamBYOBReader,
ReadableStreamBYOBRequest,
ReadableStreamDefaultController,
ReadableStreamDefaultReader,
TransformStream,
WritableStream,
WritableStreamDefaultController,
WritableStreamDefaultWriter,
} from 'node:stream/web';
import { File, FormData, Headers, Request, Response, fetch } from 'undici';
// NOTE: This file does not intend to polyfill everything that exists, its main goal is to make life easier
// for users deploying to runtime that do support these features. In the future, we hope for this file to disappear.
// HACK (2023-08-18) Stackblitz does not support Node 18 yet, so we'll fake Node 16 support for some time until it's supported
// TODO: Remove when Node 18 is supported on Stackblitz. File should get imported from `node:buffer` instead of `undici` once this is removed
const isStackblitz = process.env.SHELL === '/bin/jsh' && process.versions.webcontainer != null;
export function apply() { export function apply() {
if (isStackblitz) {
const neededPolyfills = {
ByteLengthQueuingStrategy,
CountQueuingStrategy,
ReadableByteStreamController,
ReadableStream,
ReadableStreamBYOBReader,
ReadableStreamBYOBRequest,
ReadableStreamDefaultController,
ReadableStreamDefaultReader,
TransformStream,
WritableStream,
WritableStreamDefaultController,
WritableStreamDefaultWriter,
File,
FormData,
Headers,
Request,
Response,
fetch,
};
for (let polyfillName of Object.keys(neededPolyfills)) {
if (Object.hasOwnProperty.call(globalThis, polyfillName)) continue;
// Add polyfill to globalThis
Object.defineProperty(globalThis, polyfillName, {
configurable: true,
enumerable: true,
writable: true,
value: neededPolyfills[polyfillName as keyof typeof neededPolyfills],
});
}
}
// Remove when Node 18 is dropped for Node 20 // Remove when Node 18 is dropped for Node 20
if (!globalThis.crypto) { if (!globalThis.crypto) {
Object.defineProperty(globalThis, 'crypto', { Object.defineProperty(globalThis, 'crypto', {
@ -68,7 +12,7 @@ export function apply() {
// Remove when Node 18 is dropped for Node 20 // Remove when Node 18 is dropped for Node 20
if (!globalThis.File) { if (!globalThis.File) {
Object.defineProperty(globalThis, 'File', { Object.defineProperty(globalThis, 'File', {
value: File, value: buffer.File,
}); });
} }
} }

View file

@ -10,6 +10,15 @@ type State = {
}; };
type Events = 'astro:page-load' | 'astro:after-swap'; type Events = 'astro:page-load' | 'astro:after-swap';
let viteDevIds: { static: Record<string, string[]>; dynamic: Record<string, string[]> };
if (import.meta.env.DEV) {
// viteDevIds on a page
viteDevIds = JSON.parse(
sessionStorage.getItem('astro:viteDevIds') || '{"static":{},"dynamic":{}}'
);
}
const page = (url: { origin: string; pathname: string }) => url.origin + url.pathname;
// only update history entries that are managed by us // only update history entries that are managed by us
// leave other entries alone and do not accidently add state. // leave other entries alone and do not accidently add state.
const persistState = (state: State) => history.state && history.replaceState(state, ''); const persistState = (state: State) => history.state && history.replaceState(state, '');
@ -44,8 +53,10 @@ const PERSIST_ATTR = 'data-astro-transition-persist';
const parser = new DOMParser(); const parser = new DOMParser();
// explained at its usage // explained at its usage
let noopEl: HTMLDivElement; let noopEl: HTMLDivElement;
let reloadEl: HTMLDivElement;
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
noopEl = document.createElement('div'); noopEl = document.createElement('div');
reloadEl = document.createElement('div');
} }
// The History API does not tell you if navigation is forward or back, so // The History API does not tell you if navigation is forward or back, so
@ -198,20 +209,40 @@ async function updateDOM(
const href = el.getAttribute('href'); const href = el.getAttribute('href');
return newDocument.head.querySelector(`link[rel=stylesheet][href="${href}"]`); return newDocument.head.querySelector(`link[rel=stylesheet][href="${href}"]`);
} }
// What follows is a fix for an issue (#8472) with missing client:only styles after transition.
// That problem exists only in dev mode where styles are injected into the page by Vite.
// Returning a noop element ensures that the styles are not removed from the old document.
// Guarding the code below with the dev mode check
// allows tree shaking to remove this code in production.
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
if (el.tagName === 'STYLE' && el.dataset.viteDevId) { const viteDevId = el.getAttribute('data-vite-dev-id');
const devId = el.dataset.viteDevId; if (!viteDevId) {
// If this same style tag exists, remove it from the new page return null;
return ( }
newDocument.querySelector(`style[data-vite-dev-id="${devId}"]`) || const newDevEl = newDocument.head.querySelector(`[data-vite-dev-id="${viteDevId}"]`);
// Otherwise, keep it anyways. This is client:only styles. if (newDevEl) {
noopEl return newDevEl;
); }
// What follows is a fix for an issue (#8472) with missing client:only styles after transition.
// That problem exists only in dev mode where styles are injected into the page by Vite.
// Returning a noop element ensures that the styles are not removed from the old document.
// Guarding the code below with the dev mode check
// allows tree shaking to remove this code in production.
if (
document.querySelector(
`[${PERSIST_ATTR}] astro-island[client="only"], astro-island[client="only"][${PERSIST_ATTR}]`
)
) {
const here = page(toLocation);
const dynamicViteDevIds = viteDevIds.dynamic[here];
if (!dynamicViteDevIds) {
console.info(`
${toLocation.pathname}
Development mode only: This page uses view transitions with persisted client:only Astro islands.
On the first transition to this page, Astro did a full page reload to capture the dynamic effects of the client only code.
`);
location.href = toLocation.href;
return reloadEl;
}
if (dynamicViteDevIds?.includes(viteDevId)) {
return noopEl;
}
} }
} }
return null; return null;
@ -249,12 +280,16 @@ async function updateDOM(
// Swap head // Swap head
for (const el of Array.from(document.head.children)) { for (const el of Array.from(document.head.children)) {
const newEl = persistedHeadElement(el as HTMLElement); const newEl = persistedHeadElement(el as HTMLElement);
if (newEl === reloadEl) {
return;
}
// If the element exists in the document already, remove it // If the element exists in the document already, remove it
// from the new document and leave the current node alone // from the new document and leave the current node alone
if (newEl) { if (newEl) {
newEl.remove(); newEl.remove();
} else { } else {
// Otherwise remove the element in the head. It doesn't exist in the new page. // Otherwise remove the element from the head.
// It doesn't exist in the new page or will be re-inserted after this loop
el.remove(); el.remove();
} }
} }
@ -336,6 +371,20 @@ async function transition(
options: Options, options: Options,
popState?: State popState?: State
) { ) {
if (import.meta.env.DEV) {
const thisPageStaticViteDevIds = viteDevIds.static[page(location)];
if (thisPageStaticViteDevIds) {
const allViteDevIds = new Set<string>();
document.head
.querySelectorAll('[data-vite-dev-id]')
.forEach((el) => allViteDevIds.add(el.getAttribute('data-vite-dev-id')!));
viteDevIds.dynamic[page(location)] = [...allViteDevIds].filter(
(x) => !thisPageStaticViteDevIds.includes(x)
);
sessionStorage.setItem('astro:viteDevIds', JSON.stringify(viteDevIds, null, 2));
}
}
let finished: Promise<void>; let finished: Promise<void>;
const href = toLocation.href; const href = toLocation.href;
const response = await fetchHTML(href); const response = await fetchHTML(href);
@ -360,6 +409,14 @@ async function transition(
location.href = href; location.href = href;
return; return;
} }
if (import.meta.env.DEV) {
const staticViteDevIds = new Set<string>();
newDocument.querySelectorAll('head > [data-vite-dev-id]').forEach((el) => {
staticViteDevIds.add(el.getAttribute('data-vite-dev-id')!);
});
viteDevIds.static[page(toLocation)] = [...staticViteDevIds];
sessionStorage.setItem('astro:viteDevIds', JSON.stringify(viteDevIds, null, 2));
}
if (!popState) { if (!popState) {
// save the current scroll position before we change the DOM and transition to the new page // save the current scroll position before we change the DOM and transition to the new page

View file

@ -170,7 +170,7 @@ export async function loadFixture(inlineConfig) {
try { try {
return await fetch(resolvedUrl, init); return await fetch(resolvedUrl, init);
} catch (err) { } catch (err) {
// undici throws a vague error when it fails, so we log the url here to easily debug it // node fetch throws a vague error when it fails, so we log the url here to easily debug it
if (err.message?.includes('fetch failed')) { if (err.message?.includes('fetch failed')) {
console.error(`[astro test] failed to fetch ${resolvedUrl}`); console.error(`[astro test] failed to fetch ${resolvedUrl}`);
console.error(err); console.error(err);

View file

@ -39,5 +39,8 @@
"devDependencies": { "devDependencies": {
"astro": "workspace:*", "astro": "workspace:*",
"astro-scripts": "workspace:*" "astro-scripts": "workspace:*"
},
"publishConfig": {
"provenance": true
} }
} }

View file

@ -58,5 +58,8 @@
"cheerio": "1.0.0-rc.12", "cheerio": "1.0.0-rc.12",
"mocha": "^10.2.0", "mocha": "^10.2.0",
"wrangler": "^3.5.1" "wrangler": "^3.5.1"
},
"publishConfig": {
"provenance": true
} }
} }

View file

@ -59,5 +59,8 @@
"peerDependencies": { "peerDependencies": {
"@webcomponents/template-shadowroot": "^0.2.1", "@webcomponents/template-shadowroot": "^0.2.1",
"lit": "^2.7.0" "lit": "^2.7.0"
},
"publishConfig": {
"provenance": true
} }
} }

View file

@ -94,5 +94,8 @@
}, },
"engines": { "engines": {
"node": ">=18.14.1" "node": ">=18.14.1"
},
"publishConfig": {
"provenance": true
} }
} }

View file

@ -79,5 +79,8 @@
}, },
"engines": { "engines": {
"node": ">=18.14.1" "node": ">=18.14.1"
},
"publishConfig": {
"provenance": true
} }
} }

View file

@ -49,7 +49,9 @@
"cheerio": "1.0.0-rc.12", "cheerio": "1.0.0-rc.12",
"express": "^4.18.2", "express": "^4.18.2",
"mocha": "^10.2.0", "mocha": "^10.2.0",
"node-mocks-http": "^1.13.0", "node-mocks-http": "^1.13.0"
"undici": "^5.23.0" },
"publishConfig": {
"provenance": true
} }
} }

View file

@ -8,15 +8,12 @@ import type { OutgoingHttpHeaders } from 'node:http';
* @returns NodeJS OutgoingHttpHeaders object with multiple set-cookie handled as an array of values * @returns NodeJS OutgoingHttpHeaders object with multiple set-cookie handled as an array of values
*/ */
export const createOutgoingHttpHeaders = ( export const createOutgoingHttpHeaders = (
webHeaders: Headers | undefined | null headers: Headers | undefined | null
): OutgoingHttpHeaders | undefined => { ): OutgoingHttpHeaders | undefined => {
if (!webHeaders) { if (!headers) {
return undefined; return undefined;
} }
// re-type to access Header.getSetCookie()
const headers = webHeaders as HeadersWithGetSetCookie;
// at this point, a multi-value'd set-cookie header is invalid (it was concatenated as a single CSV, which is not valid for set-cookie) // at this point, a multi-value'd set-cookie header is invalid (it was concatenated as a single CSV, which is not valid for set-cookie)
const nodeHeaders: OutgoingHttpHeaders = Object.fromEntries(headers.entries()); const nodeHeaders: OutgoingHttpHeaders = Object.fromEntries(headers.entries());
@ -26,7 +23,8 @@ export const createOutgoingHttpHeaders = (
// if there is > 1 set-cookie header, we have to fix it to be an array of values // if there is > 1 set-cookie header, we have to fix it to be an array of values
if (headers.has('set-cookie')) { if (headers.has('set-cookie')) {
const cookieHeaders = headers.getSetCookie(); // @ts-expect-error
const cookieHeaders = headers.getSetCookie() as string[];
if (cookieHeaders.length > 1) { if (cookieHeaders.length > 1) {
// the Headers.entries() API already normalized all header names to lower case so we can safely index this as 'set-cookie' // the Headers.entries() API already normalized all header names to lower case so we can safely index this as 'set-cookie'
nodeHeaders['set-cookie'] = cookieHeaders; nodeHeaders['set-cookie'] = cookieHeaders;
@ -35,8 +33,3 @@ export const createOutgoingHttpHeaders = (
return nodeHeaders; return nodeHeaders;
}; };
interface HeadersWithGetSetCookie extends Headers {
// the @astrojs/webapi polyfill makes this available (as of undici@5.19.0), but tsc doesn't pick it up on the built-in Headers type from DOM lib
getSetCookie(): string[];
}

View file

@ -38,5 +38,8 @@
"devDependencies": { "devDependencies": {
"astro": "workspace:*", "astro": "workspace:*",
"astro-scripts": "workspace:*" "astro-scripts": "workspace:*"
},
"publishConfig": {
"provenance": true
} }
} }

View file

@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url';
import sirv from './sirv.js'; import sirv from './sirv.js';
const resolve = createRequire(import.meta.url).resolve; const resolve = createRequire(import.meta.url).resolve;
type PartytownOptions = { export type PartytownOptions = {
config?: PartytownConfig; config?: PartytownConfig;
}; };

View file

@ -52,5 +52,8 @@
}, },
"engines": { "engines": {
"node": ">=18.14.1" "node": ">=18.14.1"
},
"publishConfig": {
"provenance": true
} }
} }

View file

@ -40,5 +40,8 @@
}, },
"dependencies": { "dependencies": {
"throttles": "^1.0.1" "throttles": "^1.0.1"
},
"publishConfig": {
"provenance": true
} }
} }

View file

@ -67,5 +67,8 @@
}, },
"engines": { "engines": {
"node": ">=18.14.1" "node": ">=18.14.1"
},
"publishConfig": {
"provenance": true
} }
} }

View file

@ -43,5 +43,8 @@
"chai": "^4.3.7", "chai": "^4.3.7",
"mocha": "^10.2.0", "mocha": "^10.2.0",
"xml2js": "0.6.2" "xml2js": "0.6.2"
},
"publishConfig": {
"provenance": true
} }
} }

View file

@ -47,5 +47,8 @@
}, },
"engines": { "engines": {
"node": ">=18.14.1" "node": ">=18.14.1"
},
"publishConfig": {
"provenance": true
} }
} }

View file

@ -53,5 +53,8 @@
}, },
"engines": { "engines": {
"node": ">=18.14.1" "node": ">=18.14.1"
},
"publishConfig": {
"provenance": true
} }
} }

View file

@ -45,5 +45,8 @@
"peerDependencies": { "peerDependencies": {
"astro": "workspace:^3.2.2", "astro": "workspace:^3.2.2",
"tailwindcss": "^3.0.24" "tailwindcss": "^3.0.24"
},
"publishConfig": {
"provenance": true
} }
} }

View file

@ -72,5 +72,8 @@
"chai-jest-snapshot": "^2.0.0", "chai-jest-snapshot": "^2.0.0",
"cheerio": "1.0.0-rc.12", "cheerio": "1.0.0-rc.12",
"mocha": "^10.2.0" "mocha": "^10.2.0"
},
"publishConfig": {
"provenance": true
} }
} }

View file

@ -61,5 +61,8 @@
}, },
"engines": { "engines": {
"node": ">=18.14.1" "node": ">=18.14.1"
},
"publishConfig": {
"provenance": true
} }
} }

View file

@ -37,5 +37,8 @@
"keywords": [ "keywords": [
"astro", "astro",
"astro-component" "astro-component"
] ],
"publishConfig": {
"provenance": true
}
} }

View file

@ -57,5 +57,8 @@
"chai": "^4.3.7", "chai": "^4.3.7",
"mdast-util-mdx-expression": "^1.3.2", "mdast-util-mdx-expression": "^1.3.2",
"mocha": "^10.2.0" "mocha": "^10.2.0"
},
"publishConfig": {
"provenance": true
} }
} }

View file

@ -76,8 +76,8 @@ export function remarkShiki({
// It would become this before hitting our regexes: // It would become this before hitting our regexes:
// &lt;span class=&quot;line&quot; // &lt;span class=&quot;line&quot;
// Replace "shiki" class naming with "astro" and add "is:raw". // Replace "shiki" class naming with "astro".
html = html.replace(/<pre class="(.*?)shiki(.*?)"/, `<pre is:raw class="$1astro-code$2"`); html = html.replace(/<pre class="(.*?)shiki(.*?)"/, `<pre class="$1astro-code$2"`);
// Add "user-select: none;" for "+"/"-" diff symbols // Add "user-select: none;" for "+"/"-" diff symbols
if (node.lang === 'diff') { if (node.lang === 'diff') {
html = html.replace( html = html.replace(

View file

@ -0,0 +1,12 @@
import { createMarkdownProcessor } from '../dist/index.js';
import chai from 'chai';
describe('shiki syntax highlighting', async () => {
const processor = await createMarkdownProcessor();
it('does not add is:raw to the output', async () => {
const { code } = await processor.render('```\ntest\n```');
chai.expect(code).not.to.contain('is:raw');
});
});

View file

@ -35,7 +35,6 @@
"dset": "^3.1.2", "dset": "^3.1.2",
"is-docker": "^3.0.0", "is-docker": "^3.0.0",
"is-wsl": "^3.0.0", "is-wsl": "^3.0.0",
"undici": "^5.23.0",
"which-pm-runs": "^1.1.0" "which-pm-runs": "^1.1.0"
}, },
"devDependencies": { "devDependencies": {
@ -49,5 +48,8 @@
}, },
"engines": { "engines": {
"node": ">=18.14.1" "node": ">=18.14.1"
},
"publishConfig": {
"provenance": true
} }
} }

View file

@ -1,5 +1,4 @@
const ASTRO_TELEMETRY_ENDPOINT = `https://telemetry.astro.build/api/v1/record`; const ASTRO_TELEMETRY_ENDPOINT = `https://telemetry.astro.build/api/v1/record`;
import { fetch } from 'undici';
export function post(body: Record<string, any>): Promise<any> { export function post(body: Record<string, any>): Promise<any> {
return fetch(ASTRO_TELEMETRY_ENDPOINT, { return fetch(ASTRO_TELEMETRY_ENDPOINT, {

View file

@ -38,5 +38,8 @@
"keywords": [ "keywords": [
"astro", "astro",
"astro-component" "astro-component"
] ],
"publishConfig": {
"provenance": true
}
} }

View file

@ -628,9 +628,6 @@ importers:
tsconfig-resolver: tsconfig-resolver:
specifier: ^3.0.1 specifier: ^3.0.1
version: 3.0.1 version: 3.0.1
undici:
specifier: ^5.23.0
version: 5.23.0
unist-util-visit: unist-util-visit:
specifier: ^4.1.2 specifier: ^4.1.2
version: 4.1.2 version: 4.1.2
@ -4307,9 +4304,6 @@ importers:
node-mocks-http: node-mocks-http:
specifier: ^1.13.0 specifier: ^1.13.0
version: 1.13.0 version: 1.13.0
undici:
specifier: ^5.23.0
version: 5.23.0
packages/integrations/node/test/fixtures/api-route: packages/integrations/node/test/fixtures/api-route:
dependencies: dependencies:
@ -5005,9 +4999,6 @@ importers:
is-wsl: is-wsl:
specifier: ^3.0.0 specifier: ^3.0.0
version: 3.0.0 version: 3.0.0
undici:
specifier: ^5.23.0
version: 5.23.0
which-pm-runs: which-pm-runs:
specifier: ^1.1.0 specifier: ^1.1.0
version: 1.1.0 version: 1.1.0
@ -17276,6 +17267,7 @@ packages:
engines: {node: '>=14.0'} engines: {node: '>=14.0'}
dependencies: dependencies:
busboy: 1.6.0 busboy: 1.6.0
dev: true
/unherit@3.0.1: /unherit@3.0.1:
resolution: {integrity: sha512-akOOQ/Yln8a2sgcLj4U0Jmx0R5jpIg2IUyRrWOzmEbjBtGzBdHtSeFKgoEcoH4KYIG/Pb8GQ/BwtYm0GCq1Sqg==} resolution: {integrity: sha512-akOOQ/Yln8a2sgcLj4U0Jmx0R5jpIg2IUyRrWOzmEbjBtGzBdHtSeFKgoEcoH4KYIG/Pb8GQ/BwtYm0GCq1Sqg==}