426 lines
19 KiB
Plaintext
426 lines
19 KiB
Plaintext
import { markdownConfigDefaults } from "@astrojs/markdown-remark";
|
|
import { bundledThemes } from "shikiji";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import { z } from "zod";
|
|
import { appendForwardSlash, prependForwardSlash, removeTrailingForwardSlash } from "../path.js";
|
|
import "mdast-util-to-hast";
|
|
import "shikiji-core";
|
|
const ASTRO_CONFIG_DEFAULTS = {
|
|
root: ".",
|
|
srcDir: "./src",
|
|
publicDir: "./public",
|
|
outDir: "./dist",
|
|
cacheDir: "./node_modules/.astro",
|
|
base: "/",
|
|
trailingSlash: "ignore",
|
|
build: {
|
|
format: "directory",
|
|
client: "./dist/client/",
|
|
server: "./dist/server/",
|
|
assets: "_astro",
|
|
serverEntry: "entry.mjs",
|
|
redirects: true,
|
|
inlineStylesheets: "auto"
|
|
},
|
|
image: {
|
|
service: { entrypoint: "astro/assets/services/sharp", config: {} }
|
|
},
|
|
devToolbar: {
|
|
enabled: true
|
|
},
|
|
compressHTML: true,
|
|
server: {
|
|
host: false,
|
|
port: 4321,
|
|
open: false
|
|
},
|
|
integrations: [],
|
|
markdown: markdownConfigDefaults,
|
|
vite: {},
|
|
legacy: {},
|
|
redirects: {},
|
|
experimental: {
|
|
optimizeHoistedScript: false,
|
|
contentCollectionCache: false,
|
|
clientPrerender: false,
|
|
globalRoutePriority: false,
|
|
i18nDomains: false
|
|
}
|
|
};
|
|
const AstroConfigSchema = z.object({
|
|
root: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.root).transform((val) => new URL(val)),
|
|
srcDir: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.srcDir).transform((val) => new URL(val)),
|
|
publicDir: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.publicDir).transform((val) => new URL(val)),
|
|
outDir: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.outDir).transform((val) => new URL(val)),
|
|
cacheDir: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.cacheDir).transform((val) => new URL(val)),
|
|
site: z.string().url().optional(),
|
|
compressHTML: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.compressHTML),
|
|
base: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.base),
|
|
trailingSlash: z.union([z.literal("always"), z.literal("never"), z.literal("ignore")]).optional().default(ASTRO_CONFIG_DEFAULTS.trailingSlash),
|
|
output: z.union([z.literal("static"), z.literal("server"), z.literal("hybrid")]).optional().default("static"),
|
|
scopedStyleStrategy: z.union([z.literal("where"), z.literal("class"), z.literal("attribute")]).optional().default("attribute"),
|
|
adapter: z.object({ name: z.string(), hooks: z.object({}).passthrough().default({}) }).optional(),
|
|
db: z.object({}).passthrough().default({}).optional(),
|
|
integrations: z.preprocess(
|
|
// preprocess
|
|
(val) => Array.isArray(val) ? val.flat(Infinity).filter(Boolean) : val,
|
|
// validate
|
|
z.array(z.object({ name: z.string(), hooks: z.object({}).passthrough().default({}) })).default(ASTRO_CONFIG_DEFAULTS.integrations)
|
|
),
|
|
build: z.object({
|
|
format: z.union([z.literal("file"), z.literal("directory"), z.literal("preserve")]).optional().default(ASTRO_CONFIG_DEFAULTS.build.format),
|
|
client: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.client).transform((val) => new URL(val)),
|
|
server: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.server).transform((val) => new URL(val)),
|
|
assets: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.assets),
|
|
assetsPrefix: z.string().optional(),
|
|
serverEntry: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.serverEntry),
|
|
redirects: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.build.redirects),
|
|
inlineStylesheets: z.enum(["always", "auto", "never"]).optional().default(ASTRO_CONFIG_DEFAULTS.build.inlineStylesheets)
|
|
}).default({}),
|
|
server: z.preprocess(
|
|
// preprocess
|
|
// NOTE: Uses the "error" command here because this is overwritten by the
|
|
// individualized schema parser with the correct command.
|
|
(val) => typeof val === "function" ? val({ command: "error" }) : val,
|
|
// validate
|
|
z.object({
|
|
open: z.union([z.string(), z.boolean()]).optional().default(ASTRO_CONFIG_DEFAULTS.server.open),
|
|
host: z.union([z.string(), z.boolean()]).optional().default(ASTRO_CONFIG_DEFAULTS.server.host),
|
|
port: z.number().optional().default(ASTRO_CONFIG_DEFAULTS.server.port),
|
|
headers: z.custom().optional()
|
|
}).default({})
|
|
),
|
|
redirects: z.record(
|
|
z.string(),
|
|
z.union([
|
|
z.string(),
|
|
z.object({
|
|
status: z.union([
|
|
z.literal(300),
|
|
z.literal(301),
|
|
z.literal(302),
|
|
z.literal(303),
|
|
z.literal(304),
|
|
z.literal(307),
|
|
z.literal(308)
|
|
]),
|
|
destination: z.string()
|
|
})
|
|
])
|
|
).default(ASTRO_CONFIG_DEFAULTS.redirects),
|
|
prefetch: z.union([
|
|
z.boolean(),
|
|
z.object({
|
|
prefetchAll: z.boolean().optional(),
|
|
defaultStrategy: z.enum(["tap", "hover", "viewport", "load"]).optional()
|
|
})
|
|
]).optional(),
|
|
image: z.object({
|
|
endpoint: z.string().optional(),
|
|
service: z.object({
|
|
entrypoint: z.union([
|
|
z.literal("astro/assets/services/sharp"),
|
|
z.literal("astro/assets/services/squoosh"),
|
|
z.string()
|
|
]).default(ASTRO_CONFIG_DEFAULTS.image.service.entrypoint),
|
|
config: z.record(z.any()).default({})
|
|
}).default(ASTRO_CONFIG_DEFAULTS.image.service),
|
|
domains: z.array(z.string()).default([]),
|
|
remotePatterns: z.array(
|
|
z.object({
|
|
protocol: z.string().optional(),
|
|
hostname: z.string().refine(
|
|
(val) => !val.includes("*") || val.startsWith("*.") || val.startsWith("**."),
|
|
{
|
|
message: "wildcards can only be placed at the beginning of the hostname"
|
|
}
|
|
).optional(),
|
|
port: z.string().optional(),
|
|
pathname: z.string().refine((val) => !val.includes("*") || val.endsWith("/*") || val.endsWith("/**"), {
|
|
message: "wildcards can only be placed at the end of a pathname"
|
|
}).optional()
|
|
})
|
|
).default([])
|
|
}).default(ASTRO_CONFIG_DEFAULTS.image),
|
|
devToolbar: z.object({
|
|
enabled: z.boolean().default(ASTRO_CONFIG_DEFAULTS.devToolbar.enabled)
|
|
}).default(ASTRO_CONFIG_DEFAULTS.devToolbar),
|
|
markdown: z.object({
|
|
syntaxHighlight: z.union([z.literal("shiki"), z.literal("prism"), z.literal(false)]).default(ASTRO_CONFIG_DEFAULTS.markdown.syntaxHighlight),
|
|
shikiConfig: z.object({
|
|
langs: z.custom().array().transform((langs) => {
|
|
for (const lang of langs) {
|
|
if (typeof lang === "object") {
|
|
if (lang.id) {
|
|
lang.name = lang.id;
|
|
}
|
|
if (lang.grammar) {
|
|
Object.assign(lang, lang.grammar);
|
|
}
|
|
}
|
|
}
|
|
return langs;
|
|
}).default([]),
|
|
theme: z.enum(Object.keys(bundledThemes)).or(z.custom()).default(ASTRO_CONFIG_DEFAULTS.markdown.shikiConfig.theme),
|
|
experimentalThemes: z.record(
|
|
z.enum(Object.keys(bundledThemes)).or(z.custom())
|
|
).default(ASTRO_CONFIG_DEFAULTS.markdown.shikiConfig.experimentalThemes),
|
|
wrap: z.boolean().or(z.null()).default(ASTRO_CONFIG_DEFAULTS.markdown.shikiConfig.wrap),
|
|
transformers: z.custom().array().default(ASTRO_CONFIG_DEFAULTS.markdown.shikiConfig.transformers)
|
|
}).default({}),
|
|
remarkPlugins: z.union([
|
|
z.string(),
|
|
z.tuple([z.string(), z.any()]),
|
|
z.custom((data) => typeof data === "function"),
|
|
z.tuple([z.custom((data) => typeof data === "function"), z.any()])
|
|
]).array().default(ASTRO_CONFIG_DEFAULTS.markdown.remarkPlugins),
|
|
rehypePlugins: z.union([
|
|
z.string(),
|
|
z.tuple([z.string(), z.any()]),
|
|
z.custom((data) => typeof data === "function"),
|
|
z.tuple([z.custom((data) => typeof data === "function"), z.any()])
|
|
]).array().default(ASTRO_CONFIG_DEFAULTS.markdown.rehypePlugins),
|
|
remarkRehype: z.custom((data) => data instanceof Object && !Array.isArray(data)).optional().default(ASTRO_CONFIG_DEFAULTS.markdown.remarkRehype),
|
|
gfm: z.boolean().default(ASTRO_CONFIG_DEFAULTS.markdown.gfm),
|
|
smartypants: z.boolean().default(ASTRO_CONFIG_DEFAULTS.markdown.smartypants)
|
|
}).default({}),
|
|
vite: z.custom((data) => data instanceof Object && !Array.isArray(data)).default(ASTRO_CONFIG_DEFAULTS.vite),
|
|
i18n: z.optional(
|
|
z.object({
|
|
defaultLocale: z.string(),
|
|
locales: z.array(
|
|
z.union([
|
|
z.string(),
|
|
z.object({
|
|
path: z.string(),
|
|
codes: z.string().array().nonempty()
|
|
})
|
|
])
|
|
),
|
|
domains: z.record(
|
|
z.string(),
|
|
z.string().url(
|
|
"The domain value must be a valid URL, and it has to start with 'https' or 'http'."
|
|
)
|
|
).optional(),
|
|
fallback: z.record(z.string(), z.string()).optional(),
|
|
routing: z.object({
|
|
prefixDefaultLocale: z.boolean().default(false),
|
|
redirectToDefaultLocale: z.boolean().default(true),
|
|
strategy: z.enum(["pathname"]).default("pathname")
|
|
}).default({}).refine(
|
|
({ prefixDefaultLocale, redirectToDefaultLocale }) => {
|
|
return !(prefixDefaultLocale === false && redirectToDefaultLocale === false);
|
|
},
|
|
{
|
|
message: "The option `i18n.redirectToDefaultLocale` is only useful when the `i18n.prefixDefaultLocale` is set to `true`. Remove the option `i18n.redirectToDefaultLocale`, or change its value to `true`."
|
|
}
|
|
)
|
|
}).optional().transform((i18n) => {
|
|
if (i18n) {
|
|
let { routing, domains } = i18n;
|
|
let strategy;
|
|
const hasDomains = domains ? Object.keys(domains).length > 0 : false;
|
|
if (!hasDomains) {
|
|
if (routing.prefixDefaultLocale === true) {
|
|
if (routing.redirectToDefaultLocale) {
|
|
strategy = "pathname-prefix-always";
|
|
} else {
|
|
strategy = "pathname-prefix-always-no-redirect";
|
|
}
|
|
} else {
|
|
strategy = "pathname-prefix-other-locales";
|
|
}
|
|
} else {
|
|
if (routing.prefixDefaultLocale === true) {
|
|
if (routing.redirectToDefaultLocale) {
|
|
strategy = "domains-prefix-always";
|
|
} else {
|
|
strategy = "domains-prefix-other-no-redirect";
|
|
}
|
|
} else {
|
|
strategy = "domains-prefix-other-locales";
|
|
}
|
|
}
|
|
return { ...i18n, routing: strategy };
|
|
}
|
|
return void 0;
|
|
}).superRefine((i18n, ctx) => {
|
|
if (i18n) {
|
|
const { defaultLocale, locales: _locales, fallback, domains, routing } = i18n;
|
|
const locales = _locales.map((locale) => {
|
|
if (typeof locale === "string") {
|
|
return locale;
|
|
} else {
|
|
return locale.path;
|
|
}
|
|
});
|
|
if (!locales.includes(defaultLocale)) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: `The default locale \`${defaultLocale}\` is not present in the \`i18n.locales\` array.`
|
|
});
|
|
}
|
|
if (fallback) {
|
|
for (const [fallbackFrom, fallbackTo] of Object.entries(fallback)) {
|
|
if (!locales.includes(fallbackFrom)) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: `The locale \`${fallbackFrom}\` key in the \`i18n.fallback\` record doesn't exist in the \`i18n.locales\` array.`
|
|
});
|
|
}
|
|
if (fallbackFrom === defaultLocale) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: `You can't use the default locale as a key. The default locale can only be used as value.`
|
|
});
|
|
}
|
|
if (!locales.includes(fallbackTo)) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: `The locale \`${fallbackTo}\` value in the \`i18n.fallback\` record doesn't exist in the \`i18n.locales\` array.`
|
|
});
|
|
}
|
|
}
|
|
}
|
|
if (domains) {
|
|
const entries = Object.entries(domains);
|
|
if (entries.length > 0) {
|
|
if (routing !== "domains-prefix-other-locales" && routing !== "domains-prefix-other-no-redirect" && routing !== "domains-prefix-always") {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: `When specifying some domains, the property \`i18n.routingStrategy\` must be set to \`"domains"\`.`
|
|
});
|
|
}
|
|
}
|
|
for (const [domainKey, domainValue] of Object.entries(domains)) {
|
|
if (!locales.includes(domainKey)) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: `The locale \`${domainKey}\` key in the \`i18n.domains\` record doesn't exist in the \`i18n.locales\` array.`
|
|
});
|
|
}
|
|
if (!domainValue.startsWith("https") && !domainValue.startsWith("http")) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "The domain value must be a valid URL, and it has to start with 'https' or 'http'.",
|
|
path: ["domains"]
|
|
});
|
|
} else {
|
|
try {
|
|
const domainUrl = new URL(domainValue);
|
|
if (domainUrl.pathname !== "/") {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: `The URL \`${domainValue}\` must contain only the origin. A subsequent pathname isn't allowed here. Remove \`${domainUrl.pathname}\`.`,
|
|
path: ["domains"]
|
|
});
|
|
}
|
|
} catch {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
),
|
|
experimental: z.object({
|
|
optimizeHoistedScript: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.optimizeHoistedScript),
|
|
contentCollectionCache: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.contentCollectionCache),
|
|
clientPrerender: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.clientPrerender),
|
|
globalRoutePriority: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.globalRoutePriority),
|
|
i18nDomains: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.i18nDomains)
|
|
}).strict(
|
|
`Invalid or outdated experimental feature.
|
|
Check for incorrect spelling or outdated Astro version.
|
|
See https://docs.astro.build/en/reference/configuration-reference/#experimental-flags for a list of all current experiments.`
|
|
).default({}),
|
|
legacy: z.object({}).default({})
|
|
});
|
|
function createRelativeSchema(cmd, fileProtocolRoot) {
|
|
const AstroConfigRelativeSchema = AstroConfigSchema.extend({
|
|
root: z.string().default(ASTRO_CONFIG_DEFAULTS.root).transform((val) => resolveDirAsUrl(val, fileProtocolRoot)),
|
|
srcDir: z.string().default(ASTRO_CONFIG_DEFAULTS.srcDir).transform((val) => resolveDirAsUrl(val, fileProtocolRoot)),
|
|
compressHTML: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.compressHTML),
|
|
publicDir: z.string().default(ASTRO_CONFIG_DEFAULTS.publicDir).transform((val) => resolveDirAsUrl(val, fileProtocolRoot)),
|
|
outDir: z.string().default(ASTRO_CONFIG_DEFAULTS.outDir).transform((val) => resolveDirAsUrl(val, fileProtocolRoot)),
|
|
cacheDir: z.string().default(ASTRO_CONFIG_DEFAULTS.cacheDir).transform((val) => resolveDirAsUrl(val, fileProtocolRoot)),
|
|
build: z.object({
|
|
format: z.union([z.literal("file"), z.literal("directory"), z.literal("preserve")]).optional().default(ASTRO_CONFIG_DEFAULTS.build.format),
|
|
client: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.client).transform((val) => resolveDirAsUrl(val, fileProtocolRoot)),
|
|
server: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.server).transform((val) => resolveDirAsUrl(val, fileProtocolRoot)),
|
|
assets: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.assets),
|
|
assetsPrefix: z.string().optional(),
|
|
serverEntry: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.serverEntry),
|
|
redirects: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.build.redirects),
|
|
inlineStylesheets: z.enum(["always", "auto", "never"]).optional().default(ASTRO_CONFIG_DEFAULTS.build.inlineStylesheets)
|
|
}).optional().default({}),
|
|
server: z.preprocess(
|
|
// preprocess
|
|
(val) => {
|
|
if (typeof val === "function") {
|
|
return val({ command: cmd === "dev" ? "dev" : "preview" });
|
|
} else {
|
|
return val;
|
|
}
|
|
},
|
|
// validate
|
|
z.object({
|
|
open: z.union([z.string(), z.boolean()]).optional().default(ASTRO_CONFIG_DEFAULTS.server.open),
|
|
host: z.union([z.string(), z.boolean()]).optional().default(ASTRO_CONFIG_DEFAULTS.server.host),
|
|
port: z.number().optional().default(ASTRO_CONFIG_DEFAULTS.server.port),
|
|
headers: z.custom().optional(),
|
|
streaming: z.boolean().optional().default(true)
|
|
}).optional().default({})
|
|
)
|
|
}).transform((config) => {
|
|
if (!config.build.server.toString().startsWith(config.outDir.toString()) && config.build.server.toString().endsWith("dist/server/")) {
|
|
config.build.server = new URL("./dist/server/", config.outDir);
|
|
}
|
|
if (!config.build.client.toString().startsWith(config.outDir.toString()) && config.build.client.toString().endsWith("dist/client/")) {
|
|
config.build.client = new URL("./dist/client/", config.outDir);
|
|
}
|
|
if (config.trailingSlash === "never") {
|
|
config.base = prependForwardSlash(removeTrailingForwardSlash(config.base));
|
|
} else if (config.trailingSlash === "always") {
|
|
config.base = prependForwardSlash(appendForwardSlash(config.base));
|
|
} else {
|
|
config.base = prependForwardSlash(config.base);
|
|
}
|
|
return config;
|
|
}).refine((obj) => !obj.outDir.toString().startsWith(obj.publicDir.toString()), {
|
|
message: "The value of `outDir` must not point to a path within the folder set as `publicDir`, this will cause an infinite loop"
|
|
}).superRefine((configuration, ctx) => {
|
|
const { site, experimental, i18n, output } = configuration;
|
|
if (experimental.i18nDomains) {
|
|
if (i18n?.routing === "domains-prefix-other-locales" || i18n?.routing === "domains-prefix-other-no-redirect" || i18n?.routing === "domains-prefix-always") {
|
|
if (!site) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "The option `site` isn't set. When using the 'domains' strategy for `i18n`, `site` is required to create absolute URLs for locales that aren't mapped to a domain."
|
|
});
|
|
}
|
|
if (output !== "server") {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: 'Domain support is only available when `output` is `"server"`.'
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
return AstroConfigRelativeSchema;
|
|
}
|
|
function resolveDirAsUrl(dir, root) {
|
|
let resolvedDir = path.resolve(root, dir);
|
|
if (!resolvedDir.endsWith(path.sep)) {
|
|
resolvedDir += path.sep;
|
|
}
|
|
return pathToFileURL(resolvedDir);
|
|
}
|
|
export {
|
|
AstroConfigSchema,
|
|
createRelativeSchema
|
|
};
|