Ignore braces when building Sandpack file map (#7996)

Previously, `createFileMap` split the MDX meta string on spaces and assumed the first token was the filename. Once we prefixed code fences with `{expectedErrors: ...}`, it would incorrectly parse the meta and crash.

This PR updates createFileMap to skip tokens in the meta containing a start and end brace pair (using a stack to ensure we close on the correct brace) while tokenizing the meta string as expected.

Test plan: pages reported in #7994 no longer crash on the next PR

Closes #7994
This commit is contained in:
lauren
2025-09-18 19:18:09 -04:00
committed by GitHub
parent 2a9ef2d173
commit f369f3efdf

View File

@@ -16,6 +16,66 @@ export const AppJSPath = `/src/App.js`;
export const StylesCSSPath = `/src/styles.css`;
export const SUPPORTED_FILES = [AppJSPath, StylesCSSPath];
/**
* Tokenize meta attributes while ignoring brace-wrapped metadata (e.g. {expectedErrors: …}).
*/
function splitMeta(meta: string): string[] {
const tokens: string[] = [];
let current = '';
let depth = 0;
const trimmed = meta.trim();
for (let ii = 0; ii < trimmed.length; ii++) {
const char = trimmed[ii];
if (char === '{') {
if (depth === 0 && current) {
tokens.push(current);
current = '';
}
depth += 1;
continue;
}
if (char === '}') {
if (depth > 0) {
depth -= 1;
}
if (depth === 0) {
current = '';
}
if (depth < 0) {
throw new Error(`Unexpected closing brace in meta: ${meta}`);
}
continue;
}
if (depth > 0) {
continue;
}
if (/\s/.test(char)) {
if (current) {
tokens.push(current);
current = '';
}
continue;
}
current += char;
}
if (current) {
tokens.push(current);
}
if (depth !== 0) {
throw new Error(`Unclosed brace in meta: ${meta}`);
}
return tokens;
}
export const createFileMap = (codeSnippets: any) => {
return codeSnippets.reduce(
(result: Record<string, SandpackFile>, codeSnippet: React.ReactElement) => {
@@ -37,12 +97,17 @@ export const createFileMap = (codeSnippets: any) => {
let fileActive = false; // if the file tab is shown by default
if (props.meta) {
const [name, ...params] = props.meta.split(' ');
filePath = '/' + name;
if (params.includes('hidden')) {
const tokens = splitMeta(props.meta);
const name = tokens.find(
(token) => token.includes('/') || token.includes('.')
);
if (name) {
filePath = name.startsWith('/') ? name : `/${name}`;
}
if (tokens.includes('hidden')) {
fileHidden = true;
}
if (params.includes('active')) {
if (tokens.includes('active')) {
fileActive = true;
}
} else {
@@ -57,6 +122,18 @@ export const createFileMap = (codeSnippets: any) => {
}
}
if (!filePath) {
if (props.className === 'language-js') {
filePath = AppJSPath;
} else if (props.className === 'language-css') {
filePath = StylesCSSPath;
} else {
throw new Error(
`Code block is missing a filename: ${props.children}`
);
}
}
if (result[filePath]) {
throw new Error(
`File ${filePath} was defined multiple times. Each file snippet should have a unique path name`