mirror of
https://github.com/withastro/astro.git
synced 2025-01-22 10:31:53 -05:00
chore: first implementation for serialized config (#13000)
Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev>
This commit is contained in:
parent
78fd73a0df
commit
ddbcc17b41
15 changed files with 409 additions and 0 deletions
12
packages/astro/client.d.ts
vendored
12
packages/astro/client.d.ts
vendored
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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.`,
|
||||
|
|
|
@ -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 }),
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
102
packages/astro/src/manifest/virtual-module.ts
Normal file
102
packages/astro/src/manifest/virtual-module.ts
Normal 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';
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
26
packages/astro/src/types/public/manifest.ts
Normal file
26
packages/astro/src/types/public/manifest.ts
Normal 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;
|
||||
};
|
13
packages/astro/test/fixtures/astro-manifest/astro.config.mjs
vendored
Normal file
13
packages/astro/test/fixtures/astro-manifest/astro.config.mjs
vendored
Normal 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",
|
||||
}
|
||||
});
|
8
packages/astro/test/fixtures/astro-manifest/package.json
vendored
Normal file
8
packages/astro/test/fixtures/astro-manifest/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "@test/astro-manifest",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"astro": "workspace:*"
|
||||
}
|
||||
}
|
20
packages/astro/test/fixtures/astro-manifest/src/pages/client.astro
vendored
Normal file
20
packages/astro/test/fixtures/astro-manifest/src/pages/client.astro
vendored
Normal 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>
|
19
packages/astro/test/fixtures/astro-manifest/src/pages/index.astro
vendored
Normal file
19
packages/astro/test/fixtures/astro-manifest/src/pages/index.astro
vendored
Normal 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>
|
21
packages/astro/test/fixtures/astro-manifest/src/pages/server.astro
vendored
Normal file
21
packages/astro/test/fixtures/astro-manifest/src/pages/server.astro
vendored
Normal 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>
|
149
packages/astro/test/serializeManifest.test.js
Normal file
149
packages/astro/test/serializeManifest.test.js
Normal 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/'));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue