Skip to content
Open
Show file tree
Hide file tree
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
1 change: 0 additions & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"recommendations": [
"biomejs.biome",
"dbaeumer.vscode-eslint",
"MylesMurphy.prettify-ts",
"oven.bun-vscode",
"stylelint.vscode-stylelint",
"tekumara.typos-vscode",
Expand Down
8 changes: 0 additions & 8 deletions .vscode/mcp.json

This file was deleted.

277 changes: 154 additions & 123 deletions bun.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
"$schema" = "https://www.schemastore.org/bunfig.json"

[run]
bun = true

[install]
auto = "disable"
linker = "isolated"
peer = false
minimumReleaseAge = 604800 # 7 days
minimumReleaseAgeExcludes = ["@maxmilton/*"]

[test]
preload = ["./test/setup.ts"]
2 changes: 0 additions & 2 deletions docs/dev/.npmrc

This file was deleted.

11 changes: 11 additions & 0 deletions docs/dev/bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"$schema" = "https://www.schemastore.org/bunfig.json"

logLevel = "debug"

[install]
linker = "isolated"
peer = false
minimumReleaseAge = 604800 # 7 days

[install.lockfile]
save = false
5 changes: 0 additions & 5 deletions docs/dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,5 @@
"devDependencies": {
"microdoc": "*",
"sirv-cli": "*"
},
"__pnpm": {
"overrides": {
"microdoc": "../../../microdoc"
}
}
}
2 changes: 0 additions & 2 deletions docs/dev/pnpm-workspace.yaml

This file was deleted.

4 changes: 3 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ export default defineConfig(
rules: {
"@typescript-eslint/prefer-nullish-coalescing": "warn", // compatibility with node >= v12
"no-plusplus": "off", // byte savings
"unicorn/consistent-boolean-name": "off",
"unicorn/no-array-callback-reference": "off", // byte savings
"unicorn/no-array-for-each": "off", // slower but more compact (for non-performance-critical code)
"unicorn/prefer-at": "off", // bad browser support
"unicorn/prefer-code-point": "off", // faster
"unicorn/prefer-global-this": "off", // prefer to clearly separate Bun and DOM
"unicorn/prefer-string-replace-all": "off", // bad browser support and slower
"unicorn/prefer-uint8array-base64": "off", // not available in node < v25
Expand Down Expand Up @@ -69,5 +71,5 @@ export default defineConfig(
"unicorn/prefer-minimal-ternary": "off",
},
},
{ ignores: ["**/*.bak", "**/dist", "coverage", "packages/framework/*.d.ts"] },
{ ignores: ["**/*.bak", "**/dist", ".claude", "coverage", "packages/framework/*.d.ts"] },
);
18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,23 @@
"test:node:c8": "c8 npm run test:node"
},
"devDependencies": {
"@biomejs/biome": "2.5.1",
"@biomejs/biome": "2.5.5",
"@eslint/js": "10.0.1",
"@maxmilton/eslint-config": "0.3.2",
"@maxmilton/eslint-config": "0.3.4",
"@maxmilton/stylelint-config": "0.4.3",
"@maxmilton/test-utils": "0.0.15",
"@types/bun": "1.3.14",
"@types/node": "26.1.0",
"c8": "11.0.0",
"@types/node": "26.1.1",
"c8": "12.0.0",
"ekscss": "workspace:*",
"eslint": "10.6.0",
"eslint-plugin-unicorn": "70.0.0",
"eslint": "10.8.0",
"eslint-plugin-unicorn": "72.0.0",
"postcss-ekscss": "workspace:*",
"stylelint": "17.14.0",
"stylelint": "17.14.1",
"stylelint-config-standard": "40.0.0",
"turbo": "2.10.2",
"turbo": "2.10.7",
"typescript": "6.0.3",
"typescript-eslint": "8.62.1",
"typescript-eslint": "8.65.0",
"uvu": "0.5.6"
},
"packageManager": "bun@1.99.0"
Expand Down
2 changes: 1 addition & 1 deletion packages/bun-plugin-ekscss/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bun-plugin-ekscss",
"version": "0.0.3",
"version": "0.0.4-next.0",
"description": "Bun plugin to add support for XCSS",
"homepage": "https://ekscss.js.org",
"repository": "github:maxmilton/ekscss",
Expand Down
7 changes: 1 addition & 6 deletions packages/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,7 @@ module.exports = async (src, dest, opts) => {

if (sourcemap) {
if (config.banner) {
const bannerLineCount = config.banner.split("\n").length;
for (let i = 0; i < bannerLineCount; i++) {
// @ts-expect-error - internal property
// eslint-disable-next-line
sourcemap._map._mappings.unshift([]);
}
sourcemap.shift(config.banner.split("\n").length);
}

await Promise.all([
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ekscss/cli",
"version": "0.0.21",
"version": "0.0.22-next.0",
"description": "A simple, fast, and powerful CSS preprocessor",
"homepage": "https://ekscss.js.org",
"repository": "github:maxmilton/ekscss",
Expand Down
2 changes: 1 addition & 1 deletion packages/ekscss/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const out1 = await build({
"process.env.BROWSER": "false",
"process.env.NODE_ENV": JSON.stringify(mode),
},
external: ["@jridgewell/gen-mapping", "stylis"],
external: ["@jridgewell/*", "stylis"],
bundle: true,
sourcemap: true,
minify: !isDev,
Expand Down
4 changes: 3 additions & 1 deletion packages/ekscss/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ekscss",
"version": "0.0.23",
"version": "0.0.24-next.0",
"description": "A simple, fast, and powerful CSS preprocessor",
"homepage": "https://ekscss.js.org",
"repository": "github:maxmilton/ekscss",
Expand All @@ -19,12 +19,14 @@
},
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.13",
"@jridgewell/remapping": "^2.3.5",
"@types/stylis": "^4.2.7",
"stylis": "^4.3.6"
},
"devDependencies": {
"@ekscss/build-tools": "workspace:*",
"@ekscss/plugin-null": "workspace:*",
"@jridgewell/trace-mapping": "0.3.31",
"esbuild": "0.28.1"
}
}
34 changes: 19 additions & 15 deletions packages/ekscss/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,27 +36,31 @@ export function compile(

ctx.rootDir = rootDir;
ctx.from = from;
ctx.raw = map ? code : undefined;
ctx.pos = map ? new Map<string | undefined, number[]>() : undefined;
ctx.fn = fn;
ctx.x = x;
ctx.dependencies = dependencies;
ctx.warnings = warnings;

for (const run of beforeBuild) run();
try {
for (const run of beforeBuild) run();

const interpolated = interpolate(code)(xcss, x, fn);
const ast = stylis.compile(interpolated);
const css = stylis.serialize(ast, stylis.middleware(middlewares));
const interpolated = interpolate(code)(xcss, x, fn);
const ast = stylis.compile(interpolated);
const css = stylis.serialize(ast, stylis.middleware(middlewares));

for (const run of afterBuild) run();
for (const run of afterBuild) run();

// @ts-expect-error - reset ctx to initial state
// dprint-ignore
ctx.rootDir = ctx.from = ctx.fn = ctx.x = ctx.dependencies = ctx.warnings = undefined; // eslint-disable-line no-multi-assign

return {
css,
map: map ? compileSourceMap(code, ast, rootDir, from, to, warnings) : undefined,
dependencies,
warnings,
};
return {
css,
map: map ? compileSourceMap(interpolated, ast, to) : undefined,
dependencies,
warnings,
};
} finally {
// @ts-expect-error - reset ctx for next compile
// biome-ignore format: Reset ctx
ctx.rootDir = ctx.from = ctx.raw = ctx.pos = ctx.fn = ctx.x = ctx.dependencies = ctx.warnings = undefined; // eslint-disable-line no-multi-assign
}
}
104 changes: 79 additions & 25 deletions packages/ekscss/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const toType = (value: unknown): string => Object.prototype.toString.call(value)
export const ctx = {
rootDir: undefined,
from: undefined,
raw: undefined,
pos: undefined,
fn: undefined,
x: undefined,
dependencies: undefined,
Expand Down Expand Up @@ -85,11 +87,9 @@ export function accessorsProxy<T extends object>(obj: T, parentPath: string): T

// eslint-disable-next-line guard-for-in
for (const key in baseTarget) {
// eslint-disable-next-line unicorn/no-unsafe-property-key
const value = baseTarget[key];

if (typeof value === "object" && value !== null) {
// eslint-disable-next-line unicorn/no-unsafe-property-key
baseTarget[key] = accessorsProxy(value, `${parentPath}.${key}`);
}
}
Expand Down Expand Up @@ -183,6 +183,33 @@ export function each<T>(
return out;
}

function resolveExpressionValue(value: Expression): string {
// Reduce XCSS function expressions to their final value
while (typeof value === "function") {
// eslint-disable-next-line no-param-reassign
value = value(ctx.x, ctx.fn);
}

if (typeof value === "object" && value !== null) {
if (typeof value.toString === "function") {
// eslint-disable-next-line no-param-reassign
value = value.toString();
} else {
ctx.warnings.push({
code: "expression-invalid",
message: `Invalid XCSS template expression. Must be string, object with toString() method, number, or falsely but got ${toType(
value,
)}`,
file: ctx.from,
});

return "INVALID";
}
}

return value === false || value == null ? "" : String(value);
}

/**
* XCSS template literal tag function.
*
Expand All @@ -195,35 +222,62 @@ export function xcss(template: TemplateStringsArray, ...expressions: Expression[
let index = 0;
let out = "";

for (; index < len; index++) {
let value = expressions[index - 1];

// Reduce XCSS function expressions to their final value
while (typeof value === "function") {
value = value(ctx.x, ctx.fn);
// eslint-disable-next-line prefer-destructuring
const raw = ctx.raw;
const track = raw !== undefined && len > 1;

// Flat `[genStart, origStart, genStart, origStart, ...]` pairs
let segments: number[] | undefined;
let offsets: number[] | undefined;

if (track) {
segments = [];
ctx.pos!.set(ctx.from, segments);

// Literal chunks (`.raw`) are exact, in-order, non-overlapping
// substrings of the original source, so their original offsets can be
// recovered with a sequential scan. We don't have the source text of
// `${...}` expressions themselves (only their evaluated value), so
// expression segments below collapse to a single point instead.
offsets = [];
let cursor = 0;

for (let i = 0; i < len; i++) {
const found = raw.indexOf(strings[i], cursor);
const start = found === -1 ? cursor : found;
offsets[i] = start;
cursor = start + strings[i].length;
}

if (typeof value === "object" && value !== null) {
if (typeof value.toString === "function") {
value = value.toString();
} else {
ctx.warnings.push({
code: "expression-invalid",
message: `Invalid XCSS template expression. Must be string, object with toString() method, number, or falsely but got ${toType(
value,
)}`,
file: ctx.from,
});
// Guard against reentrancy while position-tracking: an expression can
// itself call `xcss` e.g., `${fn.each(items, (k, v) => xcss`...`)}`.
ctx.raw = undefined;
}

try {
for (; index < len; index++) {
const resolved = resolveExpressionValue(expressions[index - 1]);

if (track && index > 0) {
// The expression value interpolated between the previous literal
// chunk and this one collapses to a single point mapping at the
// original `${` boundary.
segments!.push(out.length, offsets![index - 1] + strings[index - 1].length);
}

out += resolved;

value = "INVALID";
if (track) {
segments!.push(out.length, offsets![index]);
}

out += strings[index];
}

// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing, @typescript-eslint/restrict-plus-operands
out += (value || (value == null || value === false ? "" : value)) + strings[index];
return out;
} finally {
if (track) ctx.raw = raw;
}

return out;
}

/**
Expand All @@ -242,7 +296,7 @@ export function resolvePlugins(plugins: (Middleware | string)[]): Middleware[] {
if (typeof plugin !== "string") return plugin;

try {
// eslint-disable-next-line
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment, global-require
const mod = require(plugin);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return (mod.default ?? mod) as Middleware;
Expand Down
Loading
Loading