Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/markdown/extractImports.mjs
Original file line number Diff line number Diff line change
@@ -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'));
}
193 changes: 134 additions & 59 deletions packages/markdown/loader.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// @ts-check

import { promises as fs, readdirSync, statSync } from 'fs';
import path from 'path';
import prepareMarkdown from './prepareMarkdown.mjs';
Expand Down Expand Up @@ -28,9 +30,24 @@ function moduleIDToJSIdentifier(moduleID) {
.join('');
}

/**
* @typedef {Record<string, Record<string, string>> } 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) => {
Expand All @@ -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.<string, ModuleData[]>} [relativeModules]
*/

/**
* @type {import('webpack').LoaderDefinitionFunction<LoaderOptions>}
* @this {import('webpack').LoaderContext<LoaderOptions>}
*/
export default async function demoLoader() {
const englishFilepath = this.resourcePath;
Expand All @@ -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
Expand All @@ -121,25 +180,33 @@ export default async function demoLoader() {
options,
});

/** @type {Record<string, Demo>} */
const demos = {};
/** @type {Set<string>} */
const importedModuleIDs = new Set();
/** @type {Record<string, string>} */
const components = {};
/** @type {Set<string>} */
const demoModuleIDs = new Set();
/** @type {Set<string>} */
const componentModuleIDs = new Set();
/** @type {Set<string>} */
const nonEditableDemos = new Set();
/** @type {Map<string, Map<string, string[]>>} */
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;
}),
),
);

Expand Down Expand Up @@ -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.<string, ModuleData[]>} */ (
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];
}
}

Expand Down Expand Up @@ -460,10 +527,15 @@ export default async function demoLoader() {
demos[demoName].relativeModules = {};
}

/** @type {Record<string, Set<string>>} */
const addedModulesRelativeToModulePathPerVariant = {};

const demoRelativeModules = /** @type {Map<string, string[]>} */ (
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 =
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}),
),
);

Expand Down
18 changes: 17 additions & 1 deletion packages/markdown/prepareMarkdown.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import {
getTitle,
} from './parseMarkdown.mjs';

/**
* @type {string | string[]}
*/
const BaseUIReexportedComponents = [];

/**
Expand Down Expand Up @@ -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<string | ComponentEntry | DemoEntry> }} 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<string, TranslatedDoc> }} - Mapping of locale to its prepared markdown
*/
function prepareMarkdown(config) {
const { fileRelativeContext, translations, componentPackageMapping = {}, options } = config;

/**
* @type {Record<string, { rendered: Array<string | { component: string } | { demo:string }> }>}
* @type {Record<string, TranslatedDoc>}
*/
const docs = {};
const headingHashes = {};
Expand Down
Loading