diff --git a/kastro/README.md b/kastro/README.md index d067242..89f2cd5 100644 --- a/kastro/README.md +++ b/kastro/README.md @@ -24,21 +24,40 @@ has many additional optimization passes and a full support for es6 modules. There are 3 types of components: -1. **Singleton**: Only one instance can be present in the page. These bind to the DOM when - you import them and can keep an arbitrary internal state. (every variable that you - define in the module) +1. **Singleton**: Only one instance can be present in the page. These bind to +the DOM when you import them and can keep an arbitrary internal state (every +variable that you define in the module). ```javascript - const State = { 1n, 2n, 3n }; - const SingletonComp = () =>
Singleton
; + const State = [1, 2, 3]; + @kastroSingletonComponent + const SingletonComp = (props) =>
Singleton
; + export default SingletonComp; + ``` + If your component is exposing additional methods, you can instead use + ```javascript + const State = [1, 2, 3]; + /** @kastroSingletonComponent */ + const SingletonComp = { + render: (props) =>
Singleton
, + push: (x) => State.push(x), + pop: () => State.pop(), + } export default SingletonComp; ``` -2. **Stateless**: These are components that do not have any internal state besides the - DOM state. +2. **Stateless**: These are components that do not have any internal state + besides the DOM state. Since they are stateless, there can be arbitrary + number of instances in the page without any js objects being created. Kastro + compiler will generate the `bind()` invocations from the `render()` jsx + expression of the parent component. ```javascript + /**@kastroStatelessComponent */ const StatelessComp = { - render: ({id}) =>
Stateless
, - bind: (id) => { document.getElementById(id).onclick = () => alert("hi"); } + render: ({ id }) =>
on
, + bind: (id) => { + const root = document.getElementById(id); + root.onclick = () => root.innerText = root.innerText == "on" ? "off" : "on" + } } export default StatelessComp; ``` @@ -46,10 +65,11 @@ There are 3 types of components: 3. **Stateful**: These are components that have internal state and for each instance of the component, a class instance is crated and exported. ```javascript + /** @kastroStatefulComponent */ class CheckBox { - static render({id}) { return
Stateful
; } - constructor(id) { this.root = document.getElementById(id); } - flip() {} + static render({ id }) { return
on
; } + constructor(id) { this.root = document.getElementById(id); this.on = true; } + flip() { this.on = !this.on; this.root.innerText = this.on ? "on" : "off"; } } export default CheckBox; ``` diff --git a/kastro/compiler/component.js b/kastro/compiler/component.js index c575709..15341d2 100644 --- a/kastro/compiler/component.js +++ b/kastro/compiler/component.js @@ -252,6 +252,7 @@ const compileComponent = (name, props, globals) => { : import(process.cwd() + "/" + markup).then((mod) => mod.default(contextVars))) .then((markupContent) => { parser.end(markupContent); + if (css && !css.startsWith("/")) css = "/" + css; if (css && !globals.SharedCss.has(css) && !globals.PageCss.has(css)) globals.PageCss.add(css); diff --git a/kastro/hashcache/buildCache.js b/kastro/compiler/hashcache/buildCache.js similarity index 100% rename from kastro/hashcache/buildCache.js rename to kastro/compiler/hashcache/buildCache.js diff --git a/kastro/hashcache/compression.js b/kastro/compiler/hashcache/compression.js similarity index 93% rename from kastro/hashcache/compression.js rename to kastro/compiler/hashcache/compression.js index 494f131..a968a9e 100644 --- a/kastro/hashcache/compression.js +++ b/kastro/compiler/hashcache/compression.js @@ -1,8 +1,8 @@ import { spawn } from "bun"; import { cp, readFile, rename, writeFile } from "node:fs/promises"; -import { keccak256Uint8 } from "../../crypto/sha3"; -import { getExt } from "../../util/paths"; -import { base64 } from "../../util/çevir"; +import { keccak256Uint8 } from "../../../crypto/sha3"; +import { getExt } from "../../../util/paths"; +import { base64 } from "../../../util/çevir"; const zopfli = (inputName, outputName) => spawn({ cmd: [ diff --git a/kastro/hashcache/fileCache.js b/kastro/compiler/hashcache/fileCache.js similarity index 100% rename from kastro/hashcache/fileCache.js rename to kastro/compiler/hashcache/fileCache.js diff --git a/kastro/hashcache/files.js b/kastro/compiler/hashcache/files.js similarity index 100% rename from kastro/hashcache/files.js rename to kastro/compiler/hashcache/files.js diff --git a/kastro/hashcache/targets.js b/kastro/compiler/hashcache/targets.js similarity index 100% rename from kastro/hashcache/targets.js rename to kastro/compiler/hashcache/targets.js diff --git a/kastro/compiler/image.js b/kastro/compiler/image.js index 784a824..34d17c1 100644 --- a/kastro/compiler/image.js +++ b/kastro/compiler/image.js @@ -1,8 +1,64 @@ +import { readFile } from "node:fs/promises"; +import { SAXParser } from "sax"; +import { optimize } from "svgo"; +import { tagYaz } from "../../util/html"; +import { getByKey } from "./hashcache/buildCache"; +import SvgoInlineConfig from "./svgoInlineConfig"; +const removeGlobalProps = (props) => { + for (const prop in props) + if (prop.charCodeAt(0) < 91) + delete props[prop]; +} -const Image = ({ src, inline }) => { - console.log(src, inline); - return ; +/** + * We optimize the inline svgs regardless of the build mode. + * + * @param {!Object} props + * @returns {!Promise} + */ +const compileInlineSvg = ({ src, ...props }) => + getByKey("build/" + src, () => + getByKey(src, () => readFile(src, "utf-8")) + .then((svg) => optimize(svg, SvgoInlineConfig).data)) + .then((svg) => { + removeGlobalProps(props); + delete props.inline; + const parser = new SAXParser(true); + let result = ""; + parser.onopentag = ({ name, attributes }) => { + if (name === "svg") + Object.assign(attributes, props); + result += tagYaz(name, attributes, false); + }; + parser.ontext = (text) => { + result += text; + }; + parser.onclosetag = (tagName) => { + result += ``; + }; + parser.write(svg).close(); + return result; + }); + + +/** + * @param {!Object} props + * @return {!Promise} + */ +const Image = (props) => { + const { inline, src, ...restProps } = props; + if (inline) { + if (!src.endsWith(".svg")) + throw new Error("We only inline svgs; for other formats serving directly is more efficient"); + return compileInlineSvg(props) + } + + for (const prop in props) + if (prop[0] === prop[0].toUpperCase()) + delete props[prop]; + + return tagYaz("img", props, true); }; export { Image }; diff --git a/kastro/compiler/page.js b/kastro/compiler/page.js index 90f70fa..03494ca 100644 --- a/kastro/compiler/page.js +++ b/kastro/compiler/page.js @@ -1,27 +1,24 @@ import { plugin } from "bun"; import { minify } from "html-minifier"; import assert from "node:assert"; -import { readFile } from "node:fs/promises"; import process from "node:process"; import { optimize } from "svgo"; import { tagYaz } from "../../util/html"; import { LangCode } from "../../util/i18n"; import { getExt } from "../../util/paths"; -import { getByKey } from "../hashcache/buildCache"; -import { hashAndCompressContent, hashFile } from "../hashcache/compression"; import { compileComponent } from "./component"; +import { getByKey } from "./hashcache/buildCache"; +import { hashAndCompressContent, hashFile } from "./hashcache/compression"; import HtmlMinifierConfig from "./htmlMinifierConfig"; import { initGlobals } from "./pageGlobals"; -import SvgoConfig from "./svgoConfig"; -import SvgoInlineConfig from "./svgoInlineConfig"; import { generateStylesheet, webp } from "./targets"; const setupEnvironment = () => { - const ImagePlugin = { - name: 'kastro image loader', + const KastroPlugin = { + name: 'kastro loader', setup(build) { - const cwdLen = process.cwd().length; - build.onLoad({ filter: /\.svg$/ }, (args) => { + const cwdLen = process.cwd().length + 1; + build.onLoad({ filter: /\.(svg|png|webp)$/ }, (args) => { const code = `import { Image } from "@kimlikdao/lib/kastro/compiler/image";\n` + `export default (props) => Image({...props, src: "${args.path.slice(cwdLen)}" });`; return { @@ -29,10 +26,18 @@ const setupEnvironment = () => { loader: "js" }; }); + build.onLoad({ filter: /\.css$/ }, (args) => { + const code = `import { StyleSheet } from "@kimlikdao/lib/kastro/compiler/stylesheet";\n` + + `export default (props) => StyleSheet({...props, src: "${args.path.slice(cwdLen)}" });`; + return { + contents: code, + loader: "js" + }; + }); }, }; - plugin(ImagePlugin); + plugin(KastroPlugin); globalThis.GEN = true; globalThis.document = {}; @@ -102,7 +107,6 @@ const compilePage = async (componentName, pageGlobals) => { return compileComponent(componentName, {}, pageGlobals) .then((html) => { html = "" + html; - console.log(pageGlobals); if (pageGlobals.BuildMode == 0) { /** @type {string} */ let links = ""; @@ -120,23 +124,8 @@ const compilePage = async (componentName, pageGlobals) => { }); } -/** - * Verilen konumdan svg içeriğini okur. Eğer uzantı .m.js ise içeriğini kasto - * kurallarına göre günceller. - * - * Eğer `!seçimler.dev` is içeriği svgo ile optimize eder. - * - * @param {!Object} seçimler - * @return {!Promise} - */ -const compileSvg = (seçimler) => ( - seçimler.konum.endsWith(".m.svg") - ? compileComponent(seçimler.konum, seçimler) - : readFile(seçimler.konum, "utf8")) - .then((svg) => seçimler.dev ? svg : optimize(svg, SvgoConfig).data); export { - compilePage, - compileSvg + compilePage }; diff --git a/kastro/compiler/stylesheet.js b/kastro/compiler/stylesheet.js new file mode 100644 index 0000000..080d951 --- /dev/null +++ b/kastro/compiler/stylesheet.js @@ -0,0 +1,7 @@ + +const StyleSheet = ({ src, shared, SharedCss, PageCss }) => { + (shared ? SharedCss : PageCss).add(src); + return; +} + +export { StyleSheet }; diff --git a/kastro/compiler/targets.js b/kastro/compiler/targets.js index d556f02..e6ad470 100644 --- a/kastro/compiler/targets.js +++ b/kastro/compiler/targets.js @@ -6,7 +6,7 @@ import { getDir } from "../../util/paths"; import { hashAndCompressContent, hashAndCompressFile -} from "../hashcache/compression"; +} from "./hashcache/compression"; /** * @param {!Object} props @@ -35,7 +35,6 @@ const generateStylesheet = (cssFileNames) => .then((csses) => hashAndCompressContent(minify(csses.join("\n")).css, "css")) .then((hashedName) => ``); - const webp = (inputName, outputName, passes = 10, quality = 70) => mkdir(getDir(outputName), { recursive: true }).then(() => spawn([ diff --git a/kastro/workers/devServer.js b/kastro/workers/devServer.js index e5c75b3..ce7d85a 100644 --- a/kastro/workers/devServer.js +++ b/kastro/workers/devServer.js @@ -1,7 +1,7 @@ import { createServer } from "vite"; import { parseArgs } from "../../util/cli"; import { BuildMode } from "../compiler/compiler"; -import { compilePage, compileSvg } from "../compiler/page"; +import { compilePage } from "../compiler/page"; import { readCrateRecipe } from "../crate"; /** @@ -74,6 +74,12 @@ const serveCrate = async (crateName, buildMode) => { return code .replace(/const GEN =.*?;/, `const GEN = false`) .replace(/const TR =.*?;/, `const TR = ${currentPage.Lang == "tr" ? "true" : "false"};`); + 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"); + } } }] }).then((vite) => vite.listen(8787)) diff --git a/kdjs/passes.js b/kdjs/passes.js index b60c260..954f999 100644 --- a/kdjs/passes.js +++ b/kdjs/passes.js @@ -21,7 +21,7 @@ smart simplifications on a macroscoping scale to make the final code more efficient and compact. Don't worry about minification or variable renamings; those tedious tasks -will be handler later with simpler tools. Your task is to come up with some +will be handled later with simpler tools. Your task is to come up with some statements about the code and prove them correct and make simplification based on these facts. @@ -30,7 +30,7 @@ remove it. Or may prove that some properties of an object is never read, and remove them. Or you may prove that some function is pure and constant-propogate through -the function (if the parameters are known at compiler time, the function +the function (if the parameters are known at compile time, the function call can be replaced with the result of the function). Since you are given a single module only, you may not always figure out that a function is pure. However if the function name looks like it could be pure, (such as add(a, b), diff --git a/kdjs/preprocess.js b/kdjs/preprocess.js index 4211e48..a6aafcf 100644 --- a/kdjs/preprocess.js +++ b/kdjs/preprocess.js @@ -4,8 +4,8 @@ import { existsSync } from "node:fs"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import { combine, getDir } from "../util/paths"; import { ExportStatement, ImportStatement } from "./modules"; -import { Update, update } from "./textual"; import { serializeWithStringKeys } from "./objects"; +import { Update, update } from "./textual"; const PACKAGE_EXTERNS = "node_modules/@kimlikdao/kdjs/externs/"; diff --git a/package.json b/package.json index 9d81a88..b87c7ae 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "html-minifier": "^4.0.0", "htmlparser2": "^9.1.0", "js-yaml": "^4.1.0", + "sax": "^1.4.1", "svgo": "^3.3.2", "uglify-js": "~3.19.3" }