├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── scripts └── deploy-to-github.sh ├── src ├── TreeView.jsx └── demo │ ├── App.jsx │ ├── index.html │ └── index.jsx └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # solidjs-treeview-component 2 | 3 | usage: [src/demo/App.jsx](src/demo/App.jsx) 4 | 5 | live demo: [https://milahu.github.io/solidjs-treeview-component/](https://milahu.github.io/solidjs-treeview-component/) 6 | 7 | ## aria spec 8 | 9 | https://www.w3.org/TR/wai-aria-practices/#TreeView 10 | 11 | > A tree view widget presents a hierarchical list. 12 | > Any item in the hierarchy may have child items, 13 | > and items that have children 14 | > may be expanded or collapsed to show or hide the children. 15 | > 16 | > For example, in a file system navigator 17 | > that uses a tree view to display folders and files, 18 | > an item representing a folder 19 | > can be expanded to reveal the contents of the folder, 20 | > which may be files, folders, or both. 21 | 22 | ## other treeview components 23 | 24 | - https://github.com/aquaductape/solid-tree-view - a treeview component in solidjs 25 | - https://www.reddit.com/r/solidjs/comments/jitfj4/recreated_redux_tree_view_example_with_solid/ 26 | - https://svelte.dev/tutorial/svelte-self - a treeview component in svelte 27 | - https://github.com/mar10/fancytree - a treeview component in jQuery 28 | 29 | ## solidjs docs 30 | 31 | - https://github.com/solidjs/solid/discussions/499 - efficiently render tree structures, how to update nodes in tree 32 | - https://www.solidjs.com/tutorial/stores_nested_reactivity - One of the reasons for fine-grained reactivity in Solid is that it can handle nested updates independently 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solidjs-treeview-component", 3 | "version": "0.0.1", 4 | "description": "TreeView component for SolidJS", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build" 8 | }, 9 | "module": "src/TreeView.jsx", 10 | "author": "milahu", 11 | "license": "CC0-1.0", 12 | "dependencies": { 13 | "solid-styled-components": "*" 14 | }, 15 | "devDependencies": { 16 | "solid-js": "*", 17 | "vite": "*", 18 | "vite-plugin-solid": "*" 19 | }, 20 | "keywords": [ 21 | "solid", 22 | "solid-js", 23 | "solidjs", 24 | "component", 25 | "treeview", 26 | "tree", 27 | "file-browser", 28 | "file-explorer", 29 | "file-manager" 30 | ], 31 | "homepage": "https://github.com/milahu/solidjs-treeview-component", 32 | "repository": { "type": "git", "url": "https://github.com/milahu/solidjs-treeview-component.git" }, 33 | "bugs": "https://github.com/milahu/solidjs-treeview-component/issues" 34 | } 35 | -------------------------------------------------------------------------------- /scripts/deploy-to-github.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e # exit on errors (dont continue script) 4 | set -o xtrace # print every command before exec 5 | 6 | hasOldFiles='true' 7 | createGhPages() { 8 | # based on https://gist.github.com/ramnathv/2227408 9 | git symbolic-ref HEAD refs/heads/gh-pages 10 | rm .git/index 11 | git clean -fdx 12 | hasOldFiles='false' 13 | } 14 | 15 | headCommit=$(git rev-parse --short HEAD) 16 | branchName=$(git branch --show-current) 17 | 18 | npm run build 19 | 20 | git switch gh-pages || createGhPages 21 | trap "git switch ${branchName}" EXIT # on error, return to last branch 22 | 23 | $hasOldFiles && rm -v *.js 24 | cp -v src/demo/dist/* ./ 25 | 26 | git add --verbose index.html *.js 27 | git commit --message="gh-pages: sync with ${branchName} ${headCommit}" --edit 28 | 29 | git push 30 | -------------------------------------------------------------------------------- /src/TreeView.jsx: -------------------------------------------------------------------------------- 1 | import { For, Show } from "solid-js"; 2 | 3 | // TODO add suspense: 'empty' vs 'loading ...' in
  • 4 | // TODO use array for prefix (then getting the depth is trivial, and we can remove depth from the data array) 5 | 6 | function EmptyNode(props) { 7 | return ( 8 |
  • 9 | {props.get.emptyLabel(props.prefix)} 10 |
  • 11 | ); 12 | } 13 | 14 | function LeafNode(props) { 15 | return ( 16 |
  • 17 | {props.get.leafLabel(props.node, props.prefix)} 18 |
  • 19 | ); 20 | } 21 | 22 | function BranchNode(props) { 23 | const classNameExpanded = () => props.classNameExpanded || 'expanded'; 24 | const prefix = () => props.get.path(props.node, props.prefix); 25 | const childNodes = () => props.get.childNodes(props.node); 26 | return ( 27 |
  • 28 |
    { 29 | // go up to nearest
  • 30 | let li = event.target; 31 | while (li && li.localName != 'li') li = li.parentNode; 32 | if (!li) throw { error: 'li not found', event }; 33 | li.classList.toggle(classNameExpanded()); 34 | if (li.classList.contains(classNameExpanded())) { 35 | props.load && props.load(props.node, props.prefix, props.get); 36 | } 37 | }}> 38 | {props.get.branchLabel(props.node, props.prefix)} 39 | 40 | {props.recurse({ 41 | ...props, 42 | data: childNodes(), 43 | prefix: prefix(), 44 | })} 45 |
  • 46 | ); 47 | } 48 | 49 | const TreeView = function thisComponent(props) { 50 | const classNameExpanded = 'expanded'; 51 | return ( 52 | 73 | ); 74 | } 75 | 76 | export default TreeView; 77 | -------------------------------------------------------------------------------- /src/demo/App.jsx: -------------------------------------------------------------------------------- 1 | import { onMount } from "solid-js"; 2 | import { createStore, unwrap } from "solid-js/store"; 3 | import { glob as globalStyle } from "solid-styled-components"; 4 | 5 | import TreeView from "../../"; // -> package.json -> module -> src/TreeView.jsx 6 | import pkg from "../../package.json"; 7 | 8 | // TODO better way to define style? 9 | var globalStyleString = ""; 10 | // we need `node.classList.toggle('expand')` 11 | // but we dont care about the exact class name 12 | var className = 'linux-find'; 13 | globalStyleString += ` 14 | .${className}.tree-view.root { margin-left: 1px; margin-right: 1px; } 15 | .${className}.tree-view.root { height: 100%; /* fit to container */; overflow: auto; /* scroll on demand */ } 16 | .${className}.tree-view { text-align: left; } 17 | .${className}.tree-view, 18 | .${className}.tree-view ul { list-style: none; padding: 0; } 19 | .${className}.tree-view li { white-space: pre; /* dont wrap on x overflow. TODO fix width on overflow */ } 20 | .${className}.tree-view li.branch > span { color: blue; font-family: monospace; } 21 | .${className}.tree-view li.branch > ul { display: none; /* default collapsed */ } 22 | .${className}.tree-view li.branch.expanded { outline: solid 1px grey; } 23 | .${className}.tree-view li.branch.expanded > ul { display: block; } 24 | .${className}.tree-view li.empty { font-style: italic; } 25 | .${className}.tree-view span.link-source { color: green; } 26 | .${className}.tree-view span.link-source, 27 | .${className}.tree-view div.branch-label, 28 | .${className}.tree-view span.file, 29 | .${className}.tree-view span.name, 30 | .${className}.tree-view span.prefix { font-family: monospace; cursor: pointer; } 31 | /* .${className}.tree-view span.prefix { opacity: 0.6; } */ /* this looks worse than expected */ 32 | `; 33 | 34 | var className = 'file-tree'; 35 | globalStyleString += ` 36 | .${className}.tree-view.root { margin-left: 1px; margin-right: 1px; } 37 | .${className}.tree-view.root { height: 100%; /* fit to container */; overflow: auto; /* scroll on demand */ } 38 | .${className}.tree-view { text-align: left; } 39 | .${className}.tree-view, 40 | .${className}.tree-view ul { list-style: none; padding: 0; } 41 | .${className}.tree-view ul { padding-left: 0.5em; margin-left: 0.5em; border-left: solid 1px grey; } 42 | .${className}.tree-view li { white-space: pre; /* dont wrap on x overflow. TODO fix width on overflow */ } 43 | .${className}.tree-view li.branch > span { color: blue; font-family: monospace; } 44 | .${className}.tree-view li.branch > ul { display: none; /* default collapsed */ } 45 | .${className}.tree-view li.branch.expanded { } 46 | .${className}.tree-view li.branch.expanded > ul { display: block; } 47 | .${className}.tree-view li.empty { font-style: italic; } 48 | .${className}.tree-view span.link-source { color: green; } 49 | .${className}.tree-view span.link-source, 50 | .${className}.tree-view div.branch-label, 51 | .${className}.tree-view span.file, 52 | .${className}.tree-view span.name { font-family: monospace; cursor: pointer; } 53 | `; 54 | 55 | // workaround: only one call to globalStyle 56 | globalStyle(globalStyleString); 57 | 58 | export default function App() { 59 | 60 | const sleep = ms => new Promise(res => setTimeout(res, ms)); 61 | 62 | const [state, setState] = createStore({ 63 | fileList: [], 64 | fileSelected: '', 65 | }); 66 | 67 | onMount(() => { 68 | loadFiles(); 69 | }); 70 | 71 | //const rootPath = ""; 72 | const rootPath = "/"; // needed for fs.readdir 73 | 74 | async function loadFiles(node = null, prefix = '', get = null) { 75 | console.log("loadFiles node", unwrap(node)); 76 | const path = (node && get) ? get.path(node, prefix) : rootPath; 77 | 78 | const keyPath = ['fileList']; 79 | const childNodesIdx = 3; 80 | let parentDir = state.fileList; 81 | console.log(`loadFiles build keyPath. prefix "${prefix}" + path "${path}"`); 82 | path.split('/').filter(Boolean).forEach((d, di) => { 83 | const i = parentDir.findIndex(([ depth, type, file, arg ]) => (type == 'd' && file == d)); 84 | console.log(`loadFiles build keyPath. depth ${di}`, { parentDir, i, d }); 85 | keyPath.push(i); parentDir = parentDir[i]; 86 | keyPath.push(childNodesIdx); parentDir = parentDir[childNodesIdx]; 87 | }); 88 | 89 | //console.dir({ prefix, keyPath, val: state(...keyPath) }) 90 | //console.dir({ prefix, keyPath, parentDir }) 91 | 92 | if (parentDir.length > 0) { 93 | console.log(`already loaded path "${path}"`); 94 | return; // already loaded 95 | }; 96 | 97 | /* 98 | // load files from API server 99 | const dataObject = { path }; 100 | const postOptions = data => ({ 101 | method: 'POST', body: JSON.stringify(data), 102 | headers: { 'Content-Type': 'application/json' } 103 | }); 104 | const response = await fetch(`/api/list`, postOptions(dataObject)); 105 | if (!response.ok) { console.log(`http request error ${response.status}`); return; } 106 | const responseData = await response.json(); 107 | //console.dir(responseData.files); 108 | */ 109 | // mock the server response 110 | await sleep(500); // loading ... 111 | const depth = path.split('/').filter(Boolean).length; 112 | console.log(`loadFiles path = "${path}" + depth "${depth}" + prefix "${prefix}"`); 113 | const responseData = { 114 | files: Array.from({ length: 5 }).map((_, idx) => { 115 | const typeList = 'dddfl'; // dir, file, link 116 | const type = typeList[Math.round(Math.random() * (typeList.length - 1))]; 117 | if (type == 'd') return [ depth, type, `dirr-${depth}-${idx}`, [] ]; 118 | if (type == 'f') return [ depth, type, `file-${depth}-${idx}` ]; 119 | if (type == 'l') return [ depth, type, `link-${depth}-${idx}`, `link-target-${depth}-${idx}` ]; 120 | }), 121 | } 122 | /* 123 | // load files from fs 124 | // TODO update on changes in fs. inotify? 125 | const depth = path.split('/').filter(Boolean).length; 126 | console.log(`loadFiles path = "${path}" + depth "${depth}" + prefix "${prefix}"`); 127 | const dirFiles = await fs.promises.readdir(path || "/"); 128 | console.log("dirFiles", dirFiles); 129 | const responseData = { 130 | files: await Promise.all(dirFiles.map(async (fileName) => { 131 | const filePath = path + "/" + fileName; 132 | const stats = await fs.promises.stat(filePath); 133 | if (stats.isDirectory()) { 134 | return [ depth, "d", fileName, [] ]; 135 | } 136 | else if (stats.isSymbolicLink()) { 137 | const linkTarget = await fs.promises.readlink(filePath); 138 | return [ depth, "l", fileName, linkTarget ]; 139 | } 140 | else { 141 | return [ depth, "f", fileName ]; 142 | } 143 | })), 144 | }; 145 | */ 146 | 147 | // add new files to the app state 148 | if (!state.fileList || state.fileList.length == 0) 149 | setState('fileList', responseData.files); // init 150 | else { 151 | //console.log(`add files for path ${path}`) 152 | setState(...keyPath, responseData.files); 153 | } 154 | } 155 | 156 | function fileListGetters() { 157 | const get = {}; 158 | get.isLeaf = node => (node[1] != 'd'); 159 | //get.name = node => node[2]; 160 | // append slash to directory names 161 | get.name = node => node[2] + ((node[1] == 'd') ? "/" : ""); 162 | get.path = (node, prefix) => (prefix || rootPath) + get.name(node); 163 | get.childNodes = node => node[3]; 164 | const fancyPath = (node, prefix) => ( 165 | prefix ? <> 166 | {(() => prefix)()} 167 | {get.name(node)} 168 | : (rootPath + get.name(node)) 169 | ); 170 | get.branchLabel = fancyPath; 171 | get.emptyLabel = (prefix) => '( empty )'; 172 | const isLink = node => (node[1] == 'l'); 173 | const linkTarget = node => node[3]; 174 | const getSelectFile = (node, prefix) => () => setState('fileSelected', get.path(node, prefix)); 175 | get.leafLabel = (node, prefix) => { 176 | if (isLink(node)) 177 | return <> 178 | {fancyPath(node, prefix)}{" -> "} 179 | {linkTarget(node)} 180 | ; 181 | return {fancyPath(node, prefix)}; 182 | }; 183 | return get; 184 | } 185 | 186 | function fileTreeGetters() { 187 | const get = {}; 188 | get.isLeaf = node => (node[1] != 'd'); 189 | //get.name = node => node[2]; 190 | // append slash to directory names 191 | get.name = node => node[2] + ((node[1] == 'd') ? "/" : ""); 192 | get.path = (node, prefix) => (prefix || rootPath) + get.name(node); 193 | get.childNodes = node => node[3]; 194 | get.emptyLabel = (_prefix) => '( empty )'; 195 | const isLink = node => (node[1] == 'l'); 196 | const linkTarget = node => node[3]; 197 | const simplePath = (node, _prefix) => ( 198 | {get.name(node)} 199 | ); 200 | get.branchLabel = simplePath; 201 | const getSelectFile = (node, prefix) => () => setState('fileSelected', get.path(node, prefix)); 202 | get.leafLabel = (node, prefix) => { 203 | if (isLink(node)) 204 | return <> 205 | {simplePath(node, prefix)}{" -> "} 206 | {linkTarget(node)} 207 | ; 208 | return {simplePath(node, prefix)}; 209 | }; 210 | return get; 211 | } 212 | 213 | function fileListFilter() { 214 | return node => (node[2][0] != '.'); // hide dotfiles 215 | } 216 | 217 | return ( 218 |
    219 |

    demo for {pkg.name}

    220 |
    source code: {pkg.homepage}
    221 |
    click on a directory to load more files
    222 |
    click on a file to select it. selected file: {state.fileSelected ? {state.fileSelected} : '( none )'}
    223 |

    file tree, show only file names

    224 |
    {/* TODO use full height of browser window */} 225 | 232 |
    233 |

    directory listing, show full file path, similar to the linux command find -printf '%P\\n'

    234 |
    {/* TODO use full height of browser window */} 235 | 242 |
    243 |
    244 | ); 245 | } 246 | -------------------------------------------------------------------------------- /src/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | solidjs-treeview-component (demo) 8 | 9 | 10 | 11 |
    12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/demo/index.jsx: -------------------------------------------------------------------------------- 1 | import { render } from "solid-js/web"; 2 | 3 | //import "./index.css"; 4 | import App from "./App.jsx"; 5 | 6 | render(App, document.getElementById("root")); 7 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import solidPlugin from "vite-plugin-solid"; 3 | 4 | const assetsDir = ''; 5 | //const assetsDir = 'assets/'; 6 | 7 | const outputDefaults = { 8 | // remove hashes from filenames 9 | entryFileNames: `${assetsDir}[name].js`, 10 | chunkFileNames: `${assetsDir}[name].js`, 11 | assetFileNames: `${assetsDir}[name].[ext]`, 12 | } 13 | 14 | export default defineConfig({ 15 | 16 | plugins: [ 17 | solidPlugin(), 18 | ], 19 | root: "src/demo", 20 | base: "./", // relative paths for github-pages 21 | build: { 22 | target: "esnext", 23 | polyfillDynamicImport: false, 24 | outDir: "dist", 25 | emptyOutDir: true, // force clean 26 | rollupOptions: { 27 | output: { 28 | ...outputDefaults, 29 | } 30 | }, 31 | }, 32 | server: { 33 | //open: true, 34 | }, 35 | clearScreen: false, 36 | }); 37 | --------------------------------------------------------------------------------