From 4bb6fbb05a939cb353ce368469e1d31dc7579062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=82=92=E7=B1=B3=E7=B2=89?= <970451823@qq.com> Date: Mon, 3 Jun 2024 15:49:39 +0800 Subject: [PATCH] Feat/auto loader (#140) * feat: prerelease autoloader * feat: prerelease autoloader * feat: make autoloader covers shadow dom * feat: make autoloader covers shadow dom * feat: autoloader for banana --- .changeset/tender-pianos-prove.md | 6 ++ packages/banana/src/banana-autoloader.ts | 88 ++++++++++++++++++++++++ rollup.config.mjs | 22 +++++- tsconfig.json | 1 + 4 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 .changeset/tender-pianos-prove.md create mode 100644 packages/banana/src/banana-autoloader.ts diff --git a/.changeset/tender-pianos-prove.md b/.changeset/tender-pianos-prove.md new file mode 100644 index 00000000..63afa8b3 --- /dev/null +++ b/.changeset/tender-pianos-prove.md @@ -0,0 +1,6 @@ +--- +'@banana-ui/banana': minor +'@banana-ui/react': minor +--- + +Autoloader for banana. diff --git a/packages/banana/src/banana-autoloader.ts b/packages/banana/src/banana-autoloader.ts new file mode 100644 index 00000000..b42bf994 --- /dev/null +++ b/packages/banana/src/banana-autoloader.ts @@ -0,0 +1,88 @@ +// Inpired by https://github.com/shoelace-style/shoelace/blob/next/src/shoelace-autoloader.ts + +function getBasePath(subpath = '') { + const scripts = [...document.getElementsByTagName('script')] as HTMLScriptElement[]; + const autoloader = scripts.find((script) => script.src.includes('banana-autoloader')) as HTMLScriptElement; + const basePath = autoloader.src.split('/').slice(0, -1).join('/'); + + // Return the base path without a trailing slash. If one exists, append the subpath separated by a slash. + return basePath.replace(/\/$/, '') + (subpath ? `/${subpath.replace(/^\//, '')}` : ``); +} + +const observer = new MutationObserver((mutations) => { + for (const { addedNodes } of mutations) { + for (const node of addedNodes) { + if (node.nodeType === Node.ELEMENT_NODE) { + void discover(node as Element); + } + } + } +}); + +/** + * Checks a node for undefined elements and attempts to register them. + */ +async function discover(root: Element | ShadowRoot) { + if (!root) return; + + const rootTagName = root instanceof Element ? root.tagName.toLowerCase() : ''; + const rootIsBananaElement = rootTagName?.toLowerCase().startsWith('b-'); + const rootIsCustomElement = rootTagName?.includes('-'); + + const tags = [...root.querySelectorAll(':not(:defined)')] + .map((el) => el.tagName.toLowerCase()) + .filter((tag) => tag.startsWith('b-')); + + // If the root element is an undefined Banana component, add it to the list + if (rootIsBananaElement && !customElements.get(rootTagName)) { + tags.push(rootTagName); + } + + // Make the list unique + const tagsToRegister = [...new Set(tags)]; + + const notBananaCustomElements = [...root.querySelectorAll('*')].filter( + (el) => el.tagName.includes('-') && !el.tagName.toLowerCase().startsWith('b-'), + ); + + // If the root element is a custom element and not a Banana component, add it to the list + if (rootIsCustomElement && !rootIsBananaElement && root instanceof Element) { + notBananaCustomElements.push(root); + } + + // Discover any shadow roots + const customElementsPromises = notBananaCustomElements.map((el) => { + return customElements.whenDefined(el.tagName); + }); + void Promise.allSettled(customElementsPromises).then(() => { + notBananaCustomElements.forEach((el) => { + if (el.shadowRoot) void discover(el.shadowRoot); + }); + }); + + await Promise.allSettled(tagsToRegister.map((tagName) => register(tagName))); +} + +/** + * Registers an element by tag name. + */ +function register(tagName: string): Promise { + // If the element is already defined, there's nothing more to do + if (customElements.get(tagName)) { + return Promise.resolve(); + } + + const tagWithoutPrefix = tagName.replace(/^b-/i, ''); + const path = getBasePath(`${tagWithoutPrefix}/index.js`); + + // Register it + return new Promise((resolve, reject) => { + import(path).then(() => resolve()).catch(() => reject(new Error(`Unable to autoload <${tagName}> from ${path}`))); + }); +} + +// Initial discovery +void discover(document.body); + +// Listen for new undefined elements +observer.observe(document.documentElement, { subtree: true, childList: true }); diff --git a/rollup.config.mjs b/rollup.config.mjs index 34bc66ce..2f9d1322 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -42,7 +42,7 @@ const UMDName = process .pop() .replace(/(^|-)(\w)/g, (_, _$1, $2) => $2.toUpperCase()); -export default [ +const rollupConfig = [ { input: './src/index.ts', output: { @@ -105,3 +105,23 @@ export default [ ], }, ]; + +// 如果存在./src/autoloader.ts,则添加到rollupConfig中 +if (fs.existsSync('./src/banana-autoloader.ts')) { + rollupConfig.push({ + input: './src/banana-autoloader.ts', + output: { + dir: 'dist', + format: 'es', + entryFileNames: 'banana-autoloader.js', + }, + plugins: [ + typescript(), + nodeResolve({ + extensions: ['.ts', '.js'], + }), + ], + }); +} + +export default rollupConfig; diff --git a/tsconfig.json b/tsconfig.json index b504b8d2..5a66f307 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "experimentalDecorators": true, "useDefineForClassFields": false, "jsx": "react-jsx", + "module": "esnext", "moduleResolution": "node", "baseUrl": "./", "paths": {