Skip to content

Commit

Permalink
🎨 Add initial css modules support to kastro
Browse files Browse the repository at this point in the history
  • Loading branch information
KimlikDAO-bot committed Jan 1, 2025
1 parent a0a63b5 commit 234b50f
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 22 deletions.
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ jobs:
bun run test --filter did crypto
compile_kdjs_with_kdjs:
if: false # Temporarily disabled
name: Compile `kdjs` with `kdjs`
runs-on: ubuntu-latest
steps:
Expand Down
31 changes: 20 additions & 11 deletions kastro/compiler/loader/stylesheetLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,27 @@ const extractIdentifiers = () => {
const parsedCss = parse(cssContent);
const rulesStack = [parsedCss.stylesheet.rules];

for (let rules; rules = rulesStack.pop();)
rules.forEach((rule) => {
if (rule.type === "rule") {
rule.selectors.forEach((selector) => {
const matches = selector.match(/[.#][a-zA-Z_-][a-zA-Z0-9_-]*/g);
if (matches)
matches.forEach((match) =>
ExportedStyleSheet[match.replace("#", "$").replace(".", "")] = match.slice(1));
});
} else if (rule.type === "media" || rule.type === "supports")
for (let rules; rules = rulesStack.pop();) {
let maybeNamed;
for (const rule of rules) {
if (rule.type == "comment") {
const matches = rule.comment.match(/@(?:export|name)\s*{(.*)}/);
if (matches) maybeNamed = matches[1].trim();
} else if (rule.type == "rule") {
if (maybeNamed) {
if (rule.selectors.length != 1 || rule.selectors[0].includes(" "))
throw `Named or exported selectors must be singletons. Violating rule: ${rule.position.content}`;
ExportedStyleSheet[maybeNamed] = rule.selectors[0].slice(1);
}
for (const selector of rule.selectors) {
if ((selector.startsWith(".") || selector.startsWith("#")) && !selector.match(/[>.: ]/))
ExportedStyleSheet[selector.replace("#", "$").replace(".", "")] = selector.slice(1);
}
maybeNamed = null;
} else if (rule.type == "media" || rule.type == "supports")
rulesStack.push(rule.rules);
});
}
}
};

extractIdentifiers();
Expand Down
35 changes: 32 additions & 3 deletions kastro/kastro.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { getGlobals } from "./compiler/pageGlobals";
import { scriptTarget } from "./compiler/script";
import { stylesheetTarget } from "./compiler/stylesheet";
import { registerTargetFunction } from "./compiler/targetRegistry";
import { processCss } from "../kdjs/stylesheet";

const setupKastro = () => {
registerTargetFunction(".html", pageTarget);
Expand Down Expand Up @@ -110,12 +111,14 @@ const serveCrate = async (crateName, buildMode) => {
const crate = await import(crateName);
const map = cratePageProps(crate, buildMode);
let currentPageProps;
let currentPageGlobalsPattern;

createServer({
appType: "mpa",
publicDir: buildMode == compiler.BuildMode.Dev ? "" : "build/crate",
plugins: [{
name: "kastro-js",
enforce: "pre",

configureServer(server) {
server.middlewares.use(async (req, res, next) => {
Expand All @@ -124,15 +127,41 @@ const serveCrate = async (crateName, buildMode) => {
server.moduleGraph.invalidateAll();
currentPageProps = map[req.originalUrl];
compiler.forceBuildTarget(currentPageProps.targetName, currentPageProps)
.then((content) => res.end(content));
.then((content) => {
const globals = getGlobals();
globals.GEN = false;
currentPageGlobalsPattern = new RegExp(Object.keys(globals)
.map((key) => `/\\*\\* @define \\{[^}]*\\} \\*/\\s*const ${key} =.*?;`)
.join("|"), "g");
res.end(content)
});
} else next();
})
},

resolveId(source, importer) {
if (source.endsWith(".css"))
return importer.slice(0, importer.lastIndexOf("/") + 1) + source + ".js";
},

load(id) {
if (id.endsWith(".css.js"))
return readFile(id.slice(0, -3), "utf8").then((css) => processCss(css));
},

transform(code, id) {
if (id.endsWith(".jsx")) {
const lines = code.split("\n");
const filteredLines = lines.filter((line) => line.includes("util/dom") ||
line.trim().startsWith("export const"));
return filteredLines.join("\n");
}
const globals = getGlobals();
if (crate.devModeJsTransform)
return crate.devModeJsTransform(id, code, globals);
return code.replace(currentPageGlobalsPattern, (match) => {
const constIdx = match.indexOf("const");
const varName = match.slice(match.indexOf("\nconst") + 6, match.indexOf("=", constIdx)).trim();
return `\nconst ${varName} = ${JSON.stringify(globals[varName])};`
});
}
}]
}).then((vite) => vite.listen(8787))
Expand Down
10 changes: 10 additions & 0 deletions kdjs/externs/css.d.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** @externs */

/** @const */
const css = {};

/**
* @param {string} content
* @return {!Object<string, string>}
*/
css.parse = (content) => { };
18 changes: 10 additions & 8 deletions kdjs/preprocess.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
import { combine, getDir } from "../util/paths";
import { ExportStatement, ImportStatement } from "./modules";
import { serializeWithStringKeys } from "./objects";
import { processCss } from "./stylesheet";
import { Update, update } from "./textual";

const PACKAGE_EXTERNS = "node_modules/@kimlikdao/kdjs/externs/";

/**
* TODO(KimlikDAO-bot): Fix this
* @param {string} fileName
* @return {string}
*/
const ensureExtension = (fileName) => fileName.endsWith(".js") || fileName.endsWith(".jsx")
const ensureExtension = (fileName) => fileName.endsWith(".js") || fileName.endsWith(".jsx") || fileName.endsWith(".css")
? fileName : fileName + ".js";

/**
Expand Down Expand Up @@ -209,14 +211,12 @@ const processJs = (isEntry, file, content, files, globals, unlinkedImports) => {
}

/**
* @param {boolean} isEntry
* @param {string} file name of the file
* @param {string} content of the file
* @param {!Array<string>} files
* @param {!Object<string, *>} globals
* @return {string} file after preprocessing
*/
const processJsx = (isEntry, file, content, files, globals) => {
const processJsx = (file, content, files) => {
/** @const {!Array<string>} */
const lines = content.split("\n");
/** @const {!Array<string>} */
Expand All @@ -227,7 +227,7 @@ const processJsx = (isEntry, file, content, files, globals) => {
if (lines[i].trim().startsWith("export const")) {
if (i > 0) result.push(lines[i - 1]);
result.push(lines[i]);
} else if (lines[i].includes("import dom") && lines[i].includes("util/dom")) {
} else if (lines[i].includes("import") && (lines[i].includes("util/dom") || lines[i].includes(".css"))) {
const importName = lines[i].slice(
lines[i].indexOf('"') + 1,
lines[i].lastIndexOf('"'));
Expand Down Expand Up @@ -263,9 +263,11 @@ const preprocessAndIsolate = async (entryFile, isolateDir, externs, globals) =>
allFiles.add(file);
/** @const {string} */
const content = await readFile(file, "utf8");
const newContent = file.endsWith(".jsx")
? processJsx(file == entryFile, file, content, files, globals)
: processJs(file == entryFile, file, content, files, globals, unlinkedImports);
const newContent = file.endsWith(".js")
? processJs(file == entryFile, file, content, files, globals, unlinkedImports)
: file.endsWith(".jsx")
? processJsx(file, content, files)
: processCss(content);
const outFile = combine(isolateDir, file);
writePromises.push(mkdir(getDir(outFile), { recursive: true })
.then(() => writeFile(outFile, newContent)));
Expand Down
40 changes: 40 additions & 0 deletions kdjs/stylesheet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import css from "css";

/**
* @param {string} content css file content to be converted to a js enum
* @return {string} js code that exports the enum
*/
const processCss = (content) => {
const parsedCss = css.parse(content);
/** @const {!Object<string, string>} */
const exports = {};

const rulesStack = [parsedCss.stylesheet.rules];
for (let rules; rules = rulesStack.pop();) {
let maybeExport;
for (const rule of rules) {
if (rule.type == "comment") {
const matches = rule.comment.match(/@export\s*{(.*)}/);
if (matches) maybeExport = matches[1].trim();
} else if (rule.type == "rule") {
if (maybeExport) {
if (rule.selectors.length != 1 || rule.selectors[0].includes(" "))
throw `Named or exported selectors must be singletons. Violating rule: ${rule.position.content}`;
exports[maybeExport] = rule.selectors[0].slice(1);
}
maybeExport = null;
} else if (rule.type == "media" || rule.type == "supports") {
if (maybeExport)
throw "Only singleton selectors may be named.";
rulesStack.push(rule.rules);
}
}
}
/** @type {string} */
let output = "\n/** @enum {string} */\nconst Style = {\n";
for (const name in exports)
output += ` ${name}: "${exports[name]}",\n`;
return output + "};\n\nexport default Style;\n";
};

export { processCss };

0 comments on commit 234b50f

Please sign in to comment.