Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9e180c5
fix: fix error with `<Prism />` component in Cloudflare Workers
rururux Mar 2, 2026
8b7157c
add vite as a devDependency
rururux Mar 2, 2026
0a5445d
omg
rururux Mar 2, 2026
bf6b547
fix knip error
rururux Mar 2, 2026
d57df04
try replacing all static imports of prism with dynamic imports
rururux Mar 3, 2026
fe8ec4f
remove unnecessary files
rururux Mar 3, 2026
da6244c
oops
rururux Mar 3, 2026
86564b3
add explanatory comment
rururux Mar 3, 2026
9e098d4
Merge branch 'main' into v6-cf-prism
rururux Mar 3, 2026
e334379
Merge branch 'main' into v6-cf-prism
rururux Mar 12, 2026
8efd4b2
add `loadChainer` and verify that the language data is loading correctly
rururux Mar 17, 2026
79d01a3
change to use static import and add ``@astro/prism` detection logic t…
rururux Mar 17, 2026
3b1a04a
load prism language data files from a virtual module
rururux Mar 19, 2026
37b971f
remove vite from astro-prism's dependencies
rururux Mar 19, 2026
68f5b01
Merge branch 'main' into v6-cf-prism
rururux Mar 19, 2026
df376c0
Merge branch 'main' into v6-cf-prism
rururux Mar 26, 2026
368adda
Merge branch 'main' into v6-cf-prism
rururux May 1, 2026
782794c
update test
rururux May 1, 2026
8994f97
fix type error
rururux May 1, 2026
eabe7ed
Merge branch 'main' into v6-cf-prism
rururux May 2, 2026
d187c79
improve stability when multiple `<Prism />` components are used
rururux May 4, 2026
946c307
Merge branch 'main' into v6-cf-prism
rururux May 7, 2026
f63e1cf
update `tsconfig.build.json`
rururux May 7, 2026
ea86b2b
fix knip lint
rururux May 7, 2026
1e1d716
format
rururux May 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/pretty-canyons-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@astrojs/markdoc': patch
'@astrojs/markdown-remark': patch
---

Updates internal type usage from `@astrojs/prism`.
6 changes: 6 additions & 0 deletions .changeset/strict-coats-throw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@astrojs/prism': patch
'@astrojs/cloudflare': patch
---

Fixes an issue where the `<Prism />` component failed to work in Cloudflare Workers.
5 changes: 5 additions & 0 deletions knip.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ export default {
'@types/http-cache-semantics',
],
},
'packages/astro-prism': {
entry: [srcEntry, dtsEntry, testEntry],
// package.json#imports are not resolved at the moment
ignore: ['src/loadLanguages-workerd.ts'],
},
'packages/db': {
entry: [srcEntry, dtsEntry, testEntry, 'test/types/**/*'],
},
Expand Down
2 changes: 1 addition & 1 deletion packages/astro-prism/Prism.astro
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ interface Props {
}

const { class: className, lang, code } = Astro.props as Props;
const { classLanguage, html } = runHighlighterWithAstro(lang, code);
const { classLanguage, html } = await runHighlighterWithAstro(lang, code);
---

<pre
Expand Down
6 changes: 6 additions & 0 deletions packages/astro-prism/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
"./Prism.astro": "./Prism.astro",
"./dist/highlighter": "./dist/highlighter.js"
},
"imports": {
"#prism-loadLanguages": {
"workerd": "./dist/loadLanguages-workerd.js",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does this work? What causes this to be loaded?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is due to this feature.
In PR #15565 as well, a similar approach was taken to address an issue that only occurs in the workerd environment.

https://developers.cloudflare.com/workers/wrangler/bundling/#conditional-exports

Wrangler respects the conditional exports field ↗ in package.json. This allows developers to implement isomorphic libraries that have different implementations depending on the JavaScript runtime they are running in. When bundling, Wrangler will try to load the workerd key ↗. Refer to the Wrangler repository for an example isomorphic package ↗.

"default": "./dist/loadLanguages-default.js"
}
},
"files": [
"dist",
"Prism.astro"
Expand Down
16 changes: 8 additions & 8 deletions packages/astro-prism/src/highlighter.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import Prism from 'prismjs';
import loadLanguages from 'prismjs/components/index.js';
import { loadLanguages } from '#prism-loadLanguages';
import { addAstro } from './plugin.js';

const languageMap = new Map([['ts', 'typescript']]);

export function runHighlighterWithAstro(lang: string | undefined, code: string) {
export async function runHighlighterWithAstro(lang: string | undefined, code: string) {
if (!lang) {
lang = 'plaintext';
}
let classLanguage = `language-${lang}`;
const ensureLoaded = (language: string) => {
const ensureLoaded = async (language: string) => {
if (language && !Prism.languages[language]) {
loadLanguages([language]);
await loadLanguages([language]);
}
};

if (languageMap.has(lang)) {
ensureLoaded(languageMap.get(lang)!);
await ensureLoaded(languageMap.get(lang)!);
} else if (lang === 'astro') {
ensureLoaded('typescript');
await ensureLoaded('typescript');
addAstro(Prism);
} else {
ensureLoaded('markup-templating'); // Prism expects this to exist for a number of other langs
ensureLoaded(lang);
await ensureLoaded('markup-templating'); // Prism expects this to exist for a number of other langs
await ensureLoaded(lang);
}

if (lang && !Prism.languages[lang]) {
Expand Down
5 changes: 5 additions & 0 deletions packages/astro-prism/src/loadLanguages-default.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import prismLoadLanguages from 'prismjs/components/index.js';

export async function loadLanguages(languages: string | string[]) {
return prismLoadLanguages(languages);
}
99 changes: 99 additions & 0 deletions packages/astro-prism/src/loadLanguages-workerd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// This implementation was based from: https://github.com/PrismJS/prism/blob/76dde18a575831c91491895193f56081ac08b0c5/components/index.js
import Prism from 'prismjs';
import components from 'prismjs/components.js';
import getLoader, { type LoadChainer } from 'prismjs/dependencies.js';
import { bundledLanguages } from 'virtual:astro-cloudflare:prism';

// This `loadChainer` is required when working with Promises in Prism's loader.
// ref: https://github.com/PrismJS/prism/blob/76dde18a575831c91491895193f56081ac08b0c5/dependencies.js#L346-L360
const loadChainer: LoadChainer<Promise<void>> = {
series: async (before, after) => {
await before;
await after();
},
parallel: async (values) => {
await Promise.all(values);
},
};

// Since Prism language files are written assuming the Prism instance is defined
// as a global variable, we will temporarily set the Prism instance globally.
// As loadLanguages can be called asynchronously multiple times, there is a risk
// that globalThis.Prism may be accessed again after cleanup, even when it appears
// to be no longer needed after a Promise resolves, potentially causing a
// `Prism is not defined` error.
// To avoid this, we track the number of active references and only perform cleanup
// when the count returns to zero.
let prismRefCount = 0;

const prismRefCounter = {
increment: () => {
if (prismRefCount === 0) {
globalThis.Prism = Prism;
}

prismRefCount += 1;
},
decrement: () => {
prismRefCount -= 1;

if (prismRefCount === 0) {
// @ts-expect-error globalThis type
delete globalThis.Prism;
}
},
};

/**
* The set of all languages which have been loaded using the below function.
*
* @type {Set<string>}
*/
const loadedLanguages = new Set<string>();

/**
* Loads the given languages and adds them to the current Prism instance.
*
* If no languages are provided, __all__ Prism languages will be loaded.
*
* @param {string|string[]} [languages]
* @returns {Promise<void>}
*/
export async function loadLanguages(languages: string | string[]) {
prismRefCounter.increment();

if (languages === undefined) {
languages = Object.keys(components.languages).filter((l) => l !== 'meta');
} else if (!Array.isArray(languages)) {
languages = [languages];
}

// the user might have loaded languages via some other way or used `prism.js` which already includes some
// we don't need to validate the ids because `getLoader` will ignore invalid ones
const loaded = [...loadedLanguages, ...Object.keys(Prism.languages)];

await getLoader(components, languages, loaded).load(async (lang: string) => {
if (!(lang in components.languages)) {
if (!loadLanguages.silent) {
console.warn('Language does not exist: ' + lang);
Comment thread
ematipico marked this conversation as resolved.
}
return;
}

// remove from Prism
delete Prism.languages[lang];

if (Object.hasOwn(bundledLanguages, lang)) {
await bundledLanguages[lang]();
}

loadedLanguages.add(lang);
}, loadChainer);

prismRefCounter.decrement();
}

/**
* Set this to `true` to prevent all warning messages `loadLanguages` logs.
*/
loadLanguages.silent = false;
3 changes: 2 additions & 1 deletion packages/astro-prism/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"extends": "../../configs/tsconfig.build.json"
"extends": "../../configs/tsconfig.build.json",
"include": ["./src", "./virtual.d.ts"]
}
3 changes: 3 additions & 0 deletions packages/astro-prism/virtual.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare module 'virtual:astro-cloudflare:prism' {
export const bundledLanguages: Record<string, () => Promise<void>>;
}
4 changes: 3 additions & 1 deletion packages/integrations/cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@
"astro": "workspace:*",
"astro-scripts": "workspace:*",
"cheerio": "1.2.0",
"devalue": "^5.6.3"
"devalue": "^5.6.3",
"prismjs": "^1.30.0",
"@types/prismjs": "1.26.6"
},
"publishConfig": {
"provenance": true
Expand Down
38 changes: 36 additions & 2 deletions packages/integrations/cloudflare/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createReadStream, existsSync, readFileSync } from 'node:fs';
import { appendFile, rename, stat } from 'node:fs/promises';
import { appendFile, readFile, rename, stat } from 'node:fs/promises';
import { createInterface } from 'node:readline/promises';
import { removeLeadingForwardSlash } from '@astrojs/internal-helpers/path';
import { createRedirectsFromAstroRoutes, printAsRedirects } from '@astrojs/underscore-redirects';
Expand All @@ -22,6 +22,7 @@ import {
import { parseEnv } from 'node:util';
import { sessionDrivers } from 'astro/config';
import { createCloudflarePrerenderer } from './prerenderer.js';
import cfPrismPlugin from './vite-plugin-prism.js';

const CLOUDFLARE_KV_SESSION_DRIVER_ENTRYPOINT = sessionDrivers.cloudflareKVBinding().entrypoint;

Expand Down Expand Up @@ -130,7 +131,7 @@ export default function createIntegration({
return {
name: '@astrojs/cloudflare',
hooks: {
'astro:config:setup': ({ command, config, updateConfig, logger, addWatchFile }) => {
'astro:config:setup': async ({ command, config, updateConfig, logger, addWatchFile }) => {
if (!!process.versions.webcontainer) {
throw new Error('`workerd` does not run on Stackblitz.');
}
Expand Down Expand Up @@ -206,6 +207,21 @@ export default function createIntegration({
globalThis.astroCloudflareOptions = cfPluginConfig;
}

// Including prismjs files in `optimizeDeps.includes` when `@astrojs/prism` is not installed
// causes a "Failed to resolve dependency: @astrojs/prism > prismjs" log to appear.
// However, when using the `<Prism />` component in a Cloudflare Workers environment,
// not including prismjs files in `optimizeDeps.includes` causes
// a "The file does not exist at ..." log to appear.
// To work around this, we check whether `@astrojs/prism` is installed in the current project.
// Note: this "Failed to resolve dependency" log will not appear as long as the `@astrojs/prism` package is installed,
// even if it is not actually used.
const prismFiles = [
'@astrojs/prism > prismjs',
'@astrojs/prism > prismjs/components.js',
'@astrojs/prism > prismjs/dependencies.js',
] as const;
const isAstroPrismPackageInstalled = await getIsAstroPrismInstalled(config.root);

updateConfig({
build: {
redirects: false,
Expand Down Expand Up @@ -270,6 +286,7 @@ export default function createIntegration({
'astro/jsx-runtime',
'astro/app/entrypoint/dev',
'astro/virtual-modules/middleware.js',
...(isAstroPrismPackageInstalled ? prismFiles : []),
],
exclude: [
'unstorage/drivers/cloudflare-kv-binding',
Expand Down Expand Up @@ -330,6 +347,7 @@ export default function createIntegration({
}
: null,
}),
cfPrismPlugin(),
],
},
image: setImageConfig(imageService, config.image, command, logger),
Expand Down Expand Up @@ -532,3 +550,19 @@ export default function createIntegration({
},
};
}

// Reads the package.json at the current root to check whether `@astrojs/prism` is installed.
// Using `require.resolve()` would not work correctly for projects inside a monorepo
// (such as Astro's test fixtures), as it would traverse parent node_modules directories
// to resolve the package. For this reason, we directly read `package.json` using `readFile` instead.
async function getIsAstroPrismInstalled(rootURL: URL) {
try {
const pkgURL = new URL('./package.json', rootURL);
const input = await readFile(pkgURL, { encoding: 'utf-8' });
const pkgJson = JSON.parse(input);

return Object.hasOwn(pkgJson['dependencies'], '@astrojs/prism');
} catch {
return false;
}
}
63 changes: 63 additions & 0 deletions packages/integrations/cloudflare/src/vite-plugin-prism.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { fileURLToPath } from 'node:url';
import type { Plugin } from 'vite';
import components from 'prismjs/components.js';

const MODULE_ID = 'virtual:astro-cloudflare:prism';
const RESOLVED_MODULE_ID = '\0' + MODULE_ID;

const languages = Object.keys(components.languages).filter((l) => l !== 'meta');

export default function cfPrismPlugin(): Plugin {
return {
name: '@astrojs/cloudflare:prism',
configEnvironment(environmentName) {
if (environmentName === 'ssr') {
return {
// Because this virtual module adds a large number of dynamic import statements,
// Vite’s logs will consequently display the message “new dependencies optimized” for all languages.
// To avoid this, we explicitly specify that the module should be optimized in advance.
optimizeDeps: {
include: ['prismjs/components/prism-*.js'],
},
};
}
},
resolveId: {
filter: {
id: new RegExp(`^${MODULE_ID}$`),
},
handler() {
return RESOLVED_MODULE_ID;
},
},
load: {
filter: {
id: new RegExp(`^${RESOLVED_MODULE_ID}$`),
},
async handler() {
const importerPath = fileURLToPath(import.meta.url);

const resolvedModules = await Promise.all(
languages.map(async (lang) => {
const resolvedId = await this.resolve(
`prismjs/components/prism-${lang}.js`,
importerPath,
);

return { resolvedId: resolvedId?.id, lang };
}),
);
const prismBundledLanguages = resolvedModules
.filter(({ resolvedId }) => resolvedId !== undefined)
.map(
({ resolvedId, lang }) =>
`${JSON.stringify(lang)}: () => import(${JSON.stringify(resolvedId)})`,
);

return `
export const bundledLanguages = { ${prismBundledLanguages.join(',')} };
`;
},
},
};
}
Loading
Loading