Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for --yak-css-import to .yak files #138

Merged
merged 5 commits into from
Jul 24, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 80 additions & 9 deletions packages/next-yak/loaders/lib/resolveCrossFileSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@ import babelPlugin from "@babel/plugin-syntax-typescript";
import type { Compilation, LoaderContext } from "webpack";
import { getCssModuleLocalIdent } from "next/dist/build/webpack/config/blocks/css/loaders/getCssModuleLocalIdent.js";

const moduleSelectorRegex = /--yak-css-import\:\s*url\("([^"]+)"\)/g;
const yakCssImportRegex = /--yak-css-import\:\s*url\("([^"]+)"\)/g;

export async function resolveCrossFileSelectors(
loader: LoaderContext<{}>,
css: string,
): Promise<string> {
const matches = [...css.matchAll(moduleSelectorRegex)].map((match) => {
let fileBasedResolveCache = new Map<
string,
ReturnType<typeof resolveIdentifier>
>();
// Find cross-file-imports
const matches = [...css.matchAll(yakCssImportRegex)].map((match) => {
const [fullMatch, encodedArguments] = match;
const [moduleSpecifier, ...specifier] = encodedArguments
.split(":")
Expand All @@ -28,20 +34,19 @@ export async function resolveCrossFileSelectors(
size: fullMatch.length,
};
});

const firstMatchPosition = matches[0]?.position;
if (firstMatchPosition === undefined) {
return css;
}
// Replace the imports with the resolved values
let result = "";
for (let i = matches.length - 1; i >= 0; i--) {
const { moduleSpecifier, specifier, position, size } = matches[i];

const resolved = await resolveIdentifier(
loader,
loader.context,
const resolved = await resolveCrossFileValue(
moduleSpecifier,
specifier[0],
specifier,
fileBasedResolveCache,
loader,
);
if (resolved.type === "unsupported") {
throw new Error(
Expand All @@ -50,7 +55,6 @@ export async function resolveCrossFileSelectors(
)} from ${moduleSpecifier} - only styled-components, strings and numbers are supported`,
);
}

const replacement =
resolved.type === "styled-component"
? `:global(.${getCssModuleLocalIdent(
Expand Down Expand Up @@ -95,6 +99,66 @@ const getConstantFromResolvedValue = (
);
};

/**
* Resolve the value of from a cross-file-import
*
* For regular files the specifier is resolved with resolveIdentifier
* For .yak files the entire module is resolved and evaluated with resolveYakModule
*/
async function resolveCrossFileValue(
moduleSpecifier: string,
specifier: string[],
resolveCache: Map<string, ReturnType<typeof resolveIdentifier>>,
loader: LoaderContext<{}>,
) {
const isYak = moduleSpecifier.endsWith(".yak");
let resolvedModule: ReturnType<typeof resolveIdentifier>;
if (!isYak) {
// For non .yak files only the specifier is resolved
// therfore only the specifier can be cached
const resolveKey = `${moduleSpecifier} : ${specifier[0]}`;
let resolvedFromCache = resolveCache.get(resolveKey);
resolvedModule =
resolvedFromCache ||
resolveIdentifier(loader, loader.context, moduleSpecifier, specifier[0]);
if (!resolvedFromCache) {
resolveCache.set(resolveKey, resolvedModule);
}
} else {
// For yak files the entire module is executed with node (which is slower) and returned
// therefore the entire module can be cached as record
let resolvedFromCache = resolveCache.get(moduleSpecifier);
resolvedModule =
resolvedFromCache || resolveYakModule(loader, moduleSpecifier);
if (!resolvedFromCache) {
resolveCache.set(moduleSpecifier, resolvedModule);
}
// To align the return value with the non-yak case, we need to resolve the specifier
resolvedModule = resolvedModule.then((moduleValues) => {
if (moduleValues.type !== "record") {
throw new Error("resolveYakModule returns always a record");
}
const value = moduleValues.value[specifier[0]];
if (typeof value === "string" || typeof value === "number") {
return {
type: "constant" as const,
value,
};
}
if (value && (Array.isArray(value) || typeof value === "object")) {
return {
type: "record" as const,
value,
};
}
throw new Error(
`Could not find export ${specifier[0]} in ${moduleSpecifier}`,
);
});
}
return resolvedModule;
}

/**
* Recursively follows the import chain to resolve the identifiers
* type, name and value.
Expand Down Expand Up @@ -345,3 +409,10 @@ function parseObjectExpression(
}
return result;
}

function resolveYakModule(loader: LoaderContext<{}>, moduleSpecifier: string) {
return loader.importModule(moduleSpecifier).then((module) => ({
type: "record" as const,
value: module as RecursiveRecord,
}));
}
Loading