diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 214da10534dc..33cad9688012 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -69,6 +69,9 @@ jobs: - name: Unit testing client run: yarn test:client + - name: Unit testing libs + run: yarn test:libs + - name: Build and start server id: server env: diff --git a/build/check-images.ts b/build/check-images.ts index d7f1f3ec081c..806a6c8571e0 100644 --- a/build/check-images.ts +++ b/build/check-images.ts @@ -6,7 +6,7 @@ import path from "node:path"; import imagesize from "image-size"; -import { Document, Image } from "../content/index.js"; +import { Document, FileAttachment } from "../content/index.js"; import { FLAW_LEVELS, DEFAULT_LOCALE } from "../libs/constants/index.js"; import { findMatchesInText } from "./matches-in-text.js"; import * as cheerio from "cheerio"; @@ -140,12 +140,12 @@ export function checkImageReferences( // but all our images are going to be static. finalSrc = absoluteURL.pathname; // We can use the `finalSrc` to look up and find the image independent - // of the correct case because `Image.findByURL` operates case + // of the correct case because `FileAttachment.findByURL` operates case // insensitively. - // What follows uses the same algorithm as Image.findByURLWithFallback + // What follows uses the same algorithm as FileAttachment.findByURLWithFallback // but only adds a filePath if it exists for the DEFAULT_LOCALE - const filePath = Image.findByURL(finalSrc); + const filePath = FileAttachment.findByURL(finalSrc); let enUSFallback = false; if ( !filePath && @@ -156,7 +156,7 @@ export function checkImageReferences( new RegExp(`^/${doc.locale}/`, "i"), `/${DEFAULT_LOCALE}/` ); - if (Image.findByURL(enUSFinalSrc)) { + if (FileAttachment.findByURL(enUSFinalSrc)) { // Use the en-US src instead finalSrc = enUSFinalSrc; // Note that this `` value can work if you use the @@ -366,7 +366,7 @@ export function checkImageWidths( ); } } else if (!imgSrc.includes("://") && imgSrc.startsWith("/")) { - const filePath = Image.findByURLWithFallback(imgSrc); + const filePath = FileAttachment.findByURLWithFallback(imgSrc); if (filePath) { const dimensions = sizeOf(filePath); img.attr("width", `${dimensions.width}`); diff --git a/build/flaws/broken-links.ts b/build/flaws/broken-links.ts index edfecb1ab97b..f95ed9a60963 100644 --- a/build/flaws/broken-links.ts +++ b/build/flaws/broken-links.ts @@ -3,7 +3,7 @@ import fs from "node:fs"; import { fromMarkdown } from "mdast-util-from-markdown"; import { visit } from "unist-util-visit"; -import { Document, Redirect, Image } from "../../content/index.js"; +import { Document, Redirect, FileAttachment } from "../../content/index.js"; import { findMatchesInText } from "../matches-in-text.js"; import { DEFAULT_LOCALE, @@ -277,8 +277,8 @@ export function getBrokenLinksFlaws( const absoluteURL = new URL(href, "http://www.example.com"); const found = Document.findByURL(hrefNormalized); if (!found) { - // Before we give up, check if it's an image. - if (!Image.findByURLWithFallback(hrefNormalized)) { + // Before we give up, check if it's an attachment. + if (!FileAttachment.findByURLWithFallback(hrefNormalized)) { // Even if it's a redirect, it's still a flaw, but it'll be nice to // know what it *should* be. const resolved = Redirect.resolve(hrefNormalized); diff --git a/build/index.ts b/build/index.ts index b30f9918cae9..01f9880fc257 100644 --- a/build/index.ts +++ b/build/index.ts @@ -33,7 +33,7 @@ import LANGUAGES_RAW from "../libs/languages/index.js"; import { safeDecodeURIComponent } from "../kumascript/src/api/util.js"; import { wrapTables } from "./wrap-tables.js"; import { - getAdjacentImages, + getAdjacentFileAttachments, injectLoadingLazyAttributes, injectNoTranslate, makeTOC, @@ -382,8 +382,8 @@ export async function buildDocument( // The checkImageReferences() does 2 things. Checks image *references* and // it returns which images it checked. But we'll need to complement any // other images in the folder. - getAdjacentImages(path.dirname(document.fileInfo.path)).forEach((fp) => - fileAttachments.add(fp) + getAdjacentFileAttachments(path.dirname(document.fileInfo.path)).forEach( + (fp) => fileAttachments.add(fp) ); // Check the img tags for possible flaws and possible build-time rewrites diff --git a/build/utils.ts b/build/utils.ts index 9b76c2b8e266..f1d6151bfc53 100644 --- a/build/utils.ts +++ b/build/utils.ts @@ -12,8 +12,11 @@ import imageminSvgo from "imagemin-svgo"; import { rgPath } from "@vscode/ripgrep"; import sanitizeFilename from "sanitize-filename"; -import { VALID_MIME_TYPES } from "../libs/constants/index.js"; -import { Image } from "../content/index.js"; +import { + ANY_ATTACHMENT_REGEXP, + VALID_MIME_TYPES, +} from "../libs/constants/index.js"; +import { FileAttachment } from "../content/index.js"; import { spawnSync } from "node:child_process"; import { BLOG_ROOT } from "../libs/env/index.js"; @@ -184,15 +187,12 @@ export function splitSections(rawHTML) { * * @param {Document} document */ -export function getAdjacentImages(documentDirectory) { +export function getAdjacentFileAttachments(documentDirectory: string) { const dirents = fs.readdirSync(documentDirectory, { withFileTypes: true }); return dirents .filter((dirent) => { // This needs to match what we do in filecheck/checker.py - return ( - !dirent.isDirectory() && - /\.(png|jpeg|jpg|gif|svg|webp)$/i.test(dirent.name) - ); + return !dirent.isDirectory() && ANY_ATTACHMENT_REGEXP.test(dirent.name); }) .map((dirent) => path.join(documentDirectory, dirent.name)); } @@ -249,9 +249,9 @@ export function postLocalFileLinks($, doc) { const href = element.attribs.href; // This test is merely here to quickly bail if there's no hope to find the - // image as a local file link. There are a LOT of hyperlinks throughout - // the content and this simple if statement means we can skip 99% of the - // links, so it's presumed to be worth it. + // file attachment as a local file link. There are a LOT of hyperlinks + // throughout the content and this simple if statement means we can skip 99% + // of the links, so it's presumed to be worth it. if ( !href || /^(\/|\.\.|http|#|mailto:|about:|ftp:|news:|irc:|ftp:)/i.test(href) @@ -259,11 +259,11 @@ export function postLocalFileLinks($, doc) { return; } // There are a lot of links that don't match. E.g. `` - // So we'll look-up a lot "false positives" that are not images. + // So we'll look-up a lot "false positives" that are not file attachments. // Thankfully, this lookup is fast. const url = `${doc.mdn_url}/${href}`; - const image = Image.findByURLWithFallback(url); - if (image) { + const fileAttachment = FileAttachment.findByURLWithFallback(url); + if (fileAttachment) { $(element).attr("href", url); } }); diff --git a/client/src/react-app.d.ts b/client/src/react-app.d.ts index 4a3ff36d684d..6084dd416356 100644 --- a/client/src/react-app.d.ts +++ b/client/src/react-app.d.ts @@ -34,16 +34,41 @@ declare module "*.jpeg" { export default src; } +declare module "*.mp3" { + const src: string; + export default src; +} + +declare module "*.mp4" { + const src: string; + export default src; +} + +declare module "*.ogg" { + const src: string; + export default src; +} + declare module "*.png" { const src: string; export default src; } +declare module "*.webm" { + const src: string; + export default src; +} + declare module "*.webp" { const src: string; export default src; } +declare module "*.woff2" { + const src: string; + export default src; +} + declare module "*.svg" { import * as React from "react"; diff --git a/client/src/setupProxy.js b/client/src/setupProxy.js index dba0bbd277c7..8ab9ed5a2c2b 100644 --- a/client/src/setupProxy.js +++ b/client/src/setupProxy.js @@ -16,8 +16,8 @@ function config(app) { app.use("/_+(flaws|translations|open|document)", proxy); // E.g. search-index.json or index.json app.use("**/*.json", proxy); - // This has to match what we do in server/index.js in the catchall handler - app.use("**/*.(png|webp|gif|jpe?g|svg)", proxy); + // Always update libs/constant/index.js when adding/removing extensions! + app.use(`**/*.(gif|jpeg|jpg|mp3|mp4|ogg|png|svg|webm|webp|woff2)`, proxy); // All those root-level images like /favicon-48x48.png app.use("/*.(png|webp|gif|jpe?g|svg)", proxy); } diff --git a/cloud-function/src/app.ts b/cloud-function/src/app.ts index 9197425dd227..3507fce925da 100644 --- a/cloud-function/src/app.ts +++ b/cloud-function/src/app.ts @@ -1,6 +1,8 @@ import express, { Request, Response } from "express"; import { Router } from "express"; +import { ANY_ATTACHMENT_EXT } from "./internal/constants/index.js"; + import { Origin } from "./env.js"; import { proxyContent } from "./handlers/proxy-content.js"; import { proxyKevel } from "./handlers/proxy-kevel.js"; @@ -48,7 +50,7 @@ router.get( proxyContent ); router.get( - "/[^/]+/docs/*/*.(png|jpeg|jpg|gif|svg|webp)", + `/[^/]+/docs/*/*.(${ANY_ATTACHMENT_EXT.join("|")})`, requireOrigin(Origin.main, Origin.liveSamples), resolveIndexHTML, proxyContent diff --git a/cloud-function/src/utils.ts b/cloud-function/src/utils.ts index 9479c18168a8..34f0060d6608 100644 --- a/cloud-function/src/utils.ts +++ b/cloud-function/src/utils.ts @@ -1,5 +1,10 @@ import { Request, Response } from "express"; +import { + ANY_ATTACHMENT_EXT, + createRegExpFromExtensions, +} from "./internal/constants/index.js"; + import { DEFAULT_COUNTRY } from "./constants.js"; export function getRequestCountry(req: Request): string { @@ -45,8 +50,12 @@ export function isLiveSampleURL(url: string) { // These are the only extensions in client/build/*/docs/*. // `find client/build -type f | grep docs | xargs basename | sed 's/.*\.\([^.]*\)$/\1/' | sort | uniq` -const ASSET_REGEXP = /\.(gif|html|jpeg|jpg|json|png|svg|txt|xml)$/i; +const TEXT_EXT = ["html", "json", "svg", "txt", "xml"]; +const ANY_ATTACHMENT_REGEXP = createRegExpFromExtensions( + ...ANY_ATTACHMENT_EXT, + ...TEXT_EXT +); export function isAsset(url: string) { - return ASSET_REGEXP.test(url); + return ANY_ATTACHMENT_REGEXP.test(url); } diff --git a/content/image.ts b/content/file-attachment.ts similarity index 61% rename from content/image.ts rename to content/file-attachment.ts index e977b05ca9ce..152c66298639 100644 --- a/content/image.ts +++ b/content/file-attachment.ts @@ -5,14 +5,52 @@ import { readChunkSync } from "read-chunk"; import imageType from "image-type"; import isSvg from "is-svg"; -import { DEFAULT_LOCALE } from "../libs/constants/index.js"; +import { + ANY_IMAGE_EXT, + AUDIO_EXT, + DEFAULT_LOCALE, + FONT_EXT, + VIDEO_EXT, + createRegExpFromExtensions, +} from "../libs/constants/index.js"; import { ROOTS } from "../libs/env/index.js"; import { memoize, slugToFolder } from "./utils.js"; -function isImage(filePath: string) { +function isFileAttachment(filePath: string) { if (fs.statSync(filePath).isDirectory()) { return false; } + + return ( + isAudio(filePath) || + isFont(filePath) || + isVideo(filePath) || + isImage(filePath) + ); +} + +const AUDIO_FILE_REGEXP = createRegExpFromExtensions(...AUDIO_EXT); +const FONT_FILE_REGEXP = createRegExpFromExtensions(...FONT_EXT); +const VIDEO_FILE_REGEXP = createRegExpFromExtensions(...VIDEO_EXT); +const IMAGE_FILE_REGEXP = createRegExpFromExtensions(...ANY_IMAGE_EXT); + +function isAudio(filePath: string) { + return AUDIO_FILE_REGEXP.test(filePath); +} + +function isFont(filePath: string) { + return FONT_FILE_REGEXP.test(filePath); +} + +function isVideo(filePath: string) { + return VIDEO_FILE_REGEXP.test(filePath); +} + +function isImage(filePath: string) { + if (!IMAGE_FILE_REGEXP.test(filePath)) { + return false; + } + if (filePath.toLowerCase().endsWith(".svg")) { return isSvg(fs.readFileSync(filePath, "utf-8")); } @@ -37,7 +75,7 @@ function urlToFilePath(url: string) { const find = memoize((relativePath: string) => { return ROOTS.map((root) => path.join(root, relativePath)).find( - (filePath) => fs.existsSync(filePath) && isImage(filePath) + (filePath) => fs.existsSync(filePath) && isFileAttachment(filePath) ); }); diff --git a/content/index.ts b/content/index.ts index 0c41d320022c..18f2b8a4476f 100644 --- a/content/index.ts +++ b/content/index.ts @@ -2,7 +2,7 @@ export * as Document from "./document.js"; export * as Translation from "./translation.js"; export { getPopularities } from "./popularities.js"; export * as Redirect from "./redirect.js"; -export * as Image from "./image.js"; +export * as FileAttachment from "./file-attachment.js"; export { buildURL, memoize, diff --git a/filecheck/checker.ts b/filecheck/checker.ts index 81a71498535b..742bf51baff7 100644 --- a/filecheck/checker.ts +++ b/filecheck/checker.ts @@ -20,10 +20,20 @@ import { MAX_FILE_SIZE } from "../libs/env/index.js"; import { VALID_MIME_TYPES, MAX_COMPRESSION_DIFFERENCE_PERCENTAGE, + createRegExpFromExtensions, + AUDIO_EXT, + VIDEO_EXT, + FONT_EXT, } from "../libs/constants/index.js"; const { default: imageminPngquant } = imageminPngquantPkg; +const BINARY_NON_IMAGE_FILE_REGEXP = createRegExpFromExtensions( + ...AUDIO_EXT, + ...VIDEO_EXT, + ...FONT_EXT +); + function formatSize(bytes: number): string { if (bytes > 1024 * 1024) { return `${(bytes / 1024.0 / 1024.0).toFixed(1)}MB`; @@ -78,6 +88,21 @@ export async function checkFile( throw new Error(`${filePath} is 0 bytes`); } + // Ensure that binary files contain what their extension indicates. + // Exclude images, as they're checked separately in checkCompression(). + if (BINARY_NON_IMAGE_FILE_REGEXP.test(filePath)) { + const ext = filePath.split(".").pop(); + const type = await fileTypeFromFile(filePath); + if (!type) { + throw new Error(`Failed to detect type of file attachment: ${filePath}`); + } + if (ext.toLowerCase() !== type.ext) { + throw new Error( + `Unexpected type '${type.mime}' (*.${type.ext}) detected for file attachment: ${filePath}.` + ); + } + } + // FileType can't check for .svg files. // So use special case for files called '*.svg' if (path.extname(filePath) === ".svg") { @@ -177,6 +202,10 @@ export async function checkFile( ); } + await checkCompression(filePath, options); +} + +async function checkCompression(filePath: string, options: CheckerOptions) { const tempdir = temporaryDirectory(); const extension = path.extname(filePath).toLowerCase(); try { @@ -189,9 +218,12 @@ export async function checkFile( plugins.push(imageminGifsicle()); } else if (extension === ".svg") { plugins.push(imageminSvgo()); - } else { - throw new Error(`No plugin for ${extension}`); } + + if (!plugins.length) { + return; + } + const files = await imagemin([filePath], { destination: tempdir, plugins, @@ -225,7 +257,7 @@ export async function checkFile( filePath )} is too large (${formattedBefore} > ${formattedMax}), even after compressing to ${formattedAfter}.` ); - } else if (!options.saveCompression && stat.size > MAX_FILE_SIZE) { + } else if (!options.saveCompression && sizeBefore > MAX_FILE_SIZE) { throw new FixableError( `${getRelativePath( filePath diff --git a/libs/constants/index.d.ts b/libs/constants/index.d.ts index 296aa927d7b6..04e2deaad14b 100644 --- a/libs/constants/index.d.ts +++ b/libs/constants/index.d.ts @@ -6,6 +6,16 @@ export const LOCALE_ALIASES: Map; export const PREFERRED_LOCALE_COOKIE_NAME: string; export const CSP_SCRIPT_SRC_VALUES: string[]; export const CSP_VALUE: string; +export const AUDIO_EXT: string[]; +export const FONT_EXT: string[]; +export const BINARY_IMAGE_EXT: string[]; +export const ANY_IMAGE_EXT: string[]; +export const VIDEO_EXT: string[]; +export const ANY_ATTACHMENT_EXT: string[]; +export const BINARY_ATTACHMENT_EXT: string[]; +export const createRegExpFromExtensions: (...extensions: string[]) => RegExp; +export const ANY_ATTACHMENT_REGEXP: RegExp; +export const BINARY_ATTACHMENT_REGEXP: RegExp; export const FLAW_LEVELS: Readonly>; export const VALID_FLAW_CHECKS: Set; export const MDN_PLUS_TITLE: string; diff --git a/libs/constants/index.js b/libs/constants/index.js index bf209a9e56bd..d670023c3ee2 100644 --- a/libs/constants/index.js +++ b/libs/constants/index.js @@ -153,6 +153,38 @@ export const cspToString = (csp) => export const CSP_VALUE = cspToString(CSP_DIRECTIVES); +// Always update client/src/setupProxy.js when adding/removing extensions, or it won't work on the dev server! +export const AUDIO_EXT = ["mp3", "ogg"]; +export const FONT_EXT = ["woff2"]; +export const BINARY_IMAGE_EXT = ["gif", "jpeg", "jpg", "png", "webp"]; +export const ANY_IMAGE_EXT = ["svg", ...BINARY_IMAGE_EXT]; +export const VIDEO_EXT = ["mp4", "webm"]; + +export const BINARY_ATTACHMENT_EXT = [ + ...AUDIO_EXT, + ...FONT_EXT, + ...BINARY_IMAGE_EXT, + ...VIDEO_EXT, +].sort(); + +export const ANY_ATTACHMENT_EXT = [ + ...AUDIO_EXT, + ...FONT_EXT, + ...ANY_IMAGE_EXT, + ...VIDEO_EXT, +].sort(); + +export function createRegExpFromExtensions(...extensions) { + return new RegExp(`\\.(${extensions.join("|")})$`, "i"); +} + +export const ANY_ATTACHMENT_REGEXP = createRegExpFromExtensions( + ...ANY_ATTACHMENT_EXT +); +export const BINARY_ATTACHMENT_REGEXP = createRegExpFromExtensions( + ...BINARY_ATTACHMENT_EXT +); + // ----- // build // ----- @@ -202,9 +234,18 @@ export const MARKDOWN_FILENAME = "index.md"; // --------- export const VALID_MIME_TYPES = new Set([ + "audio/mp4", + "audio/mpeg", + "audio/ogg", + "audio/webm", + "font/woff2", "image/png", "image/jpeg", // this is what you get for .jpeg *and* .jpg file extensions "image/gif", + "image/webp", + "video/mp4", + "video/ogg", + "video/webm", ]); export const MAX_COMPRESSION_DIFFERENCE_PERCENTAGE = 25; // percent diff --git a/libs/constants/index.test.ts b/libs/constants/index.test.ts new file mode 100644 index 000000000000..f3d309e5d3e7 --- /dev/null +++ b/libs/constants/index.test.ts @@ -0,0 +1,62 @@ +import { + createRegExpFromExtensions, + ANY_ATTACHMENT_REGEXP, + BINARY_ATTACHMENT_REGEXP, +} from "./index.js"; + +describe("createRegExpFromExt", () => { + const regexp = createRegExpFromExtensions("foo"); + + it("accepts the extension", () => { + expect(regexp.test("test.foo")).toEqual(true); + }); + + it("accepts uppercase", () => { + expect(regexp.test("test.FOO")).toEqual(true); + }); + + it("rejects intermediate extensions", () => { + expect(regexp.test("test.foo.bar")).toEqual(false); + }); + + it("rejects other extensions", () => { + expect(regexp.test("test.bar")).toEqual(false); + }); + + it("rejects extensions starting with it", () => { + expect(regexp.test("test.foob")).toEqual(false); + }); + + it("rejects extensions ending with it", () => { + expect(regexp.test("test.afoo")).toEqual(false); + }); +}); + +describe("ANY_ATTACHMENT_REGEXP", () => { + const regexp = ANY_ATTACHMENT_REGEXP; + it("accepts audio files", () => { + expect(regexp.test("audio.mp3")).toEqual(true); + }); + + it("accepts video files", () => { + expect(regexp.test("video.mp4")).toEqual(true); + }); + + it("accepts font files", () => { + expect(regexp.test("diagram.svg")).toEqual(true); + }); + + ["index.html", "index.json", "index.md", "contributors.txt"].forEach( + (filename) => + it(`rejects ${filename}`, () => { + expect(regexp.test(filename)).toEqual(false); + }) + ); +}); + +describe("BINARY_ATTACHMENT_REGEXP", () => { + const regexp = BINARY_ATTACHMENT_REGEXP; + it("rejects svg files", () => { + expect(regexp.test("diagram.svg")).toEqual(false); + }); +}); diff --git a/package.json b/package.json index e653c15e6cfa..562874c46fe9 100644 --- a/package.json +++ b/package.json @@ -42,12 +42,13 @@ "start:static-server": "ts-node server/static.ts", "style-dictionary": "style-dictionary build -c sd-config.js", "stylelint": "stylelint \"**/*.scss\"", - "test": "yarn prettier-check && yarn test:client && yarn test:kumascript && yarn test:content && yarn test:testing", + "test": "yarn prettier-check && yarn test:client && yarn test:kumascript && yarn test:libs && yarn test:content && yarn test:testing", "test:client": "cd client && tsc --noEmit && cross-env NODE_ENV=test BABEL_ENV=test PUBLIC_URL='' node scripts/test.js --env=jsdom", "test:content": "yarn jest --rootDir content", "test:developing": "cross-env CONTENT_ROOT=mdn/content/files TESTING_DEVELOPING=true playwright test developing", "test:headless": "playwright test headless", "test:kumascript": "yarn jest --rootDir kumascript --env=node", + "test:libs": "yarn jest --rootDir libs --env=node", "test:prepare": "yarn build:prepare && yarn build && yarn start:static-server", "test:testing": "yarn jest --rootDir testing", "tool": "ts-node tool/cli.ts", diff --git a/server/index.ts b/server/index.ts index 2751bd9f4f1c..463585520b00 100644 --- a/server/index.ts +++ b/server/index.ts @@ -17,8 +17,12 @@ import { renderContributorsTxt, } from "../build/index.js"; import { findTranslations } from "../content/translations.js"; -import { Document, Redirect, Image } from "../content/index.js"; -import { CSP_VALUE, DEFAULT_LOCALE } from "../libs/constants/index.js"; +import { Document, Redirect, FileAttachment } from "../content/index.js"; +import { + ANY_ATTACHMENT_REGEXP, + CSP_VALUE, + DEFAULT_LOCALE, +} from "../libs/constants/index.js"; import { STATIC_ROOT, PROXY_HOSTNAME, @@ -292,14 +296,12 @@ app.get("/*", async (req, res, ...args) => { .sendFile(path.join(STATIC_ROOT, "en-us", "_spas", "404.html")); } - // TODO: Would be nice to have a list of all supported file extensions - // in a constants file. - if (/\.(png|webp|gif|jpe?g|svg)$/.test(req.path)) { - // Remember, Image.findByURLWithFallback() will return the absolute file path + if (ANY_ATTACHMENT_REGEXP.test(req.path)) { + // Remember, FileAttachment.findByURLWithFallback() will return the absolute file path // iff it exists on disk. // Using a "fallback" strategy here so that images embedded in live samples // are resolved if they exist in en-US but not in - const filePath = Image.findByURLWithFallback(req.path); + const filePath = FileAttachment.findByURLWithFallback(req.path); if (filePath) { // The second parameter to `send()` has to be either a full absolute // path or a path that doesn't start with `../` otherwise you'd diff --git a/server/react-app.d.ts b/server/react-app.d.ts index bf4ab9180050..ba232bc32748 100644 --- a/server/react-app.d.ts +++ b/server/react-app.d.ts @@ -30,16 +30,41 @@ declare module "*.jpeg" { export default src; } +declare module "*.mp3" { + const src: string; + export default src; +} + +declare module "*.mp4" { + const src: string; + export default src; +} + +declare module "*.ogg" { + const src: string; + export default src; +} + declare module "*.png" { const src: string; export default src; } +declare module "*.webm" { + const src: string; + export default src; +} + declare module "*.webp" { const src: string; export default src; } +declare module "*.woff2" { + const src: string; + export default src; +} + declare module "*.svg" { import * as React from "react"; diff --git a/ssr/react-app.d.ts b/ssr/react-app.d.ts index bf4ab9180050..ba232bc32748 100644 --- a/ssr/react-app.d.ts +++ b/ssr/react-app.d.ts @@ -30,16 +30,41 @@ declare module "*.jpeg" { export default src; } +declare module "*.mp3" { + const src: string; + export default src; +} + +declare module "*.mp4" { + const src: string; + export default src; +} + +declare module "*.ogg" { + const src: string; + export default src; +} + declare module "*.png" { const src: string; export default src; } +declare module "*.webm" { + const src: string; + export default src; +} + declare module "*.webp" { const src: string; export default src; } +declare module "*.woff2" { + const src: string; + export default src; +} + declare module "*.svg" { import * as React from "react";