├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github └── workflows │ ├── deploy.yml │ └── release.js.yml ├── .gitignore ├── .gitpod.Dockerfile ├── .gitpod.yml ├── .husky ├── .gitignore └── commit-msg ├── .npmrc ├── .nvmrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── env.js ├── gulpfile.js ├── package.json ├── plugins ├── config-dts.js ├── sass.js ├── wasm │ ├── index.js │ ├── module.js │ └── wasm.js └── worker.js ├── pnpm-lock.yaml ├── repl.ts ├── src ├── assets │ ├── BingSiteAuth.xml │ ├── _headers │ ├── badge-dark.svg │ ├── badge-light.svg │ ├── bundle.svg │ ├── dprint-typescript-plugin.wasm │ ├── favicon │ │ ├── android-144x144.png │ │ ├── android-192x192.png │ │ ├── android-36x36.png │ │ ├── android-48x48.png │ │ ├── android-72x72.png │ │ ├── android-96x96.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── android-chrome-maskable-192x192.png │ │ ├── android-chrome-maskable-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-api.ico │ │ ├── favicon.ico │ │ ├── favicon.svg │ │ ├── maskable_icon_x192.png │ │ ├── maskable_icon_x512.png │ │ ├── safari-pinned-tab.svg │ │ ├── screenshot-bundled.png │ │ ├── screenshot-desktop.png │ │ ├── screenshot-error.png │ │ ├── screenshot-light-mode.png │ │ ├── screenshot-mobile.png │ │ ├── screenshot-poster.png │ │ └── social-preview.png │ ├── manifest.json │ ├── open-search.xml │ ├── robots.txt │ ├── sponsors │ │ ├── upstash-long.svg │ │ ├── upstash.svg │ │ ├── vercel-long.svg │ │ └── vercel.svg │ └── wasm_brotli_browser_bg.wasm ├── css │ ├── app.scss │ ├── codicon.scss │ ├── components │ │ ├── _highlightjs.scss │ │ ├── _monaco.scss │ │ ├── _navbar.scss │ │ ├── _search.scss │ │ ├── _variables.scss │ │ └── fonts │ │ │ ├── _manrope.scss │ │ │ └── _material-icons.scss │ └── fonts.scss ├── missing-types.d.ts ├── pug │ ├── 404.pug │ ├── about.pug │ ├── components │ │ ├── footer.pug │ │ ├── navbar.pug │ │ ├── product-hunt.pug │ │ └── seo.pug │ ├── faq.pug │ ├── index.pug │ └── layouts │ │ └── layout.pug ├── ts │ ├── components │ │ ├── Console.tsx │ │ └── SearchResults.tsx │ ├── configs │ │ └── bundle-options.ts │ ├── deno │ │ ├── base64 │ │ │ └── mod.ts │ │ ├── brotli │ │ │ ├── mod.ts │ │ │ └── wasm.js │ │ ├── denoflate │ │ │ ├── mod.ts │ │ │ └── pkg │ │ │ │ ├── denoflate.d.ts │ │ │ │ ├── denoflate.js │ │ │ │ ├── denoflate_bg.wasm │ │ │ │ ├── denoflate_bg.wasm.d.ts │ │ │ │ └── denoflate_bg.wasm.js │ │ ├── lz4 │ │ │ ├── mod.ts │ │ │ └── wasm.js │ │ ├── media-types │ │ │ └── mod.ts │ │ ├── path │ │ │ ├── _constants.ts │ │ │ ├── _interface.ts │ │ │ ├── _util.ts │ │ │ ├── glob.ts │ │ │ ├── mod.ts │ │ │ └── posix.ts │ │ ├── tar │ │ │ ├── deno.json │ │ │ ├── mod.ts │ │ │ ├── tar_stream.ts │ │ │ ├── unstable_fixed_chunk_stream.ts │ │ │ └── untar_stream.ts │ │ └── zstd │ │ │ ├── deno.js │ │ │ ├── encoded.wasm.ts │ │ │ ├── mod.ts │ │ │ └── zstd.wasm │ ├── index.ts │ ├── main.ts │ ├── measure.ts │ ├── modules │ │ ├── details.ts │ │ ├── editor.all.ts │ │ └── monaco.ts │ ├── plugins │ │ ├── alias.ts │ │ ├── analyzer │ │ │ ├── index.ts │ │ │ ├── plugin │ │ │ │ ├── build-stats.ts │ │ │ │ ├── compress.ts │ │ │ │ ├── data.ts │ │ │ │ ├── index.ts │ │ │ │ └── module-mapper.ts │ │ │ ├── src │ │ │ │ ├── color.ts │ │ │ │ ├── network │ │ │ │ │ ├── chart.tsx │ │ │ │ │ ├── color.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── main.tsx │ │ │ │ │ ├── network.tsx │ │ │ │ │ ├── tooltip.tsx │ │ │ │ │ └── util.ts │ │ │ │ ├── sidebar.tsx │ │ │ │ ├── sizes.ts │ │ │ │ ├── style │ │ │ │ │ ├── _base.scss │ │ │ │ │ ├── style-network.scss │ │ │ │ │ ├── style-sunburst.scss │ │ │ │ │ └── style-treemap.scss │ │ │ │ ├── sunburst │ │ │ │ │ ├── chart.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── main.tsx │ │ │ │ │ ├── node.tsx │ │ │ │ │ ├── sunburst.tsx │ │ │ │ │ └── tooltip.tsx │ │ │ │ ├── treemap │ │ │ │ │ ├── chart.tsx │ │ │ │ │ ├── color.ts │ │ │ │ │ ├── const.ts │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── main.tsx │ │ │ │ │ ├── node.tsx │ │ │ │ │ ├── tooltip.tsx │ │ │ │ │ └── treemap.tsx │ │ │ │ ├── uid.ts │ │ │ │ └── use-filter.ts │ │ │ ├── tsconfig.json │ │ │ ├── types │ │ │ │ ├── metafile.d.ts │ │ │ │ ├── rollup.d.ts │ │ │ │ ├── template-types.ts │ │ │ │ └── types.d.ts │ │ │ └── utils │ │ │ │ └── is-module-tree.ts │ │ ├── cdn.ts │ │ ├── external.ts │ │ ├── http.ts │ │ └── virtual-fs.ts │ ├── register-sw.ts │ ├── services │ │ └── Navbar.ts │ ├── theme.ts │ ├── util │ │ ├── WebWorker.ts │ │ ├── ansi.ts │ │ ├── brotli-wasm.js │ │ ├── debounce.ts │ │ ├── deep-equal.ts │ │ ├── encode-decode.ts │ │ ├── fetch-and-cache.ts │ │ ├── filesystem.ts │ │ ├── github-dark.ts │ │ ├── github-light.ts │ │ ├── loader.ts │ │ ├── npm-search.ts │ │ ├── parse-query.ts │ │ ├── path.ts │ │ ├── pretty-bytes.ts │ │ ├── rollup.ts │ │ ├── semver.ts │ │ ├── serialize-javascript.ts │ │ ├── util-cdn.ts │ │ └── worker-init.ts │ └── workers │ │ ├── editor.ts │ │ ├── empty.ts │ │ ├── esbuild.ts │ │ ├── json.ts │ │ ├── sandbox.ts │ │ ├── ts-worker-factory.ts │ │ └── typescript.ts └── typescript-worker.d.ts ├── tailwind.config.cjs ├── tsconfig.json ├── util.js └── vercel.json /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.192.0/containers/typescript-node/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Node.js version: 16, 14, 12 4 | ARG VARIANT="16-buster" 5 | FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT} 6 | 7 | # [Optional] Uncomment this section to install additional OS packages. 8 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 9 | # && apt-get -y install --no-install-recommends 10 | 11 | # [Optional] Uncomment if you want to install an additional version of node using nvm 12 | # ARG EXTRA_NODE_VERSION=10 13 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 14 | 15 | # [Optional] Uncomment if you want to install more global node packages 16 | # RUN su node -c "npm install -g " 17 | RUN su node -c "npm install -g pnpm gulp" 18 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.192.0/containers/typescript-node 3 | { 4 | "name": "Node.js & TypeScript", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | // Update 'VARIANT' to pick a Node version: 12, 14, 16 8 | "args": { 9 | "VARIANT": "16" 10 | } 11 | }, 12 | 13 | // Set *default* container specific settings.json values on container create. 14 | "settings": {}, 15 | 16 | 17 | // Add the IDs of extensions you want installed when the container is created. 18 | "extensions": [ 19 | "vivaxy.vscode-conventional-commits" 20 | ], 21 | 22 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 23 | // "forwardPorts": [], 24 | 25 | // Use 'postCreateCommand' to run commands after the container is created. 26 | "postCreateCommand": "pnpm install", 27 | 28 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 29 | "remoteUser": "node" 30 | } -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | branches: main 5 | pull_request: 6 | branches: main 7 | 8 | jobs: 9 | deploy: 10 | name: Deploy 11 | runs-on: ubuntu-latest 12 | 13 | permissions: 14 | id-token: write # Needed for auth with Deno Deploy 15 | contents: read # Needed to clone the repository 16 | 17 | steps: 18 | - name: Clone repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Install Deno 22 | uses: denoland/setup-deno@v1 23 | with: 24 | deno-version: v1.x 25 | 26 | - name: Install Node.js 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: lts/* 30 | 31 | - name: Install step 32 | run: "pnpm install" 33 | 34 | - name: Build step 35 | run: "deno task build:wasm" 36 | 37 | - name: Upload to Deno Deploy 38 | uses: denoland/deployctl@v1 39 | with: 40 | project: "bundlejs" 41 | entrypoint: "packages/edge/mod.ts" 42 | root: "" 43 | -------------------------------------------------------------------------------- /.github/workflows/release.js.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: workflow_dispatch 3 | 4 | jobs: 5 | release: 6 | name: Release 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [16.x] 12 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | with: 18 | persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal token 19 | fetch-depth: 0 # otherwise, you will failed to push refs to dest repo 20 | 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - name: Cache .pnpm-store 27 | uses: actions/cache@v1 28 | with: 29 | path: ~/.pnpm-store 30 | key: ${{ runner.os }}-node${{ matrix.node-version }}-${{ hashFiles('**/pnpm-lock.yaml') }} 31 | 32 | - name: Install pnpm 33 | run: curl -f https://get.pnpm.io/v6.js | node - add --global pnpm@6 34 | 35 | - name: Install dependencies 36 | run: pnpm install 37 | 38 | - name: Release 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | run: pnpm semantic-release 43 | 44 | - name: Push changes 45 | if: ${{ github.ref == 'refs/heads/main' }} 46 | uses: ad-m/github-push-action@master 47 | with: 48 | github_token: ${{ secrets.GITHUB_TOKEN }} 49 | branch: ${{ github.ref }} 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Jest 107 | coverage/ 108 | 109 | # Ultra 110 | .ultra.cache.json 111 | 112 | # VSCode 113 | # .vscode/ 114 | 115 | # Theia 116 | .theia/ 117 | docs/ 118 | dist 119 | 120 | # Yarn 121 | .yarn/* 122 | !.yarn/releases 123 | !.yarn/plugins 124 | !.yarn/sdks 125 | !.yarn/versions 126 | .pnp.* 127 | 128 | # dependencies 129 | .snowpack/ 130 | 131 | # macOS-specific files 132 | .DS_Store 133 | .vercel 134 | -------------------------------------------------------------------------------- /.gitpod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-full:latest 2 | 3 | # Install custom tools, runtime, etc. using apt-get 4 | # For example, the command below would install "bastet" - a command line tetris clone: 5 | # 6 | # RUN sudo apt-get -q update && # sudo apt-get install -yq bastet && # sudo rm -rf /var/lib/apt/lists/* 7 | # 8 | # More information: https://www.gitpod.io/docs/config-docker/ 9 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: .gitpod.Dockerfile 3 | 4 | # List the ports you want to expose and what to do when they are served. See https://www.gitpod.io/docs/43_config_ports/ 5 | ports: 6 | - port: 3000 7 | onOpen: ignore 8 | - port: 3001 9 | onOpen: ignore 10 | 11 | # List the start up tasks. You can start them in parallel in multiple terminals. See https://www.gitpod.io/docs/44_config_start_tasks/ 12 | tasks: 13 | - init: > 14 | nvm install && 15 | npm install -g pnpm && 16 | pnpm setup && 17 | pnpm install -g gulp commitizen && 18 | pnpm install 19 | command: > 20 | nvm install && 21 | npm install -g pnpm && 22 | pnpm setup && 23 | pnpm install -g gulp commitizen && 24 | pnpm watch 25 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ## force pnpm to hoist 2 | shamefully-hoist = true 3 | @jsr:registry=https://npm.jsr.io 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "scss.lint.unknownAtRules": "ignore", 3 | "scss.lint.unknownProperties": "ignore", 4 | "scss.validate": false, 5 | "conventionalCommits.scopes": [ 6 | "demo", 7 | "docs" 8 | ], 9 | "typescript.tsdk": "node_modules/typescript/lib" 10 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Okiki Ojo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /env.js: -------------------------------------------------------------------------------- 1 | export const ENABLE_ALL = false; 2 | export const ENABLE_SW = true; // ENABLE_ALL ?? 3 | export const USE_SHAREDWORKER = false; // ENABLE_ALL ?? true 4 | export const PRODUCTION_MODE = true; // ENABLE_ALL ?? 5 | -------------------------------------------------------------------------------- /plugins/config-dts.js: -------------------------------------------------------------------------------- 1 | // Based on https://medium.com/@martin_hotell/typescript-library-tips-rollup-your-types-995153cc81c7 2 | // ^ This was a great help 3 | import { rollup } from 'rollup'; 4 | import dts from 'rollup-plugin-dts'; 5 | 6 | // optionally pass a base path 7 | const basePath = "."; 8 | 9 | let cacheResult; 10 | const generateDTS = async () => { 11 | if (cacheResult) return cacheResult; 12 | 13 | const bundle = await rollup({ 14 | // path to your declaration files root 15 | input: `${basePath}/src/ts/configs/bundle-options.ts`, // (await files).flat(), 16 | // treeshake: true, 17 | plugins: [dts({ 18 | respectExternal: true 19 | })], 20 | }); 21 | 22 | const { output } = await bundle.generate({ 23 | file: 'dist/js/config.d.ts', 24 | format: 'es' 25 | }); 26 | 27 | const result = output?.[0]?.code; 28 | cacheResult = result; 29 | return result; 30 | }; 31 | 32 | export const CONFIG_DTS = () => { 33 | /** 34 | * @type {import('esbuild').Plugin} 35 | */ 36 | return { 37 | name: "config-dts", 38 | setup(build) { 39 | build.onResolve({ filter: /^dts\:/ }, async (args) => { 40 | // Feel free to remove this logline once you verify that the plugin works for your setup 41 | console.debug( 42 | `The \`config-dts\` plugin matched an import to ${args.path} from ${args.importer}` 43 | ); 44 | return { 45 | path: args.path.replace(/^dts\:/, ""), 46 | namespace: "config-dts", 47 | pluginData: { importer: args.importer, dts: await generateDTS() }, 48 | }; 49 | }); 50 | 51 | build.onLoad( 52 | { filter: /.*/, namespace: "config-dts" }, 53 | async (args) => { 54 | return { 55 | contents: ` 56 | // This file is generated by esbuild to expose the schema script as a class, like Webpack's schema-loader 57 | export default ${JSON.stringify(args.pluginData.dts)}`, 58 | }; 59 | } 60 | ); 61 | }, 62 | }; 63 | }; -------------------------------------------------------------------------------- /plugins/sass.js: -------------------------------------------------------------------------------- 1 | import * as sass from "sass"; 2 | 3 | export const SASS = () => { 4 | /** 5 | * @type {import('esbuild').Plugin} 6 | */ 7 | return { 8 | name: "sass", 9 | setup(build) { 10 | build.onLoad({ filter: /\.scss$/ }, async (args) => { 11 | const result = await sass.compileAsync(args.path, { style: "compressed" }); 12 | return { 13 | contents: result.css.toString("utf-8"), 14 | loader: "css", 15 | }; 16 | }); 17 | }, 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /plugins/wasm/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { generateWasmModule } from './module.js'; 4 | const WASM_MODULE_NAMESPACE = 'wasm-module'; 5 | const WASM_DEFERRED_NAMESPACE = 'wasm-deferred'; 6 | const WASM_EMBEDDED_NAMESPACE = 'wasm-embedded'; 7 | /** 8 | * Loads `.wasm` files as a js module. 9 | */ 10 | function wasmLoader(options) { 11 | var _a; 12 | const embed = ((_a = options === null || options === void 0 ? void 0 : options.mode) === null || _a === void 0 ? void 0 : _a.toLowerCase()) == 'embedded'; 13 | return { 14 | name: 'wasm', 15 | setup(build) { 16 | // Catch "*.wasm" files in the resolve phase and redirect them to our custom namespaces 17 | build.onResolve({ filter: /\.(?:wasm)$/ }, (args) => { 18 | // If it's already in the virtual module namespace, redirect to the file loader 19 | if (args.namespace === WASM_MODULE_NAMESPACE) { 20 | return { path: args.path, namespace: embed ? WASM_EMBEDDED_NAMESPACE : WASM_DEFERRED_NAMESPACE }; 21 | } 22 | // Ignore unresolvable paths 23 | if (args.resolveDir === '') 24 | return; 25 | // Redirect to the virtual module namespace 26 | return { 27 | path: path.isAbsolute(args.path) ? args.path : path.join(args.resolveDir, args.path), 28 | namespace: WASM_MODULE_NAMESPACE, 29 | }; 30 | }); 31 | // For virtual module loading, build a virtual module for the wasm file 32 | build.onLoad({ filter: /.*/, namespace: WASM_MODULE_NAMESPACE }, async (args) => { 33 | return { 34 | contents: await generateWasmModule(args.path), 35 | resolveDir: path.dirname(args.path) 36 | }; 37 | }); 38 | // For deffered file loading, get the wasm binary data and pass it to esbuild's built-in `file` loader 39 | build.onLoad({ filter: /.*/, namespace: WASM_DEFERRED_NAMESPACE }, async (args) => ({ 40 | contents: await fs.promises.readFile(args.path), 41 | loader: 'file' 42 | })); 43 | // For embedded file loading, get the wasm binary data and pass it to esbuild's built-in `binary` loader 44 | build.onLoad({ filter: /.*/, namespace: WASM_EMBEDDED_NAMESPACE }, async (args) => ({ 45 | contents: await fs.promises.readFile(args.path), 46 | loader: 'binary' 47 | })); 48 | } 49 | }; 50 | } 51 | export { wasmLoader, wasmLoader as WASM, wasmLoader as default }; -------------------------------------------------------------------------------- /plugins/wasm/wasm.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | /** 3 | * Gets metadata for a WASM binary. 4 | * @param path The path to the WASM binary. 5 | * @returns Metadata for the WASM binary. 6 | */ 7 | export async function getWasmMetadata(path) { 8 | const module = await WebAssembly.compile(await fs.promises.readFile(path)); 9 | // console.log(WebAssembly.Module.imports(module)) 10 | return { 11 | imports: WebAssembly.Module.imports(module), 12 | exports: WebAssembly.Module.exports(module) 13 | }; 14 | } -------------------------------------------------------------------------------- /src/assets/BingSiteAuth.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 563BCF2F5A52416328FB9ABFA1959AEE 4 | -------------------------------------------------------------------------------- /src/assets/_headers: -------------------------------------------------------------------------------- 1 | /* 2 | X-Frame-Options: SAMEORIGIN 3 | X-Content-Type-Options: nosniff 4 | X-XSS-Protection: 1; mode=block 5 | Referrer-Policy: strict-origin-when-cross-origin 6 | Strict-Transport-Security: max-age=63072000; includeSubDomains; preload 7 | Cache-Control: max-age=480, stale-while-revalidate=360, public 8 | Accept-CH: DPR, Viewport-Width, Width 9 | X-UA-Compatible: IE=edge 10 | Content-Security-Policy: default-src 'self'; font-src 'self' https://fonts.gstatic.com; style-src 'self' 'unsafe-inline'; img-src 'self' https://res.cloudinary.com https://api.producthunt.com https://opencollective.com https://*.bundlejs.com https://bundlejs.com https://bundlesize.com data:; script-src 'self' https://opencollective.com https://*.bundlejs.com https://bundlejs.com https://bundlesize.com 'unsafe-eval' 'unsafe-inline'; connect-src 'self' https:; block-all-mixed-content; upgrade-insecure-requests; base-uri 'self'; object-src 'none'; worker-src 'self'; manifest-src 'self'; media-src 'self' https://res.cloudinary.com; form-action 'self'; frame-src 'self' https://opencollective.com https://*.bundlejs.com https://bundlejs.com https://bundlesize.com; frame-ancestors 'self' https:; 11 | Permissions-Policy: geolocation=(), microphone=(), usb=(), sync-xhr=(self), camera=(), browsing-topics=(), join-ad-interest-group=(), run-ad-auction=() 12 | Link: ; rel=modulepreload; importance=high 13 | 14 | /*.html 15 | Link: ; rel=preload; as=style 16 | Link: ; rel=modulepreload; importance=high 17 | # Link: ; rel=modulepreload; importance=high 18 | # Link: ; rel=modulepreload 19 | 20 | / 21 | Link: ; rel=modulepreload; importance=high 22 | Link: ; rel=modulepreload 23 | Link: ; rel=preload; as=font; type=font/ttf; importance=low; crossorigin 24 | Link: ; rel=prefetch 25 | Link: ; rel=prefetch; importance=high 26 | Link: ; rel=modulepreload 27 | 28 | /*.css 29 | Cache-Control: public, max-age=4800, stale-while-revalidate=480 30 | Content-Type: text/css 31 | 32 | /js/*.ttf 33 | Cache-Control: public, max-age=4800, stale-while-revalidate=480 34 | Content-Type: font/ttf 35 | 36 | /css/fonts/*.woff2 37 | Cache-Control: public, max-age=4800, stale-while-revalidate=480 38 | Content-Type: font/woff2 39 | 40 | /*.js 41 | Cache-Control: public, max-age=4800, stale-while-revalidate=480 42 | Content-Type: text/javascript 43 | 44 | /js/sw.js 45 | Cache-Control: public, max-age=60, stale-while-revalidate=480 46 | Content-Type: text/javascript 47 | 48 | /js/*.worker.js 49 | Cache-Control: public, max-age=4800, stale-while-revalidate=480 50 | Content-Type: text/javascript 51 | 52 | /js/*.min.js 53 | Cache-Control: public, max-age=4800, stale-while-revalidate=480 54 | Content-Type: text/javascript 55 | 56 | /js/esbuild.wasm 57 | Cache-Control: public, max-age=4800, stale-while-revalidate=480 58 | Content-Type: wasm 59 | 60 | /js/theme.min.js 61 | Cache-Control: public, max-age=4800, stale-while-revalidate=480 62 | Content-Type: text/javascript 63 | 64 | /manifest.json 65 | Cache-Control: public, max-age=4800, stale-while-revalidate=480 66 | Content-Type: application/manifest+json 67 | 68 | /favicon/* 69 | Cache-Control: public, max-age=4800, stale-while-revalidate=480 70 | 71 | /*.svg 72 | Cache-Control: public, max-age=4800, stale-while-revalidate=480 73 | Content-Type: image/svg+xml 74 | -------------------------------------------------------------------------------- /src/assets/bundle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/dprint-typescript-plugin.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/dprint-typescript-plugin.wasm -------------------------------------------------------------------------------- /src/assets/favicon/android-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/android-144x144.png -------------------------------------------------------------------------------- /src/assets/favicon/android-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/android-192x192.png -------------------------------------------------------------------------------- /src/assets/favicon/android-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/android-36x36.png -------------------------------------------------------------------------------- /src/assets/favicon/android-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/android-48x48.png -------------------------------------------------------------------------------- /src/assets/favicon/android-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/android-72x72.png -------------------------------------------------------------------------------- /src/assets/favicon/android-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/android-96x96.png -------------------------------------------------------------------------------- /src/assets/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/assets/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/assets/favicon/android-chrome-maskable-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/android-chrome-maskable-192x192.png -------------------------------------------------------------------------------- /src/assets/favicon/android-chrome-maskable-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/android-chrome-maskable-512x512.png -------------------------------------------------------------------------------- /src/assets/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /src/assets/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/favicon/favicon-api.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/favicon-api.ico -------------------------------------------------------------------------------- /src/assets/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/favicon.ico -------------------------------------------------------------------------------- /src/assets/favicon/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/favicon/maskable_icon_x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/maskable_icon_x192.png -------------------------------------------------------------------------------- /src/assets/favicon/maskable_icon_x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/maskable_icon_x512.png -------------------------------------------------------------------------------- /src/assets/favicon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/favicon/screenshot-bundled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/screenshot-bundled.png -------------------------------------------------------------------------------- /src/assets/favicon/screenshot-desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/screenshot-desktop.png -------------------------------------------------------------------------------- /src/assets/favicon/screenshot-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/screenshot-error.png -------------------------------------------------------------------------------- /src/assets/favicon/screenshot-light-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/screenshot-light-mode.png -------------------------------------------------------------------------------- /src/assets/favicon/screenshot-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/screenshot-mobile.png -------------------------------------------------------------------------------- /src/assets/favicon/screenshot-poster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/screenshot-poster.png -------------------------------------------------------------------------------- /src/assets/favicon/social-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/favicon/social-preview.png -------------------------------------------------------------------------------- /src/assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bundlejs", 3 | "short_name": "bundle", 4 | "id": "bundle", 5 | "start_url": "/", 6 | "author": "@okikio", 7 | "dir": "ltr", 8 | "display": "standalone", 9 | "theme_color": "#000000", 10 | "background_color": "#000000", 11 | "description": "A quick and easy way to bundle, minify, and compress (gzip and brotli) your ts, js, jsx and npm projects all locally on your device, with the bundle file size.", 12 | "icons": [ 13 | { 14 | "src": "/favicon/android-chrome-192x192.png", 15 | "sizes": "192x192", 16 | "type": "image/png" 17 | }, 18 | { 19 | "src": "/favicon/android-chrome-512x512.png", 20 | "sizes": "512x512", 21 | "type": "image/png" 22 | }, 23 | { 24 | "src": "/favicon/maskable_icon_x192.png", 25 | "sizes": "192x192", 26 | "type": "image/png", 27 | "purpose": "maskable" 28 | }, 29 | { 30 | "src": "/favicon/maskable_icon_x512.png", 31 | "sizes": "512x512", 32 | "type": "image/png", 33 | "purpose": "maskable" 34 | }, 35 | { 36 | "src": "/favicon/favicon.svg", 37 | "sizes": "any", 38 | "type": "image/svg+xml", 39 | "purpose": "any" 40 | } 41 | ], 42 | "lang": "en", 43 | "screenshots": [ 44 | { 45 | "src": "/favicon/screenshot-desktop.png", 46 | "type": "image/png", 47 | "sizes": "3100x1550" 48 | }, 49 | { 50 | "src": "/favicon/screenshot-bundled.png", 51 | "type": "image/png", 52 | "sizes": "3100x1550" 53 | }, 54 | { 55 | "src": "/favicon/screenshot-light-mode.png", 56 | "type": "image/png", 57 | "sizes": "3100x1550" 58 | }, 59 | { 60 | "src": "/favicon/screenshot-error.png", 61 | "type": "image/png", 62 | "sizes": "3100x1550" 63 | }, 64 | { 65 | "src": "/favicon/screenshot-mobile.png", 66 | "type": "image/png", 67 | "sizes": "750x1500" 68 | } 69 | ], 70 | "share_target": { 71 | "action": "/", 72 | "method": "GET", 73 | "enctype": "application/x-www-form-urlencoded", 74 | "params": { 75 | "text": "q" 76 | } 77 | }, 78 | "orientation": "natural", 79 | "categories": [ 80 | "productivity", 81 | "utility", 82 | "text", 83 | "development" 84 | ], 85 | "scope": "/" 86 | } 87 | -------------------------------------------------------------------------------- /src/assets/open-search.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | bundlejs 4 | bundlejs - a quick and easy way to bundle your projects, minify and see it's compressed size 5 | https://bundlejs.com/favicon/favicon.ico 6 | UTF-8 7 | hey@okikio.dev 8 | 9 | https://bundlejs.com 10 | 11 | -------------------------------------------------------------------------------- /src/assets/robots.txt: -------------------------------------------------------------------------------- 1 | Sitemap: https://bundlejs.com/sitemap.xml 2 | User-agent: * 3 | Allow: / -------------------------------------------------------------------------------- /src/assets/sponsors/upstash-long.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/sponsors/upstash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/sponsors/vercel-long.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/sponsors/vercel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/wasm_brotli_browser_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/assets/wasm_brotli_browser_bg.wasm -------------------------------------------------------------------------------- /src/css/components/_search.scss: -------------------------------------------------------------------------------- 1 | .dom-loaded { 2 | .search { 3 | transition: box-shadow ease 0.25s; 4 | // @apply transition duration-300 ease-out; 5 | 6 | .clear { 7 | transition-property: background-color, opacity, box-shadow, transform, 8 | filter, backdrop-filter; 9 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 10 | transition-duration: 150ms; 11 | } 12 | } 13 | 14 | .search-results { 15 | @apply transform scale-90; 16 | transition-property: transform, opacity; 17 | @apply duration-200 ease-out; 18 | } 19 | 20 | .card section.add { 21 | // @apply transition; 22 | .btn { 23 | // @apply transition duration-150; 24 | } 25 | } 26 | } 27 | 28 | .search { 29 | // @apply bg-gray-200 dark:bg-quaternary; 30 | @apply focus-within:ring-4 focus-within:ring-blue-500 focus-within:ring-opacity-50; 31 | @apply bg-white border border-gray-300; 32 | @apply dark:bg-black dark:border-gray-700; 33 | 34 | @apply flex items-center pl-4 rounded-lg; 35 | 36 | input#search-input { 37 | @apply w-full ml-2 p-2 bg-transparent align-middle focus:outline-none; 38 | // @apply text-gray-600; 39 | 40 | &::placeholder { 41 | @apply text-gray-400; 42 | } 43 | } 44 | 45 | .clear { 46 | @apply transform scale-[0.85] active:scale-75 active:bg-gray-600; 47 | @apply hover:bg-gray-200 dark:hover:bg-tertiary; 48 | @apply text-black dark:text-white m-0; 49 | } 50 | } 51 | 52 | .search-container { 53 | // @apply mb-5; 54 | } 55 | 56 | .search-container, 57 | .search-results-container { 58 | transform-origin: center top; 59 | // .search-results:active, 60 | &:focus-within .search-results { 61 | @apply scale-100 opacity-100 pointer-events-auto; 62 | } 63 | } 64 | 65 | .search-results-container { 66 | @apply w-full relative block; 67 | @apply pt-2 rounded-lg; 68 | } 69 | 70 | .search-results { 71 | height: fit-content; 72 | max-height: 410px; 73 | 74 | @apply opacity-0 pointer-events-none; 75 | @apply border border-gray-200 dark:border-quaternary; 76 | @apply flex flex-col; 77 | @apply overflow-auto w-full; 78 | @apply absolute z-20 bg-white dark:bg-black; 79 | @apply rounded-lg shadow-lg; 80 | @apply divide-y divide-gray-200 dark:divide-quaternary; 81 | 82 | &.empty { 83 | @apply border-transparent dark:border-transparent; 84 | } 85 | } 86 | 87 | .card { 88 | // @apply border border-gray-200 dark:border-quaternary rounded-lg; 89 | @apply grid gap-6 sm:grid-cols-12; 90 | @apply p-5; 91 | 92 | section { 93 | @apply col-span-12; 94 | 95 | &.content { 96 | @apply sm:col-span-9; 97 | 98 | &.error { 99 | @apply sm:col-span-12; 100 | } 101 | } 102 | 103 | p, 104 | h3 { 105 | line-height: 1.5; 106 | } 107 | 108 | h3 { 109 | font-size: 1rem; 110 | @apply mb-1; 111 | } 112 | 113 | p { 114 | font-size: 0.9rem; 115 | } 116 | 117 | .updated-time { 118 | @apply text-gray-500 mt-1; 119 | font-size: 0.8rem; 120 | } 121 | 122 | &.add { 123 | // @apply transform scale-100 active:scale-90; 124 | @apply flex sm:text-center sm:col-span-3; 125 | justify-content: end; 126 | align-items: center; 127 | 128 | .btn { 129 | @apply w-full relative block m-0; 130 | @apply bg-gray-200 dark:bg-quaternary; 131 | @apply hover:bg-blue-200 dark:hover:bg-gray-600; 132 | @apply active:bg-blue-100 dark:active:bg-gray-800; 133 | 134 | .btn-text { 135 | @apply whitespace-nowrap opacity-100; 136 | } 137 | } 138 | } 139 | } 140 | } 141 | 142 | @keyframes fade-in { 143 | 50% { 144 | opacity: 1; 145 | } 146 | to { 147 | opacity: 0; 148 | } 149 | } 150 | 151 | @keyframes fade-out { 152 | 50% { 153 | opacity: 0; 154 | } 155 | to { 156 | opacity: 1; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/css/components/_variables.scss: -------------------------------------------------------------------------------- 1 | 2 | // "Material Symbols Outlined", "Material Symbols Sharp", 3 | $icon-font: "Material Symbols Rounded", "Material Icons-fallback"; -------------------------------------------------------------------------------- /src/css/components/fonts/_manrope.scss: -------------------------------------------------------------------------------- 1 | /* cyrillic-ext */ 2 | @font-face { 3 | font-family: "Manrope"; 4 | font-style: normal; 5 | font-weight: 500 600 700 800; 6 | font-display: swap; 7 | src: url(https://fonts.gstatic.com/s/manrope/v12/xn7gYHE41ni1AdIRggqxSuXd.woff2) 8 | format("woff2"); 9 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, 10 | U+FE2E-FE2F; 11 | } 12 | /* cyrillic */ 13 | @font-face { 14 | font-family: "Manrope"; 15 | font-style: normal; 16 | font-weight: 500 600 700 800; 17 | font-display: swap; 18 | src: url(https://fonts.gstatic.com/s/manrope/v12/xn7gYHE41ni1AdIRggOxSuXd.woff2) 19 | format("woff2"); 20 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; 21 | } 22 | /* greek */ 23 | @font-face { 24 | font-family: "Manrope"; 25 | font-style: normal; 26 | font-weight: 500 600 700 800; 27 | font-display: swap; 28 | src: url(https://fonts.gstatic.com/s/manrope/v12/xn7gYHE41ni1AdIRggSxSuXd.woff2) 29 | format("woff2"); 30 | unicode-range: U+0370-03FF; 31 | } 32 | /* vietnamese */ 33 | @font-face { 34 | font-family: "Manrope"; 35 | font-style: normal; 36 | font-weight: 500 600 700 800; 37 | font-display: swap; 38 | src: url(https://fonts.gstatic.com/s/manrope/v12/xn7gYHE41ni1AdIRggixSuXd.woff2) 39 | format("woff2"); 40 | unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, 41 | U+01AF-01B0, U+1EA0-1EF9, U+20AB; 42 | } 43 | /* latin-ext */ 44 | @font-face { 45 | font-family: "Manrope"; 46 | font-style: normal; 47 | font-weight: 500 600 700 800; 48 | font-display: swap; 49 | src: url(https://fonts.gstatic.com/s/manrope/v12/xn7gYHE41ni1AdIRggmxSuXd.woff2) 50 | format("woff2"); 51 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 52 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 53 | } 54 | /* latin */ 55 | @font-face { 56 | font-family: "Manrope"; 57 | font-style: normal; 58 | font-weight: 500 600 700 800; 59 | font-display: swap; 60 | src: url(https://fonts.gstatic.com/s/manrope/v12/xn7gYHE41ni1AdIRggexSg.woff2) 61 | format("woff2"); 62 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 63 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, 64 | U+FEFF, U+FFFD; 65 | } 66 | -------------------------------------------------------------------------------- /src/css/components/fonts/_material-icons.scss: -------------------------------------------------------------------------------- 1 | /* fallback */ 2 | @font-face { 3 | font-family: "Material Symbols Rounded"; 4 | font-style: normal; 5 | font-weight: 100 700; 6 | src: url(https://fonts.gstatic.com/s/materialsymbolsrounded/v7/sykg-zNym6YjUruM-QrEh7-nyTnjDwKNJ_190Fjzag.woff2) 7 | format("woff2"); 8 | font-display: block; 9 | } 10 | 11 | /* fallback */ 12 | @font-face { 13 | font-family: "Material Symbols Outlined"; 14 | font-style: normal; 15 | font-weight: 100 700; 16 | src: url(https://fonts.gstatic.com/s/materialsymbolsoutlined/v7/kJEhBvYX7BgnkSrUwT8OhrdQw4oELdPIeeII9v6oFsI.woff2) 17 | format("woff2"); 18 | font-display: block; 19 | } 20 | 21 | /* fallback */ 22 | @font-face { 23 | font-family: "Material Symbols Sharp"; 24 | font-style: normal; 25 | font-weight: 100 700; 26 | src: url(https://fonts.gstatic.com/s/materialsymbolssharp/v7/gNMVW2J8Roq16WD5tFNRaeLQk6-SHQ_R00k4aWE.woff2) 27 | format("woff2"); 28 | font-display: block; 29 | } 30 | 31 | .material-symbols-outlined, 32 | .material-symbols-rounded, 33 | .material-symbols-sharp { 34 | font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 48; 35 | font-weight: 400; 36 | font-style: normal; 37 | font-size: 24px; 38 | line-height: 1; 39 | letter-spacing: normal; 40 | text-transform: none; 41 | display: inline-block; 42 | white-space: nowrap; 43 | word-wrap: normal; 44 | direction: ltr; 45 | -webkit-font-smoothing: antialiased; 46 | } 47 | 48 | .material-symbols-rounded { 49 | font-family: "Material Symbols Rounded"; 50 | } 51 | 52 | .material-symbols-outlined { 53 | font-family: "Material Symbols Outlined"; 54 | } 55 | 56 | .material-symbols-sharp { 57 | font-family: "Material Symbols Sharp"; 58 | } 59 | -------------------------------------------------------------------------------- /src/css/fonts.scss: -------------------------------------------------------------------------------- 1 | // Generate CLS less font fallback https://deploy-preview-15--upbeat-shirley-608546.netlify.app/perfect-ish-font-fallback/?font=Manrope 2 | @use "./components/fonts/manrope"; 3 | @use "./components/fonts/material-icons"; 4 | @import "./components/_variables"; 5 | 6 | @font-face { 7 | font-family: "Manrope-fallback"; 8 | size-adjust: 102.95%; 9 | ascent-override: 105%; 10 | src: local("Arial"); 11 | } 12 | 13 | @font-face { 14 | font-family: "Material Icons-fallback"; 15 | size-adjust: 182.39999999999998%; 16 | ascent-override: 105%; 17 | src: local("Arial"); 18 | } 19 | 20 | .icon { 21 | -webkit-font-feature-settings: "liga" off, "dlig"; 22 | -moz-font-feature-settings: "liga=0, dlig=1"; 23 | font-feature-settings: "liga", "dlig"; 24 | -webkit-font-smoothing: antialiased; 25 | -moz-osx-font-smoothing: grayscale; 26 | text-rendering: optimizeLegibility; 27 | font-family: $icon-font; 28 | vertical-align: middle; 29 | letter-spacing: normal; 30 | display: inline-block; 31 | text-decoration: none; 32 | text-transform: none; 33 | white-space: nowrap; 34 | font-weight: normal; 35 | position: relative; 36 | font-style: normal; 37 | word-wrap: normal; 38 | font-size: 24px; 39 | line-height: 1; 40 | direction: ltr; 41 | height: 1em; 42 | width: 1em; 43 | } 44 | -------------------------------------------------------------------------------- /src/missing-types.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'worker:*' { 2 | let value: string; 3 | export default value; 4 | } 5 | 6 | declare module '*.wasm' { 7 | let value: Promise>; 8 | export default value; 9 | } 10 | 11 | declare module 'dts:*' { 12 | let value: Record; 13 | export default value; 14 | } -------------------------------------------------------------------------------- /src/pug/404.pug: -------------------------------------------------------------------------------- 1 | extends /layouts/layout.pug 2 | 3 | block title 4 | title 404 Page 5 | 6 | block content 7 | .container.px-5.py-2(class="sm:max-w-screen-lg") 8 | .my-5.px-2.py-24(class="sm:px-8 md:px-12") 9 | h1.text-6xl.mb-2.font-extrabold 404 Page 10 | p.font-light.text-lg You might be lost, wanna 11 | a(href="/") go back home? 12 | -------------------------------------------------------------------------------- /src/pug/about.pug: -------------------------------------------------------------------------------- 1 | extends /layouts/layout.pug 2 | 3 | block pageInfo 4 | - 5 | let page = { 6 | url: "/about", 7 | title: "About - bundlejs", 8 | description: "Details about how bundle was made as well, as well links, sources, and all the tools I used to make bundle." 9 | }; 10 | 11 | block content 12 | #about.container.max-w-screen-lg 13 | h2.text-center.mb-8 About 14 | p 15 | strong bundlejs 16 | | is a quick and easy way to bundle your projects, minify and see their gzip and brotli size. It's an online tool similar to 17 | a(href="https://bundlephobia.com" target="_blank" rel="noopener") bundlephobia 18 | | , but 19 | strong bundlejs 20 | | does all the bundling locally on you browser and can treeshake and bundle multiple packages (both commonjs and esm) together, all without having to install any npm packages and with typescript support. 21 | br 22 | br 23 | 24 | p 25 | | I used 26 | a(href="https://github.com/microsoft/monaco-editor" target="_blank" rel="noopener") monaco-editor 27 | | for the code-editor, 28 | a(href="https://github.com/evanw/esbuild" target="_blank" rel="noopener") esbuild 29 | | as bundler and treeshaker respectively, 30 | a(href="https://github.com/hazae41/denoflate" target="_blank" rel="noopener") denoflate 31 | | as a wasm port of gzip, 32 | a(href="https://github.com/denosaurs/deno_brotli" target="_blank" rel="noopener") deno_brotli 33 | | as a wasm port of brotli, 34 | a(href="https://github.com/denosaurs/deno_lz4" target="_blank" rel="noopener") deno_lz4 35 | | as a wasm port of lz4, 36 | a(href="https://github.com/visionmedia/bytes.js" target="_blank" rel="noopener") bytes 37 | | to convert the compressed size to human readable values, 38 | a(href="https://github.com/btd/esbuild-visualizer" target="_blank" rel="noopener") esbuild-visualizer 39 | | to visualize and analyze your esbuild bundle to see which modules are taking up space, and 40 | a(href="https://github.com/mikecao/umami" target="_blank" rel="noopener") umami 41 | | for private, publicly available analytics and general usage stats all without cookies. 42 | br 43 | br 44 | | This project was greatly influenced by 45 | a(href="https://github.com/hardfist" target="_blank" rel="noopener") @hardfists 46 | a(href="https://github.com/hardfist/neo-tools" target="_blank" rel="noopener") neo-tools 47 | | and 48 | a(href="https://github.com/mizchi" target="_blank" rel="noopener") @mizchi's 49 | a(href="https://github.com/mizchi/uniroll" target="_blank" rel="noopener") uniroll 50 | | project. 51 | br 52 | br 53 | :markdown-it(linkify langPrefix='language-') 54 | > Some of bundlejs's latest features were inspired by [egoist/play-esbuild](https://github.com/egoist/play-esbuild) and [hyrious/esbuild-repl](https://github.com/hyrious/esbuild-repl), check them out they each use esbuild in different ways. 55 | 56 | span= "The project isn't perfect, I am still working on an autocomplete, better mobile support and the high memory usage of " 57 | strong esbuild 58 | span= " and " 59 | strong monaco 60 | span= " as well as some edge case packages, e.g. " 61 | strong monaco-editor. 62 | br 63 | br 64 | | If there is something I missed, a mistake, or a feature you would like added please create an issue or a pull request and I'll try to get to it. You can contribute to this project at 65 | a(href="https://github.com/okikio/bundlejs" target="_blank" rel="noopener") okikio/bundle. 66 | br 67 | br 68 | strong bundle 69 | | uses 70 | a(href="https://www.conventionalcommits.org/en/v1.0.0/" target="_blank" rel="noopener") Conventional Commits 71 | | as the style of commit, and the 72 | a(href="http://commitizen.github.io/cz-cli/" target="_blank" rel="noopener") Commitizen CLI 73 | | to make commits easier. 74 | br 75 | br 76 | | You can join the discussion on 77 | a(href="https://github.com/okikio/bundle/discussions" target="_blank" rel="noopener") github discussions 78 | | . 79 | br 80 | br 81 | div.flex.items-center.flex-wrap.gap-2 82 | +product-hunt(loading="lazy") 83 | div 84 | a(href='https://opencollective.com/bundle/donate' target='_blank' rel="noopener") 85 | img(src='https://opencollective.com/bundle/donate/button@2x.png?color=blue' width='300' height='54') 86 | 87 | -------------------------------------------------------------------------------- /src/pug/components/footer.pug: -------------------------------------------------------------------------------- 1 | mixin footer 2 | footer.footer.my-6 3 | .container.flex.flex-wrap.gap-2(class="flex-col sm:flex-row") 4 | .flex.flex-grow 5 | div(class="lt-sm:space-y-2") 6 | label(for="pet-select") Change Theme: 7 | 8 | select.theme-options(name="Theme Selector") 9 | option(value="system" selected) System 10 | option(value="light") Light 11 | option(value="dark") Dark 12 | span.ml-2 13 | button#reset-cache.btn.btn-highlight(type="button" title="Clear Cache / Force Reload Site") 14 | svg.icon(viewbox='0 0 24 24' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink') 15 | path(d='M12 4.5a7.5 7.5 0 1 0 7.419 6.392c-.067-.454.265-.892.724-.892.37 0 .696.256.752.623A9 9 0 1 1 18 5.292V4.25a.75.75 0 0 1 1.5 0v3a.75.75 0 0 1-.75.75h-3a.75.75 0 0 1 0-1.5h1.35a7.474 7.474 0 0 0-5.1-2Z' fill="currentColor") 16 | 17 | .flex-grow(class="text-right") 18 | div.px-4.py-2.inline-block (c) #[span=`${new Date().getFullYear()}`] #[a(href='https://okikio.dev' target='_blank' rel="noopener") Okiki Ojo] 19 | .flex-grow 20 | div.flex.flex-wrap.gap-2.justify-end 21 | p.bg-gray-200(class="dark:bg-quaternary").inline-block.px-4.py-2.rounded-md 22 | span=" Made by " 23 | //- a(href='https://okikio.dev' target='_blank' rel="noopener") Okiki Ojo 24 | //- | ( 25 | a(href='https://github.com/okikio' target='_blank' rel="noopener") @okikio 26 | //- | ). 27 | -------------------------------------------------------------------------------- /src/pug/components/product-hunt.pug: -------------------------------------------------------------------------------- 1 | mixin product-hunt 2 | #product-hunt&attributes(attributes) 3 | a(href='https://www.producthunt.com/posts/bundle-6?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-bundle-6', target='_blank' rel="noopener").umami--click--product-hunt-badge 4 | figure(width='250', height='54') 5 | img.dark(src='https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=300568&theme=dark' alt="Product hunt badge" loading="lazy" importance="high" width='250', height='54')&attributes(attributes) 6 | img(src='https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=300568&theme=light' alt="Product hunt badge" loading="lazy" importance="high" width='250', height='54')&attributes(attributes) 7 | figcaption A link to bundlejs.com's Product Hunt page; a small public display of what bundlejs.com is all about -------------------------------------------------------------------------------- /src/pug/faq.pug: -------------------------------------------------------------------------------- 1 | extends /layouts/layout.pug 2 | 3 | block pageInfo 4 | - 5 | let page = { 6 | url: "/faq", 7 | title: "FAQ - bundlejs", 8 | description: "Frequently Asked Questions. I frequently get asked what was the point in building bundlejs, this is a short, quick, and simple summary of frequently asked questions and my reasons for building bundlejs." 9 | }; 10 | 11 | block content 12 | #faq.container.max-w-screen-lg 13 | h2.text-center.mb-8(aria-label="Frequently Asked Questions" title="Frequently Asked Questions") FAQ 14 | p I frequently get asked what was the point in building bundlejs, this is a short, quick, and simple summary of frequently asked questions and my reasons for building bundlejs. 15 | br 16 | br 17 | h3.mb-2 18 | | What is the advantage over 19 | a(href="https://bundlephobia.com" target="_blank" rel="noopener") bundlephobia 20 | | ? 21 | p 22 | :markdown-it(linkify langPrefix='language-') 23 | There are a couple reasons for this, but the main one is that `bundlephobia` wasn't reliable enough. 24 | br 25 | h3.mb-2 What do you mean wasn't reliable enough? 26 | p Right now #[code bundlephobia], is a bit hit or miss when it comes to treeshaking. So, I built #[strong bundlejs.com], which can treeshake bundles accurately. 27 | br 28 | br 29 | :markdown-it(linkify langPrefix='language-') 30 | For example, try treeshaking the `Event` class from `@okikio/emitter` using [bundlephobia](https://bundlephobia.com/package/@okikio/emitter) and try treeshaking the `Event` class using [bundlejs.com](/?bundle&q=@okikio/emitter&treeshake=Event). 31 | ```ts 32 | export { Event } from "@okikio/emitter"; 33 | ``` 34 | br 35 | :markdown-it(linkify langPrefix='language-') 36 | You will notice a major package size disparity. This is only one example and I am sure others exist, I hope the above example illustrates my point. 37 | br 38 | :markdown-it(linkify langPrefix='language-') 39 | Another problem with `bundlephobia` is the lack of good error reporting if (for whatever reason) `bundlephobia` isn't able to bundle your package, it logs this to the console, 40 | ```ts 41 | { 42 | "code": "BuildError", 43 | "message": "Failed to build this package.", 44 | "details": { 45 | "name": "BuildError" 46 | } 47 | } 48 | ``` 49 | 50 | this just isn't very useful for debugging. 51 | br 52 | :markdown-it(linkify langPrefix='language-') 53 | On the other hand, since, `bundlejs.com` runs locally right on your computer, when an error occurs it logs the exact error you would see when using [esbuild](https://esbuild.github.io/) or other bundlers in your build process, making it much easier to know exactly what is wrong with your js bundle. 54 | br 55 | h3.mb-2 Wait, locally as in no external servers? 56 | p 57 | 58 | :markdown-it(linkify langPrefix='language-') 59 | Yes, locally right on your browser, I used [esbuild-wasm](https://esbuild.github.io/getting-started/#wasm) for the main bundler and [rollup](https://rollupjs.org/guide/en/) for more accurate treeshaking. 60 | br 61 | h3.mb-2 Can it bundle multiple packages and treeshake them? 62 | p 63 | :markdown-it(linkify langPrefix='language-') 64 | Yes. `bundlejs.com` can treeshake all packages, and it can do so accurately. 65 | -------------------------------------------------------------------------------- /src/ts/configs/bundle-options.ts: -------------------------------------------------------------------------------- 1 | import type { BuildOptions } from "esbuild-wasm"; 2 | import type { OutputOptions } from "rollup"; 3 | import type { TemplateType } from "../plugins/analyzer/types/template-types"; 4 | import type { PackageJson } from "../plugins/cdn"; 5 | 6 | import { deepAssign } from "../util/deep-equal"; 7 | import { DEFAULT_CDN_HOST } from "../util/util-cdn"; 8 | 9 | /** The compression algorithim to use, there are currently 4 options "gzip", "brotli", "zstd", and "lz4" */ 10 | export type CompressionType = "gzip" | "brotli" | "lz4" | "zstd"; 11 | 12 | /** 13 | * You can configure the quality of the compression using an object, 14 | * e.g. 15 | * ```ts 16 | * { 17 | * ... 18 | * "compression": { 19 | * "type": "brotli", 20 | * "quality": 5 21 | * } 22 | * } 23 | * ``` 24 | */ 25 | export type CompressionOptions = { 26 | /** The compression algorithim to use, there are currently 4 options "gzip", "brotli", "zstd", and "lz4" */ 27 | type: CompressionType, 28 | 29 | /** Compression quality ranging from 1 to 11 */ 30 | quality: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 31 | }; 32 | 33 | export type BundleConfigOptions = { 34 | /** Enable using rollup for treeshaking. Only works while the `esbuild.treeShaking` option is true */ 35 | // rollup?: OutputOptions | boolean, 36 | 37 | /** esbuild config options https://esbuild.github.io/api/#build-api */ 38 | esbuild?: BuildOptions, 39 | 40 | /** Polyfill Node Built-ins */ 41 | polyfill?: boolean, 42 | 43 | /** 44 | * The package.json to use when trying to bundle files 45 | */ 46 | "package.json"?: PackageJson; 47 | 48 | /** 49 | * Support TSX 50 | */ 51 | "tsx"?: boolean, 52 | 53 | /** The default CDN to import packages from */ 54 | cdn?: "https://unpkg.com" | "https://esm.run" | "https://esm.sh" | "https://esm.sh/jsr" | "https://cdn.skypack.dev" | "https://cdn.jsdelivr.net/npm" | "https://cdn.jsdelivr.net/gh" | "https://deno.land/x" | "https://raw.githubusercontent.com" | "unpkg" | "esm.run" | "esm.sh" | "esm" | "jsr" | "skypack" | "jsdelivr" | "jsdelivr.gh" | "github" | "deno" | (string & {}), 55 | 56 | /** Aliases for replacing packages with different ones, e.g. replace "fs" with "memfs", so, it can work on the web, etc... */ 57 | alias?: Record, 58 | 59 | /** 60 | * The compression algorithim to use, there are currently 3 options "gzip", "brotli", and "lz4". 61 | * You can also configure the quality of the compression using an object, 62 | * e.g. 63 | * ```ts 64 | * { 65 | * ... 66 | * "compression": { 67 | * "type": "brotli", 68 | * "quality": 5 69 | * } 70 | * } 71 | * ``` 72 | */ 73 | compression?: CompressionOptions | CompressionType 74 | 75 | /** 76 | * Generates interactive zoomable charts displaing the size of output files. 77 | * It's a great way to determine what causes the bundle size to be so large. 78 | */ 79 | analysis?: TemplateType | boolean 80 | }; 81 | 82 | export const EasyDefaultConfig: BundleConfigOptions = { 83 | "cdn": DEFAULT_CDN_HOST, 84 | "compression": "gzip", 85 | "analysis": false, 86 | "polyfill": false, 87 | "tsx": false, 88 | "package.json": { 89 | "name": "bundled-code", 90 | "version": "0.0.0" 91 | }, 92 | "esbuild": { 93 | "target": ["esnext"], 94 | "format": "esm", 95 | "bundle": true, 96 | "minify": true, 97 | 98 | "treeShaking": true, 99 | "platform": "browser" 100 | } 101 | }; 102 | 103 | export const DefaultConfig: BundleConfigOptions = deepAssign({}, EasyDefaultConfig, { 104 | "esbuild": { 105 | "color": true, 106 | "globalName": "BundledCode", 107 | 108 | "logLevel": "info", 109 | "sourcemap": false, 110 | } 111 | }); -------------------------------------------------------------------------------- /src/ts/deno/base64/mod.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. 2 | 3 | /** 4 | * {@linkcode encode} and {@linkcode decode} for 5 | * [base64](https://en.wikipedia.org/wiki/Base64) encoding. 6 | * 7 | * This module is browser compatible. 8 | * 9 | * @example 10 | * ```ts 11 | * import { 12 | * decode, 13 | * encode, 14 | * } from "https://deno.land/std@$STD_VERSION/encoding/base64.ts"; 15 | * 16 | * const b64Repr = "Zm9vYg=="; 17 | * 18 | * const binaryData = decode(b64Repr); 19 | * console.log(binaryData); 20 | * // => Uint8Array [ 102, 111, 111, 98 ] 21 | * 22 | * console.log(encode(binaryData)); 23 | * // => Zm9vYg== 24 | * ``` 25 | * 26 | * @module 27 | */ 28 | 29 | const base64abc = [ 30 | "A", 31 | "B", 32 | "C", 33 | "D", 34 | "E", 35 | "F", 36 | "G", 37 | "H", 38 | "I", 39 | "J", 40 | "K", 41 | "L", 42 | "M", 43 | "N", 44 | "O", 45 | "P", 46 | "Q", 47 | "R", 48 | "S", 49 | "T", 50 | "U", 51 | "V", 52 | "W", 53 | "X", 54 | "Y", 55 | "Z", 56 | "a", 57 | "b", 58 | "c", 59 | "d", 60 | "e", 61 | "f", 62 | "g", 63 | "h", 64 | "i", 65 | "j", 66 | "k", 67 | "l", 68 | "m", 69 | "n", 70 | "o", 71 | "p", 72 | "q", 73 | "r", 74 | "s", 75 | "t", 76 | "u", 77 | "v", 78 | "w", 79 | "x", 80 | "y", 81 | "z", 82 | "0", 83 | "1", 84 | "2", 85 | "3", 86 | "4", 87 | "5", 88 | "6", 89 | "7", 90 | "8", 91 | "9", 92 | "+", 93 | "/", 94 | ]; 95 | 96 | /** 97 | * CREDIT: https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727 98 | * Encodes a given Uint8Array, ArrayBuffer or string into RFC4648 base64 representation 99 | * @param data 100 | */ 101 | export function encode(data: ArrayBuffer | string): string { 102 | const uint8 = typeof data === "string" 103 | ? new TextEncoder().encode(data) 104 | : data instanceof Uint8Array 105 | ? data 106 | : new Uint8Array(data); 107 | let result = "", 108 | i; 109 | const l = uint8.length; 110 | for (i = 2; i < l; i += 3) { 111 | result += base64abc[uint8[i - 2] >> 2]; 112 | result += base64abc[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)]; 113 | result += base64abc[((uint8[i - 1] & 0x0f) << 2) | (uint8[i] >> 6)]; 114 | result += base64abc[uint8[i] & 0x3f]; 115 | } 116 | if (i === l + 1) { 117 | // 1 octet yet to write 118 | result += base64abc[uint8[i - 2] >> 2]; 119 | result += base64abc[(uint8[i - 2] & 0x03) << 4]; 120 | result += "=="; 121 | } 122 | if (i === l) { 123 | // 2 octets yet to write 124 | result += base64abc[uint8[i - 2] >> 2]; 125 | result += base64abc[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)]; 126 | result += base64abc[(uint8[i - 1] & 0x0f) << 2]; 127 | result += "="; 128 | } 129 | return result; 130 | } 131 | 132 | /** 133 | * Decodes a given RFC4648 base64 encoded string 134 | * @param b64 135 | */ 136 | export function decode(b64: string): Uint8Array { 137 | const binString = atob(b64); 138 | const size = binString.length; 139 | const bytes = new Uint8Array(size); 140 | for (let i = 0; i < size; i++) { 141 | bytes[i] = binString.charCodeAt(i); 142 | } 143 | return bytes; 144 | } 145 | 146 | /** 147 | * Decodes data assuming the output is in string type 148 | * @param data input to decode 149 | */ 150 | export function decodeString(data: string): string { 151 | return atob(data); 152 | } -------------------------------------------------------------------------------- /src/ts/deno/brotli/mod.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-present the denosaurs team. All rights reserved. MIT license. 2 | import init, { 3 | source, 4 | compress as wasm_compress, 5 | decompress as wasm_decompress, 6 | } from "./wasm.js"; 7 | 8 | let initialized = false; 9 | export const getWASM = async () => { 10 | if (!initialized) await init(source); 11 | return (initialized = true); 12 | } 13 | 14 | /** 15 | * Compress a byte array. 16 | * 17 | * ```typescript 18 | * import { compress } from "https://deno.land/x/brotli/mod.ts"; 19 | * const text = new TextEncoder().encode("X".repeat(64)); 20 | * console.log(text.length); // 64 Bytes 21 | * console.log(compress(text).length); // 10 Bytes 22 | * ``` 23 | * 24 | * @param input Input data. 25 | * @param bufferSize Read buffer size 26 | * @param quality Controls the compression-speed vs compression- 27 | * density tradeoff. The higher the quality, the slower the compression. 28 | * @param lgwin Base 2 logarithm of the sliding window size. 29 | */ 30 | export async function compress( 31 | input: Uint8Array, 32 | bufferSize: number = 4096, 33 | quality: number = 6, 34 | lgwin: number = 22, 35 | ): Promise { 36 | await getWASM(); 37 | return wasm_compress(input, bufferSize, quality, lgwin); 38 | } 39 | 40 | /** 41 | * Decompress a byte array. 42 | * 43 | * ```typescript 44 | * import { decompress } from "https://deno.land/x/brotli/mod.ts"; 45 | * const compressed = Uint8Array.from([ 27, 63, 0, 0, 36, 176, 226, 153, 64, 18 ]); 46 | * console.log(compressed.length); // 10 Bytes 47 | * console.log(decompress(compressed).length); // 64 Bytes 48 | * ``` 49 | * 50 | * @param input Input data. 51 | * @param bufferSize Read buffer size 52 | */ 53 | export async function decompress( 54 | input: Uint8Array, 55 | bufferSize: number = 4096, 56 | ): Promise { 57 | await getWASM(); 58 | return wasm_decompress(input, bufferSize); 59 | } -------------------------------------------------------------------------------- /src/ts/deno/denoflate/mod.ts: -------------------------------------------------------------------------------- 1 | export { 2 | deflate, 3 | inflate, 4 | gzip, 5 | gunzip, 6 | zlib, 7 | unzlib 8 | } from "./pkg/denoflate.js"; 9 | 10 | import type { InitOutput } from "./pkg/denoflate"; 11 | 12 | import init from "./pkg/denoflate.js"; 13 | 14 | // @ts-ignore 15 | import { wasm as WASM } from "./pkg/denoflate_bg.wasm.js"; 16 | 17 | export let wasm: InitOutput; 18 | export const getWASM = async () => { 19 | if (wasm) return wasm; 20 | return (wasm = await init(WASM)); 21 | } 22 | 23 | export default wasm; 24 | -------------------------------------------------------------------------------- /src/ts/deno/denoflate/pkg/denoflate.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * @param {Uint8Array} input 5 | * @param {number | undefined} compression 6 | * @returns {Uint8Array} 7 | */ 8 | export function deflate(input: Uint8Array, compression?: number): Uint8Array; 9 | /** 10 | * @param {Uint8Array} input 11 | * @returns {Uint8Array} 12 | */ 13 | export function inflate(input: Uint8Array): Uint8Array; 14 | /** 15 | * @param {Uint8Array} input 16 | * @param {number | undefined} compression 17 | * @returns {Uint8Array} 18 | */ 19 | export function gzip(input: Uint8Array, compression?: number): Uint8Array; 20 | /** 21 | * @param {Uint8Array} input 22 | * @returns {Uint8Array} 23 | */ 24 | export function gunzip(input: Uint8Array): Uint8Array; 25 | /** 26 | * @param {Uint8Array} input 27 | * @param {number | undefined} compression 28 | * @returns {Uint8Array} 29 | */ 30 | export function zlib(input: Uint8Array, compression?: number): Uint8Array; 31 | /** 32 | * @param {Uint8Array} input 33 | * @returns {Uint8Array} 34 | */ 35 | export function unzlib(input: Uint8Array): Uint8Array; 36 | 37 | export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; 38 | 39 | export interface InitOutput { 40 | readonly memory: WebAssembly.Memory; 41 | readonly deflate: (a: number, b: number, c: number, d: number, e: number) => void; 42 | readonly inflate: (a: number, b: number, c: number) => void; 43 | readonly gzip: (a: number, b: number, c: number, d: number, e: number) => void; 44 | readonly gunzip: (a: number, b: number, c: number) => void; 45 | readonly zlib: (a: number, b: number, c: number, d: number, e: number) => void; 46 | readonly unzlib: (a: number, b: number, c: number) => void; 47 | readonly __wbindgen_add_to_stack_pointer: (a: number) => number; 48 | readonly __wbindgen_malloc: (a: number) => number; 49 | readonly __wbindgen_free: (a: number, b: number) => void; 50 | } 51 | 52 | /** 53 | * If `module_or_path` is {RequestInfo} or {URL}, makes a request and 54 | * for everything else, calls `WebAssembly.instantiate` directly. 55 | * 56 | * @param {InitInput | Promise} module_or_path 57 | * 58 | * @returns {Promise} 59 | */ 60 | export default function init (module_or_path?: InitInput | Promise): Promise; -------------------------------------------------------------------------------- /src/ts/deno/denoflate/pkg/denoflate_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/ts/deno/denoflate/pkg/denoflate_bg.wasm -------------------------------------------------------------------------------- /src/ts/deno/denoflate/pkg/denoflate_bg.wasm.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export const memory: WebAssembly.Memory; 4 | export function deflate(a: number, b: number, c: number, d: number, e: number): void; 5 | export function inflate(a: number, b: number, c: number): void; 6 | export function gzip(a: number, b: number, c: number, d: number, e: number): void; 7 | export function gunzip(a: number, b: number, c: number): void; 8 | export function zlib(a: number, b: number, c: number, d: number, e: number): void; 9 | export function unzlib(a: number, b: number, c: number): void; 10 | export function __wbindgen_add_to_stack_pointer(a: number): number; 11 | export function __wbindgen_malloc(a: number): number; 12 | export function __wbindgen_free(a: number, b: number): void; -------------------------------------------------------------------------------- /src/ts/deno/lz4/mod.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020-present the denosaurs team. All rights reserved. MIT license. 2 | 3 | import init, { 4 | source, 5 | lz4_compress, 6 | lz4_decompress, 7 | } from "./wasm.js"; 8 | 9 | let initialized = false; 10 | const getWASM = async () => { 11 | if (!initialized) await init(source); 12 | return (initialized = true); 13 | } 14 | 15 | /** 16 | * Compress a byte array using lz4. 17 | * 18 | * ```typescript 19 | * import { compress } from "https://deno.land/x/lz4/mod.ts"; 20 | * const text = new TextEncoder().encode("X".repeat(64)); 21 | * console.log(text.length); // 64 Bytes 22 | * console.log(compress(text).length); // 6 Bytes 23 | * ``` 24 | * 25 | * @param input Input data. 26 | */ 27 | export async function compress(input: Uint8Array): Promise { 28 | await getWASM(); 29 | return lz4_compress(input); 30 | } 31 | 32 | /** 33 | * Decompress a byte array using lz4. 34 | * 35 | * ```typescript 36 | * import { decompress } from "https://deno.land/x/lz4/mod.ts"; 37 | * const compressed = Uint8Array.from([ 31, 88, 1, 0, 44, 0 ]); 38 | * console.log(compressed.length); // 6 Bytes 39 | * console.log(decompress(compressed).length); // 64 Bytes 40 | * ``` 41 | * 42 | * @param input Input data. 43 | */ 44 | export async function decompress(input: Uint8Array): Promise { 45 | await getWASM(); 46 | return lz4_decompress(input); 47 | } -------------------------------------------------------------------------------- /src/ts/deno/media-types/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "@std/media-types"; -------------------------------------------------------------------------------- /src/ts/deno/path/_constants.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. 2 | // Copyright the Browserify authors. MIT License. 3 | // Ported from https://github.com/browserify/path-browserify/ 4 | // This module is browser compatible. 5 | 6 | // Alphabet chars. 7 | export const CHAR_UPPERCASE_A = 65; /* A */ 8 | export const CHAR_LOWERCASE_A = 97; /* a */ 9 | export const CHAR_UPPERCASE_Z = 90; /* Z */ 10 | export const CHAR_LOWERCASE_Z = 122; /* z */ 11 | 12 | // Non-alphabetic chars. 13 | export const CHAR_DOT = 46; /* . */ 14 | export const CHAR_FORWARD_SLASH = 47; /* / */ 15 | export const CHAR_BACKWARD_SLASH = 92; /* \ */ 16 | export const CHAR_VERTICAL_LINE = 124; /* | */ 17 | export const CHAR_COLON = 58; /* : */ 18 | export const CHAR_QUESTION_MARK = 63; /* ? */ 19 | export const CHAR_UNDERSCORE = 95; /* _ */ 20 | export const CHAR_LINE_FEED = 10; /* \n */ 21 | export const CHAR_CARRIAGE_RETURN = 13; /* \r */ 22 | export const CHAR_TAB = 9; /* \t */ 23 | export const CHAR_FORM_FEED = 12; /* \f */ 24 | export const CHAR_EXCLAMATION_MARK = 33; /* ! */ 25 | export const CHAR_HASH = 35; /* # */ 26 | export const CHAR_SPACE = 32; /* */ 27 | export const CHAR_NO_BREAK_SPACE = 160; /* \u00A0 */ 28 | export const CHAR_ZERO_WIDTH_NOBREAK_SPACE = 65279; /* \uFEFF */ 29 | export const CHAR_LEFT_SQUARE_BRACKET = 91; /* [ */ 30 | export const CHAR_RIGHT_SQUARE_BRACKET = 93; /* ] */ 31 | export const CHAR_LEFT_ANGLE_BRACKET = 60; /* < */ 32 | export const CHAR_RIGHT_ANGLE_BRACKET = 62; /* > */ 33 | export const CHAR_LEFT_CURLY_BRACKET = 123; /* { */ 34 | export const CHAR_RIGHT_CURLY_BRACKET = 125; /* } */ 35 | export const CHAR_HYPHEN_MINUS = 45; /* - */ 36 | export const CHAR_PLUS = 43; /* + */ 37 | export const CHAR_DOUBLE_QUOTE = 34; /* " */ 38 | export const CHAR_SINGLE_QUOTE = 39; /* ' */ 39 | export const CHAR_PERCENT = 37; /* % */ 40 | export const CHAR_SEMICOLON = 59; /* ; */ 41 | export const CHAR_CIRCUMFLEX_ACCENT = 94; /* ^ */ 42 | export const CHAR_GRAVE_ACCENT = 96; /* ` */ 43 | export const CHAR_AT = 64; /* @ */ 44 | export const CHAR_AMPERSAND = 38; /* & */ 45 | export const CHAR_EQUAL = 61; /* = */ 46 | 47 | // Digits 48 | export const CHAR_0 = 48; /* 0 */ 49 | export const CHAR_9 = 57; /* 9 */ 50 | 51 | export const SEP = "/"; 52 | export const SEP_PATTERN = /\/+/; 53 | 54 | export type OSType = "windows" | "linux" | "darwin"; -------------------------------------------------------------------------------- /src/ts/deno/path/_interface.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. 2 | // This module is browser compatible. 3 | 4 | /** 5 | * A parsed path object generated by path.parse() or consumed by path.format(). 6 | */ 7 | export interface ParsedPath { 8 | /** 9 | * The root of the path such as '/' or 'c:\' 10 | */ 11 | root: string; 12 | /** 13 | * The full directory path such as '/home/user/dir' or 'c:\path\dir' 14 | */ 15 | dir: string; 16 | /** 17 | * The file name including extension (if any) such as 'index.html' 18 | */ 19 | base: string; 20 | /** 21 | * The file extension (if any) such as '.html' 22 | */ 23 | ext: string; 24 | /** 25 | * The file name without extension (if any) such as 'index' 26 | */ 27 | name: string; 28 | } 29 | 30 | export type FormatInputPathObject = Partial; 31 | -------------------------------------------------------------------------------- /src/ts/deno/path/_util.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. 2 | // Copyright the Browserify authors. MIT License. 3 | // Ported from https://github.com/browserify/path-browserify/ 4 | // This module is browser compatible. 5 | 6 | import type { FormatInputPathObject } from "./_interface"; 7 | import { 8 | CHAR_BACKWARD_SLASH, 9 | CHAR_DOT, 10 | CHAR_FORWARD_SLASH, 11 | CHAR_LOWERCASE_A, 12 | CHAR_LOWERCASE_Z, 13 | CHAR_UPPERCASE_A, 14 | CHAR_UPPERCASE_Z, 15 | } from "./_constants"; 16 | 17 | export function assertPath(path: string): void { 18 | if (typeof path !== "string") { 19 | throw new TypeError( 20 | `Path must be a string. Received ${JSON.stringify(path)}`, 21 | ); 22 | } 23 | } 24 | 25 | export function isPosixPathSeparator(code: number): boolean { 26 | return code === CHAR_FORWARD_SLASH; 27 | } 28 | 29 | export function isPathSeparator(code: number): boolean { 30 | return isPosixPathSeparator(code) || code === CHAR_BACKWARD_SLASH; 31 | } 32 | 33 | export function isWindowsDeviceRoot(code: number): boolean { 34 | return ( 35 | (code >= CHAR_LOWERCASE_A && code <= CHAR_LOWERCASE_Z) || 36 | (code >= CHAR_UPPERCASE_A && code <= CHAR_UPPERCASE_Z) 37 | ); 38 | } 39 | 40 | // Resolves . and .. elements in a path with directory names 41 | export function normalizeString( 42 | path: string, 43 | allowAboveRoot: boolean, 44 | separator: string, 45 | isPathSeparator: (code: number) => boolean, 46 | ): string { 47 | let res = ""; 48 | let lastSegmentLength = 0; 49 | let lastSlash = -1; 50 | let dots = 0; 51 | let code: number | undefined; 52 | for (let i = 0, len = path.length; i <= len; ++i) { 53 | if (i < len) code = path.charCodeAt(i); 54 | else if (isPathSeparator(code!)) break; 55 | else code = CHAR_FORWARD_SLASH; 56 | 57 | if (isPathSeparator(code!)) { 58 | if (lastSlash === i - 1 || dots === 1) { 59 | // NOOP 60 | } else if (lastSlash !== i - 1 && dots === 2) { 61 | if ( 62 | res.length < 2 || 63 | lastSegmentLength !== 2 || 64 | res.charCodeAt(res.length - 1) !== CHAR_DOT || 65 | res.charCodeAt(res.length - 2) !== CHAR_DOT 66 | ) { 67 | if (res.length > 2) { 68 | const lastSlashIndex = res.lastIndexOf(separator); 69 | if (lastSlashIndex === -1) { 70 | res = ""; 71 | lastSegmentLength = 0; 72 | } else { 73 | res = res.slice(0, lastSlashIndex); 74 | lastSegmentLength = res.length - 1 - res.lastIndexOf(separator); 75 | } 76 | lastSlash = i; 77 | dots = 0; 78 | continue; 79 | } else if (res.length === 2 || res.length === 1) { 80 | res = ""; 81 | lastSegmentLength = 0; 82 | lastSlash = i; 83 | dots = 0; 84 | continue; 85 | } 86 | } 87 | if (allowAboveRoot) { 88 | if (res.length > 0) res += `${separator}..`; 89 | else res = ".."; 90 | lastSegmentLength = 2; 91 | } 92 | } else { 93 | if (res.length > 0) res += separator + path.slice(lastSlash + 1, i); 94 | else res = path.slice(lastSlash + 1, i); 95 | lastSegmentLength = i - lastSlash - 1; 96 | } 97 | lastSlash = i; 98 | dots = 0; 99 | } else if (code === CHAR_DOT && dots !== -1) { 100 | ++dots; 101 | } else { 102 | dots = -1; 103 | } 104 | } 105 | return res; 106 | } 107 | 108 | export function _format( 109 | sep: string, 110 | pathObject: FormatInputPathObject, 111 | ): string { 112 | const dir: string | undefined = pathObject.dir || pathObject.root; 113 | const base: string = pathObject.base || 114 | (pathObject.name || "") + (pathObject.ext || ""); 115 | if (!dir) return base; 116 | if (dir === pathObject.root) return dir + base; 117 | return dir + sep + base; 118 | } 119 | 120 | const WHITESPACE_ENCODINGS: Record = { 121 | "\u0009": "%09", 122 | "\u000A": "%0A", 123 | "\u000B": "%0B", 124 | "\u000C": "%0C", 125 | "\u000D": "%0D", 126 | "\u0020": "%20", 127 | }; 128 | 129 | export function encodeWhitespace(string: string): string { 130 | return string.replaceAll(/[\s]/g, (c) => { 131 | return WHITESPACE_ENCODINGS[c] ?? c; 132 | }); 133 | } -------------------------------------------------------------------------------- /src/ts/deno/path/mod.ts: -------------------------------------------------------------------------------- 1 | // // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. 2 | // // Copyright the Browserify authors. MIT License. 3 | // // Ported mostly from https://github.com/browserify/path-browserify/ 4 | // // This module is browser compatible. 5 | 6 | // import * as _posix from "./posix"; 7 | 8 | // const path = _posix; 9 | // export const posix = _posix; 10 | // export const { 11 | // basename, 12 | // delimiter, 13 | // dirname, 14 | // extname, 15 | // format, 16 | // fromFileUrl, 17 | // isAbsolute, 18 | // join, 19 | // normalize, 20 | // parse, 21 | // relative, 22 | // resolve, 23 | // sep, 24 | // toFileUrl, 25 | // toNamespacedPath, 26 | // } = path; 27 | 28 | // export { SEP, SEP_PATTERN } from "./_constants"; 29 | // export * from "./_interface"; 30 | // export * from "./glob"; 31 | import * as _posix from "@std/path/posix"; 32 | 33 | const path = _posix; 34 | export const posix = _posix; 35 | export const { 36 | basename, 37 | delimiter, 38 | dirname, 39 | extname, 40 | format, 41 | fromFileUrl, 42 | isAbsolute, 43 | join, 44 | normalize, 45 | parse, 46 | relative, 47 | resolve, 48 | sep, 49 | toFileUrl, 50 | toNamespacedPath, 51 | } = path; 52 | 53 | export { SEPARATOR as SEP, SEPARATOR_PATTERN as SEP_PATTERN } from "@std/path/constants"; 54 | export * from "@std/path/glob-to-regexp"; 55 | 56 | // export * from "./_interface"; 57 | // export * from "@std/path"; 58 | // export * from "@std/path/posix"; 59 | // export * from "@std/path/posix/extname"; 60 | // export * from "@std/path/posix/join"; -------------------------------------------------------------------------------- /src/ts/deno/tar/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@std/tar", 3 | "version": "0.1.0", 4 | "exports": { 5 | ".": "./mod.ts", 6 | "./tar-stream": "./tar_stream.ts", 7 | "./untar-stream": "./untar_stream.ts" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/ts/deno/tar/mod.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | /** 4 | * Streaming utilities for working with tar archives. 5 | * 6 | * Files are not compressed, only collected into the archive. 7 | * 8 | * ```ts no-eval 9 | * import { UntarStream } from "@std/tar/untar-stream"; 10 | * import { dirname, normalize } from "@std/path"; 11 | * 12 | * for await ( 13 | * const entry of (await Deno.open("./out.tar.gz")) 14 | * .readable 15 | * .pipeThrough(new DecompressionStream("gzip")) 16 | * .pipeThrough(new UntarStream()) 17 | * ) { 18 | * const path = normalize(entry.path); 19 | * await Deno.mkdir(dirname(path)); 20 | * await entry.readable?.pipeTo((await Deno.create(path)).writable); 21 | * } 22 | * ``` 23 | * 24 | * @experimental **UNSTABLE**: New API, yet to be vetted. 25 | * 26 | * @module 27 | */ 28 | export * from "./tar_stream.ts"; 29 | export * from "./untar_stream.ts"; 30 | -------------------------------------------------------------------------------- /src/ts/deno/tar/unstable_fixed_chunk_stream.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. 2 | 3 | /** 4 | * A transform stream that resize {@linkcode Uint8Array} chunks into perfectly 5 | * `size` chunks with the exception of the last chunk. 6 | * 7 | * @experimental **UNSTABLE**: New API, yet to be vetted. 8 | * 9 | * @example Usage 10 | * ```ts 11 | * import { FixedChunkStream } from "@std/streams/unstable-fixed-chunk-stream"; 12 | * import { assertEquals } from "@std/assert/equals"; 13 | * 14 | * const readable = ReadableStream.from(function* () { 15 | * let count = 0 16 | * for (let i = 0; i < 100; ++i) { 17 | * const array = new Uint8Array(Math.floor(Math.random() * 1000)); 18 | * count += array.length; 19 | * yield array; 20 | * } 21 | * yield new Uint8Array(512 - count % 512) 22 | * }()) 23 | * .pipeThrough(new FixedChunkStream(512)) 24 | * .pipeTo(new WritableStream({ 25 | * write(chunk, _controller) { 26 | * assertEquals(chunk.length, 512) 27 | * } 28 | * })) 29 | * ``` 30 | */ 31 | export class FixedChunkStream extends TransformStream { 32 | /** 33 | * Constructs a new instance. 34 | * 35 | * @param size The size of the chunks to be resized to. 36 | */ 37 | constructor(size: number) { 38 | let push: Uint8Array | undefined; 39 | super({ 40 | transform(chunk, controller) { 41 | if (push !== undefined) { 42 | const concat = new Uint8Array(push.length + chunk.length); 43 | concat.set(push); 44 | concat.set(chunk, push.length); 45 | chunk = concat; 46 | } 47 | 48 | for (let i = size; i <= chunk.length; i += size) { 49 | controller.enqueue(chunk.slice(i - size, i)); 50 | } 51 | const remainder = -chunk.length % size; 52 | push = remainder ? chunk.slice(remainder) : undefined; 53 | }, 54 | flush(controller) { 55 | if (push?.length) { 56 | controller.enqueue(push); 57 | } 58 | }, 59 | }); 60 | } 61 | } -------------------------------------------------------------------------------- /src/ts/deno/zstd/zstd.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okikio/bundlejs/d3429967593b048862fd24727492bbae9062eed0/src/ts/deno/zstd/zstd.wasm -------------------------------------------------------------------------------- /src/ts/plugins/alias.ts: -------------------------------------------------------------------------------- 1 | import type { OnResolveArgs, OnResolveResult, Plugin } from 'esbuild'; 2 | 3 | import { parse as parsePackageName } from "parse-package-name"; 4 | import { EXTERNALS_NAMESPACE } from './external'; 5 | import { HTTP_RESOLVE } from './http'; 6 | 7 | import { getCDNUrl, DEFAULT_CDN_HOST } from '../util/util-cdn'; 8 | import { isBareImport } from '../util/path'; 9 | 10 | /** Alias Plugin Namespace */ 11 | export const ALIAS_NAMESPACE = 'alias-globals'; 12 | 13 | /** 14 | * Checks if a package has an alias 15 | * 16 | * @param id The package to find an alias for 17 | * @param aliases An object with package as the key and the package alias as the value, e.g. { "fs": "memfs" } 18 | */ 19 | export const isAlias = (id: string, aliases = {}) => { 20 | if (!isBareImport(id)) return false; 21 | 22 | let aliasKeys = Object.keys(aliases); 23 | let path = id.replace(/^node\:/, ""); 24 | let pkgDetails = parsePackageName(path); 25 | 26 | return aliasKeys.find((it: string): boolean => { 27 | return pkgDetails.name === it; // import 'foo' & alias: { 'foo': 'bar@5.0' } 28 | }); 29 | }; 30 | 31 | /** 32 | * Resolution algorithm for the esbuild ALIAS plugin 33 | * 34 | * @param aliases An object with package as the key and the package alias as the value, e.g. { "fs": "memfs" } 35 | * @param host The default host origin to use if an import doesn't already have one 36 | * @param logger Console log 37 | */ 38 | export const ALIAS_RESOLVE = (packageSizeMap = new Map(), aliases = {}, host = DEFAULT_CDN_HOST, logger = console.log) => { 39 | return async (args: OnResolveArgs): Promise => { 40 | let path = args.path.replace(/^node\:/, ""); 41 | let { path: argPath } = getCDNUrl(path); 42 | 43 | if (isAlias(argPath, aliases)) { 44 | let pkgDetails = parsePackageName(argPath); 45 | let aliasPath = aliases[pkgDetails.name]; 46 | return HTTP_RESOLVE(packageSizeMap, host, logger)({ 47 | ...args, 48 | path: aliasPath 49 | }); 50 | } 51 | }; 52 | }; 53 | 54 | /** 55 | * Esbuild ALIAS plugin 56 | * 57 | * @param aliases An object with package as the key and the package alias as the value, e.g. { "fs": "memfs" } 58 | * @param host The default host origin to use if an import doesn't already have one 59 | * @param logger Console log 60 | */ 61 | export const ALIAS = (packageSizeMap = new Map(), aliases = {}, host = DEFAULT_CDN_HOST, logger = console.log): Plugin => { 62 | return { 63 | name: ALIAS_NAMESPACE, 64 | setup(build) { 65 | // Intercept import paths starting with "http:" and "https:" so 66 | // esbuild doesn't attempt to map them to a file system location. 67 | // Tag them with the "http-url" namespace to associate them with 68 | // this plugin. 69 | build.onResolve({ filter: /^node\:.*/ }, (args) => { 70 | if (isAlias(args.path, aliases)) 71 | return ALIAS_RESOLVE(packageSizeMap, aliases, host, logger)(args); 72 | 73 | return { 74 | path: args.path, 75 | namespace: EXTERNALS_NAMESPACE, 76 | external: true 77 | }; 78 | }); 79 | 80 | // We also want to intercept all import paths inside downloaded 81 | // files and resolve them against the original URL. All of these 82 | // files will be in the "http-url" namespace. Make sure to keep 83 | // the newly resolved URL in the "http-url" namespace so imports 84 | // inside it will also be resolved as URLs recursively. 85 | build.onResolve({ filter: /.*/ }, ALIAS_RESOLVE(packageSizeMap, aliases, host, logger)); 86 | build.onResolve({ filter: /.*/, namespace: ALIAS_NAMESPACE }, ALIAS_RESOLVE(packageSizeMap, aliases, host, logger)); 87 | }, 88 | }; 89 | }; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/index.ts: -------------------------------------------------------------------------------- 1 | // Based off of https://github.com/btd/esbuild-visualizer 2 | import type { TemplateType } from "./types/template-types"; 3 | import type { ModuleMeta, ModulePart, ModuleTree, ModuleUID, VisualizerData } from "./types/types"; 4 | import type { Metadata } from "./types/metafile"; 5 | import type { OutputFile } from "esbuild-wasm"; 6 | import { AnalyzerOptions, visualizer } from "./plugin/index"; 7 | 8 | export const analyze = async (metadata: Metadata, outputFiles: OutputFile[], opts: AnalyzerOptions = {}, logger = console.log) => { 9 | try { 10 | return await visualizer(metadata, outputFiles, { 11 | title: "Bundle Analysis", 12 | ...opts 13 | }); 14 | } catch (err) { 15 | let { stack } = (err as Error); 16 | logger([`[Analyzer] ${err}`, stack], "warning"); 17 | console.warn(err, stack); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/plugin/build-stats.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import type { VisualizerData } from "../types/types"; 3 | import type { TemplateType } from "../types/template-types"; 4 | 5 | // import { getRequest } from "../../../util/cache"; 6 | 7 | const htmlEscape = (str: string) => 8 | str.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(//g, ">"); 9 | 10 | interface BuildHtmlOptions { 11 | title: string; 12 | data: VisualizerData; 13 | template: TemplateType; 14 | } 15 | 16 | export async function buildHtml({ title, data, template }: BuildHtmlOptions): Promise { 17 | // const [script, style]: Response[] = await Promise.all([ 18 | // getRequest(`/js/${template}.min.js`), 19 | // getRequest(`/js/${template}.min.css`) 20 | // ]); 21 | 22 | return ` 23 | 24 | 25 | 26 | 27 | 28 | 29 | ${htmlEscape(title)} 30 | 31 | 32 | 33 |
34 | 51 | 52 | 53 | `; 54 | } 55 | -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/plugin/compress.ts: -------------------------------------------------------------------------------- 1 | import { gzip, getWASM } from "../../../deno/denoflate/mod"; 2 | import { compress } from "../../../deno/brotli/mod"; 3 | 4 | import { encode } from "../../../util/encode-decode"; 5 | 6 | export type SizeGetter = (code: Uint8Array) => Promise; 7 | 8 | export const emptySizeGetter: SizeGetter = () => Promise.resolve(0); 9 | export const gzipSizeGetter: SizeGetter = async (code: Uint8Array) => { 10 | await getWASM(); 11 | const data = await gzip(code, 9); 12 | return data.length; 13 | }; 14 | 15 | export const brotliSizeGetter: SizeGetter = async (code: Uint8Array) => { 16 | const data = await compress(code, code.length, 11); 17 | return data.length; 18 | }; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/plugin/data.ts: -------------------------------------------------------------------------------- 1 | import type { GetModuleInfo } from "../types/rollup"; 2 | import type { ModuleLengths, ModuleTree, ModuleTreeLeaf } from "../types/types"; 3 | 4 | import { isModuleTree } from "../utils/is-module-tree"; 5 | import { ModuleMapper } from "./module-mapper"; 6 | 7 | interface MappedNode { 8 | uid: string; 9 | } 10 | 11 | const addToPath = (moduleId: string, tree: ModuleTree, modulePath: string[], node: MappedNode): void => { 12 | if (modulePath.length === 0) { 13 | throw new Error(`Error adding node to path ${moduleId}`); 14 | } 15 | 16 | const [head, ...rest] = modulePath; 17 | 18 | if (rest.length === 0) { 19 | tree.children.push({ ...node, name: head }); 20 | return; 21 | } else { 22 | let newTree = tree.children.find((folder): folder is ModuleTree => folder.name === head && isModuleTree(folder)); 23 | 24 | if (!newTree) { 25 | newTree = { name: head, children: [] }; 26 | tree.children.push(newTree); 27 | } 28 | addToPath(moduleId, newTree, rest, node); 29 | return; 30 | } 31 | }; 32 | 33 | // TODO try to make it without recursion, but still typesafe 34 | const mergeSingleChildTrees = (tree: ModuleTree): ModuleTree | ModuleTreeLeaf => { 35 | if (tree.children.length === 1) { 36 | const child = tree.children[0]; 37 | const name = `${tree.name}/${child.name}`; 38 | if (isModuleTree(child)) { 39 | tree.name = name; 40 | tree.children = child.children; 41 | return mergeSingleChildTrees(tree); 42 | } else { 43 | return { 44 | name, 45 | uid: child.uid, 46 | }; 47 | } 48 | } else { 49 | tree.children = tree.children.map((node) => { 50 | if (isModuleTree(node)) { 51 | return mergeSingleChildTrees(node); 52 | } else { 53 | return node; 54 | } 55 | }); 56 | return tree; 57 | } 58 | }; 59 | 60 | export const buildTree = ( 61 | bundleId: string, 62 | modules: Array, 63 | mapper: ModuleMapper 64 | ): ModuleTree => { 65 | const tree: ModuleTree = { 66 | name: bundleId, 67 | children: [], 68 | }; 69 | 70 | for (const { id, renderedLength, gzipLength, brotliLength } of modules) { 71 | const bundleModuleUid = mapper.setNodePart(bundleId, id, { renderedLength, gzipLength, brotliLength }); 72 | 73 | const trimmedModuleId = mapper.trimProjectRootId(id); 74 | 75 | const pathParts = trimmedModuleId.split(/\\|\//).filter((p) => p !== ""); 76 | addToPath(trimmedModuleId, tree, pathParts, { uid: bundleModuleUid }); 77 | } 78 | 79 | tree.children = tree.children.map((node) => { 80 | if (isModuleTree(node)) { 81 | return mergeSingleChildTrees(node); 82 | } else { 83 | return node; 84 | } 85 | }); 86 | 87 | return tree; 88 | }; 89 | 90 | export const mergeTrees = (trees: Array): ModuleTree => { 91 | const newTree = { 92 | name: "root", 93 | children: trees, 94 | isRoot: true, 95 | }; 96 | 97 | return newTree; 98 | }; 99 | 100 | export const addLinks = (startModuleId: string, getModuleInfo: GetModuleInfo, mapper: ModuleMapper): void => { 101 | const processedNodes: Record = {}; 102 | 103 | const moduleIds = [startModuleId]; 104 | 105 | while (moduleIds.length > 0) { 106 | const moduleId = moduleIds.shift() as string; 107 | 108 | if (processedNodes[moduleId]) { 109 | continue; 110 | } else { 111 | processedNodes[moduleId] = true; 112 | } 113 | 114 | const moduleInfo = getModuleInfo(moduleId); 115 | 116 | if (!moduleInfo) { 117 | return; 118 | } 119 | 120 | if (moduleInfo.isEntry) { 121 | mapper.setNodeMeta(moduleId, { isEntry: true }); 122 | } 123 | if (moduleInfo.isExternal) { 124 | mapper.setNodeMeta(moduleId, { isExternal: true }); 125 | } 126 | 127 | for (const importedId of moduleInfo.importedIds) { 128 | mapper.addImportedByLink(importedId, moduleId); 129 | mapper.addImportedLink(moduleId, importedId); 130 | 131 | moduleIds.push(importedId); 132 | } 133 | for (const importedId of moduleInfo.dynamicallyImportedIds) { 134 | mapper.addImportedByLink(importedId, moduleId); 135 | mapper.addImportedLink(moduleId, importedId, true); 136 | 137 | moduleIds.push(importedId); 138 | } 139 | } 140 | }; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/plugin/index.ts: -------------------------------------------------------------------------------- 1 | import type { TemplateType } from "../types/template-types"; 2 | import type { ModuleLengths, ModuleTree, ModuleTreeLeaf, VisualizerData } from "../types/types"; 3 | import type { Metadata, MetadataOutput } from "../types/metafile"; 4 | import type { ModuleInfo } from "../types/rollup"; 5 | import type { OutputFile } from "esbuild-wasm"; 6 | 7 | import { ModuleMapper } from "./module-mapper"; 8 | import { addLinks, buildTree, mergeTrees } from "./data"; 9 | import { buildHtml } from "./build-stats"; 10 | 11 | import { gzipSizeGetter, brotliSizeGetter, emptySizeGetter } from "./compress"; 12 | 13 | /** 14 | * Analyzer options 15 | */ 16 | export interface AnalyzerOptions { 17 | title?: string; 18 | template?: TemplateType | boolean; 19 | gzipSize?: boolean; 20 | brotliSize?: boolean; 21 | } 22 | 23 | export const visualizer = async (metadata: Metadata, outputFiles: OutputFile[], opts: AnalyzerOptions = {}): Promise => { 24 | const title = opts.title ?? "Esbuild Visualizer"; 25 | const template = (opts.template == true ? "treemap" : opts.template as TemplateType) ?? "treemap"; 26 | const projectRoot = ""; 27 | 28 | let outputFilesMap = new Map(); 29 | outputFiles.forEach(({ path, contents }) => { 30 | outputFilesMap.set(path, contents); 31 | }); 32 | // console.log(metadata, outputFiles, Array.from(outputFilesMap.entries())); 33 | 34 | const gzipSize = !!opts.gzipSize; 35 | const brotliSize = !!opts.brotliSize; 36 | const gzip = gzipSize ? gzipSizeGetter : emptySizeGetter; 37 | const brotli = brotliSize ? brotliSizeGetter : emptySizeGetter; 38 | 39 | const ModuleLengths = async ({ 40 | id, 41 | mod 42 | }: { 43 | id: string; 44 | mod: { bytesInOutput: number }; 45 | }): Promise => { 46 | const code = outputFilesMap.get(id); 47 | let faultyCode = code == null || code == undefined || code?.length == 0; 48 | let [gzipLength, brotliLength, renderedLength] = await Promise.all(faultyCode ? [0, 0, mod.bytesInOutput] : [gzip(code), brotli(code), code?.length]) 49 | const result = { 50 | id, 51 | gzipLength, 52 | brotliLength, 53 | renderedLength 54 | }; 55 | return result; 56 | }; 57 | 58 | const roots: Array = []; 59 | const mapper = new ModuleMapper(projectRoot); 60 | 61 | // collect trees 62 | for (const [bundleId, bundle] of Object.entries(metadata.outputs)) { 63 | const modules = await Promise.all( 64 | Object 65 | .entries(bundle.inputs) 66 | .map(([id, mod]) => ModuleLengths({ id, mod })) 67 | ); 68 | const tree = buildTree(bundleId, modules, mapper); 69 | 70 | const code = outputFilesMap.get(bundleId); 71 | if (tree.children.length === 0 && code) { 72 | const bundleSizes = await ModuleLengths({ 73 | id: bundleId, 74 | mod: { bytesInOutput: code?.length } 75 | }); 76 | 77 | const facadeModuleId = `${bundleId}-unknown`; 78 | const bundleUid = mapper.setNodePart(bundleId, facadeModuleId, bundleSizes); 79 | mapper.setNodeMeta(facadeModuleId, { isEntry: true }); 80 | const leaf: ModuleTreeLeaf = { name: bundleId, uid: bundleUid }; 81 | roots.push(leaf); 82 | } else { 83 | roots.push(tree); 84 | } 85 | } 86 | 87 | const getModuleInfo = (bundle: MetadataOutput) => (moduleId: string): ModuleInfo => { 88 | const input = metadata.inputs?.[moduleId]; 89 | 90 | const imports = input?.imports.map((i) => i.path); 91 | 92 | const code = outputFilesMap.get(moduleId); 93 | 94 | return { 95 | renderedLength: code?.length ?? bundle.inputs?.[moduleId]?.bytesInOutput ?? 0, 96 | importedIds: imports ?? [], 97 | dynamicallyImportedIds: [], 98 | isEntry: bundle.entryPoint === moduleId, 99 | isExternal: false, 100 | }; 101 | }; 102 | 103 | for (const [bundleId, bundle] of Object.entries(metadata.outputs)) { 104 | if (bundle.entryPoint == null) continue; 105 | 106 | addLinks(bundleId, getModuleInfo(bundle), mapper); 107 | } 108 | 109 | const tree = mergeTrees(roots); 110 | 111 | const data: VisualizerData = { 112 | version: 3.0, 113 | tree, 114 | nodeParts: mapper.getNodeParts(), 115 | nodeMetas: mapper.getNodeMetas(), 116 | env: {}, 117 | options: { 118 | gzip: gzipSize, 119 | brotli: brotliSize 120 | }, 121 | }; 122 | 123 | const fileContent: string = await buildHtml({ 124 | title, 125 | data, 126 | template, 127 | }); 128 | 129 | return fileContent; 130 | }; 131 | -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/color.ts: -------------------------------------------------------------------------------- 1 | import type { ModuleTree, ModuleTreeLeaf } from "../types/types"; 2 | import { HierarchyRectangularNode } from "d3-hierarchy"; 3 | 4 | export type CssColor = string; 5 | 6 | export const COLOR_DEFAULT_FILE: CssColor = "#db7100"; 7 | export const COLOR_DEFAULT_OWN_SOURCE: CssColor = "#487ea4"; 8 | export const COLOR_DEFAULT_VENDOR_SOURCE: CssColor = "#599e59"; 9 | 10 | export const COLOR_BASE: CssColor = "#cecece"; 11 | 12 | const colorDefault = (node: HierarchyRectangularNode): CssColor => { 13 | if (node.children && node.children.length) { 14 | const parents = node.ancestors(); 15 | const hasNodeModules = parents.some(({ data: { name } }) => name === "node_modules"); 16 | return hasNodeModules ? COLOR_DEFAULT_VENDOR_SOURCE : COLOR_DEFAULT_OWN_SOURCE; 17 | } else { 18 | return COLOR_DEFAULT_FILE; 19 | } 20 | }; 21 | 22 | export default colorDefault; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/network/chart.tsx: -------------------------------------------------------------------------------- 1 | import { h, Fragment, FunctionalComponent } from "preact"; 2 | import { useState, useEffect } from "preact/hooks"; 3 | import webcola from "webcola"; 4 | 5 | import type { SizeKey } from "../../types/types"; 6 | import { Tooltip } from "./tooltip"; 7 | import { Network } from "./network"; 8 | import { NetworkLink, NetworkNode } from "./index"; 9 | 10 | export interface ChartProps { 11 | sizeProperty: SizeKey; 12 | links: NetworkLink[]; 13 | nodes: NetworkNode[]; 14 | groups: Record; 15 | } 16 | 17 | export const Chart: FunctionalComponent = ({ sizeProperty, links, nodes, groups }) => { 18 | const [showTooltip, setShowTooltip] = useState(false); 19 | const [tooltipNode, setTooltipNode] = useState(undefined); 20 | 21 | useEffect(() => { 22 | const handleMouseOut = () => { 23 | setShowTooltip(false); 24 | }; 25 | 26 | document.addEventListener("mouseover", handleMouseOut); 27 | return () => { 28 | document.removeEventListener("mouseover", handleMouseOut); 29 | }; 30 | }, []); 31 | 32 | return ( 33 | <> 34 | { 39 | setTooltipNode(node); 40 | setShowTooltip(true); 41 | }} 42 | /> 43 | 44 | 45 | ); 46 | }; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/network/color.ts: -------------------------------------------------------------------------------- 1 | import { COLOR_DEFAULT_OWN_SOURCE, COLOR_DEFAULT_VENDOR_SOURCE, COLOR_BASE, CssColor } from "../color"; 2 | import { NODE_MODULES } from "./util"; 3 | 4 | export const getModuleColor = ({ renderedLength, id }: { renderedLength: number; id: string }): CssColor => 5 | renderedLength === 0 ? COLOR_BASE : NODE_MODULES.test(id) ? COLOR_DEFAULT_VENDOR_SOURCE : COLOR_DEFAULT_OWN_SOURCE; 6 | -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/network/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ModuleMeta, ModuleLengths, ModuleUID, SizeKey, VisualizerData } from "../../types/types"; 2 | 3 | import { h, createContext, render } from "preact"; 4 | import webcola from "webcola"; 5 | 6 | import { Main } from "./main"; 7 | 8 | import { getAvailableSizeOptions } from "../sizes"; 9 | import { CssColor } from "../color"; 10 | 11 | import "../style/style-treemap.scss"; 12 | 13 | export type NetworkNode = NodeInfo & { color: CssColor; radius: number } & webcola.Node; 14 | export type NetworkLink = webcola.Link; 15 | 16 | export interface StaticData { 17 | data: VisualizerData; 18 | availableSizeProperties: SizeKey[]; 19 | width: number; 20 | height: number; 21 | } 22 | 23 | export type NodeInfo = { uid: ModuleUID } & ModuleMeta & ModuleLengths; 24 | export type ModuleNodeInfo = Map; 25 | 26 | export interface ChartData { 27 | nodes: Record; 28 | } 29 | 30 | export type Context = StaticData & ChartData; 31 | 32 | export const StaticContext = createContext({} as unknown as Context); 33 | 34 | const createNodeInfo = (data: VisualizerData, availableSizeProperties: SizeKey[], uid: ModuleUID): NodeInfo => { 35 | const meta = data.nodeMetas[uid]; 36 | const entries: ModuleLengths[] = Object.values(meta.moduleParts).map((partUid) => data.nodeParts[partUid]); 37 | const sizes = Object.fromEntries(availableSizeProperties.map((key) => [key, 0])) as unknown as ModuleLengths; 38 | 39 | for (const renderInfo of entries) { 40 | for (const sizeKey of availableSizeProperties) { 41 | sizes[sizeKey] += renderInfo[sizeKey] ?? 0; 42 | } 43 | } 44 | return { uid, ...sizes, ...meta }; 45 | }; 46 | 47 | const drawChart = (parentNode: Element, data: VisualizerData, width: number, height: number): void => { 48 | const availableSizeProperties = getAvailableSizeOptions(data.options); 49 | 50 | const nodes: Record = {}; 51 | for (const uid of Object.keys(data.nodeMetas)) { 52 | nodes[uid] = createNodeInfo(data, availableSizeProperties, uid); 53 | } 54 | 55 | render( 56 | 65 |
66 | , 67 | parentNode 68 | ); 69 | }; 70 | 71 | export default drawChart; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/network/network.tsx: -------------------------------------------------------------------------------- 1 | import { h, FunctionalComponent } from "preact"; 2 | import { useContext } from "preact/hooks"; 3 | import webcola from "webcola"; 4 | import { NetworkLink, NetworkNode, StaticContext } from "./index"; 5 | import { COLOR_BASE } from "../color"; 6 | 7 | export interface NetworkProps { 8 | onNodeHover: (event: NetworkNode) => void; 9 | nodes: NetworkNode[]; 10 | links: NetworkLink[]; 11 | 12 | groups: Record; 13 | } 14 | 15 | export const Network: FunctionalComponent = ({ links, nodes, groups, onNodeHover }) => { 16 | const { width, height } = useContext(StaticContext); 17 | return ( 18 | 19 | 20 | {Object.entries(groups).map(([name, group]) => { 21 | const bounds = group.bounds; 22 | return ( 23 | 36 | ); 37 | })} 38 | 39 | 40 | {links.map((link) => { 41 | return ( 42 | 50 | ); 51 | })} 52 | 53 | 54 | {nodes.map((node) => { 55 | return ( 56 | { 63 | evt.stopPropagation(); 64 | onNodeHover(node); 65 | }} 66 | /> 67 | ); 68 | })} 69 | 70 | 71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/network/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import type { SizeKey } from "../../types/types"; 2 | 3 | import { h, Fragment, FunctionalComponent } from "preact"; 4 | import { useContext, useEffect, useMemo, useRef, useState, type MutableRef } from "preact/hooks"; 5 | import { format as formatBytes } from "bytes"; 6 | import { StaticContext, NetworkNode } from "./index"; 7 | import { LABELS } from "../sizes"; 8 | 9 | export interface TooltipProps { 10 | node?: NetworkNode; 11 | sizeProperty: SizeKey; 12 | visible: boolean; 13 | } 14 | 15 | const Tooltip_marginX = 10; 16 | const Tooltip_marginY = 30; 17 | 18 | export const Tooltip: FunctionalComponent = ({ node, visible, sizeProperty }) => { 19 | const { availableSizeProperties, data } = useContext(StaticContext); 20 | 21 | const ref = useRef(null); 22 | const [style, setStyle] = useState({}); 23 | const content = useMemo(() => { 24 | if (!node) return null; 25 | 26 | return ( 27 | <> 28 |
{node.id}
29 | {availableSizeProperties.map((sizeProp) => { 30 | if (sizeProp === sizeProperty) { 31 | return ( 32 |
33 | 34 | {LABELS[sizeProp]}: {formatBytes(node[sizeProp] ?? 0)} 35 | 36 |
37 | ); 38 | } else { 39 | return ( 40 |
41 | {LABELS[sizeProp]}: {formatBytes(node[sizeProp] ?? 0)} 42 |
43 | ); 44 | } 45 | })} 46 | {node.uid && ( 47 |
48 |
49 | Imported By: 50 |
51 | {data.nodeMetas[node.uid].importedBy.map(({ uid }) => { 52 | const { id } = data.nodeMetas[uid]; 53 | return
{id}
; 54 | })} 55 |
56 | )} 57 | 58 | ); 59 | }, [availableSizeProperties, data, node, sizeProperty]); 60 | 61 | const updatePosition = (mouseCoords: { x: number; y: number }) => { 62 | if (!ref.current) return; 63 | 64 | const pos = { 65 | left: mouseCoords.x + Tooltip_marginX, 66 | top: mouseCoords.y + Tooltip_marginY, 67 | }; 68 | 69 | const boundingRect = ref.current.getBoundingClientRect(); 70 | 71 | if (pos.left + boundingRect.width > window.innerWidth) { 72 | // Shifting horizontally 73 | pos.left = window.innerWidth - boundingRect.width; 74 | } 75 | 76 | if (pos.top + boundingRect.height > window.innerHeight) { 77 | // Flipping vertically 78 | pos.top = mouseCoords.y - Tooltip_marginY - boundingRect.height; 79 | } 80 | 81 | setStyle(pos); 82 | }; 83 | 84 | useEffect(() => { 85 | const handleMouseMove = (event: MouseEvent) => { 86 | updatePosition({ 87 | x: event.pageX, 88 | y: event.pageY, 89 | }); 90 | }; 91 | 92 | document.addEventListener("mousemove", handleMouseMove, true); 93 | return () => { 94 | document.removeEventListener("mousemove", handleMouseMove, true); 95 | }; 96 | }, []); 97 | 98 | return ( 99 |
100 | {content} 101 |
102 | ); 103 | }; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/network/util.ts: -------------------------------------------------------------------------------- 1 | export const NODE_MODULES = /.*(?:\/|\\\\)?node_modules(?:\/|\\\\)([^/\\]+)(?:\/|\\\\).+/; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import type { SizeKey } from "../types/types"; 2 | 3 | import { h, FunctionalComponent, JSX } from "preact"; 4 | import { useState } from "preact/hooks"; 5 | import { LABELS } from "./sizes"; 6 | 7 | export interface SideBarProps { 8 | availableSizeProperties: SizeKey[]; 9 | sizeProperty: SizeKey; 10 | setSizeProperty: (key: SizeKey) => void; 11 | onExcludeChange: (value: string) => void; 12 | onIncludeChange: (value: string) => void; 13 | } 14 | 15 | export const SideBar: FunctionalComponent = ({ 16 | availableSizeProperties, 17 | sizeProperty, 18 | setSizeProperty, 19 | onExcludeChange, 20 | onIncludeChange, 21 | }) => { 22 | const [includeValue, setIncludeValue] = useState(""); 23 | const [excludeValue, setExcludeValue] = useState(""); 24 | 25 | const handleSizePropertyChange = (sizeProp: SizeKey) => () => { 26 | if (sizeProp !== sizeProperty) { 27 | setSizeProperty(sizeProp); 28 | } 29 | }; 30 | 31 | const handleIncludeChange = (event: JSX.TargetedEvent) => { 32 | const value = event.currentTarget.value; 33 | setIncludeValue(value); 34 | onIncludeChange(value); 35 | }; 36 | 37 | const handleExcludeChange = (event: JSX.TargetedEvent) => { 38 | const value = event.currentTarget.value; 39 | setExcludeValue(value); 40 | onExcludeChange(value); 41 | }; 42 | 43 | return ( 44 | 73 | ); 74 | }; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/sizes.ts: -------------------------------------------------------------------------------- 1 | import type { SizeKey, VisualizerData } from "../types/types"; 2 | 3 | export const LABELS: Record = { 4 | renderedLength: "Rendered", 5 | gzipLength: "Gzip", 6 | brotliLength: "Brotli", 7 | }; 8 | 9 | export const getAvailableSizeOptions = (options: VisualizerData["options"]): SizeKey[] => { 10 | const availableSizeProperties: SizeKey[] = ["renderedLength"]; 11 | if (options.gzip) { 12 | availableSizeProperties.push("gzipLength"); 13 | } 14 | if (options.brotli) { 15 | availableSizeProperties.push("brotliLength"); 16 | } 17 | 18 | return availableSizeProperties; 19 | }; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/style/_base.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 3 | "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", 4 | "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 5 | --background-color: #f7eedf; 6 | --text-color: #333; 7 | } 8 | 9 | @media (prefers-color-scheme: dark) { 10 | :root { 11 | --background-color: #2b2d42; 12 | --text-color: #edf2f4; 13 | } 14 | } 15 | 16 | html { 17 | box-sizing: border-box; 18 | } 19 | 20 | *, 21 | *:before, 22 | *:after { 23 | box-sizing: inherit; 24 | } 25 | 26 | html { 27 | background-color: var(--background-color); 28 | color: var(--text-color); 29 | font-family: var(--font-family); 30 | } 31 | 32 | body { 33 | padding: 0; 34 | margin: 0; 35 | } 36 | 37 | html, 38 | body { 39 | height: 100%; 40 | width: 100%; 41 | overflow: hidden; 42 | } 43 | 44 | body { 45 | display: flex; 46 | flex-direction: column; 47 | } 48 | 49 | svg { 50 | vertical-align: middle; 51 | width: 100%; 52 | height: 100%; 53 | max-height: 100vh; 54 | } 55 | 56 | main { 57 | flex-grow: 1; 58 | height: 100vh; 59 | padding: 20px; 60 | } 61 | 62 | .tooltip { 63 | position: absolute; 64 | z-index: 1070; 65 | 66 | border: 2px solid; 67 | border-radius: 5px; 68 | 69 | padding: 5px; 70 | 71 | white-space: nowrap; 72 | 73 | font-size: 0.875rem; 74 | 75 | background-color: var(--background-color); 76 | color: var(--text-color); 77 | } 78 | 79 | .tooltip-hidden { 80 | visibility: hidden; 81 | opacity: 0; 82 | } 83 | 84 | .sidebar { 85 | position: fixed; 86 | top: 0; 87 | left: 0; 88 | right: 0; 89 | display: flex; 90 | flex-direction: row; 91 | font-size: 0.7rem; 92 | align-items: center; 93 | margin: 0 50px; 94 | height: 20px; 95 | } 96 | 97 | .size-selectors { 98 | display: flex; 99 | flex-direction: row; 100 | align-items: center; 101 | } 102 | 103 | .size-selector { 104 | display: flex; 105 | flex-direction: row; 106 | align-items: center; 107 | justify-content: center; 108 | margin-right: 1rem; 109 | 110 | input { 111 | margin: 0 0.3rem 0 0; 112 | } 113 | } 114 | 115 | .filters { 116 | flex: 1; 117 | display: flex; 118 | flex-direction: row; 119 | align-items: center; 120 | } 121 | 122 | .module-filters { 123 | display: flex; 124 | } 125 | 126 | .module-filter { 127 | display: flex; 128 | flex-direction: row; 129 | align-items: center; 130 | justify-content: center; 131 | 132 | flex: 1; 133 | 134 | input { 135 | flex: 1; 136 | height: 1rem; 137 | padding: 0.01rem; 138 | font-size: 0.7rem; 139 | margin-left: 0.3rem; 140 | } 141 | 142 | & + & { 143 | margin-left: 0.5rem; 144 | } 145 | } -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/style/style-network.scss: -------------------------------------------------------------------------------- 1 | @import "./base"; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/style/style-sunburst.scss: -------------------------------------------------------------------------------- 1 | @import "./base"; 2 | 3 | .details { 4 | position: absolute; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: center; 9 | 10 | top: calc(50% - 85px); 11 | left: calc(50% - 85px); 12 | 13 | width: 170px; 14 | height: 170px; 15 | 16 | font-size: 14px; 17 | text-align: center; 18 | color: var(--font-color); 19 | z-index: 100; 20 | overflow: hidden; 21 | text-overflow: ellipsis; 22 | } 23 | 24 | .details-size { 25 | font-size: 0.8em; 26 | } 27 | 28 | .details-name { 29 | font-weight: bold; 30 | } 31 | 32 | .details-percentage { 33 | margin: 0.4em 0em; 34 | font-size: 2.4em; 35 | line-height: 1em; 36 | } -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/style/style-treemap.scss: -------------------------------------------------------------------------------- 1 | @import "./base"; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/sunburst/chart.tsx: -------------------------------------------------------------------------------- 1 | import type { ModuleTree, ModuleTreeLeaf, SizeKey } from "../../types/types"; 2 | 3 | import { h, Fragment, FunctionalComponent } from "preact"; 4 | import { useState, useEffect, useMemo } from "preact/hooks"; 5 | 6 | import { HierarchyRectangularNode } from "d3-hierarchy"; 7 | import { Tooltip } from "./tooltip"; 8 | import { SunBurst } from "./sunburst"; 9 | 10 | export interface ChartProps { 11 | root: HierarchyRectangularNode; 12 | sizeProperty: SizeKey; 13 | selectedNode: HierarchyRectangularNode | undefined; 14 | setSelectedNode: (node: HierarchyRectangularNode | undefined) => void; 15 | } 16 | 17 | type NodeSelectHandler = (node: HierarchyRectangularNode) => boolean; 18 | 19 | export const Chart: FunctionalComponent = ({ root, sizeProperty, selectedNode, setSelectedNode }) => { 20 | const [tooltipNode, setTooltipNode] = useState(root); 21 | 22 | const isNodeHighlighted = useMemo(() => { 23 | const highlightedNodes = new Set(tooltipNode === root ? root.descendants() : tooltipNode.ancestors()); 24 | return (node: HierarchyRectangularNode): boolean => { 25 | return highlightedNodes.has(node); 26 | }; 27 | }, [root, tooltipNode]); 28 | 29 | useEffect(() => { 30 | const handleMouseOut = () => { 31 | setTooltipNode(root); 32 | }; 33 | 34 | handleMouseOut(); 35 | document.addEventListener("mouseover", handleMouseOut); 36 | return () => { 37 | document.removeEventListener("mouseover", handleMouseOut); 38 | }; 39 | }, [root]); 40 | 41 | return ( 42 | <> 43 | { 46 | setTooltipNode(node); 47 | }} 48 | isNodeHighlighted={isNodeHighlighted} 49 | selectedNode={selectedNode} 50 | onNodeClick={(node) => { 51 | setSelectedNode(selectedNode === node ? undefined : node); 52 | }} 53 | /> 54 | 55 | 56 | ); 57 | }; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/sunburst/index.tsx: -------------------------------------------------------------------------------- 1 | import type { ModuleLengths, ModuleTree, ModuleTreeLeaf, SizeKey, VisualizerData } from "../../types/types"; 2 | 3 | import { h, createContext, render } from "preact"; 4 | import { hierarchy, HierarchyNode, HierarchyRectangularNode, partition, PartitionLayout } from "d3-hierarchy"; 5 | import { Arc, arc as d3arc } from "d3-shape"; 6 | import { scaleLinear, scaleSqrt } from "d3-scale"; 7 | 8 | import { isModuleTree } from "../../utils/is-module-tree"; 9 | 10 | import { Main } from "./main"; 11 | 12 | import { getAvailableSizeOptions } from "../sizes"; 13 | import { generateUniqueId, Id } from "../uid"; 14 | 15 | import "../style/style-sunburst.scss"; 16 | 17 | export interface StaticData { 18 | data: VisualizerData; 19 | availableSizeProperties: SizeKey[]; 20 | width: number; 21 | height: number; 22 | } 23 | 24 | export interface ModuleIds { 25 | nodeUid: Id; 26 | } 27 | 28 | export interface ChartData { 29 | layout: PartitionLayout; 30 | rawHierarchy: HierarchyNode; 31 | getModuleSize: (node: ModuleTree | ModuleTreeLeaf, sizeKey: SizeKey) => number; 32 | getModuleIds: (node: ModuleTree | ModuleTreeLeaf) => ModuleIds; 33 | size: number; 34 | radius: number; 35 | 36 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 37 | arc: Arc>; 38 | } 39 | 40 | export type Context = StaticData & ChartData; 41 | 42 | export const StaticContext = createContext({} as unknown as Context); 43 | 44 | const drawChart = (parentNode: Element, data: VisualizerData, width: number, height: number): void => { 45 | const availableSizeProperties = getAvailableSizeOptions(data.options); 46 | 47 | const layout = partition(); 48 | 49 | const rawHierarchy = hierarchy(data.tree); 50 | 51 | const nodeSizesCache = new Map(); 52 | 53 | const nodeIdsCache = new Map(); 54 | 55 | const getModuleSize = (node: ModuleTree | ModuleTreeLeaf, sizeKey: SizeKey) => 56 | nodeSizesCache.get(node)?.[sizeKey] ?? 0; 57 | 58 | rawHierarchy.eachAfter((node) => { 59 | const nodeData = node.data; 60 | 61 | nodeIdsCache.set(nodeData, { 62 | nodeUid: generateUniqueId("node"), 63 | }); 64 | 65 | const sizes: ModuleLengths = { renderedLength: 0, gzipLength: 0, brotliLength: 0 }; 66 | if (isModuleTree(nodeData)) { 67 | for (const sizeKey of availableSizeProperties) { 68 | sizes[sizeKey] = nodeData.children.reduce((acc, child) => getModuleSize(child, sizeKey) + acc, 0); 69 | } 70 | } else { 71 | for (const sizeKey of availableSizeProperties) { 72 | sizes[sizeKey] = data.nodeParts[nodeData.uid][sizeKey]; 73 | } 74 | } 75 | nodeSizesCache.set(nodeData, sizes); 76 | }); 77 | 78 | const getModuleIds = (node: ModuleTree | ModuleTreeLeaf) => nodeIdsCache.get(node) as ModuleIds; 79 | 80 | const size = Math.min(width, height); 81 | const radius = size / 2; 82 | 83 | const x = scaleLinear().range([0, 2 * Math.PI]); 84 | const y = scaleSqrt().range([0, radius]); 85 | 86 | const arc = d3arc>() 87 | .startAngle((d) => Math.max(0, Math.min(2 * Math.PI, x(d.x0)))) 88 | .endAngle((d) => Math.max(0, Math.min(2 * Math.PI, x(d.x1)))) 89 | .innerRadius((d) => y(d.y0)) 90 | .outerRadius((d) => y(d.y1)); 91 | 92 | render( 93 | 108 |
109 | , 110 | parentNode 111 | ); 112 | }; 113 | 114 | export default drawChart; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/sunburst/main.tsx: -------------------------------------------------------------------------------- 1 | import type { ModuleTree, ModuleTreeLeaf, SizeKey } from "../../types/types"; 2 | 3 | import { h, Fragment, FunctionalComponent } from "preact"; 4 | import { useContext, useMemo, useState } from "preact/hooks"; 5 | import { HierarchyRectangularNode } from "d3-hierarchy"; 6 | 7 | import { SideBar } from "../sidebar"; 8 | import { useFilter } from "../use-filter"; 9 | import { Chart } from "./chart"; 10 | 11 | import { StaticContext } from "./index"; 12 | import { isModuleTree } from "../../utils/is-module-tree"; 13 | 14 | export const Main: FunctionalComponent = () => { 15 | const { availableSizeProperties, rawHierarchy, getModuleSize, layout, data } = useContext(StaticContext); 16 | 17 | const [sizeProperty, setSizeProperty] = useState(availableSizeProperties[0]); 18 | 19 | const [selectedNode, setSelectedNode] = useState | undefined>( 20 | undefined 21 | ); 22 | 23 | const { getModuleFilterMultiplier, setExcludeFilter, setIncludeFilter } = useFilter(); 24 | 25 | const getNodeSizeMultiplier = useMemo(() => { 26 | if (selectedNode === undefined) { 27 | return (): number => 1; 28 | } else if (isModuleTree(selectedNode.data)) { 29 | const descendants = new Set(selectedNode.descendants().map((d) => d.data)); 30 | return (node: ModuleTree | ModuleTreeLeaf): number => { 31 | if (descendants.has(node)) { 32 | return 3; 33 | } 34 | return 1; 35 | }; 36 | } else { 37 | return (node: ModuleTree | ModuleTreeLeaf): number => { 38 | if (node === selectedNode.data) { 39 | return 3; 40 | } 41 | return 1; 42 | }; 43 | } 44 | }, [selectedNode]); 45 | 46 | // root here always be the same as rawHierarchy even after layouting 47 | const root = useMemo(() => { 48 | const rootWithSizesAndSorted = rawHierarchy 49 | .sum((node) => { 50 | if (isModuleTree(node)) return 0; 51 | const ownSize = getModuleSize(node, sizeProperty); 52 | const zoomMultiplier = getNodeSizeMultiplier(node); 53 | const filterMultiplier = getModuleFilterMultiplier(data.nodeMetas[data.nodeParts[node.uid].mainUid]); 54 | 55 | return ownSize * zoomMultiplier * filterMultiplier; 56 | }) 57 | .sort((a, b) => getModuleSize(a.data, sizeProperty) - getModuleSize(b.data, sizeProperty)); 58 | 59 | return layout(rootWithSizesAndSorted); 60 | }, [data, getModuleFilterMultiplier, getModuleSize, getNodeSizeMultiplier, layout, rawHierarchy, sizeProperty]); 61 | 62 | return ( 63 | <> 64 | 71 | 72 | 73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/sunburst/node.tsx: -------------------------------------------------------------------------------- 1 | import type { ModuleTree, ModuleTreeLeaf } from "../../types/types"; 2 | 3 | import { h, FunctionalComponent } from "preact"; 4 | import { HierarchyRectangularNode } from "d3-hierarchy"; 5 | import color from "../color"; 6 | 7 | type NodeEventHandler = (event: HierarchyRectangularNode) => void; 8 | 9 | export interface NodeProps { 10 | node: HierarchyRectangularNode; 11 | onMouseOver: NodeEventHandler; 12 | path: string; 13 | highlighted: boolean; 14 | selected: boolean; 15 | onClick: NodeEventHandler; 16 | } 17 | 18 | export const Node: FunctionalComponent = ({ node, onMouseOver, onClick, path, highlighted, selected }) => { 19 | return ( 20 | { 26 | evt.stopPropagation(); 27 | onMouseOver(node); 28 | }} 29 | onClick={(evt: MouseEvent) => { 30 | evt.stopPropagation(); 31 | onClick(node); 32 | }} 33 | opacity={highlighted ? 1 : 0.3} 34 | stroke-width={selected ? 3 : undefined} 35 | /> 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/sunburst/sunburst.tsx: -------------------------------------------------------------------------------- 1 | import type { ModuleTree, ModuleTreeLeaf } from "../../types/types"; 2 | 3 | import { h, FunctionalComponent } from "preact"; 4 | import { HierarchyRectangularNode } from "d3-hierarchy"; 5 | import { useContext } from "preact/hooks"; 6 | import { StaticContext } from "./index"; 7 | 8 | import { Node } from "./node"; 9 | 10 | export interface SunBurstProps { 11 | root: HierarchyRectangularNode; 12 | onNodeHover: (event: HierarchyRectangularNode) => void; 13 | isNodeHighlighted: (node: HierarchyRectangularNode) => boolean; 14 | selectedNode: HierarchyRectangularNode | undefined; 15 | onNodeClick: (node: HierarchyRectangularNode) => void; 16 | } 17 | 18 | export const SunBurst: FunctionalComponent = ({ 19 | root, 20 | onNodeHover, 21 | isNodeHighlighted, 22 | selectedNode, 23 | onNodeClick, 24 | }) => { 25 | const { getModuleIds, size, arc, radius } = useContext(StaticContext); 26 | 27 | return ( 28 | 29 | 30 | {root.descendants().map((node) => { 31 | return ( 32 | 41 | ); 42 | })} 43 | 44 | 45 | ); 46 | }; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/sunburst/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import type { ModuleTree, ModuleTreeLeaf, SizeKey } from "../../types/types"; 2 | 3 | import { h, Fragment, FunctionalComponent } from "preact"; 4 | import { useContext, useMemo } from "preact/hooks"; 5 | 6 | import { format as formatBytes } from "bytes"; 7 | 8 | import { HierarchyRectangularNode } from "d3-hierarchy"; 9 | 10 | import { LABELS } from "../sizes"; 11 | import { StaticContext } from "./index"; 12 | 13 | export interface TooltipProps { 14 | node: HierarchyRectangularNode; 15 | root: HierarchyRectangularNode; 16 | sizeProperty: SizeKey; 17 | } 18 | 19 | export const Tooltip: FunctionalComponent = ({ node, root, sizeProperty }) => { 20 | const { availableSizeProperties, getModuleSize } = useContext(StaticContext); 21 | 22 | const content = useMemo(() => { 23 | if (!node) return null; 24 | 25 | const mainSize = getModuleSize(node.data, sizeProperty); 26 | 27 | const percentageNum = (100 * mainSize) / getModuleSize(root.data, sizeProperty); 28 | const percentage = percentageNum.toFixed(2); 29 | const percentageString = percentage + "%"; 30 | 31 | return ( 32 | <> 33 |
{node.data.name}
34 |
{percentageString}
35 | {availableSizeProperties.map((sizeProp) => { 36 | if (sizeProp === sizeProperty) { 37 | return ( 38 |
39 | 40 | {LABELS[sizeProp]}: {formatBytes(getModuleSize(node.data, sizeProp))} 41 | 42 |
43 | ); 44 | } else { 45 | return ( 46 |
47 | {LABELS[sizeProp]}: {formatBytes(getModuleSize(node.data, sizeProp))} 48 |
49 | ); 50 | } 51 | })} 52 | 53 | ); 54 | }, [availableSizeProperties, getModuleSize, node, root.data, sizeProperty]); 55 | 56 | return
{content}
; 57 | }; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/treemap/chart.tsx: -------------------------------------------------------------------------------- 1 | import type { ModuleTree, ModuleTreeLeaf, SizeKey } from "../../types/types"; 2 | 3 | import { h, Fragment, FunctionalComponent } from "preact"; 4 | import { useState, useEffect } from "preact/hooks"; 5 | import { HierarchyRectangularNode } from "d3-hierarchy"; 6 | 7 | import { TreeMap } from "./treemap"; 8 | import { Tooltip } from "./tooltip"; 9 | 10 | export interface ChartProps { 11 | root: HierarchyRectangularNode; 12 | sizeProperty: SizeKey; 13 | selectedNode: HierarchyRectangularNode | undefined; 14 | setSelectedNode: (node: HierarchyRectangularNode | undefined) => void; 15 | } 16 | 17 | export const Chart: FunctionalComponent = ({ root, sizeProperty, selectedNode, setSelectedNode }) => { 18 | const [showTooltip, setShowTooltip] = useState(false); 19 | const [tooltipNode, setTooltipNode] = useState | undefined>( 20 | undefined 21 | ); 22 | 23 | useEffect(() => { 24 | const handleMouseOut = () => { 25 | setShowTooltip(false); 26 | }; 27 | 28 | document.addEventListener("mouseover", handleMouseOut); 29 | return () => { 30 | document.removeEventListener("mouseover", handleMouseOut); 31 | }; 32 | }, []); 33 | 34 | return ( 35 | <> 36 | { 39 | setTooltipNode(node); 40 | setShowTooltip(true); 41 | }} 42 | selectedNode={selectedNode} 43 | onNodeClick={(node) => { 44 | setSelectedNode(selectedNode === node ? undefined : node); 45 | }} 46 | /> 47 | 48 | 49 | ); 50 | }; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/treemap/color.ts: -------------------------------------------------------------------------------- 1 | import type { ModuleTree, ModuleTreeLeaf } from "../../types/types"; 2 | 3 | import * as d3 from "d3"; 4 | import { scaleSequential, scaleLinear } from "d3-scale"; 5 | import { hsl, RGBColor } from "d3-color"; 6 | 7 | import { COLOR_BASE, CssColor } from "../color"; 8 | import { HierarchyNode } from "d3-hierarchy"; 9 | 10 | // https://www.w3.org/TR/WCAG20/#relativeluminancedef 11 | const rc = 0.2126; 12 | const gc = 0.7152; 13 | const bc = 0.0722; 14 | // low-gamma adjust coefficient 15 | const lowc = 1 / 12.92; 16 | 17 | function adjustGamma(p: number) { 18 | return Math.pow((p + 0.055) / 1.055, 2.4); 19 | } 20 | 21 | function relativeLuminance(o: RGBColor) { 22 | const rsrgb = o.r / 255; 23 | const gsrgb = o.g / 255; 24 | const bsrgb = o.b / 255; 25 | 26 | const r = rsrgb <= 0.03928 ? rsrgb * lowc : adjustGamma(rsrgb); 27 | const g = gsrgb <= 0.03928 ? gsrgb * lowc : adjustGamma(gsrgb); 28 | const b = bsrgb <= 0.03928 ? bsrgb * lowc : adjustGamma(bsrgb); 29 | 30 | return r * rc + g * gc + b * bc; 31 | } 32 | 33 | export interface NodeColor { 34 | backgroundColor: CssColor; 35 | fontColor: CssColor; 36 | } 37 | 38 | export type NodeColorGetter = (node: HierarchyNode) => NodeColor; 39 | 40 | const createRainbowColor = (root: HierarchyNode): NodeColorGetter => { 41 | const colorParentMap = new Map, CssColor>(); 42 | colorParentMap.set(root, COLOR_BASE); 43 | 44 | if (root.children != null) { 45 | // d3.scaleSequential([8, 0], d3.interpolateCool) 46 | let colorScale = scaleSequential([Math.max(root?.children?.length, 10), 0], d3.interpolateCool); // hsl(360 * n, 0.3, 0.85) 47 | root.children.forEach((c, id) => { 48 | colorParentMap.set(c, colorScale(id).toString()); 49 | 50 | colorScale = scaleSequential([Math.max(c?.children?.length ?? 0, 15), 5], d3.interpolateWarm); 51 | c?.children?.forEach((child, id) => { 52 | colorParentMap.set(child, colorScale(id).toString()); 53 | 54 | colorScale = scaleSequential([Math.max(child?.children?.length ?? 0, 20), 10], d3.interpolateCool); 55 | child?.children?.forEach((c, id) => { 56 | colorParentMap.set(c, colorScale(id).toString()); 57 | }); 58 | }); 59 | }); 60 | } 61 | 62 | const colorMap = new Map, NodeColor>(); 63 | 64 | const lightScale = scaleLinear().domain([0, root.height]).range([0.9, 0.3]); 65 | 66 | const getBackgroundColor = (node: HierarchyNode) => { 67 | const parents = node.ancestors(); 68 | const colorStr = 69 | parents.length === 1 ? colorParentMap.get(parents[0]) : colorParentMap.get(parents[parents.length - 2]); 70 | 71 | const hslColor = hsl(colorStr as string); 72 | hslColor.l = lightScale(node.depth); 73 | 74 | return hslColor; 75 | }; 76 | 77 | return (node: HierarchyNode): NodeColor => { 78 | if (!colorMap.has(node)) { 79 | const backgroundColor = getBackgroundColor(node); 80 | const l = relativeLuminance(backgroundColor.rgb()); 81 | const fontColor = l > 0.19 ? "#000" : "#fff"; 82 | colorMap.set(node, { 83 | backgroundColor: backgroundColor.toString(), 84 | fontColor, 85 | }); 86 | } 87 | 88 | return colorMap.get(node) as NodeColor; 89 | }; 90 | }; 91 | 92 | export default createRainbowColor; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/treemap/const.ts: -------------------------------------------------------------------------------- 1 | export const TOP_PADDING = 20; 2 | export const PADDING = 2; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/treemap/index.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | ModuleLengths, 3 | ModuleTree, 4 | ModuleTreeLeaf, 5 | SizeKey, 6 | VisualizerData, 7 | } from "../../types/types"; 8 | 9 | import { h, createContext, render } from "preact"; 10 | import { hierarchy, HierarchyNode, treemap, TreemapLayout, treemapResquarify } from "d3-hierarchy"; 11 | 12 | import { Main } from "./main"; 13 | 14 | import { generateUniqueId, Id } from "../uid"; 15 | import { getAvailableSizeOptions } from "../sizes"; 16 | import createRainbowColor, { NodeColorGetter } from "./color"; 17 | 18 | import { isModuleTree } from "../../utils/is-module-tree"; 19 | 20 | import "../style/style-treemap.scss"; 21 | import { PADDING, TOP_PADDING } from "./const"; 22 | 23 | export interface StaticData { 24 | data: VisualizerData; 25 | availableSizeProperties: SizeKey[]; 26 | width: number; 27 | height: number; 28 | } 29 | 30 | export interface ModuleIds { 31 | nodeUid: Id; 32 | clipUid: Id; 33 | } 34 | 35 | export interface ChartData { 36 | layout: TreemapLayout; 37 | rawHierarchy: HierarchyNode; 38 | getModuleSize: (node: ModuleTree | ModuleTreeLeaf, sizeKey: SizeKey) => number; 39 | getModuleIds: (node: ModuleTree | ModuleTreeLeaf) => ModuleIds; 40 | getModuleColor: NodeColorGetter; 41 | } 42 | 43 | export type Context = StaticData & ChartData; 44 | 45 | export const StaticContext = createContext({} as unknown as Context); 46 | 47 | const drawChart = (parentNode: Element, data: VisualizerData, width: number, height: number): void => { 48 | const availableSizeProperties = getAvailableSizeOptions(data.options); 49 | 50 | console.time("layout create"); 51 | 52 | const layout = treemap() 53 | .size([width, height]) 54 | .paddingOuter(PADDING) 55 | .paddingTop(TOP_PADDING) 56 | .paddingInner(PADDING) 57 | .round(true) 58 | .tile(treemapResquarify); 59 | 60 | console.timeEnd("layout create"); 61 | 62 | console.time("rawHierarchy create"); 63 | const rawHierarchy = hierarchy(data.tree); 64 | console.timeEnd("rawHierarchy create"); 65 | 66 | const nodeSizesCache = new Map(); 67 | 68 | const nodeIdsCache = new Map(); 69 | 70 | const getModuleSize = (node: ModuleTree | ModuleTreeLeaf, sizeKey: SizeKey) => 71 | nodeSizesCache.get(node)?.[sizeKey] ?? 0; 72 | 73 | console.time("rawHierarchy eachAfter cache"); 74 | rawHierarchy.eachAfter((node) => { 75 | const nodeData = node.data; 76 | 77 | nodeIdsCache.set(nodeData, { 78 | nodeUid: generateUniqueId("node"), 79 | clipUid: generateUniqueId("clip"), 80 | }); 81 | 82 | const sizes: ModuleLengths = { renderedLength: 0, gzipLength: 0, brotliLength: 0 }; 83 | if (isModuleTree(nodeData)) { 84 | for (const sizeKey of availableSizeProperties) { 85 | sizes[sizeKey] = nodeData.children.reduce((acc, child) => getModuleSize(child, sizeKey) + acc, 0); 86 | } 87 | } else { 88 | for (const sizeKey of availableSizeProperties) { 89 | sizes[sizeKey] = data.nodeParts[nodeData.uid][sizeKey] ?? 0; 90 | } 91 | } 92 | nodeSizesCache.set(nodeData, sizes); 93 | }); 94 | console.timeEnd("rawHierarchy eachAfter cache"); 95 | 96 | const getModuleIds = (node: ModuleTree | ModuleTreeLeaf) => nodeIdsCache.get(node) as ModuleIds; 97 | 98 | console.time("color"); 99 | const getModuleColor = createRainbowColor(rawHierarchy); 100 | console.timeEnd("color"); 101 | 102 | render( 103 | 116 |
117 | , 118 | parentNode 119 | ); 120 | }; 121 | 122 | export default drawChart; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/treemap/main.tsx: -------------------------------------------------------------------------------- 1 | import type { ModuleTree, ModuleTreeLeaf, SizeKey } from "../../types/types"; 2 | 3 | import { h, Fragment, FunctionalComponent } from "preact"; 4 | import { useContext, useMemo, useState } from "preact/hooks"; 5 | import { HierarchyRectangularNode } from "d3-hierarchy"; 6 | 7 | import { SideBar } from "../sidebar"; 8 | import { Chart } from "./chart"; 9 | 10 | import { StaticContext } from "./index"; 11 | import { useFilter } from "../use-filter"; 12 | import { isModuleTree } from "../../utils/is-module-tree"; 13 | 14 | export const Main: FunctionalComponent = () => { 15 | const { availableSizeProperties, rawHierarchy, getModuleSize, layout, data } = useContext(StaticContext); 16 | 17 | const [sizeProperty, setSizeProperty] = useState(availableSizeProperties[0]); 18 | 19 | const [selectedNode, setSelectedNode] = useState | undefined>( 20 | undefined 21 | ); 22 | 23 | const { getModuleFilterMultiplier, setExcludeFilter, setIncludeFilter } = useFilter(); 24 | 25 | console.time("getNodeSizeMultiplier"); 26 | const getNodeSizeMultiplier = useMemo(() => { 27 | const rootSize = getModuleSize(rawHierarchy.data, sizeProperty); 28 | const selectedSize = selectedNode ? getModuleSize(selectedNode.data, sizeProperty) : 1; 29 | const multiplier = rootSize * 0.2 > selectedSize ? (rootSize * 0.2) / selectedSize : 3; 30 | if (selectedNode === undefined) { 31 | return (): number => 1; 32 | } else if (isModuleTree(selectedNode.data)) { 33 | const leaves = new Set(selectedNode.leaves().map((d) => d.data)); 34 | return (node: ModuleTree | ModuleTreeLeaf): number => { 35 | if (leaves.has(node)) { 36 | return multiplier; 37 | } 38 | return 1; 39 | }; 40 | } else { 41 | return (node: ModuleTree | ModuleTreeLeaf): number => { 42 | if (node === selectedNode.data) { 43 | return multiplier; 44 | } 45 | return 1; 46 | }; 47 | } 48 | }, [getModuleSize, rawHierarchy.data, selectedNode, sizeProperty]); 49 | console.timeEnd("getNodeSizeMultiplier"); 50 | 51 | console.time("root hierarchy compute"); 52 | // root here always be the same as rawHierarchy even after layouting 53 | const root = useMemo(() => { 54 | const rootWithSizesAndSorted = rawHierarchy 55 | .sum((node) => { 56 | if (isModuleTree(node)) return 0; 57 | const ownSize = getModuleSize(node, sizeProperty); 58 | const zoomMultiplier = getNodeSizeMultiplier(node); 59 | const filterMultiplier = getModuleFilterMultiplier(data.nodeMetas[data.nodeParts[node.uid].mainUid]); 60 | 61 | return ownSize * zoomMultiplier * filterMultiplier; 62 | }) 63 | .sort((a, b) => getModuleSize(a.data, sizeProperty) - getModuleSize(b.data, sizeProperty)); 64 | 65 | return layout(rootWithSizesAndSorted); 66 | }, [data, getModuleFilterMultiplier, getModuleSize, getNodeSizeMultiplier, layout, rawHierarchy, sizeProperty]); 67 | 68 | console.timeEnd("root hierarchy compute"); 69 | 70 | return ( 71 | <> 72 | 79 | 80 | 81 | ); 82 | }; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/treemap/node.tsx: -------------------------------------------------------------------------------- 1 | import type { ModuleTree, ModuleTreeLeaf, SizeKey } from "../../types/types"; 2 | 3 | import { h, FunctionalComponent } from "preact"; 4 | import { useContext, useLayoutEffect, useRef } from "preact/hooks"; 5 | import { format as formatBytes } from "bytes"; 6 | import { HierarchyRectangularNode } from "d3-hierarchy"; 7 | import { StaticContext } from "./index"; 8 | import { PADDING, TOP_PADDING } from "./const"; 9 | 10 | type NodeEventHandler = (event: HierarchyRectangularNode) => void; 11 | 12 | export interface NodeProps { 13 | node: HierarchyRectangularNode; 14 | onMouseOver: NodeEventHandler; 15 | selected: boolean; 16 | onClick: NodeEventHandler; 17 | } 18 | 19 | export const Node: FunctionalComponent = ({ node, onMouseOver, onClick, selected }) => { 20 | const { getModuleColor } = useContext(StaticContext); 21 | const { backgroundColor, fontColor } = getModuleColor(node); 22 | const { x0, x1, y1, y0, data, children = null } = node; 23 | 24 | const textRef = useRef(null); 25 | const textRectRef = useRef(); 26 | 27 | const width = x1 - x0; 28 | const height = y1 - y0; 29 | 30 | const textProps: Record = { 31 | "font-size": "0.7em", 32 | "dominant-baseline": "middle", 33 | "text-anchor": "middle", 34 | x: width / 2, 35 | }; 36 | if (children != null) { 37 | textProps.y = (TOP_PADDING + PADDING) / 2; 38 | } else { 39 | textProps.y = height / 2; 40 | } 41 | 42 | useLayoutEffect(() => { 43 | if (width == 0 || height == 0 || !textRef.current) { 44 | return; 45 | } 46 | 47 | if (textRectRef.current == null) { 48 | textRectRef.current = textRef.current.getBoundingClientRect(); 49 | } 50 | 51 | let scale = 1; 52 | if (children != null) { 53 | scale = Math.min( 54 | (width * 0.9) / textRectRef.current.width, 55 | Math.min(height, TOP_PADDING + PADDING) / textRectRef.current.height 56 | ); 57 | scale = Math.min(1, scale); 58 | textRef.current.setAttribute("y", String(Math.min(TOP_PADDING + PADDING, height) / 2 / scale)); 59 | textRef.current.setAttribute("x", String(width / 2 / scale)); 60 | } else { 61 | scale = Math.min((width * 0.9) / textRectRef.current.width, (height * 0.9) / textRectRef.current.height); 62 | scale = Math.min(1, scale); 63 | textRef.current.setAttribute("y", String(height / 2 / scale)); 64 | textRef.current.setAttribute("x", String(width / 2 / scale)); 65 | } 66 | 67 | textRef.current.setAttribute("transform", `scale(${scale.toFixed(2)})`); 68 | }, [children, height, width]); 69 | 70 | if (width == 0 || height == 0) { 71 | return null; 72 | } 73 | 74 | return ( 75 | { 79 | event.stopPropagation(); 80 | onClick(node); 81 | }} 82 | onMouseOver={(event: MouseEvent) => { 83 | event.stopPropagation(); 84 | onMouseOver(node); 85 | }} 86 | > 87 | 96 | { 100 | if (window.getSelection()?.toString() !== "") { 101 | event.stopPropagation(); 102 | } 103 | }} 104 | {...textProps} 105 | > 106 | {data.name} 107 | 108 | 109 | ); 110 | }; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/treemap/treemap.tsx: -------------------------------------------------------------------------------- 1 | import type { ModuleTree, ModuleTreeLeaf, SizeKey } from "../../types/types"; 2 | 3 | import { h, FunctionalComponent } from "preact"; 4 | import { useContext, useMemo } from "preact/hooks"; 5 | import { group } from "d3-array"; 6 | import { HierarchyNode, HierarchyRectangularNode } from "d3-hierarchy"; 7 | 8 | import { Node } from "./node"; 9 | import { StaticContext } from "./index"; 10 | 11 | export interface TreeMapProps { 12 | root: HierarchyRectangularNode; 13 | onNodeHover: (event: HierarchyRectangularNode) => void; 14 | selectedNode: HierarchyRectangularNode | undefined; 15 | onNodeClick: (node: HierarchyRectangularNode) => void; 16 | } 17 | 18 | export const TreeMap: FunctionalComponent = ({ root, onNodeHover, selectedNode, onNodeClick }) => { 19 | const { width, height, getModuleIds } = useContext(StaticContext); 20 | 21 | console.time("layering"); 22 | // this will make groups by height 23 | const nestedData = useMemo(() => { 24 | const nestedDataMap = group(root.descendants(), (d: HierarchyNode) => d.height); 25 | const nestedData = Array.from(nestedDataMap, ([key, values]) => ({ 26 | key, 27 | values, 28 | })); 29 | nestedData.sort((a, b) => b.key - a.key); 30 | return nestedData; 31 | }, [root]); 32 | console.timeEnd("layering"); 33 | 34 | return ( 35 | 36 | {nestedData.map(({ key, values }) => { 37 | return ( 38 | 39 | {values.map((node) => { 40 | return ( 41 | } 44 | onMouseOver={onNodeHover} 45 | selected={selectedNode === node} 46 | onClick={onNodeClick} 47 | /> 48 | ); 49 | })} 50 | 51 | ); 52 | })} 53 | 54 | ); 55 | }; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/uid.ts: -------------------------------------------------------------------------------- 1 | let count = 0; 2 | 3 | export class Id { 4 | private _id: string; 5 | private _href: string; 6 | 7 | constructor(id: string) { 8 | this._id = id; 9 | const url = new URL(window.location.href); 10 | url.hash = id; 11 | this._href = url.toString(); 12 | } 13 | 14 | get id(): Id["_id"] { 15 | return this._id; 16 | } 17 | 18 | get href(): Id["_href"] { 19 | return this._href; 20 | } 21 | 22 | toString(): string { 23 | return `url(${this.href})`; 24 | } 25 | } 26 | 27 | export function generateUniqueId(name: string): Id { 28 | count += 1; 29 | const id = ["O", name, count].filter(Boolean).join("-"); 30 | return new Id(id); 31 | } -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/src/use-filter.ts: -------------------------------------------------------------------------------- 1 | import { useState, useMemo } from "preact/hooks"; 2 | 3 | export type FilterSetter = (value: string) => void; 4 | 5 | const throttleFilter = (callback: FilterSetter, limit: number): typeof callback => { 6 | let waiting = false; 7 | return (val: string): void => { 8 | if (!waiting) { 9 | callback(val); 10 | waiting = true; 11 | setTimeout(() => { 12 | waiting = false; 13 | }, limit); 14 | } 15 | }; 16 | }; 17 | 18 | export type UseFilter = { 19 | includeFilter: string; 20 | excludeFilter: string; 21 | setIncludeFilter: FilterSetter; 22 | setExcludeFilter: FilterSetter; 23 | getModuleFilterMultiplier: (data: { id: string }) => number; 24 | }; 25 | 26 | export const useFilter = (): UseFilter => { 27 | const [includeFilter, setIncludeFilter] = useState(""); 28 | const [excludeFilter, setExcludeFilter] = useState(""); 29 | 30 | const setIncludeFilterTrottled = useMemo(() => throttleFilter(setIncludeFilter, 200), []); 31 | const setExcludeFilterTrottled = useMemo(() => throttleFilter(setExcludeFilter, 200), []); 32 | 33 | const isModuleIncluded = useMemo(() => { 34 | if (includeFilter === "") { 35 | return () => true; 36 | } 37 | try { 38 | const re = new RegExp(includeFilter); 39 | return ({ id }: { id: string }) => re.test(id); 40 | } catch (err) { 41 | return () => false; 42 | } 43 | }, [includeFilter]); 44 | 45 | const isModuleExcluded = useMemo(() => { 46 | if (excludeFilter === "") { 47 | return () => false; 48 | } 49 | try { 50 | const re = new RegExp(excludeFilter); 51 | return ({ id }: { id: string }) => re.test(id); 52 | } catch (err) { 53 | return () => false; 54 | } 55 | }, [excludeFilter]); 56 | 57 | const isDefaultInclude = includeFilter === ""; 58 | 59 | const getModuleFilterMultiplier = useMemo(() => { 60 | return (data: { id: string }) => { 61 | if (isDefaultInclude) { 62 | return isModuleExcluded(data) ? 0 : 1; 63 | } 64 | return isModuleExcluded(data) && !isModuleIncluded(data) ? 0 : 1; 65 | }; 66 | }, [isDefaultInclude, isModuleExcluded, isModuleIncluded]); 67 | 68 | return { 69 | getModuleFilterMultiplier, 70 | includeFilter, 71 | excludeFilter, 72 | setExcludeFilter: setExcludeFilterTrottled, 73 | setIncludeFilter: setIncludeFilterTrottled, 74 | }; 75 | }; -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "jsxImportSource": "preact", 5 | "moduleResolution": "node", 6 | "target": "ES2021", 7 | "module": "ES2020", 8 | "lib": [ 9 | "ES2021", 10 | "DOM", 11 | "DOM.Iterable", 12 | "WebWorker" 13 | ], 14 | "sourceMap": true, 15 | "outDir": "lib", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "esModuleInterop": true 19 | }, 20 | "include": [ 21 | "src" 22 | ], 23 | "exclude": [ 24 | "node_modules", 25 | "docs", 26 | "dist" 27 | ] 28 | } -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/types/metafile.d.ts: -------------------------------------------------------------------------------- 1 | export interface MetadataOutput { 2 | bytes: number; 3 | inputs: { 4 | [path: string]: { 5 | bytesInOutput: number; 6 | }; 7 | }; 8 | imports: { 9 | path: string; 10 | kind: string; 11 | }[]; 12 | exports: string[]; 13 | entryPoint?: string; 14 | } 15 | 16 | export interface Metadata { 17 | inputs: { 18 | [path: string]: { 19 | bytes: number; 20 | imports: { 21 | path: string; 22 | kind: string; 23 | }[]; 24 | }; 25 | }; 26 | outputs: { 27 | [path: string]: MetadataOutput; 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/types/rollup.d.ts: -------------------------------------------------------------------------------- 1 | export type ModuleInfo = { 2 | renderedLength: number; 3 | isEntry: boolean; 4 | isExternal: boolean; 5 | importedIds: string[]; 6 | dynamicallyImportedIds: string[]; 7 | }; 8 | 9 | export type GetModuleInfo = (moduleId: string) => ModuleInfo | null; 10 | -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/types/template-types.ts: -------------------------------------------------------------------------------- 1 | export type TemplateType = "sunburst" | "treemap" | "network"; 2 | 3 | export const templates: ReadonlyArray = ["sunburst", "treemap", "network"]; 4 | 5 | export default templates; 6 | -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/types/types.d.ts: -------------------------------------------------------------------------------- 1 | export type SizeKey = "renderedLength" | "gzipLength" | "brotliLength"; 2 | 3 | export type ModuleUID = string; 4 | export type BundleId = string; 5 | 6 | export interface ModuleTreeLeaf { 7 | name: string; 8 | uid: ModuleUID; 9 | } 10 | 11 | export interface ModuleTree { 12 | name: string; 13 | children: Array; 14 | } 15 | 16 | export type ModulePart = { 17 | mainUid: ModuleUID; 18 | } & ModuleLengths; 19 | 20 | export type ModuleImport = { 21 | uid: ModuleUID; 22 | dynamic?: boolean; 23 | }; 24 | 25 | export type ModuleMeta = { 26 | moduleParts: Record; 27 | importedBy: ModuleImport[]; 28 | imported: ModuleImport[]; 29 | isEntry?: boolean; 30 | isExternal?: boolean; 31 | id: string; 32 | }; 33 | 34 | export interface ModuleLengths { 35 | renderedLength: number; 36 | gzipLength: number; 37 | brotliLength: number; 38 | } 39 | 40 | export interface VisualizerData { 41 | version: number; 42 | tree: ModuleTree; 43 | nodeParts: Record; 44 | nodeMetas: Record; 45 | env: { 46 | [key: string]: unknown; 47 | }; 48 | options: { 49 | gzip: boolean; 50 | brotli: boolean; 51 | }; 52 | } -------------------------------------------------------------------------------- /src/ts/plugins/analyzer/utils/is-module-tree.ts: -------------------------------------------------------------------------------- 1 | import type { ModuleTree, ModuleTreeLeaf } from "../types/types"; 2 | 3 | export const isModuleTree = (mod: ModuleTree | ModuleTreeLeaf): mod is ModuleTree => "children" in mod; -------------------------------------------------------------------------------- /src/ts/plugins/virtual-fs.ts: -------------------------------------------------------------------------------- 1 | // Based on https://github.com/okikio/bundle/blob/main/src/ts/plugins/virtual-fs.ts 2 | import type { Plugin, OnResolveArgs } from 'esbuild'; 3 | import { getResolvedPath, getFile } from '../util/filesystem'; 4 | import { inferLoader } from '../util/loader'; 5 | 6 | export async function VIRTUAL_FS_RESOLVE(args: OnResolveArgs) { 7 | const resolvedPath = getResolvedPath(args.path, args?.pluginData?.importer); 8 | const content = getFile(args.path, "string", args?.pluginData?.importer); 9 | console.log({ 10 | // resolvedPath, 11 | importer: args?.pluginData?.importer, 12 | path: args.path, 13 | content 14 | }) 15 | 16 | if (content && content.length > 0) { 17 | return { 18 | path: args.path, 19 | pluginData: args.pluginData ?? {}, 20 | namespace: VIRTUAL_FILESYSTEM_NAMESPACE 21 | }; 22 | } 23 | } 24 | 25 | export const VIRTUAL_FILESYSTEM_NAMESPACE = "virtual-filesystem"; 26 | export const VIRTUAL_FS = (): Plugin => { 27 | return { 28 | name: VIRTUAL_FILESYSTEM_NAMESPACE, 29 | setup(build) { 30 | build.onResolve({ filter: /.*/ }, VIRTUAL_FS_RESOLVE); 31 | 32 | build.onLoad({ filter: /.*/, namespace: VIRTUAL_FILESYSTEM_NAMESPACE }, async (args) => { 33 | const resolvedPath = getResolvedPath(args.path, args?.pluginData?.importer); 34 | const content = getFile(args.path, "string", args?.pluginData?.importer); 35 | 36 | return { 37 | contents: content, 38 | pluginData: Object.assign({}, args.pluginData, { 39 | importer: resolvedPath, 40 | }), 41 | loader: inferLoader(resolvedPath) 42 | }; 43 | }); 44 | }, 45 | }; 46 | }; -------------------------------------------------------------------------------- /src/ts/services/Navbar.ts: -------------------------------------------------------------------------------- 1 | import { Service } from "@okikio/native"; 2 | 3 | export class Navbar extends Service { 4 | public navbar: HTMLElement; 5 | public elements: HTMLElement[]; 6 | public menu: HTMLElement; 7 | public collapseSection: HTMLElement; 8 | public navbarList: HTMLElement; 9 | public toggleStatus: boolean; 10 | public mobileEls: HTMLElement[]; 11 | 12 | public init() { 13 | // Elements 14 | this.navbar = document.querySelector(".navbar") as HTMLElement; 15 | this.collapseSection = this.navbar.querySelector(".navbar-collapse.mobile") as HTMLElement; 16 | this.navbarList = this.navbar.querySelector(".navbar-list") as HTMLElement; 17 | this.mobileEls = Array.from(this.collapseSection.querySelectorAll(".navbar-list a")); 18 | this.elements = Array.from(this.navbar.querySelectorAll(".navbar-list a")); 19 | this.menu = this.navbar.querySelector(".navbar-toggle") as HTMLElement; 20 | this.toggleStatus = false; 21 | 22 | this.fixTabindex(); 23 | this.toggleClick = this.toggleClick.bind(this); 24 | } 25 | 26 | public activateLink() { 27 | let { href } = window.location; 28 | 29 | for (let el of this.elements) { 30 | let itemHref = 31 | el.getAttribute("data-path") || 32 | (el as HTMLAnchorElement).href; 33 | if (!itemHref || itemHref.length < 1) return; 34 | 35 | let URLmatch = new RegExp(itemHref).test(href); 36 | let isActive = el.classList.contains("active"); 37 | if (!(URLmatch && isActive)) { 38 | el.classList.toggle("active", URLmatch); 39 | } 40 | } 41 | 42 | if (this.toggleStatus) { 43 | this.toggleClick(); 44 | } 45 | } 46 | 47 | public fixTabindex() { 48 | for (let el of this.mobileEls) { 49 | el.setAttribute("tabindex", `${this.toggleStatus ? 0 : -1}`); 50 | // el.style.setProperty("visibility", `${this.toggleStatus ? "visible" : "hidden"}`); 51 | } 52 | } 53 | 54 | public toggleClick() { 55 | this.collapseSection.style?.setProperty?.("--height", `${this.navbarList.clientHeight}px`); 56 | this.toggleStatus = !this.toggleStatus; 57 | this.collapseSection.classList.toggle("collapse", !this.toggleStatus); 58 | this.collapseSection.classList.toggle("show", this.toggleStatus); 59 | this.fixTabindex(); 60 | } 61 | 62 | public scroll() { 63 | this.navbar.classList.toggle("active-shadow", window.scrollY >= 5); 64 | } 65 | 66 | public initEvents() { 67 | this.menu.addEventListener("click", this.toggleClick); 68 | this.emitter.on("scroll", this.scroll, this); 69 | this.emitter.on("READY", this.activateLink, this); 70 | this.emitter.on("GO", this.activateLink, this); 71 | } 72 | 73 | public stopEvents() { 74 | this.navbar.removeEventListener("click", this.toggleClick); 75 | this.emitter.off("scroll", this.scroll, this); 76 | this.emitter.off("READY", this.activateLink, this); 77 | this.emitter.off("GO", this.activateLink, this); 78 | } 79 | 80 | public uninstall() { 81 | while (this.elements.length) this.elements.pop(); 82 | this.elements = null; 83 | this.menu = null; 84 | this.navbar = null; 85 | } 86 | } -------------------------------------------------------------------------------- /src/ts/theme.ts: -------------------------------------------------------------------------------- 1 | export const ThemeChange = new Event('theme-change', { 2 | bubbles: true, 3 | cancelable: true, 4 | composed: false 5 | }); 6 | 7 | // Based on [joshwcomeau.com/gatsby/dark-mode/] 8 | export let getTheme = (): string | null => { 9 | let theme = window.localStorage.getItem("theme"); 10 | // If the user has explicitly chosen light or dark, 11 | // let's use it. Otherwise, this value will be null. 12 | if (typeof theme === "string") return theme; 13 | 14 | // If they are using a browser/OS that doesn't support 15 | // color themes, let's default to 'light'. 16 | return null; 17 | }; 18 | 19 | export let setTheme = (theme: string): void => { 20 | // If the user has explicitly chosen light or dark, store the default theme 21 | if (typeof theme === "string") window.localStorage.setItem("theme", theme); 22 | }; 23 | 24 | export let mediaTheme = (): string | null => { 25 | // If they haven't been explicitly set, let's check the media query 26 | let mql = window.matchMedia("(prefers-color-scheme: dark)"); 27 | let hasMediaQueryPreference = typeof mql.matches === "boolean"; 28 | if (hasMediaQueryPreference) return mql.matches ? "dark" : "light"; 29 | return null; 30 | }; 31 | 32 | let html = document.querySelector("html"); 33 | // Get theme from html tag, if it has a theme or get it from localStorage 34 | export let themeGet = () => { 35 | let themeAttr = html.getAttribute("data-theme"); 36 | if (typeof themeAttr === "string" && themeAttr.length) { 37 | return themeAttr; 38 | } 39 | 40 | return getTheme(); 41 | }; 42 | 43 | // Set theme in localStorage, as well as in the html tag 44 | export let themeSet = (theme: string) => { 45 | let themeColor = theme == "system" ? mediaTheme() : theme; 46 | html.setAttribute("data-theme", theme); 47 | html.classList.toggle("dark", themeColor == "dark"); 48 | setTheme(theme); 49 | document.dispatchEvent(ThemeChange); 50 | }; 51 | 52 | export let runTheme = () => { 53 | try { 54 | let theme = getTheme(); 55 | if (theme === null) { 56 | themeSet("system"); 57 | } else themeSet(theme); 58 | 59 | window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => { 60 | themeSet("system"); 61 | }); 62 | 63 | window.matchMedia("(prefers-color-scheme: light)").addEventListener("change", (e) => { 64 | themeSet("system"); 65 | }); 66 | } catch (e) { 67 | console.warn("Theming isn't available on this browser.", e); 68 | } 69 | }; 70 | 71 | runTheme(); -------------------------------------------------------------------------------- /src/ts/util/WebWorker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SharedWorkerPolyfill as WebWorker, 3 | SharedWorkerSupported, 4 | } from "@okikio/sharedworker"; 5 | 6 | import EMPTY_WORKER_URL from "worker:../workers/empty.ts"; 7 | 8 | // https://stackoverflow.com/questions/62954570/javascript-feature-detect-module-support-for-web-workers 9 | export const ModuleWorkerTest = () => { 10 | let support = false; 11 | const test = { 12 | get type() { 13 | support = true; 14 | return "module"; 15 | }, 16 | }; 17 | 18 | try { 19 | // We use "blob://" as url to avoid an useless network request. 20 | // This will either throw in Chrome 21 | // either fire an error event in Firefox 22 | // which is perfect since 23 | // we don't need the worker to actually start, 24 | // checking for the type of the script is done before trying to load it. 25 | 26 | // @ts-ignore 27 | // `data:application/javascript;base64,${Buffer.from("export {};").toString('base64')}` 28 | // const worker = new Worker(`blob://`, test); 29 | 30 | // Use an empty worker file, to avoid 31 | const worker = new Worker( 32 | EMPTY_WORKER_URL.replace(/\.js$/, ".mjs"), 33 | test as WorkerOptions 34 | ); 35 | 36 | worker?.terminate(); 37 | return support; 38 | } catch (e) { 39 | console.log(e); 40 | } 41 | 42 | return false; 43 | }; 44 | 45 | export let ModuleWorkerSupported = ModuleWorkerTest(); 46 | export const WorkerType: WorkerOptions["type"] = ModuleWorkerSupported 47 | ? "module" 48 | : "classic"; 49 | 50 | export const WorkerConfig = (url: URL | string, name?: WorkerOptions["name"]) => { 51 | return [url.toString().replace(/\.js$/, WorkerType == "module" ? ".mjs" : ".js"), { 52 | name, 53 | type: WorkerType 54 | }] as [string | URL, WorkerOptions]; 55 | }; 56 | 57 | console.log( 58 | `This browser supports ${WorkerType} workers!` 59 | ); 60 | console.log( 61 | `This browser supports ${ 62 | SharedWorkerSupported ? "Shared Web Workers" : "Normal Web Workers" 63 | }!` 64 | ); 65 | 66 | export { WebWorker }; 67 | export default WebWorker; 68 | -------------------------------------------------------------------------------- /src/ts/util/ansi.ts: -------------------------------------------------------------------------------- 1 | // Based off of @hyrious esbuild-repl https://github.com/hyrious/esbuild-repl/blob/main/src/helpers/ansi.ts 2 | // https://github.com/evanw/esbuild/blob/master/internal/logger/logger.go 3 | const ESCAPE_TO_COLOR = { 4 | "37": "dim", 5 | "31": "red", 6 | "32": "green", 7 | "34": "blue", 8 | "36": "cyan", 9 | "35": "magenta", 10 | "33": "yellow", 11 | "41;31": "red-bg-red", 12 | "41;97": "red-bg-white", 13 | "42;32": "green-bg-green", 14 | "42;97": "green-bg-white", 15 | "44;34": "blue-bg-blue", 16 | "44;97": "blue-bg-white", 17 | "46;36": "cyan-bg-cyan", 18 | "46;30": "cyan-bg-black", 19 | "45;35": "magenta-bg-magenta", 20 | "45;30": "magenta-bg-black", 21 | "43;33": "yellow-bg-yellow", 22 | "43;30": "yellow-bg-black", 23 | } as const; 24 | 25 | type Escape = "0" | "1" | "4" | keyof typeof ESCAPE_TO_COLOR; 26 | 27 | type Color = typeof ESCAPE_TO_COLOR[keyof typeof ESCAPE_TO_COLOR]; 28 | 29 | // https://github.com/sindresorhus/escape-goat 30 | function htmlEscape(string: string) { 31 | return string 32 | .replace(/\/g, "\n") 33 | .replace(/\&/g, "&") 34 | .replace(/\"/g, """) 35 | .replace(/\'/g, "'") 36 | .replace(/\/g, ">"); 38 | } 39 | 40 | class AnsiBuffer { 41 | result = ""; 42 | _stack: string[] = []; 43 | _bold = false; 44 | _underline = false; 45 | _link = false; 46 | text(text: string) { 47 | this.result += htmlEscape(text); 48 | } 49 | reset() { 50 | let close: string | undefined; 51 | while ((close = this._stack.pop())) { 52 | this.result += close; 53 | } 54 | } 55 | bold() { 56 | if (!this._bold) { 57 | this._bold = true; 58 | this.result += ""; 59 | this._stack.push(""); 60 | } 61 | } 62 | underline() { 63 | if (!this._underline) { 64 | this._underline = true; 65 | this.result += ""; 66 | this._stack.push(""); 67 | } 68 | } 69 | last() { 70 | return this._stack[this._stack.length - 1]; 71 | } 72 | color(color: Color) { 73 | let close: string | undefined; 74 | while ((close = this.last()) === "") { 75 | this._stack.pop(); 76 | this.result += close; 77 | } 78 | this.result += ``; 79 | this._stack.push(""); 80 | } 81 | done() { 82 | this.reset(); 83 | return this.result; 84 | } 85 | } 86 | 87 | export function render(ansi: string) { 88 | ansi = ansi.trimEnd(); 89 | let i = 0; 90 | const buffer = new AnsiBuffer(); 91 | for (let m of ansi.matchAll(/\x1B\[([\d;]+)m/g)) { 92 | const escape = m[1] as Escape; 93 | buffer.text(ansi.slice(i, m.index)); 94 | i = m.index! + m[0].length; 95 | /* */ if (escape === "0") { 96 | buffer.reset(); 97 | } else if (escape === "1") { 98 | buffer.bold(); 99 | } else if (escape === "4") { 100 | buffer.underline(); 101 | } else if (ESCAPE_TO_COLOR[escape]) { 102 | buffer.color(ESCAPE_TO_COLOR[escape]); 103 | } 104 | } 105 | if (i < ansi.length) { 106 | buffer.text(ansi.slice(i)); 107 | } 108 | return buffer.done(); 109 | } -------------------------------------------------------------------------------- /src/ts/util/debounce.ts: -------------------------------------------------------------------------------- 1 | // Based on https://davidwalsh.name/javascript-debounce-function 2 | // Returns a function, that, as long as it continues to be invoked, will not 3 | // be triggered. The function will be called after it stops being called for 4 | // N milliseconds. If `immediate` is passed, trigger the function on the 5 | // leading edge, instead of the trailing. 6 | export const debounce = (func: (...args: any[]) => any, wait: number = 300, immediate?: boolean) => { 7 | let timeout: number | null; 8 | return function(...args: any[]) { 9 | let context = this; 10 | let later = () => { 11 | timeout = null; 12 | if (!immediate) func.apply(context, args); 13 | }; 14 | 15 | let callNow = immediate && !timeout; 16 | clearTimeout(timeout); 17 | 18 | // @ts-ignore 19 | timeout = setTimeout(later, wait); 20 | if (callNow) func.apply(context, args); 21 | }; 22 | }; -------------------------------------------------------------------------------- /src/ts/util/deep-equal.ts: -------------------------------------------------------------------------------- 1 | export const isObject = (obj: any) => typeof obj === "object" && obj != null; 2 | export const isPrimitive = (val) => (typeof val === 'object' ? val === null : typeof val !== 'function'); 3 | export const isValidKey = key => { 4 | return key !== '__proto__' && key !== 'constructor' && key !== 'prototype'; 5 | }; 6 | 7 | // Based on https://gist.github.com/egardner/efd34f270cc33db67c0246e837689cb9 8 | // Deep Equality comparison example 9 | // 10 | // This is an example of how to implement an object-comparison function in 11 | // JavaScript (ES5+). A few points of interest here: 12 | // 13 | // * You can get an array of all an object's properties in ES5+ by calling 14 | // the class method Object.keys(obj). 15 | // * The function recursively calls itself in the for / in loop when it 16 | // compares the contents of each property 17 | // * You can hide a "private" function inside a function of this kind by 18 | // placing one function declaration inside of another. The inner function 19 | // is not hoisted out into the global scope, so it is only visible inside 20 | // of the parent function. 21 | // * The reason this nested helper function is necessary is that 22 | // `typeof null` is still "object" in JS, a major "gotcha" to watch out for. 23 | // 24 | export const deepEqual = (obj1: any, obj2: any) => { 25 | if (obj1 === obj2) { 26 | return true; 27 | } else if (isObject(obj1) && isObject(obj2)) { 28 | if (Object.keys(obj1).length !== Object.keys(obj2).length) { return false; } 29 | for (var prop in obj1) { 30 | if (!deepEqual(obj1[prop], obj2[prop])) return false; 31 | } 32 | 33 | return true; 34 | } 35 | }; 36 | 37 | /** Compares 2 objects and only keep the keys that are different in both objects */ 38 | export const deepDiff = (obj1: any, obj2: any) => { 39 | let keys = Object.keys(obj2); 40 | let result: Record = {}; 41 | let i = 0; 42 | for (; i < keys.length; i++) { 43 | let key = keys[i]; 44 | let value = obj2[key]; 45 | 46 | if (key in obj1) { 47 | let bothAreArrays = Array.isArray(obj1[key]) && Array.isArray(value); 48 | if (obj1[key] == value) { 49 | continue; 50 | } else if (bothAreArrays) { 51 | if (!deepEqual(obj1[key], value)) 52 | result[key] = value; 53 | else continue; 54 | } else if (isObject(obj1[key]) && isObject(value)) { 55 | // Remove empty objects 56 | let diff = deepDiff(obj1[key], value); 57 | if (Object.keys(diff).length) 58 | result[key] = diff; 59 | } else { 60 | result[key] = value; 61 | } 62 | } else { 63 | result[key] = value; 64 | } 65 | } 66 | 67 | return result; 68 | }; 69 | 70 | /*! 71 | * Based on assign-deep 72 | * 73 | * Copyright (c) 2017-present, Jon Schlinkert. 74 | * Released under the MIT License. 75 | */ 76 | export const deepAssign = (target, ...args) => { 77 | let i = 0; 78 | if (isPrimitive(target)) target = args[i++]; 79 | if (!target) target = {}; 80 | for (; i < args.length; i++) { 81 | if (isObject(args[i])) { 82 | for (const key of Object.keys(args[i])) { 83 | if (isValidKey(key)) { 84 | if (isObject(target[key]) && isObject(args[i][key])) { 85 | target[key] = deepAssign(Array.isArray(target[key]) ? [] : {}, target[key], args[i][key]); 86 | } else { 87 | target[key] = args[i][key]; 88 | } 89 | } 90 | } 91 | } 92 | } 93 | 94 | return target; 95 | }; 96 | -------------------------------------------------------------------------------- /src/ts/util/encode-decode.ts: -------------------------------------------------------------------------------- 1 | // export const { encode } = new TextEncoder(); 2 | // export const { decode } = new TextDecoder(); 3 | 4 | export const encode = (str: string) => new TextEncoder().encode(str); 5 | export const decode = (buf: BufferSource) => new TextDecoder().decode(buf); -------------------------------------------------------------------------------- /src/ts/util/fetch-and-cache.ts: -------------------------------------------------------------------------------- 1 | export const CACHE = new Map(); 2 | export const CACHE_NAME = "EXTERNAL_FETCHES"; 3 | export const SUPPORTS_CACHE_API = "caches" in globalThis; 4 | export const SUPPORTS_REQUEST_API = "Request" in globalThis; 5 | 6 | export function requestKey(request: RequestInfo) { 7 | return SUPPORTS_REQUEST_API && request instanceof Request ? request.url.toString() : request.toString() 8 | } 9 | 10 | export async function newRequest(request: RequestInfo, cache?: Cache, fetchOpts?: RequestInit, clone = true) { 11 | const networkResponse: Response = await fetch(request, fetchOpts); 12 | 13 | if (!fetchOpts?.method || (fetchOpts?.method && fetchOpts.method.toUpperCase() !== "GET")) 14 | return networkResponse; 15 | 16 | if (clone) { 17 | const clonedResponse = networkResponse.clone(); 18 | if (SUPPORTS_CACHE_API && cache) { 19 | cache.put(request, networkResponse); 20 | } else { 21 | const reqKey = requestKey(request); 22 | CACHE.set(reqKey, networkResponse); 23 | } 24 | 25 | return clonedResponse; 26 | } 27 | 28 | if (SUPPORTS_CACHE_API && cache) { 29 | cache.put(request, networkResponse); 30 | } else { 31 | const reqKey = requestKey(request); 32 | CACHE.set(reqKey, networkResponse); 33 | } 34 | } 35 | 36 | export let OPEN_CACHE: Cache; 37 | export async function openCache() { 38 | if (OPEN_CACHE) return OPEN_CACHE; 39 | return (OPEN_CACHE = await caches.open(CACHE_NAME)); 40 | } 41 | 42 | export async function getRequest(url: RequestInfo | URL, permanent = false, fetchOpts?: RequestInit) { 43 | const request = SUPPORTS_REQUEST_API ? new Request(url.toString(), fetchOpts) : url.toString(); 44 | let response: Response; 45 | 46 | let cache: Cache | undefined; 47 | let cacheResponse: Response | undefined; 48 | 49 | // In specific situations the browser will sometimes disable access to cache storage, 50 | // so, I create my own in memory cache 51 | if (SUPPORTS_CACHE_API) { 52 | cache = await openCache(); 53 | cacheResponse = await cache.match(request); 54 | } else { 55 | const reqKey = requestKey(request); 56 | cacheResponse = CACHE.get(reqKey); 57 | } 58 | 59 | if (cacheResponse) 60 | response = cacheResponse; 61 | 62 | // If permanent is true, use the cache first and only go to the network if there is nothing in the cache, 63 | // otherwise, still use cache first, but in the background queue up a network request 64 | if (!cacheResponse) 65 | response = await newRequest(request, cache, fetchOpts); 66 | else if (!permanent) { 67 | newRequest(request, cache, fetchOpts, false); 68 | } 69 | 70 | return response!; 71 | } 72 | -------------------------------------------------------------------------------- /src/ts/util/filesystem.ts: -------------------------------------------------------------------------------- 1 | import { dirname, resolve } from "../deno/path/mod"; 2 | import { decode, encode } from "./encode-decode"; 3 | 4 | /** Filesystem Storage */ 5 | export const FileSystem = new Map(); 6 | 7 | (globalThis as any).Deno ??= { 8 | cwd() { return "" } 9 | } 10 | 11 | /** 12 | * Resolves path to a format the works with the virtual file system storage 13 | * 14 | * @param path the relative or absolute path to resolve to 15 | * @param importer an absolute path to use to determine relative file paths 16 | * @returns resolved final path 17 | */ 18 | export const getResolvedPath = (path: string, importer?: string) => { 19 | let resolvedPath = path; 20 | if (importer && !path.startsWith('/')) 21 | resolvedPath = resolve(dirname(importer), path); 22 | 23 | if (FileSystem.has(resolvedPath)) return resolvedPath; 24 | throw `File "${resolvedPath}" does not exist`; 25 | } 26 | 27 | /** 28 | * Retrevies file from virtual file system storage 29 | * 30 | * @param path path of file in virtual file system storage 31 | * @param type format to retrieve file in, buffer and string are the 2 option available 32 | * @param importer an absolute path to use to determine a relative file path 33 | * @returns file from file system storage in either string format or as a Uint8Array buffer 34 | */ 35 | export function getFile(path: string, type: 'string', importer?: string): string; 36 | export function getFile(path: string, type: 'buffer', importer?: string): Uint8Array; 37 | export function getFile (path: string, type: 'string' | 'buffer' = "buffer", importer?: string) { 38 | let resolvedPath = getResolvedPath(path, importer); 39 | 40 | if (FileSystem.has(resolvedPath)) { 41 | let file = FileSystem.get(resolvedPath); 42 | return type == "string" ? decode(file) : file; 43 | } 44 | } 45 | 46 | /** 47 | * Writes file to filesystem in either string or uint8array buffer format 48 | * @param path path of file in virtual file system storage 49 | * @param content contents of file to store, you can store buffers and/or strings 50 | * @param importer an absolute path to use to determine a relative file path 51 | */ 52 | export const setFile = (path: string, content: Uint8Array | string, importer?: string) => { 53 | let resolvedPath = path; 54 | if (importer && path.startsWith('.')) 55 | resolvedPath = resolve(dirname(importer), path); 56 | 57 | try { 58 | FileSystem.set(resolvedPath, content instanceof Uint8Array ? content : encode(content)); 59 | } catch (e) { 60 | throw `Error occurred while writing to "${resolvedPath}"`; 61 | } 62 | } -------------------------------------------------------------------------------- /src/ts/util/loader.ts: -------------------------------------------------------------------------------- 1 | import type { Loader } from 'esbuild'; 2 | import { extname } from './path'; 3 | 4 | import { extension } from "../deno/media-types/mod"; 5 | 6 | /** Based on https://github.com/egoist/play-esbuild/blob/main/src/lib/esbuild.ts */ 7 | export const RESOLVE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".css", ".json"]; 8 | 9 | export const _knownExtensions = [ 10 | // Remove period `.tsx` -> `tsx` 11 | ...RESOLVE_EXTENSIONS.map(x => x.slice(1)), 12 | "mjs", "cjs", "mts", "cts", "scss", 13 | "png", "jpeg", "ttf", "svg", 14 | "html", "txt", "wasm" 15 | ] 16 | 17 | /** 18 | * Based on the file extention determine the esbuild loader to use 19 | */ 20 | export const inferLoader = (urlStr: string, contentType?: string | null): Loader => { 21 | const ext = extname(urlStr); 22 | if (RESOLVE_EXTENSIONS.includes(ext)) 23 | // Resolve all .js and .jsx files to .ts and .tsx files 24 | return (/\.js(x)?$/.test(ext) ? ext.replace(/^\.js/, ".ts") : ext).slice(1) as Loader; 25 | 26 | if (ext === ".mjs" || ext === ".cjs") return "ts"; // "js" 27 | if (ext === ".mts" || ext === ".cts") return "ts"; 28 | 29 | if (ext == ".scss") return "css"; 30 | 31 | if (ext == ".png" || ext == ".jpeg" || ext == ".ttf") return "dataurl"; 32 | if (ext == ".svg" || ext == ".html" || ext == ".txt") return "text"; 33 | if (ext == ".wasm") return "file"; 34 | 35 | if (contentType) { 36 | const _ext = extension(contentType); 37 | if (_ext && _knownExtensions.includes(_ext)) 38 | return inferLoader(urlStr + `.${_ext}`); 39 | } 40 | 41 | return ext.length ? "text" : "ts"; 42 | } -------------------------------------------------------------------------------- /src/ts/util/path.ts: -------------------------------------------------------------------------------- 1 | import { isAbsolute, join } from "../deno/path/mod"; 2 | import { encodeWhitespace } from "../deno/path/_util"; 3 | 4 | export { extname, join, isAbsolute } from "@std/path/posix"; 5 | // export * from "@std/path/posix/join"; 6 | // export * from "../deno/path/mod.ts"; 7 | 8 | /** 9 | * Based on https://github.com/egoist/play-esbuild/blob/main/src/lib/path.ts#L123 10 | * 11 | * Support joining paths to a URL 12 | */ 13 | export const urlJoin = (urlStr: string, ...args: string[]) => { 14 | const url = new URL(urlStr); 15 | url.pathname = encodeWhitespace( 16 | join(url.pathname, ...args).replace(/%/g, "%25").replace(/\\/g, "%5C"), 17 | ); 18 | return url.toString(); 19 | } 20 | 21 | /** 22 | * An import counts as a bare import if it's neither a relative import of an absolute import 23 | */ 24 | export const isBareImport = (importStr: string) => { 25 | return /^(?!\.).*/.test(importStr) && !isAbsolute(importStr); 26 | } -------------------------------------------------------------------------------- /src/ts/util/rollup.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import type { BundleConfigOptions } from "../configs/bundle-options"; 3 | import type { Plugin } from "rollup"; 4 | 5 | import { rollup } from "rollup"; 6 | import * as path from "../deno/path/mod"; 7 | 8 | export const isFileSchema = (id: string) => 9 | id.startsWith("file://") || id.startsWith("/"); 10 | 11 | export const isRelativePath = (id: string) => stripSchema(id).startsWith("."); 12 | export const stripSchema = (id: string) => id.replace(/^file\:(\/\/)?/, ""); 13 | 14 | export const SEARCH_EXTENSIONS = [ 15 | "/index.tsx", 16 | "/index.ts", 17 | "/index.js", 18 | ".tsx", 19 | ".ts", 20 | ".json", 21 | ".js", 22 | ]; 23 | 24 | export function searchFile( 25 | vfs: Map, 26 | filepath: string, 27 | extensions: string[] 28 | ) { 29 | for (const ext of ["", ...extensions]) { 30 | // console.log("searching...", filepath + ext); 31 | if (vfs.has(filepath + ext)) { 32 | return filepath + ext; 33 | } 34 | } 35 | } 36 | 37 | export const virtualfs = (vfs: Map) => { 38 | return { 39 | name: "virtual-fs", 40 | resolveId(id: string, importer: string | undefined) { 41 | // const exts = extensions ?; 42 | const normalized = stripSchema(id); 43 | // entry point 44 | if (isFileSchema(id) && importer == null) { 45 | return searchFile(vfs, normalized, SEARCH_EXTENSIONS); 46 | } 47 | 48 | // relative filepath 49 | if (importer && isFileSchema(importer) && isRelativePath(id)) { 50 | const rawImporter = importer.replace(/^file\:/, ""); 51 | const fullpath = rawImporter 52 | ? path.resolve(path.dirname(rawImporter), normalized) 53 | : id; 54 | const reslovedWithExt = searchFile(vfs, fullpath, SEARCH_EXTENSIONS); 55 | if (reslovedWithExt) return reslovedWithExt; 56 | this.warn(`[rollup-plugin-virtual-fs] can not resolve id: ${fullpath}`); 57 | } 58 | }, 59 | load(id: string) { 60 | const real = stripSchema(id); 61 | const ret = vfs.get(real); 62 | if (ret) return ret; 63 | throw new Error(`[virtualFs] ${id} is not found on files`); 64 | }, 65 | } as Plugin; 66 | }; 67 | 68 | // Use rollup to treeshake 69 | export const treeshake = async (code: string, options: BundleConfigOptions["esbuild"] = {}, rollupOpts: BundleConfigOptions["rollup"] = {}) => { 70 | const inputFile = "/input.js"; 71 | const vfs = new Map(Object.entries({ 72 | [inputFile]: code 73 | })); 74 | 75 | const build = await rollup({ 76 | input: inputFile, 77 | treeshake: true, 78 | plugins: [ virtualfs(vfs) ] 79 | }); 80 | 81 | const { output } = await build.generate({ 82 | format: options?.format ?? "esm", 83 | compact: options?.minify, 84 | name: options?.globalName, 85 | ...Object.assign({}, rollupOpts as object) 86 | }); 87 | 88 | let content = output[0].code; 89 | return content?.trim?.(); // Remove unesscary space 90 | } 91 | 92 | export default treeshake; 93 | -------------------------------------------------------------------------------- /src/ts/util/worker-init.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { SimpleWorkerServer } from 'monaco-editor/esm/vs/base/common/worker/simpleWorker'; 3 | import { EditorSimpleWorker } from 'monaco-editor/esm/vs/editor/common/services/editorSimpleWorker'; 4 | export function initialize(foreignModule, port, initialized) { 5 | if (initialized) { 6 | return; 7 | } 8 | initialized = true; 9 | const simpleWorker = new SimpleWorkerServer((msg) => { 10 | port.postMessage(msg); 11 | }, (host) => new EditorSimpleWorker(host, foreignModule)); 12 | port.onmessage = (e) => { 13 | simpleWorker.onmessage(e.data); 14 | }; 15 | } -------------------------------------------------------------------------------- /src/ts/workers/editor.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // export * from "../../../node_modules/monaco-editor/esm/vs/editor/editor.worker.js"; 3 | 4 | /*--------------------------------------------------------------------------------------------- 5 | * Copyright (c) Microsoft Corporation. All rights reserved. 6 | * Licensed under the MIT License. See License.txt in the project root for license information. 7 | *--------------------------------------------------------------------------------------------*/ 8 | // import { initialize } from "../util/worker-init"; 9 | 10 | // The Editor worker is really not nesscary, so, I set it up to be self terminating 11 | const connect = (port) => { 12 | console.log('Empty Editor Worker'); 13 | self.close(); 14 | 15 | // let initialized = false; 16 | // port.onmessage = (e) => { 17 | // // Ignore first message in this case and initialize if not yet initialized 18 | // if (!initialized) { 19 | // initialize(null, port, initialized); 20 | // } 21 | // }; 22 | } 23 | 24 | // @ts-ignore 25 | self.onconnect = (e) => { 26 | let [port] = e.ports; 27 | connect(port) 28 | } 29 | 30 | if (!("SharedWorkerGlobalScope" in self)) { 31 | connect(self); 32 | } 33 | 34 | export { }; -------------------------------------------------------------------------------- /src/ts/workers/empty.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export {}; 3 | 4 | 5 | 6 | // const TypeAquisition = setupTypeAcquisition({ 7 | // projectName: "My ATA Project", 8 | // typescript: ts, 9 | // logger: console, 10 | // async fetcher(input, init) { 11 | // return await getRequest(input, false, init); 12 | // }, 13 | // delegate: { 14 | // started: () => { 15 | // console.log("Types Aquisition Start") 16 | // }, 17 | // receivedFile: (code: string, path: string) => { 18 | // // Add code to your runtime at the path... 19 | // languages.typescript.typescriptDefaults.addExtraLib(code, "file://" + path); 20 | 21 | // const uri = Uri.file(path); 22 | // if (Editor.getModel(uri) === null) { 23 | // Editor.createModel(code, "typescript", uri); 24 | // } 25 | // }, 26 | // progress: (downloaded: number, total: number) => { 27 | // console.log(`Got ${downloaded} out of ${total}`) 28 | // }, 29 | // finished: vfs => { 30 | // console.log("Types Aquisition Done"); 31 | // }, 32 | // }, 33 | // }); 34 | 35 | // const getTypescriptTypes = async (model: typeof inputModel) => { 36 | // try { 37 | // const worker = await languages.typescript.getTypeScriptWorker(); 38 | // await worker(model.uri); 39 | 40 | // // @ts-ignore 41 | // TypeAquisition(model.getValue()); 42 | // } catch (e) { 43 | // console.warn(e); 44 | // } 45 | // }; 46 | 47 | // setTimeout(() => { 48 | // getTypescriptTypes(inputModel); 49 | // }, 1000); 50 | -------------------------------------------------------------------------------- /src/ts/workers/json.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // export * from "../../../node_modules/monaco-editor/esm/vs/language/json/json.worker.js"; 3 | import '../../../node_modules/monaco-editor/esm/vs/language/json/json.worker.js'; 4 | 5 | /*--------------------------------------------------------------------------------------------- 6 | * Copyright (c) Microsoft Corporation. All rights reserved. 7 | * Licensed under the MIT License. See License.txt in the project root for license information. 8 | *--------------------------------------------------------------------------------------------*/ 9 | // 'use strict'; 10 | // import { initialize } from "../util/worker-init"; 11 | // import { create } from '../../../node_modules/monaco-editor/esm/vs/language/json/json.worker.js'; 12 | 13 | // export const connect = (port) => { 14 | // let initialized = false; 15 | // port.onmessage = (e) => { 16 | // initialize(function (ctx, createData) { 17 | // return create(ctx, createData); 18 | // }, port, initialized); 19 | // }; 20 | // } 21 | 22 | // // @ts-ignore 23 | // self.onconnect = (e) => { 24 | // let [port] = e.ports; 25 | // connect(port); 26 | // } 27 | 28 | // if (!("SharedWorkerGlobalScope" in self)) { 29 | // connect(self); 30 | // } 31 | 32 | // export { }; -------------------------------------------------------------------------------- /src/ts/workers/sandbox.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { initialize, transform } from "esbuild-wasm"; 3 | import { DefaultConfig } from "../configs/bundle-options"; 4 | export let _initialized = false; 5 | 6 | const initPromise = (async () => { 7 | try { 8 | if (!_initialized) { 9 | await initialize({ 10 | worker: false, 11 | wasmURL: `./esbuild.wasm` 12 | }); 13 | 14 | _initialized = true; 15 | } 16 | } catch (error) { 17 | console.warn("Sandbox", error) 18 | } 19 | })(); 20 | 21 | const configs = new Map(); 22 | 23 | /** 24 | * Contains the entire esbuild worker script 25 | * 26 | * @param port The Shared Worker port to post messages on 27 | */ 28 | export const start = async (port: MessagePort) => { 29 | let $port: MessagePort; 30 | 31 | const onmessage = (_port: MessagePort) => { 32 | return async function ({ data: arr }: MessageEvent<[string, boolean, boolean, boolean, string | boolean | null, boolean, string | undefined]>) { 33 | try { 34 | await initPromise; 35 | 36 | const [data, analysis, polyfill, tsx, sourcemap, minify, format] = arr; 37 | const config = configs.has(data) ? configs.get(data) : ( 38 | await transform(data, { 39 | loader: 'ts', 40 | format: 'iife', 41 | globalName: 'std_global', 42 | treeShaking: true 43 | }) 44 | ).code; 45 | 46 | console.log({ 47 | analysis, 48 | data, 49 | sourcemap, 50 | config 51 | }) 52 | 53 | const result = await Function('"use strict";return (async function () { "use strict";' + config + 'return (await std_global?.default); })()')(); 54 | // const result = (0, eval)(config + ' std_global'); 55 | result.analysis = analysis || result.analysis || (analysis ?? result.analysis ?? DefaultConfig.analysis); 56 | result.polyfill = polyfill || result.polyfill || (polyfill ?? result.polyfill ?? DefaultConfig.polyfill); 57 | result.tsx = tsx || result.tsx || (tsx ?? result.tsx ?? DefaultConfig.tsx); 58 | 59 | if (!result.esbuild) result.esbuild ??= {}; 60 | result.esbuild.sourcemap = sourcemap || result.esbuild.sourcemap || (sourcemap ?? result.esbuild.sourcemap ?? DefaultConfig.esbuild.sourcemap); 61 | result.esbuild.minify = minify || result.esbuild.minify || (minify ?? result.esbuild.minify ?? DefaultConfig.esbuild.minify); 62 | result.esbuild.format = format || result.esbuild.format || (format ?? result.esbuild.format ?? DefaultConfig.esbuild.format); 63 | 64 | configs.set(data, config); 65 | 66 | console.log({ result }) 67 | 68 | _port.postMessage(result); 69 | } catch (e) { 70 | console.warn(e); 71 | _port.postMessage({}); 72 | } 73 | } 74 | } 75 | 76 | const msg = onmessage(port as unknown as MessagePort); 77 | port.onmessage = ({ data }) => { 78 | if (data?.port) { 79 | const { port: $$port } = data; 80 | $port = $$port; 81 | $port.start(); 82 | $port.onmessage = onmessage($port); 83 | } else { 84 | msg({ data } as MessageEvent<[string, boolean, boolean, boolean, string | boolean | null, boolean, string | undefined]>); 85 | } 86 | }; 87 | } 88 | 89 | // @ts-ignore 90 | (self as SharedWorkerGlobalScope).onconnect = (e) => { 91 | let [port] = e.ports; 92 | start(port); 93 | } 94 | 95 | // If the script is running in a normal webworker then don't worry about the Shared Worker message ports 96 | if (!("SharedWorkerGlobalScope" in self)) 97 | start(self as typeof self & MessagePort); 98 | 99 | export { }; -------------------------------------------------------------------------------- /src/ts/workers/typescript.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // export * from "../../../node_modules/monaco-editor/esm/vs/language/typescript/ts.worker.js"; 3 | 4 | /*--------------------------------------------------------------------------------------------- 5 | * Copyright (c) Microsoft Corporation. All rights reserved. 6 | * Licensed under the MIT License. See License.txt in the project root for license information. 7 | *--------------------------------------------------------------------------------------------*/ 8 | // 'use strict'; 9 | import { initialize } from "../util/worker-init"; 10 | import { create } from '../../../node_modules/monaco-editor/esm/vs/language/typescript/ts.worker.js'; 11 | 12 | export const connect = (port) => { 13 | let initialized = false; 14 | port.onmessage = (e) => { 15 | initialize(function (ctx, createData) { 16 | return create(ctx, createData); 17 | }, port, initialized); 18 | }; 19 | } 20 | 21 | // @ts-ignore 22 | self.onconnect = (e) => { 23 | let [port] = e.ports; 24 | connect(port) 25 | } 26 | 27 | if (!("SharedWorkerGlobalScope" in self)) { 28 | connect(self); 29 | } 30 | 31 | export { }; -------------------------------------------------------------------------------- /src/typescript-worker.d.ts: -------------------------------------------------------------------------------- 1 | import type ts from "typescript"; 2 | 3 | export declare class TypeScriptWorker implements ts.LanguageServiceHost { 4 | private _ctx 5 | private _extraLibs 6 | private _languageService 7 | private _compilerOptions 8 | constructor(ctx: any, createData: any) 9 | getCompilationSettings(): ts.CompilerOptions 10 | getScriptFileNames(): string[] 11 | private _getModel 12 | getScriptVersion(fileName: string): string 13 | getScriptSnapshot(fileName: string): ts.IScriptSnapshot | undefined 14 | getScriptKind?(fileName: string): ts.ScriptKind 15 | getCurrentDirectory(): string 16 | getDefaultLibFileName(options: ts.CompilerOptions): string 17 | isDefaultLibFileName(fileName: string): boolean 18 | private static clearFiles 19 | getSyntacticDiagnostics(fileName: string): Promise 20 | getSemanticDiagnostics(fileName: string): Promise 21 | getSuggestionDiagnostics(fileName: string): Promise 22 | getCompilerOptionsDiagnostics(fileName: string): Promise 23 | getCompletionsAtPosition(fileName: string, position: number): Promise 24 | getCompletionEntryDetails( 25 | fileName: string, 26 | position: number, 27 | entry: string 28 | ): Promise 29 | getSignatureHelpItems(fileName: string, position: number): Promise 30 | getQuickInfoAtPosition(fileName: string, position: number): Promise 31 | getOccurrencesAtPosition(fileName: string, position: number): Promise | undefined> 32 | getDefinitionAtPosition(fileName: string, position: number): Promise | undefined> 33 | getReferencesAtPosition(fileName: string, position: number): Promise 34 | getNavigationBarItems(fileName: string): Promise 35 | getFormattingEditsForDocument(fileName: string, options: ts.FormatCodeOptions): Promise 36 | getFormattingEditsForRange( 37 | fileName: string, 38 | start: number, 39 | end: number, 40 | options: ts.FormatCodeOptions 41 | ): Promise 42 | getFormattingEditsAfterKeystroke( 43 | fileName: string, 44 | postion: number, 45 | ch: string, 46 | options: ts.FormatCodeOptions 47 | ): Promise 48 | findRenameLocations( 49 | fileName: string, 50 | positon: number, 51 | findInStrings: boolean, 52 | findInComments: boolean, 53 | providePrefixAndSuffixTextForRename: boolean 54 | ): Promise 55 | getRenameInfo(fileName: string, positon: number, options: ts.RenameInfoOptions): Promise 56 | getEmitOutput(fileName: string): Promise 57 | getCodeFixesAtPosition( 58 | fileName: string, 59 | start: number, 60 | end: number, 61 | errorCodes: number[], 62 | formatOptions: ts.FormatCodeOptions 63 | ): Promise> 64 | updateExtraLibs(extraLibs: IExtraLibs): void 65 | } 66 | 67 | export interface IExtraLib { 68 | content: string 69 | version: number 70 | } 71 | export interface IExtraLibs { 72 | [path: string]: IExtraLib 73 | } -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | darkMode: 'class', 3 | content: [ 4 | './src/pug/**/*.pug', 5 | './src/ts/**/*.tsx', 6 | './src/ts/**/*.ts' 7 | ], 8 | mode: "jit", 9 | theme: { 10 | extend: { 11 | fontFamily: { 12 | "manrope": ["Manrope", "Manrope-fallback", "Century Gothic", "sans-serif"] 13 | }, 14 | colors: { 15 | "elevated": "#1C1C1E", 16 | "elevated-2": "#262628", 17 | "label": "#ddd", 18 | "secondary": "#bbb", 19 | "tertiary": "#555", 20 | "quaternary": "#333", 21 | "center-container-dark": "#121212" 22 | }, 23 | screens: { 24 | 'lt-2xl': { 'max': '1536px' }, 25 | // => @media (max-width: 1536px) { ... } 26 | 27 | 'lt-xl': { 'max': '1280px' }, 28 | // => @media (max-width: 1280px) { ... } 29 | 30 | 'lt-lg': { 'max': '1024px' }, 31 | // => @media (max-width: 1024px) { ... } 32 | 33 | 'lt-md': { 'max': '768px' }, 34 | // => @media (max-width: 768px) { ... } 35 | 36 | 'lt-sm': { 'max': '640px' }, 37 | // => @media (max-width: 640px) { ... } 38 | 39 | 'lt-xsm': { 'max': '440px' }, 40 | // => @media (max-width: 480px) { ... } 41 | }, 42 | container: { 43 | center: 'true', 44 | }, 45 | }, 46 | }, 47 | /* ... */ 48 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "preserve", 4 | "jsxImportSource": "solid-js", 5 | "moduleResolution": "node", 6 | "target": "ES2021", 7 | "module": "ES2020", 8 | "lib": ["ES2021", "DOM", "DOM.Iterable", "WebWorker"], 9 | "sourceMap": true, 10 | "outDir": "lib", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "esModuleInterop": true, 14 | // Allow importing `.ts` files directly 15 | "allowImportingTsExtensions": true, 16 | // "rewriteRelativeImportExtensions": true, 17 | // Emit only declaration files 18 | "emitDeclarationOnly": true, 19 | "declaration": true 20 | }, 21 | "include": [ 22 | "src", 23 | "bundlejs/src" 24 | ], 25 | "exclude": [ 26 | "node_modules", 27 | "docs", 28 | "dist" 29 | ] 30 | } -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | import _gulp from "gulp"; 2 | 3 | export const { src, dest, parallel, task, series, watch } = _gulp; 4 | export const gulp = _gulp; 5 | 6 | // Streamline Gulp Tasks 7 | export const stream = (_src, _opt = {}) => { 8 | return new Promise((resolve) => { 9 | let _end = _opt.end; 10 | let host = 11 | typeof _src !== "string" && !Array.isArray(_src) 12 | ? _src 13 | : src(_src, _opt.opts), 14 | _pipes = _opt.pipes || [], 15 | _dest = _opt.dest === undefined ? "." : _opt.dest, 16 | _log = _opt.log || (() => { }); 17 | 18 | _pipes.forEach((val) => { 19 | if (val !== undefined && val !== null) { 20 | host = host.pipe(val); 21 | } 22 | }); 23 | 24 | if (_dest !== null) host = host.pipe(dest(_dest)); 25 | host.on("data", _log); 26 | host = host.on("end", (...args) => { 27 | if (typeof _end === "function") _end(...args); 28 | resolve(host); 29 | }); // Output 30 | 31 | if (Array.isArray(_end)) { 32 | _end.forEach((val) => { 33 | if (val !== undefined && val !== null) { 34 | host = host.pipe(val); 35 | } 36 | }); 37 | } 38 | 39 | return host; 40 | }); 41 | }; 42 | 43 | // A list of streams 44 | export const streamList = (...args) => { 45 | return Promise.all( 46 | (Array.isArray(args[0]) ? args[0] : args).map((_stream) => { 47 | return Array.isArray(_stream) ? stream(..._stream) : _stream; 48 | }) 49 | ); 50 | }; 51 | 52 | // A list of gulp tasks 53 | export const tasks = (list) => { 54 | let entries = Object.entries(list); 55 | for (let [name, fn] of entries) { 56 | task(name, (...args) => fn(...args)); 57 | } 58 | }; 59 | 60 | export const parallelFn = (...args) => { 61 | let tasks = args.filter((x) => x !== undefined && x !== null); 62 | return function parallelrun(done) { 63 | return parallel(...tasks)(done); 64 | }; 65 | }; 66 | 67 | export const seriesFn = (...args) => { 68 | let tasks = args.filter((x) => x !== undefined && x !== null); 69 | return function seriesrun(done) { 70 | return series(...tasks)(done); 71 | }; 72 | }; --------------------------------------------------------------------------------