Skip to content

Commit

Permalink
Merge branch 'main' into mk/prioritize-prerendered-routes
Browse files Browse the repository at this point in the history
  • Loading branch information
MoustaphaDev committed May 30, 2023
2 parents 786d836 + e20a3b2 commit 5a1f8c2
Show file tree
Hide file tree
Showing 50 changed files with 577 additions and 111 deletions.
5 changes: 5 additions & 0 deletions .changeset/eighty-gifts-cheer.md
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fix CSS deduping and missing chunks
7 changes: 7 additions & 0 deletions .changeset/eleven-walls-explain.md
@@ -0,0 +1,7 @@
---
'@astrojs/preact': patch
'@astrojs/react': patch
'@astrojs/vue': patch
---

Fix `astro-static-slot` hydration mismatch error
5 changes: 5 additions & 0 deletions .changeset/fifty-months-mix.md
@@ -0,0 +1,5 @@
---
'@astrojs/webapi': minor
---

Add polyfill for `crypto`
5 changes: 5 additions & 0 deletions .changeset/small-wombats-know.md
@@ -0,0 +1,5 @@
---
'astro': patch
---

fix miss a head when the templaterender has a promise
5 changes: 5 additions & 0 deletions .changeset/strange-socks-give.md
@@ -0,0 +1,5 @@
---
'astro': patch
---

Use `AstroError` for `Astro.glob` errors
5 changes: 5 additions & 0 deletions .changeset/wise-cars-hear.md
@@ -0,0 +1,5 @@
---
'astro': patch
---

The `src` property returned by ESM importing images with `astro:assets` is now an absolute path, unlocking support for importing images outside the project.
23 changes: 21 additions & 2 deletions packages/astro/e2e/errors.test.js
@@ -1,14 +1,21 @@
import { expect } from '@playwright/test';
import { getErrorOverlayContent, testFactory } from './test-utils.js';
import { getErrorOverlayContent, silentLogging, testFactory } from './test-utils.js';

const test = testFactory({
root: './fixtures/errors/',
// Only test the error overlay, don't print to console
vite: {
logLevel: 'silent',
},
});

let devServer;

test.beforeAll(async ({ astro }) => {
devServer = await astro.startDevServer();
devServer = await astro.startDevServer({
// Only test the error overlay, don't print to console
logging: silentLogging,
});
});

test.afterAll(async ({ astro }) => {
Expand Down Expand Up @@ -89,4 +96,16 @@ test.describe('Error display', () => {

expect(await page.locator('vite-error-overlay').count()).toEqual(0);
});

test('astro glob no match error', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/astro-glob-no-match'), { waitUntil: 'networkidle' });
const message = (await getErrorOverlayContent(page)).message;
expect(message).toMatch('did not return any matching files');
});

test('astro glob used outside of an astro file', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/astro-glob-outside-astro'), { waitUntil: 'networkidle' });
const message = (await getErrorOverlayContent(page)).message;
expect(message).toMatch('can only be used in');
});
});
@@ -0,0 +1,3 @@
export function globSomething(Astro) {
return Astro.glob('./*.lua')
}
@@ -0,0 +1,3 @@
---
Astro.glob('./*.lua')
---
@@ -0,0 +1,5 @@
---
import { globSomething } from '../components/AstroGlobOutsideAstro'
globSomething(Astro)
---
16 changes: 16 additions & 0 deletions packages/astro/e2e/nested-in-react.test.js
Expand Up @@ -14,6 +14,22 @@ test.afterAll(async () => {
});

test.describe('Nested Frameworks in React', () => {
test('No hydration mismatch', async ({ page, astro }) => {
// Get browser logs
const logs = [];
page.on('console', (msg) => logs.push(msg.text()));

await page.goto(astro.resolveUrl('/'));

// wait for root island to hydrate
const counter = page.locator('#react-counter');
await waitForHydrate(page, counter);

for (const log of logs) {
expect(log, 'React hydration mismatch').not.toMatch('An error occurred during hydration');
}
});

test('React counter', async ({ astro, page }) => {
await page.goto(astro.resolveUrl('/'));

Expand Down
16 changes: 16 additions & 0 deletions packages/astro/e2e/nested-in-vue.test.js
Expand Up @@ -14,6 +14,22 @@ test.afterAll(async () => {
});

test.describe('Nested Frameworks in Vue', () => {
test('no hydration mismatch', async ({ page, astro }) => {
// Get browser logs
const logs = [];
page.on('console', (msg) => logs.push(msg.text()));

await page.goto(astro.resolveUrl('/'));

// wait for root island to hydrate
const counter = page.locator('#vue-counter');
await waitForHydrate(page, counter);

for (const log of logs) {
expect(log, 'Vue hydration mismatch').not.toMatch('Hydration node mismatch');
}
});

test('React counter', async ({ astro, page }) => {
await page.goto(astro.resolveUrl('/'));

Expand Down
2 changes: 2 additions & 0 deletions packages/astro/e2e/test-utils.js
Expand Up @@ -5,6 +5,8 @@ import { loadFixture as baseLoadFixture } from '../test/test-utils.js';

export const isWindows = process.platform === 'win32';

export { silentLogging } from '../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));
const testFileToPort = new Map();
Expand Down
9 changes: 9 additions & 0 deletions packages/astro/src/assets/services/service.ts
Expand Up @@ -109,6 +109,7 @@ export type BaseServiceTransform = {
*/
export const baseService: Omit<LocalImageService, 'transform'> = {
validateOptions(options) {
// `src` is missing or is `undefined`.
if (!options.src || (typeof options.src !== 'string' && typeof options.src !== 'object')) {
throw new AstroError({
...AstroErrorData.ExpectedImage,
Expand All @@ -117,6 +118,14 @@ export const baseService: Omit<LocalImageService, 'transform'> = {
}

if (!isESMImportedImage(options.src)) {
// User passed an `/@fs/` path instead of the full image.
if (options.src.startsWith('/@fs/')) {
throw new AstroError({
...AstroErrorData.LocalImageUsedWrongly,
message: AstroErrorData.LocalImageUsedWrongly.message(options.src),
});
}

// For remote images, width and height are explicitly required as we can't infer them from the file
let missingDimension: 'width' | 'height' | 'both' | undefined;
if (!options.width && !options.height) {
Expand Down
27 changes: 3 additions & 24 deletions packages/astro/src/assets/utils/emitAsset.ts
Expand Up @@ -2,14 +2,13 @@ import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import slash from 'slash';
import type { AstroConfig, AstroSettings } from '../../@types/astro';
import { prependForwardSlash } from '../../core/path.js';
import { imageMetadata, type Metadata } from './metadata.js';

export async function emitESMImage(
id: string | undefined,
watchMode: boolean,
fileEmitter: any,
settings: Pick<AstroSettings, 'config'>
fileEmitter: any
): Promise<Metadata | undefined> {
if (!id) {
return undefined;
Expand Down Expand Up @@ -40,34 +39,14 @@ export async function emitESMImage(
url.searchParams.append('origHeight', meta.height.toString());
url.searchParams.append('origFormat', meta.format);

meta.src = rootRelativePath(settings.config, url);
meta.src = `/@fs` + prependForwardSlash(fileURLToNormalizedPath(url));
}

return meta;
}

/**
* Utilities inlined from `packages/astro/src/core/util.ts`
* Avoids ESM / CJS bundling failures when accessed from integrations
* due to Vite dependencies in core.
*/

function rootRelativePath(config: Pick<AstroConfig, 'root'>, url: URL): string {
const basePath = fileURLToNormalizedPath(url);
const rootPath = fileURLToNormalizedPath(config.root);
return prependForwardSlash(basePath.slice(rootPath.length));
}

function prependForwardSlash(filePath: string): string {
return filePath[0] === '/' ? filePath : '/' + filePath;
}

function fileURLToNormalizedPath(filePath: URL): string {
// Uses `slash` package instead of Vite's `normalizePath`
// to avoid CJS bundling issues.
return slash(fileURLToPath(filePath) + filePath.search).replace(/\\/g, '/');
}

export function emoji(char: string, fallback: string): string {
return process.platform !== 'win32' ? char : fallback;
}
9 changes: 4 additions & 5 deletions packages/astro/src/assets/vite-plugin-assets.ts
Expand Up @@ -107,13 +107,12 @@ export default function assets({
}

const url = new URL(req.url, 'file:');
const filePath = url.searchParams.get('href');

if (!filePath) {
if (!url.searchParams.has('href')) {
return next();
}

const filePathURL = new URL('.' + filePath, settings.config.root);
const filePath = url.searchParams.get('href')?.slice('/@fs'.length);
const filePathURL = new URL('.' + filePath, 'file:');
const file = await fs.readFile(filePathURL);

// Get the file's metadata from the URL
Expand Down Expand Up @@ -243,7 +242,7 @@ export default function assets({

const cleanedUrl = removeQueryString(id);
if (/\.(jpeg|jpg|png|tiff|webp|gif|svg)$/.test(cleanedUrl)) {
const meta = await emitESMImage(id, this.meta.watchMode, this.emitFile, settings);
const meta = await emitESMImage(id, this.meta.watchMode, this.emitFile);
return `export default ${JSON.stringify(meta)}`;
}
},
Expand Down
10 changes: 2 additions & 8 deletions packages/astro/src/content/runtime-assets.ts
@@ -1,21 +1,15 @@
import type { PluginContext } from 'rollup';
import { z } from 'zod';
import type { AstroSettings } from '../@types/astro.js';
import { emitESMImage } from '../assets/index.js';

export function createImage(
settings: Pick<AstroSettings, 'config'>,
pluginContext: PluginContext,
entryFilePath: string
) {
export function createImage(pluginContext: PluginContext, entryFilePath: string) {
return () => {
return z.string().transform(async (imagePath, ctx) => {
const resolvedFilePath = (await pluginContext.resolve(imagePath, entryFilePath))?.id;
const metadata = await emitESMImage(
resolvedFilePath,
pluginContext.meta.watchMode,
pluginContext.emitFile,
settings
pluginContext.emitFile
);

if (!metadata) {
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/content/utils.ts
Expand Up @@ -111,7 +111,7 @@ export async function getEntryData(
}

schema = schema({
image: createImage({ config }, pluginContext, entry._internal.filePath),
image: createImage(pluginContext, entry._internal.filePath),
});
}

Expand Down
11 changes: 7 additions & 4 deletions packages/astro/src/core/build/internal.ts
Expand Up @@ -8,10 +8,13 @@ import type { PageBuildData, StylesheetAsset, ViteID } from './types';

export interface BuildInternals {
/**
* The module ids of all CSS chunks, used to deduplicate CSS assets between
* SSR build and client build in vite-plugin-css.
* Each CSS module is named with a chunk id derived from the Astro pages they
* are used in by default. It's easy to crawl this relation in the SSR build as
* the Astro pages are the entrypoint, but not for the client build as hydratable
* components are the entrypoint instead. This map is used as a cache from the SSR
* build so the client can pick up the same information and use the same chunk ids.
*/
cssChunkModuleIds: Set<string>;
cssModuleToChunkIdMap: Map<string, string>;

// A mapping of hoisted script ids back to the exact hoisted scripts it references
hoistedScriptIdToHoistedMap: Map<string, Set<string>>;
Expand Down Expand Up @@ -92,7 +95,7 @@ export function createBuildInternals(): BuildInternals {
const hoistedScriptIdToPagesMap = new Map<string, Set<string>>();

return {
cssChunkModuleIds: new Set(),
cssModuleToChunkIdMap: new Map(),
hoistedScriptIdToHoistedMap,
hoistedScriptIdToPagesMap,
entrySpecifierToBundleMap: new Map<string, string>(),
Expand Down
41 changes: 17 additions & 24 deletions packages/astro/src/core/build/plugins/plugin-css.ts
Expand Up @@ -64,20 +64,7 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
const cssBuildPlugin: VitePlugin = {
name: 'astro:rollup-plugin-build-css',

transform(_, id) {
// In the SSR build, styles that are bundled are tracked in `internals.cssChunkModuleIds`.
// In the client build, if we're also bundling the same style, return an empty string to
// deduplicate the final CSS output.
if (options.target === 'client' && internals.cssChunkModuleIds.has(id)) {
return '';
}
},

outputOptions(outputOptions) {
// Skip in client builds as its module graph doesn't have reference to Astro pages
// to be able to chunk based on it's related top-level pages.
if (options.target === 'client') return;

const assetFileNames = outputOptions.assetFileNames;
const namingIncludesHash = assetFileNames?.toString().includes('[hash]');
const createNameForParentPages = namingIncludesHash
Expand All @@ -89,16 +76,31 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
// For CSS, create a hash of all of the pages that use it.
// This causes CSS to be built into shared chunks when used by multiple pages.
if (isBuildableCSSRequest(id)) {
// For client builds that has hydrated components as entrypoints, there's no way
// to crawl up and find the pages that use it. So we lookup the cache during SSR
// build (that has the pages information) to derive the same chunk id so they
// match up on build, making sure both builds has the CSS deduped.
// NOTE: Components that are only used with `client:only` may not exist in the cache
// and that's okay. We can use Rollup's default chunk strategy instead as these CSS
// are outside of the SSR build scope, which no dedupe is needed.
if (options.target === 'client') {
return internals.cssModuleToChunkIdMap.get(id)!;
}

for (const [pageInfo] of walkParentInfos(id, {
getModuleInfo: meta.getModuleInfo,
})) {
if (new URL(pageInfo.id, 'file://').searchParams.has(PROPAGATED_ASSET_FLAG)) {
// Split delayed assets to separate modules
// so they can be injected where needed
return createNameHash(id, [id]);
const chunkId = createNameHash(id, [id]);
internals.cssModuleToChunkIdMap.set(id, chunkId);
return chunkId;
}
}
return createNameForParentPages(id, meta);
const chunkId = createNameForParentPages(id, meta);
internals.cssModuleToChunkIdMap.set(id, chunkId);
return chunkId;
}
},
});
Expand All @@ -113,15 +115,6 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] {
// Skip if the chunk has no CSS, we want to handle CSS chunks only
if (meta.importedCss.size < 1) continue;

// In the SSR build, keep track of all CSS chunks' modules as the client build may
// duplicate them, e.g. for `client:load` components that render in SSR and client
// for hydation.
if (options.target === 'server') {
for (const id of Object.keys(chunk.modules)) {
internals.cssChunkModuleIds.add(id);
}
}

// For the client build, client:only styles need to be mapped
// over to their page. For this chunk, determine if it's a child of a
// client:only component and if so, add its CSS to the page it belongs to.
Expand Down

0 comments on commit 5a1f8c2

Please sign in to comment.