UNPKG

next-yak

Version:

next-yak is a CSS-in-JS solution tailored for Next.js that seamlessly combines the expressive power of styled-components syntax with efficient build-time extraction of CSS using Next.js's built-in CSS configuration

1 lines 69.9 kB
{"version":3,"file":"webpack-loader.cjs","names":["resolveCrossFileConstant","resolveModule","genericResolveCrossFileConstant"],"sources":["../../loaders/lib/debugLogger.ts","../../loaders/lib/extractCss.ts","../../cross-file-resolver/parseModule.ts","../../cross-file-resolver/Errors.ts","../../cross-file-resolver/resolveCrossFileConstant.ts","../../loaders/lib/resolveCrossFileSelectors.ts","../../loaders/webpack-loader.ts"],"sourcesContent":["import { relative } from \"path\";\nimport type { YakConfigOptions } from \"../../withYak/index.js\";\n\ntype DebugOptions = Required<YakConfigOptions>[\"experiments\"][\"debug\"];\ntype DebugType = NonNullable<Exclude<DebugOptions, true | undefined>>[\"types\"] extends\n | Array<infer T>\n | undefined\n ? T\n : never;\n\n/**\n * Creates a debug logger function that conditionally logs messages\n * based on debug options and file paths.\n */\nexport function createDebugLogger(debugOptions: DebugOptions | undefined, rootPath: string) {\n if (!debugOptions) {\n return () => {};\n }\n\n throwOnDeprecatedDebugOptions(debugOptions);\n\n // Handle true (log all) vs object (with optional filtering)\n const pattern = debugOptions === true ? undefined : debugOptions.pattern;\n const typesArray = debugOptions === true ? undefined : debugOptions.types;\n\n // Validate and pre-compile regex pattern\n let compiledPattern: RegExp | null = null;\n if (pattern) {\n try {\n compiledPattern = new RegExp(pattern);\n } catch (error) {\n throw new Error(\n `Invalid debug pattern: \"${pattern}\" is not a valid regular expression. ${\n error instanceof Error ? error.message : \"\"\n }`,\n );\n }\n }\n\n const types = typesArray ? new Set(typesArray) : null;\n\n return (\n messageType: DebugType,\n message: string | Buffer<ArrayBufferLike> | undefined,\n filePath: string,\n ) => {\n // Filter by type if specified\n if (types && !types.has(messageType)) {\n return;\n }\n\n const relativePath = relative(rootPath, filePath);\n\n // Filter by pattern if specified, or log all if no pattern\n if (!compiledPattern || compiledPattern.test(relativePath)) {\n console.log(\"🐮 Yak\", `[${messageType}]`, relativePath, \"\\n\\n\", message);\n }\n };\n}\n\n/**\n * Detects deprecated debug option shapes and throws helpful migration errors.\n * TODO: Remove this function in the next major version.\n */\nfunction throwOnDeprecatedDebugOptions(debugOptions: DebugOptions): void {\n // Old API: debug: \"regex-string\"\n if (typeof debugOptions === \"string\") {\n const suggestion =\n suggestTypesForExtensionPattern(debugOptions) ?? `debug: { pattern: \"${debugOptions}\" }`;\n throw new Error(\n `The debug option no longer accepts a string. Please update your config:\\n` +\n ` Before: debug: \"${debugOptions}\"\\n` +\n ` After: ${suggestion}`,\n );\n }\n\n // Old API: debug: { filter: Function, type: string }\n if (typeof debugOptions === \"object\" && \"filter\" in debugOptions) {\n throw new Error(\n `The debug option no longer accepts { filter, type }. Please update your config:\\n` +\n ` Before: debug: { filter: ..., type: \"...\" }\\n` +\n ` After: debug: { pattern: \"...\", types: [\"ts\", \"css\", \"css-resolved\"] }`,\n );\n }\n\n // Old convention: pattern used \".css$\" or \".css-resolved$\" as file extension\n // for type filtering — the pattern now only matches file paths\n if (typeof debugOptions === \"object\" && debugOptions.pattern) {\n const suggestion = suggestTypesForExtensionPattern(debugOptions.pattern);\n if (suggestion) {\n throw new Error(\n `The debug pattern \"${debugOptions.pattern}\" looks like it's filtering by output type using the old file extension convention.\\n` +\n `The pattern now only matches file paths. Use the \"types\" option to filter by output type:\\n` +\n ` Before: debug: { pattern: \"${debugOptions.pattern}\" }\\n` +\n ` After: ${suggestion}`,\n );\n }\n }\n}\n\n/**\n * Checks if a pattern string uses the old \".css$\" / \".css-resolved$\" file\n * extension convention for type filtering. Returns a suggested replacement\n * or null if the pattern doesn't match.\n */\nfunction suggestTypesForExtensionPattern(pattern: string): string | null {\n const extensionMatch = pattern.match(/\\.\\(?(?:css-resolved|css)\\)?\\$?$/);\n if (!extensionMatch) {\n return null;\n }\n const type = extensionMatch[0].includes(\"css-resolved\") ? \"css-resolved\" : \"css\";\n const remaining = pattern.slice(0, extensionMatch.index);\n return remaining\n ? `debug: { pattern: \"${remaining}\", types: [\"${type}\"] }`\n : `debug: { types: [\"${type}\"] }`;\n}\n","import { YakConfigOptions } from \"../../withYak/index.js\";\n\n/**\n * Extracts CSS content from code that contains YAK-generated CSS comments.\n * Parses the input code and returns the extracted CSS, optionally adding\n * a cssmodules directive based on the transpilation mode.\n */\nexport function extractCss(\n code: string | Buffer<ArrayBufferLike>,\n transpilationMode: NonNullable<YakConfigOptions[\"experiments\"]>[\"transpilationMode\"],\n): string {\n let codeString: string;\n\n if (typeof code === \"string\") {\n codeString = code;\n } else if (code instanceof Buffer) {\n codeString = code.toString(\"utf-8\");\n } else if (code instanceof ArrayBuffer) {\n codeString = new TextDecoder(\"utf-8\").decode(code);\n } else {\n throw new Error(\"Invalid input type: code must be string, Buffer, or ArrayBuffer\");\n }\n\n const codeParts = codeString.split(\"/*YAK Extracted CSS:\\n\");\n let result = \"\";\n for (let i = 1; i < codeParts.length; i++) {\n const codeUntilEnd = codeParts[i].split(\"*/\")[0];\n result += codeUntilEnd;\n }\n if (result && transpilationMode !== \"Css\") {\n result = \"/* cssmodules-pure-no-check */\\n\" + result;\n }\n\n return result;\n}\n","import { type Cache } from \"./types.js\";\n\nexport async function parseModule(\n context: ParseContext,\n modulePath: string,\n): Promise<ParsedModule> {\n try {\n const isYak =\n modulePath.endsWith(\".yak.ts\") ||\n modulePath.endsWith(\".yak.tsx\") ||\n modulePath.endsWith(\".yak.js\") ||\n modulePath.endsWith(\".yak.jsx\");\n\n // handle yak file by evaluating and mapping exported value to the\n // `ModuleExport` format. This operation is not cached to always get a fresh\n // value from those modules\n if (isYak && context.evaluateYakModule) {\n const yakModule = await context.evaluateYakModule(modulePath);\n const yakExports = objectToModuleExport(yakModule);\n\n return {\n type: \"yak\",\n exports: { importYak: false, named: yakExports, all: [] },\n path: modulePath,\n };\n }\n\n if (context.cache?.parse === undefined) {\n return await uncachedParseModule(context, modulePath);\n }\n\n const cached = context.cache.parse.get(modulePath);\n if (cached === undefined) {\n // We cache the parsed file to avoid re-parsing it.\n // It's ok, that initial parallel requests to the same file will parse it multiple times.\n // This avoid deadlocks do to the fact that we load multiple modules in the chain for cross file references.\n const parsedModule = await uncachedParseModule(context, modulePath);\n\n context.cache.parse.set(modulePath, parsedModule);\n if (context.cache.parse.addDependency) {\n context.cache.parse.addDependency(modulePath, modulePath);\n }\n return parsedModule;\n }\n\n return cached;\n } catch (error) {\n const causeMessage = error instanceof Error ? error.message : String(error);\n throw new Error(`Error parsing file \"${modulePath}\"\\n Caused by: ${causeMessage}`);\n }\n}\n\nexport async function uncachedParseModule(\n context: ParseContext,\n modulePath: string,\n): Promise<ParsedModule> {\n const exports = await context.extractExports(modulePath);\n\n // early exit if no yak import was found\n if (!exports.importYak) {\n return {\n type: \"regular\",\n path: modulePath,\n exports,\n };\n }\n\n const transformed = await context.getTransformed(modulePath);\n const mixins = parseMixins(transformed.code);\n const styledComponents = parseStyledComponents(transformed.code, context.transpilationMode);\n\n return {\n type: \"regular\",\n path: modulePath,\n js: transformed,\n exports,\n styledComponents,\n mixins,\n };\n}\n\nfunction parseMixins(sourceContents: string): Record<string, Mixin> {\n // Mixins are always in the following format:\n // /*YAK EXPORTED MIXIN:fancy:aspectRatio:16:9\n // css\n // */\n const mixinParts = sourceContents.split(\"/*YAK EXPORTED MIXIN:\");\n let mixins: Record<string, { type: \"mixin\"; value: string; nameParts: string[] }> = {};\n\n for (let i = 1; i < mixinParts.length; i++) {\n const [comment] = mixinParts[i].split(\"*/\", 1);\n const position = comment.indexOf(\"\\n\");\n const name = comment.slice(0, position);\n const value = comment.slice(position + 1);\n mixins[name] = {\n type: \"mixin\",\n value,\n nameParts: name.split(\":\").map((part) => decodeURIComponent(part)),\n };\n }\n return mixins;\n}\n\nfunction parseStyledComponents(\n sourceContents: string,\n transpilationMode?: \"Css\" | \"CssModule\",\n): Record<string, StyledComponent> {\n // cross-file Styled Components are always in the following format:\n // /*YAK EXPORTED STYLED:ComponentName:ClassName*/\n const styledParts = sourceContents.split(\"/*YAK EXPORTED STYLED:\");\n let styledComponents: Record<string, StyledComponent> = {};\n\n for (let i = 1; i < styledParts.length; i++) {\n const [comment] = styledParts[i].split(\"*/\", 1);\n const [componentName, className] = comment.split(\":\");\n styledComponents[componentName] = {\n type: \"styled-component\",\n nameParts: componentName.split(\".\"),\n value: transpilationMode === \"Css\" ? `.${className}` : `:global(.${className})`,\n };\n }\n\n return styledComponents;\n}\n\nfunction objectToModuleExport(object: object) {\n return Object.fromEntries(\n Object.entries(object).map(([key, value]): [string, ModuleExport] => {\n if (typeof value === \"string\" || typeof value === \"number\") {\n return [key, { type: \"constant\" as const, value }];\n } else if (value && (typeof value === \"object\" || Array.isArray(value))) {\n return [key, { type: \"record\" as const, value: objectToModuleExport(value) }];\n } else {\n return [key, { type: \"unsupported\" as const, hint: String(value) }];\n }\n }),\n );\n}\n\nexport type ParseContext = {\n cache?: { parse?: Cache<ParsedModule> };\n transpilationMode?: \"Css\" | \"CssModule\";\n evaluateYakModule?: (\n modulePath: string,\n ) => Promise<Record<string, unknown>> | Record<string, unknown>;\n extractExports: (modulePath: string) => Promise<ModuleExports> | ModuleExports;\n getTransformed: (\n modulePath: string,\n ) => Promise<{ code: string; map?: string }> | { code: string; map?: string };\n};\n\nexport type ModuleExports = {\n importYak: boolean;\n named: Record<string, ModuleExport>;\n all: string[];\n};\n\nexport type ConstantExport = { type: \"constant\"; value: string | number };\nexport type RecordExport = {\n type: \"record\";\n value: Record<string, ModuleExport>;\n};\nexport type UnsupportedExport = {\n type: \"unsupported\";\n hint?: string;\n /**\n * Source location of the offending expression. Populated by the parser\n * when it has access to source text; left undefined for runtime-evaluated\n * yak modules. The error formatter is responsible for any rendering.\n */\n source?: UnsupportedExportSource;\n};\n\nexport type UnsupportedExportSource = {\n /** 1-based line number, 0-based column — same convention as Babel `loc`. */\n start: { line: number; column: number };\n end: { line: number; column: number };\n /** The text of `start.line` from the original source — kept so the\n * error formatter can render a snippet without re-reading the file. */\n lineText: string;\n};\nexport type ReExport = { type: \"re-export\"; name: string; from: string };\nexport type NamespaceReExport = { type: \"namespace-re-export\"; from: string };\nexport type TagTemplateExport = { type: \"tag-template\" };\n\nexport type ModuleExport =\n | ConstantExport\n | TagTemplateExport\n | RecordExport\n | UnsupportedExport\n | ReExport\n | NamespaceReExport;\n\nexport type ParsedModule = {\n path: string;\n exports: ModuleExports;\n} & (\n | {\n type: \"regular\";\n js?: { code: string; map?: string };\n styledComponents?: Record<string, StyledComponent>;\n mixins?: Record<string, Mixin>;\n }\n | {\n type: \"yak\";\n }\n);\n\nexport type StyledComponent = {\n type: \"styled-component\";\n value: string;\n nameParts: string[];\n};\n\nexport type Mixin = { type: \"mixin\"; value: string; nameParts: string[] };\n","export class CauseError extends Error {\n circular?: boolean;\n constructor(message: string, options?: { cause?: unknown }) {\n super(\n `${message}${options?.cause ? `\\n Caused by: ${typeof options.cause === \"object\" && options.cause !== null && \"message\" in options.cause ? options.cause.message : String(options.cause)}` : \"\"}`,\n );\n\n if (options?.cause instanceof CauseError && options.cause.circular) {\n this.circular = true;\n }\n }\n}\n\nexport class ResolveError extends CauseError {}\n\n/**\n * Thrown for `unsupported` exports. Subclassed so the surrounding\n * `resolveModuleExport` catch can recognise that the error already\n * carries its own \"Unable to resolve … in module …\" wrapper and\n * doesn't need another one stacked on top.\n */\nexport class UnsupportedExportError extends ResolveError {}\n\nexport class CircularDependencyError extends CauseError {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.circular = true;\n }\n}\n","import type {\n ConstantExport,\n ModuleExport,\n ParsedModule,\n RecordExport,\n UnsupportedExportSource,\n} from \"./parseModule.js\";\nimport { Cache } from \"./types.js\";\nimport {\n CauseError,\n CircularDependencyError,\n ResolveError,\n UnsupportedExportError,\n} from \"./Errors.js\";\n\nconst yakCssImportRegex =\n // Make mixin and selector non optional once we dropped support for the babel plugin\n /--yak-css-import:\\s*url\\(\"([^\"]+)\",?(|mixin|selector)\\)(;?)/g;\n\n/**\n * Resolves cross-file selectors in css files\n *\n * e.g.:\n * theme.ts:\n * ```ts\n * export const colors = {\n * primary: \"#ff0000\",\n * secondary: \"#00ff00\",\n * };\n * ```\n *\n * styles.ts:\n * ```ts\n * import { colors } from \"./theme\";\n * export const button = css`\n * background-color: ${colors.primary};\n * `;\n */\nexport async function resolveCrossFileConstant(\n context: ResolveContext,\n filePath: string,\n css: string,\n): Promise<{ resolved: string; dependencies: string[] }> {\n const resolveCrossFileConstant = context.cache?.resolveCrossFileConstant;\n if (resolveCrossFileConstant === undefined) {\n return uncachedResolveCrossFileConstant(context, filePath, css);\n }\n\n const cacheKey = await sha1(filePath + \":\" + css);\n\n const cached = resolveCrossFileConstant.get(cacheKey);\n\n if (cached === undefined) {\n const resolvedCrossFilConstantPromise = uncachedResolveCrossFileConstant(\n context,\n filePath,\n css,\n );\n resolveCrossFileConstant.set(cacheKey, resolvedCrossFilConstantPromise);\n\n if (resolveCrossFileConstant.addDependency) {\n resolveCrossFileConstant.addDependency(cacheKey, filePath);\n resolvedCrossFilConstantPromise.then((value) => {\n for (const dep of value.dependencies) {\n resolveCrossFileConstant!.addDependency!(cacheKey, dep);\n }\n });\n }\n\n return resolvedCrossFilConstantPromise;\n }\n\n return cached;\n}\n\nexport async function uncachedResolveCrossFileConstant(\n context: ResolveContext,\n filePath: string,\n css: string,\n): Promise<{ resolved: string; dependencies: string[] }> {\n const yakImports = await parseYakCssImport(context, filePath, css);\n\n if (yakImports.length === 0) {\n return { resolved: css, dependencies: [] };\n }\n\n try {\n const dependencies = new Set<string>();\n\n const resolvedValues = await Promise.all(\n yakImports.map(async ({ moduleSpecifier, specifier }) => {\n const { resolved: resolvedModule } = await resolveModule(context, moduleSpecifier);\n\n const resolvedValue = await resolveModuleSpecifierRecursively(\n context,\n resolvedModule,\n specifier,\n );\n\n for (const dependency of resolvedValue.from) {\n dependencies.add(dependency);\n }\n\n return resolvedValue;\n }),\n );\n\n // Replace the imports with the resolved values\n let result = css;\n for (let i = yakImports.length - 1; i >= 0; i--) {\n const { position, size, importKind, specifier, semicolon } = yakImports[i];\n const resolved = resolvedValues[i];\n\n let replacement: string;\n\n if (resolved.type === \"unresolved-tag\") {\n // tag that could not be resolved to styled-components or mixins are\n // interpolated to produce valid CSS with minimal impact (since we don't\n // know what the value should actually be). For mixins (CSS rules) we\n // interpolate an empty string, and for selectors we interpolate\n // \"undefined\" (a selector that would match nothing)\n replacement = importKind === \"mixin\" ? \"\" : \"undefined\";\n } else {\n if (importKind === \"selector\") {\n if (resolved.type !== \"styled-component\" && resolved.type !== \"constant\") {\n throw new Error(\n `Found \"${\n resolved.type\n }\" but expected a selector - did you forget a semicolon after \"${specifier.join(\n \".\",\n )}\"?`,\n );\n }\n }\n\n replacement =\n resolved.type === \"styled-component\"\n ? resolved.value\n : resolved.value +\n // resolved.value can be of two different types:\n // - mixin:\n // ${mixinName};\n // - constant:\n // color: ${value};\n // For mixins the semicolon is already included in the value\n // but for constants it has to be added manually\n ([\"}\", \";\"].includes(String(resolved.value).trimEnd().slice(-1)) ? \"\" : semicolon);\n }\n\n result = result.slice(0, position) + String(replacement) + result.slice(position + size);\n }\n\n return { resolved: result, dependencies: Array.from(dependencies) };\n } catch (error) {\n throw new CauseError(`Error while resolving cross-file selectors in file \"${filePath}\"`, {\n cause: error,\n });\n }\n}\n\n/**\n * Search for --yak-css-import: url(\"path/to/module\") in the css\n */\nasync function parseYakCssImport(\n context: ResolveContext,\n filePath: string,\n css: string,\n): Promise<YakCssImport[]> {\n const yakImports: YakCssImport[] = [];\n\n for (const match of css.matchAll(yakCssImportRegex)) {\n const [fullMatch, encodedArguments, importKind, semicolon] = match;\n const [moduleSpecifier, ...specifier] = encodedArguments\n .split(\":\")\n .map((entry) => decodeURIComponent(entry));\n\n yakImports.push({\n encodedArguments,\n moduleSpecifier: await context.resolve(moduleSpecifier, filePath),\n specifier,\n importKind: importKind as YakImportKind,\n semicolon,\n position: match.index,\n size: fullMatch.length,\n });\n }\n\n return yakImports;\n}\n\nasync function resolveModule(context: ResolveContext, filePath: string) {\n if (context.cache?.resolve === undefined) {\n return uncachedResolveModule(context, filePath);\n }\n\n const cached = context.cache.resolve.get(filePath);\n if (cached === undefined) {\n const resolvedPromise = uncachedResolveModule(context, filePath);\n context.cache.resolve.set(filePath, resolvedPromise);\n\n if (context.cache.resolve.addDependency) {\n context.cache.resolve.addDependency(filePath, filePath);\n resolvedPromise.then((value) => {\n for (const dep of value.dependencies) {\n context.cache!.resolve!.addDependency!(filePath, dep);\n }\n });\n }\n\n return resolvedPromise;\n }\n\n return cached;\n}\n\nasync function uncachedResolveModule(\n context: ResolveContext,\n filePath: string,\n): Promise<{ resolved: ResolvedModule; dependencies: string[] }> {\n const parsedModule = await context.parse(filePath);\n\n const exports = parsedModule.exports as ResolvedExports;\n\n if (parsedModule.type !== \"regular\") {\n return {\n resolved: {\n path: parsedModule.path,\n exports,\n },\n dependencies: [],\n };\n }\n\n const dependencies = new Set<string>();\n\n // Reconcile styled-component \"name\" structure with export structure\n if (parsedModule.styledComponents) {\n Object.values(parsedModule.styledComponents).map((styledComponent) => {\n if (styledComponent.nameParts.length === 1) {\n exports.named[styledComponent.nameParts[0]] = {\n type: \"styled-component\",\n className: styledComponent.value,\n };\n } else {\n let exportEntry = exports.named[styledComponent.nameParts[0]];\n\n if (!exportEntry) {\n exportEntry = { type: \"record\", value: {} };\n exports.named[styledComponent.nameParts[0]] = exportEntry;\n } else if (exportEntry.type !== \"record\") {\n throw new CauseError(`Error parsing file \"${parsedModule.path}\"`, {\n cause: `\"${styledComponent.nameParts[0]}\" is not a record`,\n });\n }\n\n let current = exportEntry.value;\n for (let i = 1; i < styledComponent.nameParts.length - 1; i++) {\n let next = current[styledComponent.nameParts[i]];\n if (!next) {\n next = { type: \"record\", value: {} };\n current[styledComponent.nameParts[i]] = next;\n } else if (next.type !== \"record\") {\n throw new CauseError(`Error parsing file \"${parsedModule.path}\"`, {\n cause: `\"${styledComponent.nameParts.slice(0, i + 1).join(\".\")}\" is not a record`,\n });\n }\n current = next.value;\n }\n current[styledComponent.nameParts[styledComponent.nameParts.length - 1]] = {\n type: \"styled-component\",\n className: styledComponent.value,\n };\n }\n });\n }\n\n // Recursively resolve cross-file constants in mixins\n // e.g. cross file mixins inside a cross file mixin\n // or a cross file selector inside a cross file mixin\n if (parsedModule.mixins) {\n await Promise.all(\n Object.values(parsedModule.mixins).map(async (mixin) => {\n const { resolved, dependencies: deps } = await resolveCrossFileConstant(\n context,\n parsedModule.path,\n mixin.value,\n );\n\n for (const dep of deps) {\n dependencies.add(dep);\n }\n\n if (mixin.nameParts.length === 1) {\n exports.named[mixin.nameParts[0]] = {\n type: \"mixin\",\n value: resolved,\n };\n } else {\n let exportEntry = exports.named[mixin.nameParts[0]];\n\n if (!exportEntry) {\n exportEntry = { type: \"record\", value: {} };\n exports.named[mixin.nameParts[0]] = exportEntry;\n } else if (exportEntry.type !== \"record\") {\n throw new CauseError(`Error parsing file \"${parsedModule.path}\"`, {\n cause: `\"${mixin.nameParts[0]}\" is not a record`,\n });\n }\n\n let current = exportEntry.value;\n for (let i = 1; i < mixin.nameParts.length - 1; i++) {\n let next = current[mixin.nameParts[i]];\n if (!next) {\n next = { type: \"record\", value: {} };\n current[mixin.nameParts[i]] = next;\n } else if (next.type !== \"record\") {\n throw new CauseError(`Error parsing file \"${parsedModule.path}\"`, {\n cause: `\"${mixin.nameParts.slice(0, i + 1).join(\".\")}\" is not a record`,\n });\n }\n current = next.value;\n }\n current[mixin.nameParts[mixin.nameParts.length - 1]] = {\n type: \"mixin\",\n value: resolved,\n };\n }\n }),\n );\n }\n return {\n resolved: {\n path: parsedModule.path,\n exports,\n },\n dependencies: Array.from(dependencies),\n };\n}\n\nasync function resolveModuleSpecifierRecursively(\n context: ResolveContext,\n resolvedModule: ResolvedModule,\n specifiers: string[],\n seen = new Set<string>(),\n): Promise<ResolvedCssImport> {\n const exportName = specifiers[0];\n const exportValue = resolvedModule.exports.named[exportName];\n if (exportValue !== undefined) {\n if (seen.has(resolvedModule.path + \":\" + exportName)) {\n throw new CircularDependencyError(\n `Unable to resolve \"${specifiers.join(\".\")}\" in module \"${resolvedModule.path}\"`,\n { cause: \"Circular dependency detected\" },\n );\n }\n\n seen.add(resolvedModule.path + \":\" + exportName);\n return resolveModuleExport(context, resolvedModule.path, exportValue, specifiers, seen);\n }\n\n let i = 1;\n for (const from of resolvedModule.exports.all) {\n if (context.exportAllLimit && i++ > context.exportAllLimit) {\n throw new ResolveError(\n `Unable to resolve \"${specifiers.join(\".\")}\" in module \"${resolvedModule.path}\"`,\n {\n cause: `More than ${context.exportAllLimit} star exports are not supported for performance reasons`,\n },\n );\n }\n\n try {\n const resolved = await resolveModuleExport(\n context,\n resolvedModule.path,\n {\n type: \"re-export\",\n from,\n name: exportName,\n },\n specifiers,\n seen,\n );\n\n if (seen.has(resolvedModule.path + \":*\")) {\n throw new CircularDependencyError(\n `Unable to resolve \"${specifiers.join(\".\")}\" in module \"${resolvedModule.path}\"`,\n { cause: \"Circular dependency detected\" },\n );\n }\n\n seen.add(resolvedModule.path + \":*\");\n\n return resolved;\n } catch (error) {\n // ignore resolve error, it means the specifier was not found in the\n // current module, we just have to continue the loop.\n if (!(error instanceof ResolveError)) {\n throw error;\n }\n // if the cause of the error is a circular dependency down the road do not\n // ignore the error\n if (error.circular) {\n throw error;\n }\n }\n }\n\n throw new ResolveError(`Unable to resolve \"${specifiers.join(\".\")}\"`, {\n cause: `no matching export found in module \"${resolvedModule.path}\"`,\n });\n}\n\nasync function resolveModuleExport(\n context: ResolveContext,\n filePath: string,\n moduleExport: ResolvedExport,\n specifiers: string[],\n seen: Set<string>,\n): Promise<ResolvedCssImport> {\n const failureMessage = `Unable to resolve \"${specifiers.join(\".\")}\" in module \"${filePath}\"`;\n try {\n switch (moduleExport.type) {\n case \"re-export\": {\n const { resolved: reExportedModule } = await resolveModule(\n context,\n await context.resolve(moduleExport.from, filePath),\n );\n const resolved = await resolveModuleSpecifierRecursively(\n context,\n reExportedModule,\n [moduleExport.name, ...specifiers.slice(1)],\n seen,\n );\n if (resolved) {\n resolved.from.push(filePath);\n }\n return resolved;\n }\n case \"namespace-re-export\": {\n const { resolved: reExportedModule } = await resolveModule(\n context,\n await context.resolve(moduleExport.from, filePath),\n );\n const resolved = await resolveModuleSpecifierRecursively(\n context,\n reExportedModule,\n specifiers.slice(1),\n seen,\n );\n if (resolved) {\n resolved.from.push(filePath);\n }\n return resolved;\n }\n case \"styled-component\": {\n return {\n type: \"styled-component\",\n from: [filePath],\n source: filePath,\n name: specifiers[specifiers.length - 1],\n value: moduleExport.className,\n };\n }\n // usually at this point `tag-template` exports where already resolved to\n // styled-components if a matching styled-component comment was generated\n // by yak-swc. So resolving a value to a `tag-template` at this stage\n // would mean that the user tried to use the result of a call to a\n // different tag-template than yak's styled in a template. This is usually\n // invalid.\n //\n // But there is an issue with Nextjs. Next build in two passes, once for\n // the server bundle, once for the client bundle. During the server-side\n // build, each module with the `\"use client\"` directive is transformed to\n // throw errors if the exported symbol are used. This transformation\n // removes the comments generated by `yak-swc`, so instead of the expected\n // `styled-component`, calls to `styled` resolve to a `tag-template`\n // (because no classname was found in the now absent comments).\n //\n // To summarize, if a \"use client\" bundle exports a styled component that\n // is used in a \"standard\" module, the resolve logic would throw with\n // \"unknown type tag-template\".\n //\n // To avoid this error, the resolve logic must handle those `tag-template`\n // with a special type `unresolved-tag`. Those will be interpolated to\n // valid CSS with minimal effect (to avoid CSS syntax error in the case of\n // Nextjs server build)\n case \"tag-template\": {\n return {\n type: \"unresolved-tag\",\n from: [filePath],\n source: filePath,\n name: specifiers[specifiers.length - 1],\n };\n }\n case \"constant\": {\n return {\n type: \"constant\",\n from: [filePath],\n source: filePath,\n value: moduleExport.value,\n };\n }\n case \"record\": {\n const resolvedInRecord = resolveSpecifierInRecord(\n moduleExport,\n specifiers[0],\n specifiers.slice(1),\n );\n return resolveModuleExport(context, filePath, resolvedInRecord, specifiers, seen);\n }\n case \"mixin\": {\n return {\n type: \"mixin\",\n from: [filePath],\n source: filePath,\n value: moduleExport.value,\n };\n }\n case \"unsupported\": {\n throw new UnsupportedExportError(failureMessage, {\n cause: explainUnsupported(\n filePath,\n specifiers.join(\".\"),\n moduleExport.hint,\n moduleExport.source,\n ),\n });\n }\n }\n } catch (error) {\n // UnsupportedExportError already carries this frame's \"Unable to resolve …\"\n // wrapper, so it bubbles up unchanged to avoid duplicating the line.\n if (error instanceof UnsupportedExportError) {\n throw error;\n }\n throw new ResolveError(failureMessage, { cause: error });\n }\n}\n\nfunction explainUnsupported(\n filePath: string,\n specifier: string,\n hint: string | undefined,\n source: UnsupportedExportSource | undefined,\n): string {\n const isYakFile = /\\.yak\\.(?:ts|tsx|js|jsx)$/.test(filePath);\n const docs =\n \"https://yak.js.org/docs/migration-from-styled-components#move-some-code-to-yak-files\";\n const snippet = source ? renderSourceSnippet(filePath, source, hint ?? \"\") : undefined;\n\n const lines: string[] = [];\n if (isYakFile) {\n const got = hint ? ` (got \\`${hint}\\`)` : \"\";\n lines.push(`\\`${specifier}\\` evaluated to a value that cannot be inlined into CSS${got}.`);\n if (snippet) lines.push(snippet);\n lines.push(\n ` help: replace it with a string, number, or plain object/array of those`,\n ` see: ${docs}`,\n );\n return lines.join(\"\\n\");\n }\n\n const got = hint ? ` (got a ${hint})` : \"\";\n lines.push(`\\`${specifier}\\` is not a string or number literal${got}.`);\n if (snippet) lines.push(snippet);\n lines.push(\n ` help: rename \"${filePath}\" to \"${suggestYakFileName(filePath)}\" so its exports run at build time`,\n ` (or replace \\`${specifier}\\` with a literal value)`,\n ` see: ${docs}`,\n );\n return lines.join(\"\\n\");\n}\n\nfunction suggestYakFileName(filePath: string): string {\n const match = filePath.match(/^(.*)\\.(ts|tsx|js|jsx)$/);\n if (!match) return filePath;\n return `${match[1]}.yak.${match[2]}`;\n}\n\n/**\n * Render a Rust-compiler-style snippet from a structural source location:\n *\n * --> /foo/colors.ts:5:18\n * |\n * 5 | export const bg = `var(${v})`;\n * | ^^^^^^^^^^^^ TemplateLiteral\n */\nfunction renderSourceSnippet(\n filePath: string,\n source: UnsupportedExportSource,\n label: string,\n): string {\n const startLine = source.start.line;\n const startCol = source.start.column;\n const sameLine = source.end.line === startLine;\n const caretLen = sameLine\n ? Math.max(1, source.end.column - startCol)\n : Math.max(1, source.lineText.length - startCol);\n\n const lineNumStr = String(startLine);\n const gutterPad = \" \".repeat(lineNumStr.length);\n\n return [\n ` --> ${filePath}:${startLine}:${startCol + 1}`,\n ` ${gutterPad} |`,\n ` ${lineNumStr} | ${source.lineText}`,\n ` ${gutterPad} | ${\" \".repeat(startCol)}${\"^\".repeat(caretLen)} ${label}`,\n ].join(\"\\n\");\n}\n\nfunction resolveSpecifierInRecord(\n record: ExtendedRecordExport,\n name: string,\n specifiers: string[],\n): ConstantExport | ResolvedStyledComponent | ResolvedMixin {\n if (specifiers.length === 0) {\n throw new ResolveError(\"did not expect an object\");\n }\n let depth = 0;\n let current: ResolvedExport = record;\n while (current && current.type === \"record\" && depth < specifiers.length) {\n current = current.value[specifiers[depth]];\n depth += 1;\n }\n\n if (current === undefined || depth !== specifiers.length) {\n throw new ResolveError(\n `Unable to resolve \"${specifiers.join(\".\")}\" in object/array \"${name}\"`,\n { cause: \"path not found\" },\n );\n }\n\n if (\n current.type === \"constant\" ||\n current.type === \"styled-component\" ||\n current.type === \"mixin\"\n ) {\n return current;\n }\n\n // mixins in .yak files are wrapped inside an object with a __yak key\n if (\n current.type === \"record\" &&\n \"__yak\" in current.value &&\n current.value.__yak.type === \"constant\"\n ) {\n return { type: \"mixin\", value: String(current.value.__yak.value) };\n }\n\n throw new ResolveError(`Unable to resolve \"${specifiers.join(\".\")}\" in object/array \"${name}\"`, {\n cause: \"only string and numbers are supported\",\n });\n}\n\n/**\n * hex SHA-1 hash of a message using webcrypto\n * Keeps yak independent from node api (therefore executable in browser)\n */\nasync function sha1(message: string) {\n const resultBuffer = await globalThis.crypto.subtle.digest(\n \"SHA-1\",\n new TextEncoder().encode(message),\n );\n return Array.from(new Uint8Array(resultBuffer), (byte) =>\n byte.toString(16).padStart(2, \"0\"),\n ).join(\"\");\n}\n\ntype ResolvedCssImport =\n | {\n type: \"styled-component\";\n source: string;\n from: string[];\n name: string;\n value: string;\n }\n | {\n type: \"unresolved-tag\";\n source: string;\n from: string[];\n name: string;\n }\n | { type: \"mixin\"; source: string; from: string[]; value: string | number }\n | {\n type: \"constant\";\n source: string;\n from: string[];\n value: string | number;\n };\n\nexport type ResolveContext = {\n parse: (modulePath: string) => Promise<ParsedModule> | ParsedModule;\n cache?: {\n resolve?: Cache<Promise<{ resolved: ResolvedModule; dependencies: string[] }>>;\n resolveCrossFileConstant?: Cache<Promise<{ resolved: string; dependencies: string[] }>>;\n };\n exportAllLimit?: number;\n resolve: (specifier: string, importer: string) => Promise<string> | string;\n};\n\ntype YakImportKind = \"mixin\" | \"selector\";\n\ntype YakCssImport = {\n encodedArguments: string;\n moduleSpecifier: string;\n specifier: string[];\n importKind: YakImportKind;\n semicolon: string;\n position: number;\n size: number;\n};\n\nexport type ExtendedRecordExport = {\n type: \"record\";\n value: Record<string, ResolvedExport>;\n};\n\nexport type ResolvedMixin = { type: \"mixin\"; value: string };\nexport type ResolvedStyledComponent = {\n type: \"styled-component\";\n className: string;\n};\n\nexport type ResolvedExport =\n | Exclude<ModuleExport, RecordExport>\n | ExtendedRecordExport\n | ResolvedStyledComponent\n | ResolvedMixin;\n\nexport type ResolvedExports = {\n named: Record<string, ResolvedExport>;\n all: string[];\n};\n\nexport type ResolvedModule = {\n path: string;\n exports: ResolvedExports;\n};\n","import { parse } from \"@babel/parser\";\nimport type { Compilation, LoaderContext } from \"webpack\";\nimport {\n ModuleExport,\n ModuleExports,\n ParseContext,\n ParsedModule,\n UnsupportedExportSource,\n parseModule,\n} from \"../../cross-file-resolver/parseModule.js\";\nimport {\n ResolveContext,\n resolveCrossFileConstant as genericResolveCrossFileConstant,\n} from \"../../cross-file-resolver/resolveCrossFileConstant.js\";\nimport { YakConfigOptions } from \"../../withYak/index.js\";\n\nconst compilationCache = new WeakMap<\n Compilation,\n {\n parsedFiles: Map<string, ParsedModule>;\n }\n>();\n\nexport async function resolveCrossFileConstant(\n loader: LoaderContext<{}>,\n pathContext: string,\n css: string,\n): Promise<string> {\n const { resolved } = await genericResolveCrossFileConstant(\n getResolveContext(loader),\n loader.resourcePath,\n css,\n );\n return resolved;\n}\n\nfunction getCompilationCache(loader: LoaderContext<YakConfigOptions>) {\n const compilation = loader._compilation;\n if (!compilation) {\n throw new Error(\"Webpack compilation object not available\");\n }\n let cache = compilationCache.get(compilation);\n if (!cache) {\n cache = {\n parsedFiles: new Map(),\n };\n compilationCache.set(compilation, cache);\n }\n return cache;\n}\n\nfunction getParseContext(loader: LoaderContext<YakConfigOptions>): ParseContext {\n return {\n cache: { parse: getCompilationCache(loader).parsedFiles },\n async extractExports(modulePath) {\n const sourceContents = new Promise<string>((resolve, reject) =>\n loader.fs.readFile(modulePath, \"utf-8\", (err, result) => {\n if (err) return reject(err);\n resolve(result || \"\");\n }),\n );\n return parseExports(await sourceContents);\n },\n async getTransformed(modulePath) {\n const transformedSource = new Promise<string>((resolve, reject) => {\n loader.loadModule(modulePath, (err, source) => {\n if (err) {\n // When webpack reports \"The loaded module contains errors\",\n // the actual errors are stored on the module in the compilation.\n // Extract and report the real errors for better debugging.\n const compilation = loader._compilation;\n if (compilation) {\n try {\n for (const mod of compilation.modules) {\n if (\"resource\" in mod && mod.resource === modulePath) {\n const errors = mod.getErrors();\n if (errors) {\n const messages = Array.from(errors)\n .map((e) => e.message)\n .filter(Boolean);\n if (messages.length > 0) {\n return reject(new Error(messages.join(\"\\n\")));\n }\n }\n }\n }\n } catch {\n // Ignore errors while trying to extract module errors\n }\n }\n return reject(err);\n }\n let sourceString: string;\n if (typeof source === \"string\") {\n sourceString = source;\n } else if (source instanceof Buffer) {\n sourceString = source.toString(\"utf-8\");\n } else if (source instanceof ArrayBuffer) {\n sourceString = new TextDecoder(\"utf-8\").decode(source);\n } else {\n throw new Error(\"Invalid input type: code must be string, Buffer, or ArrayBuffer\");\n }\n resolve(sourceString || \"\");\n });\n });\n return { code: await transformedSource };\n },\n async evaluateYakModule(modulePath) {\n return loader.importModule(modulePath);\n },\n transpilationMode: loader.getOptions().experiments?.transpilationMode,\n };\n}\n\nfunction getResolveContext(loader: LoaderContext<YakConfigOptions>): ResolveContext {\n const parseContext = getParseContext(loader);\n return {\n parse: (modulePath) => parseModule(parseContext, modulePath),\n resolve: async (specifier, importer) => {\n return resolveModule(loader, specifier, dirname(importer));\n },\n };\n}\n\n/**\n * Resolves a module by wrapping loader.resolve in a promise\n */\nexport async function resolveModule(\n loader: LoaderContext<{}>,\n moduleSpecifier: string,\n context: string,\n): Promise<string> {\n return new Promise<string>((resolve, reject) => {\n loader.resolve(context, moduleSpecifier, (err, result) => {\n if (err) return reject(err);\n if (!result) return reject(new Error(`Could not resolve ${moduleSpecifier}`));\n resolve(result);\n });\n });\n}\n\nexport async function parseExports(sourceContents: string): Promise<ModuleExports> {\n try {\n const ast = parse(sourceContents, {\n sourceType: \"module\",\n plugins: [\"jsx\", \"typescript\"] as const,\n });\n\n // Derive importYak from top-level imports (no traverse needed)\n const importYak = ast.program.body.some(\n (node) => node.type === \"ImportDeclaration\" && node.source.value === \"next-yak\",\n );\n\n const moduleExports: ModuleExports = {\n importYak,\n named: {},\n all: [],\n };\n\n // Track variable declarations for default export lookup\n const variableDeclarations: Record<string, babel.types.Expression> = {};\n let defaultIdentifier: string | null = null;\n\n for (const node of ast.program.body) {\n // Track top-level variable declarations for default export lookup\n if (node.type === \"VariableDeclaration\") {\n for (const decl of node.declarations) {\n if (decl.id.type === \"Identifier\" && decl.init) {\n variableDeclarations[decl.id.name] = decl.init;\n }\n }\n }\n\n if (node.type === \"ExportNamedDeclaration\") {\n if (node.source) {\n // export { x } from \"./file\", export { x as y } from \"./file\"\n for (const specifier of node.specifiers) {\n if (\n specifier.type === \"ExportSpecifier\" &&\n specifier.exported.type === \"Identifier\" &&\n specifier.local.type === \"Identifier\"\n ) {\n moduleExports.named[specifier.exported.name] = {\n type: \"re-export\",\n from: node.source.value,\n name: specifier.local.name,\n };\n }\n // export * as ns from \"./file\"\n if (\n specifier.type === \"ExportNamespaceSpecifier\" &&\n specifier.exported.type === \"Identifier\"\n ) {\n moduleExports.named[specifier.exported.name] = {\n type: \"namespace-re-export\",\n from: node.source.value,\n };\n }\n }\n } else if (node.declaration?.type === \"VariableDeclaration\") {\n // export const x = ...\n for (const declaration of node.declaration.declarations) {\n if (declaration.id.type === \"Identifier\" && declaration.init) {\n variableDeclarations[declaration.id.name] = declaration.init;\n const parsed = parseExportValueExpression(declaration.init, sourceContents);\n if (parsed) {\n moduleExports.named[declaration.id.name] = parsed;\n }\n }\n }\n }\n }\n\n if (node.type === \"ExportDefaultDeclaration\") {\n if (node.declaration.type === \"Identifier\") {\n // e.g. export default variableName;\n // Save the identifier name to look up later\n defaultIdentifier = node.declaration.name;\n } else if (\n node.declaration.type === \"FunctionDeclaration\" ||\n node.declaration.type === \"ClassDeclaration\"\n ) {\n // e.g. export default function() {...} or export default class {...}\n moduleExports.named[\"default\"] = {\n type: \"unsupported\",\n hint: node.declaration.type,\n source: extractUnsupportedSource(node.declaration.loc, sourceContents),\n };\n } else {\n // e.g. export default { ... } or export default \"value\"\n moduleExports.named[\"default\"] = parseExportValueExpression(\n node.declaration as babel.types.Expression,\n sourceContents,\n );\n }\n }\n\n // export * from \"./file\"\n if (node.type === \"ExportAllDeclaration\") {\n moduleExports.all.push(node.source.value);\n }\n }\n\n // If we found a default export that's an identifier, look up its value\n if (defaultIdentifier && variableDeclarations[defaultIdentifier]) {\n moduleExports.named[\"default\"] = parseExportValueExpression(\n variableDeclarations[defaultIdentifier],\n sourceContents,\n );\n }\n\n return moduleExports;\n } catch (error) {\n throw new Error(`Error parsing exports: ${(error as Error).message}`);\n }\n}\n\n/**\n * Unpacks TS type assertions (as, satisfies) to the underlying expression\n */\nfunction unpackTSAsExpression(\n node: babel.types.TSAsExpression | babel.types.Expression,\n): babel.types.Expression {\n if (node.type === \"TSAsExpression\" || node.type === \"TSSatisfiesExpression\") {\n return unpackTSAsExpression((node as babel.types.TSAsExpression).expression);\n }\n return node;\n}\n\nfunction parseExportValueExpression(node: babel.types.Expression, code?: string): ModuleExport {\n // ignores `as` casts so it doesn't interfere with the ast node type detection\n const expression = unpackTSAsExpression(node);\n if (expression.type === \"CallExpression\" || expression.type === \"TaggedTemplateExpression\") {\n return { type: \"tag-template\" };\n } else if (expression.type === \"StringLiteral\" || expression.type === \"NumericLiteral\") {\n return { type: \"constant\", value: expression.value };\n } else if (\n expression.type === \"UnaryExpression\" &&\n expression.operator === \"-\" &&\n expression.argument.type === \"NumericLiteral\"\n ) {\n return { type: \"constant\", value: -expression.argument.value };\n } else if (expression.type === \"TemplateLiteral\" && expression.quasis.length === 1) {\n return { type: \"constant\", value: expression.quasis[0].value.raw };\n } else if (expression.type === \"ObjectExpression\") {\n return {\n type: \"record\",\n value: parseObjectExpression(expression, code),\n };\n }\n return {\n type: \"unsupported\",\n hint: expression.type,\n source: extractUnsupportedSource(expression.loc, code),\n };\n}\n\nfunction parseObjectExpression(\n node: babel.types.ObjectExpression,\n code?: string,\n): Record<string, ModuleExport> {\n let result: Record<string, ModuleExport> = {};\n for (const property of node.properties) {\n if (property.type === \"ObjectProperty\" && property.key.type === \"Identifier\") {\n const key = property.key.name;\n const parsed = parseExportValueExpression(property.value as babel.types.Expression, code);\n if (parsed) {\n result[key] = parsed;\n }\n }\n }\n return result;\n}\n\n/**\n * Pull the structural source-location data the error formatter needs to\n * render a snippet — the formatter (not this parser) is responsible for\n * any presentation. Returns undefined if loc or source text is missing.\n */\nfunction extractUnsupportedSource(\n loc:\n | {\n start: { line: number; column: number };\n end: { line: number; column: number };\n }\n | null\n | undefined,\n code: string | undefined,\n): UnsupportedExportSource | undefined {\n if (!loc || !code) return undefined;\n const lineText = cod