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
JavaScript
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