From a74b9cd7d9119f32651cce940ab368a238e01e4a Mon Sep 17 00:00:00 2001 From: Janpot <2109932+Janpot@users.noreply.github.com> Date: Tue, 14 Oct 2025 11:23:26 +0200 Subject: [PATCH] [coe-infra] Add types for markdown loader --- packages/markdown/extractImports.mjs | 6 + packages/markdown/loader.mjs | 193 ++++++++++++++++++-------- packages/markdown/prepareMarkdown.mjs | 18 ++- 3 files changed, 157 insertions(+), 60 deletions(-) diff --git a/packages/markdown/extractImports.mjs b/packages/markdown/extractImports.mjs index 9d21c13b328f1d..6fddfd56999060 100644 --- a/packages/markdown/extractImports.mjs +++ b/packages/markdown/extractImports.mjs @@ -1,6 +1,12 @@ +// @ts-check + const importModuleRegexp = /^\s*import (?:["'\s]*(?:[\w*{}\n, ]+)from\s*)?["'\s]*([^"'{}$\s]+)["'\s].*/gm; +/** + * @param {string} code + * @returns {string[]} + */ export default function extractImports(code) { return (code.match(importModuleRegexp) || []).map((x) => x.replace(importModuleRegexp, '$1')); } diff --git a/packages/markdown/loader.mjs b/packages/markdown/loader.mjs index 76ba437b2e6f2a..5a68a3fb78e23d 100644 --- a/packages/markdown/loader.mjs +++ b/packages/markdown/loader.mjs @@ -1,3 +1,5 @@ +// @ts-check + import { promises as fs, readdirSync, statSync } from 'fs'; import path from 'path'; import prepareMarkdown from './prepareMarkdown.mjs'; @@ -28,9 +30,24 @@ function moduleIDToJSIdentifier(moduleID) { .join(''); } +/** + * @typedef {Record> } ComponentPackageMapping + */ + +/** @type {ComponentPackageMapping | null} */ let componentPackageMapping = null; +/** + * @typedef {Object} Package + * @property {string[]} paths + * @property {string} productId + */ + +/** + * @param {Package[]} packages + */ function findComponents(packages) { + /** @type {ComponentPackageMapping} */ const mapping = {}; packages.forEach((pkg) => { @@ -57,7 +74,48 @@ function findComponents(packages) { } /** - * @type {import('webpack').loader.Loader} + * @typedef {Object} LoaderOptions + * @property {Package[]} packages + * @property {string[]} languagesInProgress + * @property {string} workspaceRoot + */ + +/** + * @typedef {Object} ModuleData + * @property {string} module + * @property {string} raw + */ + +/** + * @typedef {Object} Translation + * @property {string} filename + * @property {string} userLanguage + * @property {string} [markdown] + */ + +/** + * @typedef {Object} Demo + * @property {string} module + * @property {string} [moduleTS] + * @property {string} [moduleTailwind] + * @property {string} [moduleTSTailwind] + * @property {string} [moduleCSS] + * @property {string} [moduleTSCSS] + * @property {string} raw + * @property {string} [rawTS] + * @property {string} [rawTailwind] + * @property {string} [rawTailwindTS] + * @property {string} [rawCSS] + * @property {string} [rawCSSTS] + * @property {string} [jsxPreview] + * @property {string} [tailwindJsxPreview] + * @property {string} [cssJsxPreview] + * @property {Object.} [relativeModules] + */ + +/** + * @type {import('webpack').LoaderDefinitionFunction} + * @this {import('webpack').LoaderContext} */ export default async function demoLoader() { const englishFilepath = this.resourcePath; @@ -71,41 +129,42 @@ export default async function demoLoader() { const files = await fs.readdir(path.dirname(englishFilepath)); const translations = await Promise.all( - files - .map((filename) => { - if (filename === `${englishFilename}.md`) { - return { - filename, - userLanguage: 'en', - }; - } + /** @type {Translation[]} */ ( + files + .map((filename) => { + if (filename === `${englishFilename}.md`) { + return { + filename, + userLanguage: 'en', + }; + } - const matchNotEnglishMarkdown = filename.match(notEnglishMarkdownRegExp); - - if ( - filename.startsWith(englishFilename) && - matchNotEnglishMarkdown !== null && - options.languagesInProgress.includes(matchNotEnglishMarkdown[1]) - ) { - return { - filename, - userLanguage: matchNotEnglishMarkdown[1], - }; - } + const matchNotEnglishMarkdown = filename.match(notEnglishMarkdownRegExp); + + if ( + filename.startsWith(englishFilename) && + matchNotEnglishMarkdown !== null && + options.languagesInProgress.includes(matchNotEnglishMarkdown[1]) + ) { + return { + filename, + userLanguage: matchNotEnglishMarkdown[1], + }; + } - return null; - }) - .filter((translation) => translation) - .map(async (translation) => { - const filepath = path.join(path.dirname(englishFilepath), translation.filename); - this.addDependency(filepath); - const markdown = await fs.readFile(filepath, { encoding: 'utf8' }); - - return { - ...translation, - markdown, - }; - }), + return null; + }) + .filter((translation) => translation) + ).map(async (translation) => { + const filepath = path.join(path.dirname(englishFilepath), translation.filename); + this.addDependency(filepath); + const markdown = await fs.readFile(filepath, { encoding: 'utf8' }); + + return { + ...translation, + markdown, + }; + }), ); // Use .. as the docs runs from the /docs folder @@ -121,25 +180,33 @@ export default async function demoLoader() { options, }); + /** @type {Record} */ const demos = {}; + /** @type {Set} */ const importedModuleIDs = new Set(); + /** @type {Record} */ const components = {}; + /** @type {Set} */ const demoModuleIDs = new Set(); + /** @type {Set} */ const componentModuleIDs = new Set(); + /** @type {Set} */ const nonEditableDemos = new Set(); + /** @type {Map>} */ const relativeModules = new Map(); + /** @type {string[]} */ const demoNames = Array.from( new Set( - docs.en.rendered - .filter((markdownOrComponentConfig) => { + /** @type {import('./prepareMarkdown.mjs').DemoEntry[]} */ ( + docs.en.rendered.filter((markdownOrComponentConfig) => { return typeof markdownOrComponentConfig !== 'string' && markdownOrComponentConfig.demo; }) - .map((demoConfig) => { - if (demoConfig.hideToolbar) { - nonEditableDemos.add(demoConfig.demo); - } - return demoConfig.demo; - }), + ).map((demoConfig) => { + if (demoConfig.hideToolbar) { + nonEditableDemos.add(demoConfig.demo); + } + return demoConfig.demo; + }), ), ); @@ -204,21 +271,21 @@ export default async function demoLoader() { /** * Inserts the moduleData into the relativeModules object - * @param string demoName - * @param {*} moduleData - * @param string variant - * @example updateRelativeModules(demoName, {module: 'constants.js', raw: ... }, 'JS') => demos[demoName].relativeModules[variant].push(moduleData) + * @param {string} demoName + * @param {ModuleData} moduleData + * @param {string} variant */ function updateRelativeModules(demoName, moduleData, variant) { - if (demos[demoName].relativeModules[variant]) { + const variantModule = /** @type {Object.} */ ( + demos[demoName].relativeModules + ); + if (variantModule[variant]) { // Avoid duplicates - if ( - !demos[demoName].relativeModules[variant].some((elem) => elem.module === moduleData.module) - ) { - demos[demoName].relativeModules[variant].push(moduleData); + if (!variantModule[variant].some((elem) => elem.module === moduleData.module)) { + variantModule[variant].push(moduleData); } } else { - demos[demoName].relativeModules[variant] = [moduleData]; + variantModule[variant] = [moduleData]; } } @@ -460,10 +527,15 @@ export default async function demoLoader() { demos[demoName].relativeModules = {}; } + /** @type {Record>} */ const addedModulesRelativeToModulePathPerVariant = {}; + const demoRelativeModules = /** @type {Map} */ ( + relativeModules.get(demoName) + ); + await Promise.all( - Array.from(relativeModules.get(demoName)).map(async ([relativeModuleID, variants]) => { + Array.from(demoRelativeModules).map(async ([relativeModuleID, variants]) => { for (const variant of variants) { addedModulesRelativeToModulePathPerVariant[variant] ??= new Set(); const addedModulesRelativeToModulePath = @@ -523,7 +595,9 @@ export default async function demoLoader() { // We are only iterating through an array that looks // like this: ['JS', 'TS'], so it is safe to await // eslint-disable-next-line no-await-in-loop - const rawEntry = await fs.readFile(entryModuleFilePath, { encoding: 'utf8' }); + const rawEntry = await fs.readFile(entryModuleFilePath, { + encoding: 'utf8', + }); extractImports(rawEntry).forEach((importModuleID) => { // detect relative import @@ -570,17 +644,18 @@ export default async function demoLoader() { }), ); + /** @type {string[]} */ const componentNames = Array.from( new Set( - docs.en.rendered - .filter((markdownOrComponentConfig) => { + /** @type {import('./prepareMarkdown.mjs').ComponentEntry[]} */ ( + docs.en.rendered.filter((markdownOrComponentConfig) => { return ( typeof markdownOrComponentConfig !== 'string' && markdownOrComponentConfig.component ); }) - .map((componentConfig) => { - return componentConfig.component; - }), + ).map((componentConfig) => { + return componentConfig.component; + }), ), ); diff --git a/packages/markdown/prepareMarkdown.mjs b/packages/markdown/prepareMarkdown.mjs index b12a073220b00d..b188e747e54ba2 100644 --- a/packages/markdown/prepareMarkdown.mjs +++ b/packages/markdown/prepareMarkdown.mjs @@ -11,6 +11,9 @@ import { getTitle, } from './parseMarkdown.mjs'; +/** + * @type {string | string[]} + */ const BaseUIReexportedComponents = []; /** @@ -47,17 +50,30 @@ function resolveComponentApiUrl(productId, componentPkg, component) { return `/${productId}/api/${kebabCase(component)}/`; } +/** + * @typedef {{ component: string, demo?: undefined }} ComponentEntry + * @typedef {{ component?: undefined, demo: string, hideToolbar?: boolean }} DemoEntry + */ + +/** + * @typedef {{ rendered: Array }} TranslatedDoc + */ + /** * @param {object} config * @param {Array<{ markdown: string, filename: string, userLanguage: string }>} config.translations - Mapping of locale to its markdown * @param {string} config.fileRelativeContext - posix filename relative to repository root directory * @param {object} config.options - provided to the webpack loader + * @param {string} config.options.workspaceRoot - The absolute path of the repository root directory + * @param {object} [config.componentPackageMapping] - Mapping of productId to mapping of component name to package name + * @example { 'material': { 'Button': 'mui-material' } } + * @returns {{ docs: Record }} - Mapping of locale to its prepared markdown */ function prepareMarkdown(config) { const { fileRelativeContext, translations, componentPackageMapping = {}, options } = config; /** - * @type {Record }>} + * @type {Record} */ const docs = {}; const headingHashes = {};