chore: first implementation for serialized config (#13000)

Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev>
This commit is contained in:
Emanuele Stoppa 2025-01-21 15:53:54 +00:00 committed by GitHub
parent 78fd73a0df
commit ddbcc17b41
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 409 additions and 0 deletions

View file

@ -189,6 +189,18 @@ declare module 'astro:middleware' {
export * from 'astro/virtual-modules/middleware.js';
}
declare module 'astro:manifest/server' {
type ServerConfigSerialized = import('./dist/types/public/manifest.js').ServerConfigSerialized;
const manifest: ServerConfigSerialized;
export default manifest;
}
declare module 'astro:manifest/client' {
type ClientConfigSerialized = import('./dist/types/public/manifest.js').ClientConfigSerialized;
const manifest: ClientConfigSerialized;
export default manifest;
}
declare module 'astro:components' {
export * from 'astro/components';
}

View file

@ -97,6 +97,7 @@ export const ASTRO_CONFIG_DEFAULTS = {
contentIntellisense: false,
responsiveImages: false,
svg: false,
serializeManifest: false,
},
} satisfies AstroUserConfig & { server: { open: boolean } };
@ -589,6 +590,10 @@ export const AstroConfigSchema = z.object({
}
return svgConfig;
}),
serializeManifest: z
.boolean()
.optional()
.default(ASTRO_CONFIG_DEFAULTS.experimental.serializeManifest),
})
.strict(
`Invalid or outdated experimental feature.\nCheck for incorrect spelling or outdated Astro version.\nSee https://docs.astro.build/en/reference/experimental-flags/ for a list of all current experiments.`,

View file

@ -16,6 +16,7 @@ import { createEnvLoader } from '../env/env-loader.js';
import { astroEnv } from '../env/vite-plugin-env.js';
import { importMetaEnv } from '../env/vite-plugin-import-meta-env.js';
import astroInternationalization from '../i18n/vite-plugin-i18n.js';
import astroVirtualManifestPlugin from '../manifest/virtual-module.js';
import astroPrefetch from '../prefetch/vite-plugin-prefetch.js';
import astroDevToolbar from '../toolbar/vite-plugin-dev-toolbar.js';
import astroTransitions from '../transitions/vite-plugin-transitions.js';
@ -141,6 +142,7 @@ export async function createVite(
exclude: ['astro', 'node-fetch'],
},
plugins: [
astroVirtualManifestPlugin({ settings, logger }),
configAliasVitePlugin({ settings }),
astroLoadFallbackPlugin({ fs, root: settings.config.root }),
astroVitePlugin({ settings, logger }),

View file

@ -1782,6 +1782,18 @@ export const ActionCalledFromServerError = {
hint: 'See the `Astro.callAction()` reference for usage examples: https://docs.astro.build/en/reference/api-reference/#callaction',
} satisfies ErrorData;
/**
* @docs
* @description
* Cannot the module without enabling the experimental feature
*/
export const CantUseManifestModule = {
name: 'CantUseManifestModule',
title: 'Cannot the module without enabling the experimental feature.',
message: (moduleName) =>
`Cannot import the module "${moduleName}" because the experimental feature is disabled. Enable \`experimental.serializeManifest\` in your \`astro.config.mjs\` `,
} satisfies ErrorData;
// Generic catch-all - Only use this in extreme cases, like if there was a cosmic ray bit flip.
export const UnknownError = { name: 'UnknownError', title: 'Unknown Error.' } satisfies ErrorData;

View file

@ -0,0 +1,102 @@
import type { Plugin } from 'vite';
import { CantUseManifestModule } from '../core/errors/errors-data.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js';
import type { Logger } from '../core/logger/core.js';
import type { AstroSettings } from '../types/astro.js';
import type {
AstroConfig,
ClientConfigSerialized,
ServerConfigSerialized,
} from '../types/public/index.js';
const VIRTUAL_SERVER_ID = 'astro:manifest/server';
const RESOLVED_VIRTUAL_SERVER_ID = '\0' + VIRTUAL_SERVER_ID;
const VIRTUAL_CLIENT_ID = 'astro:manifest/client';
const RESOLVED_VIRTUAL_CLIENT_ID = '\0' + VIRTUAL_CLIENT_ID;
export default function virtualModulePlugin({
settings,
logger: _logger,
}: { settings: AstroSettings; logger: Logger }): Plugin {
return {
enforce: 'pre',
name: 'astro-manifest-plugin',
resolveId(id) {
// Resolve the virtual module
if (VIRTUAL_SERVER_ID === id) {
return RESOLVED_VIRTUAL_SERVER_ID;
} else if (VIRTUAL_CLIENT_ID === id) {
return RESOLVED_VIRTUAL_CLIENT_ID;
}
},
load(id, opts) {
// client
if (id === RESOLVED_VIRTUAL_CLIENT_ID) {
if (!settings.config.experimental.serializeManifest) {
throw new AstroError({
...CantUseManifestModule,
message: CantUseManifestModule.message(VIRTUAL_CLIENT_ID),
});
}
// There's nothing wrong about using `/client` on the server
return `${serializeClientConfig(settings.config)};`;
}
// server
else if (id == RESOLVED_VIRTUAL_SERVER_ID) {
if (!settings.config.experimental.serializeManifest) {
throw new AstroError({
...CantUseManifestModule,
message: CantUseManifestModule.message(VIRTUAL_SERVER_ID),
});
}
if (!opts?.ssr) {
throw new AstroError({
...AstroErrorData.ServerOnlyModule,
message: AstroErrorData.ServerOnlyModule.message(VIRTUAL_SERVER_ID),
});
}
return `${serializeServerConfig(settings.config)};`;
}
},
};
}
function serializeClientConfig(config: AstroConfig): string {
const serClientConfig: ClientConfigSerialized = {
base: config.base,
i18n: config.i18n,
build: {
format: config.build.format,
redirects: config.build.redirects,
},
trailingSlash: config.trailingSlash,
compressHTML: config.compressHTML,
site: config.site,
legacy: config.legacy,
};
const output = [];
for (const [key, value] of Object.entries(serClientConfig)) {
output.push(`export const ${key} = ${JSON.stringify(value)};`);
}
return output.join('\n') + '\n';
}
function serializeServerConfig(config: AstroConfig): string {
const serverConfig: ServerConfigSerialized = {
build: {
client: config.build.client,
server: config.build.server,
},
cacheDir: config.cacheDir,
outDir: config.outDir,
publicDir: config.publicDir,
srcDir: config.srcDir,
root: config.root,
};
const output = [];
for (const [key, value] of Object.entries(serverConfig)) {
output.push(`export const ${key} = ${JSON.stringify(value)};`);
}
return output.join('\n') + '\n';
}

View file

@ -2059,6 +2059,19 @@ export interface ViteUserConfig extends OriginalViteUserConfig {
*/
mode: SvgRenderMode;
};
/**
* @name experimental.serializeManifest
* @type {boolean}
* @default `false`
* @version 5.x
* @description
*
* Allows to use the virtual modules `astro:manifest/server` and `astro:manifest/client`.
*
* These two virtual modules contain a serializable subset of the Astro configuration.
*/
serializeManifest?: boolean;
};
}

View file

@ -9,6 +9,7 @@ export type * from './context.js';
export type * from './preview.js';
export type * from './content.js';
export type * from './common.js';
export type * from './manifest.js';
export type { AstroIntegrationLogger } from '../../core/logger/core.js';
export type { ToolbarServerHelpers } from '../../runtime/client/dev-toolbar/helpers.js';

View file

@ -0,0 +1,26 @@
/**
* **IMPORTANT**: use the `Pick` interface to select only the properties that we want to expose
* to the users. Using blanket types could expose properties that we don't want. So if we decide to expose
* properties, we need to be good at justifying them. For example: why you need this config? can't you use an integration?
* why do you need access to the shiki config? (very low-level confiig)
*/
import type { AstroConfig } from './config.js';
export type SerializedClientBuild = Pick<AstroConfig['build'], 'format' | 'redirects'>;
export type SerializedServerBuild = Pick<AstroConfig['build'], 'client' | 'server'>;
export type ClientConfigSerialized = Pick<
AstroConfig,
'base' | 'i18n' | 'trailingSlash' | 'compressHTML' | 'site' | 'legacy'
> & {
build: SerializedClientBuild;
};
export type ServerConfigSerialized = Pick<
AstroConfig,
'cacheDir' | 'outDir' | 'publicDir' | 'srcDir' | 'root'
> & {
build: SerializedServerBuild;
};

View file

@ -0,0 +1,13 @@
import { defineConfig } from "astro/config";
// https://astro.build/config
export default defineConfig({
site: "https://astro.build/",
experimental: {
serializeManifest: true,
},
i18n: {
locales: ["en", "fr"],
defaultLocale: "en",
}
});

View file

@ -0,0 +1,8 @@
{
"name": "@test/astro-manifest",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}

View file

@ -0,0 +1,20 @@
---
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Document</title>
</head>
<body>
<h1>Hello, World!</h1>
<p>Welcome to this Astro page.</p>
<script>
import { outDir } from "astro:manifest/server"
console.log(outDir)
</script>
</body>
</html>

View file

@ -0,0 +1,19 @@
---
import { base, i18n, trailingSlash, compressHTML, site, legacy, build } from "astro:manifest/client";
const config = JSON.stringify({ base, i18n, build, trailingSlash, compressHTML, site, legacy });
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Document</title>
</head>
<body>
<h1>Hello, World!</h1>
<p>Welcome to this Astro page.</p>
<p id="config">{config}</p>
</body>
</html>

View file

@ -0,0 +1,21 @@
---
import { root, outDir, srcDir, build, cacheDir } from "astro:manifest/server";
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Document</title>
</head>
<body>
<h1>Hello, World!</h1>
<p>Welcome to this Astro page.</p>
<p id="out-dir">{outDir}</p>
<p id="src-dir">{srcDir}</p>
<p id="root">{root}</p>
<p id="cache-dir">{cacheDir}</p>
<p id="build-client">{build.client}</p>
<p id="build-server">{build.server}</p>
</body>
</html>

View file

@ -0,0 +1,149 @@
import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import * as cheerio from 'cheerio';
import { ServerOnlyModule } from '../dist/core/errors/errors-data.js';
import { AstroError } from '../dist/core/errors/index.js';
import { loadFixture } from './test-utils.js';
describe('astro:manifest/client', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
let devServer;
describe('when the experimental flag is not enabled', async () => {
before(async () => {
fixture = await loadFixture({
root: './fixtures/astro-manifest/',
experimental: {
serializeManifest: false,
},
});
devServer = await fixture.startDevServer();
});
after(async () => {
await devServer.stop();
});
it('should throw an error when importing the module', async () => {
const response = await fixture.fetch('/');
const html = await response.text();
assert.match(html, /CantUseManifestModule/);
});
});
describe('when the experimental flag is enabled', async () => {
before(async () => {
fixture = await loadFixture({
root: './fixtures/astro-manifest/',
});
devServer = await fixture.startDevServer();
});
after(async () => {
await devServer.stop();
});
it('should return the expected properties', async () => {
const response = await fixture.fetch('/');
const html = await response.text();
const $ = cheerio.load(html);
assert.deepEqual(
$('#config').text(),
JSON.stringify({
base: '/',
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr'],
routing: {
prefixDefaultLocale: false,
redirectToDefaultLocale: true,
fallbackType: 'redirect',
},
},
build: {
format: 'directory',
redirects: true,
},
trailingSlash: 'ignore',
compressHTML: true,
site: 'https://astro.build/',
legacy: {
collections: false,
},
}),
);
});
});
});
describe('astro:manifest/server', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
let devServer;
describe('when build', () => {
before(async () => {
fixture = await loadFixture({
root: './fixtures/astro-manifest/',
});
});
it('should return an error when using inside a client script', async () => {
const error = await fixture.build().catch((err) => err);
assert.equal(error instanceof AstroError, true);
assert.equal(error.name, ServerOnlyModule.name);
});
});
describe('when the experimental flag is not enabled', async () => {
before(async () => {
fixture = await loadFixture({
root: './fixtures/astro-manifest/',
experimental: {
serializeManifest: false,
},
});
devServer = await fixture.startDevServer();
});
after(async () => {
await devServer.stop();
});
it('should throw an error when importing the module', async () => {
const response = await fixture.fetch('/server');
const html = await response.text();
assert.match(html, /CantUseManifestModule/);
});
});
describe('when the experimental flag is enabled', async () => {
before(async () => {
fixture = await loadFixture({
root: './fixtures/astro-manifest/',
});
devServer = await fixture.startDevServer();
});
after(async () => {
await devServer.stop();
});
it('should return the expected properties', async () => {
const response = await fixture.fetch('/server');
const html = await response.text();
const $ = cheerio.load(html);
assert.ok($('#out-dir').text().endsWith('/dist/'));
assert.ok($('#src-dir').text().endsWith('/src/'));
assert.ok($('#cache-dir').text().endsWith('/.astro/'));
assert.ok($('#root').text().endsWith('/'));
assert.ok($('#build-client').text().endsWith('/dist/client/'));
assert.ok($('#build-server').text().endsWith('/dist/server/'));
});
});
});

View file

@ -2159,6 +2159,12 @@ importers:
specifier: workspace:*
version: link:../../..
packages/astro/test/fixtures/astro-manifest:
dependencies:
astro:
specifier: workspace:*
version: link:../../..
packages/astro/test/fixtures/astro-markdown:
dependencies:
astro: