diff --git a/.eslintrc.js b/.eslintrc.cjs similarity index 93% rename from .eslintrc.js rename to .eslintrc.cjs index 3ee18d5bcbd2..7fccfd4b81ad 100644 --- a/.eslintrc.js +++ b/.eslintrc.cjs @@ -1,5 +1,5 @@ -const { readGitignoreFiles } = require("eslint-gitignore"); const path = require("node:path"); +const { readGitignoreFiles } = require("eslint-gitignore"); const ignores = readGitignoreFiles({ cwd: path.join(".git", "info"), @@ -40,6 +40,12 @@ module.exports = { "warn", { ignoreRestSiblings: true }, ], + "n/no-extraneous-import": [ + "error", + { + allowModules: ["@jest/globals"], + }, + ], "n/no-missing-import": "off", "n/no-unpublished-import": "off", "n/shebang": "off", diff --git a/.nvmrc b/.nvmrc index 6f7f377bf514..8122c281de1d 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v16 +v16.19 diff --git a/build/build-options.ts b/build/build-options.ts index 7d0c6b8ee563..0c531c2a9f1c 100644 --- a/build/build-options.ts +++ b/build/build-options.ts @@ -1,7 +1,7 @@ import dotenv from "dotenv"; dotenv.config(); -import { FLAW_LEVELS, VALID_FLAW_CHECKS } from "../libs/constants"; +import { FLAW_LEVELS, VALID_FLAW_CHECKS } from "../libs/constants/index.js"; import { DEFAULT_FLAW_LEVELS, FILES, @@ -11,7 +11,7 @@ import { FIX_FLAWS_DRY_RUN, FIX_FLAWS_TYPES, FIX_FLAWS_VERBOSE, -} from "../libs/env"; +} from "../libs/env/index.js"; const options = Object.freeze({ flawLevels: parseFlawLevels(DEFAULT_FLAW_LEVELS), diff --git a/build/check-images.ts b/build/check-images.ts index 7e02fc1289a0..00651c017bf1 100644 --- a/build/check-images.ts +++ b/build/check-images.ts @@ -4,12 +4,13 @@ import path from "node:path"; -import sizeOf from "image-size"; +import imagesize from "image-size"; -import { Document, Image } from "../content"; -import { FLAW_LEVELS } from "../libs/constants"; -import { findMatchesInText } from "./matches-in-text"; -import { DEFAULT_LOCALE } from "../libs/constants"; +import { Document, Image } from "../content/index.js"; +import { FLAW_LEVELS, DEFAULT_LOCALE } from "../libs/constants/index.js"; +import { findMatchesInText } from "./matches-in-text.js"; + +const { default: sizeOf } = imagesize; /** * Mutate the `$` instance for image reference and if appropriate, diff --git a/build/cli.ts b/build/cli.ts index 7d500ab49ed5..fa58797c1ad0 100644 --- a/build/cli.ts +++ b/build/cli.ts @@ -6,22 +6,32 @@ import zlib from "node:zlib"; import chalk from "chalk"; import cliProgress from "cli-progress"; -import { program } from "@caporal/core"; -import { prompt } from "inquirer"; - -import { Document, slugToFolder, translationsOf } from "../content"; -import { CONTENT_ROOT, CONTENT_TRANSLATED_ROOT } from "../libs/env"; -import { VALID_LOCALES } from "../libs/constants"; +import caporal from "@caporal/core"; +import inquirer from "inquirer"; + +import { Document, slugToFolder, translationsOf } from "../content/index.js"; +import { + CONTENT_ROOT, + CONTENT_TRANSLATED_ROOT, + BUILD_OUT_ROOT, +} from "../libs/env/index.js"; +import { VALID_LOCALES } from "../libs/constants/index.js"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore -import { renderHTML } from "../ssr/dist/main"; -import options from "./build-options"; -import { buildDocument, BuiltDocument, renderContributorsTxt } from "."; -import { DocMetadata, Flaws } from "../libs/types"; -import SearchIndex from "./search-index"; -import { BUILD_OUT_ROOT } from "../libs/env"; -import { makeSitemapXML, makeSitemapIndexXML } from "./sitemaps"; -import { humanFileSize } from "./utils"; +import { renderHTML } from "../ssr/dist/main.js"; +import options from "./build-options.js"; +import { + buildDocument, + BuiltDocument, + renderContributorsTxt, +} from "./index.js"; +import { DocMetadata, Flaws } from "../libs/types/document.js"; +import SearchIndex from "./search-index.js"; +import { makeSitemapXML, makeSitemapIndexXML } from "./sitemaps.js"; +import { humanFileSize } from "./utils.js"; + +const { program } = caporal; +const { prompt } = inquirer; export type DocumentBuild = SkippedDocumentBuild | InteractiveDocumentBuild; diff --git a/build/document-utils.ts b/build/document-utils.ts index e707bea61d75..ed371c00b3b9 100644 --- a/build/document-utils.ts +++ b/build/document-utils.ts @@ -1,4 +1,4 @@ -import { Document } from "../content"; +import { Document } from "../content/index.js"; const TRANSFORM_STRINGS = new Map( Object.entries({ diff --git a/build/extract-sections.ts b/build/extract-sections.ts index a24d090f133c..be4f6f8a6d2b 100644 --- a/build/extract-sections.ts +++ b/build/extract-sections.ts @@ -1,6 +1,6 @@ import * as cheerio from "cheerio"; -import { ProseSection, Section } from "../libs/types"; -import { extractSpecifications } from "./extract-specifications"; +import { ProseSection, Section } from "../libs/types/document.js"; +import { extractSpecifications } from "./extract-specifications.js"; type SectionsAndFlaws = [Section[], string[]]; diff --git a/build/extract-sidebar.ts b/build/extract-sidebar.ts index 37774eef39d8..0ff8fa8871fa 100644 --- a/build/extract-sidebar.ts +++ b/build/extract-sidebar.ts @@ -1,5 +1,5 @@ import * as cheerio from "cheerio"; -import { Doc } from "../libs/types/document"; +import { Doc } from "../libs/types/document.js"; /** Extract and mutate the $ if it as a "Quick_links" section. * But only if it exists. diff --git a/build/extract-specifications.ts b/build/extract-specifications.ts index 3215db5f0448..479e8386269d 100644 --- a/build/extract-specifications.ts +++ b/build/extract-specifications.ts @@ -1,8 +1,8 @@ -import { packageBCD } from "./resolve-bcd"; -import * as bcd from "@mdn/browser-compat-data/types"; -import { Specification } from "../libs/types/document"; -import specs from "web-specs"; -import web from "../kumascript/src/api/web"; +import { packageBCD } from "./resolve-bcd.js"; +import bcd from "@mdn/browser-compat-data/types"; +import { Specification } from "../libs/types/document.js"; +import specs from "web-specs/index.json" assert { type: "json" }; +import web from "../kumascript/src/api/web.js"; export function extractSpecifications( query: string | undefined, diff --git a/build/extract-summary.ts b/build/extract-summary.ts index f35f3cf4f7d2..6bd2ab2cb143 100644 --- a/build/extract-summary.ts +++ b/build/extract-summary.ts @@ -1,5 +1,5 @@ import * as cheerio from "cheerio"; -import { ProseSection, Section } from "../libs/types/document"; +import { ProseSection, Section } from "../libs/types/document.js"; /** * Given an array of sections, return a plain text diff --git a/build/flaws/bad-bcd-queries.ts b/build/flaws/bad-bcd-queries.ts index 119257a561b1..d6e3d6f7312d 100644 --- a/build/flaws/bad-bcd-queries.ts +++ b/build/flaws/bad-bcd-queries.ts @@ -1,4 +1,4 @@ -import { packageBCD } from "../resolve-bcd"; +import { packageBCD } from "../resolve-bcd.js"; // Bad BCD queries are when the `
` tags have an // ID (or even lack the `id` attribute) that don't match anything in the diff --git a/build/flaws/broken-links.ts b/build/flaws/broken-links.ts index c7ed1cfa0d69..d104bb645c5e 100644 --- a/build/flaws/broken-links.ts +++ b/build/flaws/broken-links.ts @@ -1,19 +1,16 @@ import fs from "node:fs"; -import path from "node:path"; import fromMarkdown from "mdast-util-from-markdown"; import visit from "unist-util-visit"; -import { Document, Redirect, Image } from "../../content"; -import { findMatchesInText } from "../matches-in-text"; +import { Document, Redirect, Image } from "../../content/index.js"; +import { findMatchesInText } from "../matches-in-text.js"; import { DEFAULT_LOCALE, FLAW_LEVELS, VALID_LOCALES, -} from "../../libs/constants"; -import { isValidLocale } from "../../libs/locale-utils"; - -const dirname = __dirname; +} from "../../libs/constants/index.js"; +import { isValidLocale } from "../../libs/locale-utils/index.js"; function findMatchesInMarkdown(rawContent, href) { const matches = []; @@ -27,10 +24,14 @@ function findMatchesInMarkdown(rawContent, href) { } const _safeToHttpsDomains = new Map(); + function getSafeToHttpDomains() { if (!_safeToHttpsDomains.size) { const fileParsed = JSON.parse( - fs.readFileSync(path.join(dirname, "safe-to-https-domains.json"), "utf-8") + fs.readFileSync( + new URL("safe-to-https-domains.json", import.meta.url), + "utf-8" + ) ); Object.entries(fileParsed).forEach(([key, value]) => _safeToHttpsDomains.set(key, value) diff --git a/build/flaws/heading-links.ts b/build/flaws/heading-links.ts index 9002779a37e2..f9b03619a9d5 100644 --- a/build/flaws/heading-links.ts +++ b/build/flaws/heading-links.ts @@ -1,4 +1,4 @@ -import { findMatchesInText } from "../matches-in-text"; +import { findMatchesInText } from "../matches-in-text.js"; // You're not allowed to have `` elements inside `

` or `

` elements // because those will be rendered out as "links to themselves". diff --git a/build/flaws/index.ts b/build/flaws/index.ts index e1c48c6ec705..41f5fae78dc8 100644 --- a/build/flaws/index.ts +++ b/build/flaws/index.ts @@ -2,21 +2,24 @@ import path from "node:path"; import chalk from "chalk"; import { RequestError } from "got"; -import { Document } from "../../content"; -import { FLAW_LEVELS, VALID_FLAW_CHECKS } from "../../libs/constants"; -import { DEFAULT_LOCALE } from "../../libs/constants"; +import { Document } from "../../content/index.js"; +import { + FLAW_LEVELS, + VALID_FLAW_CHECKS, + DEFAULT_LOCALE, +} from "../../libs/constants/index.js"; import { replaceMatchesInText, replaceMatchingLinksInMarkdown, -} from "../matches-in-text"; -import { forceExternalURL, downloadAndResizeImage } from "../utils"; -import { getBadBCDQueriesFlaws } from "./bad-bcd-queries"; -import { getBrokenLinksFlaws } from "./broken-links"; -import { getHeadingLinksFlaws } from "./heading-links"; -import { getPreTagFlaws } from "./pre-tags"; -export { injectSectionFlaws } from "./sections"; -import { getUnsafeHTMLFlaws } from "./unsafe-html"; -import { injectTranslationDifferences } from "./translation-differences"; +} from "../matches-in-text.js"; +import { forceExternalURL, downloadAndResizeImage } from "../utils.js"; +import { getBadBCDQueriesFlaws } from "./bad-bcd-queries.js"; +import { getBrokenLinksFlaws } from "./broken-links.js"; +import { getHeadingLinksFlaws } from "./heading-links.js"; +import { getPreTagFlaws } from "./pre-tags.js"; +export { injectSectionFlaws } from "./sections.js"; +import { getUnsafeHTMLFlaws } from "./unsafe-html.js"; +import { injectTranslationDifferences } from "./translation-differences.js"; export interface Flaw { explanation: any; diff --git a/build/flaws/pre-tags.ts b/build/flaws/pre-tags.ts index 5f6d6e198aa9..d7cf274c39c7 100644 --- a/build/flaws/pre-tags.ts +++ b/build/flaws/pre-tags.ts @@ -1,6 +1,6 @@ -import { Flaw } from "."; +import { Flaw } from "./index.js"; -import { getFirstMatchInText } from "../matches-in-text"; +import { getFirstMatchInText } from "../matches-in-text.js"; const escapeHTML = (s) => s .replace(/&/g, "&") diff --git a/build/flaws/sections.ts b/build/flaws/sections.ts index 0c1f8b1a1116..3458323d3727 100644 --- a/build/flaws/sections.ts +++ b/build/flaws/sections.ts @@ -1,4 +1,4 @@ -import { FLAW_LEVELS } from "../../libs/constants"; +import { FLAW_LEVELS } from "../../libs/constants/index.js"; export function injectSectionFlaws(doc, flaws, options) { if (!flaws.length) { diff --git a/build/flaws/translation-differences.ts b/build/flaws/translation-differences.ts index 2960473400fe..b968bfb2bc0b 100644 --- a/build/flaws/translation-differences.ts +++ b/build/flaws/translation-differences.ts @@ -1,7 +1,7 @@ -import { Flaw } from "."; +import { Flaw } from "./index.js"; -import { Document, Translation } from "../../content"; -import { DEFAULT_LOCALE } from "../../libs/constants"; +import { Document, Translation } from "../../content/index.js"; +import { DEFAULT_LOCALE } from "../../libs/constants/index.js"; export function injectTranslationDifferences(doc, $, document): Flaw[] { const flaws = []; diff --git a/build/flaws/unsafe-html.ts b/build/flaws/unsafe-html.ts index 6ee2a58b41e9..6066b6bd0a2a 100644 --- a/build/flaws/unsafe-html.ts +++ b/build/flaws/unsafe-html.ts @@ -1,10 +1,10 @@ -import { Flaw } from "."; +import { Flaw } from "./index.js"; import { INTERACTIVE_EXAMPLES_BASE_URL, LIVE_SAMPLES_BASE_URL, -} from "../../libs/env"; -import { findMatchesInText } from "../matches-in-text"; +} from "../../libs/env/index.js"; +import { findMatchesInText } from "../matches-in-text.js"; const safeIFrameSrcs = [ // EmbedGHLiveSample.ejs diff --git a/build/git-history.ts b/build/git-history.ts index 76c72c933099..8386c43a16f7 100644 --- a/build/git-history.ts +++ b/build/git-history.ts @@ -1,8 +1,8 @@ import fs from "node:fs"; import path from "node:path"; -import { execGit } from "../content"; -import { CONTENT_ROOT } from "../libs/env"; +import { execGit } from "../content/index.js"; +import { CONTENT_ROOT } from "../libs/env/index.js"; function getFromGit(contentRoot = CONTENT_ROOT) { // If `contentRoot` was a symlink, the `repoRoot` won't be. That'll make it diff --git a/build/index.ts b/build/index.ts index 0a7f92b61b52..f8979f4e703c 100644 --- a/build/index.ts +++ b/build/index.ts @@ -1,4 +1,3 @@ -import { Doc } from "../libs/types"; import fs from "node:fs"; import path from "node:path"; @@ -7,29 +6,34 @@ import { MacroInvocationError, MacroLiveSampleError, MacroRedirectedLinkError, -} from "../kumascript/src/errors"; - -import { Document, Image, execGit } from "../content"; -import { CONTENT_ROOT, REPOSITORY_URLS } from "../libs/env"; -import * as kumascript from "../kumascript"; - -import { FLAW_LEVELS } from "../libs/constants"; -import { extractSections } from "./extract-sections"; -import { extractSidebar } from "./extract-sidebar"; -import { extractSummary } from "./extract-summary"; -export { default as SearchIndex } from "./search-index"; -import { addBreadcrumbData } from "./document-utils"; -import { fixFixableFlaws, injectFlaws, injectSectionFlaws } from "./flaws"; -import { checkImageReferences, checkImageWidths } from "./check-images"; -import { getPageTitle } from "./page-title"; -import { syntaxHighlight } from "./syntax-highlight"; -import { formatNotecards } from "./format-notecards"; -import buildOptions from "./build-options"; -export { gather as gatherGitHistory } from "./git-history"; -export { buildSPAs } from "./spas"; -import LANGUAGES_RAW from "../libs/languages"; -import { safeDecodeURIComponent } from "../kumascript/src/api/util"; -import { wrapTables } from "./wrap-tables"; +} from "../kumascript/src/errors.js"; + +import { Doc } from "../libs/types/document.js"; +import { Document, Image, execGit } from "../content/index.js"; +import { CONTENT_ROOT, REPOSITORY_URLS } from "../libs/env/index.js"; +import * as kumascript from "../kumascript/index.js"; + +import { FLAW_LEVELS } from "../libs/constants/index.js"; +import { extractSections } from "./extract-sections.js"; +import { extractSidebar } from "./extract-sidebar.js"; +import { extractSummary } from "./extract-summary.js"; +export { default as SearchIndex } from "./search-index.js"; +import { addBreadcrumbData } from "./document-utils.js"; +import { + fixFixableFlaws, + injectFlaws, + injectSectionFlaws, +} from "./flaws/index.js"; +import { checkImageReferences, checkImageWidths } from "./check-images.js"; +import { getPageTitle } from "./page-title.js"; +import { syntaxHighlight } from "./syntax-highlight.js"; +import { formatNotecards } from "./format-notecards.js"; +import buildOptions from "./build-options.js"; +export { gather as gatherGitHistory } from "./git-history.js"; +export { buildSPAs } from "./spas.js"; +import LANGUAGES_RAW from "../libs/languages/index.js"; +import { safeDecodeURIComponent } from "../kumascript/src/api/util.js"; +import { wrapTables } from "./wrap-tables.js"; const LANGUAGES = new Map( Object.entries(LANGUAGES_RAW).map(([locale, data]) => { diff --git a/build/page-title.ts b/build/page-title.ts index 5d665082d4fa..e555be5d5a9f 100644 --- a/build/page-title.ts +++ b/build/page-title.ts @@ -1,4 +1,4 @@ -import { Document } from "../content"; +import { Document } from "../content/index.js"; /** * Return the appropriate document title to go into the HTML diff --git a/build/resolve-bcd.ts b/build/resolve-bcd.ts index cc46dab04a7b..72357358446a 100644 --- a/build/resolve-bcd.ts +++ b/build/resolve-bcd.ts @@ -1,6 +1,7 @@ -// Note! This is copied verbatim from stumptown-content +import bcdUntyped from "@mdn/browser-compat-data/forLegacyNode"; +import { CompatData } from "@mdn/browser-compat-data/types"; -import bcd from "@mdn/browser-compat-data"; +const bcd = bcdUntyped as CompatData; export function packageBCD(query) { const data = query.split(".").reduce((prev, curr) => { diff --git a/build/search-index.ts b/build/search-index.ts index f5197728f779..c1495714bb37 100644 --- a/build/search-index.ts +++ b/build/search-index.ts @@ -1,4 +1,4 @@ -import { getPopularities } from "../content"; +import { getPopularities } from "../content/index.js"; // getPopularities() is memoized so it's fast to call repeatedly const getPopularity = (item) => getPopularities().get(item.url) || 0; diff --git a/build/spas.ts b/build/spas.ts index fb28bcc34ba7..dbb4c389dbe6 100644 --- a/build/spas.ts +++ b/build/spas.ts @@ -1,30 +1,31 @@ import fs from "node:fs"; import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import cheerio from "cheerio"; import frontmatter from "front-matter"; import { fdir, PathsOutput } from "fdir"; +import gotPkg from "got"; -import { m2h } from "../markdown"; +import { m2h } from "../markdown/index.js"; -import { VALID_LOCALES, MDN_PLUS_TITLE } from "../libs/constants"; +import { VALID_LOCALES, MDN_PLUS_TITLE } from "../libs/constants/index.js"; import { CONTENT_ROOT, CONTENT_TRANSLATED_ROOT, CONTRIBUTOR_SPOTLIGHT_ROOT, BUILD_OUT_ROOT, -} from "../libs/env"; -import { isValidLocale } from "../libs/locale-utils"; -import { DocFrontmatter } from "../libs/types/document"; +} from "../libs/env/index.js"; +import { isValidLocale } from "../libs/locale-utils/index.js"; +import { DocFrontmatter, NewsItem } from "../libs/types/document.js"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore -import { renderHTML } from "../ssr/dist/main"; -import got from "got"; -import { splitSections } from "./utils"; -import cheerio from "cheerio"; -import { findByURL } from "../content/document"; -import { buildDocument } from "."; -import { NewsItem } from "../client/src/homepage/latest-news"; +import { renderHTML } from "../ssr/dist/main.js"; +import { splitSections } from "./utils.js"; +import { findByURL } from "../content/document.js"; +import { buildDocument } from "./index.js"; -const dirname = __dirname; +const { default: got } = gotPkg; const FEATURED_ARTICLES = [ "Web/API/WebGL_API/Tutorial/Getting_started_with_WebGL", @@ -224,7 +225,7 @@ export async function buildSPAs(options) { } await buildStaticPages( - path.join(dirname, "../copy/plus/"), + fileURLToPath(new URL("../copy/plus/", import.meta.url)), "plus/docs", "MDN Plus" ); diff --git a/build/syntax-highlight.ts b/build/syntax-highlight.ts index 49b8c0f01e6a..a07770a4de54 100644 --- a/build/syntax-highlight.ts +++ b/build/syntax-highlight.ts @@ -1,5 +1,5 @@ import Prism from "prismjs"; -import loadLanguages from "prismjs/components/index"; +import loadLanguages from "prismjs/components/index.js"; const lazy = (creator) => { let res; diff --git a/build/tsconfig.json b/build/tsconfig.json deleted file mode 100644 index 379a994d81f6..000000000000 --- a/build/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../tsconfig.json", - "include": ["."] -} diff --git a/build/utils.ts b/build/utils.ts index 576e1c33990b..1a031755c495 100644 --- a/build/utils.ts +++ b/build/utils.ts @@ -2,16 +2,19 @@ import fs from "node:fs"; import path from "node:path"; import * as cheerio from "cheerio"; -import got from "got"; +import gotPkg from "got"; import FileType from "file-type"; import imagemin from "imagemin"; -import imageminPngquant from "imagemin-pngquant"; +import imageminPngquantPkg from "imagemin-pngquant"; import imageminMozjpeg from "imagemin-mozjpeg"; import imageminGifsicle from "imagemin-gifsicle"; import imageminSvgo from "imagemin-svgo"; import sanitizeFilename from "sanitize-filename"; -import { VALID_MIME_TYPES } from "../libs/constants"; +import { VALID_MIME_TYPES } from "../libs/constants/index.js"; + +const { default: got } = gotPkg; +const { default: imageminPngquant } = imageminPngquantPkg; export function humanFileSize(size) { if (size < 1024) return `${size} B`; diff --git a/client/config/env.js b/client/config/env.js index 703cf6fbadf8..81e658551002 100644 --- a/client/config/env.js +++ b/client/config/env.js @@ -1,10 +1,9 @@ -const fs = require("fs"); -const path = require("path"); -const paths = require("./paths"); - -// Make sure that including paths.js after env.js will read .env variables. -delete require.cache[require.resolve("./paths")]; +import fs from "node:fs"; +import path from "node:path"; +import dotenv from "dotenv"; +import paths from "./paths.js"; +// Double-check to make sure NODE_ENV is defined const NODE_ENV = process.env.NODE_ENV; if (!NODE_ENV) { throw new Error( @@ -17,7 +16,7 @@ const dotenvFile = ENV_FILE ? paths.dotenv.replace(".env", ENV_FILE) : paths.dotenv; -require("dotenv").config({ +dotenv.config({ path: dotenvFile, }); @@ -82,4 +81,4 @@ function getClientEnvironment(publicUrl) { return { raw, stringified }; } -module.exports = getClientEnvironment; +export default getClientEnvironment; diff --git a/client/config/getHttpsConfig.js b/client/config/getHttpsConfig.js index 5706e1b30fc0..fdfa66898605 100644 --- a/client/config/getHttpsConfig.js +++ b/client/config/getHttpsConfig.js @@ -1,8 +1,8 @@ -const fs = require("fs"); -const path = require("path"); -const crypto = require("crypto"); -const chalk = require("react-dev-utils/chalk"); -const paths = require("./paths"); +import fs from "node:fs"; +import path from "node:path"; +import crypto from "node:crypto"; +import chalk from "react-dev-utils/chalk.js"; +import paths from "./paths.js"; // Ensure the certificate and key provided are valid and if not // throw an easy to debug error @@ -61,4 +61,4 @@ function getHttpsConfig() { return isHttps; } -module.exports = getHttpsConfig; +export default getHttpsConfig; diff --git a/client/config/jest/babelTransform.js b/client/config/jest/babelTransform.js index 9709898f82b7..1597d57e3002 100644 --- a/client/config/jest/babelTransform.js +++ b/client/config/jest/babelTransform.js @@ -1,4 +1,7 @@ -const babelJest = require("babel-jest").default; +import { createRequire } from "node:module"; +import babelJest from "babel-jest"; + +const require = createRequire(import.meta.url); const hasJsxRuntime = (() => { if (process.env.DISABLE_NEW_JSX_TRANSFORM === "true") { @@ -13,7 +16,7 @@ const hasJsxRuntime = (() => { } })(); -module.exports = babelJest.createTransformer({ +export default babelJest.createTransformer({ presets: [ [ require.resolve("babel-preset-react-app"), diff --git a/client/config/jest/cssTransform.js b/client/config/jest/cssTransform.js index fc14ad7a9a18..03f7979f4511 100644 --- a/client/config/jest/cssTransform.js +++ b/client/config/jest/cssTransform.js @@ -1,7 +1,7 @@ // This is a custom Jest transformer turning style imports into empty objects. // http://facebook.github.io/jest/docs/en/webpack.html -module.exports = { +const transform = { process() { return { code: "module.exports = {};" }; }, @@ -10,3 +10,5 @@ module.exports = { return "cssTransform"; }, }; + +export default transform; diff --git a/client/config/jest/fileTransform.js b/client/config/jest/fileTransform.js index d678aa986b39..5bc7c6302acb 100644 --- a/client/config/jest/fileTransform.js +++ b/client/config/jest/fileTransform.js @@ -1,10 +1,10 @@ -const path = require("path"); -const camelcase = require("camelcase"); +import path from "node:path"; +import camelcase from "camelcase"; // This is a custom Jest transformer turning file imports into filenames. // http://facebook.github.io/jest/docs/en/webpack.html -module.exports = { +const transform = { process(src, filename) { const assetFilename = JSON.stringify(path.basename(filename)); @@ -38,3 +38,5 @@ module.exports = { return { code: `module.exports = ${assetFilename};` }; }, }; + +export default transform; diff --git a/client/config/modules.js b/client/config/modules.js index 44e8f2bbe499..10f693813941 100644 --- a/client/config/modules.js +++ b/client/config/modules.js @@ -1,8 +1,8 @@ -const fs = require("fs"); -const path = require("path"); -const paths = require("./paths"); -const chalk = require("react-dev-utils/chalk"); -const resolve = require("resolve"); +import fs from "node:fs"; +import path from "node:path"; +import resolve from "resolve"; +import chalk from "react-dev-utils/chalk.js"; +import paths from "./paths.js"; /** * Get additional module paths based on the baseUrl of a compilerOptions object. @@ -89,7 +89,7 @@ function getJestAliases(options = {}) { } } -function getModules() { +async function getModules() { // Check if TypeScript is setup const hasTsConfig = fs.existsSync(paths.appTsConfig); const hasJsConfig = fs.existsSync(paths.appJsConfig); @@ -106,14 +106,17 @@ function getModules() { // TypeScript project and set up the config // based on tsconfig.json if (hasTsConfig) { - const ts = require(resolve.sync("typescript", { - basedir: paths.appNodeModules, - })); + const { default: ts } = await import( + "file://" + + resolve.sync("typescript", { + basedir: paths.appNodeModules, + }) + ); config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config; // Otherwise we'll check if there is jsconfig.json // for non TS projects. } else if (hasJsConfig) { - config = require(paths.appJsConfig); + config = await import("file://" + paths.appJsConfig).default; } config = config || {}; @@ -129,4 +132,4 @@ function getModules() { }; } -module.exports = getModules(); +export default getModules(); diff --git a/client/config/paths.js b/client/config/paths.js index 81f9a522b13d..e240d3803ba2 100644 --- a/client/config/paths.js +++ b/client/config/paths.js @@ -1,11 +1,14 @@ -const path = require("path"); -const fs = require("fs"); -const getPublicUrlOrPath = require("react-dev-utils/getPublicUrlOrPath"); +import fs from "node:fs"; +import { fileURLToPath } from "node:url"; +import getPublicUrlOrPath from "react-dev-utils/getPublicUrlOrPath.js"; // Make sure any symlinks in the project folder are resolved: // https://github.com/facebook/create-react-app/issues/637 -const appDirectory = fs.realpathSync(process.cwd()); -const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath); +const appDirectory = new URL("..", import.meta.url); +const resolveApp = (relativePath) => + fileURLToPath(new URL(relativePath, appDirectory)); + +const appPackage = JSON.parse(fs.readFileSync(resolveApp("package.json"))); // We use `PUBLIC_URL` environment variable or "homepage" field to infer // "public path" at which the app is served. @@ -15,7 +18,7 @@ const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath); // like /todos/42/static/js/bundle.7289d.js. We have to know the root. const publicUrlOrPath = getPublicUrlOrPath( process.env.NODE_ENV === "development", - require(resolveApp("package.json")).homepage, + appPackage.homepage, process.env.PUBLIC_URL ); @@ -49,7 +52,7 @@ const resolveModule = (resolveFn, filePath) => { }; // config after eject: we're in ./config/ -module.exports = { +const config = { dotenv: resolveApp("../.env"), appPath: resolveApp("."), appBuild: resolveApp(buildPath), @@ -69,6 +72,7 @@ module.exports = { swSrc: resolveModule(resolveApp, "src/service-worker"), publicUrlOrPath, libsPath: resolveApp("../libs"), + moduleFileExtensions, }; -module.exports.moduleFileExtensions = moduleFileExtensions; +export default config; diff --git a/client/config/webpack.config.js b/client/config/webpack.config.js index 46952f238979..f7ef4d6f729b 100644 --- a/client/config/webpack.config.js +++ b/client/config/webpack.config.js @@ -1,44 +1,48 @@ -const fs = require("fs"); -const path = require("path"); -const webpack = require("webpack"); -const resolve = require("resolve"); -const HtmlWebpackPlugin = require("html-webpack-plugin"); -const CaseSensitivePathsPlugin = require("case-sensitive-paths-webpack-plugin"); -const InlineChunkHtmlPlugin = require("react-dev-utils/InlineChunkHtmlPlugin"); -const TerserPlugin = require("terser-webpack-plugin"); -const MiniCssExtractPlugin = require("mini-css-extract-plugin"); -const CssMinimizerPlugin = require("css-minimizer-webpack-plugin"); -const { WebpackManifestPlugin } = require("webpack-manifest-plugin"); -const InterpolateHtmlPlugin = require("react-dev-utils/InterpolateHtmlPlugin"); -const WorkboxWebpackPlugin = require("workbox-webpack-plugin"); -const ModuleScopePlugin = require("react-dev-utils/ModuleScopePlugin"); -const getCSSModuleLocalIdent = require("react-dev-utils/getCSSModuleLocalIdent"); -const ESLintPlugin = require("eslint-webpack-plugin"); -const paths = require("./paths"); -const modules = require("./modules"); -const getClientEnvironment = require("./env"); -const ModuleNotFoundPlugin = require("react-dev-utils/ModuleNotFoundPlugin"); -const ForkTsCheckerWebpackPlugin = - process.env.TSC_COMPILE_ON_ERROR === "true" - ? require("react-dev-utils/ForkTsCheckerWarningWebpackPlugin") - : require("react-dev-utils/ForkTsCheckerWebpackPlugin"); -const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin"); +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import resolve from "resolve"; +import webpack from "webpack"; +import HtmlWebpackPlugin from "html-webpack-plugin"; +import CaseSensitivePathsPlugin from "case-sensitive-paths-webpack-plugin"; +import InlineChunkHtmlPlugin from "react-dev-utils/InlineChunkHtmlPlugin.js"; +import TerserPlugin from "terser-webpack-plugin"; +import MiniCssExtractPlugin from "mini-css-extract-plugin"; +import CssMinimizerPlugin from "css-minimizer-webpack-plugin"; +import { WebpackManifestPlugin } from "webpack-manifest-plugin"; +import InterpolateHtmlPlugin from "react-dev-utils/InterpolateHtmlPlugin.js"; +import WorkboxWebpackPlugin from "workbox-webpack-plugin"; +import ModuleScopePlugin from "react-dev-utils/ModuleScopePlugin.js"; +import getCSSModuleLocalIdent from "react-dev-utils/getCSSModuleLocalIdent.js"; +import ESLintPlugin from "eslint-webpack-plugin"; +import ModuleNotFoundPlugin from "react-dev-utils/ModuleNotFoundPlugin.js"; +import ReactRefreshWebpackPlugin from "@pmmmwh/react-refresh-webpack-plugin"; -const createEnvironmentHash = require("./webpack/persistentCache/createEnvironmentHash"); +import paths from "./paths.js"; +import modules from "./modules.js"; +import getClientEnvironment from "./env.js"; +import createEnvironmentHash from "./webpack/persistentCache/createEnvironmentHash.js"; + +const { default: ForkTsCheckerWebpackPlugin } = await import( + process.env.TSC_COMPILE_ON_ERROR === "true" + ? "react-dev-utils/ForkTsCheckerWarningWebpackPlugin.js" + : "react-dev-utils/ForkTsCheckerWebpackPlugin.js" +); // Source maps are resource heavy and can cause out of memory issue for large source files. const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== "false"; -const reactRefreshRuntimeEntry = require.resolve("react-refresh/runtime"); -const reactRefreshWebpackPluginRuntimeEntry = require.resolve( +const reactRefreshRuntimeEntry = resolve.sync("react-refresh/runtime"); +const reactRefreshWebpackPluginRuntimeEntry = resolve.sync( "@pmmmwh/react-refresh-webpack-plugin" ); -const babelRuntimeEntry = require.resolve("babel-preset-react-app"); -const babelRuntimeEntryHelpers = require.resolve( +const babelRuntimeEntry = resolve.sync("babel-preset-react-app"); +const babelRuntimeEntryHelpers = resolve.sync( "@babel/runtime/helpers/esm/assertThisInitialized", { paths: [babelRuntimeEntry] } ); -const babelRuntimeRegenerator = require.resolve("@babel/runtime/regenerator", { +const babelRuntimeRegenerator = resolve.sync("@babel/runtime/regenerator", { paths: [babelRuntimeEntry], }); @@ -76,7 +80,7 @@ const hasJsxRuntime = (() => { } try { - require.resolve("react/jsx-runtime"); + resolve.sync("react/jsx-runtime"); return true; } catch (e) { return false; @@ -85,7 +89,7 @@ const hasJsxRuntime = (() => { // This is the production and development configuration. // It is focused on developer experience, fast rebuilds, and a minimal bundle. -module.exports = function (webpackEnv) { +function config(webpackEnv) { const isEnvDevelopment = webpackEnv === "development"; const isEnvProduction = webpackEnv === "production"; @@ -105,7 +109,7 @@ module.exports = function (webpackEnv) { // common function to get style loaders const getStyleLoaders = (cssOptions, preProcessor) => { const loaders = [ - isEnvDevelopment && require.resolve("style-loader"), + isEnvDevelopment && resolve.sync("style-loader"), isEnvProduction && { loader: MiniCssExtractPlugin.loader, // css is located in `static/css`, use '../../' to locate index.html folder @@ -115,14 +119,14 @@ module.exports = function (webpackEnv) { : {}, }, { - loader: require.resolve("css-loader"), + loader: resolve.sync("css-loader"), options: cssOptions, }, { // Options for PostCSS as we reference these options twice // Adds vendor prefixing based on your specified browser support in // package.json - loader: require.resolve("postcss-loader"), + loader: resolve.sync("postcss-loader"), options: { postcssOptions: { // Necessary for external CSS imports to work @@ -167,14 +171,14 @@ module.exports = function (webpackEnv) { if (preProcessor) { loaders.push( { - loader: require.resolve("resolve-url-loader"), + loader: resolve.sync("resolve-url-loader"), options: { sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment, root: paths.appSrc, }, }, { - loader: require.resolve(preProcessor), + loader: resolve.sync(preProcessor), options: { sourceMap: true, }, @@ -235,7 +239,7 @@ module.exports = function (webpackEnv) { store: "pack", buildDependencies: { defaultWebpack: ["webpack/lib/"], - config: [__filename], + config: [fileURLToPath(import.meta.url)], tsconfig: [paths.appTsConfig, paths.appJsConfig].filter((f) => fs.existsSync(f) ), @@ -344,7 +348,7 @@ module.exports = function (webpackEnv) { enforce: "pre", exclude: /@babel(?:\/|\\{1,2})runtime/, test: /\.(js|mjs|jsx|ts|tsx|css)$/, - loader: require.resolve("source-map-loader"), + loader: resolve.sync("source-map-loader"), }, { // "oneOf" will traverse all following loaders until one will @@ -379,7 +383,7 @@ module.exports = function (webpackEnv) { test: /\.svg$/, use: [ { - loader: require.resolve("@svgr/webpack"), + loader: resolve.sync("@svgr/webpack"), options: { prettier: false, svgo: false, @@ -391,7 +395,7 @@ module.exports = function (webpackEnv) { }, }, { - loader: require.resolve("file-loader"), + loader: resolve.sync("file-loader"), options: { name: "static/media/[name].[hash].[ext]", }, @@ -406,14 +410,14 @@ module.exports = function (webpackEnv) { { test: /\.(js|mjs|jsx|ts|tsx)$/, include: paths.appSrc, - loader: require.resolve("babel-loader"), + loader: resolve.sync("babel-loader"), options: { - customize: require.resolve( + customize: resolve.sync( "babel-preset-react-app/webpack-overrides" ), presets: [ [ - require.resolve("babel-preset-react-app"), + resolve.sync("babel-preset-react-app"), { runtime: hasJsxRuntime ? "automatic" : "classic", }, @@ -423,7 +427,8 @@ module.exports = function (webpackEnv) { plugins: [ isEnvDevelopment && shouldUseReactRefresh && - require.resolve("react-refresh/babel"), + resolve.sync("react-refresh/babel"), + resolve.sync("@babel/plugin-syntax-import-assertions"), ].filter(Boolean), // This is a feature of `babel-loader` for webpack (not Babel itself). // It enables caching results in ./node_modules/.cache/babel-loader/ @@ -439,14 +444,14 @@ module.exports = function (webpackEnv) { { test: /\.(js|mjs)$/, exclude: /@babel(?:\/|\\{1,2})runtime/, - loader: require.resolve("babel-loader"), + loader: resolve.sync("babel-loader"), options: { babelrc: false, configFile: false, compact: false, presets: [ [ - require.resolve("babel-preset-react-app/dependencies"), + resolve.sync("babel-preset-react-app/dependencies"), { helpers: true }, ], ], @@ -726,8 +731,8 @@ module.exports = function (webpackEnv) { new ESLintPlugin({ // Plugin options extensions: ["js", "mjs", "jsx", "ts", "tsx"], - formatter: require.resolve("react-dev-utils/eslintFormatter"), - eslintPath: require.resolve("eslint"), + formatter: resolve.sync("react-dev-utils/eslintFormatter.js"), + eslintPath: resolve.sync("eslint"), failOnError: !(isEnvDevelopment && emitErrorsAsWarnings), context: paths.appSrc, cache: true, @@ -737,9 +742,11 @@ module.exports = function (webpackEnv) { ), // ESLint class options cwd: paths.appPath, - resolvePluginsRelativeTo: __dirname, + resolvePluginsRelativeTo: fileURLToPath( + new URL(".", import.meta.url) + ), baseConfig: { - extends: [require.resolve("eslint-config-react-app/base")], + extends: [resolve.sync("eslint-config-react-app/base")], rules: { ...(!hasJsxRuntime && { "react/react-in-jsx-scope": "error", @@ -752,4 +759,6 @@ module.exports = function (webpackEnv) { // our own hints via the FileSizeReporter performance: false, }; -}; +} + +export default config; diff --git a/client/config/webpack/persistentCache/createEnvironmentHash.js b/client/config/webpack/persistentCache/createEnvironmentHash.js index 0c20d189f6c1..62d622389a1d 100644 --- a/client/config/webpack/persistentCache/createEnvironmentHash.js +++ b/client/config/webpack/persistentCache/createEnvironmentHash.js @@ -1,8 +1,10 @@ -const { createHash } = require("crypto"); +import { createHash } from "node:crypto"; -module.exports = (env) => { +const createEnvironmentHash = (env) => { const hash = createHash("md5"); hash.update(JSON.stringify(env)); return hash.digest("hex"); }; + +export default createEnvironmentHash; diff --git a/client/config/webpackDevServer.config.js b/client/config/webpackDevServer.config.js index 3537d7314d86..dd7361dae8ec 100644 --- a/client/config/webpackDevServer.config.js +++ b/client/config/webpackDevServer.config.js @@ -1,17 +1,22 @@ -const fs = require("fs"); -const evalSourceMapMiddleware = require("react-dev-utils/evalSourceMapMiddleware"); -const noopServiceWorkerMiddleware = require("react-dev-utils/noopServiceWorkerMiddleware"); -const ignoredFiles = require("react-dev-utils/ignoredFiles"); -const redirectServedPath = require("react-dev-utils/redirectServedPathMiddleware"); -const paths = require("./paths"); -const getHttpsConfig = require("./getHttpsConfig"); +import fs from "node:fs"; +import evalSourceMapMiddleware from "react-dev-utils/evalSourceMapMiddleware.js"; +import noopServiceWorkerMiddleware from "react-dev-utils/noopServiceWorkerMiddleware.js"; +import ignoredFiles from "react-dev-utils/ignoredFiles.js"; +import redirectServedPath from "react-dev-utils/redirectServedPathMiddleware.js"; + +import paths from "./paths.js"; +import getHttpsConfig from "./getHttpsConfig.js"; const host = process.env.HOST || "0.0.0.0"; const sockHost = process.env.WDS_SOCKET_HOST; const sockPath = process.env.WDS_SOCKET_PATH; // default: '/ws' const sockPort = process.env.WDS_SOCKET_PORT; -module.exports = function (proxy, allowedHost) { +const proxySetup = fs.existsSync(paths.proxySetup) + ? (await import("file://" + paths.proxySetup)).default + : null; + +function config(proxy, allowedHost) { const disableFirewall = !proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === "true"; return { @@ -107,9 +112,9 @@ module.exports = function (proxy, allowedHost) { evalSourceMapMiddleware(devServer) ); - if (fs.existsSync(paths.proxySetup)) { + if (proxySetup) { // This registers user provided middleware for proxy reasons - require(paths.proxySetup)(devServer.app); + proxySetup(devServer.app); } middlewares.push( @@ -127,4 +132,6 @@ module.exports = function (proxy, allowedHost) { return middlewares; }, }; -}; +} + +export default config; diff --git a/client/package.json b/client/package.json index 6471c9f20cfb..8b4daa5191e8 100644 --- a/client/package.json +++ b/client/package.json @@ -2,6 +2,7 @@ "name": "client", "private": false, "license": "MPL-2.0", + "type": "module", "babel": { "presets": [ "react-app" diff --git a/client/pwa/package.json b/client/pwa/package.json index c05558c857fd..4aa36277efbe 100644 --- a/client/pwa/package.json +++ b/client/pwa/package.json @@ -4,6 +4,7 @@ "description": "MDN Offline service worker", "license": "MPL-2.0", "author": "MDN Web Docs", + "type": "module", "main": "service-worker.ts", "scripts": { "build": "webpack", diff --git a/client/pwa/src/db.ts b/client/pwa/src/db.ts index ee15e79fbc4a..eb8031511e32 100644 --- a/client/pwa/src/db.ts +++ b/client/pwa/src/db.ts @@ -85,9 +85,9 @@ export class MDNOfflineDB extends Dexie { } } -const offlineDb = new MDNOfflineDB(); +export const offlineDb = new MDNOfflineDB(); -async function getContentStatus(): Promise<ContentStatus> { +export async function getContentStatus(): Promise<ContentStatus> { const current = await offlineDb.contentStatusHistory.toCollection().last(); return ( @@ -101,7 +101,7 @@ async function getContentStatus(): Promise<ContentStatus> { ); } -async function patchContentStatus( +export async function patchContentStatus( changes: Omit<Partial<ContentStatus>, "id" | "timestamp"> ) { const db = offlineDb; @@ -129,5 +129,3 @@ async function patchContentStatus( } }); } - -export { offlineDb, getContentStatus, patchContentStatus }; diff --git a/client/pwa/src/fetch-interceptors.ts b/client/pwa/src/fetch-interceptors.ts index 0bc1e0a3161c..fb915dcd1913 100644 --- a/client/pwa/src/fetch-interceptors.ts +++ b/client/pwa/src/fetch-interceptors.ts @@ -1,4 +1,4 @@ -import { MDNOfflineDB } from "./db"; +import { MDNOfflineDB } from "./db.js"; const PATH_WHOAMI = "/api/v1/whoami"; @@ -15,7 +15,7 @@ function jsonBlob(json) { }); } -class WhoamiInterceptor implements FetchInterceptor { +export class WhoamiInterceptor implements FetchInterceptor { db: MDNOfflineDB; constructor(db: MDNOfflineDB) { @@ -44,7 +44,7 @@ class WhoamiInterceptor implements FetchInterceptor { } } -class DefaultApiInterceptor implements FetchInterceptor { +export class DefaultApiInterceptor implements FetchInterceptor { db: MDNOfflineDB; constructor(db: MDNOfflineDB) { @@ -76,5 +76,3 @@ class DefaultApiInterceptor implements FetchInterceptor { } } } - -export { WhoamiInterceptor, DefaultApiInterceptor }; diff --git a/client/pwa/src/fetcher.ts b/client/pwa/src/fetcher.ts index df9b9af035ad..71f3f8117136 100644 --- a/client/pwa/src/fetcher.ts +++ b/client/pwa/src/fetcher.ts @@ -1,9 +1,14 @@ /* eslint no-restricted-globals: ["off", "self"] */ -import { WhoamiInterceptor, DefaultApiInterceptor } from "./fetch-interceptors"; -import { offlineDb } from "./db"; -import { INTERACTIVE_EXAMPLES_URL } from "./service-worker"; -import { USER_CONTENT_URL } from "./service-worker"; -import { openCache } from "./caches"; +import { + WhoamiInterceptor, + DefaultApiInterceptor, +} from "./fetch-interceptors.js"; +import { offlineDb } from "./db.js"; +import { + INTERACTIVE_EXAMPLES_URL, + USER_CONTENT_URL, +} from "./service-worker.js"; +import { openCache } from "./caches.js"; let interceptors = [new WhoamiInterceptor(offlineDb)]; diff --git a/client/pwa/src/service-worker.ts b/client/pwa/src/service-worker.ts index 0a8a12e890c0..41dfc42d586a 100644 --- a/client/pwa/src/service-worker.ts +++ b/client/pwa/src/service-worker.ts @@ -1,17 +1,17 @@ /* eslint no-restricted-globals: ["off", "location"] */ /// <reference lib="WebWorker" /> -import { cacheName, contentCache, openCache } from "./caches"; -import { respond } from "./fetcher"; -import { unpackAndCache } from "./unpack-cache"; +import { cacheName, contentCache, openCache } from "./caches.js"; +import { respond } from "./fetcher.js"; +import { unpackAndCache } from "./unpack-cache.js"; import { ContentStatusPhase, getContentStatus, patchContentStatus, RemoteContentStatus, SwType, -} from "./db"; -import { fetchWithExampleOverride } from "./fetcher"; +} from "./db.js"; +import { fetchWithExampleOverride } from "./fetcher.js"; export const INTERACTIVE_EXAMPLES_URL = new URL( "https://interactive-examples.mdn.mozilla.net" diff --git a/client/pwa/src/unpack-cache.ts b/client/pwa/src/unpack-cache.ts index a80845c310f9..1232eef35df4 100644 --- a/client/pwa/src/unpack-cache.ts +++ b/client/pwa/src/unpack-cache.ts @@ -1,6 +1,6 @@ /* eslint no-restricted-globals: ["off", "self"] */ import * as zip from "@zip.js/zip.js"; -import { openContentCache } from "./caches"; +import { openContentCache } from "./caches.js"; zip.configure({ useWebWorkers: false, diff --git a/client/pwa/tsconfig.json b/client/pwa/tsconfig.json index 398b3c0c6da2..1f3d0ed8ddf0 100644 --- a/client/pwa/tsconfig.json +++ b/client/pwa/tsconfig.json @@ -1,15 +1,12 @@ { + "extends": "../tsconfig.json", "compilerOptions": { - "module": "ES2022", - "target": "ES2021", - "lib": ["es2021", "webworker"], - "jsx": "react", - "noImplicitAny": false, - "sourceMap": true, + "lib": ["dom", "dom.iterable", "es2021", "webworker"], + "noEmit": false, "preserveConstEnums": true, - "outDir": "../public", - "skipLibCheck": true, - "moduleResolution": "node" + "strictNullChecks": false, + "target": "ES2021" }, - "include": ["src/*.ts", "src/**/*.ts", "src/*.tsx", "src/**/*.tsx"] + "include": ["src"], + "exclude": [] } diff --git a/client/pwa/webpack.config.js b/client/pwa/webpack.config.js index 4b8889dafe8c..33c7f7fa4a4d 100644 --- a/client/pwa/webpack.config.js +++ b/client/pwa/webpack.config.js @@ -1,20 +1,17 @@ -const path = require("path"); -const commitHash = require("child_process") - .execSync("git rev-parse --short HEAD") - .toString() - .trim(); -const webpack = require("webpack"); +import { fileURLToPath } from "node:url"; +import { execSync } from "node:child_process"; +import webpack from "webpack"; -const dirname = __dirname; +const commitHash = execSync("git rev-parse --short HEAD").toString().trim(); -module.exports = { +const config = { entry: { - bundle: path.join(dirname, "./src/service-worker.ts"), + bundle: "./src/service-worker.ts", }, output: { filename: "service-worker.js", - path: path.join(dirname, "../public/"), + path: fileURLToPath(new URL("../public/", import.meta.url)), }, mode: "development", @@ -32,6 +29,9 @@ module.exports = { resolve: { extensions: [".ts", ".tsx", ".js", ".json"], + extensionAlias: { + ".js": [".ts", ".js"], + }, plugins: [], }, // plugins: [new webpack.optimize.ModuleConcatenationPlugin()], @@ -44,3 +44,5 @@ module.exports = { ], }, }; + +export default config; diff --git a/client/pwa/webpack.production.config.js b/client/pwa/webpack.production.config.js index 71ba13c73e7e..036b71fc6248 100644 --- a/client/pwa/webpack.production.config.js +++ b/client/pwa/webpack.production.config.js @@ -1,20 +1,17 @@ -const path = require("path"); -const commitHash = require("child_process") - .execSync("git rev-parse --short HEAD") - .toString() - .trim(); -const webpack = require("webpack"); +import { fileURLToPath } from "node:url"; +import { execSync } from "node:child_process"; +import webpack from "webpack"; -const dirname = __dirname; +const commitHash = execSync("git rev-parse --short HEAD").toString().trim(); -module.exports = { +const config = { entry: { - bundle: path.join(dirname, "./src/service-worker.ts"), + bundle: "./src/service-worker.ts", }, output: { filename: "service-worker.js", - path: path.join(dirname, "../public/"), + path: fileURLToPath(new URL("../public/", import.meta.url)), }, mode: "production", @@ -31,6 +28,9 @@ module.exports = { resolve: { extensions: [".ts", ".tsx", ".js", ".json"], + extensionAlias: { + ".js": [".ts", ".js"], + }, plugins: [], }, @@ -43,3 +43,5 @@ module.exports = { ], }, }; + +export default config; diff --git a/client/scripts/build.js b/client/scripts/build.js index 0dbadefe8092..0f4d6b6f8473 100644 --- a/client/scripts/build.js +++ b/client/scripts/build.js @@ -1,6 +1,20 @@ -// Do this as the first thing so that any code reading it knows the right env. -process.env.BABEL_ENV = "production"; -process.env.NODE_ENV = "production"; +// Ensure environment variables are read. +import "../config/env.js"; + +import path from "node:path"; +import chalk from "react-dev-utils/chalk.js"; +import fs from "fs-extra"; +import bfj from "bfj"; +import webpack from "webpack"; +import { checkBrowsers } from "react-dev-utils/browsersHelper.js"; +import checkRequiredFiles from "react-dev-utils/checkRequiredFiles.js"; +import formatWebpackMessages from "react-dev-utils/formatWebpackMessages.js"; +import printHostingInstructions from "react-dev-utils/printHostingInstructions.js"; +import FileSizeReporter from "react-dev-utils/FileSizeReporter.js"; +import printBuildError from "react-dev-utils/printBuildError.js"; + +import configFactory from "../config/webpack.config.js"; +import paths from "../config/paths.js"; // Makes the script crash on unhandled rejections instead of silently // ignoring them. In the future, promise rejections that are not handled will @@ -9,21 +23,7 @@ process.on("unhandledRejection", (err) => { throw err; }); -// Ensure environment variables are read. -require("../config/env"); - -const path = require("path"); -const chalk = require("react-dev-utils/chalk"); -const fs = require("fs-extra"); -const bfj = require("bfj"); -const webpack = require("webpack"); -const configFactory = require("../config/webpack.config"); -const paths = require("../config/paths"); -const checkRequiredFiles = require("react-dev-utils/checkRequiredFiles"); -const formatWebpackMessages = require("react-dev-utils/formatWebpackMessages"); -const printHostingInstructions = require("react-dev-utils/printHostingInstructions"); -const FileSizeReporter = require("react-dev-utils/FileSizeReporter"); -const printBuildError = require("react-dev-utils/printBuildError"); +const appPackage = JSON.parse(fs.readFileSync(paths.appPackageJson)); const measureFileSizesBeforeBuild = FileSizeReporter.measureFileSizesBeforeBuild; @@ -49,7 +49,6 @@ const config = configFactory("production"); // We require that you explicitly set browsers and do not fall back to // browserslist defaults. -const { checkBrowsers } = require("react-dev-utils/browsersHelper"); checkBrowsers(paths.appPath, isInteractive) .then(() => { // First, read the current file sizes in build directory. @@ -94,7 +93,6 @@ checkBrowsers(paths.appPath, isInteractive) ); console.log(); - const appPackage = require(paths.appPackageJson); const publicUrl = paths.publicUrlOrPath; const publicPath = config.output.publicPath; const buildFolder = path.relative(process.cwd(), paths.appBuild); diff --git a/client/scripts/start.js b/client/scripts/start.js index 637fea7a9184..b322ca708ce6 100644 --- a/client/scripts/start.js +++ b/client/scripts/start.js @@ -1,6 +1,27 @@ -// Do this as the first thing so that any code reading it knows the right env. -process.env.BABEL_ENV = "development"; -process.env.NODE_ENV = "development"; +// Ensure environment variables are read. +import "../config/env.js"; + +import fs from "node:fs"; +import chalk from "react-dev-utils/chalk.js"; +import webpack from "webpack"; +import WebpackDevServer from "webpack-dev-server"; +import clearConsole from "react-dev-utils/clearConsole.js"; +import checkRequiredFiles from "react-dev-utils/checkRequiredFiles.js"; +import { checkBrowsers } from "react-dev-utils/browsersHelper.js"; +import { + choosePort, + createCompiler, + prepareProxy, + prepareUrls, +} from "react-dev-utils/WebpackDevServerUtils.js"; +import openBrowser from "react-dev-utils/openBrowser.js"; +import semver from "semver"; +import react from "react"; + +import paths from "../config/paths.js"; +import configFactory from "../config/webpack.config.js"; +import createDevServerConfig from "../config/webpackDevServer.config.js"; +import getClientEnvironment from "../config/env.js"; // Makes the script crash on unhandled rejections instead of silently // ignoring them. In the future, promise rejections that are not handled will @@ -9,28 +30,7 @@ process.on("unhandledRejection", (err) => { throw err; }); -// Ensure environment variables are read. -require("../config/env"); - -const fs = require("fs"); -const chalk = require("react-dev-utils/chalk"); -const webpack = require("webpack"); -const WebpackDevServer = require("webpack-dev-server"); -const clearConsole = require("react-dev-utils/clearConsole"); -const checkRequiredFiles = require("react-dev-utils/checkRequiredFiles"); -const { - choosePort, - createCompiler, - prepareProxy, - prepareUrls, -} = require("react-dev-utils/WebpackDevServerUtils"); -const openBrowser = require("react-dev-utils/openBrowser"); -const semver = require("semver"); -const paths = require("../config/paths"); -const configFactory = require("../config/webpack.config"); -const createDevServerConfig = require("../config/webpackDevServer.config"); -const getClientEnvironment = require("../config/env"); -const react = require(require.resolve("react", { paths: [paths.appPath] })); +const appPackageJson = JSON.parse(fs.readFileSync(paths.appPackageJson)); const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1)); const useYarn = fs.existsSync(paths.yarnLockFile); @@ -64,7 +64,6 @@ if (process.env.HOST) { // We require that you explicitly set browsers and do not fall back to // browserslist defaults. -const { checkBrowsers } = require("react-dev-utils/browsersHelper"); checkBrowsers(paths.appPath, isInteractive) .then(() => { // We attempt to use the default port but if it is busy, we offer the user to @@ -79,7 +78,7 @@ checkBrowsers(paths.appPath, isInteractive) const config = configFactory("development"); const protocol = process.env.HTTPS === "true" ? "https" : "http"; - const appName = require(paths.appPackageJson).name; + const appName = appPackageJson.name; const useTypeScript = fs.existsSync(paths.appTsConfig); const urls = prepareUrls( @@ -98,7 +97,7 @@ checkBrowsers(paths.appPath, isInteractive) webpack, }); // Load proxy config - const proxySetting = require(paths.appPackageJson).proxy; + const proxySetting = appPackageJson.proxy; const proxyConfig = prepareProxy( proxySetting, paths.appPublic, diff --git a/client/scripts/test.js b/client/scripts/test.js index 8b03376f37a1..2ef4b4d29c5a 100644 --- a/client/scripts/test.js +++ b/client/scripts/test.js @@ -1,7 +1,8 @@ -// Do this as the first thing so that any code reading it knows the right env. -process.env.BABEL_ENV = "test"; -process.env.NODE_ENV = "test"; -process.env.PUBLIC_URL = ""; +// Ensure environment variables are read. +import "../config/env.js"; + +import jest from "jest"; +import { execSync } from "node:child_process"; // Makes the script crash on unhandled rejections instead of silently // ignoring them. In the future, promise rejections that are not handled will @@ -10,11 +11,6 @@ process.on("unhandledRejection", (err) => { throw err; }); -// Ensure environment variables are read. -require("../config/env"); - -const jest = require("jest"); -const execSync = require("child_process").execSync; let argv = process.argv.slice(2); function isInGitRepository() { diff --git a/client/src/document/index.test.tsx b/client/src/document/index.test.tsx index 09d3a06e6b4a..e78dc6c6164d 100644 --- a/client/src/document/index.test.tsx +++ b/client/src/document/index.test.tsx @@ -1,7 +1,8 @@ -import { Document } from "./index"; import { Route, Routes, MemoryRouter } from "react-router-dom"; import React from "react"; -const { render, waitFor } = require("@testing-library/react"); +import { render, waitFor } from "@testing-library/react"; + +import { Document } from "./index"; declare var global: Window; diff --git a/client/src/homepage/latest-news/index.tsx b/client/src/homepage/latest-news/index.tsx index b5322d5859a0..1b17a45b8e12 100644 --- a/client/src/homepage/latest-news/index.tsx +++ b/client/src/homepage/latest-news/index.tsx @@ -3,22 +3,12 @@ import relativeTime from "dayjs/plugin/relativeTime"; import useSWR from "swr"; import { CRUD_MODE } from "../../env"; import { HydrationData } from "../../types/hydration"; +import { NewsItem } from "../../../../libs/types/document"; import "./index.scss"; dayjs.extend(relativeTime); -export interface NewsItem { - url: string; - title: string; - author?: string; - source: { - name: string; - url: string; - }; - published_at: string; -} - export function LatestNews(props: HydrationData<any>) { const fallbackData = props.hyData ? props : undefined; diff --git a/client/src/settings/db.ts b/client/src/settings/db.ts index ee15e79fbc4a..eb8031511e32 100644 --- a/client/src/settings/db.ts +++ b/client/src/settings/db.ts @@ -85,9 +85,9 @@ export class MDNOfflineDB extends Dexie { } } -const offlineDb = new MDNOfflineDB(); +export const offlineDb = new MDNOfflineDB(); -async function getContentStatus(): Promise<ContentStatus> { +export async function getContentStatus(): Promise<ContentStatus> { const current = await offlineDb.contentStatusHistory.toCollection().last(); return ( @@ -101,7 +101,7 @@ async function getContentStatus(): Promise<ContentStatus> { ); } -async function patchContentStatus( +export async function patchContentStatus( changes: Omit<Partial<ContentStatus>, "id" | "timestamp"> ) { const db = offlineDb; @@ -129,5 +129,3 @@ async function patchContentStatus( } }); } - -export { offlineDb, getContentStatus, patchContentStatus }; diff --git a/client/src/setupProxy.js b/client/src/setupProxy.js index 6b0e9a7a0b07..3d60e5ce1f57 100644 --- a/client/src/setupProxy.js +++ b/client/src/setupProxy.js @@ -1,9 +1,10 @@ -const { createProxyMiddleware } = require("http-proxy-middleware"); +import { createProxyMiddleware } from "http-proxy-middleware"; const SERVER_PORT = process.env.SERVER_PORT || 5042; console.log(`Setting up a Proxy to localhost:${SERVER_PORT}`); -module.exports = function (app) { + +function config(app) { const proxy = createProxyMiddleware(["!**/*.hot-update.json"], { target: `http://localhost:${SERVER_PORT}`, changeOrigin: true, @@ -17,4 +18,6 @@ module.exports = function (app) { app.use("**/*.(png|webp|gif|jpe?g|svg)", proxy); // All those root-level images like /favicon-48x48.png app.use("/*.(png|webp|gif|jpe?g|svg)", proxy); -}; +} + +export default config; diff --git a/client/tsconfig.json b/client/tsconfig.json index 14c196eeb146..d98e742dc40f 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -1,14 +1,23 @@ { - "extends": "../tsconfig.json", "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, - "module": "esnext", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, "isolatedModules": true, + "jsx": "react-jsx", + "lib": ["dom", "dom.iterable", "es2020"], + "module": "ES2020", + "moduleResolution": "Node", "noEmit": true, "noFallthroughCasesInSwitch": true, - "jsx": "react-jsx", - "strictNullChecks": true + "noImplicitAny": false, + "preserveWatchOutput": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ES2020" }, "include": ["src"] } diff --git a/content/document-paths.ts b/content/document-paths.ts index a442af7f1208..a2ace0a9d7f5 100644 --- a/content/document-paths.ts +++ b/content/document-paths.ts @@ -2,8 +2,8 @@ import path from "node:path"; import { fdir, PathsOutput } from "fdir"; -import { HTML_FILENAME, MARKDOWN_FILENAME } from "../libs/constants"; -import { CONTENT_ROOT, CONTENT_TRANSLATED_ROOT } from "../libs/env"; +import { HTML_FILENAME, MARKDOWN_FILENAME } from "../libs/constants/index.js"; +import { CONTENT_ROOT, CONTENT_TRANSLATED_ROOT } from "../libs/env/index.js"; type Tree = { [key: string]: Tree | null }; diff --git a/content/document.test.ts b/content/document.test.ts index 997403690106..67a99924a33b 100644 --- a/content/document.test.ts +++ b/content/document.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; -import * as Document from "./document"; +import * as Document from "./document.js"; describe("Document.findAll()", () => { it("should always return files that exist", () => { diff --git a/content/document.ts b/content/document.ts index 9f8a42ff05b0..b8a48e58d0ee 100644 --- a/content/document.ts +++ b/content/document.ts @@ -6,18 +6,22 @@ import fm from "front-matter"; import yaml from "js-yaml"; import { fdir, PathsOutput } from "fdir"; -import { CONTENT_TRANSLATED_ROOT, CONTENT_ROOT, ROOTS } from "../libs/env"; +import { + CONTENT_TRANSLATED_ROOT, + CONTENT_ROOT, + ROOTS, +} from "../libs/env/index.js"; import { ACTIVE_LOCALES, HTML_FILENAME, MARKDOWN_FILENAME, VALID_LOCALES, -} from "../libs/constants"; -import { isValidLocale } from "../libs/locale-utils"; -import { getPopularities } from "./popularities"; -import { getWikiHistories } from "./wikihistories"; -import { getGitHistories } from "./githistories"; -import { childrenFoldersForPath } from "./document-paths"; +} from "../libs/constants/index.js"; +import { isValidLocale } from "../libs/locale-utils/index.js"; +import { getPopularities } from "./popularities.js"; +import { getWikiHistories } from "./wikihistories.js"; +import { getGitHistories } from "./githistories.js"; +import { childrenFoldersForPath } from "./document-paths.js"; import { buildURL, @@ -28,10 +32,11 @@ import { urlToFolderPath, toPrettyJSON, MEMOIZE_INVALIDATE, -} from "./utils"; -export { urlToFolderPath, MEMOIZE_INVALIDATE } from "./utils"; -import * as Redirect from "./redirect"; -import { DocFrontmatter } from "../libs/types"; +} from "./utils.js"; +import * as Redirect from "./redirect.js"; +import { DocFrontmatter } from "../libs/types/document.js"; + +export { urlToFolderPath, MEMOIZE_INVALIDATE } from "./utils.js"; function buildPath(localeFolder: string, slug: string) { return path.join(localeFolder, slugToFolder(slug)); diff --git a/content/image.ts b/content/image.ts index 8551c47cc4a6..dad347cf9dac 100644 --- a/content/image.ts +++ b/content/image.ts @@ -5,9 +5,9 @@ import readChunk from "read-chunk"; import imageType from "image-type"; import isSvg from "is-svg"; -import { DEFAULT_LOCALE } from "../libs/constants"; -import { ROOTS } from "../libs/env"; -import { memoize, slugToFolder } from "./utils"; +import { DEFAULT_LOCALE } from "../libs/constants/index.js"; +import { ROOTS } from "../libs/env/index.js"; +import { memoize, slugToFolder } from "./utils.js"; function isImage(filePath) { if (fs.statSync(filePath).isDirectory()) { @@ -16,7 +16,6 @@ function isImage(filePath) { if (filePath.toLowerCase().endsWith(".svg")) { return isSvg(fs.readFileSync(filePath)); } - const buffer = readChunk.sync(filePath, 0, 12); if (buffer.length === 0) { return false; diff --git a/content/index.ts b/content/index.ts index 0852182fd1db..0c41d320022c 100644 --- a/content/index.ts +++ b/content/index.ts @@ -1,8 +1,16 @@ -export * as Document from "./document"; -export * as Translation from "./translation"; -export { getPopularities } from "./popularities"; -export * as Redirect from "./redirect"; -export * as Image from "./image"; -export { buildURL, memoize, slugToFolder, execGit, getRoot } from "./utils"; -export { resolveFundamental } from "../libs/fundamental-redirects"; -export { translationsOf } from "./translations"; +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 { + buildURL, + memoize, + slugToFolder, + execGit, + getRoot, + urlToFolderPath, + MEMOIZE_INVALIDATE, +} from "./utils.js"; +export { resolveFundamental } from "../libs/fundamental-redirects/index.js"; +export { translationsOf } from "./translations.js"; diff --git a/content/popularities.ts b/content/popularities.ts index 77c2298c7486..158dceece619 100644 --- a/content/popularities.ts +++ b/content/popularities.ts @@ -1,7 +1,4 @@ import fs from "node:fs"; -import path from "node:path"; - -const dirname = __dirname; // Module-level cache const popularities = new Map<string, number>(); @@ -9,9 +6,7 @@ const popularities = new Map<string, number>(); export function getPopularities() { if (!popularities.size) { // This is the file that's *not* checked into git. - const filePath = path.resolve( - path.join(dirname, "..", "popularities.json") - ); + const filePath = new URL("../popularities.json", import.meta.url); Object.entries(JSON.parse(fs.readFileSync(filePath, "utf-8"))).forEach( ([url, value]: [string, unknown]) => { popularities.set(url, value as number); diff --git a/content/redirect.test.ts b/content/redirect.test.ts index e4bb4b2d9df4..8535b0af18c9 100644 --- a/content/redirect.test.ts +++ b/content/redirect.test.ts @@ -1,4 +1,4 @@ -import * as Redirect from "./redirect"; +import * as Redirect from "./redirect.js"; describe("short cuts", () => { it("simple chain", () => { diff --git a/content/redirect.ts b/content/redirect.ts index d21fe4d7c24b..02fec571b58a 100644 --- a/content/redirect.ts +++ b/content/redirect.ts @@ -1,11 +1,11 @@ import fs from "node:fs"; import path from "node:path"; -import { resolveFundamental } from "../libs/fundamental-redirects"; -import { decodePath, slugToFolder } from "../libs/slug-utils"; -import { CONTENT_ROOT, CONTENT_TRANSLATED_ROOT } from "../libs/env"; -import { VALID_LOCALES } from "../libs/constants"; -import { getRoot } from "./utils"; +import { resolveFundamental } from "../libs/fundamental-redirects/index.js"; +import { decodePath, slugToFolder } from "../libs/slug-utils/index.js"; +import { CONTENT_ROOT, CONTENT_TRANSLATED_ROOT } from "../libs/env/index.js"; +import { VALID_LOCALES } from "../libs/constants/index.js"; +import { getRoot } from "./utils.js"; type Pair = [string, string]; type Pairs = Pair[]; diff --git a/content/translation.ts b/content/translation.ts index e97b6dd0c34d..69507cb226b6 100644 --- a/content/translation.ts +++ b/content/translation.ts @@ -1,4 +1,4 @@ -import Parser from "../kumascript/src/parser.js"; +import * as Parser from "../kumascript/src/parser.js"; function* fastKSParser(s: string) { for (const match of s.matchAll( diff --git a/content/translations.ts b/content/translations.ts index 039e18b1faf9..c25f4be7f44f 100644 --- a/content/translations.ts +++ b/content/translations.ts @@ -1,6 +1,6 @@ -import * as Document from "./document"; -import { VALID_LOCALES } from "../libs/constants"; -import LANGUAGES_RAW from "../libs/languages"; +import * as Document from "./document.js"; +import { VALID_LOCALES } from "../libs/constants/index.js"; +import LANGUAGES_RAW from "../libs/languages/index.js"; const LANGUAGES = new Map( Object.entries(LANGUAGES_RAW).map(([locale, data]) => { diff --git a/content/tsconfig.json b/content/tsconfig.json deleted file mode 100644 index 379a994d81f6..000000000000 --- a/content/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../tsconfig.json", - "include": ["."] -} diff --git a/content/utils.ts b/content/utils.ts index b24c47f5b512..6156f918d57a 100644 --- a/content/utils.ts +++ b/content/utils.ts @@ -1,9 +1,18 @@ import path from "node:path"; import childProcess from "node:child_process"; + import LRU from "lru-cache"; -import { CONTENT_ROOT, CONTENT_TRANSLATED_ROOT } from "../libs/env"; -import { slugToFolder as _slugToFolder } from "../libs/slug-utils"; +import { CONTENT_ROOT, CONTENT_TRANSLATED_ROOT } from "../libs/env/index.js"; +import { slugToFolder as _slugToFolder } from "../libs/slug-utils/index.js"; + +let prettier = null; + +try { + prettier = (await import("prettier")).default; +} catch (e) { + // If we failed to import Prettier, that's okay +} export const MEMOIZE_INVALIDATE = Symbol("force cache update"); @@ -106,17 +115,19 @@ export function execGit(args, opts: { cwd?: string } = {}, root = null) { export function toPrettyJSON(value) { const json = JSON.stringify(value, null, 2) + "\n"; - try { - // eslint-disable-next-line n/no-unpublished-require - return require("prettier").format(json, { parser: "json" }); - } catch (e) { - return json; + if (prettier) { + try { + return prettier.format(json, { parser: "json" }); + } catch (e) { + // If Prettier formatting failed, don't worry + } } + return json; } export function urlToFolderPath(url) { const [, locale, , ...slugParts] = url.split("/"); - return path.join(locale.toLowerCase(), slugToFolder(slugParts.join("/"))); + return path.join(locale.toLowerCase(), _slugToFolder(slugParts.join("/"))); } export function slugToFolder(slug) { diff --git a/deployer/aws-lambda/mdn-stripe-price-ids/tests/handler.test.js b/deployer/aws-lambda/mdn-stripe-price-ids/tests/handler.test.js index e444a8f2b02e..cf37abf35b98 100644 --- a/deployer/aws-lambda/mdn-stripe-price-ids/tests/handler.test.js +++ b/deployer/aws-lambda/mdn-stripe-price-ids/tests/handler.test.js @@ -1,4 +1,8 @@ +import { createRequire } from "node:module"; import { handler } from "../index.js"; + +const require = createRequire(import.meta.url); + jest.mock("../plans-stage-lookup.json", () => { return require("./__mocks__/plans-stage-lookup-test.json"); }); diff --git a/docs/conventions.md b/docs/conventions.md index acea94734ffd..faa630eeb07e 100644 --- a/docs/conventions.md +++ b/docs/conventions.md @@ -19,7 +19,7 @@ which do not adhere to its formatting. Things should be defined in order in which they are used. E.g.: ```javascript -const dependency = require("dependency"); +import dependency from "dependency"; function doThing() { // ... diff --git a/filecheck/checker.ts b/filecheck/checker.ts index ef7edfa0464c..ddc0d805b0da 100644 --- a/filecheck/checker.ts +++ b/filecheck/checker.ts @@ -10,17 +10,19 @@ import tempy from "tempy"; import * as cheerio from "cheerio"; import FileType from "file-type"; import imagemin from "imagemin"; -import imageminPngquant from "imagemin-pngquant"; +import imageminPngquantPkg from "imagemin-pngquant"; import imageminMozjpeg from "imagemin-mozjpeg"; import imageminGifsicle from "imagemin-gifsicle"; import imageminSvgo from "imagemin-svgo"; import isSvg from "is-svg"; -import { MAX_FILE_SIZE } from "../libs/env"; +import { MAX_FILE_SIZE } from "../libs/env/index.js"; import { VALID_MIME_TYPES, MAX_COMPRESSION_DIFFERENCE_PERCENTAGE, -} from "../libs/constants"; +} from "../libs/constants/index.js"; + +const { default: imageminPngquant } = imageminPngquantPkg; function formatSize(bytes: number): string { if (bytes > 1024 * 1024) { diff --git a/filecheck/cli.ts b/filecheck/cli.ts index 89cf787099fb..21e76255f61a 100644 --- a/filecheck/cli.ts +++ b/filecheck/cli.ts @@ -1,11 +1,13 @@ #!/usr/bin/env node import path from "node:path"; -import { ActionParameters, program } from "@caporal/core"; +import caporal, { ActionParameters } from "@caporal/core"; -import { runChecker } from "./checker"; -import { MAX_COMPRESSION_DIFFERENCE_PERCENTAGE } from "../libs/constants"; -import { CONTENT_ROOT, CONTENT_TRANSLATED_ROOT } from "../libs/env"; +import { runChecker } from "./checker.js"; +import { MAX_COMPRESSION_DIFFERENCE_PERCENTAGE } from "../libs/constants/index.js"; +import { CONTENT_ROOT, CONTENT_TRANSLATED_ROOT } from "../libs/env/index.js"; + +const { program } = caporal; interface FilecheckArgsAndOptions extends ActionParameters { args: { diff --git a/filecheck/tsconfig.json b/filecheck/tsconfig.json deleted file mode 100644 index 379a994d81f6..000000000000 --- a/filecheck/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../tsconfig.json", - "include": ["."] -} diff --git a/jest.config.json b/jest.config.json index bb41e8ee81f6..589b291d5804 100644 --- a/jest.config.json +++ b/jest.config.json @@ -1,11 +1,16 @@ { - "preset": "ts-jest", + "extensionsToTreatAsEsm": [".ts", ".tsx"], + "moduleNameMapper": { + "^(\\.{1,2}/.*)\\.js$": "$1" + }, + "preset": "ts-jest/presets/default-esm", "testPathIgnorePatterns": ["headless*", "developing.spec.ts"], "transform": { "\\.tsx?$": [ "ts-jest", { - "babelConfig": true + "babelConfig": true, + "useESM": true } ] } diff --git a/kumascript/index.ts b/kumascript/index.ts index 2050f9822e28..b7cb1f446233 100644 --- a/kumascript/index.ts +++ b/kumascript/index.ts @@ -1,19 +1,19 @@ import LRU from "lru-cache"; +import * as cheerio from "cheerio"; -import { Document } from "../content"; -import { m2h } from "../markdown"; +import { Document } from "../content/index.js"; +import { m2h } from "../markdown/index.js"; -import info from "./src/info"; -import { render as renderMacros } from "./src/render"; -export { buildLiveSamplePages } from "./src/live-sample"; -import { HTMLTool } from "./src/api/util"; -import { DEFAULT_LOCALE } from "../libs/constants"; +import info from "./src/info.js"; +import { render as renderMacros } from "./src/render.js"; +export { buildLiveSamplePages } from "./src/live-sample.js"; +import { HTMLTool } from "./src/api/util.js"; +import { DEFAULT_LOCALE } from "../libs/constants/index.js"; import { INTERACTIVE_EXAMPLES_BASE_URL, LIVE_SAMPLES_BASE_URL, -} from "../libs/env"; -import { SourceCodeError } from "./src/errors"; -import * as cheerio from "cheerio"; +} from "../libs/env/index.js"; +import { SourceCodeError } from "./src/errors.js"; const DEPENDENCY_LOOP_INTRO = 'The following documents form a circular dependency when rendering (via the "page" macros):'; diff --git a/kumascript/jest.config.js b/kumascript/jest.config.js deleted file mode 100644 index 6908203f16dd..000000000000 --- a/kumascript/jest.config.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - testEnvironment: "node", - coveragePathIgnorePatterns: ["./src/parser.js"], -}; diff --git a/kumascript/macros/CSSAnimatedProperties.ejs b/kumascript/macros/CSSAnimatedProperties.ejs index c5023786cf5a..cbbc587b8fa9 100644 --- a/kumascript/macros/CSSAnimatedProperties.ejs +++ b/kumascript/macros/CSSAnimatedProperties.ejs @@ -6,7 +6,8 @@ // Read all CSSData and extract animated properties -var data = require('mdn-data/css'); +const data = require('mdn-data/css'); + var animatedProps = []; // Go through all properties, and test if they are animated. If so, add them to diff --git a/kumascript/macros/CSSInfo.ejs b/kumascript/macros/CSSInfo.ejs index 515c40d8cf8c..6a8c570279dd 100644 --- a/kumascript/macros/CSSInfo.ejs +++ b/kumascript/macros/CSSInfo.ejs @@ -6,12 +6,14 @@ $1 - @-rule - defaults to the @-rule within the URL */ +const data = require('mdn-data/css'); +const localStrings = require('mdn-data/l10n/css'); + let slug = env.slug; let name = $0 || (slug ? slug.split("/").pop().toLowerCase() : "preview-wiki-content"); let atRule = $1; -var data = require('mdn-data/css'); let formattedError = "<span style=\"color:red;\">$1$</span>"; let locale = env.locale; let localize = mdn.getLocalString; @@ -21,7 +23,6 @@ let localWiki = wiki; let currentPage = page; let cssLocalStrings = web.getJSONData("L10n-CSS"); -let localStrings = require('mdn-data/l10n/css'); /* Creates a link to another API, e.g. a CSS property or SVG element diff --git a/kumascript/macros/CSS_Ref.ejs b/kumascript/macros/CSS_Ref.ejs index a2bf755d7ab9..5f77e1f23c07 100644 --- a/kumascript/macros/CSS_Ref.ejs +++ b/kumascript/macros/CSS_Ref.ejs @@ -14,6 +14,7 @@ */ const data = require('mdn-data/css'); + let types = ["properties", "selectors", "types", "syntaxes", "at-rules", "descriptors", "units"]; let groupingType = "alphabetically"; diff --git a/kumascript/src/api/mdn.ts b/kumascript/src/api/mdn.ts index 2fecbc3a70d8..1055234ac126 100644 --- a/kumascript/src/api/mdn.ts +++ b/kumascript/src/api/mdn.ts @@ -1,6 +1,8 @@ -import got from "got"; -import { KumaThis } from "../environment"; -import * as util from "./util"; +import gotPkg from "got"; +import { KumaThis } from "../environment.js"; +import * as util from "./util.js"; + +const { default: got } = gotPkg; // Module level caching for repeat calls to fetchWebExtExamples(). let webExtExamples: any = null; diff --git a/kumascript/src/api/page.ts b/kumascript/src/api/page.ts index 7196d1055445..90b875afb6f4 100644 --- a/kumascript/src/api/page.ts +++ b/kumascript/src/api/page.ts @@ -1,5 +1,5 @@ -import { KumaThis } from "../environment"; -import { getBadgeTemplates } from "../lib/badges"; +import { KumaThis } from "../environment.js"; +import { getBadgeTemplates } from "../lib/badges.js"; const page = { // Determines whether or not the page has the specified tag. Returns diff --git a/kumascript/src/api/web.ts b/kumascript/src/api/web.ts index 8b87c2c8dd0d..e79bf48c7efe 100644 --- a/kumascript/src/api/web.ts +++ b/kumascript/src/api/web.ts @@ -1,12 +1,12 @@ import fs from "node:fs"; import path from "node:path"; -import * as util from "./util"; +import * as util from "./util.js"; -const DUMMY_BASE_URL = "https://example.com"; +import { CONTENT_ROOT } from "../../../libs/env/index.js"; +import { KumaThis } from "../environment.js"; -import { CONTENT_ROOT } from "../../../libs/env"; -import { KumaThis } from "../environment"; +const DUMMY_BASE_URL = "https://example.com"; const _warned = new Map(); // The purpose of this function is to make sure `console.warn` is only called once diff --git a/kumascript/src/api/wiki.ts b/kumascript/src/api/wiki.ts index a2601f27e113..db8b3b15473d 100644 --- a/kumascript/src/api/wiki.ts +++ b/kumascript/src/api/wiki.ts @@ -1,5 +1,5 @@ -import { KumaThis } from "../environment"; -import * as util from "./util"; +import { KumaThis } from "../environment.js"; +import * as util from "./util.js"; const wiki = { // diff --git a/kumascript/src/environment.ts b/kumascript/src/environment.ts index f77d66c8b68b..ad36e6aad89f 100644 --- a/kumascript/src/environment.ts +++ b/kumascript/src/environment.ts @@ -22,6 +22,9 @@ * from the rest of the file which is well tested. */ +import { createRequire } from "node:module"; +const require = createRequire(import.meta.url); + // The properties of this object will be globals in the macro // execution environment. const globalsPrototype = { @@ -35,12 +38,12 @@ const globalsPrototype = { require, }; -import mdnPrototype from "./api/mdn"; -import wikiPrototype from "./api/wiki"; -import webPrototype from "./api/web"; -import pagePrototype from "./api/page"; -import info from "./info"; -import Templates from "./templates"; +import mdnPrototype from "./api/mdn.js"; +import wikiPrototype from "./api/wiki.js"; +import webPrototype from "./api/web.js"; +import pagePrototype from "./api/page.js"; +import info from "./info.js"; +import Templates from "./templates.js"; export interface KumaThis { mdn: typeof mdnPrototype; diff --git a/kumascript/src/errors.ts b/kumascript/src/errors.ts index 1aaec8af802c..a4347e8c575f 100644 --- a/kumascript/src/errors.ts +++ b/kumascript/src/errors.ts @@ -2,7 +2,7 @@ * Error classes that can be thrown when trying to render the macros on a page. */ -import { RedirectInfo } from "../../libs/types/document"; +import { RedirectInfo } from "../../libs/types/document.js"; /** * This is the common superclass of the other error classes here. diff --git a/kumascript/src/info.ts b/kumascript/src/info.ts index 13de3cb90789..14519ce424d9 100644 --- a/kumascript/src/info.ts +++ b/kumascript/src/info.ts @@ -1,9 +1,9 @@ import cheerio from "cheerio"; -import Parser from "./parser.js"; -import { Document, Redirect } from "../../content"; -import { isValidLocale } from "../../libs/locale-utils"; -import { m2hSync } from "../../markdown"; +import * as Parser from "./parser.js"; +import { Document, Redirect } from "../../content/index.js"; +import { isValidLocale } from "../../libs/locale-utils/index.js"; +import { m2hSync } from "../../markdown/index.js"; const DUMMY_BASE_URL = "https://example.com"; @@ -59,7 +59,7 @@ function repairURL(url) { return url; } -const info = { +export const info = { getPathname(url) { // This function returns just the pathname of the given "url", removing // any trailing "/". diff --git a/kumascript/src/lib/badges.ts b/kumascript/src/lib/badges.ts index 440c2f944e33..c7f3f1b41e75 100644 --- a/kumascript/src/lib/badges.ts +++ b/kumascript/src/lib/badges.ts @@ -1,5 +1,5 @@ -import { KumaThis } from "../environment"; -import page from "../api/page"; +import { KumaThis } from "../environment.js"; +import page from "../api/page.js"; const badges = [ { diff --git a/kumascript/src/live-sample.ts b/kumascript/src/live-sample.ts index 7a2d56e6f64c..292bc95bee22 100644 --- a/kumascript/src/live-sample.ts +++ b/kumascript/src/live-sample.ts @@ -1,8 +1,8 @@ import cheerio from "cheerio"; import ejs from "ejs"; -import { MacroLiveSampleError } from "./errors"; -import { HTMLTool, KumascriptError, slugify } from "./api/util"; +import { MacroLiveSampleError } from "./errors.js"; +import { HTMLTool, KumascriptError, slugify } from "./api/util.js"; const LIVE_SAMPLE_HTML = ` <!DOCTYPE html> diff --git a/kumascript/src/parser.js b/kumascript/src/parser.js index 15e7e26819ee..bddf083bca80 100644 --- a/kumascript/src/parser.js +++ b/kumascript/src/parser.js @@ -2165,7 +2165,5 @@ function peg$parse(input, options) { } } -module.exports = { - SyntaxError: peg$SyntaxError, - parse: peg$parse, -}; +export const SyntaxError = peg$SyntaxError; +export const parse = peg$parse; diff --git a/kumascript/src/render.ts b/kumascript/src/render.ts index 2ca4984357a5..642adf449ae6 100644 --- a/kumascript/src/render.ts +++ b/kumascript/src/render.ts @@ -38,9 +38,9 @@ * - the environment object that defines per-page values such as * locale, title and slug. */ -import Parser from "./parser.js"; -import Templates from "./templates"; -import Environment from "./environment"; +import * as Parser from "./parser.js"; +import Templates from "./templates.js"; +import Environment from "./environment.js"; import { MacroInvocationError, MacroNotFoundError, @@ -51,8 +51,8 @@ import { MacroWrongXRefError, MacroDeprecatedError, MacroPagesError, -} from "./errors"; -import { RedirectInfo } from "../../libs/types/document"; +} from "./errors.js"; +import { RedirectInfo } from "../../libs/types/document.js"; const defaultTemplates = new Templates(); diff --git a/kumascript/src/templates.ts b/kumascript/src/templates.ts index 6d53d0810193..30169b343391 100644 --- a/kumascript/src/templates.ts +++ b/kumascript/src/templates.ts @@ -25,11 +25,12 @@ */ import fs from "node:fs"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import ejs from "ejs"; -const dirname = __dirname; - -const DEFAULT_MACROS_DIRECTORY = path.normalize(`${dirname}/../macros/`); +const DEFAULT_MACROS_DIRECTORY = path.normalize( + fileURLToPath(new URL("../macros", import.meta.url)) +); export default class Templates { private macroDirectory: string; @@ -103,7 +104,7 @@ export default class Templates { try { const rendered = await ejs.renderFile(path, args, { async: true, - cache: process.env.NODE_ENV === "production", + cache: args.cache || process.env.NODE_ENV === "production", }); return rendered.trim(); } catch (error) { diff --git a/kumascript/tests/environment.test.ts b/kumascript/tests/environment.test.ts index 46213b97848d..4df4b1b9c0e9 100644 --- a/kumascript/tests/environment.test.ts +++ b/kumascript/tests/environment.test.ts @@ -1,4 +1,5 @@ -import Environment from "../src/environment"; +import { jest } from "@jest/globals"; +import Environment from "../src/environment.js"; // We test using `with` because that is what EJS uses. But Jest // runs tests in strict mode, so we have to hide the with inside diff --git a/kumascript/tests/fixtures/server/macros/autorequire-lib1.ejs b/kumascript/tests/fixtures/server/macros/autorequire-lib1.ejs index cfeff7e7fa9f..5bc3d7c8b908 100644 --- a/kumascript/tests/fixtures/server/macros/autorequire-lib1.ejs +++ b/kumascript/tests/fixtures/server/macros/autorequire-lib1.ejs @@ -1,5 +1,5 @@ <% -module.exports = buildAPI({ +export default buildAPI({ result: function (str) { return str + '!'; } diff --git a/kumascript/tests/fixtures/server/macros/library1-used.ejs b/kumascript/tests/fixtures/server/macros/library1-used.ejs index 2c54e96d1b4b..4e1da1954414 100644 --- a/kumascript/tests/fixtures/server/macros/library1-used.ejs +++ b/kumascript/tests/fixtures/server/macros/library1-used.ejs @@ -1,2 +1,2 @@ <% var library1 = require_macro('library1'); %> -The result <%= library1.result($0) %> +The result <%= library1($0) %> diff --git a/kumascript/tests/fixtures/server/macros/library1.ejs b/kumascript/tests/fixtures/server/macros/library1.ejs index d3c380dd2859..08eb05662d86 100644 --- a/kumascript/tests/fixtures/server/macros/library1.ejs +++ b/kumascript/tests/fixtures/server/macros/library1.ejs @@ -1,5 +1,7 @@ <% -module.exports.result = function (str) { +function result (str) { return "was " + str + "!"; }; + +export default result; %> diff --git a/kumascript/tests/fixtures/server/macros/require-used.ejs b/kumascript/tests/fixtures/server/macros/require-used.ejs index 94d62c1aeb78..f164f20cf109 100644 --- a/kumascript/tests/fixtures/server/macros/require-used.ejs +++ b/kumascript/tests/fixtures/server/macros/require-used.ejs @@ -1,6 +1,7 @@ <% +import bcd from '@mdn/browser-compat-data' assert { type: "json" }; + var queryString = $0; -var bcd = require('@mdn/browser-compat-data'); function getData() { return queryString.split('.').reduce(function(prev, curr) { diff --git a/kumascript/tests/index.test.ts b/kumascript/tests/index.test.ts index 80b22178ae2e..53cce7721f1a 100644 --- a/kumascript/tests/index.test.ts +++ b/kumascript/tests/index.test.ts @@ -1,76 +1,104 @@ -import { Document } from "../../content"; -import info from "../src/info"; -import { render } from "../index"; +import { jest } from "@jest/globals"; + +import * as Content from "../../content/index.js"; import { MacroNotFoundError, MacroBrokenLinkError, MacroRedirectedLinkError, -} from "../src/errors"; +} from "../src/errors.js"; + +const source = ` +{{cssxref("bigfoot")}} +{{nonExistentMacro("yada")}} +{{cssxref("dumber")}} +{{cssxref("number")}} +`.trim(); + +const findByURL = jest.fn((url: string) => { + return { + "/en-us/docs/web/a": { + url: "/en-US/docs/Web/A", + metadata: { + title: "A", + locale: "en-US", + slug: "Web/A", + tags: ["Web"], + }, + rawBody: source, + isMarkdown: false, + fileInfo: { + path: "testing/content/files/en-us/web/a", + frontMatterOffset: 8, + }, + }, + "/en-us/docs/web/b": { + url: "/en-US/docs/Web/B", + metadata: { + title: "B", + locale: "en-US", + slug: "Web/B", + tags: ["Web"], + }, + rawBody: '<p>{{cssxref("bigfoot")}}</p>', + isMarkdown: false, + fileInfo: { + path: "testing/content/files/en-us/web/b", + frontMatterOffset: 4, + }, + }, + "/en-us/docs/web/css/number": { + url: "/en-US/docs/Web/CSS/number", + metadata: { + title: "<number>", + locale: "en-US", + slug: "Web/Number", + "page-type": "css-type", + }, + rawBody: "<p>This is the number test page.</p>", + isMarkdown: false, + fileInfo: { + path: "testing/content/files/en-us/web/css/number", + frontMatterOffset: 12, + }, + }, + }[url]; +}); + +jest.unstable_mockModule("../../content/index.js", () => ({ + ...Content, + Document: { + ...Content.Document, + findByURL, + }, +})); + +// Depends on mocking Document.findByURL +const { default: info } = await import("../src/info.js"); + +const cleanURL = jest.fn((url: string) => { + const result = url.toLowerCase(); + if (result === "/en-us/docs/web/css/dumber") { + return "/en-us/docs/web/css/number"; + } + return result; +}); + +jest.unstable_mockModule("../src/info.js", () => ({ + default: { + ...info, + cleanURL, + getPageByURL: jest.fn((url: string) => { + const document = findByURL(cleanURL(url)); + return document ? info.getPage(document) : {}; + }), + }, +})); + +// Depends on module mocking +const { render } = await import("../index.js"); describe("testing the main render() function", () => { it("non-fatal errors in macros are returned by render()", async () => { - info.cleanURL = jest.fn((url) => { - const result = url.toLowerCase(); - if (result === "/en-us/docs/web/css/dumber") { - return "/en-us/docs/web/css/number"; - } - return result; - }); - const source = ` - {{cssxref("bigfoot")}} - {{nonExistentMacro("yada")}} - {{cssxref("dumber")}} - {{cssxref("number")}} - `.trim(); - Document.findByURL = jest.fn((url) => { - return { - "/en-us/docs/web/a": { - url: "/en-US/docs/Web/A", - metadata: { - title: "A", - locale: "en-US", - slug: "Web/A", - tags: ["Web"], - }, - rawBody: source, - isMarkdown: false, - fileInfo: { - path: "testing/content/files/en-us/web/a", - frontMatterOffset: 8, - }, - }, - "/en-us/docs/web/b": { - url: "/en-US/docs/Web/B", - metadata: { - title: "B", - locale: "en-US", - slug: "Web/B", - tags: ["Web"], - }, - rawBody: '<p>{{cssxref("bigfoot")}}</p>', - isMarkdown: false, - fileInfo: { - path: "testing/content/files/en-us/web/b", - frontMatterOffset: 4, - }, - }, - "/en-us/docs/web/css/number": { - url: "/en-US/docs/Web/CSS/number", - metadata: { - title: "<number>", - locale: "en-US", - slug: "Web/Number", - "page-type": "css-type", - }, - rawBody: "<p>This is the number test page.</p>", - isMarkdown: false, - fileInfo: { - path: "testing/content/files/en-us/web/css/number", - frontMatterOffset: 12, - }, - }, - }[url]; - }); const [$, errors] = await render("/en-us/docs/web/a"); const result = $.html(); // First, let's check the result. @@ -104,7 +132,7 @@ describe("testing the main render() function", () => { expect(errors[1]).toBeInstanceOf(MacroNotFoundError); expect(errors[1]).toMatchObject({ line: 9, - column: 7, + column: 1, filepath: "testing/content/files/en-us/web/a", macroName: "nonExistentMacro", errorStack: expect.stringContaining("Unknown macro nonexistentmacro"), @@ -113,7 +141,7 @@ describe("testing the main render() function", () => { expect(errors[2]).toBeInstanceOf(MacroRedirectedLinkError); expect(errors[2]).toMatchObject({ line: 10, - column: 7, + column: 1, filepath: "testing/content/files/en-us/web/a", errorStack: expect.stringContaining( "/en-US/docs/Web/CSS/dumber redirects to /en-US/docs/Web/CSS/number" diff --git a/kumascript/tests/macros.test.ts b/kumascript/tests/macros.test.ts index 842c12216f83..68db362113be 100644 --- a/kumascript/tests/macros.test.ts +++ b/kumascript/tests/macros.test.ts @@ -2,13 +2,16 @@ * Verify that all of the macros in ../macros/ compile without errors */ import fs from "node:fs"; +import { fileURLToPath } from "node:url"; + import ejs from "ejs"; -import Templates from "../src/templates"; -const dirname = __dirname; +import Templates from "../src/templates.js"; describe("macros/ directory", () => { describe("compile all macros", () => { - const templates = new Templates(`${dirname}/../macros`); + const templates = new Templates( + fileURLToPath(new URL("../macros", import.meta.url)) + ); const templateMap = templates.getTemplateMap(); const macroNames = Array.from(templateMap.keys()); diff --git a/kumascript/tests/macros/Compat.test.ts b/kumascript/tests/macros/Compat.test.ts index 162e50b9230e..b986149daf3a 100644 --- a/kumascript/tests/macros/Compat.test.ts +++ b/kumascript/tests/macros/Compat.test.ts @@ -1,18 +1,17 @@ -import { assert, itMacro, describeMacro, lintHTML } from "./utils"; +import { assert, itMacro, describeMacro, lintHTML } from "./utils.js"; import fs from "node:fs"; -import path from "node:path"; import { JSDOM } from "jsdom"; import extend from "extend"; -const dirname = __dirname; -const fixture_dir = path.resolve(dirname, "fixtures/compat"); + +const fixture_dir = new URL("./fixtures/compat/", import.meta.url); let fixtureCompatData = {}; fs.readdirSync(fixture_dir).forEach(function (fn) { fixtureCompatData = extend( true, fixtureCompatData, - JSON.parse(fs.readFileSync(path.resolve(fixture_dir, fn), "utf-8")) + JSON.parse(fs.readFileSync(new URL(fn, fixture_dir), "utf-8")) ); }); diff --git a/kumascript/tests/macros/DefaultAPISidebar.test.ts b/kumascript/tests/macros/DefaultAPISidebar.test.ts index cd15e8878af1..2453f7890cce 100644 --- a/kumascript/tests/macros/DefaultAPISidebar.test.ts +++ b/kumascript/tests/macros/DefaultAPISidebar.test.ts @@ -1,33 +1,32 @@ +import fs from "node:fs"; import { JSDOM } from "jsdom"; +import { jest } from "@jest/globals"; -import { beforeEachMacro, describeMacro, itMacro, lintHTML } from "./utils"; - -const dirname = __dirname; +import { beforeEachMacro, describeMacro, itMacro, lintHTML } from "./utils.js"; /** * Load all the fixtures. */ -import fs from "node:fs"; -import path from "node:path"; -const pagesFixturePath = path.resolve( - dirname, - "fixtures/defaultapisidebar/pages.json" + +const pagesFixturePath = new URL( + "./fixtures/defaultapisidebar/pages.json", + import.meta.url ); const pagesJSON = JSON.parse(fs.readFileSync(pagesFixturePath, "utf-8")); const subpagesJSON = [ pagesJSON["/en-US/docs/Web/API/TestInterface_API/MyGuidePage1"], pagesJSON["/en-US/docs/Web/API/TestInterface_API/MyGuidePage2"], ]; -const commonl10nFixturePath = path.resolve( - dirname, - "fixtures/defaultapisidebar/commonl10n.json" +const commonl10nFixturePath = new URL( + "./fixtures/defaultapisidebar/commonl10n.json", + import.meta.url ); const commonl10nFixture = JSON.parse( fs.readFileSync(commonl10nFixturePath, "utf-8") ); -const groupDataFixturePath = path.resolve( - dirname, - "fixtures/defaultapisidebar/groupdata.json" +const groupDataFixturePath = new URL( + "./fixtures/defaultapisidebar/groupdata.json", + import.meta.url ); const groupDataFixture = JSON.parse( fs.readFileSync(groupDataFixturePath, "utf-8") @@ -310,7 +309,7 @@ describeMacro("DefaultAPISidebar", function () { throw new Error(`Unimplemented mock fixture ${name}`); }); // Mock calls to wiki.getPage() - macro.ctx.wiki.getPage = jest.fn(async (url) => { + macro.ctx.wiki.getPage = jest.fn(async (url: string) => { return pagesJSON[url]; }); }); diff --git a/kumascript/tests/macros/Deprecated.test.ts b/kumascript/tests/macros/Deprecated.test.ts index a9de4912cda1..7310543ca530 100644 --- a/kumascript/tests/macros/Deprecated.test.ts +++ b/kumascript/tests/macros/Deprecated.test.ts @@ -1,4 +1,4 @@ -import { assert, itMacro, describeMacro } from "./utils"; +import { assert, itMacro, describeMacro } from "./utils.js"; // TODO: Add tests for other {{Deprecated_*}} macros describeMacro("Deprecated_Inline", function () { diff --git a/kumascript/tests/macros/EmbedInteractiveExample.test.ts b/kumascript/tests/macros/EmbedInteractiveExample.test.ts index 3bbdade930d4..4c67a6cab68d 100644 --- a/kumascript/tests/macros/EmbedInteractiveExample.test.ts +++ b/kumascript/tests/macros/EmbedInteractiveExample.test.ts @@ -1,4 +1,4 @@ -import { assert, itMacro, describeMacro } from "./utils"; +import { assert, itMacro, describeMacro } from "./utils.js"; describeMacro("EmbedInteractiveExample", function () { itMacro("Typical settings and argument", function (macro) { diff --git a/kumascript/tests/macros/EmbedLiveSample.test.ts b/kumascript/tests/macros/EmbedLiveSample.test.ts index c8e0de40dc7f..9ce4c6a72130 100644 --- a/kumascript/tests/macros/EmbedLiveSample.test.ts +++ b/kumascript/tests/macros/EmbedLiveSample.test.ts @@ -1,4 +1,5 @@ -import { assert, itMacro, describeMacro, beforeEachMacro } from "./utils"; +import { jest } from "@jest/globals"; +import { assert, itMacro, describeMacro, beforeEachMacro } from "./utils.js"; describeMacro("EmbedLiveSample", function () { beforeEachMacro(function (macro) { diff --git a/kumascript/tests/macros/HTTPSidebar.test.ts b/kumascript/tests/macros/HTTPSidebar.test.ts index 5e236dd6999a..532baad2dbb8 100644 --- a/kumascript/tests/macros/HTTPSidebar.test.ts +++ b/kumascript/tests/macros/HTTPSidebar.test.ts @@ -1,43 +1,13 @@ import fs from "node:fs"; -import path from "node:path"; import { JSDOM } from "jsdom"; -import { Document } from "../../../content"; -import { - assert, - itMacro, - beforeEachMacro, - describeMacro, - lintHTML, -} from "./utils"; +import { jest } from "@jest/globals"; +import * as Content from "../../../content/index.js"; -const dirname = __dirname; - -// Load fixture data. -const fixtureData = JSON.parse( - fs.readFileSync( - path.resolve(dirname, "fixtures", "documentData2.json"), - "utf-8" - ) -) as any; - -const locales = { - "en-US": { - ResourcesURI: "Resources and URIs", - }, - es: { - ResourcesURI: "Recursons y URIs", - }, -}; - -function checkSidebarDom(dom, locale) { - const summaries = dom.querySelectorAll("summary"); - assert.equal(summaries[0].textContent, locales[locale].ResourcesURI); -} - -describeMacro("HTTPSidebar", function () { - beforeEachMacro(function (macro) { - macro.ctx.env.url = "/en-US/docs/Web/HTTP/Overview"; - Document.findByURL = jest.fn((url) => { +jest.unstable_mockModule("../../../content/index.js", () => ({ + ...Content, + Document: { + ...Content.Document, + findByURL: jest.fn((url: string) => { const data = fixtureData[url.toLowerCase()]; if (!data) { return null; @@ -52,8 +22,8 @@ describeMacro("HTTPSidebar", function () { tags: data.tags, }, }; - }); - Document.findChildren = jest.fn((url) => { + }), + findChildren: jest.fn((url: string) => { const result: any[] = []; const parent = `${url.toLowerCase()}/`; for (const [key, data] of Object.entries(fixtureData) as any) { @@ -72,7 +42,38 @@ describeMacro("HTTPSidebar", function () { } } return result; - }); + }), + }, +})); + +const { assert, itMacro, beforeEachMacro, describeMacro, lintHTML } = + await import("./utils.js"); + +// Load fixture data. +const fixtureData = JSON.parse( + fs.readFileSync( + new URL("./fixtures/documentData2.json", import.meta.url), + "utf-8" + ) +) as any; + +const locales = { + "en-US": { + ResourcesURI: "Resources and URIs", + }, + es: { + ResourcesURI: "Recursons y URIs", + }, +}; + +function checkSidebarDom(dom, locale) { + const summaries = dom.querySelectorAll("summary"); + assert.equal(summaries[0].textContent, locales[locale].ResourcesURI); +} + +describeMacro("HTTPSidebar", function () { + beforeEachMacro(function (macro) { + macro.ctx.env.url = "/en-US/docs/Web/HTTP/Overview"; }); itMacro("Creates a sidebar object for en-US", function (macro) { diff --git a/kumascript/tests/macros/ListGroups.test.ts b/kumascript/tests/macros/ListGroups.test.ts index 8db03a3a1fa3..f314b7a8f195 100644 --- a/kumascript/tests/macros/ListGroups.test.ts +++ b/kumascript/tests/macros/ListGroups.test.ts @@ -1,17 +1,17 @@ -import { JSDOM } from "jsdom"; +import fs from "node:fs"; -import { beforeEachMacro, describeMacro, itMacro, lintHTML } from "./utils"; +import { jest } from "@jest/globals"; +import { JSDOM } from "jsdom"; -const dirname = __dirname; +import { beforeEachMacro, describeMacro, itMacro, lintHTML } from "./utils.js"; /** * Load all the fixtures. */ -import fs from "node:fs"; -import path from "node:path"; -const groupDataFixturePath = path.resolve( - dirname, - "fixtures/listgroups/groupdata.json" + +const groupDataFixturePath = new URL( + "./fixtures/listgroups/groupdata.json", + import.meta.url ); const groupDataFixture = JSON.parse( fs.readFileSync(groupDataFixturePath, "utf-8") @@ -108,7 +108,7 @@ describeMacro("ListGroups", () => { beforeEachMacro((macro) => { macro.ctx.env.locale = "en-US"; // Mock calls to wiki.page - macro.ctx.wiki.getPage = jest.fn((name) => { + macro.ctx.wiki.getPage = jest.fn((name: string) => { return overviewPages[name]; }); // Mock calls to GroupData diff --git a/kumascript/tests/macros/LiveSampleURL.test.ts b/kumascript/tests/macros/LiveSampleURL.test.ts index 9f5d0ef011b2..218c695d7772 100644 --- a/kumascript/tests/macros/LiveSampleURL.test.ts +++ b/kumascript/tests/macros/LiveSampleURL.test.ts @@ -1,4 +1,5 @@ -import { assert, itMacro, describeMacro, beforeEachMacro } from "./utils"; +import { jest } from "@jest/globals"; +import { assert, itMacro, describeMacro, beforeEachMacro } from "./utils.js"; describeMacro("LiveSampleURL", function () { beforeEachMacro(function (macro) { @@ -33,7 +34,7 @@ describeMacro("LiveSampleURL", function () { base_url: "https://mdn.mozillademos.org", }; macro.ctx.info.hasPage = jest.fn(() => false); - macro.ctx.info.getDescription = jest.fn((url) => url.toLowerCase()); + macro.ctx.info.getDescription = jest.fn((url: string) => url.toLowerCase()); macro.ctx.env.url = "/en-US/docs/Learn/HTML"; await expect( macro.call("No_JS", "/en-US/docs/does/not/exist") diff --git a/kumascript/tests/macros/Specifications.test.ts b/kumascript/tests/macros/Specifications.test.ts index 2704232c8190..770b79b99772 100644 --- a/kumascript/tests/macros/Specifications.test.ts +++ b/kumascript/tests/macros/Specifications.test.ts @@ -1,4 +1,4 @@ -import { assert, itMacro, describeMacro, lintHTML } from "./utils"; +import { assert, itMacro, describeMacro, lintHTML } from "./utils.js"; import { JSDOM } from "jsdom"; diff --git a/kumascript/tests/macros/WebAssemblySidebar.test.ts b/kumascript/tests/macros/WebAssemblySidebar.test.ts index 4fa395edb7f1..7a7affdb79f5 100644 --- a/kumascript/tests/macros/WebAssemblySidebar.test.ts +++ b/kumascript/tests/macros/WebAssemblySidebar.test.ts @@ -1,4 +1,4 @@ -import { assert, itMacro, describeMacro } from "./utils"; +import { assert, itMacro, describeMacro } from "./utils.js"; const expected = `\ <section id="Quick_links"> diff --git a/kumascript/tests/macros/addonsidebar.test.ts b/kumascript/tests/macros/addonsidebar.test.ts index 6111dad51e3f..2102ed34029d 100644 --- a/kumascript/tests/macros/addonsidebar.test.ts +++ b/kumascript/tests/macros/addonsidebar.test.ts @@ -1,6 +1,7 @@ import { JSDOM } from "jsdom"; +import { jest } from "@jest/globals"; -import { beforeEachMacro, describeMacro, itMacro, lintHTML } from "./utils"; +import { beforeEachMacro, describeMacro, itMacro, lintHTML } from "./utils.js"; const SUMMARIES = { "en-US": [ diff --git a/kumascript/tests/macros/apiref.test.ts b/kumascript/tests/macros/apiref.test.ts index 90ce2f5dd45e..428cb02c2167 100644 --- a/kumascript/tests/macros/apiref.test.ts +++ b/kumascript/tests/macros/apiref.test.ts @@ -1,46 +1,46 @@ -import { JSDOM } from "jsdom"; +import fs from "node:fs"; -import { beforeEachMacro, describeMacro, itMacro, lintHTML } from "./utils"; +import { JSDOM } from "jsdom"; +import { jest } from "@jest/globals"; -const dirname = __dirname; +import { beforeEachMacro, describeMacro, itMacro, lintHTML } from "./utils.js"; /** * Load all the fixtures. */ -import fs from "node:fs"; -import path from "node:path"; -const subpagesFixturePath = path.resolve( - dirname, - "fixtures/apiref/subpages.json" + +const subpagesFixturePath = new URL( + "./fixtures/apiref/subpages.json", + import.meta.url ); const subpagesFixture = JSON.parse( fs.readFileSync(subpagesFixturePath, "utf-8") ); -const commonl10nFixturePath = path.resolve( - dirname, - "fixtures/apiref/commonl10n.json" +const commonl10nFixturePath = new URL( + "./fixtures/apiref/commonl10n.json", + import.meta.url ); const commonl10nFixture = JSON.parse( fs.readFileSync(commonl10nFixturePath, "utf-8") ); -const groupDataFixturePath = path.resolve( - dirname, - "fixtures/apiref/groupdata.json" +const groupDataFixturePath = new URL( + "./fixtures/apiref/groupdata.json", + import.meta.url ); const groupDataFixture = JSON.parse( fs.readFileSync(groupDataFixturePath, "utf-8") ); -const interfaceDataNoEntriesFixturePath = path.resolve( - dirname, - "fixtures/apiref/interfacedata_no_entries.json" +const interfaceDataNoEntriesFixturePath = new URL( + "./fixtures/apiref/interfacedata_no_entries.json", + import.meta.url ); const interfaceDataNoEntriesFixture = fs.readFileSync( interfaceDataNoEntriesFixturePath, "utf-8" ); -const interfaceDataFixturePath = path.resolve( - dirname, - "fixtures/apiref/interfacedata.json" +const interfaceDataFixturePath = new URL( + "./fixtures/apiref/interfacedata.json", + import.meta.url ); const interfaceDataFixture = JSON.parse( fs.readFileSync(interfaceDataFixturePath, "utf-8") diff --git a/kumascript/tests/macros/cssxref.test.ts b/kumascript/tests/macros/cssxref.test.ts index 9eb3206fa8ea..3beda9da0f6d 100644 --- a/kumascript/tests/macros/cssxref.test.ts +++ b/kumascript/tests/macros/cssxref.test.ts @@ -1,4 +1,5 @@ -import { assert, itMacro, describeMacro, beforeEachMacro } from "./utils"; +import { jest } from "@jest/globals"; +import { assert, itMacro, describeMacro, beforeEachMacro } from "./utils.js"; // Basic const const CSS_BASE_URL = "/en-US/docs/Web/CSS"; diff --git a/kumascript/tests/macros/firefoxsidebar.test.ts b/kumascript/tests/macros/firefoxsidebar.test.ts index 7c311f7fcb49..3e891c758a7f 100644 --- a/kumascript/tests/macros/firefoxsidebar.test.ts +++ b/kumascript/tests/macros/firefoxsidebar.test.ts @@ -1,4 +1,4 @@ -import { assert, itMacro, describeMacro } from "./utils"; +import { assert, itMacro, describeMacro } from "./utils.js"; import { JSDOM } from "jsdom"; const locales = { diff --git a/kumascript/tests/macros/gamessidebar.test.ts b/kumascript/tests/macros/gamessidebar.test.ts index 964391c29d2a..1c5089adb62f 100644 --- a/kumascript/tests/macros/gamessidebar.test.ts +++ b/kumascript/tests/macros/gamessidebar.test.ts @@ -1,4 +1,4 @@ -import { assert, itMacro, describeMacro } from "./utils"; +import { assert, itMacro, describeMacro } from "./utils.js"; import { JSDOM } from "jsdom"; const locales = { diff --git a/kumascript/tests/macros/httpheader.test.ts b/kumascript/tests/macros/httpheader.test.ts index 1e692443d7e1..18e759f4d965 100644 --- a/kumascript/tests/macros/httpheader.test.ts +++ b/kumascript/tests/macros/httpheader.test.ts @@ -1,4 +1,5 @@ -import { assert, itMacro, describeMacro } from "./utils"; +import { jest } from "@jest/globals"; +import { assert, itMacro, describeMacro } from "./utils.js"; describeMacro("httpheader", function () { itMacro("No arguments (en-US)", function (macro) { diff --git a/kumascript/tests/macros/jsxref.test.ts b/kumascript/tests/macros/jsxref.test.ts index 365add253dc6..079468ff4bdd 100644 --- a/kumascript/tests/macros/jsxref.test.ts +++ b/kumascript/tests/macros/jsxref.test.ts @@ -1,4 +1,6 @@ -import { assert, itMacro, describeMacro } from "./utils"; +import { jest } from "@jest/globals"; + +import { assert, itMacro, describeMacro } from "./utils.js"; const js_ref_slug = "Web/JavaScript/Reference/"; const js_ref_url = `/en-US/docs/${js_ref_slug}`; diff --git a/kumascript/tests/macros/mdnsidebar.test.ts b/kumascript/tests/macros/mdnsidebar.test.ts index 0d92316503e1..063114ffeae9 100644 --- a/kumascript/tests/macros/mdnsidebar.test.ts +++ b/kumascript/tests/macros/mdnsidebar.test.ts @@ -1,4 +1,4 @@ -import { assert, itMacro, describeMacro } from "./utils"; +import { assert, itMacro, describeMacro } from "./utils.js"; import { JSDOM } from "jsdom"; const locales = { diff --git a/kumascript/tests/macros/page-api.test.ts b/kumascript/tests/macros/page-api.test.ts index 53d40611e1e2..8a13456a2794 100644 --- a/kumascript/tests/macros/page-api.test.ts +++ b/kumascript/tests/macros/page-api.test.ts @@ -3,16 +3,62 @@ // part of ../../src/environment.js, but we're still testing them here. import fs from "node:fs"; -import path from "node:path"; -import { Document } from "../../../content"; -import { assert, itMacro, describeMacro, beforeEachMacro } from "./utils"; +import { jest } from "@jest/globals"; -const dirname = __dirname; +import * as Content from "../../../content/index.js"; + +jest.unstable_mockModule("../../../content/index.js", () => ({ + ...Content, + Document: { + ...Content.Document, + findByURL: jest.fn((url: string) => { + const data = fixtureData[url.toLowerCase()]; + if (!data) { + return null; + } + return { + url: data.url, + metadata: { + title: data.title, + locale: data.locale, + summary: data.summary, + slug: data.slug, + tags: data.tags, + }, + }; + }), + findChildren: jest.fn((url: string) => { + const result: any[] = []; + const parent = `${url.toLowerCase()}/`; + for (const [key, data] of Object.entries(fixtureData) as any) { + if (!key.replace(parent, "").includes("/")) { + key.replace(`${url.toLowerCase()}/`, ""); + result.push({ + url: data.url, + metadata: { + title: data.title, + locale: data.locale, + summary: data.summary, + slug: data.slug, + tags: data.tags, + }, + }); + } + } + return result; + }), + }, +})); + +// Depends on module mocking +const { assert, itMacro, describeMacro, beforeEachMacro } = await import( + "./utils.js" +); // Load fixture data. const fixtureData = JSON.parse( fs.readFileSync( - path.resolve(dirname, "fixtures", "documentData1.json"), + new URL("./fixtures/documentData1.json", import.meta.url), "utf-8" ) ); @@ -74,45 +120,9 @@ function checkSubpagesResult(res) { describeMacro("page API tests", function () { beforeEachMacro(function (macro) { - macro.ctx.info.cleanURL = jest.fn((url) => + macro.ctx.info.cleanURL = jest.fn((url: string) => new URL(url, "https://example.com").pathname.toLowerCase() ); - Document.findByURL = jest.fn((url) => { - const data = fixtureData[url.toLowerCase()]; - if (!data) { - return null; - } - return { - url: data.url, - metadata: { - title: data.title, - locale: data.locale, - summary: data.summary, - slug: data.slug, - tags: data.tags, - }, - }; - }); - Document.findChildren = jest.fn((url) => { - const result = []; - const parent = `${url.toLowerCase()}/`; - for (const [key, data] of Object.entries(fixtureData) as any) { - if (!key.replace(parent, "").includes("/")) { - key.replace(`${url.toLowerCase()}/`, ""); - result.push({ - url: data.url, - metadata: { - title: data.title, - locale: data.locale, - summary: data.summary, - slug: data.slug, - tags: data.tags, - }, - }); - } - } - return result; - }); }); itMacro("dummy", function (macro) { const pkg = macro.ctx.page; diff --git a/kumascript/tests/macros/svgattr.test.ts b/kumascript/tests/macros/svgattr.test.ts index 649c9dda884c..5d2618a1b61e 100644 --- a/kumascript/tests/macros/svgattr.test.ts +++ b/kumascript/tests/macros/svgattr.test.ts @@ -1,4 +1,4 @@ -import { assert, itMacro, describeMacro } from "./utils"; +import { assert, itMacro, describeMacro } from "./utils.js"; describeMacro("SVGAttr", () => { for (const locale of ["en-US", "fr"]) { diff --git a/kumascript/tests/macros/svginfo.test.ts b/kumascript/tests/macros/svginfo.test.ts index b93095422304..84626630b41e 100644 --- a/kumascript/tests/macros/svginfo.test.ts +++ b/kumascript/tests/macros/svginfo.test.ts @@ -1,8 +1,9 @@ import fs from "node:fs"; import path from "node:path"; import cheerio from "cheerio"; +import { jest } from "@jest/globals"; -import { itMacro, describeMacro, beforeEachMacro } from "./utils"; +import { itMacro, describeMacro, beforeEachMacro } from "./utils.js"; const CONTENT_ROOT = process.env.CONTENT_ROOT; if (!CONTENT_ROOT) { diff --git a/kumascript/tests/macros/utils.test.ts b/kumascript/tests/macros/utils.test.ts index f6224cbdd1f2..a94973f273bb 100644 --- a/kumascript/tests/macros/utils.test.ts +++ b/kumascript/tests/macros/utils.test.ts @@ -1,4 +1,4 @@ -import { lintHTML } from "./utils"; +import { lintHTML } from "./utils.js"; const ERROR_TEST_CASES = [ { diff --git a/kumascript/tests/macros/utils.ts b/kumascript/tests/macros/utils.ts index fdda687ce7a9..ff179a47fdf7 100644 --- a/kumascript/tests/macros/utils.ts +++ b/kumascript/tests/macros/utils.ts @@ -1,9 +1,9 @@ -import { HtmlValidate } from "html-validate"; +import { fileURLToPath } from "node:url"; -import Environment from "../../src/environment"; -import Templates from "../../src/templates"; +import { HtmlValidate } from "html-validate"; -const dirname = __dirname; +import Environment from "../../src/environment.js"; +import Templates from "../../src/templates.js"; // When we were doing mocha testing, we used this.macro to hold this. // But Jest doesn't use the this object, so we just store the object here. @@ -55,7 +55,9 @@ assert.sameMembers = (a1, a2) => { }; function createMacroTestObject(macroName) { - const templates = new Templates(`${dirname}/../../macros/`); + const templates = new Templates( + fileURLToPath(new URL("../../macros/", import.meta.url)) + ); const pageContext = { locale: "en-US", url: "", diff --git a/kumascript/tests/macros/wiki-api.test.ts b/kumascript/tests/macros/wiki-api.test.ts index 23e55020f76b..9cb8d1516469 100644 --- a/kumascript/tests/macros/wiki-api.test.ts +++ b/kumascript/tests/macros/wiki-api.test.ts @@ -2,7 +2,7 @@ // The functions defined by that macro have been moved to // ../../src/environment.js, but the tests that are still relevant remain here. -import { assert, itMacro, describeMacro } from "./utils"; +import { assert, itMacro, describeMacro } from "./utils.js"; describeMacro("dekiscript-wiki", function () { itMacro("basic API", function (macro) { diff --git a/kumascript/tests/parser.test.ts b/kumascript/tests/parser.test.ts index 69d137640ea9..89e098810746 100644 --- a/kumascript/tests/parser.test.ts +++ b/kumascript/tests/parser.test.ts @@ -1,4 +1,4 @@ -import Parser from "../src/parser.js"; +import * as Parser from "../src/parser.js"; describe("Parser", function () { it("input with no macros", () => { diff --git a/kumascript/tests/render.test.ts b/kumascript/tests/render.test.ts index 62486f1ff68f..932f53f1d2d5 100644 --- a/kumascript/tests/render.test.ts +++ b/kumascript/tests/render.test.ts @@ -1,20 +1,21 @@ import fs from "node:fs"; -import Templates from "../src/templates"; -import { render } from "../src/render"; +import { fileURLToPath } from "node:url"; +import { jest } from "@jest/globals"; + +import Templates from "../src/templates.js"; +import { render } from "../src/render.js"; import { MacroInvocationError, MacroNotFoundError, MacroCompilationError, MacroExecutionError, -} from "../src/errors"; - -const dirname = __dirname; +} from "../src/errors.js"; const PAGE_ENV = { slug: "" }; describe("render() function", () => { function fixture(name) { - return `${dirname}/fixtures/render/${name}`; + return fileURLToPath(new URL(`./fixtures/render/${name}`, import.meta.url)); } function get(name) { return fs.readFileSync(fixture(name), "utf-8"); diff --git a/kumascript/tests/templates.test.ts b/kumascript/tests/templates.test.ts index 81b378c00c8b..009c8d4ee33e 100644 --- a/kumascript/tests/templates.test.ts +++ b/kumascript/tests/templates.test.ts @@ -1,8 +1,9 @@ -import EJS from "ejs"; -import Templates from "../src/templates"; +import { fileURLToPath } from "node:url"; import path from "node:path"; -const dirname = __dirname; +import EJS from "ejs"; +import { jest } from "@jest/globals"; +import Templates from "../src/templates.js"; describe("Templates class", () => { it("has the expected methods", () => { @@ -12,7 +13,9 @@ describe("Templates class", () => { }); function dir(name) { - return path.resolve(dirname, "fixtures", "templates", name); + return fileURLToPath( + new URL(`./fixtures/templates/${name}`, import.meta.url) + ); } it("throws on non-existent dir", () => { @@ -74,21 +77,14 @@ describe("Templates class", () => { expect(result).toEqual("3"); }); - ["development", "production"].forEach((mode) => { - it(`loads files ${ - mode === "production" ? "only once" : "for each call" - } in ${mode} mode`, async () => { - process.env.NODE_ENV = mode; + [false, true].forEach((doCache) => { + it(`loads files ${doCache ? "only once" : "for each call"} in ${ + doCache ? "production" : "development" + } mode`, async () => { /** - * Without `JSON.stringify(…)`, `\` in the file path would be treated - * as part of an escape sequence (e.g.: - * `C:\nodejs\...\kumascript\tests\fixtures\templates\test.ejs` - * would be interpreted as: - * - * ```none - * C: - * odejs...kumascript ests␌ixtures emplates est.ejs - * ``` + * JSON.stringify is used here to handle Windows file paths. Without + * it, `\` in the file path would be treated as part of an escape + * sequence. */ const mockLoader = jest.fn( (filename) => `<%= ${JSON.stringify(filename)} -%>` @@ -98,21 +94,25 @@ describe("Templates class", () => { const directory = dir("macros"); const macros = new Templates(directory); - const result1 = await macros.render("test1"); + const result1 = await macros.render("test1", { + cache: doCache, + }); expect(result1).toBe(path.resolve(directory, "test1.ejs")); expect(mockLoader.mock.calls).toHaveLength(1); - const result2 = await macros.render("test2"); + const result2 = await macros.render("test2", { + cache: doCache, + }); expect(result2).toBe(path.resolve(directory, "Test2.ejs")); expect(mockLoader.mock.calls).toHaveLength(2); // Render the macros again, but don't expect any more loads // when we're in production mode. - await macros.render("test1"); - await macros.render("test2"); - await macros.render("test1"); - await macros.render("test2"); - expect(mockLoader.mock.calls).toHaveLength(mode === "production" ? 2 : 6); + await macros.render("test1", { cache: doCache }); + await macros.render("test2", { cache: doCache }); + await macros.render("test1", { cache: doCache }); + await macros.render("test2", { cache: doCache }); + expect(mockLoader.mock.calls).toHaveLength(doCache ? 2 : 6); }); }); }); diff --git a/kumascript/tsconfig.json b/kumascript/tsconfig.json deleted file mode 100644 index 379a994d81f6..000000000000 --- a/kumascript/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../tsconfig.json", - "include": ["."] -} diff --git a/libs/constants/index.js b/libs/constants/index.js index c46364b4a084..74c7384086d4 100644 --- a/libs/constants/index.js +++ b/libs/constants/index.js @@ -1,10 +1,10 @@ -const VALID_LOCALES = new Map( +export const VALID_LOCALES = new Map( ["en-US", "es", "fr", "ja", "ko", "pt-BR", "ru", "zh-CN", "zh-TW"].map( (x) => [x.toLowerCase(), x] ) ); -const RETIRED_LOCALES = new Map( +export const RETIRED_LOCALES = new Map( [ "ar", "bg", @@ -32,9 +32,9 @@ const RETIRED_LOCALES = new Map( ].map((x) => [x.toLowerCase(), x]) ); -const DEFAULT_LOCALE = "en-US"; +export const DEFAULT_LOCALE = "en-US"; -const LOCALE_ALIASES = new Map([ +export const LOCALE_ALIASES = new Map([ // Case is not important on either the keys or the values. ["en", "en-us"], ["pt", "pt-br"], @@ -46,8 +46,8 @@ const LOCALE_ALIASES = new Map([ // This must match what we do in `language-menu/index.tsx` where the cookie // gets set in the client! -const PREFERRED_LOCALE_COOKIE_NAME = "preferredlocale"; -const ACTIVE_LOCALES = new Set([ +export const PREFERRED_LOCALE_COOKIE_NAME = "preferredlocale"; +export const ACTIVE_LOCALES = new Set([ "en-us", "es", "fr", @@ -59,7 +59,7 @@ const ACTIVE_LOCALES = new Set([ "zh-tw", ]); -const CSP_SCRIPT_SRC_VALUES = [ +export const CSP_SCRIPT_SRC_VALUES = [ "'report-sample'", "'self'", @@ -83,7 +83,7 @@ const CSP_SCRIPT_SRC_VALUES = [ // - Current hash: "'sha256-uogddBLIKmJa413dyT0iPejBg3VFcO+4x6B+vw3jng0='", ]; -const CSP_DIRECTIVES = { +export const CSP_DIRECTIVES = { "default-src": ["'self'"], "script-src": CSP_SCRIPT_SRC_VALUES, "script-src-elem": CSP_SCRIPT_SRC_VALUES, @@ -146,18 +146,18 @@ const CSP_DIRECTIVES = { "worker-src": ["'self'"], }; -const cspToString = (csp) => +export const cspToString = (csp) => Object.entries(csp) .map(([directive, values]) => `${directive} ${values.join(" ")};`) .join(" "); -const CSP_VALUE = cspToString(CSP_DIRECTIVES); +export const CSP_VALUE = cspToString(CSP_DIRECTIVES); // ----- // build // ----- -const FLAW_LEVELS = Object.freeze({ +export const FLAW_LEVELS = Object.freeze({ ERROR: "error", IGNORE: "ignore", WARN: "warn", @@ -171,7 +171,7 @@ const FLAW_LEVELS = Object.freeze({ // // This list needs to be synced with the code. And the CLI arguments // used with --flaw-checks needs to match this set. -const VALID_FLAW_CHECKS = new Set([ +export const VALID_FLAW_CHECKS = new Set([ "macros", "broken_links", "bad_bcd_queries", @@ -188,50 +188,23 @@ const VALID_FLAW_CHECKS = new Set([ // client // ------ -const MDN_PLUS_TITLE = "MDN Plus"; +export const MDN_PLUS_TITLE = "MDN Plus"; // ------- // content // ------- -const HTML_FILENAME = "index.html"; -const MARKDOWN_FILENAME = "index.md"; +export const HTML_FILENAME = "index.html"; +export const MARKDOWN_FILENAME = "index.md"; // --------- // filecheck // --------- -const VALID_MIME_TYPES = new Set([ +export const VALID_MIME_TYPES = new Set([ "image/png", "image/jpeg", // this is what you get for .jpeg *and* .jpg file extensions "image/gif", ]); -const MAX_COMPRESSION_DIFFERENCE_PERCENTAGE = 25; // percent - -module.exports = { - ACTIVE_LOCALES, - VALID_LOCALES, - RETIRED_LOCALES, - DEFAULT_LOCALE, - LOCALE_ALIASES, - PREFERRED_LOCALE_COOKIE_NAME, - - CSP_SCRIPT_SRC_VALUES, - CSP_VALUE, - - // build - FLAW_LEVELS, - VALID_FLAW_CHECKS, - - // client - MDN_PLUS_TITLE, - - // content - HTML_FILENAME, - MARKDOWN_FILENAME, - - // filecheck - VALID_MIME_TYPES, - MAX_COMPRESSION_DIFFERENCE_PERCENTAGE, -}; +export const MAX_COMPRESSION_DIFFERENCE_PERCENTAGE = 25; // percent diff --git a/libs/constants/package.json b/libs/constants/package.json index 526b38a99480..2dbfbf33f64d 100644 --- a/libs/constants/package.json +++ b/libs/constants/package.json @@ -3,6 +3,8 @@ "version": "0.0.1", "private": true, "license": "MPL-2.0", + "type": "module", + "exports": "./index.js", "main": "index.js", "types": "index.d.ts" } diff --git a/libs/env/index.js b/libs/env/index.js index d0b246b579e7..93feb114f5c9 100644 --- a/libs/env/index.js +++ b/libs/env/index.js @@ -1,12 +1,12 @@ -const fs = require("fs"); -const path = require("path"); +import fs from "node:fs"; +import path from "node:path"; +import { cwd } from "node:process"; -const dotenv = require("dotenv"); -const dirname = __dirname; +import dotenv from "dotenv"; -const { VALID_FLAW_CHECKS } = require("../constants"); +import { VALID_FLAW_CHECKS } from "../constants/index.js"; -const ROOT = path.join(dirname, "..", ".."); +export const ROOT = path.join(cwd(), "."); dotenv.config({ path: path.join(ROOT, process.env.ENV_FILE || ".env"), @@ -16,27 +16,27 @@ dotenv.config({ // build // ----- -const BUILD_OUT_ROOT = +export const BUILD_OUT_ROOT = process.env.BUILD_OUT_ROOT || path.join(ROOT, "client", "build"); // TODO (far future): Switch to "error" when number of flaws drops. -const DEFAULT_FLAW_LEVELS = process.env.BUILD_FLAW_LEVELS || "*:warn"; +export const DEFAULT_FLAW_LEVELS = process.env.BUILD_FLAW_LEVELS || "*:warn"; -const FILES = process.env.BUILD_FILES || ""; -const FOLDERSEARCH = process.env.BUILD_FOLDERSEARCH || ""; -const GOOGLE_ANALYTICS_ACCOUNT = +export const FILES = process.env.BUILD_FILES || ""; +export const FOLDERSEARCH = process.env.BUILD_FOLDERSEARCH || ""; +export const GOOGLE_ANALYTICS_ACCOUNT = process.env.BUILD_GOOGLE_ANALYTICS_ACCOUNT || ""; -const GOOGLE_ANALYTICS_DEBUG = JSON.parse( +export const GOOGLE_ANALYTICS_DEBUG = JSON.parse( process.env.BUILD_GOOGLE_ANALYTICS_DEBUG || "false" ); -const NO_PROGRESSBAR = Boolean( +export const NO_PROGRESSBAR = Boolean( JSON.parse(process.env.BUILD_NO_PROGRESSBAR || process.env.CI || "false") ); -const FIX_FLAWS = JSON.parse(process.env.BUILD_FIX_FLAWS || "false"); -const FIX_FLAWS_DRY_RUN = JSON.parse( +export const FIX_FLAWS = JSON.parse(process.env.BUILD_FIX_FLAWS || "false"); +export const FIX_FLAWS_DRY_RUN = JSON.parse( process.env.BUILD_FIX_FLAWS_DRY_RUN || "false" ); -const FIX_FLAWS_TYPES = new Set( +export const FIX_FLAWS_TYPES = new Set( (process.env.BUILD_FIX_FLAWS_TYPES && process.env.BUILD_FIX_FLAWS_TYPES.split(",")) || [...VALID_FLAW_CHECKS] ); @@ -49,14 +49,14 @@ if ([...FIX_FLAWS_TYPES].some((flawType) => !VALID_FLAW_CHECKS.has(flawType))) { ); } -const FIX_FLAWS_VERBOSE = JSON.parse( +export const FIX_FLAWS_VERBOSE = JSON.parse( // It's on by default because it's such a sensible option to always have // on. process.env.BUILD_FIX_FLAWS_VERBOSE || "true" ); // See explanation in docs/envvars.md -const ALWAYS_ALLOW_ROBOTS = JSON.parse( +export const ALWAYS_ALLOW_ROBOTS = JSON.parse( process.env.BUILD_ALWAYS_ALLOW_ROBOTS || "false" ); @@ -64,27 +64,27 @@ const ALWAYS_ALLOW_ROBOTS = JSON.parse( // content // ------- -const CONTENT_ROOT = correctContentPathFromEnv("CONTENT_ROOT"); +export const CONTENT_ROOT = correctContentPathFromEnv("CONTENT_ROOT"); -const CONTENT_TRANSLATED_ROOT = correctContentPathFromEnv( +export const CONTENT_TRANSLATED_ROOT = correctContentPathFromEnv( "CONTENT_TRANSLATED_ROOT" ); -const CONTRIBUTOR_SPOTLIGHT_ROOT = correctContentPathFromEnv( +export const CONTRIBUTOR_SPOTLIGHT_ROOT = correctContentPathFromEnv( "CONTRIBUTOR_SPOTLIGHT_ROOT" ); // This makes it possible to know, give a root folder, what is the name of // the repository on GitHub. // E.g. `'https://github.com/' + REPOSITORY_URLS[document.fileInfo.root]` -const REPOSITORY_URLS = { +export const REPOSITORY_URLS = { [CONTENT_ROOT]: "mdn/content", }; // Make a combined array of all truthy roots. This way, you don't // need to constantly worry about CONTENT_TRANSLATED_ROOT potentially being // null. -const ROOTS = [CONTENT_ROOT]; +export const ROOTS = [CONTENT_ROOT]; if (CONTENT_TRANSLATED_ROOT) { ROOTS.push(CONTENT_TRANSLATED_ROOT); REPOSITORY_URLS[CONTENT_TRANSLATED_ROOT] = "mdn/translated-content"; @@ -115,7 +115,7 @@ function correctContentPathFromEnv(envVarName) { // filecheck // --------- -const MAX_FILE_SIZE = JSON.parse( +export const MAX_FILE_SIZE = JSON.parse( process.env.FILECHECK_MAX_FILE_SIZE || 500 * 1024 // 500KiB ); @@ -128,12 +128,12 @@ const SERVER_URL = `http://localhost:${SERVER_PORT}`; // Allow the `process.env.BUILD_LIVE_SAMPLES_BASE_URL` to be falsy // if it *is* set. -const LIVE_SAMPLES_BASE_URL = +export const LIVE_SAMPLES_BASE_URL = process.env.BUILD_LIVE_SAMPLES_BASE_URL !== undefined ? process.env.BUILD_LIVE_SAMPLES_BASE_URL : SERVER_URL; -const INTERACTIVE_EXAMPLES_BASE_URL = +export const INTERACTIVE_EXAMPLES_BASE_URL = process.env.BUILD_INTERACTIVE_EXAMPLES_BASE_URL || "https://interactive-examples.mdn.mozilla.net"; @@ -141,45 +141,11 @@ const INTERACTIVE_EXAMPLES_BASE_URL = // server // ------ -const STATIC_ROOT = +export const STATIC_ROOT = process.env.SERVER_STATIC_ROOT || path.join(ROOT, "client", "build"); -const PROXY_HOSTNAME = +export const PROXY_HOSTNAME = process.env.REACT_APP_KUMA_HOST || "developer.mozilla.org"; -const CONTENT_HOSTNAME = process.env.SERVER_CONTENT_HOST; -const OFFLINE_CONTENT = process.env.SERVER_OFFLINE_CONTENT === "true"; - -const FAKE_V1_API = JSON.parse(process.env.SERVER_FAKE_V1_API || false); - -module.exports = { - ROOT, - // build - BUILD_OUT_ROOT, - DEFAULT_FLAW_LEVELS, - FILES, - FOLDERSEARCH, - GOOGLE_ANALYTICS_ACCOUNT, - GOOGLE_ANALYTICS_DEBUG, - NO_PROGRESSBAR, - FIX_FLAWS, - FIX_FLAWS_DRY_RUN, - FIX_FLAWS_TYPES, - FIX_FLAWS_VERBOSE, - ALWAYS_ALLOW_ROBOTS, - // content - CONTENT_ROOT, - CONTENT_TRANSLATED_ROOT, - CONTRIBUTOR_SPOTLIGHT_ROOT, - REPOSITORY_URLS, - ROOTS, - // filecheck - MAX_FILE_SIZE, - // kumascript, - LIVE_SAMPLES_BASE_URL, - INTERACTIVE_EXAMPLES_BASE_URL, - // server - STATIC_ROOT, - PROXY_HOSTNAME, - CONTENT_HOSTNAME, - OFFLINE_CONTENT, - FAKE_V1_API, -}; +export const CONTENT_HOSTNAME = process.env.SERVER_CONTENT_HOST; +export const OFFLINE_CONTENT = process.env.SERVER_OFFLINE_CONTENT === "true"; + +export const FAKE_V1_API = JSON.parse(process.env.SERVER_FAKE_V1_API || false); diff --git a/libs/env/package.json b/libs/env/package.json index 9a0a359f3ad6..49028c3465f2 100644 --- a/libs/env/package.json +++ b/libs/env/package.json @@ -3,6 +3,8 @@ "version": "0.0.1", "private": true, "license": "MPL-2.0", + "type": "module", + "exports": "./index.js", "main": "index.js", "types": "index.d.ts" } diff --git a/libs/fundamental-redirects/index.js b/libs/fundamental-redirects/index.js index 3de55b4b92a3..a062306bae9b 100644 --- a/libs/fundamental-redirects/index.js +++ b/libs/fundamental-redirects/index.js @@ -1,9 +1,9 @@ -const { +import { DEFAULT_LOCALE, VALID_LOCALES, LOCALE_ALIASES, RETIRED_LOCALES, -} = require("../constants"); +} from "../constants/index.js"; const startRe = /^\^?\/?/; const startTemplate = /^\//; @@ -1262,7 +1262,7 @@ const REDIRECT_PATTERNS = [].concat( const STARTING_SLASH = /^\//; const ABSOLUTE_URL = /^https?:\/\/.*/; -function resolveFundamental(path) { +export function resolveFundamental(path) { if (ABSOLUTE_URL.exec(path)) { return {}; } @@ -1275,7 +1275,3 @@ function resolveFundamental(path) { } return {}; } - -module.exports = { - resolveFundamental, -}; diff --git a/libs/fundamental-redirects/package.json b/libs/fundamental-redirects/package.json index 7fcbb7c9fad7..845921aab952 100644 --- a/libs/fundamental-redirects/package.json +++ b/libs/fundamental-redirects/package.json @@ -3,6 +3,8 @@ "version": "0.0.1", "private": true, "license": "MPL-2.0", + "type": "module", + "exports": "./index.js", "main": "index.js", "types": "index.d.ts" } diff --git a/libs/languages/index.d.ts b/libs/languages/index.d.ts index 8966599136b0..432066aa9981 100644 --- a/libs/languages/index.d.ts +++ b/libs/languages/index.d.ts @@ -1,3 +1,3 @@ declare const _exports: Record<string, { English: string; native: string }>; -export = _exports; +export default _exports; diff --git a/libs/languages/index.js b/libs/languages/index.js index cac835d961fc..9b9fc2e8f31b 100644 --- a/libs/languages/index.js +++ b/libs/languages/index.js @@ -1,4 +1,4 @@ -module.exports = { +export default { ach: { English: "Acholi", native: "Acholi", diff --git a/libs/languages/package.json b/libs/languages/package.json index 78a257e38fb0..37023822aa93 100644 --- a/libs/languages/package.json +++ b/libs/languages/package.json @@ -3,6 +3,8 @@ "version": "0.0.1", "private": true, "license": "MPL-2.0", + "type": "module", + "exports": "./index.js", "main": "index.js", "types": "index.d.ts" } diff --git a/libs/locale-utils/index.js b/libs/locale-utils/index.js index 5067936544a8..52f6b8168059 100644 --- a/libs/locale-utils/index.js +++ b/libs/locale-utils/index.js @@ -1,11 +1,12 @@ -const { parse } = require("cookie"); -const acceptLanguageParser = require("accept-language-parser"); +import { parse } from "cookie"; +import acceptLanguageParser from "accept-language-parser"; -const { +import { DEFAULT_LOCALE, VALID_LOCALES, PREFERRED_LOCALE_COOKIE_NAME, -} = require("../constants"); +} from "../constants/index.js"; + const VALID_LOCALES_LIST = [...VALID_LOCALES.values()]; // From https://github.com/aws-samples/cloudfront-authorization-at-edge/blob/01c1bc843d478977005bde86f5834ce76c479eec/src/lambda-edge/shared/shared.ts#L216 @@ -29,7 +30,7 @@ function getCookie(headers, cookieKey) { return extractCookiesFromHeaders(headers)[cookieKey]; } -function getLocale(request, fallback = DEFAULT_LOCALE) { +export function getLocale(request, fallback = DEFAULT_LOCALE) { // First try by cookie. const cookieLocale = getCookie(request.headers, PREFERRED_LOCALE_COOKIE_NAME); if (cookieLocale) { @@ -48,11 +49,6 @@ function getLocale(request, fallback = DEFAULT_LOCALE) { return locale || fallback; } -function isValidLocale(locale) { +export function isValidLocale(locale) { return typeof locale === "string" && VALID_LOCALES.has(locale.toLowerCase()); } - -module.exports = { - getLocale, - isValidLocale, -}; diff --git a/libs/locale-utils/package.json b/libs/locale-utils/package.json index fafbe82503ea..2fa2fb0d4057 100644 --- a/libs/locale-utils/package.json +++ b/libs/locale-utils/package.json @@ -3,6 +3,8 @@ "version": "0.0.1", "private": true, "license": "MPL-2.0", + "type": "module", + "exports": "./index.js", "main": "index.js", "types": "index.d.ts" } diff --git a/libs/slug-utils/index.js b/libs/slug-utils/index.js index 112d132ab3d0..ead7d9c19f08 100644 --- a/libs/slug-utils/index.js +++ b/libs/slug-utils/index.js @@ -1,6 +1,6 @@ -const sanitizeFilename = require("sanitize-filename"); +import sanitizeFilename from "sanitize-filename"; -function slugToFolder(slug, joiner = "/") { +export function slugToFolder(slug, joiner = "/") { return ( slug // We have slugs with these special characters that would be @@ -19,18 +19,12 @@ function slugToFolder(slug, joiner = "/") { ); } -function decodePath(path) { +export function decodePath(path) { const decoded = path.split("/").map(decodeURIComponent).join("/"); return decoded; } -function encodePath(path) { +export function encodePath(path) { const decoded = path.split("/").map(encodeURIComponent).join("/"); return decoded; } - -module.exports = { - slugToFolder, - decodePath, - encodePath, -}; diff --git a/libs/slug-utils/package.json b/libs/slug-utils/package.json index 44a125d7c6c1..c06a5066b00a 100644 --- a/libs/slug-utils/package.json +++ b/libs/slug-utils/package.json @@ -3,9 +3,11 @@ "version": "0.0.1", "private": true, "license": "MPL-2.0", + "type": "module", "main": "index.js", "types": "index.d.ts", "dependencies": { "sanitize-filename": "^1.6.3" - } + }, + "export": "./index.js" } diff --git a/libs/types/document.ts b/libs/types/document.ts index 502ed6e4c727..db2c159b76e8 100644 --- a/libs/types/document.ts +++ b/libs/types/document.ts @@ -1,5 +1,3 @@ -import * as BCD from "@mdn/browser-compat-data/types"; - export interface Source { folder: string; github_url: string; @@ -206,3 +204,14 @@ export interface BCDSection { query: string; }; } + +export interface NewsItem { + url: string; + title: string; + author?: string; + source: { + name: string; + url: string; + }; + published_at: string; +} diff --git a/libs/types/package.json b/libs/types/package.json index 0560a39e424b..da6a922d6395 100644 --- a/libs/types/package.json +++ b/libs/types/package.json @@ -3,5 +3,7 @@ "version": "0.0.1", "private": true, "license": "MPL-2.0", - "main": "document.ts" + "type": "module", + "main": "document.ts", + "export": "./index.js" } diff --git a/libs/types/tsconfig.json b/libs/types/tsconfig.json deleted file mode 100644 index 862ab5fd9f1c..000000000000 --- a/libs/types/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "sourceMap": true - } -} diff --git a/lint-staged.config.js b/lint-staged.config.js index 2a791d1697b9..d7e5d2118e33 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -1,4 +1,4 @@ -module.exports = { +export default { "*.!{js,jsx,ts,tsx,css,scss}": "prettier --ignore-unknown --write", "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"], "*.{css,scss}": ["stylelint --fix --allow-empty-input", "prettier --write"], diff --git a/markdown/index.ts b/markdown/index.ts index 6ed68305ff21..ccfbace1718c 100644 --- a/markdown/index.ts +++ b/markdown/index.ts @@ -1,2 +1,2 @@ -export * from "./utils"; -export * from "./m2h"; +export * from "./utils/index.js"; +export * from "./m2h/index.js"; diff --git a/markdown/m2h/cli.ts b/markdown/m2h/cli.ts index b71175ce606e..433c0dd1cae3 100644 --- a/markdown/m2h/cli.ts +++ b/markdown/m2h/cli.ts @@ -1,11 +1,13 @@ import fm from "front-matter"; -import { program } from "@caporal/core"; +import caporal from "@caporal/core"; import chalk from "chalk"; -import { Document } from "../../content"; -import { saveFile } from "../../content/document"; -import { VALID_LOCALES } from "../../libs/constants"; +import { Document } from "../../content/index.js"; +import { saveFile } from "../../content/document.js"; +import { VALID_LOCALES } from "../../libs/constants/index.js"; -import { m2h } from "./"; +import { m2h } from "./index.js"; + +const { program } = caporal; interface CliOptions { v?: boolean; diff --git a/markdown/m2h/handlers/dl.ts b/markdown/m2h/handlers/dl.ts index de3a7ade7452..bc4763d27ad9 100644 --- a/markdown/m2h/handlers/dl.ts +++ b/markdown/m2h/handlers/dl.ts @@ -1,4 +1,4 @@ -import { all, wrap } from "./mdast-util-to-hast-utils"; +import { all, wrap } from "./mdast-util-to-hast-utils.js"; export const DEFINITION_PREFIX = ": "; diff --git a/markdown/m2h/handlers/index.ts b/markdown/m2h/handlers/index.ts index 30ff9daccffd..7ab0a95005dc 100644 --- a/markdown/m2h/handlers/index.ts +++ b/markdown/m2h/handlers/index.ts @@ -1,25 +1,26 @@ import fs from "node:fs"; -import path from "node:path"; -import { DEFAULT_LOCALE } from "../../../libs/constants"; -import { code } from "./code"; -import { asDefinitionList, isDefinitionList } from "./dl"; -import { one, all, wrap } from "./mdast-util-to-hast-utils"; -const dirname = __dirname; +import { DEFAULT_LOCALE } from "../../../libs/constants/index.js"; +import { code } from "./code.js"; +import { asDefinitionList, isDefinitionList } from "./dl.js"; +import { one, all, wrap } from "./mdast-util-to-hast-utils.js"; /* A utilitary function which parses a JSON gettext file to return a Map with each localized string and its matching ID */ function getL10nCardMap(locale = DEFAULT_LOCALE) { // Test if target localization file exists, if // not, fallback on English - let localeFilePath = path.join(dirname, `../../localizations/${locale}.json`); + let localeFilePath = new URL( + `../../localizations/${locale}.json`, + import.meta.url + ); if (!fs.existsSync(localeFilePath)) { - localeFilePath = path.join( - dirname, - `../../localizations/${DEFAULT_LOCALE}.json` + localeFilePath = new URL( + `../../localizations/${DEFAULT_LOCALE}.json`, + import.meta.url ); } - const listMsgObj = JSON.parse(fs.readFileSync(localeFilePath, "utf-8"))[ + const listMsgObj = JSON.parse(fs.readFileSync(localeFilePath, "utf8"))[ "translations" ][""]; const l10nCardMap = new Map(); diff --git a/markdown/m2h/index.ts b/markdown/m2h/index.ts index efc0e46dd666..717ec86f42ee 100644 --- a/markdown/m2h/index.ts +++ b/markdown/m2h/index.ts @@ -6,8 +6,8 @@ import gfm from "remark-gfm"; import raw from "rehype-raw"; import format from "rehype-format"; -import { buildLocalizedHandlers } from "./handlers"; -import { decodeKS, encodeKS } from "../utils"; +import { buildLocalizedHandlers } from "./handlers/index.js"; +import { decodeKS, encodeKS } from "../utils/index.js"; interface ProcessorOptions { locale?: string; diff --git a/markdown/tsconfig.json b/markdown/tsconfig.json deleted file mode 100644 index 379a994d81f6..000000000000 --- a/markdown/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../tsconfig.json", - "include": ["."] -} diff --git a/package.json b/package.json index 1f47fe588b91..9481280c1bde 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "repository": "https://github.com/mdn/yari", "license": "MPL-2.0", "author": "MDN Web Docs", + "type": "module", "bin": { "yari-build": "build/cli.js", "yari-filecheck": "filecheck/cli.js", @@ -14,37 +15,38 @@ "analyze": "source-map-explorer 'client/build/static/js/*.js'", "analyze:css": "source-map-explorer 'client/build/static/css/*.css'", "build": "cross-env NODE_ENV=production ts-node build/cli.ts", - "build:client": "cd client && cross-env INLINE_RUNTIME_CHUNK=false node scripts/build.js", + "build:client": "cd client && cross-env NODE_ENV=production BABEL_ENV=production INLINE_RUNTIME_CHUNK=false node scripts/build.js", "build:dist": "tsc -p tsconfig.dist.json", "build:glean": "cd client && cross-env VIRTUAL_ENV=venv glean translate src/telemetry/metrics.yaml src/telemetry/pings.yaml -f typescript -o src/telemetry/generated", "build:prepare": "yarn build:client && yarn build:ssr && yarn tool optimize-client-build && yarn tool google-analytics-code && yarn tool popularities && yarn tool spas && yarn tool gather-git-history && yarn tool build-robots-txt", "build:ssr": "cd ssr && webpack --mode=production", "build:sw": "cd client/pwa && yarn && yarn build:prod", "build:sw-dev": "cd client/pwa && yarn && yarn build", - "check:tsc": "find . -mindepth 2 -name 'tsconfig.json' ! -wholename '**/node_modules/**' -print0 | xargs -n1 -P 2 -0 bash -c 'cd `dirname $0` && echo \"🔄 $(pwd)\" && npx tsc --noEmit && echo \"☑️ $(pwd)\" || exit 255'", + "check:tsc": "find . -name 'tsconfig.json' ! -wholename '**/node_modules/**' -print0 | xargs -n1 -P 2 -0 bash -c 'cd `dirname $0` && echo \"🔄 $(pwd)\" && npx tsc --noEmit && echo \"☑️ $(pwd)\" || exit 255'", "dev": "yarn build:prepare && nf -j Procfile.dev start", "eslint": "eslint .", "filecheck": "ts-node filecheck/cli.ts", + "jest": "cross-env NODE_OPTIONS=--experimental-vm-modules jest", "m2h": "ts-node markdown/m2h/cli.ts", "prepack": "yarn build:dist", "prepare": "husky install", "prettier-check": "prettier --check .", "prettier-format": "prettier --write .", "start": "(test -f client/build/index.html || yarn build:client) && (test -f ssr/dist/main.js || yarn build:ssr) && (test -d client/build/en-us/_spas || yarn tool spas) && nf -j Procfile.start start", - "start:client": "cd client && cross-env BROWSER=none PORT=3000 node scripts/start.js", - "start:dev-server": "node-dev server/index.ts", + "start:client": "cd client && cross-env NODE_ENV=development BABEL_ENV=development BROWSER=none PORT=3000 node scripts/start.js", + "start:dev-server": "node-dev --experimental-loader ts-node/esm server/index.ts", "start:server": "ts-node server", "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:client": "cd client && tsc --noEmit && node scripts/test.js --env=jsdom", - "test:content": "jest content", + "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 content", "test:developing": "cross-env CONTENT_ROOT=mdn/content/files TESTING_DEVELOPING=true playwright test developing", "test:headless": "playwright test headless", - "test:kumascript": "jest kumascript --env=node", + "test:kumascript": "yarn jest kumascript --env=node", "test:prepare": "yarn build:prepare && yarn build && yarn start:static-server", - "test:testing": "jest --rootDir testing", + "test:testing": "yarn jest --rootDir testing", "tool": "ts-node tool/cli.ts", "watch:ssr": "cd ssr && webpack --mode=production --watch" }, @@ -131,7 +133,7 @@ "@types/async": "^3.2.16", "@types/cli-progress": "^3.11.0", "@types/hast": "^2.3.4", - "@types/imagemin": "^8.0.0", + "@types/imagemin": "^7.0.1", "@types/jest": "^29.4.0", "@types/mdast": "^3.0.10", "@types/node": "^16.18.12", @@ -238,6 +240,6 @@ "workbox-webpack-plugin": "^6.5.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^16.17.0 || >=18.0.0" } } diff --git a/playwright.config.js b/playwright.config.js index a12f7d420513..a191f735af54 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -1,4 +1,4 @@ -const config = { +export default { use: { channel: "chrome", // See more interesting options at https://playwright.dev/docs/test-configuration/ @@ -6,5 +6,3 @@ const config = { // video: "retain-on-failure", }, }; - -module.exports = config; diff --git a/sd-config.js b/sd-config.js index f8258f3518da..08c0a742bd74 100644 --- a/sd-config.js +++ b/sd-config.js @@ -1,4 +1,4 @@ -const StyleDictionary = require("style-dictionary"); // eslint-disable-line n/no-unpublished-require +import StyleDictionary from "style-dictionary"; // eslint-disable-line n/no-unpublished-import StyleDictionary.registerTransform({ name: "value/rewrite", @@ -45,7 +45,7 @@ StyleDictionary.registerTransform({ }, }); -module.exports = { +export default { source: ["./client/src/ui/style-dictionary/**/*.json"], platforms: { scss: { diff --git a/server/dev.ts b/server/dev.ts index 687392763430..46b3223f181f 100644 --- a/server/dev.ts +++ b/server/dev.ts @@ -1,13 +1,11 @@ +import webpack from "webpack"; +import webpackDevMiddleware from "webpack-dev-middleware"; +import webpackHotMiddleware from "webpack-hot-middleware"; import type { WebpackConfiguration } from "webpack-dev-server"; export const devMiddlewares = []; if (process.env.NODE_ENV === "development") { - /* eslint-disable n/no-unpublished-require */ - const webpack = require("webpack"); - const webpackDevMiddleware = require("webpack-dev-middleware"); - const webpackHotMiddleware = require("webpack-hot-middleware"); - const webpackConfig: WebpackConfiguration = { entry: { app: [ diff --git a/server/document.ts b/server/document.ts index 24a471803e3f..d855081742b3 100644 --- a/server/document.ts +++ b/server/document.ts @@ -3,10 +3,10 @@ import path from "node:path"; import express, { Request } from "express"; -import { Document, slugToFolder } from "../content"; -import { buildDocument } from "../build"; -import { BUILD_OUT_ROOT } from "../libs/env"; -import { Doc } from "../libs/types/document"; +import { Document, slugToFolder } from "../content/index.js"; +import { buildDocument } from "../build/index.js"; +import { BUILD_OUT_ROOT } from "../libs/env/index.js"; +import { Doc } from "../libs/types/document.js"; const router = express(); diff --git a/server/flaws.ts b/server/flaws.ts index ec035f90ab20..31ef17fadde5 100644 --- a/server/flaws.ts +++ b/server/flaws.ts @@ -1,16 +1,14 @@ -import { Doc } from "../libs/types/document"; -import { FlawFilters } from "./types"; - import fs from "node:fs"; import path from "node:path"; import { fdir, PathsOutput } from "fdir"; -import { getPopularities } from "../content"; -import buildOptions from "../build/build-options"; - -import { FLAW_LEVELS } from "../libs/constants"; -import { BUILD_OUT_ROOT } from "../libs/env"; +import { Doc } from "../libs/types/document.js"; +import { FlawFilters } from "./types.js"; +import { getPopularities } from "../content/index.js"; +import buildOptions from "../build/build-options.js"; +import { FLAW_LEVELS } from "../libs/constants/index.js"; +import { BUILD_OUT_ROOT } from "../libs/env/index.js"; // Module-level cache const allPopularityValues = []; diff --git a/server/index.ts b/server/index.ts index 1cffffd4a67d..5d76a8428620 100644 --- a/server/index.ts +++ b/server/index.ts @@ -15,30 +15,31 @@ import { buildDocument, buildLiveSamplePageFromURL, renderContributorsTxt, -} from "../build"; -import { findDocumentTranslations } from "../content/translations"; -import { Document, Redirect, Image } from "../content"; -import { CONTENT_ROOT, CONTENT_TRANSLATED_ROOT } from "../libs/env"; -import { CSP_VALUE, DEFAULT_LOCALE } from "../libs/constants"; +} from "../build/index.js"; +import { findDocumentTranslations } from "../content/translations.js"; +import { Document, Redirect, Image } from "../content/index.js"; +import { CSP_VALUE, DEFAULT_LOCALE } from "../libs/constants/index.js"; import { STATIC_ROOT, PROXY_HOSTNAME, FAKE_V1_API, CONTENT_HOSTNAME, OFFLINE_CONTENT, -} from "../libs/env"; - -import documentRouter from "./document"; -import fakeV1APIRouter from "./fake-v1-api"; -import { searchIndexRoute } from "./search-index"; -import flawsRoute from "./flaws"; -import { router as translationsRouter } from "./translations"; -import { staticMiddlewares, originRequestMiddleware } from "./middlewares"; -import { getRoot } from "../content/utils"; + CONTENT_ROOT, + CONTENT_TRANSLATED_ROOT, +} from "../libs/env/index.js"; + +import documentRouter from "./document.js"; +import fakeV1APIRouter from "./fake-v1-api.js"; +import { searchIndexRoute } from "./search-index.js"; +import flawsRoute from "./flaws.js"; +import { router as translationsRouter } from "./translations.js"; +import { staticMiddlewares, originRequestMiddleware } from "./middlewares.js"; +import { getRoot } from "../content/utils.js"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore -import { renderHTML } from "../ssr/dist/main"; +import { renderHTML } from "../ssr/dist/main.js"; async function buildDocumentFromURL(url) { const document = Document.findByURL(url); diff --git a/server/middlewares.ts b/server/middlewares.ts index 4cd286c64409..140acd30d0b7 100644 --- a/server/middlewares.ts +++ b/server/middlewares.ts @@ -1,10 +1,10 @@ import express from "express"; -import { CSP_VALUE } from "../libs/constants"; -import { STATIC_ROOT } from "../libs/env"; -import { resolveFundamental } from "../libs/fundamental-redirects"; -import { getLocale } from "../libs/locale-utils"; -import { devMiddlewares } from "./dev"; +import { CSP_VALUE } from "../libs/constants/index.js"; +import { STATIC_ROOT } from "../libs/env/index.js"; +import { resolveFundamental } from "../libs/fundamental-redirects/index.js"; +import { getLocale } from "../libs/locale-utils/index.js"; +import { devMiddlewares } from "./dev.js"; // Lowercase every request because every possible file we might have // on disk is always in lowercase. diff --git a/server/search-index.ts b/server/search-index.ts index 9a8a681be92c..0e8b91fb1a6e 100644 --- a/server/search-index.ts +++ b/server/search-index.ts @@ -4,10 +4,10 @@ import path from "node:path"; import { fdir, PathsOutput } from "fdir"; import fm from "front-matter"; -import { VALID_LOCALES } from "../libs/constants"; -import { CONTENT_ROOT, CONTENT_TRANSLATED_ROOT } from "../libs/env"; -import { SearchIndex } from "../build"; -import { isValidLocale } from "../libs/locale-utils"; +import { VALID_LOCALES } from "../libs/constants/index.js"; +import { CONTENT_ROOT, CONTENT_TRANSLATED_ROOT } from "../libs/env/index.js"; +import { SearchIndex } from "../build/index.js"; +import { isValidLocale } from "../libs/locale-utils/index.js"; interface DocAttributes { locale: string; diff --git a/server/static.ts b/server/static.ts index 1ed9d50b5bc4..a3519ca254d6 100644 --- a/server/static.ts +++ b/server/static.ts @@ -4,9 +4,9 @@ import express from "express"; import compression from "compression"; import cookieParser from "cookie-parser"; -import { staticMiddlewares } from "./middlewares"; -import { resolveFundamental } from "../content"; -import { STATIC_ROOT } from "../libs/env"; +import { staticMiddlewares } from "./middlewares.js"; +import { resolveFundamental } from "../content/index.js"; +import { STATIC_ROOT } from "../libs/env/index.js"; const app = express(); app.use(express.json()); diff --git a/server/translations.ts b/server/translations.ts index e0df07598e67..c6c8190985f7 100644 --- a/server/translations.ts +++ b/server/translations.ts @@ -1,21 +1,21 @@ import fs from "node:fs"; import path from "node:path"; -import { fdir } from "fdir"; - import express from "express"; -export const router = express.Router(); +import { fdir } from "fdir"; -import { getPopularities, Document, Translation } from "../content"; +import { getPopularities, Document, Translation } from "../content/index.js"; import { VALID_LOCALES, ACTIVE_LOCALES, DEFAULT_LOCALE, -} from "../libs/constants"; -import { CONTENT_ROOT, CONTENT_TRANSLATED_ROOT } from "../libs/env"; -import { getLastCommitURL } from "../build"; -import LANGUAGES_RAW from "../libs/languages"; -import { isValidLocale } from "../libs/locale-utils"; +} from "../libs/constants/index.js"; +import { CONTENT_ROOT, CONTENT_TRANSLATED_ROOT } from "../libs/env/index.js"; +import { getLastCommitURL } from "../build/index.js"; +import LANGUAGES_RAW from "../libs/languages/index.js"; +import { isValidLocale } from "../libs/locale-utils/index.js"; + +export const router = express.Router(); // Module-level cache const allPopularityValues = []; @@ -60,7 +60,7 @@ function packageTranslationDifferences(translationDifferences) { } const _foundDocumentsCache = new Map(); -function findDocuments({ locale }) { +export function findDocuments({ locale }) { const counts = { // Number of documents found that aren't skipped found: 0, diff --git a/server/tsconfig.json b/server/tsconfig.json deleted file mode 100644 index 379a994d81f6..000000000000 --- a/server/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../tsconfig.json", - "include": ["."] -} diff --git a/ssr/index.ts b/ssr/index.ts index c15db913c065..ffff42b41e15 100644 --- a/ssr/index.ts +++ b/ssr/index.ts @@ -1,4 +1,4 @@ -import path from "node:path"; +import { fileURLToPath } from "node:url"; import dotenv from "dotenv"; import React from "react"; @@ -7,12 +7,12 @@ import { StaticRouter } from "react-router-dom/server"; import { App } from "../client/src/app"; import render from "./render"; -const dirname = __dirname; - // This is necessary because the ssr.js is in dist/ssr.js // and we need to reach the .env this way. dotenv.config({ - path: path.join(dirname, "..", process.env.ENV_FILE || ".env"), + path: fileURLToPath( + new URL("../" + (process.env.ENV_FILE || ".env"), import.meta.url) + ), }); export function renderHTML(url, context) { diff --git a/ssr/react-app.d.ts b/ssr/react-app.d.ts new file mode 100644 index 000000000000..bf4ab9180050 --- /dev/null +++ b/ssr/react-app.d.ts @@ -0,0 +1,67 @@ +declare namespace NodeJS { + interface ProcessEnv { + readonly NODE_ENV: "development" | "production" | "test"; + readonly PUBLIC_URL: string; + } +} + +declare module "*.avif" { + const src: string; + export default src; +} + +declare module "*.bmp" { + const src: string; + export default src; +} + +declare module "*.gif" { + const src: string; + export default src; +} + +declare module "*.jpg" { + const src: string; + export default src; +} + +declare module "*.jpeg" { + const src: string; + export default src; +} + +declare module "*.png" { + const src: string; + export default src; +} + +declare module "*.webp" { + const src: string; + export default src; +} + +declare module "*.svg" { + import * as React from "react"; + + export const ReactComponent: React.FunctionComponent< + React.SVGProps<SVGSVGElement> & { title?: string } + >; + + const src: string; + export default src; +} + +declare module "*.module.css" { + const classes: { readonly [key: string]: string }; + export default classes; +} + +declare module "*.module.scss" { + const classes: { readonly [key: string]: string }; + export default classes; +} + +declare module "*.module.sass" { + const classes: { readonly [key: string]: string }; + export default classes; +} diff --git a/ssr/render.ts b/ssr/render.ts index 95c4acf0c4fd..309751cb9dc8 100644 --- a/ssr/render.ts +++ b/ssr/render.ts @@ -1,11 +1,13 @@ import fs from "node:fs"; import path from "node:path"; +import { fileURLToPath } from "node:url"; + import { renderToString } from "react-dom/server"; import { DEFAULT_LOCALE } from "../libs/constants"; import { ALWAYS_ALLOW_ROBOTS, BUILD_OUT_ROOT } from "../libs/env"; -const dirname = __dirname; +const dirname = path.dirname(fileURLToPath(new URL(".", import.meta.url))); // When there are multiple options for a given language, this gives the // preferred locale for that language (language => preferred locale). @@ -63,6 +65,7 @@ const lazy = (creator) => { }; }; +// Path strings are preferred over URLs here to mitigate Webpack resolution const clientBuildRoot = path.resolve(dirname, "../../client/build"); const readBuildHTML = lazy(() => { diff --git a/ssr/tsconfig.json b/ssr/tsconfig.json index e82a7a7c434b..bf2c0bd5252e 100644 --- a/ssr/tsconfig.json +++ b/ssr/tsconfig.json @@ -1,6 +1,5 @@ { - "extends": "../tsconfig.json", - "compilerOptions": { - "sourceMap": true - } + "extends": "../client/tsconfig.json", + "include": ["."], + "exclude": ["dist"] } diff --git a/ssr/webpack.config.js b/ssr/webpack.config.js index fbbb078707b9..e66ff19f784b 100644 --- a/ssr/webpack.config.js +++ b/ssr/webpack.config.js @@ -1,24 +1,19 @@ -const path = require("node:path"); +import { fileURLToPath } from "node:url"; +import nodeExternals from "webpack-node-externals"; +import webpack from "webpack"; -const nodeExternals = require("webpack-node-externals"); -const webpack = require("webpack"); - -const dirname = __dirname; - -module.exports = { - context: path.resolve(dirname, "."), +const config = { + context: fileURLToPath(new URL(".", import.meta.url)), entry: "./index.ts", output: { - path: path.resolve(dirname, "dist"), + path: fileURLToPath(new URL("dist", import.meta.url)), filename: "[name].js", sourceMapFilename: "[name].js.map", - libraryTarget: "commonjs2", + library: { type: "module" }, + chunkFormat: "module", }, target: "node", - node: { - __dirname: false, - __filename: false, - }, + node: false, // See all options here: // https://webpack.js.org/configuration/stats/ stats: "errors-warnings", @@ -75,4 +70,9 @@ module.exports = { maxChunks: 1, }), ], + experiments: { + outputModule: true, + }, }; + +export default config; diff --git a/testing/tests/csp.test.ts b/testing/tests/csp.test.ts index 9aceb3910906..ac507e2989ee 100644 --- a/testing/tests/csp.test.ts +++ b/testing/tests/csp.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; import crypto from "node:crypto"; -import { CSP_SCRIPT_SRC_VALUES } from "../../libs/constants"; +import { CSP_SCRIPT_SRC_VALUES } from "../../libs/constants/index.js"; describe("Content-Security-Policy", () => { test('All inline <script> tags must have a corresponding "script-src" CSP entry.', () => { diff --git a/testing/tests/developing.spec.ts b/testing/tests/developing.spec.ts index aa289d49b6c7..72674c792ae0 100644 --- a/testing/tests/developing.spec.ts +++ b/testing/tests/developing.spec.ts @@ -1,5 +1,7 @@ import { test, expect } from "@playwright/test"; -import got from "got"; +import gotPkg from "got"; + +const { default: got } = gotPkg; export {}; diff --git a/testing/tests/filecheck.test.ts b/testing/tests/filecheck.test.ts index 5ca04a1bdb5b..a3043a5e6647 100644 --- a/testing/tests/filecheck.test.ts +++ b/testing/tests/filecheck.test.ts @@ -1,15 +1,16 @@ import fs from "node:fs"; -import path from "node:path"; +import { fileURLToPath } from "node:url"; -import { checkFile } from "../../filecheck/checker"; +import { checkFile } from "../../filecheck/checker.js"; -const dirname = __dirname; - -const SAMPLES_DIRECTORY = path.join(dirname, "filechecker", "samplefiles-html"); +const SAMPLES_DIRECTORY = new URL( + "filechecker/samplefiles-html/", + import.meta.url +); describe("checking files", () => { it("should spot SVGs with scripts inside them", async () => { - const filePath = path.join(SAMPLES_DIRECTORY, "script.svg"); + const filePath = fileURLToPath(new URL("./script.svg", SAMPLES_DIRECTORY)); // Sanity check the test itself console.assert(fs.existsSync(filePath), `${filePath} does not exist`); await expect(checkFile(filePath)).rejects.toThrow( @@ -17,7 +18,9 @@ describe("checking files", () => { ); }); it("should spot SVGs with onLoad inside an element", async () => { - const filePath = path.join(SAMPLES_DIRECTORY, "onhandler.svg"); + const filePath = fileURLToPath( + new URL("./onhandler.svg", SAMPLES_DIRECTORY) + ); // Sanity check the test itself console.assert(fs.existsSync(filePath), `${filePath} does not exist`); await expect(checkFile(filePath)).rejects.toThrow( @@ -26,16 +29,15 @@ describe("checking files", () => { }); it("should spot files that are not mentioned in source", async () => { - const filePath = path.join(SAMPLES_DIRECTORY, "orphan.png"); + const filePath = fileURLToPath(new URL("./orphan.png", SAMPLES_DIRECTORY)); // Sanity check the test itself console.assert(fs.existsSync(filePath), `${filePath} does not exist`); await expect(checkFile(filePath)).rejects.toThrow("is not mentioned in"); }); it("should spot files that are not mentioned in md source", async () => { - const filePath = path.join( - path.join(dirname, "filechecker", "samplefiles-md"), - "orphan.png" + const filePath = fileURLToPath( + new URL("./filechecker/samplefiles-md/orphan.png", import.meta.url) ); // Sanity check the test itself console.assert(fs.existsSync(filePath), `${filePath} does not exist`); @@ -43,14 +45,14 @@ describe("checking files", () => { }); it("should spot files that are completely empty", async () => { - const filePath = path.join(SAMPLES_DIRECTORY, "zero.gif"); + const filePath = fileURLToPath(new URL("./zero.gif", SAMPLES_DIRECTORY)); // Sanity check the test itself console.assert(fs.existsSync(filePath), `${filePath} does not exist`); await expect(checkFile(filePath)).rejects.toThrow("is 0 bytes"); }); it("should spot mismatch between file-type and file extension", async () => { - const filePath = path.join(SAMPLES_DIRECTORY, "png.jpeg"); + const filePath = fileURLToPath(new URL("./png.jpeg", SAMPLES_DIRECTORY)); // Sanity check the test itself console.assert(fs.existsSync(filePath), `${filePath} does not exist`); await expect(checkFile(filePath)).rejects.toThrow( diff --git a/testing/tests/index.test.ts b/testing/tests/index.test.ts index ae35c3ae7c71..4dab48c34620 100644 --- a/testing/tests/index.test.ts +++ b/testing/tests/index.test.ts @@ -2,14 +2,16 @@ import fs from "node:fs"; import path from "node:path"; import cheerio from "cheerio"; -import sizeOf from "image-size"; +import imagesize from "image-size"; + +const { default: sizeOf } = imagesize; import type { BCDSection, Doc, ProseSection, SpecificationsSection, -} from "../../libs/types/document"; +} from "../../libs/types/document.js"; const buildRoot = path.join("client", "build"); diff --git a/testing/tests/redirects.test.ts b/testing/tests/redirects.test.ts index 0a791adc6ee7..d737aad4ac43 100644 --- a/testing/tests/redirects.test.ts +++ b/testing/tests/redirects.test.ts @@ -1,6 +1,8 @@ -import got from "got"; +import gotPkg from "got"; import braces from "braces"; +const { default: got } = gotPkg; + function serverURL(pathname = "/") { const PORT = parseInt(process.env.SERVER_PORT || "5042"); return `http://localhost:${PORT}${pathname}`; diff --git a/testing/tsconfig.json b/testing/tsconfig.json deleted file mode 100644 index d7b4f968356b..000000000000 --- a/testing/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../tsconfig.json", - "include": ["tests"], - "compilerOptions": { - "noEmit": true - } -} diff --git a/tool/build-robots-txt.ts b/tool/build-robots-txt.ts index 626b7761cb23..94d17874fe4e 100644 --- a/tool/build-robots-txt.ts +++ b/tool/build-robots-txt.ts @@ -5,8 +5,8 @@ */ import fs from "node:fs"; -import { VALID_LOCALES } from "../libs/constants"; -import { ALWAYS_ALLOW_ROBOTS } from "../libs/env"; +import { VALID_LOCALES } from "../libs/constants/index.js"; +import { ALWAYS_ALLOW_ROBOTS } from "../libs/env/index.js"; const ALLOW_TEXT = ` User-agent: * diff --git a/tool/cli.ts b/tool/cli.ts index cdf8784c33bf..b40bff822345 100644 --- a/tool/cli.ts +++ b/tool/cli.ts @@ -1,43 +1,49 @@ #!/usr/bin/env node -import { isValidLocale } from "../libs/locale-utils"; -import type { Doc } from "../libs/types/document"; import fs from "node:fs"; import path from "node:path"; +import { fileURLToPath } from "node:url"; + import { fdir, PathsOutput } from "fdir"; import frontmatter from "front-matter"; -import { program } from "@caporal/core"; +import caporal from "@caporal/core"; import chalk from "chalk"; -import { prompt } from "inquirer"; +import inquirer from "inquirer"; import openEditor from "open-editor"; import open from "open"; import log from "loglevel"; +import { Action, ActionParameters, Logger } from "types"; -const dirname = __dirname; - -import { DEFAULT_LOCALE, VALID_LOCALES } from "../libs/constants"; -import { CONTENT_ROOT, CONTENT_TRANSLATED_ROOT } from "../libs/env"; -import { Redirect, Document, buildURL, getRoot } from "../content"; -import { buildDocument, gatherGitHistory, buildSPAs } from "../build"; - -import { VALID_FLAW_CHECKS } from "../libs/constants"; +import { + DEFAULT_LOCALE, + VALID_LOCALES, + VALID_FLAW_CHECKS, +} from "../libs/constants/index.js"; +import { Redirect, Document, buildURL, getRoot } from "../content/index.js"; +import { buildDocument, gatherGitHistory, buildSPAs } from "../build/index.js"; +import { isValidLocale } from "../libs/locale-utils/index.js"; +import type { Doc } from "../libs/types/document.js"; import { ALWAYS_ALLOW_ROBOTS, BUILD_OUT_ROOT, + CONTENT_ROOT, + CONTENT_TRANSLATED_ROOT, GOOGLE_ANALYTICS_ACCOUNT, GOOGLE_ANALYTICS_DEBUG, -} from "../libs/env"; -import { runMakePopularitiesFile } from "./popularities"; -import { runOptimizeClientBuild } from "./optimize-client-build"; -import { runBuildRobotsTxt } from "./build-robots-txt"; -import { syncAllTranslatedContent } from "./sync-translated-content"; -import * as kumascript from "../kumascript"; -import { Action, ActionParameters, Logger } from "types"; +} from "../libs/env/index.js"; +import { runMakePopularitiesFile } from "./popularities.js"; +import { runOptimizeClientBuild } from "./optimize-client-build.js"; +import { runBuildRobotsTxt } from "./build-robots-txt.js"; +import { syncAllTranslatedContent } from "./sync-translated-content.js"; +import { macroUsageReport } from "./macro-usage-report.js"; +import * as kumascript from "../kumascript/index.js"; import { MacroInvocationError, MacroRedirectedLinkError, -} from "../kumascript/src/errors"; -import { macroUsageReport } from "./macro-usage-report"; +} from "../kumascript/src/errors.js"; + +const { program } = caporal; +const { prompt } = inquirer; const PORT = parseInt(process.env.SERVER_PORT || "5042"); @@ -820,7 +826,7 @@ program "Convert an AWS Athena log aggregation CSV into a popularities.json file" ) .option("--outfile <path>", "output file", { - default: path.resolve(path.join(dirname, "..", "popularities.json")), + default: fileURLToPath(new URL("../popularities.json", import.meta.url)), }) .option("--max-uris <number>", "limit to top <number> entries", { default: MAX_GOOGLE_ANALYTICS_URIS, @@ -897,7 +903,7 @@ program if (account) { const dntHelperCode = fs .readFileSync( - path.join(dirname, "mozilla.dnthelper.min.js"), + new URL("mozilla.dnthelper.min.js", import.meta.url), "utf-8" ) .trim(); diff --git a/tool/macro-usage-report.ts b/tool/macro-usage-report.ts index 8888a9e624a3..ced92407e313 100644 --- a/tool/macro-usage-report.ts +++ b/tool/macro-usage-report.ts @@ -1,15 +1,18 @@ import fs from "node:fs/promises"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import { spawn } from "node:child_process"; -import { ACTIVE_LOCALES, DEFAULT_LOCALE } from "../libs/constants"; -import { CONTENT_ROOT, CONTENT_TRANSLATED_ROOT } from "../libs/env"; +import { ACTIVE_LOCALES, DEFAULT_LOCALE } from "../libs/constants/index.js"; +import { CONTENT_ROOT, CONTENT_TRANSLATED_ROOT } from "../libs/env/index.js"; -const YARI = path.normalize(path.join(__dirname, "..")); -const MACRO_PATH = path.join(YARI, "kumascript", "macros"); +const YARI_URL = new URL("..", import.meta.url); +const MACROS_URL = new URL("kumascript/macros", YARI_URL); +const YARI_PATH = fileURLToPath(YARI_URL); +const MACROS_PATH = fileURLToPath(MACROS_URL); async function getMacros(): Promise<string[]> { - const macroFilenames = await fs.readdir(MACRO_PATH); + const macroFilenames = await fs.readdir(MACROS_PATH); const macros = macroFilenames .map((filename) => path.basename(filename, ".ejs")) .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); @@ -61,7 +64,9 @@ async function getFilesByMacro( `\\{\\{\\s*(${macroNames.join("|")})\\b`, [CONTENT_ROOT, CONTENT_TRANSLATED_ROOT].filter(Boolean) ), - findMatches(`template\\(["'](${macroNames.join("|")})["']`, [MACRO_PATH]), + findMatches(`template\\(["'](${macroNames.join("|")})["']`, [ + MACROS_PATH, + ]), ]) ).flat(); @@ -113,7 +118,7 @@ async function filterDeprecatedMacros(macros: string[]) { } async function isMacroDeprecated(macro: string) { - const file = path.join(MACRO_PATH, `${macro}.ejs`); + const file = path.join(MACROS_PATH, `${macro}.ejs`); const content = await fs.readFile(file, "utf-8"); return content.includes("mdn.deprecated()"); @@ -138,7 +143,7 @@ async function writeMarkdownTable( } ) { const columns = ["yari"]; - const paths = [MACRO_PATH]; + const paths = [MACROS_PATH]; for (const locale of ACTIVE_LOCALES) { const path = getPathByLocale(locale); @@ -194,7 +199,7 @@ function writeJson( file .replace(CONTENT_ROOT, "content") .replace(CONTENT_TRANSLATED_ROOT, "translated-content") - .replace(YARI, "yari") + .replace(YARI_PATH, "yari") ), }; } diff --git a/tool/popularities.ts b/tool/popularities.ts index 380d2ddd391b..dd733fab5077 100644 --- a/tool/popularities.ts +++ b/tool/popularities.ts @@ -12,7 +12,9 @@ import fs from "node:fs"; import * as csv from "@fast-csv/parse"; -import got from "got"; +import gotPkg from "got"; + +const { default: got } = gotPkg; const CURRENT_URL = "https://mdn-popularities-prod.s3.amazonaws.com/current.txt"; diff --git a/tool/sync-translated-content.ts b/tool/sync-translated-content.ts index a307dd361ae4..c9ed47565ee6 100644 --- a/tool/sync-translated-content.ts +++ b/tool/sync-translated-content.ts @@ -13,14 +13,14 @@ import { slugToFolder, Document, Redirect, -} from "../content"; +} from "../content/index.js"; import { HTML_FILENAME, MARKDOWN_FILENAME, VALID_LOCALES, -} from "../libs/constants"; -import { CONTENT_ROOT, CONTENT_TRANSLATED_ROOT } from "../libs/env"; -import { DocFrontmatter } from "../libs/types/document"; +} from "../libs/constants/index.js"; +import { CONTENT_ROOT, CONTENT_TRANSLATED_ROOT } from "../libs/env/index.js"; +import { DocFrontmatter } from "../libs/types/document.js"; const CONFLICTING = "conflicting"; const ORPHANED = "orphaned"; diff --git a/tool/tsconfig.json b/tool/tsconfig.json deleted file mode 100644 index 8f6beb0e0a02..000000000000 --- a/tool/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "importsNotUsedAsValues": "remove" - }, - "include": ["."] -} diff --git a/tsconfig.dist.json b/tsconfig.dist.json index e2e01253c053..425acf7505c3 100644 --- a/tsconfig.dist.json +++ b/tsconfig.dist.json @@ -1,8 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "sourceMap": true + "noEmit": false }, "include": ["build", "content", "filecheck", "markdown", "server", "tool"] } diff --git a/tsconfig.json b/tsconfig.json index 1d39a0bff52b..b04b539ef81b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,17 +4,27 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "jsx": "react-jsx", - "moduleResolution": "node", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "noImplicitAny": false, + "paths": { + "front-matter": ["./type-fixes/front-matter.js"], + "@mdn/browser-compat-data/types": [ + "./node_modules/@mdn/browser-compat-data/types.d.ts" + ] + }, + "preserveWatchOutput": true, "resolveJsonModule": true, "skipLibCheck": true, - "target": "ES2020", - "preserveWatchOutput": true, + "sourceMap": true, "strict": true, - "noImplicitAny": false, - "strictNullChecks": false + "strictNullChecks": false, + "target": "ES2020" }, - "files": [], "ts-node": { + "esm": true, "swc": true - } + }, + "exclude": ["client", "ssr"] } diff --git a/type-fixes/front-matter.ts b/type-fixes/front-matter.ts new file mode 100644 index 000000000000..cd6b4835aa21 --- /dev/null +++ b/type-fixes/front-matter.ts @@ -0,0 +1,5 @@ +import frontmatter from "../node_modules/front-matter/index.js"; + +// frontmatter 4.0.2 exposes incorrect types for an esm consumer: +// types are on frontmatter.default, however frontmatter.default is actually undefined +export default frontmatter as unknown as typeof frontmatter.default; diff --git a/yarn.lock b/yarn.lock index 8cf46ca57004..18d83743e66e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2262,10 +2262,10 @@ dependencies: "@types/node" "*" -"@types/imagemin@^8.0.0": - version "8.0.0" - resolved "https://registry.npmmirror.com/@types/imagemin/-/imagemin-8.0.0.tgz#bf5bbe1feff3b112c7e0de06d024712ad261e033" - integrity sha512-B9X2CUeDv/uUeY9CqkzSTfmsLkeJP6PkmXlh4lODBbf9SwpmNuLS30WzUOi863dgsjY3zt3gY5q2F+UdifRi1A== +"@types/imagemin@^7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@types/imagemin/-/imagemin-7.0.1.tgz#11ca1e65ccb3871a8469d9b23033b95d3838eda0" + integrity sha512-xEn5+M3lDBtI3JxLy6eU3ksoVurygnlG7OYhTqJfGGP4PcvYnfn+IABCmMve7ziM/SneHDm5xgJFKC8hCYPicw== dependencies: "@types/node" "*"