UNPKG

storybook-addon-sdc

Version:

Drupal Single Directory Components as stories

1,236 lines (1,198 loc) 40.8 kB
// src/preset.ts import { existsSync as existsSync4, statSync as statSync2 } from "fs"; import { join as join5, dirname as dirname5, relative as relative4 } from "path"; // src/vite-plugin-storybook-yaml-stories.ts import { readdirSync as readdirSync2, readFileSync } from "fs"; import { parse as parseYaml } from "yaml"; import { join as join2, basename as basename2, dirname as dirname3, extname } from "path"; // src/logger.ts import pino from "pino"; var logger = pino({ transport: { target: "pino-pretty", options: { colorize: true } } }); // src/utils.ts import { dirname, sep } from "path"; var capitalize = (str) => str[0].toUpperCase() + str.slice(1); var convertToKebabCase = (str) => str.replace(/[-:]/g, ""); var toAttributes = (attrs) => { if (!attrs) return ""; return " " + Object.entries(attrs).map(([key, value]) => { if (Array.isArray(value)) { value = value.join(" "); } return `${key}="${value}"`; }).join(" "); }; var deriveGroupFromPath = (fileName) => { const dir = dirname(fileName); const parts = dir.split(sep).filter(Boolean); const lower = parts.map((p) => p.toLowerCase()); const index = lower.lastIndexOf("components"); if (index !== -1 && parts[index + 2]) { return parts[index + 1]; } return "SDC"; }; var sanitizeStoryKey = (key) => { if (/^\d/.test(key)) { return `_${key}`; } return key; }; // src/argsGenerator.ts import { generate } from "json-schema-faker"; var createGenerateOptions = (defs, jsonSchemaFakerOptions) => { const defsRecord = defs ?? {}; const userRefResolver = jsonSchemaFakerOptions.refResolver; if (Object.keys(defsRecord).length === 0 && !userRefResolver) { return jsonSchemaFakerOptions; } return { ...jsonSchemaFakerOptions, refResolver: async (ref) => { if (Object.prototype.hasOwnProperty.call(defsRecord, ref)) { return defsRecord[ref]; } if (userRefResolver) { return userRefResolver(ref); } return void 0; } }; }; var generateArgs = (schema, defs, jsonSchemaFakerOptions) => { return Object.entries(schema).reduce( async (accPromise, [key, property]) => { const acc = await accPromise; const schemaWithDefs = { ...property, ...defs ? { $defs: defs } : {} }; acc[key] = await generate(schemaWithDefs, jsonSchemaFakerOptions); if (!Array.isArray(acc[key]) && acc[key] instanceof Object && property.type !== "object") { acc[key] = Object.values(acc[key]); } return acc; }, Promise.resolve({}) ); }; var slotsToSchemaProperties = (slots) => { return Object.fromEntries( Object.entries(slots).map(([key, value]) => [ key, { type: "string", ...value } ]) ); }; async function generateStorybookArgs(content, jsonSchemaFakerOptions) { const { props, slots, $defs } = content; const generateOptions = createGenerateOptions($defs, jsonSchemaFakerOptions); const generatedArgs = { ...props?.properties && await generateArgs(props.properties, $defs, generateOptions), ...slots && await generateArgs( slotsToSchemaProperties(slots), $defs, generateOptions ) }; return generatedArgs; } // src/argTypesGenerator.ts var schemaToArgtypes = (prop) => ({ ...prop, ...prop.enum && { control: "radio", options: prop.enum } }); var argTypesGenerator_default = (content) => { const generated = content?.props?.properties ? Object.entries(content.props.properties).reduce((acc, [key, value]) => { acc[key] = schemaToArgtypes(value); return acc; }, {}) : {}; return generated; }; // src/storyNodeRender.ts var generateArgs2 = (args, isSlot = false) => Object.entries(args).map(([key, value]) => `${key}: ${formatArgValue(value, isSlot)},`).join("\n"); var formatArgValue = (value, isSlot) => { if (Array.isArray(value)) { const arrayContent = value.map((item) => storyNodeRenderer.render(item)); return `new TwigSafeArray(${arrayContent.join(", ")})`; } return storyNodeRenderer.render(value); }; var StoryNodeRenderService = class { renderer = []; register(renderers) { renderers.forEach((renderer) => { this.renderer.push(renderer); this.renderer.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)); }); } render(item) { const renderer = this.renderer.find((h) => h.appliesTo(item)); return renderer?.render ? renderer.render(item) : JSON.stringify(item); } }; var renderComponent = (item) => { const kebabCaseName = convertToKebabCase(item.component); const componentProps = `...{ ${generateArgs2(item.props ?? {}, false)}}, ...{${generateArgs2(item.slots ?? {}, true)}}`; const storyArgs = item.story ? `...${kebabCaseName}.${capitalize(item.story)}.args` : "...{}"; return `${kebabCaseName}.default.component({...${kebabCaseName}.Basic.baseArgs, ${storyArgs}, ${componentProps}})`; }; var renderImage = (item) => { return JSON.stringify( `<img src="${item.uri}"${toAttributes(item.attributes)}>` ); }; var renderElement = (item) => { return JSON.stringify( `<${item.tag ?? "div"}${toAttributes(item.attributes)}> ${item.value} </${item.tag ?? "div"}>` ); }; var renderMarkup = (item) => { return JSON.stringify(`${item.markup}`); }; var defaultStoryNodes = [ { appliesTo: (item) => item?.type === "component", render: (item) => renderComponent(item), priority: -4 }, { appliesTo: (item) => item?.theme === "image" || item?.type === "image", render: (item) => renderImage(item), priority: -1 }, { appliesTo: (item) => item?.type === "element" || item?.theme === "element", render: (item) => renderElement(item), priority: -2 }, { appliesTo: (item) => item?.type === "markup" || item?.theme === "markup", render: (item) => renderMarkup(item), priority: -2 } ]; var storyNodeRenderer = new StoryNodeRenderService(); storyNodeRenderer.register(defaultStoryNodes); // src/storiesGenerator.ts var storiesGenerator_default = (stories, componentGlobals = {}) => Object.entries(stories).map( ([ storyKey, { props = {}, slots = {}, variants = {}, description = "", name = void 0, library_wrapper = "", parameters = void 0, globals = void 0, thirdPartySettings = void 0 } ]) => { const storyParameters = thirdPartySettings?.sdcStorybook?.parameters ?? parameters ?? {}; const storyGlobals = thirdPartySettings?.sdcStorybook?.globals ?? globals ?? {}; const mergedGlobals = { ...componentGlobals, ...storyGlobals }; const globalsBlock = Object.keys(mergedGlobals).length > 0 ? ` globals: ${JSON.stringify(mergedGlobals, null, 2)},` : ""; const capitalizedKey = capitalize(storyKey); const exportName = capitalizedKey === "Basic" ? `Variant_${capitalizedKey}` : capitalizedKey; return ` export const ${exportName} = { parameters: {...${JSON.stringify(storyParameters, null, 2)}, ...{docs: {description: {story: ${JSON.stringify(description, null, 2)}}}}}, ${globalsBlock} name: ${JSON.stringify(name ?? capitalizedKey, null, 2)}, args: { ...Basic.baseArgs, ${processPropsAttributes(props)} ${generateArgs2(slots, true)} ${generateVariants(variants)} }, ${library_wrapper ? `decorators: [ (Story) => { const wrapper = ${JSON.stringify(library_wrapper)}; if (!wrapper) return Story(); // Replace {{ _story }} with the actual story component const wrappedHtml = wrapper.replace('{{ _story }}', Story()); return wrappedHtml; } ],` : ""} play: async ({ canvasElement }) => { Drupal.attachBehaviors(canvasElement, window.drupalSettings); }, }; `; } ).join("\n"); var generateVariants = (variants) => { return Object.entries(variants).map( ([variantKey, variantValue]) => `${variantKey}: ${JSON.stringify(variantValue.title)},` ).join("\n"); }; var processPropsAttributes = (props) => { if (!props || !props.attributes || Object.keys(props.attributes).length === 0) { return generateArgs2(props, false); } const { attributes, ...otherProps } = props; const propsArgs = generateArgs2(otherProps, false); const attributeEntries = Object.entries(attributes).map(([key, value]) => `['${key}', ${JSON.stringify(value)}]`).join(", "); return propsArgs + (propsArgs ? "\n" : "") + `defaultAttributes: [...Basic.baseArgs.defaultAttributes || [], ${attributeEntries}],`; }; // src/componentMetadata.ts import { dirname as dirname2, relative } from "path"; import { cwd } from "process"; var componentMetadata_default = (id, content) => { return { path: relative(cwd(), dirname2(id)), machineName: id, status: content.status || "stable", name: content.name, group: content.group }; }; // src/validateJson.ts import { Validator } from "jsonschema"; import fetch from "node-fetch"; var validator = new Validator(); var schemaCache = /* @__PURE__ */ new Map(); var fetchSchema = async (url) => { if (schemaCache.has(url)) { return schemaCache.get(url); } try { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch schema: ${response.statusText}`); } const schema = await response.json(); schemaCache.set(url, schema); return schema; } catch (error) { logger.error("Error fetching schema:", error); throw new Error(`Could not fetch schema from ${url}`); } }; var validateJson = async (data, schemaUrl) => { const rootSchema = await fetchSchema(schemaUrl); validator.addSchema(rootSchema, schemaUrl); const unresolvedRefs = validator.unresolvedRefs; while (unresolvedRefs.length > 0) { const refUrl = unresolvedRefs.shift(); const schema = await fetchSchema(refUrl); validator.addSchema(schema, refUrl); } const validationResult = validator.validate(data, rootSchema); if (validationResult.errors.length > 0) { logger.warn(` ${data.name}.component.yml has validation errors: ${validationResult.errors.map((error) => error.stack).join("\n")} `); } }; // src/namespaces.ts import { readdirSync, existsSync } from "fs"; import { basename, join, resolve, sep as sep2, relative as relative2 } from "path"; import { cwd as cwd2 } from "process"; import { normalizePath } from "vite"; var getProjectName = (p) => { const fullPath = resolve(p); const parts = fullPath.split(sep2); const i = parts.lastIndexOf("components"); if (i < 1) { throw new Error(`Could not find 'components' folder in path: ${fullPath}`); } return parts[i - 1]; }; var getAllSubdirectoriesRecursive = (baseDir) => { const result = []; const scan = (dir) => { try { const entries = readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { const fullPath = join(dir, entry.name); result.push(fullPath); scan(fullPath); } } } catch (error) { } }; scan(baseDir); return result; }; var resolveComponentPath = (namespace, component, namespaces) => { const baseDir = namespaces.findPath(namespace); if (!baseDir) return void 0; const componentFileName = `${component}.component.yml`; const directPath = join(baseDir, "components", component, componentFileName); if (existsSync(directPath)) return directPath; const componentsDir = join(baseDir, "components"); const directories = [ componentsDir, ...getAllSubdirectoriesRecursive(componentsDir) ]; for (const dir of directories) { const possiblePath = join(dir, component, componentFileName); if (existsSync(possiblePath)) return possiblePath; } return void 0; }; var Namespaces = class { namespaces; stripTrailingSlash = (p) => p.replace(/\/+$/g, ""); constructor(namespaceDefinition) { this.namespaces = namespaceDefinition.namespaces ?? {}; if (namespaceDefinition.namespace) { this.namespaces[namespaceDefinition.namespace] = cwd2(); } else { this.namespaces[basename(cwd2())] = cwd2(); } logger.info(`REGISTER NAMESPACES: ${JSON.stringify(this.namespaces)}`); } toViteAlias() { const aliases = []; Object.entries(this.namespaces).forEach(([namespace, path]) => { const hasComponents = existsSync(join(path, "components")); aliases.push({ find: "@" + namespace, replacement: normalizePath( hasComponents ? join(path, "components") : path ) }); }); logger.info(`REGISTER VITE ALIASES: ${JSON.stringify(aliases)}`); return aliases; } toTwigJsNamespaces() { let namespaces = {}; for (const [ns, path] of Object.entries(this.namespaces)) { const componentsPath = join(path, "components"); if (existsSync(componentsPath)) { namespaces[ns] = componentsPath; } else { namespaces[ns] = path; } } return namespaces; } toTwingNamespaces() { let namespaces = {}; for (const [ns, path] of Object.entries(this.namespaces)) { const componentsPath = join(path, "components"); if (existsSync(componentsPath)) { namespaces[ns] = [componentsPath]; } else { namespaces[ns] = [path]; } } return namespaces; } entries() { return Object.entries(this.namespaces); } findPath = (namespace) => { return this.namespaces[namespace] || ""; }; find = (fsPath) => { const fp = this.stripTrailingSlash(fsPath); let bestPath; let bestNs; for (const [ns, path] of Object.entries(this.namespaces)) { const base = this.stripTrailingSlash(path); if (fp === base || fp.startsWith(base + "/")) { if (!bestPath || base.length > bestPath.length) { bestPath = base; bestNs = ns; } } } return bestNs; }; pathToNamespace(fsPath, makeComponentIdFormat = false) { const ns = this.find(fsPath); if (!ns) { throw new Error( `Could not find valid 'namespace' for folder: ${fsPath} in namespaces: ${JSON.stringify(this.namespaces)}` ); } const rootPath = resolve(this.namespaces[ns]); const componentsDir = join(rootPath, "components"); if (existsSync(componentsDir)) { const isExact = fsPath === componentsDir; const isChild = fsPath.startsWith(componentsDir + sep2); if (!isExact && !isChild) { throw new Error( `Could not find 'components' folder in path: ${fsPath}, namespace: ${ns} (${Object.values(this.namespaces).join(", ")})` ); } let rel = relative2(componentsDir, fsPath); if (sep2 !== "/") { rel = rel.split(sep2).join("/"); } if (makeComponentIdFormat) { return `${ns}:${rel}`; } return `@${ns}/${rel}`; } return `@${ns}`; } }; var toNamespaces = (namespaceDefinition) => { return new Namespaces(namespaceDefinition ?? {}); }; // src/vite-plugin-storybook-yaml-stories.ts var readSDC = (filePath, defs, validate) => { const sdcSchema = { $defs: defs, ...parseYaml(readFileSync(filePath, "utf8")) }; if (typeof validate === "string" && validate.length > 0) { validateJson(sdcSchema, sdcSchema["$schema"] || validate); } return sdcSchema; }; var generateImports = (directory, namespaces) => { const componentName = basename2(directory); return readdirSync2(directory).filter((file) => { if (extname(file) === ".yml") { return file.endsWith(".story.yml"); } return [".css", ".js", ".mjs", ".twig"].includes(extname(file)); }).map((file) => { const filePath = `./${file}`; const namespace = namespaces.pathToNamespace(directory); logger.info(`IMPORT ASSET ${directory}/${file}`); if (extname(file) === ".twig") { const fileName = basename2(file, ".twig"); if (fileName === componentName) { return `import COMPONENT from '${namespace}/${file}';`; } logger.info(`Skipping variant template: ${file}`); return ""; } return `import '${filePath}';`; }).filter(Boolean).join("\n"); }; var dynamicImports = (stories, namespaces) => { const imports = /* @__PURE__ */ new Set(); const importComponent = (item) => { const [namespace, componentName] = item.component.split(":"); const resolvedPath = resolveComponentPath( namespace, componentName, namespaces ); const kebabCaseName = convertToKebabCase(item.component); if (resolvedPath) { imports.add(`import * as ${kebabCaseName} from '${resolvedPath}';`); } }; const extractComponentImports = (args) => { Object.values(args).forEach((value) => { if (Array.isArray(value)) { value.forEach((item) => { if (item.type === "component") { importComponent(item); } extractComponentImports(value); }); } else if (value && typeof value === "object") { if (value.type === "component") { importComponent(value); } extractComponentImports(value); } }); }; Object.values(stories).forEach( ({ slots = {}, props = {} }) => extractComponentImports({ ...slots, ...props }) ); return Array.from(imports).join("\n"); }; var createStoryIndex = (fileName, baseTitle, stories, disabledStories, tags) => { const storiesIndex = []; const isAllDisabled = disabledStories.includes("all"); if (!isAllDisabled && !disabledStories.includes("basic")) { storiesIndex.push({ type: "story", importPath: fileName, exportName: "Basic", title: baseTitle, tags }); } if (stories && !isAllDisabled) { Object.keys(stories).forEach((storyKey) => { if (!disabledStories.includes(storyKey)) { const capitalizedKey = capitalize(storyKey); const exportName = capitalizedKey === "Basic" ? `Variant_${capitalizedKey}` : capitalizedKey; storiesIndex.push({ type: "story", importPath: fileName, exportName, title: baseTitle, tags }); } }); } return storiesIndex; }; var vite_plugin_storybook_yaml_stories_default = ({ jsonSchemaFakerOptions = {}, sdcStorybookOptions = {}, globalDefs = {}, namespaces = {} }) => ({ name: "vite-plugin-storybook-yaml-stories", async load(id) { if (id.endsWith("story.yml")) { return ""; } if (!id.endsWith("component.yml")) return; try { const content = readSDC(id, globalDefs, sdcStorybookOptions.validate); const imports = generateImports(dirname3(id), namespaces); const previewsStories = { ...content.thirdPartySettings?.sdcStorybook?.stories || {}, ...loadStoryFilesSync(id) }; storyNodeRenderer.register(sdcStorybookOptions.storyNodesRenderer ?? []); const storiesImports = dynamicImports(previewsStories, namespaces); const metadata = componentMetadata_default(id, content); const componentGlobals = content?.thirdPartySettings?.sdcStorybook?.globals ?? {}; const argTypes = { componentMetadata: { table: { disable: true } }, defaultAttributes: { table: { disable: true } }, ...content.variants && { variant: { control: "select", options: Object.keys(content.variants) } }, ...argTypesGenerator_default(content) }; const baseArgs = { defaultAttributes: [ ["data-component-id", namespaces.pathToNamespace(dirname3(id), true)] ], componentMetadata: metadata, ...content.variants && { variant: Object.keys(content.variants)[0] } }; const generatedArgs = await generateStorybookArgs(content, jsonSchemaFakerOptions); const args = sdcStorybookOptions.useBasicArgsForStories ? { ...baseArgs, ...generatedArgs } : baseArgs; const basicArgs = sdcStorybookOptions.useBasicArgsForStories ? baseArgs : { ...baseArgs, ...generatedArgs }; const stories = previewsStories ? storiesGenerator_default(previewsStories, componentGlobals) : ""; return ` ${imports} ${storiesImports} class TwigSafeArray extends Array { toString() { return this.join(''); } } export default { component: COMPONENT, parameters: {...${JSON.stringify(content?.thirdPartySettings?.sdcStorybook?.parameters ?? {}, null, 2)}, ...{docs: {description: {component: ${JSON.stringify(content.description, null, 2)}}}}}, ${Object.keys(componentGlobals).length > 0 ? `globals: ${JSON.stringify(componentGlobals, null, 2)},` : ""} argTypes: ${JSON.stringify(argTypes, null, 2)}, args: ${JSON.stringify(args, null, 2)}, }; export const Basic = { args: ${JSON.stringify(basicArgs, null, 2)}, baseArgs: ${JSON.stringify(args, null, 2)}, play: async ({ canvasElement }) => { Drupal.attachBehaviors(canvasElement, window.drupalSettings); }, }; ${stories} `; } catch (error) { logger.error(`Error loading component YAML file: ${id}, ${error}`); throw error; } } }); var yamlStoriesIndexer = { test: /component\.yml$/, createIndex: async (fileName, { makeTitle }) => { try { const content = readSDC(fileName); const group = content.group || deriveGroupFromPath(fileName); const baseTitle = makeTitle( `${getProjectName(fileName)}/${capitalize(group)}/${content.name}` ); const stories = content.thirdPartySettings?.sdcStorybook?.stories; const storiesContent = loadStoryFilesSync(fileName); const mergedStories = { ...stories, ...storiesContent }; const tags = content?.thirdPartySettings?.sdcStorybook?.tags ?? []; const oldDisableBasicStory = content.thirdPartySettings?.sdcStorybook?.disableBasicStory; const newDisabledStories = content.thirdPartySettings?.sdcStorybook?.disabledStories; let disabledStories = []; if (oldDisableBasicStory === true) { disabledStories = ["basic"]; } else if (newDisabledStories) { disabledStories = newDisabledStories; } return createStoryIndex( fileName, baseTitle, mergedStories, disabledStories, tags ); } catch (error) { logger.error(`Error creating index for YAML file: ${fileName}, ${error}`); throw error; } } }; var loadStoryFilesSync = (fileName) => { const folderPath = dirname3(fileName); const storyFiles = readdirSync2(folderPath).filter((file) => file.endsWith(".story.yml")).map((file) => join2(folderPath, file)); return storyFiles.reduce( (acc, file) => { const content = readFileSync(file, "utf8"); const rawKey = basename2(file).split(".")[1]; const key = sanitizeStoryKey(rawKey); return { ...acc, [key]: parseYaml(content) }; }, {} ); }; // src/preset.ts import { mergeConfig } from "vite"; // src/definitions.ts import { parse } from "yaml"; import { readFileSync as readFileSync2 } from "fs"; import fetch2 from "node-fetch"; async function loadExternalDef(defPath) { try { if (defPath.startsWith("http://") || defPath.startsWith("https://")) { const response = await fetch2(defPath); if (!response.ok) { throw new Error(`Failed to fetch ${defPath}: ${response.statusText}`); } const content = await response.text(); return parse(content); } else { const content = readFileSync2(defPath, "utf8"); return parse(content); } } catch (error) { logger.error(`Error loading external definition from ${defPath}: ${error}`); throw error; } } async function loadAndMergeDefinitions(externalDefs, customDefs) { const globalDefs = {}; if (externalDefs) { await Promise.all( externalDefs.map(async (defPath) => { const def = await loadExternalDef(defPath); Object.entries(def).forEach(([component, schema]) => { globalDefs[component] = schema; }); }) ); } if (customDefs) { Object.entries(customDefs).forEach(([component, schema]) => { globalDefs[component] = schema; }); } if (Object.keys(globalDefs).length > 0) { logger.info( `REGISTER CUSTOM DEFINITIONS ${JSON.stringify(Object.keys(globalDefs))}` ); } return globalDefs; } // src/constants.ts var DEFAULT_ADDON_OPTIONS = { sdcStorybookOptions: { useBasicArgsForStories: true, twigLib: "twig", validate: false // validate: // 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json', }, vitePluginTwingDrupalOptions: { include: /\.twig(\?.*)?$/ }, vitePluginTwigDrupalOptions: {}, jsonSchemaFakerOptions: { failOnInvalidTypes: false, useExamplesValue: true, useDefaultValue: true } }; // src/preset.ts import { merge as lodashMerge } from "lodash-es"; // src/vite-plugin-sdc-icon-packs.ts import { existsSync as existsSync3 } from "fs"; import { join as join4 } from "path"; // src/icon-packs.ts import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync as readdirSync3, statSync } from "fs"; import { join as join3, resolve as resolve2, basename as basename3, dirname as dirname4, extname as extname2, relative as relative3 } from "path"; import { parse as parseYaml2 } from "yaml"; function parseSvgFile(raw) { const attrs = {}; const openTagMatch = raw.match(/<svg([^>]*)>/i); if (openTagMatch) { const attrRegex = /([\w:.-]+)=(?:"([^"]*)"|'([^']*)')/g; let m; while ((m = attrRegex.exec(openTagMatch[1])) !== null) { attrs[m[1]] = m[2] ?? m[3] ?? ""; } } const innerMatch = raw.match(/<svg[^>]*>([\s\S]*)<\/svg>/i); const content = innerMatch ? innerMatch[1].trim() : ""; return { content, attrs }; } function scanDir(dir, baseDir, filterExt) { const results = []; let entries; try { entries = readdirSync3(dir, { withFileTypes: true }); } catch { return results; } for (const entry of entries) { const fullPath = join3(dir, entry.name); if (entry.isDirectory()) { results.push(...scanDir(fullPath, baseDir, filterExt)); } else if (entry.isFile()) { const ext = extname2(entry.name); if (filterExt && ext.toLowerCase() !== filterExt.toLowerCase()) continue; results.push({ absPath: fullPath, iconId: basename3(entry.name, ext), group: relative3(baseDir, dir) }); } } return results; } function resolveSourceEntry(rawSource, nsRoot) { if (/^https?:\/\//.test(rawSource)) { return [{ absPath: rawSource, iconId: "*", group: "" }]; } const absPath = resolve2(nsRoot, rawSource); const extMatch = rawSource.match(/\*(\.[^/{}*]+)$/); const filterExt = extMatch ? extMatch[1] : null; const wildcardIdx = rawSource.search(/[*{]/); const dirPart = wildcardIdx >= 0 ? rawSource.substring(0, wildcardIdx) : rawSource; const baseDir = resolve2(nsRoot, dirPart.replace(/[/\\]$/, "")); if (!rawSource.includes("*") && !rawSource.includes("{") && existsSync2(absPath)) { try { if (statSync(absPath).isFile()) { return [ { absPath, iconId: basename3(absPath, extname2(absPath)), group: "" } ]; } } catch { } } if (!existsSync2(baseDir)) return []; return scanDir(baseDir, baseDir, filterExt); } function loadIconPackFile(iconsFilePath) { const watchFiles = [iconsFilePath]; const packs = {}; if (!existsSync2(iconsFilePath)) return { packs, watchFiles }; const nsRoot = dirname4(iconsFilePath); const ns = basename3(iconsFilePath, ".icons.yml"); let raw; try { raw = parseYaml2(readFileSync3(iconsFilePath, "utf8")); } catch { return { packs, watchFiles }; } if (!raw || typeof raw !== "object") return { packs, watchFiles }; const toUrl = (absPath) => `/sdc-icons/${ns}/${relative3(nsRoot, absPath).replace(/\\/g, "/")}`; for (const [packId, packDef] of Object.entries(raw)) { if (!packDef || typeof packDef !== "object") continue; if (packDef.enabled === false) continue; const extractor = packDef.extractor ?? "svg"; const rawSources = packDef.config?.sources ?? []; const sources = []; const sourceUrls = []; for (const src of rawSources) { if (/^https?:\/\//.test(src)) { sources.push(src); sourceUrls.push(src); } else { const absFile = resolve2(nsRoot, src); const absBase = resolve2( nsRoot, src.replace(/[*{].*$/, "").replace(/[/\\]$/, "") ); const isSpriteFile = extractor === "svg_sprite" && existsSync2(absFile) && statSync(absFile).isFile(); const finalAbs = isSpriteFile ? absFile : absBase; sources.push(finalAbs); sourceUrls.push(toUrl(finalAbs)); if (isSpriteFile) watchFiles.push(absFile); } } const svgIcons = {}; const pathIcons = {}; if (extractor === "svg") { for (const src of rawSources) { for (const file of resolveSourceEntry(src, nsRoot)) { if (file.iconId === "*") continue; watchFiles.push(file.absPath); try { const { content, attrs } = parseSvgFile( readFileSync3(file.absPath, "utf8") ); svgIcons[file.iconId] = { content, attrs, sourceUrl: toUrl(file.absPath), group: file.group }; } catch { } } } } else if (extractor === "path") { for (const src of rawSources) { for (const file of resolveSourceEntry(src, nsRoot)) { if (file.iconId === "*") continue; pathIcons[file.iconId] = { sourceUrl: toUrl(file.absPath), group: file.group }; } } } packs[packId] = { packId, label: packDef.label ?? packId, extractor, sources, sourceUrls, settings: packDef.settings ?? {}, template: packDef.template ?? "", svgIcons, pathIcons }; } return { packs, watchFiles }; } // src/vite-plugin-sdc-icon-packs.ts var VIRTUAL_TWIG = "virtual:sdc-icon-packs:twig"; var RESOLVED_TWIG = "\0" + VIRTUAL_TWIG; var VIRTUAL_TWING = "virtual:sdc-icon-packs:twing"; var RESOLVED_TWING = "\0" + VIRTUAL_TWING; var PACK_PREFIX = "\0icons-pack:"; var SHARED_HELPERS = ( /* js */ ` function _sdcBuildIconContext(DrupalAttribute, pack, iconId, settings) { if (settings instanceof Map) settings = Object.fromEntries(settings); var resolved = {}; var packSettings = pack.settings || {}; var settingKeys = Object.keys(packSettings); for (var i = 0; i < settingKeys.length; i++) { var key = settingKeys[i]; var def = packSettings[key]; resolved[key] = settings && Object.prototype.hasOwnProperty.call(settings, key) ? settings[key] : def && def.default !== undefined ? def.default : ''; } var source = ''; var content = ''; var svgAttrs = {}; var group = ''; if (pack.extractor === 'svg_sprite') { source = pack.sourceUrls && pack.sourceUrls[0] ? pack.sourceUrls[0] : ''; } else if (pack.extractor === 'svg') { var svgData = pack.svgIcons && pack.svgIcons[iconId]; if (svgData) { source = svgData.sourceUrl || ''; content = svgData.content || ''; svgAttrs = svgData.attrs || {}; group = svgData.group || ''; } } else { var pathData = pack.pathIcons && pack.pathIcons[iconId]; if (pathData) { source = pathData.sourceUrl || ''; group = pathData.group || ''; } else if (pack.sourceUrls && pack.sourceUrls[0]) { source = pack.sourceUrls[0] + '/' + iconId; } } var attributes = new DrupalAttribute(Object.entries(svgAttrs)); return Object.assign( { icon_id: iconId, source: source, content: content, attributes: attributes, group: group }, resolved ); } ` ); function packImports(paths) { const imports = paths.map((p, i) => `import _p${i} from '${PACK_PREFIX}${p}';`).join("\n"); const args = paths.map((_, i) => `_p${i}`).join(", "); return { imports, mergeExpr: args ? `Object.assign({}, ${args})` : "{}" }; } function generateTwigModule(packFilePaths) { const { imports, mergeExpr } = packImports(packFilePaths); return ` import DrupalAttribute from 'drupal-attribute'; ${imports} ${SHARED_HELPERS} var _sdcIconPacks = ${mergeExpr}; export function registerIconFunction(Twig) { if (Twig.__sdcIconRegistered) return; Twig.__sdcIconRegistered = true; Twig.extendFunction('icon', function(packId, iconId, settings) { var pack = _sdcIconPacks[packId]; if (!pack || !iconId) return ''; var ctx = _sdcBuildIconContext(DrupalAttribute, pack, iconId, settings || {}); try { var tmpl = Twig.twig({ data: pack.template }); return tmpl.render(ctx); } catch (e) { console.error('[SDC Icons] Error rendering icon "' + packId + ':' + iconId + '":', e); return ''; } }); } `; } function generateTwingModule(packFilePaths) { const { imports, mergeExpr } = packImports(packFilePaths); return ` import { createSynchronousFunction } from 'twing'; import DrupalAttribute from 'drupal-attribute'; ${imports} ${SHARED_HELPERS} var _sdcIconPacks = ${mergeExpr}; export function registerIconFunction(env) { // Register icon templates into the existing loader so env.render() finds them. // createSynchronousArrayLoader (used by vite-plugin-twing-drupal's SDC loader) // stores templates by reference \u2014 setTemplate() propagates to both the wrapper // loader and the base array loader. var loader = env.loader; if (loader && typeof loader.setTemplate === 'function') { var packIds = Object.keys(_sdcIconPacks); for (var i = 0; i < packIds.length; i++) { loader.setTemplate('_sdc_icon_' + packIds[i], _sdcIconPacks[packIds[i]].template); } } var func = createSynchronousFunction( 'icon', function(_twingCtx, packId, iconId, settings) { var pack = _sdcIconPacks[packId]; if (!pack || !iconId) return ''; var ctx = _sdcBuildIconContext(DrupalAttribute, pack, iconId, settings || {}); try { return env.render('_sdc_icon_' + packId, ctx); } catch (e) { console.error('[SDC Icons] Error rendering icon "' + packId + ':' + iconId + '":', e); return ''; } }, [ { name: 'pack_id' }, { name: 'icon_id' }, { name: 'settings', defaultValue: {} }, ] ); env.addFunction(func); } `; } var TWIG_JS_MARKER = "from 'drupal-twig-extensions/twig'"; var TWIG_JS_INJECT_AFTER = "addDrupalExtensions(Twig);"; var TWING_MARKER = "createSynchronousEnvironment"; var TWING_INJECT_AFTER = "addDrupalExtensions(env);"; var INJECTED_GUARD = "_sdcRegisterIcon"; function iconPacksPlugin(namespaces) { return { name: "vite-plugin-sdc-icon-packs", resolveId(id) { if (id === VIRTUAL_TWIG) return RESOLVED_TWIG; if (id === VIRTUAL_TWING) return RESOLVED_TWING; if (id.startsWith(PACK_PREFIX)) return id; }, load(id) { if (id === RESOLVED_TWIG || id === RESOLVED_TWING) { const packFilePaths = namespaces.entries().map(([ns, root]) => join4(root, `${ns}.icons.yml`)).filter((p) => existsSync3(p)); return id === RESOLVED_TWIG ? generateTwigModule(packFilePaths) : generateTwingModule(packFilePaths); } if (id.startsWith(PACK_PREFIX)) { const filePath = id.slice(PACK_PREFIX.length); const { packs, watchFiles } = loadIconPackFile(filePath); watchFiles.forEach((f) => this.addWatchFile(f)); return `export default ${JSON.stringify(packs)};`; } }, handleHotUpdate({ modules, server }) { const hasIconPack = modules.some( (m) => m.id === RESOLVED_TWIG || m.id === RESOLVED_TWING || (m.id?.startsWith(PACK_PREFIX) ?? false) ); if (hasIconPack) { server.ws.send({ type: "full-reload" }); return []; } }, transform(code, id) { if (!id.match(/\.twig(\?.*)?$/)) return null; if (code.includes(INJECTED_GUARD)) return null; if (code.includes(TWING_MARKER) && code.includes(TWING_INJECT_AFTER)) { return { code: `import { registerIconFunction as ${INJECTED_GUARD} } from '${VIRTUAL_TWING}'; ` + code.replace( TWING_INJECT_AFTER, `${TWING_INJECT_AFTER} ${INJECTED_GUARD}(env);` ), map: null }; } if (code.includes(TWIG_JS_MARKER) && code.includes(TWIG_JS_INJECT_AFTER)) { return { code: `import { registerIconFunction as ${INJECTED_GUARD} } from '${VIRTUAL_TWIG}'; ` + code.replace( TWIG_JS_INJECT_AFTER, `${TWIG_JS_INJECT_AFTER} ${INJECTED_GUARD}(Twig);` ), map: null }; } return null; }, configureServer(server) { namespaces.entries().forEach(([, root]) => server.watcher.add(root)); server.watcher.on("add", (p) => { if (!p.endsWith(".icons.yml")) return; for (const rid of [RESOLVED_TWIG, RESOLVED_TWING]) { const mod = server.moduleGraph.getModuleById(rid); if (mod) server.moduleGraph.invalidateModule(mod); } server.ws.send({ type: "full-reload" }); }); } }; } // src/preset.ts async function viteFinal(config, options) { options = lodashMerge({}, DEFAULT_ADDON_OPTIONS, options); const { sdcStorybookOptions, vitePluginTwigDrupalOptions, vitePluginTwingDrupalOptions } = options; const { customDefs, externalDefs } = sdcStorybookOptions; const { nodePolyfills } = await import("vite-plugin-node-polyfills"); const globalDefs = await loadAndMergeDefinitions(externalDefs, customDefs); const namespaces = toNamespaces(sdcStorybookOptions); let twigPlugin = null; if (sdcStorybookOptions.twigLib === "twing") { options.vitePluginTwingDrupalOptions = { ...vitePluginTwingDrupalOptions, namespaces: { ...namespaces.toTwingNamespaces() } }; const { default: twing } = await import("vite-plugin-twing-drupal"); twigPlugin = twing(options.vitePluginTwingDrupalOptions); } else if (sdcStorybookOptions.twigLib === "twig") { options.vitePluginTwigDrupalOptions = { ...vitePluginTwigDrupalOptions, namespaces: { ...namespaces.toTwigJsNamespaces() } }; const { default: twig } = await import("vite-plugin-twig-drupal"); twigPlugin = twig(options.vitePluginTwigDrupalOptions); } return mergeConfig(config, { plugins: [ nodePolyfills({ include: ["buffer", "stream", "path"] }), ...twigPlugin ? [twigPlugin] : [], vite_plugin_storybook_yaml_stories_default({ ...options, globalDefs, namespaces }), iconPacksPlugin(namespaces) ], optimizeDeps: { exclude: ["vite-plugin-twig-drupal", "vite-plugin-twing-drupal"] }, resolve: { alias: [...namespaces.toViteAlias()] } }); } var experimental_indexers = async (existingIndexers) => [ ...existingIndexers || [], yamlStoriesIndexer ]; var staticDirs = async (existing, options) => { const merged = lodashMerge({}, DEFAULT_ADDON_OPTIONS, options); const namespaces = toNamespaces(merged.sdcStorybookOptions); const iconDirs = []; for (const [ns, nsRoot] of namespaces.entries()) { const iconsFile = join5(nsRoot, `${ns}.icons.yml`); if (!existsSync4(iconsFile)) continue; const { packs } = loadIconPackFile(iconsFile); const seen = /* @__PURE__ */ new Set(); for (const pack of Object.values(packs)) { if (pack.extractor === "svg") continue; for (const src of pack.sources) { if (src.startsWith("http")) continue; if (!existsSync4(src)) continue; const dir = statSync2(src).isDirectory() ? src : dirname5(src); if (seen.has(dir)) continue; seen.add(dir); const relDir = relative4(nsRoot, dir).replace(/\\/g, "/"); iconDirs.push({ from: dir, to: `/sdc-icons/${ns}/${relDir}` }); } } } return [...existing || [], ...iconDirs]; }; var previewHead = (head) => ` <style> .visually-hidden { position: absolute !important; overflow: hidden; clip: rect(1px, 1px, 1px, 1px); width: 1px; height: 1px; word-wrap: normal; } </style> <script src="https://cdn.jsdelivr.net/gh/drupal/drupal/core/misc/drupalSettingsLoader.js"></script> <script src="https://cdn.jsdelivr.net/gh/drupal/drupal/core/misc/drupal.js"></script> <script src="https://cdn.jsdelivr.net/gh/drupal/drupal/core/misc/drupal.init.js"></script> <script src="https://cdn.jsdelivr.net/npm/@drupal/once@1.0.1/dist/once.min.js"></script> ${head} `; export { experimental_indexers, previewHead, staticDirs, viteFinal };