mirror of
https://github.com/sern-handler/website
synced 2026-06-21 23:32:22 +00:00
319 lines
12 KiB
JavaScript
319 lines
12 KiB
JavaScript
// src/index.ts
|
|
import rehypeExpressiveCode from "rehype-expressive-code";
|
|
|
|
// src/ec-config.ts
|
|
function getEcConfigFileUrl(projectRootUrl) {
|
|
return new URL("./ec.config.mjs", projectRootUrl);
|
|
}
|
|
async function loadEcConfigFile(projectRootUrl) {
|
|
const pathsToTry = [
|
|
// This path works in most scenarios, but not when the integration is processed by Vite
|
|
// due to a Vite bug affecting import URLs using the "file:" protocol
|
|
new URL(`./ec.config.mjs?t=${Date.now()}`, projectRootUrl).href
|
|
];
|
|
if (import.meta.env?.BASE_URL?.length) {
|
|
pathsToTry.push(`/ec.config.mjs?t=${Date.now()}`);
|
|
}
|
|
function coerceError(error) {
|
|
if (typeof error === "object" && error !== null && "message" in error) {
|
|
return error;
|
|
}
|
|
return { message: error };
|
|
}
|
|
for (const path of pathsToTry) {
|
|
try {
|
|
const module = await import(
|
|
/* @vite-ignore */
|
|
path
|
|
);
|
|
if (!module.default) {
|
|
throw new Error(`Missing or invalid default export. Please export your Expressive Code config object as the default export.`);
|
|
}
|
|
return module.default;
|
|
} catch (error) {
|
|
const { message, code } = coerceError(error);
|
|
if (code === "ERR_MODULE_NOT_FOUND" || code === "ERR_LOAD_URL") {
|
|
if (message.replace(/(imported )?from .*$/, "").includes("ec.config.mjs"))
|
|
continue;
|
|
}
|
|
throw new Error(
|
|
`Your project includes an Expressive Code config file ("ec.config.mjs")
|
|
that could not be loaded due to ${code ? `the error ${code}` : "the following error"}: ${message}`.replace(/\s+/g, " "),
|
|
error instanceof Error ? { cause: error } : void 0
|
|
);
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
// src/renderer.ts
|
|
import { createRenderer, getStableObjectHash } from "rehype-expressive-code";
|
|
|
|
// src/astro-config.ts
|
|
function serializePartialAstroConfig(config) {
|
|
const partialConfig = {
|
|
base: config.base,
|
|
root: config.root,
|
|
srcDir: config.srcDir
|
|
};
|
|
if (config.build) {
|
|
partialConfig.build = {};
|
|
if (config.build.assets)
|
|
partialConfig.build.assets = config.build.assets;
|
|
if (config.build.assetsPrefix)
|
|
partialConfig.build.assetsPrefix = config.build.assetsPrefix;
|
|
}
|
|
if (config.markdown?.shikiConfig?.langs) {
|
|
partialConfig.markdown = { shikiConfig: { langs: config.markdown.shikiConfig.langs } };
|
|
}
|
|
return JSON.stringify(partialConfig);
|
|
}
|
|
function getAssetsPrefix(fileExtension, assetsPrefix) {
|
|
if (!assetsPrefix)
|
|
return "";
|
|
if (typeof assetsPrefix === "string")
|
|
return assetsPrefix;
|
|
const dotLessFileExtension = fileExtension.slice(1);
|
|
if (assetsPrefix[dotLessFileExtension]) {
|
|
return assetsPrefix[dotLessFileExtension];
|
|
}
|
|
return assetsPrefix.fallback;
|
|
}
|
|
function getAssetsBaseHref(fileExtension, assetsPrefix, base) {
|
|
return (getAssetsPrefix(fileExtension, assetsPrefix) || base || "").trim().replace(/\/+$/g, "");
|
|
}
|
|
|
|
// src/renderer.ts
|
|
async function createAstroRenderer({ ecConfig, astroConfig, logger }) {
|
|
const { emitExternalStylesheet = true, customCreateRenderer, plugins = [], shiki = true, ...rest } = ecConfig ?? {};
|
|
const assetsDir = astroConfig.build?.assets || "_astro";
|
|
const hashedStyles = [];
|
|
const hashedScripts = [];
|
|
plugins.push({
|
|
name: "astro-expressive-code",
|
|
hooks: {
|
|
postprocessRenderedBlockGroup: ({ renderData, renderedGroupContents }) => {
|
|
const isFirstGroupInDocument = renderedGroupContents[0]?.codeBlock.parentDocument?.positionInDocument?.groupIndex === 0;
|
|
if (!isFirstGroupInDocument)
|
|
return;
|
|
const extraElements = [];
|
|
hashedStyles.forEach(([hashedRoute]) => {
|
|
extraElements.push({
|
|
type: "element",
|
|
tagName: "link",
|
|
properties: { rel: "stylesheet", href: `${getAssetsBaseHref(".css", astroConfig.build?.assetsPrefix, astroConfig.base)}${hashedRoute}` },
|
|
children: []
|
|
});
|
|
});
|
|
hashedScripts.forEach(([hashedRoute]) => {
|
|
extraElements.push({
|
|
type: "element",
|
|
tagName: "script",
|
|
properties: { type: "module", src: `${getAssetsBaseHref(".js", astroConfig.build?.assetsPrefix, astroConfig.base)}${hashedRoute}` },
|
|
children: []
|
|
});
|
|
});
|
|
if (!extraElements.length)
|
|
return;
|
|
renderData.groupAst.children.unshift(...extraElements);
|
|
}
|
|
}
|
|
});
|
|
const mergedShikiConfig = shiki === true ? {} : shiki;
|
|
if (mergedShikiConfig && !mergedShikiConfig.langs && astroConfig.markdown?.shikiConfig?.langs) {
|
|
mergedShikiConfig.langs = astroConfig.markdown.shikiConfig.langs;
|
|
}
|
|
const renderer = await (customCreateRenderer ?? createRenderer)({
|
|
plugins,
|
|
logger,
|
|
shiki: mergedShikiConfig,
|
|
...rest
|
|
});
|
|
renderer.hashedStyles = hashedStyles;
|
|
renderer.hashedScripts = hashedScripts;
|
|
if (emitExternalStylesheet) {
|
|
const combinedStyles = `${renderer.baseStyles}${renderer.themeStyles}`;
|
|
hashedStyles.push(getHashedRouteWithContent(combinedStyles, `/${assetsDir}/ec.{hash}.css`));
|
|
renderer.baseStyles = "";
|
|
renderer.themeStyles = "";
|
|
}
|
|
const uniqueJsModules = [...new Set(renderer.jsModules)];
|
|
const mergedJsCode = uniqueJsModules.join("\n");
|
|
renderer.jsModules = [];
|
|
hashedScripts.push(getHashedRouteWithContent(mergedJsCode, `/${assetsDir}/ec.{hash}.js`));
|
|
return renderer;
|
|
}
|
|
function getHashedRouteWithContent(content, routeTemplate) {
|
|
const contentHash = getStableObjectHash(content, { hashLength: 5 });
|
|
return [routeTemplate.replace("{hash}", contentHash), content];
|
|
}
|
|
|
|
// src/vite-plugin.ts
|
|
import { stableStringify } from "rehype-expressive-code";
|
|
function vitePluginAstroExpressiveCode({
|
|
styles,
|
|
scripts,
|
|
ecIntegrationOptions,
|
|
astroConfig,
|
|
command
|
|
}) {
|
|
const modules = {};
|
|
const configModuleContents = [];
|
|
configModuleContents.push(`export const astroConfig = ${serializePartialAstroConfig(astroConfig)}`);
|
|
const { customConfigPreprocessors, ...otherEcIntegrationOptions } = ecIntegrationOptions;
|
|
configModuleContents.push(`export const ecIntegrationOptions = ${stableStringify(otherEcIntegrationOptions)}`);
|
|
const strEcConfigFileUrlHref = JSON.stringify(getEcConfigFileUrl(astroConfig.root).href);
|
|
configModuleContents.push(
|
|
`let ecConfigFileOptions = {}`,
|
|
`try {`,
|
|
` ecConfigFileOptions = (await import('virtual:astro-expressive-code/ec-config')).default`,
|
|
`} catch (e) {`,
|
|
` console.error('*** Failed to load Expressive Code config file ${strEcConfigFileUrlHref}. You can ignore this message if you just renamed/removed the file.\\n\\n(Full error message: "' + (e?.message || e) + '")\\n')`,
|
|
`}`,
|
|
`export { ecConfigFileOptions }`
|
|
);
|
|
modules["virtual:astro-expressive-code/config"] = configModuleContents.join("\n");
|
|
modules["virtual:astro-expressive-code/ec-config"] = "export default {}";
|
|
modules["virtual:astro-expressive-code/preprocess-config"] = customConfigPreprocessors?.preprocessComponentConfig || `export default ({ ecConfig }) => ecConfig`;
|
|
const noQuery = (source) => source.split("?")[0];
|
|
const getVirtualModuleContents = (source) => {
|
|
if (command === "dev") {
|
|
for (const file of [...styles, ...scripts]) {
|
|
const [fileName, contents] = file;
|
|
if (noQuery(fileName) === noQuery(source))
|
|
return contents;
|
|
}
|
|
}
|
|
return source in modules ? modules[source] : void 0;
|
|
};
|
|
return {
|
|
name: "vite-plugin-astro-expressive-code",
|
|
async resolveId(source, importer) {
|
|
if (source === "virtual:astro-expressive-code/api") {
|
|
const resolved = await this.resolve("astro-expressive-code", importer);
|
|
if (resolved)
|
|
return resolved;
|
|
return await this.resolve("astro-expressive-code");
|
|
}
|
|
if (source === "virtual:astro-expressive-code/ec-config") {
|
|
const resolved = await this.resolve("./ec.config.mjs");
|
|
if (resolved)
|
|
return resolved;
|
|
}
|
|
if (getVirtualModuleContents(source))
|
|
return `\0${source}`;
|
|
},
|
|
load: (id) => id?.[0] === "\0" ? getVirtualModuleContents(id.slice(1)) : void 0,
|
|
// If any file imported by the EC config file changes, restart the server
|
|
async handleHotUpdate({ modules: modules2, server }) {
|
|
if (!modules2 || !server)
|
|
return;
|
|
const isImportedByEcConfig = (module, depth = 0) => {
|
|
if (!module || !module.importers || depth >= 6)
|
|
return false;
|
|
for (const importingModule of module.importers) {
|
|
if (noQuery(module.url).endsWith("/ec.config.mjs")) {
|
|
return true;
|
|
}
|
|
if (isImportedByEcConfig(importingModule, depth + 1))
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
if (modules2.some((module) => isImportedByEcConfig(module))) {
|
|
await server.restart();
|
|
}
|
|
},
|
|
buildEnd() {
|
|
if (command === "build") {
|
|
for (const file of [...styles, ...scripts]) {
|
|
const [fileName, source] = file;
|
|
this.emitFile({
|
|
type: "asset",
|
|
// Remove leading slash and any query params
|
|
fileName: noQuery(fileName.slice(1)),
|
|
source
|
|
});
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// src/index.ts
|
|
export * from "rehype-expressive-code";
|
|
function astroExpressiveCode(integrationOptions = {}) {
|
|
const integration = {
|
|
name: "astro-expressive-code",
|
|
hooks: {
|
|
"astro:config:setup": async (args) => {
|
|
const { command, config: astroConfig, updateConfig, logger, addWatchFile } = args;
|
|
const ownPosition = astroConfig.integrations.findIndex((integration2) => integration2.name === "astro-expressive-code");
|
|
const mdxPosition = astroConfig.integrations.findIndex((integration2) => integration2.name === "@astrojs/mdx");
|
|
if (ownPosition > -1 && mdxPosition > -1 && mdxPosition < ownPosition) {
|
|
throw new Error(
|
|
`Incorrect integration order: To allow code blocks on MDX pages to use
|
|
astro-expressive-code, please move astroExpressiveCode() before mdx()
|
|
in the "integrations" array of your Astro config file.`.replace(/\s+/g, " ")
|
|
);
|
|
}
|
|
addWatchFile(getEcConfigFileUrl(astroConfig.root));
|
|
const ecConfigFileOptions = await loadEcConfigFile(astroConfig.root);
|
|
const mergedOptions = { ...ecConfigFileOptions, ...integrationOptions };
|
|
const forwardedIntegrationOptions = { ...integrationOptions };
|
|
delete forwardedIntegrationOptions.customConfigPreprocessors;
|
|
if (Object.keys(ecConfigFileOptions).length > 0 && Object.keys(forwardedIntegrationOptions).length > 0) {
|
|
logger.warn(
|
|
`Your project includes an Expressive Code config file ("ec.config.mjs"),
|
|
but your Astro config file also contains Expressive Code options.
|
|
To avoid unexpected results from merging multiple config sources,
|
|
move all Expressive Code options into its config file.
|
|
Found options: ${Object.keys(forwardedIntegrationOptions).join(", ")}`.replace(/\s+/g, " ")
|
|
);
|
|
}
|
|
const processedEcConfig = await mergedOptions.customConfigPreprocessors?.preprocessAstroIntegrationConfig({ ecConfig: mergedOptions, astroConfig }) || mergedOptions;
|
|
const { customCreateAstroRenderer } = processedEcConfig;
|
|
delete processedEcConfig.customCreateAstroRenderer;
|
|
delete processedEcConfig.customConfigPreprocessors;
|
|
const { hashedStyles, hashedScripts, ...renderer } = await (customCreateAstroRenderer ?? createAstroRenderer)({ astroConfig, ecConfig: processedEcConfig, logger });
|
|
const rehypeExpressiveCodeOptions = {
|
|
// Even though we have created a custom renderer, some options are used
|
|
// by the rehype integration itself (e.g. `tabWidth`, `getBlockLocale`),
|
|
// so we pass all of them through just to be safe
|
|
...processedEcConfig,
|
|
// Pass our custom renderer to the rehype integration
|
|
customCreateRenderer: () => renderer
|
|
};
|
|
updateConfig({
|
|
vite: {
|
|
plugins: [
|
|
vitePluginAstroExpressiveCode({
|
|
styles: hashedStyles,
|
|
scripts: hashedScripts,
|
|
ecIntegrationOptions: integrationOptions,
|
|
astroConfig,
|
|
command
|
|
})
|
|
]
|
|
},
|
|
markdown: {
|
|
syntaxHighlight: false,
|
|
rehypePlugins: [[rehypeExpressiveCode, rehypeExpressiveCodeOptions]]
|
|
}
|
|
});
|
|
}
|
|
}
|
|
};
|
|
return integration;
|
|
}
|
|
function defineEcConfig(config) {
|
|
return config;
|
|
}
|
|
var src_default = astroExpressiveCode;
|
|
export {
|
|
astroExpressiveCode,
|
|
createAstroRenderer,
|
|
src_default as default,
|
|
defineEcConfig
|
|
};
|
|
//# sourceMappingURL=index.js.map
|