├── src ├── noop.ts ├── index.ts ├── impack │ ├── index.ts │ └── pack.ts ├── tests │ ├── impack │ │ ├── example-jsx │ │ │ ├── no-names.ts │ │ │ ├── user.ts │ │ │ ├── app.ts │ │ │ ├── form.ts │ │ │ ├── input.ts │ │ │ ├── component.ts │ │ │ └── index.ts │ │ ├── import-map.json │ │ ├── example │ │ │ └── index.ts │ │ ├── index.ts │ │ └── workerd-tests.template.capnp │ └── index.tsx ├── is.ts └── cli.ts ├── .npmignore ├── .prettierignore ├── .gitignore ├── .nycrc ├── CONTRIBUTING.md ├── .github └── workflows │ ├── test-actions.yml │ ├── release-actions.yml │ └── codeql-analysis.yml ├── tsconfig.json ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── LICENSE.md ├── package.json ├── scripts ├── post-build.js └── correct-import-extensions.js ├── CODE-OF-CONDUCT.md ├── README.md └── yarn.lock /src/noop.ts: -------------------------------------------------------------------------------- 1 | export default {}; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./impack"; -------------------------------------------------------------------------------- /src/impack/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./pack"; -------------------------------------------------------------------------------- /src/tests/impack/example-jsx/no-names.ts: -------------------------------------------------------------------------------- 1 | export {} -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | node_modules 4 | coverage 5 | .env -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | node_modules 4 | esnext 5 | coverage 6 | *.md 7 | .env -------------------------------------------------------------------------------- /src/tests/impack/example-jsx/user.ts: -------------------------------------------------------------------------------- 1 | export async function *User(options: Record, input?: unknown) { 2 | 3 | } -------------------------------------------------------------------------------- /src/tests/impack/example-jsx/app.ts: -------------------------------------------------------------------------------- 1 | export async function *App(options: Record, input?: unknown) { 2 | 3 | } -------------------------------------------------------------------------------- /src/tests/impack/example-jsx/form.ts: -------------------------------------------------------------------------------- 1 | export async function *Form(options: Record, input?: unknown) { 2 | 3 | } -------------------------------------------------------------------------------- /src/tests/impack/example-jsx/input.ts: -------------------------------------------------------------------------------- 1 | export async function *Input(options: Record, input?: unknown) { 2 | 3 | } -------------------------------------------------------------------------------- /src/tests/impack/example-jsx/component.ts: -------------------------------------------------------------------------------- 1 | export async function *Component(options: Record, input?: unknown) { 2 | 3 | } -------------------------------------------------------------------------------- /src/tests/impack/example-jsx/index.ts: -------------------------------------------------------------------------------- 1 | import "./no-names"; 2 | 3 | export * from "./app"; 4 | export * from "./component"; 5 | export * from "./form"; 6 | export * from "./input"; 7 | export * from "./user"; 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | node_modules 4 | esnext 5 | esnext-workerd 6 | esnext-test 7 | coverage 8 | .env 9 | /test-results/ 10 | /playwright-report/ 11 | /playwright/.cache/ 12 | workerd-tests 13 | workerd-tests.capnp -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | 4 | ], 5 | "reporter": [ 6 | "clover", 7 | "json-summary", 8 | "html", 9 | "text-summary" 10 | ], 11 | "branches": 80, 12 | "lines": 80, 13 | "functions": 80, 14 | "statements": 80 15 | } 16 | -------------------------------------------------------------------------------- /src/tests/index.tsx: -------------------------------------------------------------------------------- 1 | /* c8 ignore start */ 2 | try { 3 | await import("./impack"); 4 | } catch (error) { 5 | console.error(error); 6 | if (typeof process !== "undefined") { 7 | process.exit(1); 8 | } 9 | throw error; 10 | } 11 | 12 | export default 1; 13 | -------------------------------------------------------------------------------- /src/tests/impack/import-map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "@virtualstate/promise": "./node_modules/@virtualstate/promise/esnext/index.js", 4 | "@virtualstate/promise/the-thing": "./node_modules/@virtualstate/promise/esnext/the-thing.js", 5 | "@virtualstate/union": "./node_modules/@virtualstate/union/lib/index.js", 6 | "filehound": "https://cdn.skypack.dev/filehound" 7 | } 8 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make by way of an issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | We are open to all ideas big or small, and are greatly appreciative of any and all contributions. 7 | 8 | Please note we have a code of conduct, please follow it in all your interactions with the project. 9 | -------------------------------------------------------------------------------- /.github/workflows/test-actions.yml: -------------------------------------------------------------------------------- 1 | name: test-actions 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | env: 8 | NO_COVERAGE_BADGE_UPDATE: 1 9 | PROMPT_NAME: test name 10 | PROMPT_EMAIL: test+email@example.com 11 | steps: 12 | - run: uname -a 13 | - uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | - uses: actions/setup-node@v2 17 | with: 18 | node-version: "18.x" 19 | registry-url: "https://registry.npmjs.org" 20 | - run: | 21 | yarn install 22 | - run: yarn build 23 | -------------------------------------------------------------------------------- /src/tests/impack/example/index.ts: -------------------------------------------------------------------------------- 1 | import { anAsyncThing } from "@virtualstate/promise/the-thing"; 2 | import { all, union } from "@virtualstate/promise"; 3 | 4 | const asyncThing = anAsyncThing({ 5 | async *[Symbol.asyncIterator]() { 6 | yield 1; 7 | yield 2; 8 | } 9 | }); 10 | for await (const thing of asyncThing) { 11 | console.log({ thing }); 12 | } 13 | 14 | console.log({ await: await asyncThing }); 15 | 16 | for await (const [a, b] of union([asyncThing, asyncThing])) { 17 | console.log({ a, b }); 18 | } 19 | 20 | console.log({ await: await anAsyncThing( 21 | union([ 22 | asyncThing, 23 | asyncThing 24 | ]) 25 | )}) -------------------------------------------------------------------------------- /src/tests/impack/index.ts: -------------------------------------------------------------------------------- 1 | import {cp, rmdir} from "node:fs/promises"; 2 | import {pack} from "#impack"; 3 | import {dirname} from "node:path"; 4 | import {chmod} from "fs/promises"; 5 | 6 | const directory = dirname(new URL(import.meta.url).pathname); 7 | 8 | { 9 | await rmdir("esnext-test").catch(error => void error); 10 | await cp("esnext", "esnext-test", { 11 | recursive: true 12 | }); 13 | await chmod("./esnext-test/cli.js", 0x777); 14 | 15 | await pack({ 16 | argv: [], 17 | paths: { 18 | importMap: "src/tests/impack/import-map.json", 19 | directory: "esnext-test", 20 | entrypoint: `${directory}/example` 21 | } 22 | }) 23 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "lib": ["es2018", "esnext"], 5 | "types": ["node"], 6 | "esModuleInterop": true, 7 | "target": "esnext", 8 | "noImplicitAny": true, 9 | "downlevelIteration": true, 10 | "moduleResolution": "node", 11 | "declaration": true, 12 | "sourceMap": true, 13 | "outDir": "esnext", 14 | "baseUrl": "./src", 15 | "jsx": "react", 16 | "jsxFactory": "h", 17 | "jsxFragmentFactory": "createFragment", 18 | "allowJs": true, 19 | "paths": { 20 | "#impack": ["./impack"] 21 | }, 22 | "resolveJsonModule": true 23 | }, 24 | "include": ["src/**/*", "src/**/*.json"], 25 | "typeRoots": ["./node_modules/@types", "src/types"], 26 | "exclude": ["node_modules/@opennetwork/vdom"] 27 | } 28 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/is.ts: -------------------------------------------------------------------------------- 1 | /* c8 ignore start */ 2 | export function isArray(value: unknown): value is T[]; 3 | export function isArray(value: unknown): value is unknown[]; 4 | export function isArray(value: unknown): boolean { 5 | return Array.isArray(value); 6 | } 7 | 8 | function isObjectLike(node: unknown): node is Record { 9 | return !!node && (typeof node === "object" || typeof node === "function"); 10 | } 11 | 12 | export function isPromise(input: unknown): input is Promise { 13 | return isObjectLike(input) && typeof input.then === "function"; 14 | } 15 | 16 | export function ok( 17 | value: unknown, 18 | message?: string 19 | ): asserts value; 20 | export function ok( 21 | value: unknown, 22 | message?: string 23 | ): asserts value is T; 24 | export function ok( 25 | value: unknown, 26 | message?: string 27 | ): asserts value { 28 | if (!value) { 29 | throw new Error(message ?? "Expected value"); 30 | } 31 | } -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import {isFile, pack} from "./impack"; 3 | import {dirname} from "node:path"; 4 | 5 | const argv = process.argv; 6 | 7 | 8 | const withoutPrefix = argv.filter(value => !value.startsWith("-")); 9 | 10 | const importMap = withoutPrefix.find(name => name.endsWith(".json")) 11 | const path = withoutPrefix.at(-1); 12 | 13 | const capnp = argv.find(value => value.startsWith("--capnp=")); 14 | const capnpTemplate = capnp && capnp.split("=").at(1); 15 | 16 | let directory: string, 17 | entrypoint: string = undefined; 18 | 19 | if (await isFile(path)) { 20 | directory = dirname(path); 21 | entrypoint = path; 22 | } else { 23 | directory = path; 24 | } 25 | 26 | const exclude = argv.filter(value => value.startsWith("--exclude=")) 27 | .map(value => value.replace("--exclude=", "").replace(/^['"](.+)['"]$/, "$1")) 28 | 29 | await pack({ 30 | argv, 31 | paths: { 32 | importMap, 33 | directory, 34 | entrypoint, 35 | capnpTemplate, 36 | exclude 37 | } 38 | }) -------------------------------------------------------------------------------- /.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 | // Add the IDs of extensions you want installed when the container is created. 17 | "extensions": ["dbaeumer.vscode-eslint"], 18 | 19 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 20 | // "forwardPorts": [], 21 | 22 | // Use 'postCreateCommand' to run commands after the container is created. 23 | // "postCreateCommand": "yarn install", 24 | 25 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 26 | "remoteUser": "node" 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Axiom Applied Technologies and Development Limited 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 | -------------------------------------------------------------------------------- /src/tests/impack/workerd-tests.template.capnp: -------------------------------------------------------------------------------- 1 | # Imports the base schema for workerd configuration files. 2 | 3 | # Refer to the comments in /src/workerd/server/workerd.capnp for more details. 4 | 5 | using Workerd = import "/workerd/workerd.capnp"; 6 | 7 | const network :Workerd.Network = ( 8 | allow = ["public"] 9 | ); 10 | 11 | # A constant of type Workerd.Config defines the top-level configuration for an 12 | # instance of the workerd runtime. A single config file can contain multiple 13 | # Workerd.Config definitions and must have at least one. 14 | const config :Workerd.Config = ( 15 | 16 | # Every workerd instance consists of a set of named services. A worker, for instance, 17 | # is a type of service. Other types of services can include external servers, the 18 | # ability to talk to a network, or accessing a disk directory. Here we create a single 19 | # worker service. The configuration details for the worker are defined below. 20 | services = [ (name = "main", worker = .test) ], 21 | 22 | # Every configuration defines the one or more sockets on which the server will listen. 23 | # Here, we create a single socket that will listen on localhost port 3000, and will 24 | # dispatch to the "main" service that we defined above. 25 | sockets = [ ( name = "http", address = "*:3000", http = (), service = "main" ) ] 26 | ); 27 | 28 | # The definition of the actual worker exposed using the "main" service. 29 | # In this example the worker is implemented as a single simple script. 30 | # The compatibilityDate is required. For more details on compatibility dates see: 31 | # https://developers.cloudflare.com/workers/platform/compatibility-dates/ 32 | 33 | const test :Workerd.Worker = ( 34 | modules = [], 35 | compatibilityDate = "2022-09-16", 36 | ); -------------------------------------------------------------------------------- /.github/workflows/release-actions.yml: -------------------------------------------------------------------------------- 1 | name: release-actions 2 | on: 3 | push: 4 | branches: 5 | - main 6 | release: 7 | types: 8 | - created 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - run: uname -a 14 | - uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | - uses: actions/setup-node@v2 18 | with: 19 | node-version: "18.x" 20 | registry-url: "https://registry.npmjs.org" 21 | - run: | 22 | yarn install 23 | - run: yarn build 24 | - name: Package Registry Publish - npm 25 | run: | 26 | git config user.name "${{ github.actor }}" 27 | git config user.email "${{ github.actor}}@users.noreply.github.com" 28 | npm set "registry=https://registry.npmjs.org/" 29 | npm set "@virtualstate:registry=https://registry.npmjs.org/" 30 | npm set "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" 31 | npm publish --access=public 32 | continue-on-error: true 33 | env: 34 | YARN_TOKEN: ${{ secrets.YARN_TOKEN }} 35 | NPM_TOKEN: ${{ secrets.YARN_TOKEN }} 36 | NODE_AUTH_TOKEN: ${{ secrets.YARN_TOKEN }} 37 | - uses: actions/setup-node@v2 38 | with: 39 | node-version: "16.x" 40 | registry-url: "https://npm.pkg.github.com" 41 | - name: Package Registry Publish - GitHub 42 | run: | 43 | git config user.name "${{ github.actor }}" 44 | git config user.email "${{ github.actor}}@users.noreply.github.com" 45 | npm set "registry=https://npm.pkg.github.com/" 46 | npm set "@virtualstate:registry=https://npm.pkg.github.com/virtualstate" 47 | npm set "//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}" 48 | npm publish --access=public 49 | env: 50 | YARN_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | NPM_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | continue-on-error: true 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@virtualstate/impack", 3 | "version": "1.0.0-alpha.15", 4 | "main": "./esnext/index.js", 5 | "module": "./esnext/index.js", 6 | "bin": { 7 | "impack": "./esnext/cli.js" 8 | }, 9 | "imports": { 10 | "#impack": "./esnext/impack/index.js", 11 | "#*": "./esnext/*/index.js" 12 | }, 13 | "types": "./esnext/index.d.ts", 14 | "typesVersions": { 15 | "*": { 16 | "*": [ 17 | "./esnext/index.d.ts" 18 | ], 19 | "tests": [ 20 | "./esnext/tests/index.d.ts" 21 | ], 22 | "routes": [ 23 | "./esnext/routes/index.d.ts" 24 | ] 25 | } 26 | }, 27 | "type": "module", 28 | "sideEffects": false, 29 | "keywords": [], 30 | "exports": { 31 | ".": "./esnext/index.js", 32 | "./tests": "./esnext/tests/index.js" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/virtualstate/impack.git" 37 | }, 38 | "bugs": { 39 | "url": "https://github.com/virtualstate/impack/issues" 40 | }, 41 | "homepage": "https://github.com/virtualstate/impack#readme", 42 | "author": "Fabian Cook ", 43 | "license": "MIT", 44 | "dependencies": { 45 | "filehound": "^1.17.4" 46 | }, 47 | "devDependencies": { 48 | "@types/bluebird": "^3.5.37", 49 | "@types/node": "^17.0.1", 50 | "@virtualstate/promise": "^1.2.1", 51 | "@virtualstate/union": "^2.48.1", 52 | "@virtualstate/listen": "^1.0.0-alpha.14", 53 | "typescript": "^4.4.3" 54 | }, 55 | "scripts": { 56 | "test:all": "yarn test:node", 57 | "build": "rm -rf esnext && tsc", 58 | "postbuild": "mkdir -p coverage && node scripts/post-build.js", 59 | "prepublishOnly": "npm run build", 60 | "test": "yarn build && yarn test:all", 61 | "test:node": "export $(cat .env | xargs) && node --enable-source-maps esnext/tests/index.js", 62 | "coverage": "export $(cat .env | xargs) && c8 node esnext/tests/index.js && yarn postbuild", 63 | "test:workerd:compile": "workerd compile workerd-tests.capnp > workerd-tests", 64 | "test:workerd": "yarn test:workerd:compile && ./workerd-tests --experimental", 65 | "impack": "yarn build && ./esnext/cli.js", 66 | "impack:test": "yarn build && ./esnext/cli.js ./esnext/tests/impack/example-jsx/index.js --capnp=./src/tests/impack/workerd-tests.template.capnp --listen-jsx > ./workerd-tests.capnp" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [main] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [main] 20 | schedule: 21 | - cron: "15 10 * * 5" 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ["javascript"] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /scripts/post-build.js: -------------------------------------------------------------------------------- 1 | import { promises as fs } from "fs"; 2 | import { dirname, resolve } from "path"; 3 | import {chmod} from "fs/promises"; 4 | 5 | await import("./correct-import-extensions.js"); 6 | 7 | 8 | await chmod("./esnext/cli.js", 0x755); 9 | 10 | const { pathname } = new URL(import.meta.url); 11 | const cwd = resolve(dirname(pathname), ".."); 12 | 13 | if (!process.env.NO_COVERAGE_BADGE_UPDATE) { 14 | const badges = []; 15 | 16 | const { name } = await fs.readFile("package.json", "utf-8").then(JSON.parse); 17 | 18 | badges.push( 19 | "### Support\n\n", 20 | "![Node.js supported](https://img.shields.io/badge/node-%3E%3D18.7.0-blue)", 21 | ); 22 | // 23 | // badges.push( 24 | // "\n\n### Test Coverage\n\n" 25 | // // `![nycrc config on GitHub](https://img.shields.io/nycrc/${name.replace(/^@/, "")})` 26 | // ); 27 | // 28 | // // const wptResults = await fs 29 | // // .readFile("coverage/wpt.results.json", "utf8") 30 | // // .then(JSON.parse) 31 | // // .catch(() => ({})); 32 | // // if (wptResults?.total) { 33 | // // const message = `${wptResults.pass}/${wptResults.total}`; 34 | // // const name = "Web Platform Tests"; 35 | // // badges.push( 36 | // // `![${name} ${message}](https://img.shields.io/badge/${encodeURIComponent( 37 | // // name 38 | // // )}-${encodeURIComponent(message)}-brightgreen)` 39 | // // ); 40 | // // } 41 | // 42 | // const coverage = await fs 43 | // .readFile("coverage/coverage-summary.json", "utf8") 44 | // .then(JSON.parse) 45 | // .catch(() => ({})); 46 | // const coverageConfig = await fs.readFile(".nycrc", "utf8").then(JSON.parse); 47 | // for (const [name, { pct }] of Object.entries(coverage?.total ?? {})) { 48 | // const good = coverageConfig[name]; 49 | // if (!good) continue; // not configured 50 | // const color = pct >= good ? "brightgreen" : "yellow"; 51 | // const message = `${pct}%25`; 52 | // badges.push( 53 | // `![${message} ${name} covered](https://img.shields.io/badge/${name}-${message}-${color})` 54 | // ); 55 | // } 56 | 57 | const tag = "[//]: # (badges)"; 58 | 59 | const readMe = await fs.readFile("README.md", "utf8"); 60 | const badgeStart = readMe.indexOf(tag); 61 | const badgeStartAfter = badgeStart + tag.length; 62 | if (badgeStart === -1) { 63 | throw new Error(`Expected to find "${tag}" in README.md`); 64 | } 65 | const badgeEnd = badgeStartAfter + readMe.slice(badgeStartAfter).indexOf(tag); 66 | const badgeEndAfter = badgeEnd + tag.length; 67 | const readMeBefore = readMe.slice(0, badgeStart); 68 | const readMeAfter = readMe.slice(badgeEndAfter); 69 | 70 | const readMeNext = `${readMeBefore}${tag}\n\n${badges.join( 71 | " " 72 | )}\n\n${tag}${readMeAfter}`; 73 | await fs.writeFile("README.md", readMeNext); 74 | // console.log("Wrote coverage badges!"); 75 | } 76 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [conduct+axiom@fabiancook.dev](mailto:conduct+axiom@fabiancook.dev). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `@virtualstate/impack` 2 | 3 | ESM Tree Importer 4 | 5 | [//]: # (badges) 6 | 7 | ### Support 8 | 9 | ![Node.js supported](https://img.shields.io/badge/node-%3E%3D18.7.0-blue) 10 | 11 | [//]: # (badges) 12 | 13 | # Usage 14 | 15 | The below command will re-write all import & export urls so that they are 16 | fully resolved. 17 | 18 | ```shell 19 | npx @virtualstate/impack esnext 20 | ``` 21 | 22 | This command will take 23 | 24 | ```javascript 25 | import { run } from "./path/to"; 26 | ``` 27 | 28 | If `./path/to` exists, the import is kept as is 29 | 30 | If `./path/to.js` exists, the import will be re-written as: 31 | 32 | ```javascript 33 | import { run } from "./path/to.js"; 34 | ``` 35 | 36 | If `./path/to/index.js` exists, the import will be re-written as: 37 | 38 | ```javascript 39 | import { run } from "./path/to/index.js"; 40 | ``` 41 | 42 | ## [Import maps](https://github.com/WICG/import-maps) 43 | 44 | The below command will output an import map with all files in the folder & dependencies 45 | 46 | ```shell 47 | # 48 | npx @virtualstate/impack import-map.json esnext 49 | ``` 50 | 51 | For example giving the import map of: 52 | 53 | ```json 54 | { 55 | "imports": { 56 | "@virtualstate/promise": "./node_modules/@virtualstate/promise/esnext/index.js" 57 | } 58 | } 59 | ``` 60 | 61 | Along with re-writing all import urls, you will get the output: 62 | 63 | ```json 64 | { 65 | "imports": { 66 | "@virtualstate/promise": "esnext/@virtualstate/promise/esnext/index.js", 67 | "esnext/path/to/inner.js": "esnext/path/to/inner.js", 68 | "esnext/path/to/index.js": "esnext/path/to/index.js" 69 | } 70 | } 71 | ``` 72 | 73 | The below command will output an import map with only the dependent files of this entrypoint file 74 | 75 | ```shell 76 | # 77 | npx @virtualstate/impack import-map.json esnext/tests/index.js 78 | ``` 79 | 80 | Any dependency urls that are not provided in the initial import map, are not replaced. 81 | 82 | ## [Node subpath patterns](https://nodejs.org/api/packages.html#subpath-imports) 83 | 84 | If you were to have an import that used a node subpath pattern, starting with 85 | `#`, if a replacement is not found in the provided import map, the closest 86 | package.json with a matching pattern will be used. 87 | 88 | For example if your `package.json` 89 | 90 | ```json 91 | { 92 | "imports": { 93 | "#internal/*.js": "./src/internal/*.js" 94 | } 95 | } 96 | ``` 97 | 98 | And used the import: 99 | 100 | ```javascript 101 | import Users from "#internal/users"; 102 | import Storage from "#internal/storage"; 103 | ``` 104 | 105 | You would get the output: 106 | 107 | ```json 108 | { 109 | "imports": { 110 | "src/index.js": "src/index.js", 111 | "src/internal/users.js": "src/internal/users.js", 112 | "src/internal/storage.js": "src/internal/storage.js" 113 | } 114 | } 115 | ``` 116 | 117 | ## [Cap'n Proto](https://capnproto.org/) 118 | 119 | The below command will output modules ready to use in a capnp file 120 | 121 | ```shell 122 | npx @virtualstate/impack import-map.json esnext --capnp 123 | ``` 124 | 125 | For Example: 126 | 127 | ```capnp 128 | modules = [ 129 | (name = "esnext/@virtualstate/promise/esnext/index.js", esModule = embed "esnext/@virtualstate/promise/esnext/index.js"), 130 | (name = "esnext/path/to/inner.js", esModule = embed "esnext/path/to/inner.js"), 131 | (name = "esnext/path/to/index.js", esModule = embed "esnext/path/to/index.js") 132 | ] 133 | ``` 134 | 135 | ```shell 136 | # Will replace the modules of workers in a capnp template file 137 | npx @virtualstate/impack import-map.json esnext --capnp=workerd-template.capnp 138 | ``` 139 | 140 | For example the below template: 141 | 142 | ```capnp 143 | const test :Workerd.Worker = ( 144 | modules = [], 145 | compatibilityDate = "2022-09-16", 146 | ); 147 | ``` 148 | 149 | Will output as: 150 | 151 | ```capnp 152 | const test :Workerd.Worker = ( 153 | modules = [ 154 | (name = "esnext/@virtualstate/promise/esnext/index.js", esModule = embed "esnext/@virtualstate/promise/esnext/index.js"), 155 | (name = "esnext/path/to/inner.js", esModule = embed "esnext/path/to/inner.js"), 156 | (name = "esnext/path/to/index.js", esModule = embed "esnext/path/to/index.js") 157 | ], 158 | compatibilityDate = "2022-09-16", 159 | ); 160 | ``` -------------------------------------------------------------------------------- /scripts/correct-import-extensions.js: -------------------------------------------------------------------------------- 1 | import FileHound from "filehound"; 2 | import { promises as fs } from "fs"; 3 | import path from "path"; 4 | 5 | export const STATEMENT_REGEX = /(?:(?:import|export)(?: .+ from)?\s+['"].+['"]|(?:import\s+\(['"].+["']\))|(?:import\(['"].+["']\)))/g; 6 | 7 | async function isFile(file) { 8 | const stat = await fs.stat(file).catch(() => undefined); 9 | return !!(stat && stat.isFile()); 10 | } 11 | 12 | async function isDirectory(file) { 13 | const stat = await fs.stat(file).catch(() => undefined); 14 | return !!(stat && stat.isDirectory()); 15 | } 16 | 17 | let newModules = []; 18 | 19 | async function importPaths(buildPath, importMapPath = process.env.IMPORT_MAP, readPath = buildPath) { 20 | // console.log({ 21 | // buildPath, 22 | // readPath 23 | // }) 24 | 25 | const filePaths = await FileHound.create() 26 | .paths(readPath) 27 | .discard("node_modules") 28 | .ext("js") 29 | .find(); 30 | 31 | // await Promise.all( 32 | // filePaths.map(getFileContents) 33 | // ); 34 | 35 | for (const filePath of filePaths) { 36 | await getFileContents(filePath); 37 | } 38 | 39 | async function getFileContents(filePath) { 40 | const filePathParts = filePath.split("/"); 41 | const srcShift = [...filePathParts] 42 | .slice(2) 43 | // Replace with shift up directory 44 | .map(() => "..") 45 | .join("/") 46 | const cwdShift = [...filePathParts] 47 | .slice(1) 48 | // Replace with shift up directory 49 | .map(() => "..") 50 | .join("/"); 51 | 52 | // console.log("Reading", { filePath }); 53 | const initialContents = await fs.readFile(filePath, "utf-8"); 54 | 55 | const importMapSource = importMapPath 56 | ? JSON.parse( 57 | await fs.readFile(importMapPath, "utf-8").catch(() => `{"imports":{}}`) 58 | ) 59 | : undefined; 60 | 61 | const importMap = { 62 | imports: { 63 | ...importMapSource?.imports 64 | } 65 | } 66 | 67 | let contents = initialContents, 68 | previousContents = initialContents; 69 | 70 | let times = 0; 71 | do { 72 | times += 1; 73 | previousContents = contents; 74 | contents = await process( 75 | contents 76 | ); 77 | } while (contents !== previousContents); 78 | 79 | // console.log({ times }); 80 | 81 | await fs.writeFile(filePath, contents, "utf-8"); 82 | 83 | async function process(initialContents) { 84 | const statements = initialContents.match(STATEMENT_REGEX); 85 | if (!statements) { 86 | return initialContents; 87 | } 88 | let contents = initialContents; 89 | for (const statement of statements) { 90 | contents = await replaceStatement(contents, statement); 91 | } 92 | return contents; 93 | } 94 | 95 | async function replaceStatement(contents, statement) { 96 | // console.log({ filePath, statement }); 97 | const initial = statement.match(/"(.+)"/)[1]; 98 | 99 | let url = initial; 100 | 101 | const importMapReplacement = importMap.imports[url]; 102 | 103 | // External dependency 104 | if (importMapReplacement && importMapReplacement.startsWith("./node_modules/")) { 105 | let moduleName, 106 | fileName; 107 | 108 | const moduleUrl = importMapReplacement.replace("./node_modules/", "") 109 | if (moduleUrl.startsWith("@")) { 110 | const [namespace, scopedName, ...rest] = moduleUrl 111 | .split("/") 112 | moduleName = `${namespace}/${scopedName}`; 113 | fileName = rest.join("/"); 114 | } else { 115 | const [name, ...rest] = moduleUrl 116 | .split("/"); 117 | moduleName = name; 118 | fileName = rest.join("/"); 119 | } 120 | 121 | const moduleTargetPath = `${buildPath}/${moduleName}`; 122 | url = `${srcShift}/${moduleName}/${fileName}`; 123 | 124 | // console.log({ filePath, statement, importMapReplacement, moduleName, fileName, moduleUrl, url }); 125 | 126 | if (!await isDirectory(moduleTargetPath)) { 127 | await fs.cp(`./node_modules/${moduleName}`, moduleTargetPath, { 128 | recursive: true 129 | }); 130 | newModules.push({ 131 | moduleName, 132 | moduleTargetPath 133 | }) 134 | } 135 | } else if (importMapReplacement) { 136 | if (importMapReplacement.includes("./src")) { 137 | url = importMapReplacement 138 | .replace("./src", srcShift) 139 | .replace(/\.tsx?$/, ".js"); 140 | } else if (importMapReplacement.startsWith("./")) { 141 | url = importMapReplacement 142 | .replace(/^\./, cwdShift); 143 | } else { 144 | url = importMapReplacement; 145 | } 146 | } 147 | 148 | const replacement = await getResolvedStatUrl(url); 149 | 150 | // console.log({ url, importMapReplacement, importMap, replacement }) 151 | 152 | return contents.replace( 153 | statement, 154 | statement.replace( 155 | initial, 156 | replacement 157 | ) 158 | ); 159 | 160 | async function getResolvedStatUrl(url) { 161 | const [existing, js, index] = await Promise.all([ 162 | isFile(path.resolve(path.dirname(filePath), url)), 163 | isFile(path.resolve(path.dirname(filePath), url + ".js")), 164 | isFile(path.resolve(path.dirname(filePath), url + "/index.js")), 165 | ]); 166 | // console.log({ 167 | // url, 168 | // existing, js, index 169 | // }) 170 | if (existing) { 171 | return url; 172 | } 173 | if (js) { 174 | return url + ".js"; 175 | } 176 | if (index) { 177 | return url + "/index.js"; 178 | } 179 | 180 | // console.error(`Don't know what to do with ${url} for ${statement} in ${filePath}`); 181 | // throw ""; 182 | return url; 183 | } 184 | } 185 | } 186 | } 187 | 188 | await importPaths("esnext"); 189 | 190 | 191 | // await fs.rmdir("esnext-workerd").catch(error => void error); 192 | // await fs.cp("esnext", "esnext-workerd", { 193 | // recursive: true 194 | // }); 195 | // 196 | // await importPaths("esnext-workerd", process.env.IMPORT_MAP_WORKERD || "import-map-workerd.json") 197 | // 198 | // while (newModules.length) { 199 | // newModules = []; 200 | // await importPaths("esnext-workerd", process.env.IMPORT_MAP_WORKERD || "import-map-workerd.json") 201 | // } 202 | 203 | // console.log("Import Extensions done"); 204 | 205 | // import { URLPattern } from "../../../../urlpattern-polyfill/esnext/dist/index.js"; -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/bluebird@^3.5.37": 6 | version "3.5.37" 7 | resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.37.tgz#b99e5c7fe382c2c6d5252dc99d9fba6810fedbeb" 8 | integrity sha512-g2qEd+zkfkTEudA2SrMAeAvY7CrFqtbsLILm2dT2VIeKTqMqVzcdfURlvu6FU3srRgbmXN1Srm94pg34EIehww== 9 | 10 | "@types/node@^17.0.1": 11 | version "17.0.45" 12 | resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" 13 | integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== 14 | 15 | "@virtualstate/focus@^1.4.10-alpha.4": 16 | version "1.4.10-alpha.4" 17 | resolved "https://registry.yarnpkg.com/@virtualstate/focus/-/focus-1.4.10-alpha.4.tgz#3e3f36ee89e06e6324dde944eb59b5fdf036fd69" 18 | integrity sha512-6MvDFBLYje0+R4xa32Ka8lT2NqPY6mgEAltblfKIy/LeJTVoc/OoUXzQzH8eNwkk1q2ATR2zmdpWlQGUf4RlVw== 19 | 20 | "@virtualstate/listen@^1.0.0-alpha.14": 21 | version "1.0.0-alpha.14" 22 | resolved "https://registry.yarnpkg.com/@virtualstate/listen/-/listen-1.0.0-alpha.14.tgz#a4ca90f21349542d7911290e4983045bbf5eb6b7" 23 | integrity sha512-CSEhpCF8aald9hqoE/7f7CeCnFgWweZS4NkJXNi2meT4l2thVlRgFPnHEFhhFHWV1S8ubSUZTA+Xv9uVW9cgxg== 24 | dependencies: 25 | "@virtualstate/focus" "^1.4.10-alpha.4" 26 | "@virtualstate/promise" "^1.2.1" 27 | abort-controller "^3.0.0" 28 | uuid "^8.3.2" 29 | whatwg-url "^9.1.0" 30 | 31 | "@virtualstate/promise@^1.2.1": 32 | version "1.2.1" 33 | resolved "https://registry.yarnpkg.com/@virtualstate/promise/-/promise-1.2.1.tgz#085a5d5830464f1331f180c3180b38414b1cffd6" 34 | integrity sha512-+1Yc2xS9za2zOeaoYgidC96XQ3YwmFSej87p+LWojP+S7GVaLSxQCNYiL83Pv0MpGIJT+j0XDEG+LufhB3Xzug== 35 | 36 | "@virtualstate/union@^2.48.1": 37 | version "2.48.1" 38 | resolved "https://registry.yarnpkg.com/@virtualstate/union/-/union-2.48.1.tgz#2aa43f9608affe65c21bfe10d6e5f02ed6bbb911" 39 | integrity sha512-Ow72T9Bg+98heNCB2ADn2hezwd6j2Nbr/pcIO/EVknZT9IHc8KNE9Wft9+HMEQ3Rwzn7j7ZZmTqxP3MlDbvXTA== 40 | 41 | abort-controller@^3.0.0: 42 | version "3.0.0" 43 | resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" 44 | integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== 45 | dependencies: 46 | event-target-shim "^5.0.0" 47 | 48 | balanced-match@^1.0.0: 49 | version "1.0.2" 50 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 51 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 52 | 53 | bluebird@^3.4.7, bluebird@^3.7.2: 54 | version "3.7.2" 55 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" 56 | integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== 57 | 58 | brace-expansion@^1.1.7: 59 | version "1.1.11" 60 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 61 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 62 | dependencies: 63 | balanced-match "^1.0.0" 64 | concat-map "0.0.1" 65 | 66 | brace-expansion@^2.0.1: 67 | version "2.0.1" 68 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" 69 | integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== 70 | dependencies: 71 | balanced-match "^1.0.0" 72 | 73 | concat-map@0.0.1: 74 | version "0.0.1" 75 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 76 | integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== 77 | 78 | err-code@^1.0.0: 79 | version "1.1.2" 80 | resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960" 81 | integrity sha512-CJAN+O0/yA1CKfRn9SXOGctSpEM7DCon/r/5r2eXFMY2zCCJBasFhcM5I+1kh3Ap11FsQCX+vGHceNPvpWKhoA== 82 | 83 | event-target-shim@^5.0.0: 84 | version "5.0.1" 85 | resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" 86 | integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== 87 | 88 | extend@^3.0.0: 89 | version "3.0.2" 90 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" 91 | integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== 92 | 93 | file-js@0.3.0: 94 | version "0.3.0" 95 | resolved "https://registry.yarnpkg.com/file-js/-/file-js-0.3.0.tgz#fab46bf782346c9294499f1f0d2ad07d838f25d1" 96 | integrity sha512-nZlX1pxpV6Mt8BghM3Z150bpsCT1zqil97UryusstZLSs9caYAe0Wph2UKPC3awfM2Dq4ri1Sv99KuK4EIImlA== 97 | dependencies: 98 | bluebird "^3.4.7" 99 | minimatch "^3.0.3" 100 | proper-lockfile "^1.2.0" 101 | 102 | filehound@^1.17.4: 103 | version "1.17.6" 104 | resolved "https://registry.yarnpkg.com/filehound/-/filehound-1.17.6.tgz#d5d87bd694316ea673bd0642b776b508d3f98a1d" 105 | integrity sha512-5q4zjFkI8W2zLmvbvyvI//K882IpEj6sMNXPUQlk5H6W4Wh3OSSylEAIEmMLELP9G7ileYjTKPXOn0YzzS55Lg== 106 | dependencies: 107 | bluebird "^3.7.2" 108 | file-js "0.3.0" 109 | lodash "^4.17.21" 110 | minimatch "^5.0.0" 111 | moment "^2.29.1" 112 | unit-compare "^1.0.1" 113 | 114 | graceful-fs@^4.1.2: 115 | version "4.2.10" 116 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" 117 | integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== 118 | 119 | lodash@^4.17.21: 120 | version "4.17.21" 121 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" 122 | integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== 123 | 124 | minimatch@^3.0.3: 125 | version "3.1.2" 126 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" 127 | integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== 128 | dependencies: 129 | brace-expansion "^1.1.7" 130 | 131 | minimatch@^5.0.0: 132 | version "5.1.0" 133 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" 134 | integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg== 135 | dependencies: 136 | brace-expansion "^2.0.1" 137 | 138 | moment@^2.14.1, moment@^2.29.1: 139 | version "2.29.4" 140 | resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" 141 | integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== 142 | 143 | proper-lockfile@^1.2.0: 144 | version "1.2.0" 145 | resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-1.2.0.tgz#ceff5dd89d3e5f10fb75e1e8e76bc75801a59c34" 146 | integrity sha512-YNjxtCoY3A+lohlLXWCYrHDhUdfU3MMnuC+ADhloDvJo586LKW23dPrjxGvRGuus05Amcf0cQy6vrjjtbJhWpw== 147 | dependencies: 148 | err-code "^1.0.0" 149 | extend "^3.0.0" 150 | graceful-fs "^4.1.2" 151 | retry "^0.10.0" 152 | 153 | punycode@^2.1.1: 154 | version "2.1.1" 155 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" 156 | integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== 157 | 158 | retry@^0.10.0: 159 | version "0.10.1" 160 | resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" 161 | integrity sha512-ZXUSQYTHdl3uS7IuCehYfMzKyIDBNoAuUblvy5oGO5UJSUTmStUUVPXbA9Qxd173Bgre53yCQczQuHgRWAdvJQ== 162 | 163 | tr46@^2.1.0: 164 | version "2.1.0" 165 | resolved "https://registry.yarnpkg.com/tr46/-/tr46-2.1.0.tgz#fa87aa81ca5d5941da8cbf1f9b749dc969a4e240" 166 | integrity sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw== 167 | dependencies: 168 | punycode "^2.1.1" 169 | 170 | typescript@^4.4.3: 171 | version "4.8.2" 172 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.2.tgz#e3b33d5ccfb5914e4eeab6699cf208adee3fd790" 173 | integrity sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw== 174 | 175 | unit-compare@^1.0.1: 176 | version "1.0.1" 177 | resolved "https://registry.yarnpkg.com/unit-compare/-/unit-compare-1.0.1.tgz#0c7459f0e5bf53637ea873ca3cee18de2eeca386" 178 | integrity sha512-AeLMQr8gcen2WOTwV0Gvi1nKKbY4Mms79MoltZ6hrZV/VANgE/YQly3jtWZJA/fa9m4ajhynq3XMqh5rOyZclA== 179 | dependencies: 180 | moment "^2.14.1" 181 | 182 | uuid@^8.3.2: 183 | version "8.3.2" 184 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" 185 | integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== 186 | 187 | webidl-conversions@^6.1.0: 188 | version "6.1.0" 189 | resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514" 190 | integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== 191 | 192 | whatwg-url@^9.1.0: 193 | version "9.1.0" 194 | resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-9.1.0.tgz#1b112cf237d72cd64fa7882b9c3f6234a1c3050d" 195 | integrity sha512-CQ0UcrPHyomtlOCot1TL77WyMIm/bCwrJ2D6AOKGwEczU9EpyoqAokfqrf/MioU9kHcMsmJZcg1egXix2KYEsA== 196 | dependencies: 197 | tr46 "^2.1.0" 198 | webidl-conversions "^6.1.0" 199 | -------------------------------------------------------------------------------- /src/impack/pack.ts: -------------------------------------------------------------------------------- 1 | import {readFile} from "node:fs/promises"; 2 | import {promises as fs} from "node:fs"; 3 | import FileHound from "filehound"; 4 | import {dirname, resolve, join} from "node:path"; 5 | import {isPromise, ok} from "../is"; 6 | import {writeFile} from "fs/promises"; 7 | import {createHash } from "node:crypto"; 8 | 9 | export interface PackPaths { 10 | importMap?: string; 11 | directory: string; 12 | entrypoint?: string; 13 | capnpTemplate?: string; 14 | exclude?: string | string[]; 15 | } 16 | 17 | export interface ResolveIdOptions extends Record { 18 | isEntry: boolean; 19 | } 20 | 21 | export interface ResolvedObject extends Record { 22 | id: string; 23 | } 24 | export type Resolved = string | ResolvedObject; 25 | export type MaybeResolved = Resolved | undefined; 26 | 27 | // 👀 28 | // https://rollupjs.org/guide/en/ 29 | // https://vitejs.dev/guide/api-plugin.html 30 | export interface ResolveFn { 31 | ( 32 | source: string, 33 | // This will always be undefined for now 34 | importer: string | undefined, 35 | options: ResolveIdOptions 36 | ): MaybeResolved | Promise 37 | } 38 | 39 | export interface PackOptions { 40 | argv?: string[]; 41 | paths: PackPaths; 42 | resolve?: ResolveFn | ResolveFn[]; 43 | } 44 | 45 | export interface ImportMap { 46 | imports: Record 47 | } 48 | 49 | export const STATEMENT_REGEX = /(?:(?:import|export)(?: .+ from)?\s+['"].+['"]|(?:import\s+\(['"].+["']\))|(?:import\(['"].+["']\)))/g; 50 | 51 | // Must be empty modules & bindings to be auto updated 52 | export const CAPNP_MODULES_REGEX = /modules\s*=\s*\[],?/g; 53 | export const CAPNP_BINDINGS_REGEX = /bindings\s*=\s*\[],?/g; 54 | 55 | export const JSX_MODULE_FOOTER_MARKER = "// Auto Generated: JSX Service"; 56 | export const JSX_MODULE_FOOTER_MARKER_START = `${JSX_MODULE_FOOTER_MARKER} Start`; 57 | export const JSX_MODULE_FOOTER_MARKER_END = `${JSX_MODULE_FOOTER_MARKER} End`; 58 | 59 | async function getCapnPTemplate({ capnpTemplate }: PackPaths): Promise { 60 | if (!capnpTemplate) return undefined; 61 | return await readFile(capnpTemplate, "utf-8").catch(() => undefined) 62 | } 63 | 64 | function getDefaultMap(): ImportMap { 65 | return { imports: {} } 66 | } 67 | 68 | async function getImportMap({ importMap }: PackPaths): Promise { 69 | if (!importMap) return getDefaultMap(); 70 | const contents = await readFile(importMap, "utf-8"); 71 | if (contents) { 72 | try { 73 | const map = JSON.parse(contents); 74 | return { 75 | ...map, 76 | imports: { 77 | ...map?.imports 78 | } 79 | } 80 | } catch { 81 | 82 | } 83 | } 84 | return getDefaultMap(); 85 | } 86 | 87 | 88 | async function getFilePaths({ directory, exclude }: PackPaths): Promise { 89 | let hound = FileHound.create() 90 | .paths(directory) 91 | .discard(join(directory, "node_modules")) 92 | .ext("js") 93 | if (Array.isArray(exclude)) { 94 | hound = exclude.reduce((hound, exclude) => { 95 | return hound.discard(exclude) 96 | }, hound) 97 | } else if (exclude) { 98 | hound = hound.discard(exclude); 99 | } 100 | return hound.find(); 101 | } 102 | 103 | export async function pack(options: PackOptions) { 104 | const { paths, argv, resolve: externalResolves } = options; 105 | const importMap = await getImportMap(paths); 106 | const processedFiles = new Set(); 107 | 108 | const cwd = process.cwd(); 109 | 110 | let anyImportsProcessed: boolean; 111 | do { 112 | anyImportsProcessed = await importPaths(); 113 | } while (anyImportsProcessed); 114 | 115 | const completeImportMap = await getCompleteImportMap(); 116 | 117 | const capnp = argv?.includes("--capnp") || paths.capnpTemplate; 118 | // const binary = argv.includes("--binary"); 119 | const silent = argv?.includes("--silent"); 120 | 121 | if (!capnp) { 122 | const json = JSON.stringify( 123 | completeImportMap, 124 | undefined, 125 | " " 126 | ); 127 | if (!silent) { 128 | console.log(json); 129 | } 130 | return json; 131 | } else { 132 | const capnp = await getCapnP(completeImportMap); 133 | if (!silent) { 134 | console.log(capnp); 135 | } 136 | return capnp; 137 | } 138 | 139 | function tab(string: string, tabs = " ") { 140 | return string 141 | .split("\n") 142 | .map(value => `${tabs}${value}`) 143 | .join("\n") 144 | } 145 | 146 | async function getCapnP(importMap: ImportMap): Promise { 147 | 148 | 149 | const modules = getModulesString(importMap) 150 | 151 | const capnpTemplate = await getCapnPTemplate(paths); 152 | 153 | if (!capnpTemplate) { 154 | return `modules = [\n${tab(modules)}\n]`; 155 | } 156 | 157 | let output = capnpTemplate; 158 | 159 | let lines = output.split("\n"); 160 | 161 | for (const [foundString] of output.matchAll(CAPNP_MODULES_REGEX)) { 162 | const suffix = foundString.endsWith(",") ? "," : ""; 163 | const line = lines.find(line => line.includes(foundString)); 164 | if (!line) continue; 165 | const [whitespace] = line.split(foundString); 166 | output = output.replace(foundString, `modules = [\n${tab(modules, `${whitespace}${whitespace}`)}\n${whitespace}]${suffix}`); 167 | 168 | lines = output.split("\n"); 169 | } 170 | 171 | if (argv?.includes("--listen-jsx")) { 172 | const { services, bindings } = await getCapnPListenJSXBindings(modules, importMap); 173 | // console.log({ services, bindings }); 174 | if (services && bindings.length) { 175 | output = `${output}\n\n${services}`; 176 | lines = output.split("\n"); 177 | } 178 | } 179 | 180 | return output; 181 | } 182 | 183 | function getModulesString(importMap: ImportMap, entryPoint?: string) { 184 | const entries = Object.entries(importMap.imports) 185 | .filter(([key, value]) => key !== entryPoint && value !== entryPoint) ; 186 | if (entryPoint) { 187 | entries.unshift([entryPoint, entryPoint]) 188 | } 189 | return entries 190 | .map( 191 | ([key, value]) => ( 192 | `(name = "${key}", esModule = embed "${value}")` 193 | ) 194 | ) 195 | .join(",\n") 196 | } 197 | 198 | async function getCapnPListenJSXBindings(modules: string, importMap: ImportMap) { 199 | 200 | const cwd = process.cwd(); 201 | 202 | // console.log({ cwd }); 203 | 204 | const processed = ( 205 | await Promise.all( 206 | Object.entries(importMap.imports) 207 | .map( 208 | ([, path]) => processJSXFunctionFile(path) 209 | ) 210 | ) 211 | ) 212 | .filter(Boolean); 213 | 214 | const services = processed.map( 215 | ({ services }) => services 216 | ).join("\n\n"); 217 | 218 | const bindings = processed.reduce( 219 | (all: string[], { bindings }) => all.concat(bindings), 220 | [] 221 | ); 222 | 223 | return { services, bindings }; 224 | 225 | async function processJSXFunctionFile(path: string): Promise<{ services: string, bindings: string[] } | undefined> { 226 | 227 | const module = await import( 228 | `${cwd}/${path}` 229 | ).catch(() => undefined); 230 | 231 | if (!module) return undefined; 232 | 233 | let contents = await readFile(path, "utf-8"); 234 | 235 | if (contents.includes(JSX_MODULE_FOOTER_MARKER_START) && contents.includes(JSX_MODULE_FOOTER_MARKER_END)) { 236 | contents = `${ 237 | contents.slice(0, contents.indexOf(JSX_MODULE_FOOTER_MARKER_START)) 238 | }${ 239 | contents.slice(contents.indexOf(JSX_MODULE_FOOTER_MARKER_END) + JSX_MODULE_FOOTER_MARKER_END.length) 240 | }` 241 | } 242 | 243 | let tryCount = 0; 244 | 245 | const bindings: string[] = []; 246 | const existingBindings = []; 247 | 248 | let baseServiceName: string = `internalJSXResolver${(await hash(path)).toUpperCase()}`; 249 | 250 | for (const [key, value] of Object.entries(module)) { 251 | if (typeof value !== "function") continue; 252 | 253 | if (Symbol.for(":jsx/type") in value && Symbol.asyncIterator in value) { 254 | bindings.push(key); 255 | existingBindings.push(key); 256 | continue; 257 | } 258 | 259 | const script = makeScript(key); 260 | await writeFile(path, withFooter(script), "utf-8"); 261 | 262 | tryCount += 1; 263 | 264 | let loaded; 265 | 266 | try { 267 | loaded = await import( 268 | `${cwd}/${path}?try=${tryCount}&bust=${Date.now()}` 269 | ); 270 | } catch (error) { 271 | // console.error(error); 272 | continue; 273 | } 274 | 275 | if ( 276 | typeof loaded[key][Symbol.for(":jsx/type")] === "function" && 277 | typeof loaded[key][Symbol.asyncIterator] === "function" 278 | ) { 279 | bindings.push(key); 280 | } 281 | } 282 | 283 | if (!bindings.length) { 284 | await writeFile(path, contents, "utf-8"); 285 | return undefined; 286 | } 287 | 288 | // console.log(bindings); 289 | 290 | const script = bindings.map(makeScript).join("\n\n"); 291 | 292 | await writeFile(path, withFooter(script), "utf-8"); 293 | 294 | const services = bindings.map( 295 | key => { 296 | const serviceName = getServiceName(key); 297 | return [ 298 | `const ${serviceName} :Workerd.Worker = (`, 299 | ` modules = [\n${tab(getModulesString(importMap, path), " ")}\n ],`, 300 | ` bindings = [\n (name = "${serviceName}")\n ]`, // This will be replaced with all bindings! 301 | `);` 302 | ].join("\n") 303 | } 304 | ).join("\n\n"); 305 | 306 | return { services, bindings: bindings.map(getServiceName) } as const; 307 | 308 | function getServiceName(key: string) { 309 | return `${baseServiceName}${key}` 310 | } 311 | 312 | function withFooter(script: string) { 313 | return [ 314 | contents, 315 | JSX_MODULE_FOOTER_MARKER_START, 316 | script, 317 | JSX_MODULE_FOOTER_MARKER_END 318 | ].join("\n") 319 | } 320 | 321 | function makeScript(key: string) { 322 | const serviceName = getServiceName(key); 323 | return ` 324 | const $___IsRawFetchEvent = Symbol("Raw Fetch Event"); 325 | 326 | ${key}[Symbol.for(":jsx/type")] = function ${key}Resolver(options, input) { 327 | if (options[$___IsRawFetchEvent]) { 328 | // Allow to pass directly to the function 329 | return ${key}(options, input); 330 | } 331 | 332 | if (typeof env === "undefined") { 333 | // No environment, use directly 334 | return ${key}(options, input); 335 | } 336 | 337 | if (!env[key] || !env[key].fetch) { 338 | // No fetch, use directly 339 | return ${key}(options, input); 340 | } 341 | 342 | async function fetcher(...args) { 343 | return env[key].fetch(...args); 344 | } 345 | 346 | console.log("Using service"); 347 | 348 | return async function FetchLike() { 349 | const { Fetch } = await import("@virtualstate/listen"); 350 | 351 | return Fetch( 352 | { 353 | ...options, 354 | fetch: fetcher 355 | }, 356 | input 357 | ); 358 | } 359 | } 360 | ${key}[Symbol.asyncIterator] = async function *EmptyResolve() { 361 | 362 | } 363 | 364 | export async function ${serviceName}(event) { 365 | const { respondWith } = await import("@virtualstate/listen"); 366 | const options = { 367 | event, 368 | request: event.request, 369 | }; 370 | let f; 371 | if (typeof h !== "undefined") { 372 | f = h; 373 | } else { 374 | const { h } = await import("@virtualstate/focus"); 375 | 376 | } 377 | return respondWith(event, ); 378 | } 379 | `.trim(); 380 | } 381 | } 382 | 383 | 384 | } 385 | 386 | 387 | async function hash(value: string) { 388 | const container = createHash("sha256"); 389 | container.update(value); 390 | return container.digest().toString("hex"); 391 | } 392 | 393 | async function getCompleteImportMap(): Promise { 394 | 395 | const { entrypoint } = paths; 396 | 397 | if (entrypoint) { 398 | return getFromEntrypoint(entrypoint); 399 | } 400 | 401 | return getAllFiles(); 402 | 403 | async function getAllFiles(): Promise { 404 | const filePaths = await getFilePaths(paths); 405 | return { 406 | imports: Object.fromEntries( 407 | filePaths.map(path => [path, path]) 408 | ) 409 | } 410 | } 411 | 412 | async function getFromEntrypoint(entrypoint: string): Promise { 413 | const urls = await getImportUrls(entrypoint, new Set()); 414 | return { 415 | imports: Object.fromEntries( 416 | [ 417 | ...new Set([ 418 | entrypoint 419 | .replace(/^\.\//, ""), 420 | ...urls, 421 | 422 | ]) 423 | ].map(path => [path, path]) 424 | ) 425 | }; 426 | } 427 | 428 | async function getImportUrls(moduleUrl: string, seen: Set): Promise { 429 | 430 | const directory = dirname(moduleUrl); 431 | 432 | const file = await readFile(moduleUrl, "utf-8").catch(error => { 433 | // console.error(`Can't read ${moduleUrl}`); 434 | // throw error; 435 | return ""; 436 | }); 437 | 438 | if (!file) return []; 439 | 440 | // console.log(file); 441 | 442 | const statements = file.match(STATEMENT_REGEX); 443 | 444 | if (!statements?.length) return []; 445 | 446 | const urls = statements 447 | .map(statement => statement.match(/"(.+)"/)[1]) 448 | .filter(Boolean) 449 | .filter(url => url.startsWith("../") || url.startsWith("./")) 450 | .map(url => resolve(directory, url) 451 | .replace(`${cwd}/`, "")) 452 | 453 | const nextSeen = new Set([ 454 | ...seen, 455 | ...urls 456 | ]); 457 | return [ 458 | ...urls, 459 | ...( 460 | await Promise.all( 461 | urls 462 | .filter(url => !seen.has(url)) 463 | .map(url => getImportUrls(url, nextSeen)) 464 | ) 465 | ).reduce((sum, array) => sum.concat(array), []) 466 | ]; 467 | } 468 | 469 | } 470 | 471 | async function importPaths(): Promise { 472 | const filePaths = await getFilePaths(paths); 473 | let any = false; 474 | 475 | for (const filePath of filePaths) { 476 | if (processedFiles.has(filePath)) { 477 | continue; 478 | } 479 | await processFile(filePath); 480 | processedFiles.add(filePath); 481 | any = true; 482 | } 483 | 484 | return any; 485 | 486 | async function processFile(filePath: string) { 487 | 488 | const initialContents = await fs.readFile(filePath, "utf-8"); 489 | 490 | const contents = await process( 491 | initialContents 492 | ); 493 | 494 | try { 495 | 496 | await fs.writeFile(filePath, contents, "utf-8"); 497 | } catch (error) { 498 | console.warn(`Failed to write ${filePath}`) 499 | } 500 | 501 | async function process(initialContents: string) { 502 | const statements = initialContents.match(STATEMENT_REGEX); 503 | if (!statements) { 504 | return initialContents; 505 | } 506 | let contents = initialContents; 507 | for (const statement of statements) { 508 | contents = await replaceStatement(contents, statement); 509 | } 510 | return contents; 511 | } 512 | 513 | async function replaceStatement(contents: string, statement: string) { 514 | const initial = statement.match(/["'](.+)["']/)[1]; 515 | 516 | const replacement = await getReplacementUrl(initial); 517 | 518 | // console.log(url, replacement); 519 | 520 | return contents.replace( 521 | statement, 522 | statement.replace( 523 | initial, 524 | replacement 525 | ) 526 | ); 527 | 528 | async function getReplacementUrl(url: string): Promise { 529 | const initial = url; 530 | 531 | let importMapReplacement = importMap.imports[url]; 532 | 533 | if (!importMapReplacement && url.startsWith("#")) { 534 | const packageImport = await getPackageImport(url); 535 | if (packageImport) { 536 | importMapReplacement = packageImport; 537 | } 538 | } 539 | 540 | if (!importMapReplacement) { 541 | const resolved = await externalResolve(url); 542 | if (resolved) { 543 | importMapReplacement = resolved; 544 | } 545 | } 546 | 547 | if (!importMapReplacement && (url.startsWith("node:") || url.startsWith("#"))) { 548 | return url; 549 | } 550 | 551 | // External dependency 552 | if (importMapReplacement && importMapReplacement.startsWith("./node_modules/")) { 553 | let moduleName, 554 | fileName; 555 | 556 | const moduleUrl = importMapReplacement.replace("./node_modules/", "") 557 | if (moduleUrl.startsWith("@")) { 558 | const [namespace, scopedName, ...rest] = moduleUrl 559 | .split("/") 560 | moduleName = `${namespace}/${scopedName}`; 561 | fileName = rest.join("/"); 562 | } else { 563 | const [name, ...rest] = moduleUrl 564 | .split("/"); 565 | moduleName = name; 566 | fileName = rest.join("/"); 567 | } 568 | 569 | const filePathParts = filePath 570 | .replace(`${paths.directory.replace(/^\.\//, "")}/`, "") 571 | .split("/"); 572 | const srcShift = [".", ...filePathParts.slice(1).map(() => "..")].join("/"); 573 | 574 | const moduleTargetPath = `${paths.directory}/${moduleName}`; 575 | url = `${srcShift}/${moduleName}/${fileName}`; 576 | 577 | if (!await isDirectory(moduleTargetPath)) { 578 | await fs.cp(`./node_modules/${moduleName}`, moduleTargetPath, { 579 | recursive: true 580 | }); 581 | } 582 | } else if (importMapReplacement) { 583 | if (importMapReplacement.startsWith("./")) { 584 | const shared = getSharedParentPath(paths.directory, dirname(importMapReplacement)); 585 | 586 | const shift = filePath 587 | .replace(`${shared.replace(/^\.\//, "")}/`, "") 588 | .split("/") 589 | .map(() => ".."); 590 | 591 | if (shared) { 592 | url = importMapReplacement.slice(shared.length).substring(1); 593 | const srcShift = shift.slice(1); 594 | if (srcShift.length) { 595 | url = `${srcShift.join("/")}/${url}` 596 | } else { 597 | url = `./${url}`; 598 | } 599 | 600 | } else { 601 | url = importMapReplacement 602 | .replace(/^\.\//, "") 603 | url = `${shift.join("/")}/${url}`; 604 | } 605 | 606 | // console.log({ shared, url }) 607 | } else { 608 | url = importMapReplacement; 609 | } 610 | } 611 | const replacement = await getResolvedStatUrl(url); 612 | if (replacement === initial) return replacement; 613 | // Allow another loop to continue resolution if 614 | // there is more replacement that could happen 615 | return getReplacementUrl(replacement); 616 | 617 | async function externalResolve(id: string) { 618 | 619 | for (const fn of getResolves()) { 620 | const maybe = fn( 621 | id, 622 | undefined, 623 | { 624 | isEntry: filePath === paths.entrypoint, 625 | // TODO Not yet resolved, but should be 626 | assertions: {}, 627 | custom: {} 628 | } 629 | ); 630 | let value: string; 631 | if (isPromise(maybe)) { 632 | value = getStringId(await maybe); 633 | } else { 634 | value = getStringId(maybe); 635 | } 636 | if (value && value !== id) { 637 | return value; 638 | } 639 | } 640 | 641 | 642 | function getStringId(value: MaybeResolved): string { 643 | if (!value) return undefined; 644 | if (typeof value === "string") return value; 645 | return value.id; 646 | } 647 | 648 | function getResolves(): ResolveFn[] { 649 | if (!externalResolves) return []; 650 | if (Array.isArray(externalResolves)) { 651 | return externalResolves; 652 | } 653 | return [externalResolves]; 654 | } 655 | 656 | } 657 | } 658 | 659 | async function getResolvedStatUrl(url: string) { 660 | const [existing, js, index] = await Promise.all([ 661 | isFile(resolve(dirname(filePath), url)), 662 | isFile(resolve(dirname(filePath), url + ".js")), 663 | isFile(resolve(dirname(filePath), url + "/index.js")), 664 | ]); 665 | if (existing) { 666 | return url; 667 | } 668 | if (js) { 669 | return url + ".js"; 670 | } 671 | if (index) { 672 | return url + "/index.js"; 673 | } 674 | return url; 675 | } 676 | 677 | } 678 | 679 | async function getPackageImport(url: string): Promise { 680 | interface Package { 681 | imports: Record 682 | } 683 | 684 | let packageDirectory = dirname(filePath); 685 | 686 | ok(url.startsWith("#")); 687 | 688 | let match; 689 | do { 690 | match = await getPackageMatch(packageDirectory); 691 | if (!match) { 692 | if (packageDirectory === cwd) { 693 | return undefined; 694 | } 695 | packageDirectory = dirname(packageDirectory); 696 | } 697 | } while (!match); 698 | 699 | return match; 700 | 701 | async function getPackageMatch(dir: string) { 702 | const { imports } = await getPackage(dir); 703 | 704 | if (!imports) return undefined; 705 | 706 | for (const key in imports) { 707 | const match = getMatch(key); 708 | if (match) { 709 | return match; 710 | } 711 | } 712 | 713 | function getMatch(key: string) { 714 | if (!key.startsWith("#")) return undefined; // ?? 715 | if (key === url) { 716 | return imports[key]; 717 | } 718 | if (!key.includes("*")) { 719 | return undefined; 720 | } 721 | 722 | const keySplit = key.split("*"); 723 | 724 | ok(keySplit.length === 2, "Expected one * in import"); 725 | 726 | const [prefix, suffix] = keySplit; 727 | 728 | ok(prefix, "Expected prefix for import, at least #"); 729 | 730 | if (!url.startsWith(prefix)) { 731 | return undefined; 732 | } 733 | if (suffix && !url.endsWith(suffix)) { 734 | return undefined; 735 | } 736 | const value = imports[key]; 737 | 738 | if (!value.includes("*")) { 739 | return value; 740 | } 741 | 742 | let wildcard = url.substring(prefix.length); 743 | if (suffix) { 744 | wildcard = url.substring(0, -suffix.length) 745 | } 746 | 747 | return value.replaceAll("*", wildcard); 748 | } 749 | } 750 | 751 | async function getPackage(dir: string): Promise { 752 | const path = `${dir}/package.json` 753 | if (!await isFile(path)) { 754 | if (dir === cwd) { 755 | return { imports: {} } 756 | } 757 | return getPackage( 758 | // Jump to parent dir 759 | dirname(dir) 760 | ); 761 | } 762 | const file = await readFile(path, "utf-8"); 763 | return JSON.parse(file); 764 | } 765 | 766 | } 767 | } 768 | } 769 | } 770 | 771 | export async function isFile(path: string) { 772 | const stat = await fs.stat(path).catch(() => undefined); 773 | return !!(stat && stat.isFile()); 774 | } 775 | 776 | export async function isDirectory(path: string) { 777 | const stat = await fs.stat(path).catch(() => undefined); 778 | return !!(stat && stat.isDirectory()); 779 | } 780 | 781 | function getSharedParentPath(pathA: string, pathB: string) { 782 | const splitA = pathA.split("/"); 783 | const splitB = pathB.split("/"); 784 | let shared; 785 | do { 786 | const nextA = splitA.shift(); 787 | const nextB = splitB.shift(); 788 | if (nextA === nextB) { 789 | if (shared) { 790 | shared = `${shared}/${nextA}`; 791 | } else { 792 | shared = nextA; 793 | } 794 | } 795 | } while (splitA.length && splitB.length); 796 | return shared || pathA; 797 | } 798 | 799 | --------------------------------------------------------------------------------