diff --git a/src/compiler/plugin.js b/src/compiler/plugin.js index ec21bd7..c2c624e 100644 --- a/src/compiler/plugin.js +++ b/src/compiler/plugin.js @@ -27,7 +27,10 @@ const compileJSXPlugin = (babel, options) => { throw path.buildCodeFrameError("JSXFragment is not supported."); }, Program(path) { - shared.set("templateFunctionName", path.scope.generateUidIdentifier("tmpl").name); + shared.set( + "templateFunctionName", + path.scope.generateUidIdentifier("template").name + ); shared.set("firstElementChild", path.scope.generateUidIdentifier("fec").name); shared.set("nextElementSibling", path.scope.generateUidIdentifier("nes").name); shared.set("spreadFunctionName", path.scope.generateUidIdentifier("sprd").name); diff --git a/src/compiler/shared.js b/src/compiler/shared.js index e182915..3436445 100644 --- a/src/compiler/shared.js +++ b/src/compiler/shared.js @@ -22,6 +22,9 @@ const shared = Osake({ spreadFunctionName: "grim_$s", firstElementChild: "grim_$fec", nextElementSibling: "grim_$nes", + + programPath: /** @type {babel.NodePath | null} */ (null), + sharedNodes: /** @type {Record} */ ({}), }); export { shared }; diff --git a/src/compiler/transforms/jsxElement.js b/src/compiler/transforms/jsxElement.js index e339521..db93e7e 100644 --- a/src/compiler/transforms/jsxElement.js +++ b/src/compiler/transforms/jsxElement.js @@ -8,6 +8,7 @@ import { getJSXElementName, getAttributeName, createTemplateLiteralBuilder, + createIIFE, } from "../utils"; /** @@ -17,7 +18,7 @@ import { function JSXElement(path) { const { parent, node } = path; - const { babel, enableCommentOptions, inuse } = shared(); + const { babel, enableCommentOptions, inuse, programPath } = shared(); const { types: t } = babel; if ( @@ -51,7 +52,7 @@ function JSXElement(path) { const template = createTemplateLiteralBuilder(); - let templateName = t.identifier("tmpl"); + let templateName = (programPath || path).scope.generateUidIdentifier("el"); const opts = { enableStringMode: shared().enableStringMode }; @@ -143,7 +144,7 @@ function JSXElement(path) { if (t.isIdentifier(expression) || t.isMemberExpression(expression)) { const right = current.length > 0 - ? createMemberExpression(templateName, ...current) ?? templateName + ? createMemberExpression(templateName, ...current) || templateName : templateName; for (const item of current) { @@ -173,7 +174,7 @@ function JSXElement(path) { template.push(insertAttrubute(name, value)); } else if (t.isJSXExpressionContainer(attr.value)) { - const expression = attr.value.expression; + const { expression } = attr.value; if (t.isObjectExpression(expression)) { const attr = objectExpressionToAttribute(expression); @@ -276,24 +277,69 @@ function JSXElement(path) { template.push(``); } - if (expressions.length > 0) { - path.replaceWith( - t.callExpression( - t.arrowFunctionExpression( - [], - t.blockStatement([ - t.variableDeclaration("const", [ - t.variableDeclarator(templateName, templateCall), - ]), - ...expressions, - t.returnStatement(templateName), - ]) + /** + * We are lucky today, because this is just an _static_ html. + * TemplateLiteral does not have any expressions, so template could be extracted + */ + if (programPath && template.template.quasis.length === 1) { + const current_raw = template.template.quasis[0].value.raw; + const { sharedNodes } = shared(); + + /** @type {babel.types.VariableDeclaration | null} */ + let decl = null; + + /** + * If there are identical elements, we reuse them + */ + if (sharedNodes[current_raw]) { + decl = sharedNodes[current_raw]; + } else { + decl = t.variableDeclaration("let", [ + t.variableDeclarator( + (programPath || path).scope.generateUidIdentifier("tmpl"), + templateCall ), - [] - ) + ]); + + programPath.node.body.unshift(decl); + + shared().sharedNodes[current_raw] = decl; + } + + /** @type {babel.types.Identifier} */ + // @ts-ignore - We create these declarations and know it is Identifier + const object = decl.declarations[0].id; + + const call = t.callExpression( + t.memberExpression(object, t.identifier("cloneNode")), + [t.booleanLiteral(true)] ); + + if (expressions.length > 0) { + path.replaceWith( + createIIFE( + t.variableDeclaration("let", [t.variableDeclarator(templateName, call)]), + ...expressions, + t.returnStatement(templateName) + ) + ); + } else { + path.replaceWith(call); + } } else { - path.replaceWith(templateCall); + if (expressions.length > 0) { + path.replaceWith( + createIIFE( + t.variableDeclaration("let", [ + t.variableDeclarator(templateName, templateCall), + ]), + ...expressions, + t.returnStatement(templateName) + ) + ); + } else { + path.replaceWith(templateCall); + } } } } diff --git a/src/compiler/transforms/post.js b/src/compiler/transforms/post.js index b9d6a01..86f401c 100644 --- a/src/compiler/transforms/post.js +++ b/src/compiler/transforms/post.js @@ -129,6 +129,8 @@ function post(file) { } else { produceImports(); } + + shared.set("sharedNodes", {}); } export { post }; diff --git a/src/compiler/transforms/pre.js b/src/compiler/transforms/pre.js index cf579ff..a2efe32 100644 --- a/src/compiler/transforms/pre.js +++ b/src/compiler/transforms/pre.js @@ -52,6 +52,9 @@ const createPre = (options) => { } } } + + shared.set("programPath", file.path); + shared.set("sharedNodes", {}); } return pre; diff --git a/src/compiler/utils/create-iife.js b/src/compiler/utils/create-iife.js new file mode 100644 index 0000000..0d69f52 --- /dev/null +++ b/src/compiler/utils/create-iife.js @@ -0,0 +1,14 @@ +import { shared } from "../shared"; + +/** + * + * @param {...babel.types.Statement} body + * @returns + */ +const createIIFE = (...body) => { + const { types: t } = shared().babel; + + return t.callExpression(t.arrowFunctionExpression([], t.blockStatement(body)), []); +}; + +export { createIIFE }; diff --git a/src/compiler/utils/index.js b/src/compiler/utils/index.js index be4b081..75668e0 100644 --- a/src/compiler/utils/index.js +++ b/src/compiler/utils/index.js @@ -7,4 +7,5 @@ export { getAttributeName } from "./get-attribute-name"; export { jsxMemberExpressionToMemberExpression } from "./jsx-member-expression-to-member-expression"; export { createTemplateLiteralBuilder } from "./template-literal-builder"; export { isObject } from "./is-object"; +export { createIIFE } from "./create-iife"; export * as constants from "./constants"; diff --git a/tests/dynamic attrs and child/expected.snapshot b/tests/dynamic attrs and child/expected.snapshot index ae885f6..c6ad54a 100644 --- a/tests/dynamic attrs and child/expected.snapshot +++ b/tests/dynamic attrs and child/expected.snapshot @@ -1,10 +1,10 @@ -import { template as _tmpl } from "grim-jsx/runtime.js"; +import { template as _template } from "grim-jsx/runtime.js"; import styles from './styles.module.css'; const Paragraph = ({ children }) => { - const p = _tmpl(`

${children}

`); + const p = _template(`

${children}

`); return p; }; \ No newline at end of file diff --git a/tests/dynamic tag name/expected.snapshot b/tests/dynamic tag name/expected.snapshot index 09cfc3c..7c01611 100644 --- a/tests/dynamic tag name/expected.snapshot +++ b/tests/dynamic tag name/expected.snapshot @@ -1,7 +1,7 @@ -import { template as _tmpl } from "grim-jsx/runtime.js"; +import { template as _template } from "grim-jsx/runtime.js"; const As = props => { - const el = _tmpl(`<${props.as}>`); + const el = _template(`<${props.as}>`); return el; }; \ No newline at end of file diff --git a/tests/index.js b/tests/index.js index 898593c..8e6e872 100644 --- a/tests/index.js +++ b/tests/index.js @@ -63,12 +63,13 @@ async function main() { plugins: [ [ compileJSXPlugin, - options !== null - ? { ...defaultOptions, ...options } - : defaultOptions, + options !== null ? { ...defaultOptions, ...options } : defaultOptions, ], ], babelrc: false, + browserslistConfigFile: false, + configFile: false, + highlightCode: false, comments: false, filename: entry.replaceAll(" ", "_"), }); diff --git a/tests/inline runtime/expected.snapshot b/tests/inline runtime/expected.snapshot index 5653235..51c7616 100644 --- a/tests/inline runtime/expected.snapshot +++ b/tests/inline runtime/expected.snapshot @@ -2,7 +2,7 @@ const _sprd = (props, attr) => { return Object.entries(props).map(([key, value]) => value == null ? '' : attr ? `${key}:${value};` : `${key}="${value}"`).join(" "); }; -const _tmpl = (html, isSVG) => { +const _template = (html, isSVG) => { const t = document.createElement("template"); t.innerHTML = html; let node = t.content.firstChild; @@ -16,5 +16,5 @@ let cmp = props => { className, ...attrs } = props; - return _tmpl(`

${children}

`); + return _template(`

${children}

`); }; \ No newline at end of file diff --git a/tests/object as attribute/expected.snapshot b/tests/object as attribute/expected.snapshot index 22e53d1..348940a 100644 --- a/tests/object as attribute/expected.snapshot +++ b/tests/object as attribute/expected.snapshot @@ -1,11 +1,13 @@ -import { template as _tmpl, spread as _sprd } from "grim-jsx/runtime.js"; +import { template as _template, spread as _sprd } from "grim-jsx/runtime.js"; -const div = _tmpl(`
Hello
`); +let _tmpl = _template(`
Hello
`); + +const div = _tmpl.cloneNode(true); let key = window.style_key; let value = window.style_value(); -const elements = [_tmpl(`Hello`), _tmpl(`Hello`), _template(`Hello`)]; \ No newline at end of file diff --git a/tests/simple node/code.snapshot b/tests/simple node/code.snapshot deleted file mode 100644 index 3e8649e..0000000 --- a/tests/simple node/code.snapshot +++ /dev/null @@ -1 +0,0 @@ -const div =
Hello
\ No newline at end of file diff --git a/tests/simple node/expected.snapshot b/tests/simple node/expected.snapshot deleted file mode 100644 index 01a6acb..0000000 --- a/tests/simple node/expected.snapshot +++ /dev/null @@ -1,3 +0,0 @@ -import { template as _tmpl } from "grim-jsx/runtime.js"; - -const div = _tmpl(`
Hello
`); \ No newline at end of file diff --git a/tests/spread/expected.snapshot b/tests/spread/expected.snapshot index 98d827c..2dbd77a 100644 --- a/tests/spread/expected.snapshot +++ b/tests/spread/expected.snapshot @@ -1,3 +1,3 @@ -import { template as _tmpl, spread as _sprd } from "grim-jsx/runtime.js"; +import { template as _template, spread as _sprd } from "grim-jsx/runtime.js"; -const Component = props => _tmpl(`
`); \ No newline at end of file +const Component = props => _template(`
`); \ No newline at end of file diff --git a/tests/string mode/expected.snapshot b/tests/string mode/expected.snapshot index 683e774..e491e76 100644 --- a/tests/string mode/expected.snapshot +++ b/tests/string mode/expected.snapshot @@ -1,7 +1,10 @@ -import { template as _tmpl } from "grim-jsx/runtime.js"; +import { template as _template } from "grim-jsx/runtime.js"; + +let _tmpl = _template(`
`); + const people = ["Artem", "Ivan", "Arina", "Roman", "Kenzi"]; let str = `

Hello!

    ${people.map(person => `
  • ${person}
  • `).join("")}
`; -const node = _tmpl(`
`); +const node = _tmpl.cloneNode(true); str = `
It should be a string
`; \ No newline at end of file diff --git a/tests/svg/expected.snapshot b/tests/svg/expected.snapshot index a307e3f..0a10903 100644 --- a/tests/svg/expected.snapshot +++ b/tests/svg/expected.snapshot @@ -1,7 +1,11 @@ -import { template as _tmpl } from "grim-jsx/runtime.js"; +import { template as _template } from "grim-jsx/runtime.js"; -const icon = _tmpl(``); +let _tmpl2 = _template(``, true); -const path = _tmpl(``, true); +let _tmpl = _template(``); + +const icon = _tmpl.cloneNode(true); + +const path = _tmpl2.cloneNode(true); icon.appendChild(path); \ No newline at end of file diff --git a/tests/using refs/expected.snapshot b/tests/using refs/expected.snapshot index 080f975..6b2af01 100644 --- a/tests/using refs/expected.snapshot +++ b/tests/using refs/expected.snapshot @@ -1,18 +1,23 @@ -import { template as _tmpl, firstElementChild as _fec, nextElementSibling as _nes } from "grim-jsx/runtime.js"; +import { template as _template, firstElementChild as _fec, nextElementSibling as _nes } from "grim-jsx/runtime.js"; + +let _tmpl2 = _template(`

Copyright © 2069

`); + +let _tmpl = _template(`

Hello!

Today we are going to find out something

`); + let button; const article = (() => { - const tmpl = _tmpl(`

Hello!

Today we are going to find out something

`); + const _el = _tmpl.cloneNode(true); - button = tmpl[_fec][_nes][_nes]; - return tmpl; + button = _el[_fec][_nes][_nes]; + return _el; })(); let foo; const footer = (() => { - const tmpl = _tmpl(`

Copyright © 2069

`); + const _el2 = _tmpl2.cloneNode(true); - foo = tmpl; - return tmpl; + foo = _el2; + return _el2; })(); \ No newline at end of file diff --git a/tests/variables scoping/code.snapshot b/tests/variables scoping/code.snapshot index 0b6d379..2b0a139 100644 --- a/tests/variables scoping/code.snapshot +++ b/tests/variables scoping/code.snapshot @@ -1,10 +1,10 @@ -let _tmpl = 'Noodles'; +let _template = 'Noodles'; let eggs; const list = (