UNPKG

marko

Version:

UI Components + streaming, async, high performance, HTML templating for Node.js and the browser.

613 lines (557 loc) • 18.9 kB
import { types as t } from "@marko/compiler"; import { findParentTag, getTagDef, importDefault, importNamed, isAttributeTag, isDynamicTag, isMacroTag, isNativeTag, loadFileForTag, parseExpression, parseStatements, resolveRelativePath, resolveTagImport, } from "@marko/compiler/babel-utils"; import { version } from "marko/package.json"; import { resolve } from "path"; import MarkoCDATA from "./cdata"; import MarkoClass from "./class"; import MarkoComment from "./comment"; import MarkoDeclaration from "./declaration"; import MarkoDocumentType from "./document-type"; import MarkoPlaceholder from "./placeholder"; import MarkoScriptlet from "./scriptlet"; import MarkoTag from "./tag"; import MarkoText from "./text"; import addDependencies from "./util/add-dependencies"; import getComponentFiles from "./util/get-component-files"; import { optimizeHTMLWrites } from "./util/optimize-html-writes"; import { analyzeStaticVDOM } from "./util/optimize-vdom-create"; export const tagDiscoveryDirs = ["components"]; export { optionalTaglibs, default as taglibs } from "./taglib"; export { entryBuilder as internalEntryBuilder } from "./util/add-dependencies"; export const preferAPI = "class"; export const analyze = { Program: { enter(program) { // Pre populate metadata for component files. const meta = program.hub.file.metadata.marko; getComponentFiles(program); if (!meta.hasComponent && !meta.hasComponentBrowser) { meta.hasComponent = program .get("body") .some((child) => child.isMarkoClass()); } }, exit(program) { const { file } = program.hub; const meta = file.metadata.marko; const componentFiles = getComponentFiles(program); const { styleFile, packageFile, componentBrowserFile } = componentFiles; if (packageFile) { meta.deps.unshift(`package: ${packageFile}`); } if (styleFile) { meta.deps.unshift(styleFile); } if (!meta.widgetBind) { if (meta.hasComponentBrowser) { meta.component = componentBrowserFile; } else if ( meta.hasComponent || meta.hasStatefulTagParams || meta.hasFunctionEventHandlers ) { meta.component = file.opts.filename; } else if (meta.hasStringEventHandlers) { meta.implicitSplitComponent = true; meta.component = componentFiles.componentBrowserFile = "marko/src/runtime/helpers/empty-component.js"; meta.hasComponentBrowser = true; } } meta.component = meta.component && resolveRelativePath(file, meta.component); meta.deps = meta.deps.map((filename) => typeof filename === "string" ? resolveRelativePath(file, filename) : filename, ); }, }, MarkoTag(tag) { const { file } = tag.hub; const tagDef = getTagDef(tag); // Check if tag uses stateful tag params. const meta = tag.hub.file.metadata.marko; let relativePath; if (tagDef) { if (tagDef.html && !tagDef.template && !tagDef.renderer) { if (tagDef.htmlType === "custom-element") { if (tagDef.parseOptions && tagDef.parseOptions.import) { // TODO: the taglib should be updated to support this as a top level option. meta.deps.push( resolve( tagDef.dir, resolve(tagDef.dir, tagDef.parseOptions.import), ), ); } } } else if (t.isStringLiteral(tag.node.name)) { relativePath = resolveRelativeTagEntry(file, tagDef); } if (tagDef.translator && tagDef.translator.path) { if (!meta.watchFiles.includes(tagDef.translator.path)) { meta.watchFiles.push(tagDef.translator.path); } } } if (!relativePath && t.isStringLiteral(tag.node.name)) { const tagName = tag.node.name.value; const binding = /^[A-Z][a-zA-Z0-9_$]*$/.test(tagName) && tag.scope.getBinding(tagName); if (binding && binding.kind === "module" && binding.identifier.loc) { const importSource = binding.path.parent.source.value; relativePath = resolveTagImport(tag, importSource) || importSource; tag.node.extra = tag.node.extra || {}; tag.node.extra.tagNameImported = relativePath; } } if (relativePath) { tag.node.extra = tag.node.extra || {}; tag.node.extra.relativePath = relativePath; if (!meta.tags.includes(relativePath)) { meta.tags.push(relativePath); } const childFile = loadFileForTag(tag); if (childFile?.ast.program.extra?.featureType === "tags") { tag.node.extra.featureType = "tags"; (file.path.node.extra ??= {}).needsCompat = true; } } if (!(meta.hasFunctionEventHandlers || meta.hasStringEventHandlers)) { for (const attr of tag.node.attributes) { if ( t.isMarkoAttribute(attr) && attr.arguments && /^on[-A-Z]/.test(attr.name) ) { if ( attr.arguments.length >= 1 && attr.arguments[0].type === "StringLiteral" ) { meta.hasStringEventHandlers = true; } else { meta.hasFunctionEventHandlers = true; } break; } } } if ( meta.hasStatefulTagParams || isNativeTag(tag) || isMacroTag(tag) || !tag.get("body").get("params").length ) { return; } if (isDynamicTag(tag)) { meta.hasStatefulTagParams = true; return; } let curTag = tag; while (isAttributeTag(curTag)) { curTag = findParentTag(curTag); } const tagFile = loadFileForTag(curTag); const childMeta = tagFile && tagFile.metadata.marko; meta.hasStatefulTagParams = childMeta && (childMeta.hasStatefulTagParams || childMeta.hasFunctionEventHandlers || (childMeta.hasComponent && !childMeta.hasComponentBrowser)); }, ImportDeclaration: { exit(path) { const source = path.get("source"); const tagEntry = resolveTagImport(source, source.node.value); if (tagEntry) { const meta = path.hub.file.metadata.marko; source.node.value = tagEntry; if (!meta.tags.includes(tagEntry)) { meta.tags.push(tagEntry); } } }, }, }; export const translate = { MarkoDocumentType, MarkoDeclaration, MarkoCDATA, MarkoTag, MarkoText, MarkoPlaceholder, MarkoScriptlet, MarkoClass, MarkoComment, ReferencedIdentifier(path) { if (path.node.name === "component" && !path.scope.hasBinding("component")) { path.replaceWith(path.hub.file._componentInstanceIdentifier); } }, Program: { enter(path) { const { hub: { file }, } = path; if (file.markoOpts.output === "hydrate") { addDependencies(file, true); return; } else if ( file.markoOpts.resolveVirtualDependency && file.markoOpts.output !== "html" ) { addDependencies(file, false); } if (file.metadata.marko.moduleCode) { path .replaceWith( t.program( parseStatements(file, file.metadata.marko.moduleCode), undefined, file.markoOpts.modules === "cjs" ? "script" : "module", ), )[0] .skip(); return; } file._componentDefIdentifier = path.scope.generateUidIdentifier("componentDef"); file._componentInstanceIdentifier = path.scope.generateUidIdentifier("component"); // Pre-Analyze tree analyzeStaticVDOM(path); // Move non static content into the renderBody. const [renderBlock] = path.pushContainer("body", t.blockStatement([])); path .get("body") .filter(isRenderContent) .forEach((childPath) => { renderBlock.pushContainer("body", childPath.node); childPath.remove(); }); file._renderBlock = renderBlock; path.scope.crawl(); }, exit(path) { const { hub: { file }, } = path; const { markoOpts, _inlineComponentClass } = file; const includeMetaInSource = markoOpts.meta !== false; const meta = file.metadata.marko; const { componentFile, componentBrowserFile } = getComponentFiles(path); const isHTML = markoOpts.output === "html"; const renderBlock = file._renderBlock; const componentClass = (componentFile && importDefault( file, resolveRelativePath(file, componentFile), "marko_component", )) || _inlineComponentClass || t.objectExpression([]); const componentIdentifier = path.scope.generateUidIdentifier("marko_component"); const componentTypeIdentifier = path.scope.generateUidIdentifier( "marko_componentType", ); const templateIdentifier = path.scope.generateUidIdentifier("marko_template"); const rendererIdentifier = importDefault( file, "marko/src/runtime/components/renderer.js", "marko_renderer", ); const templateRendererMember = t.memberExpression( templateIdentifier, t.identifier("_"), ); const templateMetaMember = t.memberExpression( templateIdentifier, t.identifier("meta"), ); if (markoOpts.writeVersionComment) { path.addComment( "leading", ` Compiled using marko@${version} - DO NOT EDIT`, true, ); } const runtimeTemplateIdentifier = path.scope.generateUidIdentifier("t"); path.unshiftContainer( "body", [ path.node.extra?.needsCompat && t.importDeclaration( [], t.stringLiteral( `marko/${markoOpts.optimize ? "dist" : "src"}/runtime/helpers/tags-compat/${ markoOpts.output === "html" ? "html" : "dom" }${markoOpts.optimize ? "" : "-debug"}.${markoOpts.modules === "esm" ? "mjs" : "js"}`, ), ), t.importDeclaration( [t.importSpecifier(runtimeTemplateIdentifier, t.identifier("t"))], t.stringLiteral( `marko/${markoOpts.optimize ? "dist" : "src"}/runtime/${ isHTML ? "html" : "vdom" }/${markoOpts.hot ? "hot-reload.js" : "index.js"}`, ), ), t.variableDeclaration("const", [ t.variableDeclarator( componentTypeIdentifier, t.stringLiteral(meta.id), ), t.variableDeclarator( templateIdentifier, t.callExpression(runtimeTemplateIdentifier, [ componentTypeIdentifier, ]), ), ]), includeMetaInSource && t.expressionStatement( t.assignmentExpression( "=", t.memberExpression(templateIdentifier, t.identifier("path")), t.identifier("__filename"), ), ), t.exportDefaultDeclaration(templateIdentifier), ].filter(Boolean), ); path.pushContainer( "body", [ !isHTML && t.expressionStatement( t.callExpression( importNamed( file, "marko/src/runtime/components/registry.js", "r", "marko_registerComponent", ), [ componentTypeIdentifier, t.arrowFunctionExpression( [], componentBrowserFile && !meta.implicitSplitComponent ? importDefault( file, resolveRelativePath(file, componentBrowserFile), "marko_split_component", ) : templateIdentifier, ), ], ), ), t.variableDeclaration("const", [ t.variableDeclarator(componentIdentifier, componentClass), ]), ].filter(Boolean), ); const templateRenderOptionsProps = [ t.objectProperty(t.identifier("t"), componentTypeIdentifier), ]; if (!meta.component) { templateRenderOptionsProps.push( t.objectProperty(t.identifier("i"), t.booleanLiteral(true)), ); } if (componentBrowserFile) { templateRenderOptionsProps.push( t.objectProperty(t.identifier("s"), t.booleanLiteral(true)), ); } if (!markoOpts.optimize) { templateRenderOptionsProps.push( t.objectProperty(t.identifier("d"), t.booleanLiteral(true)), ); } let rendererAssignment = t.assignmentExpression( "=", templateRendererMember, t.callExpression(rendererIdentifier, [ t.functionExpression( null, [ t.identifier("input"), t.identifier("out"), file._componentDefIdentifier, file._componentInstanceIdentifier, t.identifier("state"), t.identifier("$global"), ], renderBlock.node, ), t.objectExpression(templateRenderOptionsProps), componentIdentifier, ]), ); if (!isHTML && componentBrowserFile && !meta.implicitSplitComponent) { rendererAssignment = t.assignmentExpression( "=", t.memberExpression( importDefault( file, resolveRelativePath(file, componentBrowserFile), "marko_split_component", ), t.identifier("renderer"), ), rendererAssignment, ); } path.pushContainer("body", t.expressionStatement(rendererAssignment)); if (meta.implicitSplitComponent && isHTML) { renderBlock.unshiftContainer( "body", t.expressionStatement( t.callExpression( importDefault( file, "marko/src/runtime/helpers/skip-serialize.js", "marko_skip_serialize", ), [t.identifier("input")], ), ), ); } renderBlock.remove(); if (!isHTML) { path.pushContainer( "body", t.expressionStatement( t.assignmentExpression( "=", t.memberExpression(templateIdentifier, t.identifier("Component")), t.callExpression( importDefault( file, "marko/src/runtime/components/defineComponent.js", "marko_defineComponent", ), [componentIdentifier, templateRendererMember], ), ), ), ); } if (includeMetaInSource) { const metaObject = t.objectExpression([ t.objectProperty(t.identifier("id"), componentTypeIdentifier), ]); if (meta.component) { metaObject.properties.push( t.objectProperty( t.identifier("component"), t.stringLiteral(meta.component), ), ); } if (meta.deps.length) { metaObject.properties.push( t.objectProperty( t.identifier("deps"), parseExpression(file, JSON.stringify(meta.deps)), ), ); } if (meta.tags.length) { metaObject.properties.push( t.objectProperty( t.identifier("tags"), t.arrayExpression(meta.tags.map((tag) => t.stringLiteral(tag))), ), ); } path.pushContainer( "body", t.expressionStatement( t.assignmentExpression("=", templateMetaMember, metaObject), ), ); } optimizeHTMLWrites(path); }, }, }; export function getRuntimeEntryFiles(output, optimize) { const base = `marko/${optimize ? "dist" : "src"}/`; return [ `${base}runtime/components/index.js`, `${base}runtime/components/defineComponent.js`, `${base}runtime/components/renderer.js`, `${base}runtime/components/registry.js`, `${base}runtime/components/attach-detach.js`, `${base}runtime/helpers/assign.js`, `${base}runtime/helpers/class-value.js`, `${base}runtime/helpers/of-fallback.js`, `${base}runtime/helpers/dynamic-tag.js`, `${base}runtime/helpers/attr-tag.js`, `${base}runtime/helpers/merge.js`, `${base}runtime/helpers/render-tag.js`, `${base}runtime/helpers/style-value.js`, `${base}runtime/helpers/to-string.js`, `${base}runtime/helpers/empty-component.js`, `${base}core-tags/components/preserve-tag.js`, ...(output === "html" ? [ `${base}runtime/html/index.js`, `${base}runtime/html/hot-reload.js`, `${base}runtime/html/helpers/attr.js`, `${base}runtime/html/helpers/attrs.js`, `${base}runtime/html/helpers/class-attr.js`, `${base}runtime/html/helpers/data-marko.js`, `${base}runtime/html/helpers/escape-script-placeholder.js`, `${base}runtime/html/helpers/escape-style-placeholder.js`, `${base}runtime/html/helpers/escape-xml.js`, `${base}runtime/html/helpers/merge-attrs.js`, `${base}runtime/html/helpers/props-script.js`, `${base}runtime/html/helpers/style-attr.js`, `${base}core-tags/components/init-components-tag.js`, `${base}core-tags/components/preferred-script-location-tag.js`, `${base}core-tags/core/__flush_here_and_after__.js`, `${base}core-tags/core/await/renderer.js`, `${base}core-tags/core/await/reorderer-renderer.js`, `${base}runtime/helpers/skip-serialize.js`, `${base}runtime/helpers/tags-compat/html${optimize ? "" : "-debug"}.mjs`, ] : [ `${base}runtime/vdom/index.js`, `${base}runtime/vdom/hot-reload.js`, `${base}runtime/vdom/helpers/attrs.js`, `${base}runtime/vdom/helpers/const-element.js`, `${base}runtime/helpers/tags-compat/dom${optimize ? "" : "-debug"}.mjs`, ]), ]; } function isRenderContent({ node }) { return /^Marko/.test(node.type) && !node.static; } function resolveRelativeTagEntry(file, tagDef) { // TODO: support transform and other entries. const entry = tagDef.template || tagDef.renderer; return entry && resolveRelativePath(file, entry); }