diff --git a/.eslintrc.cjs b/.eslintrc.cjs index c3e825cfc1..a5e9f80d80 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -7,13 +7,14 @@ module.exports = { 'plugin:@typescript-eslint/recommended-type-checked', 'plugin:@typescript-eslint/stylistic-type-checked', 'prettier', + 'plugin:regexp/recommended', ], parser: '@typescript-eslint/parser', parserOptions: { project: ['./packages/*/tsconfig.json', './tsconfig.eslint.json'], tsconfigRootDir: __dirname, }, - plugins: ['@typescript-eslint', 'prettier', 'no-only-tests'], + plugins: ['@typescript-eslint', 'prettier', 'no-only-tests', 'regexp'], rules: { // These off/configured-differently-by-default rules fit well for us '@typescript-eslint/switch-exhaustiveness-check': 'error', @@ -72,6 +73,9 @@ module.exports = { // These rules enabled by the preset configs don't work well for us '@typescript-eslint/await-thenable': 'off', 'prefer-const': 'off', + + // In some cases, using explicit letter-casing is more performant than the `i` flag + 'regexp/use-ignore-case': 'off', }, overrides: [ { diff --git a/package.json b/package.json index f85212d923..d26d4fac25 100644 --- a/package.json +++ b/package.json @@ -59,12 +59,13 @@ "eslint-config-prettier": "^9.0.0", "eslint-plugin-no-only-tests": "^3.1.0", "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-regexp": "^2.2.0", + "globby": "^14.0.0", "only-allow": "^1.1.1", "organize-imports-cli": "^0.10.0", "prettier": "^3.1.0", "prettier-plugin-astro": "^0.12.2", "tiny-glob": "^0.2.9", - "globby": "^14.0.0", "turbo": "^1.10.12", "typescript": "~5.2.2" }, diff --git a/packages/astro-prism/src/plugin.ts b/packages/astro-prism/src/plugin.ts index cbee66c337..a50b9d7ef6 100644 --- a/packages/astro-prism/src/plugin.ts +++ b/packages/astro-prism/src/plugin.ts @@ -16,6 +16,7 @@ export function addAstro(Prism: typeof import('prismjs')) { let script = Prism.util.clone(Prism.languages[scriptLang]); + // eslint-disable-next-line regexp/no-useless-assertions let space = /(?:\s|\/\/.*(?!.)|\/\*(?:[^*]|\*(?!\/))\*\/)/.source; let braces = /(?:\{(?:\{(?:\{[^{}]*\}|[^{}])*\}|[^{}])*\})/.source; let spread = /(?:\{*\.{3}(?:[^{}]|)*\})/.source; @@ -39,13 +40,13 @@ export function addAstro(Prism: typeof import('prismjs')) { Prism.languages.astro = Prism.languages.extend('markup', script); (Prism.languages.astro as any).tag.pattern = re( - /<\/?(?:[\w.:-]+(?:+(?:[\w.:$-]+(?:=(?:"(?:\\[^]|[^\\"])*"|'(?:\\[^]|[^\\'])*'|[^\s{'"/>=]+|))?|))**\/?)?>/ + /<\/?(?:[\w.:-]+(?:+(?:[\w.:$-]+(?:=(?:"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*'|[^\s{'"/>=]+|))?|))**\/?)?>/ .source ); - (Prism.languages.astro as any).tag.inside['tag'].pattern = /^<\/?[^\s>\/]*/i; + (Prism.languages.astro as any).tag.inside['tag'].pattern = /^<\/?[^\s>/]*/; (Prism.languages.astro as any).tag.inside['attr-value'].pattern = - /=(?!\{)(?:"(?:\\[^]|[^\\"])*"|'(?:\\[^]|[^\\'])*'|[^\s'">]+)/i; + /=(?!\{)(?:"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*'|[^\s'">]+)/; (Prism.languages.astro as any).tag.inside['tag'].inside['class-name'] = /^[A-Z]\w*(?:\.[A-Z]\w*)*$/; (Prism.languages.astro as any).tag.inside['comment'] = script['comment']; @@ -71,7 +72,7 @@ export function addAstro(Prism: typeof import('prismjs')) { pattern: re(/=/.source), inside: { 'script-punctuation': { - pattern: /^=(?={)/, + pattern: /^=(?=\{)/, alias: 'punctuation', }, rest: Prism.languages.astro, diff --git a/packages/astro-rss/src/util.ts b/packages/astro-rss/src/util.ts index bc15897807..1e49b3d775 100644 --- a/packages/astro-rss/src/util.ts +++ b/packages/astro-rss/src/util.ts @@ -10,10 +10,10 @@ export function createCanonicalURL( let pathname = url.replace(/\/index.html$/, ''); // index.html is not canonical if (trailingSlash === false) { // remove the trailing slash - pathname = pathname.replace(/(\/+)?$/, ''); + pathname = pathname.replace(/\/*$/, ''); } else if (!getUrlExtension(url)) { // add trailing slash if thereโ€™s no extension or `trailingSlash` is true - pathname = pathname.replace(/(\/+)?$/, '/'); + pathname = pathname.replace(/\/*$/, '/'); } pathname = pathname.replace(/\/+/g, '/'); // remove duplicate slashes (URL() wonโ€™t) diff --git a/packages/astro/src/assets/services/vendor/squoosh/codecs.ts b/packages/astro/src/assets/services/vendor/squoosh/codecs.ts index 80aae75207..e4705e10b9 100644 --- a/packages/astro/src/assets/services/vendor/squoosh/codecs.ts +++ b/packages/astro/src/assets/services/vendor/squoosh/codecs.ts @@ -291,6 +291,8 @@ export const codecs = { avif: { name: 'AVIF', extension: 'avif', + // Disable eslint rule to not touch the original code + // eslint-disable-next-line no-control-regex, regexp/control-character-escape detectors: [/^\x00\x00\x00 ftypavif\x00\x00\x00\x00/], dec: () => instantiateEmscriptenWasm(avifDec as DecodeModuleFactory, avifDecWasm), @@ -321,6 +323,8 @@ export const codecs = { oxipng: { name: 'OxiPNG', extension: 'png', + // Disable eslint rule to not touch the original code + // eslint-disable-next-line no-control-regex, regexp/control-character-escape detectors: [/^\x89PNG\x0D\x0A\x1A\x0A/], dec: async () => { await pngEncDecInit() diff --git a/packages/astro/src/cli/add/index.ts b/packages/astro/src/cli/add/index.ts index b8cb1949cd..e1b64eb185 100644 --- a/packages/astro/src/cli/add/index.ts +++ b/packages/astro/src/cli/add/index.ts @@ -384,11 +384,11 @@ const toIdent = (name: string) => { const ident = name .trim() // Remove astro or (astrojs) prefix and suffix - .replace(/[-_\.\/]?astro(?:js)?[-_\.]?/g, '') + .replace(/[-_./]?astro(?:js)?[-_.]?/g, '') // drop .js suffix .replace(/\.js/, '') // convert to camel case - .replace(/(?:[\.\-\_\/]+)([a-zA-Z])/g, (_, w) => w.toUpperCase()) + .replace(/[.\-_/]+([a-zA-Z])/g, (_, w) => w.toUpperCase()) // drop invalid first characters .replace(/^[^a-zA-Z$_]+/, ''); return `${ident[0].toLowerCase()}${ident.slice(1)}`; diff --git a/packages/astro/src/core/build/css-asset-name.ts b/packages/astro/src/core/build/css-asset-name.ts index 29fc14294e..64852b366a 100644 --- a/packages/astro/src/core/build/css-asset-name.ts +++ b/packages/astro/src/core/build/css-asset-name.ts @@ -103,7 +103,7 @@ function getFirstParentId(parents: [ModuleInfo, number, number][]) { return parents[0]?.[0].id; } -const charsToReplaceRe = /[.\[\]]/g; +const charsToReplaceRe = /[.[\]]/g; const underscoresRe = /_+/g; /** * Prettify base names so they're easier to read: diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts index e5a1c1b065..6b54135835 100644 --- a/packages/astro/src/core/build/plugins/plugin-manifest.ts +++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts @@ -19,7 +19,7 @@ import type { StaticBuildOptions } from '../types.js'; import { normalizeTheLocale } from '../../../i18n/index.js'; const manifestReplace = '@@ASTRO_MANIFEST_REPLACE@@'; -const replaceExp = new RegExp(`['"](${manifestReplace})['"]`, 'g'); +const replaceExp = new RegExp(`['"]${manifestReplace}['"]`, 'g'); export const SSR_MANIFEST_VIRTUAL_MODULE_ID = '@astrojs-manifest'; export const RESOLVED_SSR_MANIFEST_VIRTUAL_MODULE_ID = '\0' + SSR_MANIFEST_VIRTUAL_MODULE_ID; diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts index a1278c8033..3fc40035d6 100644 --- a/packages/astro/src/core/build/static-build.ts +++ b/packages/astro/src/core/build/static-build.ts @@ -524,7 +524,7 @@ export function makeAstroPageEntryPointFileName( const name = route?.route ?? pageModuleId; return `pages${name .replace(/\/$/, '/index') - .replaceAll(/[\[\]]/g, '_') + .replaceAll(/[[\]]/g, '_') .replaceAll('...', '---')}.astro.mjs`; } diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts index 0b0c7d47ff..b2de6afb5c 100644 --- a/packages/astro/src/core/create-vite.ts +++ b/packages/astro/src/core/create-vite.ts @@ -90,7 +90,7 @@ export async function createVite( pkgJson.keywords?.includes('astro') || pkgJson.keywords?.includes('astro-component') || // Attempt: package is named `astro-something` or `@scope/astro-something`. โœ… Likely a community package - /^(@[^\/]+\/)?astro\-/.test(pkgJson.name) + /^(?:@[^/]+\/)?astro-/.test(pkgJson.name) ); }, isFrameworkPkgByName(pkgName) { diff --git a/packages/astro/src/core/dev/restart.ts b/packages/astro/src/core/dev/restart.ts index 23e6af369a..7a1b15ed05 100644 --- a/packages/astro/src/core/dev/restart.ts +++ b/packages/astro/src/core/dev/restart.ts @@ -29,8 +29,8 @@ async function createRestartedContainer( return newContainer; } -const configRE = new RegExp(`.*astro\.config\.((mjs)|(cjs)|(js)|(ts))$`); -const preferencesRE = new RegExp(`.*\.astro\/settings\.json$`); +const configRE = /.*astro.config.(?:mjs|cjs|js|ts)$/; +const preferencesRE = /.*\.astro\/settings.json$/; export function shouldRestartContainer( { settings, inlineConfig, restartInFlight }: Container, diff --git a/packages/astro/src/core/errors/dev/utils.ts b/packages/astro/src/core/errors/dev/utils.ts index cda9e4227f..c391e462b0 100644 --- a/packages/astro/src/core/errors/dev/utils.ts +++ b/packages/astro/src/core/errors/dev/utils.ts @@ -132,7 +132,7 @@ export function collectErrorMetadata(e: any, rootFolder?: URL | undefined): Erro function generateHint(err: ErrorWithMetadata): string | undefined { const commonBrowserAPIs = ['document', 'window']; - if (/Unknown file extension \"\.(jsx|vue|svelte|astro|css)\" for /.test(err.message)) { + if (/Unknown file extension "\.(?:jsx|vue|svelte|astro|css)" for /.test(err.message)) { return 'You likely need to add this package to `vite.ssr.noExternal` in your astro config file.'; } else if (commonBrowserAPIs.some((api) => err.toString().includes(api))) { const hint = `Browser APIs are not available on the server. @@ -172,10 +172,12 @@ function collectInfoFromStacktrace(error: SSRError & { stack: string }): StackIn error.id || // TODO: this could be better, `src` might be something else stackText.split('\n').find((ln) => ln.includes('src') || ln.includes('node_modules')); + // Disable eslint as we're not sure how to improve this regex yet + // eslint-disable-next-line regexp/no-super-linear-backtracking const source = possibleFilePath?.replace(/^[^(]+\(([^)]+).*$/, '$1').replace(/^\s+at\s+/, ''); - let file = source?.replace(/(:[0-9]+)/g, ''); - const location = /:([0-9]+):([0-9]+)/g.exec(source!) ?? []; + let file = source?.replace(/:\d+/g, ''); + const location = /:(\d+):(\d+)/.exec(source!) ?? []; const line = location[1]; const column = location[2]; @@ -195,8 +197,8 @@ function collectInfoFromStacktrace(error: SSRError & { stack: string }): StackIn // Derive plugin from stack (if possible) if (!stackInfo.plugin) { stackInfo.plugin = - /withastro\/astro\/packages\/integrations\/([\w-]+)/gim.exec(stackText)?.at(1) || - /(@astrojs\/[\w-]+)\/(server|client|index)/gim.exec(stackText)?.at(1) || + /withastro\/astro\/packages\/integrations\/([\w-]+)/i.exec(stackText)?.at(1) || + /(@astrojs\/[\w-]+)\/(server|client|index)/i.exec(stackText)?.at(1) || undefined; } @@ -208,7 +210,7 @@ function collectInfoFromStacktrace(error: SSRError & { stack: string }): StackIn function cleanErrorStack(stack: string) { return stack - .split(/\n/g) + .split(/\n/) .map((l) => l.replace(/\/@fs\//g, '/')) .join('\n'); } @@ -233,10 +235,10 @@ export function getDocsForError(err: ErrorWithMetadata): string | undefined { * Render a subset of Markdown to HTML or a CLI output */ export function renderErrorMarkdown(markdown: string, target: 'html' | 'cli') { - const linkRegex = /\[([^\[]+)\]\((.*)\)/gm; - const boldRegex = /\*\*(.+)\*\*/gm; - const urlRegex = / (\b(https?|ftp):\/\/[-A-Z0-9+&@#\\/%?=~_|!:,.;]*[-A-Z0-9+&@#\\/%=~_|])/gim; - const codeRegex = /`([^`]+)`/gim; + const linkRegex = /\[([^[]+)\]\((.*)\)/g; + const boldRegex = /\*\*(.+)\*\*/g; + const urlRegex = / ((?:https?|ftp):\/\/[-\w+&@#\\/%?=~|!:,.;]*[-\w+&@#\\/%=~|])/gi; + const codeRegex = /`([^`]+)`/g; if (target === 'html') { return escape(markdown) diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts index 11bc3570d2..aaa71b02c4 100644 --- a/packages/astro/src/core/errors/errors-data.ts +++ b/packages/astro/src/core/errors/errors-data.ts @@ -587,7 +587,7 @@ export const PrerenderDynamicEndpointPathCollide = { message: (pathname: string) => `Could not render \`${pathname}\` with an \`undefined\` param as the generated path will collide during prerendering. Prevent passing \`undefined\` as \`params\` for the endpoint's \`getStaticPaths()\` function, or add an additional extension to the endpoint's filename.`, hint: (filename: string) => - `Rename \`${filename}\` to \`${filename.replace(/\.(js|ts)/, (m) => `.json` + m)}\``, + `Rename \`${filename}\` to \`${filename.replace(/\.(?:js|ts)/, (m) => `.json` + m)}\``, } satisfies ErrorData; /** * @docs diff --git a/packages/astro/src/core/logger/vite.ts b/packages/astro/src/core/logger/vite.ts index 9604a68f05..ca803e0ff3 100644 --- a/packages/astro/src/core/logger/vite.ts +++ b/packages/astro/src/core/logger/vite.ts @@ -12,15 +12,15 @@ function isAstroSrcFile(id: string | null) { } // capture "page reload some/Component.vue (additional info)" messages -const vitePageReloadMsg = /page reload (.*)( \(.*\))?/; +const vitePageReloadMsg = /page reload (.*)/; // capture "hmr update some/Component.vue" messages const viteHmrUpdateMsg = /hmr update (.*)/; // capture "vite v5.0.0 building SSR bundle for production..." and "vite v5.0.0 building for production..." messages const viteBuildMsg = /vite.*building.*for production/; // capture "\n Shortcuts" messages -const viteShortcutTitleMsg = /^\s*Shortcuts\s*$/s; +const viteShortcutTitleMsg = /^\s*Shortcuts\s*$/; // capture "press * + enter to ..." messages -const viteShortcutHelpMsg = /press\s+(.*?)\s+to\s+(.*)$/s; +const viteShortcutHelpMsg = /press (.+?) to (.+)$/s; export function createViteLogger( astroLogger: AstroLogger, @@ -39,8 +39,7 @@ export function createViteLogger( // Rewrite HMR page reload message if ((m = vitePageReloadMsg.exec(stripped))) { if (isAstroSrcFile(m[1])) return; - const extra = m[2] ?? ''; - astroLogger.info('watch', m[1] + extra); + astroLogger.info('watch', m[1]); } // Rewrite HMR update message else if ((m = viteHmrUpdateMsg.exec(stripped))) { diff --git a/packages/astro/src/core/messages.ts b/packages/astro/src/core/messages.ts index 5b424944fe..f69b846974 100644 --- a/packages/astro/src/core/messages.ts +++ b/packages/astro/src/core/messages.ts @@ -225,7 +225,7 @@ export function formatConfigErrorMessage(err: ZodError) { // a regex to match the first line of a stack trace const STACK_LINE_REGEXP = /^\s+at /g; -const IRRELEVANT_STACK_REGEXP = /(node_modules|astro[\/\\]dist)/g; +const IRRELEVANT_STACK_REGEXP = /node_modules|astro[/\\]dist/g; function formatErrorStackTrace( err: Error | ErrorWithMetadata, showFullStacktrace: boolean diff --git a/packages/astro/src/core/preview/vite-plugin-astro-preview.ts b/packages/astro/src/core/preview/vite-plugin-astro-preview.ts index 9ec940c681..a425807dcc 100644 --- a/packages/astro/src/core/preview/vite-plugin-astro-preview.ts +++ b/packages/astro/src/core/preview/vite-plugin-astro-preview.ts @@ -7,7 +7,7 @@ import { notFoundTemplate, subpathNotUsedTemplate } from '../../template/4xx.js' import { cleanUrl } from '../../vite-plugin-utils/index.js'; import { stripBase } from './util.js'; -const HAS_FILE_EXTENSION_REGEXP = /^.*\.[^\\]+$/; +const HAS_FILE_EXTENSION_REGEXP = /\.[^/]+$/; export function vitePluginAstroPreview(settings: AstroSettings): Plugin { const { base, outDir, trailingSlash } = settings.config; diff --git a/packages/astro/src/core/routing/manifest/create.ts b/packages/astro/src/core/routing/manifest/create.ts index 3818f08c76..6a1064c052 100644 --- a/packages/astro/src/core/routing/manifest/create.ts +++ b/packages/astro/src/core/routing/manifest/create.ts @@ -43,13 +43,15 @@ function countOccurrences(needle: string, haystack: string) { function getParts(part: string, file: string) { const result: RoutePart[] = []; + // Disable eslint as we're not sure how to improve this regex yet + // eslint-disable-next-line regexp/no-super-linear-backtracking part.split(/\[(.+?\(.+?\)|.+?)\]/).map((str, i) => { if (!str) return; const dynamic = i % 2 === 1; const [, content] = dynamic ? /([^(]+)$/.exec(str) || [null, null] : [null, str]; - if (!content || (dynamic && !/^(\.\.\.)?[a-zA-Z0-9_$]+$/.test(content))) { + if (!content || (dynamic && !/^(?:\.\.\.)?[\w$]+$/.test(content))) { throw new Error(`Invalid route ${file} โ€” parameter name must match /^[a-zA-Z0-9_$]+$/`); } diff --git a/packages/astro/src/events/error.ts b/packages/astro/src/events/error.ts index b3326091de..8b8e9767e6 100644 --- a/packages/astro/src/events/error.ts +++ b/packages/astro/src/events/error.ts @@ -26,7 +26,7 @@ interface ConfigErrorEventPayload extends ErrorEventPayload { * This is only used for errors that do not come from us so we can get a basic * and anonymous idea of what the error is about. */ -const ANONYMIZE_MESSAGE_REGEX = /^(\w| )+/; +const ANONYMIZE_MESSAGE_REGEX = /^(?:\w| )+/; function anonymizeErrorMessage(msg: string): string | undefined { const matchedMessage = msg.match(ANONYMIZE_MESSAGE_REGEX); if (!matchedMessage?.[0]) { @@ -100,7 +100,7 @@ function getSafeErrorMessage(message: string | Function): string { .trim() .slice(1, -1) .replace( - /\${([^}]+)}/gm, + /\$\{([^}]+)\}/g, (str, match1) => `${match1 .split(/\.?(?=[A-Z])/) diff --git a/packages/astro/src/i18n/middleware.ts b/packages/astro/src/i18n/middleware.ts index 9fabff13af..5e9f17a6a8 100644 --- a/packages/astro/src/i18n/middleware.ts +++ b/packages/astro/src/i18n/middleware.ts @@ -32,13 +32,6 @@ function pathnameHasLocale(pathname: string, locales: Locales): boolean { return false; } -type MiddlewareOptions = { - i18n: SSRManifest['i18n']; - base: SSRManifest['base']; - trailingSlash: SSRManifest['trailingSlash']; - buildFormat: SSRManifest['buildFormat']; -}; - export function createI18nMiddleware( i18n: SSRManifest['i18n'], base: SSRManifest['base'], diff --git a/packages/astro/src/runtime/client/dev-toolbar/apps/audit/a11y.ts b/packages/astro/src/runtime/client/dev-toolbar/apps/audit/a11y.ts index ac1624cd9f..2b49439084 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/apps/audit/a11y.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/apps/audit/a11y.ts @@ -240,7 +240,7 @@ export const a11y: AuditRuleWithSelector[] = [ message: 'Screen readers already announce `img` elements as an image. There is no need to use words such as "image", "photo", and/or "picture".', selector: 'img[alt]:not([aria-hidden])', - match: (img: HTMLImageElement) => /\b(image|picture|photo)\b/i.test(img.alt), + match: (img: HTMLImageElement) => /\b(?:image|picture|photo)\b/i.test(img.alt), }, { code: 'a11y-incorrect-aria-attribute-type', diff --git a/packages/astro/src/runtime/server/render/component.ts b/packages/astro/src/runtime/server/render/component.ts index 3fcb6f2aa3..6d21175459 100644 --- a/packages/astro/src/runtime/server/render/component.ts +++ b/packages/astro/src/runtime/server/render/component.ts @@ -65,8 +65,8 @@ function isHTMLComponent(Component: unknown) { return Component && (Component as any)['astro:html'] === true; } -const ASTRO_SLOT_EXP = /\<\/?astro-slot\b[^>]*>/g; -const ASTRO_STATIC_SLOT_EXP = /\<\/?astro-static-slot\b[^>]*>/g; +const ASTRO_SLOT_EXP = /<\/?astro-slot\b[^>]*>/g; +const ASTRO_STATIC_SLOT_EXP = /<\/?astro-static-slot\b[^>]*>/g; function removeStaticAstroSlot(html: string, supportsAstroStaticSlot: boolean) { const exp = supportsAstroStaticSlot ? ASTRO_STATIC_SLOT_EXP : ASTRO_SLOT_EXP; return html.replace(exp, ''); @@ -390,7 +390,7 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr } function sanitizeElementName(tag: string) { - const unsafe = /[&<>'"\s]+/g; + const unsafe = /[&<>'"\s]+/; if (!unsafe.test(tag)) return tag; return tag.trim().split(unsafe)[0].trim(); } diff --git a/packages/astro/src/runtime/server/render/util.ts b/packages/astro/src/runtime/server/render/util.ts index 0e3f413833..91883024e9 100644 --- a/packages/astro/src/runtime/server/render/util.ts +++ b/packages/astro/src/runtime/server/render/util.ts @@ -7,17 +7,17 @@ import { HTMLString, markHTMLString } from '../escape.js'; export const voidElementNames = /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i; const htmlBooleanAttributes = - /^(allowfullscreen|async|autofocus|autoplay|controls|default|defer|disabled|disablepictureinpicture|disableremoteplayback|formnovalidate|hidden|loop|nomodule|novalidate|open|playsinline|readonly|required|reversed|scoped|seamless|itemscope)$/i; -const htmlEnumAttributes = /^(contenteditable|draggable|spellcheck|value)$/i; + /^(?:allowfullscreen|async|autofocus|autoplay|controls|default|defer|disabled|disablepictureinpicture|disableremoteplayback|formnovalidate|hidden|loop|nomodule|novalidate|open|playsinline|readonly|required|reversed|scoped|seamless|itemscope)$/i; +const htmlEnumAttributes = /^(?:contenteditable|draggable|spellcheck|value)$/i; // Note: SVG is case-sensitive! -const svgEnumAttributes = /^(autoReverse|externalResourcesRequired|focusable|preserveAlpha)$/i; +const svgEnumAttributes = /^(?:autoReverse|externalResourcesRequired|focusable|preserveAlpha)$/i; const STATIC_DIRECTIVES = new Set(['set:html', 'set:text']); // converts (most) arbitrary strings to valid JS identifiers const toIdent = (k: string) => - k.trim().replace(/(?:(?!^)\b\w|\s+|[^\w]+)/g, (match, index) => { - if (/[^\w]|\s/.test(match)) return ''; + k.trim().replace(/(?!^)\b\w|\s+|\W+/g, (match, index) => { + if (/\W/.test(match)) return ''; return index === 0 ? match : match.toUpperCase(); }); diff --git a/packages/astro/src/virtual-modules/content.ts b/packages/astro/src/virtual-modules/content.ts index a3e9a68289..8424f3b06c 100644 --- a/packages/astro/src/virtual-modules/content.ts +++ b/packages/astro/src/virtual-modules/content.ts @@ -66,6 +66,7 @@ export const reference = noop; /** Run `astro sync` to generate high fidelity types */ export type CollectionKey = any; /** Run `astro sync` to generate high fidelity types */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars export type CollectionEntry = any; /** Run `astro sync` to generate high fidelity types */ export type ContentCollectionKey = any; diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index f04e236418..0cc8a81932 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -108,7 +108,7 @@ export async function matchRoute( // Try without `.html` extensions or `index.html` in request URLs to mimic // routing behavior in production builds. This supports both file and directory // build formats, and is necessary based on how the manifest tracks build targets. - const altPathname = pathname.replace(/(index)?\.html$/, ''); + const altPathname = pathname.replace(/(?:index)?\.html$/, ''); if (altPathname !== pathname) { return await matchRoute(altPathname, manifestData, pipeline); } @@ -229,6 +229,8 @@ export async function handleRoute({ return ''; }, params: [], + // Disable eslint as we only want to generate an empty RegExp + // eslint-disable-next-line prefer-regex-literals pattern: new RegExp(''), prerender: false, segments: [], diff --git a/packages/astro/src/vite-plugin-astro/compile.ts b/packages/astro/src/vite-plugin-astro/compile.ts index 15fc9ba73b..3a1d4c6f64 100644 --- a/packages/astro/src/vite-plugin-astro/compile.ts +++ b/packages/astro/src/vite-plugin-astro/compile.ts @@ -4,6 +4,7 @@ import { compile, type CompileProps, type CompileResult } from '../core/compile/ import type { Logger } from '../core/logger/core.js'; import { getFileInfo } from '../vite-plugin-utils/index.js'; import type { CompileMetadata } from './types.js'; +import { frontmatterRE } from './utils.js'; interface CompileAstroOption { compileProps: CompileProps; @@ -23,8 +24,6 @@ interface EnhanceCompilerErrorOptions { logger: Logger; } -const FRONTMATTER_PARSE_REGEXP = /^\-\-\-(.*)^\-\-\-/ms; - export async function compileAstro({ compileProps, astroFileToCompileMetadata, @@ -107,7 +106,7 @@ async function enhanceCompileError({ // Before throwing, it is better to verify the frontmatter here, and // let esbuild throw a more specific exception if the code is invalid. // If frontmatter is valid or cannot be parsed, then continue. - const scannedFrontmatter = FRONTMATTER_PARSE_REGEXP.exec(source); + const scannedFrontmatter = frontmatterRE.exec(source); if (scannedFrontmatter) { // Top-level return is not supported, so replace `return` with throw const frontmatter = scannedFrontmatter[1].replace(/\breturn\b/g, 'throw'); diff --git a/packages/astro/src/vite-plugin-astro/hmr.ts b/packages/astro/src/vite-plugin-astro/hmr.ts index 949fc1d6cb..28527f90e4 100644 --- a/packages/astro/src/vite-plugin-astro/hmr.ts +++ b/packages/astro/src/vite-plugin-astro/hmr.ts @@ -4,6 +4,7 @@ import type { HmrContext } from 'vite'; import type { Logger } from '../core/logger/core.js'; import type { CompileAstroResult } from './compile.js'; import type { CompileMetadata } from './types.js'; +import { frontmatterRE } from './utils.js'; export interface HandleHotUpdateOptions { logger: Logger; @@ -58,8 +59,10 @@ export async function handleHotUpdate( } } -const frontmatterRE = /^\-\-\-.*?^\-\-\-/ms; +// Disable eslint as we're not sure how to improve this regex yet +// eslint-disable-next-line regexp/no-super-linear-backtracking const scriptRE = /.*?<\/script>/gs; +// eslint-disable-next-line regexp/no-super-linear-backtracking const styleRE = /.*?<\/style>/gs; function isStyleOnlyChanged(oldCode: string, newCode: string) { diff --git a/packages/astro/src/vite-plugin-astro/utils.ts b/packages/astro/src/vite-plugin-astro/utils.ts new file mode 100644 index 0000000000..8bb5b617a3 --- /dev/null +++ b/packages/astro/src/vite-plugin-astro/utils.ts @@ -0,0 +1 @@ +export const frontmatterRE = /^---(.*?)^---/ms; diff --git a/packages/astro/src/vite-plugin-env/index.ts b/packages/astro/src/vite-plugin-env/index.ts index 2e16cc5bf3..6621d21795 100644 --- a/packages/astro/src/vite-plugin-env/index.ts +++ b/packages/astro/src/vite-plugin-env/index.ts @@ -13,7 +13,7 @@ interface EnvPluginOptions { const importMetaEnvOnlyRe = /\bimport\.meta\.env\b(?!\.)/; // Match valid JS variable names (identifiers), which accepts most alphanumeric characters, // except that the first character cannot be a number. -const isValidIdentifierRe = /^[_$a-zA-Z][_$a-zA-Z0-9]*$/; +const isValidIdentifierRe = /^[_$a-zA-Z][\w$]*$/; // Match `export const prerender = import.meta.env.*` since `vite=plugin-scanner` requires // the `import.meta.env.*` to always be replaced. const exportConstPrerenderRe = /\bexport\s+const\s+prerender\s*=\s*import\.meta\.env\.(.+?)\b/; diff --git a/packages/astro/src/vite-plugin-head/index.ts b/packages/astro/src/vite-plugin-head/index.ts index 228e4e437f..0350e9d777 100644 --- a/packages/astro/src/vite-plugin-head/index.ts +++ b/packages/astro/src/vite-plugin-head/index.ts @@ -9,7 +9,7 @@ import type { BuildInternals } from '../core/build/internal.js'; import { getAstroMetadata } from '../vite-plugin-astro/index.js'; // Detect this in comments, both in .astro components and in js/ts files. -const injectExp = /(^\/\/|\/\/!)\s*astro-head-inject/; +const injectExp = /(?:^\/\/|\/\/!)\s*astro-head-inject/; export default function configHeadVitePlugin(): vite.Plugin { let server: vite.ViteDevServer; diff --git a/packages/astro/src/vite-plugin-html/transform/utils.ts b/packages/astro/src/vite-plugin-html/transform/utils.ts index 88cb226e5d..dd0ebcd14d 100644 --- a/packages/astro/src/vite-plugin-html/transform/utils.ts +++ b/packages/astro/src/vite-plugin-html/transform/utils.ts @@ -1,7 +1,7 @@ import type { Element } from 'hast'; import type MagicString from 'magic-string'; -const splitAttrsTokenizer = /([\$\{\}\@a-z0-9_\:\-]*)\s*?=\s*?(['"]?)(.*?)\2\s+/gim; +const splitAttrsTokenizer = /([${}@\w:\-]*)\s*=\s*?(['"]?)(.*?)\2\s+/g; export function replaceAttribute(s: MagicString, node: Element, key: string, newValue: string) { splitAttrsTokenizer.lastIndex = 0; @@ -12,7 +12,7 @@ export function replaceAttribute(s: MagicString, node: Element, key: string, new if (offset === -1) return; const start = node.position!.start.offset! + offset; const tokens = text.slice(offset).split(splitAttrsTokenizer); - const token = tokens[0].replace(/([^>])(\>[\s\S]*$)/gim, '$1'); + const token = tokens[0].replace(/([^>])>[\s\S]*$/gm, '$1'); if (token.trim() === key) { const end = start + key.length; return s.overwrite(start, end, newValue, { contentOnly: true }); diff --git a/packages/astro/src/vite-plugin-scanner/scan.ts b/packages/astro/src/vite-plugin-scanner/scan.ts index 6c277567d4..4e0e5fbfe0 100644 --- a/packages/astro/src/vite-plugin-scanner/scan.ts +++ b/packages/astro/src/vite-plugin-scanner/scan.ts @@ -65,7 +65,7 @@ export async function scan( .trim(); // For a given export, check the value of the first non-whitespace token. // Basically extract the `true` from the statement `export const prerender = true` - const suffix = code.slice(endOfLocalName).trim().replace(/\=/, '').trim().split(/[;\n]/)[0]; + const suffix = code.slice(endOfLocalName).trim().replace(/=/, '').trim().split(/[;\n]/)[0]; if (prefix !== 'const' || !(isTruthy(suffix) || isFalsy(suffix))) { throw new AstroError({ ...AstroErrorData.InvalidPrerenderExport, diff --git a/packages/astro/src/vite-plugin-utils/index.ts b/packages/astro/src/vite-plugin-utils/index.ts index 6f672d7d99..21ade5b0a4 100644 --- a/packages/astro/src/vite-plugin-utils/index.ts +++ b/packages/astro/src/vite-plugin-utils/index.ts @@ -17,7 +17,7 @@ export function getFileInfo(id: string, config: AstroConfig) { let fileUrl = fileId.includes('/pages/') ? fileId .replace(/^.*?\/pages\//, sitePathname) - .replace(/(\/index)?\.(md|markdown|mdown|mkdn|mkd|mdwn|md|astro)$/, '') + .replace(/(?:\/index)?\.(?:md|markdown|mdown|mkdn|mkd|mdwn|astro)$/, '') : undefined; if (fileUrl && config.trailingSlash === 'always') { fileUrl = appendForwardSlash(fileUrl); diff --git a/packages/astro/test/0-css.test.js b/packages/astro/test/0-css.test.js index 3a4b9241dc..c8c5af6d8f 100644 --- a/packages/astro/test/0-css.test.js +++ b/packages/astro/test/0-css.test.js @@ -42,7 +42,7 @@ describe('CSS', function () { const classes = $('#class'); let scopedAttribute; for (const [key] of Object.entries(classes[0].attribs)) { - if (/^data-astro-cid-[A-Za-z0-9-]+/.test(key)) { + if (/^data-astro-cid-[A-Za-z\d-]+/.test(key)) { // Ema: this is ugly, but for reasons that I don't want to explore, cheerio // lower case the hash of the attribute scopedAttribute = key; @@ -72,7 +72,7 @@ describe('CSS', function () { it('Child inheritance', (done) => { for (const [key] of Object.entries($('#passed-in')[0].attribs)) { - if (/^data-astro-cid-[A-Za-z0-9-]+/.test(key)) { + if (/^data-astro-cid-[A-Za-z\d-]+/.test(key)) { done(); } } @@ -84,25 +84,25 @@ describe('CSS', function () { }); it('