瀏覽代碼

new version, entirely rewritten

Matheus Giovani 3 年之前
父節點
當前提交
54434f1fa4
共有 67 個文件被更改,包括 1450 次插入1460 次删除
  1. 0 1
      .gitignore
  2. 0 23
      modules/webpack/index.js
  3. 3 0
      nodemon.json
  4. 14 19
      package.json
  5. 29 0
      packages/compiler/package.json
  6. 6 4
      packages/compiler/src/core/Lexer.ts
  7. 5 1
      packages/compiler/src/core/Plugin.ts
  8. 0 0
      packages/compiler/src/core/lexer/Token.ts
  9. 150 0
      packages/compiler/src/core/lexer/tokens/Component.ts
  10. 52 0
      packages/compiler/src/core/lexer/tokens/ForEach.ts
  11. 22 32
      packages/compiler/src/core/lexer/tokens/If.ts
  12. 16 33
      packages/compiler/src/core/lexer/tokens/Import.ts
  13. 8 5
      packages/compiler/src/core/lexer/tokens/Property.ts
  14. 40 0
      packages/compiler/src/core/lexer/tokens/PupperToAlpine.ts
  15. 147 0
      packages/compiler/src/core/lexer/tokens/component/ScriptParser.ts
  16. 18 0
      packages/compiler/src/core/lexer/tokens/component/StyleParser.ts
  17. 98 0
      packages/compiler/src/index.ts
  18. 0 0
      packages/compiler/src/types/pug.d.ts
  19. 10 0
      packages/compiler/tsconfig.json
  20. 12 0
      packages/compiler/types/core/Lexer.d.ts
  21. 37 0
      packages/compiler/types/core/Plugin.d.ts
  22. 37 0
      packages/compiler/types/core/lexer/Token.d.ts
  23. 24 0
      packages/compiler/types/core/lexer/tokens/Component.d.ts
  24. 5 0
      packages/compiler/types/core/lexer/tokens/ForEach.d.ts
  25. 5 0
      packages/compiler/types/core/lexer/tokens/If.d.ts
  26. 11 0
      packages/compiler/types/core/lexer/tokens/Import.d.ts
  27. 15 0
      packages/compiler/types/core/lexer/tokens/Property.d.ts
  28. 6 0
      packages/compiler/types/core/lexer/tokens/PupperToAlpine.d.ts
  29. 21 0
      packages/compiler/types/core/lexer/tokens/component/ScriptParser.d.ts
  30. 11 0
      packages/compiler/types/core/lexer/tokens/component/StyleParser.d.ts
  31. 69 0
      packages/compiler/types/index.d.ts
  32. 28 0
      packages/renderer/package.json
  33. 289 0
      packages/renderer/src/core/Component.ts
  34. 8 0
      packages/renderer/src/index.ts
  35. 8 0
      packages/renderer/src/types/pupper.d.ts
  36. 9 0
      packages/renderer/tsconfig.json
  37. 112 0
      packages/renderer/types/core/Component.d.ts
  38. 6 0
      packages/renderer/types/index.d.ts
  39. 26 0
      packages/webpack-loader/index.js
  40. 5 2
      packages/webpack-loader/package.json
  41. 0 14
      src/core/App.ts
  42. 0 99
      src/core/Compiler.ts
  43. 0 67
      src/core/Component.ts
  44. 0 572
      src/core/Renderer.ts
  45. 0 20
      src/core/compiler/lexer/tokens/Bind.ts
  46. 0 63
      src/core/compiler/lexer/tokens/ForEach.ts
  47. 0 73
      src/core/renderer/Reactive.ts
  48. 0 151
      src/core/renderer/Reactor.ts
  49. 0 102
      src/core/renderer/reactors/ForEach.ts
  50. 0 9
      src/core/renderer/reactors/HTML.ts
  51. 0 9
      src/core/renderer/reactors/HTMLAttribute.ts
  52. 0 19
      src/core/renderer/reactors/If.ts
  53. 0 9
      src/core/renderer/reactors/Text.ts
  54. 0 38
      src/index.ts
  55. 0 7
      src/pupper.ts
  56. 0 3
      src/types/deep-get-set.d.ts
  57. 0 24
      src/types/observable-slim.d.ts
  58. 0 6
      src/types/pupper.d.ts
  59. 15 8
      test/index.js
  60. 0 14
      test/node.js
  61. 15 0
      test/templates/ExportedComponent.pupper
  62. 21 0
      test/templates/ImportedComponent.pupper
  63. 0 4
      test/templates/foreach.pupper
  64. 0 13
      test/templates/puppy.pupper
  65. 24 11
      test/templates/template.pupper
  66. 4 3
      tsconfig.json
  67. 9 2
      webpack.config.js

+ 0 - 1
.gitignore

@@ -7,7 +7,6 @@ node_modules
 yarn-error.log
 yarn.lock
 
-types/
 !src/types
 
 # Logs

+ 0 - 23
modules/webpack/index.js

@@ -1,23 +0,0 @@
-/**
- * pupper.js - Webpack Loader
- * @author Matheus Giovani <matheus@ad3com.com.br>
- * @license AGPL-3.0
- */
-
-const pupper = require("../..");
-
-/**
- * @param {string} source The source filename
- * @param {import("../../types/pupper").Compiler.Options} options Any options to be passed to the pupper compiler
- * @returns {String}
- */
-module.exports = function(source, options) {
-    const contents = pupper.compileToStringSync(source, {
-        ...options,
-        pug: {
-            filename: this.resourcePath
-        }
-    });
-
-    return contents;
-};

+ 3 - 0
nodemon.json

@@ -0,0 +1,3 @@
+{
+    "ignore": ["./**/src/**/*", "./test/out/**/*"]
+}

+ 14 - 19
package.json

@@ -1,34 +1,29 @@
 {
-  "name": "@pupperjs/core",
+  "name": "pupperjs",
   "version": "1.0.0",
-  "description": "A reactive template engine based in pugjs",
-  "main": "out/",
+  "description": "A reactive template engine based in Pug and Alpine.js",
   "author": "Matheus Giovani <matheus@ad3com.com.br>",
   "license": "AGPL-3.0",
-  "private": false,
+  "private": true,
+  "packageManager": "yarn@1.22.10",
+  "workspaces": [
+    "./packages/compiler",
+    "./packages/renderer",
+    "./packages/webpack-loader"
+  ],
   "scripts": {
-    "watch": "npm-run-all -p -r watch-core watch-test",
-    "watch-core": "tsc -watch",
-    "watch-test": "webpack",
-    "test-node": "nodemon --ignore test/out test/node"
+    "watch": "npm-run-all -p -r watch:*",
+    "watch:compiler": "cd packages/compiler && yarn watch",
+    "watch:renderer": "cd packages/renderer && yarn watch",
+    "watch:test": "nodemon --exec \"webpack\""
   },
   "dependencies": {
-    "deep-get-set": "^1.1.1",
-    "observable-slim": "^0.1.5",
+    "alpinejs": "^3.10.2",
     "pug": "^3.0.2"
   },
-  "types": "types/",
   "devDependencies": {
-    "@types/node": "^16.7.6",
-    "@types/pug": "^2.0.5",
-    "debug": "^4.3.2",
-    "jest": "^27.4.7",
-    "jest-puppeteer": "^6.1.0",
-    "js-beautify": "^1.14.0",
     "npm-run-all": "^4.1.5",
     "source-map-loader": "^3.0.0",
-    "tsc": "^2.0.3",
-    "typescript": "^4.4.2",
     "webpack": "^5.51.1",
     "webpack-cli": "^4.8.0"
   }

+ 29 - 0
packages/compiler/package.json

@@ -0,0 +1,29 @@
+{
+  "name": "@pupperjs/compiler",
+  "version": "1.0.0",
+  "description": "A reactive template engine based in Pug and Alpine.js",
+  "author": "Matheus Giovani <matheus@ad3com.com.br>",
+  "license": "AGPL-3.0",
+  "private": false,
+  "main": "./out/",
+  "scripts": {
+    "watch": "npm-run-all -p -r watch:*",
+    "watch:ts": "tsc -watch"
+  },
+  "dependencies": {
+    "alpinejs": "^3.10.2",
+    "pug": "^3.0.2",
+    "ts-morph": "^15.1.0"
+  },
+  "types": "./types/",
+  "devDependencies": {
+    "@types/node": "^16.7.6",
+    "@types/pug": "^2.0.5",
+    "debug": "^4.3.2",
+    "js-beautify": "^1.14.0",
+    "tsc": "^2.0.3",
+    "typescript": "^4.4.2",
+    "webpack": "^5.51.1",
+    "webpack-cli": "^4.8.0"
+  }
+}

+ 6 - 4
src/core/compiler/Lexer.ts → packages/compiler/src/core/Lexer.ts

@@ -3,15 +3,17 @@ import type Token from "./lexer/Token";
 import ForEach from "./lexer/tokens/ForEach";
 import Property from "./lexer/tokens/Property";
 import IfToken from "./lexer/tokens/If";
-import Bind from "./lexer/tokens/Bind";
 import Import from "./lexer/tokens/Import";
+import PupperToAlpine from "./lexer/tokens/PupperToAlpine";
+import Component from "./lexer/tokens/Component";
 
 export default class Lexer {
     public static Tokens: typeof Token[] = [
-        Property,
-        ForEach,
         IfToken,
-        Bind,
+        ForEach,
+        Component,
+        Property,
+        PupperToAlpine,
         Import
     ];
 

+ 5 - 1
src/core/compiler/Plugin.ts → packages/compiler/src/core/Plugin.ts

@@ -2,6 +2,7 @@ import Lexer from "./Lexer";
 import Token from "./lexer/Token";
 
 import { PugPlugin, PugToken, PugAST, PugNode, Options } from "pug";
+import PupperCompiler from "..";
 
 export { PugToken, PugAST, PugNode };
 
@@ -27,7 +28,10 @@ export default class Plugin implements PugPlugin {
     public lex = new Lexer();
 
     constructor(
-        protected options: Options
+        public compiler: PupperCompiler,
+        public options: Options & {
+            contents?: string
+        }
     ) {
         for(let token of Lexer.Tokens) {
             this.tokens.push(new token(this));

+ 0 - 0
src/core/compiler/lexer/Token.ts → packages/compiler/src/core/lexer/Token.ts


+ 150 - 0
packages/compiler/src/core/lexer/tokens/Component.ts

@@ -0,0 +1,150 @@
+import { PugNode } from "../../Plugin";
+import Token from "../Token";
+import { ScriptParser } from "./component/ScriptParser";
+
+const DefaultExportSymbol = Symbol("ExportedComponent");
+
+export interface IComponent {
+    name: string | symbol;
+    template: string;
+    script?: string;
+    setupScript?: string;
+    style?: string;
+    exported?: boolean;
+}
+
+export default class Component extends Token {
+    /**
+     * Parses a pug node into a component.
+     * @param node The pug node to be parsed.
+     * @returns 
+     */
+    public parseNode(node: PugNode, nextNode: PugNode) {
+        const name = node.attrs.find((node) => node.name === "name")?.val.replace(/"/g, "");
+
+        const template = node.block?.nodes.find((node) => node.type === "Tag" && node.name === "template");
+        const script = node.block?.nodes.find((node) => node.type === "Tag" && node.name === "script");
+        const style = node.block?.nodes.find((node) => node.type === "Tag" && node.name === "style");
+
+        // If no script tag was found
+        if (!script) {
+            throw new Error("Components must have at least a script tag.");
+        }
+
+        /**
+         * Create the component
+         */
+        const component: IComponent = {
+            name,
+            template: null,
+            script: null,
+            style: null,
+            exported: node.attrs.find((attr) => attr.name === "export") !== undefined
+        };
+
+        // If the component is not exported and has no name
+        if (!component.exported && (!name || !name.length)) {
+            throw new Error("Scoped components must have a name.");
+        }
+
+        // If the component has no name
+        if (!name || !name.length) {
+            // Assume it's the default export
+            component.name = DefaultExportSymbol;
+        }
+
+        // If has a template
+        if (template) {
+            let lines = this.plugin.options.contents.split("\n");
+
+            const nextNodeAfterTemplate = node.block.nodes[node.block.nodes.indexOf(template) + 1];
+
+            lines = lines.slice(
+                template.line,
+                nextNodeAfterTemplate ? nextNodeAfterTemplate.line - 1 : nextNode.line - 1
+            );
+
+            // Detect identation
+            const identation = /^([\t\n]*) */.exec(lines[0]);
+
+            const contents = lines
+                // Replace the first identation
+                .map((line) => line.replace(identation[0], ""))
+                .join("\n");
+
+            const templateAsString = this.plugin.compiler.compileTemplate(contents);
+            component.template = templateAsString;
+        }
+
+        // If has a script
+        if (script) {
+            const scriptContent = script.block.nodes.map((node) => node.val).join("");
+            component.script = scriptContent;
+        }
+
+        // If has a style
+        if (style) {
+            console.log(style);
+        }
+
+        return component;
+    }
+
+    /**
+     * The imports that will later be putted into the template header
+     */
+    protected components: Record<string | symbol, IComponent> = {};
+
+    public parse(nodes: PugNode[]) {
+        for(let i = 0; i < nodes.length; i++) {
+            const node = nodes[i];
+
+            // Check if it's a tag node
+            if (node.type === "Tag") {
+                // If it's a component tag
+                if (node.name === "component") {
+                    // Parse the component
+                    const component = this.parseNode(node, nodes[i + 1]);
+
+                    // Save the component
+                    this.components[component.name] = component;
+
+                    // Remove the node from the body
+                    nodes.splice(nodes.indexOf(node), 1);
+
+                    continue;
+                }
+            }
+
+            // Parses the block
+            if (node.block) {
+                node.block.nodes = this.parse(node.block.nodes);
+            }
+        }
+
+        return nodes;
+    }
+
+    public afterCompile(code: string) {
+        const exportedComponent = this.components[DefaultExportSymbol];
+
+        // Check if has any exported components
+        if (exportedComponent) {
+            // Parse the script
+            const parsedScript = new ScriptParser(
+                exportedComponent,
+                this.plugin.getOptions().filename,
+                this.components,
+                this.plugin
+            ).parse();
+
+            code += `\n\n${parsedScript}\n`;
+
+            if (exportedComponent.style) {
+                code += `\n${exportedComponent.style}\n`;
+            }
+        }
+
+        return code;
+    }
+};

+ 52 - 0
packages/compiler/src/core/lexer/tokens/ForEach.ts

@@ -0,0 +1,52 @@
+import { PugNode } from "../../Plugin";
+import Token from "../Token";
+
+export default class ForEach extends Token {
+    public parse(nodes: PugNode[]) {
+        for(let index = 0; index < nodes.length; index++) {
+            const node = nodes[index];
+
+            // Check if it's an each
+            if (node.type === "Each") {
+                // Turn it into a <template x-each>
+                node.type = "Tag";
+                node.name = "template";
+                node.selfClosing = false;
+                node.attributeBlocks = [];
+                node.isInline = false;
+                node.attrs = [
+                    {
+                        name: "x-for",
+                        val: `"${node.val.trim()} of ${node.obj.trim()}"`,
+                        mustEscape: false
+                    }
+                ];
+
+                if (node.block.nodes.length > 1) {
+                    node.block.nodes = [
+                        {
+                            type: "Tag",
+                            name: "div",
+                            selfClosing: false,
+                            attributeBlocks: [],
+                            isInline: false,
+                            attrs: [],
+                            block: node.block
+                        }
+                    ]
+                }
+
+                delete node.obj;
+                delete node.key;
+                delete node.val;
+            }
+
+            // Parses the block
+            if (node.block) {
+                node.block.nodes = this.parse(node.block.nodes);
+            }
+        }
+
+        return nodes;
+    }
+};

+ 22 - 32
src/core/compiler/lexer/tokens/If.ts → packages/compiler/src/core/lexer/tokens/If.ts

@@ -11,59 +11,49 @@ export default class ForEach extends Token {
                 // Clone it
                 const conditional = { ...node };
 
-                // Replace with an if markup tag
+                // Replace with an if <template x-if>
                 node.type = "Tag";
-                node.name = "p:if";
+                node.name = "template";
                 node.selfClosing = false;
                 node.attributeBlocks = [];
                 node.isInline = false;
                 node.attrs = [{
-                    name: "condition",
-                    val: `"@p:conditional(${conditional.test})"`,
+                    name: "x-if",
+                    val: `"${conditional.test}"`,
                     mustEscape: false
                 }];
-
-                // <p:then>
                 node.block = {
                     type: "Block",
-                    nodes: [
+                    nodes: this.parse(conditional.consequent.nodes)
+                };
+
+                // <template v-if!>
+                if (!!conditional.alternate) {
+                    nodes.splice(index + 1, 0, 
                         {
                             type: "Tag",
-                            name: "p:then",
-                            selfClosing: false,
-                            attrs: [],
+                            name: "template",
+                            start: 0,
+                            end: 0,
                             attributeBlocks: [],
                             isInline: false,
+                            selfClosing: false,
+                            attrs: [{
+                                name: "x-if",
+                                val: `"!(${conditional.test})"`,
+                                mustEscape: false
+                            }],
                             block: {
                                 type: "Block",
-                                nodes: this.parse(conditional.consequent.nodes)
+                                nodes: this.parse(conditional.alternate.nodes)
                             }
-                        }
-                    ]
-                };
-
-                // <p:else>
-                if (!!conditional.alternate) {
-                    node.block.nodes.push({
-                        type: "Tag",
-                        name: "p:else",
-                        start: 0,
-                        end: 0,
-                        attributeBlocks: [],
-                        isInline: false,
-                        selfClosing: false,
-                        block: {
-                            type: "Block",
-                            nodes: this.parse(conditional.alternate.nodes)
-                        }
-                    });
+                        }   
+                    );
                 }
 
                 delete node.test;
                 delete node.consequent;
                 delete node.alternate;
-
-                continue;
             }
 
             // Parses the block

+ 16 - 33
src/core/compiler/lexer/tokens/Import.ts → packages/compiler/src/core/lexer/tokens/Import.ts

@@ -24,15 +24,16 @@ export default class Import extends Token {
 
                     const { identifier, filename } = condition.groups;
 
-                    this.imports[identifier] = filename;
+                    this.plugin.sharedData.imports = this.plugin.sharedData.imports || {};
+                    this.plugin.sharedData.imports[identifier] = filename;
 
                     // Remove the node from it
-                    //nodes.splice(nodes.indexOf(node), 1);
+                    nodes.splice(nodes.indexOf(node), 1);
 
                     continue;
                 } else
                 // If it's trying to import a previously imported template
-                if (this.imports[node.name] !== undefined) {
+                if (this.plugin.sharedData.imports?.[node.name] !== undefined) {
                     // If has a body
                     if (node.block?.nodes.length > 0) {
                         throw new Error("Template tags can't have a body.");
@@ -41,39 +42,21 @@ export default class Import extends Token {
                     const templateName = node.name;
 
                     // Replace it with an import markup tag
-                    node.name = "p:import";
+                    node.name = "div";
                     node.selfClosing = true;
                     node.isInline = false;
-
-                    // Prevent pug from escaping the attributes
-                    node.attrs = node.attrs.map((attr) => {
-                        attr.mustEscape = false;
-                        
-                        switch(attr.name) {
-                            case "data":
-                            case "methods":
-                                attr.val = attr.val.trim();
-
-                                // If it's a JSON object
-                                if (attr.val.startsWith("{") && attr.val.endsWith("}")) {
-                                    // Remove whitespace
-                                    attr.val = `"${attr.val.replace(/^\s+|\s+$|\s+(?=\s)/g, " ")}"`;
-                                }
-                            break;
-
-                            default:
-                                throw new Error("Invalid template attribute " + attr.name);
+                    node.attrs = [
+                        {
+                            name: "x-data",
+                            val: node.attrs.find((n) => n.name === "data")?.val.trim(),
+                            mustEscape: true
+                        },
+                        {
+                            name: "x-template",
+                            val: templateName,
+                            mustEscape: true
                         }
-
-                        return attr;
-                    });
-
-                    // Add the template name to the variables
-                    node.attrs.unshift({
-                        name: "template",
-                        val: `"${templateName}"`,
-                        mustEscape: false
-                    });
+                    ]
                 }
             }
 

+ 8 - 5
src/core/compiler/lexer/tokens/Property.ts → packages/compiler/src/core/lexer/tokens/Property.ts

@@ -17,7 +17,7 @@ export default class Property extends Token {
     }
 
     public lex(tokens: PugToken[]) {
-        return tokens.map((token, index) => {
+        return tokens.map((token) => {
             // We want only attribute and code tokens
             if (token.type !== "attribute" && token.type !== "code") {
                 return token;
@@ -31,13 +31,16 @@ export default class Property extends Token {
 
                 const fn = result.tag === "{" ? "escape" : "literal";
 
+                // If it's an attribute
                 if (token.type === "attribute") {
-                    // Replace with our escape
-                    token.val = `"@p:${fn}(${value})"`;
+                    // Replace with our shorthand escape
+                    token.name = ":" + token.name;
+                    token.val = `"${value}"`;
                     token.mustEscape = false;
                 } else {
-                    // Replace it with a comment tag
-                    token.val = `"<!-- @p:${fn}(${value}) -->"`;
+                    const textOrHtml = fn === "escape" ? "text" : "html";
+
+                    token.val = /*html*/`"<span x-${textOrHtml}=\\"${value}\\"></span>"`;
                     token.mustEscape = false;
                 }
             }

+ 40 - 0
packages/compiler/src/core/lexer/tokens/PupperToAlpine.ts

@@ -0,0 +1,40 @@
+import { PugToken } from "../../Plugin";
+import Token from "../Token";
+
+export default class PupperToAlpine extends Token {
+    public static Directives: Record<string, string> = {
+        "p-show": "x-show",
+        "p-on": "x-on",
+        "p-text": "x-text",
+        "p-html": "x-html",
+        "p-model": "x-model",
+        "p-modelable": "x-modelable",
+        "p-for": "x-for",
+        "p-each": "x-each",
+        "p-transition": "x-transition",
+        "p-effect": "x-effect",
+        "p-ignore": "x-ignore",
+        "p-ref": "x-ref",
+        "ref": "x-ref",
+        "p-cloak": "x-cloak",
+        "p-if": "x-if",
+        "p-id": "x-id",
+        "p-teleport": "x-teleport"
+    };
+
+    public lex(tokens: PugToken[]) {
+        return tokens.map((token, index) => {
+            // We want only attribute tokens
+            if (token.type !== "attribute") {
+                return token;
+            }
+
+            // If it's a replaceable directive
+            if (token.name in PupperToAlpine.Directives) {
+                token.name = PupperToAlpine.Directives[token.name];
+            }
+
+            return token;
+        });
+    }
+};

+ 147 - 0
packages/compiler/src/core/lexer/tokens/component/ScriptParser.ts

@@ -0,0 +1,147 @@
+import { Project, ScriptTarget, SourceFile, SyntaxKind, ObjectLiteralExpression, CallExpression, PropertyAccessExpression } from "ts-morph";
+import Plugin from "../../../Plugin";
+
+import { IComponent } from "../Component";
+
+export class ScriptParser {
+    protected sourceFile: SourceFile;
+
+    constructor(
+        protected component: IComponent,
+        protected fileName: string,
+        protected availableComponents: Record<string, IComponent>,
+        protected plugin: Plugin
+    ) {
+        
+    }
+
+    /**
+     * Parses a script contents
+     * @returns 
+     */
+    public parse() {
+        // Load it in ts-morph
+        const project = new Project({
+            useInMemoryFileSystem: true,
+            compilerOptions: {
+                allowJs: true,
+                noImplicitAny: false,
+                target: ScriptTarget.ESNext
+            }
+        });
+
+        // Create a new source file
+        this.sourceFile = project.createSourceFile(this.fileName, this.component.script);
+
+        this.processDefaultComponent();
+        this.processImportedComponents();
+
+        return this.sourceFile.getText();
+    }
+
+    private processImportedComponents() {
+        if (!("imports" in this.plugin.sharedData)) {
+            return;
+        }
+
+        const componentPropsComponents = this.findComponentImportedComponentsObj();
+
+        // Iterate over all imported components
+        for(let alias in this.plugin.sharedData.imports) {
+            // Add the import to the beginning
+            this.sourceFile.addImportDeclaration({
+                defaultImport: alias,
+                moduleSpecifier: this.plugin.sharedData.imports[alias]
+            });
+
+            // Add it to the component components
+            componentPropsComponents.addPropertyAssignment({
+                name: alias,
+                initializer: alias
+            })
+        }
+    }
+
+    private findComponentImportedComponentsObj() {
+        const componentProps = this.findComponentPropsObj();
+
+        // Try finding an existing "components" expression
+        let exportedComponents = componentProps.getProperty("components");
+
+        if (exportedComponents) {
+            return exportedComponents;
+        }
+
+        return componentProps.addPropertyAssignment({
+            name: "components",
+            initializer: "{}"
+        }).getInitializer() as ObjectLiteralExpression;
+    }
+
+    private processDefaultComponent() {
+        const componentProps = this.findComponentPropsObj();
+
+        // Add the "render" function to it
+        componentProps.addPropertyAssignment({
+            name: "render",
+            initializer: `() => ${JSON.stringify(this.component.template)}`
+        });
+
+        // Filter components that are not the current one
+        const remainingComponents = Object.keys(this.availableComponents)
+                .map((k) => this.availableComponents[k])
+                .filter((c) => c.name !== this.component.name);
+
+        // If has any other exported components
+        if (remainingComponents.length) {
+            const importedComponents = this.findComponentImportedComponentsObj();
+
+            // Add them to the components
+            remainingComponents.forEach((component) => {
+                importedComponents.addPropertyAssignment({
+                    name: component.name,
+                    initializer: component.name as string
+                });
+            });
+        }
+    }
+
+    private findComponentPropsObj() {
+        // Find the default export
+        let defaultExport = this.findDefaultExport();
+
+        // If it's not a defineComponent()
+        const callExp = defaultExport.getFirstChildByKindOrThrow(SyntaxKind.CallExpression);
+
+        // If it's calling like "Pupper."
+        const propAccessExp = callExp.getFirstChildByKind(SyntaxKind.PropertyAccessExpression);
+
+        let parent: CallExpression | PropertyAccessExpression = callExp;
+
+        if (propAccessExp) {
+            parent = propAccessExp;
+        }
+
+        // If the last identifier is not "defineComponent"
+        if (parent.getLastChildByKindOrThrow(SyntaxKind.Identifier).getText() !== "defineComponent") {
+            throw new Error("Pupper components needs to export a call to defineComponent()");
+        }
+
+        // Find the object
+        return callExp.getFirstChildByKindOrThrow(SyntaxKind.ObjectLiteralExpression);
+    }
+
+    private findDefaultExport() {
+        // Export assignment is "export = " or "export default"
+        const defaultExport = this.sourceFile.getFirstChildByKind(SyntaxKind.ExportAssignment);
+
+        // If found
+        if (defaultExport) {
+            return defaultExport;
+        }
+
+        // Try finding a ExpressionStatement that contains a BinaryExpression with PropertyAccessExpression
+        // (module.exports)
+        const module = this.sourceFile.getFirstChildByKindOrThrow(SyntaxKind.ExpressionStatement);
+    }
+}

+ 18 - 0
packages/compiler/src/core/lexer/tokens/component/StyleParser.ts

@@ -0,0 +1,18 @@
+import { IComponent } from "../Component";
+
+export class StyleParser {
+    constructor(
+        protected content: string,
+        protected availableComponents: Record<string, IComponent>
+    ) {
+        
+    }
+
+    /**
+     * Parses a script contents
+     * @returns 
+     */
+    public parse() {
+        return this.content;
+    }
+}

+ 98 - 0
packages/compiler/src/index.ts

@@ -0,0 +1,98 @@
+import pug from "pug";
+import Plugin from "./core/Plugin";
+
+interface ICompilerOptions {
+    /**
+     * If set to true, the function source will be included in the compiled template
+     * for better error messages. It is not enabled by default.
+     */
+    debug?: boolean,
+
+    /**
+     * Any configurations to be passed to pug
+     */
+    pug?: pug.Options
+}
+
+export = class PupperCompiler {
+    /**
+     * Creates a new pupper.js compiler
+     * @returns 
+     */
+    public static createCompiler() {
+        return new PupperCompiler();
+    }
+
+    /**
+     * Compiles a pupper template to a Javascript module
+     * @param template The template to be compiled
+     * @param options 
+     * @returns 
+     */
+    public compileToString(template: string, options: ICompilerOptions = {}): string {
+        try {
+            const pugOptions = this.getPugOptions(options);
+            pugOptions.contents = template;
+
+            let rendered = pug.compileClient(template, pugOptions);
+
+            // If nothing has been exported
+            if (!rendered.includes("export default") && !rendered.includes("module.exports = ")) {
+                // Export the render function as the default
+                rendered += "\n";
+                rendered += /*js*/`module.exports = ${pugOptions.name};`;
+            }
+
+            return rendered;
+        } catch(e) {
+            throw (options.debug ? e : new Error("Failed to compile template:" + e.message));
+        }
+    }
+
+    /**
+     * Compiles a pupper template into HTML.
+     * @param template The template to be compiled
+     * @param options 
+     * @returns 
+     */
+    public compileTemplate(template: string, options: ICompilerOptions = {}): string {
+        try {
+            const pugOptions = this.getPugOptions(options);
+            pugOptions.contents = template;
+            const rendered = pug.compile(template, pugOptions);
+
+            return rendered();
+        } catch(e) {
+            throw (options.debug ? e : new Error("Failed to compile template:" + e.message));
+        }
+    }
+
+    /**
+     * Parses the compiler options into pug options
+     * and put our plugins into it
+     * @param options The compiler options
+     * @returns 
+     */
+    public getPugOptions(options: ICompilerOptions = {}): pug.Options & {
+        contents?: string
+    } {
+        const pugOptions: pug.Options = {
+            // We use "template" as the function name
+            name: "template",
+            // The default filename (when no filename is given) is template.pupper
+            filename: "template.pupper",
+            compileDebug: options.debug || false,
+            // Always use self to prevent conflicts with other compilers
+            self: true,
+            plugins: [],
+            ...options.pug
+        };
+
+        // Create a new parser for this pug instance
+        pugOptions.plugins.push(
+            new Plugin(this, pugOptions)
+        );
+
+        return pugOptions;
+    }
+}

+ 0 - 0
src/types/pug.d.ts → packages/compiler/src/types/pug.d.ts


+ 10 - 0
packages/compiler/tsconfig.json

@@ -0,0 +1,10 @@
+{
+    "extends": "../../tsconfig.json",
+    "compilerOptions": {
+        "target": "esnext",
+        "outDir": "./out",
+        "declarationDir": "./types",
+    },
+    "include": ["./src/**/*.ts"],
+    "exclude": ["node_modules"],
+}

+ 12 - 0
packages/compiler/types/core/Lexer.d.ts

@@ -0,0 +1,12 @@
+import type PugLexer from "pug-lexer";
+import type Token from "./lexer/Token";
+export default class Lexer {
+    static Tokens: typeof Token[];
+    /**
+     * Checks if a given expression is valid
+     * @param lexer The pug lexer instance
+     * @param exp The expression to be checked against
+     * @returns
+     */
+    isExpression(lexer: PugLexer.Lexer, exp: string): boolean;
+}

+ 37 - 0
packages/compiler/types/core/Plugin.d.ts

@@ -0,0 +1,37 @@
+import Lexer from "./Lexer";
+import { PugPlugin, PugToken, PugAST, PugNode, Options } from "pug";
+import PupperCompiler from "..";
+export { PugToken, PugAST, PugNode };
+/**
+ * Documentation for this class is available in the PugPlugin interface
+ */
+export default class Plugin implements PugPlugin {
+    compiler: PupperCompiler;
+    options: Options & {
+        contents?: string;
+    };
+    /**
+     * The instances of the tokens that will be used to parse the template file
+     */
+    private tokens;
+    /**
+     * A handler for the plugin hooks
+     */
+    private hooks;
+    /**
+     * Any data to be shared between hooks and phases
+     */
+    sharedData: Record<any, any>;
+    lex: Lexer;
+    constructor(compiler: PupperCompiler, options: Options & {
+        contents?: string;
+    });
+    getOptions(): Options & {
+        contents?: string;
+    };
+    addHook(hook: string, callback: Function): number;
+    applyFilters(hook: string, initialValue: any): any;
+    preParse(tokens: PugToken[]): any;
+    postParse(block: PugAST): any;
+    postCodeGen(code: string): string;
+}

+ 37 - 0
packages/compiler/types/core/lexer/Token.d.ts

@@ -0,0 +1,37 @@
+import Plugin, { PugAST } from "../Plugin";
+import { PugToken, PugNode } from "pug";
+export default class Token {
+    protected plugin: Plugin;
+    static readonly REGEX: RegExp;
+    constructor(plugin: Plugin);
+    /**
+     * Tests if the token matches with the given expression
+     * @param exp The expression to be tested
+     * @returns
+     */
+    static testExpression(exp: string): boolean;
+    /**
+     * Lexes the given token and return new ones
+     * @param tokens The tokens to be parsed
+     * @returns Parsed tokens
+     */
+    lex(tokens: PugToken[]): PugToken[];
+    /**
+     * Parses the given token and return new ones
+     * @param nodes The nodes to be parsed
+     * @returns Parsed nodes
+     */
+    parse(nodes: PugNode[]): PugNode[];
+    /**
+     * Called before the AST is compiled into Javascript
+     * @param ast The pug AST to be compiled
+     * @returns
+     */
+    beforeCompile(ast: PugAST): PugAST;
+    /**
+     * Called after the AST is compiled into Javascript
+     * @param code The generated Javascript code
+     * @returns
+     */
+    afterCompile(code: string): string;
+}

+ 24 - 0
packages/compiler/types/core/lexer/tokens/Component.d.ts

@@ -0,0 +1,24 @@
+import { PugNode } from "../../Plugin";
+import Token from "../Token";
+export interface IComponent {
+    name: string | symbol;
+    template: string;
+    script?: string;
+    setupScript?: string;
+    style?: string;
+    exported?: boolean;
+}
+export default class Component extends Token {
+    /**
+     * Parses a pug node into a component.
+     * @param node The pug node to be parsed.
+     * @returns
+     */
+    parseNode(node: PugNode, nextNode: PugNode): IComponent;
+    /**
+     * The imports that will later be putted into the template header
+     */
+    protected components: Record<string | symbol, IComponent>;
+    parse(nodes: PugNode[]): PugNode[];
+    afterCompile(code: string): string;
+}

+ 5 - 0
packages/compiler/types/core/lexer/tokens/ForEach.d.ts

@@ -0,0 +1,5 @@
+import { PugNode } from "../../Plugin";
+import Token from "../Token";
+export default class ForEach extends Token {
+    parse(nodes: PugNode[]): PugNode[];
+}

+ 5 - 0
packages/compiler/types/core/lexer/tokens/If.d.ts

@@ -0,0 +1,5 @@
+import { PugNode } from "../../Plugin";
+import Token from "../Token";
+export default class ForEach extends Token {
+    parse(nodes: PugNode[]): PugNode[];
+}

+ 11 - 0
packages/compiler/types/core/lexer/tokens/Import.d.ts

@@ -0,0 +1,11 @@
+import { PugNode } from "../../Plugin";
+import Token from "../Token";
+export default class Import extends Token {
+    private static readonly IMPORT_CONDITION;
+    /**
+     * The imports that will later be putted into the template header
+     */
+    protected imports: Record<string, string>;
+    parse(nodes: PugNode[]): PugNode[];
+    afterCompile(code: string): string;
+}

+ 15 - 0
packages/compiler/types/core/lexer/tokens/Property.d.ts

@@ -0,0 +1,15 @@
+import { PugToken } from "../../Plugin";
+import Token from "../Token";
+export default class Property extends Token {
+    /**
+     * The regex to test if an expression is a valid reactive item
+     */
+    static REGEX: RegExp;
+    /**
+     * Tests if the token matches with the given expression
+     * @param exp The expression to be tested
+     * @returns
+     */
+    static testExpression(exp: string): boolean;
+    lex(tokens: PugToken[]): PugToken[];
+}

+ 6 - 0
packages/compiler/types/core/lexer/tokens/PupperToAlpine.d.ts

@@ -0,0 +1,6 @@
+import { PugToken } from "../../Plugin";
+import Token from "../Token";
+export default class PupperToAlpine extends Token {
+    static Directives: Record<string, string>;
+    lex(tokens: PugToken[]): PugToken[];
+}

+ 21 - 0
packages/compiler/types/core/lexer/tokens/component/ScriptParser.d.ts

@@ -0,0 +1,21 @@
+import { SourceFile } from "ts-morph";
+import Plugin from "../../../Plugin";
+import { IComponent } from "../Component";
+export declare class ScriptParser {
+    protected component: IComponent;
+    protected fileName: string;
+    protected availableComponents: Record<string, IComponent>;
+    protected plugin: Plugin;
+    protected sourceFile: SourceFile;
+    constructor(component: IComponent, fileName: string, availableComponents: Record<string, IComponent>, plugin: Plugin);
+    /**
+     * Parses a script contents
+     * @returns
+     */
+    parse(): string;
+    private processImportedComponents;
+    private findComponentImportedComponentsObj;
+    private processDefaultComponent;
+    private findComponentPropsObj;
+    private findDefaultExport;
+}

+ 11 - 0
packages/compiler/types/core/lexer/tokens/component/StyleParser.d.ts

@@ -0,0 +1,11 @@
+import { IComponent } from "../Component";
+export declare class StyleParser {
+    protected content: string;
+    protected availableComponents: Record<string, IComponent>;
+    constructor(content: string, availableComponents: Record<string, IComponent>);
+    /**
+     * Parses a script contents
+     * @returns
+     */
+    parse(): string;
+}

+ 69 - 0
packages/compiler/types/index.d.ts

@@ -0,0 +1,69 @@
+import pug from "pug";
+interface ICompilerOptions {
+    /**
+     * If set to true, the function source will be included in the compiled template
+     * for better error messages. It is not enabled by default.
+     */
+    debug?: boolean;
+    /**
+     * Any configurations to be passed to pug
+     */
+    pug?: pug.Options;
+}
+declare const _default: {
+    new (): {
+        /**
+         * Compiles a pupper template to a Javascript module
+         * @param template The template to be compiled
+         * @param options
+         * @returns
+         */
+        compileToString(template: string, options?: ICompilerOptions): string;
+        /**
+         * Compiles a pupper template into HTML.
+         * @param template The template to be compiled
+         * @param options
+         * @returns
+         */
+        compileTemplate(template: string, options?: ICompilerOptions): string;
+        /**
+         * Parses the compiler options into pug options
+         * and put our plugins into it
+         * @param options The compiler options
+         * @returns
+         */
+        getPugOptions(options?: ICompilerOptions): pug.Options & {
+            contents?: string;
+        };
+    };
+    /**
+     * Creates a new pupper.js compiler
+     * @returns
+     */
+    createCompiler(): {
+        /**
+         * Compiles a pupper template to a Javascript module
+         * @param template The template to be compiled
+         * @param options
+         * @returns
+         */
+        compileToString(template: string, options?: ICompilerOptions): string;
+        /**
+         * Compiles a pupper template into HTML.
+         * @param template The template to be compiled
+         * @param options
+         * @returns
+         */
+        compileTemplate(template: string, options?: ICompilerOptions): string;
+        /**
+         * Parses the compiler options into pug options
+         * and put our plugins into it
+         * @param options The compiler options
+         * @returns
+         */
+        getPugOptions(options?: ICompilerOptions): pug.Options & {
+            contents?: string;
+        };
+    };
+};
+export = _default;

+ 28 - 0
packages/renderer/package.json

@@ -0,0 +1,28 @@
+{
+  "name": "@pupperjs/renderer",
+  "version": "1.0.0",
+  "description": "A reactive template engine based in Pug and Alpine.js",
+  "author": "Matheus Giovani <matheus@ad3com.com.br>",
+  "license": "AGPL-3.0",
+  "private": false,
+  "main": "./out/",
+  "scripts": {
+    "watch": "npm-run-all -p -r watch:*",
+    "watch:ts": "tsc -watch"
+  },
+  "dependencies": {
+    "alpinejs": "^3.10.2",
+    "pug": "^3.0.2"
+  },
+  "types": "./types/",
+  "devDependencies": {
+    "@types/alpinejs": "^3.7.0",
+    "@types/node": "^16.7.6",
+    "debug": "^4.3.2",
+    "npm-run-all": "^4.1.5",
+    "tsc": "^2.0.3",
+    "typescript": "^4.4.2",
+    "webpack": "^5.51.1",
+    "webpack-cli": "^4.8.0"
+  }
+}

+ 289 - 0
packages/renderer/src/core/Component.ts

@@ -0,0 +1,289 @@
+import Alpine from "alpinejs";
+
+/**
+ * Represents a slot.
+ */
+interface Slot {
+    /**
+     * The comment holding the slot position.
+     */
+    container: Element | Comment;
+
+    /**
+     * All fallback nodes
+     */
+    fallbackNodes: Node[]
+}
+
+/**
+ * Represents a component's data.
+ */
+interface IComponent<
+    TData extends Record<string, any>,
+    TMethods extends Record<string, CallableFunction>
+> {
+    /**
+     * Component-related
+     */
+
+    /**
+     * The function that renders the template HTML.
+     */
+    render?: (data: Record<string, any>) => string;
+
+    /**
+     * Any data to be passed to the template.
+     */
+    data?: TData | (() => TData);
+
+    /**
+     * Any methods that can be called from the component.
+     */
+    methods?: TMethods;
+
+    /**
+     * Events
+     */
+
+    /**
+     * Called when the component is mounted
+     */
+    created?: (this: PupperComponent) => any,
+
+    /**
+     * Called when the component is mounted.
+     */
+    mounted?: (this: PupperComponent) => any;
+}
+
+export class PupperComponent {
+    public static create<
+        TMethods extends Record<string, CallableFunction>,
+        TData extends Record<string, any>
+    >(component: IComponent<TData, TMethods>) {
+        return new PupperComponent(component) as (PupperComponent & TMethods);
+    }
+
+    /**
+     * A unique identifier for this component.
+     */
+    protected $identifier: string;
+
+    /**
+     * All the data that will be passed to the renderer and Alpine.
+     */
+    private $data: Record<string, any> = {};
+
+    /**
+     * Any slots references.
+     */
+    public $slots: Record<string, Slot> = {};
+
+    /**
+     * Any templates references.
+     */
+    public $templates: Record<string, CallableFunction> = {};
+
+    /**
+     * Any component references.
+     */
+    public $refs: Record<string, HTMLElement> = {};
+
+    constructor(
+        /**
+         * The component properties.
+         */
+        protected component: IComponent<any, any>
+    ) {
+        // If has methods
+        if (component?.methods) {
+            for(let method in component.methods) {
+                this.$data[method] = component.methods[method];
+            }
+        }
+
+        // If has data
+        if (component?.data) {
+            if (typeof component.data === "function") {
+                component.data = component.data();
+            }
+
+            for(let key in component.data) {
+                // If it's already registered
+                if (key in this.$data) {
+                    throw new Error("There's already a property named " + key + " registered in the component. Property names should be unique.");
+                }
+
+                this.$data[key] = component.data[key];
+            }
+        }
+
+        // For each generated data
+        for(let key in this.$data) {
+            // Prepare a descriptor for the base component
+            const attributes: PropertyDescriptor = {
+                get() {
+                    return this.$data[key]
+                }
+            };
+
+            // If it's not a function
+            if (typeof this.$data[key] !== "function") {
+                attributes.set = (value) => this.$data[key] = value;
+            }
+
+            // Define the property inside the component
+            Object.defineProperty(this, key, attributes);
+        }
+
+        if (this.component?.created) {
+            this.component.created.call(this);
+        }
+    }
+
+    /**
+     * Registers a single template.
+     * @param templateName The template name.
+     * @param template The template render function.
+     */
+    public registerTemplate(templateName: string, template: CallableFunction) {
+        this.$templates[templateName] = template;
+    }
+
+    /**
+     * Replaces an element with a comment placeholder element.
+     * @param element The element to be replaced.
+     * @returns 
+     */
+    private replaceWithCommentPlaceholder(element: HTMLElement) {
+        const comment = document.createComment("");
+
+        if (!element.parentElement) {
+            element.replaceWith(comment);
+        } else {
+            element.parentElement.insertBefore(comment, element);
+            element.parentElement.removeChild(element);
+        }
+
+        return comment;
+    }
+
+    /**
+     * Renders a template and return the rendered child nodes.
+     * @param template The template name to be rendered
+     * @param data The template data
+     * @returns 
+     */
+    public renderTemplate(template: string) {
+        return this.renderStringToTemplate(
+            this.$templates[template](this)
+        ).content.children[0].childNodes;
+    }
+
+    /**
+     * Renders a template string into a template tag with a div with [data-rendered-template] attribute.
+     * @param string The template string to be rendered.
+     * @returns 
+     */
+    private renderStringToTemplate(string: string): HTMLTemplateElement {
+        const renderContainer = document.createElement("template");
+        renderContainer.innerHTML = `<div data-rendered-template>${string}</div>`;
+
+        return renderContainer;
+    }
+
+    /**
+     * Renders the template function into a div tag.
+     */
+    public render(data?: Record<string, any>) {
+        // Render the initial string
+        const renderContainer = this.renderStringToTemplate(
+            this.component.render(data)
+        );
+
+        // Find all slots, templates and references
+        const slots = Array.from(renderContainer.content.querySelectorAll("slot"));
+        const templates = Array.from(renderContainer.content.querySelectorAll("template"));
+        const refs = Array.from(renderContainer.content.querySelectorAll("[ref]"));
+
+        // Iterate over all slots
+        for(let slot of slots) {
+            // Replace it with a comment tag
+            const comment = this.replaceWithCommentPlaceholder(slot);
+
+            // If it's a named slot
+            if (slot.hasAttribute("name")) {
+                // Save it
+                this.$slots[slot.getAttribute("name")] = {
+                    container: comment,
+                    fallbackNodes: [...comment.childNodes]
+                };
+            }
+        }
+
+        // Iterate over all templates
+        for(let childrenTemplate of templates) {
+            // If it's a named template
+            if (childrenTemplate.hasAttribute("name")) {
+                // Remove it from the DOM
+                childrenTemplate.parentElement.removeChild(childrenTemplate);
+
+                // Save it
+                this.$templates[childrenTemplate.getAttribute("name")] = () => {
+                    return [...childrenTemplate.content.children].map((node) => node.innerHTML);
+                };
+            }
+        }
+        
+        // Iterate over all references
+        for(let ref of refs) {
+            // Save it
+            this.$refs[ref.getAttribute("ref")] = ref as HTMLElement;
+
+            // Remove the attribute
+            ref.removeAttribute("ref");
+        }
+
+        const container = renderContainer.content.children[0];
+
+        this.$identifier = "pup_" + String(Date.now());
+        container.setAttribute("x-data", this.$identifier);
+
+        return container;
+    }
+
+    /**
+     * Renders and mounts the template into a given element.
+     * @param target The target element where the element will be mounted.
+     * @returns 
+     */
+    public mount(target: HTMLElement | Slot) {
+        const rendered = this.render();
+
+        if (!(target instanceof HTMLElement)) {
+            target.container.replaceWith(rendered);
+            target.container = rendered;
+        } else {
+            target.appendChild(rendered);
+        }
+
+        // Initialize the data
+        Alpine.data(this.$identifier, () => {
+            return this.$data;
+        });
+
+        // Initialize Alpine for it
+        Alpine.start();
+
+        // Remove the identifier
+        rendered.removeAttribute("x-data");
+
+        // Save a reference to the internal Alpine data proxy
+        // @ts-ignore
+        this.$data = Alpine.$data(rendered);
+
+        if (this.component?.mounted) {
+            this.component.mounted.call(this);
+        }        
+    }
+}

+ 8 - 0
packages/renderer/src/index.ts

@@ -0,0 +1,8 @@
+import { PupperComponent as Component } from "./core/Component";
+
+const defineComponent = Component.create;
+
+export = {
+    Component,
+    defineComponent
+}

+ 8 - 0
packages/renderer/src/types/pupper.d.ts

@@ -0,0 +1,8 @@
+import { PupperComponent } from "../core/Component";
+
+/**
+ * Used to represent whats is a pupper module
+ */
+declare module "*.pupper" {
+    export default function(data: object): PupperComponent;
+}

+ 9 - 0
packages/renderer/tsconfig.json

@@ -0,0 +1,9 @@
+{
+    "extends": "../../tsconfig.json",
+    "compilerOptions": {
+        "outDir": "./out",
+        "declarationDir": "./types",
+    },
+    "include": ["./src/**/*.ts"],
+    "exclude": ["node_modules"],
+}

+ 112 - 0
packages/renderer/types/core/Component.d.ts

@@ -0,0 +1,112 @@
+/**
+ * Represents a slot.
+ */
+interface Slot {
+    /**
+     * The comment holding the slot position.
+     */
+    container: Element | Comment;
+    /**
+     * All fallback nodes
+     */
+    fallbackNodes: Node[];
+}
+/**
+ * Represents a component's data.
+ */
+interface IComponent<TData extends Record<string, any>, TMethods extends Record<string, CallableFunction>> {
+    /**
+     * Component-related
+     */
+    /**
+     * The function that renders the template HTML.
+     */
+    render?: (data: Record<string, any>) => string;
+    /**
+     * Any data to be passed to the template.
+     */
+    data?: TData | (() => TData);
+    /**
+     * Any methods that can be called from the component.
+     */
+    methods?: TMethods;
+    /**
+     * Events
+     */
+    /**
+     * Called when the component is mounted
+     */
+    created?: (this: PupperComponent) => any;
+    /**
+     * Called when the component is mounted.
+     */
+    mounted?: (this: PupperComponent) => any;
+}
+export declare class PupperComponent {
+    /**
+     * The component properties.
+     */
+    protected component: IComponent<any, any>;
+    static create<TMethods extends Record<string, CallableFunction>, TData extends Record<string, any>>(component: IComponent<TData, TMethods>): PupperComponent & TMethods;
+    /**
+     * A unique identifier for this component.
+     */
+    protected $identifier: string;
+    /**
+     * All the data that will be passed to the renderer and Alpine.
+     */
+    private $data;
+    /**
+     * Any slots references.
+     */
+    $slots: Record<string, Slot>;
+    /**
+     * Any templates references.
+     */
+    $templates: Record<string, CallableFunction>;
+    /**
+     * Any component references.
+     */
+    $refs: Record<string, HTMLElement>;
+    constructor(
+    /**
+     * The component properties.
+     */
+    component: IComponent<any, any>);
+    /**
+     * Registers a single template.
+     * @param templateName The template name.
+     * @param template The template render function.
+     */
+    registerTemplate(templateName: string, template: CallableFunction): void;
+    /**
+     * Replaces an element with a comment placeholder element.
+     * @param element The element to be replaced.
+     * @returns
+     */
+    private replaceWithCommentPlaceholder;
+    /**
+     * Renders a template and return the rendered child nodes.
+     * @param template The template name to be rendered
+     * @param data The template data
+     * @returns
+     */
+    renderTemplate(template: string): NodeListOf<ChildNode>;
+    /**
+     * Renders a template string into a template tag with a div with [data-rendered-template] attribute.
+     * @param string The template string to be rendered.
+     * @returns
+     */
+    private renderStringToTemplate;
+    /**
+     * Renders the template function into a div tag.
+     */
+    render(data?: Record<string, any>): Element;
+    /**
+     * Renders and mounts the template into a given element.
+     * @param target The target element where the element will be mounted.
+     * @returns
+     */
+    mount(target: HTMLElement | Slot): void;
+}
+export {};

+ 6 - 0
packages/renderer/types/index.d.ts

@@ -0,0 +1,6 @@
+import { PupperComponent as Component } from "./core/Component";
+declare const _default: {
+    Component: typeof Component;
+    defineComponent: typeof Component.create;
+};
+export = _default;

+ 26 - 0
packages/webpack-loader/index.js

@@ -0,0 +1,26 @@
+/**
+ * pupper.js - Webpack Loader
+ * @author Matheus Giovani <matheus@ad3com.com.br>
+ * @license AGPL-3.0
+ */
+
+const Compiler = require("@pupperjs/compiler");
+
+/**
+ * @param {string} source The source file content
+ * @param {CompilerOptions} options Any options to be passed to the pupper compiler
+ * @returns {String}
+ */
+module.exports = function(source, options) {
+    const contents = Compiler.createCompiler().compileToString(
+        source,
+        {
+            ...options,
+            pug: {
+                filename: this.resourcePath
+            }
+        }
+    );
+
+    return contents;
+};

+ 5 - 2
modules/webpack/package.json → packages/webpack-loader/package.json

@@ -4,5 +4,8 @@
   "description": "Webpack loader for pupper.js templates",
   "main": "index.js",
   "author": "Matheus Giovani <matheus@ad3com.com.br>",
-  "license": "AGPL-3.0"
-}
+  "license": "AGPL-3.0",
+  "peerDependencies": {
+    "webpack": "^5.51.1"
+  }
+}

+ 0 - 14
src/core/App.ts

@@ -1,14 +0,0 @@
-export interface AppSettings {
-    /**
-     * The target element related to this application
-     */
-    el?: HTMLElement | string
-}
-
-export class App {
-    constructor(
-        protected settings: AppSettings
-    ) {
-
-    }
-}

+ 0 - 99
src/core/Compiler.ts

@@ -1,99 +0,0 @@
-import pug from "pug";
-import fs from "fs";
-import path from "path";
-
-import { CompiledTemplate, Renderer } from "./Renderer";
-import Plugin from "./compiler/Plugin";
-
-export namespace Compiler {
-    export interface Options {
-        /**
-         * If set to true, the function source will be included in the compiled template
-         * for better error messages. It is not enabled by default.
-         */
-        debug?: boolean,
-
-        /**
-         * Any configurations to be passed to pug
-         */
-        pug?: pug.Options
-    }
-}
-
-export default class PupperCompiler {
-    /**
-     * Compiles a template string into a renderer instance
-     * @param template The template string to be compiled
-     * @param options The compiler options
-     * @returns 
-     */
-    public compile(template: string, options: Compiler.Options = {}): Renderer {
-        try {
-            return new Renderer(
-                pug.compile(template, this.getPugOptions(options)) as CompiledTemplate
-            );
-        } catch(e) {
-            throw (options.debug ? e : new Error("Failed to compile template:" + e.message));
-        }
-    }
-
-    /**
-     * Compiles a single template file into a renderer instance
-     * @param file The file to be compiled
-     * @returns
-     */
-    public compileFile(file: string, options: Compiler.Options = {}): Renderer {
-        const pugOptions = this.getPugOptions(options);
-        pugOptions.basedir = path.dirname(file);
-        pugOptions.filename = file;
-
-        return this.compile(fs.readFileSync(file, "utf8"), pugOptions);
-    }
-
-    /**
-     * Compiles a pupper template to a Javascript module
-     * @param template The template to be compiled
-     * @param options 
-     * @returns 
-     */
-    public compileToString(template: string, options: Compiler.Options = {}): string {
-        try {
-            const pugOptions = this.getPugOptions(options);
-            const rendered = pug.compileClient(template, pugOptions);
-
-            return /*javascript*/`
-                ${rendered}
-                module.exports = ${pugOptions.name};
-            `;
-        } catch(e) {
-            throw (options.debug ? e : new Error("Failed to compile template:" + e.message));
-        }
-    }
-
-    /**
-     * Parses the compiler options into pug options
-     * and put our plugins into it
-     * @param options The compiler options
-     * @returns 
-     */
-    private getPugOptions(options: Compiler.Options = {}): pug.Options {
-        const pugOptions: pug.Options = {
-            // We use "render" as the function name
-            name: "render",
-            // The default filename (when no filename is given) is template.pupper
-            filename: "template.pupper",
-            compileDebug: options.debug || false,
-            // Always use self to prevent conflicts with other compilers
-            self: true,
-            plugins: [],
-            ...options.pug
-        };
-
-        // Create a new parser for this pug instance
-        pugOptions.plugins.push(
-            new Plugin(pugOptions)
-        );
-
-        return pugOptions;
-    }
-}

+ 0 - 67
src/core/Component.ts

@@ -1,67 +0,0 @@
-import { Renderer } from "./Renderer";
-import type { CompiledTemplate } from "./Renderer";
-import { Reactive } from "./renderer/Reactive";
-
-export interface ComponentSettings {
-    /**
-     * The template to this component
-     */
-    template: CompiledTemplate;
-
-    /**
-     * The data to be passed to this component
-     */
-    data?: Record<any, any>;
-
-    /**
-     * The component methods
-     */
-    methods?: Reactive.ReactiveMethods;
-}
-
-export class Component {
-    /**
-     * The renderer related to this component
-     */
-    public renderer: Renderer;
-
-    constructor(
-        protected settings: ComponentSettings
-    ) {
-        this.renderer = new Renderer(this.settings.template, this.settings.data);
-        this.methods = settings.methods;
-        this.data = settings.data;
-    }
-
-    public get data() {
-        return this.renderer.data;
-    }
-
-    public set data(data: Record<any, any>) {
-        this.renderer.setData(data);
-    }
-
-    public get methods() {
-        return this.renderer.methods;
-    }
-
-    public set methods(methods: Reactive.ReactiveMethods) {
-        this.renderer.methods = methods;
-    }
-
-    /**
-     * Renders the component
-     */
-    public async render() {
-        return this.renderer.render();
-    }
-
-    /**
-     * Renders the component into a HTML element
-     * @param element The HTML element that will receive the element
-     * @returns 
-     */
-    public async renderTo(element: string | HTMLElement) {
-        return this.renderer.renderTo(element);
-    }
-}

+ 0 - 572
src/core/Renderer.ts

@@ -1,572 +0,0 @@
-import type pug from "pug";
-import deepGetSet from "deep-get-set";
-import observableSlim from "observable-slim";
-import Reactor from "./renderer/Reactor";
-import { Reactive } from "./renderer/Reactive";
-
-const debug = require("debug")("pupperjs:renderer");
-
-/**
- * Represents the final contents of a pupper.js file
- * It's the render function itself plus some useful things
- */
-export type CompiledTemplate = pug.compileTemplate & {
-    /**
-     * A handler for all imports that this compiled template uses
-     * where the key is the tag name, and the value is the compiled template
-     */
-    imports?: Record<string, CompiledTemplate>;
-}
-
-export interface NodeOptions {
-    /**
-     * Any prefix to be passed to the dot notation
-     */
-    pathPrefix?: string,
-
-    /**
-     * Any additional context indexes to search for values
-     */
-    context?: Record<string, any>
-}
-
-export enum NodePreparationResult {
-    SKIP_CHILD,
-    SUCCESS,
-    FAILED
-}
-
-export class PupperHelper {
-    constructor(
-        protected renderer: Renderer
-    ) {
-
-    }
-
-    /**
-     * 
-     * @param key The path to the data to be retrieved
-     * @param context Any additional contexts
-     * @returns 
-     */
-    public getValue(key: string, context?: Record<string, any>) {
-        let value;
-
-        // First, try from the context
-        if (context !== undefined) {
-            value = deepGetSet(context, key);
-        }
-
-        // Then try from the data itself
-        if (value === undefined) {
-            value = deepGetSet(this.renderer.getData(), key);
-        }
-
-        debug("retrieving value %s: %O", key, value);
-
-        return value;
-    }
-
-    /**
-     * Retrieves an escaped value to be displayed
-     * @param key The path to the data to be escaped
-     * @returns 
-     */
-    public escape(key: string, context?: Record<string, any>): string {
-        const text = document.createTextNode(
-            this.getValue(key, context)
-        );
-
-        return text.textContent;
-    }
-
-    /**
-     * Retrieves a literal value to be displayed
-     * @param key The path to the data to be retrieved
-     * @returns 
-     */
-    public literal<T>(key: string, context?: Record<string, any>): T {
-        return this.getValue(key, context);
-    }
-}
-
-export class Renderer {
-    private static SYNTAX_REGEX = /(?: +)?\@p\:(?<command>.+)\((?<property>.+?)\)(?: +)?/;
-
-    /**
-     * The pug compiled template function
-     */
-    private template: CompiledTemplate;
-
-    /**
-     * The reactive data
-     */
-    public data: ProxyHandler<Reactive.ReactiveData> = {};
-
-    /**
-     * The methods to be attributed with the elements
-     */
-    // @ts-ignore
-    public methods: Reactive.ReactiveMethods = {};
-
-    /**
-     * The DOM element that will receive all children
-     */
-    private dom: HTMLElement;
-
-    /**
-     * The reactor for this renderer
-     */
-    public reactor: Reactor;
-
-    /**
-     * The cached helpers for this Renderer
-     */
-    public helpers: Record<string, any> & {
-        deepGetSet: (object: object, key: any, value?: any) => any;
-
-        /**
-         * The methods related to this renderer
-         */
-        $methods: Reactive.ReactiveMethods;
-
-        /**
-         * The data related to this renderer
-         */
-        $data: ProxyHandler<Reactive.ReactiveData>;
-
-        /**
-         * Pupper helpers
-         */
-        pupper: PupperHelper;
-    };
-
-    /**
-     * Creates a new renderer instance
-     * @param template The pug compiled template function
-     * @param data The data that will be used for reactivity
-     */
-    constructor(template: CompiledTemplate, settings?: {
-        data?: Reactive.ReactiveData,
-        methods?: Reactive.ReactiveMethods
-    }) {
-        this.template = template;
-
-        // Create the reactor
-        this.reactor = new Reactor(this);
-
-        if (settings?.data) {
-            this.setData(settings.data);
-        }
-
-        if (settings?.methods) {
-            this.methods = settings.methods;
-        }
-    }
-
-    /**
-     * Retrieves all pupper runtime helpers
-     * @returns 
-     */
-    private getHelpers() {
-        if (this.helpers === undefined) {
-            const self = this;
-
-            this.helpers = {
-                deepGetSet,
-
-                /**
-                 * The methods related to this renderer
-                 */
-                $methods: this.methods,
-
-                /**
-                 * The data related to this renderer
-                 */
-                $data: this.data,
-
-                /**
-                 * Pupper helpers
-                 */
-                pupper: new PupperHelper(this)
-            };
-        }
-
-        return this.helpers;
-    }
-
-    /**
-     * Retrieves the underlying pug template function
-     * @returns 
-     */
-    public getTemplateFn() {
-        return this.template;
-    }
-
-    /**
-     * Retrieves the template data
-     * @returns 
-     */
-    public getData() {
-        return this.data;
-    }
-
-    /**
-     * Replaces all the object data with new proxied data
-     * @param data The new template data
-     * @returns The proxied data object
-     */
-    public setData<T extends Record<any, any>>(data: T): ProxyHandler<T> {
-        // Prepare the proxy
-        const proxy = {
-            get(target: Record<any, any>, key: string): any {
-                if (key == "isProxy") {
-                    return true;
-                }
-
-                const prop = target[key];
-
-                // return if property not found
-                if (typeof prop === "undefined") {
-                    return;
-                }
-
-                if (prop === null) {
-                    return null;
-                }
-
-                // Set value as proxy if object
-                if (!prop.isProxy && typeof prop === "object") {
-                    target[key] = new Proxy(prop, proxy);
-                }
-
-                return target[key];
-            },
-            set: this.reactor.onPropertyChange.bind(this.reactor)
-        };
-
-        this.data = observableSlim.create(data, true, this.reactor.onPropertyChange.bind(this.reactor));
-
-        return this.data;
-    }
-
-    /**
-     * Retrieves the context that will be passed to the template "locals" variable
-     * @returns 
-     */
-    public getTemplateContext() {
-        return {
-            ...this.getHelpers(),
-            ...this.getData()
-        };
-    }
-
-    /**
-     * Renders the template into a string
-     * @returns 
-     */
-    public renderToString() {
-        return this.template(this.getTemplateContext());
-    }
-
-    /**
-     * Renders the template into a string
-     * @returns 
-     */
-    public render() {
-        // Convert into the final tag so we can parse it
-        const target = document.createElement("div");
-        target.classList.add("pupper");
-        
-        this.dom = this.renderTo(target);
-
-        return target;
-    }
-
-    /**
-     * Renders the template into a target element
-     * @param target The target element
-     * @returns 
-     */
-    public renderTo(target: string | HTMLElement = document.body) {
-        // Render the template
-        const rendered = this.renderToString();
-
-        // Create a template tag and set the contents of the template to it
-        const template = document.createElement("template");
-        template.innerHTML = rendered;
-
-        // Prepare the nodes
-        this.prepareNodes(template.content.childNodes);
-
-        // Append it to the DOM itself
-        const targetEl: HTMLElement = target instanceof HTMLElement ? target : document.querySelector(target);
-        targetEl.appendChild(template.content);
-
-        this.dom = targetEl;
-
-        return this.dom;
-    }
-
-    /**
-     * Parses a single pupper syntax
-     * @param command The command / syntax to be parsed
-     * @param nodeOptions The node options that will be used for parsing
-     * @returns 
-     */
-    private parseCommand(command: string, nodeOptions: NodeOptions = {}) {
-        command = command.trim();
-
-        // Parse it
-        const parsed = command.match(Renderer.SYNTAX_REGEX);
-        
-        if (parsed === null) {
-            throw new Error("Failed to parse command \"" + command + "\"");
-        }
-
-        const fn: Reactive.ReactiveCommand = (parsed.groups.command as Reactive.ReactiveCommand);
-        const property = parsed.groups.property;
-
-        let value = property;
-
-        switch(fn) {
-            // If it's an escape call
-            case "escape":
-                value = this.getEscapedValue(property, nodeOptions.context);
-            break;
-
-            // If it's a literal call
-            case "literal":
-                value = this.getLiteralValue(property, nodeOptions.context);
-            break;
-        }
-
-        return {
-            value,
-            command: fn,
-            property
-        };
-    }
-
-    /**
-     * Retrieves an HTML-escaped text value
-     * @param property The property to be retrieved
-     * @returns 
-     */
-    public getEscapedValue(property: string, context?: Record<string, any>) {
-        return this.getHelpers().pupper.escape(property, context);
-    }
-
-    /**
-     * Retrieves a literal value (as-is, without any treatment)
-     * @param property The property to be retrieved
-     * @returns 
-     */
-    public getLiteralValue<T>(property: string, context?: Record<string, any>): T {
-        return this.getHelpers().pupper.literal<T>(property, context);
-    }
-
-    /**
-     * Prepares nodes to be reactive
-     * @param nodes The node list to be prepared
-     * @param context The proxy context to the nodes; defaults to `this.data`
-     * @returns
-     */
-    public prepareNodes(nodes: NodeListOf<ChildNode>, options?: NodeOptions) {
-        for(let element of [...nodes]) {
-            const result = this.prepareNode(element as HTMLElement, options);
-
-            // Check if failed
-            if (result === NodePreparationResult.FAILED) {
-                // Stop parsing
-                return false;
-            }
-
-            // Check if doesn't want to skip the child items
-            if (result !== NodePreparationResult.SKIP_CHILD) {
-                // If has children, fix the children too
-                if (element.hasChildNodes()) {
-                    this.prepareNodes(element.childNodes, options);
-                }
-            }
-        }
-
-        return true;
-    }
-
-    private scopedEval(expr: string, context: Record<string, any>) {
-        const evaluator = Function.apply(
-            null,
-            [
-                ...Object.keys(context),
-                "expr",
-                `return ${expr}`
-            ]
-        );
-
-        return evaluator.apply(null, [...Object.values(context), expr]);
-    }
-
-    /**
-     * Prepares a single HTML element
-     * @param element The element to be prepared
-     * @param context The proxy context to the node; defaults to `this.data`
-     * @returns The preparation result
-     */
-    private prepareNode(element: HTMLElement | Element, options: NodeOptions = {}): NodePreparationResult {
-        // Copy the default options
-        options = {
-            pathPrefix: "",
-            ...options
-        };
-
-        // Check if it's a comment
-        if (element instanceof Comment) {
-            const comment = (element as Comment);
-
-            // Check if it's not a pupper comment
-            if (comment.textContent.indexOf("@p:") === -1) {
-                return;
-            }
-
-            // Parse it
-            const parsed = this.parseCommand(comment.textContent, options);
-            const text = document.createTextNode(parsed.value || "");
-
-            // Replace with a text node
-            element.replaceWith(text);
-
-            // Set it as reactive
-            this.reactor.addReactivity(text, options.pathPrefix + parsed.property, "text", {
-                command: parsed.command,
-                initialValue: parsed.value,
-                nodeOptions: options
-            });
-        } else
-        // Check if it's a foreach
-        if (element.tagName === "P:FOREACH") {
-            // Retrieve the foreach attributes
-            const array = element.getAttribute("array");
-            const variable = element.getAttribute("var");
-            const type = element.getAttribute("type");
-            const html = element.innerHTML;
-
-            const comment = document.createComment(" ");
-            element.replaceWith(comment);
-
-            /**
-             * @todo move this to a sub class to manage this better.
-             * For. God's. Sake.
-             */
-
-            // Add reactivity for it
-            this.reactor.addReactivity(comment, options.pathPrefix + array, "foreach", {
-                var: variable,
-                type: type,
-                innerHTML: html,
-                initialValue: this.getLiteralValue(array),
-                nodeOptions: options
-            });
-
-            // Skip children preparation
-            return NodePreparationResult.SKIP_CHILD;
-        } else
-        // Check if it's an if
-        if (element.tagName === "P:IF") {
-            const condition = element.getAttribute("condition").match(/\@p\:conditional\((.+?)\)/)[1];
-            const then = element.querySelector("p\\:then")?.innerHTML;
-            const otherwise = element.querySelector("p\\:else")?.innerHTML;
-
-            const comment = document.createComment(" ");
-            element.replaceWith(comment);
-
-            const regex = /\(?(?<var>[\w\."'()]+)(\s*[=!]\s*)?\)?/g;
-
-            let variables: string[] = [];
-            let variable;
-
-            while(variable = regex.exec(condition)) {
-                variables.push(variable[1]);
-            }
-
-            variables.forEach((variable) => {
-                this.reactor.addReactivity(comment, options.pathPrefix + variable, "if", {
-                    condition, then, otherwise
-                });
-            });
-        } else
-        // Check if it's an import
-        if (element.tagName === "P:IMPORT") {
-            const template = element.getAttribute("template");
-            const data = element.getAttribute("data");
-            const methods = element.getAttribute("methods");
-
-            // Get the compiled template
-            const compiledTemplate: CompiledTemplate = this.template.imports?.[template];
-
-            // If the template doesn't exists
-            if (compiledTemplate === undefined) {
-                throw new Error("Tried to import an unknown template named " + template)
-            }
-
-            const contextualizedHelpers = { ...this.getHelpers(), ...options.context };
-
-            const compiledData = data ? this.scopedEval(data, contextualizedHelpers) : this.data;
-            const compiledMethods = methods ? this.scopedEval(methods, contextualizedHelpers) : this.methods;
-
-            console.log(data, compiledData);
-
-            // Create the renderer for this template
-            const renderer = new Renderer(compiledTemplate, {
-                data: compiledData,
-                methods: compiledMethods
-            });
-
-            // Render the template and replace the element with it
-            element.replaceWith(...renderer.render().childNodes);
-        } else
-        // Check if it's an HTML element
-        if (element instanceof HTMLElement) {
-            // Iterate over all the attributes
-            element.getAttributeNames().forEach((attr) => {
-                // Check if it's a bind attribute
-                if (attr.startsWith("@p:bind:")) {
-                    this.reactor.bindEvent(
-                        element,
-                        attr.replace("@p:bind:", ""),
-                        element.getAttribute(attr)
-                    );
-
-                    element.removeAttribute(attr);
-
-                    return;
-                }
-
-                // Check if it doesn't start with our identifier
-                if (element.getAttribute(attr).indexOf("@p:") === -1) {
-                    return;
-                }
-
-                // Parse the attribute
-                const value = element.getAttribute(attr);
-
-                // Parse it
-                const parsed = this.parseCommand(value, options);
-
-                if (!!parsed) {
-                    element.removeAttribute(attr);
-                }
-
-                // Set it as reactive
-                this.reactor.addReactivity(element, options.pathPrefix + parsed.property, "attribute", {
-                    key: attr,
-                    command: parsed.command,
-                    initialValue: parsed.value,
-                    nodeOptions: options
-                });
-            });
-        }
-
-        return NodePreparationResult.SUCCESS;
-    }    
-}

+ 0 - 20
src/core/compiler/lexer/tokens/Bind.ts

@@ -1,20 +0,0 @@
-import { PugToken } from "../../Plugin";
-import Token from "../Token";
-
-export default class Bind extends Token {
-    public lex(tokens: PugToken[]) {
-        return tokens.map((token, index) => {
-            // We want only attribute tokens
-            if (token.type !== "attribute") {
-                return token;
-            }
-
-            // Check if it's binding an event
-            if (token.mustEscape && token.name.startsWith("@")) {
-                token.name = "@p:bind:" + token.name.substring(1);
-            }
-
-            return token;
-        });
-    }
-};

+ 0 - 63
src/core/compiler/lexer/tokens/ForEach.ts

@@ -1,63 +0,0 @@
-import { PugNode } from "../../Plugin";
-import Token from "../Token";
-
-export default class ForEach extends Token {
-    private static readonly FOREACH_CONDITION = /for(each)? (?<variable>.+?) (?<type>in|of) (?<arr>.+)$/;
-
-    public parse(nodes: PugNode[]) {
-        for(let index = 0; index < nodes.length; index++) {
-            const node = nodes[index];
-
-            // Check if it's a foreach
-            if (node.type === "Tag" && node.name === "foreach") {
-                let condition: RegExpMatchArray;
-
-                // Check if the next node is a text node
-                if (node.block?.nodes[0].type === "Text") {
-                    condition = ("foreach " + node.block?.nodes[0].val).match(ForEach.FOREACH_CONDITION);
-
-                    // Remove the text from the nodes
-                    node.block.nodes.splice(0, 1);
-                } else {
-                    condition = ("foreach " + node.attrs.map((attr) => attr.name).join(" ")).match(ForEach.FOREACH_CONDITION);
-                }
-
-                // Check if it's an invalid foreach condition
-                if (!condition) {
-                    throw new TypeError("Invalid foreach condition. It needs to have a variable (array) and a condition.");
-                }
-
-                const { variable, type, arr } = condition.groups;
-
-                // Set the tag name
-                node.name = "p:foreach";
-
-                // Setup the attributes for the foreach
-                node.attrs = [
-                    {
-                        name: "var",
-                        val: `"${variable}"`,
-                        mustEscape: false
-                    },
-                    {
-                        name: "type",
-                        val: `"${type}"`,
-                        mustEscape: false
-                    },
-                    {
-                        name: "array",
-                        val: `"${arr}"`,
-                        mustEscape: false
-                    }
-                ];
-            }
-
-            // Parses the block
-            if (node.block) {
-                node.block.nodes = this.parse(node.block.nodes);
-            }
-        }
-
-        return nodes;
-    }
-};

+ 0 - 73
src/core/renderer/Reactive.ts

@@ -1,73 +0,0 @@
-import type { Renderer } from "../../pupper";
-import type { NodeOptions } from "../Renderer";
-import type Reactor from "./Reactor";
-
-export namespace Reactive {
-    type K = keyof HTMLElementEventMap;
-
-    export type HTMLEventCallback = (this: HTMLElement, ev: HTMLElementEventMap[K]) => any;
-
-    export type ReactiveData = Record<string, any>;
-    export type ReactiveTarget = "text" | "html" | "attribute" | "foreach" | "if";
-    export type ReactiveCommand = "escape" | "literal" | null;
-    export type ReactiveMethods = Record<string, HTMLEventCallback>;
-
-    export type Context = (ProxyHandler<Reactive.ReactiveData> | Record<any, any>);
-
-    export interface ReactiveNodeOptions extends Record<string, any> {
-        /**
-         * The reactive command for this item
-         */
-        command?: Reactive.ReactiveCommand,
-
-        /**
-         * The initial item value
-         */
-        initialValue?: any,
-        
-        /**
-         * Any node options to be passed to the reactive node
-         */
-        nodeOptions?: NodeOptions
-    }
-
-    export class AbstractReactor {
-        public static readonly Type: string;
-
-        protected readonly reactor: Reactor;
-
-        constructor(
-            public readonly renderer: Renderer,
-            public readonly path: string,
-            public readonly element: HTMLElement | Element | Node,
-            public readonly options: Reactive.ReactiveNodeOptions,
-        ) {
-            this.reactor = renderer.reactor;
-        }
-
-        /**
-         * Retrieves the reactor path
-         * @returns 
-         */
-        public getPath(): string | RegExp {
-            return this.path;
-        }
-
-        /**
-         * Tests if the path can be handled by the reactor
-         * @param path The path to be tested
-         */
-        public test(path: string): boolean {
-            return this.path === path;
-        }
-
-        /**
-         * Handles a new reacted value
-         * @param path The path to be handled
-         * @param newValue The value to be handled
-         */
-        public handle(path: string, newValue?: any): any {
-            throw new Error("Abstract class not implemented.");
-        }
-    }
-}

+ 0 - 151
src/core/renderer/Reactor.ts

@@ -1,151 +0,0 @@
-import type { Renderer } from "../Renderer";
-import { ObservableChange } from "observable-slim";
-import { Reactive } from "./Reactive";
-
-import ForEachReactor from "./reactors/ForEach";
-import HTMLReactor from "./reactors/HTML";
-import HTMLAttributeReactor from "./reactors/HTMLAttribute";
-import TextReactor from "./reactors/Text";
-import IfReactor from "./reactors/If";
-
-const debug = require("debug")("pupperjs:renderer:reactor");
-
-export default class Reactor {
-    // @ts-ignore
-    private static readonly Reactors: Record<string, typeof Reactive.AbstractReactor> = {
-        foreach: ForEachReactor,
-        html: HTMLReactor,
-        attribute: HTMLAttributeReactor,
-        text: TextReactor,
-        if: IfReactor
-    };
-
-    /**
-     * The renderer related to this reactor
-     */
-    private renderer: Renderer;
-
-    /**
-     * A list of reactive properties with their respective elements
-     */
-    private reactive: Reactive.AbstractReactor[] = [];
-
-    constructor(renderer: Renderer) {
-        this.renderer = renderer;
-    }
-
-    /**
-     * When a data property has changed
-     * @param changes The observed changes
-     */
-    public onPropertyChange(changes: ObservableChange[]) {
-        changes.forEach((change) => this.triggerChangeFor(change.currentPath, change.newValue));
-    }
-
-    /**
-     * Checks if has reactivity for the given path
-     * @param path The dot notation path to be checked
-     * @returns 
-     */
-    public hasReactivityFor(path: string) {
-        return this.reactive.some((reactive) => reactive.path === path);
-    }
-
-    /**
-     * Retrieves all registered reactors
-     * @returns 
-     */
-    public getReactors() {
-        return this.reactive;
-    }
-
-    /**
-     * Retrieves all registered reactors as an object that
-     * the object index is the reactor path
-     * @returns 
-     */
-    public getPathIndexedReactors() {
-        const reactors: Record<string, Reactive.AbstractReactor[]> = {};
-
-        this.reactive.forEach((reactor) => {
-            reactors[reactor.path] = reactors[reactor.path] || [];
-            reactors[reactor.path].push(reactor);
-        });
-
-        return reactors;
-    }
-
-    /**
-     * Triggers reactivity changes for an object path
-     * @param path The object dot notation path
-     * @param newValue The new value that the object received
-     */
-    public triggerChangeFor(path: string, newValue?: any) {
-        if (newValue === undefined) {
-            return false;
-        }
-
-        const result = this.reactive.some((reactive) => {
-            if (reactive.test(path)) {
-                debug("%s handled by %s", path, reactive.constructor.name);
-
-                reactive.handle(path, newValue);
-
-                return true;
-            }
-
-            return false;
-        });
-
-        if (!result) {
-            debug("%s failed to be handled", path);
-        }
-    }
-
-    /**
-     * Turns an element into a reactive element that will listen for object changes.
-     * @param element The element that will be reactive
-     * @param property The element property that will changed
-     * @param command The reactivity command, optional
-     * @param target The reactivity target
-     * @param options Any options to be passed to the reactivity renderer
-     */
-    public addReactivity(element: HTMLElement | Element | Node, property: string, target?: Reactive.ReactiveTarget, options: Reactive.ReactiveNodeOptions = {}) {
-        const reactor = Reactor.Reactors[target];
-
-        const instance = new reactor(
-            this.renderer,
-            property,
-            element,
-            options
-        );
-
-        // Add it to the default property
-        this.reactive.push(instance);
-
-        // Check if has an initial value
-        if (options.initialValue !== undefined) {
-            this.triggerChangeFor(property, options.initialValue);
-        }
-    }
-
-    /**
-     * Binds an event to an element
-     * @param element The element that will receive the event
-     * @param event The event name
-     * @param method The event method name
-     * @returns 
-     */
-    public bindEvent(element: HTMLElement | Element | Node, event: string, method: string) {
-        const callback = this.renderer.methods[method];
-
-        if (callback === undefined) {
-            debug("method %s was not found when binding %s for %O", method, event, element);
-            return false;
-        }
-
-        element.addEventListener(event, callback);
-
-        return true;
-    }
-}

+ 0 - 102
src/core/renderer/reactors/ForEach.ts

@@ -1,102 +0,0 @@
-import { Reactive } from "../Reactive";
-
-const debug = require("debug")("pupperjs:renderer:reactors:foreach");
-
-export default class ForEachReactor extends Reactive.AbstractReactor {
-    public static readonly Type: "foreach";
-
-    private regex: RegExp = null;
-
-    private reacted: Record<string | number, Node> = {};
-
-    public getPath() {
-        if (this.regex === null) {
-            this.regex = new RegExp(`^(?<literal>${this.path.replace(/\./g, "\\.")})\.(?<index>.+?)\.(?<value>\.+)$`);
-        }
-
-        return this.regex;
-    }
-
-    public test(path: string): boolean {
-        return !path.includes("." + this.options.var + ".") && (this.path === path || this.getPath().exec(path) !== null);
-    }
-
-    private handleAll(path: string, newValue: any) {
-        let target: any[] | object = newValue;
-        let indexes: number[] | string[];
-
-        debug("new for...%s loop for \"%s\" (%O)", this.options.type, path, target);
-
-        // Check if it's a for...in
-        if (this.options.type === "in") {
-            // Check if the target is an array
-            if (Array.isArray(target)) {
-                console.warn("Tried to iterate using for...in in a non-object variable", path);
-                return false;
-            }
-
-            indexes = Object.keys(target);
-        } else {
-            indexes = Array.from(
-                Array((target as any[]).length).keys()
-            );
-        }
-
-        // Iterate over all changed array targets
-        for(let index of indexes) {
-            // @ts-ignore
-            this.addSingle(index, target[index]);
-        }
-    }
-
-    private addSingle(index: number|string, value: any) {
-        const content = document.createElement("template");
-        content.innerHTML = this.options.innerHTML;
-
-        // Prepare the context
-        const context = {
-            [this.options.var]: value
-        };
-
-        // Prepare the nodes
-        this.renderer.prepareNodes(content.content.childNodes, {
-            pathPrefix: this.path + "." + index + ".",
-            context
-        });
-
-        debug("\tparsed children %s", this.path + "." + index);
-
-        this.reacted[index] = content.content;
-
-        // Append it to the parent element
-        (this.element as Comment).parentElement.appendChild(content.content);
-    }
-
-    private handleSingle(path: string, newValue: any) {
-        const { groups } = path.match(this.regex);
-
-        // Check if it's array.length (pushing a new object into the array)
-        if (path === this.path + ".length") {
-            path = String(newValue - 1);
-
-            this.addSingle(path, this.renderer.getLiteralValue(this.path + "." + path));
-
-            return true;
-        }
-
-        const realPath = groups.literal + "." + groups.index + (groups.value ?  "." + this.options.var + "." + groups.value : "");
-
-        debug("path %s was converted to %s", path, realPath);
-
-        return this.reactor.triggerChangeFor(realPath, newValue);
-    }
-
-    public handle(path: string, newValue: any) {
-        // Check if it's the root array value
-        if (this.path === path) {
-            this.handleAll(path, newValue);
-        } else {
-            this.handleSingle(path, newValue);
-        }
-    }
-}

+ 0 - 9
src/core/renderer/reactors/HTML.ts

@@ -1,9 +0,0 @@
-import { Reactive } from "../Reactive";
-
-export default class HTMLReactor extends Reactive.AbstractReactor {
-    public static readonly Type: "html";
-
-    public handle(path: string, newValue: any) {
-        (this.element as HTMLElement).innerHTML = newValue;
-    }
-}

+ 0 - 9
src/core/renderer/reactors/HTMLAttribute.ts

@@ -1,9 +0,0 @@
-import { Reactive } from "../Reactive";
-
-export default class HTMLAttributeReactor extends Reactive.AbstractReactor {
-    public static readonly Type: "attribute";
-
-    public handle(path: string, newValue: any) {
-        (this.element as HTMLElement).setAttribute(this.options.key, newValue);
-    }
-}

+ 0 - 19
src/core/renderer/reactors/If.ts

@@ -1,19 +0,0 @@
-import { Reactive } from "../Reactive";
-
-const debug = require("debug")("pupperjs:renderer:reactors:if");
-
-export default class IfReactor extends Reactive.AbstractReactor {
-    public static readonly Type: "if";
-
-    public test(path: string) {
-        return super.test(path);
-    }
-
-    public handle(path: string, newValue: any) {
-        if (newValue) {
-            
-        }
-
-        console.log(path, newValue);
-    }
-}

+ 0 - 9
src/core/renderer/reactors/Text.ts

@@ -1,9 +0,0 @@
-import { Reactive } from "../Reactive";
-
-export default class TextReactor extends Reactive.AbstractReactor {
-    public static readonly Type: "text";
-
-    public handle(path: string, newValue: any) {
-        this.element.textContent = newValue;
-    }
-}

+ 0 - 38
src/index.ts

@@ -1,38 +0,0 @@
-import { Compiler } from "./core/Compiler";
-import PupperCompiler from "./core/Compiler";
-import { Renderer } from "./core/Renderer";
-
-class PupperStatic {
-    static readonly Compiler = PupperCompiler;
-    static readonly Renderer = Renderer;
-    static readonly Pupper = import("./pupper");
-
-    /**
-     * Compiles a string
-     * @param file The string to be compiled
-     * @returns 
-     */
-    static compileSync(str: string, options?: Compiler.Options) {
-        return new PupperCompiler().compile(str, options);
-    }
-
-    /**
-     * Compiles a string
-     * @param file The string to be compiled
-     * @returns 
-     */
-    static compileToStringSync(str: string, options?: Compiler.Options) {
-        return new PupperCompiler().compileToString(str, options);
-    }
-
-    /**
-     * Compiles a single file
-     * @param file The file to be compiled
-     * @returns 
-     */
-    static compileFileSync(file: string, options?: Compiler.Options) {
-        return new PupperCompiler().compileFile(file, options);
-    }
-}
-
-export = PupperStatic;

+ 0 - 7
src/pupper.ts

@@ -1,7 +0,0 @@
-import Compiler from "./core/Compiler";
-import { Renderer } from "./core/Renderer";
-
-export {
-    Compiler,
-    Renderer
-}

+ 0 - 3
src/types/deep-get-set.d.ts

@@ -1,3 +0,0 @@
-declare module "deep-get-set" {
-    export default function(object: object, key: any, value?: any);
-}

+ 0 - 24
src/types/observable-slim.d.ts

@@ -1,24 +0,0 @@
-/**
- * Because observable-slim is not documented
- */
-declare module "observable-slim" {
-    declare type ObservableChange = {
-        type: "add" | "delete" | "update",
-        target: any[] | Record<any, any>,
-        property: string,
-        newValue: any,
-        currentPath: string,
-        jsonPointer: string,
-        proxy: Record<any, any>
-    };
-
-    /**
-     * 
-     * @param target Plain javascript object that we want to observe for changes
-     * @param domDelay If true, then observable slim will batch up observed changes to `target` on a 10ms delay (via setTimeout).
-     *                 If false, then `observer` will be immediately invoked after each individual change made to `target`. It is helpful to
-     *                 set `domDelay` to `true` when your `observer` function makes DOM manipulations.
-     * @param observer Will be invoked when a change is made to the proxy of `target`.
-     */
-    export function create(target: Record<any, any>, domDelay: boolean, observer: (changes: ObservableChange[]) => any | void);
-}

+ 0 - 6
src/types/pupper.d.ts

@@ -1,6 +0,0 @@
-/**
- * Used to represent whats is a pupper module
- */
-declare module "*.pupper" {
-    export default function(data: object): string;
-}

+ 15 - 8
test/browser.js → test/index.js

@@ -1,10 +1,14 @@
-const { Component } = require("../out/core/Component");
+import Template from "./templates/template.pupper";
+import { defineComponent } from "@pupperjs/renderer";
 
-const pupper = new Component({
-    template: require("./templates/template.pupper"),
+import ImportedComponent from "./templates/ImportedComponent.pupper";
+import ExportedComponent from "./templates/ExportedComponent.pupper";
+
+const pupper = defineComponent({
+    render: Template,
     methods: {
-        onClickPuppy(e) {
-            alert("You clicked a puppy! :D");
+        onClickPuppy(puppy) {
+            alert("You clicked puppy " + puppy.id + "! :D");
         }
     },
     data: {
@@ -31,12 +35,15 @@ const pupper = new Component({
 });
 
 window.component = pupper;
-pupper.renderTo(document.getElementById("app"));
+pupper.mount(document.getElementById("app"));
 
-pupper.data.puppies.push({
+pupper.puppies.push({
     id: 3,
     title: "Wow, a shibe!",
     description: "Cute shiberino!!!",
     thumbnail: "https://media.istockphoto.com/photos/happy-shiba-inu-dog-on-yellow-redhaired-japanese-dog-smile-portrait-picture-id1197121742?k=20&m=1197121742&s=170667a&w=0&h=SDkUmO-JcBKWXl7qK2GifsYzVH19D7e6DAjNpAGJP2M=",
     shibe: true
-});
+});
+
+ExportedComponent.mount(pupper.$slots.slot);
+ImportedComponent.mount(pupper.$slots.slot2);

+ 0 - 14
test/node.js

@@ -1,14 +0,0 @@
-const pupper = require("../");
-const fs = require("fs");
-const beautify = require("js-beautify");
-
-const result = beautify(
-    pupper.compileToStringSync(fs.readFileSync(__dirname + "/templates/template.pupper"), {
-        debug: true,
-        pug: {
-            filename: __dirname + "/templates/template.pupper"
-        }
-    })
-);
-
-fs.writeFileSync(__dirname + "/out/node.js", result);

+ 15 - 0
test/templates/ExportedComponent.pupper

@@ -0,0 +1,15 @@
+component(export)
+    template
+        |This component was exported and loaded by another component!
+
+        div
+            a.text-primary(href="#", @click="alert('Hello world!')")="Testing a link with an event"
+
+        hr
+
+    script(lang="ts", type="text/javascript").
+        import Pupper from "@pupperjs/renderer";
+
+        export default Pupper.defineComponent({
+
+        });

+ 21 - 0
test/templates/ImportedComponent.pupper

@@ -0,0 +1,21 @@
+import(ExportedComponent from "./ExportedComponent.pupper")
+
+component(export)
+    template
+        div
+            |This component has an imported component!
+            ExportedComponent()
+
+        div
+            ="Also mounted into a slot:"
+            slot(name="slot")
+
+    script(lang="ts", type="text/javascript").
+        import Pupper from "@pupperjs/renderer";
+
+        export default Pupper.defineComponent({
+            mounted() {
+                console.log("Mounted, rendering", ExportedComponent);
+                ExportedComponent.mount(this.$slots.slot);
+            }
+        });

+ 0 - 4
test/templates/foreach.pupper

@@ -1,4 +0,0 @@
-- const items = [1, 2, 3, 4];
-
-foreach(item in items)
-    span=item

+ 0 - 13
test/templates/puppy.pupper

@@ -1,13 +0,0 @@
-.col-5.mb-5
-    .puppy.card.px-0(data-id={{ puppy.id }}, :click="onClickPuppy").text-dark
-        img.card-img-top(src={- puppy.thumbnail -}, crossorigin="auto")
-
-        .card-header
-            h5.card-title={{ puppy.title }}
-            small.text-muted|Served by pupper.js
-
-        .card-body
-            ={- puppy.description -}
-
-            if (puppy.shibe)
-                p.text-warning|shibe!!!

+ 24 - 11
test/templates/template.pupper

@@ -2,8 +2,6 @@
 link(href="https://getbootstrap.com/docs/4.0/dist/css/bootstrap.min.css", rel="stylesheet")
 link(href="https://getbootstrap.com/docs/4.0/examples/cover/cover.css", rel="stylesheet")
 
-import(Puppy from "./puppy.pupper")
-
 .text-center
     .cover-container.d-flex.h-100.p-3.mx-auto.flex-column
         header.masthead.mb-auto 
@@ -17,15 +15,30 @@ import(Puppy from "./puppy.pupper")
             p.lead={{ page.lead }}
 
             .row.mt-5.justify-content-around.align-items-center
-                //- Reactive puppies
-                foreach(puppy of puppies)
-                    //- Render the puppy and share the onClickPuppy method with it
-                    Puppy(
-                        data={ puppy },
-                        methods={
-                            onClickPuppy: $methods.onClickPuppy
-                        }
-                    )
+                if !puppies
+                    |Oh noe! No puppies to show :(
+                else
+                    //- Render the puppies and share the onClickPuppy method with it
+                    each puppy in puppies
+                        .col-5.mb-5
+                                .puppy.card.px-0(:data-id="puppy.id", @click="onClickPuppy(puppy)").text-dark
+                                    img.card-img-top(:src="puppy.thumbnail", crossorigin="auto")
+
+                                    .card-header
+                                        h5.card-title={{ puppy.title }}
+                                        small.text-muted|Served by pupper.js
+
+                                    .card-body
+                                        ={- puppy.description -}
+
+                                        if puppy.shibe
+                                            p.text-warning|shibe!!!
+
+            div 
+                |Testing slots: 
+
+                slot(name="slot")
+                slot(name="slot2")
 
         footer.mastfoot.mt-auto
             .inner

+ 4 - 3
tsconfig.json

@@ -1,6 +1,6 @@
 {
     "compilerOptions": {
-        "target": "es2015",
+        "target": "esnext",
         "allowJs": true,
         "esModuleInterop": true,
         "outDir": "out",
@@ -12,8 +12,9 @@
         "emitDecoratorMetadata": true,
         "moduleResolution": "node",
         "module": "commonjs",
-        "skipLibCheck": true
+        "skipLibCheck": true,
+        "typeRoots": ["./packages/common/types"]
     },
-    "include": ["src/**/*.ts"],
+    "include": ["./**/*.ts"],
     "exclude": ["node_modules"],
 }

+ 9 - 2
webpack.config.js

@@ -1,17 +1,19 @@
 module.exports = {
-    entry: __dirname + "/test/browser.js",
+    entry: __dirname + "/test/index.js",
     output: {
         path: __dirname + "/test/out",
         filename: "index.js",
         publicPath: "./"
     },
+    cache: false,
     watch: true,
     mode: "development",
+    devtool: "source-map",
     module: {
         rules: [
             {
                 test: /\.pupper$/,
-                use: [__dirname + "/modules/webpack"]
+                use: ["@pupperjs/webpack-loader"]
             },
             {
                 test: /\.js$/,
@@ -20,5 +22,10 @@ module.exports = {
             },
         ]
     },
+    resolve: {
+        alias: {
+            "pupper.js": __dirname + "/out/"
+        }
+    },
     mode: "development"
 }