├── .changeset ├── README.md ├── config.json └── real-games-lie.md ├── .editorconfig ├── .github └── workflows │ ├── linter.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode ├── launch.json └── settings.json ├── README.md ├── index.html ├── package.json ├── packages ├── compartment │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── Evaluators.ts │ │ ├── Module.ts │ │ ├── ModuleSource.ts │ │ ├── commonjs │ │ │ └── runtime.ts │ │ ├── index.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── assert.ts │ │ │ ├── async-task.ts │ │ │ ├── makeGlobalThis.ts │ │ │ ├── normalize.ts │ │ │ ├── shapeCheck.ts │ │ │ └── spec.ts │ ├── test │ │ ├── commonjs │ │ │ └── direct-use.ts │ │ ├── evaluator.ts │ │ ├── module-source.ts │ │ ├── module │ │ │ ├── alias-import.ts │ │ │ ├── dynamic-import.ts │ │ │ ├── re-export.ts │ │ │ ├── self-import.ts │ │ │ └── top-level-await.ts │ │ ├── tsconfig.json │ │ └── utils │ │ │ ├── global.d.ts │ │ │ └── setup.ts │ └── tsconfig.json ├── intrinsic-snapshot │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── utils │ │ │ ├── allowList.ts │ │ │ ├── clone.ts │ │ │ └── intrinsic.ts │ └── tsconfig.json ├── membrane │ ├── CHANGELOG.md │ ├── build.mjs │ ├── chrome-devtools.js │ ├── package.json │ ├── src │ │ ├── createMembraneMarshall.ts │ │ ├── evaluator.ts │ │ ├── index.ts │ │ └── membrane.ts │ └── tsconfig.json ├── static-module-record-swc │ ├── .cargo │ │ └── config │ ├── .gitignore │ ├── CHANGELOG.md │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ ├── commonjs.d.mts │ ├── commonjs.mjs │ ├── compile.js │ ├── eval.d.ts │ ├── eval.js │ ├── package.json │ ├── src │ │ ├── lib.rs │ │ ├── module │ │ │ ├── binding_descriptor.rs │ │ │ ├── codegen.rs │ │ │ ├── config.rs │ │ │ ├── mod.rs │ │ │ ├── scanner.rs │ │ │ └── transformer.rs │ │ ├── script.rs │ │ ├── test.rs │ │ └── utils.rs │ └── tests │ │ ├── .editorconfig │ │ ├── fixture │ │ ├── arguments.js │ │ ├── config-template-callback-infer.js │ │ ├── config-template-callback.js │ │ ├── dynamic-import.js │ │ ├── empty.js │ │ ├── example-callback-infer.js │ │ ├── example-callback.js │ │ ├── example-eval.js │ │ ├── example.js │ │ ├── export-declaration.js │ │ ├── export-default-class-ident.js │ │ ├── export-default-class.js │ │ ├── export-default-expr.js │ │ ├── export-default-fn-ident.js │ │ ├── export-default-fn.js │ │ ├── export-named.js │ │ ├── import-default.js │ │ ├── import-mixed.js │ │ ├── import-named.js │ │ ├── import-namespace.js │ │ ├── import-no-binding.js │ │ ├── import-then-export.js │ │ ├── import.meta.js │ │ ├── name-conflict.js │ │ ├── reexport.js │ │ ├── shared-intrinsics.js │ │ ├── top-level-await.js │ │ └── unresolved-ident.js │ │ └── snapshot │ │ ├── arguments.js │ │ ├── config-template-callback-infer.js │ │ ├── config-template-callback.js │ │ ├── dynamic-import.js │ │ ├── empty.js │ │ ├── example-callback-infer.js │ │ ├── example-callback.js │ │ ├── example-eval.js │ │ ├── example.js │ │ ├── export-declaration.js │ │ ├── export-default-class-ident.js │ │ ├── export-default-class.js │ │ ├── export-default-expr.js │ │ ├── export-default-fn-ident.js │ │ ├── export-default-fn.js │ │ ├── export-named.js │ │ ├── import-default.js │ │ ├── import-mixed.js │ │ ├── import-named.js │ │ ├── import-namespace.js │ │ ├── import-no-binding.js │ │ ├── import-then-export.js │ │ ├── import.meta.js │ │ ├── name-conflict.js │ │ ├── reexport.js │ │ ├── shared-intrinsics.js │ │ ├── top-level-await.js │ │ └── unresolved-ident.js ├── web-endowments │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── fetch.ts │ │ ├── index.ts │ │ └── timers.ts │ └── tsconfig.json └── webpack-reflected-module │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ ├── index.ts │ ├── lib │ │ ├── dependency.ts │ │ ├── module.ts │ │ └── moduleFactory.ts │ ├── utils │ │ └── parseExpr.ts │ └── webpack.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json └── vite.config.ts /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.changeset/real-games-lie.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@masknet/static-module-record-swc': patch 3 | --- 4 | 5 | update swc 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 4 5 | charset = utf-8 6 | end_of_line = lf 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | indent_style = space 10 | 11 | [{.*rc,package.json}] 12 | indent_size = 2 13 | 14 | [*.{yaml,yml,md}] 15 | indent_size = 2 16 | 17 | [packages/static-module-record-swc/Cargo.lock] 18 | charset = unset 19 | end_of_line = unset 20 | insert_final_newline = unset 21 | trim_trailing_whitespace = unset 22 | indent_style = unset 23 | indent_size = unset 24 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: 4 | push 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | prettier: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | - run: npx prettier@3.1.0 --check . 17 | anti-trojan-source: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | - uses: actions/setup-node@v3 22 | - run: npx anti-trojan-source --files='packages/**/src/**/*.{ts,js}' 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | permissions: 14 | pull-requests: write 15 | contents: write 16 | id-token: write 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 0 22 | - uses: pnpm/action-setup@v2 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: "20" 26 | cache: "pnpm" 27 | - run: pnpm install 28 | - run: npm install -g npm 29 | - uses: actions-rs/toolchain@v1 30 | with: 31 | profile: minimal 32 | toolchain: stable 33 | override: true 34 | target: wasm32-unknown-unknown 35 | - uses: Swatinem/rust-cache@v1 36 | with: 37 | working-directory: ./packages/static-module-record-swc/ 38 | - uses: changesets/action@v1 39 | with: 40 | publish: pnpm run release 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 44 | NPM_CONFIG_PROVENANCE: true 45 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: pnpm/action-setup@v2 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: "18" 19 | cache: "pnpm" 20 | - run: pnpm install 21 | - run: pnpm run build 22 | - uses: actions-rs/toolchain@v1 23 | with: 24 | profile: minimal 25 | toolchain: stable 26 | override: true 27 | target: wasm32-unknown-unknown 28 | - uses: Swatinem/rust-cache@v1 29 | with: 30 | working-directory: ./packages/static-module-record-swc/ 31 | - run: rustup component add rustfmt 32 | - run: rustup component add clippy 33 | - uses: actions-rs/cargo@v1 34 | with: 35 | command: fmt 36 | args: --manifest-path ./packages/static-module-record-swc/Cargo.toml --all -- --check 37 | - uses: actions-rs/cargo@v1 38 | with: 39 | command: clippy 40 | args: --manifest-path ./packages/static-module-record-swc/Cargo.toml -- -D warnings 41 | - uses: actions-rs/cargo@v1 42 | with: 43 | command: test 44 | args: --manifest-path ./packages/static-module-record-swc/Cargo.toml 45 | - run: git diff --exit-code 46 | - uses: actions-rs/cargo@v1 47 | with: 48 | command: build 49 | args: --manifest-path ./packages/static-module-record-swc/Cargo.toml --release --target wasm32-unknown-unknown 50 | - run: pnpm test 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .swc 2 | test-dist 3 | 4 | # Created by https://www.toptal.com/developers/gitignore/api/node 5 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 6 | 7 | ### Node ### 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | .pnpm-debug.log* 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | *.lcov 32 | 33 | # nyc test coverage 34 | .nyc_output 35 | 36 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | bower_components 41 | 42 | # node-waf configuration 43 | .lock-wscript 44 | 45 | # Compiled binary addons (https://nodejs.org/api/addons.html) 46 | build/Release 47 | 48 | # Dependency directories 49 | node_modules/ 50 | jspm_packages/ 51 | 52 | # Snowpack dependency directory (https://snowpack.dev/) 53 | web_modules/ 54 | 55 | # TypeScript cache 56 | *.tsbuildinfo 57 | 58 | # Optional npm cache directory 59 | .npm 60 | 61 | # Optional eslint cache 62 | .eslintcache 63 | 64 | # Optional stylelint cache 65 | .stylelintcache 66 | 67 | # Microbundle cache 68 | .rpt2_cache/ 69 | .rts2_cache_cjs/ 70 | .rts2_cache_es/ 71 | .rts2_cache_umd/ 72 | 73 | # Optional REPL history 74 | .node_repl_history 75 | 76 | # Output of 'npm pack' 77 | *.tgz 78 | 79 | # Yarn Integrity file 80 | .yarn-integrity 81 | 82 | # dotenv environment variable files 83 | .env 84 | .env.development.local 85 | .env.test.local 86 | .env.production.local 87 | .env.local 88 | 89 | # parcel-bundler cache (https://parceljs.org/) 90 | .cache 91 | .parcel-cache 92 | 93 | # Next.js build output 94 | .next 95 | out 96 | 97 | # Nuxt.js build / generate output 98 | .nuxt 99 | dist 100 | 101 | # Gatsby files 102 | .cache/ 103 | # Comment in the public line in if your project uses Gatsby and not Next.js 104 | # https://nextjs.org/blog/next-9-1#public-directory-support 105 | # public 106 | 107 | # vuepress build output 108 | .vuepress/dist 109 | 110 | # vuepress v2.x temp and cache directory 111 | .temp 112 | 113 | # Docusaurus cache and generated files 114 | .docusaurus 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | 138 | ### Node Patch ### 139 | # Serverless Webpack directories 140 | .webpack/ 141 | 142 | # Optional stylelint cache 143 | 144 | # SvelteKit build / generate output 145 | .svelte-kit 146 | 147 | # End of https://www.toptal.com/developers/gitignore/api/node 148 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .github 2 | package.json 3 | pnpm-lock.yaml 4 | packages/static-module-record-swc/tests/**/* 5 | packages/static-module-record-swc/target/**/* 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "printWidth": 120, 4 | "semi": false, 5 | "singleQuote": true, 6 | "bracketSameLine": true, 7 | "tabWidth": 4, 8 | "experimentalTernaries": true 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Debug Current Test File", 8 | "autoAttachChildProcesses": true, 9 | "skipFiles": ["/**", "**/node_modules/**"], 10 | "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", 11 | "args": ["run", "${relativeFile}"], 12 | "smartStep": true, 13 | "console": "integratedTerminal" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ahead-of-time Secure EcmaScript 2 | 3 | The monorepo contains a set of packages that helps adopt [SES](https://github.com/tc39/proposal-ses) in a pre-compiled 4 | way. 5 | 6 | ## Security Assumptions 7 | 8 | This project has the following security assumptions (based on our usage): 9 | 10 | 1. Environment is already `lockdown()` by [ses](https://github.com/endojs/endo/tree/master/packages/ses). 11 | 2. Dynamic code execution (`eval` and `Function`) is not possible (if it is possible, please use the Compartment 12 | provided by [ses](https://github.com/endojs/endo/tree/master/packages/ses)). 13 | 3. Files executed are either precompiled into [VirtualModuleRecord][1] or trusted. 14 | 15 | ## Roadmap 16 | 17 | - ✅ `@masknet/static-module-record-swc`: A [swc][2] plugin to transform ES Module into [VirtualModuleRecord][1]. 18 | - ✅ `@masknet/compartment`: An eval-less implementation of [Compartment][1]. 19 | - ⌛ `@masknet/web-endowments`: Provide common Web APIs, with `AbortSignal` support to cancel out all side 20 | effects within a compartment, and provide attenuations (e.g. limits accessible databases of `indexedDB`, or limit 21 | accessible domains in `fetch`). 22 | - ✅`@masknet/membrane`: A membrane library. 23 | 24 | [1]: https://github.com/tc39/proposal-compartments#sketch 25 | [2]: https://github.com/swc-project/swc 26 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 11 | 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "pnpm@8.15.1", 3 | "name": "@masknet/aot-secure-ecmascript", 4 | "private": true, 5 | "version": "0.0.0", 6 | "license": "MIT", 7 | "scripts": { 8 | "test": "vitest", 9 | "coverage": "vitest run --coverage", 10 | "release": "pnpm run build && pnpm run -r build && npx changeset publish", 11 | "build": "tsc -b ./tsconfig.json", 12 | "build:watch": "tsc -b ./tsconfig.json --watch" 13 | }, 14 | "devDependencies": { 15 | "@changesets/cli": "^2.27.1", 16 | "@masknet/config": "^0.2.1", 17 | "@swc/core": "^1.4.0", 18 | "@vitest/coverage-c8": "^0.33.0", 19 | "c8": "^9.1.0", 20 | "prettier": "^3.2.5", 21 | "rollup": "^4.10.0", 22 | "rollup-plugin-swc3": "^0.11.0", 23 | "typescript": "5.3.3", 24 | "vite": "^5.1.1", 25 | "vitest": "^1.2.2" 26 | }, 27 | "pnpm": { 28 | "onlyBuiltDependencies": [] 29 | }, 30 | "dependencies": { 31 | "@vitest/ui": "^1.2.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/compartment/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @masknet/compartment 2 | 3 | ## 0.6.0 4 | 5 | ### Minor Changes 6 | 7 | - 6dd3bed: update dependencies 8 | 9 | ## 0.5.0 10 | 11 | ### Minor Changes 12 | 13 | - da873b1: add commonjs support 14 | 15 | ### Patch Changes 16 | 17 | - 63dfba3: upgrade dependencies 18 | 19 | ## 0.4.2 20 | 21 | ### Patch Changes 22 | 23 | - 05d5513: rollup bug https://github.com/rollup/rollup/issues/4696 workaround 24 | 25 | ## 0.4.1 26 | 27 | ### Patch Changes 28 | 29 | - 65acfdf: avoid call importHook multiple times, follow upstream spec https://github.com/tc39/proposal-compartments/commit/5c57371744880c72ec52c6138c6b73af24c1ea9e 30 | 31 | ## 0.4.0 32 | 33 | ### Minor Changes 34 | 35 | - 4128bdc: change signature of Module constructor to follow upstream spec 36 | 37 | ### Patch Changes 38 | 39 | - b2d840c: follow spec change https://github.com/tc39/ecma262/commit/24f5631ca109f14c7efe545ee1e4549a8123811d 40 | - edb67fb: throw for async execute() that does not marked as isAsync: true 41 | - 2836929: follow spec change https://github.com/tc39/ecma262/commit/4b83bcca92eb54126e2523d4d01290f3cd8521e1 42 | 43 | ## 0.3.12 44 | 45 | ### Patch Changes 46 | 47 | - 319ace2: follow upstream spec change 48 | - abd0433: support async stack tagging API 49 | 50 | ## 0.3.11 51 | 52 | ### Patch Changes 53 | 54 | - 48e5022: update dependencies 55 | 56 | ## 0.3.10 57 | 58 | ### Patch Changes 59 | 60 | - 2a8c9f7: follow tc39/ecma262#2905 61 | 62 | ## 0.3.9 63 | 64 | ### Patch Changes 65 | 66 | - bafad7d: adopt new module loading refactor spec 67 | 68 | ## 0.3.8 69 | 70 | ### Patch Changes 71 | 72 | - d708b09: fix: live export does not setup correctly 73 | 74 | ## 0.3.7 75 | 76 | ### Patch Changes 77 | 78 | - 91a63b6: fix: alias import and default import not working 79 | 80 | ## 0.3.6 81 | 82 | ### Patch Changes 83 | 84 | - 6ecb93a: fix: hangs when a module import itself 85 | 86 | ## 0.3.5 87 | 88 | ### Patch Changes 89 | 90 | - 6de38a5: add bundle 91 | 92 | ## 0.3.4 93 | 94 | ### Patch Changes 95 | 96 | - 8ce3aff: fix: namespace object does not reflect to live export 97 | 98 | ## 0.3.3 99 | 100 | ### Patch Changes 101 | 102 | - 72460dc: fix env object will cause chrome devtools crash 103 | 104 | ## 0.3.2 105 | 106 | ### Patch Changes 107 | 108 | - be8c572: add types field 109 | 110 | ## 0.3.1 111 | 112 | ### Patch Changes 113 | 114 | - 9429d89: add default prototype of makeGlobalThis 115 | 116 | ## 0.3.0 117 | 118 | ### Minor Changes 119 | 120 | - 8cdc134: update api signature of Module 121 | - 99f8965: remove Compartment, createModuleCache, createWebImportMeta, URLResolveHook 122 | 123 | ## 0.2.3 124 | 125 | ### Patch Changes 126 | 127 | - 762d129: add generic to Module and ModuleSource 128 | - 00f2da8: fix: module env record throws on the setter 129 | - 7da5cdb: move globalThis out of environment record 130 | - 762d129: add Symbol.toString tag to Module and ModuleSource 131 | 132 | ## 0.2.2 133 | 134 | ### Patch Changes 135 | 136 | - 1ba4f60: remove unsupported syntax in safari 14 137 | - 02eed40: rename SyntheticModuleRecord to VirtualModuleRecord 138 | - f4913e0: fix env object cannot be set 139 | - 1dcaf05: remove StaticModuleRecord constructor 140 | - 393fe2a: add source property to reflect the module source 141 | - c3cc4cc: fix dynamic import the same record multiple times 142 | - 8eb9860: rename initialize in virtual module record to execute 143 | - d9e88f8: change shape of Module constructor 144 | 145 | ## 0.2.1 146 | 147 | ### Patch Changes 148 | 149 | - c44b77b: Implement new Evaluators API 150 | - 212a2be: Allow initialize function to be undefined 151 | - 3783fc3: Rename ExecutionContext to Evaluators 152 | - 0580f03: Set this when calling initialize of virtual module 153 | 154 | ## 0.2.0 155 | 156 | ### Minor Changes 157 | 158 | - 1d5a849: Add ModuleSource constructor 159 | - 9bda3a7: Implement new virtual module binding format 160 | - f152912: Deprecate Compartment constructor before rewrite 161 | - 6398846: Add ExecutionContext constructor 162 | - a606687: Implement live binding 163 | 164 | ### Patch Changes 165 | 166 | - 9ab17fd: Add Module to ExecutionContext 167 | - dafaa0e: Fix export \* as name from 'mod' not handled 168 | - b836c8a: Add ModuleSource to ExecutionContext 169 | - f152912: Add options bag to ExecutionContext 170 | - fddd147: Add 3rd parameter to helper function createModuleCache to add extra binding for reexports 171 | 172 | ## 0.1.1 173 | 174 | ### Patch Changes 175 | 176 | - 557e321: [Spec] Rename ThirdPartyStaticModuleRecord to SyntheticModuleRecord 177 | 178 | ## 0.1.0 179 | 180 | ### Minor Changes 181 | 182 | - 4139fc5: First release 🎉 183 | -------------------------------------------------------------------------------- /packages/compartment/README.md: -------------------------------------------------------------------------------- 1 | # @masknet/compartment 2 | 3 | > WARNING: This package currently does not follow the [Semantic Versioning](https://semver.org/) because the original standard is still developing. The minor version might include breaking changes! 4 | 5 | This package implements a user-land [Virtual Module Source][layer-2] evaluator. 6 | 7 | This package currently implements the following specs/API explainers: 8 | 9 | - [Module Expressions proposal](https://tc39.es/proposal-module-expressions/) 10 | - [Compartment proposal: Layer 0: Module and ModuleSource constructor][layer-0] 11 | - [Compartment proposal: Layer 1: ModuleSource reflection][layer-1] 12 | - [Compartment proposal: Layer 2: Virtual Module Source][layer-2] 13 | - [Compartment proposal: Layer 3: Evaluator][layer-3] 14 | 15 | ## Assumptions and runtime requirements 16 | 17 | 1. The environment is already `lockdown()` by [ses][ses]. 18 | 2. Dynamic code execution (`eval` and `Function`) is not possible. 19 | 3. Code executed are either trusted or precompiled into a [Virtual Module Source][layer-2] by a compiler like [@masknet/static-module-record-swc](../static-module-record-swc/). 20 | 4. ECMAScript 2022 syntax is available. 21 | 22 | ## APIs 23 | 24 | ### `ModuleSource` constructor 25 | 26 | Implements `ModuleSource` from [layer 0][layer-0] and [layer 1][layer-1] of the compartment proposal. 27 | 28 | This constructor always throws like it is in an environment that cannot use eval. 29 | 30 | ```ts 31 | new ModuleSource() 32 | // EvalError: Refused to evaluate a string as JavaScript. 33 | ``` 34 | 35 | ### `Module` constructor 36 | 37 | Implements `Module` from [layer 0][layer-0] and [layer-2][layer-2] of the compartment proposal. 38 | 39 | ```ts 40 | import { Module, imports, type VirtualModuleRecord } from '@masknet/compartment' 41 | const virtualModule: VirtualModuleRecord = { 42 | execute(environment, context) { 43 | console.log('module constructed!') 44 | }, 45 | } 46 | const module = new Module(virtualModule, import.meta.url, () => null) 47 | // ^referral ^importHook 48 | const moduleNamespace = await imports(module) 49 | ``` 50 | 51 | ### `imports` function 52 | 53 | This function is a user-land dynamic import that accepts `Module` instances. 54 | 55 | This function does not accept strings as dynamic import does. 56 | 57 | ### `Evaluators` constructor 58 | 59 | This constructor implements `Evaluators` from [layer 3][layer-3] of the compartment proposal. 60 | 61 | ```ts 62 | import { Evaluators, Module, imports, type VirtualModuleRecord } from '@masknet/compartment' 63 | const globalThis = { answer: 42 } 64 | const evaluators = new Evaluators({ globalThis }) 65 | const virtualModule: VirtualModuleRecord = { 66 | bindings: [{ export: 'x' }], 67 | execute(environment, { globalThis }) { 68 | environment.x = globalThis.answer // 42 69 | }, 70 | } 71 | const module = new evaluators.Module(virtualModule, import.meta.url, () => null) 72 | const moduleNamespace = await imports(module) 73 | moduleNamespace.x // 42 74 | ``` 75 | 76 | ### `makeGlobalThis` function 77 | 78 | This function is a utility function that creates a new object that contains only items from the ECMAScript specification. 79 | Those items are from the **current realm**, therefore sharing them with the Evaluators without [lockdown()](ses) might bring serious problems. 80 | 81 | ```ts 82 | import { makeGlobalThis } from '@masknet/compartment' 83 | const globalThis = makeGlobalThis() 84 | globalThis.Array // [Function: Array] 85 | ``` 86 | 87 | [ses]: https://github.com/endojs/endo/tree/master/packages/ses 88 | [layer-0]: https://tc39.es/proposal-compartments/0-module-and-module-source.html 89 | [layer-1]: https://github.com/tc39/proposal-compartments/blob/master/1-static-analysis.md 90 | [layer-2]: https://github.com/tc39/proposal-compartments/blob/master/2-virtual-module-source.md 91 | [layer-3]: https://github.com/tc39/proposal-compartments/blob/master/3-evaluator.md 92 | -------------------------------------------------------------------------------- /packages/compartment/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@masknet/compartment", 3 | "version": "0.6.0", 4 | "type": "module", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "sideEffects": false, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/DimensionDev/aot-secure-ecmascript" 11 | }, 12 | "exports": { 13 | "types": "./dist/index.d.ts", 14 | "default": [ 15 | "./dist/bundle.js", 16 | "./dist/index.js" 17 | ] 18 | }, 19 | "scripts": { 20 | "build": "pnpx rollup -c" 21 | }, 22 | "devDependencies": { 23 | "@swc/core": "^1.4.0" 24 | }, 25 | "files": [ 26 | "dist/bundle.js", 27 | "dist/**/*.d.ts", 28 | "dist/**/*.d.ts.map", 29 | "src" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /packages/compartment/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'rollup' 2 | import { defineRollupSwcOption } from 'rollup-plugin-swc3' 3 | import { swc } from 'rollup-plugin-swc3' 4 | 5 | export default defineConfig({ 6 | input: './src/index.ts', 7 | output: { 8 | file: './dist/bundle.js', 9 | format: 'esm', 10 | sourcemap: true, 11 | }, 12 | plugins: [ 13 | swc( 14 | defineRollupSwcOption({ 15 | sourceMaps: true, 16 | jsc: { target: 'es2022' }, 17 | }), 18 | ), 19 | ], 20 | }) 21 | -------------------------------------------------------------------------------- /packages/compartment/src/Evaluators.ts: -------------------------------------------------------------------------------- 1 | import { Module, setParentGlobalThis, setParentImportHook, setParentImportMetaHook } from './Module.js' 2 | import type { ModuleSource } from './ModuleSource.js' 3 | import type { ImportHook, ModuleHandler, VirtualModuleRecord } from './types.js' 4 | 5 | export interface EvaluatorsOptions { 6 | globalThis?: object | undefined 7 | importHook?: ImportHook | undefined 8 | importMeta?: object | null | undefined 9 | } 10 | export class Evaluators { 11 | Module: typeof Module 12 | Evaluators: typeof Evaluators 13 | get globalThis() { 14 | return this.#AssignGlobalThis 15 | } 16 | // We do not support `eval` and `Function`. 17 | eval = eval 18 | Function = Function 19 | 20 | // implementation 21 | constructor(handler: EvaluatorsOptions) { 22 | const { globalThis, importHook, importMeta } = handler 23 | this.#Handler = handler 24 | 25 | if (typeof globalThis !== 'object' && globalThis !== undefined) 26 | throw new TypeError('globalThis must be an object') 27 | if (typeof importHook !== 'function' && importHook !== undefined) 28 | throw new TypeError('importHook must be a function') 29 | if (typeof importMeta !== 'object' && importMeta !== undefined && importMeta !== null) 30 | throw new TypeError('importMeta must be an object') 31 | 32 | const parent = this 33 | class Evaluators extends TopEvaluators { 34 | constructor(options: EvaluatorsOptions) { 35 | super(options) 36 | this.#ParentEvaluator = parent 37 | } 38 | } 39 | class Module extends TopModule { 40 | constructor(moduleSource: ModuleSource | VirtualModuleRecord, handler: ModuleHandler) { 41 | super(moduleSource, handler) 42 | setParentGlobalThis(this, (parent.#CalculatedGlobalThis ??= parent.#GetGlobalThis())) 43 | setParentImportHook(this, (parent.#CalculatedImportHook ??= parent.#GetImportHook())) 44 | setParentImportMetaHook(this, (meta) => 45 | Object.assign(meta, (parent.#CalculatedImportMeta ??= parent.#GetImportMeta())), 46 | ) 47 | } 48 | } 49 | this.#AssignedImportHook = importHook 50 | this.#AssignedImportMeta = importMeta 51 | this.#AssignGlobalThis = globalThis 52 | 53 | this.Module = Module 54 | this.Evaluators = Evaluators 55 | } 56 | #ParentEvaluator: Evaluators | undefined 57 | #AssignGlobalThis: object | undefined 58 | #AssignedImportHook: ImportHook | undefined 59 | #AssignedImportMeta: object | undefined | null 60 | #CalculatedGlobalThis: object | undefined 61 | #CalculatedImportHook: ImportHook | undefined 62 | #CalculatedImportMeta: object | undefined | null 63 | #Handler: EvaluatorsOptions 64 | 65 | #GetGlobalThis(): object { 66 | if (this.#AssignGlobalThis) return this.#AssignGlobalThis 67 | if (this.#ParentEvaluator) return this.#ParentEvaluator.#GetGlobalThis() 68 | return realGlobalThis 69 | } 70 | #GetImportHook(): ImportHook { 71 | if (this.#AssignedImportHook) return this.#AssignedImportHook.bind(this.#Handler) 72 | if (this.#ParentEvaluator) return this.#ParentEvaluator.#GetImportHook() 73 | return defaultImportHook 74 | } 75 | #GetImportMeta(): object | null { 76 | if (this.#AssignedImportMeta) return this.#AssignedImportMeta 77 | if (this.#ParentEvaluator) return this.#ParentEvaluator.#GetImportMeta() 78 | return null 79 | } 80 | } 81 | const TopEvaluators = Evaluators 82 | const TopModule = Module 83 | const realGlobalThis = globalThis 84 | 85 | /** @internal */ 86 | export function defaultImportHook(): never { 87 | throw new TypeError(`This evaluator does not have any import resolution strategy.`) 88 | } 89 | -------------------------------------------------------------------------------- /packages/compartment/src/ModuleSource.ts: -------------------------------------------------------------------------------- 1 | import type { Binding } from './types.js' 2 | 3 | export class ModuleSource { 4 | #code: unknown 5 | get bindings(): Binding[] { 6 | this.#code 7 | return [] 8 | } 9 | constructor(source: string) { 10 | throw new EvalError(`Refused to evaluate a string as JavaScript.`) 11 | } 12 | } 13 | 14 | Reflect.defineProperty(ModuleSource.prototype, Symbol.toStringTag, { 15 | configurable: true, 16 | value: 'ModuleSource', 17 | }) 18 | -------------------------------------------------------------------------------- /packages/compartment/src/commonjs/runtime.ts: -------------------------------------------------------------------------------- 1 | import { imports, Module } from '../Module.js' 2 | import type { 3 | ExportBinding, 4 | ImportHook, 5 | ImportMetaHook, 6 | VirtualModuleRecord, 7 | VirtualModuleRecordExecuteContext, 8 | } from '../types.js' 9 | import { normalizeVirtualModuleRecord } from '../utils/normalize.js' 10 | import { isExportAllBinding, isImportAllBinding, isImportBinding, isReExportBinding } from '../utils/shapeCheck.js' 11 | 12 | // an commonjs object might be used across multiple commonjs modules (module.exports = require('...')) 13 | // when this assignment appears, we add the relationship so we can update ES Module exports later 14 | const commonjsExportsLink = new WeakMap>() 15 | 16 | export let requires: (module: CommonJSModule) => any 17 | export type RequireHook = (specifier: string) => CommonJSModule | null 18 | export interface CommonJSHandler { 19 | requireHook?: RequireHook | undefined 20 | import?: ImportHook 21 | globalThis: typeof globalThis 22 | } 23 | export class CommonJSModule { 24 | constructor(id: string, source: VirtualModuleRecord, handler: CommonJSHandler) { 25 | Object.defineProperties(this, { 26 | id: { enumerable: true, value: String(id) }, 27 | exports: { 28 | enumerable: true, 29 | get: () => this.#exports, 30 | set: (value) => { 31 | commonjsExportsLink.get(this.#exports)?.delete(this) 32 | this.#exports = value 33 | commonjsExportsLink.get(this.#exports)?.add(this) 34 | this.#updateSyntheticExports() 35 | }, 36 | }, 37 | }) 38 | 39 | this.#source = normalizeVirtualModuleRecord(source) 40 | if (!isValidCommonJSReflectionModule(this.#source)) { 41 | throw new TypeError('CommonJS module source must be a valid reflection module') 42 | } 43 | 44 | const { globalThis, requireHook, import: importHook } = handler 45 | if (typeof globalThis !== 'object') throw new TypeError('globalThis must be an object') 46 | this.#globalThis = globalThis 47 | 48 | if (requireHook !== undefined && typeof requireHook !== 'function') 49 | throw new TypeError('requireHook must be a function') 50 | this.#requireHook = requireHook 51 | 52 | if (importHook !== undefined && typeof importHook !== 'function') 53 | throw new TypeError('importHook must be a function') 54 | this.#importHook = importHook 55 | 56 | const self = this 57 | this.#lexicals = { 58 | module: this, 59 | __filename: this.id, 60 | require: (path) => this.#require(path), 61 | get exports() { 62 | return self.#exports 63 | }, 64 | } 65 | } 66 | declare readonly id: string 67 | declare exports: any 68 | /** 69 | * Create a CommonJS from an object. 70 | * @example 71 | * CommonJSModule.of(require('fs')) 72 | */ 73 | static of(exports: any) { 74 | const exportList = Object.keys(exports) 75 | const module = new CommonJSModule( 76 | '', 77 | { 78 | bindings: exportList.map((key): ExportBinding => ({ export: key })).concat({ export: 'default' }), 79 | needsImportMeta: true, 80 | execute(environment, context) { 81 | const [{ module }, skip] = ((context.importMeta as any).commonjs as CommonJSInitialize)( 82 | (value) => (environment.default = value), 83 | Object.fromEntries(exportList.map((key) => [key, (value) => (environment[key] = value)])), 84 | ) 85 | if (!skip) module.exports = exports 86 | }, 87 | }, 88 | { globalThis }, 89 | ) 90 | module.#execute() 91 | return module 92 | } 93 | get asESModule() { 94 | if (this.#ESModule) return this.#ESModule 95 | this.#ESModule = new Module(this.#source, { 96 | importHook: this.#importHook, 97 | importMetaHook: (meta) => { 98 | Object.assign(meta, { 99 | commonjs: ((a, b) => this.#commonjs(a, b, this.#evaluated, true)) as CommonJSInitialize, 100 | }) 101 | }, 102 | }) 103 | return this.#ESModule 104 | } 105 | 106 | // Implementation details below 107 | #evaluated = false 108 | #ESModule: Module | undefined 109 | #exports: any = CommonJSModule.#createCommonJsNamespaceObject(this) 110 | #source: VirtualModuleRecord 111 | readonly #requireHook: RequireHook | undefined 112 | readonly #importHook: CommonJSHandler['import'] | undefined 113 | readonly #globalThis: typeof globalThis 114 | #LoadedModules = new Map() 115 | #require(id: string) { 116 | id = String(id) 117 | if (!this.#LoadedModules.has(id)) { 118 | if (!this.#requireHook) throw new Error(`Cannot find module '${id}'`) 119 | const module = this.#requireHook(id) 120 | if (!module || !module.#LoadedModules) throw new Error('Invalid module returned from requireHook') 121 | this.#LoadedModules.set(id, module) 122 | } 123 | return this.#LoadedModules.get(id)!.#execute() 124 | } 125 | #execute() { 126 | if (this.#evaluated) return this.#exports 127 | this.#evaluated = true 128 | const { execute, needsImport } = this.#source 129 | const context: VirtualModuleRecordExecuteContext = { 130 | globalThis: this.#globalThis, 131 | importMeta: { 132 | commonjs: ((a, b) => this.#commonjs(a, b, false, false)) as CommonJSInitialize, 133 | }, 134 | } 135 | if (needsImport) 136 | context.import = async (spec) => { 137 | spec = String(spec) 138 | const hook = this.#importHook 139 | if (!hook) throw new Error('importHook is not defined') 140 | const module = await hook(spec) 141 | if (!module) throw new Error(`Module ${spec} not found`) 142 | return imports(module) 143 | } 144 | 145 | try { 146 | execute!({}, context) 147 | return this.#exports 148 | } finally { 149 | this.#lexicals = null 150 | } 151 | } 152 | // Only be set after this module is imported as a ES Module 153 | #defaultReflect: CommonJSExportReflect | undefined 154 | // Only be set after this module is imported as a ES Module 155 | #namedExportReflects: Map | undefined 156 | // set to null after evaluation 157 | #lexicals: CommonJSGlobalLexicals | null 158 | #commonjs( 159 | defaultReflect: CommonJSExportReflect, 160 | namedReflects: Record, 161 | skipEvaluation: boolean, 162 | syntheticExport: boolean, 163 | ): [lexicals: CommonJSGlobalLexicals, skipEvaluation: boolean] { 164 | // this module has been already evaluated 165 | if (syntheticExport) { 166 | this.#defaultReflect = defaultReflect 167 | this.#namedExportReflects = new Map() 168 | try { 169 | for (const [key, value] of Object.entries(namedReflects)) { 170 | if (typeof value !== 'function') continue 171 | this.#namedExportReflects.set(key, value) 172 | } 173 | } catch {} 174 | if (skipEvaluation) this.#updateSyntheticExports() 175 | } 176 | if (!this.#lexicals) return [{} as any, true] 177 | return [this.#lexicals, skipEvaluation] 178 | } 179 | #updateSyntheticExports() { 180 | if (!this.#defaultReflect || !this.#namedExportReflects) return 181 | tryApply(this.#defaultReflect, this.#exports) 182 | for (const [key, value] of this.#namedExportReflects) { 183 | tryApply(value, this.#exports[key]) 184 | } 185 | } 186 | static #updateNamedExport(namespaceObject: object, key: PropertyKey) { 187 | if (typeof key !== 'string') return 188 | const links = commonjsExportsLink.get(namespaceObject) 189 | if (!links) return 190 | for (const module of links) { 191 | if (!module.#namedExportReflects) continue 192 | const reflect = module.#namedExportReflects.get(key) 193 | if (!reflect) continue 194 | tryApply(reflect, (namespaceObject as any)[key]) 195 | } 196 | } 197 | static #createCommonJsNamespaceObject(initialModule: CommonJSModule) { 198 | const proxy = new Proxy( 199 | {}, 200 | { 201 | defineProperty(target, property, attributes) { 202 | const result = Reflect.defineProperty(target, property, attributes) 203 | CommonJSModule.#updateNamedExport(proxy, property) 204 | return result 205 | }, 206 | deleteProperty(target, p) { 207 | const result = Reflect.deleteProperty(target, p) 208 | CommonJSModule.#updateNamedExport(proxy, p) 209 | return result 210 | }, 211 | set(target, p, newValue, receiver) { 212 | const result = Reflect.set(target, p, newValue, receiver) 213 | CommonJSModule.#updateNamedExport(proxy, p) 214 | return result 215 | }, 216 | }, 217 | ) 218 | commonjsExportsLink.set(proxy, new Set([initialModule])) 219 | return proxy 220 | } 221 | static { 222 | requires = (module) => module.#execute() 223 | } 224 | } 225 | 226 | export type CommonJSInitialize = ( 227 | exportDefaultReflect: CommonJSExportReflect, 228 | exportsReflect: Record, 229 | ) => [lexicals: CommonJSGlobalLexicals, skipEvaluation: boolean] 230 | export type CommonJSExportReflect = (value: any) => void 231 | export interface CommonJSGlobalLexicals { 232 | module: CommonJSModule 233 | readonly exports: object 234 | require: (specifier: string) => any 235 | __filename: string 236 | __dirname?: string 237 | } 238 | 239 | function tryApply(f: CommonJSExportReflect, x: any) { 240 | try { 241 | f(x) 242 | } catch {} 243 | } 244 | 245 | function isValidCommonJSReflectionModule(module: VirtualModuleRecord) { 246 | if (!module.needsImportMeta || !module.execute || module.isAsync || !module.bindings?.length) return false 247 | const invalidBinding = module.bindings.find((binding) => { 248 | return ( 249 | isImportAllBinding(binding) || 250 | isImportBinding(binding) || 251 | isExportAllBinding(binding) || 252 | isReExportBinding(binding) 253 | ) 254 | }) 255 | if (invalidBinding) return false 256 | return (module.bindings as ExportBinding[]).find((x) => x.export === 'default') 257 | } 258 | -------------------------------------------------------------------------------- /packages/compartment/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | Binding, 3 | ImportBinding, 4 | ImportAllBinding, 5 | ExportBinding, 6 | ExportAllBinding, 7 | VirtualModuleRecord, 8 | VirtualModuleRecordExecuteContext, 9 | ModuleNamespace, 10 | ImportHook, 11 | ImportMetaHook, 12 | ModuleHandler, 13 | } from './types.js' 14 | export { ModuleSource } from './ModuleSource.js' 15 | export { Evaluators } from './Evaluators.js' 16 | export { Module, imports } from './Module.js' 17 | 18 | export { makeGlobalThis } from './utils/makeGlobalThis.js' 19 | 20 | export { 21 | type CommonJSExportReflect, 22 | type CommonJSGlobalLexicals, 23 | type CommonJSHandler, 24 | type CommonJSInitialize, 25 | CommonJSModule, 26 | type RequireHook as CommonJSRequireHook, 27 | requires as commonjsRequires, 28 | } from './commonjs/runtime.js' 29 | -------------------------------------------------------------------------------- /packages/compartment/src/types.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/tc39/proposal-compartments/blob/775024d93830ee6464363b4b373d9353425a0776/README.md 2 | 3 | import type { Module } from './Module.js' 4 | 5 | export type Binding = ImportBinding | ExportBinding | ImportAllBinding | ExportAllBinding 6 | /** 7 | * ``` 8 | * import { X as Y } from 'Z' 9 | * ``` 10 | * 11 | * import = X, as = Y, from = Z 12 | */ 13 | export interface ImportBinding { 14 | import: string 15 | as?: string | undefined 16 | from: string 17 | } 18 | export interface ImportAllBinding { 19 | importAllFrom: string 20 | as: string 21 | } 22 | /** 23 | * ``` 24 | * export { X as Y } from 'Z' 25 | * ``` 26 | * 27 | * export = X, as = Y, from = Z 28 | */ 29 | export interface ExportBinding { 30 | export: string 31 | as?: string | undefined 32 | from?: string | undefined 33 | } 34 | export interface ExportAllBinding { 35 | exportAllFrom: string 36 | as?: string | undefined 37 | } 38 | export interface VirtualModuleRecord { 39 | bindings?: Array | undefined 40 | execute?: ((environment: any, context: VirtualModuleRecordExecuteContext) => void | Promise) | undefined 41 | needsImportMeta?: boolean | undefined 42 | needsImport?: boolean | undefined 43 | isAsync?: boolean | undefined 44 | } 45 | 46 | export type ModuleNamespace = Record 47 | export interface VirtualModuleRecordExecuteContext { 48 | importMeta?: object 49 | import?( 50 | spec: string | Module, 51 | options?: ImportCallOptions, 52 | ): Promise 53 | globalThis: typeof globalThis 54 | } 55 | 56 | export type ImportHook = (importSpecifier: string) => PromiseLike | Module | null 57 | export type ImportMetaHook = (importMeta: object) => void 58 | export interface ModuleHandler { 59 | importHook?: ImportHook | undefined 60 | importMetaHook?: ImportMetaHook 61 | } 62 | -------------------------------------------------------------------------------- /packages/compartment/src/utils/assert.ts: -------------------------------------------------------------------------------- 1 | function getOpaqueProxy() { 2 | const x = Proxy.revocable({}, {}) 3 | x.revoke() 4 | return x.proxy 5 | } 6 | 7 | /** @internal */ 8 | export const opaqueProxy = /*#__PURE__*/ getOpaqueProxy() 9 | /** @internal */ 10 | export function assertFailed(message?: string): never { 11 | throw new TypeError('Assertion failed.' + (message ? ' ' : '') + message) 12 | } 13 | /** @internal */ 14 | export function unreachable(val: never): never { 15 | throw new TypeError('Unreachable') 16 | } 17 | -------------------------------------------------------------------------------- /packages/compartment/src/utils/async-task.ts: -------------------------------------------------------------------------------- 1 | let asyncTaskPolyfill: Task 2 | function getAsyncTaskAPI() { 3 | if (typeof console === 'object' && typeof console.createTask === 'function') return console.createTask 4 | return (_name: string): Task => 5 | (asyncTaskPolyfill ||= { 6 | run: Function.prototype.call.bind(Function.call), 7 | }) 8 | } 9 | /** 10 | * @internal 11 | * @see https://developer.chrome.com/docs/devtools/console/api/#createtask 12 | */ 13 | export const createTask = /*#__PURE__*/ getAsyncTaskAPI() 14 | 15 | // declaration 16 | interface Console { 17 | createTask?(name: string): Task 18 | } 19 | 20 | export interface Task { 21 | run(f: () => T): T 22 | } 23 | declare const console: Console | undefined 24 | -------------------------------------------------------------------------------- /packages/compartment/src/utils/makeGlobalThis.ts: -------------------------------------------------------------------------------- 1 | import { Evaluators } from '../Evaluators.js' 2 | import { ModuleSource } from '../ModuleSource.js' 3 | 4 | export function makeGlobalThis(prototype: object | null = Object.prototype): typeof globalThis { 5 | const global = Object.create(null) 6 | 7 | Object.defineProperties( 8 | global, 9 | intrinsic.reduce((previous, name) => { 10 | previous[name] = Object.getOwnPropertyDescriptor(globalThis, name) 11 | return previous 12 | }, Object.create(null)), 13 | ) 14 | 15 | Object.defineProperties(global, { 16 | globalThis: { writable: true, configurable: true, value: global }, 17 | Evaluators: { writable: true, configurable: true, value: Evaluators }, 18 | ModuleSource: { writable: true, configurable: true, value: ModuleSource }, 19 | Module: { writable: true, configurable: true, value: new Evaluators({ globalThis: global }) }, 20 | }) 21 | 22 | return Object.setPrototypeOf(global, prototype) 23 | } 24 | 25 | // https://tc39.es/ecma262/multipage/global-object.html#sec-global-object 26 | const intrinsic = [ 27 | 'Infinity', 28 | 'NaN', 29 | 'undefined', 30 | 31 | // Note: This library has an assumption that NO eval is available due to CSP. 32 | // Therefore we use the original version to make it looks like a "native function". 33 | 34 | // This library runs under SES lockdown environment, so we cannot modify Function.prototype.toString to do that. 35 | 'eval', 36 | 'isFinite', 37 | 'isNaN', 38 | 'parseFloat', 39 | 'parseInt', 40 | 'decodeURI', 41 | 'decodeURIComponent', 42 | 'encodeURI', 43 | 'encodeURIComponent', 44 | 45 | 'AggregateError', 46 | 'Array', 47 | 'ArrayBuffer', 48 | 'BigInt', 49 | 'BigInt64Array', 50 | 'BigUint64Array', 51 | 'Boolean', 52 | 'Date', 53 | 'DataView', 54 | 'Error', 55 | 'EvalError', 56 | 'FinalizationRegistry', 57 | 'Float32Array', 58 | 'Float64Array', 59 | // Note: Same as eval. 60 | 'Function', 61 | 'Int8Array', 62 | 'Int16Array', 63 | 'Int32Array', 64 | 'Map', 65 | 'Number', 66 | 'Object', 67 | 'Promise', 68 | 'Proxy', 69 | 'RangeError', 70 | 'ReferenceError', 71 | 'RegExp', 72 | 'Set', 73 | // NO SharedArrayBuffer! 74 | 'String', 75 | 'Symbol', 76 | 'SyntaxError', 77 | 'TypeError', 78 | 'Uint8Array', 79 | 'Uint8ClampedArray', 80 | 'Uint16Array', 81 | 'Uint32Array', 82 | 'URIError', 83 | 'WeakMap', 84 | 'WeakRef', 85 | 'WeakSet', 86 | 87 | // NO Atomics (because we don't have SharedArrayBuffer) 88 | 'JSON', 89 | 'Math', 90 | 'Reflect', 91 | ] 92 | -------------------------------------------------------------------------------- /packages/compartment/src/utils/normalize.ts: -------------------------------------------------------------------------------- 1 | import type { VirtualModuleRecord, Binding, ExportAllBinding, ImportBinding } from '../types.js' 2 | import { assertFailed, unreachable } from './assert.js' 3 | import { hasFromField, isExportAllBinding, isExportBinding, isImportAllBinding, isImportBinding } from './shapeCheck.js' 4 | import { all, allButDefault, namespace, type ModuleExportEntry, type ModuleImportEntry } from './spec.js' 5 | 6 | /** @internal */ 7 | export function normalizeVirtualModuleRecord(module: VirtualModuleRecord): VirtualModuleRecord { 8 | const { execute, bindings, needsImport, needsImportMeta, isAsync } = module 9 | if (execute !== undefined && execute !== null && typeof execute !== 'function') { 10 | throw new TypeError('VirtualModuleRecord.execute must be a function') 11 | } 12 | return { 13 | execute, 14 | needsImportMeta: Boolean(needsImportMeta), 15 | needsImport: Boolean(needsImport), 16 | isAsync: Boolean(isAsync), 17 | bindings: normalizeBindings(bindings), 18 | } 19 | } 20 | 21 | /** @internal */ 22 | export function normalizeImportMeta(importMeta: object | undefined | null) { 23 | if (!importMeta) return undefined 24 | if (typeof importMeta !== 'object') throw new TypeError('importMeta must be an object') 25 | return importMeta 26 | } 27 | 28 | /** @internal */ 29 | export function normalizeBindings(binding: Binding[] | undefined): Binding[] { 30 | if (!binding) return [] 31 | if (!Array.isArray(binding)) throw new TypeError('bindings must be an array.') 32 | const result: Binding[] = [] 33 | for (const item of binding) { 34 | if ('importAllFrom' in item) { 35 | if (!('as' in item)) throw new TypeError('ImportAllBinding must have an "as" field.') 36 | if ('exportAllFrom' in item || 'import' in item || 'export' in item || 'from' in item) 37 | throw new TypeError( 38 | 'ImportAllBinding cannot have "exportAllFrom", "import", "export", or "from" fields.', 39 | ) 40 | result.push({ 41 | importAllFrom: normalizeString(item.importAllFrom), 42 | as: normalizeString(item.as), 43 | }) 44 | } else if ('exportAllFrom' in item) { 45 | if ('importAllFrom' in item || 'import' in item || 'export' in item || 'from' in item) 46 | throw new TypeError( 47 | 'ExportAllBinding cannot have "importAllFrom", "import", "export", or "from" fields.', 48 | ) 49 | result.push({ 50 | exportAllFrom: normalizeString(item.exportAllFrom), 51 | as: normalizeStringOrUndefined(item.as), 52 | }) 53 | } else if ('import' in item) { 54 | if (!('from' in item)) throw new TypeError('ImportBinding must have a "from" field.') 55 | if ('exportAllFrom' in item || 'importAllFrom' in item || 'export' in item) 56 | throw new TypeError('ImportBinding cannot have "exportAllFrom", "importAllFrom", or "export" fields.') 57 | result.push({ 58 | import: normalizeString(item.import), 59 | from: normalizeString(item.from), 60 | as: normalizeStringOrUndefined(item.as), 61 | }) 62 | } else if ('export' in item) { 63 | if ('exportAllFrom' in item || 'importAllFrom' in item || 'import' in item) 64 | throw new TypeError('ExportBinding cannot have "exportAllFrom", "importAllFrom", or "import" fields.') 65 | result.push({ 66 | export: normalizeString(item.export), 67 | from: normalizeStringOrUndefined(item.from), 68 | as: normalizeStringOrUndefined(item.as), 69 | }) 70 | } else { 71 | throw new TypeError( 72 | 'binding must be one of ImportBinding, ExportBinding, ImportAllBinding or ExportAllBinding.', 73 | ) 74 | } 75 | } 76 | Object.freeze(result) 77 | 78 | const LexicallyDeclaredNames = new Set() 79 | const ExportedNames = new Set() 80 | for (const item of result) { 81 | if (isImportBinding(item) || isImportAllBinding(item)) { 82 | const bind = item.as || (item as ImportBinding).import 83 | if (LexicallyDeclaredNames.has(bind)) throw new TypeError(`Duplicate lexical binding for "${bind}"`) 84 | LexicallyDeclaredNames.add(bind) 85 | } else if (isExportBinding(item)) { 86 | const bind = item.as || item.export 87 | if (ExportedNames.has(bind)) throw new TypeError(`Duplicate export binding for "${bind}"`) 88 | ExportedNames.add(bind) 89 | } else { 90 | const _: ExportAllBinding = item 91 | } 92 | } 93 | return result 94 | } 95 | 96 | /** @internal */ 97 | export function normalizeBindingsToSpecRecord(bindings: Binding[] | undefined) { 98 | // bindings = normalizeBindings(bindings) || [] 99 | bindings ??= [] 100 | const requestedModules: string[] = [] 101 | for (const binding of bindings) { 102 | if (isImportBinding(binding)) requestedModules.push(binding.from) 103 | else if (isImportAllBinding(binding)) requestedModules.push(binding.importAllFrom) 104 | else if (isExportBinding(binding)) { 105 | if (hasFromField(binding)) requestedModules.push(binding.from) 106 | } else if (isExportAllBinding(binding)) requestedModules.push(binding.exportAllFrom) 107 | else unreachable(binding) 108 | } 109 | const importEntries: ModuleImportEntry[] = [] 110 | for (const binding of bindings) { 111 | if (isImportBinding(binding)) { 112 | importEntries.push({ 113 | ImportName: binding.import, 114 | LocalName: binding.as ?? binding.import, 115 | ModuleRequest: binding.from, 116 | }) 117 | } else if (isImportAllBinding(binding)) { 118 | importEntries.push({ 119 | ImportName: namespace, 120 | LocalName: binding.as, 121 | ModuleRequest: binding.importAllFrom, 122 | }) 123 | } 124 | } 125 | const indirectExportEntries: ModuleExportEntry[] = [] 126 | const localExportEntries: ModuleExportEntry[] = [] 127 | const starExportEntries: ModuleExportEntry[] = [] 128 | 129 | for (const binding of bindings) { 130 | // step 10.a: if ee.ModuleRequest is null, then append ee to localExportEntries 131 | // export { ... } 132 | if (isExportBinding(binding) && !hasFromField(binding)) { 133 | localExportEntries.push({ 134 | ExportName: binding.as ?? binding.export, 135 | ImportName: null, 136 | ModuleRequest: null, 137 | }) 138 | } 139 | // step 10.b: else if ee.ImportName is all-but-default, then append ee to starExportEntries 140 | // export * from '...' 141 | else if (isExportAllBinding(binding) && binding.as === undefined) { 142 | starExportEntries.push({ 143 | // LocalName: null, 144 | ExportName: null, 145 | ImportName: allButDefault, 146 | ModuleRequest: binding.exportAllFrom, 147 | }) 148 | } 149 | // step 10.c: else, append ee to indirectExportEntries 150 | // export * as T from '...' 151 | // export { T } from '...' 152 | else { 153 | if (isExportAllBinding(binding)) { 154 | if (!(binding.as !== undefined)) assertFailed() 155 | indirectExportEntries.push({ 156 | ExportName: binding.as, 157 | ImportName: all, 158 | ModuleRequest: binding.exportAllFrom, 159 | }) 160 | } else if (isExportBinding(binding)) { 161 | indirectExportEntries.push({ 162 | ExportName: binding.as ?? binding.export, 163 | ImportName: binding.export, 164 | ModuleRequest: binding.from, 165 | }) 166 | } 167 | } 168 | } 169 | 170 | return { 171 | requestedModules: [...new Set(requestedModules)], 172 | importEntries, 173 | indirectExportEntries, 174 | localExportEntries, 175 | starExportEntries, 176 | } 177 | } 178 | 179 | function normalizeString(x: any) { 180 | return `${x}` 181 | } 182 | function normalizeStringOrUndefined(x: any) { 183 | return x === undefined ? undefined : `${x}` 184 | } 185 | -------------------------------------------------------------------------------- /packages/compartment/src/utils/shapeCheck.ts: -------------------------------------------------------------------------------- 1 | import type { Binding, ImportBinding, ExportBinding, ImportAllBinding, ExportAllBinding } from '../types.js' 2 | 3 | /** @internal */ 4 | export function isImportBinding(binding: Binding): binding is ImportBinding { 5 | return 'import' in binding 6 | } 7 | 8 | /** @internal */ 9 | export function isExportBinding(binding: Binding): binding is ExportBinding { 10 | return 'export' in binding 11 | } 12 | 13 | /** @internal */ 14 | export function isReExportBinding(binding: Binding): binding is ExportBinding & { from: string } { 15 | return 'export' in binding && typeof binding.from === 'string' 16 | } 17 | 18 | /** @internal */ 19 | export function isImportAllBinding(binding: Binding): binding is ImportAllBinding { 20 | return 'importAllFrom' in binding 21 | } 22 | 23 | /** @internal */ 24 | export function isExportAllBinding(binding: Binding): binding is ExportAllBinding { 25 | return 'exportAllFrom' in binding 26 | } 27 | 28 | /** @internal */ 29 | export function hasFromField(binding: T): binding is T & { from: string } { 30 | return 'from' in binding && typeof binding.from === 'string' 31 | } 32 | -------------------------------------------------------------------------------- /packages/compartment/src/utils/spec.ts: -------------------------------------------------------------------------------- 1 | /** @internal */ 2 | export const empty = /*#__PURE__*/ Symbol('empty') 3 | /** @internal */ 4 | export type empty = typeof empty 5 | /** @internal */ 6 | export const namespace = /*#__PURE__*/ Symbol('namespace') 7 | /** @internal */ 8 | export const ambiguous = /*#__PURE__*/ Symbol('ambiguous') 9 | /** @internal */ 10 | export const all = /*#__PURE__*/ Symbol('all') 11 | /** @internal */ 12 | export const allButDefault = /*#__PURE__*/ Symbol('all-but-default') 13 | 14 | /** @internal */ 15 | export interface PromiseCapability { 16 | readonly Promise: Promise 17 | readonly Reject: (reason: unknown) => void 18 | readonly Resolve: (val: T) => void 19 | Status: 20 | | { Type: 'Pending'; Promise: Promise } 21 | | { Type: 'Fulfilled'; Value: T } 22 | | { Type: 'Rejected'; Reason: unknown } 23 | } 24 | /** @internal */ 25 | export function PromiseCapability(): PromiseCapability { 26 | let ok: any, err: any 27 | const promise = new Promise((a, b) => { 28 | ok = (val: T) => { 29 | capability.Status = { Type: 'Fulfilled', Value: val } 30 | a(val) 31 | } 32 | err = (error: unknown) => { 33 | capability.Status = { Type: 'Rejected', Reason: error } 34 | b(error) 35 | } 36 | }) 37 | const capability: PromiseCapability = { 38 | Promise: promise, 39 | Resolve: ok!, 40 | Reject: err!, 41 | Status: { Type: 'Pending', Promise: promise }, 42 | } 43 | return capability 44 | } 45 | /** @internal */ 46 | export interface ModuleImportEntry { 47 | ModuleRequest: string 48 | ImportName: string | typeof namespace 49 | LocalName: string 50 | } 51 | /** @internal */ 52 | export interface ModuleExportEntry { 53 | ExportName: string | null 54 | ModuleRequest: string | null 55 | ImportName: string | typeof all | typeof allButDefault | null 56 | // LocalName: string | null 57 | } 58 | /** @internal */ 59 | export interface NormalCompletion { 60 | Type: 'normal' 61 | Value: T 62 | } 63 | /** @internal */ 64 | export interface ThrowCompletion { 65 | Type: 'throw' 66 | Value: unknown 67 | } 68 | /** @internal */ 69 | export type Completion = NormalCompletion | ThrowCompletion 70 | /** @internal */ 71 | export function NormalCompletion(value: T): NormalCompletion { 72 | return { 73 | Type: 'normal', 74 | Value: value, 75 | } 76 | } 77 | /** @internal */ 78 | export function ThrowCompletion(error: unknown): ThrowCompletion { 79 | return { 80 | Type: 'throw', 81 | Value: error, 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/compartment/test/commonjs/direct-use.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from 'vitest' 2 | import type { RequireHook } from '../../src/commonjs/runtime.js' 3 | import { CommonJSModule, commonjsRequires, imports } from '../../src/index.js' 4 | import { convertCommonJS } from '../../../static-module-record-swc/commonjs.mjs' 5 | 6 | const src = new ModuleSource( 7 | convertCommonJS(` 8 | const React = require('react') 9 | module.exports.Component = function Component() {} 10 | module.exports.use = React.use`), 11 | ) 12 | const react = new ModuleSource(convertCommonJS(`module.exports = require('./dist/react-development.js')`, ['use'])) 13 | const react2 = new ModuleSource(convertCommonJS(`module.exports.use = function use() {}`)) 14 | 15 | it('can use as a normal CommonJS runtime', async () => { 16 | const map: Record = {} 17 | const requireHook: RequireHook = (path) => map[path] ?? null 18 | map['react'] = new CommonJSModule('react', react, { globalThis, requireHook }) 19 | map['src'] = new CommonJSModule('./src/index.js', src, { globalThis, requireHook }) 20 | map['./dist/react-development.js'] = new CommonJSModule('react/dev', react2, { globalThis, requireHook }) 21 | 22 | const srcInstance = commonjsRequires(map['src']) 23 | const reactInstance = commonjsRequires(map['./dist/react-development.js']) 24 | expect(srcInstance.Component).toBeTypeOf('function') 25 | expect(srcInstance.use).toBe(reactInstance.use) 26 | 27 | // import as ES module 28 | const ns = await imports(map['src'].asESModule) 29 | const ns2 = await imports(map['react'].asESModule) 30 | expect(ns.default.Component).toBe(srcInstance.Component) 31 | 32 | const f = () => {} 33 | reactInstance.use = f 34 | const f2 = () => {} 35 | reactInstance.use = f2 36 | expect(ns2.use).toBe(f2) 37 | }) 38 | -------------------------------------------------------------------------------- /packages/compartment/test/evaluator.ts: -------------------------------------------------------------------------------- 1 | import { Evaluators, imports } from '../src/index.js' 2 | import { expect, it } from 'vitest' 3 | 4 | it('can virtualize the global object', async () => { 5 | const global = { a: undefined } 6 | const { Module } = new Evaluators({ 7 | globalThis: global, 8 | importHook: async () => null, 9 | importMeta: {}, 10 | }) 11 | const src = new ModuleSource(`a = 1`) 12 | await imports(new Module(src, {})) 13 | expect(global.a).toBe(1) 14 | }) 15 | 16 | it('can inherit the import.meta', async () => { 17 | const { Module } = new Evaluators({ 18 | importHook: async () => null, 19 | importMeta: { url: 'hello' }, 20 | }) 21 | const src = new ModuleSource(`export const url = import.meta.url`) 22 | const { url } = await imports(new Module(src, {})) 23 | expect(url).toBe('hello') 24 | }) 25 | 26 | it('returns the given globalThis', () => { 27 | const localThis = {} 28 | const { globalThis } = new Evaluators({ globalThis: localThis }) 29 | expect(globalThis).toBe(localThis) 30 | }) 31 | 32 | it('checks the argument', () => { 33 | expect(() => new Evaluators({ globalThis: 1 as any })).toThrow(TypeError) 34 | expect(() => new Evaluators({ importHook: 1 as any })).toThrow(TypeError) 35 | expect(() => new Evaluators({ importMeta: 1 as any })).toThrow(TypeError) 36 | }) 37 | 38 | it('can inherit nested Evaluators', async () => { 39 | const levelA = {} 40 | 41 | const LevelA = new Evaluators({ globalThis: levelA }) 42 | const LevelB = new LevelA.Evaluators({}) 43 | 44 | let innerGlobalThis: any 45 | await imports( 46 | new LevelB.Module( 47 | { 48 | execute(environment, context) { 49 | innerGlobalThis = context.globalThis 50 | }, 51 | }, 52 | {}, 53 | ), 54 | ) 55 | expect(innerGlobalThis).toBe(levelA) 56 | }) 57 | -------------------------------------------------------------------------------- /packages/compartment/test/module-source.ts: -------------------------------------------------------------------------------- 1 | import { ModuleSource } from '../src/index.js' 2 | import { expect, it } from 'vitest' 3 | 4 | it('throws', () => { 5 | expect(() => new ModuleSource('')).toThrow(EvalError) 6 | expect(() => ModuleSource.prototype.bindings).toThrow(TypeError) 7 | }) 8 | -------------------------------------------------------------------------------- /packages/compartment/test/module/alias-import.ts: -------------------------------------------------------------------------------- 1 | import { Module, Evaluators, imports } from '../../src/index.js' 2 | import { it } from 'vitest' 3 | 4 | it('can handle default import correctly', async () => { 5 | const src1 = new ModuleSource(`export default function () {}`) 6 | const src2 = new ModuleSource(` 7 | import x from 'src1' 8 | x() 9 | `) 10 | const { Module } = new Evaluators({ 11 | importHook: (spec) => (spec === 'src1' ? mod1 : null), 12 | globalThis: {}, 13 | }) 14 | const mod1: Module = new Module(src1, {}) 15 | const mod2: Module = new Module(src2, {}) 16 | 17 | await imports(mod2) 18 | }) 19 | 20 | it('can handle alias import correctly', async () => { 21 | const src1 = new ModuleSource(`export function x() {}`) 22 | const src2 = new ModuleSource(` 23 | import { x as y } from 'src1' 24 | y() 25 | `) 26 | const { Module } = new Evaluators({ 27 | importHook: (spec) => (spec === 'src1' ? mod1 : null), 28 | globalThis: {}, 29 | }) 30 | const mod1: Module = new Module(src1, {}) 31 | const mod2: Module = new Module(src2, {}) 32 | 33 | await imports(mod2) 34 | }) 35 | -------------------------------------------------------------------------------- /packages/compartment/test/module/dynamic-import.ts: -------------------------------------------------------------------------------- 1 | import { type Module, Evaluators, imports } from '../../src/index.js' 2 | import { expect, it } from 'vitest' 3 | 4 | it('can use dynamic import', async () => { 5 | const src1 = new ModuleSource(`export default await import('src2')`) 6 | const src2 = new ModuleSource(`export const value = 1`) 7 | const { Module } = new Evaluators({ 8 | importHook: (spec) => (spec === 'src2' ? mod2 : null), 9 | globalThis: {}, 10 | }) 11 | const mod1 = new Module(src1, {}) 12 | const mod2: Module = new Module(src2, {}) 13 | 14 | const { default: a } = await imports(mod1) 15 | expect(a.value).toBe(1) 16 | }) 17 | -------------------------------------------------------------------------------- /packages/compartment/test/module/re-export.ts: -------------------------------------------------------------------------------- 1 | import { Module, Evaluators, imports } from '../../src/index.js' 2 | import { expect, it } from 'vitest' 3 | 4 | it('can handle `export * from` correctly', async () => { 5 | const src1 = new ModuleSource(`export function echo() {}`) 6 | const src2 = new ModuleSource(`export * from 'src1'`) 7 | const { Module } = new Evaluators({ 8 | importHook: (spec) => (spec === 'src1' ? mod1 : null), 9 | globalThis: { 10 | sleep: (time: number) => new Promise((resolve) => setTimeout(resolve, time)), 11 | }, 12 | }) 13 | const mod1: Module = new Module(src1, {}) 14 | const mod2: Module = new Module(src2, {}) 15 | 16 | const mod2Namespace = await imports(mod2) 17 | expect(mod2Namespace.echo).toBeTypeOf('function') 18 | }) 19 | 20 | it('can handle `export * as T` all correctly', async () => { 21 | const src1 = new ModuleSource(`export function echo() {}`) 22 | const src2 = new ModuleSource(`export * as T from 'src1'`) 23 | const { Module } = new Evaluators({ 24 | importHook: (spec) => (spec === 'src1' ? mod1 : null), 25 | globalThis: { 26 | sleep: (time: number) => new Promise((resolve) => setTimeout(resolve, time)), 27 | }, 28 | }) 29 | const mod1: Module = new Module(src1, {}) 30 | const mod2: Module = new Module(src2, {}) 31 | 32 | const { T } = await imports(mod2) 33 | expect(T.echo).toBeTypeOf('function') 34 | }) 35 | -------------------------------------------------------------------------------- /packages/compartment/test/module/self-import.ts: -------------------------------------------------------------------------------- 1 | import { Module, Evaluators, imports } from '../../src/index.js' 2 | import { expect, it } from 'vitest' 3 | 4 | it('can import itself', async () => { 5 | const src1 = new ModuleSource(` 6 | // @ts-ignore 7 | import * as self from "self" 8 | // @ts-expect-error 9 | export { self } 10 | `) 11 | const { Module } = new Evaluators({ 12 | importHook: () => mod1, 13 | globalThis: {}, 14 | }) 15 | const mod1: Module = new Module(src1, {}) 16 | 17 | const { self } = await imports(mod1) 18 | expect(self).toBeTypeOf('object') 19 | }) 20 | -------------------------------------------------------------------------------- /packages/compartment/test/module/top-level-await.ts: -------------------------------------------------------------------------------- 1 | import { Module, Evaluators, imports } from '../../src/index.js' 2 | import { expect, it } from 'vitest' 3 | 4 | it('can initialize top-level-await module correctly', async () => { 5 | const src1 = new ModuleSource(` 6 | import { value } from "src2" 7 | export const a = value 8 | `) 9 | const src2 = new ModuleSource(` 10 | await sleep(5) 11 | export const value = 1 12 | `) 13 | const { Module } = new Evaluators({ 14 | importHook: (spec) => (spec === 'src2' ? mod2 : null), 15 | globalThis: { 16 | sleep: (time: number) => new Promise((resolve) => setTimeout(resolve, time)), 17 | }, 18 | }) 19 | const mod1 = new Module(src1, {}) 20 | const mod2: Module = new Module(src2, {}) 21 | 22 | const { a } = await imports(mod1) 23 | expect(a).toBe(1) 24 | }) 25 | -------------------------------------------------------------------------------- /packages/compartment/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./", 5 | "noEmit": true 6 | }, 7 | "include": ["./"], 8 | "references": [{ "path": "../tsconfig.json" }] 9 | } 10 | -------------------------------------------------------------------------------- /packages/compartment/test/utils/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Module { 2 | get source(): ModuleSource | null 3 | } 4 | interface ModuleConstructor { 5 | // TODO: virtual module record 6 | new (source: ModuleSource): Module 7 | } 8 | declare var Module: ModuleConstructor 9 | 10 | interface ModuleSource { 11 | // TODO: bindings properties 12 | } 13 | interface ModuleSourceConstructor { 14 | new (sourceText: string): ModuleSource 15 | } 16 | declare var ModuleSource: ModuleSourceConstructor 17 | -------------------------------------------------------------------------------- /packages/compartment/test/utils/setup.ts: -------------------------------------------------------------------------------- 1 | import { ModuleSource } from '../../../static-module-record-swc/eval.js' 2 | 3 | Reflect.set(globalThis, 'ModuleSource', ModuleSource) 4 | -------------------------------------------------------------------------------- /packages/compartment/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src/", 5 | "outDir": "./dist/", 6 | "tsBuildInfoFile": "./dist/.tsbuildinfo", 7 | "allowJs": true, 8 | "sourceMap": false 9 | }, 10 | "include": ["./src"], 11 | "references": [] 12 | } 13 | -------------------------------------------------------------------------------- /packages/intrinsic-snapshot/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @masknet/intrinsic-snapshot 2 | 3 | ## 0.1.2 4 | 5 | ### Patch Changes 6 | 7 | - 2230a09: fix crashes when there is no harden 8 | - 2230a09: change type of allow list 9 | - 2230a09: add createCloneKnowledge() 10 | - 2230a09: add a private field on the cloned object for debug 11 | - 2230a09: remove direct access to document.all 12 | 13 | ## 0.1.1 14 | 15 | ### Patch Changes 16 | 17 | - 03f16d9: add list of safe to share after lockdown 18 | - 2029916: improve dx for this uncurry intrinsic 19 | 20 | ## 0.1.0 21 | 22 | ### Minor Changes 23 | 24 | - f6fc76c: Initial implementation 25 | -------------------------------------------------------------------------------- /packages/intrinsic-snapshot/README.md: -------------------------------------------------------------------------------- 1 | # @masknet/intrinsic-snapshot 2 | 3 | This package is deprecated, please do not use it. It has known issues and DOES NOT safe to isolate. 4 | -------------------------------------------------------------------------------- /packages/intrinsic-snapshot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@masknet/intrinsic-snapshot", 3 | "version": "0.1.2", 4 | "type": "module", 5 | "main": "./dist/index.js", 6 | "sideEffects": false, 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/DimensionDev/aot-secure-ecmascript" 10 | }, 11 | "exports": { 12 | "default": "./dist/index.js" 13 | }, 14 | "scripts": { 15 | "run": "node ./dist/index.js" 16 | }, 17 | "dependencies": {}, 18 | "devDependencies": {}, 19 | "peerDependencies": {}, 20 | "files": [ 21 | "dist", 22 | "!dist/.tsbuildinfo", 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /packages/intrinsic-snapshot/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils/clone.js' 2 | export * from './utils/allowList.js' 3 | -------------------------------------------------------------------------------- /packages/intrinsic-snapshot/src/utils/allowList.ts: -------------------------------------------------------------------------------- 1 | /** Those intrinsic are not suggested to be cloned. */ 2 | export const undeniable: readonly object[] = /*#__PURE__*/ buildUndeniable() 3 | /** 4 | * The object and functions in the list are safe to share after lockdown() and no-eval. 5 | */ 6 | export const safeToShareAfterLockdownAndNoEval: readonly object[] = /*#__PURE__*/ buildSafeToShareList() 7 | 8 | function buildUndeniable() { 9 | const undeniable: readonly object[] = [ 10 | // 11 | Object.prototype, 12 | Function.prototype, 13 | Array.prototype, 14 | RegExp.prototype, 15 | ] 16 | Object.freeze(undeniable) 17 | return undeniable 18 | } 19 | 20 | // https://github.com/endojs/endo/blob/master/packages/ses/src/whitelist.js 21 | function buildSafeToShareList() { 22 | const intrinsic = new Set([ 23 | isFinite, 24 | isNaN, 25 | parseFloat, 26 | parseInt, 27 | decodeURI, 28 | decodeURIComponent, 29 | encodeURI, 30 | encodeURIComponent, 31 | 32 | Array, 33 | ArrayBuffer, 34 | BigInt, 35 | BigInt64Array, 36 | BigUint64Array, 37 | Boolean, 38 | DataView, 39 | EvalError, 40 | Float32Array, 41 | Float64Array, 42 | Int8Array, 43 | Int16Array, 44 | Int32Array, 45 | Map, 46 | Number, 47 | Object, 48 | Promise, 49 | Proxy, 50 | RangeError, 51 | ReferenceError, 52 | Set, 53 | String, 54 | Symbol, 55 | SyntaxError, 56 | TypeError, 57 | Uint8Array, 58 | Uint8ClampedArray, 59 | Uint16Array, 60 | Uint32Array, 61 | URIError, 62 | WeakMap, 63 | WeakSet, 64 | 65 | JSON, 66 | Reflect, 67 | 68 | typeof escape !== 'undefined' ? escape : undefined, 69 | typeof unescape !== 'undefined' ? unescape : undefined, 70 | 71 | // lockdown 72 | typeof harden !== 'undefined' ? harden : undefined, 73 | // HandledPromise 74 | 75 | Date, 76 | Error, 77 | RegExp, 78 | 79 | Math, 80 | 81 | eval, 82 | Function, 83 | // Compartment 84 | 85 | EvalError, 86 | RangeError, 87 | ReferenceError, 88 | SyntaxError, 89 | TypeError, 90 | URIError, 91 | 92 | // IteratorPrototype 93 | Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())), 94 | 95 | // AsyncIteratorPrototype 96 | Object.getPrototypeOf(Object.getPrototypeOf(async function* () {}.prototype)), 97 | 98 | // GeneratorFunction 99 | function* () {}.constructor, 100 | 101 | // GeneratorPrototype 102 | Object.getPrototypeOf(function* () {}).prototype, 103 | 104 | // AsyncGeneratorFunction 105 | async function* () {}.constructor, 106 | 107 | // AsyncGeneratorPrototype 108 | Object.getPrototypeOf(async function* () {}).prototype, 109 | 110 | // AsyncFunction 111 | async function () {}.constructor, 112 | ]) 113 | 114 | for (const items of [...intrinsic]) { 115 | items && intrinsic.add(Object.getPrototypeOf(items)) 116 | } 117 | 118 | return Object.freeze([...intrinsic].filter(Boolean)) 119 | } 120 | declare var harden: unknown 121 | -------------------------------------------------------------------------------- /packages/intrinsic-snapshot/src/utils/clone.ts: -------------------------------------------------------------------------------- 1 | import { ArrayMap, WeakMapGet, WeakMapHas, WeakMapSet } from './intrinsic.js' 2 | 3 | const originalFromCloned = new WeakMap() 4 | export function createCloneKnowledge(): CloneKnowledge { 5 | return { 6 | clonedFromOriginal: new WeakMap(), 7 | descriptorOverride: new WeakMap(), 8 | emptyObjectOverride: new WeakMap(), 9 | debugField: false, 10 | } 11 | } 12 | export interface CloneKnowledge { 13 | /** 14 | * Map from original => cloned 15 | * 16 | * This is not a global property, because there might be multiple isolated clone of the same object. 17 | */ 18 | clonedFromOriginal: WeakMap 19 | /** 20 | * A map of original => overriding property descriptors 21 | * 22 | * When clone(original), it will override the property descriptor onto the original one. 23 | */ 24 | descriptorOverride: WeakMap 25 | /** 26 | * A map of original => overring cloned object 27 | * 28 | * When clone(original), it will use the overriding cloned object instead of {} or []. 29 | * 30 | * This allows an exotic object to be cloned. 31 | */ 32 | emptyObjectOverride: WeakMap 33 | /** 34 | * Attach a #original field on the cloned object for debug usage. 35 | */ 36 | debugField?: boolean 37 | } 38 | 39 | /** 40 | * Clone a value. 41 | */ 42 | export function clone(o: any, knowledge: CloneKnowledge): any { 43 | // Note: this means o is document.all 44 | if (typeof o === 'undefined' && o !== undefined) return undefined 45 | 46 | // 1. If o is a primitive, return o. 47 | if (typeof o !== 'object' && typeof o !== 'function') return o 48 | if (o === null) return o 49 | 50 | // 2. Return previous clone result if there is one. 51 | if (WeakMapHas(knowledge.clonedFromOriginal, o)) return WeakMapGet(knowledge.clonedFromOriginal, o) 52 | 53 | // 3. If o is 54 | // a. an exotic object (specified in knowledge.emptyObjectOverride), return the overriding object. 55 | // b. a function, let newVal be a new forwarding function. 56 | // c. an Array, let newVal be a new Array. 57 | // d. an Object, let newVal be a new Object. 58 | const isFunction = typeof o === 'function' 59 | const c = 60 | WeakMapHas(knowledge.emptyObjectOverride, o) ? WeakMapGet(knowledge.emptyObjectOverride, o) 61 | : isArray(o) ? [] 62 | : isFunction ? forwardingFunction(o, knowledge) 63 | : {} 64 | // Attach a #original private field 65 | if (knowledge.debugField) new DebuggerPrivateField(c, o) 66 | 67 | // 4. Cache the clone result 68 | WeakMapSet(originalFromCloned, c, o) 69 | WeakMapSet(knowledge.clonedFromOriginal, o, c) 70 | 71 | // 5. Set newVal.[[Prototype]] to cloned val.[[Prototype]]. 72 | const proto = clone(getPrototypeOf(o), knowledge) 73 | setPrototypeOf(c, proto) 74 | 75 | // 6. Clone object descriptors 76 | const descriptors = getOwnPropertyDescriptors(o) 77 | const overrideDescriptors = WeakMapGet(knowledge.descriptorOverride, o) 78 | for (const key in descriptors) { 79 | if (isFunction && (key === 'arguments' || key === 'caller' || key === 'callee')) continue 80 | 81 | let descriptor = descriptors[key]! 82 | if (overrideDescriptors && hasOwn(overrideDescriptors, key)) { 83 | descriptor = overrideDescriptors[key]! 84 | descriptors[key] = descriptor 85 | } 86 | if (!descriptor) continue 87 | if (hasOwn(descriptor, 'get')) descriptor.get = clone(descriptor.get, knowledge) 88 | if (hasOwn(descriptor, 'set')) descriptor.set = clone(descriptor.set, knowledge) 89 | if (hasOwn(descriptor, 'value')) descriptor.value = clone(descriptor.value, knowledge) 90 | } 91 | defineProperties(c, descriptors) 92 | return c 93 | } 94 | 95 | // Let's keep the result of toString if possible 96 | try { 97 | const old = Function.prototype.toString 98 | const ff = () => 99 | function (this: any) { 100 | if (WeakMapHas(originalFromCloned, this)) return apply(old, WeakMapGet(originalFromCloned, this), arguments) 101 | return apply(old, this, arguments) 102 | } 103 | const f = ff() 104 | Function.prototype.toString = f 105 | } catch {} 106 | 107 | /** 108 | * Create a function that forward [[Call]] and [[Construct]] to the original function. 109 | * 110 | * It does not contain extra properties like [[Prototype]], .prototype, .length. 111 | * 112 | * @param oldF The function to be called 113 | * @param knowledge The clone knowledge 114 | * @returns Cloned function 115 | */ 116 | function forwardingFunction(oldF: Function, knowledge: CloneKnowledge): Function { 117 | const f = { 118 | [oldF.name]: function () { 119 | const args = ArrayMap(arguments, (value) => WeakMapGet(originalFromCloned, value) ?? value) 120 | 121 | try { 122 | if (new.target) { 123 | return clone( 124 | construct(oldF, args, (WeakMapGet(originalFromCloned, new.target) as Function) ?? new.target), 125 | knowledge, 126 | ) 127 | } 128 | return clone(apply(oldF, WeakMapGet(originalFromCloned, this) ?? this, args), knowledge) 129 | } catch (error) { 130 | throw clone(error, knowledge) 131 | } 132 | }, 133 | }[oldF.name] 134 | return f! 135 | } 136 | 137 | const { isArray } = Array 138 | const { apply, construct, getPrototypeOf, setPrototypeOf } = Reflect 139 | const { getOwnPropertyDescriptors, hasOwn, defineProperties } = Object 140 | 141 | function identity(value: object) { 142 | return value 143 | } 144 | class DebuggerPrivateField extends (identity as any) { 145 | constructor(value: object, originalValue: object) { 146 | super(value) 147 | this.#original = originalValue 148 | } 149 | #original: object 150 | } 151 | -------------------------------------------------------------------------------- /packages/intrinsic-snapshot/src/utils/intrinsic.ts: -------------------------------------------------------------------------------- 1 | const f = (f: Function) => Function.prototype.call.bind(f) 2 | export const WeakMapGet: (_this: WeakMap, key: T) => undefined | T = f(WeakMap.prototype.get) 3 | export const WeakMapSet: (_this: WeakMap, key: T, value: Q) => WeakMap = f( 4 | WeakMap.prototype.set, 5 | ) 6 | export const WeakMapHas: (_this: WeakMap, key: T) => boolean = f(WeakMap.prototype.has) 7 | export const ArrayMap: (_this: ArrayLike, callback: (value: T, index: number, array: T[]) => U) => U[] = f( 8 | Array.prototype.map, 9 | ) 10 | export const ArrayForEach: (_this: ArrayLike, callback: (value: T, index: number, array: T[]) => void) => void = 11 | f(Array.prototype.forEach) 12 | -------------------------------------------------------------------------------- /packages/intrinsic-snapshot/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src/", 5 | "outDir": "./dist/", 6 | "tsBuildInfoFile": "./dist/.tsbuildinfo", 7 | "allowJs": true, 8 | "lib": ["DOM", "ESNext"] 9 | }, 10 | "include": ["./src"], 11 | "references": [] 12 | } 13 | -------------------------------------------------------------------------------- /packages/membrane/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @masknet/membrane 2 | 3 | ## 0.2.0 4 | 5 | ### Minor Changes 6 | 7 | - 6dd3bed: update dependencies 8 | 9 | ## 0.1.6 10 | 11 | ### Patch Changes 12 | 13 | - 63dfba3: upgrade dependencies 14 | 15 | ## 0.1.5 16 | 17 | ### Patch Changes 18 | 19 | - 8d80a6a: feat: add devtools debugger entry point 20 | 21 | ## 0.1.4 22 | 23 | ### Patch Changes 24 | 25 | - 48e5022: update dependencies 26 | - 7a2c83c: update dependencies 27 | 28 | ## 0.1.3 29 | 30 | ### Patch Changes 31 | 32 | - 7b32451: fix blue connector run as ShadowRealm mode 33 | 34 | ## 0.1.2 35 | 36 | ### Patch Changes 37 | 38 | - 6cc8a56: improve dx by attach original target to the proxy 39 | 40 | ## 0.1.1 41 | 42 | ### Patch Changes 43 | 44 | - be8c572: add types field 45 | 46 | ## 0.1.0 47 | 48 | ### Minor Changes 49 | 50 | - 7657b62: first release 51 | -------------------------------------------------------------------------------- /packages/membrane/build.mjs: -------------------------------------------------------------------------------- 1 | import { createMembraneMarshall } from '@locker/near-membrane-base' 2 | import { writeFile } from 'node:fs/promises' 3 | import { transform } from '@swc/core' 4 | import { createRequire } from 'node:module' 5 | import { format } from 'prettier' 6 | 7 | const require = createRequire(import.meta.url) 8 | 9 | const { code } = await transform( 10 | `import { debugTargetBookkeeping, attachDebuggerTarget, proxyTargetToLazyPropertyDescriptorStateMap } from '' 11 | export default ${createMembraneMarshall 12 | .toString() 13 | .replace(`localEval(sourceText)`, `sourceText()`) 14 | .replace(/return pointer/, `debugTargetBookkeeping?.(pointer, originalTarget); return pointer`) 15 | .replace( 16 | `this.foreignTargetPointer = foreignTargetPointer`, 17 | `attachDebuggerTarget?.(proxy, foreignTargetPointer); this.foreignTargetPointer = foreignTargetPointer`, 18 | )} 19 | `, 20 | { 21 | jsc: { 22 | target: 'es2022', 23 | experimental: { 24 | plugins: [ 25 | [ 26 | require.resolve( 27 | '../static-module-record-swc/target/wasm32-unknown-unknown/release/swc_transformer_static_module_record.wasm', 28 | ), 29 | {}, 30 | ], 31 | ], 32 | }, 33 | }, 34 | }, 35 | ) 36 | await writeFile( 37 | new URL('./src/createMembraneMarshall.ts', import.meta.url), 38 | `// @ts-nocheck 39 | // This file is built from '@locker/near-membrane-base'.createMembraneMarshall. 40 | // DO NOT edit it manually. 41 | ` + 42 | (await format(code.replaceAll('_.undefined', 'undefined'), { 43 | trailingComma: 'all', 44 | printWidth: 120, 45 | semi: false, 46 | singleQuote: true, 47 | bracketSameLine: true, 48 | tabWidth: 4, 49 | parser: 'babel', 50 | })), 51 | ) 52 | -------------------------------------------------------------------------------- /packages/membrane/chrome-devtools.js: -------------------------------------------------------------------------------- 1 | import '@locker/near-membrane-dom/custom-devtools-formatter.js' 2 | -------------------------------------------------------------------------------- /packages/membrane/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@masknet/membrane", 3 | "version": "0.2.0", 4 | "type": "module", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/DimensionDev/aot-secure-ecmascript" 10 | }, 11 | "exports": { 12 | "./chrome-devtools": "./chrome-devtools.js", 13 | ".": { 14 | "types": "./dist/index.d.ts", 15 | "default": "./dist/index.js" 16 | } 17 | }, 18 | "dependencies": { 19 | "@locker/near-membrane-base": "0.13.7", 20 | "@locker/near-membrane-dom": "0.13.7" 21 | }, 22 | "devDependencies": { 23 | "@swc/core": "1.4.0" 24 | }, 25 | "scripts": { 26 | "update": "node ./build.mjs" 27 | }, 28 | "files": [ 29 | "dist", 30 | "!dist/.tsbuildinfo", 31 | "src", 32 | "chrome-devtools.js" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /packages/membrane/src/evaluator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assignFilteredGlobalDescriptorsFromPropertyDescriptorMap, 3 | getFilteredGlobalOwnKeys, 4 | linkIntrinsics, 5 | VirtualEnvironment, 6 | type DistortionCallback, 7 | type Instrumentation, 8 | } from '@locker/near-membrane-base' 9 | import { createConnector } from './membrane.js' 10 | 11 | export interface EnvironmentOptions { 12 | distortionCallback?: DistortionCallback | undefined 13 | endowments?: PropertyDescriptorMap | undefined 14 | globalObjectShape?: object | undefined 15 | instrumentation?: Instrumentation | undefined 16 | debugPrivateFieldAttach?: boolean 17 | } 18 | 19 | const ObjectCtor = Object 20 | const { assign: ObjectAssign } = ObjectCtor 21 | const TypeErrorCtor = TypeError 22 | 23 | let defaultGlobalOwnKeys: PropertyKey[] | null = null 24 | 25 | export interface MembraneInstance { 26 | execute(func: () => T): T 27 | virtualEnvironment: VirtualEnvironment 28 | } 29 | export default function createVirtualEnvironment( 30 | globalObject: typeof globalThis, 31 | redGlobalObject: typeof globalThis, 32 | options?: EnvironmentOptions, 33 | ): MembraneInstance { 34 | if (typeof globalObject !== 'object' || globalObject === null) { 35 | throw new TypeErrorCtor('Missing global object virtualization target.') 36 | } 37 | const { 38 | distortionCallback, 39 | endowments, 40 | globalObjectShape, 41 | instrumentation, 42 | debugPrivateFieldAttach = true, 43 | } = ObjectAssign({ __proto__: null }, options) as EnvironmentOptions 44 | const blueConnector = createConnector(globalObject, true, debugPrivateFieldAttach) 45 | const env = new VirtualEnvironment({ 46 | blueConnector, 47 | distortionCallback: distortionCallback!, 48 | instrumentation: instrumentation!, 49 | redConnector: createConnector(redGlobalObject, false, debugPrivateFieldAttach), 50 | }) 51 | linkIntrinsics(env, globalObject) 52 | 53 | const shouldUseDefaultGlobalOwnKeys = typeof globalObjectShape !== 'object' || globalObjectShape === null 54 | if (shouldUseDefaultGlobalOwnKeys && defaultGlobalOwnKeys === null) { 55 | defaultGlobalOwnKeys = getFilteredGlobalOwnKeys(redGlobalObject) 56 | } 57 | 58 | env.lazyRemapProperties( 59 | globalObject, 60 | shouldUseDefaultGlobalOwnKeys ? defaultGlobalOwnKeys! : getFilteredGlobalOwnKeys(globalObjectShape), 61 | ) 62 | 63 | if (endowments) { 64 | const filteredEndowments = {} 65 | assignFilteredGlobalDescriptorsFromPropertyDescriptorMap(filteredEndowments, endowments) 66 | env.remapProperties(globalObject, filteredEndowments) 67 | } 68 | // we patched it's evaluate method to receive a method instead of a string. 69 | return { 70 | execute(f) { 71 | return env.evaluate(f as any) 72 | }, 73 | virtualEnvironment: env, 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/membrane/src/index.ts: -------------------------------------------------------------------------------- 1 | export { type EnvironmentOptions, type MembraneInstance, default } from './evaluator.js' 2 | export { 3 | type Connector, 4 | type DistortionCallback, 5 | type Instrumentation, 6 | type VirtualEnvironment, 7 | } from '@locker/near-membrane-base' 8 | -------------------------------------------------------------------------------- /packages/membrane/src/membrane.ts: -------------------------------------------------------------------------------- 1 | import lib from './createMembraneMarshall.js' 2 | import type { Connector } from '@locker/near-membrane-base' 3 | 4 | const connectorMap = /*#__PURE__*/ new WeakMap() 5 | const debuggerMap = /*#__PURE__*/ new WeakMap() 6 | export function createConnector(globalThis: object, isMainIsolate: boolean, allowDebug = false): Connector { 7 | if (connectorMap.has(globalThis)) return connectorMap.get(globalThis)! 8 | let f: Function 9 | lib.execute( 10 | { 11 | set default(value: Function) { 12 | f = value 13 | }, 14 | debugTargetBookkeeping: allowDebug ? debuggerMap.set.bind(debuggerMap) : undefined, 15 | attachDebuggerTarget: allowDebug ? TransferablePointerTarget.attachDebuggerTarget : undefined, 16 | proxyTargetToLazyPropertyDescriptorStateMap: isMainIsolate ? new WeakMap() : undefined, 17 | }, 18 | { globalThis }, 19 | ) 20 | const connector = f!(isMainIsolate ? globalThis : undefined) 21 | connectorMap.set(globalThis, connector) 22 | return connector 23 | } 24 | 25 | // @ts-ignore 26 | class TransferablePointerTarget extends function (f) { 27 | return f 28 | } { 29 | constructor(obj: object) { 30 | super(obj) 31 | } 32 | // Note: This field MUST NOT be read. otherwise it might break the isolation. 33 | #Target: unknown 34 | static attachDebuggerTarget(key: TransferablePointerTarget, target: object) { 35 | if (!debuggerMap.has(target)) return 36 | // Note: although attach a private field might fail (https://github.com/tc39/ecma262/pull/2807) 37 | // but key should always be a proxy, therefore 2807 does not apply. 38 | // It's also impossible to call on the same target twice because we only call it for newly created Proxy. 39 | new TransferablePointerTarget(key) 40 | key.#Target = debuggerMap.get(target) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/membrane/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src/", 5 | "outDir": "./dist/", 6 | "tsBuildInfoFile": "./dist/.tsbuildinfo" 7 | }, 8 | "include": ["./src"], 9 | "references": [] 10 | } 11 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/.cargo/config: -------------------------------------------------------------------------------- 1 | # These command aliases are not final, may change 2 | [alias] 3 | # Alias to build actual plugin binary for the specified target. 4 | prepublish = "build --target wasm32-unknown-unknown" 5 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | ^target/ 3 | target 4 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @masknet/static-module-record-swc 2 | 3 | ## 0.6.0 4 | 5 | ### Minor Changes 6 | 7 | - 6dd3bed: update dependencies 8 | 9 | ## 0.5.0 10 | 11 | ### Minor Changes 12 | 13 | - da873b1: add commonjs support 14 | 15 | ### Patch Changes 16 | 17 | - be3ac75: upgrade to swc latest 18 | - 63dfba3: upgrade dependencies 19 | 20 | ## 0.4.1 21 | 22 | ### Patch Changes 23 | 24 | - 04e8825: update swc 25 | 26 | ## 0.4.0 27 | 28 | ### Minor Changes 29 | 30 | - d52bc2e: add new transform options: eval 31 | 32 | ### Patch Changes 33 | 34 | - d52bc2e: upgrade deps 35 | 36 | ## 0.3.5 37 | 38 | ### Patch Changes 39 | 40 | - 48e5022: update dependencies 41 | - 54dd8c4: follow swc version 42 | 43 | ## 0.3.4 44 | 45 | ### Patch Changes 46 | 47 | - 3e77997: follow swc abi change 48 | 49 | ## 0.3.3 50 | 51 | ### Patch Changes 52 | 53 | - 6339ec3: update dependencies 54 | 55 | ## 0.3.2 56 | 57 | ### Patch Changes 58 | 59 | - 7da5cdb: move globalThis out of environment record 60 | 61 | ## 0.3.1 62 | 63 | ### Patch Changes 64 | 65 | - 4f10e99: fix assignment pattern not handled 66 | - 4f10e99: fix import-then-export not recognized 67 | - 4f10e99: remove unnecessary (0, \_.access) wrapper 68 | - 8eb9860: rename initialize in virtual module record to execute 69 | - 8bd2ec5: "this" of an imported value should be undefined. 70 | 71 | ## 0.3.0 72 | 73 | ### Minor Changes 74 | 75 | - c0b8659: Implement new binding format in virtual module 76 | 77 | ### Patch Changes 78 | 79 | - 1657c4f: Add isAsync hint 80 | 81 | ## 0.2.2 82 | 83 | ### Patch Changes 84 | 85 | - 6791968: Fix object literal shorthand property not handled 86 | - a05de8c: Remove unused tracing statements 87 | - e0c998f: Fix arguments always transformed as unresolved 88 | 89 | ## 0.2.1 90 | 91 | ### Patch Changes 92 | 93 | - d1f8d16: Add a workaround for imports without binding cannot be imported after converted into ThirdPartyStaticModuleRecord 94 | 95 | ## 0.2.0 96 | 97 | ### Minor Changes 98 | 99 | - 0398516: [Spec] Merge the initialize 2nd and 3rd parameter into a context object 100 | 101 | ## 0.1.0 102 | 103 | ### Major Changes 104 | 105 | - 9f33549: First release 🎉 106 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "swc-transformer-static-module-record" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib"] 8 | 9 | [dependencies] 10 | serde = "1" 11 | serde_json = "1" 12 | swc_core = { version = "0.85.8", features = [ 13 | "ecma_ast", 14 | "ecma_utils", 15 | "ecma_visit", 16 | "ecma_plugin_transform", 17 | "swc_atoms", 18 | "swc_plugin", 19 | "testing_transform", 20 | ] } 21 | testing = "0.35.0" 22 | tracing = { version = "0.1.39", features = ["release_max_level_off"] } 23 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/README.md: -------------------------------------------------------------------------------- 1 | # @masknet/static-module-record-swc 2 | 3 | > WARNING: This package currently does not follow the [Semantic Versioning](https://semver.org/) because the original standard is still developing. Minor version might include breaking changes! 4 | 5 | A @swc/core plugin to convert an ES Module into a [VirtualModuleSource](https://github.com/tc39/proposal-compartments/blob/master/2-virtual-module-source.md). 6 | 7 | ## Usage 8 | 9 | Add it in @swc/core config 10 | 11 | ```js 12 | const config = { 13 | jsc: { 14 | experimental: { 15 | plugins: [ 16 | [ 17 | '@masknet/static-module-record-swc', 18 | // see "config" section below 19 | {}, 20 | ], 21 | ], 22 | }, 23 | }, 24 | } 25 | ``` 26 | 27 | ## Configs 28 | 29 | ### `config.template` 30 | 31 | Specify the emit template 32 | 33 | #### `config.template.type: "export-default"` 34 | 35 | This is the default option. 36 | 37 | ```json 38 | [ 39 | "@masknet/static-module-record-swc", 40 | { 41 | "template": { 42 | "type": "export-default" 43 | } 44 | } 45 | ] 46 | ``` 47 | 48 | It will convert code 49 | 50 | ```js 51 | import { writeFile } from 'node:fs/promises' 52 | 53 | export const url = new URL('./here.txt', import.meta.url) 54 | await writeFile(url, 'Hello World') 55 | ``` 56 | 57 | into [this file](./tests/snapshot/example.js) 58 | 59 | #### `config.template.type: "eval"` 60 | 61 | ```json 62 | [ 63 | "@masknet/static-module-record-swc", 64 | { 65 | "template": { 66 | "type": "eval" 67 | } 68 | } 69 | ] 70 | ``` 71 | 72 | It will convert code 73 | 74 | ```js 75 | import { writeFile } from 'node:fs/promises' 76 | 77 | export const url = new URL('./here.txt', import.meta.url) 78 | await writeFile(url, 'Hello World') 79 | ``` 80 | 81 | into [this file](./tests/snapshot/example-eval.js) 82 | 83 | #### `config.template.type: "callback"` 84 | 85 | ```json 86 | [ 87 | "@masknet/static-module-record-swc", 88 | { "template": { "type": "callback", "callback": "__register", "firstArg": "/index.js" } } 89 | ] 90 | ``` 91 | 92 | It will convert code 93 | 94 | ```js 95 | import { writeFile } from 'node:fs/promises' 96 | 97 | export const url = new URL('./here.txt', import.meta.url) 98 | await writeFile(url, 'Hello World') 99 | ``` 100 | 101 | into [this file](./tests/snapshot/example-callback.js) 102 | 103 | #### `config.template.type: "callback-cwd"` 104 | 105 | It is similar to `callback` but it will try to infer the file name to URI as the first argument of the callback. 106 | 107 | Let's take `/home/jack/aot-ses/packages/static-module-record-swc/tests/fixtures/example-callback-infer.js` as an example. 108 | 109 | ```json 110 | [ 111 | "@masknet/static-module-record-swc", 112 | { 113 | "template": { 114 | "type": "callback-cwd", 115 | "callback": "__register", 116 | "cwd": "/home/jack/aot-ses/packages/static-module-record-swc/" 117 | } 118 | } 119 | ] 120 | ``` 121 | 122 | Warning: due to the limitation of the swc plugin system, we need a `cwd` to resolve the file name into URI. This `cwd` must contain all input files, otherwise, it will panic. 123 | 124 | ```js 125 | import { writeFile } from 'node:fs/promises' 126 | 127 | export const url = new URL('./here.txt', import.meta.url) 128 | await writeFile(url, 'Hello World') 129 | ``` 130 | 131 | into [this file](./tests/snapshot/example-callback-infer.js) 132 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/commonjs.d.mts: -------------------------------------------------------------------------------- 1 | export function convertCommonJS(code: string, reExports?: string[]): string 2 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/commonjs.mjs: -------------------------------------------------------------------------------- 1 | import { init, parse } from 'cjs-module-lexer' 2 | await init() 3 | 4 | /** 5 | * @param {string} code 6 | * @param {string[]} reexports 7 | */ 8 | export function convertCommonJS(code, reexports) { 9 | const ident = { count: 0 } 10 | const next = { 11 | get ident() { 12 | return findIdent(code, ident) 13 | }, 14 | } 15 | const [lexicals, skipRender] = [next.ident, next.ident] 16 | const defaultExport = next.ident 17 | const exports = [...new Set(parse(code).exports.concat(reexports))].map((name) => [name, next.ident]) 18 | 19 | let str = [''] 20 | // var [a, b] = import.meta.commonjs(f, obj) 21 | str.push(`var [${lexicals}, ${skipRender}] = import.meta.commonjs(`) 22 | str.push(`x => ${defaultExport} = x, `) 23 | str.push(`{\n`) 24 | for (const [name, ident] of exports) { 25 | str.push(` [${JSON.stringify(name)}]: x => ${ident} = x,\n`) 26 | } 27 | str.push(`});\n`) 28 | 29 | // var a, b, c, d, ... 30 | str.push(`var ${[defaultExport, ...exports.map((x) => x[1])].join(', ')};\n`) 31 | 32 | // export { a as originalName, b as originalName2, ... } 33 | str.push(`export { `) 34 | str.push(`${defaultExport} as default, `) 35 | for (const [name, ident] of exports) { 36 | str.push(`${ident} as ${JSON.stringify(name)}, `) 37 | } 38 | str.push(`};\n`) 39 | 40 | // commonjs wrapper 41 | str.push(`;(function ({ exports, require, module, __filename, __dirname }) {\n`) 42 | str.push(`if (${skipRender}) return;\n`) 43 | str.push(code) 44 | str.push(`\n}.call(${lexicals}.exports, ${lexicals}));`) 45 | 46 | return str.join('') 47 | } 48 | 49 | /** 50 | * @param {string} code 51 | * @param {{count: number}} index 52 | */ 53 | function findIdent(code, index) { 54 | while (code.includes(`_` + index.count)) index.count++ 55 | return `_` + index.count++ 56 | } 57 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/compile.js: -------------------------------------------------------------------------------- 1 | import { transform } from './eval.js' 2 | import { writeFile } from 'node:fs/promises' 3 | 4 | await writeFile( 5 | './out.js', 6 | transform(` 7 | for([x] of [[1],[2, 3], [4,5, 6]]); 8 | var x; export {x} 9 | `), 10 | ) 11 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/eval.d.ts: -------------------------------------------------------------------------------- 1 | export function transform(sourceText: string, sourceMap?: string | boolean): string 2 | export class ModuleSource { 3 | constructor(sourceText: string) 4 | } 5 | export class ModuleSourceWithSourceMap { 6 | constructor(sourceText: string) 7 | } 8 | export function setupTrustedTypes(policy?: TrustedTypePolicy): void 9 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/eval.js: -------------------------------------------------------------------------------- 1 | import { transformSync } from '@swc/core' 2 | import { fileURLToPath } from 'node:url' 3 | 4 | /** 5 | * @param {string} sourceText 6 | * @param {string | boolean} sourceMap 7 | */ 8 | export function transform(sourceText, sourceMap = false) { 9 | if (typeof sourceText !== 'string') throw new TypeError() 10 | const result = transformSync(sourceText, { 11 | isModule: true, 12 | sourceMaps: sourceMap ? 'inline' : false, 13 | inlineSourcesContent: true, 14 | filename: typeof sourceMap === 'string' ? sourceMap : 'virtual-module.js', 15 | jsc: { 16 | target: 'es2022', 17 | experimental: { 18 | plugins: [ 19 | [ 20 | fileURLToPath( 21 | new URL( 22 | './target/wasm32-unknown-unknown/release/swc_transformer_static_module_record.wasm', 23 | import.meta.url, 24 | ), 25 | ), 26 | { 27 | template: { 28 | type: 'eval', 29 | }, 30 | }, 31 | ], 32 | ], 33 | }, 34 | }, 35 | }) 36 | return result.code 37 | } 38 | 39 | export class ModuleSource { 40 | /** 41 | * @param {string} sourceText 42 | */ 43 | constructor(sourceText) { 44 | if (typeof sourceText !== 'string') throw new TypeError() 45 | const compiled = transform(sourceText) 46 | if (availablePolicy) return (0, eval)(availablePolicy.createScript(compiled)) 47 | return (0, eval)(compiled) 48 | } 49 | } 50 | 51 | export class ModuleSourceWithSourceMap { 52 | /** 53 | * @param {string} sourceText 54 | */ 55 | constructor(sourceText) { 56 | if (typeof sourceText !== 'string') throw new TypeError() 57 | const compiled = transform(sourceText, true) 58 | if (availablePolicy) return (0, eval)(availablePolicy.createScript(compiled)) 59 | return (0, eval)(compiled) 60 | } 61 | } 62 | 63 | let availablePolicy = undefined 64 | /** 65 | * @param {TrustedTypePolicy} policy 66 | */ 67 | export function setupTrustedTypes(policy = defaultTrustedTypes()) { 68 | availablePolicy = policy 69 | } 70 | 71 | /** 72 | * @returns {TrustedTypePolicy} 73 | */ 74 | function defaultTrustedTypes() { 75 | if (typeof trustedTypes !== 'object') return 76 | return trustedTypes.createPolicy('virtual-module-source', { 77 | createScript(module) { 78 | return module 79 | }, 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@masknet/static-module-record-swc", 3 | "publishConfig": { 4 | "access": "public" 5 | }, 6 | "version": "0.6.0", 7 | "description": "@swc/core plugin to transform ES Module into Virtual Module Record.", 8 | "type": "module", 9 | "author": "Jack Works", 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/DimensionDev/aot-secure-ecmascript" 14 | }, 15 | "keywords": [ 16 | "swc-plugin" 17 | ], 18 | "main": "target/wasm32-unknown-unknown/release/swc_transformer_static_module_record.wasm", 19 | "scripts": { 20 | "build": "cargo build --release --target wasm32-unknown-unknown", 21 | "test": "cargo fmt && cargo clippy && cargo test" 22 | }, 23 | "files": [ 24 | "target/wasm32-unknown-unknown/release/swc_transformer_static_module_record.wasm" 25 | ], 26 | "peerDependencies": { 27 | "@swc/core": "^1.3.81" 28 | }, 29 | "peerDependenciesMeta": { 30 | "@swc/core": { 31 | "optional": true 32 | } 33 | }, 34 | "dependencies": { 35 | "cjs-module-lexer": "^1.2.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::not_unsafe_ptr_arg_deref)] 2 | 3 | use module::{config::Config, VirtualModuleRecordTransformer}; 4 | use script::ErrorTransformer; 5 | use swc_core::common::DUMMY_SP; 6 | use swc_core::ecma::ast::*; 7 | use swc_core::ecma::visit::FoldWith; 8 | use swc_core::plugin::{ 9 | metadata::{TransformPluginMetadataContextKind, TransformPluginProgramMetadata}, 10 | plugin_transform, 11 | }; 12 | use utils::emit_error; 13 | 14 | mod module; 15 | mod script; 16 | mod utils; 17 | 18 | #[cfg(test)] 19 | mod test; 20 | 21 | #[plugin_transform] 22 | pub fn process_transform(program: Program, metadata: TransformPluginProgramMetadata) -> Program { 23 | let config = 24 | serde_json::from_str::(&metadata.get_transform_plugin_config().unwrap_or_default()); 25 | let filename = metadata.get_context(&TransformPluginMetadataContextKind::Filename); 26 | match config { 27 | Ok(config) => match &program { 28 | Program::Script(script) => { 29 | emit_error( 30 | script.span, 31 | "VirtualModuleRecord transformer must run in the Module mode.", 32 | ); 33 | program.fold_with(&mut ErrorTransformer { 34 | msg: "VirtualModuleRecord transformer must run in the Module mode.".to_string(), 35 | }) 36 | } 37 | Program::Module(_) => program.fold_with(&mut VirtualModuleRecordTransformer::new( 38 | config, 39 | filename, 40 | metadata.unresolved_mark, 41 | )), 42 | }, 43 | Err(err) => { 44 | emit_error(DUMMY_SP, &format!("{}", err)); 45 | program.fold_with(&mut ErrorTransformer { 46 | msg: format!("{}", err), 47 | }) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/src/module/binding_descriptor.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::*; 2 | use swc_core::common::DUMMY_SP; 3 | use swc_core::ecma::ast::*; 4 | use swc_core::ecma::utils::quote_ident; 5 | 6 | #[derive(Clone, PartialEq, Eq)] 7 | pub enum Binding { 8 | Import(ImportBinding), 9 | Export(ExportBinding), 10 | } 11 | 12 | #[derive(Clone, PartialEq, Eq)] 13 | pub struct ImportBinding { 14 | pub import: ModuleBinding, 15 | pub alias: Option, 16 | pub from: Str, 17 | } 18 | 19 | #[derive(Clone, PartialEq, Eq)] 20 | pub struct ExportBinding { 21 | pub export: ModuleBinding, 22 | pub alias: Option, 23 | pub from: Option, 24 | } 25 | 26 | impl ExportBinding { 27 | pub fn simple(ident: &Ident) -> Self { 28 | Self { 29 | export: (ident.clone()).into(), 30 | alias: None, 31 | from: None, 32 | } 33 | } 34 | } 35 | 36 | #[derive(Clone, PartialEq, Eq)] 37 | pub enum ModuleBinding { 38 | ModuleExportName(ModuleExportName), 39 | Namespace, 40 | } 41 | 42 | impl ModuleBinding { 43 | pub fn ident(&self) -> Option<&Ident> { 44 | match self { 45 | ModuleBinding::ModuleExportName(ModuleExportName::Ident(ident)) => Some(ident), 46 | _ => None, 47 | } 48 | } 49 | } 50 | 51 | fn module_export_name_to_str(binding: &ModuleExportName) -> String { 52 | match binding { 53 | ModuleExportName::Ident(ident) => ident.to_id().0.to_string(), 54 | ModuleExportName::Str(str) => str.value.to_string(), 55 | } 56 | } 57 | 58 | impl From for ModuleBinding { 59 | fn from(x: Ident) -> Self { 60 | ModuleBinding::ModuleExportName(x.into()) 61 | } 62 | } 63 | impl From for ModuleBinding { 64 | fn from(x: ModuleExportName) -> Self { 65 | ModuleBinding::ModuleExportName(x) 66 | } 67 | } 68 | impl From for ModuleBinding { 69 | fn from(x: Str) -> ModuleBinding { 70 | ModuleBinding::ModuleExportName(x.into()) 71 | } 72 | } 73 | 74 | impl From for Expr { 75 | fn from(x: ModuleBinding) -> Expr { 76 | match x { 77 | ModuleBinding::ModuleExportName(id) => match id { 78 | ModuleExportName::Ident(id) => Str { 79 | raw: None, 80 | value: id.to_id().0, 81 | span: id.span, 82 | } 83 | .into(), 84 | ModuleExportName::Str(f) => f.into(), 85 | }, 86 | ModuleBinding::Namespace => "*".into(), 87 | } 88 | } 89 | } 90 | impl ModuleBinding { 91 | pub fn default_export() -> ModuleBinding { 92 | ModuleBinding::ModuleExportName(ModuleExportName::Ident(quote_ident!("default"))) 93 | } 94 | } 95 | impl ImportBinding { 96 | pub fn to_object_lit(&self) -> ObjectLit { 97 | let mut result: Vec = Vec::with_capacity(3); 98 | match &self.import { 99 | ModuleBinding::Namespace => { 100 | result.push(key_value("importAllFrom".into(), self.from.clone().into())); 101 | result.push(key_value( 102 | "as".into(), 103 | self.alias.clone().unwrap().to_id().0.into(), 104 | )); 105 | } 106 | ModuleBinding::ModuleExportName(binding) => { 107 | result.push(key_value("import".into(), self.import.clone().into())); 108 | result.push(key_value("from".into(), self.from.clone().into())); 109 | if let Some(alias) = &self.alias { 110 | if alias.to_id().0 != module_export_name_to_str(binding) { 111 | result.push(key_value("as".into(), alias.to_id().0.into())); 112 | } 113 | } 114 | } 115 | } 116 | ObjectLit { 117 | span: DUMMY_SP, 118 | props: result, 119 | } 120 | } 121 | } 122 | 123 | impl From for Binding { 124 | fn from(x: ImportBinding) -> Self { 125 | Binding::Import(x) 126 | } 127 | } 128 | 129 | impl From for Binding { 130 | fn from(x: ExportBinding) -> Self { 131 | Binding::Export(x) 132 | } 133 | } 134 | 135 | impl ExportBinding { 136 | pub fn to_object_lit(&self) -> ObjectLit { 137 | let mut result: Vec = Vec::with_capacity(3); 138 | match &self.export { 139 | ModuleBinding::Namespace => { 140 | result.push(key_value( 141 | "exportAllFrom".into(), 142 | self.from.clone().unwrap().into(), 143 | )); 144 | if let Some(alias) = &self.alias { 145 | result.push(key_value( 146 | "as".into(), 147 | module_export_name_to_str(alias).into(), 148 | )); 149 | } 150 | } 151 | ModuleBinding::ModuleExportName(export_value) => { 152 | let actual_export_name = self 153 | .alias 154 | .clone() 155 | .map(|alias| module_export_name_to_str(&alias)); 156 | let original_export_name = module_export_name_to_str(export_value); 157 | 158 | if let Some(actual_export_name) = actual_export_name { 159 | if actual_export_name == original_export_name { 160 | result.push(key_value("export".into(), original_export_name.into())); 161 | } else if self.from.is_none() { 162 | result.push(key_value("export".into(), actual_export_name.into())); 163 | } else { 164 | result.push(key_value("export".into(), original_export_name.into())); 165 | result.push(key_value("as".into(), actual_export_name.into())); 166 | } 167 | } else { 168 | result.push(key_value("export".into(), original_export_name.into())); 169 | } 170 | if let Some(from) = &self.from { 171 | result.push(key_value("from".into(), from.clone().into())) 172 | } 173 | } 174 | } 175 | 176 | ObjectLit { 177 | span: DUMMY_SP, 178 | props: result, 179 | } 180 | } 181 | 182 | #[inline] 183 | pub fn local(ident: &Ident) -> Binding { 184 | ExportBinding { 185 | export: (ident.clone()).into(), 186 | alias: None, 187 | from: None, 188 | } 189 | .into() 190 | } 191 | } 192 | impl Binding { 193 | #[inline] 194 | pub(crate) fn to_array_lit(bindings: &Vec) -> ArrayLit { 195 | let mut item: Vec> = Vec::with_capacity(bindings.len()); 196 | for binding in bindings { 197 | let lit = match binding { 198 | Binding::Import(import) => import.to_object_lit(), 199 | Binding::Export(export) => export.to_object_lit(), 200 | }; 201 | item.push(Some(ExprOrSpread { 202 | spread: None, 203 | expr: Box::new(lit.into()), 204 | })); 205 | } 206 | ArrayLit { 207 | span: DUMMY_SP, 208 | elems: item, 209 | } 210 | } 211 | } 212 | 213 | pub struct LiveExportTracingBinding { 214 | pub local_ident: Ident, 215 | pub export: ModuleExportName, 216 | } 217 | impl LiveExportTracingBinding { 218 | pub fn simple(id: &Ident) -> LiveExportTracingBinding { 219 | LiveExportTracingBinding { 220 | local_ident: id.clone(), 221 | export: ModuleExportName::Ident(id.clone()), 222 | } 223 | } 224 | pub fn complex(id: &Ident, export: &ModuleExportName) -> LiveExportTracingBinding { 225 | LiveExportTracingBinding { 226 | local_ident: id.clone(), 227 | export: export.clone(), 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/src/module/codegen.rs: -------------------------------------------------------------------------------- 1 | use super::{binding_descriptor::Binding, config::Template, VirtualModuleRecordTransformer}; 2 | use crate::utils::*; 3 | use swc_core::common::util::take::Take; 4 | use swc_core::common::DUMMY_SP; 5 | use swc_core::ecma::ast::*; 6 | use swc_core::ecma::utils::{quote_ident, ExprFactory}; 7 | 8 | impl VirtualModuleRecordTransformer { 9 | pub fn codegen(&self, stmt: Vec, transformer: &VirtualModuleRecordTransformer) -> Module { 10 | let expr = self.virtual_module_record(stmt, transformer); 11 | Module { 12 | body: match &self.config.template { 13 | Template::ExportDefault => export_default_expr(expr), 14 | Template::Eval => eval_expr(expr), 15 | Template::Callback { 16 | callback_name, 17 | first_arg, 18 | } => callback( 19 | Ident::new(callback_name.clone().into(), DUMMY_SP), 20 | Expr::Lit(first_arg.clone().into()), 21 | expr, 22 | ), 23 | Template::CallbackInfer { callback_name, cwd } => callback( 24 | Ident::new(callback_name.clone().into(), DUMMY_SP), 25 | Expr::Lit(relative(self.file_name.as_ref().unwrap(), cwd).into()), 26 | expr, 27 | ), 28 | }, 29 | ..Module::dummy() 30 | } 31 | } 32 | fn virtual_module_record( 33 | &self, 34 | mut stmts: Vec, 35 | transformer: &VirtualModuleRecordTransformer, 36 | ) -> Expr { 37 | if self.uses_global_lookup { 38 | stmts.insert( 39 | 0, 40 | Stmt::Decl(Decl::Var(Box::new(VarDecl { 41 | span: DUMMY_SP, 42 | kind: VarDeclKind::Var, 43 | declare: false, 44 | decls: vec![VarDeclarator { 45 | span: DUMMY_SP, 46 | name: self.global_this_ident.clone().into(), 47 | init: Some(Box::new(prop_access( 48 | self.import_context_ident.clone(), 49 | quote_ident!("globalThis"), 50 | ))), 51 | definite: false, 52 | }], 53 | }))), 54 | ); 55 | } 56 | let init_fn = Function { 57 | is_async: self.uses_top_level_await, 58 | body: Some(BlockStmt { 59 | span: DUMMY_SP, 60 | stmts, 61 | }), 62 | params: { 63 | let emit_import_context = 64 | self.uses_import_meta || self.uses_dynamic_import || self.uses_global_lookup; 65 | 66 | let mut result = vec![param(transformer.module_env_record_ident.clone())]; 67 | if emit_import_context { 68 | result.push(param(transformer.import_context_ident.clone())); 69 | } 70 | result 71 | }, 72 | ..Function::dummy() 73 | }; 74 | 75 | let mut props = vec![]; 76 | 77 | if !self.bindings.is_empty() { 78 | props.push(key_value( 79 | "bindings".into(), 80 | Binding::to_array_lit(&self.bindings).into(), 81 | )); 82 | } 83 | 84 | let t = Bool { 85 | span: DUMMY_SP, 86 | value: true, 87 | }; 88 | 89 | if self.uses_top_level_await { 90 | props.push(key_value("isAsync".into(), t.into())); 91 | } 92 | 93 | if self.uses_import_meta { 94 | props.push(key_value("needsImportMeta".into(), t.into())); 95 | } 96 | 97 | if self.uses_dynamic_import { 98 | props.push(key_value("needsImport".into(), t.into())); 99 | } 100 | 101 | props.push(key_value( 102 | "execute".into(), 103 | FnExpr { 104 | ident: None, 105 | function: Box::new(init_fn), 106 | } 107 | .into(), 108 | )); 109 | 110 | ObjectLit { 111 | span: DUMMY_SP, 112 | props, 113 | } 114 | .into() 115 | } 116 | } 117 | 118 | fn export_default_expr(expr: Expr) -> Vec { 119 | let export_default_expr: ModuleDecl = ExportDefaultExpr { 120 | span: DUMMY_SP, 121 | expr: Box::new(expr), 122 | } 123 | .into(); 124 | vec![export_default_expr.into()] 125 | } 126 | 127 | fn eval_expr(expr: Expr) -> Vec { 128 | let use_strict = Expr::from("use strict").into_stmt(); 129 | 130 | vec![ 131 | ModuleItem::Stmt(use_strict), 132 | ModuleItem::Stmt( 133 | ParenExpr { 134 | expr: expr.into(), 135 | span: DUMMY_SP, 136 | } 137 | .into_stmt(), 138 | ), 139 | ] 140 | } 141 | 142 | fn callback(callee: Ident, first_arg: Expr, second_arg: Expr) -> Vec { 143 | let call = CallExpr { 144 | callee: Callee::Expr(Box::new(callee.into())), 145 | args: vec![ 146 | ExprOrSpread { 147 | expr: Box::new(first_arg), 148 | spread: None, 149 | }, 150 | ExprOrSpread { 151 | expr: Box::new(second_arg), 152 | spread: None, 153 | }, 154 | ], 155 | ..CallExpr::dummy() 156 | }; 157 | vec![ 158 | ModuleItem::Stmt(Expr::Lit("use strict".into()).into_stmt()), 159 | ModuleItem::Stmt(call.into_stmt()), 160 | ] 161 | } 162 | 163 | pub fn prop_access(obj: Ident, prop: Ident) -> Expr { 164 | MemberExpr { 165 | obj: obj.into(), 166 | prop: prop.into(), 167 | span: DUMMY_SP, 168 | } 169 | .into() 170 | } 171 | 172 | pub fn undefined_this_wrapper(expr: Expr) -> Expr { 173 | ParenExpr { 174 | expr: Box::new( 175 | SeqExpr { 176 | exprs: vec![0.0.into(), Box::new(expr)], 177 | span: DUMMY_SP, 178 | } 179 | .into(), 180 | ), 181 | span: DUMMY_SP, 182 | } 183 | .into() 184 | } 185 | 186 | pub fn assign_prop(obj: Ident, assign_to: MemberProp, expr: Box) -> Expr { 187 | AssignExpr { 188 | left: PatOrExpr::Expr(Box::new( 189 | MemberExpr { 190 | obj: Box::new(obj.into()), 191 | prop: assign_to, 192 | span: DUMMY_SP, 193 | } 194 | .into(), 195 | )), 196 | op: op!("="), 197 | right: expr, 198 | span: DUMMY_SP, 199 | } 200 | .into() 201 | } 202 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/src/module/config.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug, Clone)] 4 | #[serde(default)] 5 | pub struct Config { 6 | /// The template of code generation 7 | pub template: Template, 8 | } 9 | 10 | impl Default for Config { 11 | fn default() -> Self { 12 | Self { 13 | template: Template::ExportDefault, 14 | } 15 | } 16 | } 17 | 18 | #[derive(Serialize, Deserialize, Debug, Clone)] 19 | #[serde(tag = "type")] 20 | pub enum Template { 21 | /// export default { ... } 22 | #[serde(rename = "export-default")] 23 | ExportDefault, 24 | 25 | /// callback_name("first_arg", { ... }) 26 | #[serde(rename = "callback")] 27 | Callback { 28 | #[serde(rename = "callback")] 29 | callback_name: String, 30 | #[serde(rename = "firstArg")] 31 | first_arg: String, 32 | }, 33 | /// callback_name("/path_from/cwd", { ... }) 34 | #[serde(rename = "callback-cwd")] 35 | CallbackInfer { 36 | #[serde(rename = "callback")] 37 | callback_name: String, 38 | #[serde(rename = "cwd")] 39 | cwd: String, 40 | }, 41 | 42 | /// "use strict"; ({ ... }) 43 | #[serde(rename = "eval")] 44 | Eval, 45 | } 46 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/src/module/mod.rs: -------------------------------------------------------------------------------- 1 | /// Describes the import/export bindings of a JS module. 2 | mod binding_descriptor; 3 | /// Code generation for VirtualModuleRecord. 4 | mod codegen; 5 | pub mod config; 6 | /// Scan the binding_descriptor inside a JS module. 7 | mod scanner; 8 | /// Transform bindings into VirtualModuleRecord. 9 | mod transformer; 10 | 11 | use std::collections::HashMap; 12 | 13 | use self::{binding_descriptor::*, config::Config}; 14 | 15 | use swc_core::common::{Mark, SyntaxContext}; 16 | use swc_core::ecma::ast::*; 17 | use swc_core::ecma::utils::private_ident; 18 | 19 | /// Convert code into VirtualModuleRecord 20 | pub struct VirtualModuleRecordTransformer { 21 | uses_import_meta: bool, 22 | uses_top_level_await: bool, 23 | uses_dynamic_import: bool, 24 | uses_global_lookup: bool, 25 | 26 | bindings: Vec, 27 | imported_ident: HashMap, 28 | local_resolved_bindings: Vec, 29 | unresolved: SyntaxContext, 30 | 31 | module_env_record_ident: Ident, 32 | import_context_ident: Ident, 33 | global_this_ident: Ident, 34 | 35 | may_include_implicit_arguments: bool, 36 | 37 | pub config: Config, 38 | pub file_name: Option, 39 | } 40 | 41 | impl VirtualModuleRecordTransformer { 42 | pub fn new(config: Config, file_name: Option, unresolved_mark: Mark) -> Self { 43 | Self { 44 | uses_import_meta: false, 45 | uses_top_level_await: false, 46 | uses_dynamic_import: false, 47 | uses_global_lookup: false, 48 | may_include_implicit_arguments: false, 49 | bindings: Vec::new(), 50 | local_resolved_bindings: Vec::new(), 51 | module_env_record_ident: private_ident!("__"), 52 | import_context_ident: private_ident!("context"), 53 | global_this_ident: private_ident!("_"), 54 | config, 55 | file_name, 56 | unresolved: SyntaxContext::empty().apply_mark(unresolved_mark), 57 | imported_ident: HashMap::new(), 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/src/module/scanner.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use super::{binding_descriptor::*, VirtualModuleRecordTransformer}; 4 | use swc_core::common::DUMMY_SP; 5 | use swc_core::ecma::ast::*; 6 | use swc_core::ecma::utils::{contains_top_level_await, private_ident}; 7 | use swc_core::ecma::visit::{Visit, VisitWith}; 8 | 9 | struct ScannerFirstPass(HashMap); 10 | impl Visit for ScannerFirstPass { 11 | fn visit_import_decl(&mut self, n: &ImportDecl) { 12 | if n.type_only { 13 | return; 14 | } 15 | for specifier in n.specifiers.iter() { 16 | match specifier { 17 | ImportSpecifier::Named(named) => { 18 | self.0.insert( 19 | named.local.to_id(), 20 | ( 21 | named 22 | .imported 23 | .clone() 24 | .unwrap_or_else(|| named.local.clone().into()) 25 | .into(), 26 | *n.src.clone(), 27 | ), 28 | ); 29 | } 30 | ImportSpecifier::Default(default) => { 31 | self.0.insert( 32 | default.local.to_id(), 33 | (ModuleBinding::default_export(), *n.src.clone()), 34 | ); 35 | } 36 | ImportSpecifier::Namespace(namespace) => { 37 | self.0.insert( 38 | namespace.local.to_id(), 39 | (ModuleBinding::Namespace, *n.src.clone()), 40 | ); 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | struct ScannerSecondPass { 48 | bindings: Vec, 49 | phantom_import_binding_id: u32, 50 | imported_ident: HashMap, 51 | live_export_tracing_bindings: Vec, 52 | } 53 | impl Visit for ScannerSecondPass { 54 | /// Scan all import/export bindings inside a ModuleDecl 55 | fn visit_module_decl(&mut self, decl: &ModuleDecl) { 56 | match decl { 57 | ModuleDecl::Import(import) if !import.type_only => { 58 | if import.specifiers.is_empty() { 59 | self.phantom_import_binding_id += 1; 60 | self.bindings.push( 61 | ImportBinding { 62 | import: ModuleBinding::Namespace, 63 | from: *import.src.clone(), 64 | alias: Some(private_ident!(format!( 65 | "import_{}", 66 | self.phantom_import_binding_id 67 | ))), 68 | } 69 | .into(), 70 | ) 71 | } 72 | for item in &import.specifiers { 73 | match item { 74 | ImportSpecifier::Named(spec) => { 75 | if import.type_only { 76 | continue; 77 | } 78 | let local_ident = spec.local.clone(); 79 | let imported_ident = if let Some(imported) = &spec.imported { 80 | imported.clone() 81 | } else { 82 | ModuleExportName::Ident(spec.local.clone()) 83 | }; 84 | self.bindings.push( 85 | ImportBinding { 86 | import: imported_ident.into(), 87 | alias: Some(local_ident), 88 | from: *import.src.clone(), 89 | } 90 | .into(), 91 | ); 92 | } 93 | ImportSpecifier::Default(spec) => self.bindings.push( 94 | ImportBinding { 95 | import: ModuleBinding::default_export(), 96 | alias: Some(spec.local.clone()), 97 | from: *import.src.clone(), 98 | } 99 | .into(), 100 | ), 101 | ImportSpecifier::Namespace(spec) => self.bindings.push( 102 | ImportBinding { 103 | import: ModuleBinding::Namespace, 104 | alias: Some(spec.local.clone()), 105 | from: *import.src.clone(), 106 | } 107 | .into(), 108 | ), 109 | } 110 | } 111 | } 112 | ModuleDecl::ExportDecl(export) => match &export.decl { 113 | // export using x = expr is illegal 114 | Decl::Using(_) => (), 115 | Decl::Class(class) => { 116 | self.bindings.push(ExportBinding::local(&class.ident)); 117 | self.live_export_tracing_bindings 118 | .push(LiveExportTracingBinding::simple(&class.ident)); 119 | } 120 | Decl::Fn(f) => { 121 | self.bindings.push(ExportBinding::local(&f.ident)); 122 | self.live_export_tracing_bindings 123 | .push(LiveExportTracingBinding::simple(&f.ident)); 124 | } 125 | Decl::Var(var) => { 126 | for decl in &var.decls { 127 | self.visit_pat_inner(&decl.name); 128 | } 129 | } 130 | // No TS support. 131 | Decl::TsInterface(_) => unimplemented!(), 132 | Decl::TsTypeAlias(_) => unimplemented!(), 133 | Decl::TsEnum(_) => unimplemented!(), 134 | Decl::TsModule(_) => unimplemented!(), 135 | }, 136 | ModuleDecl::ExportNamed(export) if !export.type_only => { 137 | for spec in &export.specifiers { 138 | match spec { 139 | ExportSpecifier::Namespace(ns) => { 140 | assert!(export.src.is_some()); 141 | self.bindings.push( 142 | ExportBinding { 143 | export: ModuleBinding::Namespace, 144 | alias: Some(ns.name.clone()), 145 | from: export.src.clone().map(|from| *from), 146 | } 147 | .into(), 148 | ); 149 | } 150 | ExportSpecifier::Default(spec) => { 151 | assert!(export.src.is_some()); 152 | self.bindings.push( 153 | ExportBinding { 154 | export: ModuleBinding::default_export(), 155 | alias: Some(spec.exported.clone().into()), 156 | from: export.src.clone().map(|from| *from), 157 | } 158 | .into(), 159 | ); 160 | } 161 | ExportSpecifier::Named(spec) => { 162 | let mut bindings_pushed = false; 163 | if let ModuleExportName::Ident(ident) = &spec.orig { 164 | let id = self.imported_ident.get(&ident.to_id()); 165 | if let Some((binding, from)) = id { 166 | assert!(export.src.is_none()); 167 | self.bindings.push( 168 | ExportBinding { 169 | export: binding.clone(), 170 | alias: Some( 171 | spec.exported 172 | .clone() 173 | .unwrap_or_else(|| ident.clone().into()), 174 | ), 175 | from: Some(from.clone()), 176 | } 177 | .into(), 178 | ); 179 | bindings_pushed = true; 180 | } 181 | } 182 | if !bindings_pushed { 183 | self.bindings.push( 184 | ExportBinding { 185 | export: spec.orig.clone().into(), 186 | alias: spec.exported.clone(), 187 | from: export.src.clone().map(|from| *from), 188 | } 189 | .into(), 190 | ) 191 | } 192 | if export.src.is_none() { 193 | if let ModuleExportName::Ident(local_name) = &spec.orig { 194 | if let Some(export_name) = &spec.exported { 195 | self.live_export_tracing_bindings.push( 196 | LiveExportTracingBinding::complex( 197 | local_name, 198 | export_name, 199 | ), 200 | ); 201 | } else { 202 | self.live_export_tracing_bindings 203 | .push(LiveExportTracingBinding::simple(local_name)); 204 | } 205 | } 206 | } 207 | } 208 | } 209 | } 210 | } 211 | ModuleDecl::ExportDefaultDecl(export) => { 212 | let local_ident = match &export.decl { 213 | DefaultDecl::Class(class) => &class.ident, 214 | DefaultDecl::Fn(f) => &f.ident, 215 | DefaultDecl::TsInterfaceDecl(_) => unimplemented!(), 216 | }; 217 | if let Some(local_ident) = local_ident { 218 | let default_ident = Ident::new("default".into(), DUMMY_SP); 219 | self.live_export_tracing_bindings 220 | .push(LiveExportTracingBinding { 221 | local_ident: local_ident.clone(), 222 | export: ModuleExportName::Ident(default_ident.clone()), 223 | }); 224 | self.bindings.push( 225 | ExportBinding { 226 | from: None, 227 | export: local_ident.clone().into(), 228 | alias: Some(default_ident.into()), 229 | } 230 | .into(), 231 | ); 232 | } else { 233 | self.bindings.push( 234 | ExportBinding { 235 | from: None, 236 | export: ModuleBinding::default_export(), 237 | alias: None, 238 | } 239 | .into(), 240 | ); 241 | } 242 | } 243 | ModuleDecl::ExportDefaultExpr(_) => { 244 | self.bindings.push( 245 | ExportBinding { 246 | from: None, 247 | export: ModuleBinding::default_export(), 248 | alias: None, 249 | } 250 | .into(), 251 | ); 252 | } 253 | ModuleDecl::ExportAll(export) => { 254 | self.bindings.push( 255 | ExportBinding { 256 | from: Some(*export.src.clone()), 257 | export: ModuleBinding::Namespace, 258 | alias: None, 259 | } 260 | .into(), 261 | ); 262 | } 263 | _ => {} 264 | }; 265 | decl.visit_children_with(self); 266 | } 267 | } 268 | 269 | impl ScannerSecondPass { 270 | /// Scan all bindings inside a BindingPattern 271 | fn visit_pat_inner(&mut self, pat: &Pat) { 272 | match pat { 273 | Pat::Ident(id) => { 274 | self.bindings.push(ExportBinding::simple(&id.id).into()); 275 | self.live_export_tracing_bindings 276 | .push(LiveExportTracingBinding::simple(&id.id)); 277 | } 278 | Pat::Array(arr) => { 279 | for elem in arr.elems.iter().flatten() { 280 | self.visit_pat_inner(elem); 281 | } 282 | } 283 | Pat::Rest(rest) => self.visit_pat_inner(&rest.arg), 284 | Pat::Object(obj) => { 285 | for item in &obj.props { 286 | match item { 287 | ObjectPatProp::KeyValue(kv) => self.visit_pat_inner(&kv.value), 288 | ObjectPatProp::Assign(assign) => { 289 | self.bindings 290 | .push(ExportBinding::simple(&assign.key).into()); 291 | self.live_export_tracing_bindings 292 | .push(LiveExportTracingBinding::simple(&assign.key)); 293 | } 294 | ObjectPatProp::Rest(RestPat { arg, .. }) => self.visit_pat_inner(arg), 295 | } 296 | } 297 | } 298 | Pat::Assign(assign) => self.visit_pat_inner(&assign.left), 299 | Pat::Invalid(_) => unreachable![], 300 | Pat::Expr(_) => {} 301 | } 302 | } 303 | } 304 | 305 | impl VirtualModuleRecordTransformer { 306 | pub fn scan(&mut self, module: &Module) { 307 | let mut scanner_first_pass = ScannerFirstPass(HashMap::new()); 308 | module.visit_with(&mut scanner_first_pass); 309 | 310 | let mut scanner_second_pass = ScannerSecondPass { 311 | imported_ident: scanner_first_pass.0, 312 | bindings: vec![], 313 | phantom_import_binding_id: 0, 314 | live_export_tracing_bindings: vec![], 315 | }; 316 | module.visit_with(&mut scanner_second_pass); 317 | 318 | self.bindings = scanner_second_pass.bindings; 319 | self.imported_ident = scanner_second_pass.imported_ident; 320 | self.local_resolved_bindings = scanner_second_pass.live_export_tracing_bindings; 321 | self.uses_top_level_await = contains_top_level_await(module); 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/src/script.rs: -------------------------------------------------------------------------------- 1 | use swc_core::common::DUMMY_SP; 2 | use swc_core::ecma::ast::*; 3 | use swc_core::ecma::visit::Fold; 4 | 5 | pub struct ErrorTransformer { 6 | pub msg: String, 7 | } 8 | 9 | fn err(msg: &str) -> ThrowStmt { 10 | ThrowStmt { 11 | span: DUMMY_SP, 12 | arg: Box::new(msg.into()), 13 | } 14 | } 15 | 16 | impl Fold for ErrorTransformer { 17 | fn fold_module(&mut self, module: Module) -> Module { 18 | Module { 19 | body: vec![ModuleItem::Stmt(err(&self.msg).into())], 20 | ..module 21 | } 22 | } 23 | /// Convert code into 24 | /// ```js 25 | /// throw "error" 26 | /// ``` 27 | fn fold_script(&mut self, n: Script) -> Script { 28 | Script { 29 | body: vec![err(&self.msg).into()], 30 | ..n 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/src/test.rs: -------------------------------------------------------------------------------- 1 | use std::env::current_dir; 2 | use std::fs::{read_to_string, write}; 3 | use std::{path::PathBuf, rc::Rc}; 4 | use swc_core::common::{chain, comments::SingleThreadedComments, Mark}; 5 | use swc_core::ecma::transforms::base::hygiene::hygiene; 6 | use swc_core::ecma::transforms::base::resolver; 7 | use swc_core::ecma::transforms::testing::Tester; 8 | 9 | use crate::module::config::{Config, Template}; 10 | use crate::VirtualModuleRecordTransformer; 11 | 12 | #[testing::fixture("tests/fixture/**/*.js")] 13 | fn test(input: PathBuf) { 14 | let output = calc_output_path(&input); 15 | let file = read_to_string(&input).unwrap(); 16 | let config = parse_config(&file); 17 | 18 | Tester::run(|tester| { 19 | let input_url = format!("{}", input.as_path().display()).replace("\\\\?\\", ""); 20 | let unresolved_mark = Mark::new(); 21 | let top_level_mark = Mark::new(); 22 | let actual = tester.apply_transform( 23 | chain!( 24 | resolver(unresolved_mark, top_level_mark, false), 25 | VirtualModuleRecordTransformer::new( 26 | config.unwrap_or_default(), 27 | Some(input_url), 28 | unresolved_mark 29 | ), 30 | hygiene() 31 | ), 32 | "input.js", 33 | Default::default(), 34 | file.as_str(), 35 | )?; 36 | let result = tester.print(&actual, &Rc::new(SingleThreadedComments::default())); 37 | // TODO: why comments are missing? 38 | write(&output, result).unwrap(); 39 | Ok(()) 40 | }) 41 | } 42 | 43 | fn parse_config(file: &String) -> Option { 44 | if file.starts_with("/// ") { 45 | let first_line = file.lines().next()?; 46 | let mut config = serde_json::from_str::(&first_line[4..]).unwrap(); 47 | if let Template::CallbackInfer { 48 | callback_name, 49 | cwd: _, 50 | } = config.template 51 | { 52 | config.template = Template::CallbackInfer { 53 | callback_name, 54 | cwd: format!("{}", current_dir().unwrap().as_path().display()), 55 | }; 56 | } 57 | Some(config) 58 | } else { 59 | None 60 | } 61 | } 62 | 63 | fn calc_output_path(path: &PathBuf) -> PathBuf { 64 | let mut output = path.clone(); 65 | output.pop(); 66 | output.pop(); 67 | output.push("snapshot"); 68 | output.push(path.file_name().unwrap()); 69 | output 70 | } 71 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/src/utils.rs: -------------------------------------------------------------------------------- 1 | use swc_core::common::{errors, MultiSpan, Span, DUMMY_SP}; 2 | use swc_core::ecma::ast::*; 3 | use swc_core::ecma::atoms::JsWord; 4 | 5 | pub fn key_value(key: JsWord, expr: Expr) -> PropOrSpread { 6 | PropOrSpread::Prop(Box::new( 7 | KeyValueProp { 8 | key: Ident::new(key, DUMMY_SP).into(), 9 | value: Box::new(expr), 10 | } 11 | .into(), 12 | )) 13 | } 14 | 15 | pub fn param(id: Ident) -> Param { 16 | Param { 17 | span: DUMMY_SP, 18 | decorators: vec![], 19 | pat: BindingIdent { id, type_ann: None }.into(), 20 | } 21 | } 22 | 23 | pub fn emit_error(span: Span, msg: &str) { 24 | let mut m_span = MultiSpan::new(); 25 | m_span.push_span_label(span, "here".into()); 26 | errors::HANDLER.with(|reporter| reporter.emit(&m_span, msg, errors::Level::Error)); 27 | } 28 | 29 | pub fn relative(file_name: &String, base: &String) -> String { 30 | if !file_name.starts_with(base) { 31 | panic!("file_name {} should starts with cwd {}", file_name, base); 32 | } 33 | 34 | let uri = file_name[base.len()..].to_string().replace('\\', "/"); 35 | 36 | if uri.starts_with('/') { 37 | uri 38 | } else { 39 | format!("/{}", uri) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/arguments.js: -------------------------------------------------------------------------------- 1 | // no transform 2 | 'use strict' 3 | { 4 | function x() { 5 | arguments 6 | const x = { arguments } 7 | } 8 | function x1() { 9 | return () => arguments 10 | } 11 | class T { 12 | f() { 13 | return arguments 14 | } 15 | } 16 | } 17 | 18 | // transform 19 | { 20 | arguments 21 | const x = () => arguments 22 | x() 23 | const y = () => { 24 | const x = { arguments } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/config-template-callback-infer.js: -------------------------------------------------------------------------------- 1 | /// {"template": {"type": "callback-cwd", "callback": "__register", "cwd": "BLANK_IN_TEST"}} 2 | await 1 3 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/config-template-callback.js: -------------------------------------------------------------------------------- 1 | /// {"template": {"type": "callback", "callback": "__register", "firstArg": "node:fs"}} 2 | export function writeFileSync() {} 3 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/dynamic-import.js: -------------------------------------------------------------------------------- 1 | import('x') 2 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/empty.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DimensionDev/aot-secure-ecmascript/379ca62785e261753bec9908ce4349a2988a3e9e/packages/static-module-record-swc/tests/fixture/empty.js -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/example-callback-infer.js: -------------------------------------------------------------------------------- 1 | /// {"template": {"type": "callback-cwd", "callback": "__register", "cwd": "BLANK_IN_TEST"}} 2 | import { writeFile } from 'node:fs/promises' 3 | 4 | export const url = new URL('./here.txt', import.meta.url) 5 | await writeFile(url, 'Hello World') 6 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/example-callback.js: -------------------------------------------------------------------------------- 1 | /// {"template": {"type": "callback", "callback": "__register", "firstArg": "/index.js"}} 2 | import { writeFile } from 'node:fs/promises' 3 | 4 | export const url = new URL('./here.txt', import.meta.url) 5 | await writeFile(url, 'Hello World') 6 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/example-eval.js: -------------------------------------------------------------------------------- 1 | /// {"template": {"type": "eval"}} 2 | import { writeFile } from 'node:fs/promises' 3 | 4 | export const url = new URL('./here.txt', import.meta.url) 5 | await writeFile(url, 'Hello World') 6 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/example.js: -------------------------------------------------------------------------------- 1 | import { writeFile } from 'node:fs/promises' 2 | 3 | export const url = new URL('./here.txt', import.meta.url) 4 | await writeFile(url, 'Hello World') 5 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/export-declaration.js: -------------------------------------------------------------------------------- 1 | export var a1 2 | export var a2, a3 3 | 4 | export let b1 5 | export let b2, b3 6 | 7 | export const c1 = 0 8 | export const c2 = 0, 9 | c3 = 0 10 | 11 | export function f() { 12 | f = function () {} 13 | T = class T2 { } 14 | 15 | c1 = c2 = c3 = a1 = a2 = a3 = b1 = b2 = b3 = 0 16 | } 17 | export class T {} 18 | 19 | 20 | export let [x1, { key: x2, ...x3 }] = expr; 21 | [x1, x2, x3] = expr2; 22 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/export-default-class-ident.js: -------------------------------------------------------------------------------- 1 | export default class T {} 2 | 3 | if (Math.random()) { 4 | T = class T2 {} 5 | } 6 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/export-default-class.js: -------------------------------------------------------------------------------- 1 | export default class {} 2 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/export-default-expr.js: -------------------------------------------------------------------------------- 1 | export default 1 + 1 2 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/export-default-fn-ident.js: -------------------------------------------------------------------------------- 1 | export default function x() { 2 | x = function name() { 3 | 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/export-default-fn.js: -------------------------------------------------------------------------------- 1 | export default function () {} 2 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/export-named.js: -------------------------------------------------------------------------------- 1 | export { a, a as b, c } 2 | 3 | { 4 | var a = 1 5 | function c() { 6 | a = 2 7 | } 8 | } 9 | 10 | export { f } 11 | 12 | { 13 | for (var f = 0; f < [].length; f++) { 14 | } 15 | 16 | for (f of []) { } 17 | 18 | for (f in {}) { } 19 | } 20 | 21 | export { x1, x2, x3 } 22 | 23 | let [x1, { key: x2, ...x3 }] = expr; 24 | [x1, x2, x3] = expr2; 25 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/import-default.js: -------------------------------------------------------------------------------- 1 | import x from 'mod' 2 | import { default as y, 'default' as z } from 'mod2' 3 | console.log(x, y, z) 4 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/import-mixed.js: -------------------------------------------------------------------------------- 1 | import a, { b } from 'mod' 2 | import c, * as d from 'mod2' 3 | console.log(a, b, c, d) 4 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/import-named.js: -------------------------------------------------------------------------------- 1 | import { a, x as b, 'c wasm' as c } from 'mod' 2 | console.log(a, b, c) 3 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/import-namespace.js: -------------------------------------------------------------------------------- 1 | import * as a from 'a' 2 | import { '*' as b } from 'b' 3 | console.log(a, b) 4 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/import-no-binding.js: -------------------------------------------------------------------------------- 1 | import 'mod' 2 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/import-then-export.js: -------------------------------------------------------------------------------- 1 | export { a, b, c, z, b as w } 2 | 3 | import a, { b, _ as c } from 'x' 4 | import * as z from 'x' 5 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/import.meta.js: -------------------------------------------------------------------------------- 1 | import.meta.url 2 | import.meta() 3 | import.meta++ 4 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/name-conflict.js: -------------------------------------------------------------------------------- 1 | var _, context 2 | console.log(_, context) 3 | import.meta 4 | import('') 5 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/reexport.js: -------------------------------------------------------------------------------- 1 | export * from 'mod' 2 | export * as x2 from 'mod2' 3 | export { default, default as x3, 'some export' as x4, x5 } from 'mod3' 4 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/shared-intrinsics.js: -------------------------------------------------------------------------------- 1 | // To support this, we need to distinguish import bindings from unresolved bindings. 2 | // Now we treat them as the same. 3 | undefined 4 | Math 5 | 6 | // exclude following 7 | Function 8 | eval 9 | globalThis 10 | Compartment 11 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/top-level-await.js: -------------------------------------------------------------------------------- 1 | await 1 2 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/fixture/unresolved-ident.js: -------------------------------------------------------------------------------- 1 | globalThis // global lookup 2 | function f(globalThis) { 3 | globalThis // local lookup 4 | } 5 | 6 | const obj = { 7 | a, // global lookup 8 | } 9 | 10 | // update expressions 11 | a = 1; 12 | ({ a } = { a: 2 }); 13 | [a, b] = [1, 2]; 14 | ({ ...a } = expr); 15 | a *= 4; 16 | a++ 17 | 18 | // local vairable 19 | function yy({ a = x }) {} 20 | 21 | // tagged template 22 | css` 23 | body {} 24 | ` 25 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/arguments.js: -------------------------------------------------------------------------------- 1 | export default { 2 | execute: function(__, context) { 3 | var _ = context.globalThis; 4 | 'use strict'; 5 | { 6 | function x1() { 7 | arguments; 8 | const x = { 9 | arguments 10 | }; 11 | } 12 | function x11() { 13 | return ()=>arguments; 14 | } 15 | class T { 16 | f() { 17 | return arguments; 18 | } 19 | } 20 | } 21 | { 22 | _.arguments; 23 | const x = ()=>_.arguments; 24 | x(); 25 | const y = ()=>{ 26 | const x = { 27 | arguments: _.arguments 28 | }; 29 | }; 30 | } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/config-template-callback-infer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | __register("/tests/fixture/config-template-callback-infer.js", { 3 | isAsync: true, 4 | execute: async function(__) { 5 | await 1; 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/config-template-callback.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | __register("node:fs", { 3 | bindings: [ 4 | { 5 | export: "writeFileSync" 6 | } 7 | ], 8 | execute: function(__) { 9 | function writeFileSync() {} 10 | __.writeFileSync = writeFileSync; 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/dynamic-import.js: -------------------------------------------------------------------------------- 1 | export default { 2 | needsImport: true, 3 | execute: function(__, context) { 4 | context.import('x'); 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/empty.js: -------------------------------------------------------------------------------- 1 | export default { 2 | execute: function(__) {} 3 | }; 4 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/example-callback-infer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | __register("/tests/fixture/example-callback-infer.js", { 3 | bindings: [ 4 | { 5 | import: "writeFile", 6 | from: 'node:fs/promises' 7 | }, 8 | { 9 | export: "url" 10 | } 11 | ], 12 | isAsync: true, 13 | needsImportMeta: true, 14 | execute: async function(__, context) { 15 | var _ = context.globalThis; 16 | const url = new _.URL('./here.txt', context.importMeta.url); 17 | __.url = url; 18 | await (0, __.writeFile)(url, 'Hello World'); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/example-callback.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | __register("/index.js", { 3 | bindings: [ 4 | { 5 | import: "writeFile", 6 | from: 'node:fs/promises' 7 | }, 8 | { 9 | export: "url" 10 | } 11 | ], 12 | isAsync: true, 13 | needsImportMeta: true, 14 | execute: async function(__, context) { 15 | var _ = context.globalThis; 16 | const url = new _.URL('./here.txt', context.importMeta.url); 17 | __.url = url; 18 | await (0, __.writeFile)(url, 'Hello World'); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/example-eval.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | ({ 3 | bindings: [ 4 | { 5 | import: "writeFile", 6 | from: 'node:fs/promises' 7 | }, 8 | { 9 | export: "url" 10 | } 11 | ], 12 | isAsync: true, 13 | needsImportMeta: true, 14 | execute: async function(__, context) { 15 | var _ = context.globalThis; 16 | const url = new _.URL('./here.txt', context.importMeta.url); 17 | __.url = url; 18 | await (0, __.writeFile)(url, 'Hello World'); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/example.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bindings: [ 3 | { 4 | import: "writeFile", 5 | from: 'node:fs/promises' 6 | }, 7 | { 8 | export: "url" 9 | } 10 | ], 11 | isAsync: true, 12 | needsImportMeta: true, 13 | execute: async function(__, context) { 14 | var _ = context.globalThis; 15 | const url = new _.URL('./here.txt', context.importMeta.url); 16 | __.url = url; 17 | await (0, __.writeFile)(url, 'Hello World'); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/export-declaration.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bindings: [ 3 | { 4 | export: "a1" 5 | }, 6 | { 7 | export: "a2" 8 | }, 9 | { 10 | export: "a3" 11 | }, 12 | { 13 | export: "b1" 14 | }, 15 | { 16 | export: "b2" 17 | }, 18 | { 19 | export: "b3" 20 | }, 21 | { 22 | export: "c1" 23 | }, 24 | { 25 | export: "c2" 26 | }, 27 | { 28 | export: "c3" 29 | }, 30 | { 31 | export: "f" 32 | }, 33 | { 34 | export: "T" 35 | }, 36 | { 37 | export: "x1" 38 | }, 39 | { 40 | export: "x2" 41 | }, 42 | { 43 | export: "x3" 44 | } 45 | ], 46 | execute: function(__, context) { 47 | var _ = context.globalThis; 48 | var a1; 49 | __.a1 = a1; 50 | var a2, a3; 51 | __.a2 = a2; 52 | __.a3 = a3; 53 | let b1; 54 | __.b1 = b1; 55 | let b2, b3; 56 | __.b2 = b2; 57 | __.b3 = b3; 58 | const c1 = 0; 59 | __.c1 = c1; 60 | const c2 = 0, c3 = 0; 61 | __.c2 = c2; 62 | __.c3 = c3; 63 | function f() { 64 | [ 65 | f = function() {}, 66 | __.f = f 67 | ][0]; 68 | [ 69 | T = class T2 { 70 | }, 71 | __.T = T 72 | ][0]; 73 | [ 74 | c1 = [ 75 | c2 = [ 76 | c3 = [ 77 | a1 = [ 78 | a2 = [ 79 | a3 = [ 80 | b1 = [ 81 | b2 = [ 82 | b3 = 0, 83 | __.b3 = b3 84 | ][0], 85 | __.b2 = b2 86 | ][0], 87 | __.b1 = b1 88 | ][0], 89 | __.a3 = a3 90 | ][0], 91 | __.a2 = a2 92 | ][0], 93 | __.a1 = a1 94 | ][0], 95 | __.c3 = c3 96 | ][0], 97 | __.c2 = c2 98 | ][0], 99 | __.c1 = c1 100 | ][0]; 101 | } 102 | __.f = f; 103 | class T { 104 | } 105 | __.T = T; 106 | let [x1, { key: x2, ...x3 }] = _.expr; 107 | __.x1 = x1; 108 | __.x2 = x2; 109 | __.x3 = x3; 110 | [ 111 | [x1, x2, x3] = _.expr2, 112 | __.x1 = x1, 113 | __.x2 = x2, 114 | __.x3 = x3 115 | ][0]; 116 | } 117 | }; 118 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/export-default-class-ident.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bindings: [ 3 | { 4 | export: "default" 5 | } 6 | ], 7 | execute: function(__, context) { 8 | var _ = context.globalThis; 9 | class T { 10 | } 11 | __.default = T; 12 | if (_.Math.random()) { 13 | [ 14 | T = class T2 { 15 | }, 16 | __.default = T 17 | ][0]; 18 | } 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/export-default-class.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bindings: [ 3 | { 4 | export: "default" 5 | } 6 | ], 7 | execute: function(__) { 8 | __.default = class { 9 | }; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/export-default-expr.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bindings: [ 3 | { 4 | export: "default" 5 | } 6 | ], 7 | execute: function(__) { 8 | __.default = 1 + 1; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/export-default-fn-ident.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bindings: [ 3 | { 4 | export: "default" 5 | } 6 | ], 7 | execute: function(__) { 8 | function x() { 9 | [ 10 | x = function name() {}, 11 | __.default = x 12 | ][0]; 13 | } 14 | __.default = x; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/export-default-fn.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bindings: [ 3 | { 4 | export: "default" 5 | } 6 | ], 7 | execute: function(__) { 8 | __.default = function() {}; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/export-named.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bindings: [ 3 | { 4 | export: "a" 5 | }, 6 | { 7 | export: "b" 8 | }, 9 | { 10 | export: "c" 11 | }, 12 | { 13 | export: "f" 14 | }, 15 | { 16 | export: "x1" 17 | }, 18 | { 19 | export: "x2" 20 | }, 21 | { 22 | export: "x3" 23 | } 24 | ], 25 | execute: function(__, context) { 26 | var _ = context.globalThis; 27 | { 28 | var a = 1; 29 | __.b = __.a = a; 30 | function c() { 31 | [ 32 | a = 2, 33 | __.b = __.a = a 34 | ][0]; 35 | } 36 | } 37 | { 38 | for(var f = 0; f < [].length; f++){ 39 | __.f = f; 40 | } 41 | for (f of []){ 42 | __.f = f; 43 | } 44 | for(f in {}){ 45 | __.f = f; 46 | } 47 | } 48 | let [x1, { key: x2, ...x3 }] = _.expr; 49 | __.x1 = x1; 50 | __.x2 = x2; 51 | __.x3 = x3; 52 | [ 53 | [x1, x2, x3] = _.expr2, 54 | __.x1 = x1, 55 | __.x2 = x2, 56 | __.x3 = x3 57 | ][0]; 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/import-default.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bindings: [ 3 | { 4 | import: "default", 5 | from: 'mod', 6 | as: "x" 7 | }, 8 | { 9 | import: "default", 10 | from: 'mod2', 11 | as: "y" 12 | }, 13 | { 14 | import: 'default', 15 | from: 'mod2', 16 | as: "z" 17 | } 18 | ], 19 | execute: function(__, context) { 20 | var _ = context.globalThis; 21 | _.console.log(__.x, __.y, __.z); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/import-mixed.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bindings: [ 3 | { 4 | import: "default", 5 | from: 'mod', 6 | as: "a" 7 | }, 8 | { 9 | import: "b", 10 | from: 'mod' 11 | }, 12 | { 13 | import: "default", 14 | from: 'mod2', 15 | as: "c" 16 | }, 17 | { 18 | importAllFrom: 'mod2', 19 | as: "d" 20 | } 21 | ], 22 | execute: function(__, context) { 23 | var _ = context.globalThis; 24 | _.console.log(__.a, __.b, __.c, __.d); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/import-named.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bindings: [ 3 | { 4 | import: "a", 5 | from: 'mod' 6 | }, 7 | { 8 | import: "x", 9 | from: 'mod', 10 | as: "b" 11 | }, 12 | { 13 | import: 'c wasm', 14 | from: 'mod', 15 | as: "c" 16 | } 17 | ], 18 | execute: function(__, context) { 19 | var _ = context.globalThis; 20 | _.console.log(__.a, __.b, __.c); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/import-namespace.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bindings: [ 3 | { 4 | importAllFrom: 'a', 5 | as: "a" 6 | }, 7 | { 8 | import: '*', 9 | from: 'b', 10 | as: "b" 11 | } 12 | ], 13 | execute: function(__, context) { 14 | var _ = context.globalThis; 15 | _.console.log(__.a, __.b); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/import-no-binding.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bindings: [ 3 | { 4 | importAllFrom: 'mod', 5 | as: "import_1" 6 | } 7 | ], 8 | execute: function(__) {} 9 | }; 10 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/import-then-export.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bindings: [ 3 | { 4 | export: "default", 5 | as: "a", 6 | from: 'x' 7 | }, 8 | { 9 | export: "b", 10 | from: 'x' 11 | }, 12 | { 13 | export: "_", 14 | as: "c", 15 | from: 'x' 16 | }, 17 | { 18 | exportAllFrom: 'x', 19 | as: "z" 20 | }, 21 | { 22 | export: "b", 23 | as: "w", 24 | from: 'x' 25 | }, 26 | { 27 | import: "default", 28 | from: 'x', 29 | as: "a" 30 | }, 31 | { 32 | import: "b", 33 | from: 'x' 34 | }, 35 | { 36 | import: "_", 37 | from: 'x', 38 | as: "c" 39 | }, 40 | { 41 | importAllFrom: 'x', 42 | as: "z" 43 | } 44 | ], 45 | execute: function(__) {} 46 | }; 47 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/import.meta.js: -------------------------------------------------------------------------------- 1 | export default { 2 | needsImportMeta: true, 3 | execute: function(__, context) { 4 | context.importMeta.url; 5 | context.importMeta(); 6 | context.importMeta++; 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/name-conflict.js: -------------------------------------------------------------------------------- 1 | export default { 2 | needsImportMeta: true, 3 | needsImport: true, 4 | execute: function(__, context1) { 5 | var _1 = context1.globalThis; 6 | var _, context; 7 | _1.console.log(_, context); 8 | context1.importMeta; 9 | context1.import(''); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/reexport.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bindings: [ 3 | { 4 | exportAllFrom: 'mod' 5 | }, 6 | { 7 | exportAllFrom: 'mod2', 8 | as: "x2" 9 | }, 10 | { 11 | export: "default", 12 | from: 'mod3' 13 | }, 14 | { 15 | export: "default", 16 | as: "x3", 17 | from: 'mod3' 18 | }, 19 | { 20 | export: "some export", 21 | as: "x4", 22 | from: 'mod3' 23 | }, 24 | { 25 | export: "x5", 26 | from: 'mod3' 27 | } 28 | ], 29 | execute: function(__) {} 30 | }; 31 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/shared-intrinsics.js: -------------------------------------------------------------------------------- 1 | export default { 2 | execute: function(__, context) { 3 | var _ = context.globalThis; 4 | _.undefined; 5 | _.Math; 6 | _.Function; 7 | _.eval; 8 | _.globalThis; 9 | _.Compartment; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/top-level-await.js: -------------------------------------------------------------------------------- 1 | export default { 2 | isAsync: true, 3 | execute: async function(__) { 4 | await 1; 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /packages/static-module-record-swc/tests/snapshot/unresolved-ident.js: -------------------------------------------------------------------------------- 1 | export default { 2 | execute: function(__, context) { 3 | var _ = context.globalThis; 4 | _.globalThis; 5 | function f(globalThis) { 6 | globalThis; 7 | } 8 | const obj = { 9 | a: _.a 10 | }; 11 | _.a = 1; 12 | ({ a: _.a } = { 13 | a: 2 14 | }); 15 | [_.a, _.b] = [ 16 | 1, 17 | 2 18 | ]; 19 | ({ ..._.a } = _.expr); 20 | _.a *= 4; 21 | _.a++; 22 | function yy({ a = _.x }) {} 23 | (0, _.css)` 24 | body {} 25 | `; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /packages/web-endowments/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @masknet/web-endowments 2 | 3 | ## 0.1.1 4 | 5 | ### Patch Changes 6 | 7 | - 2d26f5d: add normalize url to fetch 8 | 9 | ## 0.1.0 10 | 11 | ### Minor Changes 12 | 13 | - e415e2b: add timers 14 | - 929bae6: create package 15 | - 4fad8d4: add fetch endowment 16 | -------------------------------------------------------------------------------- /packages/web-endowments/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@masknet/web-endowments", 3 | "version": "0.1.1", 4 | "type": "module", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/DimensionDev/aot-secure-ecmascript" 10 | }, 11 | "exports": { 12 | "types": "./dist/index.d.ts", 13 | "default": "./dist/index.js" 14 | }, 15 | "dependencies": {}, 16 | "devDependencies": {}, 17 | "files": [ 18 | "dist", 19 | "!dist/.tsbuildinfo", 20 | "src" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/web-endowments/src/fetch.ts: -------------------------------------------------------------------------------- 1 | export interface FetchOptions { 2 | fetch?: typeof fetch 3 | signal?: AbortSignal 4 | normalizeURL?(url: string): string 5 | /** 6 | * Virtualize a url 7 | * @param url URL to be rewrite 8 | * @param direction Direction of this rewrite. 9 | * 'in' means the url is from the outside world and should be virtualized. 10 | * 'out' means the url is from the inside world and should be de-virtualized to fetch the real target. 11 | */ 12 | rewriteURL?(url: string, direction: 'in' | 'out'): string 13 | replaceRequest?(request: Request): Request | PromiseLike 14 | replaceResponse?(response: Response): Response | PromiseLike 15 | canConnect?(url: string): boolean | PromiseLike 16 | } 17 | export function createFetch(options: FetchOptions) { 18 | const { 19 | fetch: _fetch = fetch, 20 | signal, 21 | rewriteURL, 22 | replaceRequest, 23 | replaceResponse, 24 | canConnect, 25 | normalizeURL, 26 | } = options 27 | 28 | return async function fetch(input: RequestInfo, init?: RequestInit) { 29 | let request = new Request(input, { 30 | ...init, 31 | signal: getMergedSignal(init?.signal, signal) || null, 32 | }) 33 | 34 | if (normalizeURL) request = new Request(normalizeURL(request.url), request) 35 | if (canConnect && !(await canConnect(request.url))) throw new TypeError('Failed to fetch') 36 | if (rewriteURL) request = new Request(rewriteURL(request.url, 'out'), request) 37 | if (replaceRequest) request = await replaceRequest(request) 38 | 39 | let response = await _fetch(request) 40 | 41 | if (rewriteURL) { 42 | const { url, redirected, type } = response 43 | // Note: Response constructor does not allow us to set the url of a response. 44 | // we have to define the own property on it. This is not a good simulation. 45 | // To prevent get the original url by Response.prototype.[[get url]].call(response) 46 | // we copy a response and set it's url to empty. 47 | response = new Response(response.body, response) 48 | Object.defineProperties(response, { 49 | url: { value: url, configurable: true }, 50 | redirected: { value: redirected, configurable: true }, 51 | type: { value: type, configurable: true }, 52 | }) 53 | Object.defineProperty(response, 'url', { 54 | configurable: true, 55 | value: rewriteURL(url, 'in'), 56 | }) 57 | } 58 | if (replaceResponse) response = await replaceResponse(response) 59 | return response 60 | } 61 | } 62 | function getMergedSignal(signal: AbortSignal | undefined | null, signal2: AbortSignal | undefined | null) { 63 | if (!signal) return signal2 64 | if (!signal2) return signal 65 | 66 | const abortController = new AbortController() 67 | signal.addEventListener('abort', () => abortController.abort(), { once: true }) 68 | signal2.addEventListener('abort', () => abortController.abort(), { once: true }) 69 | return abortController.signal 70 | } 71 | -------------------------------------------------------------------------------- /packages/web-endowments/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './timers.js' 2 | export * from './fetch.js' 3 | -------------------------------------------------------------------------------- /packages/web-endowments/src/timers.ts: -------------------------------------------------------------------------------- 1 | export interface Timers { 2 | setTimeout: typeof setTimeout 3 | clearTimeout: typeof clearTimeout 4 | setInterval: typeof setInterval 5 | clearInterval: typeof clearInterval 6 | requestAnimationFrame: typeof requestAnimationFrame 7 | cancelAnimationFrame: typeof cancelAnimationFrame 8 | requestIdleCallback?: typeof requestIdleCallback | undefined 9 | cancelIdleCallback?: typeof cancelIdleCallback | undefined 10 | queueMicrotask: typeof queueMicrotask 11 | } 12 | 13 | export function createTimers( 14 | abortSignal: AbortSignal, 15 | originalTimes: Timers = { 16 | requestAnimationFrame, 17 | cancelAnimationFrame, 18 | requestIdleCallback: typeof requestIdleCallback === 'function' ? requestIdleCallback : undefined, 19 | cancelIdleCallback: typeof cancelIdleCallback === 'function' ? cancelIdleCallback : undefined, 20 | setTimeout, 21 | clearTimeout, 22 | setInterval, 23 | clearInterval, 24 | queueMicrotask, 25 | }, 26 | ): Timers { 27 | const { 28 | requestAnimationFrame: _requestAnimationFrame, 29 | cancelAnimationFrame: _cancelAnimationFrame, 30 | setInterval: _setInterval, 31 | clearInterval: _clearInterval, 32 | setTimeout: _setTimeout, 33 | clearTimeout: _clearTimeout, 34 | cancelIdleCallback: _cancelIdleCallback, 35 | requestIdleCallback: _requestIdleCallback, 36 | queueMicrotask: _queueMicrotask, 37 | } = originalTimes 38 | 39 | const interval_timer_id: number[] = [] 40 | const idle_id: number[] = [] 41 | const raf_id: number[] = [] 42 | 43 | abortSignal.addEventListener( 44 | 'abort', 45 | () => { 46 | raf_id.forEach(_cancelAnimationFrame) 47 | interval_timer_id.forEach(_clearInterval) 48 | _cancelIdleCallback && idle_id.forEach(_cancelIdleCallback) 49 | }, 50 | { once: true }, 51 | ) 52 | 53 | return { 54 | // id is a positive number, it never repeats. 55 | requestAnimationFrame(callback) { 56 | raf_id[raf_id.length] = _requestAnimationFrame(callback) 57 | return raf_id.length 58 | }, 59 | cancelAnimationFrame(handle) { 60 | const id = raf_id[handle - 1] 61 | if (!id) return 62 | _cancelAnimationFrame(id) 63 | }, 64 | setInterval(handler, timeout) { 65 | interval_timer_id[interval_timer_id.length] = (_setInterval as any)(...arguments) 66 | return interval_timer_id.length 67 | }, 68 | clearInterval(id) { 69 | if (!id) return 70 | const handle = interval_timer_id[id - 1] 71 | if (!handle) return 72 | _clearInterval(handle) 73 | }, 74 | setTimeout(handler, timeout) { 75 | idle_id[idle_id.length] = (_setTimeout as any)(...arguments) 76 | return idle_id.length 77 | }, 78 | clearTimeout(id) { 79 | if (!id) return 80 | const handle = idle_id[id - 1] 81 | if (!handle) return 82 | _clearTimeout(handle) 83 | }, 84 | requestIdleCallback: 85 | _requestIdleCallback ? 86 | function requestIdleCallback(callback, options) { 87 | idle_id[idle_id.length] = _requestIdleCallback(callback, options) 88 | return idle_id.length 89 | } 90 | : undefined, 91 | cancelIdleCallback: 92 | _cancelIdleCallback ? 93 | function cancelIdleCallback(handle) { 94 | const id = idle_id[handle - 1] 95 | if (!id) return 96 | _cancelIdleCallback(id) 97 | } 98 | : undefined, 99 | queueMicrotask(callback) { 100 | _queueMicrotask(() => abortSignal.aborted || callback()) 101 | }, 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /packages/web-endowments/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src/", 5 | "outDir": "./dist/", 6 | "tsBuildInfoFile": "./dist/.tsbuildinfo", 7 | "lib": ["DOM", "ES2022"] 8 | }, 9 | "include": ["./src"], 10 | "references": [] 11 | } 12 | -------------------------------------------------------------------------------- /packages/webpack-reflected-module/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @masknet/webpack-reflect-module-plugin 2 | 3 | ## 0.2.0 4 | 5 | ### Minor Changes 6 | 7 | - 6dd3bed: update dependencies 8 | 9 | ### Patch Changes 10 | 11 | - Updated dependencies [6dd3bed] 12 | - @masknet/static-module-record-swc@0.6.0 13 | 14 | ## 0.1.0 15 | 16 | ### Minor Changes 17 | 18 | - 7a9038b: first release 19 | 20 | ### Patch Changes 21 | 22 | - 63dfba3: upgrade dependencies 23 | - Updated dependencies [da873b1] 24 | - Updated dependencies [be3ac75] 25 | - Updated dependencies [63dfba3] 26 | - @masknet/static-module-record-swc@0.5.0 27 | -------------------------------------------------------------------------------- /packages/webpack-reflected-module/README.md: -------------------------------------------------------------------------------- 1 | # webpack-reflect-module-plugin 2 | 3 | This plugin adds experimental support for the [Import Reflection proposal](https://github.com/tc39/proposal-import-reflection). 4 | 5 | ## Limitation 6 | 7 | ### Static reflection syntax 8 | 9 | This plugin cannot extend the JavaScript parser, therefore we cannot support the following syntax added in the proposal: 10 | 11 | ```js 12 | // Proposal: 13 | import module x from './file.js' 14 | ``` 15 | 16 | Instead, we use [Import Assertion](https://v8.dev/features/import-assertions) syntax as a temporarily replacement for the new syntax. 17 | 18 | ```js 19 | // This plugin 20 | import x from './file.js' assert { reflect: 'module' } 21 | ``` 22 | 23 | ### Transitive dependencies 24 | 25 | The transitive dependencies will be bundled as a reflected module too, this allows a userland VirtualModuleSource runtime to dynamic import an unknown reflected module. 26 | 27 | > a 28 | 29 | ```js 30 | import x from 'b' 31 | ``` 32 | 33 | > b 34 | 35 | ```js 36 | export default function hello() {} 37 | ``` 38 | 39 | > src/index.js 40 | 41 | ```js 42 | import a from 'a' assert { reflect: 'module' } 43 | assert(a.bindings[0].from === 'b') 44 | 45 | // The expression is dynamic, but can be supported because it is a transitive dependency of a. 46 | import(a.bindings[0].from, { reflect: 'module' }) 47 | 48 | // Runtime error: "b/lib.js" is not in the pre-reflected module list. 49 | // This should success if it runs in a real runtime that supports native Import Reflection. 50 | import(a.bindings[0].from + '/lib.js', { reflect: 'module' }) 51 | ``` 52 | 53 | ### Circular import fails but works in real ES Module 54 | 55 | This is a [limitation of Virtual Module Source](https://github.com/tc39/proposal-compartments/issues/70). 56 | This format cannot express hoisted functions correctly. 57 | 58 | ### CommonJS modules 59 | 60 | How this proposal interact with CommonJS module is unknown, and things get complex when it across the ESM-CommonJS boundary, but we will try to support it. 61 | -------------------------------------------------------------------------------- /packages/webpack-reflected-module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@masknet/webpack-reflect-module-plugin", 3 | "version": "0.2.0", 4 | "private": true, 5 | "type": "commonjs", 6 | "main": "./dist/index.js", 7 | "types": "./dist/index.d.ts", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/DimensionDev/aot-secure-ecmascript" 11 | }, 12 | "devDependencies": { 13 | "@swc/core": "^1.4.0", 14 | "@types/estree": "^1.0.5", 15 | "@types/loader-runner": "^2.2.8", 16 | "esbuild": "^0.20.0", 17 | "html-webpack-plugin": "^5.6.0", 18 | "webpack": "^5.90.1", 19 | "webpack-cli": "^5.1.4" 20 | }, 21 | "scripts": {}, 22 | "files": [ 23 | "dist", 24 | "!dist/.tsbuildinfo", 25 | "src" 26 | ], 27 | "peerDependencies": { 28 | "@swc/core": "^1.3.81", 29 | "esbuild": "^0.15.15", 30 | "webpack": "^5.75.0" 31 | }, 32 | "dependencies": { 33 | "@masknet/static-module-record-swc": "workspace:^", 34 | "loader-runner": "^4.3.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/webpack-reflected-module/src/index.ts: -------------------------------------------------------------------------------- 1 | import { AsyncDependenciesBlock, WebpackError, Compiler, javascript } from 'webpack' 2 | import { getImportReflectionOptions } from './utils/parseExpr' 3 | import { ReflectedModuleDependency } from './lib/dependency' 4 | import { ReflectedModuleFactory } from './lib/moduleFactory' 5 | import type { JavascriptParserOptions } from './webpack' 6 | 7 | export = class ImportReflectionPlugin { 8 | apply(compiler: Compiler) { 9 | compiler.hooks.compilation.tap(ImportReflectionPlugin.name, (compilation, { normalModuleFactory }) => { 10 | compilation.dependencyTemplates.set(ReflectedModuleDependency, new ReflectedModuleDependency.Template()) 11 | compilation.dependencyFactories.set( 12 | ReflectedModuleDependency, 13 | new ReflectedModuleFactory(normalModuleFactory), 14 | ) 15 | // we parse import('spec', { reflect: "module" }) and handle it. 16 | function parserHandler(parser: javascript.JavascriptParser, options: JavascriptParserOptions) { 17 | parser.hooks.importCall.tap('BundleTransitiveAsVirtualModuleSource', (expression) => { 18 | if (!expression.loc || !expression.range) return 19 | const result = getImportReflectionOptions(expression, (e) => parser.evaluateExpression(e)) 20 | if (result === null) return 21 | if (result instanceof WebpackError) { 22 | parser.state.current.addError(result) 23 | return 24 | } 25 | 26 | const { request, exclude } = result 27 | // TODO: support magic comments: chunkName, prefetchOrder, preloadOrder, exports 28 | const dependencyBlock = new AsyncDependenciesBlock({}, expression.loc, request) 29 | const dependency = new ReflectedModuleDependency(request, expression.loc, expression.range, exclude) 30 | dependencyBlock.addDependency(dependency) 31 | parser.state.current.addBlock(dependencyBlock) 32 | return true 33 | }) 34 | } 35 | normalModuleFactory.hooks.parser.for('javascript/auto').tap(ImportReflectionPlugin.name, parserHandler) 36 | normalModuleFactory.hooks.parser.for('javascript/esm').tap(ImportReflectionPlugin.name, parserHandler) 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/webpack-reflected-module/src/lib/dependency.ts: -------------------------------------------------------------------------------- 1 | import type { SourceLocation } from 'estree' 2 | import { AsyncDependenciesBlock, dependencies, Dependency, type sources } from 'webpack' 3 | import type { ImportReflectionOptions } from '../utils/parseExpr' 4 | import type { DependencyTemplateContext } from '../webpack' 5 | 6 | const DependencyTemplate = dependencies.ModuleDependency.Template 7 | 8 | export class ReflectedModuleDependency extends Dependency { 9 | declare userRequest: string 10 | constructor( 11 | public request: string, 12 | public override loc: SourceLocation, 13 | public range: [number, number], 14 | public excludes: ImportReflectionOptions['exclude'], 15 | ) { 16 | super() 17 | } 18 | override get type() { 19 | return 'import() reflected' 20 | } 21 | override get category() { 22 | return 'esm' 23 | } 24 | override getResourceIdentifier() { 25 | return `import() reflected ${this.request}|${this.excludes}` 26 | } 27 | 28 | static Template = class ReflectedModuleTemplate extends DependencyTemplate { 29 | override apply( 30 | dep: ReflectedModuleDependency, 31 | source: sources.ReplaceSource, 32 | context: DependencyTemplateContext, 33 | ) { 34 | const { moduleGraph, chunkGraph, runtimeTemplate, runtimeRequirements, module } = context 35 | const block = moduleGraph.getParentBlock(dep) as AsyncDependenciesBlock 36 | const content = runtimeTemplate.moduleNamespacePromise({ 37 | chunkGraph, 38 | block, 39 | module: moduleGraph.getModule(dep)!, 40 | request: dep.request, 41 | strict: module.buildMeta?.strictHarmonyModule!, 42 | message: 'import() reflected', 43 | runtimeRequirements, 44 | }) 45 | 46 | source.replace(dep.range[0], dep.range[1] - 1, content) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/webpack-reflected-module/src/lib/module.ts: -------------------------------------------------------------------------------- 1 | import { getContext } from 'loader-runner' 2 | import { Module, sources, type Compilation, WebpackError, NormalModule, RuntimeGlobals, util } from 'webpack' 3 | import type { CodeGenerationResult } from '../webpack' 4 | import { build } from 'esbuild' 5 | import { transform } from '@swc/core' 6 | 7 | export class ReflectedModule extends Module { 8 | request: string 9 | userRequest: string | undefined 10 | error: Error | undefined 11 | _source: sources.Source | undefined 12 | _sourceSizes: Map | undefined 13 | /** 14 | * @param {Parameters[0]['createData']} data 15 | */ 16 | constructor( 17 | data: Parameters<(typeof import('webpack').IgnorePlugin)['prototype']['checkIgnore']>[0]['createData'], 18 | ) { 19 | const { resource, request, context = getContext(resource!), layer, userRequest } = data 20 | super('javascript/esm/reflection', context, layer) 21 | if (!request) throw new Error('No request') 22 | this.request = request 23 | this.userRequest = userRequest 24 | this.error = undefined 25 | this._source = undefined 26 | this._sourceSizes = undefined 27 | } 28 | override identifier() { 29 | if (this.layer === null) { 30 | return `${this.type}|${this.request}` 31 | } else { 32 | return `${this.type}|${this.request}|${this.layer}` 33 | } 34 | } 35 | 36 | /** 37 | * @param {import('webpack').Compilation['requestShortener']} requestShortener the request shortener 38 | * @returns {string} a user readable identifier of the module 39 | */ 40 | override readableIdentifier(requestShortener: import('webpack').Compilation['requestShortener']): string { 41 | return requestShortener.shorten(this.userRequest)! 42 | } 43 | 44 | /** 45 | * @param {import('webpack').WebpackOptionsNormalized} options webpack options 46 | * @param {Compilation} compilation the compilation 47 | * @param {Parameters[2]} resolver the resolver 48 | * @param {Parameters[3]} fs the file system 49 | * @param {function(WebpackError=): void} callback callback function 50 | * @returns {void} 51 | */ 52 | override build( 53 | options: import('webpack').WebpackOptionsNormalized, 54 | compilation: Compilation, 55 | resolver: Parameters[2], 56 | fs: Parameters[3], 57 | callback: (arg0: WebpackError | undefined) => void, 58 | ): void { 59 | this.error = undefined 60 | this.clearWarningsAndErrors() 61 | this.clearDependenciesAndBlocks() 62 | this.buildMeta = {} 63 | const info = (this.buildInfo = { 64 | // TODO: support cache 65 | cacheable: false, 66 | parsed: false, 67 | fileDependencies: new util.LazySet(), 68 | contextDependencies: new util.LazySet(), 69 | missingDependencies: new util.LazySet(), 70 | buildDependencies: new util.LazySet(), 71 | valueDependencies: new util.LazySet(), 72 | hash: undefined, 73 | assets: undefined, 74 | assetsInfo: undefined, 75 | }) 76 | info.buildDependencies.addAll([ 77 | require.resolve('esbuild/package.json'), 78 | require.resolve('@swc/core/package.json'), 79 | require.resolve('@masknet/static-module-record-swc/package.json'), 80 | ]) 81 | build({ 82 | entryPoints: { main: this.request! }, 83 | target: 'esnext', 84 | // TODO: 85 | platform: 'browser', 86 | bundle: true, 87 | // TODO: 88 | define: {}, 89 | absWorkingDir: this.context || undefined!, 90 | // TODO: 91 | external: [], 92 | write: false, 93 | format: 'esm', 94 | // https://esbuild.github.io/api/#incremental 95 | // incremental: true, 96 | metafile: true, 97 | }) 98 | .then((result) => { 99 | // TODO: use metafile to fill dependencies 100 | // TODO: report errors 101 | // TODO: report warnings 102 | const esm = result.outputFiles[0]!.text 103 | return transform(esm, { 104 | jsc: { 105 | experimental: { 106 | plugins: [ 107 | [ 108 | '@masknet/static-module-record-swc', 109 | { 110 | template: { 111 | type: 'export-default', 112 | }, 113 | }, 114 | ], 115 | ], 116 | }, 117 | target: 'es2022', 118 | }, 119 | }) 120 | }) 121 | .then(({ code }) => { 122 | // Note: duck typing call here! it is using .identifier() at the time of writing this. 123 | this._source = NormalModule.prototype.createSource.call( 124 | this, 125 | options.context!, 126 | code.replace(/^export default /, 'module.exports = '), 127 | /** sourceMap */ undefined, 128 | compilation.compiler.root, 129 | ) 130 | this._sourceSizes?.clear() 131 | // @ts-expect-error duck typing call here! it is using this.buildInfo and this._source 132 | NormalModule.prototype._initBuildHash.call(this, compilation) 133 | // TODO: handle cache logic in NormalModule.build/handleBuildDone 134 | callback(undefined) 135 | }) 136 | .catch((error) => { 137 | callback(new WebpackError(`Failed to call esbuild: ${error.message}`)) 138 | }) 139 | } 140 | override getSourceTypes() { 141 | return new Set(['javascript']) 142 | } 143 | override size() { 144 | return this._source?.size() ?? 39 // magic number in JavascriptGenerator? 145 | } 146 | override codeGeneration(context: Parameters[0]): any { 147 | const runtimeRequirements: Set = new Set() 148 | 149 | if (!this.buildInfo?.['parsed']) { 150 | runtimeRequirements.add(RuntimeGlobals.module) 151 | runtimeRequirements.add(RuntimeGlobals.exports) 152 | runtimeRequirements.add(RuntimeGlobals.thisAsExports) 153 | } 154 | 155 | let data: Map = new Map() 156 | const sourcesMap = new Map() 157 | for (const type of context.sourceTypes || context.chunkGraph.getModuleSourceTypes(this)) { 158 | const source = 159 | this.error ? 160 | new sources.RawSource('throw new Error(' + JSON.stringify(this.error.message) + ');') 161 | : this._source 162 | 163 | if (source) { 164 | sourcesMap.set(type, new sources.CachedSource(source)) 165 | } 166 | } 167 | 168 | const resultEntry: CodeGenerationResult = { 169 | sources: sourcesMap, 170 | runtimeRequirements, 171 | data, 172 | } 173 | return resultEntry 174 | } 175 | override addCacheDependencies( 176 | fileDependencies: util.LazySet, 177 | contextDependencies: util.LazySet, 178 | missingDependencies: util.LazySet, 179 | buildDependencies: util.LazySet, 180 | ) {} 181 | } 182 | -------------------------------------------------------------------------------- /packages/webpack-reflected-module/src/lib/moduleFactory.ts: -------------------------------------------------------------------------------- 1 | import { util, Module } from 'webpack' 2 | import type { 3 | NormalModuleFactory, 4 | ModuleFactoryCreateData, 5 | ModuleFactoryCreateDataCallback, 6 | ResolveData, 7 | } from '../webpack' 8 | import { ReflectedModuleDependency } from './dependency' 9 | import { ReflectedModule } from './module' 10 | 11 | export class ReflectedModuleFactory { 12 | constructor(private normalModuleFactory: NormalModuleFactory) {} 13 | create(data: ModuleFactoryCreateData, callback: ModuleFactoryCreateDataCallback) { 14 | const { contextInfo, context, resolveOptions, dependencies } = data 15 | assertReflectedDependencyArray(dependencies) 16 | 17 | const [dependency] = dependencies 18 | if (dependencies.length !== 1) { 19 | // we assert all dependencies are equal, 20 | // otherwise webpack should split them into different create calls 21 | // this is based on my simple experiment and observation 22 | const target = dependency.request 23 | // TODO: test if webpack will feed request with different exclude options to us 24 | for (const dep of dependencies) { 25 | if (dep.request !== target) { 26 | throw new Error('Assert failed: webpack feed different dependency in a single create call') 27 | } 28 | } 29 | } 30 | const { request } = dependency 31 | const fileDependencies = new util.LazySet() as util.LazySet & Set 32 | const missingDependencies = new util.LazySet() as util.LazySet & Set 33 | const contextDependencies = new util.LazySet() as util.LazySet & Set 34 | 35 | const resolveData: ResolveData = { 36 | contextInfo, 37 | resolveOptions: resolveOptions!, 38 | context, 39 | request, 40 | // assertions: undefined!, 41 | dependencies, 42 | dependencyType: dependency.type, 43 | fileDependencies, 44 | missingDependencies, 45 | contextDependencies, 46 | createData: {}, 47 | cacheable: true, 48 | } 49 | 50 | this.normalModuleFactory.hooks.beforeResolve.callAsync(resolveData, (err, result) => { 51 | if (err) { 52 | return callback(err, { 53 | fileDependencies, 54 | missingDependencies, 55 | contextDependencies, 56 | cacheable: false, 57 | }) 58 | } 59 | if (result === false) { 60 | return callback(undefined, { 61 | fileDependencies, 62 | missingDependencies, 63 | contextDependencies, 64 | cacheable: resolveData.cacheable, 65 | }) 66 | } 67 | this.normalModuleFactory.hooks.resolve.callAsync(resolveData, (err, result) => { 68 | if (err) return callback(err) 69 | if (result === false) return callback() 70 | // note: deprecated path 71 | if (result instanceof Module) return callback(undefined, result as any) 72 | 73 | this.normalModuleFactory.hooks.afterResolve.callAsync(resolveData, (err, result) => { 74 | if (err) return callback(err) 75 | if (result === false) return callback() 76 | const createData = resolveData.createData 77 | callback(undefined, { 78 | cacheable: resolveData.cacheable, 79 | module: new ReflectedModule(createData), 80 | contextDependencies, 81 | fileDependencies, 82 | missingDependencies, 83 | }) 84 | }) 85 | }) 86 | }) 87 | } 88 | } 89 | function assertReflectedDependencyArray( 90 | array: unknown[], 91 | ): asserts array is [ReflectedModuleDependency, ...ReflectedModuleDependency[]] { 92 | if (array.length === 0) throw new TypeError(`${ReflectedModuleFactory.name} receives an empty array`) 93 | if (!array.every((x) => x instanceof ReflectedModuleDependency)) { 94 | throw new TypeError( 95 | `${ReflectedModuleFactory.name} receives an array with something is not a ${ReflectedModuleDependency.name}`, 96 | ) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /packages/webpack-reflected-module/src/utils/parseExpr.ts: -------------------------------------------------------------------------------- 1 | import type { Expression } from 'estree' 2 | import { WebpackError } from 'webpack' 3 | import type { EvaluateExpression } from '../webpack' 4 | 5 | export interface ImportReflectionOptions { 6 | exclude: undefined | '*' | string[] 7 | request: string 8 | } 9 | const UNKNOWN_EXCLUDES = 'excludes option of import reflection can only use "*" | string[] | undefined.' 10 | const NO_DYNAMIC_EXPRESSION = 'import reflection does not support dynamic expression yet.' 11 | /** 12 | * Get option for import reflection. null means we should not handle this import call. 13 | */ 14 | export function getImportReflectionOptions( 15 | expression: Expression, 16 | evaluateExpression: EvaluateExpression, 17 | ): ImportReflectionOptions | WebpackError | null { 18 | if (expression.type !== 'ImportExpression') return null 19 | 20 | // get 2nd argument 21 | if (!('arguments' in expression)) return null 22 | const [importCallOptions, ...rest] = expression.arguments as Expression[] 23 | if (!importCallOptions || rest.length) return null 24 | const optionsExpression = evaluateExpression(importCallOptions) 25 | if (optionsExpression.expression?.type !== 'ObjectExpression') return null 26 | 27 | let request: string | undefined = undefined 28 | let exclude: ImportReflectionOptions['exclude'] = undefined 29 | let meetOurOptions = false 30 | 31 | for (const property of optionsExpression.expression.properties) { 32 | if (property.type === 'SpreadElement') return null 33 | if (property.key.type === 'PrivateIdentifier') return null 34 | 35 | const key = evaluateExpression(property.key) 36 | if ( 37 | property.value.type === 'ObjectPattern' || 38 | property.value.type === 'ArrayPattern' || 39 | property.value.type === 'RestElement' || 40 | property.value.type === 'AssignmentPattern' 41 | ) { 42 | return null 43 | } 44 | 45 | if (key.identifier === 'reflect' || key.string === 'reflect') { 46 | meetOurOptions = true 47 | const value = evaluateExpression(property.value) 48 | if (value.string === 'module') { 49 | const spec = evaluateExpression(expression.source) 50 | if (!spec.isString()) return createErrorForExpression(NO_DYNAMIC_EXPRESSION, expression) 51 | request = spec.string 52 | } else { 53 | return null 54 | } 55 | } else if (key.identifier === 'excludes' || key.string === 'excludes') { 56 | meetOurOptions = true 57 | const value = evaluateExpression(property.value) 58 | if (value.isString()) { 59 | if (value.string !== '*') return createErrorForExpression(UNKNOWN_EXCLUDES, expression) 60 | exclude = '*' 61 | } else if (value.isArray()) { 62 | if (!value.items?.every((x) => x.isString())) return null 63 | exclude = value.items.map((x) => x.string!) 64 | } else if (!value.isUndefined()) { 65 | return createErrorForExpression(UNKNOWN_EXCLUDES, expression) 66 | } 67 | } else { 68 | if (meetOurOptions) { 69 | return createErrorForExpression( 70 | `import reflection does not support unknown option ${key.identifier || key.string}`, 71 | expression, 72 | ) 73 | } 74 | return null 75 | } 76 | } 77 | if (request === undefined) return null 78 | return { exclude, request } 79 | } 80 | function createErrorForExpression(message: string, node: Expression) { 81 | const error = new WebpackError(message) 82 | error.name = 'ImportReflectionError' 83 | if (node.loc) error.loc = node.loc 84 | return error 85 | } 86 | -------------------------------------------------------------------------------- /packages/webpack-reflected-module/src/webpack.ts: -------------------------------------------------------------------------------- 1 | import type { dependencies, Compilation, javascript, Module, ModuleOptions } from 'webpack' 2 | 3 | export type NormalModuleFactory = Compilation['params']['normalModuleFactory'] 4 | export type ModuleFactoryCreateData = Parameters[0] 5 | export type ModuleFactoryCreateDataCallback = Parameters[1] 6 | export type CodeGenerationResult = ReturnType 7 | export type EvaluateExpression = javascript.JavascriptParser['evaluateExpression'] 8 | export type ResolveData = Parameters<(typeof import('webpack').IgnorePlugin)['prototype']['checkIgnore']>[0] 9 | export type DependencyTemplateContext = Parameters< 10 | (typeof dependencies.ModuleDependency.Template)['prototype']['apply'] 11 | >[2] 12 | export type JavascriptParserOptions = NonNullable['javascript/auto'] 13 | -------------------------------------------------------------------------------- /packages/webpack-reflected-module/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src/", 5 | "outDir": "./dist/", 6 | "tsBuildInfoFile": "./dist/.tsbuildinfo", 7 | "verbatimModuleSyntax": false 8 | }, 9 | "include": ["./src"], 10 | "references": [] 11 | } 12 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@masknet/config/tsconfig.json", 3 | "references": [ 4 | { "path": "./packages/compartment" }, 5 | { "path": "./packages/compartment/test" }, 6 | { "path": "./packages/intrinsic-snapshot" }, 7 | { "path": "./packages/membrane" }, 8 | { "path": "./packages/web-endowments" }, 9 | { "path": "./packages/webpack-reflected-module" } 10 | ], 11 | "compilerOptions": { 12 | "exactOptionalPropertyTypes": true, 13 | "noPropertyAccessFromIndexSignature": true, 14 | "noUncheckedIndexedAccess": true, 15 | // Exclude everything by default. Include them in each project on-demand 16 | "types": [] 17 | }, 18 | "files": [] 19 | } 20 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['./packages/compartment/test/**/*.ts'], 6 | setupFiles: ['./packages/compartment/test/utils/setup.ts'], 7 | exclude: ['./packages/compartment/test/utils/**/*.ts'], 8 | }, 9 | }) 10 | --------------------------------------------------------------------------------