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

798 lines (790 loc) 32.9 kB
import { createRequire } from "node:module"; import { transform } from "@swc/core"; import { readFileSync } from "node:fs"; import { dirname, relative, resolve } from "node:path"; import { normalizePath } from "vite"; import { createEvaluator } from "../isolated-source-eval/index.js"; import { resolveYakContext } from "../withYak/index.js"; import { relative as relative$1 } from "path"; import { parse } from "@babel/parser"; //#region cross-file-resolver/parseModule.ts async function parseModule(context, modulePath) { try { if ((modulePath.endsWith(".yak.ts") || modulePath.endsWith(".yak.tsx") || modulePath.endsWith(".yak.js") || modulePath.endsWith(".yak.jsx")) && context.evaluateYakModule) return { type: "yak", exports: { importYak: false, named: objectToModuleExport(await context.evaluateYakModule(modulePath)), all: [] }, path: modulePath }; if (context.cache?.parse === void 0) return await uncachedParseModule(context, modulePath); const cached = context.cache.parse.get(modulePath); if (cached === void 0) { const parsedModule = await uncachedParseModule(context, modulePath); context.cache.parse.set(modulePath, parsedModule); if (context.cache.parse.addDependency) context.cache.parse.addDependency(modulePath, modulePath); return parsedModule; } return cached; } catch (error) { const causeMessage = error instanceof Error ? error.message : String(error); throw new Error(`Error parsing file "${modulePath}"\n Caused by: ${causeMessage}`); } } async function uncachedParseModule(context, modulePath) { const exports = await context.extractExports(modulePath); if (!exports.importYak) return { type: "regular", path: modulePath, exports }; const transformed = await context.getTransformed(modulePath); const mixins = parseMixins(transformed.code); return { type: "regular", path: modulePath, js: transformed, exports, styledComponents: parseStyledComponents(transformed.code, context.transpilationMode), mixins }; } function parseMixins(sourceContents) { const mixinParts = sourceContents.split("/*YAK EXPORTED MIXIN:"); let mixins = {}; for (let i = 1; i < mixinParts.length; i++) { const [comment] = mixinParts[i].split("*/", 1); const position = comment.indexOf("\n"); const name = comment.slice(0, position); mixins[name] = { type: "mixin", value: comment.slice(position + 1), nameParts: name.split(":").map((part) => decodeURIComponent(part)) }; } return mixins; } function parseStyledComponents(sourceContents, transpilationMode) { const styledParts = sourceContents.split("/*YAK EXPORTED STYLED:"); let styledComponents = {}; for (let i = 1; i < styledParts.length; i++) { const [comment] = styledParts[i].split("*/", 1); const [componentName, className] = comment.split(":"); styledComponents[componentName] = { type: "styled-component", nameParts: componentName.split("."), value: transpilationMode === "Css" ? `.${className}` : `:global(.${className})` }; } return styledComponents; } function objectToModuleExport(object) { return Object.fromEntries(Object.entries(object).map(([key, value]) => { if (typeof value === "string" || typeof value === "number") return [key, { type: "constant", value }]; else if (value && (typeof value === "object" || Array.isArray(value))) return [key, { type: "record", value: objectToModuleExport(value) }]; else return [key, { type: "unsupported", hint: String(value) }]; })); } //#endregion //#region cross-file-resolver/Errors.ts var CauseError = class CauseError extends Error { constructor(message, options) { super(`${message}${options?.cause ? `\n Caused by: ${typeof options.cause === "object" && options.cause !== null && "message" in options.cause ? options.cause.message : String(options.cause)}` : ""}`); if (options?.cause instanceof CauseError && options.cause.circular) this.circular = true; } }; var ResolveError = class extends CauseError {}; var UnsupportedExportError = class extends ResolveError {}; var CircularDependencyError = class extends CauseError { constructor(message, options) { super(message, options); this.circular = true; } }; //#endregion //#region cross-file-resolver/resolveCrossFileConstant.ts const yakCssImportRegex = /--yak-css-import:\s*url\("([^"]+)",?(|mixin|selector)\)(;?)/g; async function resolveCrossFileConstant(context, filePath, css) { const resolveCrossFileConstant = context.cache?.resolveCrossFileConstant; if (resolveCrossFileConstant === void 0) return uncachedResolveCrossFileConstant(context, filePath, css); const cacheKey = await sha1(filePath + ":" + css); const cached = resolveCrossFileConstant.get(cacheKey); if (cached === void 0) { const resolvedCrossFilConstantPromise = uncachedResolveCrossFileConstant(context, filePath, css); resolveCrossFileConstant.set(cacheKey, resolvedCrossFilConstantPromise); if (resolveCrossFileConstant.addDependency) { resolveCrossFileConstant.addDependency(cacheKey, filePath); resolvedCrossFilConstantPromise.then((value) => { for (const dep of value.dependencies) resolveCrossFileConstant.addDependency(cacheKey, dep); }); } return resolvedCrossFilConstantPromise; } return cached; } async function uncachedResolveCrossFileConstant(context, filePath, css) { const yakImports = await parseYakCssImport(context, filePath, css); if (yakImports.length === 0) return { resolved: css, dependencies: [] }; try { const dependencies = /* @__PURE__ */ new Set(); const resolvedValues = await Promise.all(yakImports.map(async ({ moduleSpecifier, specifier }) => { const { resolved: resolvedModule } = await resolveModule(context, moduleSpecifier); const resolvedValue = await resolveModuleSpecifierRecursively(context, resolvedModule, specifier); for (const dependency of resolvedValue.from) dependencies.add(dependency); return resolvedValue; })); let result = css; for (let i = yakImports.length - 1; i >= 0; i--) { const { position, size, importKind, specifier, semicolon } = yakImports[i]; const resolved = resolvedValues[i]; let replacement; if (resolved.type === "unresolved-tag") replacement = importKind === "mixin" ? "" : "undefined"; else { if (importKind === "selector") { if (resolved.type !== "styled-component" && resolved.type !== "constant") throw new Error(`Found "${resolved.type}" but expected a selector - did you forget a semicolon after "${specifier.join(".")}"?`); } replacement = resolved.type === "styled-component" ? resolved.value : resolved.value + (["}", ";"].includes(String(resolved.value).trimEnd().slice(-1)) ? "" : semicolon); } result = result.slice(0, position) + String(replacement) + result.slice(position + size); } return { resolved: result, dependencies: Array.from(dependencies) }; } catch (error) { throw new CauseError(`Error while resolving cross-file selectors in file "${filePath}"`, { cause: error }); } } async function parseYakCssImport(context, filePath, css) { const yakImports = []; for (const match of css.matchAll(yakCssImportRegex)) { const [fullMatch, encodedArguments, importKind, semicolon] = match; const [moduleSpecifier, ...specifier] = encodedArguments.split(":").map((entry) => decodeURIComponent(entry)); yakImports.push({ encodedArguments, moduleSpecifier: await context.resolve(moduleSpecifier, filePath), specifier, importKind, semicolon, position: match.index, size: fullMatch.length }); } return yakImports; } async function resolveModule(context, filePath) { if (context.cache?.resolve === void 0) return uncachedResolveModule(context, filePath); const cached = context.cache.resolve.get(filePath); if (cached === void 0) { const resolvedPromise = uncachedResolveModule(context, filePath); context.cache.resolve.set(filePath, resolvedPromise); if (context.cache.resolve.addDependency) { context.cache.resolve.addDependency(filePath, filePath); resolvedPromise.then((value) => { for (const dep of value.dependencies) context.cache.resolve.addDependency(filePath, dep); }); } return resolvedPromise; } return cached; } async function uncachedResolveModule(context, filePath) { const parsedModule = await context.parse(filePath); const exports = parsedModule.exports; if (parsedModule.type !== "regular") return { resolved: { path: parsedModule.path, exports }, dependencies: [] }; const dependencies = /* @__PURE__ */ new Set(); if (parsedModule.styledComponents) Object.values(parsedModule.styledComponents).map((styledComponent) => { if (styledComponent.nameParts.length === 1) exports.named[styledComponent.nameParts[0]] = { type: "styled-component", className: styledComponent.value }; else { let exportEntry = exports.named[styledComponent.nameParts[0]]; if (!exportEntry) { exportEntry = { type: "record", value: {} }; exports.named[styledComponent.nameParts[0]] = exportEntry; } else if (exportEntry.type !== "record") throw new CauseError(`Error parsing file "${parsedModule.path}"`, { cause: `"${styledComponent.nameParts[0]}" is not a record` }); let current = exportEntry.value; for (let i = 1; i < styledComponent.nameParts.length - 1; i++) { let next = current[styledComponent.nameParts[i]]; if (!next) { next = { type: "record", value: {} }; current[styledComponent.nameParts[i]] = next; } else if (next.type !== "record") throw new CauseError(`Error parsing file "${parsedModule.path}"`, { cause: `"${styledComponent.nameParts.slice(0, i + 1).join(".")}" is not a record` }); current = next.value; } current[styledComponent.nameParts[styledComponent.nameParts.length - 1]] = { type: "styled-component", className: styledComponent.value }; } }); if (parsedModule.mixins) await Promise.all(Object.values(parsedModule.mixins).map(async (mixin) => { const { resolved, dependencies: deps } = await resolveCrossFileConstant(context, parsedModule.path, mixin.value); for (const dep of deps) dependencies.add(dep); if (mixin.nameParts.length === 1) exports.named[mixin.nameParts[0]] = { type: "mixin", value: resolved }; else { let exportEntry = exports.named[mixin.nameParts[0]]; if (!exportEntry) { exportEntry = { type: "record", value: {} }; exports.named[mixin.nameParts[0]] = exportEntry; } else if (exportEntry.type !== "record") throw new CauseError(`Error parsing file "${parsedModule.path}"`, { cause: `"${mixin.nameParts[0]}" is not a record` }); let current = exportEntry.value; for (let i = 1; i < mixin.nameParts.length - 1; i++) { let next = current[mixin.nameParts[i]]; if (!next) { next = { type: "record", value: {} }; current[mixin.nameParts[i]] = next; } else if (next.type !== "record") throw new CauseError(`Error parsing file "${parsedModule.path}"`, { cause: `"${mixin.nameParts.slice(0, i + 1).join(".")}" is not a record` }); current = next.value; } current[mixin.nameParts[mixin.nameParts.length - 1]] = { type: "mixin", value: resolved }; } })); return { resolved: { path: parsedModule.path, exports }, dependencies: Array.from(dependencies) }; } async function resolveModuleSpecifierRecursively(context, resolvedModule, specifiers, seen = /* @__PURE__ */ new Set()) { const exportName = specifiers[0]; const exportValue = resolvedModule.exports.named[exportName]; if (exportValue !== void 0) { if (seen.has(resolvedModule.path + ":" + exportName)) throw new CircularDependencyError(`Unable to resolve "${specifiers.join(".")}" in module "${resolvedModule.path}"`, { cause: "Circular dependency detected" }); seen.add(resolvedModule.path + ":" + exportName); return resolveModuleExport(context, resolvedModule.path, exportValue, specifiers, seen); } let i = 1; for (const from of resolvedModule.exports.all) { if (context.exportAllLimit && i++ > context.exportAllLimit) throw new ResolveError(`Unable to resolve "${specifiers.join(".")}" in module "${resolvedModule.path}"`, { cause: `More than ${context.exportAllLimit} star exports are not supported for performance reasons` }); try { const resolved = await resolveModuleExport(context, resolvedModule.path, { type: "re-export", from, name: exportName }, specifiers, seen); if (seen.has(resolvedModule.path + ":*")) throw new CircularDependencyError(`Unable to resolve "${specifiers.join(".")}" in module "${resolvedModule.path}"`, { cause: "Circular dependency detected" }); seen.add(resolvedModule.path + ":*"); return resolved; } catch (error) { if (!(error instanceof ResolveError)) throw error; if (error.circular) throw error; } } throw new ResolveError(`Unable to resolve "${specifiers.join(".")}"`, { cause: `no matching export found in module "${resolvedModule.path}"` }); } async function resolveModuleExport(context, filePath, moduleExport, specifiers, seen) { const failureMessage = `Unable to resolve "${specifiers.join(".")}" in module "${filePath}"`; try { switch (moduleExport.type) { case "re-export": { const { resolved: reExportedModule } = await resolveModule(context, await context.resolve(moduleExport.from, filePath)); const resolved = await resolveModuleSpecifierRecursively(context, reExportedModule, [moduleExport.name, ...specifiers.slice(1)], seen); if (resolved) resolved.from.push(filePath); return resolved; } case "namespace-re-export": { const { resolved: reExportedModule } = await resolveModule(context, await context.resolve(moduleExport.from, filePath)); const resolved = await resolveModuleSpecifierRecursively(context, reExportedModule, specifiers.slice(1), seen); if (resolved) resolved.from.push(filePath); return resolved; } case "styled-component": return { type: "styled-component", from: [filePath], source: filePath, name: specifiers[specifiers.length - 1], value: moduleExport.className }; case "tag-template": return { type: "unresolved-tag", from: [filePath], source: filePath, name: specifiers[specifiers.length - 1] }; case "constant": return { type: "constant", from: [filePath], source: filePath, value: moduleExport.value }; case "record": return resolveModuleExport(context, filePath, resolveSpecifierInRecord(moduleExport, specifiers[0], specifiers.slice(1)), specifiers, seen); case "mixin": return { type: "mixin", from: [filePath], source: filePath, value: moduleExport.value }; case "unsupported": throw new UnsupportedExportError(failureMessage, { cause: explainUnsupported(filePath, specifiers.join("."), moduleExport.hint, moduleExport.source) }); } } catch (error) { if (error instanceof UnsupportedExportError) throw error; throw new ResolveError(failureMessage, { cause: error }); } } function explainUnsupported(filePath, specifier, hint, source) { const isYakFile = /\.yak\.(?:ts|tsx|js|jsx)$/.test(filePath); const docs = "https://yak.js.org/docs/migration-from-styled-components#move-some-code-to-yak-files"; const snippet = source ? renderSourceSnippet(filePath, source, hint ?? "") : void 0; const lines = []; if (isYakFile) { const got = hint ? ` (got \`${hint}\`)` : ""; lines.push(`\`${specifier}\` evaluated to a value that cannot be inlined into CSS${got}.`); if (snippet) lines.push(snippet); lines.push(` help: replace it with a string, number, or plain object/array of those`, ` see: ${docs}`); return lines.join("\n"); } const got = hint ? ` (got a ${hint})` : ""; lines.push(`\`${specifier}\` is not a string or number literal${got}.`); if (snippet) lines.push(snippet); lines.push(` help: rename "${filePath}" to "${suggestYakFileName(filePath)}" so its exports run at build time`, ` (or replace \`${specifier}\` with a literal value)`, ` see: ${docs}`); return lines.join("\n"); } function suggestYakFileName(filePath) { const match = filePath.match(/^(.*)\.(ts|tsx|js|jsx)$/); if (!match) return filePath; return `${match[1]}.yak.${match[2]}`; } function renderSourceSnippet(filePath, source, label) { const startLine = source.start.line; const startCol = source.start.column; const caretLen = source.end.line === startLine ? Math.max(1, source.end.column - startCol) : Math.max(1, source.lineText.length - startCol); const lineNumStr = String(startLine); const gutterPad = " ".repeat(lineNumStr.length); return [ ` --> ${filePath}:${startLine}:${startCol + 1}`, ` ${gutterPad} |`, ` ${lineNumStr} | ${source.lineText}`, ` ${gutterPad} | ${" ".repeat(startCol)}${"^".repeat(caretLen)} ${label}` ].join("\n"); } function resolveSpecifierInRecord(record, name, specifiers) { if (specifiers.length === 0) throw new ResolveError("did not expect an object"); let depth = 0; let current = record; while (current && current.type === "record" && depth < specifiers.length) { current = current.value[specifiers[depth]]; depth += 1; } if (current === void 0 || depth !== specifiers.length) throw new ResolveError(`Unable to resolve "${specifiers.join(".")}" in object/array "${name}"`, { cause: "path not found" }); if (current.type === "constant" || current.type === "styled-component" || current.type === "mixin") return current; if (current.type === "record" && "__yak" in current.value && current.value.__yak.type === "constant") return { type: "mixin", value: String(current.value.__yak.value) }; throw new ResolveError(`Unable to resolve "${specifiers.join(".")}" in object/array "${name}"`, { cause: "only string and numbers are supported" }); } async function sha1(message) { const resultBuffer = await globalThis.crypto.subtle.digest("SHA-1", new TextEncoder().encode(message)); return Array.from(new Uint8Array(resultBuffer), (byte) => byte.toString(16).padStart(2, "0")).join(""); } //#endregion //#region loaders/lib/debugLogger.ts function createDebugLogger(debugOptions, rootPath) { if (!debugOptions) return () => {}; throwOnDeprecatedDebugOptions(debugOptions); const pattern = debugOptions === true ? void 0 : debugOptions.pattern; const typesArray = debugOptions === true ? void 0 : debugOptions.types; let compiledPattern = null; if (pattern) try { compiledPattern = new RegExp(pattern); } catch (error) { throw new Error(`Invalid debug pattern: "${pattern}" is not a valid regular expression. ${error instanceof Error ? error.message : ""}`); } const types = typesArray ? new Set(typesArray) : null; return (messageType, message, filePath) => { if (types && !types.has(messageType)) return; const relativePath = relative$1(rootPath, filePath); if (!compiledPattern || compiledPattern.test(relativePath)) console.log("🐮 Yak", `[${messageType}]`, relativePath, "\n\n", message); }; } function throwOnDeprecatedDebugOptions(debugOptions) { if (typeof debugOptions === "string") { const suggestion = suggestTypesForExtensionPattern(debugOptions) ?? `debug: { pattern: "${debugOptions}" }`; throw new Error(`The debug option no longer accepts a string. Please update your config:\n Before: debug: "${debugOptions}"\n After: ${suggestion}`); } if (typeof debugOptions === "object" && "filter" in debugOptions) throw new Error("The debug option no longer accepts { filter, type }. Please update your config:\n Before: debug: { filter: ..., type: \"...\" }\n After: debug: { pattern: \"...\", types: [\"ts\", \"css\", \"css-resolved\"] }"); if (typeof debugOptions === "object" && debugOptions.pattern) { const suggestion = suggestTypesForExtensionPattern(debugOptions.pattern); if (suggestion) throw new Error(`The debug pattern "${debugOptions.pattern}" looks like it's filtering by output type using the old file extension convention.\nThe pattern now only matches file paths. Use the "types" option to filter by output type:\n Before: debug: { pattern: "${debugOptions.pattern}" }\n After: ${suggestion}`); } } function suggestTypesForExtensionPattern(pattern) { const extensionMatch = pattern.match(/\.\(?(?:css-resolved|css)\)?\$?$/); if (!extensionMatch) return null; const type = extensionMatch[0].includes("css-resolved") ? "css-resolved" : "css"; const remaining = pattern.slice(0, extensionMatch.index); return remaining ? `debug: { pattern: "${remaining}", types: ["${type}"] }` : `debug: { types: ["${type}"] }`; } //#endregion //#region loaders/lib/extractCss.ts function extractCss(code, transpilationMode) { let codeString; if (typeof code === "string") codeString = code; else if (code instanceof Buffer) codeString = code.toString("utf-8"); else if (code instanceof ArrayBuffer) codeString = new TextDecoder("utf-8").decode(code); else throw new Error("Invalid input type: code must be string, Buffer, or ArrayBuffer"); const codeParts = codeString.split("/*YAK Extracted CSS:\n"); let result = ""; for (let i = 1; i < codeParts.length; i++) { const codeUntilEnd = codeParts[i].split("*/")[0]; result += codeUntilEnd; } if (result && transpilationMode !== "Css") result = "/* cssmodules-pure-no-check */\n" + result; return result; } //#endregion //#region loaders/lib/resolveCrossFileSelectors.ts async function parseExports(sourceContents) { try { const ast = parse(sourceContents, { sourceType: "module", plugins: ["jsx", "typescript"] }); const moduleExports = { importYak: ast.program.body.some((node) => node.type === "ImportDeclaration" && node.source.value === "next-yak"), named: {}, all: [] }; const variableDeclarations = {}; let defaultIdentifier = null; for (const node of ast.program.body) { if (node.type === "VariableDeclaration") { for (const decl of node.declarations) if (decl.id.type === "Identifier" && decl.init) variableDeclarations[decl.id.name] = decl.init; } if (node.type === "ExportNamedDeclaration") { if (node.source) for (const specifier of node.specifiers) { if (specifier.type === "ExportSpecifier" && specifier.exported.type === "Identifier" && specifier.local.type === "Identifier") moduleExports.named[specifier.exported.name] = { type: "re-export", from: node.source.value, name: specifier.local.name }; if (specifier.type === "ExportNamespaceSpecifier" && specifier.exported.type === "Identifier") moduleExports.named[specifier.exported.name] = { type: "namespace-re-export", from: node.source.value }; } else if (node.declaration?.type === "VariableDeclaration") { for (const declaration of node.declaration.declarations) if (declaration.id.type === "Identifier" && declaration.init) { variableDeclarations[declaration.id.name] = declaration.init; const parsed = parseExportValueExpression(declaration.init, sourceContents); if (parsed) moduleExports.named[declaration.id.name] = parsed; } } } if (node.type === "ExportDefaultDeclaration") if (node.declaration.type === "Identifier") defaultIdentifier = node.declaration.name; else if (node.declaration.type === "FunctionDeclaration" || node.declaration.type === "ClassDeclaration") moduleExports.named["default"] = { type: "unsupported", hint: node.declaration.type, source: extractUnsupportedSource(node.declaration.loc, sourceContents) }; else moduleExports.named["default"] = parseExportValueExpression(node.declaration, sourceContents); if (node.type === "ExportAllDeclaration") moduleExports.all.push(node.source.value); } if (defaultIdentifier && variableDeclarations[defaultIdentifier]) moduleExports.named["default"] = parseExportValueExpression(variableDeclarations[defaultIdentifier], sourceContents); return moduleExports; } catch (error) { throw new Error(`Error parsing exports: ${error.message}`); } } function unpackTSAsExpression(node) { if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression") return unpackTSAsExpression(node.expression); return node; } function parseExportValueExpression(node, code) { const expression = unpackTSAsExpression(node); if (expression.type === "CallExpression" || expression.type === "TaggedTemplateExpression") return { type: "tag-template" }; else if (expression.type === "StringLiteral" || expression.type === "NumericLiteral") return { type: "constant", value: expression.value }; else if (expression.type === "UnaryExpression" && expression.operator === "-" && expression.argument.type === "NumericLiteral") return { type: "constant", value: -expression.argument.value }; else if (expression.type === "TemplateLiteral" && expression.quasis.length === 1) return { type: "constant", value: expression.quasis[0].value.raw }; else if (expression.type === "ObjectExpression") return { type: "record", value: parseObjectExpression(expression, code) }; return { type: "unsupported", hint: expression.type, source: extractUnsupportedSource(expression.loc, code) }; } function parseObjectExpression(node, code) { let result = {}; for (const property of node.properties) if (property.type === "ObjectProperty" && property.key.type === "Identifier") { const key = property.key.name; const parsed = parseExportValueExpression(property.value, code); if (parsed) result[key] = parsed; } return result; } function extractUnsupportedSource(loc, code) { if (!loc || !code) return void 0; const lineText = code.split(/\r?\n/)[loc.start.line - 1]; if (lineText === void 0) return void 0; return { start: { line: loc.start.line, column: loc.start.column }, end: { line: loc.end.line, column: loc.end.column }, lineText }; } //#endregion //#region loaders/vite-plugin.ts const require = createRequire(import.meta.url); const defaultSwcOptions = { jsc: { parser: { syntax: "typescript", tsx: true, decorators: false, dynamicImport: true }, transform: { react: { runtime: "preserve" } }, target: "es2022", loose: false, minify: { compress: false, mangle: false }, preserveAllComments: true }, minify: false, isModule: true }; async function viteYak(userOptions = {}) { const yakOptions = { experiments: { transpilationMode: "Css", suppressDeprecationWarnings: false, ...userOptions.experiments }, minify: userOptions.minify ?? process.env.NODE_ENV === "production", prefix: userOptions.prefix, contextPath: userOptions.contextPath, swcOptions: deepMerge(defaultSwcOptions, userOptions.swcOptions ?? {}) }; yakOptions.displayNames = userOptions.displayNames ?? yakOptions.displayNames ?? !yakOptions.minify; let basePath = userOptions.basePath ?? ""; let hasWarnedAboutBasePath = false; let debugLog = () => {}; let isServe = false; const sourceFileRegex = /\.(tsx?|m?jsx?)\??/; const virtualModuleRegex = /^virtual:yak-css:/; const virtualCssModuleRegex = /^\0virtual:yak-css:/; const yakSwcPath = await findYakSwcPlugin(); const evaluator = await createEvaluator(); return { name: "vite-plugin-yak:css:pre", enforce: "pre", config: (config) => { const context = resolveYakContext(yakOptions.contextPath, config.root ?? process.cwd()); if (!context) return; config.resolve ||= {}; if (Array.isArray(config.resolve.alias)) config.resolve.alias.push({ find: "next-yak/context/baseContext", replacement: context }); else config.resolve.alias = { ...config.resolve.alias, "next-yak/context/baseContext": context }; }, configResolved(config) { basePath = basePath ? resolve(config.root, basePath) : config.root; debugLog = createDebugLogger(yakOptions.experiments?.debug, basePath); isServe = config.command === "serve"; }, resolveId: { filter: { id: virtualModuleRegex }, handler(id) { return "\0" + id; } }, load: { filter: { id: virtualCssModuleRegex }, async handler(id) { const queryStringStart = id.indexOf("?"); const queryString = queryStringStart === -1 ? "" : id.slice(queryStringStart); const relativeId = id.slice(17, -4 - queryString.length); const originalId = resolve(basePath, relativeId); this.addWatchFile(originalId); const extractedCss = extractCss((await transform$1(await this.fs.readFile(originalId, { encoding: "utf8" }), originalId, basePath, yakSwcPath, yakOptions)).code, "Css"); debugLog("css", extractedCss, originalId); const { resolved } = await resolveCrossFileConstant({ parse: (modulePath) => { return parseModule({ transpilationMode: "Css", extractExports: async (modulePath) => { const sourceContent = await this.fs.readFile(modulePath, { encoding: "utf8" }); this.addWatchFile(modulePath); return parseExports(sourceContent); }, getTransformed: async (modulePath) => { return transform$1(await this.fs.readFile(modulePath, { encoding: "utf8" }), modulePath, basePath, yakSwcPath, yakOptions); }, evaluateYakModule: async (modulePath) => { this.addWatchFile(modulePath); const result = await evaluator.evaluate(modulePath); if (!result.ok) throw new Error(result.error.message); for (const dep of result.dependencies) this.addWatchFile(dep); return result.value; } }, modulePath); }, resolve: async (moduleSpecifier, context) => { let importer = context; const resolved = await this.resolve(moduleSpecifier, importer); if (!resolved) throw new Error(`Could not resolve ${moduleSpecifier} from ${context}`); return resolved.id; } }, originalId + queryString, extractedCss); debugLog("css-resolved", resolved, originalId); return resolved; } }, configureServer(server) { server.watcher.on("change", (file) => { evaluator.invalidate(file); }); }, transform: { filter: { id: { include: sourceFileRegex, exclude: [/packages\/next-yak/] }, code: "next-yak" }, async handler(code, id) { try { const filePath = id.split("?")[0]; if (!hasWarnedAboutBasePath) { if (relative(basePath, filePath).startsWith("..")) { hasWarnedAboutBasePath = true; console.warn(`[next-yak] Source file "${filePath}" is outside the project root "${basePath}".\nThis may cause CSS resolution issues in monorepo setups.\nSet the "basePath" option to your monorepo root:\n\n viteYak({ basePath: "/absolute/path/to/monorepo/root" })\n`); } } const result = await transform$1(code, filePath, basePath, yakSwcPath, yakOptions, isServe); debugLog("ts", result.code, id); return { code: result.code, map: result.map }; } catch (error) { this.error(`[YAK Plugin] Error transforming ${id}: ${error.message}`); } } }, hotUpdate({ modules, file, type }) { if (type !== "update" && type !== "create") return; if (!sourceFileRegex.test(file)) return; const virtualId = "\0virtual:yak-css:" + normalizePath(relative(basePath, file)) + ".css"; const mod = this.environment.moduleGraph.getModuleById(virtualId); if (mod) { this.environment.moduleGraph.invalidateModule(mod); return [...modules, mod]; } } }; } async function findYakSwcPlugin() { try { const packageJsonPath = require.resolve("yak-swc/package.json"); return resolve(dirname(packageJsonPath), JSON.parse(readFileSync(packageJsonPath, "utf-8")).main); } catch (e) { throw new Error(`Could not resolve yak-swc plugin: ${e}`); } } function transform$1(data, modulePath, rootPath, yakSwcPath, yakOptions, reactRefreshReg) { return transform(data, { filename: modulePath, inputSourceMap: void 0, sourceMaps: true, sourceFileName: modulePath, sourceRoot: rootPath, ...yakOptions.swcOptions, jsc: { ...yakOptions.swcOptions?.jsc, experimental: { plugins: [[yakSwcPath, { minify: yakOptions.minify, basePath: rootPath, prefix: yakOptions.prefix, displayNames: yakOptions.displayNames, suppressDeprecationWarnings: yakOptions.experiments?.suppressDeprecationWarnings, ...reactRefreshReg ? { reactRefreshReg: true } : {}, importMode: { value: "virtual:yak-css:{{__MODULE_PATH__}}.css", transpilation: "Css", encoding: "None" } }]] } } }); } function deepMerge(target, source) { const result = { ...target }; for (const key of Object.keys(source)) { const sourceValue = source[key]; const targetValue = target[key]; if (sourceValue !== void 0 && typeof sourceValue === "object" && sourceValue !== null && !Array.isArray(sourceValue) && typeof targetValue === "object" && targetValue !== null && !Array.isArray(targetValue)) result[key] = deepMerge(targetValue, sourceValue); else if (sourceValue !== void 0) result[key] = sourceValue; } return result; } //#endregion export { viteYak }; //# sourceMappingURL=vite-plugin.js.map