소스 검색

reformulated component declaration
added data and methods tags

Matheus Giovani 2 년 전
부모
커밋
dd48e19f34

+ 7 - 1
nodemon.json

@@ -1,3 +1,9 @@
 {
-    "ignore": ["./**/src/**/*", "./test/out/**/*"]
+    "$schema": "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/nodemon.json",
+    "ignore": ["./**/src/**/*", "./test/out/**/*"],
+    "ext": "js",
+    "exec": "webpack",
+    "env": {
+        "NODE_OPTIONS": "-r source-map-support/register"
+    }
 }

+ 3 - 2
package.json

@@ -12,10 +12,10 @@
     "./packages/webpack-loader"
   ],
   "scripts": {
-    "watch": "npm-run-all -p -r watch:*",
+    "watch": "npm-run-all -p watch:*",
     "watch:compiler": "cd packages/compiler && yarn watch",
     "watch:renderer": "cd packages/renderer && yarn watch",
-    "watch:test": "nodemon --exec \"webpack\""
+    "watch:test": "nodemon"
   },
   "dependencies": {
     "alpinejs": "^3.10.2",
@@ -24,6 +24,7 @@
   "devDependencies": {
     "npm-run-all": "^4.1.5",
     "source-map-loader": "^3.0.0",
+    "source-map-support": "^0.5.21",
     "webpack": "^5.51.1",
     "webpack-cli": "^4.8.0"
   }

+ 204 - 0
packages/compiler/src/core/Compiler.ts

@@ -0,0 +1,204 @@
+import { Pug } from "../typings/pug";
+import pug from "pug";
+import Plugin, { PugAST } from "./Plugin";
+
+import pugError from "pug-error";
+import lex from "pug-lexer";
+import parse from "pug-parser";
+import link from "pug-linker";
+import codeGen from "pug-code-gen";
+
+export enum CompilationType {
+    TEMPLATE
+}
+
+export 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;
+
+    /**
+     * The compilation file name.
+     */
+    fileName: string;
+
+    /**
+     * Any configurations to be passed to pug.
+     * @internal Not meant to be used externally.
+     */
+    pug?: pug.Options;
+}
+
+export class PupperCompiler {
+    /**
+     * The contents of the current template being rendered.
+     */
+    public contents: string;
+
+    public plugin = new Plugin(this);
+
+    public compilationType: CompilationType;
+
+    constructor(
+        /**
+         * Any options to be passed to the compiler.
+         */
+        public options: ICompilerOptions
+    ) {
+        
+    }
+
+    /**
+     * Makes a compilation error.
+     * @param code The error code.
+     * @param message The error message.
+     * @param data The error data.
+     * @returns 
+     */
+    public makeError(code: string, message: string, data: {
+        line?: number;
+        column?: number;
+    } = {}) {
+        return pugError(code, message, {
+            ...data,
+            filename: this.getFileName(),
+            src: this.contents
+        } as any);
+    }
+
+    /**
+     * Makes an error with "PARSE_ERROR" code.
+     * @param message The error message.
+     * @param data The error data.
+     * @returns 
+     */
+    public makeParseError(message: string, data: {
+        line?: number;
+        column?: number;
+    } = {}) {
+        return this.makeError("PARSE_ERROR", message, data);
+    }
+
+    /**
+     * Makes an error with "LEX_ERROR" code.
+     * @param message The error message.
+     * @param data The error data.
+     * @returns 
+     */
+    public makeLexError(message: string, data: {
+        line?: number;
+        column?: number;
+    } = {}) {
+        return this.makeError("LEX_ERROR", "Lexer error: " + message, data);
+    }
+
+    protected lexAndParseString(template: string) {
+        let carrier: any;
+
+        const options = this.makePugOptions();
+
+        this.plugin.prepareHooks();
+
+        try {
+            this.contents = this.plugin.preLex(template);
+
+            carrier = lex(this.contents, {
+                ...options,
+                plugins: [this.plugin.lex as any as lex.LexerFunction]
+            });
+
+            carrier = this.plugin.preParse(carrier);
+        } catch(e) {
+            throw this.makeLexError(e.message, e);
+        }
+
+        try {
+            carrier = parse(carrier, this.makePugOptions() as any);
+            carrier = link(carrier);
+
+            carrier = this.plugin.postParse(carrier);
+        } catch(e) {
+            throw e instanceof pugError ? e : this.makeParseError(e.message, e);
+        }
+
+        return carrier as PugAST;
+    }
+
+    protected generateJavaScript(ast: Pug.PugAST): string {
+        ast = this.plugin.preCodeGen(ast);
+
+        let code = codeGen(ast, this.makePugOptions());
+
+        code = this.plugin.postCodeGen(code);
+
+        return code;
+    }
+
+    /**
+     * Compiles a pupper component into a JavaScript component.
+     * @param template The template to be compiled.
+     * @returns 
+     */
+    public compileComponent(template: string): string {
+        this.contents = template;
+
+        const ast = this.lexAndParseString(template);
+        let rendered = this.generateJavaScript(ast);
+
+        return rendered;
+    }
+
+    /**
+     * Compiles a pupper template tag into HTML.
+     * @param template The template to be compiled.
+     * @returns 
+     */
+    public compileTemplate(template: string): string {
+        const pugOptions = this.makePugOptions();
+        this.contents = template;
+
+        this.compilationType = CompilationType.TEMPLATE;
+
+        this.plugin.prepareHooks();
+
+        const fn = pug.compile(template, pugOptions);
+        const rendered = fn();
+
+        return rendered;///*js*/`function $h(h) { return ${htmlToHs({ syntax: "h" })(rendered)}; }`;
+    }
+
+    public getFileName() {
+        return this.options.fileName || this.options.pug.filename || "template.pupper";
+    }
+
+    /**
+     * Sets the internal compiler file name.
+     * @param fileName The new file name.
+     * @returns 
+     */
+    public setFileName(fileName: string) {
+        this.options.fileName = fileName;
+        return this;
+    }
+
+    /**
+     * Make the options for the pug compiler.
+     */
+    protected makePugOptions() {
+        const pugOptions: pug.Options & { filename: string } = {
+            // We use "$render" as the internal function name.
+            name: "$render",
+            filename: this.getFileName(),
+            compileDebug: this.options.debug || false,
+            // Always use self to prevent conflicts with other compilers.
+            self: true,
+            plugins: []
+        };
+
+        pugOptions.plugins.push(this.plugin);
+
+        return pugOptions;
+    }
+}

+ 64 - 54
packages/compiler/src/core/Plugin.ts

@@ -1,12 +1,8 @@
-import type PugLexer from "pug-lexer";
-import type { PugPlugin, PugToken, PugAST, PugNode, PugNodes, PugNodeAttribute, LexerPlugin, Options } from "pug";
-import type PupperCompiler from "..";
+import type { PugPlugin, PugToken, PugAST, PugNode, PugNodes, PugNodeAttribute, LexerPlugin } from "pug";
 
 import { Hook } from "./plugin/Hook";
 
 import { ConditionalHook } from "./plugin/hooks/ConditionalHook";
-import { ForEachHook } from "./plugin/hooks/ForEachHook";
-import { ComponentHook } from "./plugin/hooks/ComponentHook";
 import { PropertyHook } from "./plugin/hooks/PropertyHook";
 import { PupperToAlpineHook } from "./plugin/hooks/PupperToAlpineHook";
 import { ImportHook } from "./plugin/hooks/ImportHook";
@@ -19,11 +15,14 @@ import { TagNode } from "./plugin/nodes/TagNode";
 import { NodeModel } from "../model/core/NodeModel";
 import { MixinNode } from "./plugin/nodes/MixinNode";
 import { ConditionalNode } from "./plugin/nodes/ConditionalNode";
-import pugError from "pug-error";
 import { Pug } from "../typings/pug";
 import { TemplateTagNode } from "./plugin/nodes/tags/TemplateTagNode";
+import { PrepareComponents } from "./plugin/phases/PrepareComponentsHook";
+import { CompilationType, PupperCompiler } from "./Compiler";
+import lex from "pug-lexer";
 
-type THookArray = { new(plugin: Plugin): Hook }[];
+type THookConstructor = { new(plugin: Plugin): Hook };
+type THookArray = THookConstructor[];
 
 export type TPugNodesWithTypes = {
     [key in PugNodes["type"]]: Extract<PugNodes, { type: key }>
@@ -65,14 +64,20 @@ export { PugToken, PugAST, PugNode, PugNodeAttribute, PugNodes, CompilerNode as
 export default class Plugin implements PugPlugin {
     public static Hooks: THookArray = [
         ConditionalHook,
-        //ForEachHook,
-        ComponentHook,
         PropertyHook,
         PupperToAlpineHook,
         ImportHook,
         StyleAndScriptHook
     ];
 
+    /**
+     * All phases to be executed.
+     * Phases are executed before hooks.
+     */
+    public static Phases: THookArray = [
+        PrepareComponents
+    ];
+
     /**
      * Creates a compiler node from a pug node.
      * @param node The pug node.
@@ -122,7 +127,7 @@ export default class Plugin implements PugPlugin {
     /**
      * A handler for the plugin filters.
      */
-    private filters: Record<string, Function[]> = {};
+    private filters: Record<string, { callback: Function, hook: THookArray[0] }[]> = {};
 
     /**
      * Any data to be shared between hooks and phases.
@@ -132,26 +137,34 @@ export default class Plugin implements PugPlugin {
     public lex: LexerPlugin;
 
     constructor(
-        public compiler: PupperCompiler,
-        public options: Options & {
-            contents?: string
-        }
+        public compiler: PupperCompiler
     ) {
-        this.prepareHooks();
-
         // Create the lexer
         this.lex = {
-            isExpression: (lexer: PugLexer.Lexer, exp: string) => 
+            isExpression: (lexer: lex.Lexer, exp: string) => 
                 this.applyFilters<string, boolean>("testExpression", exp)
         };
     }
 
+    public get options() {
+        return this.compiler.options;
+    }
+
     /**
      * Prepares a list of ordered hooks.
      */
-    protected prepareHooks() {
+    public prepareHooks() {
         const hookOrder: string[] = [];
 
+        if (this.compiler.compilationType !== CompilationType.TEMPLATE) {
+            Plugin.Phases
+                .map((Phase) => new Phase(this))
+                .forEach((phase) => {
+                    phase.prepareFilters();
+                    hookOrder.push(phase.constructor.name);
+                });
+        }
+
         Plugin.Hooks
             // Create the hooks instances
             .map((Hook) => new Hook(this))
@@ -202,15 +215,26 @@ export default class Plugin implements PugPlugin {
      * @param callback The filter callback.
      * @returns 
      */
-    public addFilter(filter: string, callback: Function) {
+    public addFilter(filter: string, callback: Function, hook: THookConstructor) {
         if (this.filters[filter] === undefined) {
             this.filters[filter] = [];
         }
 
-        return this.filters[filter].push(callback);
+        return this.filters[filter].push({
+            callback,
+            hook
+        });
     }
 
-    public applyFilters<TValue, TResultingValue = TValue>(filter: string, value: TValue): TResultingValue {
+    /**
+     * Applies all hooks filters for a given value.
+     * @param filter The filter name to be applied. 
+     * @param value The filter initial value.
+     * @returns 
+     */
+    public applyFilters<TValue, TResultingValue = TValue>(filter: string, value: TValue, options?: {
+        skip: THookArray
+    }): TResultingValue {
         // If has no filters, return the initial value
         if (this.filters[filter] === undefined) {
             return value as any as TResultingValue;
@@ -218,7 +242,12 @@ export default class Plugin implements PugPlugin {
 
         try {
             for(let callback of this.filters[filter]) {
-                value = callback(value);
+                // @ts-ignore
+                if (options?.skip?.some((sk) => callback.hook instanceof sk)) {
+                    continue;
+                }
+
+                value = callback.callback(value);
             }
         } catch(e) {
             console.error(e);
@@ -233,9 +262,13 @@ export default class Plugin implements PugPlugin {
      * @param node The node or node array to be parsed.
      * @returns 
      */
-    public parseChildren<TInput extends NodeModel | NodeModel[], TResult>(node: TInput) {
+    public parseChildren<TInput extends NodeModel | NodeModel[], TResult>(node: TInput, skipComponentCheck: boolean = false) {
+        let options = skipComponentCheck ? {
+            skip: [PrepareComponents]
+        } : undefined;
+
         if (Array.isArray(node)) {
-            this.applyFilters("parse", node);
+            this.applyFilters("parse", node, options);
 
             node.forEach((node) => {
                 this.parseChildren(node);
@@ -245,7 +278,7 @@ export default class Plugin implements PugPlugin {
         }
 
         node.setChildren(
-            this.applyFilters("parse", node.getChildren())
+            this.applyFilters("parse", node.getChildren(), options)
         );
 
         node.getChildren().forEach((child) => {
@@ -281,8 +314,8 @@ export default class Plugin implements PugPlugin {
      */
 
     public preLex(template: string) {
-        this.options.contents = this.applyFilters("preLex", template);
-        return this.options.contents;
+        this.compiler.contents = this.applyFilters("preLex", template);
+        return this.compiler.contents;
     }
 
     public preParse(tokens: PugToken[]) {
@@ -301,34 +334,11 @@ export default class Plugin implements PugPlugin {
         return this.applyFilters("postCodeGen", code);
     }
 
-    /**
-     * Makes a compilation error.
-     * @param code The error code.
-     * @param message The error message.
-     * @param data The error data.
-     * @returns 
-     */
-    public makeError(code: string, message: string, data: {
-        line?: number;
-        column?: number;
-    } = {}) {
-        return pugError(code, message, {
-            ...data,
-            filename: this.options.filename,
-            src: this.options.contents
-        } as any);
+    public get makeError() {
+        return this.compiler.makeError.bind(this.compiler);
     }
 
-    /**
-     * Makes an error with "COMPILATION_ERROR" code.
-     * @param message The error message.
-     * @param data The error data.
-     * @returns 
-     */
-    public makeParseError(message: string, data: {
-        line?: number;
-        column?: number;
-    } = {}) {
-        return this.makeError("COMPILATION_ERROR", message, data);
+    public get makeParseError() {
+        return this.compiler.makeParseError.bind(this.compiler);
     }
 }

+ 13 - 12
packages/compiler/src/core/plugin/Hook.ts

@@ -64,39 +64,40 @@ export abstract class Hook {
         
     }
 
+    public get compiler() {
+        return this.plugin.compiler;
+    }
+
     /**
      * Prepares this hook filters.
      */
     public prepareFilters() {
+        if ("testExpression" in this) {
+            this.plugin.addFilter("testExpression", this.testExpression.bind(this), this);
+        }
+
         if ("beforeStart" in this) {
-            this.plugin.addFilter("preLex", this.beforeStart.bind(this));
+            this.plugin.addFilter("preLex", this.beforeStart.bind(this), this);
         }
 
         if ("lex" in this) {
-            this.plugin.addFilter("lex", this.lex.bind(this));
+            this.plugin.addFilter("lex", this.lex.bind(this), this);
         }
 
         if ("parse" in this) {
-            this.plugin.addFilter("parse", this.parse.bind(this));
+            this.plugin.addFilter("parse", this.parse.bind(this), this);
         }
 
         if ("beforeCompile" in this) {
-            this.plugin.addFilter("preCodeGen", this.beforeCompile.bind(this));
+            this.plugin.addFilter("preCodeGen", this.beforeCompile.bind(this), this);
         }
 
         if ("afterCompile" in this) {
-            this.plugin.addFilter("postCodeGen", this.afterCompile.bind(this));
+            this.plugin.addFilter("postCodeGen", this.afterCompile.bind(this), this);
         }
     }
 
     protected makeNode(node: PugNodes, parent: CompilerNode) {
         return Plugin.createNode(node, parent);
     }
-
-    protected makeError(code: string, message: string, data: {
-        line?: number;
-        column?: number;
-    } = {}) {
-        return this.plugin.makeError(code, message, data);
-    }
 }

+ 0 - 146
packages/compiler/src/core/plugin/hooks/ComponentHook.ts

@@ -1,146 +0,0 @@
-import { IPluginNode } from "../../Plugin";
-import { Hook } from "../Hook";
-import { TagNode } from "../nodes/TagNode";
-import { ScriptParser } from "./component/ScriptParser";
-import { ConditionalHook } from "./ConditionalHook";
-import { StyleAndScriptHook } from "./StyleAndScriptHook";
-
-const DefaultExportSymbol = Symbol("ExportedComponent");
-
-export interface IComponent {
-    name: string | symbol;
-    template: string;
-    script?: string;
-    setupScript?: string;
-    style?: string;
-    exported?: boolean;
-}
-
-export class ComponentHook extends Hook {
-    public $after = [ConditionalHook, StyleAndScriptHook];
-
-    /**
-     * The imports that will later be putted into the template header
-     */
-    protected components: Record<string | symbol, IComponent> = {};
- 
-    public parseComponentNode(node: TagNode) {
-        const name = node.getAttribute("name")?.replace(/"/g, "");
-
-        const template = node.findFirstChildByTagName("template") as TagNode;
-        const script = node.findFirstChildByTagName("script") as TagNode;
-        const style = node.findFirstChildByTagName("style") as TagNode;
-
-        // If no script tag was found
-        if (!script) {
-            throw this.makeError("COMPONENT_HAS_NO_SCRIPT_TAG", "Components must have a a script tag.", {
-                line: node.getLine(),
-                column: node.getColumn()
-            });
-        }
-
-        /**
-         * Create the component
-         */
-        const component: IComponent = {
-            name,
-            template: null,
-            script: null,
-            style: null,
-            exported: node.hasAttribute("export")
-        };
-
-        // 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) {
-            this.plugin.parseChildren(template);
-            let lines = this.plugin.options.contents.split("\n");
-
-            const nextNodeAfterTemplate = template.getNextNode();
-
-            lines = lines.slice(
-                template.getLine(),
-                nextNodeAfterTemplate ? nextNodeAfterTemplate.getLine() - 1 : (node.hasNext() ? node.getNextNode().getLine() - 1 : lines.length)
-            );
-
-            // 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) {
-            this.plugin.parseChildren(script);
-
-            const scriptContent = script.getChildren().map((node) => node.getProp("val")).join("");
-            component.script = scriptContent;
-        }
-
-        // If has a style
-        if (style) {
-            console.log(style);
-        }
-
-        return component;
-    }
-
-    public parse(nodes: IPluginNode[]) {
-        for(let node of nodes) {
-            // Check if it's a tag "component" node
-            if (node.isType("Tag") && node.isName("component")) {
-                // Parse the component
-                const component = this.parseComponentNode(node as TagNode);
-
-                // Save the component
-                this.components[component.name] = component;
-
-                // Remove the node from the template
-                node.delete();
-
-                continue;
-            }
-        }
-
-        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.getCompilerOptions().filename,
-                this.components,
-                this.plugin
-            ).parse();
-
-            code = `${parsedScript}\n`;
-
-            if (exportedComponent.style) {
-                code += `\n${exportedComponent.style}\n`;
-            }
-        }
-
-        return code;
-    }
-};

+ 0 - 23
packages/compiler/src/core/plugin/hooks/ForEachHook.ts

@@ -1,23 +0,0 @@
-import { IPluginNode } from "../../Plugin";
-import { Hook } from "../Hook";
-
-export class ForEachHook extends Hook {
-    public parse(nodes: IPluginNode[]) {
-        for(let node of nodes) {
-            // Check if it's an each
-            if (node.isType("Each")) {
-                // Turn it into a <div x-each>
-                node.replaceWith({
-                    type: "Tag",
-                    name: "template",
-                    attributes: {
-                        "x-for": /*js*/`${node.getProp("val").trim()} of ${node.getProp("obj").trim()}`
-                    },
-                    children: node.getChildren()
-                });
-            }
-        }
-
-        return nodes;
-    }
-};

+ 4 - 4
packages/compiler/src/core/plugin/hooks/PropertyHook.ts

@@ -5,9 +5,9 @@ export class PropertyHook extends Hook {
     /**
      * The regex to test if an expression is a valid reactive item
      */
-    public static REGEX = /\{(?<tag>\{|-) ?(?<exp>(?:[\w+]|\.)+) ?(\}|-)\}/;
+    public REGEX = /\{(?<tag>\{|-) ?(?<exp>(?:[\w+]|\.)+) ?(\}|-)\}/;
 
-    public static testExpression(exp: string) {
+    public testExpression(exp: string) {
         return this.REGEX.test(exp);
     }
 
@@ -19,9 +19,9 @@ export class PropertyHook extends Hook {
             }
 
             // Check if it's a reactive item
-            if (token.mustEscape && PropertyHook.REGEX.test(token.val)) {
+            if (token.mustEscape && this.REGEX.test(token.val)) {
                 // Extract the token value
-                const result = token.val.match(PropertyHook.REGEX).groups;
+                const result = token.val.match(this.REGEX).groups;
                 const value = result.exp.replace(/\"/g, "\\\"");
 
                 const fn = result.tag === "{" ? "escape" : "literal";

+ 4 - 14
packages/compiler/src/core/plugin/hooks/StyleAndScriptHook.ts

@@ -1,23 +1,13 @@
 import { Hook } from "../Hook";
-import { ComponentHook } from "./ComponentHook";
 
 export class StyleAndScriptHook extends Hook {
-    public $before = [ComponentHook];
-
     public beforeStart(code: string) {
-        const regex = /^\s*(script|style).+?$/;
+        const matches = code.matchAll(/^\s*(script|style).*[^.]$/gm);
 
         // Add dots to ending "script" and "style" tags
-        code = code.split(/[\r\n]/)
-        .filter((line) => line.trim().length)
-        .map((line) => {
-            if (line.match(regex) !== null && !line.trim().endsWith(".")) {
-                return line.trimEnd() + ".";
-            }
-
-            return line;
-        })
-        .join("\n");
+        for(let match of matches) {
+            code = code.replace(match[0], match[0].trimEnd() + ".");
+        }
 
         return code;
     }

+ 137 - 12
packages/compiler/src/core/plugin/hooks/component/ScriptParser.ts

@@ -10,10 +10,11 @@ import {
 
 import Plugin from "../../../Plugin";
 
-import { IComponent } from "../ComponentHook";
+import { IComponent } from "../../phases/PrepareComponentsHook";
 
 export class ScriptParser {
     protected sourceFile: SourceFile;
+    protected project: Project;
 
     constructor(
         protected component: IComponent,
@@ -30,7 +31,7 @@ export class ScriptParser {
      */
     public parse() {
         // Load it in ts-morph
-        const project = new Project({
+        this.project = new Project({
             useInMemoryFileSystem: true,
             compilerOptions: {
                 allowJs: true,
@@ -40,20 +41,116 @@ export class ScriptParser {
         });
 
         // Create a new source file
-        this.sourceFile = project.createSourceFile(this.fileName, this.component.script);
+        this.sourceFile = this.project.createSourceFile(this.fileName, this.component.script);
 
         this.processDefaultComponent();
         this.processImportedComponents();
+        
+        if (this.component.data?.length) {
+            this.processComponentData();
+        }
+
+        if (this.component.methods?.length) {
+            this.processComponentMethods();
+        }
 
         return this.sourceFile.getText();
     }
 
+    private processComponentData() {
+        // Create the data parser
+        const data = this.project.createSourceFile("data.js", this.component.data);
+        const componentData = this.findOrCreateComponentObj("data");
+
+        // Retrieve all expressions
+        data.getChildrenOfKind(SyntaxKind.ExpressionStatement).forEach((declaration) => {
+            // Try finding the first binary expression
+            const binaryExp = declaration.getExpressionIfKind(SyntaxKind.BinaryExpression);
+
+            // If not found, it's not a variable declaration
+            if (!binaryExp) {
+                return;
+            }
+
+
+            // Left = identifier, Right = initializer
+            const left = binaryExp.getLeft().getText();
+            const right = binaryExp.getRight().getText();
+
+            // Add it to the component data
+            componentData.addPropertyAssignment({
+                name: left,
+                initializer: right
+            });
+        });
+    }
+
+    /**
+     * Processes the component methods.
+     */
+    private processComponentMethods() {
+        const identation = this.component.methods.match(/^(\t| )+/gm)[0];
+
+        let currentMethod = {
+            name: "",
+            body: ""
+        };
+
+        const parsedMethods: {
+            name: string;
+            body: string;
+        }[] = [];
+
+        this.component.methods.split(/[\r\n]/).forEach((line) => {
+            // If the line isn't idented
+            if (!line.startsWith(identation)) {
+                currentMethod = {
+                    name: line.trim(),
+                    body: ""
+                };
+
+                parsedMethods.push(currentMethod);
+            } else {
+                currentMethod.body += line + "\n";
+            }
+        });
+
+        // Create the data parser
+        const data = this.project.createSourceFile(
+            "methods.js",
+            parsedMethods.map((method) =>
+                `function ${method.name} {\n${method.body.trimEnd()}\n}`
+            ).join("\n\n")
+        );
+
+        const componentData = this.findOrCreateComponentObj("methods");
+
+        // Retrieve all function declaration expressions
+        data.getChildrenOfKind(SyntaxKind.FunctionDeclaration).forEach((declaration) => {
+            // Add it to the component
+            const method = componentData.addMethod({
+                name: declaration.getName(),
+                parameters: declaration.getParameters().map((param) => ({
+                    name: param.getName()
+                }))
+            });
+
+            method.setBodyText(declaration.getBodyText());
+        });
+    }
+
+    /**
+     * Processes the components imported by this component.
+     * @returns 
+     */
     private processImportedComponents() {
+        // Ignore if has no imports
         if (!("imports" in this.plugin.sharedData)) {
             return;
         }
 
-        const componentPropsComponents = this.findComponentImportedComponentsObj();
+        // Find the imported components object inside the default export
+        const componentPropsComponents = this.findOrCreateComponentObj("components");
 
         // Iterate over all imported components
         for(let alias in this.plugin.sharedData.imports) {
@@ -71,22 +168,30 @@ export class ScriptParser {
         }
     }
 
-    private findComponentImportedComponentsObj() {
+    /**
+     * Finds or creates an object inside the component object with a given key.
+     * @param key The key to be find or created.
+     * @returns 
+     */
+    private findOrCreateComponentObj(key: string) {
         const componentProps = this.findComponentPropsObj();
 
-        // Try finding an existing "components" expression
-        let exportedComponents = componentProps.getProperty("components");
+        // Try finding an existing property with the given key
+        let exportedComponents = componentProps.getProperty(key);
 
         if (exportedComponents) {
             return exportedComponents.getFirstChildByKindOrThrow(SyntaxKind.ObjectLiteralExpression);
         }
 
         return componentProps.addPropertyAssignment({
-            name: "components",
+            name: key,
             initializer: "{}"
         }).getInitializer() as ObjectLiteralExpression;
     }
 
+    /**
+     * Processes the exported component.
+     */
     private processDefaultComponent() {
         const componentProps = this.findComponentPropsObj();
 
@@ -111,7 +216,7 @@ export class ScriptParser {
 
         // If has any other exported components
         if (remainingComponents.length) {
-            const importedComponents = this.findComponentImportedComponentsObj();
+            const importedComponents = this.findOrCreateComponentObj("components");
 
             // Add them to the components
             remainingComponents.forEach((component) => {
@@ -123,9 +228,13 @@ export class ScriptParser {
         }
     }
 
+    /**
+     * Finds the exported component properties object.
+     * @returns 
+     */
     private findComponentPropsObj() {
         // Find the default export
-        let defaultExport = this.findDefaultExport();
+        let defaultExport = this.findOrCreateDefaultExport();
 
         // If it's not a defineComponent()
         const callExp = defaultExport.getFirstChildByKindOrThrow(SyntaxKind.CallExpression);
@@ -148,7 +257,11 @@ export class ScriptParser {
         return callExp.getFirstChildByKindOrThrow(SyntaxKind.ObjectLiteralExpression);
     }
 
-    private findDefaultExport() {
+    /**
+     * Finds or creates an "export default" expression (ExportAssignment).
+     * @returns 
+     */
+    private findOrCreateDefaultExport() {
         // Export assignment is "export = " or "export default"
         const defaultExport = this.sourceFile.getFirstChildByKind(SyntaxKind.ExportAssignment);
 
@@ -159,6 +272,18 @@ export class ScriptParser {
 
         // Try finding a ExpressionStatement that contains a BinaryExpression with PropertyAccessExpression
         // (module.exports)
-        const module = this.sourceFile.getFirstChildByKindOrThrow(SyntaxKind.ExpressionStatement);
+        //const module = this.sourceFile.getFirstChildByKindOrThrow(SyntaxKind.ExpressionStatement);
+
+        // Add an import to "defineComponent"
+        this.sourceFile.addImportDeclaration({
+            namedImports: ["defineComponent"],
+            moduleSpecifier: "@pupperjs/renderer"
+        })
+
+        // Create it
+        return this.sourceFile.addExportAssignment({
+            expression: "defineComponent({})",
+            isExportEquals: false
+        });
     }
 }

+ 1 - 1
packages/compiler/src/core/plugin/hooks/component/StyleParser.ts

@@ -1,4 +1,4 @@
-import { IComponent } from "../ComponentHook";
+import { IComponent } from "../../phases/PrepareComponentsHook";
 
 export class StyleParser {
     constructor(

+ 5 - 4
packages/compiler/src/core/plugin/nodes/AstNode.ts

@@ -1,7 +1,8 @@
 import { NodeModel } from "../../../model/core/NodeModel";
+import { CompilerNode } from "../../../model/core/nodes/CompilerNode";
 import Plugin, { PugAST } from "../../Plugin";
 
-export class AstNode extends NodeModel {
+export class AstNode extends CompilerNode {
     constructor(
         protected node: PugAST,
 
@@ -10,11 +11,11 @@ export class AstNode extends NodeModel {
          */
         public plugin: Plugin
     ) {
-        super();
-        
+        super(node, null, plugin);
+
         node.nodes.forEach((node) => {
             this.children.push(
-                Plugin.createNode(node, this)
+                Plugin.createNode(node, this) as any
             );
         });
     }

+ 1 - 1
packages/compiler/src/core/plugin/nodes/tags/TemplateTagNode.ts

@@ -7,7 +7,7 @@ export class TemplateTagNode extends TagNode {
 
     public toPugNode() {
         // Template tags can only have one children
-        if (this.getChildren().length > 1) {
+        if (this.getChildren().filter((child) => child.isType("Comment")).length > 1) {
             throw this.makeParseError("Template tags should only have one children");
         }
 

+ 184 - 0
packages/compiler/src/core/plugin/phases/PrepareComponentsHook.ts

@@ -0,0 +1,184 @@
+import { IPluginNode } from "../../Plugin";
+import { Hook } from "../Hook";
+import { TagNode } from "../nodes/TagNode";
+import { ScriptParser } from "../hooks/component/ScriptParser";
+import { AstNode } from "../nodes/AstNode";
+import { Console } from "console";
+import { PupperCompiler } from "../../Compiler";
+import { CompilerNode } from "../../../model/core/nodes/CompilerNode";
+
+const DefaultExportSymbol = Symbol("ExportedComponent");
+
+export interface IComponent {
+    name: string | symbol;
+
+    template: string;
+    methods?: string;
+    script?: string;
+    style?: string;
+    data?: string;
+
+    setupScript?: string;
+    exported?: boolean;
+}
+
+export class PrepareComponents extends Hook {
+    /**
+     * The imports that will later be putted into the template header
+     */
+    protected components: Record<string | symbol, IComponent> = {};
+
+    protected exportedData: Record<string, string> = {};
+
+    public beforeStart(code: string) {
+        const matches = code.matchAll(/^\s*(methods|data).*[^.]$/gm);
+
+        // Add dots to ending "methods" and "data" tags
+        for(let match of matches) {
+            code = code.replace(match[0], match[0].trimEnd() + ".");
+        }
+
+        return code;
+    }
+
+    public parse(nodes: IPluginNode[]) {
+        for(let node of nodes) {
+            // Ignore components that aren't in the root
+            if (!(node?.parent instanceof AstNode)) {
+                continue;
+            }
+
+            // Parse them as a component
+            // Parse the component
+            const component = this.parseComponentNode(node.parent);
+
+            // Save the component
+            this.components[component.name] = component;
+
+            break;
+        }
+
+        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.getCompilerOptions().fileName,
+                this.components,
+                this.plugin
+            ).parse();
+
+            code = `${parsedScript}\n`;
+
+            if (exportedComponent.style) {
+                code += `\n${exportedComponent.style}\n`;
+            }
+        }
+
+        return code;
+    }
+
+    public parseComponentNode(node: AstNode | TagNode) {
+        const isRootComponent = node instanceof AstNode;
+        const name = !isRootComponent ? node.getAttribute("name")?.replace(/"/g, "") : DefaultExportSymbol;
+
+        const template = node.findFirstChildByTagName("template");
+        const methods = node.findFirstChildByTagName("methods");
+        const script = node.findFirstChildByTagName("script");
+        const style = node.findFirstChildByTagName("style");
+        const data = node.findFirstChildByTagName("data");
+
+        // If no script and no template tag was found
+        if (!script && !template) {
+            throw this.compiler.makeParseError("Components must have at least a script tag or a template tag.", {
+                line: node.getLine() || 1,
+                column: node.getColumn()
+            });
+        }
+
+        /**
+         * Create the component
+         */
+        const component: IComponent = {
+            name,
+            template: null,
+            methods: null,
+            script: null,
+            style: null,
+            data: null,
+            exported: isRootComponent
+        };
+
+        // If the component is not exported and has no name
+        if (!component.exported && (!name || !name.length)) {
+            throw this.compiler.makeParseError("Scoped components must have a name.", {
+                line: node.getLine() || 1,
+                column: node.getColumn()
+            });
+        }
+
+        // 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) {
+            //this.plugin.parseChildren(template, true);
+
+            let lines = this.plugin.compiler.contents.split("\n");
+
+            const nextNodeAfterTemplate = template.getNextNode();
+
+            lines = lines.slice(
+                template.getLine(),
+                nextNodeAfterTemplate ? nextNodeAfterTemplate.getLine() - 1 : (node.hasNext() ? node.getNextNode().getLine() - 1 : lines.length)
+            );
+
+            // 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 = new PupperCompiler(this.compiler.options).compileTemplate(contents);
+            component.template = templateAsString;
+        }
+
+        // If has a script
+        if (script) {
+            component.script = this.consumeChildrenAsString(script);
+        }
+
+        // If has a style
+        if (style) {
+            console.log(style);
+        }
+
+        // If has data
+        if (data) {
+            component.data = this.consumeChildrenAsString(data);
+        }
+
+        // If has methods
+        if (methods) {
+            component.methods = this.consumeChildrenAsString(methods);
+        }
+
+        return component;
+    }
+
+    protected consumeChildrenAsString(node: CompilerNode) {
+        this.plugin.parseChildren(node);
+        return node.getChildren().map((child) => child.getProp("val")).join("");
+    }
+};

+ 6 - 95
packages/compiler/src/index.ts

@@ -1,100 +1,11 @@
-import pug from "pug";
-import Plugin from "./core/Plugin";
+import { ICompilerOptions, PupperCompiler } from "./core/Compiler";
 
-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 
-     */
+export = class Pupper {
     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));
-        }
+        return new Pupper();
     }
 
-    /**
-     * 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 fn = pug.compile(template, pugOptions);
-            const rendered = fn();
-
-            return rendered;///*js*/`function $h(h) { return ${htmlToHs({ syntax: "h" })(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;
-    }
+    public compileToString(template: string, options: ICompilerOptions) {
+        return new PupperCompiler(options).compileComponent(template);
+    }  
 }

+ 5 - 4
packages/compiler/src/model/core/nodes/CompilerNode.ts

@@ -1,5 +1,6 @@
 import Plugin, { PugNodes, PugNodeAttribute, TPugNodeTypes, TCompilerNode, TPugNodesWithTypes } from "../../../core/Plugin";
 import { AstNode } from "../../../core/plugin/nodes/AstNode";
+import { TagNode } from "../../../core/plugin/nodes/TagNode";
 import { NodeModel } from "../NodeModel";
 
 export interface IParserNode {
@@ -127,9 +128,9 @@ export class CompilerNode<TNode extends PugNodes = any> extends NodeModel<Compil
                 if (parent?.parent) {
                     parent = parent.parent;
                 }
-            } while(!(parent instanceof AstNode));
+            } while(parent.parent !== null);
 
-            this.plugin = parent.plugin;
+            this.plugin = (parent as AstNode).plugin;
         }
     }
 
@@ -147,8 +148,8 @@ export class CompilerNode<TNode extends PugNodes = any> extends NodeModel<Compil
      * @param name The children node tag name.
      * @returns 
      */
-    public findFirstChildByTagName(name: string) {
-        return this.children.find((child) => child.isType("Tag") && child.isName(name));
+    public findFirstChildByTagName(name: string): TagNode {
+        return this.children.find((child) => child.isType("Tag") && child.isName(name)) as TagNode;
     }
 
     /**

+ 15 - 0
packages/compiler/src/typings/pug-code-gen.d.ts

@@ -0,0 +1,15 @@
+import pug from "./pug";
+
+export declare module "pug-code-gen" {
+    export declare interface ICodeGenOptions {
+        compileDebug?: boolean;
+        pretty?: boolean;
+        inlineRuntimeFunctions?: boolean;
+        templateName?: string;
+        self?: boolean;
+        globals?: string[],
+        doctype?: string
+    }
+
+    declare export default function CodeGen(ast: pug.Pug.PugAST, options: ICodeGenOptions): string;
+}

+ 11 - 0
packages/compiler/src/typings/pug-parser.d.ts

@@ -0,0 +1,11 @@
+import pug from "./pug";
+
+export declare module "pug-parser" {
+    declare interface IParserOptions {
+        filename?: string;
+        plugins?: pug.Pug.PugPlugin[];
+        src?: string;
+    }
+
+    declare export default function Parse(ast: pug.Pug.PugToken[], options: IParserOptions): pug.Pug.PugAST;
+}

+ 2 - 1
packages/compiler/src/typings/pug.d.ts

@@ -1,9 +1,10 @@
 import type pug from "pug";
 import type PugLexer from "pug-lexer";
+
 import { LexTokenType } from "pug-lexer";
 
 export declare namespace Pug {
-    export interface LexerPlugin {
+    export interface LexerPlugin extends Record<string, CallableFunction> {
         /**
          * Checks if a given expression is valid
          * @param lexer The pug lexer instance

+ 6 - 1
packages/compiler/tsconfig.json

@@ -3,7 +3,12 @@
     "compilerOptions": {
         "target": "esnext",
         "outDir": "./out",
-        "declarationDir": "./types"
+        "declarationDir": "./types",
+        "typeRoots": [
+            "./src/typings",
+            "./node_modules/@types",
+            "../../node_modules/@types"
+        ]
     },
     "include": ["./src/**/*.ts"],
     "exclude": ["node_modules"],

+ 1 - 3
packages/webpack-loader/index.js

@@ -16,9 +16,7 @@ module.exports = function(source, options) {
         source,
         {
             ...options,
-            pug: {
-                filename: this.resourcePath
-            }
+            fileName: this.resourcePath
         }
     );
 

+ 6 - 40
test/index.js

@@ -1,47 +1,13 @@
 import Template from "./templates/template.pupper";
-import { defineComponent } from "@pupperjs/renderer";
 
 import ImportedComponent from "./templates/ImportedComponent.pupper";
 import ExportedComponent from "./templates/ExportedComponent.pupper";
 
-    (async function() {
-    const pupper = defineComponent({
-        render: Template,
-        methods: {
-            onClickPuppy(puppy) {
-                alert("You clicked puppy " + puppy.id + "! :D");
-            }
-        },
-        data: {
-            page: {
-                title: "pupper.js is awesome!",
-                description: "I use pupper.js because I love puppies!",
-                lead: "Also we love puppers, shiberinos and other doggos too! 🐶"
-            },
-            puppies: [
-                {
-                    id: 1,
-                    title: "A cutie pup",
-                    description: "Look at this cutie",
-                    thumbnail: "https://placedog.net/800",
-                    properties: ["beautiful", "doge"]
-                },
-                {
-                    id: 2,
-                    title: "Another cute pup",
-                    description: "Isn't it a cute doggo?!",
-                    thumbnail: "https://placedog.net/400",
-                    properties: ["wow", "much woof"]
-                }
-            ]
-        }
-    });
-
-    window.component = pupper;
-    
-    await pupper.mount(document.getElementById("app"));
+(async function() {
+    window.component = Template;
+    await Template.mount(document.getElementById("app"));
 
-    pupper.puppies.push({
+    Template.puppies.push({
         id: 3,
         title: "Wow, a shibe!",
         description: "Cute shiberino!!!",
@@ -49,6 +15,6 @@ import ExportedComponent from "./templates/ExportedComponent.pupper";
         shibe: true
     });
 
-    ExportedComponent.mount(pupper.$slots.slot);
-    ImportedComponent.mount(pupper.$slots.slot2);
+    ExportedComponent.mount(Template.$slots.slot);
+    ImportedComponent.mount(Template.$slots.slot2);
 }());

+ 10 - 11
test/templates/ExportedComponent.pupper

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

+ 15 - 16
test/templates/ImportedComponent.pupper

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

+ 80 - 50
test/templates/template.pupper

@@ -1,50 +1,80 @@
-//- Stylesheets
-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")
-
-.text-center
-    .cover-container.d-flex.h-100.p-3.mx-auto.flex-column
-        header.masthead.mb-auto 
-            .inner
-                h3.masthead-brand={{ page.title }}
-
-        //- Main contents
-        main.inner.cover.my-3(role="main")
-            h1.cover-heading={{ page.description }}
-
-            p.lead={{ page.lead }}
-
-            .row.mt-5.justify-content-around.align-items-center
-                if puppies === undefined || puppies.length === 0
-                    |Oh noe! No puppies to show :(
-                else
-                    //- Render the puppies and share the onClickPuppy method with it
-                    each index, puppy in puppies
-                        .col-5.mb-5
-                            .puppy.card.px-0(:data-pop="index", :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 === true
-                                        p.text-warning|shibe!!!
-
-                                    if puppy.properties
-                                        each property in puppy.properties
-                                            span.badge.badge-info={{property}}
-
-            div 
-                |Testing slots: 
-
-                slot(name="slot")
-                slot(name="slot2")
-
-        footer.mastfoot.mt-auto
-            .inner
-                p
-                    |Cover template for <a href="https://getbootstrap.com/">Bootstrap</a>, by <a href="https://twitter.com/mdo">@mdo</a>
+template
+    .container
+        //- Stylesheets
+        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")
+
+        .text-center
+            .cover-container.d-flex.h-100.p-3.mx-auto.flex-column
+                header.masthead.mb-auto 
+                    .inner
+                        h3.masthead-brand={{ page.title }}
+
+                //- Main contents
+                main.inner.cover.my-3(role="main")
+                    h1.cover-heading={{ page.description }}
+
+                    p.lead={{ page.lead }}
+
+                    .row.mt-5.justify-content-around.align-items-center
+                        if puppies === undefined || puppies.length === 0
+                            |Oh noe! No puppies to show :(
+                        else
+                            //- Render the puppies and share the onClickPuppy method with it
+                            each index, puppy in puppies
+                                .col-5.mb-5
+                                    .puppy.card.px-0(:data-pop="index", :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 === true
+                                                p.text-warning|shibe!!!
+
+                                            if puppy.properties
+                                                each property in puppy.properties
+                                                    span.badge.badge-info={{property}}
+
+                    div 
+                        |Testing slots: 
+
+                        slot(name="slot")
+                        slot(name="slot2")
+
+                footer.mastfoot.mt-auto
+                    .inner
+                        p
+                            |Cover template for <a href="https://getbootstrap.com/">Bootstrap</a>, by <a href="https://twitter.com/mdo">@mdo</a>
+
+data
+    page = {
+        title: "pupper.js is awesome!",
+        description: "I use pupper.js because I love puppies!",
+        lead: "Also we love puppers, shiberinos and other doggos too! 🐶"
+    }
+
+    puppies = [
+        {
+            id: 1,
+            title: "A cutie pup",
+            description: "Look at this cutie",
+            thumbnail: "https://placedog.net/800",
+            properties: ["beautiful", "doge"]
+        },
+        {
+            id: 2,
+            title: "Another cute pup",
+            description: "Isn't it a cute doggo?!",
+            thumbnail: "https://placedog.net/400",
+            properties: ["wow", "much woof"]
+        }
+    ]
+
+methods
+    onClickPuppy(puppy)
+        alert("You clicked puppy " + puppy.id + "! :D");

+ 2 - 2
tsconfig.json

@@ -4,7 +4,6 @@
         "allowJs": true,
         "esModuleInterop": true,
         "outDir": "out",
-        "sourceMap": true,
         "declaration": true,
         "declarationDir": "types",
         "noImplicitAny": true,
@@ -12,7 +11,8 @@
         "emitDecoratorMetadata": true,
         "moduleResolution": "node",
         "module": "commonjs",
-        "skipLibCheck": true
+        "skipLibCheck": true,
+        "inlineSourceMap": true
     },
     "include": ["./**/*.ts"],
     "exclude": ["node_modules"],