From eb2dfa881a6b26ce1a1267a8833acad65c18f45a Mon Sep 17 00:00:00 2001 From: KimlikDAO-bot Date: Wed, 5 Feb 2025 11:17:01 -0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=A0=20Add=20kdjs=20type=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kastro/transpiler/css.js | 11 ++- kastro/transpiler/jsx.js | 4 +- kastro/transpiler/test/css.test.js | 144 +++++++++++++++++++++++++++++ kastro/transpiler/transpiler.js | 10 +- kdjs/types/test/types.test.js | 38 +++++++- kdjs/types/types.js | 69 ++++++++++---- 6 files changed, 247 insertions(+), 29 deletions(-) create mode 100644 kastro/transpiler/test/css.test.js diff --git a/kastro/transpiler/css.js b/kastro/transpiler/css.js index 9b52cc3..aa923f1 100644 --- a/kastro/transpiler/css.js +++ b/kastro/transpiler/css.js @@ -7,7 +7,7 @@ import { DomIdMapper } from "./domIdMapper"; * default: !Object * }} */ -const CssModule = {}; +export const CssModule = {}; /** @const {!RegExp} */ const ExportAsPattern = /@export\s*{(.*)}/; @@ -138,7 +138,7 @@ const getEnum = (file, content, domIdMapper) => { * @param {!DomIdMapper} domIdMapper * @return {string} js code that exports the enum */ -const transpileCss = (file, content, domIdMapper) => { +const transpile = (file, content, domIdMapper) => { return "\n/** @enum {string} */\nconst Style = " + getEnum(file, content, domIdMapper) + ";\n\nexport default Style;\n"; @@ -156,7 +156,7 @@ const transpileCss = (file, content, domIdMapper) => { * enumEntries: (!Object) * }} */ -const minifyCss = (file, content, domIdMapper) => { +const minify = (file, content, domIdMapper) => { /** @const {string} */ const context = `${splitFullExt(file)[0]}.jsx`; /** @const {!csstree.StyleSheet} */ @@ -234,8 +234,9 @@ const minifyCss = (file, content, domIdMapper) => { }; export default { + EnumModule: CssModule, getEnum, - minifyCss, + minify, selectorToEnumKey, - transpileCss + transpile }; diff --git a/kastro/transpiler/jsx.js b/kastro/transpiler/jsx.js index 8e6d2f5..eee70f7 100644 --- a/kastro/transpiler/jsx.js +++ b/kastro/transpiler/jsx.js @@ -38,7 +38,7 @@ const SpecifierState = { * @param {DomIdMapper} domIdMapper * @return {string} The transpiled js file */ -const transpileJsx = (isEntry, file, content, domIdMapper) => { +const transpile = (isEntry, file, content, domIdMapper) => { /** @const {!Array} */ const comments = []; /** @const {!Array} */ @@ -268,4 +268,4 @@ const transpileJsx = (isEntry, file, content, domIdMapper) => { return update(content, updates); }; -export default { transpileJsx }; +export default { transpile }; diff --git a/kastro/transpiler/test/css.test.js b/kastro/transpiler/test/css.test.js new file mode 100644 index 0000000..62171da --- /dev/null +++ b/kastro/transpiler/test/css.test.js @@ -0,0 +1,144 @@ +import { describe, expect, it } from "bun:test"; +import { importCode } from "../../../util/reflection"; +import css, { CssModule } from "../css"; +import { GlobalMapper } from "../domIdMapper"; + +describe("selectorToEnumKey", () => { + it("should convert selector to enum key", () => { + expect(css.selectorToEnumKey("blue-button")).toBe("BlueButton"); + expect(css.selectorToEnumKey("blue_button")).toBe("BlueButton"); + expect(css.selectorToEnumKey("blueButton")).toBe("blueButton"); + expect(css.selectorToEnumKey("PascalCase")).toBe("PascalCase"); + expect(css.selectorToEnumKey("a__PascalCase")).toBe("APascalCase"); + expect(css.selectorToEnumKey("a-PascalCase")).toBe("APascalCase"); + expect(css.selectorToEnumKey("mina")).toBe("mina"); + expect(css.selectorToEnumKey("x1")).toBe("x1"); + }); +}); + +describe("transpileCss", () => { + it("should parse exported selectors", async () => { + const domIdMapper = new GlobalMapper(); + const cssCode = css.transpile("test.jsx", ` + body { color: red; } + /** @export */ + .blue-button { color: blue; } + /** @export */ .green-button { color: green; } + `, domIdMapper); + + /** @const {CssModule} */ + const cssModule = /** @type {CssModule} */(await importCode(cssCode)); + expect(cssModule.default).toEqual({ + "BlueButton": domIdMapper.map("mpa", "test.jsx", "blue-button"), + "GreenButton": domIdMapper.map("mpa", "test.jsx", "green-button"), + }); + }); + it("should parse export as directives", async () => { + const domIdMapper = new GlobalMapper(); + const cssCode = css.transpile("test.jsx", ` + body { color: red; } + /** @export {ButtonWhichIsBlue} */ + .blue-button { color: blue; } + `, domIdMapper); + + const cssModule = /** @type {CssModule} */(await importCode(cssCode)); + expect(cssModule.default).toEqual({ + "ButtonWhichIsBlue": domIdMapper.map("mpa", "test.jsx", "blue-button"), + }); + }); + it("should parse domNamespace directive", async () => { + const domIdMapper = new GlobalMapper(); + const cssCode = css.transpile("test.jsx", ` + body { color: red; } + /** @domNamespace {iframe1} */ + /** @export */ + .blue-button { color: blue; } + `, domIdMapper); + + const cssModule = /** @type {CssModule} */(await importCode(cssCode)); + expect(cssModule.default).toEqual({ + "BlueButton": domIdMapper.map("iframe1", "test.jsx", "blue-button"), + }); + }); + it("should throw on retroactive domNamespace directive", () => { + const domIdMapper = new GlobalMapper(); + expect(() => css.transpile("test.jsx", ` + body { color: red; } + /** @export */ + .blue-button { color: blue; } + /** @domNamespace {iframe1} */ + /** @export */ + .green-button { color: green; } + `, domIdMapper)).toThrow(); + }); + it("should handle pseudo-classes", async () => { + const domIdMapper = new GlobalMapper(); + const cssCode = css.transpile("test.jsx", ` + /** @export {GreenishButton} */ + .green-button:hover { color: green; } + /** @export */ + .blue-button:active { color: blue; } + `, domIdMapper); + const cssModule = /** @type {CssModule} */(await importCode(cssCode)); + expect(cssModule.default).toEqual({ + "GreenishButton": domIdMapper.map("mpa", "test.jsx", "green-button"), + "BlueButton": domIdMapper.map("mpa", "test.jsx", "blue-button"), + }); + }); +}); + +describe("minifyCss", () => { + it("should create enum entries for all selectors", () => { + const domIdMapper = new GlobalMapper(); + const minified = css.minify("test.jsx", ` + /** @domNamespace {iframe1} */ + .blue-button > .green-button, .red-button { color: blue; } + `, domIdMapper); + + expect(minified.enumEntries["BlueButton"]) + .toBe(domIdMapper.map("iframe1", "test.jsx", "blue-button")); + expect(minified.enumEntries["GreenButton"]) + .toBe(domIdMapper.map("iframe1", "test.jsx", "green-button")); + expect(minified.enumEntries["RedButton"]) + .toBe(domIdMapper.map("iframe1", "test.jsx", "red-button")); + }); + + it("should handle pseudo-classes", () => { + const domIdMapper = new GlobalMapper(); + const minified = css.minify("test.jsx", ` + /** @domNamespace {iframe1} */ + .blue-button:active > .green-button:hover, .red-button { color: blue; } + `, domIdMapper); + + expect(minified.enumEntries["BlueButton"]) + .toBe(domIdMapper.map("iframe1", "test.jsx", "blue-button")); + expect(minified.enumEntries["GreenButton"]) + .toBe(domIdMapper.map("iframe1", "test.jsx", "green-button")); + expect(minified.enumEntries["RedButton"]) + .toBe(domIdMapper.map("iframe1", "test.jsx", "red-button")); + }); + + it("should respect export as directives", () => { + const domIdMapper = new GlobalMapper(); + const minified = css.minify("test.jsx", ` + /** + * @domNamespace {spa} + * @export {ButtonWhichIsBlue} + */ + .blue-button { color: blue; } + `, domIdMapper); + + expect(minified.enumEntries["ButtonWhichIsBlue"]) + .toBe(domIdMapper.map("spa", "test.jsx", "blue-button")); + }); + + it("should throw on retroactive export as directive", () => { + const domIdMapper = new GlobalMapper(); + expect(() => css.minify("test.jsx", ` + /** @export {ButtonWhichIsBlue} */ + .green-button { color: green; } + /** @domNamespace {iframe1} */ + .blue-button { color: blue; } + `, domIdMapper)).toThrow(); + }); +}); diff --git a/kastro/transpiler/transpiler.js b/kastro/transpiler/transpiler.js index 40567de..b53dc26 100644 --- a/kastro/transpiler/transpiler.js +++ b/kastro/transpiler/transpiler.js @@ -6,17 +6,17 @@ import jsx from "./jsx"; const Mapper = new GlobalMapper(); const minifyCss = (content, file) => - css.minifyCss(file, content, Mapper); + css.minify(file, content, Mapper); const transpileCss = (content, file) => - css.transpileCss(file, content, Mapper); + css.transpile(file, content, Mapper); const transpileJsx = (content, file, isEntry) => - jsx.transpileJsx(isEntry, file, content, Mapper); + jsx.transpile(isEntry, file, content, Mapper); const transpile = (content, file, isEntry) => file.endsWith(".jsx") - ? transpileJsx(content, file, isEntry) - : transpileCss(content, file); + ? jsx.transpile(isEntry, file, content, Mapper) + : css.transpile(file, content, Mapper) export { minifyCss, diff --git a/kdjs/types/test/types.test.js b/kdjs/types/test/types.test.js index 147be69..f33c1b0 100644 --- a/kdjs/types/test/types.test.js +++ b/kdjs/types/test/types.test.js @@ -1 +1,37 @@ -import { test, expect } from "bun:test"; +import { expect, test } from "bun:test"; +import { + FunctionType, + GenericType, + PrimitiveType, + UnionType +} from "../types"; + +test("FunctionType.toString()", () => { + const returnType = new PrimitiveType("string", false); + const paramTypes = [ + new PrimitiveType("number", false), + new PrimitiveType("boolean", true) + ]; + const fn = new FunctionType(returnType, paramTypes, 1); + expect(fn.toString()).toBe("function(number,?boolean=):string"); +}); + +test("GenericType.toString()", () => { + const arrayType = new GenericType("Array", [new PrimitiveType("string")], false); + console.log(arrayType.toString()); + expect(arrayType.toString()).toBe("!Array"); + + const objectType = new GenericType("Object", [ + new PrimitiveType("string"), new PrimitiveType("number") + ], false); + expect(objectType.toString()).toBe("!Object"); +}); + +test("UnionType.toString()", () => { + const unionType = new UnionType([ + new PrimitiveType("string"), + new PrimitiveType("number"), + new PrimitiveType("null") + ]); + expect(unionType.toString()).toBe("string | number | null"); +}); diff --git a/kdjs/types/types.js b/kdjs/types/types.js index 9219124..5e11ac9 100644 --- a/kdjs/types/types.js +++ b/kdjs/types/types.js @@ -1,20 +1,39 @@ -class Type { } +/** @enum {number} */ +const Modifier = { + Nullable: 1, + + PureOrBreakMyCode: 64, + NoSideEffects: 128 +}; + +class Type { + /** + * + * @param {number=} modifiers + */ + constructor(modifiers) { + this.modifiers = modifiers || 0; + } + + isNullable() { + return this.modifiers & Modifier.Nullable; + } +} class PrimitiveType extends Type { /** * @param {string} name - * @param {boolean} isNullable + * @param {boolean=} isNullable */ - constructor(name, isNullable) { + constructor(name, isNullable = false) { + super(isNullable ? Modifier.Nullable : 0); this.name = name; - this.isNullable = isNullable; } toString() { - return (this.isNullable ? "?" : "") + this.name + return (this.isNullable() ? "?" : "") + this.name } - } class InstanceType extends Type { @@ -23,12 +42,12 @@ class InstanceType extends Type { * @param {boolean} isNullable */ constructor(name, isNullable) { + super(isNullable ? Modifier.Nullable : 0); this.name = name; - this.isNullable = isNullable; } toString() { - return (this.isNullable ? "" : "!") + this.name + return (this.isNullable() ? "" : "!") + this.name } } @@ -37,11 +56,18 @@ class UnionType extends Type { * @param {!Array} types */ constructor(types) { + const idx = types.findIndex((type) => + type instanceof PrimitiveType && type.name == "null"); + if (idx != -1) { + if (idx == types.length - 1) types.pop(); + else types[idx] = types.pop(); + } + super(idx != -1 ? Modifier.Nullable : 0); this.types = types; } toString() { - return this.types.join(" | "); + return this.types.join(" | ") + (this.isNullable() ? " | null" : ""); } } @@ -53,14 +79,17 @@ class GenericType extends Type { /** * @param {string} name * @param {!Array} params + * @param {boolean} isNullable */ - constructor(name, params) { + constructor(name, params, isNullable) { + super(isNullable ? Modifier.Nullable : 0); this.name = name; this.params = params; } toString() { - return `${this.name}<${this.params.toString()}>`; + const typeParams = this.params.map((param) => param.toString()).join(", ") + return `${(this.isNullable() ? "" : "!") + this.name}<${typeParams}>`; } } @@ -69,6 +98,7 @@ class RecordType extends Type { * @param {!Object} members */ constructor(members) { + super(); this.members = members; } @@ -85,20 +115,27 @@ class FunctionType extends Type { /** * @param {!Type} returnType * @param {!Array} params - * @param {number} optionalAfter + * @param {number=} optionalAfter */ constructor(returnType, params, optionalAfter) { + super(); this.returnType = returnType; this.params = params; - this.optionalAfter = optionalAfter; + this.optionalAfter = optionalAfter ?? params.length; } toString() { - return `function(${this.params.join(",")}):${this.returnType.toString()}`; + return `function(${this.params.map((type, i) => + `${type.toString()}${i >= this.optionalAfter ? "=" : ""}` + ).join(",")}):${this.returnType.toString()}`; } toJsDoc() { - + let counter = 0; + const paramDocs = this.params.map((type, i) => + ` * @param {${type.toString()}${i >= this.optionalAfter ? "=" : ""}} param${++counter}` + ).join("\n"); + return `/**\n${paramDocs}\n * @return {${this.returnType.toString()}}\n */`; } } @@ -109,5 +146,5 @@ export { PrimitiveType, RecordType, Type, - UnionType, + UnionType };