Repository: PlasmoHQ/plasmo Branch: main Commit: 9369e2835de2 Files: 334 Total size: 506.4 KB Directory structure: gitextract_fsxmfk6d/ ├── .github/ │ ├── CODE_OF_CONDUCT.md │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── 0.rfc.yml │ │ ├── 1.bug.yml │ │ ├── 2.example.yml │ │ └── config.yml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── SECURITY.md │ └── workflows/ │ ├── test-examples.yml │ └── test-package.yml ├── .gitignore ├── .gitmodules ├── .npmrc ├── .prettierrc.mjs ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── LICENSE ├── api/ │ ├── messaging/ │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── jest.config.mjs │ │ ├── package.json │ │ ├── src/ │ │ │ ├── background.ts │ │ │ ├── hook.ts │ │ │ ├── index.ts │ │ │ ├── message.ts │ │ │ ├── port.ts │ │ │ ├── pub-sub.ts │ │ │ ├── relay.test.ts │ │ │ ├── relay.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ └── tsconfig.json │ ├── persistent/ │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── background.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ └── selector/ │ ├── .gitignore │ ├── LICENSE │ ├── package.json │ ├── src/ │ │ ├── background.ts │ │ ├── hook.ts │ │ ├── index.ts │ │ ├── monitor.ts │ │ └── types.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── cli/ │ ├── create-plasmo/ │ │ ├── bin/ │ │ │ └── index.mjs │ │ ├── package.json │ │ ├── src/ │ │ │ ├── commands.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ └── plasmo/ │ ├── .eslintrc.js │ ├── LICENSE │ ├── README.md │ ├── bin/ │ │ └── index.mjs │ ├── i18n/ │ │ ├── README.de-DE.md │ │ ├── README.fr-FR.md │ │ ├── README.id-ID.md │ │ ├── README.ja-JP.md │ │ ├── README.ko-KR.md │ │ ├── README.ru-RU.md │ │ ├── README.tr-TR.md │ │ ├── README.vi-VN.md │ │ └── README.zh-CN.md │ ├── index.mjs │ ├── package.json │ ├── src/ │ │ ├── commands/ │ │ │ ├── build.ts │ │ │ ├── dev.ts │ │ │ ├── help.ts │ │ │ ├── index.ts │ │ │ ├── init.ts │ │ │ ├── package.ts │ │ │ ├── start.ts │ │ │ └── version.ts │ │ ├── features/ │ │ │ ├── background-service-worker/ │ │ │ │ ├── bgsw-entry.ts │ │ │ │ ├── bgsw-main-world-script.ts │ │ │ │ ├── bgsw-messaging-declaration.ts │ │ │ │ ├── bgsw-messaging.ts │ │ │ │ └── update-bgsw-entry.ts │ │ │ ├── env/ │ │ │ │ ├── env-config.ts │ │ │ │ └── env-declaration.ts │ │ │ ├── extension-devtools/ │ │ │ │ ├── common-path.ts │ │ │ │ ├── content-script-config.ts │ │ │ │ ├── generate-icons.ts │ │ │ │ ├── get-bundle-config.ts │ │ │ │ ├── git-ignore.ts │ │ │ │ ├── package-file.ts │ │ │ │ ├── parse-ast.ts │ │ │ │ ├── project-path.ts │ │ │ │ ├── project-watcher.ts │ │ │ │ ├── strip-underscore.ts │ │ │ │ ├── template-path.ts │ │ │ │ └── tsconfig.ts │ │ │ ├── extra/ │ │ │ │ ├── cache-busting.ts │ │ │ │ └── next-new-tab.ts │ │ │ ├── framework-update/ │ │ │ │ └── version-tracker.ts │ │ │ ├── helpers/ │ │ │ │ ├── create-parcel-bundler.ts │ │ │ │ ├── crypto.ts │ │ │ │ ├── flag.ts │ │ │ │ ├── loading-animation.ts │ │ │ │ ├── package-manager.ts │ │ │ │ ├── print.ts │ │ │ │ ├── prompt.ts │ │ │ │ └── traverse.ts │ │ │ ├── manifest-factory/ │ │ │ │ ├── base.ts │ │ │ │ ├── create-manifest.ts │ │ │ │ ├── mv2.ts │ │ │ │ ├── mv3.ts │ │ │ │ ├── scaffolder.ts │ │ │ │ ├── ui-library.ts │ │ │ │ └── zip.ts │ │ │ └── project-creator/ │ │ │ ├── from-existing-manifest.ts │ │ │ ├── get-raw-name.ts │ │ │ ├── git-init.ts │ │ │ ├── index.ts │ │ │ ├── install-dependencies.ts │ │ │ └── print-ready.ts │ │ ├── index.ts │ │ └── type.ts │ ├── templates/ │ │ ├── plasmo.d.ts │ │ ├── static/ │ │ │ ├── background/ │ │ │ │ └── index.ts │ │ │ ├── common/ │ │ │ │ ├── csui-container-react.tsx │ │ │ │ ├── csui-container-vanilla.tsx │ │ │ │ ├── csui.ts │ │ │ │ ├── react.ts │ │ │ │ └── vue.ts │ │ │ ├── react17/ │ │ │ │ ├── content-script-ui-mount.tsx │ │ │ │ ├── index.html │ │ │ │ └── index.tsx │ │ │ ├── react18/ │ │ │ │ ├── content-script-ui-mount.tsx │ │ │ │ ├── index.html │ │ │ │ └── index.tsx │ │ │ ├── react19/ │ │ │ │ ├── content-script-ui-mount.tsx │ │ │ │ ├── index.html │ │ │ │ └── index.tsx │ │ │ ├── svelte4/ │ │ │ │ ├── content-script-ui-mount.ts │ │ │ │ ├── index.html │ │ │ │ └── index.ts │ │ │ ├── vanilla/ │ │ │ │ ├── index.html │ │ │ │ └── index.ts │ │ │ └── vue3/ │ │ │ ├── content-script-ui-mount.ts │ │ │ ├── index.html │ │ │ └── index.ts │ │ └── tsconfig.base.json │ └── tsconfig.json ├── core/ │ ├── parcel-bundler/ │ │ ├── .gitignore │ │ ├── package.json │ │ ├── src/ │ │ │ ├── bit-set.ts │ │ │ ├── can-merge.ts │ │ │ ├── create-bundle.ts │ │ │ ├── create-ideal-graph.ts │ │ │ ├── decorate-legacy-graph.ts │ │ │ ├── get-entry-by-target.ts │ │ │ ├── get-reachable-bundle-root.ts │ │ │ ├── index.ts │ │ │ ├── remove-bundle.ts │ │ │ └── types.ts │ │ └── tsconfig.json │ ├── parcel-compressor-utf8/ │ │ ├── .gitignore │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── utf8-transform.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── parcel-config/ │ │ ├── .gitignore │ │ ├── index.json │ │ └── package.json │ ├── parcel-core/ │ │ ├── .gitignore │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ ├── resolve-options.ts │ │ │ └── types.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── parcel-namer-manifest/ │ │ ├── .gitignore │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── parcel-optimizer-encapsulate/ │ │ ├── .gitignore │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── parcel-optimizer-es/ │ │ ├── .gitignore │ │ ├── package.json │ │ ├── src/ │ │ │ ├── blob-to-string.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── parcel-packager/ │ │ ├── .gitignore │ │ ├── package.json │ │ ├── src/ │ │ │ ├── get-web-accessible-resources.ts │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ └── tsconfig.json │ ├── parcel-resolver/ │ │ ├── .gitignore │ │ ├── index.mjs │ │ ├── package.json │ │ ├── src/ │ │ │ ├── dev-polyfills/ │ │ │ │ ├── react-refresh/ │ │ │ │ │ └── runtime.ts │ │ │ │ └── react-refresh.ts │ │ │ ├── handle-absolute-root.ts │ │ │ ├── handle-alias.ts │ │ │ ├── handle-plasmo-internal.ts │ │ │ ├── handle-polyfill.ts │ │ │ ├── handle-remote-caching.ts │ │ │ ├── handle-tilde-src.ts │ │ │ ├── index.ts │ │ │ ├── polyfills/ │ │ │ │ ├── assert.ts │ │ │ │ ├── buffer.ts │ │ │ │ ├── console.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── crc-32/ │ │ │ │ │ └── crc32c.ts │ │ │ │ ├── crc-32.ts │ │ │ │ ├── crypto.ts │ │ │ │ ├── domain.ts │ │ │ │ ├── events.ts │ │ │ │ ├── http.ts │ │ │ │ ├── https.ts │ │ │ │ ├── os.ts │ │ │ │ ├── path.ts │ │ │ │ ├── process.ts │ │ │ │ ├── punycode.ts │ │ │ │ ├── querystring.ts │ │ │ │ ├── stream.ts │ │ │ │ ├── string_decoder.ts │ │ │ │ ├── sys.ts │ │ │ │ ├── timers.ts │ │ │ │ ├── tty.ts │ │ │ │ ├── url.ts │ │ │ │ ├── util.ts │ │ │ │ ├── vm.ts │ │ │ │ └── zlib.ts │ │ │ └── shared.ts │ │ └── tsconfig.json │ ├── parcel-resolver-post/ │ │ ├── .gitignore │ │ ├── package.json │ │ ├── src/ │ │ │ ├── handle-hacks.ts │ │ │ ├── handle-module-exports.ts │ │ │ ├── handle-ts-path.ts │ │ │ ├── index.ts │ │ │ ├── shared.ts │ │ │ └── utils.ts │ │ └── tsconfig.json │ ├── parcel-runtime/ │ │ ├── .gitignore │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ ├── runtimes/ │ │ │ │ ├── background-service-runtime.ts │ │ │ │ ├── page-runtime.ts │ │ │ │ └── script-runtime.ts │ │ │ ├── types.ts │ │ │ └── utils/ │ │ │ ├── 0-patch-module.ts │ │ │ ├── bgsw.ts │ │ │ ├── hmr-check.ts │ │ │ ├── hmr-utils.ts │ │ │ ├── inject-socket.ts │ │ │ ├── loading-indicator.ts │ │ │ └── react-refresh.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── parcel-transformer-inject-env/ │ │ ├── .gitignore │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── parcel-transformer-inline-css/ │ │ ├── .gitignore │ │ ├── package.json │ │ ├── src/ │ │ │ ├── get-tagets.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── parcel-transformer-lab/ │ │ ├── .gitignore │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── state.ts │ │ └── tsconfig.json │ ├── parcel-transformer-manifest/ │ │ ├── .gitignore │ │ ├── package.json │ │ ├── runtime/ │ │ │ └── plasmo-default-background.ts │ │ ├── src/ │ │ │ ├── csp-patch-hmr.ts │ │ │ ├── handle-action.ts │ │ │ ├── handle-background.ts │ │ │ ├── handle-content-scripts.ts │ │ │ ├── handle-declarative-net-request.ts │ │ │ ├── handle-deep-loc.ts │ │ │ ├── handle-dictionaries.ts │ │ │ ├── handle-locales.ts │ │ │ ├── handle-sandboxes.ts │ │ │ ├── handle-tabs.ts │ │ │ ├── index.ts │ │ │ ├── normalize-manifest.ts │ │ │ ├── schema.ts │ │ │ ├── state.ts │ │ │ ├── utils.ts │ │ │ └── validate-version.ts │ │ └── tsconfig.json │ ├── parcel-transformer-svelte/ │ │ ├── .gitignore │ │ ├── package.json │ │ ├── src/ │ │ │ ├── convert-error.ts │ │ │ ├── convert-loc.ts │ │ │ ├── index.ts │ │ │ ├── source-map.ts │ │ │ └── types.ts │ │ └── tsconfig.json │ └── parcel-transformer-vue/ │ ├── .gitignore │ ├── package.json │ ├── src/ │ │ └── index.ts │ └── tsconfig.json ├── eslint.config.mjs ├── package.json ├── packages/ │ ├── framework-shared/ │ │ ├── build-socket/ │ │ │ ├── event.ts │ │ │ └── index.ts │ │ ├── package.json │ │ └── tsconfig.json │ └── init/ │ ├── .gitignore │ ├── bpp.yml │ ├── entries/ │ │ ├── background.ts │ │ ├── content.ts │ │ ├── contents/ │ │ │ ├── inline.tsx │ │ │ └── overlay.tsx │ │ ├── newtab.tsx │ │ ├── options.tsx │ │ └── popup.tsx │ ├── index.json │ ├── package.json │ ├── templates/ │ │ ├── README.md │ │ └── tsconfig.json │ └── tsconfig.json ├── pnpm-workspace.yaml ├── renovate.json ├── scripts/ │ └── move-prettier-cjs-to-mjs.bash └── turbo.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CODE_OF_CONDUCT.md ================================================ ## Code of Conduct ### Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ### Our Standards Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions, viewpoints, and experiences - Giving and gracefully accepting constructive feedback - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience - Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: - The use of sexualized language or imagery, and sexual attention or advances of any kind - Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment - Publishing others’ private information, such as a physical or email address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ### Enforcement Responsibilities Project maintainers are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ### Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ### Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project team responsible for enforcement at [foss@plasmo.com](mailto:foss@plasmo.com). All complaints will be reviewed and investigated promptly and fairly. All project maintainers are obligated to respect the privacy and security of the reporter of any incident. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ### Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct/][version] [homepage]: http://contributor-covenant.org [version]: https://www.contributor-covenant.org/version/2/1 ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing to Plasmo To contribute to [our examples](https://github.com/PlasmoHQ/examples/), please see **[Adding examples](#adding-examples)** below. ## Contributing 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your own GitHub account and then [clone](https://help.github.com/articles/cloning-a-repository/) it: ```bash git clone git@github.com:/plasmo.git --recurse-submodules ``` **NOTE:** Replace `` with your GitHub username or organization. 1. Work on your fork's `main` branch, then [open a PR](https://github.com/PlasmoHQ/plasmo/compare). Please ensure the PR name follows the naming convention: `feat: some new feature` Replacing `feat` with `fix`, `bug` or `doc` accordingly ## Adding examples When you add an example to the [examples](https://github.com/PlasmoHQ/examples/) repository: - Use `pnpm create plasmo --exp` to create the example. - The name of the example should have a `with-*` prefix. - To add additional notes, add a `## Notes` section at the start of the generated readme. - Your PR should be pointed to the [examples project](https://github.com/PlasmoHQ/examples/). ## Developing The development branch is `main`, and this is the branch that all pull requests should be made against. To develop locally: 1. Install [pnpm](https://pnpm.io/) - DO NOT install pnpm as a global npm dependency, we need pnpm to be linked directly to your $PATH. - Recommended installation method is with corepack or with brew (on macOS) - If installed with brew, you might need to include the pnpm $PATH to your debugger 2. Install the dependencies with: ``` pnpm i ``` 3. Start developing and watch for code changes: ``` pnpm dev:cli ``` ## Developing with your local version of Plasmo ### As global link 1. Link `plasmo` to your local registry: ```sh cd plasmo/cli/plasmo pnpm link --global ``` 2. Invoke plasmo directly: ```sh plasmo init plasmo dev plasmo build ``` 3. To revert the linking later on: ```sh pnpm rm -g plasmo ``` Note: The `create-plasmo` CLI tool is not meant to be run locally. If you have already linked it, please run ```sh pnpm -g unlink create-plasmo ``` to unlink it. ## Building You can build the project, including all type definitions, with: ```bash pnpm build ``` ## Naming convention ### Files and directories Any files that require attention for reading should be `UPPER_CASE`. Examples: - README.md - LICENSE - SECURITY.md - CONTRIBUTING.md Directory and source file should use `kebab-case`, unless required by tooling. Examples: - cli/plasmo/src/features/extension-devtools/plasmo-extension-manifest.ts ### Code | Concept | Naming convention | | -------------------- | ----------------------- | | Local constants | `UPPER_CASE` | | Enum namespace | `PascalCase` | | Enum values | `PascalCase` | | TS types | `PascalCase` | | TS fields | `camelCase` | | React component | `PascalCase` | | React hook | `camelCase` | | Local variable | `camelCase` | | Unused argument | `_paddedCamelCase` | | Template Placeholder | `__snake_case_padded__` | | Functions | `camelCase` | | API Routes | `kebab-case` | ## For Core Maintainers / Admin Plasmo has 2 deployed environments: | env name | purpose | requirement | | -------- | -------------- | --------------------- | | lab | For WIP test | Admin deploy directly | | latest | Stable release | Merge to `stable` | Reviewer approves and merges PRs to `main` branch -> deploys to `latest` > NOTE: Please make sure to use the `Squash and Merge` strategy For `hotfix`, the workflow is: 1. Creates a `hotfix-FFFF` branch off of `stable` and a PR to `stable` ```sh git checkout stable git checkout -b hotfix-FFFF ``` PR name: `hotfix: some quick patch` `FFFF` is an issue number 1. Admin reviews, approves and merges `hotfix-FFFF` to `main` -> deploys to `latest` ### Merge strategy 1. Admin review PR 1. If the rough idea is good, code owner season the PR or guide the author to make it better 1. Merge and deploy following the table below: | From | To | Strategy | Deploy to | | ---------- | ------ | ---------------- | --------- | | `feat-*` | `main` | Squash and Merge | latest | | `hotfix-*` | `main` | Squash and Merge | latest | ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: PlasmoHQ # patreon: # Replace with a single Patreon username # open_collective: # Replace with a single Open Collective username # ko_fi: # Replace with a single Ko-fi username # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry # liberapay: # Replace with a single Liberapay username # issuehunt: # Replace with a single IssueHunt username # otechie: # Replace with a single Otechie username # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/ISSUE_TEMPLATE/0.rfc.yml ================================================ name: ⚡ Request for Comments description: File an RFC for Feature Request/Enhancement/Refactor title: "[RFC] " labels: ["enhancement"] body: - type: markdown attributes: value: | **Thank you for taking the time to fill out this RFC!** 🥳 - type: textarea id: description attributes: label: How do you envision this feature/change to look/work like? description: Please be as detailed as possible, providing any relevant context. placeholder: The framework should read icon-1024.png and transforms them into smaller icons... validations: required: true - type: textarea id: purpose attributes: label: What is the purpose of this change/feature? Why? description: Please provide a simple summary/abstraction. placeholder: | The current image is not versatile enough, i.e it is too small. validations: required: true - type: textarea id: examples attributes: label: (OPTIONAL) Example implementations description: If you have any examples of how this feature/change works, please list them here. validations: required: false - type: checkboxes id: contribution attributes: label: (OPTIONAL) Contribution description: Would you be willing to create a PR to solve this issue? options: - label: I would like to contribute to this RFC via a PR required: false - type: checkboxes attributes: label: Verify canary release description: "`plasmo@canary` is the canary version of Plasmo framework that ships daily. It includes all features and fixes that have not been released to the stable version yet. Think of canary as a public beta. Some issues may already be fixed in the canary version, so please verify that your issue reproduces before opening a new issue." options: - label: I verified that the issue exists in `plasmo` canary release required: true - type: checkboxes id: terms attributes: label: Code of Conduct description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CONTRIBUTING.md). options: - label: I agree to follow this project's Code of Conduct required: true - label: I checked the [current issues](https://github.com/PlasmoHQ/plasmo/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement+) for duplicate problems. required: true ================================================ FILE: .github/ISSUE_TEMPLATE/1.bug.yml ================================================ name: 🐛 Bug Report description: File a bug report title: "[BUG] " labels: ["bug", "triage"] body: - type: markdown attributes: value: | **Thank you for taking the time to fill out this bug report!** 🥳 - type: textarea id: what-happened attributes: label: What happened? description: Also tell us, what did you expect to happen? placeholder: Tell us what you see! value: "A bug happened!" validations: required: true # - type: checkboxes # attributes: # label: Verify canary release # description: '`plasmo@canary` is the canary version of Plasmo framework that ships daily. It includes all features and fixes that have not been released to the stable version yet. Think of canary as a public beta. Some issues may already be fixed in the canary version, so please verify that your issue reproduces before opening a new issue.' # options: # - label: I verified that the issue exists in `plasmo` canary release # required: true - type: dropdown id: version attributes: label: Version description: What version of the framework are you using? options: - Latest - Canary validations: required: true - type: dropdown id: operating-system attributes: label: What OS are you seeing the problem on? multiple: true options: - Windows - MacOSX - Linux - Other - type: dropdown id: browsers attributes: label: What browsers are you seeing the problem on? multiple: true options: - Chrome - Microsoft Edge - Opera - Safari - Firefox - type: textarea id: logs attributes: label: Relevant log output description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. render: Shell - type: checkboxes id: contribution attributes: label: (OPTIONAL) Contribution description: Would you be willing to create a PR to solve this issue? options: - label: I would like to fix this BUG via a PR required: false - type: checkboxes id: terms attributes: label: Code of Conduct description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CONTRIBUTING.md). options: - label: I agree to follow this project's Code of Conduct required: true - label: I checked the [current issues](https://github.com/PlasmoHQ/plasmo/issues?q=is%3Aopen+is%3Aissue+label%3Abug) for duplicate problems. required: true ================================================ FILE: .github/ISSUE_TEMPLATE/2.example.yml ================================================ name: 📓 Request/Improve an Example description: Request or Improve a Plasmo Framework with-* example title: "[EXP] " labels: ["documentation"] body: - type: markdown attributes: value: | **Thank you for taking the time to fill out this example request!** 🥳 - type: textarea attributes: label: What is the example you wish to see? description: "Example: I would like to see more examples of how to use `X-Framework`." validations: required: true - type: textarea attributes: label: Is there any context that might help us understand? description: A clear description of any added context that might help us understand. validations: required: false - type: checkboxes id: terms attributes: label: Code of Conduct description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CONTRIBUTING.md). options: - label: I agree to follow this project's Code of Conduct required: true - label: I checked the [current issues](https://github.com/PlasmoHQ/plasmo/issues) for duplicate problems. required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Join our Discord server url: https://www.plasmo.com/s/d about: Ask questions and discuss with other community members ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ## Details This PR ... ### Code of Conduct - [ ] I agree to follow this project's [Code of Conduct](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CODE_OF_CONDUCT.md) - [ ] I agree to license this contribution under the MIT LICENSE - [ ] I checked the [current PR](https://github.com/PlasmoHQ/plasmo/pulls) for duplication. ## Contacts - (OPTIONAL) Discord ID: If your PR is accepted, we will award you with the `Contributor` role on Discord server. To join the server, visit: https://www.plasmo.com/s/d ================================================ FILE: .github/SECURITY.md ================================================ Contact: security@plasmo.com Expires: 2100-01-01T00:00:00.000Z Acknowledgments: https://www.plasmo.com/security/hall-of-fame ================================================ FILE: .github/workflows/test-examples.yml ================================================ name: Test examples on: push: branches: ["main"] pull_request: types: [opened, synchronize] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [20.x] steps: - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - uses: actions/checkout@master with: submodules: "recursive" - name: Setup pnpm uses: pnpm/action-setup@v4.1.0 with: run_install: false - name: Get pnpm store directory id: pnpm-cache shell: bash run: | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - uses: actions/cache@v4.2.3 name: Setup pnpm cache with: path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - name: "Build local version of Plasmo" run: | pnpm i --no-frozen-lockfile pnpm run build:packages pnpm run build:api pnpm i pnpm run build:cli pnpm i - name: "Build all examples" run: pnpm run build:examples - name: "Test all examples" run: pnpm run test:examples ================================================ FILE: .github/workflows/test-package.yml ================================================ name: Test package manager execution on: release: types: [published] workflow_dispatch: inputs: tag: description: "Release tag to test, i.e latest, 1.0.0. Default latest" required: false default: latest jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [20.x] package-manager: [{ name: "pnpm", exec: "pnpm dlx" }, { name: "npm", exec: "npx -y" }] steps: - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: "Install package manager" run: npm install -g ${{ matrix.package-manager.name }} - name: Check if Plasmo command works run: ${{ matrix.package-manager.exec }} plasmo@${{ github.event.inputs.tag }} version - name: Check if plasmo init works at all run: yes "lab" | ${{ matrix.package-manager.exec }} plasmo@${{ github.event.inputs.tag }} init --verbose - name: Check if building is possible and if it built run: | pushd lab ${{ matrix.package-manager.name }} run build pushd build popd timeout 10 ${{ matrix.package-manager.name }} run dev || code=$?; if [[ $code -ne 124 && $code -ne 0 ]]; then exit $code; fi ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies node_modules .pnp .pnp.js # testing coverage # next.js .next/ out/ build dist/ # electron bundle **/resources/app # misc .DS_Store *.pem .idea # debug npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* # local env files .env*.local # turbo .turbo .tsbuildinfo ================================================ FILE: .gitmodules ================================================ [submodule "packages/rps"] path = packages/rps url = https://github.com/PlasmoHQ/rps.git branch = main [submodule "packages/puro"] path = packages/puro url = https://github.com/PlasmoHQ/puro.git branch = main [submodule "packages/gcp-refresh-token"] path = packages/gcp-refresh-token url = https://github.com/PlasmoHQ/gcp-refresh-token.git branch = main [submodule "packages/use-hashed-state"] path = packages/use-hashed-state url = https://github.com/PlasmoHQ/use-hashed-state.git branch = main [submodule "examples"] path = examples url = https://github.com/PlasmoHQ/examples.git [submodule "packages/permission-ui"] path = packages/permission-ui url = https://github.com/PlasmoHQ/permission-ui.git [submodule "packages/utils"] path = packages/utils url = https://github.com/PlasmoHQ/plasmo-utils.git [submodule "packages/constants"] path = packages/constants url = https://github.com/PlasmoHQ/plasmo-constants.git [submodule "packages/config"] path = packages/config url = https://github.com/PlasmoHQ/plasmo-config.git [submodule "api/storage"] path = api/storage url = https://github.com/PlasmoHQ/storage.git branch = main ================================================ FILE: .npmrc ================================================ save-workspace-protocol = true prefer-workspace-packages = true save-exact = true link-workspace-packages = true strict-peer-dependencies = false git-tag-version = false commit-hooks = false ================================================ FILE: .prettierrc.mjs ================================================ /** * @type {import('prettier').Options} */ export default { printWidth: 80, tabWidth: 2, useTabs: false, semi: false, singleQuote: false, trailingComma: "none", bracketSpacing: true, bracketSameLine: true, plugins: ["@ianvs/prettier-plugin-sort-imports"], importOrder: [ "", // Node.js built-in modules "", // Imports not matched by other special words or groups. "", // Empty line "^@plasmo/(.*)$", "", "^@plasmohq/(.*)$", "", "^@plasmo-static-common/(.*)$", "", "^~(.*)$", "", "^[./]", "", "__plasmo_import_module__", "__plasmo_mount_content_script__" ] } ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": ["esbenp.prettier-vscode"] } ================================================ FILE: .vscode/settings.json ================================================ { "editor.formatOnSave": true, "files.exclude": { "**/.git": true, "**/.svn": true, "**/.hg": true, "**/CVS": true, "**/.DS_Store": true, "**/Thumbs.db": true, "**/node_modules": true, "**/.turbo": true, "**/.next": true, "**/*.log": true }, "[svg]": { "editor.defaultFormatter": "jock.svg" }, "git.detectSubmodulesLimit": 99 } ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: api/messaging/.gitignore ================================================ node_modules # Lockfiles - See https://github.com/PlasmoHQ/p1asm0 pnpm-lock.yaml package-lock.json yarn.lock .turbo key.json dist/ ================================================ FILE: api/messaging/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: api/messaging/jest.config.mjs ================================================ /** * @type {import('@jest/types').Config.InitialOptions} */ const config = { clearMocks: true, testEnvironment: "jsdom", extensionsToTreatAsEsm: [".ts"], transform: { "^.+.ts?$": [ "ts-jest", { useESM: true, isolatedModules: true } ] }, testMatch: ["**/*.test.ts"], verbose: true } export default config ================================================ FILE: api/messaging/package.json ================================================ { "name": "@plasmohq/messaging", "version": "0.7.2", "description": "Type-safe, zero-config messaging library for modern browser extensions", "type": "module", "module": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { "./hook": { "types": "./dist/hook.d.ts", "import": "./dist/hook.js", "require": "./dist/hook.cjs" }, "./relay": { "types": "./dist/relay.d.ts", "import": "./dist/relay.js", "require": "./dist/relay.cjs" }, "./port": { "types": "./dist/port.d.ts", "import": "./dist/port.js", "require": "./dist/port.cjs" }, "./pub-sub": { "types": "./dist/pub-sub.d.ts", "import": "./dist/pub-sub.js", "require": "./dist/pub-sub.cjs" }, "./background": { "types": "./dist/background.d.ts", "import": "./dist/background.js", "require": "./dist/background.cjs" }, "./message": { "types": "./dist/message.d.ts", "import": "./dist/message.js", "require": "./dist/message.cjs" }, ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" } }, "typesVersions": { "*": { "message": [ "./dist/message.d.ts" ], "hook": [ "./dist/hook.d.ts" ], "relay": [ "./dist/relay.d.ts" ], "port": [ "./dist/port.d.ts" ], "pub-sub": [ "./dist/pub-sub.d.ts" ], "background": [ "./dist/background.d.ts" ] } }, "files": [ "dist" ], "tsup": { "entry": [ "src/index.ts", "src/port.ts", "src/pub-sub.ts", "src/relay.ts", "src/message.ts", "src/background.ts", "src/hook.ts" ], "format": [ "esm", "cjs" ], "target": "esnext", "platform": "node", "splitting": false, "bundle": true }, "scripts": { "dev": "run-p dev:*", "dev:compile": "tsup --watch --sourcemap --dts-resolve", "dev:test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --watch", "build": "tsup --dts-resolve --minify --clean", "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest", "prepublishOnly": "pnpm build" }, "author": "Plasmo Corp. ", "contributors": [ "@louisgv", "@ColdSauce" ], "repository": { "type": "git", "url": "https://github.com/PlasmoHQ/plasmo.git", "directory": "api/messaging" }, "license": "MIT", "keywords": [ "react-hook", "browser-extension", "chrome-extension" ], "peerDependencies": { "react": "^16.8.6 || ^17 || ^18 || ^19.0.0" }, "peerDependenciesMeta": { "react": { "optional": true } }, "devDependencies": { "@jest/globals": "29.7.0", "@jest/types": "29.6.3", "@testing-library/react": "16.2.0", "@testing-library/dom": "10.4.0", "@types/chrome": "0.0.312", "@types/node": "22.13.13", "@types/react": "19.0.12", "canvas": "3.1.0", "cross-env": "7.0.3", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "react": "19.0.0", "react-dom": "19.0.0", "rimraf": "6.0.1", "ts-jest": "29.3.0", "tsup": "8.4.0", "typescript": "5.8.2" }, "dependencies": { "nanoid": "5.1.5" } } ================================================ FILE: api/messaging/src/background.ts ================================================ import type { PlasmoMessaging, PortName } from "./index" import { getExtRuntime } from "./utils" export const getPortMap = (): Map => globalThis.__plasmoInternalPortMap export const getPort = (name: PortName): chrome.runtime.Port => { const portMap = getPortMap() const port = portMap.get(name) if (!port) { throw new Error(`Port ${name} not found`) } return port } getExtRuntime().onMessage.addListener( (request: PlasmoMessaging.InternalRequest, _sender, sendResponse) => { switch (request.__PLASMO_INTERNAL_SIGNAL__) { case "__PLASMO_MESSAGING_PING__": { sendResponse(true) break } } return true } ) ================================================ FILE: api/messaging/src/hook.ts ================================================ import { useEffect, useRef, useState } from "react" import { relayMessage, type MessageName, type PlasmoMessaging } from "./index" import { listen as messageListen } from "./message" import { listen as portListen } from "./port" import { relay } from "./relay" /** * Used in any extension context to listen and send messages to background. */ export const useMessage = ( handler: PlasmoMessaging.Handler ) => { const [data, setData] = useState() useEffect( () => messageListen(async (req, res) => { setData(req.body) await handler(req, res) }), [handler] ) return { data } } export const usePort: PlasmoMessaging.PortHook = (name) => { const portRef = useRef(undefined) const reconnectRef = useRef(0) const [data, setData] = useState() useEffect(() => { if (!name) { return null } const { port, disconnect } = portListen( name, (msg) => { setData(msg) }, () => { reconnectRef.current = reconnectRef.current + 1 } ) portRef.current = port return disconnect }, [ name, reconnectRef.current // This is needed to force a new port ref ]) return { data, send: (body) => { portRef.current.postMessage({ name, body }) }, listen: (handler) => portListen(name, handler) } } /** * TODO: Perhaps add a way to detect if this hook is being used inside CS? */ export function useMessageRelay( req: PlasmoMessaging.Request ) { useEffect(() => relayMessage(req), []) } export const useRelay: PlasmoMessaging.RelayFx = (req, onMessage) => { const relayRef = useRef<() => void>(undefined) useEffect(() => { relayRef.current = relay(req, onMessage) return relayRef.current }, []) return () => relayRef.current?.() } ================================================ FILE: api/messaging/src/index.ts ================================================ import { relay as rawRelay, sendViaRelay as rawSendViaRelay } from "./relay" import type { MessageName, PlasmoMessaging } from "./types" import { getActiveTab, getExtRuntime, getExtTabs } from "./utils" export type { PlasmoMessaging, MessageName, PortName, PortsMetadata, MessagesMetadata, OriginContext } from "./types" /** * Send to Background Service Workers from Content Scripts or Extension pages. * `extensionId` is required to send a message from a Content Script in the main world */ // TODO: Add a framework runtime check, using a global variable export const sendToBackground: PlasmoMessaging.SendFx = async ( req ) => { return getExtRuntime().sendMessage(req.extensionId ?? null, req) } /** * Send to Content Scripts from Extension pages or Background Service Workers. * Default to active tab if no tabId is provided in the request */ export const sendToContentScript: PlasmoMessaging.SendFx = async (req) => { const tabId = typeof req.tabId === "number" ? req.tabId : (await getActiveTab())?.id if (!tabId) { throw new Error("No active tab found to send message to.") } return getExtTabs().sendMessage(tabId, req) } /** * @deprecated Renamed to `sendToContentScript` */ export const sendToActiveContentScript = sendToContentScript /** * Any request sent to this relay get send to background, then emitted back as a response */ export const relayMessage: PlasmoMessaging.MessageRelayFx = (req) => rawRelay(req, sendToBackground) /** * @deprecated Migrated to `relayMessage` */ export const relay = relayMessage export const sendToBackgroundViaRelay: PlasmoMessaging.SendFx = rawSendViaRelay /** * @deprecated Migrated to `sendToBackgroundViaRelay` */ export const sendViaRelay = sendToBackgroundViaRelay ================================================ FILE: api/messaging/src/message.ts ================================================ import { type PlasmoMessaging } from "./index" import { getExtRuntime } from "./utils" export const listen = ( handler: PlasmoMessaging.Handler ) => { const metaListener = async ( req: any, sender: chrome.runtime.MessageSender, sendResponse: (response?: ResponseBody) => void ) => { await handler?.( { ...req, sender }, { send: (p) => sendResponse(p) } ) } const listener = ( req: any, sender: chrome.runtime.MessageSender, sendResponse: (response?: ResponseBody) => void ) => { metaListener(req, sender, sendResponse) return true // Synchronous return to indicate this is an async listener } getExtRuntime().onMessage.addListener(listener) return () => { getExtRuntime().onMessage.removeListener(listener) } } ================================================ FILE: api/messaging/src/port.ts ================================================ import type { PortName } from "./index" import { getExtRuntime } from "./utils" const portMap = new Map() export const getPort = (name: PortName) => { const port = portMap.get(name) if (!!port) { return port } const newPort = getExtRuntime().connect({ name }) portMap.set(name, newPort) return newPort } export const removePort = (name: PortName) => { portMap.delete(name) } export const listen = ( name: PortName, handler: (msg: ResponseBody) => Promise | void, onReconnect?: () => void ) => { const port = getPort(name) function reconnectHandler() { removePort(name) onReconnect?.() } port.onMessage.addListener(handler) port.onDisconnect.addListener(reconnectHandler) return { port, disconnect: () => { port.onMessage.removeListener(handler) port.onDisconnect.removeListener(reconnectHandler) } } } ================================================ FILE: api/messaging/src/pub-sub.ts ================================================ import { getExtRuntime } from "./utils" export type PubSubMessage = { from?: number to?: number payload: any } // Only usable from BGSW export const getHubMap = (): Map => globalThis.__plasmoInternalHubMap // Only usable by BGSW export const startHub = () => { const runtime = getExtRuntime() if (!runtime.onConnectExternal) { throw new Error( "onConnect External not available. You need externally_connectable entry possibly" ) } globalThis.__plasmoInternalHubMap = new Map() const hub = getHubMap() runtime.onConnectExternal.addListener((port) => { const tabId = port.sender.tab.id if (!hub.has(tabId)) { hub.set(tabId, port) port.onMessage.addListener((message) => { broadcast({ from: tabId, payload: message }) }) port.onDisconnect.addListener(() => { //TODO - Should we log? hub.delete(tabId) }) } }) } // Only usable by BGSW export const broadcast = (pubSubMessage: PubSubMessage) => { const hub = getHubMap() hub.forEach((port, tabId) => { const skipBroadcast = tabId === pubSubMessage.from if (skipBroadcast) { return } port.postMessage({ ...pubSubMessage, to: tabId }) }) } export const connectToHub = (extensionId: string) => { const runtime = getExtRuntime() if (!runtime.connect) { throw new Error( "runtime.connect not available. You need to use startHub in BGSW" ) } const port = runtime.connect(extensionId) return port } ================================================ FILE: api/messaging/src/relay.test.ts ================================================ import { beforeEach, describe, expect, jest, test } from "@jest/globals" import type { PlasmoMessaging } from "./types" const { relay, sendViaRelay } = await import("./relay") class MessagePortMock { callbacks = new Set() addEventListener = (_message, callback) => { this.callbacks.add(callback) } removeEventListener = (_message, callback) => { this.callbacks.delete(callback) } postMessage = (data) => { const event = { data, source: globalThis.window } this.callbacks.forEach((callback) => { callback(event) }) } clear() { this.callbacks.clear() } } /** * Message port callbacks happen synchronously * But promises get resolved in event queue */ const waitForMicroTasks = () => Promise.resolve() describe("sendViaRelay", () => { const port = new MessagePortMock() const req: PlasmoMessaging.Request = { name: "test", body: { foo: "bar" }, relayId: "1" } beforeEach(() => { port.clear() }) test("posts message to provided message port", (done) => { port.addEventListener("message", (event) => { expect(event.data).toMatchObject(req) done() }) sendViaRelay(req, port) expect(port.callbacks.size).toBe(2) }) test("appends random instanceId to relayed request", (done) => { port.addEventListener("message", (event) => { expect(Object.hasOwn(event.data, "instanceId")).toBeTruthy() done() }) sendViaRelay(req, port) }) test("only resolves body with matching instanceId", (done) => { let response = null port.addEventListener("message", async (event) => { if (event.data.relayed) { return } port.postMessage({ ...event.data, body: { bar: "foo" }, relayed: true, instanceId: "123" }) await waitForMicroTasks() expect(response).toEqual(null) port.postMessage({ ...event.data, body: { bar: "foo" }, relayed: true }) await waitForMicroTasks() expect(response).toEqual({ bar: "foo" }) done() }) sendViaRelay(req, port).then((res) => (response = res)) }) }) describe("relay", () => { const port = new MessagePortMock() const req: PlasmoMessaging.Request = { name: "test", relayId: "1" } const handler = (data) => { return Promise.resolve({ echo: data.body }) } beforeEach(() => { port.clear() }) test("returns cleanup function", () => { const cleanup = relay(req, handler, port) expect(port.callbacks.size).toBe(1) cleanup() expect(port.callbacks.size).toBe(0) }) test("does not handle relayed messages", () => { const notCalledHandler = jest.fn((data) => Promise.resolve(data)) relay(req, notCalledHandler, port) port.postMessage({ ...req, relayed: true }) expect(notCalledHandler).not.toBeCalled() }) test("posts back resolution result and instanceId", (done) => { relay(req, handler, port) const mockedReq = { ...req, instanceId: "123", body: { foo: "bar" } } port.addEventListener("message", async (event) => { if (!event.data.relayed) { return } expect(event.data.body).toEqual({ echo: mockedReq.body }) expect(event.data.instanceId).toEqual(mockedReq.instanceId) done() }) port.postMessage(mockedReq) }) }) ================================================ FILE: api/messaging/src/relay.ts ================================================ import { nanoid } from "nanoid" import type { PlasmoMessaging } from "./index" import { isSameOrigin } from "./utils" /** * Raw relay abstracting window.postMessage */ export const relay: PlasmoMessaging.RelayFx = ( req, onMessage, messagePort = globalThis.window ) => { const relayHandler = async ( event: MessageEvent ) => { if (isSameOrigin(event, req) && !event.data.relayed) { const relayPayload = { name: req.name, relayId: req.relayId, body: event.data.body } const backgroundResponse = await onMessage?.(relayPayload) messagePort.postMessage( { name: req.name, relayId: req.relayId, instanceId: event.data.instanceId, body: backgroundResponse, relayed: true }, { targetOrigin: req.targetOrigin || "/" } ) } } messagePort.addEventListener("message", relayHandler) return () => messagePort.removeEventListener("message", relayHandler) } export const sendViaRelay: PlasmoMessaging.SendFx = ( req, messagePort = globalThis.window ) => new Promise((resolve, _reject) => { const instanceId = nanoid() const abortController = new AbortController() messagePort.addEventListener( "message", (event: MessageEvent) => { if ( isSameOrigin(event, req) && event.data.relayed && event.data.instanceId === instanceId ) { resolve(event.data.body) abortController.abort() } }, { signal: abortController.signal } ) messagePort.postMessage( { ...req, instanceId } as PlasmoMessaging.RelayMessage, { targetOrigin: req.targetOrigin || "/" } ) }) ================================================ FILE: api/messaging/src/types.ts ================================================ export interface MessagesMetadata {} export interface PortsMetadata {} export type MessageName = keyof MessagesMetadata export type PortName = keyof PortsMetadata export type InternalSignal = "__PLASMO_MESSAGING_PING__" export namespace PlasmoMessaging { export type Request = { name: TName extensionId?: string port?: chrome.runtime.Port sender?: chrome.runtime.MessageSender body?: TBody tabId?: number relayId?: string // Target origin to send the message to (for relay), default to "/" targetOrigin?: string } export type RelayMessage = Request & { /** * Used to resolve corresponding window.postMessage messages */ instanceId: string relayed: boolean } export type InternalRequest = { __PLASMO_INTERNAL_SIGNAL__: InternalSignal } export type Response = { send: (body: TBody) => void } export type InternalHandler = (request: InternalRequest) => void export type Handler< RequestName = string, RequestBody = any, ResponseBody = any > = ( request: Request, response: Response ) => void | Promise | boolean export type PortHandler = Handler< PortName, RequestBody, ResponseBody > export type MessageHandler = Handler< MessageName, RequestBody, ResponseBody > export interface SendFx { ( request: Request, messagePort?: | Pick< MessagePort, "addEventListener" | "removeEventListener" | "postMessage" > | Window ): Promise } export interface RelayFx { ( request: Request, onMessage?: ( request: Request ) => Promise, messagePort?: | Pick< MessagePort, "addEventListener" | "removeEventListener" | "postMessage" > | Window ): () => void } export interface MessageRelayFx { (request: Request): () => void } export interface PortHook { , TResponseBody = any>( name: PortName ): { data?: TResponseBody send: (payload: TRequestBody) => void listen: ( handler: (msg: T) => void ) => { port: chrome.runtime.Port disconnect: () => void } } } } export type OriginContext = | "background" | "extension-page" | "sandbox-page" | "content-script" | "window" ================================================ FILE: api/messaging/src/utils.ts ================================================ import type { PlasmoMessaging } from "./index" const extTabs = (globalThis.browser?.tabs || globalThis.chrome?.tabs) as typeof chrome.tabs export const getExtRuntime = () => { // TODO: Move this to a broader utils package later on const extRuntime = (globalThis.browser?.runtime || globalThis.chrome?.runtime) as typeof chrome.runtime if (!extRuntime) { throw new Error("Extension runtime is not available") } return extRuntime } export const getExtTabs = () => { if (!extTabs) { throw new Error("Extension tabs API is not available") } return extTabs } export const getActiveTab = async () => { const extTabs = getExtTabs() const [tab] = await extTabs.query({ active: true, currentWindow: true }) return tab as chrome.tabs.Tab | undefined } export const isSameOrigin = ( event: MessageEvent, req: any ): req is PlasmoMessaging.Request => !req.__internal && event.source === globalThis.window && event.data.name === req.name && (req.relayId === undefined || event.data.relayId === req.relayId) export const getRuntimeContext = () => { // If chrome API available but they cannot access // OR we can mark them directly (?), by injecting a tag at runtime itself } ================================================ FILE: api/messaging/tsconfig.json ================================================ { "compilerOptions": { "outDir": "dist", "resolveJsonModule": true, "sourceMap": true, "strict": false, "declaration": true, "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, "noImplicitReturns": true, "esModuleInterop": true, "noImplicitAny": false, "moduleResolution": "node", "module": "ESNext", "target": "ESNext", "allowJs": true, "verbatimModuleSyntax": true }, "include": ["src/**/*.ts"] } ================================================ FILE: api/persistent/.gitignore ================================================ node_modules # Lockfiles - See https://github.com/PlasmoHQ/p1asm0 pnpm-lock.yaml package-lock.json yarn.lock .turbo key.json dist/ ================================================ FILE: api/persistent/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: api/persistent/README.md ================================================ # Plasmo Persistent runtime This library contains a couple of hacks to keep the BGSW alive for MV3 transitioning. Usage in a background service worker: ```ts import { keepAlive } from "@plasmohq/persistent/background" keepAlive() ``` ================================================ FILE: api/persistent/package.json ================================================ { "name": "@plasmohq/persistent", "version": "0.0.6", "description": "A couple of hacks to keep the BGSW alive in a library", "type": "module", "module": "./src/index.ts", "types": "./src/index.ts", "typesVersions": { "*": { "background": [ "./src/background.ts" ] } }, "publishConfig": { "module": "./dist/index.js", "types": "./dist/index.d.ts", "typesVersions": { "*": { "background": [ "./dist/background.d.ts" ] } } }, "exports": { "./background": { "types": "./dist/background.d.ts", "import": "./dist/background.js", "require": "./dist/background.cjs" }, ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" } }, "files": [ "dist" ], "scripts": { "dev": "run-p dev:*", "dev:compile": "tsup --watch", "dev:test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --watch", "build": "tsup", "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest", "prepublishOnly": "pnpm build" }, "author": "Plasmo Corp. ", "contributors": [ "@louisgv" ], "repository": { "type": "git", "url": "https://github.com/PlasmoHQ/plasmo.git", "directory": "api/persistent" }, "license": "MIT", "keywords": [ "browser-extension", "chrome-extension" ], "devDependencies": { "@jest/globals": "29.7.0", "@jest/types": "29.6.3", "@plasmo/config": "workspace:*", "@types/chrome": "0.0.312", "@types/node": "22.13.13", "cross-env": "7.0.3", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "ts-jest": "29.3.0", "tsup": "8.4.0", "typescript": "5.8.2" } } ================================================ FILE: api/persistent/src/background.ts ================================================ export const keepAlive = () => { const extRuntime = (globalThis.browser?.runtime || globalThis.chrome?.runtime) as typeof chrome.runtime const _keepAlive = () => setInterval(extRuntime.getPlatformInfo, 24_000) // 24 seconds extRuntime.onStartup.addListener(_keepAlive) _keepAlive() } ================================================ FILE: api/persistent/src/index.ts ================================================ export const life = 42 ================================================ FILE: api/persistent/tsconfig.json ================================================ { "extends": "@plasmo/config/ts/utils", "include": ["src/**/*.ts"] } ================================================ FILE: api/persistent/tsup.config.ts ================================================ import { defineConfig } from "tsup" export default defineConfig((opt) => { const isProd = !opt.watch return { entry: ["src/index.ts", "src/background.ts"], format: ["esm", "cjs"], target: "esnext", platform: "node", splitting: false, bundle: true, dts: true, watch: opt.watch, sourcemap: !isProd, minify: isProd, clean: isProd } }) ================================================ FILE: api/selector/.gitignore ================================================ node_modules # Lockfiles - See https://github.com/PlasmoHQ/p1asm0 pnpm-lock.yaml package-lock.json yarn.lock .turbo key.json dist/ ================================================ FILE: api/selector/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: api/selector/package.json ================================================ { "name": "@plasmohq/selector", "version": "0.0.7", "description": "Powerful Selector API with Dedicated Monitoring Supports", "type": "module", "module": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { "./hook": { "types": "./dist/hook.d.ts", "import": "./dist/hook.js", "require": "./dist/hook.cjs" }, "./monitor": { "types": "./dist/monitor.d.ts", "import": "./dist/monitor.js", "require": "./dist/monitor.cjs" }, "./background": { "types": "./dist/background.d.ts", "import": "./dist/background.js", "require": "./dist/background.cjs" }, ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" } }, "typesVersions": { "*": { "hook": [ "./dist/hook.d.ts" ], "monitor": [ "./dist/monitor.d.ts" ], "background": [ "./dist/background.d.ts" ] } }, "files": [ "dist" ], "scripts": { "dev": "run-p dev:*", "dev:compile": "tsup --watch", "dev:test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --watch", "build": "tsup", "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest", "prepublishOnly": "pnpm build" }, "author": "Plasmo Corp. ", "contributors": [ "@louisgv", "@ColdSauce" ], "repository": { "type": "git", "url": "https://github.com/PlasmoHQ/plasmo.git", "directory": "api/selector" }, "license": "MIT", "keywords": [ "react-hook", "browser-extension", "chrome-extension" ], "peerDependencies": { "react": "^16.8.6 || ^17 || ^18 || ^19.0.0" }, "peerDependenciesMeta": { "react": { "optional": true } }, "devDependencies": { "@jest/globals": "29.7.0", "@jest/types": "29.6.3", "@testing-library/react": "16.2.0", "@types/chrome": "0.0.312", "@types/node": "22.13.13", "@types/react": "19.0.12", "cross-env": "7.0.3", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "react": "19.0.0", "react-dom": "19.0.0", "rimraf": "6.0.1", "ts-jest": "29.3.0", "tsup": "8.4.0", "typescript": "5.8.2" } } ================================================ FILE: api/selector/src/background.ts ================================================ import type { SelectorMessage } from "./types" // Simple cache, it won't persist, but it will do for now const softCache = new Set() async function selectorMessageHandler( message: SelectorMessage, monitorId: string, sample: number ) { switch (message.name) { case "plasmo:selector:invalid": { if (!monitorId) { return } const body = JSON.stringify({ monitorId, payload: message.payload }) if (softCache.has(body) || Math.random() > sample) { return } try { softCache.add(body) await fetch( `${process.env.ITERO_MONITOR_API_BASE_URI}/api/selector/invalid`, { method: "POST", headers: { "Content-Type": "application/json" }, body } ) } catch {} } } } /** * @param monitorId id of the monitor to send invalid selectors to * @param sample percentage of invalid selectors to send to the monitor, default 47% */ export const init = ({ monitorId = "", sample = 0.47 }) => { chrome.runtime.onMessage.addListener((message: SelectorMessage) => { selectorMessageHandler(message, monitorId, sample) return true }) } ================================================ FILE: api/selector/src/hook.ts ================================================ export const useSelector = () => {} ================================================ FILE: api/selector/src/index.ts ================================================ import type { SelectorMessage } from "./types" async function sendInvalidSelectors(selectors: string[]) { try { return await chrome?.runtime?.sendMessage({ name: "plasmo:selector:invalid", payload: { selectors, url: window.location.href } } as SelectorMessage) } catch {} } export const querySelectors = (selectors: string[]) => { const result: Element[] = [] const invalidSelectors: string[] = [] for (const selector of selectors) { const element = document.querySelector(selector) if (!element) { invalidSelectors.push(selector) } else { result.push(element) } } if (invalidSelectors.length > 0) { sendInvalidSelectors(invalidSelectors) } return result } export const querySelector = (selector: string) => { const element = document.querySelector(selector) if (!element) { sendInvalidSelectors([selector]) } return element } export const querySelectorAll = (selector: string) => { const elements = document.querySelectorAll(selector) if (elements.length === 0) { sendInvalidSelectors([selector]) } return elements } ================================================ FILE: api/selector/src/monitor.ts ================================================ export {} document.querySelector = new Proxy(document.querySelector, { apply: (target, thisArg, args) => {} }) ================================================ FILE: api/selector/src/types.ts ================================================ export type SelectorMessage = { name: "plasmo:selector:invalid" payload: { selectors: string[] url: string } } ================================================ FILE: api/selector/tsconfig.json ================================================ { "compilerOptions": { "outDir": "dist", "resolveJsonModule": true, "sourceMap": true, "strict": false, "declaration": true, "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, "noImplicitReturns": true, "esModuleInterop": true, "noImplicitAny": false, "moduleResolution": "node", "module": "ESNext", "target": "ESNext", "allowJs": true, "verbatimModuleSyntax": true, "lib": ["DOM"] }, "include": ["src/**/*.ts"] } ================================================ FILE: api/selector/tsup.config.ts ================================================ import { defineConfig } from "tsup" export default defineConfig((opt) => { const isProd = !opt.watch return { entry: [ "src/index.ts", "src/monitor.ts", "src/background.ts", "src/hook.ts" ], format: ["esm", "cjs"], target: "esnext", platform: "node", splitting: false, bundle: true, dts: true, env: { ITERO_MONITOR_API_BASE_URI: isProd ? "https://itero.plasmo.com" : "http://localhost:3000" }, watch: opt.watch, sourcemap: !isProd, minify: isProd, clean: isProd } }) ================================================ FILE: cli/create-plasmo/bin/index.mjs ================================================ #!/usr/bin/env node import "../dist/index.js" ================================================ FILE: cli/create-plasmo/package.json ================================================ { "name": "create-plasmo", "version": "0.90.5", "description": "Create Plasmo Framework Browser Extension", "main": "dist/index.js", "bin": "bin/index.mjs", "type": "module", "files": [ "bin", "dist" ], "tsup": { "format": "esm", "target": "esnext", "platform": "node", "splitting": false, "bundle": true, "minify": true, "clean": true, "banner": { "js": "import { createRequire } from 'module';const require = createRequire(import.meta.url);" } }, "scripts": { "build": "tsup src/index.ts", "prepublishOnly": "run-s build" }, "author": "Plasmo Corp. ", "homepage": "https://docs.plasmo.com/", "repository": { "type": "git", "url": "https://github.com/PlasmoHQ/plasmo.git", "directory": "cli/create-plasmo" }, "license": "MIT", "keywords": [ "plasmo", "browser-extensions", "framework" ], "dependencies": { "@plasmohq/init": "workspace:*" }, "devDependencies": { "@plasmo/config": "workspace:*", "@plasmo/constants": "workspace:*", "@plasmo/utils": "workspace:*", "plasmo": "workspace:*", "typescript": "5.8.2" } } ================================================ FILE: cli/create-plasmo/src/commands.ts ================================================ export const validCommandList = [] ================================================ FILE: cli/create-plasmo/src/index.ts ================================================ #!/usr/bin/env node import { argv, exit } from "process" import { version } from "plasmo/package.json" import init from "plasmo/src/commands/init" import { ErrorMessage } from "@plasmo/constants/error" import { aLog, eLog } from "@plasmo/utils/logging" import { exitCountDown } from "@plasmo/utils/wait" process.env.APP_VERSION = version async function main() { try { // In case someone pasted an essay into the cli if (argv.length > 10) { throw new Error(ErrorMessage.TooManyArg) } argv.splice(2, 0, "init") await init() } catch (e) { eLog((e as Error).message || ErrorMessage.Unknown) aLog(e.stack) await exitCountDown(3) exit(1) } } main() process.on("SIGINT", () => exit(0)) process.on("SIGTERM", () => exit(0)) ================================================ FILE: cli/create-plasmo/tsconfig.json ================================================ { "extends": "@plasmo/config/ts/cli.json", "include": ["src/**/*.ts"], "exclude": ["dist", "node_modules", "templates"], "compilerOptions": { "outDir": "dist", "baseUrl": ".", "paths": { "~features/*": ["../plasmo/src/features/*"], "~commands": ["./src/commands"] } } } ================================================ FILE: cli/plasmo/.eslintrc.js ================================================ module.exports = require("@plasmo/config/eslint-preset") ================================================ FILE: cli/plasmo/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: cli/plasmo/README.md ================================================

plasmo logo

See License NPM Install Follow PlasmoHQ on Twitter Watch our Live DEMO every Friday Join our Discord for support and chat about our projects

English | 简体中文 | Tiếng Việt | Deutsch | French | Indonesian | Русский | Turkish | 日本語 | 한국어

**Production Cloud:** We've built a cloud offering for browser extensions called [Itero](https://itero.plasmo.com). Check it out if you want instant beta testing and more awesome features. # Plasmo Framework The [Plasmo](https://www.plasmo.com/) Framework is a battery-packed browser extension SDK made by hackers for hackers. Build your product and stop worrying about config files and the odd peculiarities of building browser extensions. > It's like [Next.js](https://nextjs.org/) for browser extensions! ![CLI Demo](https://www.plasmo.com/assets/plasmo-cli-demo.gif) ## Highlighted Features - First-class [React](https://reactjs.org/) + [Typescript](https://www.typescriptlang.org/) Support - [Declarative Development](https://docs.plasmo.com/framework#where-is-the-manifestjson-file) - [Content Scripts UI](https://docs.plasmo.com/csui) - [Tab Pages](https://docs.plasmo.com/framework/tab-pages) - Live-reloading + React HMR - [`.env*` files](https://docs.plasmo.com/framework/env) - [Storage API](https://docs.plasmo.com/framework/storage) - [Messaging API](https://docs.plasmo.com/framework/messaging) - [Remote code bundling](https://docs.plasmo.com/framework/remote-code) (e.g., for Google Analytics) - Targeting [multiple browser and manifest pairs](https://docs.plasmo.com/framework/workflows/build#with-specific-target) - [Automated deployment via BPP](https://docs.plasmo.com/framework/workflows/submit) - Optional support for [Svelte](https://github.com/PlasmoHQ/with-svelte) and [Vue](https://github.com/PlasmoHQ/with-vue) And many, many more! 🚀 ## System Requirements - Node.js 16.x or later - MacOS, Windows, or Linux - (Strongly Recommended) [pnpm](https://pnpm.io/) ## Examples We have examples showcasing how one can use Plasmo with [Firebase Authentication](https://github.com/PlasmoHQ/examples/tree/main/with-firebase-auth), [Redux](https://github.com/PlasmoHQ/examples/tree/main/with-redux), [Supabase authentication](https://github.com/PlasmoHQ/examples/tree/main/with-supabase), [Tailwind](https://github.com/PlasmoHQ/examples/tree/main/with-tailwindcss), and many more. To check them out, [visit our examples repository](https://github.com/PlasmoHQ/examples). ## Documentation Check out the [documentation](https://docs.plasmo.com/) to get a more in-depth view into the Plasmo Framework. ## Browser Extensions Book For a more in-depth view into how browser extensions work, and how to develop them, we highly recommend Matt Frisbie's new book ["Building Browser Extensions"](https://buildingbrowserextensions.com/plasmo) ## Usage ``` pnpm create plasmo example-dir cd example-dir pnpm dev ``` The road ahead is filled with many turns. - Popup changes go in `popup.tsx` - Options page changes go in `options.tsx` - Content script changes go in `content.ts` - Background service worker changes go in `background.ts` ### Directories You can also organize these files in their own directories: ``` ext-dir ├───assets | └───icon.png ├───popup | ├───index.tsx | └───button.tsx ├───options | ├───index.tsx | ├───utils.ts | └───input.tsx ├───contents | ├───site-one.ts | ├───site-two.ts | └───site-three.ts ... ``` Finally, you can also avoid putting source code in your root directory by putting them in a `src` sub-directory, [following this guide](https://docs.plasmo.com/framework/customization/src). Note that `assets` and other config files will still need to be in the root directory. ## Supported Browsers To see a list of supported browser targets, [please refer to our documentation here](https://docs.plasmo.com/framework/workflows/faq#what-are-the-officially-supported-browser-targets). ## Community The Plasmo community can be found on [Discord](https://www.plasmo.com/s/d). This is the appropriate channel to get help with using the Plasmo Framework. Our [Code of Conduct](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CODE_OF_CONDUCT.md) applies to all Plasmo community channels. ## Contributing Please see the [contributing guidelines](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CONTRIBUTING.md) to learn more. A big thanks to all of our amazing [contributors](https://github.com/PlasmoHQ/plasmo/graphs/contributors) ❤️ Feel free to join the fun and send a PR! ### Plasmo Framework ### [Plasmo Examples](https://github.com/PlasmoHQ/examples) ### [Plasmo Storage](https://github.com/PlasmoHQ/storage) ### [Browser Platform Publisher](https://github.com/PlasmoHQ/bpp) ## Disclaimer Plasmo is currently alpha software, and some things might change from version to version, so please be mindful and use it at your own risk. # License [MIT](https://github.com/PlasmoHQ/plasmo/blob/main/LICENSE) ⭐ [Plasmo](https://www.plasmo.com) ================================================ FILE: cli/plasmo/bin/index.mjs ================================================ #!/usr/bin/env node import "../dist/index.js" ================================================ FILE: cli/plasmo/i18n/README.de-DE.md ================================================

plasmo logo

License anzeigen NPM Install Folge PlasmoHQ auf Twitter Schaue unsere Live DEMO jeden Freitag an Trete unserem Discord server bei, um zu chatten und Unterstützung für Projekte zu bekommen

English | 简体中文 | Tiếng Việt | Deutsch | French | Indonesian | Русский | Turkish | 日本語 | 한국어

# Plasmo Framework Das [Plasmo](https://www.plasmo.com/) Framework ist ein SDK zum Erstellen von Browser-Erweiterungen, das von Hackern für Hacker entwickelt wurde. Erstelle dein Produkt, ohne dir Gedanken über Konfigurationsdateien und die seltsamen Eigenheiten der Erstellung von Browsererweiterungen machen zu müssen. > Es ist wie [Next.js](https://nextjs.org/) für Browser-Erweiterungen! ![CLI Demo](https://www.plasmo.com/assets/plasmo-cli-demo.gif) ## Features - Direkte Unterstützung von [React](https://reactjs.org/) + [Typescript](https://www.typescriptlang.org/) - [Deklarative Entwicklung mit automatischer Erzeugung von "manifest.json" (MV3)](https://docs.plasmo.com/framework#where-is-the-manifestjson-file) - Automatisches Neuladen - [`.env*` Datei-Unterstützung](https://docs.plasmo.com/framework/env) - [Bundling von externen Skripten](https://docs.plasmo.com/framework/workflows/remote-code) (z.B. für gtag4) - Automatisierte Bereitstellung (über [BPP](https://docs.plasmo.com/framework/workflows/submit)) - Und viel, viel mehr! 🚀 ## Systemanforderungen - Node.js 16.x oder neuer - MacOS, Windows oder Linux - (Stark empfohlen) [pnpm](https://pnpm.io/) ## Beispiele Wir haben Beispiele, die zeigen, wie man Plasmo mit [Firebase-Authentifizierung](https://github.com/PlasmoHQ/examples/tree/main/with-firebase-auth), [Redux](https://github.com/PlasmoHQ/examples/tree/main/with-redux), [Supabase-Authentifizierung](https://github.com/PlasmoHQ/examples/tree/main/with-supabase), [Tailwind](https://github.com/PlasmoHQ/examples/tree/main/with-tailwindcss) und vielen anderen verwenden kann. Um sie auszuprobieren, [besuche unser Beispiel-Repository](https://github.com/PlasmoHQ/examples). ## Dokumentation Schaue dir die [Dokumentation](https://docs.plasmo.com/) an, um einen tieferen Einblick in das Plasmo Framework zu erhalten. ## Nutzung ``` pnpm dlx plasmo init example-dir cd example-dir pnpm dev ``` Danach stehen dir alle Wege offen. - Popup-Änderungen kommen in `popup.tsx` - Änderungen an der Optionsseite kommen in `options.tsx`. - Änderungen am Inhaltsskript kommen in `content.ts` - Änderungen am Hintergrunddienst kommen in die Datei `background.ts`. ### Ordner-Struktur Du kannst die Dateien auch in eigenen Ordnern organisieren: ``` ext-dir ├───assets | └───icon512.png ├───popup | ├───index.tsx | └───button.tsx ├───options | ├───index.tsx | ├───utils.ts | └───input.tsx ├───contents | ├───site-one.ts | ├───site-two.ts | └───site-three.ts ... ``` Außerdem kannst du auch vermeiden, dass alle Dateien im Hauptverzeichnis liegen, wenn du sie in das Unterverzeichnis `src` legen, [indem du dieser Anleitung folgst](https://docs.plasmo.com/framework/customization/src). Beachte, dass `assets` und andere Konfigurationsdateien immer noch im Hauptverzeichnis liegen müssen. ## Community Die Plasmo-Community ist auf [Discord](https://www.plasmo.com/s/d) zu finden. Das ist der richtige Kanal, um Hilfe bei der Verwendung des Plasmo-Frameworks zu erhalten. Unser [Verhaltenskodex](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CODE_OF_CONDUCT.md) gilt für alle Plasmo Community-Kanäle. ## Am Projekt beteiligen Schaue dir die [Richtlinien](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CONTRIBUTING.md) an, um mehr zu erfahren. ## Information Plasmo ist derzeit eine Alphasoftware, und einige Dinge können sich von Version zu Version ändern. Sei also bitte achtsam und benutze es auf eigenes Risiko. # Lizenz [MIT](https://github.com/PlasmoHQ/plasmo/blob/main/LICENSE) ⭐ [Plasmo](https://www.plasmo.com) ================================================ FILE: cli/plasmo/i18n/README.fr-FR.md ================================================

plasmo logo

License anzeigen NPM Install Folge PlasmoHQ auf Twitter Schaue unsere Live DEMO jeden Freitag an Trete unserem Discord server bei, um zu chatten und Unterstützung für Projekte zu bekommen

English | 简体中文 | Tiếng Việt | Deutsch | French | Indonesian | Русский | Turkish | 日本語 | 한국어

# Plasmo Framework Le [Plasmo](https://www.plasmo.com/) Framework est un SDK pour la création d'extensions de navigateur, développé par des hackers pour des hackers. Créez votre produit sans vous soucier des fichiers de configuration et des étranges particularités de la création d'extensions de navigateur. > C'est comme [Next.js](https://nextjs.org/) pour les extensions de navigateur ! ![CLI Demo](https://www.plasmo.com/assets/plasmo-cli-demo.gif) ## Fonctionnalités - Prise en charge de [React](https://reactjs.org/) + [Typescript](https://www.typescriptlang.org/) de première classe - [Développement déclaratif avec création automatique de "manifest.json" (MV3)](https://docs.plasmo.com/framework#where-is-the-manifestjson-file) - Chargement en temps réel - [Content Scripts UI](https://docs.plasmo.com/csui) - [Fichiers `.env*`](https://docs.plasmo.com/framework/env) - [Regroupement de codes distants](https://docs.plasmo.com/framework/remote-code) (par exemple pour gtag4) - Cibler [plusieurs paires de navigateurs et de manifestes](https://docs.plasmo.com/framework/workflows/build#with-specific-target) - [Déploiement automatisé via BPP](https://docs.plasmo.com/framework/workflows/submit) - [Svelte](https://github.com/PlasmoHQ/with-svelte) ou [Vue](https://github.com/PlasmoHQ/with-vue) - Et beaucoup, beaucoup plus! 🚀 ## Configuration requise - Node.js 16.x ou plus récent - MacOS, Windows ou Linux - (Fortement recommandé) [pnpm](https://pnpm.io/) ## Examples Nous avons des exemples qui montrent comment utiliser Plasmo avec [l'authentification Firebase](https://github.com/PlasmoHQ/examples/tree/main/with-firebase-auth), [Redux](https://github.com/PlasmoHQ/examples/tree/main/with-redux), [Supabase authentication](https://github.com/PlasmoHQ/examples/tree/main/with-supabase), [Tailwind](https://github.com/PlasmoHQ/examples/tree/main/with-tailwindcss), et bien d'autres. Pour les essayer, [visitez notre référentiel d'exemples](https://github.com/PlasmoHQ/examples). ## Documentation Consultez la [documentation](https://docs.plasmo.com/) pour obtenir une vue plus approfondie du cadre Plasmo. ## Utilisation ``` pnpm create plasmo example-dir cd example-dir pnpm dev ``` La route qui nous attend est pleine de virages. - Les modifications de popup viennent dans `popup.tsx`. - Les modifications de la page d'options viennent dans `options.tsx`. - Les modifications du script de contenu se trouvent dans `content.ts`. - Les modifications du service d'arrière-plan vont dans le fichier `background.ts`. ### Structure des dossiers Vous pouvez également organiser ces fichiers dans leurs propres répertoires : ``` ext-dir ├───assets | └───icon512.png ├───popup | ├───index.tsx | └───button.tsx ├───options | ├───index.tsx | ├───utils.ts | └───input.tsx ├───contents | ├───site-one.ts | ├───site-two.ts | └───site-three.ts ... ``` Enfin, vous pouvez aussi éviter de placer le code source dans votre répertoire racine en le plaçant dans un sous-répertoire `src`, [en suivant ce guide](https://docs.plasmo.com/framework/customization/src). Notez que `assets` et les autres fichiers de configuration devront toujours être dans le répertoire racine. ## Communauté La communauté Plasmo se trouve sur [Discord](https://www.plasmo.com/s/d). C'est le réseau approprié pour obtenir de l'aide sur l'utilisation du cadre Plasmo. Notre [code de conduite](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CODE_OF_CONDUCT.md) s'applique à tous les canaux de la communauté Plasmo. ## Contribution Veuillez consulter les [directives de contribution](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CONTRIBUTING.md) pour en savoir plus. ## Avis de non-responsabilité Plasmo est actuellement un logiciel alpha, et certaines choses peuvent changer d'une version à l'autre, alors soyez attentifs et utilisez-le à vos propres risques. # License [MIT](https://github.com/PlasmoHQ/plasmo/blob/main/LICENSE) ⭐ [Plasmo](https://www.plasmo.com) ================================================ FILE: cli/plasmo/i18n/README.id-ID.md ================================================

plasmo logo

See License NPM Install Follow PlasmoHQ on Twitter Watch our Live DEMO every Friday Join our Discord for support and chat about our projects

English | 简体中文 | Tiếng Việt | Deutsch | French | Indonesian | Русский | Turkish | 日本語 | 한국어

# Framework Plasmo Framework [Plasmo](https://www.plasmo.com/) adalah SDK ekstensi browser penuh daya yang dibuat oleh hacker untuk hacker. Bangun produk Anda dan berhenti khawatir terhadap file konfigurasi dan berbagai macam anomali dalam membuat ekstensi browser. > Seperti [Next.js](https://nextjs.org/) untuk ekstensi browser! ![CLI Demo](https://www.plasmo.com/assets/plasmo-cli-demo.gif) ## Fitur Utama - First-class [React](https://reactjs.org/) + [Typescript](https://www.typescriptlang.org/) Support - [Declarative Development](https://docs.plasmo.com/framework#where-is-the-manifestjson-file) - [Content Scripts UI](https://docs.plasmo.com/csui) - [Tab Pages](https://docs.plasmo.com/framework/tab-pages) - Live-reloading + React HMR - [`.env*` files](https://docs.plasmo.com/framework/env) - [Storage API](https://docs.plasmo.com/framework/storage) - [Messaging API](https://docs.plasmo.com/framework/messaging) - [Remote code bundling](https://docs.plasmo.com/framework/remote-code) (contohnya: untuk Google Analytics) - Penargetan [beberapa browser dan manifest](https://docs.plasmo.com/framework/workflows/build#with-specific-target) - [Deployment otomatis melalui BPP](https://docs.plasmo.com/framework/workflows/submit) - Dukungan opsional untuk [Svelte](https://github.com/PlasmoHQ/with-svelte) dan [Vue](https://github.com/PlasmoHQ/with-vue) Dan masih banyak lagi! 🚀 ## Kebutuhan Sistem - Node.js 16.x atau lebih tinggi - MacOS, Windows, atau Linux - (Sangat direkomendasikan) [pnpm](https://pnpm.io/) ## Examples Kami memiliki contoh yang menunjukkan bagaimana anda dapat menggunakan Plasmo [Firebase Authentication](https://github.com/PlasmoHQ/examples/tree/main/with-firebase-auth), [Redux](https://github.com/PlasmoHQ/examples/tree/main/with-redux), [Supabase authentication](https://github.com/PlasmoHQ/examples/tree/main/with-supabase), [Tailwind](https://github.com/PlasmoHQ/examples/tree/main/with-tailwindcss), dan banyak masih banyak lagi. Untuk mencobanya, [kunjungi repositori contoh kami](https://github.com/PlasmoHQ/examples). ## Dokumentasi Lihat [dokumentasi](https://docs.plasmo.com/) untuk mendapatkan gambaran yang lebih mendalam mengenai Framework Plasmo. ## Browser Extensions Book Untuk gambaran yang lebih mendalam tentang cara kerja ekstensi browser, dan cara mengembangkannya, kami sangat merekomendasikan buku baru Matt Frisbie ["Building Browser Extensions"](https://buildingbrowserextensions.com/plasmo) ## Cara Penggunaan ``` pnpm create plasmo example-dir cd example-dir pnpm dev ``` Jalan di depan dipenuhi dengan banyak pilihan. - Mengubah Popup lakukan di `popup.tsx` - Mengubah Options page lakukan di `options.tsx` - Mengubah Content script lakukan di `content.ts` - Mengubah Background service worker lakukan di `background.ts` ### Direktori Anda juga dapat mengatur file-file ini di direktori mereka sendiri: ``` ext-dir ├───assets | └───icon.png ├───popup | ├───index.tsx | └───button.tsx ├───options | ├───index.tsx | ├───utils.ts | └───input.tsx ├───contents | ├───site-one.ts | ├───site-two.ts | └───site-three.ts ... ``` Terakhir, Anda juga dapat menghindari menempatkan kode sumber di direktori root dengan menempatkannya di sub-direktori `src`, [mengikuti panduan ini](https://docs.plasmo.com/framework/customization/src). Perhatikan bahwa `assets` dan file konfigurasi lainnya masih perlu berada di direktori root. ## Browser yang Didukung Untuk melihat daftar target browser yang didukung, [lihat dokumentasi kami di sini](https://docs.plasmo.com/framework/workflows/faq#what-are-the-officially-supported-browser-targets). ## Komunitas Komunitas Plasmo dapat ditemukan di [Discord](https://www.plasmo.com/s/d). Ini adalah saluran yang tepat untuk mendapatkan bantuan dalam menggunakan Plasmo Framework. [Pedoman Perilaku](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CODE_OF_CONDUCT.md) kami berlaku untuk semua saluran komunitas Plasmo. ## Berkontribusi Silahkan lihat [pedoman kontribusi](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CONTRIBUTING.md) untuk mempelajari lebih lanjut. Terima kasih banyak untuk semua yang luar biasa [kontributor](https://github.com/PlasmoHQ/plasmo/graphs/contributors) ❤️ Jangan ragu untuk ikut bersenang-senang dan mengirim PR! ### Framework Plasmo ### [Plasmo Examples](https://github.com/PlasmoHQ/examples) ### [Plasmo Storage](https://github.com/PlasmoHQ/storage) ### [Browser Platform Publisher](https://github.com/PlasmoHQ/bpp) ## Disclaimer Plasmo saat ini adalah perangkat lunak alpha, dan beberapa hal mungkin berubah dari versi ke versi, jadi harap berhati-hati dan gunakan dengan risiko Anda sendiri. # Lisensi [MIT](https://github.com/PlasmoHQ/plasmo/blob/main/LICENSE) ⭐ [Plasmo](https://www.plasmo.com) ================================================ FILE: cli/plasmo/i18n/README.ja-JP.md ================================================

plasmo logo

See License NPM Install Follow PlasmoHQ on Twitter Watch our Live DEMO every Friday Join our Discord for support and chat about our projects

English | 简体中文 | Tiếng Việt | Deutsch | French | Indonesian | Русский | Turkish | 日本語 | 한국어

**Production Cloud:** 私たちはブラウザ拡張機能向けのクラウドサービス「Itero」を開始しました。即時のベータテストやより素晴らしい機能が必要なら、ぜひチェックしてください。 # Plasmo Framework [Plasmo](https://www.plasmo.com/) Framework は、すべての開発者のためのブラウザ拡張機能のSDKです。拡張機能のconfigファイルやビルドにおける面倒な独自仕様に悩まされずに拡張機能を作りましょう! > ブラウザ拡張機能における[Next.js](https://nextjs.org/) ![CLI Demo](https://www.plasmo.com/assets/plasmo-cli-demo.gif) ## 主な機能 - [React](https://reactjs.org/) + [Typescript](https://www.typescriptlang.org/) の全面サポート - [宣言型開発](https://docs.plasmo.com/framework#where-is-the-manifestjson-file) - [Contents Scripts UI](https://docs.plasmo.com/csui) - [Tab Pages](https://docs.plasmo.com/framework/tab-pages) - ライブリロード + React HMR - [`.env*` ファイル](https://docs.plasmo.com/framework/env) - [Storage API](https://docs.plasmo.com/framework/storage) - [Messaging API](https://docs.plasmo.com/framework/messaging) - [リモートコードバンドル](https://docs.plasmo.com/framework/remote-code) (Google Analyticsなど) - [複数ブラウザ・マニフェスト対応](https://docs.plasmo.com/framework/workflows/build#with-specific-target) - [BPPによる自動デプロイ](https://docs.plasmo.com/framework/workflows/submit) - [Svelte](https://github.com/PlasmoHQ/with-svelte)、 [Vue](https://github.com/PlasmoHQ/with-vue) にも対応 他にもたくさんの機能があります! 🚀 ## システム要件 - Node.js 16.x 以上 - MacOS, Windows, Linux のいずれか - [pnpm](https://pnpm.io/)(推奨) ## 例 [Firebase Authentication](https://github.com/PlasmoHQ/examples/tree/main/with-firebase-auth), [Redux](https://github.com/PlasmoHQ/examples/tree/main/with-redux), [Supabase authentication](https://github.com/PlasmoHQ/examples/tree/main/with-supabase), [Tailwind](https://github.com/PlasmoHQ/examples/tree/main/with-tailwindcss) などと組み合わせた例を[こちらのリポジトリ](https://github.com/PlasmoHQ/examples)で紹介しています。 ## ドキュメント さらに詳しく知りたい場合は、[ドキュメント](https://docs.plasmo.com/)をご覧ください。 ## ブラウザ拡張機能についての書籍 ブラウザ拡張機能の動作や開発方法についてさらに深く学びたい場合、Matt Frisbie氏の書籍[『Building Browser Extensions』](https://buildingbrowserextensions.com/plasmo)がおすすめです。 ## 使い方 ``` pnpm create plasmo example-dir cd example-dir pnpm dev ``` 変更したい部分によって、以下のファイルを編集してください。 - ポップアップ → `popup.tsx` - 設定ページ → `options.tsx` - コンテンツスクリプト → `content.ts` - バックグランドサービスワーカー → `background.ts` ### ディレクトリ構造 これらのファイルはそれぞれのディレクトリに分けて整理することもできます。 ``` ext-dir ├───assets | └───icon.png ├───popup | ├───index.tsx | └───button.tsx ├───options | ├───index.tsx | ├───utils.ts | └───input.tsx ├───contents | ├───site-one.ts | ├───site-two.ts | └───site-three.ts ... ``` また、ルートディレクトリに置きたくない場合は、`src` ディレクトリを作成して、そこにソースコードを置くこともできます。詳しくは[こちらのガイド](https://docs.plasmo.com/framework/customization/src)をご覧ください。 ただし、`assets` やconfigファイルはルートディレクトリに置く必要があります。 ## 対応しているブラウザ 対応しているブラウザのリストは、[こちらのドキュメント](https://docs.plasmo.com/framework/workflows/faq#what-are-the-officially-supported-browser-targets)をご覧ください。 ## コミュニティ [Discord](https://www.plasmo.com/s/d)にPlasmoのコミュニティがあります。Plasmo Framework に関するヘルプはこちらでお願いします。 [Code of Conduct](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CODE_OF_CONDUCT.md)は、全てのPlasmoコミュニティに適用されます。 ## コントリビュート 詳しくは[コントリビュートガイドライン](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CONTRIBUTING.md)をご覧ください。 素晴らしい[コントリビューターの方々](https://github.com/PlasmoHQ/plasmo/graphs/contributors)に感謝します❤️ ぜひ気軽に参加してPRを送ってください! ### Plasmo Framework ### [Plasmo Examples](https://github.com/PlasmoHQ/examples) ### [Plasmo Storage](https://github.com/PlasmoHQ/storage) ### [Browser Platform Publisher](https://github.com/PlasmoHQ/bpp) ## 免責事項 Plasmoは現在α版のソフトウェアです。バージョンアップによって変更される可能性がありますので、ご注意いただき自己責任で使用してください。 # ライセンス [MIT](https://github.com/PlasmoHQ/plasmo/blob/main/LICENSE) ⭐ [Plasmo](https://www.plasmo.com) ================================================ FILE: cli/plasmo/i18n/README.ko-KR.md ================================================

plasmo logo

See License NPM Install Follow PlasmoHQ on Twitter Watch our Live DEMO every Friday Join our Discord for support and chat about our projects

English | 简体中文 | Tiếng Việt | Deutsch | French | Indonesian | Русский | Turkish | 日本語 | 한국어

**Production Cloud:** [Itero](https://itero.plasmo.com)라는 브라우저 확장 프로그램용 클라우드 서비스를 구축했습니다. 즉시 베타 테스트 및 더 많은 멋진 기능을 원하시면 확인해보세요. # Plasmo 프레임워크 [Plasmo](https://www.plasmo.com/) 프레임워크는 개발자들을 위해 만들어진 강력한 브라우저 확장 프로그램 SDK입니다. 이를 통해 제품을 만들 때 설정 파일과 브라우저 확장 프로그램 개발의 특이한 부분에 대해 걱정하지 않고 진행할 수 있습니다. > 브라우저 확장 프로그램을 위한 [Next.js](https://nextjs.org/)와 같습니다! ![CLI Demo](https://www.plasmo.com/assets/plasmo-cli-demo.gif) ## 주요 기능 - [React](https://reactjs.org/) 및 [Typescript](https://www.typescriptlang.org/)를 위한 first-class 지원 - [선언적 개발](https://docs.plasmo.com/framework#where-is-the-manifestjson-file) - [콘텐츠 스크립트 UI](https://docs.plasmo.com/csui) - [탭 페이지](https://docs.plasmo.com/framework/tab-pages) - 라이브 리로딩 및 React HMR - [`.env*` 파일](https://docs.plasmo.com/framework/env) - [Storage API](https://docs.plasmo.com/framework/storage) - [Messaging API](https://docs.plasmo.com/framework/messaging) - [원격 코드 번들링](https://docs.plasmo.com/framework/remote-code) (ex. Google Analytics를 위한) - [여러 브라우저 및 매니페스트 타겟팅](https://docs.plasmo.com/framework/workflows/build#with-specific-target) - [BPP를 통한 자동 배포](https://docs.plasmo.com/framework/workflows/submit) - [Svelte](https://github.com/PlasmoHQ/with-svelte) 및 [Vue](https://github.com/PlasmoHQ/with-vue)의 선택적 지원 이 외에도 많은 기능이 있습니다! 🚀 ## 시스템 요구 사항 - Node.js 16.x 이상 - MacOS, Windows 또는 Linux - (매우 권장) [pnpm](https://pnpm.io/) ## 예제 [Firebase Authentication](https://github.com/PlasmoHQ/examples/tree/main/with-firebase-auth), [Redux](https://github.com/PlasmoHQ/examples/tree/main/with-redux), [Supabase authentication](https://github.com/PlasmoHQ/examples/tree/main/with-supabase), [Tailwind](https://github.com/PlasmoHQ/examples/tree/main/with-tailwindcss)와 함께 Plasmo를 사용하는 방법을 보여주는 예제를 제공하고 있습니다. 이를 확인하려면 [예제 레포지토리](https://github.com/PlasmoHQ/examples)를 방문해보세요. ## 문서 Plasmo 프레임워크에 대해 더 자세히 알아보려면 [문서](https://docs.plasmo.com/)를 확인하세요. ## 브라우저 확장 프로그램 관련 책 브라우저 확장 프로그램이 작동하는 방식과 개발하는 방법에 대해 더 자세히 알아보려면 Matt Frisbie의 새 책 ["Building Browser Extensions"](https://buildingbrowserextensions.com/plasmo)을 강력히 추천합니다. ## 사용법 ``` pnpm create plasmo example-dir cd example-dir pnpm dev ``` 변경하고자 하는 부분에 따라 아래 파일을 편집해 주세요. - 팝업 → `popup.tsx` 파일 - 옵션 페이지 → `options.tsx` - 콘텐츠 스크립트 → `content.ts` - 백그라운드 서비스 워커 → `background.ts` ### 폴더 구조 이 파일들을 각각의 디렉토리에 정리해서 넣을 수도 있습니다. ``` ext-dir ├───assets | └───icon.png ├───popup | ├───index.tsx | └───button.tsx ├───options | ├───index.tsx | ├───utils.ts | └───input.tsx ├───contents | ├───site-one.ts | ├───site-two.ts | └───site-three.ts ... ``` 마지막으로, 소스 코드를 루트 디렉토리에 넣지 않고 [이 가이드를 따라](https://docs.plasmo.com/framework/customization/src) `src` 하위 디렉토리에 넣을 수도 있습니다. 그러나 `assets` 및 기타 구성 파일은 여전히 루트 디렉토리에 있어야 합니다. ## 지원하는 브라우저 지원하는 브라우저 목록을 확인하려면 [이 문서](https://docs.plasmo.com/framework/workflows/faq#what-are-the-officially-supported-browser-targets)를 참조하세요. ## 커뮤니티 Plasmo 커뮤니티는 [Discord](https://www.plasmo.com/s/d)에서 찾을 수 있습니다. Plasmo 프레임워크 사용에 관한 도움을 받기에 적절한 채널입니다. [행동 강령(Code of Conduct)](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CODE_OF_CONDUCT.md)은 모든 Plasmo 커뮤니티 채널에 적용됩니다. ## 기여 자세한 내용은 [기여 가이드라인(Contributing Guidelines)](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CONTRIBUTING.md)을 참조하세요. 훌륭한 [컨트리뷰터](https://github.com/PlasmoHQ/plasmo/graphs/contributors) 여러분들께 큰 감사를 드립니다 ❤️ 자유롭게 참여하고 PR을 보내주세요! ### Plasmo Framework ### [Plasmo Examples](https://github.com/PlasmoHQ/examples) ### [Plasmo Storage](https://github.com/PlasmoHQ/storage) ### [Browser Platform Publisher](https://github.com/PlasmoHQ/bpp) ## 면책 조항 Plasmo는 현재 알파 버전의 소프트웨어이며, 버전 간에 일부 변경 사항이 있을 수 있으므로 주의하고 사용하십시오. # 라이선스 [MIT](https://github.com/PlasmoHQ/plasmo/blob/main/LICENSE) ⭐ [Plasmo](https://www.plasmo.com) ================================================ FILE: cli/plasmo/i18n/README.ru-RU.md ================================================

plasmo logo

See License NPM Install Follow PlasmoHQ on Twitter Watch our Live DEMO every Friday Join our Discord for support and chat about our projects

English | 简体中文 | Tiếng Việt | Deutsch | French | Indonesian | Русский | Turkish | 日本語 | 한국어

# Plasmo Framework Фреймворк [Plasmo](https://www.plasmo.com/) это SDK для разработки кроссплатформерных расширений для браузера, созданное хакерами для хакеров. Разрабатывайте расширения и перестаньте беспокоится о конфигах и специфичных особенностях браузерных расширений. > Это как [Next.js](https://nextjs.org/) для браузерных расширений! ![CLI Demo](https://www.plasmo.com/assets/plasmo-cli-demo.gif) ## Главные особенности - Первоклассная поддержка [React](https://reactjs.org/) + [Typescript](https://www.typescriptlang.org/) - [Декларативная настройка "manifest.json" (MV3)](https://docs.plasmo.com/framework#where-is-the-manifestjson-file) - [Контент скрипты с поддержкой UI](https://docs.plasmo.com/csui) - [Вкладки расширения](https://docs.plasmo.com/framework/tab-pages) - Активная перезагрузка + React HMR - [`.env*` файлы](https://docs.plasmo.com/framework/env) - [API для хранения информации](https://docs.plasmo.com/framework/storage) - [API для общения между различными частями расширения](https://docs.plasmo.com/framework/messaging) - [Подключение удаленного кода](https://docs.plasmo.com/framework/remote-code) (e.g., for Google Analytics) - Поддержка [кроссплатфоменности и различных видов манифеста](https://docs.plasmo.com/framework/workflows/build#with-specific-target) - [Автоматическое развертывание с помощью BPP](https://docs.plasmo.com/framework/workflows/submit) - Дополнительная поддержка [Svelte](https://github.com/PlasmoHQ/with-svelte) и [Vue](https://github.com/PlasmoHQ/with-vue) И многое, многое другое! 🚀 ## Системные требования - Node.js 16.x или выше - MacOS, Windows, или Linux - (Настоятельно рекомендуется) [pnpm](https://pnpm.io/) ## Примеры У нас есть примеры, показывающие, как можно использовать Plasmo с [Firebase Authentication](https://github.com/PlasmoHQ/examples/tree/main/with-firebase-auth), [Redux](https://github.com/PlasmoHQ/examples/tree/main/with-redux), [Supabase authentication](https://github.com/PlasmoHQ/examples/tree/main/with-supabase), [Tailwind](https://github.com/PlasmoHQ/examples/tree/main/with-tailwindcss), и многое другое. Чтобы посмотреть, [посетите наш репозиторий примеров](https://github.com/PlasmoHQ/examples). ## Документация Ознакомьтесь с [documentation](https://docs.plasmo.com/) чтобы получить более глубокое представление о Plasmo Framework. ## Книга расширений браузера Для более подробного ознакомления с тем, как работают расширения браузера и как их разрабатывать, мы настоятельно рекомендуем новую книгу Мэтта Фрисби ["Building Browser Extensions"](https://buildingbrowserextensions.com/plasmo) ## Использование ``` pnpm create plasmo example-dir cd example-dir pnpm dev ``` Дальнейший путь наполнен возможностями. - Изменение Popup в `popup.tsx` - Редактирование страницы настроек расширения в `options.tsx` - Настройка контент скриптов в `content.ts` - Изменение Background service worker в `background.ts` ### Каталоги Вы также можете структурировать эти файлы в собственных каталогах: ``` папка-расширения ├───assets | └───icon.png ├───popup | ├───index.tsx | └───button.tsx ├───options | ├───index.tsx | ├───utils.ts | └───input.tsx ├───contents | ├───site-one.ts | ├───site-two.ts | └───site-three.ts ... ``` Наконец, вы также можете избежать размещения исходного кода в вашем корневом каталоге, поместив их в подкаталог `src`, [следуя этому руководству](https://docs.plasmo.com/framework/customization/src). Обратите внимание что `assets` и другие конфигурационные файлы по-прежнему должны находиться в корневом каталоге. ## Поддерживаемые браузеры Чтобы просмотреть список поддерживаемых браузеров, [пожалуйста, обратитесь к нашей документации здесь](https://docs.plasmo.com/framework/workflows/faq#what-are-the-officially-supported-browser-targets). ## Сообщество Сообщество Plasmo можно найти в [Discord](https://www.plasmo.com/s/d). Это подходящий канал для получения помощи в использовании Plasmo Framework. Наш [Кодекс поведения](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CODE_OF_CONDUCT.md) применяется ко всем каналам сообщества Plasmo. ## Внести свой вклад Пожалуйста, ознакомьтесь с [рекомендациями по контрибьютингу](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CONTRIBUTING.md) чтобы узнать больше. Большое спасибо всем нашим удивительным [помощникам](https://github.com/PlasmoHQ/plasmo/graphs/contributors) ❤️ Не стесняйтесь присоединиться к веселью и отправить PR! ### Plasmo Framework ### [Примеры Plasmo](https://github.com/PlasmoHQ/examples) ### [Plasmo Storage](https://github.com/PlasmoHQ/storage) ### [Browser Platform Publisher](https://github.com/PlasmoHQ/bpp) ## Дисклеймер В настоящее время Plasmo является альфа-версией программного обеспечения, и некоторые вещи могут меняться от версии к версии, поэтому, пожалуйста, будьте внимательны и используйте его на свой страх и риск. # Лицензия [MIT](https://github.com/PlasmoHQ/plasmo/blob/main/LICENSE) ⭐ [Plasmo](https://www.plasmo.com) ================================================ FILE: cli/plasmo/i18n/README.tr-TR.md ================================================

plasmo logo

See License NPM Install Follow PlasmoHQ on Twitter Watch our Live DEMO every Friday Join our Discord for support and chat about our projects

English | 简体中文 | Tiếng Việt | Deutsch | French | Indonesian | Русский | Turkish | 日本語 | 한국어

# Plasmo Framework [Plasmo](https://www.plasmo.com/) Framework, hacker ruhlu yazılımcılar tarafından hacker ruhlu yazılımcılar için yapılmış pille dolu bir tarayıcı uzantısı geliştirme kiti'dir. > Tarayıcı uzantılarının [Next.js](https://nextjs.org/)'i gibi. ![CLI Demo](https://www.plasmo.com/assets/plasmo-cli-demo.gif) ## Öne Çıkan Özellikler - First-class [React](https://reactjs.org/) + [Typescript](https://www.typescriptlang.org/) Desteği - [Declarative Geliştirme](https://docs.plasmo.com/framework#where-is-the-manifestjson-file) - [Content Scripts UI](https://docs.plasmo.com/csui) - [Sekme Sayfaları](https://docs.plasmo.com/framework/tab-pages) - Canlı-reloading + React HMR - [`.env*` dosyaları](https://docs.plasmo.com/framework/env) - [Storage API'ı](https://docs.plasmo.com/framework/storage) - [Messaging API'ı](https://docs.plasmo.com/framework/messaging) - [Remote code bundle'lama](https://docs.plasmo.com/framework/remote-code) (örn: Google Analytics için) - [Birden çok tarayıcı ve manifest eşi](https://docs.plasmo.com/framework/workflows/build#with-specific-target) hedefleme - [BPP ile otomatik deploy](https://docs.plasmo.com/framework/workflows/submit) - İsteğe bağlı [Svelte](https://github.com/PlasmoHQ/with-svelte) ve [Vue](https://github.com/PlasmoHQ/with-vue) desteği Ve daha, daha fazlası! 🚀 ## Sistem Gereksinimleri - Node.js 16.x ve üzeri - MacOS, Windows veya Linux - (Şiddetle Tavsiye) [pnpm](https://pnpm.io/) ## Örnekler Plasmo'nun [Firebase Authentication](https://github.com/PlasmoHQ/examples/tree/main/with-firebase-auth), [Redux](https://github.com/PlasmoHQ/examples/tree/main/with-redux), [Supabase authentication](https://github.com/PlasmoHQ/examples/tree/main/with-supabase), [Tailwind](https://github.com/PlasmoHQ/examples/tree/main/with-tailwindcss) ve çok daha fazlası ile nasıl kullanılabileceğini gösteren örneklerimiz mevcut. Bunları görmek için [örnekler repomuzu ziyaret edin](https://github.com/PlasmoHQ/examples). ## Dökümantasyon Plasmo Framework'u hakkında daha derinlemesine bilgi edinmek için [dökümantasyon](https://docs.plasmo.com/)'a göz atın. ## Tarayıcı Uzantıları Kitabı Tarayıcı uzantılarının nasıl çalıştığına ve nasıl geliştirileceğine dair daha derinlemesine bir bakış için Matt Frisbie'nin yeni kitabı "[Building Browser Extensions](https://buildingbrowserextensions.com/plasmo)"ı şiddetle tavsiye ediyoruz. ## Kullanım ``` pnpm create plasmo example-dir cd example-dir pnpm dev ``` Önümüzdeki yol birçok virajla dolu. - Popup değişiklikleri `popup.tsx` dosyasına eklenir - Seçenekler sayfası değişiklikleri `options.tsx` dosyasına eklenir - Content script değişiklikleri `content.ts` dosyasına eklenir - Arka plan hizmet çalışanı değişiklikleri `background.ts` dosyasına eklenir ### Dizinler Bu dosyaları kendi dizinlerine sahip olacak şekilde de düzenleyebilirsiniz: ``` ext-dir ├───assets | └───icon.png ├───popup | ├───index.tsx | └───button.tsx ├───options | ├───index.tsx | ├───utils.ts | └───input.tsx ├───contents | ├───site-one.ts | ├───site-two.ts | └───site-three.ts ... ``` Son olarak, kaynak kodunu kök dizinine koymak yerine `src` alt dizinine koymak için [bu kılavuzu izleyebilirsin](https://docs.plasmo.com/framework/customization/src). `assets`'lerinizin ve diğer config dosyalarının yine de kök dizininde olması gerekeceğini unutmayın. ## Desteklenen Tarayıcılar Desteklenen tarayıcı hedeflerinin bir listesini görmek için [lütfen buradaki dökümantasyon'a bakın](https://docs.plasmo.com/framework/workflows/faq#what-are-the-officially-supported-browser-targets). ## Topluluk Plasmo topluluğu [Discord](https://www.plasmo.com/s/d)'da. Bu Plasmo Framework'ü kullanma konusunda yardım almak için uygun bir kanaldır. [Davranış Kurallarımız](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CODE_OF_CONDUCT.md) tüm Plasmo topluluk kanalları için geçerlidir. ## Katkıda bulunma Daha fazla bilgi edinmek için lütfen [katkıda bulunma yönergelerine](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CONTRIBUTING.md) bakın. Katkıda bulunan tüm harika [katılımcılarımıza](https://github.com/PlasmoHQ/plasmo/graphs/contributors) çok teşekkür ederiz ❤️ Eğlenceye katılmaktan ve PR göndermekten çekinmeyin! ### Plasmo Framework ### [Plasmo Örnekleri](https://github.com/PlasmoHQ/examples) ### [Plasmo Storage](https://github.com/PlasmoHQ/storage) ### [Browser Platform Publisher](https://github.com/PlasmoHQ/bpp) ## Sorumluluk Reddi Plasmo şu anda alfa yazılımıdır ve bazı şeyler sürümden sürüme değişebilir, bu nedenle lütfen dikkatli olun ve riski size ait olacak şekilde kullanın. # Lisans [MIT](https://github.com/PlasmoHQ/plasmo/blob/main/LICENSE) ⭐ [Plasmo](https://www.plasmo.com) ================================================ FILE: cli/plasmo/i18n/README.vi-VN.md ================================================

plasmo logo

Xem License NPM Install Theo dõi PlasmoHQ trên Twitter Xem trực tiếp DEMO mỗi thứ Sáu Tham gia Discord để chat về Plasmo

English | 简体中文 | Tiếng Việt | Deutsch | French | Indonesian | Русский | Turkish | 日本語 | 한국어

# Plasmo Framework [Plasmo](https://www.plasmo.com/) là một framework dùng để xây dựng ứng dụng mở rộng cho trình duyệt web (browser extension) với nhiều tính năng tối ưu hóa, tạo bởi hackers cho hackers. Xây dựng sản phẩm mà không phải lo lắng về config và những dị thù khi làm việc với extension. > Giống như [Next.js](https://nextjs.org/) cho extension! ![CLI Demo](https://www.plasmo.com/assets/plasmo-cli-demo.gif) ## Tính năng - [React](https://reactjs.org/) + [Typescript](https://www.typescriptlang.org/) - [Tự động hóa `manifest.json` với MV3](https://docs.plasmo.com/framework#where-is-the-manifestjson-file) - Tự động reload trình duyệt - [`.env*` file](https://docs.plasmo.com/framework/env) - [Content Scripts UI](https://docs.plasmo.com/csui) - [Gói mã nguồn online](https://docs.plasmo.com/framework/workflows/remote-code) (e.g for gtag4) - [Tự động xuất bản với BPP](https://docs.plasmo.com/framework/workflows/submit) - [Tạo extension cho mọi trình duyệt](https://docs.plasmo.com/framework/workflows/build#with-specific-target) - Dùng với [Svelte](https://github.com/PlasmoHQ/with-svelte) hoặc [Vue](https://github.com/PlasmoHQ/with-vue) - Và nhiều hơn nữa! 🚀 ## Yêu cầu hệ thống - Node.js 16.x trở lên - MacOS, Windows, hoặc Linux - (Khuyến khích) [pnpm](https://pnpm.io/) ## Ví dụ Chúng tôi có các ví dụ giới thiệu cách bạn có thể sử dụng Plasmo với [Firebase Authentication](https://github.com/PlasmoHQ/examples/tree/main/with-firebase-auth), [Redux](https://github.com/PlasmoHQ/examples/tree/main/with-redux), [Supabase authentication](https://github.com/PlasmoHQ/examples/tree/main/with-supabase), [Tailwind](https://github.com/PlasmoHQ/examples/tree/main/with-tailwindcss), và nhiều hơn nữa. Để xem chúng, hãy [truy cập kho ví dụ của chúng tôi](https://github.com/PlasmoHQ/examples). ## Tài liệu Xem [tài liệu](https://docs.plasmo.com/) để nhìn chuyên sâu hơn. ## Cách sử dụng ``` pnpm create plasmo example-dir cd example-dir pnpm dev ``` Con đường phía trước còn nhiều trông gai. - Thay đổi popup trong `popup.tsx` - Thay đổi trang Options trong `options.tsx` - Thay đổi Content script trong `content.ts` - Thay đổi dịch vụ nền (Background service worker) trong `background.ts` ### Thư mục Bạn có thể sắp xếp các tệp này trong thư mục riêng của chúng: ``` ext-dir ├───assets | └───icon512.png ├───popup | ├───index.tsx | └───button.tsx ├───options | ├───index.tsx | ├───utils.ts | └───input.tsx ├───contents | ├───site-one.ts | ├───site-two.ts | └───site-three.ts ... ``` Cuối cùng, bạn cũng có thể tránh đặt mã nguồn vào thư mục gốc của mình bằng cách đặt chúng vào thư mục con `src`, [làm theo hướng dẫn này](https://docs.plasmo.com/framework/customization/src). Lưu ý, thư mục `assets` và các tệp config vẫn cần phải ở trong thư mục gốc. ## Cộng đồng Cộng đồng Plasmo có thể được tìm thấy trên [Discord](https://www.plasmo.com/s/d). Đây là kênh thích hợp để nhận trợ giúp về việc sử dụng Plasmo Framework. [Quy tắc ứng xử](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CODE_OF_CONDUCT.md) của chúng tôi áp dụng cho tất cả các kênh cộng đồng của Plasmo. ## Đóng góp Vui lòng xem [hướng dẫn đóng góp](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CONTRIBUTING.md) để tìm hiểu thêm. ## Tuyên bố từ chối trách nhiệm Plasmo hiện là phần mềm alpha và một số thứ có thể thay đổi từ phiên bản này sang phiên bản khác. Xin lưu ý, Plasmo sẽ không chịu trách nhiệm nếu bạn gặp rủi ro khi xử dụng phần mềm này. # Giấy phép bản quyền [MIT](https://github.com/PlasmoHQ/plasmo/blob/main/LICENSE) ⭐ [Plasmo](https://www.plasmo.com) ================================================ FILE: cli/plasmo/i18n/README.zh-CN.md ================================================

plasmo logo

See License NPM Install Follow PlasmoHQ on Twitter Watch our Live DEMO every Friday Join our Discord for support and chat about our projects

English | 简体中文 | Tiếng Việt | Deutsch | French | Indonesian | Русский | Turkish | 日本語 | 한국어

**云服务:** 我们为浏览器扩展程序构建了一个云服务,叫 [Itero](https://itero.plasmo.com) 。如果你想要进行即时beta测试并体验更多很棒的功能,可以去尝试一下。 # Plasmo 框架 [Plasmo](https://www.plasmo.com/) 框架是一款黑客为黑客打造的功能强大的浏览器扩展程序软件开发工具包(SDK)。使用 Plasmo 来构建你的浏览器扩展程序,不需要操心扩展的配置文件和构建时的一些奇怪特性。 > 它就像浏览器扩展界的 [Next.js](https://nextjs.org/) ! ![CLI Demo](https://www.plasmo.com/assets/plasmo-cli-demo.gif) ## 特性 - 一流的 [React](https://reactjs.org/) + [Typescript](https://www.typescriptlang.org/) 支持 - [声明式开发(自动生成 manifest.json)](https://docs.plasmo.com/framework#where-is-the-manifestjson-file) - [将UI组件渲染到网页](https://docs.plasmo.com/csui) - [扩展内置页面](https://docs.plasmo.com/framework/tab-pages) - 扩展热重载 + React 模块热更新 - [`.env*` 文件](https://docs.plasmo.com/framework/env) - [扩展储存 API](https://docs.plasmo.com/framework/storage) - [扩展通信 API](https://docs.plasmo.com/framework/messaging) - [远程代码打包](https://docs.plasmo.com/framework/remote-code) (例如 Google Analytics) - 支持[多个浏览器和manifest版本](https://docs.plasmo.com/framework/workflows/build#with-specific-target) - [通过BPP进行自动部署](https://docs.plasmo.com/framework/workflows/submit) - 可选 [Svelte](https://github.com/PlasmoHQ/with-svelte) 或 [Vue](https://github.com/PlasmoHQ/with-vue) 进行开发 还有更多的功能!🚀 ## 系统要求 - Node.js 16.x 及以上 - MacOS,Windows,或 Linux - (强烈推荐) [pnpm](https://pnpm.io/) ## 代码示例 我们有一些展示如何集成 [Firebase Authentication](https://github.com/PlasmoHQ/examples/tree/main/with-firebase-auth), [Redux](https://github.com/PlasmoHQ/examples/tree/main/with-redux), [Supabase authentication](https://github.com/PlasmoHQ/examples/tree/main/with-supabase), [Tailwind](https://github.com/PlasmoHQ/examples/tree/main/with-tailwindcss) 以及更多技术的代码示例。如果想要浏览全部代码示例,请[访问示例仓库](https://github.com/PlasmoHQ/examples)。 ## 文档 阅读 [文档](https://docs.plasmo.com/) 以更深入地了解 Plasmo 框架。 ## 浏览器扩展书籍 为了更深入了解浏览器扩展工作原理和开发方法,我们强烈推荐 Matt Frisbie 的新书 ["Building Browser Extensions"](https://buildingbrowserextensions.com/plasmo)。 ## 使用 ``` pnpm create plasmo example-dir cd example-dir pnpm dev ``` 注意 - Popup 页面改动应在 `popup.tsx` - Options 页面改动应在 `options.tsx` - Content script 改动应在 `content.ts` - Background service worker 改动应在 `background.ts` ### 目录 您还可以在它们各自的目录中组织这些文件: ``` ext-dir ├───assets | └───icon.png ├───popup | ├───index.tsx | └───button.tsx ├───options | ├───index.tsx | ├───utils.ts | └───input.tsx ├───contents | ├───site-one.ts | ├───site-two.ts | └───site-three.ts ... ``` 此外,您也能够将代码放到 `src` 子目录,而不将它们放到根目录,请[参阅该指南](https://docs.plasmo.com/framework/customization/src)。注意 `assets` 和其他配置文件仍须在根目录下。 ## 支持的浏览器 要查看支持的浏览器列表,[请参考我们此处的文档](https://docs.plasmo.com/framework/workflows/faq#what-are-the-officially-supported-browser-targets). ## 社区 可以在 [Discord](https://www.plasmo.com/s/d) 找到 Plasmo 社区。这是获得 Plasmo 框架使用帮助的恰当渠道。 我们的 [行为守则](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CODE_OF_CONDUCT.md) 适用于所有 Plasmo 社区频道。 ## 贡献 请参阅 [贡献指南](https://github.com/PlasmoHQ/plasmo/blob/main/.github/CONTRIBUTING.md) 以了解更多内容。 非常感谢所有的 [贡献者](https://github.com/PlasmoHQ/plasmo/graphs/contributors) ❤️ 欢迎发送PR加入我们的行列! ### Plasmo Framework ### [Plasmo Examples](https://github.com/PlasmoHQ/examples) ### [Plasmo Storage](https://github.com/PlasmoHQ/storage) ### [Browser Platform Publisher](https://github.com/PlasmoHQ/bpp) ## 免责声明 Plasmo 当前仍为 alpha 软件,且不同版本间可能存在修改,所以在使用过程中请留意,风险自负。 # 协议 [MIT](https://github.com/PlasmoHQ/plasmo/blob/main/LICENSE) ⭐ [Plasmo](https://www.plasmo.com) ================================================ FILE: cli/plasmo/index.mjs ================================================ import { argv, exit } from "process" import { build, context } from "esbuild" import fse from "fs-extra" const watch = argv.includes("-w") /** @type import('esbuild').BuildOptions */ const commonConfig = { sourcemap: watch ? "inline" : false, minify: !watch, logLevel: watch ? "info" : "warning", bundle: true } async function main() { const config = await fse.readJson("package.json") const define = { "process.env.APP_VERSION": `"${config.version}"` } /** @type import('esbuild').BuildOptions */ const opts = { ...commonConfig, entryPoints: ["src/index.ts"], external: Object.keys(config.dependencies), platform: "node", format: "esm", define, banner: { js: "import { createRequire } from 'module';const require = createRequire(import.meta.url);" }, outfile: "dist/index.js" } if (watch) { const ctx = await context(opts) await ctx.watch() } else { await build(opts) } } main() process.on("SIGINT", () => exit(0)) process.on("SIGTERM", () => exit(0)) ================================================ FILE: cli/plasmo/package.json ================================================ { "name": "plasmo", "version": "0.90.5", "description": "The Plasmo Framework CLI", "publishConfig": { "types": "dist/type.d.ts" }, "types": "src/type.ts", "main": "dist/index.js", "bin": "bin/index.mjs", "type": "module", "files": [ "bin/index.mjs", "dist/index.js", "dist/type.d.ts", "templates" ], "scripts": { "dev": "node index.mjs -w", "build": "node index.mjs", "type": "tsup src/type.ts --format esm --dts-only --dts-resolve", "prepublishOnly": "run-p type build", "lint": "run-p lint:*", "lint:type": "tsc --noemit", "lint:code": "eslint src/**/*.ts" }, "author": "Plasmo Corp. ", "homepage": "https://docs.plasmo.com/", "repository": { "type": "git", "url": "https://github.com/PlasmoHQ/plasmo.git", "directory": "cli/plasmo" }, "license": "MIT", "keywords": [ "plasmo", "browser-extensions", "framework" ], "dependencies": { "@expo/spawn-async": "1.7.2", "@parcel/core": "2.9.3", "@parcel/fs": "2.9.3", "@parcel/package-manager": "2.9.3", "@parcel/watcher": "2.5.1", "@plasmohq/init": "workspace:*", "@plasmohq/parcel-config": "workspace:*", "@plasmohq/parcel-core": "workspace:*", "buffer": "6.0.3", "chalk": "5.4.1", "change-case": "5.4.4", "dotenv": "16.4.7", "dotenv-expand": "12.0.1", "events": "3.3.0", "fast-glob": "3.3.3", "fflate": "0.8.2", "get-port": "7.1.0", "got": "14.4.6", "ignore": "7.0.3", "inquirer": "12.5.0", "is-path-inside": "4.0.0", "json5": "2.2.3", "mnemonic-id": "3.2.7", "node-object-hash": "3.1.1", "package-json": "10.0.1", "process": "0.11.10", "semver": "7.7.1", "sharp": "0.33.5", "tempy": "3.1.0", "typescript": "5.8.2" }, "devDependencies": { "@plasmo/config": "workspace:*", "@plasmo/constants": "workspace:*", "@plasmo/framework-shared": "workspace:*", "@plasmo/utils": "workspace:*", "vue": "3.5.13" } } ================================================ FILE: cli/plasmo/src/commands/build.ts ================================================ import { getNonFlagArgvs } from "@plasmo/utils/argv" import { hasFlag } from "@plasmo/utils/flags" import { iLog, sLog } from "@plasmo/utils/logging" import { getBundleConfig } from "~features/extension-devtools/get-bundle-config" import { nextNewTab } from "~features/extra/next-new-tab" import { checkNewVersion } from "~features/framework-update/version-tracker" import { createParcelBuilder } from "~features/helpers/create-parcel-bundler" import { printHeader } from "~features/helpers/print" import { createManifest } from "~features/manifest-factory/create-manifest" import { zipBundle } from "~features/manifest-factory/zip" async function build() { printHeader() checkNewVersion() process.env.NODE_ENV = "production" const [internalCmd] = getNonFlagArgvs("build") if (internalCmd === "next-new-tab") { await nextNewTab() return } iLog("Prepare to bundle the extension...") const bundleConfig = getBundleConfig() iLog("Building for target:", bundleConfig.target) const plasmoManifest = await createManifest(bundleConfig) const bundler = await createParcelBuilder(plasmoManifest, { mode: "production", shouldDisableCache: true, shouldContentHash: false, defaultTargetOptions: { shouldOptimize: true, shouldScopeHoist: hasFlag("--hoist") } }) const result = await bundler.run() sLog(`Finished in ${result.buildTime}ms!`) await plasmoManifest.postBuild() if (hasFlag("--zip")) { await zipBundle(plasmoManifest.commonPath) } } export default build ================================================ FILE: cli/plasmo/src/commands/dev.ts ================================================ import { BuildSocketEvent, getBuildSocket } from "@plasmo/framework-shared/build-socket" import { getFlag, isVerbose } from "@plasmo/utils/flags" import { eLog, iLog, sLog, vLog } from "@plasmo/utils/logging" import { getBundleConfig } from "~features/extension-devtools/get-bundle-config" import { createProjectWatcher } from "~features/extension-devtools/project-watcher" import { checkNewVersion } from "~features/framework-update/version-tracker" import { createParcelBuilder } from "~features/helpers/create-parcel-bundler" import { startLoading, stopLoading } from "~features/helpers/loading-animation" import { printHeader } from "~features/helpers/print" import { createManifest } from "~features/manifest-factory/create-manifest" async function dev() { printHeader() checkNewVersion() process.env.NODE_ENV = "development" const rawServePort = getFlag("--serve-port") || "1012" const serveHost = getFlag("--serve-host") || "localhost" const rawHmrPort = getFlag("--hmr-port") || "1815" const hmrHost = getFlag("--hmr-host") || "localhost" iLog("Starting the extension development server...") const { default: getPort } = await import("get-port") const [servePort, hmrPort] = await Promise.all([ getPort({ port: parseInt(rawServePort) }), getPort({ port: parseInt(rawHmrPort) }) ]) const buildWatcher = getBuildSocket(hmrHost, hmrPort) vLog( `Starting dev server on ${serveHost}:${servePort}, HMR on ${hmrHost}:${hmrPort}...` ) const bundleConfig = getBundleConfig() iLog("Building for target:", bundleConfig.target) const plasmoManifest = await createManifest(bundleConfig) const projectWatcher = await createProjectWatcher(plasmoManifest) const bundler = await createParcelBuilder(plasmoManifest, { logLevel: "verbose", shouldBundleIncrementally: true, serveOptions: { host: serveHost, port: servePort }, hmrOptions: { host: hmrHost, port: hmrPort } }) const { default: chalk } = await import("chalk") const bundlerWatcher = await bundler.watch(async (err, event) => { if (err) { stopLoading() throw err } if (event === undefined) { return } if (event.type === "buildStart") { startLoading() return } if (event.type === "buildSuccess") { stopLoading() sLog(`Extension re-packaged in ${chalk.bold(event.buildTime)}ms! 🚀`) await plasmoManifest.postBuild() buildWatcher.broadcast(BuildSocketEvent.BuildReady) return } if (event.type === "buildFailure") { stopLoading() if (!isVerbose()) { eLog( chalk.redBright( `Build failed. To debug, run ${chalk.bold("plasmo dev --verbose")}.` ) ) } event.diagnostics.forEach((diagnostic) => { eLog(chalk.redBright(diagnostic.message)) if (diagnostic.stack) { vLog(diagnostic.stack) } diagnostic.hints?.forEach((hint) => { vLog(hint) }) diagnostic.codeFrames?.forEach((codeFrame) => { if (codeFrame.code) { vLog(codeFrame.code) } codeFrame.codeHighlights.forEach((codeHighlight) => { if (codeHighlight.message) { vLog(codeHighlight.message) } vLog( chalk.underline( `${codeFrame.filePath}:${codeHighlight.start.line}:${codeHighlight.start.column}` ) ) }) }) }) } process.env.__PLASMO_FRAMEWORK_INTERNAL_WATCHER_STARTED = "true" }) const cleanup = () => { projectWatcher?.unsubscribe() bundlerWatcher.unsubscribe() } process.on("SIGINT", cleanup) process.on("SIGTERM", cleanup) } export default dev ================================================ FILE: cli/plasmo/src/commands/help.ts ================================================ import { printHeader, printHelp } from "~features/helpers/print" async function help() { printHeader() printHelp() } export default help ================================================ FILE: cli/plasmo/src/commands/index.ts ================================================ export const runMap = { help: () => import("./help"), //#ifdef !IS_BINARY start: () => import("./start"), init: () => import("./init"), dev: () => import("./dev"), build: () => import("./build"), package: () => import("./package"), //#endif version: () => import("./version"), ["-v"]: () => import("./version"), ["--version"]: () => import("./version") } export type ValidCommand = keyof typeof runMap export const validCommandList = Object.keys(runMap) as ValidCommand[] export const validCommandSet = new Set(validCommandList) ================================================ FILE: cli/plasmo/src/commands/init.ts ================================================ import { resolve } from "path" import { cwd } from "process" import { kebabCase } from "change-case" import { hasFlag } from "@plasmo/utils/flags" import { ensureWritableAndEmpty } from "@plasmo/utils/fs" import { vLog } from "@plasmo/utils/logging" import { getCommonPath } from "~features/extension-devtools/common-path" import { getPackageManager } from "~features/helpers/package-manager" import { printHeader } from "~features/helpers/print" import { ProjectCreator } from "~features/project-creator" import { getRawName } from "~features/project-creator/get-raw-name" import { gitInit } from "~features/project-creator/git-init" import { installDependencies } from "~features/project-creator/install-dependencies" import { printReady } from "~features/project-creator/print-ready" async function init() { printHeader() const isExample = hasFlag("--exp") const rawName = await getRawName() const currentDirectory = cwd() // For resolving project directory const projectDirectory = resolve( currentDirectory, kebabCase(rawName) || rawName ) vLog("Project directory:", projectDirectory) const commonPath = getCommonPath(projectDirectory) vLog("Package name:", commonPath.packageName) if (isExample && !commonPath.packageName.startsWith("with-")) { throw new Error("Example extensions must have the `with-` prefix") } await ensureWritableAndEmpty(projectDirectory) const packageManager = await getPackageManager() vLog( `Using package manager: ${packageManager.name} ${packageManager?.version}` ) const creator = new ProjectCreator(commonPath, packageManager, isExample) await creator.create() await installDependencies(projectDirectory, packageManager) await gitInit(commonPath, projectDirectory) await printReady( projectDirectory, currentDirectory, commonPath, packageManager ) } export default init ================================================ FILE: cli/plasmo/src/commands/package.ts ================================================ import { hasFlag } from "@plasmo/utils/flags" import { iLog } from "@plasmo/utils/logging" import { getBundleConfig } from "~features/extension-devtools/get-bundle-config" import { checkNewVersion } from "~features/framework-update/version-tracker" import { printHeader } from "~features/helpers/print" import { createManifest } from "~features/manifest-factory/create-manifest" import { zipBundle } from "~features/manifest-factory/zip" async function packageCmd() { printHeader() checkNewVersion() process.env.NODE_ENV = "production" iLog("Prepare to package the extension bundle...") const bundleConfig = getBundleConfig() const plasmoManifest = await createManifest(bundleConfig) await zipBundle(plasmoManifest.commonPath, hasFlag("--with-source-maps")) } export default packageCmd ================================================ FILE: cli/plasmo/src/commands/start.ts ================================================ import { iLog } from "@plasmo/utils/logging" async function start() { iLog("Start the extension development...") } export default start ================================================ FILE: cli/plasmo/src/commands/version.ts ================================================ async function version() { console.log(process.env.APP_VERSION) } export default version ================================================ FILE: cli/plasmo/src/features/background-service-worker/bgsw-entry.ts ================================================ import { relative, resolve } from "path" import { ensureDir, outputFile } from "fs-extra" import { vLog } from "@plasmo/utils/logging" import { toPosix } from "@plasmo/utils/path" import { type PlasmoManifest } from "~features/manifest-factory/base" export const createBgswEntry = async ( { indexFilePath = "", withMessaging = false, withMainWorldScript = false }, plasmoManifest: PlasmoManifest ) => { vLog("Creating BGSW entry") const bgswStaticDirectory = resolve( plasmoManifest.commonPath.staticDirectory, "background" ) const bgswEntryFilePath = resolve(bgswStaticDirectory, "index.ts") const indexImportPath = relative(bgswStaticDirectory, indexFilePath) const bgswCode = [ withMessaging && `import "./messaging"`, indexFilePath && `import "${toPosix(indexImportPath).slice(0, -3)}"`, withMainWorldScript && `import "./main-world-scripts"` ] .filter(Boolean) .join("\n") await ensureDir(bgswStaticDirectory) await outputFile(bgswEntryFilePath, bgswCode) } ================================================ FILE: cli/plasmo/src/features/background-service-worker/bgsw-main-world-script.ts ================================================ import { relative, resolve } from "path" import { camelCase } from "change-case" import { outputFile } from "fs-extra" import { vLog } from "@plasmo/utils/logging" import { toPosix } from "@plasmo/utils/path" import { type PlasmoManifest } from "~features/manifest-factory/base" export const createBgswMainWorldInjector = async ( plasmoManifest: PlasmoManifest ) => { try { const outputPath = resolve( plasmoManifest.commonPath.staticDirectory, "background", "main-world-scripts.ts" ) const importStatements = plasmoManifest.mainWorldScriptList.map((s) => { const importMetadata = s.js.map((jsPath) => { const importName = camelCase(jsPath) const importPath = `url:${toPosix( relative(plasmoManifest.commonPath.entryManifestPath, jsPath) )}` return { importName, importPath } }) const topImport = importMetadata .map((i) => `import ${i.importName} from "${i.importPath}"`) .join("\n") const importScript = importMetadata.map((i) => i.importName) const regCsScript = Object.entries(s).reduce( (out, [k, v]) => { if (k !== "js") { out[camelCase(k)] = v } return out }, { id: importScript.join("-"), js: [] } ) const regCsScriptCode = JSON.stringify(regCsScript).replace( /"js":\[\]/, `"js":[${importScript .map((imSrc) => `${imSrc}.split("/").pop().split("?")[0]`) .join(",")}]` ) return [topImport, regCsScriptCode] as const }) if (importStatements.length === 0) { return false } plasmoManifest.permissionSet.add("scripting") const code = `${importStatements.map(([top]) => top).join("\n")} chrome.scripting.registerContentScripts([ ${importStatements.map(([, reg]) => reg).join(",\n ")} ]).catch(_ => {}) ` await outputFile(outputPath, code) return true } catch (e) { vLog(e.message) return false } } ================================================ FILE: cli/plasmo/src/features/background-service-worker/bgsw-messaging-declaration.ts ================================================ import { resolve } from "path" import { outputFile } from "fs-extra" import type { CommonPath } from "~features/extension-devtools/common-path" export const MESSAGING_DECLARATION = `messaging` as const const MESSAGING_DECLARATION_FILENAME = `${MESSAGING_DECLARATION}.d.ts` export const outputMessagingDeclaration = ( commonPath: CommonPath, declarationCode: string ) => outputFile( resolve(commonPath.dotPlasmoDirectory, MESSAGING_DECLARATION_FILENAME), declarationCode ) export const createDeclarationCode = (messages: string[], ports: string[]) => ` import "@plasmohq/messaging" interface MmMetadata { \t${messages.join("\n\t")} } interface MpMetadata { \t${ports.join("\n\t")} } declare module "@plasmohq/messaging" { interface MessagesMetadata extends MmMetadata {} interface PortsMetadata extends MpMetadata {} } ` ================================================ FILE: cli/plasmo/src/features/background-service-worker/bgsw-messaging.ts ================================================ import { camelCase } from "change-case" import glob from "fast-glob" import { outputFile } from "fs-extra" import { join, resolve } from "path" import { isWriteable } from "@plasmo/utils/fs" import { vLog, wLog } from "@plasmo/utils/logging" import { toPosix } from "@plasmo/utils/path" import { createDeclarationCode, outputMessagingDeclaration } from "~features/background-service-worker/bgsw-messaging-declaration" import { getRevHash } from "~features/helpers/crypto" import { type PlasmoManifest } from "~features/manifest-factory/base" const state = { md5Hash: "" } // TODO: cache these? const createEntryCode = ( importSection: string, messageSection: string, externalMessageSection: string, portSection: string ) => `// @ts-nocheck globalThis.__plasmoInternalPortMap = new Map() ${importSection} chrome.runtime.onMessageExternal.addListener((request, sender, sendResponse) => { switch (request?.name) { ${externalMessageSection} default: break } return true }) chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { switch (request.name) { ${messageSection} default: break } return true }) chrome.runtime.onConnect.addListener(function(port) { globalThis.__plasmoInternalPortMap.set(port.name, port) port.onMessage.addListener(function(request) { switch (port.name) { ${portSection} default: break } }) }) ` const getHandlerList = async ( plasmoManifest: PlasmoManifest, dirName: "messages" | "messages/external" | "ports" ) => { const handlerDir = join( plasmoManifest.projectPath.backgroundDirectory, dirName ) if (!(await isWriteable(handlerDir))) { return [] } const handlerFileList = await glob("**/*.ts", { cwd: handlerDir, onlyFiles: true, ignore: dirName === "messages" ? ["external"] : [] }) return handlerFileList.map((filePath) => { const posixFilePath = toPosix(filePath) const handlerName = posixFilePath.slice(0, -3) const importPath = `${dirName}/${handlerName}` const importName = camelCase(importPath) return { importName, name: handlerName, declaration: `"${handlerName}" : {}`, importCode: `import { default as ${importName} } from "~background/${importPath}"` } }) } const getMessageCode = (name: string, importName: string) => `case "${name}": ${importName}({ ...request, sender }, { send: (p) => sendResponse(p) }) break` const getPortCode = (name: string, importName: string) => `case "${name}": ${importName}({ port, ...request }, { send: (p) => port.postMessage(p) }) break` export const createBgswMessaging = async (plasmoManifest: PlasmoManifest) => { try { const handlerLists = await Promise.all([ getHandlerList(plasmoManifest, "messages"), getHandlerList(plasmoManifest, "messages/external"), getHandlerList(plasmoManifest, "ports") ]) const [messageHandlerList, externalMessageHandlerList, portHandlerList] = handlerLists vLog({ messageHandlerList, externalMessageHandlerList, portHandlerList }) if (handlerLists.every((list) => list.length === 0)) { return false } // check if package.json has messaging API if (!("@plasmohq/messaging" in plasmoManifest.dependencies)) { wLog("@plasmohq/messaging is not installed, skipping messaging API") return false } const declarationCode = createDeclarationCode( messageHandlerList.map(({ declaration }) => declaration), portHandlerList.map(({ declaration }) => declaration) ) const declarationMd5Hash = getRevHash(Buffer.from(declarationCode)) if (state.md5Hash === declarationMd5Hash) { return true } state.md5Hash = declarationMd5Hash const entryCode = createEntryCode( [...messageHandlerList, ...externalMessageHandlerList, ...portHandlerList] .map((code) => code.importCode) .join("\n"), messageHandlerList .map((code) => getMessageCode(code.name, code.importName)) .join("\n"), externalMessageHandlerList .map((code) => getMessageCode(code.name, code.importName)) .join("\n"), portHandlerList .map((code) => getPortCode(code.name, code.importName)) .join("\n") ) await Promise.all([ outputFile( resolve( plasmoManifest.commonPath.staticDirectory, "background", "messaging.ts" ), entryCode ), outputMessagingDeclaration(plasmoManifest.commonPath, declarationCode) ]) return true } catch (e) { vLog(e.message) return false } } ================================================ FILE: cli/plasmo/src/features/background-service-worker/update-bgsw-entry.ts ================================================ import { find } from "@plasmo/utils/array" import { isAccessible } from "@plasmo/utils/fs" import { createBgswEntry } from "~features/background-service-worker/bgsw-entry" import { createBgswMainWorldInjector } from "~features/background-service-worker/bgsw-main-world-script" import { createBgswMessaging } from "~features/background-service-worker/bgsw-messaging" import { type PlasmoManifest } from "~features/manifest-factory/base" export const updateBgswEntry = async (plasmoManifest: PlasmoManifest) => { const [bgswIndexFilePath, withMessaging, withMainWorldScript] = await Promise.all([ find(plasmoManifest.projectPath.backgroundIndexList, isAccessible), createBgswMessaging(plasmoManifest), createBgswMainWorldInjector(plasmoManifest) ] as const) const hasBgsw = Boolean(bgswIndexFilePath) || withMessaging || withMainWorldScript if (hasBgsw) { await createBgswEntry( { indexFilePath: bgswIndexFilePath, withMessaging, withMainWorldScript }, plasmoManifest ) } return plasmoManifest.toggleBackground(hasBgsw) } ================================================ FILE: cli/plasmo/src/features/env/env-config.ts ================================================ // Forked from https://github.com/vercel/next.js/blob/canary/packages/next-env/index.ts import { readFile } from "fs/promises" import { resolve } from "path" import { constantCase } from "change-case" import dotenv from "dotenv" import { expand as dotenvExpand } from "dotenv-expand" import { isFile, isReadable } from "@plasmo/utils/fs" import { eLog, iLog, vLog } from "@plasmo/utils/logging" import { getFlagMap } from "~features/helpers/flag" export type Env = Record type LoadedEnvFiles = Array<{ name: string contents: string }> export const INTERNAL_ENV_PREFIX = "PLASMO_" export const PUBLIC_ENV_PREFIX = "PLASMO_PUBLIC_" const envFileSet = new Set() export class PlasmoPublicEnv { data: Env constructor(_env: Env) { this.data = Object.keys(_env) .filter((k) => k.startsWith(PUBLIC_ENV_PREFIX)) .reduce((env, key) => { env[key] = _env[key] return env }, {} as Env) } extends(rawData: Env) { iLog("Loaded environment variables from:", [...envFileSet]) const clone = new PlasmoPublicEnv({ ...this.data }) clone.data["NODE_ENV"] = process.env.NODE_ENV Object.entries(rawData).forEach(([key, value]) => { clone.data[`${INTERNAL_ENV_PREFIX}${constantCase(key)}`] = value }) return clone } } function cascadeEnv(loadedEnvFiles: LoadedEnvFiles) { const parsed: dotenv.DotenvParseOutput = Object.assign({}, process.env) for (const { contents, name } of loadedEnvFiles) { try { envFileSet.add(name) const result = dotenvExpand({ ignoreProcessEnv: true, parsed: dotenv.parse(contents) }) if (!!result.parsed) { vLog(`Loaded env from ${name}`) const resultData = result.parsed || {} for (const [envKey, envValue] of Object.entries(resultData)) { if (typeof parsed[envKey] === "undefined") { try { parsed[envKey] = maybeParseJSON(envValue) } catch (ex) { eLog(`Failed to parse JSON directive ${envKey} in ${name}:`, ex.message) } // Pass through internal env variables if (envKey.startsWith(INTERNAL_ENV_PREFIX)) { process.env[envKey] = envValue } } } } } catch (err) { eLog(`Failed to load env from ${name}`, err) } } return parsed } const JSON_DIRECTIVE_RE = /^\s*json\((.+)\)\s*$/si function maybeParseJSON(value: string): any { const match = value.match(JSON_DIRECTIVE_RE) return match ? JSON.parse(match[1]) : value } export const setInternalEnv = (env: Record) => { for (const [key, value] of Object.entries(env)) { process.env[`${INTERNAL_ENV_PREFIX}${constantCase(key)}`] = value } } export const getEnvFileNames = () => { const nodeEnv = process.env.NODE_ENV const flagMap = getFlagMap() return [ flagMap.envPath, `.env.${flagMap.browser}.local`, `.env.${flagMap.tag}.local`, `.env.${nodeEnv}.local`, // Don't include `.env.local` for `test` environment // since normally you expect tests to produce the same // results for everyone nodeEnv !== "test" ? `.env.local` : "", `.env.${flagMap.browser}`, `.env.${flagMap.tag}`, `.env.${nodeEnv}`, ".env" ].filter((s) => !!s) } export async function loadEnvConfig(dir: string) { const allDotEnvEntries = await Promise.all( getEnvFileNames() .map((envFile) => [envFile, resolve(dir, envFile)]) .map( async ([envFile, filePath]) => [ envFile, filePath, (await isFile(filePath)) && (await isReadable(filePath)) ] as const ) ) const envFiles: LoadedEnvFiles = await Promise.all( allDotEnvEntries .filter(([, , isValid]) => isValid) .map(async ([envFile, filePath]) => ({ name: envFile, contents: await readFile(filePath, "utf8") })) ) const combinedEnv = cascadeEnv(envFiles) const plasmoPublicEnv = new PlasmoPublicEnv(combinedEnv) return { combinedEnv, plasmoPublicEnv, loadedEnvFiles: envFiles } } export type EnvConfig = Awaited> ================================================ FILE: cli/plasmo/src/features/env/env-declaration.ts ================================================ import { resolve } from "path" import { outputFile } from "fs-extra" import type { PlasmoManifest } from "~features/manifest-factory/base" export const PROCESS_ENV_DECLARATION = `process.env` as const const PROCESS_ENV_DECLARATION_FILENAME = `${PROCESS_ENV_DECLARATION}.d.ts` const createDeclarationCode = (envKeys: string[]) => ` declare namespace NodeJS { interface ProcessEnv { ${envKeys.map((e) => `\t\t${e}?: string`).join("\n")} } } ` export async function outputEnvDeclaration({ commonPath, publicEnv }: PlasmoManifest) { const envKeys = Object.keys(publicEnv.data) if (envKeys.length === 0) { return } await outputFile( resolve(commonPath.dotPlasmoDirectory, PROCESS_ENV_DECLARATION_FILENAME), createDeclarationCode(envKeys) ) } ================================================ FILE: cli/plasmo/src/features/extension-devtools/common-path.ts ================================================ import { existsSync } from "fs" import { basename, resolve } from "path" import { cwd } from "process" import { getFlagMap } from "~features/helpers/flag" export const getCommonPath = (projectDirectory = cwd()) => { const flagMap = getFlagMap() process.env.PLASMO_PROJECT_DIR = projectDirectory const packageName = basename(projectDirectory) process.env.PLASMO_SRC_PATH = flagMap.srcPath const srcDirectory = resolve(projectDirectory, flagMap.srcPath) process.env.PLASMO_SRC_DIR = existsSync(srcDirectory) ? srcDirectory : projectDirectory process.env.PLASMO_BUILD_PATH = flagMap.buildPath const buildDirectory = resolve(projectDirectory, flagMap.buildPath) process.env.PLASMO_BUILD_DIR = buildDirectory const distDirectoryName = `${flagMap.target}-${flagMap.tag}` const distDirectory = resolve(buildDirectory, distDirectoryName) const dotPlasmoDirectory = resolve(projectDirectory, ".plasmo") const cacheDirectory = resolve(dotPlasmoDirectory, "cache") return { packageName, projectDirectory, buildDirectory, distDirectory, distDirectoryName, sourceDirectory: process.env.PLASMO_SRC_DIR, packageFilePath: resolve(projectDirectory, "package.json"), gitIgnorePath: resolve(projectDirectory, ".gitignore"), assetsDirectory: resolve(projectDirectory, "assets"), parcelConfig: resolve(projectDirectory, ".parcelrc"), dotPlasmoDirectory, cacheDirectory, plasmoVersionFilePath: resolve(cacheDirectory, "plasmo.version.json"), staticDirectory: resolve(dotPlasmoDirectory, "static"), genAssetsDirectory: resolve(dotPlasmoDirectory, "gen-assets"), entryManifestPath: resolve( dotPlasmoDirectory, `${flagMap.target}.plasmo.manifest.json` ) } } export type CommonPath = ReturnType ================================================ FILE: cli/plasmo/src/features/extension-devtools/content-script-config.ts ================================================ import { readFile } from "fs/promises" import typescript, { type Node, type VariableDeclaration } from "typescript" import type { ManifestContentScript } from "@plasmo/constants" import { eLog, vLog } from "@plasmo/utils/logging" import { parseAst } from "./parse-ast" const { ScriptTarget, SyntaxKind, createSourceFile, isObjectLiteralExpression, isVariableStatement } = typescript export const extractContentScriptConfig = async (path: string) => { try { const sourceContent = await readFile(path, "utf8") if (sourceContent.length === 0) { return { isEmpty: true } } const sourceFile = createSourceFile( path, sourceContent, ScriptTarget.Latest, true ) const variableDeclarationMap = sourceFile.statements .filter(isVariableStatement) .reduce( (output, node) => { node.declarationList.forEachChild((vd: VariableDeclaration) => { output[vd.name.getText()] = vd.initializer }) return output }, {} as Record ) const configAST = variableDeclarationMap["config"] if (!configAST || !isObjectLiteralExpression(configAST)) { return null } const config = configAST.properties.reduce((output, node) => { if (node.getChildCount() < 3) { return output } const [keyNode, _, valueNode] = node.getChildren() const key = keyNode.getText() try { if (valueNode.kind === SyntaxKind.Identifier) { output[key] = parseAst(variableDeclarationMap[valueNode.getText()]) } else { output[key] = parseAst(valueNode) } } catch (error) { eLog(error) } return output }, {} as ManifestContentScript) vLog("Parsed config:", config) return { config } } catch (error) { vLog(error) return null } } ================================================ FILE: cli/plasmo/src/features/extension-devtools/generate-icons.ts ================================================ import { basename, resolve } from "path" import { copy, ensureDir } from "fs-extra" import sharp from "sharp" import { find } from "@plasmo/utils/array" import { isAccessible } from "@plasmo/utils/fs" import { vLog, wLog } from "@plasmo/utils/logging" import { getFlagMap } from "~features/helpers/flag" import type { CommonPath } from "./common-path" const getIconNameVariants = (size = 512 as string | number, name = "icon") => [ `${name}${size}`, `${name}-${size}`, `${name}-${size}x${size}` ] // Prefer icon -> medium -> large icon const baseIconNames = [ "icon", ...getIconNameVariants(), ...getIconNameVariants(1024) ] /** * We pick icon in this order * 1. tag based icon * 2. env and tag based icon * 3. plain icon * * */ const getPrioritizedIconPaths = (iconNames = baseIconNames) => { const flagMap = getFlagMap() return iconNames .map((name) => [ `${name}.${flagMap.tag}.${process.env.NODE_ENV}.png`, `${name}.${process.env.NODE_ENV}.png`, `${name}.${flagMap.tag}.png`, `${name}.png` ]) .flat() } const iconSizeList = [128, 64, 48, 32, 16] // Use this to cache the path resolving result const iconState = { baseIconPaths: [] as string[], devProvidedIcons: {} as Record } /** * Generate manifest icons from an icon in the assets directory * - One icon will be picked in the set { `icon`, `icon512`, `icon-512`, `icon-512x512`, `icon1024`, `icon-1024`, `icon-1024x1024` } * - Optionally, it will resolve `process.env.NODE_ENV` suffix, e.g: `icon.development.png`, `icon.production.png` * - The suffix is prioritized. Thus, if `icon512.development.png` exists, it will be picked over `icon512.png` * - Only png is supported */ export async function generateIcons({ assetsDirectory, genAssetsDirectory }: CommonPath) { // Precalculate the base icon paths if (iconState.baseIconPaths.length === 0) { const iconNameList = getPrioritizedIconPaths() iconState.baseIconPaths = iconNameList.map((name) => resolve(assetsDirectory, name) ) } const baseIconPath = await find(iconState.baseIconPaths, isAccessible) if (baseIconPath === undefined) { wLog("No icon found in assets directory") return } await ensureDir(genAssetsDirectory) const baseIcon = sharp(baseIconPath) const baseIconBuffer = await baseIcon.toBuffer() vLog(`${baseIconPath} found, creating resized icons`) await Promise.all( iconSizeList.map(async (width) => { if (iconState.devProvidedIcons[width] === undefined) { const devIconPath = getPrioritizedIconPaths(getIconNameVariants(width)) iconState.devProvidedIcons[width] = devIconPath.map((name) => resolve(assetsDirectory, name) ) } const devProvidedIcon = await find( iconState.devProvidedIcons[width], isAccessible ) const generatedIconPath = resolve( genAssetsDirectory, `icon${width}.plasmo.png` ) if (process.env.NODE_ENV === "development") { if (devProvidedIcon !== undefined) { if (basename(devProvidedIcon).includes(".development.")) { return copy(devProvidedIcon, generatedIconPath) } else { return sharp(devProvidedIcon).grayscale().toFile(generatedIconPath) } } else { return sharp(Buffer.from(baseIconBuffer)) .resize({ width, height: width }) .greyscale(!basename(baseIconPath).includes(".development.")) .toFile(generatedIconPath) } } else { return devProvidedIcon !== undefined ? copy(devProvidedIcon, generatedIconPath) : sharp(Buffer.from(baseIconBuffer)) .resize({ width, height: width }) .toFile(generatedIconPath) } }) ) } ================================================ FILE: cli/plasmo/src/features/extension-devtools/get-bundle-config.ts ================================================ import { getFlagMap } from "~features/helpers/flag" export const getBundleConfig = () => { const flagMap = getFlagMap() const { target, tag } = flagMap const [browser, manifestVersion] = target.split("-") // Potential runtime config here return { tag, target, browser, manifestVersion } } export type PlasmoBundleConfig = ReturnType ================================================ FILE: cli/plasmo/src/features/extension-devtools/git-ignore.ts ================================================ export const generateGitIgnore = () => ` # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage #cache .turbo .next .vercel # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* # local env files .env* out/ build/ dist/ # plasmo - https://www.plasmo.com .plasmo # bpp - https://github.com/marketplace/actions/browser-platform-publisher keys.json # typescript .tsbuildinfo ` ================================================ FILE: cli/plasmo/src/features/extension-devtools/package-file.ts ================================================ import { userInfo } from "os" import { sentenceCase } from "change-case" import getPackageJson, { type AbbreviatedVersion } from "package-json" import type { ExtensionManifestV3 } from "@plasmo/constants" import type { PackageManagerInfo } from "~features/helpers/package-manager" const _generatePackage = async ({ name = "plasmo-extension", version = "0.0.1", packageManager = {} as PackageManagerInfo }) => { const baseData = { name, displayName: sentenceCase(name), version, description: "A basic Plasmo extension.", author: userInfo().username, packageManager: undefined as string | undefined, scripts: { dev: "plasmo dev", build: "plasmo build", package: "plasmo package" }, dependencies: { plasmo: "workspace:*", react: "*", "react-dom": "*" } as Record, devDependencies: { "@types/chrome": "*", "@types/node": "*", "@types/react": "*", "@types/react-dom": "*", prettier: "*", typescript: "*" } as Record, manifest: { // permissions: [] as ValidManifestPermission[], host_permissions: ["https://*/*"] } as Partial } if (!packageManager || !packageManager.version) { delete baseData.packageManager } else { baseData.packageManager = `${packageManager.name}@${packageManager.version}` } return baseData } export type PackageJSON = Awaited> & { homepage?: string contributors?: string[] peerDependencies?: Record } type GenerateArgs = Parameters[0] export const generatePackage = async (p: GenerateArgs) => (await _generatePackage(p)) as PackageJSON export const resolveWorkspaceToLatestSemver = async ( dependencies: Record ) => { const output = {} as Record await Promise.all( Object.entries(dependencies).map(async ([key, value]) => { if (key === "plasmo") { output[key] = process.env.APP_VERSION as string } else if (value === "workspace:*") { try { const remotePackageData = (await getPackageJson(key, { version: "latest" })) as unknown as AbbreviatedVersion output[key] = remotePackageData.version } catch { output[key] = value } } else { output[key] = value } }) ) return output } ================================================ FILE: cli/plasmo/src/features/extension-devtools/parse-ast.ts ================================================ /** * Copyright (c) Plasmo Corp, foss@plasmo.com, MIT Licensed * --- * Adapted from https://github.com/dword-design/ts-ast-to-literal/blob/master/src/index.js * Copyright (c) Sebastian Landwehr info@sebastianlandwehr.com, MIT licensed */ import typescript, { type ArrayLiteralExpression, type Identifier, type LiteralExpression, type Node, type ObjectLiteralExpression, type PropertyAssignment } from "typescript" const { SyntaxKind } = typescript export const parseAst = (node: Node) => { switch (node.kind) { case SyntaxKind.StringLiteral: return (node as LiteralExpression).text case SyntaxKind.TrueKeyword: return true case SyntaxKind.FalseKeyword: return false case SyntaxKind.NullKeyword: return null case SyntaxKind.NumericLiteral: return parseFloat((node as LiteralExpression).text) case SyntaxKind.ArrayLiteralExpression: return (node as ArrayLiteralExpression).elements .filter((node) => node.kind !== SyntaxKind.SpreadElement) .map(parseAst) case SyntaxKind.ObjectLiteralExpression: return (node as ObjectLiteralExpression).properties .filter( (property) => property.kind === SyntaxKind.PropertyAssignment && (property.name.kind === SyntaxKind.Identifier || property.name.kind === SyntaxKind.StringLiteral) ) .map((property: PropertyAssignment) => [ (property.name as Identifier).escapedText || (property.name as LiteralExpression).text, parseAst(property.initializer) ]) .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) default: return undefined } } ================================================ FILE: cli/plasmo/src/features/extension-devtools/project-path.ts ================================================ import { resolve } from "path" import { getEnvFileNames } from "~features/env/env-config" import type { SupportedUiExt } from "~features/manifest-factory/ui-library" import type { CommonPath } from "./common-path" export enum WatchReason { None, EnvFile, PackageJson, AssetsDirectory, TabsDirectory, BackgroundIndex, BackgroundDirectory, ContentScriptIndex, ContentScriptsDirectory, NewtabIndex, NewtabHtml, SidePanelIndex, SidePanelHtml, DevtoolsIndex, DevtoolsHtml, PopupIndex, PopupHtml, OptionsIndex, OptionsHtml, SandboxIndex, SandboxesDirectory } type DirectoryWatchTuple = [WatchReason, string] const getWatchReasonMap = (paths: string[], reason: WatchReason) => paths.reduce( (output, path) => { output[path] = reason return output }, {} as Record ) export const getProjectPath = ( { sourceDirectory, packageFilePath, assetsDirectory }: CommonPath, browserTarget: string, uiExts: SupportedUiExt[] ) => { /** * only pointing to 1 particular file path */ const getModuleList = (moduleName: string) => [".ts", ...uiExts, ".js"].flatMap((ext) => [ resolve(sourceDirectory, `${moduleName}.${browserTarget}${ext}`), resolve(sourceDirectory, `${moduleName}.${process.env.NODE_ENV}${ext}`), resolve(sourceDirectory, `${moduleName}${ext}`) ]) /** * crawl index, and only care about one extension */ const getIndexList = (moduleName: string, exts = [".ts", ".js"]) => exts.flatMap((ext) => [ resolve(sourceDirectory, `${moduleName}.${browserTarget}${ext}`), resolve(sourceDirectory, moduleName, `index.${browserTarget}${ext}`), resolve(sourceDirectory, `${moduleName}.${process.env.NODE_ENV}${ext}`), resolve( sourceDirectory, moduleName, `index.${process.env.NODE_ENV}${ext}` ), resolve(sourceDirectory, `${moduleName}${ext}`), resolve(sourceDirectory, moduleName, `index${ext}`) ]) const popupIndexList = getIndexList("popup", uiExts) const optionsIndexList = getIndexList("options", uiExts) const devtoolsIndexList = getIndexList("devtools", uiExts) const newtabIndexList = getIndexList("newtab", uiExts) const sidepanelIndexList = getIndexList("sidepanel", uiExts) const popupHtmlList = getIndexList("popup", [".html"]) const optionsHtmlList = getIndexList("options", [".html"]) const devtoolsHtmlList = getIndexList("devtools", [".html"]) const newtabHtmlList = getIndexList("newtab", [".html"]) const sidepanelHtmlList = getIndexList("sidepanel", [".html"]) const envFileList = getEnvFileNames().map((f) => resolve(sourceDirectory, f)) const backgroundIndexList = getIndexList("background") const contentIndexList = getModuleList("content") const sandboxIndexList = getModuleList("sandbox") const watchPathReasonMap = { [packageFilePath]: WatchReason.PackageJson, ...getWatchReasonMap(envFileList, WatchReason.EnvFile), ...getWatchReasonMap(contentIndexList, WatchReason.ContentScriptIndex), ...getWatchReasonMap(sandboxIndexList, WatchReason.SandboxIndex), ...getWatchReasonMap(backgroundIndexList, WatchReason.BackgroundIndex), ...getWatchReasonMap(popupIndexList, WatchReason.PopupIndex), ...getWatchReasonMap(optionsIndexList, WatchReason.OptionsIndex), ...getWatchReasonMap(devtoolsIndexList, WatchReason.DevtoolsIndex), ...getWatchReasonMap(newtabIndexList, WatchReason.NewtabIndex), ...getWatchReasonMap(sidepanelIndexList, WatchReason.SidePanelIndex), ...getWatchReasonMap(popupHtmlList, WatchReason.PopupHtml), ...getWatchReasonMap(optionsHtmlList, WatchReason.OptionsHtml), ...getWatchReasonMap(devtoolsHtmlList, WatchReason.DevtoolsHtml), ...getWatchReasonMap(newtabHtmlList, WatchReason.NewtabHtml), ...getWatchReasonMap(sidepanelHtmlList, WatchReason.SidePanelHtml) } const contentsDirectory = resolve(sourceDirectory, "contents") const sandboxesDirectory = resolve(sourceDirectory, "sandboxes") const tabsDirectory = resolve(sourceDirectory, "tabs") const backgroundDirectory = resolve(sourceDirectory, "background") const watchDirectoryEntries = [ [WatchReason.SandboxesDirectory, sandboxesDirectory], [WatchReason.TabsDirectory, tabsDirectory], [WatchReason.ContentScriptsDirectory, contentsDirectory], [WatchReason.BackgroundDirectory, backgroundDirectory], [WatchReason.AssetsDirectory, assetsDirectory] ] as Array const knownPathSet = new Set(Object.keys(watchPathReasonMap)) const entryFileSet = new Set([ ...backgroundIndexList, ...contentIndexList, ...sandboxIndexList, ...popupIndexList, ...optionsIndexList, ...devtoolsIndexList, ...newtabIndexList, ...sidepanelIndexList ]) const isEntryPath = (path: string) => entryFileSet.has(path) return { popupIndexList, popupHtmlList, optionsIndexList, optionsHtmlList, devtoolsIndexList, devtoolsHtmlList, newtabIndexList, newtabHtmlList, backgroundIndexList, backgroundDirectory, contentIndexList, contentsDirectory, sidepanelIndexList, sidepanelHtmlList, sandboxIndexList, sandboxesDirectory, tabsDirectory, watchPathReasonMap, watchDirectoryEntries, isEntryPath, knownPathSet } } export type ProjectPath = ReturnType ================================================ FILE: cli/plasmo/src/features/extension-devtools/project-watcher.ts ================================================ import { subscribe, type Event } from "@parcel/watcher" import { PARCEL_WATCHER_BACKEND } from "@plasmo/constants/misc" import { assertUnreachable } from "@plasmo/utils/assert" import { hasFlag } from "@plasmo/utils/flags" import { iLog, vLog, wLog } from "@plasmo/utils/logging" import { updateBgswEntry } from "~features/background-service-worker/update-bgsw-entry" import { type PlasmoManifest } from "~features/manifest-factory/base" import { generateIcons } from "./generate-icons" import { WatchReason } from "./project-path" const ignore = ["node_modules", "build", ".plasmo", "coverage", ".git"] export const createProjectWatcher = async (plasmoManifest: PlasmoManifest) => { if (hasFlag("--impulse")) { return null } const { default: isPathInside } = await import("is-path-inside") const { knownPathSet, watchPathReasonMap, watchDirectoryEntries } = plasmoManifest.projectPath vLog("Watching the following files:", knownPathSet) const getWatchReason = (path: string) => { // Quick track processing files already inside watch set if (knownPathSet.has(path)) { return watchPathReasonMap[path] } const validEntry = watchDirectoryEntries.find(([, dir]) => isPathInside(path, dir) ) if (!validEntry) { return WatchReason.None } // Add new path to the watch set if they are watch directories knownPathSet.add(path) return (watchPathReasonMap[path] = validEntry[0]) } return subscribe( plasmoManifest.commonPath.projectDirectory, async (err, events) => { if (err) { throw err } await Promise.all( events.map(({ path, type }) => handleProjectFile(type, path, getWatchReason(path), plasmoManifest) ) ) await plasmoManifest.write() }, { backend: PARCEL_WATCHER_BACKEND, ignore } ) } export const handleProjectFile = async ( type: Event["type"], path: string, reason: WatchReason, plasmoManifest: PlasmoManifest ) => { const isEnabled = type !== "delete" switch (reason) { case WatchReason.None: { return } case WatchReason.EnvFile: { wLog("Environment file change detected, please restart the dev server.") await plasmoManifest.updateEnv() return } case WatchReason.AssetsDirectory: { iLog("Assets directory changed, update dynamic assets") await generateIcons(plasmoManifest.commonPath) return } case WatchReason.PackageJson: { iLog( "package.json changed, updating manifest overrides. You might need to restart the dev server." ) await plasmoManifest.updatePackageData() return } case WatchReason.BackgroundDirectory: case WatchReason.BackgroundIndex: { await updateBgswEntry(plasmoManifest) // TODO: Make this a soft-check instead of a file-write ops return } case WatchReason.ContentScriptIndex: case WatchReason.ContentScriptsDirectory: { await plasmoManifest.toggleContentScript(path, isEnabled) if (plasmoManifest.hasMainWorldScript) { await updateBgswEntry(plasmoManifest) } return } case WatchReason.SandboxIndex: case WatchReason.SandboxesDirectory: case WatchReason.TabsDirectory: { await plasmoManifest.togglePage(path, isEnabled) return } case WatchReason.SidePanelIndex: { plasmoManifest.toggleSidePanel(isEnabled) return } case WatchReason.PopupIndex: { plasmoManifest.togglePopup(isEnabled) return } case WatchReason.OptionsIndex: { plasmoManifest.toggleOptions(isEnabled) return } case WatchReason.DevtoolsIndex: { plasmoManifest.toggleDevtools(isEnabled) return } case WatchReason.NewtabIndex: { plasmoManifest.toggleNewtab(isEnabled) return } case WatchReason.PopupHtml: { await plasmoManifest.scaffolder.createPageHtml("popup", isEnabled && path) return } case WatchReason.OptionsHtml: { await plasmoManifest.scaffolder.createPageHtml( "options", isEnabled && path ) return } case WatchReason.DevtoolsHtml: { await plasmoManifest.scaffolder.createPageHtml( "devtools", isEnabled && path ) return } case WatchReason.NewtabHtml: { await plasmoManifest.scaffolder.createPageHtml( "newtab", isEnabled && path ) return } case WatchReason.SidePanelHtml: { await plasmoManifest.scaffolder.createPageHtml( "sidepanel", isEnabled && path ) return } default: assertUnreachable(reason) } } ================================================ FILE: cli/plasmo/src/features/extension-devtools/strip-underscore.ts ================================================ import { readdir, readFile, rename, stat, writeFile } from "fs/promises" import { join, resolve } from "path" const stripFileUnderscore = async (filePath: string) => { const fileContents = await readFile(resolve(filePath), "utf8") const newFileContents = fileContents.replace(/\/_/g, "/") await writeFile(filePath, newFileContents, "utf8") } export const stripUnderscore = async (dir = "") => { const entries = await readdir(dir) for (const entry of entries) { const entryPath = join(dir, entry) const entryStat = await stat(entryPath) if (entryStat.isDirectory()) { const newPath = entryPath.replace("/_", "/") await rename(entryPath, newPath) await stripUnderscore(newPath) } else { const newPath = entryPath.replace("/_", "/") await rename(entryPath, newPath) await stripFileUnderscore(newPath) } } } ================================================ FILE: cli/plasmo/src/features/extension-devtools/template-path.ts ================================================ import { dirname, resolve } from "path" import { fileURLToPath } from "url" export const getTemplatePath = () => { const packagePath = dirname(fileURLToPath(import.meta.url)) const templatePath = resolve(packagePath, "..", "templates") const staticTemplatePath = resolve(templatePath, "static") const initTemplatePackagePath = resolve( require.resolve("@plasmohq/init"), ".." ) const initTemplatePath = resolve(initTemplatePackagePath, "templates") const initEntryPath = resolve(initTemplatePackagePath, "entries") const bppYaml = resolve(initTemplatePackagePath, "bpp.yml") return { templatePath, initTemplatePath, initEntryPath, staticTemplatePath, bppYaml } } export type TemplatePath = ReturnType ================================================ FILE: cli/plasmo/src/features/extension-devtools/tsconfig.ts ================================================ import { readFile } from "fs/promises" import { resolve } from "path" import { outputFile, outputJson } from "fs-extra" import json5 from "json5" import { MESSAGING_DECLARATION } from "~features/background-service-worker/bgsw-messaging-declaration" import { PROCESS_ENV_DECLARATION } from "~features/env/env-declaration" import type { CommonPath } from "~features/extension-devtools/common-path" const DECLARATION_FILEPATH = `.plasmo/index.d.ts` const INDEX_DECLARATION_CODE = [PROCESS_ENV_DECLARATION, MESSAGING_DECLARATION] .map((e) => `import "./${e}"`) .join("\n") export const addDeclarationConfig = async ( commonPath: CommonPath, filePath: string ) => { const tsconfigFilePath = resolve(commonPath.projectDirectory, "tsconfig.json") const tsconfigFile = await readFile(tsconfigFilePath, "utf8") const tsconfig = json5.parse(tsconfigFile) const includeSet = new Set(tsconfig.include) if (includeSet.has(filePath)) { return } tsconfig.include = [filePath, ...includeSet] await outputJson(tsconfigFilePath, tsconfig, { spaces: 2 }) } export const outputIndexDeclaration = async (commonPath: CommonPath) => { const declarationFilePath = resolve( commonPath.projectDirectory, DECLARATION_FILEPATH ) await Promise.all([ outputFile(declarationFilePath, INDEX_DECLARATION_CODE), addDeclarationConfig(commonPath, DECLARATION_FILEPATH) ]) } ================================================ FILE: cli/plasmo/src/features/extra/cache-busting.ts ================================================ import { lstat } from "fs/promises" import { resolve } from "path" import { emptyDir, ensureDir } from "fs-extra" import { isAccessible } from "@plasmo/utils/fs" import { vLog } from "@plasmo/utils/logging" import type { CommonPath } from "~features/extension-devtools/common-path" export async function cleanUpDotPlasmo({ dotPlasmoDirectory, cacheDirectory }: CommonPath) { await emptyDir(dotPlasmoDirectory) await ensureDir(cacheDirectory) } export async function cleanUpLargeCache(commonPath: CommonPath) { const parcelCacheDbFilePath = resolve( commonPath.cacheDirectory, "parcel", "data.mdb" ) const hasCache = await isAccessible(parcelCacheDbFilePath) if (!hasCache) { return } const cacheDbFileSize = (await lstat(parcelCacheDbFilePath, { bigint: true })) .size const sizeInMB = cacheDbFileSize / 1024n ** 2n const sizeInGB = Number(sizeInMB) / 1024 // TODO: calculate the limit based on some heuristic around the size of the project instead of a fixed value. const cacheLimitGB = 1.47 if (sizeInGB > cacheLimitGB) { vLog(`Busting large build cache, size: ${sizeInGB.toFixed(2)} GB`) await cleanUpDotPlasmo(commonPath) } } ================================================ FILE: cli/plasmo/src/features/extra/next-new-tab.ts ================================================ import { mkdir } from "fs/promises" import { resolve } from "path" import { sentenceCase } from "change-case" import { copy, emptyDir, readJson, writeJson } from "fs-extra" import { isAccessible } from "@plasmo/utils/fs" import { sLog, vLog } from "@plasmo/utils/logging" import { getCommonPath } from "~features/extension-devtools/common-path" import type { PackageJSON } from "~features/extension-devtools/package-file" import { stripUnderscore } from "~features/extension-devtools/strip-underscore" export const generateNewTabManifest = (packageData: PackageJSON) => ({ name: sentenceCase(packageData.name), description: packageData.description, version: packageData.version, manifest_version: 3, chrome_url_overrides: { newtab: "./index.html" } }) export const nextNewTab = async () => { const { projectDirectory, packageFilePath } = getCommonPath() vLog("Creating a Plasmo + Nextjs based new tab extension") const out = resolve(projectDirectory, "out") const { default: chalk } = await import("chalk") if (!(await isAccessible(out))) { throw new Error( `${chalk.bold( "out" )} directory does not exist, did you forget to run "${chalk.underline( "next build && next export" )}"?` ) } const packageData: PackageJSON = await readJson(packageFilePath) const extensionDirectory = resolve(projectDirectory, "extension") if (await isAccessible(extensionDirectory)) { const { default: { prompt } } = await import("inquirer") const { answer } = await prompt({ type: "confirm", name: "answer", message: `${chalk.bold( "extension" )} directory already exists, do you want to overwrite it?` }) if (!answer) { throw new Error("Aborted") } await emptyDir(extensionDirectory) } await mkdir(extensionDirectory) await copy(out, extensionDirectory) vLog("Extension created at:", extensionDirectory) await stripUnderscore(extensionDirectory) // Create manifest.json with chrome_url_overrides with index.html await writeJson( resolve(extensionDirectory, "manifest.json"), generateNewTabManifest(packageData), { spaces: 2 } ) sLog("Your extension is ready in the extension/ directory") } ================================================ FILE: cli/plasmo/src/features/framework-update/version-tracker.ts ================================================ import { readJson, writeJson } from "fs-extra" import getPackageJson, { type AbbreviatedVersion } from "package-json" import semver from "semver" import { isAccessible } from "@plasmo/utils/fs" import { aLog, eLog, vLog, wLog } from "@plasmo/utils/logging" import type { CommonPath } from "~features/extension-devtools/common-path" import { cleanUpDotPlasmo } from "~features/extra/cache-busting" import { getPackageManager } from "~features/helpers/package-manager" export const updateVersionFile = async (commonPath: CommonPath) => { const { plasmoVersionFilePath } = commonPath if (!(await isAccessible(plasmoVersionFilePath))) { vLog("Plasmo version file not found, busting cache...") await cleanUpDotPlasmo(commonPath) } else { const cachedVersion = await readJson(plasmoVersionFilePath) const semverCachedVersion = semver.coerce(cachedVersion.version) const semverCurrentVersion = semver.coerce(process.env.APP_VERSION)! if ( !semverCachedVersion || semverCachedVersion.major < semverCurrentVersion.major || (semverCachedVersion.major === semverCurrentVersion.major && semverCachedVersion.minor < semverCurrentVersion.minor) ) { vLog("Plasmo updated, busting cache...") await cleanUpDotPlasmo(commonPath) } } await writeJson(plasmoVersionFilePath, { version: process.env.APP_VERSION }) } export const checkNewVersion = async () => { // If the version is different, log a warning about new version is available const currentVersion = process.env.APP_VERSION // If the version is different, log a warning about new version is available try { // Get the latest version of plasmo const latestPackageJson = (await getPackageJson("plasmo", { version: "latest" })) as unknown as AbbreviatedVersion const latestVersion = latestPackageJson.version // If the version is different, log a warning about new version is available if (semver.lt(currentVersion, latestVersion)) { const { default: chalk } = await import("chalk") wLog( chalk.yellowBright( `A new version of plasmo is available: v${latestVersion}` ) ) const updateCmd = await getUpdateCmd(latestVersion) aLog(chalk.yellow(`Run ${updateCmd} to update`)) } } catch (error) { eLog('Error fetching package information for "plasmo"', error) } } async function getUpdateCmd(version = "") { const packageManager = await getPackageManager() switch (packageManager.name) { case "npm": return `"npm i -S plasmo@${version}"` case "pnpm": return `"pnpm i plasmo@${version}"` case "yarn": return `"yarn add plasmo@${version}"` } } ================================================ FILE: cli/plasmo/src/features/helpers/create-parcel-bundler.ts ================================================ import { dirname, join, resolve } from "path" import ParcelFS from "@parcel/fs" import ParcelPM from "@parcel/package-manager" import { emptyDir, ensureDir, exists, readJson, writeJson } from "fs-extra" import { getFlag, hasFlag } from "@plasmo/utils/flags" import { wLog } from "@plasmo/utils/logging" import { Parcel, type ParcelOptions } from "@plasmohq/parcel-core" import type { PlasmoManifest } from "~features/manifest-factory/base" import { getPackageManager } from "./package-manager" import { setInternalEnv } from "~features/env/env-config" const PackageInstallerMap = { npm: ParcelPM.Npm, yarn: ParcelPM.Yarn, pnpm: ParcelPM.Pnpm } export const createParcelBuilder = async ( { commonPath, bundleConfig, publicEnv }: PlasmoManifest, { defaultTargetOptions = {}, ...options }: ParcelOptions ) => { const isProd = options.mode === "production" if (isProd) { await emptyDir(commonPath.distDirectory) } else { await ensureDir(commonPath.distDirectory) } process.env.__PLASMO_FRAMEWORK_INTERNAL_NO_MINIFY = isProd && hasFlag("--no-minify") ? "true" : "false" process.env.__PLASMO_FRAMEWORK_INTERNAL_SOURCE_MAPS = isProd ? hasFlag("--inline-source-maps") ? "inline" : hasFlag("--source-maps") ? "external" : "none" : hasFlag("--no-source-maps") ? "none" : "inline" process.env.__PLASMO_FRAMEWORK_INTERNAL_NO_CS_RELOAD = hasFlag( "--no-cs-reload" ) ? "true" : "false" process.env.__PLASMO_FRAMEWORK_INTERNAL_ES_TARGET = (getFlag("--es-target") as any) || "es2022" const pmInfo = await getPackageManager() const inputFS = new ParcelFS.NodeFS() const PackageInstaller = PackageInstallerMap[pmInfo.name] const packageManager = new ParcelPM.NodePackageManager( inputFS, commonPath.projectDirectory, new PackageInstaller() ) const baseConfig = require.resolve("@plasmohq/parcel-config") let runConfig = join(dirname(baseConfig), "run.json") const configJson = await readJson(baseConfig) if (hasFlag("--bundle-buddy")) { configJson.reporters = ["...", "@parcel/reporter-bundle-buddy"] } await writeJson(runConfig, configJson) if (await exists(commonPath.parcelConfig)) { runConfig = commonPath.parcelConfig if (isProd) { const customConfig = await readJson(runConfig) if (customConfig.extends !== "@plasmohq/parcel-config") { wLog( 'The .parcelrc does not extend "@plasmohq/parcel-config", the result may be unexpected' ) } } if (hasFlag("--bundle-buddy")) { wLog( 'The "--bundle-buddy" flag does not work with a custom .parcelrc file' ) } } const engines = { browsers: bundleConfig.manifestVersion === "mv2" && bundleConfig.browser !== "firefox" ? ["IE 11"] : ["last 1 Chrome version"] } setInternalEnv(bundleConfig) const bundler = new Parcel({ inputFS, packageManager, entries: commonPath.entryManifestPath, cacheDir: resolve(commonPath.cacheDirectory, "parcel"), config: runConfig, shouldAutoInstall: true, env: publicEnv.extends(bundleConfig).data, defaultTargetOptions: { ...defaultTargetOptions, engines, sourceMaps: process.env.__PLASMO_FRAMEWORK_INTERNAL_SOURCE_MAPS !== "none", distDir: commonPath.distDirectory }, ...options }) return bundler } ================================================ FILE: cli/plasmo/src/features/helpers/crypto.ts ================================================ import { createHash } from "crypto" /** * Fast hash for local file revving * DO NOT USE FOR SENSITIVE PURPOSES * md5 is good enough for file-revving: https://github.com/sindresorhus/rev-hash */ export const getRevHash = (buff: Buffer) => createHash("md5").update(buff).digest("hex").slice(0, 18) ================================================ FILE: cli/plasmo/src/features/helpers/flag.ts ================================================ import { kebabCase } from "change-case" import { getFlag } from "@plasmo/utils/flags" export const getFlagMap = () => { const srcPath = getFlag("--src-path") || process.env.PLASMO_SRC_PATH || "src" const buildPath = getFlag("--build-path") || process.env.PLASMO_BUILD_PATH || "build" const tag = getFlag("--tag") || process.env.PLASMO_TAG || (process.env.NODE_ENV === "production" ? "prod" : "dev") const target = kebabCase( getFlag("--target") || process.env.PLASMO_TARGET || "chrome-mv3" ) const [browser, manifestVersion] = target.split("-") const entry = getFlag("--entry") || "popup" const envPath = getFlag("--env") return { browser, manifestVersion, tag, srcPath, buildPath, target, entry, envPath } } const DEV_BUILD_COMMON_ARGS = ` --target=[string] set the target (default: chrome-mv3) --tag=[string] set the build tag (default: dev or prod depending on NODE_ENV) --src-path=[path] set the source path relative to project root (default: src) --build-path=[path] set the build path relative to project root (default: build) --entry=[name] entry point name (default: popup) --env=[path] relative path to top-level env file --no-cs-reload disable content script auto reloading` export const flagHelp = ` init --entry=[name] entry files (default: popup) --with- use an example template dev ${DEV_BUILD_COMMON_ARGS} build ${DEV_BUILD_COMMON_ARGS} --zip zip the build output ` ================================================ FILE: cli/plasmo/src/features/helpers/loading-animation.ts ================================================ const LOADING_TEXT = "🔄 Building" const state = { loadingInterval: null as NodeJS.Timeout | null, isLoading: false, dotCount: 0 } export const startLoading = () => { if (state.isLoading) { return } state.isLoading = true process.stdout.write(LOADING_TEXT) state.loadingInterval = setInterval(() => { state.dotCount = (state.dotCount + 1) % 4 let dotString = state.dotCount === 0 ? " " : ".".repeat(state.dotCount) process.stdout.write(`\r${LOADING_TEXT}${dotString}`) }, 400) } export const stopLoading = () => { if (!state.isLoading) { return } state.isLoading = false if (state.loadingInterval) { clearInterval(state.loadingInterval) state.loadingInterval = null } // Clear the loading text process.stdout.write("\r" + " ".repeat(20) + "\r") } ================================================ FILE: cli/plasmo/src/features/helpers/package-manager.ts ================================================ /** * Forked from https://github.com/vercel/next.js/blob/canary/packages/create-next-app/helpers/get-pkg-manager.ts */ import spawnAsync from "@expo/spawn-async" import semver from "semver" export type PackageManager = "npm" | "pnpm" | "yarn" export type PackageManagerInfo = { name: PackageManager version?: string } async function getPMInfo(name: PackageManager): Promise { const data = await spawnAsync(name, ["--version"]) const version = semver.valid(data.stdout.trim()) || undefined return { name, version } } export async function getPackageManager(): Promise { try { const userAgent = process.env.npm_config_user_agent if (userAgent) { if (userAgent.startsWith("yarn")) { return { name: "yarn" } } else if (userAgent.startsWith("pnpm")) { return { name: "pnpm" } } } try { return await getPMInfo("pnpm") } catch { return await getPMInfo("yarn") } } catch { return await getPMInfo("npm") } } ================================================ FILE: cli/plasmo/src/features/helpers/print.ts ================================================ import { cLog } from "@plasmo/utils/logging" import { validCommandList } from "~commands" import { flagHelp } from "~features/helpers/flag" export const printHeader = () => { console.log(`🟣 Plasmo v${process.env.APP_VERSION}`) console.log("🔴 The Browser Extension Framework") } export const printHelp = () => { cLog("🟠 CMDS", validCommandList.join(" | ")) cLog("🟡 OPTS", flagHelp) } ================================================ FILE: cli/plasmo/src/features/helpers/prompt.ts ================================================ import inquirer from "inquirer" export const quickPrompt = async (label = "", defaultValue = "") => { const { data } = await inquirer.prompt({ name: "data", prefix: "🟡", message: label, default: defaultValue }) return data as string } ================================================ FILE: cli/plasmo/src/features/helpers/traverse.ts ================================================ import { iLog } from "@plasmo/utils/logging" const defaultTransformer = (target: any) => iLog({ target }) /** * Traverse a target object and apply a transformer function to each value. * Retains only defined values. */ export const definedTraverse = ( target: any, transformer = defaultTransformer ): any => { if (Array.isArray(target)) { return target .map((item) => definedTraverse(item, transformer)) .filter((i) => i !== undefined) } else if (typeof target === "object") { const result = {} as any for (const key in target) { if (target.hasOwnProperty(key)) { result[key] = definedTraverse(target[key], transformer) if (result[key] === undefined) { delete result[key] } } } if (Object.keys(result).length === 0) { return undefined } return result } else { return transformer(target) } } ================================================ FILE: cli/plasmo/src/features/manifest-factory/base.ts ================================================ import { ok } from "assert" import { readdir } from "fs/promises" import { basename, dirname, extname, isAbsolute, join, parse, relative, resolve } from "path" import { cwd } from "process" import glob from "fast-glob" import { copy, ensureDir, existsSync, pathExists, readJson, writeJson } from "fs-extra" import { hasher as createHasher } from "node-object-hash" import type { ChromeUrlOverrideType, ExtensionManifest, ExtensionManifestV3, ManifestContentScript, ManifestPermission } from "@plasmo/constants" import { buildBroadcast, BuildSocketEvent } from "@plasmo/framework-shared/build-socket" import { assertTruthy } from "@plasmo/utils/assert" import { injectEnv } from "@plasmo/utils/env" import { isDirectory, isReadable } from "@plasmo/utils/fs" import { vLog, wLog } from "@plasmo/utils/logging" import { getSubExt, toPosix } from "@plasmo/utils/path" import { loadEnvConfig, type EnvConfig } from "~features/env/env-config" import { outputEnvDeclaration } from "~features/env/env-declaration" import { getCommonPath, type CommonPath } from "~features/extension-devtools/common-path" import { extractContentScriptConfig } from "~features/extension-devtools/content-script-config" import { generateIcons } from "~features/extension-devtools/generate-icons" import type { PlasmoBundleConfig } from "~features/extension-devtools/get-bundle-config" import type { PackageJSON } from "~features/extension-devtools/package-file" import { getProjectPath, type ProjectPath } from "~features/extension-devtools/project-path" import { getTemplatePath } from "~features/extension-devtools/template-path" import { outputIndexDeclaration } from "~features/extension-devtools/tsconfig" import { cleanUpLargeCache } from "~features/extra/cache-busting" import { updateVersionFile } from "~features/framework-update/version-tracker" import { definedTraverse } from "~features/helpers/traverse" import { Scaffolder } from "./scaffolder" import { getUiExtMap, getUiLibrary, type UiExtMap, type UiLibrary } from "./ui-library" export const iconMap = { "16": "./gen-assets/icon16.plasmo.png", "32": "./gen-assets/icon32.plasmo.png", "48": "./gen-assets/icon48.plasmo.png", "64": "./gen-assets/icon64.plasmo.png", "128": "./gen-assets/icon128.plasmo.png" } export const autoPermissionList: ManifestPermission[] = ["storage"] const hasher = createHasher({ trim: true, sort: true }) export abstract class PlasmoManifest { get browser() { return this.bundleConfig.browser as typeof process.env.PLASMO_BROWSER } #commonPath?: CommonPath public get commonPath() { return assertTruthy(this.#commonPath) } #projectPath?: ProjectPath public get projectPath() { return assertTruthy(this.#projectPath) } readonly templatePath = getTemplatePath() #envConfig?: EnvConfig public get combinedEnv() { ok(this.#envConfig) return this.#envConfig.combinedEnv } public get publicEnv() { ok(this.#envConfig) return this.#envConfig.plasmoPublicEnv } #extSet = new Set([".ts", ".js"]) #uiExtSet = new Set() #uiLibraryData?: { uiLibrary: UiLibrary uiExtMap: UiExtMap } get uiLibrary() { ok(this.#uiLibraryData) return this.#uiLibraryData.uiLibrary } get mountExt() { ok(this.#uiLibraryData) return this.#uiLibraryData.uiExtMap.mountExt } get uiExts() { ok(this.#uiLibraryData) return this.#uiLibraryData.uiExtMap.uiExts } #hash = "" #prevHash = "" protected data: Partial = {} protected overideManifest: Partial = {} protected packageData?: PackageJSON contentScriptMap: Readonly> = new Map() get mainWorldScriptList() { const output: ManifestContentScript[] = [] for (const script of this.contentScriptMap.values()) { if (script.world === "MAIN") { output.push(script) } } return output } get hasMainWorldScript() { for (const script of this.contentScriptMap.values()) { if (script.world === "MAIN") { return true } } return false } readonly copyMap = new Map() readonly permissionSet = new Set() readonly scaffolder: Scaffolder get changed() { return this.#hash !== this.#prevHash } get name() { ok(this.packageData) return this.packageData.displayName } get dependencies() { ok(this.packageData) // to support npm workspaces (mono repos) we need to fallback to // peerDependencies because dependencies will never exist return this.packageData.dependencies ?? this.packageData.peerDependencies } get devDependencies() { ok(this.packageData) return this.packageData.devDependencies } get staticScaffoldPath() { ok(this.uiLibrary) return resolve(this.templatePath.staticTemplatePath, this.uiLibrary.path) } protected constructor(public bundleConfig: PlasmoBundleConfig) { this.data.icons = iconMap this.scaffolder = new Scaffolder(this) } private async initEnv(envRootDirectory = cwd()) { this.#envConfig = await loadEnvConfig(envRootDirectory) this.#commonPath = getCommonPath(envRootDirectory) } async startup() { await this.initEnv() vLog(`Ensure exists: ${this.commonPath.dotPlasmoDirectory}`) await ensureDir(this.commonPath.dotPlasmoDirectory) await cleanUpLargeCache(this.commonPath) await updateVersionFile(this.commonPath) await generateIcons(this.commonPath) await outputIndexDeclaration(this.commonPath) await this.updateEnv() await this.updatePackageData() } async postBuild() { await Promise.all( Array.from(this.copyMap, async ([dest, src]) => { if (!(await isReadable(dest))) { await copy(src, dest) } }) ) if (!process.env.POST_BUILD_SCRIPT) { return } const postBuildPath = resolve(process.env.POST_BUILD_SCRIPT) if (!existsSync(postBuildPath)) { wLog("Post-build script is unavailable:", postBuildPath) return } const postBuild = require(postBuildPath) postBuild() } async updateEnv() { await this.initEnv(this.commonPath.projectDirectory) await outputEnvDeclaration(this) } // https://github.com/PlasmoHQ/plasmo/issues/195 prefixDev = (s = "") => process.env.NODE_ENV === "development" ? `DEV | ${s}` : s async updatePackageData() { this.packageData = await readJson(this.commonPath.packageFilePath) if (!this.packageData) { throw new Error("Invalid package.json") } this.data.version = this.packageData.version this.data.author = this.packageData.author this.data.name = this.prefixDev(this.packageData.displayName) this.data.description = this.packageData.description if (this.packageData.homepage) { this.data.homepage_url = this.packageData.homepage } for (const perm of autoPermissionList) { if (`@plasmohq/${perm}` in (this.packageData.dependencies || {})) { this.permissionSet.add(perm) } } await this.#cacheUiLibrary() this.overideManifest = await this.#getOverrideManifest() } #cacheUiLibrary = async () => { const uiLibrary = await getUiLibrary(this) const uiExtMap = getUiExtMap(uiLibrary.name) this.#uiLibraryData = { uiLibrary, uiExtMap } this.#uiExtSet = new Set(uiExtMap.uiExts) this.uiExts.forEach((uiExt) => { this.#extSet.add(uiExt) }) this.#projectPath = getProjectPath( this.commonPath, this.browser, this.uiExts ) } abstract togglePopup: (enable?: boolean) => this abstract toggleBackground: (enable?: boolean) => boolean abstract toggleSidePanel: (enable?: boolean) => this toggleOptions = (enable = false) => { if (enable) { this.data.options_ui = { page: "./options.html", open_in_tab: true } } else { delete this.data.options_ui } return this } toggleOverrides = (page: ChromeUrlOverrideType, enable = false) => { if (enable) { this.data.chrome_url_overrides = { ...this.data.chrome_url_overrides, [page]: `./${page}.html` } } else { delete this.data.chrome_url_overrides?.[page] } return this } toggleNewtab = (enable = false) => this.toggleOverrides("newtab", enable) toggleDevtools = (enable = false) => { if (enable) { this.data.devtools_page = "./devtools.html" } else { delete this.data.devtools_page } return this } isPathInvalid = (path?: string): path is undefined => { if (path === undefined) { return true } const ext = extname(path) if (!this.#extSet.has(ext)) { return true } const subExt = getSubExt(path) // Ignore if path is browser specific and does not match browser if (subExt.length > 0 && subExt !== `.${this.browser}`) { return true } // Ignore if path is browser generic and there is a browser specific path if ( subExt.length === 0 && existsSync(path.replace(ext, `.${this.browser}${ext}`)) ) { return true } return false } toggleContentScript = async (path?: string, enable = false) => { if (this.isPathInvalid(path)) { return false } if (enable) { const metadata = await extractContentScriptConfig(path) if (metadata?.isEmpty) { return false } vLog("Adding content script: ", path) let scriptPath = relative(this.commonPath.dotPlasmoDirectory, path) const scriptExt = extname(scriptPath) const isCsui = this.#uiExtSet.has(scriptExt) if (this.uiLibrary?.name !== "vanilla" && isCsui) { // copy the contents and change the manifest path const modulePath = join("lab", scriptPath).replace(/(^src)[\\/]/, "") const parsedModulePath = parse(modulePath) scriptPath = relative( this.commonPath.dotPlasmoDirectory, await this.scaffolder.createContentScriptMount(parsedModulePath) ) } // Resolve css file paths if (!!metadata?.config?.css && metadata.config.css.length > 0) { metadata.config.css = metadata.config.css.map((cssFile) => relative( this.commonPath.dotPlasmoDirectory, resolve(dirname(path), cssFile) ) ) } const contentScript = this.injectEnvToObj({ matches: [""], js: [ metadata?.config?.world === "MAIN" ? scriptPath.split(scriptExt)[0] : scriptPath ], ...(metadata?.config || {}) }) this.contentScriptMap.set(path, contentScript) } else { this.contentScriptMap.delete(path) } buildBroadcast(BuildSocketEvent.CsChanged) return enable } addDirectory = async ( path: string, toggleDynamicPath: typeof this.toggleContentScript, filterFile?: (fileName: string) => boolean ) => { if (!existsSync(path)) { return false } return readdir(path, { withFileTypes: true }) .then((files) => Promise.all( files .filter((f) => f.isFile() && filterFile ? filterFile(f.name) : true ) .map((f) => resolve(path, f.name)) .map((filePath) => toggleDynamicPath(filePath, true)) ) ) .then((results) => results.includes(true)) } addContentScriptsIndexFiles = async () => { const path = this.projectPath.contentsDirectory if (!(await isDirectory(path))) { return false } const indexFileList = [...this.#extSet].flatMap((ext) => [ `index${ext}`, `index.${this.browser}${ext}` ]) return readdir(path, { withFileTypes: true }) .then((files) => Promise.all( files .filter((f) => f.isDirectory()) .map((dir) => resolve(path, dir.name)) .map((dirPath) => this.addDirectory(dirPath, this.toggleContentScript, (fileName) => indexFileList.includes(fileName) ) ) ) ) .then((results) => results.includes(true)) } addContentScriptsDirectory = async ( contentsDirectory = this.projectPath.contentsDirectory ) => Promise.all([ this.addDirectory(contentsDirectory, this.toggleContentScript), this.addContentScriptsIndexFiles() ]).then((results) => results.includes(true)) togglePage = async (path?: string, enable = false) => { if (this.isPathInvalid(path)) { return false } if (enable) { const scriptPath = relative(this.commonPath.sourceDirectory, path) const parsedModulePath = parse(scriptPath) const { wereFilesWritten } = await this.scaffolder.createPageMount(parsedModulePath) // if enabled, and the template file was written, invalidate hash! if (wereFilesWritten) { this.#hash = "" } } else { this.#hash = "" } return enable } addPagesDirectory = async (directory: string) => this.addDirectory(directory, this.togglePage) write = (force = false) => { this.#prevHash = this.#hash const newManifest = this.toJSON() this.#hash = hasher.hash(newManifest) if (!this.changed && !force) { return } vLog("Hash changed, updating manifest") return writeJson(this.commonPath.entryManifestPath, newManifest, { spaces: 2 }) } toJSON = () => { const base = { ...this.data } const { options_ui: overrideOptionUi, permissions: overridePermissions, content_scripts: overrideContentScripts, background: overrideBackground, ...overide } = this.overideManifest as T if (base.options_ui?.page) { base.options_ui = { ...base.options_ui, ...overrideOptionUi } } base.permissions = [ ...new Set([...this.permissionSet, ...(overridePermissions || [])]) ] if (base.permissions?.length === 0) { delete base.permissions } if (this.bundleConfig.manifestVersion === "mv2") { base.background = { ...base.background, ...overrideBackground } if (Object.keys(base.background).length === 0) { delete base.background } // Host permission is coupled with permission in mv2 if (overide["host_permissions"]?.length > 0) { base.permissions = [ ...(base.permissions || []), ...overide["host_permissions"] ] } } // Populate content_scripts base.content_scripts = [ ...Array.from(this.contentScriptMap.values()).filter( (s) => s.world !== "MAIN" // TODO: Remove this when Chrome natively supports mainworld for CS ), ...(overrideContentScripts! || []) ] if (base.content_scripts.length === 0) { delete base.content_scripts } return { ...base, ...overide } } protected abstract prepareOverrideManifest: () => | Promise> | Partial #getOverrideManifest = async (): Promise> => { if (!this.packageData?.manifest) { return {} } let output = await this.prepareOverrideManifest() if ((output.web_accessible_resources?.length || 0) > 0) { output.web_accessible_resources = await this.resolveWAR( this.packageData.manifest.web_accessible_resources ) } // Sanitize the BSS if (!!output.browser_specific_settings) { switch (this.browser) { case "edge": case "safari": output.browser_specific_settings = { [this.browser]: output.browser_specific_settings[this.browser] } break case "firefox": case "gecko": output.browser_specific_settings = { gecko: output.browser_specific_settings.gecko } break default: delete output.browser_specific_settings } } if (output.overrides && output.overrides[this.browser]) { output = { ...output, ...output.overrides[this.browser] } } delete output.overrides return this.injectEnvToObj(output) } protected abstract resolveWAR: ( war: ExtensionManifestV3["web_accessible_resources"] ) => Promise protected copyProjectFile = async ( inputFilePath: string ): Promise => { try { if (inputFilePath.startsWith("~")) { return this.copyProjectFile(inputFilePath.slice(1)) } if (inputFilePath.includes("*")) { // Handling glob... const files = await glob(inputFilePath, { cwd: this.commonPath.projectDirectory, onlyFiles: true }) await Promise.all(files.map(this.copyProjectFile)) return inputFilePath } const resourceFilePath = isAbsolute(inputFilePath) ? inputFilePath : resolve(this.commonPath.projectDirectory, inputFilePath) const canCopy = !this.projectPath.isEntryPath(resourceFilePath) && (await pathExists(resourceFilePath)) if (!canCopy) { return inputFilePath } const destination = resolve(this.commonPath.distDirectory, inputFilePath) this.copyMap.set(destination, resourceFilePath) return toPosix(inputFilePath) } catch { return inputFilePath } } protected copyNodeModuleFile = async (inputFilePath: string) => { try { const resourceFilePath = require.resolve(inputFilePath, { paths: [this.commonPath.projectDirectory] }) const fileName = basename(resourceFilePath) const destination = resolve(this.commonPath.distDirectory, fileName) this.copyMap.set(destination, resourceFilePath) return fileName } catch { return null } } protected injectEnvToObj = (target: T): O => definedTraverse(target, (value) => { if (typeof value !== "string") { return value } if (!!value.match(/^\$(\w+)$/)) { return this.combinedEnv[value.substring(1)] || undefined } else { return injectEnv(value, this.combinedEnv) } }) } ================================================ FILE: cli/plasmo/src/features/manifest-factory/create-manifest.ts ================================================ import { find } from "@plasmo/utils/array" import { isAccessible } from "@plasmo/utils/fs" import { vLog, wLog } from "@plasmo/utils/logging" import { updateBgswEntry } from "~features/background-service-worker/update-bgsw-entry" import type { PlasmoBundleConfig } from "~features/extension-devtools/get-bundle-config" import { PlasmoExtensionManifestMV2 } from "./mv2" import { PlasmoExtensionManifestMV3 } from "./mv3" export async function createManifest(bundleConfig: PlasmoBundleConfig) { vLog("Creating Manifest Factory...") const plasmoManifest = bundleConfig.manifestVersion === "mv3" ? new PlasmoExtensionManifestMV3(bundleConfig) : new PlasmoExtensionManifestMV2(bundleConfig) await plasmoManifest.startup() const { contentIndexList, sandboxIndexList, tabsDirectory, sandboxesDirectory } = plasmoManifest.projectPath const [contentIndex, sandboxIndex] = await Promise.all( [contentIndexList, sandboxIndexList].map((l) => find(l, isAccessible)) ) const initResults = await Promise.all([ plasmoManifest.scaffolder.init(), plasmoManifest.togglePage(sandboxIndex, true), plasmoManifest.toggleContentScript(contentIndex, true), plasmoManifest.addContentScriptsDirectory(), plasmoManifest.addPagesDirectory(tabsDirectory), plasmoManifest.addPagesDirectory(sandboxesDirectory) ]) // BGSW needs to check CS set for main world initResults.push(await updateBgswEntry(plasmoManifest)) const hasEntrypoints = initResults.flat() if (!hasEntrypoints.includes(true)) { wLog("Unable to find any entry files. The extension might be empty") } const [hasPopup, hasOptions, hasNewtab, hasDevtools, hasSidePanel] = hasEntrypoints plasmoManifest .togglePopup(hasPopup) .toggleOptions(hasOptions) .toggleNewtab(hasNewtab) .toggleDevtools(hasDevtools) .toggleSidePanel(hasSidePanel) await plasmoManifest.write(true) return plasmoManifest } ================================================ FILE: cli/plasmo/src/features/manifest-factory/mv2.ts ================================================ import type { ExtensionManifestV2, ExtensionManifestV3 } from "@plasmo/constants" import { iLog } from "@plasmo/utils/logging" import type { PlasmoBundleConfig } from "~features/extension-devtools/get-bundle-config" import { iconMap, PlasmoManifest } from "./base" export class PlasmoExtensionManifestMV2 extends PlasmoManifest { constructor(bundleConfig: PlasmoBundleConfig) { super(bundleConfig) this.data.manifest_version = 2 this.data.browser_action = { default_icon: iconMap } } toggleSidePanel = (enable = false) => { switch (this.browser) { case "firefox": case "gecko": { if (enable) { this.data.sidebar_action = { default_panel: "./sidepanel.html" } } else { delete this.data.sidebar_action } break } default: { iLog( "SidePanel is not available on chromium-based MV2 browsers, skipping." ) } } return this } togglePopup = (enable = false) => { if (enable) { this.data.browser_action!.default_popup = "./popup.html" } else { delete this.data.browser_action!.default_popup } return this } toggleBackground = (enable = false) => { if (enable) { this.data.background = { scripts: ["./static/background/index.ts"] } } else { delete this.data.background } return enable } protected prepareOverrideManifest = () => { const { manifest } = this.packageData const output = { ...manifest } as unknown as ExtensionManifestV2 // Merge host permissions into permissions output.permissions = [ ...(manifest.permissions || []), ...(manifest.host_permissions || []) ] as any if ("host_permissions" in output) { delete output["host_permissions"] } if ("content_security_policy" in manifest) { output.content_security_policy = manifest.content_security_policy?.extension_pages } return output } protected resolveWAR = ( war: ExtensionManifestV3["web_accessible_resources"] ) => Promise.all( war!.map(async ({ resources }) => { const resolvedResources = await Promise.all( resources.map( async (resourcePath) => (await this.copyNodeModuleFile(resourcePath)) || (await this.copyProjectFile(resourcePath)) ) ) return resolvedResources.flat() }) ).then((res) => res.flat()) } ================================================ FILE: cli/plasmo/src/features/manifest-factory/mv3.ts ================================================ import type { ExtensionManifestV3 } from "@plasmo/constants" import type { PlasmoBundleConfig } from "~features/extension-devtools/get-bundle-config" import { iconMap, PlasmoManifest } from "./base" export class PlasmoExtensionManifestMV3 extends PlasmoManifest { constructor(bundleConfig: PlasmoBundleConfig) { super(bundleConfig) this.data.manifest_version = 3 this.data.action = { default_icon: iconMap } } toggleSidePanel = (enable = false) => { switch (this.browser) { case "firefox": case "gecko": { if (enable) { this.data.sidebar_action = { default_panel: "./sidepanel.html" } } else { delete this.data.sidebar_action } break } default: { if (enable) { this.data.side_panel = { default_path: "./sidepanel.html" } this.permissionSet.add("sidePanel") } else { delete this.data.side_panel this.permissionSet.delete("sidePanel") } } } return this } togglePopup = (enable = false) => { if (enable) { this.data.action!.default_popup = "./popup.html" } else { delete this.data.action!.default_popup } return this } toggleBackground = (enable = false) => { if (enable) { this.data.background = { service_worker: "./static/background/index.ts" } } else { delete this.data.background } return enable } protected prepareOverrideManifest = () => ({ ...this.packageData!.manifest }) protected resolveWAR = ( war: ExtensionManifestV3["web_accessible_resources"] ) => Promise.all( war!.map(async ({ resources, ...warProps }) => { const resolvedResources = await Promise.all( resources.map( async (resourcePath) => (await this.copyNodeModuleFile(resourcePath)) || (await this.copyProjectFile(resourcePath)) ) ) return { resources: resolvedResources.flat(), ...warProps } }) ) } ================================================ FILE: cli/plasmo/src/features/manifest-factory/scaffolder.ts ================================================ import { readFile, writeFile } from "fs/promises" import { join, relative, resolve, type ParsedPath } from "path" import { copy, ensureDir } from "fs-extra" import { find } from "@plasmo/utils/array" import { isAccessible, isFile } from "@plasmo/utils/fs" import { vLog } from "@plasmo/utils/logging" import { toPosix } from "@plasmo/utils/path" import { getRevHash } from "~features/helpers/crypto" import { type PlasmoManifest } from "./base" import { isSupportedUiExt } from "./ui-library" type ExtensionUIPage = "popup" | "options" | "devtools" | "newtab" | "sidepanel" export class Scaffolder { #templateCache = {} as Record #outputHashCache = {} as Record get projectPath() { return this.plasmoManifest.projectPath } get commonPath() { return this.plasmoManifest.commonPath } get mountExt() { return this.plasmoManifest.mountExt } constructor(private plasmoManifest: PlasmoManifest) {} async init() { const [_, ...uiPagesResult] = await Promise.all([ this.#copyStaticCommon(), this.#initUiPageTemplate("popup"), this.#initUiPageTemplate("options"), this.#initUiPageTemplate("newtab"), this.#initUiPageTemplate("devtools"), this.#initUiPageTemplate("sidepanel") ]) return uiPagesResult } #copyStaticCommon = async () => { const templateCommonDirectory = resolve( this.plasmoManifest.templatePath.staticTemplatePath, "common" ) const staticCommonDirectory = resolve( this.commonPath.staticDirectory, "common" ) return copy(templateCommonDirectory, staticCommonDirectory) } #initUiPageTemplate = async (uiPageName: ExtensionUIPage) => { vLog(`Creating static templates for ${uiPageName}`) const indexList = this.projectPath[`${uiPageName}IndexList`] const htmlList = this.projectPath[`${uiPageName}HtmlList`] const [indexFile, htmlFile] = await Promise.all( [indexList, htmlList].map((l) => find(l, isAccessible)) ) const { staticDirectory } = this.commonPath // Generate the static directory await ensureDir(staticDirectory) const hasIndex = indexFile !== undefined // console.log({ indexFile, hasIndex }) const indexImport = hasIndex ? toPosix(relative(staticDirectory, indexFile)) : `~${uiPageName}` const uiPageModulePath = resolve( staticDirectory, `${uiPageName}${this.mountExt}` ) await Promise.all([ this.#cachedGenerate(`index${this.mountExt}`, uiPageModulePath, { __plasmo_import_module__: indexImport }), this.createPageHtml(uiPageName, htmlFile) ]) return hasIndex } generateHtml = async ( outputPath = "", scriptMountPath = "", overridePath = "" ) => { const templateReplace = { __plasmo_static_index_title__: this.plasmoManifest.name, __plasmo_static_script__: scriptMountPath } const hasOverride = await isFile(overridePath) return hasOverride ? this.#copyGenerate(overridePath, outputPath, { ...templateReplace, "": `
` }) : this.#cachedGenerate("index.html", outputPath, templateReplace) } /** * @return true if file was written, false if cache hit */ createPageHtml = async (uiPageName: ExtensionUIPage, htmlFile = "") => { const outputHtmlPath = resolve( this.commonPath.dotPlasmoDirectory, `${uiPageName}.html` ) const scriptMountPath = `./static/${uiPageName}${this.mountExt}` return this.generateHtml(outputHtmlPath, scriptMountPath, htmlFile) } createPageMount = async (module: ParsedPath) => { vLog(`Creating page mount template for ${module.dir}`) const staticModulePath = resolve( this.commonPath.dotPlasmoDirectory, module.dir ) const htmlPath = resolve(staticModulePath, `${module.name}.html`) await ensureDir(staticModulePath) const isUiExt = isSupportedUiExt(module.ext) const scriptPath = resolve( staticModulePath, `${module.name}${this.mountExt}` ) const overrideHtmlPath = resolve( this.commonPath.sourceDirectory, module.dir, `${module.name}.html` ) const generateResultList = await Promise.all( isUiExt ? [ this.#cachedGenerate(`index${this.mountExt}`, scriptPath, { __plasmo_import_module__: `~${toPosix( join(module.dir, module.name) )}` }), this.generateHtml( htmlPath, `./${module.name}${this.mountExt}`, overrideHtmlPath ) ] : [ this.generateHtml( htmlPath, `~${toPosix(join(module.dir, module.name))}${module.ext}`, overrideHtmlPath ) ] ) return { htmlPath, wereFilesWritten: generateResultList.some((r) => r) } } createContentScriptMount = async (module: ParsedPath) => { vLog(`Creating content script mount for ${module.dir}`) const staticModulePath = resolve( this.commonPath.staticDirectory, module.dir ) await ensureDir(staticModulePath) const staticContentPath = resolve( staticModulePath, `${module.name}${this.mountExt}` ) // Can pass metadata to check config for type of mount as well? await this.#cachedGenerate( `content-script-ui-mount${this.mountExt}`, staticContentPath, { __plasmo_mount_content_script__: `~${toPosix( join(module.dir, module.name) )}` } ) return staticContentPath } /** * @return true if file was written, false if cache hit */ #generate = async ( templateContent: string, outputFilePath: string, replaceMap: Record ) => { const finalScaffold = Object.entries(replaceMap).reduce( (html, [key, value]) => html.replaceAll(key, value), templateContent ) const hash = getRevHash(Buffer.from(finalScaffold)) if (this.#outputHashCache[outputFilePath] === hash) { return false } this.#outputHashCache[outputFilePath] = hash await writeFile(outputFilePath, finalScaffold) return true } /** * @return true if file was written, false if cache hit */ #copyGenerate = async ( filePath: string, outputFilePath: string, replaceMap: Record ) => { const templateContent = await readFile(filePath, "utf8") return this.#generate(templateContent, outputFilePath, replaceMap) } /** * @return true if file was written, false if cache hit */ #cachedGenerate = async ( fileName: string, outputFilePath: string, replaceMap: Record ) => { this.#templateCache[fileName] ||= await readFile( resolve(this.plasmoManifest.staticScaffoldPath, fileName), "utf8" ) return this.#generate( this.#templateCache[fileName], outputFilePath, replaceMap ) } } ================================================ FILE: cli/plasmo/src/features/manifest-factory/ui-library.ts ================================================ import { resolve } from "path" import { cwd } from "process" import { readJson } from "fs-extra" import semver from "semver" import { assertUnreachable } from "@plasmo/utils/assert" import { isAccessible } from "@plasmo/utils/fs" import { type PlasmoManifest } from "./base" const supportedUiLibraries = ["react", "svelte", "vue", "vanilla"] as const type SupportedUiLibraryName = (typeof supportedUiLibraries)[number] const supportedUiExt = [".tsx", ".svelte", ".vue", ".jsx"] as const export type SupportedUiExt = (typeof supportedUiExt)[number] const supportedUiExtSet = new Set(supportedUiExt) export const isSupportedUiExt = (ext: string): ext is SupportedUiExt => supportedUiExtSet.has(ext as SupportedUiExt) export type UiLibrary = { name: SupportedUiLibraryName path: `${SupportedUiLibraryName}${number | ""}` version: number } const supportedMountExt = [".ts", ".tsx"] as const export type ScaffolderMountExt = (typeof supportedMountExt)[number] export type UiExtMap = { uiExts: SupportedUiExt[] mountExt: ScaffolderMountExt } const uiLibraryError = `No supported UI library found. You can file an RFC for a new UI Library here: https://github.com/PlasmoHQ/plasmo/issues` const getMajorVersion = async (version: string) => { if (version.includes(":")) { const [protocol, versionData] = version.split(":") switch (protocol) { case "file": default: const packageJson = await readJson( resolve(cwd(), versionData, "package.json") ) return semver.coerce(packageJson.version)?.major } } else { return semver.coerce(version)?.major } } export const getUiLibrary = async ( plasmoManifest: PlasmoManifest ): Promise => { const dependencies = plasmoManifest.dependencies ?? {} const baseLibrary = supportedUiLibraries.find((l) => l in dependencies) if (baseLibrary === undefined) { return { name: "vanilla", path: "vanilla", version: 8427 } } const majorVersion = await getMajorVersion(dependencies[baseLibrary]) if (majorVersion === undefined) { throw new Error(uiLibraryError) } // React lower than 18 can uses 17 scaffold if (baseLibrary === "react" && majorVersion < 18) { return { name: baseLibrary, path: "react17", version: majorVersion } } const uiLibraryPath = `${baseLibrary}${majorVersion}` as const const staticPath = resolve( plasmoManifest.templatePath.staticTemplatePath, uiLibraryPath ) if (!(await isAccessible(staticPath))) { throw new Error(uiLibraryError) } return { name: baseLibrary, path: uiLibraryPath, version: majorVersion } } export const getUiExtMap = ( uiLibraryName: SupportedUiLibraryName ): UiExtMap => { switch (uiLibraryName) { case "svelte": return { uiExts: [".svelte"], mountExt: ".ts" } case "vue": return { uiExts: [".vue"], mountExt: ".ts" } case "react": return { uiExts: [".tsx", ".jsx"], mountExt: ".tsx" } case "vanilla": return { uiExts: [".tsx", ".jsx"], mountExt: ".ts" } default: assertUnreachable(uiLibraryName) } } ================================================ FILE: cli/plasmo/src/features/manifest-factory/zip.ts ================================================ import { createReadStream, createWriteStream } from "fs" import { resolve } from "path" import glob from "fast-glob" import { AsyncZipDeflate, Zip } from "fflate" import { iLog } from "@plasmo/utils/logging" import type { CommonPath } from "~features/extension-devtools/common-path" function toMB(bytes: number) { return bytes / 1024 / 1024 } export const zipBundle = async ( { distDirectory, buildDirectory, distDirectoryName }: CommonPath, withMaps = false ) => { const zipFilePath = resolve(buildDirectory, `${distDirectoryName}.zip`) const output = createWriteStream(zipFilePath) const fileList = await glob( [ "**/*", // Pick all nested files !withMaps && "!**/(*.js.map|*.css.map)" // Exclude source maps ].filter(Boolean), { cwd: distDirectory, onlyFiles: true } ) return new Promise((pResolve, pReject) => { let size = 0 let aborted = false const timer = Date.now() const zip = new Zip((err, data, final) => { if (aborted) { return } else if (err) { pReject(err) } else { size += data.length output.write(data) if (final) { iLog( `Zip Package size: ${toMB(size).toFixed(2)} MB in ${ Date.now() - timer }ms` ) output.end() pResolve() } } }) // Start all the file read streams for (const file of fileList) { if (aborted) { return } const data = new AsyncZipDeflate(file, { level: 9 }) zip.add(data) const absPath = resolve(distDirectory, file) createReadStream(absPath) .on("data", (chunk: Buffer) => { data.push(chunk, false) }) .on("end", () => { data.push(new Uint8Array(0), true) // Notify completion }) .on("error", (error) => { aborted = true zip.terminate() pReject(`Error reading file ${absPath}: ${error.message}`) }) } zip.end() }) } ================================================ FILE: cli/plasmo/src/features/project-creator/from-existing-manifest.ts ================================================ import { readFile } from "fs/promises" import { extname } from "path" import { strFromU8, unzipSync, type Unzipped } from "fflate" import { readJson } from "fs-extra" import type { ExtensionManifest, ExtensionManifestV2, ExtensionManifestV3, ManifestPermission } from "@plasmo/constants" import { vLog } from "@plasmo/utils/logging" import type { CommonPath } from "~features/extension-devtools/common-path" import { generatePackage, type PackageJSON } from "~features/extension-devtools/package-file" import type { PackageManagerInfo } from "~features/helpers/package-manager" export const getManifestData = async (absPath: string) => { const data = { unzipped: {} as Unzipped, isZip: false, manifestData: {} as ExtensionManifest } const ext = extname(absPath) if (ext === ".zip") { const fileBuffer = await readFile(absPath) data.unzipped = unzipSync(fileBuffer) data.isZip = true data.manifestData = JSON.parse(strFromU8(data.unzipped["manifest.json"])) } else if (ext === ".json") { data.manifestData = await readJson(absPath) } else { return null } return data } export const generatePackageFromManifest = async ( commonPath: CommonPath, packageManager: PackageManagerInfo, { manifestData }: Awaited> ) => { const packageData = await generatePackage({ name: commonPath.packageName, packageManager }) packageData.version = manifestData.version packageData.displayName = manifestData.name packageData.description = manifestData.description if (manifestData?.author) { packageData.author = manifestData.author } if (manifestData.homepage_url) { packageData.homepage = manifestData.homepage_url } if (manifestData.version_name) { packageData.manifest.version_name = manifestData.version_name } if (manifestData.browser_specific_settings) { packageData.manifest.browser_specific_settings = manifestData.browser_specific_settings } if (manifestData.default_locale) { vLog("Convert locale") // Copy all locale json files to assets } if (manifestData.options_ui) { vLog("Convert options_ui") // Create option.tsx if it doesn't exist } if (manifestData.chrome_url_overrides) { vLog("Convert chrome_url_overrides") // Create newtab.tsx if it doesn't exist } if (manifestData.icons) { vLog("Convert icons") // Copy the largest icon to icon.png } if (manifestData.content_scripts) { vLog("Convert content_scripts") // TODO: Create blank content scripts for each js file, with the appropriate config } switch (manifestData.manifest_version) { case 2: await fromMv2(manifestData, packageData, commonPath) break case 3: await fromMv3(manifestData, packageData, commonPath) break default: throw new Error("Unknown manifest version") } return packageData } async function fromMv2( manifestData: ExtensionManifestV2, packageData: PackageJSON, commonPath: CommonPath ) { if (manifestData.content_security_policy) { vLog("Convert content_security_policy") packageData.manifest.content_security_policy = { extension_pages: manifestData.content_security_policy } } if (manifestData.web_accessible_resources) { vLog("Convert web_accessible_resources") packageData.manifest.web_accessible_resources = [ { matches: ["https://*/*"], resources: manifestData.web_accessible_resources } ] } if (manifestData.permissions) { vLog("Convert permissions") packageData.manifest.permissions = [] packageData.manifest.host_permissions = [] for (const permission of manifestData.permissions) { if (permission.startsWith("http") || permission === "") { packageData.manifest.host_permissions.push(permission) } else { packageData.manifest.permissions.push(permission as ManifestPermission) } } } if (manifestData.browser_action) { vLog("Convert browser_action") packageData.manifest.action = { default_title: manifestData.browser_action.default_title } // TODO: create popup.tsx // TODO: copy icons } if (manifestData.background) { vLog("Convert background") // TODO: create background.tsx } } async function fromMv3( manifestData: ExtensionManifestV3, packageData: PackageJSON, commonPath: CommonPath ) { if (manifestData.content_security_policy) { vLog("Convert content_security_policy") packageData.manifest.content_security_policy = { ...manifestData.content_security_policy } } if (manifestData.web_accessible_resources) { vLog("Convert web_accessible_resources") packageData.manifest.web_accessible_resources = [ ...manifestData.web_accessible_resources ] } if (manifestData.permissions) { vLog("Transfer permissions") packageData.manifest.permissions = [...manifestData.permissions] } if (manifestData.host_permissions) { vLog("Transfer host_permissions") packageData.manifest.host_permissions = [...manifestData.host_permissions] } if (manifestData.action) { vLog("Convert browser_action") packageData.manifest.action = { default_title: manifestData.action.default_title } // TODO: create popup.tsx // TODO: copy icons } if (manifestData.background) { vLog("Convert background") // TODO: create background.tsx } } ================================================ FILE: cli/plasmo/src/features/project-creator/get-raw-name.ts ================================================ import { createQuestId } from "mnemonic-id" import { getNonFlagArgvs } from "@plasmo/utils/argv" import { vLog } from "@plasmo/utils/logging" import { quickPrompt } from "~features/helpers/prompt" export const getRawName = async () => { const [rawNameNonInteractive] = getNonFlagArgvs("init") if (!!rawNameNonInteractive) { vLog("Using user-provided name:", rawNameNonInteractive) return rawNameNonInteractive } vLog("Prompting for the extension name") return await quickPrompt("Extension name:", createQuestId()) } ================================================ FILE: cli/plasmo/src/features/project-creator/git-init.ts ================================================ import spawnAsync, { type SpawnOptions } from "@expo/spawn-async" import { isAccessible } from "@plasmo/utils/fs" import { iLog, vLog, wLog } from "@plasmo/utils/logging" import type { CommonPath } from "~features/extension-devtools/common-path" const gitInitAddCommit = async (root: string) => { const { default: chalk } = await import("chalk") const commonOpt: SpawnOptions = { cwd: root, ignoreStdio: true } try { vLog("Checking if the root is a git repository") await spawnAsync("git", ["rev-parse", "--is-inside-work-tree"], commonOpt) vLog(`${root} is a git repository, bailing ${chalk.bold("git init")}`) return false } catch (e: any) { if (e.code === "ENOENT") { throw new Error("Unable to initialize git repo. `git` not in PATH.") } } iLog("Initializing git project...") await spawnAsync("git", ["init"], commonOpt) await spawnAsync("git", ["add", "--all"], commonOpt) await spawnAsync( "git", ["commit", "-m", "Created a new Plasmo extension"], commonOpt ) vLog("Added all files to git and created the initial commit.") return true } export async function gitInit( commonPath: CommonPath, root: string ): Promise { if (!(await isAccessible(commonPath.gitIgnorePath))) { return false } try { return await gitInitAddCommit(root) } catch (error: any) { wLog(error.message) return false } } ================================================ FILE: cli/plasmo/src/features/project-creator/index.ts ================================================ import { existsSync } from "fs" import { lstat, readFile, writeFile } from "fs/promises" import { isAbsolute, join, relative, resolve } from "path" import spawnAsync from "@expo/spawn-async" import { sentenceCase } from "change-case" import { copy, outputJson, readJson } from "fs-extra" import ignore from "ignore" import { temporaryDirectory } from "tempy" import { getFlag, hasFlag } from "@plasmo/utils/flags" import { isAccessible } from "@plasmo/utils/fs" import { iLog, vLog } from "@plasmo/utils/logging" import type { CommonPath } from "~features/extension-devtools/common-path" import { generateGitIgnore } from "~features/extension-devtools/git-ignore" import { resolveWorkspaceToLatestSemver, type PackageJSON } from "~features/extension-devtools/package-file" import { getTemplatePath } from "~features/extension-devtools/template-path" import type { PackageManagerInfo } from "~features/helpers/package-manager" import { quickPrompt } from "~features/helpers/prompt" import { generatePackageFromManifest, getManifestData } from "./from-existing-manifest" const withRegex = /(?:^--with-)(?:\w+-?)+(?:[^-]$)/ export class ProjectCreator { templatePath = getTemplatePath() constructor( public commonPath: CommonPath, public packageManager: PackageManagerInfo, public isExample = false ) {} async create() { return ( (await this.createFrom()) || (await this.createWith()) || (await this.createBlank()) ) } async createFrom() { const fromPath = getFlag("--from") if (!fromPath) { return false } const absFromPath = isAbsolute(fromPath) ? fromPath : resolve(fromPath) const fromStats = await lstat(absFromPath) if (fromStats.isFile()) { return await this.createFromManifest(absFromPath) } else if (fromStats.isDirectory()) { return await this.createFromLocalTemplate(absFromPath) } else { return false } } async createFromLocalTemplate(absFromPath: string) { const ig = ignore().add(["node_modules", ".git", ".env*"]) const gitIgnorePath = join(absFromPath, ".gitignore") const hasGitIgnore = await isAccessible(gitIgnorePath) if (hasGitIgnore) { const gitIgnoreData = await readFile(gitIgnorePath, "utf-8") ig.add(gitIgnoreData) } await copy(absFromPath, this.commonPath.projectDirectory, { filter: (src) => src === absFromPath || !ig.ignores(relative(absFromPath, src)) }) const packageData = await readJson(this.commonPath.packageFilePath) await this.outputPackageData(packageData) iLog(`Creating new project from ${absFromPath}`) return true } async createFromManifest(absFromPath: string) { const existingData = await getManifestData(absFromPath) if (existingData === null) { return false } await this.copyBlankInitFiles() const packageData = await generatePackageFromManifest( this.commonPath, this.packageManager, existingData ) await this.outputPackageData(packageData, { resolveWorkspaceRefs: true }) return true } async createWith() { const withExampleName = process.argv.find((arg) => withRegex.test(arg)) if (withExampleName === undefined) { return false } return this.createWithExample(withExampleName.substring(2)) } async createWithExample(exampleName: string) { // locate the tmp directory const tempDirectory = temporaryDirectory() vLog(`Download examples to temporary directory: ${tempDirectory}`) try { // download the examples await spawnAsync( "git", [ "clone", "--depth", "1", "https://github.com/PlasmoHQ/examples.git", "." ], { cwd: tempDirectory, ignoreStdio: true } ) } catch (error: any) { if (error.code === "ENOENT") { throw new Error("Unable to clone example repo. `git` is not in PATH.") } } const exampleDirectory = resolve(tempDirectory, exampleName) if (!existsSync(exampleDirectory)) { throw new Error( `Example ${exampleName} not found. You may file an example request at: https://docs.plasmo.com/exp` ) } vLog("Copy example to project directory") await Promise.all([ copy(exampleDirectory, this.commonPath.projectDirectory), this.copyBppWorkflow() ]) const packageData = await readJson(this.commonPath.packageFilePath) await this.outputPackageData(packageData, { resolveWorkspaceRefs: true }) iLog(`Creating new project ${exampleName.split("-").join(" ")}`) return true } async createBlank() { await this.createWithExample("with-popup") return true } private async outputPackageData( packageData: PackageJSON, { resolveWorkspaceRefs = false } = {} ) { const { packageName, packageFilePath } = this.commonPath packageData.name = packageName packageData.displayName = sentenceCase(packageName) packageData.description = await quickPrompt( "Extension description:", packageData.description ) if (this.isExample) { delete packageData.packageManager packageData.contributors = [ await quickPrompt("Contributor name:", packageData.author) ] packageData.author = "Plasmo Corp. " } else { delete packageData.contributors packageData.author = await quickPrompt("Author name:", packageData.author) if (resolveWorkspaceRefs) { vLog( "Replace workspace refs with the latest package version from npm registry" ) const resolvedDeps = await Promise.all([ resolveWorkspaceToLatestSemver(packageData.dependencies), resolveWorkspaceToLatestSemver(packageData.devDependencies) ]) packageData.dependencies = resolvedDeps[0] packageData.devDependencies = resolvedDeps[1] } } await outputJson(packageFilePath, packageData, { spaces: 2 }) } async copyBlankInitFiles() { const entry = getFlag("--entry") || "popup" const entryFiles = entry .split(new RegExp(",|\\s")) .flatMap((e) => [`${e}.ts`, `${e}.tsx`]) .map((e) => [ resolve(this.templatePath.initEntryPath, e), resolve(this.commonPath.projectDirectory, e) ]) .filter(([entryPath]) => existsSync(entryPath)) vLog("Using the following entry files: ", entryFiles) await Promise.all([ copy( this.templatePath.initTemplatePath, this.commonPath.projectDirectory ), this.copyBppWorkflow(), writeFile(this.commonPath.gitIgnorePath, generateGitIgnore()) ]) await Promise.all(entryFiles.map(([src, dest]) => copy(src, dest))) } async copyBppWorkflow() { if (this.isExample || hasFlag("--no-bpp")) { return } vLog(`Copying BPP workflow...`) const bppSubmitWorkflowYamlPath = resolve( this.commonPath.projectDirectory, ".github", "workflows", "submit.yml" ) return copy(this.templatePath.bppYaml, bppSubmitWorkflowYamlPath) } } ================================================ FILE: cli/plasmo/src/features/project-creator/install-dependencies.ts ================================================ import spawnAsync from "@expo/spawn-async" import { iLog, wLog } from "@plasmo/utils/logging" import type { PackageManagerInfo } from "~features/helpers/package-manager" export const installDependencies = async ( projectDirectory: string, packageManager: PackageManagerInfo ) => { try { iLog("Installing dependencies...") await spawnAsync(packageManager.name, ["install"], { cwd: projectDirectory, stdio: "inherit" }) } catch (error: any) { wLog(error.message) } } ================================================ FILE: cli/plasmo/src/features/project-creator/print-ready.ts ================================================ import { sLog } from "@plasmo/utils/logging" import type { CommonPath } from "~features/extension-devtools/common-path" import type { PackageManagerInfo } from "~features/helpers/package-manager" export const printReady = async ( projectDirectory: string, currentDirectory: string, commonPath: CommonPath, packageManager: PackageManagerInfo ) => { const { default: chalk } = await import("chalk") sLog( "Your extension is ready in: ", chalk.yellowBright(projectDirectory), `\n\n To start hacking, run:\n\n`, projectDirectory === currentDirectory ? "" : ` cd ${commonPath.packageName}\n`, ` ${packageManager.name} ${ packageManager.name === "npm" ? "run dev" : "dev" }\n`, "\n Visit https://docs.plasmo.com for documentation and more examples." ) } ================================================ FILE: cli/plasmo/src/index.ts ================================================ #!/usr/bin/env node import { argv, exit, versions } from "process" import semver from "semver" import { ErrorMessage } from "@plasmo/constants/error" import { verbose } from "@plasmo/utils/flags" import { eLog, vLog } from "@plasmo/utils/logging" import { runMap, validCommandSet, type ValidCommand } from "~commands" import { printHeader, printHelp } from "~features/helpers/print" async function defaultMode() { printHeader() printHelp() } async function main() { try { // In case someone pasted an essay into the cli if (argv.length > 10) { throw new Error(ErrorMessage.TooManyArg) } if (semver.major(versions.node) < 16) { throw new Error("Node version must be >= 16") } process.env.VERBOSE = verbose ? "true" : "false" // Setting startup policy/daemon const mode = argv.find((arg) => validCommandSet.has(arg as ValidCommand) ) as ValidCommand if (mode in runMap) { vLog("Running command:", mode) const { default: runner } = await runMap[mode]() await runner() } else { vLog("Running default mode") await defaultMode() } } catch (e) { eLog((e as Error)?.message || ErrorMessage.Unknown) vLog(e?.stack) exit(1) } } main() process.on("SIGINT", () => exit(0)) process.on("SIGTERM", () => exit(0)) ================================================ FILE: cli/plasmo/src/type.ts ================================================ import * as React from 'react'; import type { Root } from "react-dom/client" import type { ManifestContentScript } from "@plasmo/constants/manifest/content-script" // See https://www.plasmo.com/engineering/log/2022.04#update-2022.04.23 export type PlasmoCSConfig = Omit, "js"> /** * @deprecated use **PlasmoCSConfig** instead */ export type PlasmoContentScript = PlasmoCSConfig type Async = Promise | T type Getter = (props?: P) => Async type InsertPosition = "beforebegin" | "afterbegin" | "beforeend" | "afterend" type ElementInsertOptions = { element: Element insertPosition?: InsertPosition } type ElementInsertOptionsList = ElementInsertOptions[] type GetElement = Getter type GetElementInsertOptions = Getter type PlasmoCSUIOverlayAnchor = { element: Element root?: Root type: "overlay" } type PlasmoCSUIInlineAnchor = { element: Element type: "inline" insertPosition?: InsertPosition root?: Root } export type PlasmoCSUIAnchor = PlasmoCSUIOverlayAnchor | PlasmoCSUIInlineAnchor export type PlasmoCSUIProps = { anchor?: PlasmoCSUIAnchor } export type PlasmoCSUIMountState = { document: Document observer: MutationObserver | null mountInterval: NodeJS.Timer | null isMounting: boolean isMutated: boolean /** * Used to quickly check if element is already mounted */ hostSet: Set /** * Used to add more metadata to the host Set */ hostMap: WeakMap /** * Used to align overlay anchor with elements on the page */ overlayTargetList: Element[] } export type PlasmoGetRootContainer = ( props: { mountState?: PlasmoCSUIMountState } & PlasmoCSUIProps ) => Async export type PlasmoGetOverlayAnchor = GetElement export type PlasmoGetOverlayAnchorList = Getter export type PlasmoGetInlineAnchor = GetElement | GetElementInsertOptions export type PlasmoGetInlineAnchorList = Getter< NodeList | ElementInsertOptionsList > export type PlasmoMountShadowHost = ( props: { mountState?: PlasmoCSUIMountState shadowHost: Element } & PlasmoCSUIProps ) => Async export type PlasmoGetShadowHostId = Getter export type PlasmoGetStyle = Getter< HTMLStyleElement, PlasmoCSUIAnchor & { sfcStyleContent?: string } > export type PlasmoGetSfcStyleContent = Getter /** * @return a cleanup unwatch function that will be run when unmounted */ export type PlasmoWatchOverlayAnchor = ( updatePosition: () => Promise ) => (() => void) | void export type PlasmoCSUIContainerProps = { id?: string children?: React.ReactNode watchOverlayAnchor?: PlasmoWatchOverlayAnchor } & PlasmoCSUIProps export type PlasmoCSUIJSXContainer = ( p?: PlasmoCSUIContainerProps ) => React.JSX.Element export type PlasmoCSUIHTMLContainer = ( p?: PlasmoCSUIContainerProps ) => HTMLElement export type PlasmoCreateShadowRoot = ( shadowHost: HTMLElement ) => Async export type PlasmoRender = ( props: { createRootContainer?: (p?: PlasmoCSUIAnchor) => Async } & PlasmoCSUIProps, InlineCSUIContainer?: T, OverlayCSUIContainer?: T ) => Async export type PlasmoCSUIWatch = (props: { render: (anchor: PlasmoCSUIAnchor) => Async observer: { start: (render: (anchor?: PlasmoCSUIAnchor) => void) => void mountState: PlasmoCSUIMountState } }) => void export type PlasmoCSUI = { default: any getStyle: PlasmoGetStyle getSfcStyleContent: PlasmoGetSfcStyleContent getShadowHostId: PlasmoGetShadowHostId getOverlayAnchor: PlasmoGetOverlayAnchor getOverlayAnchorList: PlasmoGetOverlayAnchorList getInlineAnchor: PlasmoGetInlineAnchor getInlineAnchorList: PlasmoGetInlineAnchorList getRootContainer: PlasmoGetRootContainer createShadowRoot: PlasmoCreateShadowRoot watchOverlayAnchor: PlasmoWatchOverlayAnchor mountShadowHost: PlasmoMountShadowHost render: PlasmoRender watch: PlasmoCSUIWatch } ================================================ FILE: cli/plasmo/templates/plasmo.d.ts ================================================ declare namespace NodeJS { interface ProcessEnv { NODE_ENV: "development" | "production" PLASMO_BROWSER?: | "arc" | "brave" | "chrome" | "chromium" | "edge" | "firefox" | "gecko" | "island" | "opera" | "plasmo" | "safari" | "sigmaos" | "tor" | "vivaldi" | "waterfox" | "yandex" PLASMO_MANIFEST_VERSION?: "mv2" | "mv3" PLASMO_TARGET?: `${ProcessEnv["PLASMO_BROWSER"]}-${ProcessEnv["PLASMO_MANIFEST_VERSION"]}` PLASMO_TAG?: string } } declare module "*.module.css" declare module "*.module.less" declare module "*.module.scss" declare module "*.module.sass" declare module "*.module.styl" declare module "*.module.pcss" declare module "react:*.svg" { import type { FunctionComponent, SVGProps } from "react" const value: FunctionComponent> export default value } declare module "*.gql" declare module "*.graphql" declare module "react:*" declare module "https:*" declare module "url:*" { const value: string export default value } declare module "data-text:*" { const value: string export default value } declare module "data-base64:*" { const value: string export default value } declare module "data-env:*" { const value: string export default value } declare module "data-text-env:*" { const value: string export default value } declare module "raw:*" { const value: string export default value } declare module "raw-env:*" { const value: string export default value } ================================================ FILE: cli/plasmo/templates/static/background/index.ts ================================================ import "./messaging" import "~background" ================================================ FILE: cli/plasmo/templates/static/common/csui-container-react.tsx ================================================ import React from "react" import type { PlasmoCSUIContainerProps } from "~type" export const OverlayCSUIContainer = (props: PlasmoCSUIContainerProps) => { const [top, setTop] = React.useState(0) const [left, setLeft] = React.useState(0) React.useEffect(() => { // Handle overlay repositioning if (props.anchor.type !== "overlay") { return } const updatePosition = async () => { const rect = props.anchor.element?.getBoundingClientRect() if (!rect) { return } const pos = { left: rect.left + window.scrollX, top: rect.top + window.scrollY } setLeft(pos.left) setTop(pos.top) } updatePosition() const unwatch = props.watchOverlayAnchor?.(updatePosition) window.addEventListener("scroll", updatePosition) window.addEventListener("resize", updatePosition) return () => { if (typeof unwatch === "function") { unwatch() } window.removeEventListener("scroll", updatePosition) window.removeEventListener("resize", updatePosition) } }, [props.anchor.element]) return (
{props.children}
) } export const InlineCSUIContainer = (props: PlasmoCSUIContainerProps) => (
{props.children}
) ================================================ FILE: cli/plasmo/templates/static/common/csui-container-vanilla.tsx ================================================ import type { PlasmoCSUIContainerProps } from "~type" export const createOverlayCSUIContainer = (props: PlasmoCSUIContainerProps) => { const container = document.createElement("div") container.className = "plasmo-csui-container" container.id = props.id container.style.cssText = ` display: flex; position: relative; top: 0px; left: 0px; ` if (props.anchor.type === "overlay") { const updatePosition = async () => { const rect = props.anchor.element.getBoundingClientRect() if (!rect) { return } const pos = { left: rect.left + window.scrollX, top: rect.top + window.scrollY } container.style.top = `${pos.top}px` container.style.left = `${pos.left}px` } updatePosition() props.watchOverlayAnchor?.(updatePosition) window.addEventListener("scroll", updatePosition) window.addEventListener("resize", updatePosition) } return container } export const createInlineCSUIContainer = (props: PlasmoCSUIContainerProps) => { const container = document.createElement("div") container.className = "plasmo-csui-container" container.id = "plasmo-inline" container.style.cssText = ` display: flex; position: relative; top: 0px; left: 0px; ` return container } ================================================ FILE: cli/plasmo/templates/static/common/csui.ts ================================================ import type { PlasmoCSUI, PlasmoCSUIAnchor, PlasmoCSUIMountState } from "~type" async function createShadowDOM(Mount: PlasmoCSUI) { const shadowHost = document.createElement("plasmo-csui") const shadowRoot = typeof Mount.createShadowRoot === "function" ? await Mount.createShadowRoot(shadowHost) : shadowHost.attachShadow({ mode: "open" }) const shadowContainer = document.createElement("div") shadowContainer.id = "plasmo-shadow-container" shadowContainer.style.zIndex = "2147483647" shadowContainer.style.position = "relative" shadowRoot.appendChild(shadowContainer) return { shadowHost, shadowRoot, shadowContainer } } export type PlasmoCSUIShadowDOM = Awaited> async function injectAnchor( Mount: PlasmoCSUI, anchor: PlasmoCSUIAnchor, { shadowHost, shadowRoot }: PlasmoCSUIShadowDOM, mountState?: PlasmoCSUIMountState ) { if (typeof Mount.getStyle === "function") { const sfcStyleContent = typeof Mount.getSfcStyleContent === "function" ? await Mount.getSfcStyleContent() : "" shadowRoot.prepend(await Mount.getStyle({ ...anchor, sfcStyleContent })) } if (typeof Mount.getShadowHostId === "function") { shadowHost.id = await Mount.getShadowHostId(anchor) } if (typeof Mount.mountShadowHost === "function") { await Mount.mountShadowHost({ shadowHost, anchor, mountState }) } else if (anchor.type === "inline") { anchor.element.insertAdjacentElement( anchor.insertPosition || "afterend", shadowHost ) } else { document.documentElement.prepend(shadowHost) } } export async function createShadowContainer( Mount: PlasmoCSUI, anchor: PlasmoCSUIAnchor, mountState?: PlasmoCSUIMountState ) { const shadowDom = await createShadowDOM(Mount) mountState?.hostSet.add(shadowDom.shadowHost) mountState?.hostMap.set(shadowDom.shadowHost, anchor) await injectAnchor(Mount, anchor, shadowDom, mountState) return shadowDom.shadowContainer } const isVisible = (el: Element) => { if (!el) { return false } const elementRect = el.getBoundingClientRect() const elementStyle = globalThis.getComputedStyle(el) // console.log(elementRect, elementStyle) if (elementStyle.display === "none") { return false } if (elementStyle.visibility === "hidden") { return false } if (elementStyle.opacity === "0") { return false } if ( elementRect.width === 0 && elementRect.height === 0 && elementStyle.overflow !== "hidden" ) { return false } // Check if the element is irrevocably off-screen: if ( elementRect.x + elementRect.width < 0 || elementRect.y + elementRect.height < 0 ) { return false } return true } export function createAnchorObserver(Mount: PlasmoCSUI) { const mountState: PlasmoCSUIMountState = { document: document || window.document, observer: null, mountInterval: null, isMounting: false, isMutated: false, hostSet: new Set(), hostMap: new WeakMap(), overlayTargetList: [] } const isMounted = (el: Element | null) => el?.id ? !!document.getElementById(el.id) : el?.getRootNode({ composed: true }) === mountState.document const hasInlineAnchor = typeof Mount.getInlineAnchor === "function" const hasOverlayAnchor = typeof Mount.getOverlayAnchor === "function" const hasInlineAnchorList = typeof Mount.getInlineAnchorList === "function" const hasOverlayAnchorList = typeof Mount.getOverlayAnchorList === "function" const shouldObserve = hasInlineAnchor || hasOverlayAnchor || hasInlineAnchorList || hasOverlayAnchorList if (!shouldObserve) { return null } async function mountAnchors(render: (anchor?: PlasmoCSUIAnchor) => void) { mountState.isMounting = true const mountedInlineAnchorSet = new WeakSet() // There should only be 1 overlay mount let overlayHost: Element = null // Go through mounted sets and check if they are still mounted for (const el of mountState.hostSet) { const anchor = mountState.hostMap.get(el) const anchorExists = document.contains(anchor?.element) if (isMounted(el) && anchorExists) { if (anchor.type === "inline") { mountedInlineAnchorSet.add(anchor.element) } else if (anchor.type === "overlay") { overlayHost = el } } else { anchor.root?.unmount() // Clean up the plasmo-csui element el.remove() mountState.hostSet.delete(el) } } const [inlineAnchor, inlineAnchorList, overlayAnchor, overlayAnchorList] = await Promise.all([ hasInlineAnchor ? Mount.getInlineAnchor() : null, hasInlineAnchorList ? Mount.getInlineAnchorList() : null, hasOverlayAnchor ? Mount.getOverlayAnchor() : null, hasOverlayAnchorList ? Mount.getOverlayAnchorList() : null ]) const renderList: PlasmoCSUIAnchor[] = [] if (!!inlineAnchor) { if (inlineAnchor instanceof Element) { if (!mountedInlineAnchorSet.has(inlineAnchor)) { renderList.push({ element: inlineAnchor, type: "inline" }) } } else if ( inlineAnchor.element instanceof Element && !mountedInlineAnchorSet.has(inlineAnchor.element) ) { renderList.push({ element: inlineAnchor.element, type: "inline", insertPosition: inlineAnchor.insertPosition }) } } if ((inlineAnchorList?.length || 0) > 0) { inlineAnchorList.forEach((inlineAnchor) => { if ( inlineAnchor instanceof Element && !mountedInlineAnchorSet.has(inlineAnchor) ) { renderList.push({ element: inlineAnchor, type: "inline" }) } else if ( inlineAnchor.element instanceof Element && !mountedInlineAnchorSet.has(inlineAnchor.element) ) { renderList.push({ element: inlineAnchor.element, type: "inline", insertPosition: inlineAnchor.insertPosition }) } }) } const overlayTargetList = [] if (!!overlayAnchor && isVisible(overlayAnchor)) { overlayTargetList.push(overlayAnchor) } if ((overlayAnchorList?.length || 0) > 0) { overlayAnchorList.forEach((el) => { if (el instanceof Element && isVisible(el)) { overlayTargetList.push(el) } }) } if (overlayTargetList.length > 0) { mountState.overlayTargetList = overlayTargetList if (!overlayHost) { renderList.push({ element: document.documentElement, type: "overlay" }) } else { // force re-render } } else { overlayHost?.remove() mountState.hostSet.delete(overlayHost) } await Promise.all(renderList.map(render)) if (mountState.isMutated) { mountState.isMutated = false await mountAnchors(render) } mountState.isMounting = false } const start = (render: (anchor?: PlasmoCSUIAnchor) => void) => { mountState.observer = new MutationObserver(() => { if (mountState.isMounting) { mountState.isMutated = true return } mountAnchors(render) }) // Need to watch the subtree for shadowDOM mountState.observer.observe(document.documentElement, { childList: true, subtree: true }) mountState.mountInterval = setInterval(() => { if (mountState.isMounting) { mountState.isMutated = true return } mountAnchors(render) }, 142) } return { start, mountState } } export const createRender = ( Mount: PlasmoCSUI, containers: [T, T], mountState?: PlasmoCSUIMountState, renderFx?: (anchor: PlasmoCSUIAnchor, rootContainer: Element) => Promise ) => { const createRootContainer = (anchor: PlasmoCSUIAnchor) => typeof Mount.getRootContainer === "function" ? Mount.getRootContainer({ anchor, mountState }) : createShadowContainer(Mount, anchor, mountState) if (typeof Mount.render === "function") { return (anchor: PlasmoCSUIAnchor) => Mount.render( { anchor, createRootContainer }, ...containers ) } return async (anchor: PlasmoCSUIAnchor) => { const rootContainer = await createRootContainer(anchor) return renderFx(anchor, rootContainer) } } ================================================ FILE: cli/plasmo/templates/static/common/react.ts ================================================ import { Fragment, type FC, type ReactNode } from "react" export const getLayout = (RawImport: any): FC<{ children: ReactNode }> => typeof RawImport.Layout === "function" ? RawImport.Layout : typeof RawImport.getGlobalProvider === "function" ? RawImport.getGlobalProvider() : Fragment ================================================ FILE: cli/plasmo/templates/static/common/vue.ts ================================================ globalThis.__VUE_OPTIONS_API__ = true globalThis.__VUE_PROD_DEVTOOLS__ = process.env.NODE_ENV !== "production" globalThis.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ = false ================================================ FILE: cli/plasmo/templates/static/react17/content-script-ui-mount.tsx ================================================ import React from "react" import * as ReactDOM from "react-dom" import { createAnchorObserver, createRender } from "@plasmo-static-common/csui" import { InlineCSUIContainer, OverlayCSUIContainer } from "@plasmo-static-common/csui-container-react" import { getLayout } from "@plasmo-static-common/react" import type { PlasmoCSUI, PlasmoCSUIAnchor, PlasmoCSUIJSXContainer } from "~type" // @ts-ignore import * as RawMount from "__plasmo_mount_content_script__" // Escape parcel's static analyzer const Mount = RawMount as PlasmoCSUI const observer = createAnchorObserver(Mount) const render = createRender( Mount, [InlineCSUIContainer, OverlayCSUIContainer], observer?.mountState, async (anchor, rootContainer) => { const Layout = getLayout(RawMount) switch (anchor.type) { case "inline": { ReactDOM.render( , rootContainer ) break } case "overlay": { const targetList = observer?.mountState.overlayTargetList || [ anchor.element ] ReactDOM.render( {targetList.map((target, i) => { const id = `plasmo-overlay-${i}` const innerAnchor: PlasmoCSUIAnchor = { element: target, type: "overlay" } return ( ) })} , rootContainer ) break } } } ) if (!!observer) { observer.start(render) } else { render({ element: document.documentElement, type: "overlay" }) } if (typeof Mount.watch === "function") { Mount.watch({ observer, render }) } ================================================ FILE: cli/plasmo/templates/static/react17/index.html ================================================ __plasmo_static_index_title__
================================================ FILE: cli/plasmo/templates/static/react17/index.tsx ================================================ import React from "react" import * as ReactDOM from "react-dom" import { getLayout } from "@plasmo-static-common/react" // @ts-ignore import * as Component from "__plasmo_import_module__" let __plasmoRoot: HTMLElement = null document.addEventListener("DOMContentLoaded", () => { if (!!__plasmoRoot) { return } const Layout = getLayout(Component) __plasmoRoot = document.getElementById("__plasmo") ReactDOM.render( , __plasmoRoot ) }) ================================================ FILE: cli/plasmo/templates/static/react18/content-script-ui-mount.tsx ================================================ import React from "react" import { createRoot } from "react-dom/client" import { createAnchorObserver, createRender } from "@plasmo-static-common/csui" import { InlineCSUIContainer, OverlayCSUIContainer } from "@plasmo-static-common/csui-container-react" import { getLayout } from "@plasmo-static-common/react" import type { PlasmoCSUI, PlasmoCSUIAnchor, PlasmoCSUIJSXContainer } from "~type" // @ts-ignore import * as RawMount from "__plasmo_mount_content_script__" // Escape parcel's static analyzer const Mount = RawMount as PlasmoCSUI const observer = createAnchorObserver(Mount) const render = createRender( Mount, [InlineCSUIContainer, OverlayCSUIContainer], observer?.mountState, async (anchor, rootContainer) => { const root = createRoot(rootContainer) anchor.root = root const Layout = getLayout(RawMount) switch (anchor.type) { case "inline": { root.render( ) break } case "overlay": { const targetList = observer?.mountState.overlayTargetList || [ anchor.element ] root.render( {targetList.map((target, i) => { const id = `plasmo-overlay-${i}` const innerAnchor: PlasmoCSUIAnchor = { element: target, type: "overlay" } return ( ) })} ) break } } } ) if (!!observer) { observer.start(render) } else { render({ element: document.documentElement, type: "overlay" }) } if (typeof Mount.watch === "function") { Mount.watch({ observer, render }) } ================================================ FILE: cli/plasmo/templates/static/react18/index.html ================================================ __plasmo_static_index_title__
================================================ FILE: cli/plasmo/templates/static/react18/index.tsx ================================================ import React from "react" import { createRoot } from "react-dom/client" import { getLayout } from "@plasmo-static-common/react" // @ts-ignore import * as Component from "__plasmo_import_module__" let __plasmoRoot: HTMLElement = null document.addEventListener("DOMContentLoaded", () => { if (!!__plasmoRoot) { return } __plasmoRoot = document.getElementById("__plasmo") const root = createRoot(__plasmoRoot) const Layout = getLayout(Component) root.render( ) }) ================================================ FILE: cli/plasmo/templates/static/react19/content-script-ui-mount.tsx ================================================ import React from "react" import { createRoot } from "react-dom/client" import { createAnchorObserver, createRender } from "@plasmo-static-common/csui" import { InlineCSUIContainer, OverlayCSUIContainer } from "@plasmo-static-common/csui-container-react" import { getLayout } from "@plasmo-static-common/react" import type { PlasmoCSUI, PlasmoCSUIAnchor, PlasmoCSUIJSXContainer } from "~type" // @ts-ignore import * as RawMount from "__plasmo_mount_content_script__" // Escape parcel's static analyzer const Mount = RawMount as PlasmoCSUI const observer = createAnchorObserver(Mount) const render = createRender( Mount, [InlineCSUIContainer, OverlayCSUIContainer], observer?.mountState, async (anchor, rootContainer) => { const root = createRoot(rootContainer) anchor.root = root const Layout = getLayout(RawMount) switch (anchor.type) { case "inline": { root.render( ) break } case "overlay": { const targetList = observer?.mountState.overlayTargetList || [ anchor.element ] root.render( {targetList.map((target, i) => { const id = `plasmo-overlay-${i}` const innerAnchor: PlasmoCSUIAnchor = { element: target, type: "overlay" } return ( ) })} ) break } } } ) if (!!observer) { observer.start(render) } else { render({ element: document.documentElement, type: "overlay" }) } if (typeof Mount.watch === "function") { Mount.watch({ observer, render }) } ================================================ FILE: cli/plasmo/templates/static/react19/index.html ================================================ __plasmo_static_index_title__
================================================ FILE: cli/plasmo/templates/static/react19/index.tsx ================================================ import React from "react" import { createRoot } from "react-dom/client" import { getLayout } from "@plasmo-static-common/react" // @ts-ignore import * as Component from "__plasmo_import_module__" let __plasmoRoot: HTMLElement = null document.addEventListener("DOMContentLoaded", () => { if (!!__plasmoRoot) { return } __plasmoRoot = document.getElementById("__plasmo") const root = createRoot(__plasmoRoot) const Layout = getLayout(Component) root.render( ) }) ================================================ FILE: cli/plasmo/templates/static/svelte4/content-script-ui-mount.ts ================================================ import { createAnchorObserver, createRender } from "@plasmo-static-common/csui" import { createInlineCSUIContainer, createOverlayCSUIContainer } from "@plasmo-static-common/csui-container-vanilla" import type { PlasmoCSUI, PlasmoCSUIAnchor, PlasmoCSUIHTMLContainer } from "~type" // @ts-ignore import * as RawMount from "__plasmo_mount_content_script__" // Escape parcel's static analyzer const Mount = RawMount as PlasmoCSUI const observer = createAnchorObserver(Mount) const render = createRender( Mount, [createInlineCSUIContainer, createOverlayCSUIContainer], observer?.mountState, async (anchor, rootContainer) => { switch (anchor.type) { case "inline": { const mountPoint = createInlineCSUIContainer({ anchor }) rootContainer.appendChild(mountPoint) new Mount.default({ target: mountPoint, props: { anchor } }) break } case "overlay": { const targetList = observer?.mountState.overlayTargetList || [ anchor.element ] targetList.forEach((target, i) => { const id = `plasmo-overlay-${i}` const innerAnchor: PlasmoCSUIAnchor = { element: target, type: "overlay" } const mountPoint = createOverlayCSUIContainer({ id, anchor: innerAnchor, watchOverlayAnchor: Mount.watchOverlayAnchor }) rootContainer.appendChild(mountPoint) new Mount.default({ target: mountPoint, props: { anchor: innerAnchor } }) }) break } } } ) if (!!observer) { observer.start(render) } else { render({ element: document.documentElement, type: "overlay" }) } if (typeof Mount.watch === "function") { Mount.watch({ observer, render }) } ================================================ FILE: cli/plasmo/templates/static/svelte4/index.html ================================================ __plasmo_static_index_title__
================================================ FILE: cli/plasmo/templates/static/svelte4/index.ts ================================================ // @ts-nocheck import * as Component from "__plasmo_import_module__" let __plasmoRoot: HTMLElement = null document.addEventListener("DOMContentLoaded", () => { if (!!__plasmoRoot) { return } __plasmoRoot = document.getElementById("__plasmo") new Component.default({ target: __plasmoRoot }) }) ================================================ FILE: cli/plasmo/templates/static/vanilla/index.html ================================================ __plasmo_static_index_title__
================================================ FILE: cli/plasmo/templates/static/vanilla/index.ts ================================================ // @ts-nocheck import "__plasmo_import_module__" ================================================ FILE: cli/plasmo/templates/static/vue3/content-script-ui-mount.ts ================================================ import { createApp } from "vue" import { createAnchorObserver, createRender } from "@plasmo-static-common/csui" import { createInlineCSUIContainer, createOverlayCSUIContainer } from "@plasmo-static-common/csui-container-vanilla" import type { PlasmoCSUI, PlasmoCSUIAnchor, PlasmoCSUIHTMLContainer } from "~type" import "@plasmo-static-common/vue" // @ts-ignore import RawMount from "__plasmo_mount_content_script__" // @ts-ignore import SfcStyleContent from "style-raw:__plasmo_mount_content_script__" // Escape parcel's static analyzer const Mount = (RawMount.plasmo || {}) as PlasmoCSUI if (typeof SfcStyleContent === "string") { Mount.getSfcStyleContent = () => SfcStyleContent if (typeof Mount.getStyle !== "function") { Mount.getStyle = ({ sfcStyleContent }) => { const element = document.createElement("style") element.textContent = sfcStyleContent return element } } } const observer = createAnchorObserver(Mount) const render = createRender( Mount, [createInlineCSUIContainer, createOverlayCSUIContainer], observer?.mountState, async (anchor, rootContainer) => { switch (anchor.type) { case "inline": { const mountPoint = createInlineCSUIContainer({ anchor }) rootContainer.appendChild(mountPoint) const app = createApp(RawMount) app.config.globalProperties.$anchor = anchor app.mount(mountPoint) break } case "overlay": { const targetList = observer?.mountState.overlayTargetList || [ anchor.element ] targetList.forEach((target, i) => { const id = `plasmo-overlay-${i}` const innerAnchor: PlasmoCSUIAnchor = { element: target, type: "overlay" } const mountPoint = createOverlayCSUIContainer({ id, anchor: innerAnchor, watchOverlayAnchor: Mount.watchOverlayAnchor }) rootContainer.appendChild(mountPoint) const app = createApp(RawMount) app.config.globalProperties.$anchor = innerAnchor app.mount(mountPoint) }) break } } } ) if (!!observer) { observer.start(render) } else { render({ element: document.documentElement, type: "overlay" }) } if (typeof Mount.watch === "function") { Mount.watch({ observer, render }) } ================================================ FILE: cli/plasmo/templates/static/vue3/index.html ================================================ __plasmo_static_index_title__
================================================ FILE: cli/plasmo/templates/static/vue3/index.ts ================================================ import { createApp } from "vue" // @ts-ignore import * as Component from "__plasmo_import_module__" import "@plasmo-static-common/vue" document.addEventListener("DOMContentLoaded", () => { const app = createApp(Component.default) Component.default.prepare?.(app) app.mount("#__plasmo") }) ================================================ FILE: cli/plasmo/templates/tsconfig.base.json ================================================ { "$schema": "https://json.schemastore.org/tsconfig", "display": "Plasmo Extension", "files": ["./plasmo.d.ts"], "compilerOptions": { "strict": false, "skipLibCheck": true, "target": "esnext", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "incremental": true, "esModuleInterop": true, "module": "esnext", "resolveJsonModule": true, "declaration": false, "declarationMap": false, "inlineSources": false, "moduleResolution": "node", "noUnusedLocals": false, "noUnusedParameters": false, "preserveWatchOutput": true, "verbatimModuleSyntax": true, "jsx": "preserve" } } ================================================ FILE: cli/plasmo/tsconfig.json ================================================ { "extends": "@plasmo/config/ts/framework.json", "include": [ "src/**/*.ts", "templates/plasmo.d.ts", "templates/static/**/*.ts", "templates/static/**/*.tsx" ], "exclude": ["dist", "node_modules"], "compilerOptions": { "outDir": "dist", "baseUrl": ".", "lib": ["es2022", "dom"], "jsx": "preserve", "paths": { "~*": ["./src/*"], "@plasmo-static-common/*": ["./templates/static/common/*"] } } } ================================================ FILE: core/parcel-bundler/.gitignore ================================================ node_modules dist/ ================================================ FILE: core/parcel-bundler/package.json ================================================ { "name": "@plasmohq/parcel-bundler", "version": "0.5.6", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/PlasmoHQ/plasmo.git" }, "main": "dist/index.js", "source": "src/index.ts", "scripts": { "prepublishOnly": "pnpm build", "build": "tsup src/index.ts --minify --clean", "dev": "tsup src/index.ts --sourcemap --watch" }, "engines": { "node": ">= 16.0.0", "parcel": ">= 2.7.0" }, "dependencies": { "@parcel/core": "2.9.3", "@parcel/diagnostic": "2.9.3", "@parcel/graph": "2.9.3", "@parcel/hash": "2.9.3", "@parcel/plugin": "2.9.3", "@parcel/utils": "2.9.3", "nullthrows": "1.1.1" }, "devDependencies": { "@parcel/types": "2.9.3", "@plasmo/config": "workspace:*", "@plasmo/utils": "workspace:*", "tsup": "8.4.0" } } ================================================ FILE: core/parcel-bundler/src/bit-set.ts ================================================ import nullthrows from "nullthrows" const BIGINT_ZERO = 0n const BIGINT_ONE = 1n let numberToBigInt = (v: number): bigint => globalThis.BigInt(v) let bitUnion = (a: bigint, b: bigint): bigint => a | b export class BitSet { _value: bigint _lookup: Map _items: Array constructor({ initial, items, lookup }: { items: Array lookup: Map initial?: BitSet | bigint }) { if (initial instanceof BitSet) { this._value = initial._value } else if (initial) { this._value = initial } else { this._value = BIGINT_ZERO } this._items = items this._lookup = lookup } static from(items: Array): BitSet { let lookup: Map = new Map() for (let i = 0; i < items.length; i++) { lookup.set(items[i], numberToBigInt(i)) } return new BitSet({ items, lookup }) } static union(a: BitSet, b: BitSet): BitSet { return new BitSet({ initial: bitUnion(a._value, b._value), lookup: a._lookup, items: a._items }) } private getIndex(item: T) { return nullthrows(this._lookup.get(item), "Item is missing from BitSet") } add(item: T) { this._value |= BIGINT_ONE << this.getIndex(item) } delete(item: T) { this._value &= ~(BIGINT_ONE << this.getIndex(item)) } has(item: T): boolean { return Boolean(this._value & (BIGINT_ONE << this.getIndex(item))) } intersect(v: BitSet) { this._value = this._value & v._value } union(v: BitSet) { this._value = bitUnion(this._value, v._value) } clear() { this._value = BIGINT_ZERO } cloneEmpty(): BitSet { return new BitSet({ lookup: this._lookup, items: this._items }) } clone(): BitSet { return new BitSet({ lookup: this._lookup, items: this._items, initial: this._value }) } values(): Array { let values = [] let tmpValue = this._value let i: number while (tmpValue > BIGINT_ZERO) { i = tmpValue.toString(2).length - 1 values.push(this._items[i]) tmpValue &= ~(BIGINT_ONE << numberToBigInt(i)) } return values } } ================================================ FILE: core/parcel-bundler/src/can-merge.ts ================================================ export function canMerge(a, b) { // Bundles can be merged if they have the same type and environment, // unless they are explicitly marked as isolated or inline. return ( a.type === b.type && a.env.context === b.env.context && a.bundleBehavior == null && b.bundleBehavior == null ) } ================================================ FILE: core/parcel-bundler/src/create-bundle.ts ================================================ import type { Asset, BundleBehavior, Environment, Target } from "@parcel/types" import nullthrows from "nullthrows" import type { Bundle } from "./types" export function createBundle(opts: { uniqueKey?: string target: Target asset?: Asset env?: Environment type?: string needsStableName?: boolean bundleBehavior?: BundleBehavior | null | undefined }): Bundle { if (opts.asset == null) { return { uniqueKey: opts.uniqueKey, assets: new Set(), internalizedAssetIds: [], mainEntryAsset: null, size: 0, sourceBundles: new Set(), target: opts.target, type: nullthrows(opts.type), env: nullthrows(opts.env), needsStableName: Boolean(opts.needsStableName), bundleBehavior: opts.bundleBehavior } } let asset = nullthrows(opts.asset) return { uniqueKey: opts.uniqueKey, assets: new Set([asset]), internalizedAssetIds: [], mainEntryAsset: asset, size: asset.stats.size, sourceBundles: new Set(), target: opts.target, type: opts.type ?? asset.type, env: opts.env ?? asset.env, needsStableName: Boolean(opts.needsStableName), bundleBehavior: opts.bundleBehavior ?? asset.bundleBehavior } } ================================================ FILE: core/parcel-bundler/src/create-ideal-graph.ts ================================================ import invariant from "assert" import { ALL_EDGE_TYPES, ContentGraph, Graph, NodeId } from "@parcel/graph" import type { Asset, Dependency, MutableBundleGraph } from "@parcel/types" import { DefaultMap, setEqual, setIntersect, setUnion } from "@parcel/utils" import nullthrows from "nullthrows" import { canMerge } from "./can-merge" import { createBundle } from "./create-bundle" import { getReachableBundleRoots } from "./get-reachable-bundle-root" import { removeBundle } from "./remove-bundle" import { dependencyPriorityEdges, type Bundle, type BundleRoot, type DependencyBundleGraph, type IdealGraph, type ResolvedBundlerConfig } from "./types" export function createIdealGraph( assetGraph: MutableBundleGraph, config: ResolvedBundlerConfig, entries: Map ): IdealGraph { // Asset to the bundle and group it's an entry of let bundleRoots: Map = new Map() let bundles: Map = new Map() let dependencyBundleGraph: DependencyBundleGraph = new ContentGraph() let assetReference: DefaultMap< Asset, Array<[Dependency, Bundle]> > = new DefaultMap(() => []) // A Graph of Bundles and a root node (dummy string), which models only Bundles, and connections to their // referencing Bundle. There are no actual BundleGroup nodes, just bundles that take on that role. let bundleGraph: Graph = new Graph() let stack: Array<[BundleRoot, NodeId]> = [] let bundleRootEdgeTypes = { parallel: 1, lazy: 2 } // ContentGraph that models bundleRoots, with parallel & async deps only to inform reachability let bundleRootGraph: ContentGraph< BundleRoot | "root", typeof bundleRootEdgeTypes > = new ContentGraph() let bundleGroupBundleIds: Set = new Set() // Models bundleRoots and the assets that require it synchronously let reachableRoots: ContentGraph = new ContentGraph() let rootNodeId = nullthrows(bundleRootGraph.addNode("root")) let bundleGraphRootNodeId = nullthrows(bundleGraph.addNode("root")) bundleRootGraph.setRootNodeId(rootNodeId) bundleGraph.setRootNodeId(bundleGraphRootNodeId) // Step Create Entry Bundles for (let [asset, dependency] of entries) { let bundle = createBundle({ asset, target: nullthrows(dependency.target), needsStableName: dependency.isEntry }) let nodeId = bundleGraph.addNode(bundle) bundles.set(asset.id, nodeId) bundleRoots.set(asset, [nodeId, nodeId]) bundleRootGraph.addEdge( rootNodeId, bundleRootGraph.addNodeByContentKey(asset.id, asset) ) bundleGraph.addEdge(bundleGraphRootNodeId, nodeId) dependencyBundleGraph.addEdge( dependencyBundleGraph.addNodeByContentKeyIfNeeded(dependency.id, { value: dependency, type: "dependency" }), dependencyBundleGraph.addNodeByContentKeyIfNeeded(String(nodeId), { value: bundle, type: "bundle" }), dependencyPriorityEdges[dependency.priority] ) bundleGroupBundleIds.add(nodeId) } let assets = [] let typeChangeIds = new Set() /** * Step Create Bundles: Traverse the assetGraph (aka MutableBundleGraph) and create bundles * for asset type changes, parallel, inline, and async or lazy dependencies, * adding only that asset to each bundle, not its entire subgraph. */ assetGraph.traverse( { enter(node, context, actions) { if (node.type === "asset") { if ( context?.type === "dependency" && context?.value.isEntry && !entries.has(node.value) ) { // Skip whole subtrees of other targets by skipping those entries actions.skipChildren() return node } assets.push(node.value) let bundleIdTuple = bundleRoots.get(node.value) if (bundleIdTuple && bundleIdTuple[0] === bundleIdTuple[1]) { // Push to the stack (only) when a new bundle is created stack.push([node.value, bundleIdTuple[0]]) } else if (bundleIdTuple) { // Otherwise, push on the last bundle that marks the start of a BundleGroup stack.push([node.value, stack[stack.length - 1][1]]) } } else if (node.type === "dependency") { if (context == null) { return node } let dependency = node.value if (assetGraph.isDependencySkipped(dependency)) { actions.skipChildren() return node } invariant(context?.type === "asset") let parentAsset = context.value let assets = assetGraph.getDependencyAssets(dependency) if (assets.length === 0) { return node } for (let childAsset of assets) { if ( dependency.priority === "lazy" || childAsset.bundleBehavior === "isolated" // An isolated Dependency, or Bundle must contain all assets it needs to load. ) { let bundleId = bundles.get(childAsset.id) let bundle if (bundleId == null) { let firstBundleGroup = nullthrows( bundleGraph.getNode(stack[0][1]) ) invariant(firstBundleGroup !== "root") bundle = createBundle({ asset: childAsset, target: firstBundleGroup.target, needsStableName: dependency.bundleBehavior === "inline" || childAsset.bundleBehavior === "inline" ? false : dependency.isEntry || dependency.needsStableName, bundleBehavior: dependency.bundleBehavior ?? childAsset.bundleBehavior }) bundleId = bundleGraph.addNode(bundle) bundles.set(childAsset.id, bundleId) bundleRoots.set(childAsset, [bundleId, bundleId]) bundleGroupBundleIds.add(bundleId) bundleGraph.addEdge(bundleGraphRootNodeId, bundleId) } else { bundle = nullthrows(bundleGraph.getNode(bundleId)) invariant(bundle !== "root") if ( // If this dependency requests isolated, but the bundle is not, // make the bundle isolated for all uses. dependency.bundleBehavior === "isolated" && bundle.bundleBehavior == null ) { bundle.bundleBehavior = dependency.bundleBehavior } } dependencyBundleGraph.addEdge( dependencyBundleGraph.addNodeByContentKeyIfNeeded( dependency.id, { value: dependency, type: "dependency" } ), dependencyBundleGraph.addNodeByContentKeyIfNeeded( String(bundleId), { value: bundle, type: "bundle" } ), dependencyPriorityEdges[dependency.priority] ) continue } if ( parentAsset.type !== childAsset.type || dependency.priority === "parallel" || childAsset.bundleBehavior === "inline" ) { // The referencing bundleRoot is the root of a Bundle that first brings in another bundle (essentially the FIRST parent of a bundle, this may or may not be a bundleGroup) let [referencingBundleRoot, bundleGroupNodeId] = nullthrows( stack[stack.length - 1] ) let bundleGroup = nullthrows( bundleGraph.getNode(bundleGroupNodeId) ) invariant(bundleGroup !== "root") let bundleId let referencingBundleId = nullthrows( bundleRoots.get(referencingBundleRoot) )[0] let referencingBundle = nullthrows( bundleGraph.getNode(referencingBundleId) ) invariant(referencingBundle !== "root") let bundle bundleId = bundles.get(childAsset.id) /** * If this is an entry bundlegroup, we only allow one bundle per type in those groups * So attempt to add the asset to the entry bundle if it's of the same type. * This asset will be created by other dependency if it's in another bundlegroup * and bundles of other types should be merged in the next step */ let bundleGroupRootAsset = nullthrows(bundleGroup.mainEntryAsset) if ( entries.has(bundleGroupRootAsset) && canMerge(bundleGroupRootAsset, childAsset) && dependency.bundleBehavior == null ) { bundleId = bundleGroupNodeId } if (bundleId == null) { bundle = createBundle({ // Bundles created from type changes shouldn't have an entry asset. asset: childAsset, type: childAsset.type, env: childAsset.env, bundleBehavior: dependency.bundleBehavior ?? childAsset.bundleBehavior, target: referencingBundle.target, needsStableName: childAsset.bundleBehavior === "inline" || dependency.bundleBehavior === "inline" || (dependency.priority === "parallel" && !dependency.needsStableName) ? false : referencingBundle.needsStableName }) bundleId = bundleGraph.addNode(bundle) // Store Type-Change bundles for later since we need to know ALL bundlegroups they are part of to reduce/combine them if (parentAsset.type !== childAsset.type) { typeChangeIds.add(bundleId) } } else { bundle = bundleGraph.getNode(bundleId) invariant(bundle != null && bundle !== "root") if ( // If this dependency requests isolated, but the bundle is not, // make the bundle isolated for all uses. dependency.bundleBehavior === "isolated" && bundle.bundleBehavior == null ) { bundle.bundleBehavior = dependency.bundleBehavior } } bundles.set(childAsset.id, bundleId) // A bundle can belong to multiple bundlegroups, all the bundle groups of it's // ancestors, and all async and entry bundles before it are "bundle groups" // TODO: We may need to track bundles to all bundleGroups it belongs to in the future. bundleRoots.set(childAsset, [bundleId, bundleGroupNodeId]) bundleGraph.addEdge(referencingBundleId, bundleId) if (bundleId != bundleGroupNodeId) { dependencyBundleGraph.addEdge( dependencyBundleGraph.addNodeByContentKeyIfNeeded( dependency.id, { value: dependency, type: "dependency" } ), dependencyBundleGraph.addNodeByContentKeyIfNeeded( String(bundleId), { value: bundle, type: "bundle" } ), dependencyPriorityEdges.parallel ) } assetReference.get(childAsset).push([dependency, bundle]) continue } } } return node }, exit(node) { if (stack[stack.length - 1]?.[0] === node.value) { stack.pop() } } }, undefined ) // Step Merge Type Change Bundles: Clean up type change bundles within the exact same bundlegroups for (let [nodeIdA, a] of bundleGraph.nodes) { //if bundle b bundlegroups ==== bundle a bundlegroups then combine type changes if (!typeChangeIds.has(nodeIdA) || a === "root") continue let bundleABundleGroups = getBundleGroupsForBundle(nodeIdA) for (let [nodeIdB, b] of bundleGraph.nodes) { if ( a !== "root" && b !== "root" && a !== b && typeChangeIds.has(nodeIdB) && canMerge(a, b) ) { let bundleBbundleGroups = getBundleGroupsForBundle(nodeIdB) if (setEqual(bundleBbundleGroups, bundleABundleGroups)) { let shouldMerge = true for (let depId of dependencyBundleGraph.getNodeIdsConnectedTo( dependencyBundleGraph.getNodeIdByContentKey(String(nodeIdB)), ALL_EDGE_TYPES )) { let depNode = dependencyBundleGraph.getNode(depId) // Cannot merge Dependency URL specifier type if ( depNode && depNode.type === "dependency" && depNode.value.specifierType === "url" ) { shouldMerge = false continue } } if (!shouldMerge) continue mergeBundle(nodeIdA, nodeIdB) } } } } /** * Step Determine Reachability: Determine reachability for every asset from each bundleRoot. * This is later used to determine which bundles to place each asset in. We build up two * structures, one traversal each. ReachableRoots to store sync relationships, * and bundleRootGraph to store the minimal availability through `parallel` and `async` relationships. * The two graphs, are used to build up ancestorAssets, a structure which holds all availability by * all means for each asset. */ for (let [root] of bundleRoots) { if (!entries.has(root)) { bundleRootGraph.addNodeByContentKey(root.id, root) // Add in all bundleRoots to BundleRootGraph } } // ReachableRoots is a Graph of Asset Nodes which represents a BundleRoot, to all assets (non-bundleroot assets // available to it synchronously (directly) built by traversing the assetgraph once. for (let [root] of bundleRoots) { // Add sync relationships to ReachableRoots let rootNodeId = reachableRoots.addNodeByContentKeyIfNeeded(root.id, root) assetGraph.traverse((node, _, actions) => { if (node.value === root) { return } if (node.type === "dependency") { let dependency = node.value if (dependencyBundleGraph.hasContentKey(dependency.id)) { if (dependency.priority !== "sync") { let assets = assetGraph.getDependencyAssets(dependency) if (assets.length === 0) { return } invariant(assets.length === 1) let bundleRoot = assets[0] let bundle = nullthrows( bundleGraph.getNode(nullthrows(bundles.get(bundleRoot.id))) ) if ( bundle !== "root" && bundle.bundleBehavior == null && !bundle.env.isIsolated() && bundle.env.context === root.env.context ) { bundleRootGraph.addEdge( bundleRootGraph.getNodeIdByContentKey(root.id), bundleRootGraph.getNodeIdByContentKey(bundleRoot.id), dependency.priority === "parallel" ? bundleRootEdgeTypes.parallel : bundleRootEdgeTypes.lazy ) } } } if (dependency.priority !== "sync") { actions.skipChildren() } return } //asset node type let asset = node.value if (asset.bundleBehavior != null || root.type !== asset.type) { actions.skipChildren() return } let nodeId = reachableRoots.addNodeByContentKeyIfNeeded( node.value.id, node.value ) reachableRoots.addEdge(rootNodeId, nodeId) }, root) } // Maps a given bundleRoot to the assets reachable from it, // and the bundleRoots reachable from each of these assets let ancestorAssets: Map> = new Map() for (let entry of entries.keys()) { // Initialize an empty set of ancestors available to entries ancestorAssets.set(entry, new Set()) } // Step Determine Availability // Visit nodes in a topological order, visiting parent nodes before child nodes. // This allows us to construct an understanding of which assets will already be // loaded and available when a bundle runs, by pushing available assets downwards and // computing the intersection of assets available through all possible paths to a bundle. // We call this structure ancestorAssets, a Map that tracks a bundleRoot, // to all assets available to it (meaning they will exist guaranteed when the bundleRoot is loaded) // The topological sort ensures all parents are visited before the node we want to process. for (let nodeId of bundleRootGraph.topoSort(ALL_EDGE_TYPES)) { const bundleRoot = bundleRootGraph.getNode(nodeId) if (bundleRoot === "root") continue invariant(bundleRoot != null) let bundleGroupId = nullthrows(bundleRoots.get(bundleRoot))[1] // At a BundleRoot, we access it's available assets (via ancestorAssets), // and add to that all assets within the bundles in that BundleGroup. // This set is available to all bundles in a particular bundleGroup because // bundleGroups are just bundles loaded at the same time. However it is // not true that a bundle's available assets = all assets of all the bundleGroups // it belongs to. It's the intersection of those sets. let available if (bundleRoot.bundleBehavior === "isolated") { available = new Set() } else { available = new Set(ancestorAssets.get(bundleRoot)) for (let bundleIdInGroup of [ bundleGroupId, ...bundleGraph.getNodeIdsConnectedFrom(bundleGroupId) ]) { let bundleInGroup = nullthrows(bundleGraph.getNode(bundleIdInGroup)) invariant(bundleInGroup !== "root") if (bundleInGroup.bundleBehavior != null) { continue } for (let bundleRoot of bundleInGroup.assets) { // Assets directly connected to current bundleRoot let assetsFromBundleRoot = reachableRoots .getNodeIdsConnectedFrom( reachableRoots.getNodeIdByContentKey(bundleRoot.id) ) .map((id) => nullthrows(reachableRoots.getNode(id))) for (let asset of [bundleRoot, ...assetsFromBundleRoot]) { available.add(asset) } } } } // Now that we have bundleGroup availability, we will propagate that down to all the children // of this bundleGroup. For a child, we also must maintain parallel availability. If it has // parallel siblings that come before it, those, too, are available to it. Add those parallel // available assets to the set of available assets for this child as well. let children = bundleRootGraph.getNodeIdsConnectedFrom( nodeId, ALL_EDGE_TYPES ) let parallelAvailability: Set = new Set() for (let childId of children) { let child = bundleRootGraph.getNode(childId) invariant(child !== "root" && child != null) let bundleBehavior = getBundleFromBundleRoot(child).bundleBehavior if (bundleBehavior != null) { continue } let isParallel = bundleRootGraph.hasEdge( nodeId, childId, bundleRootEdgeTypes.parallel ) // Most of the time, a child will have many parent bundleGroups, // so the next time we peek at a child from another parent, we will // intersect the availability built there with the previously computed // availability. this ensures no matter which bundleGroup loads a particular bundle, // it will only assume availability of assets it has under any circumstance const childAvailableAssets = ancestorAssets.get(child) let currentChildAvailable = isParallel ? setUnion(parallelAvailability, available) : available if (childAvailableAssets != null) { setIntersect(childAvailableAssets, currentChildAvailable) } else { ancestorAssets.set(child, new Set(currentChildAvailable)) } if (isParallel) { let assetsFromBundleRoot = reachableRoots .getNodeIdsConnectedFrom( reachableRoots.getNodeIdByContentKey(child.id) ) .map((id) => nullthrows(reachableRoots.getNode(id))) parallelAvailability = setUnion( parallelAvailability, assetsFromBundleRoot ) parallelAvailability.add(child) //The next sibling should have older sibling available via parallel } } } // Step Internalize async bundles - internalize Async bundles if and only if, // the bundle is synchronously available elsewhere. // We can query sync assets available via reachableRoots. If the parent has // the bundleRoot by reachableRoots AND ancestorAssets, internalize it. for (let [id, bundleRoot] of bundleRootGraph.nodes) { if (bundleRoot === "root") continue let parentRoots = bundleRootGraph .getNodeIdsConnectedTo(id, ALL_EDGE_TYPES) .map((id) => nullthrows(bundleRootGraph.getNode(id))) let canDelete = getBundleFromBundleRoot(bundleRoot).bundleBehavior !== "isolated" if (parentRoots.length === 0) continue for (let parent of parentRoots) { if (parent === "root") { canDelete = false continue } if ( reachableRoots.hasEdge( reachableRoots.getNodeIdByContentKey(parent.id), reachableRoots.getNodeIdByContentKey(bundleRoot.id) ) || ancestorAssets.get(parent)?.has(bundleRoot) ) { let parentBundle = bundleGraph.getNode( nullthrows(bundles.get(parent.id)) ) invariant(parentBundle != null && parentBundle !== "root") parentBundle.internalizedAssetIds.push(bundleRoot.id) } else { canDelete = false } } if (canDelete) { deleteBundle(bundleRoot) } } // Step Insert Or Share: Place all assets into bundles or create shared bundles. Each asset // is placed into a single bundle based on the bundle entries it is reachable from. // This creates a maximally code split bundle graph with no duplication. for (let asset of assets) { // Unreliable bundleRoot assets which need to pulled in by shared bundles or other means let reachable: Array = getReachableBundleRoots( asset, reachableRoots ).reverse() let reachableEntries = [] let reachableNonEntries = [] // Filter out entries, since they can't have shared bundles. // Neither can non-splittable, isolated, or needing of stable name bundles. // Reserve those filtered out bundles since we add the asset back into them. for (let a of reachable) { if ( entries.has(a) || !a.isBundleSplittable || getBundleFromBundleRoot(a).needsStableName || getBundleFromBundleRoot(a).bundleBehavior === "isolated" ) { reachableEntries.push(a) } else { reachableNonEntries.push(a) } } reachable = reachableNonEntries // Filter out bundles from this asset's reachable array if // bundle does not contain the asset in its ancestry reachable = reachable.filter((b) => !ancestorAssets.get(b)?.has(asset)) // Finally, filter out bundleRoots (bundles) from this assets // reachable if they are subgraphs, and reuse that subgraph bundle // by drawing an edge. Essentially, if two bundles within an asset's // reachable array, have an ancestor-subgraph relationship, draw that edge. // This allows for us to reuse a bundle instead of making a shared bundle if // a bundle represents the exact set of assets a set of bundles would share // if a bundle b is a subgraph of another bundle f, reuse it, drawing an edge between the two let canReuse: Set = new Set() // console.log({ // bundles // }) for (let candidateSourceBundleRoot of reachable) { let candidateSourceBundleId = nullthrows( bundles.get(candidateSourceBundleRoot.id) ) // console.log({ // candidateSourceBundleRoot, // id: candidateSourceBundleRoot.id // }) if (candidateSourceBundleRoot.env.isIsolated()) { continue } let reuseableBundleId = bundles.get(asset.id) // console.log({ // reuseableBundleId // }) if (reuseableBundleId != null) { canReuse.add(candidateSourceBundleRoot) bundleGraph.addEdge(candidateSourceBundleId, reuseableBundleId) let reusableBundle = bundleGraph.getNode(reuseableBundleId) invariant(reusableBundle !== "root" && reusableBundle != null) reusableBundle.sourceBundles.add(candidateSourceBundleId) } else { // Asset is not a bundleRoot, but if its ancestor bundle (in the asset's reachable) can be // reused as a subgraph of another bundleRoot in its reachable, reuse it for (let otherReuseCandidate of reachable) { if (candidateSourceBundleRoot === otherReuseCandidate) continue let reusableCandidateReachable = getReachableBundleRoots( otherReuseCandidate, reachableRoots ).filter((b) => { return !ancestorAssets.get(b)?.has(otherReuseCandidate) }) if (reusableCandidateReachable.includes(candidateSourceBundleRoot)) { let reusableBundleId = nullthrows( bundles.get(otherReuseCandidate.id) ) canReuse.add(candidateSourceBundleRoot) bundleGraph.addEdge( nullthrows(bundles.get(candidateSourceBundleRoot.id)), reusableBundleId ) let reusableBundle = bundleGraph.getNode(reusableBundleId) invariant(reusableBundle !== "root" && reusableBundle != null) reusableBundle.sourceBundles.add(candidateSourceBundleId) } } } } //Bundles that are reused should not be considered for shared bundles, so filter them out reachable = reachable.filter((b) => !canReuse.has(b)) // Add assets to non-splittable bundles. for (let entry of reachableEntries) { let entryBundleId = nullthrows(bundles.get(entry.id)) let entryBundle = nullthrows(bundleGraph.getNode(entryBundleId)) invariant(entryBundle !== "root") entryBundle.assets.add(asset) entryBundle.size += asset.stats.size } // Create shared bundles for splittable bundles. if (reachable.length > config.minBundles) { let sourceBundles = reachable.map((a) => nullthrows(bundles.get(a.id))) let key = reachable.map((a) => a.id).join(",") let bundleId = bundles.get(key) let bundle if (bundleId == null) { let firstSourceBundle = nullthrows( bundleGraph.getNode(sourceBundles[0]) ) invariant(firstSourceBundle !== "root") bundle = createBundle({ target: firstSourceBundle.target, type: firstSourceBundle.type, env: firstSourceBundle.env }) bundle.sourceBundles = new Set(sourceBundles) let sharedInternalizedAssets = new Set( firstSourceBundle.internalizedAssetIds ) for (let p of sourceBundles) { let parentBundle = nullthrows(bundleGraph.getNode(p)) invariant(parentBundle !== "root") if (parentBundle === firstSourceBundle) continue setIntersect( sharedInternalizedAssets, new Set(parentBundle.internalizedAssetIds) ) } bundle.internalizedAssetIds = [...sharedInternalizedAssets] bundleId = bundleGraph.addNode(bundle) bundles.set(key, bundleId) } else { bundle = nullthrows(bundleGraph.getNode(bundleId)) invariant(bundle !== "root") } bundle.assets.add(asset) bundle.size += asset.stats.size for (let sourceBundleId of sourceBundles) { if (bundleId !== sourceBundleId) { bundleGraph.addEdge(sourceBundleId, bundleId) } } dependencyBundleGraph.addNodeByContentKeyIfNeeded(String(bundleId), { value: bundle, type: "bundle" }) } else if (reachable.length <= config.minBundles) { for (let root of reachable) { let bundle = nullthrows( bundleGraph.getNode(nullthrows(bundles.get(root.id))) ) invariant(bundle !== "root") bundle.assets.add(asset) bundle.size += asset.stats.size } } } // Step Merge Share Bundles: Merge any shared bundles under the minimum bundle size back into // their source bundles, and remove the bundle. // We should include "bundle reuse" as shared bundles that may be removed but the bundle itself would have to be retained for (let [bundleNodeId, bundle] of bundleGraph.nodes) { if (bundle === "root") continue if ( bundle.sourceBundles.size > 0 && bundle.mainEntryAsset == null && bundle.size < config.minBundleSize ) { removeBundle(bundleGraph, bundleNodeId, assetReference) } } // Step Remove Shared Bundles: Remove shared bundles from bundle groups that hit the parallel request limit. for (let bundleGroupId of bundleGraph.getNodeIdsConnectedFrom(rootNodeId)) { // Find shared bundles in this bundle group. let bundleId = bundleGroupId // We should include "bundle reuse" as shared bundles that may be removed but the bundle itself would have to be retained let bundleIdsInGroup = getBundlesForBundleGroup(bundleId) //get all bundlegrups this bundle is an ancestor of if (bundleIdsInGroup.length > config.maxParallelRequests) { let sharedBundleIdsInBundleGroup = bundleIdsInGroup.filter((b) => { let bundle = nullthrows(bundleGraph.getNode(b)) // shared bundles must have source bundles, we could have a bundle // connected to another bundle that isn't a shared bundle, so check return ( bundle !== "root" && bundle.sourceBundles.size > 0 && bundleId != b ) }) let numBundlesInGroup = bundleIdsInGroup.length // Sort the bundles so the smallest ones are removed first. let sharedBundlesInGroup = sharedBundleIdsInBundleGroup .map((id) => ({ id, bundle: nullthrows(bundleGraph.getNode(id)) })) .map(({ id, bundle }) => { // For Flow invariant(bundle !== "root") return { id, bundle } }) .sort((a, b) => b.bundle.size - a.bundle.size) // Remove bundles until the bundle group is within the parallel request limit. while ( sharedBundlesInGroup.length > 0 && numBundlesInGroup > config.maxParallelRequests ) { let bundleTuple = sharedBundlesInGroup.pop() let bundleToRemove = bundleTuple.bundle let bundleIdToRemove = bundleTuple.id //TODO add integration test where bundles in bunlde group > max parallel request limit & only remove a couple shared bundles // but total # bundles still exceeds limit due to non shared bundles // Add all assets in the shared bundle into the source bundles that are within this bundle group. let sourceBundles = [...bundleToRemove.sourceBundles].filter((b) => bundleIdsInGroup.includes(b) ) for (let sourceBundleId of sourceBundles) { let sourceBundle = nullthrows(bundleGraph.getNode(sourceBundleId)) invariant(sourceBundle !== "root") bundleToRemove.sourceBundles.delete(sourceBundleId) for (let asset of bundleToRemove.assets) { sourceBundle.assets.add(asset) sourceBundle.size += asset.stats.size } //This case is specific to reused bundles, which can have shared bundles attached to it for (let childId of bundleGraph.getNodeIdsConnectedFrom( bundleIdToRemove )) { let child = bundleGraph.getNode(childId) invariant(child !== "root" && child != null) child.sourceBundles.add(sourceBundleId) bundleGraph.addEdge(sourceBundleId, childId) } // needs to add test case where shared bundle is removed from ONE bundlegroup but not from the whole graph! // Remove the edge from this bundle group to the shared bundle. // If there is now only a single bundle group that contains this bundle, // merge it into the remaining source bundles. If it is orphaned entirely, remove it. let incomingNodeCount = bundleGraph.getNodeIdsConnectedTo(bundleIdToRemove).length if ( incomingNodeCount <= 2 && //Never fully remove reused bundles bundleToRemove.mainEntryAsset == null ) { // If one bundle group removes a shared bundle, but the other *can* keep it, still remove because that shared bundle is pointless (only one source bundle) removeBundle(bundleGraph, bundleIdToRemove, assetReference) // Stop iterating through bundleToRemove's sourceBundles as the bundle has been removed. break } else { bundleGraph.removeEdge(sourceBundleId, bundleIdToRemove) } } numBundlesInGroup-- } } } function deleteBundle(bundleRoot: BundleRoot) { bundleGraph.removeNode(nullthrows(bundles.get(bundleRoot.id))) bundleRoots.delete(bundleRoot) bundles.delete(bundleRoot.id) if (reachableRoots.hasContentKey(bundleRoot.id)) { reachableRoots.replaceNodeIdsConnectedTo( reachableRoots.getNodeIdByContentKey(bundleRoot.id), [] ) } if (bundleRootGraph.hasContentKey(bundleRoot.id)) { bundleRootGraph.removeNode( bundleRootGraph.getNodeIdByContentKey(bundleRoot.id) ) } } function getBundleGroupsForBundle(nodeId: NodeId) { let bundleGroupBundleIds = new Set() bundleGraph.traverseAncestors(nodeId, (ancestorId) => { if ( bundleGraph .getNodeIdsConnectedTo(ancestorId) //if node is root, then dont add, otherwise do add. .includes(bundleGraph.rootNodeId) ) { bundleGroupBundleIds.add(ancestorId) } }) return bundleGroupBundleIds } function getBundlesForBundleGroup(bundleGroupId) { let bundlesInABundleGroup = [] bundleGraph.traverse((nodeId) => { bundlesInABundleGroup.push(nodeId) }, bundleGroupId) return bundlesInABundleGroup } function mergeBundle(mainNodeId: NodeId, otherNodeId: NodeId) { //merges assets of "otherRoot" into "mainBundleRoot" let a = nullthrows(bundleGraph.getNode(mainNodeId)) let b = nullthrows(bundleGraph.getNode(otherNodeId)) invariant(a !== "root" && b !== "root") let bundleRootB = nullthrows(b.mainEntryAsset) let mainBundleRoot = nullthrows(a.mainEntryAsset) for (let asset of a.assets) { b.assets.add(asset) } a.assets = b.assets for (let depId of dependencyBundleGraph.getNodeIdsConnectedTo( dependencyBundleGraph.getNodeIdByContentKey(String(otherNodeId)), ALL_EDGE_TYPES )) { dependencyBundleGraph.replaceNodeIdsConnectedTo(depId, [ dependencyBundleGraph.getNodeIdByContentKey(String(mainNodeId)) ]) } //clean up asset reference for (let dependencyTuple of assetReference.get(bundleRootB)) { dependencyTuple[1] = a } //add in any lost edges for (let nodeId of bundleGraph.getNodeIdsConnectedTo(otherNodeId)) { bundleGraph.addEdge(nodeId, mainNodeId) } deleteBundle(bundleRootB) let bundleGroupOfMain = nullthrows(bundleRoots.get(mainBundleRoot))[1] bundleRoots.set(bundleRootB, [mainNodeId, bundleGroupOfMain]) bundles.set(bundleRootB.id, mainNodeId) } function getBundleFromBundleRoot(bundleRoot: BundleRoot): Bundle { let bundle = bundleGraph.getNode(nullthrows(bundleRoots.get(bundleRoot))[0]) invariant(bundle !== "root" && bundle != null) return bundle } return { bundleGraph, dependencyBundleGraph, bundleGroupBundleIds, assetReference } } ================================================ FILE: core/parcel-bundler/src/decorate-legacy-graph.ts ================================================ import invariant from "assert" import { ALL_EDGE_TYPES, NodeId } from "@parcel/graph" import type { BundleGroup, Bundle as LegacyBundle, MutableBundleGraph } from "@parcel/types" import nullthrows from "nullthrows" import type { Bundle, IdealGraph } from "./types" export function decorateLegacyGraph( idealGraph: IdealGraph, bundleGraph: MutableBundleGraph ): void { let idealBundleToLegacyBundle: Map = new Map() let { bundleGraph: idealBundleGraph, dependencyBundleGraph, bundleGroupBundleIds } = idealGraph let entryBundleToBundleGroup: Map = new Map() // Step Create Bundles: Create bundle groups, bundles, and shared bundles and add assets to them for (let [bundleNodeId, idealBundle] of idealBundleGraph.nodes) { if (idealBundle === "root") continue let entryAsset = idealBundle.mainEntryAsset let bundleGroup let bundle if (bundleGroupBundleIds.has(bundleNodeId)) { let dependencies = dependencyBundleGraph .getNodeIdsConnectedTo( dependencyBundleGraph.getNodeIdByContentKey(String(bundleNodeId)), ALL_EDGE_TYPES ) .map((nodeId) => { let dependency = nullthrows(dependencyBundleGraph.getNode(nodeId)) invariant(dependency.type === "dependency") return dependency.value }) for (let dependency of dependencies) { bundleGroup = bundleGraph.createBundleGroup( dependency, idealBundle.target ) } invariant(bundleGroup) entryBundleToBundleGroup.set(bundleNodeId, bundleGroup) bundle = nullthrows( bundleGraph.createBundle({ entryAsset: nullthrows(entryAsset), needsStableName: idealBundle.needsStableName, bundleBehavior: idealBundle.bundleBehavior, target: idealBundle.target }) ) bundleGraph.addBundleToBundleGroup(bundle, bundleGroup) } else if ( idealBundle.sourceBundles.size > 0 && !idealBundle.mainEntryAsset ) { bundle = nullthrows( bundleGraph.createBundle({ uniqueKey: [...idealBundle.assets].map((asset) => asset.id).join(",") + [...idealBundle.sourceBundles].join(","), needsStableName: idealBundle.needsStableName, bundleBehavior: idealBundle.bundleBehavior, type: idealBundle.type, target: idealBundle.target, env: idealBundle.env }) ) } else if (idealBundle.uniqueKey != null) { bundle = nullthrows( bundleGraph.createBundle({ uniqueKey: idealBundle.uniqueKey, needsStableName: idealBundle.needsStableName, bundleBehavior: idealBundle.bundleBehavior, type: idealBundle.type, target: idealBundle.target, env: idealBundle.env }) ) } else { invariant(entryAsset != null) bundle = nullthrows( bundleGraph.createBundle({ entryAsset, needsStableName: idealBundle.needsStableName, bundleBehavior: idealBundle.bundleBehavior, target: idealBundle.target }) ) } idealBundleToLegacyBundle.set(idealBundle, bundle) for (let asset of idealBundle.assets) { bundleGraph.addAssetToBundle(asset, bundle) } } // Step Internalization: Internalize dependencies for bundles for (let [, idealBundle] of idealBundleGraph.nodes) { if (idealBundle === "root") continue let bundle = nullthrows(idealBundleToLegacyBundle.get(idealBundle)) if (idealBundle.internalizedAssets) { for (let internalized of idealBundle.internalizedAssets.values()) { let incomingDeps = bundleGraph.getIncomingDependencies(internalized) for (let incomingDep of incomingDeps) { if ( incomingDep.priority === "lazy" && incomingDep.specifierType !== "url" && bundle.hasDependency(incomingDep) ) { bundleGraph.internalizeAsyncDependency(bundle, incomingDep) } } } } } // Step Add to BundleGroups: Add bundles to their bundle groups idealBundleGraph.traverse((nodeId, _, actions) => { let node = idealBundleGraph.getNode(nodeId) if (node === "root") { return } actions.skipChildren() let outboundNodeIds = idealBundleGraph.getNodeIdsConnectedFrom(nodeId) let entryBundle = nullthrows(idealBundleGraph.getNode(nodeId)) invariant(entryBundle !== "root") let legacyEntryBundle = nullthrows( idealBundleToLegacyBundle.get(entryBundle) ) for (let id of outboundNodeIds) { let siblingBundle = nullthrows(idealBundleGraph.getNode(id)) invariant(siblingBundle !== "root") let legacySiblingBundle = nullthrows( idealBundleToLegacyBundle.get(siblingBundle) ) bundleGraph.createBundleReference(legacyEntryBundle, legacySiblingBundle) } }) // Step References: Add references to all bundles for (let [asset, references] of idealGraph.assetReference) { for (let [dependency, bundle] of references) { let legacyBundle = nullthrows(idealBundleToLegacyBundle.get(bundle)) bundleGraph.createAssetReference(dependency, asset, legacyBundle) } } for (let { from, to } of idealBundleGraph.getAllEdges()) { let sourceBundle = nullthrows(idealBundleGraph.getNode(from)) if (sourceBundle === "root") { continue } invariant(sourceBundle !== "root") let legacySourceBundle = nullthrows( idealBundleToLegacyBundle.get(sourceBundle) ) let targetBundle = nullthrows(idealBundleGraph.getNode(to)) if (targetBundle === "root") { continue } invariant(targetBundle !== "root") let legacyTargetBundle = nullthrows( idealBundleToLegacyBundle.get(targetBundle) ) bundleGraph.createBundleReference(legacySourceBundle, legacyTargetBundle) } } ================================================ FILE: core/parcel-bundler/src/get-entry-by-target.ts ================================================ // @ts-nocheck import invariant from "assert" import type { Asset, Dependency, MutableBundleGraph } from "@parcel/types" import { DefaultMap } from "@parcel/utils" export function getEntryByTarget( bundleGraph: MutableBundleGraph ): DefaultMap> { // Find entries from assetGraph per target let targets: DefaultMap> = new DefaultMap( () => new Map() ) bundleGraph.traverse( { enter(node, context, actions) { if (node.type !== "asset") { return node } invariant( context != null && context.type === "dependency" && context.value.isEntry && context.value.target != null ) targets.get(context.value.target.distDir).set(node.value, context.value) actions.skipChildren() return node } }, undefined ) return targets } ================================================ FILE: core/parcel-bundler/src/get-reachable-bundle-root.ts ================================================ import nullthrows from "nullthrows" import type { BundleRoot } from "./types" export function getReachableBundleRoots(asset, graph): Array { return graph .getNodeIdsConnectedTo(graph.getNodeIdByContentKey(asset.id)) .map((nodeId) => nullthrows(graph.getNode(nodeId))) } ================================================ FILE: core/parcel-bundler/src/index.ts ================================================ /** * Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors * MIT License * * Based on: https://github.com/parcel-bundler/parcel/blob/v2/packages/bundlers/default/package.json * MIT License */ import { Bundler } from "@parcel/plugin" import { vLog } from "@plasmo/utils/logging" import { createIdealGraph } from "./create-ideal-graph" import { decorateLegacyGraph } from "./decorate-legacy-graph" import { getEntryByTarget } from "./get-entry-by-target" const EXTENSION_OPTIONS = { minBundles: 1_000_000_000, minBundleSize: 2_400, maxParallelRequests: 20 } /** * * The Bundler works by creating an IdealGraph, which contains a BundleGraph that models bundles * connected to othervbundles by what references them, and thus models BundleGroups. * * First, we enter `bundle({bundleGraph, config})`. Here, "bundleGraph" is actually just the * assetGraph turned into a type `MutableBundleGraph`, which will then be mutated in decorate, * and turned into what we expect the bundleGraph to be as per the old (default) bundler structure * & what the rest of Parcel expects a BundleGraph to be. * * `bundle({bundleGraph, config})` First gets a Mapping of target to entries, In most cases there is * only one target, and one or more entries. (Targets are pertinent in monorepos or projects where you * will have two or more distDirs, or output folders.) Then calls create IdealGraph and Decorate per target. * */ export default new Bundler({ loadConfig({ options }) { // TODO: Maybe depend on whether it's BGSW or not, we enable bundle splitting // console.log(options) return EXTENSION_OPTIONS }, bundle({ bundleGraph, config }) { vLog("@plasmohq/parcel-bundler") let targetMap = getEntryByTarget(bundleGraph) // Organize entries by target output folder/ distDir let graphs = [] for (let entries of targetMap.values()) { // Create separate bundleGraphs per distDir graphs.push(createIdealGraph(bundleGraph, config, entries)) } for (let g of graphs) { decorateLegacyGraph(g, bundleGraph) //mutate original graph } }, optimize() {} }) ================================================ FILE: core/parcel-bundler/src/remove-bundle.ts ================================================ import invariant from "assert" import type { Graph, NodeId } from "@parcel/graph" import type { Asset, Dependency } from "@parcel/types" import type { DefaultMap } from "@parcel/utils" import nullthrows from "nullthrows" import type { Bundle } from "./types" export function removeBundle( bundleGraph: Graph, bundleId: NodeId, assetReference: DefaultMap> ) { let bundle = nullthrows(bundleGraph.getNode(bundleId)) invariant(bundle !== "root") for (let asset of bundle.assets) { assetReference.set( asset, assetReference.get(asset).filter((t) => !t.includes(bundle)) ) for (let sourceBundleId of bundle.sourceBundles) { let sourceBundle = nullthrows(bundleGraph.getNode(sourceBundleId)) invariant(sourceBundle !== "root") sourceBundle.assets.add(asset) sourceBundle.size += asset.stats.size } } bundleGraph.removeNode(bundleId) } ================================================ FILE: core/parcel-bundler/src/types.ts ================================================ import type { ContentGraph, Graph, NodeId } from "@parcel/graph" import type { Asset, BundleBehavior, Dependency, Environment, Target } from "@parcel/types" import type { DefaultMap } from "@parcel/utils" import type { BitSet } from "./bit-set" type AssetId = string export type DependencyBundleGraph = ContentGraph< | { value: Bundle type: "bundle" } | { value: Dependency type: "dependency" }, number > // IdealGraph is the structure we will pass to decorate, // which mutates the assetGraph into the bundleGraph we would // expect from default bundler export type IdealGraph = { dependencyBundleGraph: DependencyBundleGraph bundleGraph: Graph bundleGroupBundleIds: Set assetReference: DefaultMap> } export type Bundle = { uniqueKey: string | null | undefined assets: Set internalizedAsset?: BitSet internalizedAssetIds: Array bundleBehavior?: BundleBehavior | null | undefined needsStableName: boolean mainEntryAsset: Asset | null | undefined size: number sourceBundles: Set target: Target env: Environment type: string } /* BundleRoot - An asset that is the main entry of a Bundle. */ export type BundleRoot = Asset export const dependencyPriorityEdges = { sync: 1, parallel: 2, lazy: 3 } export type ResolvedBundlerConfig = { minBundles: number minBundleSize: number maxParallelRequests: number } ================================================ FILE: core/parcel-bundler/tsconfig.json ================================================ { "extends": "@plasmo/config/ts/framework", "include": ["src/**/*.ts"], "exclude": ["dist", "node_modules"] } ================================================ FILE: core/parcel-compressor-utf8/.gitignore ================================================ node_modules dist/ ================================================ FILE: core/parcel-compressor-utf8/package.json ================================================ { "name": "@plasmohq/parcel-compressor-utf8", "version": "0.1.1", "description": "Plasmo UTF8 Compressor for Extension", "files": [ "dist" ], "main": "dist/index.js", "scripts": { "prepublishOnly": "pnpm build", "build": "tsup --minify --clean", "dev": "tsup --sourcemap --watch" }, "author": "Plasmo Corp. ", "homepage": "https://docs.plasmo.com/", "engines": { "parcel": ">= 2.8.0" }, "license": "MIT", "repository": { "type": "git", "url": "https://github.com/PlasmoHQ/plasmo.git" }, "devDependencies": { "@plasmo/config": "workspace:*", "@plasmo/utils": "workspace:*", "tsup": "8.4.0" }, "dependencies": { "@parcel/plugin": "2.9.3" } } ================================================ FILE: core/parcel-compressor-utf8/src/index.ts ================================================ /** * Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors * MIT License */ import { Compressor } from "@parcel/plugin" import { Utf8Transform } from "./utf8-transform" export default new Compressor({ async compress({ stream }) { return { stream: stream.pipe(new Utf8Transform()) } } }) ================================================ FILE: core/parcel-compressor-utf8/src/utf8-transform.ts ================================================ /** * Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors * MIT License */ import { Transform, type TransformCallback } from "stream" import { StringDecoder } from "string_decoder" class Utf8Transform extends Transform { private decoder = new StringDecoder("utf8") _transform(chunk: Buffer, _: BufferEncoding, callback: TransformCallback) { callback(null, this.transformChunk(this.decoder.write(chunk))) } _flush(callback: TransformCallback) { const remainder = this.decoder.end() remainder ? callback(null, this.transformChunk(remainder)) : callback() } private transformChunk(chunk: string, segmentSize: number = 1024): string { let result = "" for (let i = 0; i < chunk.length; i += segmentSize) { const endIndex = Math.min(i + segmentSize, chunk.length) const segment = chunk.substring(i, endIndex) result += this.transformSegment(segment) } return result } private transformSegment(segment: string): string { return Array.from(segment) .map((ch) => ch.charCodeAt(0) <= 0x7f ? ch : "\\u" + ("0000" + ch.charCodeAt(0).toString(16)).slice(-4) ) .join("") } } export { Utf8Transform } ================================================ FILE: core/parcel-compressor-utf8/tsconfig.json ================================================ { "extends": "@plasmo/config/ts/framework", "include": ["src/**/*.ts"], "exclude": ["dist", "node_modules"] } ================================================ FILE: core/parcel-compressor-utf8/tsup.config.ts ================================================ import { defineConfig } from "tsup" export default defineConfig({ entry: ["src/index.ts"] }) ================================================ FILE: core/parcel-config/.gitignore ================================================ run.json ================================================ FILE: core/parcel-config/index.json ================================================ { "extends": "@parcel/config-default", "bundler": "@plasmohq/parcel-bundler", "resolvers": [ "@plasmohq/parcel-resolver", "@parcel/resolver-default", "@plasmohq/parcel-resolver-post" ], "transformers": { "*.plasmo.manifest.json": ["@plasmohq/parcel-transformer-manifest"], "react:*.svg": ["@parcel/transformer-svg-react"], "react:*.{js,mjs,jsm,jsx,es6,cjs,ts,tsx}": [ "@parcel/transformer-babel", "@parcel/transformer-js", "@parcel/transformer-react-refresh-wrap" ], "data-text:*.module.{css,pcss}": [ "@parcel/transformer-postcss", "@plasmohq/parcel-transformer-inline-css", "@parcel/transformer-inline-string" ], "data-text:*": ["...", "@parcel/transformer-inline-string"], "data-base64:*": ["...", "@parcel/transformer-inline-string"], "data-env:*": ["@plasmohq/parcel-transformer-inject-env", "..."], "data-text-env:*": [ "@plasmohq/parcel-transformer-inject-env", "...", "@parcel/transformer-inline-string" ], "raw:*": ["@parcel/transformer-raw"], "raw-env:*": [ "@plasmohq/parcel-transformer-inject-env", "@parcel/transformer-raw" ], "*.vue": ["@plasmohq/parcel-transformer-vue"], "template:*.vue": ["@plasmohq/parcel-transformer-vue"], "script:*.vue": ["@plasmohq/parcel-transformer-vue"], "style:*.vue": ["@plasmohq/parcel-transformer-vue"], "style-raw:*.vue": ["@plasmohq/parcel-transformer-vue"], "custom:*.vue": ["@plasmohq/parcel-transformer-vue"], "*.svelte": ["@plasmohq/parcel-transformer-svelte"], "*.{gql,graphql}": ["@parcel/transformer-graphql"], "*.{sass,scss}": ["@parcel/transformer-sass"], "*.less": ["@parcel/transformer-less"] }, "namers": ["@plasmohq/parcel-namer-manifest", "..."], "packagers": { "manifest.json": "@plasmohq/parcel-packager" }, "optimizers": { "data-base64:*": ["...", "@parcel/optimizer-data-url"], "*.{js,mjs,cjs}": [ "@plasmohq/parcel-optimizer-encapsulate", "@plasmohq/parcel-optimizer-es" ] }, "compressors": { "*.js": ["@plasmohq/parcel-compressor-utf8"], "*": ["@parcel/compressor-raw"] }, "runtimes": [ "@parcel/runtime-js", "@plasmohq/parcel-runtime", "@parcel/runtime-service-worker" ] } ================================================ FILE: core/parcel-config/package.json ================================================ { "name": "@plasmohq/parcel-config", "version": "0.42.0", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/PlasmoHQ/plasmo.git" }, "main": "index.json", "dependencies": { "@parcel/compressor-raw": "2.9.3", "@parcel/config-default": "2.9.3", "@parcel/core": "2.9.3", "@parcel/optimizer-data-url": "2.9.3", "@parcel/reporter-bundle-buddy": "2.9.3", "@parcel/resolver-default": "2.9.3", "@parcel/runtime-js": "2.8.3", "@parcel/runtime-service-worker": "2.9.3", "@parcel/source-map": "2.1.1", "@parcel/transformer-babel": "2.9.3", "@parcel/transformer-css": "2.9.3", "@parcel/transformer-graphql": "2.9.3", "@parcel/transformer-inline-string": "2.9.3", "@parcel/transformer-js": "2.9.3", "@parcel/transformer-less": "2.9.3", "@parcel/transformer-postcss": "2.9.3", "@parcel/transformer-raw": "2.9.3", "@parcel/transformer-react-refresh-wrap": "2.9.3", "@parcel/transformer-sass": "2.9.3", "@parcel/transformer-svg-react": "2.9.3", "@parcel/transformer-worklet": "2.9.3", "@plasmohq/parcel-bundler": "workspace:*", "@plasmohq/parcel-compressor-utf8": "workspace:*", "@plasmohq/parcel-namer-manifest": "workspace:*", "@plasmohq/parcel-optimizer-encapsulate": "workspace:*", "@plasmohq/parcel-optimizer-es": "workspace:*", "@plasmohq/parcel-packager": "workspace:*", "@plasmohq/parcel-resolver": "workspace:*", "@plasmohq/parcel-resolver-post": "workspace:*", "@plasmohq/parcel-runtime": "workspace:*", "@plasmohq/parcel-transformer-inject-env": "workspace:*", "@plasmohq/parcel-transformer-inline-css": "workspace:*", "@plasmohq/parcel-transformer-manifest": "workspace:*", "@plasmohq/parcel-transformer-svelte": "workspace:*", "@plasmohq/parcel-transformer-vue": "workspace:*" } } ================================================ FILE: core/parcel-core/.gitignore ================================================ node_modules dist/ ================================================ FILE: core/parcel-core/package.json ================================================ { "name": "@plasmohq/parcel-core", "version": "0.1.11", "description": "Plasmo Parcel Core Fork", "files": [ "dist" ], "main": "dist/index.js", "types": "src/types.ts", "scripts": { "prepublishOnly": "pnpm build", "build": "tsup --minify --clean", "dev": "tsup --sourcemap --watch" }, "author": "Plasmo Corp. ", "homepage": "https://docs.plasmo.com/", "engines": { "parcel": ">= 2.7.0" }, "license": "MIT", "repository": { "type": "git", "url": "https://github.com/PlasmoHQ/plasmo.git" }, "devDependencies": { "@parcel/types": "2.8.3", "@plasmo/config": "workspace:*", "@plasmo/utils": "workspace:*", "tsup": "8.4.0" }, "dependencies": { "@parcel/cache": "2.9.3", "@parcel/core": "2.9.3", "@parcel/diagnostic": "2.9.3", "@parcel/events": "2.9.3", "@parcel/fs": "2.9.3", "@parcel/graph": "2.9.3", "@parcel/hash": "2.9.3", "@parcel/logger": "2.9.3", "@parcel/package-manager": "2.9.3", "@parcel/plugin": "2.9.3", "@parcel/source-map": "2.1.1", "@parcel/types": "2.9.3", "@parcel/utils": "2.9.3", "@parcel/watcher": "2.5.1", "@parcel/workers": "2.9.3", "abortcontroller-polyfill": "1.7.8", "nullthrows": "1.1.1" } } ================================================ FILE: core/parcel-core/src/index.ts ================================================ /** * Forked from https://github.com/parcel-bundler/parcel/blob/19fe7ff00f28f44300fe803c4e594b9fc02b25ad/packages/core/core/src/Parcel.js * MIT License */ import invariant from "assert" import { createWorkerFarm } from "@parcel/core" import dumpGraphToGraphViz from "@parcel/core/lib/dumpGraphToGraphViz" import ParcelConfig from "@parcel/core/lib/ParcelConfig" import { toProjectPath } from "@parcel/core/lib/projectPath" import { assetFromValue } from "@parcel/core/lib/public/Asset" import { PackagedBundle } from "@parcel/core/lib/public/Bundle" import BundleGraph from "@parcel/core/lib/public/BundleGraph" import ReporterRunner from "@parcel/core/lib/ReporterRunner" import createParcelBuildRequest from "@parcel/core/lib/requests/ParcelBuildRequest" import { loadParcelConfig } from "@parcel/core/lib/requests/ParcelConfigRequest" import createValidationRequest from "@parcel/core/lib/requests/ValidationRequest" import RequestTracker, { getWatcherOptions, requestGraphEdgeTypes } from "@parcel/core/lib/RequestTracker" import { BuildAbortError, registerCoreWithSerializer } from "@parcel/core/lib/utils" import ThrowableDiagnostic, { anyToDiagnostic, type Diagnostic } from "@parcel/diagnostic" import { Disposable, ValueEmitter } from "@parcel/events" import { init as initHash } from "@parcel/hash" import logger from "@parcel/logger" import { init as initSourcemaps } from "@parcel/source-map" import type { AsyncSubscription, BuildEvent, BuildSuccessEvent, InitialParcelOptions, PackagedBundle as IPackagedBundle } from "@parcel/types" import { PromiseQueue } from "@parcel/utils" import { type Options as ParcelWatcherOptions } from "@parcel/watcher" // eslint-disable-next-line no-unused-vars import { AbortController } from "abortcontroller-polyfill/dist/cjs-ponyfill" import nullthrows from "nullthrows" import { resolveOptions, type ResolvedOptions } from "./resolve-options" registerCoreWithSerializer() export const INTERNAL_TRANSFORM: symbol = Symbol("internal_transform") export const INTERNAL_RESOLVE: symbol = Symbol("internal_resolve") type SharedReference = number export class Parcel { #requestTracker: RequestTracker #config: ParcelConfig #farm #initialized = false #disposable: Disposable #initialOptions: InitialParcelOptions #reporterRunner: ReporterRunner #resolvedOptions: ResolvedOptions = null #optionsRef: SharedReference #watchAbortController /*: AbortController*/ #watchQueue = new PromiseQueue({ maxConcurrent: 1 }) #watchEvents: ValueEmitter< | { error: Error buildEvent?: void } | { buildEvent: BuildEvent error?: void } > #watcherSubscription: AsyncSubscription #watcherCount = 0 #requestedAssetIds: Set = new Set() isProfiling /*: boolean */ constructor(options: InitialParcelOptions) { this.#initialOptions = options } async _init(): Promise { if (this.#initialized) { return } await initSourcemaps await initHash let resolvedOptions = await resolveOptions(this.#initialOptions) this.#resolvedOptions = resolvedOptions let { config } = await loadParcelConfig(resolvedOptions) this.#config = new ParcelConfig(config, resolvedOptions) if (this.#initialOptions.workerFarm) { if (this.#initialOptions.workerFarm["ending"]) { throw new Error("Supplied WorkerFarm is ending") } this.#farm = this.#initialOptions.workerFarm } else { this.#farm = createWorkerFarm({ shouldPatchConsole: resolvedOptions.shouldPatchConsole }) } // @ts-ignore QUIRK: upstream def is outdated: await resolvedOptions.cache.ensure() let { dispose: disposeOptions, ref: optionsRef } = await this.#farm.createSharedReference(resolvedOptions, false) this.#optionsRef = optionsRef this.#disposable = new Disposable() if (this.#initialOptions.workerFarm) { // If we don't own the farm, dispose of only these references when // Parcel ends. this.#disposable.add(disposeOptions) } else { // Otherwise, when shutting down, end the entire farm we created. this.#disposable.add(() => this.#farm.end()) } this.#watchEvents = new ValueEmitter() this.#disposable.add(() => this.#watchEvents.dispose()) this.#requestTracker = await RequestTracker.init({ farm: this.#farm, options: resolvedOptions }) this.#reporterRunner = new ReporterRunner({ config: this.#config, options: resolvedOptions, workerFarm: this.#farm }) this.#disposable.add(this.#reporterRunner) this.#initialized = true } async run(): Promise { let startTime = Date.now() if (!this.#initialized) { await this._init() } let result = await this._build({ startTime }) await this._end() if (result.type === "buildFailure") { throw new BuildError(result.diagnostics) } return result as BuildSuccessEvent } async _end(): Promise { this.#initialized = false await Promise.all([ this.#disposable.dispose(), await this.#requestTracker.writeToCache() ]) } async _startNextBuild() { this.#watchAbortController = new AbortController() await this.#farm.callAllWorkers("clearConfigCache", []) try { let buildEvent = await this._build({ signal: this.#watchAbortController.signal }) this.#watchEvents.emit({ buildEvent }) return buildEvent } catch (err) { // Ignore BuildAbortErrors and only emit critical errors. if (!(err instanceof BuildAbortError)) { throw err } } } async watch( cb?: (err: Error, buildEvent?: BuildEvent) => any ): Promise { if (!this.#initialized) { await this._init() } let watchEventsDisposable if (cb) { watchEventsDisposable = this.#watchEvents.addListener( ({ error, buildEvent }) => cb(error, buildEvent) ) } if (this.#watcherCount === 0) { this.#watcherSubscription = await this._getWatcherSubscription() await this.#reporterRunner.report({ type: "watchStart" }) // Kick off a first build, but don't await its results. Its results will // be provided to the callback. this.#watchQueue.add(() => this._startNextBuild()) this.#watchQueue.run() } this.#watcherCount++ let unsubscribePromise const unsubscribe = async () => { if (watchEventsDisposable) { watchEventsDisposable.dispose() } this.#watcherCount-- if (this.#watcherCount === 0) { await nullthrows(this.#watcherSubscription).unsubscribe() this.#watcherSubscription = null await this.#reporterRunner.report({ type: "watchEnd" }) this.#watchAbortController.abort() await this.#watchQueue.run() await this._end() } } return { unsubscribe() { if (unsubscribePromise == null) { unsubscribePromise = unsubscribe() } return unsubscribePromise } } } async _build({ signal = null, startTime = Date.now() } = {}): Promise { this.#requestTracker.setSignal(signal) let options = nullthrows(this.#resolvedOptions) try { if (options.shouldProfile) { await this.startProfiling() } this.#watchEvents.emit({ buildEvent: { type: "buildStart" } }) this.#reporterRunner.report({ type: "buildStart" }) this.#requestTracker.graph.invalidateOnBuildNodes() let request = createParcelBuildRequest({ optionsRef: this.#optionsRef, requestedAssetIds: this.#requestedAssetIds, signal }) let { bundleGraph, bundleInfo, changedAssets, assetRequests } = await this.#requestTracker.runRequest(request, { force: true }) this.#requestedAssetIds.clear() await dumpGraphToGraphViz( // $FlowFixMe this.#requestTracker.graph, "RequestGraph", requestGraphEdgeTypes ) let event = { type: "buildSuccess" as const, changedAssets: new Map( Array.from(changedAssets).map(([id, asset]) => [ id, assetFromValue(asset, options) ]) ), bundleGraph: new BundleGraph( bundleGraph, (bundle, bundleGraph, options) => PackagedBundle.getWithInfo( bundle, bundleGraph, options, bundleInfo.get(bundle.id) ), options ), buildTime: Date.now() - startTime, requestBundle: async (bundle) => { let bundleNode = bundleGraph._graph.getNodeByContentKey(bundle.id) invariant(bundleNode?.type === "bundle", "Bundle does not exist") if (!bundleNode.value.isPlaceholder) { // Nothing to do. return { type: "buildSuccess", changedAssets: new Map(), bundleGraph: event.bundleGraph, buildTime: 0, requestBundle: event.requestBundle } } for (let assetId of bundleNode.value.entryAssetIds) { this.#requestedAssetIds.add(assetId) } if (this.#watchQueue.getNumWaiting() === 0) { if (this.#watchAbortController) { this.#watchAbortController.abort() } this.#watchQueue.add(() => this._startNextBuild()) } let results = await this.#watchQueue.run() let result = results.filter(Boolean).pop() if (result.type === "buildFailure") { throw new BuildError(result.diagnostics) } return result } } await this.#reporterRunner.report(event) await this.#requestTracker.runRequest( createValidationRequest({ optionsRef: this.#optionsRef, assetRequests }), { force: assetRequests.length > 0 } ) return event } catch (e) { if (e instanceof BuildAbortError) { throw e } let diagnostic = anyToDiagnostic(e) let event = { type: "buildFailure" as const, diagnostics: Array.isArray(diagnostic) ? diagnostic : [diagnostic] } await this.#reporterRunner.report(event) return event } finally { if (this.isProfiling) { await this.stopProfiling() } await this.#farm.callAllWorkers("clearConfigCache", []) } } async _getWatcherSubscription(): Promise { invariant(this.#watcherSubscription == null) // TODO: This is where the resolvedOptions - the watch project root, need to be fixed let resolvedOptions = nullthrows(this.#resolvedOptions) let opts: ParcelWatcherOptions = getWatcherOptions(resolvedOptions) opts.ignore.push(process.env.PLASMO_BUILD_DIR) let sub = await resolvedOptions.inputFS.watch( resolvedOptions.projectRoot, (err, events) => { if (err) { this.#watchEvents.emit({ error: err }) return } let isInvalid = this.#requestTracker.respondToFSEvents( events.map((e) => ({ type: e.type, path: toProjectPath(resolvedOptions.projectRoot, e.path) })) ) if (isInvalid && this.#watchQueue.getNumWaiting() === 0) { if (this.#watchAbortController) { this.#watchAbortController.abort() } this.#watchQueue.add(() => this._startNextBuild()) this.#watchQueue.run() } }, opts ) return { unsubscribe: () => sub.unsubscribe() } } async startProfiling(): Promise { if (this.isProfiling) { throw new Error("Parcel is already profiling") } logger.info({ origin: "@parcel/core", message: "Starting profiling..." }) this.isProfiling = true await this.#farm.startProfile() } stopProfiling(): Promise { if (!this.isProfiling) { throw new Error("Parcel is not profiling") } logger.info({ origin: "@parcel/core", message: "Stopping profiling..." }) this.isProfiling = false return this.#farm.endProfile() } takeHeapSnapshot(): Promise { logger.info({ origin: "@parcel/core", message: "Taking heap snapshot..." }) return this.#farm.takeHeapSnapshot() } } class BuildError extends ThrowableDiagnostic { constructor(diagnostic: Array | Diagnostic) { super({ diagnostic }) this.name = "BuildError" } } ================================================ FILE: core/parcel-core/src/resolve-options.ts ================================================ /** * Based on https://github.com/parcel-bundler/parcel/blob/v2/packages/core/core/src/resolveOptions.js * MIT License */ import path from "path" import { FSCache, LMDBCache } from "@parcel/cache" import { toProjectPath } from "@parcel/core/lib/projectPath" import { getResolveFrom } from "@parcel/core/lib/requests/ParcelConfigRequest" import { NodeFS, type FileSystem } from "@parcel/fs" import { hashString } from "@parcel/hash" import { NodePackageManager } from "@parcel/package-manager" import type { DependencySpecifier, FilePath, InitialParcelOptions, InitialServerOptions } from "@parcel/types" import { getRootDir, isGlob, relativePath, resolveConfig } from "@parcel/utils" // Default cache directory name const LOCK_FILE_NAMES = ["yarn.lock", "package-lock.json", "pnpm-lock.yaml"] // Generate a unique instanceId, will change on every run of parcel function generateInstanceId(entries: Array): string { return hashString( `${entries.join(",")}-${Date.now()}-${Math.round(Math.random() * 100)}` ) } export async function resolveOptions(initialOptions: InitialParcelOptions) { let inputFS = initialOptions.inputFS || new NodeFS() let outputFS = initialOptions.outputFS || new NodeFS() let inputCwd = inputFS.cwd() let outputCwd = outputFS.cwd() let entries: Array if (initialOptions.entries == null || initialOptions.entries === "") { entries = [] } else if (Array.isArray(initialOptions.entries)) { entries = initialOptions.entries.map((entry) => path.resolve(inputCwd, entry) ) } else { entries = [path.resolve(inputCwd, initialOptions.entries)] } let shouldMakeEntryReferFolder = false if (entries.length === 1 && !isGlob(entries[0])) { let [entry] = entries try { shouldMakeEntryReferFolder = (await inputFS.stat(entry)).isDirectory() } catch { // ignore failing stat call } } // getRootDir treats the input as files, so getRootDir(["/home/user/myproject"]) returns "/home/user". // Instead we need to make the entry refer to some file inside the specified folders if entries refers to the directory. let entryRoot = getRootDir( shouldMakeEntryReferFolder ? [path.join(entries[0], "index")] : entries ) let projectRootFile = (await resolveConfig( inputFS, path.join(entryRoot, "index"), [...LOCK_FILE_NAMES], path.parse(entryRoot).root )) || path.join(inputCwd, "index") // ? Should this just be rootDir let projectRoot = path.dirname(projectRootFile) let packageManager = initialOptions.packageManager || new NodePackageManager(inputFS, projectRoot) let cacheDir = path.resolve(outputCwd, initialOptions.cacheDir) let cache = initialOptions.cache ?? (outputFS instanceof NodeFS ? new LMDBCache(cacheDir) : // @ts-ignore QUIRK: upstream def is outdated new FSCache(outputFS, cacheDir)) let mode = initialOptions.mode ?? "development" let shouldOptimize = initialOptions?.defaultTargetOptions?.shouldOptimize ?? mode === "production" let publicUrl = initialOptions?.defaultTargetOptions?.publicUrl ?? "/" let distDir = initialOptions?.defaultTargetOptions?.distDir != null ? path.resolve(inputCwd, initialOptions?.defaultTargetOptions?.distDir) : undefined let shouldBuildLazily = initialOptions.shouldBuildLazily ?? false let shouldContentHash = initialOptions.shouldContentHash ?? initialOptions.mode === "production" if (shouldBuildLazily && shouldContentHash) { throw new Error("Lazy bundling does not work with content hashing") } let env = initialOptions.env let port = determinePort(initialOptions.serveOptions, process.env.PORT) return { config: getRelativeConfigSpecifier( inputFS, projectRoot, initialOptions.config ), defaultConfig: getRelativeConfigSpecifier( inputFS, projectRoot, initialOptions.defaultConfig ), shouldPatchConsole: initialOptions.shouldPatchConsole ?? false, env, mode, shouldAutoInstall: initialOptions.shouldAutoInstall ?? false, hmrOptions: initialOptions.hmrOptions ?? null, shouldBuildLazily, shouldBundleIncrementally: initialOptions.shouldBundleIncrementally ?? true, shouldContentHash, serveOptions: initialOptions.serveOptions ? { ...initialOptions.serveOptions, distDir: distDir ?? path.join(outputCwd, "dist"), port } : false, shouldDisableCache: initialOptions.shouldDisableCache ?? false, shouldProfile: initialOptions.shouldProfile ?? false, cacheDir, entries: entries.map((e) => toProjectPath(projectRoot, e)), targets: initialOptions.targets, logLevel: initialOptions.logLevel ?? "info", projectRoot, inputFS, outputFS, cache, packageManager, additionalReporters: initialOptions.additionalReporters?.map( ({ packageName, resolveFrom }) => ({ packageName, resolveFrom: toProjectPath(projectRoot, resolveFrom) }) ) ?? [], instanceId: generateInstanceId(entries), detailedReport: initialOptions.detailedReport, defaultTargetOptions: { shouldOptimize, shouldScopeHoist: initialOptions?.defaultTargetOptions?.shouldScopeHoist, sourceMaps: initialOptions?.defaultTargetOptions?.sourceMaps ?? true, publicUrl, ...(distDir != null ? { distDir: toProjectPath(projectRoot, distDir) } : { /*::...null*/ }), engines: initialOptions?.defaultTargetOptions?.engines, outputFormat: initialOptions?.defaultTargetOptions?.outputFormat, isLibrary: initialOptions?.defaultTargetOptions?.isLibrary } } } export type ResolvedOptions = Awaited> function getRelativeConfigSpecifier( fs: FileSystem, projectRoot: FilePath, specifier: DependencySpecifier | null | undefined ) { if (specifier == null) { return undefined } else if (path.isAbsolute(specifier)) { let resolveFrom = getResolveFrom(fs, projectRoot) let relative = relativePath(path.dirname(resolveFrom), specifier) // If the config is outside the project root, use an absolute path so that if the project root // moves the path still works. Otherwise, use a relative path so that the cache is portable. return relative.startsWith("..") ? specifier : relative } else { return specifier } } function determinePort( initialServerOptions: InitialServerOptions | false | void, portInEnv: string | void, defaultPort: number = 1234 ): number { function parsePort(port: string) { let parsedPort = Number(port) // return undefined if port number defined in .env is not valid integer if (!Number.isInteger(parsedPort)) { return undefined } return parsedPort } if (!initialServerOptions) { return typeof portInEnv !== "undefined" ? parsePort(portInEnv) ?? defaultPort : defaultPort } // if initialServerOptions.port is equal to defaultPort, then this means that port number is provided via PORT=~~~~ on cli. In this case, we should ignore port number defined in .env. if (initialServerOptions.port !== defaultPort) { return initialServerOptions.port } return typeof portInEnv !== "undefined" ? parsePort(portInEnv) ?? defaultPort : defaultPort } ================================================ FILE: core/parcel-core/src/types.ts ================================================ export type { InitialParcelOptions as ParcelOptions } from "@parcel/types" export { Parcel } from "./index" ================================================ FILE: core/parcel-core/tsconfig.json ================================================ { "extends": "@plasmo/config/ts/framework", "include": ["src/**/*.ts"], "exclude": ["dist", "node_modules"] } ================================================ FILE: core/parcel-core/tsup.config.ts ================================================ import { defineConfig } from "tsup" export default defineConfig({ entry: ["src/index.ts"] }) ================================================ FILE: core/parcel-namer-manifest/.gitignore ================================================ node_modules dist/ ================================================ FILE: core/parcel-namer-manifest/package.json ================================================ { "name": "@plasmohq/parcel-namer-manifest", "version": "0.3.12", "description": "Plasmo Parcel Namer for Extension Manifest", "files": [ "dist" ], "main": "dist/index.js", "scripts": { "prepublishOnly": "pnpm build", "build": "tsup src/index.ts --minify --clean", "dev": "tsup src/index.ts --sourcemap --watch" }, "author": "Plasmo Corp. ", "homepage": "https://docs.plasmo.com/", "engines": { "parcel": ">= 2.7.0" }, "license": "MIT", "repository": { "type": "git", "url": "https://github.com/PlasmoHQ/plasmo.git" }, "devDependencies": { "@plasmo/config": "workspace:*", "tsup": "8.4.0" }, "dependencies": { "@parcel/core": "2.9.3", "@parcel/plugin": "2.9.3", "@parcel/types": "2.9.3", "@parcel/utils": "2.9.3" } } ================================================ FILE: core/parcel-namer-manifest/src/index.ts ================================================ /** * Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors * MIT License */ import { Namer } from "@parcel/plugin" export default new Namer({ name({ bundle }) { const mainEntry = bundle.getMainEntry() if (!mainEntry) { return null } if ( bundle.type === "json" && mainEntry.filePath.endsWith(".plasmo.manifest.json") && mainEntry.meta?.webextEntry ) { return "manifest.json" } if (typeof mainEntry.meta?.bundlePath === "string") { return mainEntry.meta.bundlePath } return null } }) ================================================ FILE: core/parcel-namer-manifest/tsconfig.json ================================================ { "extends": "@plasmo/config/ts/cli", "include": ["src/**/*.ts"], "exclude": ["dist", "node_modules"] } ================================================ FILE: core/parcel-optimizer-encapsulate/.gitignore ================================================ node_modules dist/ ================================================ FILE: core/parcel-optimizer-encapsulate/package.json ================================================ { "name": "@plasmohq/parcel-optimizer-encapsulate", "version": "0.0.8", "description": "Plasmo ECMAScript Encapsulation for Extension", "files": [ "dist" ], "main": "dist/index.js", "scripts": { "prepublishOnly": "pnpm build", "build": "tsup --minify --clean", "dev": "tsup --sourcemap --watch" }, "author": "Plasmo Corp. ", "homepage": "https://docs.plasmo.com/", "engines": { "parcel": ">= 2.8.0" }, "license": "MIT", "repository": { "type": "git", "url": "https://github.com/PlasmoHQ/plasmo.git" }, "devDependencies": { "@plasmo/config": "workspace:*", "@plasmo/utils": "workspace:*", "tsup": "8.4.0" }, "dependencies": { "@parcel/core": "2.9.3", "@parcel/plugin": "2.9.3", "@parcel/source-map": "2.1.1", "@parcel/types": "2.9.3" } } ================================================ FILE: core/parcel-optimizer-encapsulate/src/index.ts ================================================ /** * Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors * MIT License * */ import { Optimizer } from "@parcel/plugin" import SourceMap from "@parcel/source-map" import type { PluginOptions } from "@parcel/types" import { vLog } from "@plasmo/utils/logging" const encapsulateGlobal = (name: string) => `var __${name}; typeof ${name} === "function" && (__${name}=${name},${name}=null);` const problematicGlobals = ["define"] const beforeContent = `(function(${problematicGlobals.join( "," )}){${problematicGlobals.map(encapsulateGlobal).join("")}` const afterContent = ` ${problematicGlobals .map((n) => `globalThis.${n}=__${n};`) .join("")} })(${problematicGlobals .map((n) => `globalThis.${n}`) .join(",")});` function getSourceMap(options: PluginOptions, map: SourceMap) { if (process.env.__PLASMO_FRAMEWORK_INTERNAL_SOURCE_MAPS !== "none") { const newMap = new SourceMap(options.projectRoot) const mapBuffer = map.toBuffer() const lineOffset = 1 newMap.addBuffer(mapBuffer, lineOffset) return newMap } else { return null } } export default new Optimizer({ async optimize({ bundle, contents, map, options }) { vLog( "@plasmohq/parcel-optimizer-encapsulate", bundle.name, bundle.displayName ) return { contents: `${beforeContent}\n${contents}${afterContent}`, map: getSourceMap(options, map) } } }) ================================================ FILE: core/parcel-optimizer-encapsulate/tsconfig.json ================================================ { "extends": "@plasmo/config/ts/framework", "include": ["src/**/*.ts"], "exclude": ["dist", "node_modules"] } ================================================ FILE: core/parcel-optimizer-encapsulate/tsup.config.ts ================================================ import { defineConfig } from "tsup" export default defineConfig({ entry: ["src/index.ts"] }) ================================================ FILE: core/parcel-optimizer-es/.gitignore ================================================ node_modules dist/ ================================================ FILE: core/parcel-optimizer-es/package.json ================================================ { "name": "@plasmohq/parcel-optimizer-es", "version": "0.4.2", "description": "Plasmo ECMAScript Optimizer for Extension", "files": [ "dist" ], "main": "dist/index.js", "scripts": { "prepublishOnly": "pnpm build", "build": "tsup --minify --clean", "dev": "tsup --sourcemap --watch" }, "author": "Plasmo Corp. ", "homepage": "https://docs.plasmo.com/", "engines": { "parcel": ">= 2.8.0" }, "license": "MIT", "repository": { "type": "git", "url": "https://github.com/PlasmoHQ/plasmo.git" }, "devDependencies": { "@plasmo/config": "workspace:*", "@plasmo/utils": "workspace:*", "tsup": "8.4.0" }, "dependencies": { "@parcel/core": "2.9.3", "@parcel/plugin": "2.9.3", "@parcel/source-map": "2.1.1", "@parcel/utils": "2.9.3", "@swc/core": "1.11.13", "nullthrows": "1.1.1" } } ================================================ FILE: core/parcel-optimizer-es/src/blob-to-string.ts ================================================ import { Buffer } from "buffer" import { Readable } from "stream" type Blob = Buffer | Readable | string export function bufferStream(stream: Readable): Promise { return new Promise((resolve, reject) => { let buf = Buffer.from([]) stream.on("data", (data) => { buf = Buffer.concat([buf, data]) }) stream.on("end", () => { resolve(buf) }) stream.on("error", reject) }) } export async function blobToString(blob: Blob): Promise { if (typeof blob === "string") { return blob } else if (blob instanceof Readable) { return (await bufferStream(blob)).toString() } else if (blob instanceof Buffer) { return blob.toString() } else { return blob } } ================================================ FILE: core/parcel-optimizer-es/src/index.ts ================================================ /** * Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors * MIT License * * Based on: https://github.com/parcel-bundler/parcel/blob/723e8443757cfb12a1a3af8180f0d923e666e1aa/packages/optimizers/esbuild/src/ESBuildOptimizer.js * MIT License */ import { Optimizer } from "@parcel/plugin" import SourceMap from "@parcel/source-map" import { transform as swcTransform } from "@swc/core" import nullthrows from "nullthrows" import { vLog } from "@plasmo/utils/logging" import { blobToString } from "./blob-to-string" export default new Optimizer({ async optimize({ contents, map: originalMap, bundle, options, getSourceMapReference }) { vLog( "@plasmohq/optimizer-es: ", bundle.name, bundle.displayName, options.projectRoot ) const code = await blobToString(contents) if (!bundle.env.shouldOptimize) { vLog(`optimizer-es: skipped`) return { contents: code, map: originalMap } } const sourceMapType = process.env.__PLASMO_FRAMEWORK_INTERNAL_SOURCE_MAPS const shouldMinify = process.env.__PLASMO_FRAMEWORK_INTERNAL_NO_MINIFY === "false" vLog(`optimizer-es: use SWC for ${bundle.displayName}`) const swcOutput = await swcTransform(code, { jsc: { target: process.env.__PLASMO_FRAMEWORK_INTERNAL_ES_TARGET, minify: { format: { comments: shouldMinify ? "some" : "all" }, mangle: shouldMinify, compress: shouldMinify, sourceMap: sourceMapType !== "none", toplevel: bundle.env.outputFormat === "esmodule" || bundle.env.outputFormat === "commonjs", module: bundle.env.outputFormat === "esmodule" } }, minify: shouldMinify, sourceMaps: sourceMapType === "inline" ? "inline" : sourceMapType === "external", configFile: false, swcrc: false }) let sourceMap = null let minifiedContents = nullthrows(swcOutput.code) if (swcOutput.map) { sourceMap = new SourceMap(options.projectRoot) sourceMap.addVLQMap(JSON.parse(swcOutput.map)) if (originalMap) { sourceMap.extends(originalMap) } let sourcemapReference = await getSourceMapReference(sourceMap) if (sourcemapReference) { minifiedContents += `\n//# sourceMappingURL=${sourcemapReference}\n` } } return { contents: minifiedContents, map: sourceMap } } }) ================================================ FILE: core/parcel-optimizer-es/tsconfig.json ================================================ { "extends": "@plasmo/config/ts/framework", "include": ["src/**/*.ts"], "exclude": ["dist", "node_modules"] } ================================================ FILE: core/parcel-optimizer-es/tsup.config.ts ================================================ import { defineConfig } from "tsup" export default defineConfig({ entry: ["src/index.ts"] }) ================================================ FILE: core/parcel-packager/.gitignore ================================================ node_modules dist/ ================================================ FILE: core/parcel-packager/package.json ================================================ { "name": "@plasmohq/parcel-packager", "version": "0.6.15", "description": "Plasmo Parcel Packager for Web Extension Manifest", "files": [ "dist" ], "main": "dist/index.js", "scripts": { "prepublishOnly": "pnpm build", "build": "tsup src/index.ts --minify --clean", "dev": "tsup src/index.ts --sourcemap --watch" }, "author": "Plasmo Corp. ", "homepage": "https://docs.plasmo.com/", "engines": { "parcel": ">= 2.7.0" }, "license": "MIT", "repository": { "type": "git", "url": "https://github.com/PlasmoHQ/plasmo.git" }, "devDependencies": { "@plasmo/config": "workspace:*", "@plasmo/constants": "workspace:*", "@plasmo/utils": "workspace:*", "tsup": "8.4.0" }, "dependencies": { "@parcel/core": "2.9.3", "@parcel/plugin": "2.9.3", "@parcel/types": "2.9.3", "@parcel/utils": "2.9.3", "nullthrows": "1.1.1" } } ================================================ FILE: core/parcel-packager/src/get-web-accessible-resources.ts ================================================ import type { BundleGraph, Dependency, NamedBundle, PluginOptions } from "@parcel/types" import nullthrows from "nullthrows" import type { ExtensionManifest, ExtensionManifestV2, ExtensionManifestV3 } from "@plasmo/constants" import { getRelativePath } from "./utils" type Mv3Wars = ExtensionManifestV3["web_accessible_resources"] export const getWarsFromContentScripts = ( bundle: NamedBundle, bundleGraph: BundleGraph, dependencies: readonly Dependency[], contentScripts: ExtensionManifest["content_scripts"] = [] ): Mv3Wars => contentScripts .map((contentScript) => { const srcBundles = dependencies .filter( (d) => contentScript.js?.includes(d.id) || contentScript.css?.includes(d.id) ) .map((d) => nullthrows(bundleGraph.getReferencedBundle(d, bundle))) contentScript.css = [ ...new Set( (contentScript.css || []).concat( srcBundles .flatMap((b) => bundleGraph.getReferencedBundles(b)) .filter((b) => b.type == "css") .map((b) => getRelativePath(bundle, b)) ) ) ] const resources = srcBundles .flatMap((b) => { const children: NamedBundle[] = [] const siblings = bundleGraph.getReferencedBundles(b) bundleGraph.traverseBundles((child) => { if (b !== child && !siblings.includes(child)) { children.push(child) } }, b) return children }) .map((b) => getRelativePath(bundle, b)) return resources.length > 0 ? { matches: contentScript.matches.map((match) => { if (/^(((http|ws)s?)|ftp|\*):\/\//.test(match)) { let pathIndex = match.indexOf("/", match.indexOf("://") + 3) // Avoids creating additional errors in invalid match URLs if (pathIndex === -1) { pathIndex = match.length } return match.slice(0, pathIndex) + "/*" } return match }), resources } : null }) .filter(Boolean) export function appendMv2Wars( manifest: ExtensionManifestV2, wars: Mv3Wars, _options: PluginOptions ) { const warResult = (manifest.web_accessible_resources || []).concat([ ...new Set(wars.flatMap((entry) => entry.resources)) ]) if (warResult.length > 0) { manifest.web_accessible_resources = warResult } } export function appendMv3Wars( manifest: ExtensionManifestV3, wars: Mv3Wars, options: PluginOptions ) { if (options.hmrOptions) { wars.push({ matches: [""], resources: ["__plasmo_hmr_proxy__"] }) } if (wars.length > 0) { manifest.web_accessible_resources = ( manifest.web_accessible_resources || [] ).concat(wars) } } ================================================ FILE: core/parcel-packager/src/index.ts ================================================ /** * Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors * MIT License * * Based on: https://github.com/parcel-bundler/parcel/tree/v2/packages/packagers/webextension * MIT License */ import assert from "assert" import { Packager } from "@parcel/plugin" import type { Asset } from "@parcel/types" import { replaceURLReferences } from "@parcel/utils" import type { ExtensionManifest } from "@plasmo/constants" import { vLog } from "@plasmo/utils/logging" import { appendMv2Wars, appendMv3Wars, getWarsFromContentScripts } from "./get-web-accessible-resources" export default new Packager({ async package({ bundle, bundleGraph, options }) { vLog("@plasmohq/parcel-packager") const assets: Asset[] = [] bundle.traverseAssets((asset) => { assets.push(asset) }) const manifestEntryAssets = assets.filter( (a) => a.meta.webextEntry === true ) assert( assets.length === 2 && manifestEntryAssets.length === 1, "Web extension bundles must contain exactly one manifest asset and one runtime asset" ) const [manifestAsset] = manifestEntryAssets const manifest: ExtensionManifest = JSON.parse( await manifestAsset.getCode() ) const dependencies = manifestAsset.getDependencies() const wars = getWarsFromContentScripts( bundle, bundleGraph, dependencies, manifest.content_scripts ) if (manifest.manifest_version === 2) { appendMv2Wars(manifest, wars, options) } else { appendMv3Wars(manifest, wars, options) } const { contents } = replaceURLReferences({ bundle, bundleGraph, contents: JSON.stringify(manifest) }) return { contents } } }) ================================================ FILE: core/parcel-packager/src/utils.ts ================================================ import type { NamedBundle } from "@parcel/types" import { relativeBundlePath } from "@parcel/utils" export function getRelativePath(from: NamedBundle, to: NamedBundle): string { return relativeBundlePath(from, to, { leadingDotSlash: false }) } ================================================ FILE: core/parcel-packager/tsconfig.json ================================================ { "extends": "@plasmo/config/ts/framework", "include": ["src/**/*.ts"], "exclude": ["dist", "node_modules"] } ================================================ FILE: core/parcel-resolver/.gitignore ================================================ node_modules dist/ ================================================ FILE: core/parcel-resolver/index.mjs ================================================ import { build } from "esbuild" import glob from "fast-glob" const commonConfig = { bundle: true, minify: true, platform: "browser", format: "cjs", target: ["chrome74", "safari11"], outdir: "dist/polyfills" } async function buildProdPolyfills() { const prodPolyfills = await glob("./src/polyfills/**/*.ts", { onlyFiles: true }) await build({ ...commonConfig, entryPoints: prodPolyfills, alias: { stream: "stream-browserify", http: "stream-http" } }) } async function buildDevPolyfills() { const devPolyfills = await glob("./src/dev-polyfills/**/*.ts", { onlyFiles: true }) await build({ ...commonConfig, entryPoints: devPolyfills, define: { "process.env.NODE_ENV": "'development'" } }) } async function main() { await Promise.all([buildProdPolyfills(), buildDevPolyfills()]) } main() ================================================ FILE: core/parcel-resolver/package.json ================================================ { "name": "@plasmohq/parcel-resolver", "version": "0.14.2", "description": "Plasmo Parcel Resolver", "files": [ "dist" ], "main": "dist/index.js", "scripts": { "dev": "run-s build:polyfills dev:resolver", "dev:resolver": "tsup src/index.ts --watch", "build": "run-s build:*", "build:resolver": "tsup src/index.ts --minify --clean", "build:polyfills": "node index.mjs", "prepublishOnly": "pnpm build" }, "author": "Plasmo Corp. ", "homepage": "https://docs.plasmo.com/", "engines": { "parcel": ">= 2.7.0" }, "license": "MIT", "repository": { "type": "git", "url": "https://github.com/PlasmoHQ/plasmo.git" }, "devDependencies": { "@plasmo/config": "workspace:*", "@plasmo/utils": "workspace:*", "@plasmohq/rps": "workspace:*", "assert": "2.1.0", "browserify-zlib": "0.2.0", "buffer": "6.0.3", "console-browserify": "1.2.0", "constants-browserify": "1.0.0", "crc-32": "1.2.2", "crypto-browserify": "3.12.1", "domain-browser": "5.7.0", "esbuild": "0.25.1", "events": "3.3.0", "https-browserify": "1.0.0", "os-browserify": "0.3.0", "path-browserify": "1.0.1", "process": "0.11.10", "punycode": "2.3.1", "querystring-es3": "0.2.1", "react-refresh": "0.16.0", "stream-browserify": "3.0.0", "stream-http": "3.2.0", "string_decoder": "1.3.0", "timers-browserify": "2.0.12", "tsup": "8.4.0", "tty-browserify": "0.0.1", "url": "0.11.4", "util": "0.12.5", "vm-browserify": "1.1.2" }, "dependencies": { "@parcel/core": "2.9.3", "@parcel/hash": "2.9.3", "@parcel/plugin": "2.9.3", "@parcel/types": "2.9.3", "fast-glob": "3.3.3", "fs-extra": "11.3.0", "got": "14.4.6" } } ================================================ FILE: core/parcel-resolver/src/dev-polyfills/react-refresh/runtime.ts ================================================ import refreshRuntime from "react-refresh/runtime" export * from "react-refresh/runtime" export default refreshRuntime ================================================ FILE: core/parcel-resolver/src/dev-polyfills/react-refresh.ts ================================================ import refresh from "react-refresh" export * from "react-refresh" export default refresh ================================================ FILE: core/parcel-resolver/src/handle-absolute-root.ts ================================================ import { extname, isAbsolute, join, resolve } from "path" import { pathExists, pathExistsSync } from "fs-extra" import { relevantExtensionList, resolveSourceIndex, type ResolverProps, type ResolverResult } from "./shared" export async function handleAbsoluteRoot({ specifier, dependency }: ResolverProps): Promise { if (specifier[0] !== "/") { return null } if (pathExistsSync(specifier)) { return { filePath: specifier } } const absoluteBaseFile = resolve( join(process.env.PLASMO_PROJECT_DIR, specifier.slice(1)) ) const importExt = extname(absoluteBaseFile) if (importExt.length > 0 && pathExistsSync(absoluteBaseFile)) { return { filePath: absoluteBaseFile } } const parentExt = extname(dependency.resolveFrom) const checkingExts = [ parentExt, ...relevantExtensionList.filter((ext) => ext !== parentExt) ] return resolveSourceIndex(absoluteBaseFile, checkingExts) } ================================================ FILE: core/parcel-resolver/src/handle-alias.ts ================================================ import { resolve } from "path" import { isReadable } from "@plasmo/utils/fs" import { state, type ResolverProps, type ResolverResult } from "./shared" export async function handleAlias({ specifier }: ResolverProps): Promise { if (!state.aliasMap.has(specifier)) { return null } const absPath = resolve(state.aliasMap.get(specifier)) const hasLocalAlias = await isReadable(absPath) if (!hasLocalAlias) { return null } return { filePath: absPath, priority: "sync" } } ================================================ FILE: core/parcel-resolver/src/handle-plasmo-internal.ts ================================================ import { resolve } from "path" import { resolveSourceIndex, state, type ResolverProps, type ResolverResult } from "./shared" const resolveByPrefix = (specifier = "", prefix = "", prefixPath = "") => { if (!specifier.startsWith(prefix)) { return null } const [_, specifierPath] = specifier.split(prefix) return resolveSourceIndex(resolve(prefixPath, specifierPath)) } export async function handlePlasmoInternal({ specifier }: ResolverProps): Promise { return resolveByPrefix( specifier, "@plasmo-static-common/", resolve(state.dotPlasmoDirectory, "static", "common") ) } ================================================ FILE: core/parcel-resolver/src/handle-polyfill.ts ================================================ import { state, type ResolverProps, type ResolverResult } from "./shared" export async function handlePolyfill({ specifier }: ResolverProps): Promise { if (!state.polyfillMap.has(specifier)) { return null } return { filePath: state.polyfillMap.get(specifier), priority: "sync" } } ================================================ FILE: core/parcel-resolver/src/handle-remote-caching.ts ================================================ import { resolve } from "path" import { hashString } from "@parcel/hash" import { injectEnv } from "@plasmo/utils/env" import { vLog } from "@plasmo/utils/logging" import { relevantExtensionList, state, type ResolverProps, type ResolverResult } from "./shared" const cookCode = async (target: URL, code: string) => { if (target.origin === "https://www.googletagmanager.com") { return code.replace(/http:/g, "chrome-extension:") } else { return code } } // TODO: Some kind of caching mechanism would be nice export async function handleRemoteCaching({ specifier, dependency, options }: ResolverProps): Promise { if (!specifier.startsWith("https://")) { return null } // Only these extensions parents are allowed to cache remote dependencies if ( !relevantExtensionList.some((ext) => dependency.sourcePath.endsWith(ext)) ) { return null } const target = new URL(injectEnv(specifier)) const fileType = target.searchParams.get("plasmo-ext") || "js" try { const filePath = resolve( state.remoteCacheDirectory, `${hashString(specifier)}.${fileType}` ) const code = (await state.got(target.toString()).text()) as string const cookedCode = await cookCode(target, code) await options.inputFS.rimraf(filePath) await options.inputFS.writeFile(filePath, cookedCode, { mode: 0o664 }) vLog(`Caching HTTPS dependency: ${specifier}`) return { priority: "lazy", sideEffects: true, filePath } } catch (error) { return { diagnostics: [ { message: `Cannot download the resource from ${specifier}`, hints: [error.message] } ] } } } ================================================ FILE: core/parcel-resolver/src/handle-tilde-src.ts ================================================ import { extname, join, resolve } from "path" import { customPipelineSet, relevantExtensionList, relevantExtensionSet, resolveSourceIndex, type ResolverProps, type ResolverResult } from "./shared" export async function handleTildeSrc({ pipeline, specifier, dependency }: ResolverProps): Promise { if (specifier[0] !== "~") { return null } const absoluteBaseFile = resolve( join(process.env.PLASMO_SRC_DIR, specifier.slice(1)) ) if (customPipelineSet.has(pipeline)) { return { filePath: absoluteBaseFile } } const importExt = extname(absoluteBaseFile) // TODO: Potentially resolve other type of files (less import etc...) that Parcel doesn't account for if (importExt.length > 0 && relevantExtensionSet.has(importExt as any)) { return { filePath: absoluteBaseFile } } const parentExt = extname(dependency.resolveFrom) // console.log(`tildeSrc: resolveFrom: ${dependency.resolveFrom}`) const checkingExts = [ parentExt, ...relevantExtensionList.filter((ext) => ext !== parentExt) ] // console.log(`tildeSrc: ${potentialFiles}`) return resolveSourceIndex(absoluteBaseFile, checkingExts) } ================================================ FILE: core/parcel-resolver/src/index.ts ================================================ import { Resolver } from "@parcel/plugin" import { handleAbsoluteRoot } from "./handle-absolute-root" import { handleAlias } from "./handle-alias" import { handlePlasmoInternal } from "./handle-plasmo-internal" import { handlePolyfill } from "./handle-polyfill" import { handleRemoteCaching } from "./handle-remote-caching" import { handleTildeSrc } from "./handle-tilde-src" import { initializeState } from "./shared" export default new Resolver({ async resolve(props) { await initializeState(props) return ( (await handleAlias(props)) || (await handlePlasmoInternal(props)) || (await handlePolyfill(props)) || (await handleRemoteCaching(props)) || (await handleTildeSrc(props)) || (await handleAbsoluteRoot(props)) || null ) } }) ================================================ FILE: core/parcel-resolver/src/polyfills/assert.ts ================================================ import assert from "assert/build/assert" export * from "assert/build/assert" export default assert ================================================ FILE: core/parcel-resolver/src/polyfills/buffer.ts ================================================ import buffer from "buffer/index" export * from "buffer/index" export default buffer ================================================ FILE: core/parcel-resolver/src/polyfills/console.ts ================================================ import console from "console-browserify" export * from "console-browserify" export default console ================================================ FILE: core/parcel-resolver/src/polyfills/constants.ts ================================================ import constants from "constants-browserify/constants.json" export default constants ================================================ FILE: core/parcel-resolver/src/polyfills/crc-32/crc32c.ts ================================================ import crc32c from "crc-32/crc32c" globalThis.DO_NOT_EXPORT_CRC = true export * from "crc-32/crc32c" export default crc32c ================================================ FILE: core/parcel-resolver/src/polyfills/crc-32.ts ================================================ import crc32 from "crc-32" globalThis.DO_NOT_EXPORT_CRC = true export * from "crc-32" export default crc32 ================================================ FILE: core/parcel-resolver/src/polyfills/crypto.ts ================================================ import crypto from "crypto-browserify" export * from "crypto-browserify" export default crypto ================================================ FILE: core/parcel-resolver/src/polyfills/domain.ts ================================================ import domain from "domain-browser" export * from "domain-browser" export default domain ================================================ FILE: core/parcel-resolver/src/polyfills/events.ts ================================================ import events from "events/events" export * from "events/events" export default events ================================================ FILE: core/parcel-resolver/src/polyfills/http.ts ================================================ import http from "stream-http" export * from "stream-http" export default http ================================================ FILE: core/parcel-resolver/src/polyfills/https.ts ================================================ import https from "https-browserify" export * from "https-browserify" export default https ================================================ FILE: core/parcel-resolver/src/polyfills/os.ts ================================================ import os from "os-browserify/browser" export * from "os-browserify/browser" export default os ================================================ FILE: core/parcel-resolver/src/polyfills/path.ts ================================================ import path from "path-browserify" export * from "path-browserify" export default path ================================================ FILE: core/parcel-resolver/src/polyfills/process.ts ================================================ import process from "process/browser" export * from "process/browser" export default process ================================================ FILE: core/parcel-resolver/src/polyfills/punycode.ts ================================================ import punycode from "punycode/punycode.es6" export * from "punycode/punycode.es6" export default punycode ================================================ FILE: core/parcel-resolver/src/polyfills/querystring.ts ================================================ import querystring from "querystring-es3" export * from "querystring-es3" export default querystring ================================================ FILE: core/parcel-resolver/src/polyfills/stream.ts ================================================ import stream from "stream-browserify" export * from "stream-browserify" export default stream ================================================ FILE: core/parcel-resolver/src/polyfills/string_decoder.ts ================================================ import stringDecoder from "string_decoder/lib/string_decoder" export * from "string_decoder/lib/string_decoder" export default stringDecoder ================================================ FILE: core/parcel-resolver/src/polyfills/sys.ts ================================================ import sys from "util/util" export * from "util/util" export default sys ================================================ FILE: core/parcel-resolver/src/polyfills/timers.ts ================================================ import timers from "timers-browserify" export * from "timers-browserify" export default timers ================================================ FILE: core/parcel-resolver/src/polyfills/tty.ts ================================================ import tty from "tty-browserify" export * from "tty-browserify" export default tty ================================================ FILE: core/parcel-resolver/src/polyfills/url.ts ================================================ import url from "url/url" export * from "url/url" export default url ================================================ FILE: core/parcel-resolver/src/polyfills/util.ts ================================================ import util from "util/util" export * from "util/util" export default util ================================================ FILE: core/parcel-resolver/src/polyfills/vm.ts ================================================ import vm from "vm-browserify" export * from "vm-browserify" export default vm ================================================ FILE: core/parcel-resolver/src/polyfills/zlib.ts ================================================ import zlib from "browserify-zlib" export * from "browserify-zlib" export default zlib ================================================ FILE: core/parcel-resolver/src/shared.ts ================================================ import { statSync } from "fs" import { join, resolve } from "path" import type { Resolver } from "@parcel/plugin" import type { ResolveResult } from "@parcel/types" import glob from "fast-glob" import { readJson } from "fs-extra" import type { Got } from "got" import { vLog } from "@plasmo/utils/logging" import { toPosix } from "@plasmo/utils/path" export const relevantExtensionList = [ ".ts", ".tsx", ".svelte", ".vue", ".json" ] as const export const customPipelineSet = new Set([ "data-text", "data-base64", "data-env", "data-text-env", "raw", "raw-env" ]) export const relevantExtensionSet = new Set(relevantExtensionList) type ResolveFx = ConstructorParameters[0]["resolve"] export type ResolverResult = ResolveResult export type ResolverProps = Parameters[0] export const state = { got: null as Got, dotPlasmoDirectory: null as string, remoteCacheDirectory: null as string, polyfillMap: null as Map, aliasMap: null as Map } export const initializeState = async (props: ResolverProps) => { if (state.got === null) { state.got = (await import("got")).default } if (!state.dotPlasmoDirectory) { state.dotPlasmoDirectory = resolve( process.env.PLASMO_PROJECT_DIR, ".plasmo" ) } if (!state.remoteCacheDirectory) { state.remoteCacheDirectory = resolve( state.dotPlasmoDirectory, "cache", "remote-code" ) if (!(await props.options.inputFS.exists(state.remoteCacheDirectory))) { vLog("Reinitializing remote cache directory") await props.options.inputFS.mkdirp(state.remoteCacheDirectory) } } if (!state.polyfillMap) { const polyfillsDirectory = join(__dirname, "polyfills") const polyfillHandlers = await glob("**/*.js", { cwd: polyfillsDirectory, onlyFiles: true }) state.polyfillMap = new Map( polyfillHandlers.map((handler) => [ toPosix(handler.slice(0, -3)), join(polyfillsDirectory, handler) ]) ) } if (!state.aliasMap) { const packageJson = await readJson( resolve(process.env.PLASMO_PROJECT_DIR, "package.json") ) state.aliasMap = new Map(Object.entries(packageJson.alias || {})) } } /** * Look for source code file (crawl index) */ export const resolveSourceIndex = async ( absoluteBaseFile: string, checkingExts = relevantExtensionList as readonly string[], opts = {} as Partial ): Promise => { const potentialFiles = checkingExts.flatMap((ext) => [ `${absoluteBaseFile}${ext}`, resolve(absoluteBaseFile, `index${ext}`) ]) for (const file of potentialFiles) { try { if (statSync(file).isFile()) { return { filePath: file, ...opts } } } catch {} } return null } ================================================ FILE: core/parcel-resolver/tsconfig.json ================================================ { "extends": "@plasmo/config/ts/cli", "include": ["src/**/*.ts", "tsup.config.ts"], "exclude": ["dist", "node_modules"] } ================================================ FILE: core/parcel-resolver-post/.gitignore ================================================ node_modules dist/ ================================================ FILE: core/parcel-resolver-post/package.json ================================================ { "name": "@plasmohq/parcel-resolver-post", "version": "0.4.6", "description": "Plasmo Parcel Resolver Post-processing", "files": [ "dist" ], "main": "dist/index.js", "scripts": { "prepublishOnly": "pnpm build", "build": "tsup src/index.ts --minify --clean", "dev": "tsup src/index.ts --watch" }, "author": "Plasmo Corp. ", "homepage": "https://docs.plasmo.com/", "engines": { "parcel": ">= 2.7.0" }, "license": "MIT", "repository": { "type": "git", "url": "https://github.com/PlasmoHQ/plasmo.git" }, "devDependencies": { "@plasmo/config": "workspace:*", "@plasmo/utils": "workspace:*" }, "dependencies": { "@parcel/core": "2.9.3", "@parcel/hash": "2.9.3", "@parcel/plugin": "2.9.3", "@parcel/types": "2.9.3", "@parcel/utils": "2.9.3", "tsup": "8.4.0", "typescript": "5.8.2" } } ================================================ FILE: core/parcel-resolver-post/src/handle-hacks.ts ================================================ // These are hack resolvers to get over some module resolution issues, with a PR to fix them upstream. import type { ResolverProps, ResolverResult } from "./shared" // Last resort resolver for weird packages: export async function handleHacks({ specifier, dependency }: ResolverProps): Promise { switch (specifier) { // Example of a resolved hack: // TODO: remove this when we have other hacks here... // case "svelte/internal/disclose-version": { // // https://github.com/sveltejs/svelte/pull/8874 // const sveltePjPath = require.resolve("svelte/package.json", { // paths: [dependency.resolveFrom] // }) // return { // filePath: join( // dirname(sveltePjPath), // "src", // "runtime", // "internal", // "disclose-version", // "index.js" // ) // } // } default: return null } } ================================================ FILE: core/parcel-resolver-post/src/handle-module-exports.ts ================================================ import type { ResolverProps, ResolverResult } from "./shared" // Last resort resolver for weird packages: export async function handleModuleExport({ specifier, dependency }: ResolverProps): Promise { // Ignore relative path if (specifier.startsWith("./") || specifier.startsWith("../")) { return null } try { const filePath = require.resolve(specifier, { paths: [dependency.resolveFrom] }) return { filePath } } catch {} return null } ================================================ FILE: core/parcel-resolver-post/src/handle-ts-path.ts ================================================ /** * Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors * MIT License * * Based on: https://github.com/zachbryant/parcel-resolver-tspaths * Copyright (c) 2021 Zach Bryant * MIT License */ import { dirname, extname, join, resolve } from "path" import { loadConfig } from "@parcel/utils" import type { CompilerOptions } from "typescript" import { isReadable } from "@plasmo/utils/fs" import type { ResolverProps, ResolverResult } from "./shared" import { checkWebpackSpecificImportSyntax, findModule, trimStar } from "./utils" const tsRegex = /\.(tsx?)|vue|svelte$/ const relevantExtList = [ ".ts", ".tsx", ".svelte", ".vue", ".json", ".css", ".scss", ".sass", ".less", ".svg", ".js", ".jsx" ] as const const relevantExtSet = new Set(relevantExtList) type TsPaths = string[] type TsPathsMap = Map const state = { pathsMap: null as TsPathsMap, pathsMapRegex: null as [string, TsPaths, RegExp][] } export async function handleTsPath( props: ResolverProps ): Promise { try { const { dependency, specifier } = props checkWebpackSpecificImportSyntax(dependency.specifier) const isTypescript = tsRegex.test(dependency.resolveFrom) if (!isTypescript) { return null } if (specifier.startsWith(".")) { return { filePath: findModule( resolve(dependency.resolveFrom, "..", specifier), relevantExtList ) } } const compilerOptions = await getTsconfigCompilerOptions(props) if (compilerOptions.length === 0) { return null } loadTsPathsMap(compilerOptions) const result = attemptResolve(props) if (!result) { return null } return { filePath: result } } catch { return null } } /** Populate a map with any paths from tsconfig.json starting from baseUrl */ function loadTsPathsMap(tsConfigs: TSConfig[]) { if (state.pathsMap) { return } const tsPathsMap = tsConfigs.reduce( (c, tsConfig) => loadPathsFromTSConfig(tsConfig, c), new Map() ) state.pathsMap = tsPathsMap state.pathsMapRegex = Array.from(tsPathsMap.entries()).map((entry) => [ ...entry, new RegExp(`^${entry[0].replace("*", ".*")}$`) ]) } function loadPathsFromTSConfig( tsConfig: TSConfig, tsPathsMap: Map ) { const { filePath, compilerOptions } = tsConfig const baseUrl = compilerOptions.baseUrl || "." const tsPaths = compilerOptions.paths || {} const tsConfigFolderPath = join(dirname(join(filePath)), baseUrl) for (const key in tsPaths) { tsPathsMap.set( key, tsPaths[key].map((p) => join(tsConfigFolderPath, p)) ) } return tsPathsMap } function attemptResolve({ specifier, dependency }: ResolverProps) { const { pathsMap, pathsMapRegex } = state if (pathsMap.has(specifier)) { return attemptResolveArray( specifier, specifier, pathsMap.get(specifier), dependency.resolveFrom ) } const relevantEntry = pathsMapRegex.find(([, , aliasRegex]) => aliasRegex.test(specifier) ) if (!!relevantEntry) { return attemptResolveArray( specifier, relevantEntry[0], relevantEntry[1], dependency.resolveFrom ) } return null } // TODO support resource loaders like 'url:@alias/my.svg' /** Attempt to resolve any path associated with the alias to a file or directory index */ function attemptResolveArray( from: string, alias: string, realPaths: TsPaths, parentFile: string ) { for (const option of realPaths) { const absoluteBaseFile = resolve( from.replace(trimStar(alias), trimStar(option)) ) const importExt = extname(absoluteBaseFile) if (importExt.length > 0 && relevantExtSet.has(importExt as any)) { return absoluteBaseFile } const parentExt = extname(parentFile) const checkingExts = [ parentExt, ...relevantExtList.filter((ext) => ext !== parentExt) ] const mod = findModule(absoluteBaseFile, checkingExts) if (mod !== null) { return mod } } return null } type TSConfig = { compilerOptions: CompilerOptions; filePath: string } async function getTsconfigCompilerOptions( props: ResolverProps & { tsconfigPath?: string }, tsConfigs: TSConfig[] = [], depth = 0 ): Promise { if (depth > 42) { throw new Error( "Something went wrong in loading tsconfig (depth > 42). Circular dependency?" ) } const { options, dependency, tsconfigPath } = props const tsconfigPathList = tsconfigPath ? [tsconfigPath] : ["tsconfig.json", "tsconfig.js"] const result = await loadConfig( options.inputFS, dependency.resolveFrom, tsconfigPathList, join(process.env.PLASMO_PROJECT_DIR, "lab") ) const compilerOptions = result?.config?.compilerOptions as CompilerOptions if (!compilerOptions) { return tsConfigs } const filePath = result.files[0].filePath const output = { compilerOptions, filePath } try { if (result.config.extends) { const extendsTsconfigPath = (await isReadable( resolve(result.config.extends) )) ? resolve(result.config.extends) : require.resolve(result.config.extends, { paths: [dependency.resolveFrom] }) return await getTsconfigCompilerOptions( { ...props, tsconfigPath: extendsTsconfigPath }, [output, ...tsConfigs], ++depth ) } } catch {} return [output, ...tsConfigs] } ================================================ FILE: core/parcel-resolver-post/src/index.ts ================================================ import { Resolver } from "@parcel/plugin" import { handleHacks } from "./handle-hacks" import { handleModuleExport } from "./handle-module-exports" import { handleTsPath } from "./handle-ts-path" export default new Resolver({ async resolve(props) { return ( (await handleHacks(props)) || (await handleTsPath(props)) || (await handleModuleExport(props)) || null ) } }) ================================================ FILE: core/parcel-resolver-post/src/shared.ts ================================================ import type { Resolver } from "@parcel/plugin" import type { ResolveResult } from "@parcel/types" export const relevantExtensionList = [ ".ts", ".tsx", ".svelte", ".vue", ".json", ".js", ".jsx" ] as const export const relevantExtensionSet = new Set(relevantExtensionList) type ResolveFx = ConstructorParameters[0]["resolve"] export type ResolverResult = ResolveResult export type ResolverProps = Parameters[0] ================================================ FILE: core/parcel-resolver-post/src/utils.ts ================================================ import { statSync } from "fs" import { resolve } from "path" import type { ResolveResult } from "@parcel/types" import { relevantExtensionList, type ResolverResult } from "./shared" const WEBPACK_IMPORT_REGEX = /\S+-loader\S*!\S+/g export function checkWebpackSpecificImportSyntax(specifier = "") { // Throw user friendly errors on special webpack loader syntax // ex. `imports-loader?$=jquery!./example.js` if (WEBPACK_IMPORT_REGEX.test(specifier)) { throw new Error( `The import path: ${specifier} is using webpack specific loader import syntax, which isn't supported by Parcel.` ) } } export function trimStar(str: string) { return trim(str, "*") } export function trim(str: string, trim: string) { if (str.endsWith(trim)) { str = str.substring(0, str.length - trim.length) } return str } const isFile = (filePath: string) => { try { return statSync(filePath).isFile() } catch { return false } } export function findModule( absoluteBaseFile: string, checkingExts = relevantExtensionList as readonly string[] ) { return checkingExts .flatMap((ext) => [ resolve(`${absoluteBaseFile}${ext}`), resolve(absoluteBaseFile, `index${ext}`) ]) .find(isFile) } /** * Look for source code file (crawl index) */ export const resolveSourceIndex = async ( absoluteBaseFile: string, checkingExts = relevantExtensionList as readonly string[], opts = {} as Partial ): Promise => { const filePath = findModule(absoluteBaseFile, checkingExts) if (!filePath) { return null } return { filePath, ...opts } } ================================================ FILE: core/parcel-resolver-post/tsconfig.json ================================================ { "extends": "@plasmo/config/ts/cli", "include": ["src/**/*.ts"], "exclude": ["dist", "node_modules"] } ================================================ FILE: core/parcel-runtime/.gitignore ================================================ node_modules dist/ ================================================ FILE: core/parcel-runtime/package.json ================================================ { "name": "@plasmohq/parcel-runtime", "version": "0.25.3", "description": "Plasmo Parcel Runtime", "files": [ "dist" ], "main": "dist/index.js", "scripts": { "prepublishOnly": "pnpm build", "build": "tsup --minify --clean", "dev": "tsup --sourcemap --watch" }, "author": "Plasmo Corp. ", "homepage": "https://docs.plasmo.com/", "engines": { "parcel": ">= 2.7.0" }, "license": "MIT", "repository": { "type": "git", "url": "https://github.com/PlasmoHQ/plasmo.git" }, "devDependencies": { "@plasmo/config": "workspace:*", "@plasmo/framework-shared": "workspace:*", "@plasmo/utils": "workspace:*", "@plasmohq/persistent": "workspace:*", "@types/chrome": "0.0.312", "tsup": "8.4.0" }, "dependencies": { "@types/trusted-types": "2.0.7", "@parcel/core": "2.9.3", "@parcel/plugin": "2.9.3", "react-refresh": "0.16.0" } } ================================================ FILE: core/parcel-runtime/src/index.ts ================================================ import fs from "fs" import path, { basename, dirname, join } from "path" import { Runtime } from "@parcel/plugin" import { vLog } from "@plasmo/utils/logging" import { plasmoRuntimeList, type PlasmoRuntime, type RuntimeData } from "./types" const devRuntimeMap = plasmoRuntimeList.reduce( (accumulatedRuntimeMap, currentRuntime) => ({ ...accumulatedRuntimeMap, [currentRuntime]: fs.readFileSync( path.join(__dirname, `./runtimes/${currentRuntime}.js`), "utf8" ) }), {} as Record ) export default new Runtime({ async loadConfig({ config }) { const pkg = await config .getConfigFrom<{ dependencies: Record devDependencies: Record peerDependencies: Record }>( join(process.env.PLASMO_PROJECT_DIR, "lab"), // parcel only look up ["package.json"], { exclude: true } ) .then((cfg) => cfg?.contents) // npm workspaces mono repo's do not have dependencies or devDependencies (they are defined in a parent directory), fallback to peerDependencies const hasReact = !!pkg?.dependencies?.react || !!pkg?.devDependencies?.react || !!pkg?.peerDependencies?.react return { hasReact } }, apply({ bundle, options, config, bundleGraph }) { if (bundle.name === "manifest.json") { const asset = bundle.getMainEntry() if (asset?.meta.webextEntry !== true) { return } // Hack to bust packager cache when any descendants update const descendants = [] bundleGraph.traverseBundles((b) => { descendants.push(b.id) }, bundle) return { filePath: __filename, code: JSON.stringify(descendants), isEntry: true } } if ( bundle.type !== "js" || !options.hmrOptions || bundle.env.isLibrary || bundle.env.isWorklet() || options.mode !== "development" || bundle.env.sourceType === "script" ) { return } const entryFilePath = bundle.getMainEntry()?.filePath if (!entryFilePath) { return } const isPlasmo = entryFilePath.includes(".plasmo") const isBackground = entryFilePath.startsWith( join(process.env.PLASMO_SRC_DIR, "background") ) || entryFilePath.endsWith(join("static", "background", "index.ts")) || entryFilePath.endsWith("plasmo-default-background.ts") const isPlasmoSrc = isPlasmo || isBackground || entryFilePath.startsWith(join(process.env.PLASMO_SRC_DIR, "content")) if (!isPlasmoSrc) { return } const isReact = config.hasReact && entryFilePath.endsWith(".tsx") const entryBasename = basename(entryFilePath).split(".")[0] const isContentScript = dirname(entryFilePath).endsWith("contents") || entryBasename === "content" if ( process.env.__PLASMO_FRAMEWORK_INTERNAL_NO_CS_RELOAD === "true" && isContentScript ) { return } // TODO: add production runtimes const devRuntime: PlasmoRuntime = isBackground ? "background-service-runtime" : isContentScript ? "script-runtime" : "page-runtime" vLog( "@plasmohq/parcel-runtime", "Injecting <<", devRuntime, ">> for", bundle.displayName, bundle.id, entryFilePath ) const runtimeData: RuntimeData = { isContentScript, isBackground, isReact, runtimes: [devRuntime], ...options.hmrOptions, entryFilePath: String.raw`${entryFilePath}`, bundleId: bundle.id, envHash: bundle.env.id, verbose: process.env.VERBOSE, secure: !!(options.serveOptions && options.serveOptions.https), serverPort: options.serveOptions && options.serveOptions.port } const code = devRuntimeMap[devRuntime].replace( `__plasmo_runtime_data__`, // double quote to escape JSON.stringify(runtimeData) ) return { filePath: __filename, code, isEntry: true, env: { sourceType: "module" } } } }) ================================================ FILE: core/parcel-runtime/src/runtimes/background-service-runtime.ts ================================================ /** * This runtime is injected into the background service worker */ import { BuildSocketEvent } from "@plasmo/framework-shared/build-socket/event" import { vLog } from "@plasmo/utils/logging" import { keepAlive } from "@plasmohq/persistent/background" import type { BackgroundMessage } from "../types" import { extCtx, PAGE_PORT_PREFIX, runtimeData, SCRIPT_PORT_PREFIX } from "../utils/0-patch-module" import { pollingDevServer } from "../utils/bgsw" import { isDependencyOfBundle } from "../utils/hmr-check" import { injectBuilderSocket, injectHmrSocket } from "../utils/inject-socket" const parent = module.bundle.parent const state = { buildReady: false, bgChanged: false, csChanged: false, pageChanged: false, scriptPorts: new Set(), pagePorts: new Set() } async function consolidateUpdate(forced = false) { if (forced || (state.buildReady && state.pageChanged)) { vLog("BGSW Runtime - reloading Page") for (const port of state.pagePorts) { // Mark the active tab for specific reload port.postMessage(null) } } if (forced || (state.buildReady && (state.bgChanged || state.csChanged))) { vLog("BGSW Runtime - reloading CS") const activeTabList = await extCtx?.tabs.query({ active: true }) for (const port of state.scriptPorts) { const isActive = activeTabList.some((t) => t.id === port.sender.tab?.id) // Mark the active tab for specific reload port.postMessage({ __plasmo_cs_active_tab__: isActive } as BackgroundMessage) } // Required to actually reload the CS extCtx.runtime.reload() } } if (!parent || !parent.isParcelRequire) { keepAlive() const hmrSocket = injectHmrSocket(async (updatedAssets) => { vLog("BGSW Runtime - On HMR Update") state.bgChanged ||= updatedAssets .filter((asset) => asset.envHash === runtimeData.envHash) .some((asset) => isDependencyOfBundle(module.bundle, asset.id)) const manifestChange = updatedAssets.find((e) => e.type === "json") if (!!manifestChange) { const changedIdSet = new Set(updatedAssets.map((e) => e.id)) const deps = Object.values(manifestChange.depsByBundle) .map((o) => Object.values(o)) .flat() state.bgChanged ||= deps.every((dep) => changedIdSet.has(dep)) } consolidateUpdate() }) hmrSocket.addEventListener("open", () => { // Send a ping event to the HMR server every 24 seconds to keep the connection alive const interval = setInterval(() => hmrSocket.send("ping"), 24_000) hmrSocket.addEventListener("close", () => clearInterval(interval)) }) hmrSocket.addEventListener("close", async () => { await pollingDevServer() consolidateUpdate(true) }) } injectBuilderSocket(async (event) => { vLog("BGSW Runtime - On Build Repackaged") // maybe we should wait for a bit until we determine if the build is truly ready switch (event.type) { case BuildSocketEvent.BuildReady: { state.buildReady ||= true consolidateUpdate() break } case BuildSocketEvent.CsChanged: { state.csChanged ||= true consolidateUpdate() break } } }) extCtx.runtime.onConnect.addListener(function (port) { const isPagePort = port.name.startsWith(PAGE_PORT_PREFIX) const isScriptPort = port.name.startsWith(SCRIPT_PORT_PREFIX) if (isPagePort || isScriptPort) { const portSet = isPagePort ? state.pagePorts : state.scriptPorts portSet.add(port) port.onDisconnect.addListener(() => { portSet.delete(port) }) port.onMessage.addListener(function (msg: BackgroundMessage) { vLog("BGSW Runtime - On source changed", msg) if (msg.__plasmo_cs_changed__) { state.csChanged ||= true } if (msg.__plasmo_page_changed__) { state.pageChanged ||= true } consolidateUpdate() }) } }) extCtx.runtime.onMessage.addListener(function runtimeMessageHandler( msg: BackgroundMessage ) { if (msg.__plasmo_full_reload__) { vLog("BGSW Runtime - On top-level code changed") consolidateUpdate() } return true }) ================================================ FILE: core/parcel-runtime/src/runtimes/page-runtime.ts ================================================ import { vLog } from "@plasmo/utils/logging" import type { BackgroundMessage } from "../types" import { extCtx, PAGE_PORT_PREFIX, runtimeData, triggerReload } from "../utils/0-patch-module" import { hmrAcceptCheck, hmrState, isDependencyOfBundle, resetHmrState } from "../utils/hmr-check" import { hmrAccept, hmrApplyUpdates, hmrDispose } from "../utils/hmr-utils" import { injectHmrSocket } from "../utils/inject-socket" import { injectReactRefresh } from "../utils/react-refresh" const PORT_NAME = `${PAGE_PORT_PREFIX}${module.id}__` let pagePort: chrome.runtime.Port const parent = module.bundle.parent if (!parent || !parent.isParcelRequire) { try { pagePort = extCtx?.runtime.connect({ name: PORT_NAME }) pagePort.onDisconnect.addListener(() => { triggerReload() }) // TODO: should prob use canHmr instead of isReact if (!runtimeData.isReact) { pagePort.onMessage.addListener(() => { // bgsw reloaded, all context gone triggerReload() }) } } catch (error) { vLog(error) } injectHmrSocket(async (updatedAssets) => { vLog("Page runtime - On HMR Update") if (runtimeData.isReact) { resetHmrState() // Is an extension page, can try to hot reload const assets = updatedAssets.filter( (asset) => asset.envHash === runtimeData.envHash ) const canHmr = assets.some( (asset) => asset.type === "css" || (asset.type === "js" && hmrAcceptCheck(module.bundle.root, asset.id, asset.depsByBundle)) ) if (canHmr) { try { await hmrApplyUpdates(assets) // Dispose all old assets. const disposedAssets = {} as Record for (const [asset, id] of hmrState.assetsToDispose) { if (!disposedAssets[id]) { hmrDispose(asset, id) disposedAssets[id] = true } } const acceptedAssets = {} as Record for (let i = 0; i < hmrState.assetsToAccept.length; i++) { const [asset, id] = hmrState.assetsToAccept[i] if (!acceptedAssets[id]) { hmrAccept(asset, id) acceptedAssets[id] = true } } } catch (e) { if (runtimeData.verbose === "true") { console.trace(e) alert(JSON.stringify(e)) } await triggerReload(true) } } } else { const sourceChanged = updatedAssets .filter((asset) => asset.envHash === runtimeData.envHash) .some((asset) => isDependencyOfBundle(module.bundle, asset.id)) vLog(`Page runtime -`, { sourceChanged }) if (sourceChanged) { // @ts-ignore // if (module.hot) { // // @ts-ignore // module.hot.accept() // } pagePort.postMessage({ __plasmo_page_changed__: true } as BackgroundMessage) } } }) } if (runtimeData.isReact) { vLog("Injecting react refresh") injectReactRefresh() } ================================================ FILE: core/parcel-runtime/src/runtimes/script-runtime.ts ================================================ /** * Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors * MIT License * * Port refreshing code is based on https://github.com/crxjs/chrome-extension-tools/blob/963e41149cdf327eaa338c643c58909e30d69651/packages/vite-plugin/src/client/es/hmr-client-worker.ts#L88 * MIT License * Copyright (c) 2019 jacksteamdev */ import { vLog } from "@plasmo/utils/logging" import type { BackgroundMessage } from "../types" import { extCtx, runtimeData, SCRIPT_PORT_PREFIX } from "../utils/0-patch-module" import { isDependencyOfBundle } from "../utils/hmr-check" import { injectHmrSocket } from "../utils/inject-socket" import { createLoadingIndicator } from "../utils/loading-indicator" const PORT_NAME = `${SCRIPT_PORT_PREFIX}${module.id}__` let scriptPort: chrome.runtime.Port let isActiveTab = false const loadingIndicator = createLoadingIndicator() async function consolidateUpdate() { vLog("Script Runtime - reloading") if (isActiveTab) { globalThis.location?.reload?.() } else { loadingIndicator.show({ reloadButton: true }) } } function reloadPort() { scriptPort?.disconnect() // Potentially, if MAIN world, we use the external connection instead (?) scriptPort = extCtx?.runtime.connect({ name: PORT_NAME }) scriptPort.onDisconnect.addListener(() => { consolidateUpdate() }) scriptPort.onMessage.addListener((msg: BackgroundMessage) => { // bgsw reloaded, all context gone if (msg.__plasmo_cs_reload__) { consolidateUpdate() } if (msg.__plasmo_cs_active_tab__) { isActiveTab = true } return }) } function setupPort() { if (!extCtx?.runtime) { return } try { reloadPort() setInterval(reloadPort, 24_000) } catch { return } } setupPort() injectHmrSocket(async (updatedAssets) => { vLog("Script runtime - on updated assets") const isChanged = updatedAssets .filter((asset) => asset.envHash === runtimeData.envHash) .some((asset) => isDependencyOfBundle(module.bundle, asset.id)) if (isChanged) { loadingIndicator.show() if (extCtx?.runtime) { scriptPort.postMessage({ __plasmo_cs_changed__: true } as BackgroundMessage) } else { setTimeout(() => { consolidateUpdate() }, 4_700) } } }) ================================================ FILE: core/parcel-runtime/src/types.ts ================================================ export const plasmoRuntimeList = [ "page-runtime", "script-runtime", "background-service-runtime" ] as const export type PlasmoRuntime = (typeof plasmoRuntimeList)[number] declare global { const __parcel__import__: Function const __parcel__importScripts__: Function interface Window { $RefreshReg$: any $RefreshSig$: any } interface NodeModule { bundle: ParcelBundle } } export type ExtensionApi = typeof globalThis.chrome interface ParcelModule { hot: { data: unknown accept(cb: (arg0: (...args: Array) => any) => void): void dispose(cb: (arg0: unknown) => void): void _acceptCallbacks: Array<(arg0: (...args: Array) => any) => any> _disposeCallbacks: Array<(arg0: unknown) => void> } } export interface ParcelBundle { (arg0: string): unknown cache: Record hotData: Record Module: any parent: ParcelBundle | null | undefined isParcelRequire: true modules: Record< string, [(...args: Array) => any, Record] > HMR_BUNDLE_ID: string root: ParcelBundle } export type ParcelAsset = [ParcelBundle, string] export type RuntimeData = { isContentScript: boolean isBackground: boolean isReact: boolean runtimes: PlasmoRuntime[] host?: string port?: number secure: boolean serverPort?: number verbose: "true" | "false" entryFilePath: string bundleId: string envHash: string } export type HmrAsset = { id: string url: string type: string output: string envHash: string outputFormat: string depsByBundle: Record> } export type HmrMessage = | { type: "update" assets: Array } | { type: "error" diagnostics: { ansi: Array html: Array<{ codeframe: string }> } } export type BackgroundMessage = { __plasmo_full_reload__?: boolean __plasmo_build_updated__?: boolean __plasmo_cs_ping__?: boolean __plasmo_page_changed__?: boolean __plasmo_cs_reload__?: boolean __plasmo_cs_changed__?: boolean __plasmo_cs_active_tab__?: boolean } ================================================ FILE: core/parcel-runtime/src/utils/0-patch-module.ts ================================================ import { vLog } from "@plasmo/utils/logging" import type { BackgroundMessage, ExtensionApi, RuntimeData } from "../types" // @ts-ignore export const runtimeData = __plasmo_runtime_data__ as RuntimeData module.bundle.HMR_BUNDLE_ID = runtimeData.bundleId globalThis.process = { argv: [], env: { VERBOSE: runtimeData.verbose } } as any const OldModule = module.bundle.Module function Module(moduleName: string) { OldModule.call(this, moduleName) this.hot = { data: module.bundle.hotData[moduleName], _acceptCallbacks: [], _disposeCallbacks: [], accept: function (fn) { this._acceptCallbacks.push(fn || function () {}) }, dispose: function (fn) { this._disposeCallbacks.push(fn) } } module.bundle.hotData[moduleName] = undefined } module.bundle.Module = Module module.bundle.hotData = {} export const extCtx: ExtensionApi = globalThis.browser || globalThis.chrome || null export async function triggerReload(fullReload = false) { if (fullReload) { vLog("Triggering full reload") extCtx.runtime.sendMessage({ __plasmo_full_reload__: true }) } else { globalThis.location?.reload?.() } } export function getHostname() { if (!runtimeData.host || runtimeData.host === "0.0.0.0") { return location.protocol.indexOf("http") === 0 ? location.hostname : "localhost" } return runtimeData.host } export function getSocketHostname() { if (!runtimeData.host || runtimeData.host === "0.0.0.0") { return "localhost"; } return runtimeData.host } export function getPort() { return runtimeData.port || location.port } export const PAGE_PORT_PREFIX = `__plasmo_runtime_page_` export const SCRIPT_PORT_PREFIX = `__plasmo_runtime_script_` ================================================ FILE: core/parcel-runtime/src/utils/bgsw.ts ================================================ import { extCtx, getHostname, getPort, runtimeData } from "./0-patch-module" declare const globalThis: ServiceWorkerGlobalScope const devServer = `${ runtimeData.secure ? "https" : "http" }://${getHostname()}:${getPort()}/` export async function pollingDevServer(delay = 1470) { while (true) { try { await fetch(devServer) break } catch (e) { await new Promise((resolve) => setTimeout(resolve, delay)) } } } if (extCtx.runtime.getManifest().manifest_version === 3) { const proxyLoc = extCtx.runtime.getURL("/__plasmo_hmr_proxy__?url=") globalThis.addEventListener("fetch", function (evt) { const reqUrl = evt.request.url if (reqUrl.startsWith(proxyLoc)) { const url = new URL(decodeURIComponent(reqUrl.slice(proxyLoc.length))) if ( url.hostname === runtimeData.host && url.port === `${runtimeData.port}` ) { url.searchParams.set("t", Date.now().toString()) evt.respondWith( fetch(url).then( (res) => new Response(res.body, { headers: { "Content-Type": res.headers.get("Content-Type") ?? "text/javascript" } }) ) ) } else { evt.respondWith( new Response("Plasmo HMR", { status: 200, statusText: "Testing" }) ) } } }) } ================================================ FILE: core/parcel-runtime/src/utils/hmr-check.ts ================================================ import type { ParcelAsset, ParcelBundle } from "../types" export const hmrState = { checkedAssets: {} as Record, assetsToDispose: [] as Array, assetsToAccept: [] as Array } export const resetHmrState = () => { hmrState.checkedAssets = {} hmrState.assetsToDispose = [] hmrState.assetsToAccept = [] } export function getParents( bundle: ParcelBundle, id: string ): Array { const { modules } = bundle if (!modules) { return [] } let parents = [] let modId: string, depId: string, dep: string for (modId in modules) { for (depId in modules[modId][1]) { dep = modules[modId][1][depId] if (dep === id || (Array.isArray(dep) && dep[dep.length - 1] === id)) { parents.push([bundle, modId]) } } } if (bundle.parent) { parents = parents.concat(getParents(bundle.parent, id)) } return parents } export function hmrAcceptCheck( bundle: ParcelBundle, id: string, depsByBundle: Record> ) { if (hmrAcceptCheckOne(bundle, id, depsByBundle)) { return true } // Traverse parents breadth first. All possible ancestries must accept the HMR update, or we'll reload. const parents = getParents(module.bundle.root, id) let accepted = false while (parents.length > 0) { const [parentAsset, parentId] = parents.shift() const canHmr = hmrAcceptCheckOne(parentAsset, parentId, null) if (canHmr) { // If this parent accepts, stop traversing upward, but still consider siblings. accepted = true } else { // Otherwise, queue the parents in the next level upward. const p = getParents(module.bundle.root, parentId) if (p.length === 0) { // If there are no parents, then we've reached an entry without accepting. Reload. accepted = false break } parents.push(...p) } } return accepted } function hmrAcceptCheckOne( bundle: ParcelBundle, id: string, depsByBundle: Record> ) { const { modules } = bundle if (!modules) { return false } if (depsByBundle && !depsByBundle[bundle.HMR_BUNDLE_ID]) { // If we reached the root bundle without finding where the asset should go, // there's nothing to do. Mark as "accepted" so we don't reload the page. if (!bundle.parent) { return true } return hmrAcceptCheck(bundle.parent, id, depsByBundle) } if (hmrState.checkedAssets[id]) { return true } hmrState.checkedAssets[id] = true const cached = bundle.cache[id] hmrState.assetsToDispose.push([bundle, id]) if (!cached || (cached.hot && cached.hot._acceptCallbacks.length)) { hmrState.assetsToAccept.push([bundle, id]) return true } return false } export function isDependencyOfBundle(bundle: ParcelBundle, id: string) { const { modules } = bundle if (!modules) { return false } return !!modules[id] } ================================================ FILE: core/parcel-runtime/src/utils/hmr-utils.ts ================================================ import type { HmrAsset, ParcelAsset, ParcelBundle } from "../types" import { extCtx, getHostname, getPort } from "./0-patch-module" import { getParents, hmrState } from "./hmr-check" export function hmrDownload(asset: HmrAsset) { if (asset.type === "js") { if (typeof document !== "undefined") { return new Promise((resolve, reject) => { const script = document.createElement("script") script.src = `${asset.url}?t=${Date.now()}` if (asset.outputFormat === "esmodule") { script.type = "module" } script.addEventListener("load", () => resolve(script)) script.addEventListener("error", () => reject(new Error(`Failed to download asset: ${asset.id}`)) ) document.head?.appendChild(script) }) } } } export async function hmrApplyUpdates(assets: Array) { global.parcelHotUpdate = Object.create(null) // If sourceURL comments aren't supported in eval, we need to load // the update from the dev server over HTTP so that stack traces // are correct in errors/logs. This is much slower than eval, so // we only do it if needed (currently just Safari). // https://bugs.webkit.org/show_bug.cgi?id=137297 // This path is also taken if a CSP disallows eval. assets.forEach((asset) => { asset.url = extCtx.runtime.getURL( "/__plasmo_hmr_proxy__?url=" + encodeURIComponent(`${asset.url}?t=${Date.now()}`) ) }) const scriptsToRemove = await Promise.all(assets.map(hmrDownload)) try { assets.forEach(function (asset) { hmrApply(module.bundle.root, asset) }) } finally { delete global.parcelHotUpdate if (scriptsToRemove) { scriptsToRemove.forEach((script) => { if (script) { document.head?.removeChild(script) } }) } } } function updateLink(link: Element) { const newLink = link.cloneNode() as HTMLLinkElement newLink.onload = function () { if (link.parentNode !== null) { link.parentNode.removeChild(link) } } newLink.setAttribute( "href", link.getAttribute("href").split("?")[0] + "?" + Date.now() ) link.parentNode.insertBefore(newLink, link.nextSibling) } let cssTimeout = null function reloadCSS() { if (cssTimeout) { return } cssTimeout = setTimeout(function () { const links = document.querySelectorAll('link[rel="stylesheet"]') for (var i = 0; i < links.length; i++) { const href = links[i].getAttribute("href") const hostname = getHostname() const servedFromHmrServer = hostname === "localhost" ? new RegExp( "^(https?:\\/\\/(0.0.0.0|127.0.0.1)|localhost):" + getPort() ).test(href) : href.indexOf(hostname + ":" + getPort()) const absolute = /^https?:\/\//i.test(href) && href.indexOf(location.origin) !== 0 && !servedFromHmrServer if (!absolute) { updateLink(links[i]) } } cssTimeout = null }, 47) } function hmrApply(bundle: ParcelBundle, asset: HmrAsset) { const { modules } = bundle if (!modules) { return } if (asset.type === "css") { reloadCSS() } else if (asset.type === "js") { const deps = asset.depsByBundle[bundle.HMR_BUNDLE_ID] if (deps) { if (modules[asset.id]) { // Remove dependencies that are removed and will become orphaned. // This is necessary so that if the asset is added back again, the cache is gone, and we prevent a full page reload. // debugger let oldDeps = modules[asset.id][1] for (let dep in oldDeps) { if (!deps[dep] || deps[dep] !== oldDeps[dep]) { let id = oldDeps[dep] let parents = getParents(module.bundle.root, id) if (parents.length === 1) { hmrDelete(module.bundle.root, id) } } } } const fn = global.parcelHotUpdate[asset.id] modules[asset.id] = [fn, deps] } else if (bundle.parent) { hmrApply(bundle.parent, asset) } } } function hmrDelete(bundle: ParcelBundle, id: string) { let modules = bundle.modules if (!modules) { return } if (modules[id]) { // Collect dependencies that will become orphaned when this module is deleted. let deps = modules[id][1] let orphans = [] for (let dep in deps) { let parents = getParents(module.bundle.root, deps[dep]) if (parents.length === 1) { orphans.push(deps[dep]) } } // Delete the module. This must be done before deleting dependencies in case of circular dependencies. delete modules[id] delete bundle.cache[id] // Now delete the orphans. orphans.forEach((id) => { hmrDelete(module.bundle.root, id) }) } else if (bundle.parent) { hmrDelete(bundle.parent, id) } } export function hmrDispose(bundle: ParcelBundle, id: string) { const cached = bundle.cache[id] bundle.hotData[id] = {} if (cached && cached.hot) { cached.hot.data = bundle.hotData[id] } if (cached && cached.hot && cached.hot._disposeCallbacks.length) { cached.hot._disposeCallbacks.forEach(function (cb) { cb(bundle.hotData[id]) }) } delete bundle.cache[id] } export function hmrAccept(bundle: ParcelBundle, id: string) { // Execute the module bundle(id) const cached = bundle.cache[id] if (cached && cached.hot && cached.hot._acceptCallbacks.length) { const parents = getParents(module.bundle.root, id) cached.hot._acceptCallbacks.forEach(function (cb) { const assetsToAlsoAccept: ParcelAsset[] = cb(() => parents) if (assetsToAlsoAccept && assetsToAlsoAccept.length) { assetsToAlsoAccept.forEach(([extraAsset, extraAssetId]) => { hmrDispose(extraAsset, extraAssetId) }) hmrState.assetsToAccept.push.apply( hmrState.assetsToAccept, assetsToAlsoAccept ) } }) } } ================================================ FILE: core/parcel-runtime/src/utils/inject-socket.ts ================================================ import { type BuildSocketEvent } from "@plasmo/framework-shared/build-socket/event" import { eLog, iLog, wLog } from "@plasmo/utils/logging" import type { HmrAsset, HmrMessage } from "../types" import { getSocketHostname, getPort, runtimeData } from "./0-patch-module" function getBaseSocketUri(port = getPort()) { const hostname = getSocketHostname() const protocol = runtimeData.secure || (location.protocol === "https:" && !/localhost|127.0.0.1|0.0.0.0/.test(hostname)) ? "wss" : "ws" return `${protocol}://${hostname}:${port}/` } function wsErrorHandler(e: ErrorEvent) { if (typeof e.message === "string") { eLog("[plasmo/parcel-runtime]: " + e.message) } } export function injectBuilderSocket( onData?: (data: { type: BuildSocketEvent }) => Promise ) { if (typeof globalThis.WebSocket === "undefined") { return } const builderWs = new WebSocket(getBaseSocketUri(Number(getPort()) + 1)) builderWs.addEventListener("message", async function (event) { const data = JSON.parse(event.data) await onData(data) }) builderWs.addEventListener("error", wsErrorHandler) return builderWs } export function injectHmrSocket( onUpdate: (assets: Array) => Promise ) { if (typeof globalThis.WebSocket === "undefined") { return } const hmrWs = new WebSocket(getBaseSocketUri()) hmrWs.addEventListener("message", async function (event) { const data = JSON.parse(event.data) as HmrMessage if (data.type === "update") { await onUpdate(data.assets) } if (data.type === "error") { // Log parcel errors to console for (const ansiDiagnostic of data.diagnostics.ansi) { const stack = ansiDiagnostic.codeframe || ansiDiagnostic.stack wLog( "[plasmo/parcel-runtime]: " + ansiDiagnostic.message + "\n" + stack + "\n\n" + ansiDiagnostic.hints.join("\n") ) } } }) hmrWs.addEventListener("error", wsErrorHandler) hmrWs.addEventListener("open", () => { iLog( `[plasmo/parcel-runtime]: Connected to HMR server for ${runtimeData.entryFilePath}` ) }) hmrWs.addEventListener("close", () => { wLog( `[plasmo/parcel-runtime]: Connection to the HMR server is closed for ${runtimeData.entryFilePath}` ) }) return hmrWs } ================================================ FILE: core/parcel-runtime/src/utils/loading-indicator.ts ================================================ /** * Copyright (c) Plasmo Corp, foss@plasmo.com, MIT Licensed * ************************************************** * SVG Generated by SVG Artista on 2/8/2023, 4:53:34PM * MIT license (https://opensource.org/licenses/MIT) * W. https://svgartista.net ************************************************** */ const LOADING_ID = "__plasmo-loading__" function createTrustedPolicy() { const trustedTypes = globalThis.window?.trustedTypes if (typeof trustedTypes === "undefined") { return undefined } const trustedTypeLists = ( document.querySelector('meta[name="trusted-types"]') as HTMLMetaElement )?.content?.split(" ") const trustedKey = trustedTypeLists ? trustedTypeLists[trustedTypeLists?.length - 1].replace(/;/g, "") : undefined // Function to update the CSP to allow the new trusted type policy or use existing policy const trustedPolicy = typeof trustedTypes !== "undefined" ? trustedTypes.createPolicy(trustedKey || `trusted-html-${LOADING_ID}`, { createHTML: (str) => str }) : undefined return trustedPolicy } const trustedPolicy = createTrustedPolicy() function getLoader() { return document.getElementById(LOADING_ID) } function isLoaderUnavailable() { return !getLoader() } function createLoader() { const loadingEl = document.createElement("div") loadingEl.id = LOADING_ID const htmlText = ` ` loadingEl.innerHTML = trustedPolicy ? (trustedPolicy.createHTML(htmlText) as any) : htmlText loadingEl.style.pointerEvents = "none" loadingEl.style.position = "fixed" loadingEl.style.bottom = "14.7px" loadingEl.style.right = "14.7px" loadingEl.style.fontFamily = "sans-serif" loadingEl.style.display = "flex" loadingEl.style.justifyContent = "center" loadingEl.style.alignItems = "center" loadingEl.style.padding = "14.7px" loadingEl.style.gap = "14.7px" loadingEl.style.borderRadius = "4.7px" loadingEl.style.zIndex = "2147483647" loadingEl.style.opacity = "0" loadingEl.style.transition = "all 0.47s ease-in-out" return loadingEl } function injectLoaderEl(loaderEl: HTMLElement) { return new Promise((resolve) => { if (!document.documentElement) { globalThis.addEventListener("DOMContentLoaded", () => { if (isLoaderUnavailable()) { document.documentElement.appendChild(loaderEl) } resolve() }) } else { if (isLoaderUnavailable()) { document.documentElement.appendChild(loaderEl) resolve() } resolve() } }) } export const createLoadingIndicator = () => { let injectPromise: Promise if (isLoaderUnavailable()) { const initialLoaderEl = createLoader() injectPromise = injectLoaderEl(initialLoaderEl) } return { show: async ({ reloadButton = false } = {}) => { await injectPromise const loadingEl = getLoader() loadingEl.style.opacity = "1" if (!reloadButton) { return } loadingEl.onclick = (e) => { e.stopPropagation() globalThis.location.reload() } loadingEl.querySelector("span").classList.remove("hidden") loadingEl.style.cursor = "pointer" loadingEl.style.pointerEvents = "all" }, hide: async () => { await injectPromise const loadingEl = getLoader() loadingEl.style.opacity = "0" } } } ================================================ FILE: core/parcel-runtime/src/utils/react-refresh.ts ================================================ import refreshRuntime from "react-refresh/runtime" export async function injectReactRefresh() { refreshRuntime.injectIntoGlobalHook(window) window.$RefreshReg$ = function () {} window.$RefreshSig$ = function () { return function (type) { return type } } } ================================================ FILE: core/parcel-runtime/tsconfig.json ================================================ { "extends": "@plasmo/config/ts/framework", "include": ["src/**/*.ts"], "exclude": ["dist", "node_modules"], "compilerOptions": { "lib": ["WebWorker", "dom"] } } ================================================ FILE: core/parcel-runtime/tsup.config.ts ================================================ import { defineConfig } from "tsup" export default defineConfig({ entry: ["src/index.ts", "src/runtimes/*"] }) ================================================ FILE: core/parcel-transformer-inject-env/.gitignore ================================================ node_modules dist/ ================================================ FILE: core/parcel-transformer-inject-env/package.json ================================================ { "name": "@plasmohq/parcel-transformer-inject-env", "version": "0.2.12", "description": "Plasmo Parcel Transformer to inject environment variables", "files": [ "dist" ], "main": "dist/index.js", "scripts": { "prepublishOnly": "pnpm build", "build": "tsup src/index.ts --minify --clean", "dev": "tsup src/index.ts --sourcemap --watch" }, "author": "Plasmo Corp. ", "homepage": "https://docs.plasmo.com/", "engines": { "parcel": ">= 2.7.0" }, "license": "MIT", "repository": { "type": "git", "url": "https://github.com/PlasmoHQ/plasmo.git" }, "devDependencies": { "@plasmo/config": "workspace:*", "@plasmo/utils": "workspace:*", "tsup": "8.4.0" }, "dependencies": { "@parcel/core": "2.9.3", "@parcel/plugin": "2.9.3", "@parcel/types": "2.9.3" } } ================================================ FILE: core/parcel-transformer-inject-env/src/index.ts ================================================ import { Transformer } from "@parcel/plugin" import { injectEnv } from "@plasmo/utils/env" export default new Transformer({ async transform({ asset, options }) { const code = await asset.getCode() const injectedCode = injectEnv(code, options.env) asset.setCode(injectedCode) return [asset] } }) ================================================ FILE: core/parcel-transformer-inject-env/tsconfig.json ================================================ { "extends": "@plasmo/config/ts/cli", "include": ["src/**/*.ts"], "exclude": ["dist", "node_modules"] } ================================================ FILE: core/parcel-transformer-inline-css/.gitignore ================================================ node_modules dist/ ================================================ FILE: core/parcel-transformer-inline-css/package.json ================================================ { "name": "@plasmohq/parcel-transformer-inline-css", "version": "0.3.12", "description": "Plasmo Parcel Transformer for inline CSS", "files": [ "dist" ], "main": "dist/index.js", "scripts": { "prepublishOnly": "pnpm build", "build": "tsup src/index.ts --minify --clean", "dev": "tsup src/index.ts --sourcemap --watch" }, "author": "Plasmo Corp. ", "homepage": "https://docs.plasmo.com/", "engines": { "parcel": ">= 2.7.0" }, "license": "MIT", "repository": { "type": "git", "url": "https://github.com/PlasmoHQ/plasmo.git" }, "devDependencies": { "@plasmo/config": "workspace:*", "tsup": "8.4.0" }, "dependencies": { "@parcel/core": "2.9.3", "@parcel/plugin": "2.9.3", "@parcel/utils": "2.9.3", "browserslist": "4.24.4", "lightningcss": "1.21.8" } } ================================================ FILE: core/parcel-transformer-inline-css/src/get-tagets.ts ================================================ /** * Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors * MIT License * * Based on: https://github.com/parcel-bundler/parcel/blob/7023c08b7e99a9b8fd3c04995e4ef7ca92dee5c1/packages/transformers/css/src/CSSTransformer.js * MIT License */ import browserslist from "browserslist" import { browserslistToTargets } from "lightningcss" let cache = new Map() export function getTargets(browsers) { if (browsers == null) { return undefined } let cached = cache.get(browsers) if (cached != null) { return cached } let targets = browserslistToTargets(browserslist(browsers)) cache.set(browsers, targets) return targets } ================================================ FILE: core/parcel-transformer-inline-css/src/index.ts ================================================ /** * Copyright (c) 2024 Plasmo Corp. (https://www.plasmo.com) and contributors * MIT License * * Based on: https://github.com/parcel-bundler/parcel/blob/7023c08b7e99a9b8fd3c04995e4ef7ca92dee5c1/packages/transformers/css/src/CSSTransformer.js * MIT License */ import { relative } from "path" import { Transformer } from "@parcel/plugin" import { remapSourceLocation } from "@parcel/utils" import { transform } from "lightningcss" import { getTargets } from "./get-tagets" export default new Transformer({ async transform({ asset, options }) { // Normalize the asset's environment so that properties that only affect JS don't cause CSS to be duplicated. // For example, with ESModule and CommonJS targets, only a single shared CSS bundle should be produced. const [code, originalMap] = await Promise.all([ asset.getBuffer(), asset.getMap() ]) const targets = getTargets(asset.env.engines.browsers) const res = transform({ filename: relative(options.projectRoot, asset.filePath), targets, code, cssModules: true, analyzeDependencies: asset.meta.hasDependencies !== false, sourceMap: !!asset.env.sourceMap }) asset.setBuffer(Buffer.from(res.code)) if (res.dependencies) { for (let dep of res.dependencies) { const loc = !originalMap ? dep.loc : remapSourceLocation(dep.loc, originalMap) if (dep.type === "import" && !res.exports) { asset.addDependency({ specifier: dep.url, specifierType: "url", loc, meta: { // For the glob resolver to distinguish between `@import` and other URL dependencies. isCSSImport: true, media: dep.media } }) } else if (dep.type === "url") { asset.addURLDependency(dep.url, { loc, meta: { placeholder: dep.placeholder } }) } } } return [asset] } }) ================================================ FILE: core/parcel-transformer-inline-css/tsconfig.json ================================================ { "extends": "@plasmo/config/ts/cli", "include": ["src/**/*.ts"], "exclude": ["dist", "node_modules"] } ================================================ FILE: core/parcel-transformer-lab/.gitignore ================================================ node_modules dist/ ================================================ FILE: core/parcel-transformer-lab/package.json ================================================ { "name": "@plasmohq/parcel-transformer-lab", "version": "0.1.12", "description": "Plasmo Parcel Transformer Laboratory - a way to experiment with how the transformer works", "files": [ "dist" ], "main": "dist/index.js", "scripts": { "prepublishOnly": "pnpm build", "build": "tsup src/index.ts --minify --clean", "dev": "tsup src/index.ts --sourcemap --watch" }, "author": "Plasmo Corp. ", "homepage": "https://docs.plasmo.com/", "engines": { "parcel": ">= 2.7.0" }, "license": "MIT", "repository": { "type": "git", "url": "https://github.com/PlasmoHQ/plasmo.git" }, "devDependencies": { "@plasmo/config": "workspace:*", "@plasmo/utils": "workspace:*", "tsup": "8.4.0" }, "dependencies": { "@parcel/core": "2.9.3", "@parcel/diagnostic": "2.9.3", "@parcel/fs": "2.9.3", "@parcel/plugin": "2.9.3", "@parcel/types": "2.9.3", "@parcel/utils": "2.9.3" } } ================================================ FILE: core/parcel-transformer-lab/src/index.ts ================================================ /** * Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors * MIT License */ import { Transformer } from "@parcel/plugin" import { iLog, vLog } from "@plasmo/utils/logging" import { initState } from "./state" async function collectDependencies() {} export default new Transformer({ async transform({ asset, options }) { vLog("@plasmohq/parcel-transformer-lab") const code = await asset.getCode() const { state, getAssets } = initState(asset, code, options.hmrOptions) if (asset.filePath.includes("hook.ts")) { iLog("Hook file: ", asset.filePath) } if (asset.filePath.endsWith("options.tsx")) { iLog("MONITORING: ", asset.filePath) // asset.addDependency({ // specifier: "storage-hook", // specifierType: "esm", // bundleBehavior: "isolated", // resolveFrom: asset.filePath // }) } await collectDependencies() return getAssets() } }) ================================================ FILE: core/parcel-transformer-lab/src/state.ts ================================================ import type { FileSystem } from "@parcel/fs" import type { DependencyOptions, HMROptions, MutableAsset, TransformerResult } from "@parcel/types" export const state = { code: "", hot: false, fs: null as FileSystem, filePath: "", asset: null as MutableAsset, extraAssets: [] as TransformerResult[] } export const addExtraAssets = async ( filePath: string, bundlePath: string, type = "json", dependencies = [] as DependencyOptions[] ) => { state.extraAssets.push({ type, uniqueKey: bundlePath, content: await state.asset.fs.readFile(filePath, "utf8"), pipeline: type === "json" ? "raw-env" : undefined, bundleBehavior: "isolated", isBundleSplittable: type !== "json", env: state.asset.env, dependencies, meta: { bundlePath, webextEntry: false } }) } export const initState = ( asset: MutableAsset, code: string, hmrOptions: HMROptions | null | undefined ) => { state.code = code state.fs = asset.fs state.filePath = asset.filePath state.asset = asset return { state, getAssets: () => [...state.extraAssets, asset] } } ================================================ FILE: core/parcel-transformer-lab/tsconfig.json ================================================ { "extends": "@plasmo/config/ts/cli", "include": ["src/**/*.ts"], "exclude": ["dist", "node_modules"] } ================================================ FILE: core/parcel-transformer-manifest/.gitignore ================================================ node_modules dist/ ================================================ FILE: core/parcel-transformer-manifest/package.json ================================================ { "name": "@plasmohq/parcel-transformer-manifest", "version": "0.21.1", "description": "Plasmo Parcel Transformer for Web Extension Manifest", "files": [ "dist", "runtime" ], "main": "dist/index.js", "scripts": { "prepublishOnly": "pnpm build", "build": "tsup src/index.ts --minify --clean", "dev": "tsup src/index.ts --sourcemap --watch" }, "author": "Plasmo Corp. ", "homepage": "https://docs.plasmo.com/", "engines": { "parcel": ">= 2.7.0" }, "license": "MIT", "repository": { "type": "git", "url": "https://github.com/PlasmoHQ/plasmo.git" }, "devDependencies": { "@plasmo/config": "workspace:*", "@plasmo/utils": "workspace:*", "tsup": "8.4.0" }, "dependencies": { "@mischnic/json-sourcemap": "0.1.1", "@parcel/core": "2.9.3", "@parcel/diagnostic": "2.9.3", "@parcel/fs": "2.9.3", "@parcel/plugin": "2.9.3", "@parcel/types": "2.9.3", "@parcel/utils": "2.9.3", "content-security-policy-parser": "0.6.0", "json-schema-to-ts": "3.1.1", "nullthrows": "1.1.1" } } ================================================ FILE: core/parcel-transformer-manifest/runtime/plasmo-default-background.ts ================================================ ================================================ FILE: core/parcel-transformer-manifest/src/csp-patch-hmr.ts ================================================ import parseCSP from "content-security-policy-parser" const DEFAULT_INSERT = "'unsafe-eval'" export function cspPatchHMR( policy: string | null | undefined, insert = DEFAULT_INSERT ) { const defaultSrc = insert === DEFAULT_INSERT ? "'self' blob: filesystem:" : "'self'" if (policy) { const csp = parseCSP(policy) policy = "" if (!csp["script-src"]) { csp["script-src"] = [defaultSrc] } if (!csp["script-src"].includes(insert)) { csp["script-src"].push(insert) } if (csp.sandbox && !csp.sandbox.includes("allow-scripts")) { csp.sandbox.push("allow-scripts") } for (const k in csp) { policy += `${k} ${csp[k].join(" ")};` } return policy } else { return `script-src ${defaultSrc} ${insert};` + `object-src ${defaultSrc};` } } ================================================ FILE: core/parcel-transformer-manifest/src/handle-action.ts ================================================ import { getJSONSourceLocation } from "@parcel/diagnostic" import { checkMV2, getState } from "./state" export async function handleAction() { const { program, filePath, ptrs, asset } = getState() const isMV2 = checkMV2(program) const browserActionName = isMV2 ? "browser_action" : "action" const browserAction = isMV2 ? program.browser_action : program.action if (!browserAction) { return } if (browserAction.theme_icons) { browserAction.theme_icons = browserAction.theme_icons.map( (themeIcon, themeIndex) => { for (const k of ["light", "dark"]) { const loc = getJSONSourceLocation( ptrs[`/${browserActionName}/theme_icons/${themeIndex}/${k}`], "value" ) themeIcon[k] = asset.addURLDependency(themeIcon[k], { loc: { ...loc, filePath } }) } return themeIcon } ) } } ================================================ FILE: core/parcel-transformer-manifest/src/handle-background.ts ================================================ import { getJSONSourceLocation } from "@parcel/diagnostic" import { vLog } from "@plasmo/utils/logging" import { cspPatchHMR } from "./csp-patch-hmr" import type { MV2Data, MV3Data } from "./schema" import { checkMV2, getState } from "./state" export const handleBackground = () => { const { program } = getState() const isMV2 = checkMV2(program) if (isMV2) { handleMV2Background(program) } else { handleMV3Background(program) } } const defaultBackgroundScriptPath = "../runtime/plasmo-default-background.ts" function handleMV2Background(program: MV2Data) { handleMV2BackgroundScript(program) handleMV2HotCsp(program) } function handleMV3Background(program: MV3Data) { const { env } = getState() const isFirefox = env.PLASMO_BROWSER === "firefox" || env.PLASMO_BROWSER === "gecko" // Handle Firefox preliminary MV3 support: if (isFirefox) { handleFirefoxMV3Background(program) return } handleMV3BackgroundServiceWorker(program) handleMV3HotCsp(program) } function handleFirefoxMV3Background(program: MV3Data) { const mv2Program = program as unknown as MV2Data if (program.background?.service_worker) { mv2Program.background = { scripts: [program.background.service_worker] } } handleMV2BackgroundScript(mv2Program) handleMV3HotCsp(program) } function handleMV2BackgroundScript(program: MV2Data) { const { hot, asset } = getState() if (program.background?.scripts) { vLog(`Handling MV2 background scripts`) program.background.scripts = program.background.scripts.map((bgScript) => asset.addURLDependency(bgScript, { bundleBehavior: "isolated", needsStableName: true }) ) } if (hot) { if (!program.background?.scripts) { program.background = { scripts: [ asset.addURLDependency(defaultBackgroundScriptPath, { resolveFrom: __filename }) ] } } } } function handleMV3BackgroundServiceWorker(program: MV3Data) { const { hot, asset, filePath, ptrs } = getState() if (program.background?.service_worker) { vLog(`Handling MV3 background service worker`) program.background.service_worker = asset.addURLDependency( program.background.service_worker, { bundleBehavior: "isolated", needsStableName: true, loc: { filePath, ...getJSONSourceLocation(ptrs["/background/service_worker"], "value") }, env: { context: "web-worker" } } ) // Since we bundle everything, and sw import is static (not async), we can ignore type module. if (!!program.background.type) { delete program.background.type } } if (hot) { if (!program.background) { program.background = { service_worker: asset.addURLDependency(defaultBackgroundScriptPath, { resolveFrom: __filename, env: { context: "web-worker" } }) } } } } function handleMV2HotCsp(program: MV2Data) { const { hot } = getState() if (hot) { // To enable HMR, we must override the CSP to allow 'unsafe-eval' program.content_security_policy = cspPatchHMR( program.content_security_policy ) } } // Enable eval HMR for sandbox, function handleMV3HotCsp(program: MV3Data) { const { hot } = getState() if (hot) { const csp = program.content_security_policy || {} csp.extension_pages = cspPatchHMR(csp.extension_pages, `http://localhost`) // Sandbox allows eval by default if (csp.sandbox) { csp.sandbox = cspPatchHMR(csp.sandbox) } program.content_security_policy = csp } } ================================================ FILE: core/parcel-transformer-manifest/src/handle-content-scripts.ts ================================================ import { getJSONSourceLocation } from "@parcel/diagnostic" import { getState } from "./state" export function handleContentScripts() { const { program, asset, filePath, ptrs } = getState() if (!program.content_scripts) { return } for (let i = 0; i < program.content_scripts.length; ++i) { const contentScript = program.content_scripts[i] for (const k of ["css", "js"]) { const assets = contentScript[k] || [] for (let j = 0; j < assets.length; ++j) { assets[j] = asset.addURLDependency(assets[j], { bundleBehavior: "isolated", loc: { filePath, ...getJSONSourceLocation( ptrs[`/content_scripts/${i}/${k}/${j}`], "value" ) } }) } } } } ================================================ FILE: core/parcel-transformer-manifest/src/handle-declarative-net-request.ts ================================================ import { getJSONSourceLocation } from "@parcel/diagnostic" import { getState } from "./state" export async function handleDeclarativeNetRequest() { const { program, filePath, ptrs, asset } = getState() if (!program.declarative_net_request) { return } const rrs = program.declarative_net_request.rule_resources if (!rrs) { return } program.declarative_net_request.rule_resources = rrs.map((resources, i) => { resources.path = asset.addURLDependency(resources.path, { pipeline: "raw-env", loc: { filePath, ...getJSONSourceLocation( ptrs[`/declarative_net_request/rule_resources/${i}/path`], "value" ) } }) return resources }) } ================================================ FILE: core/parcel-transformer-manifest/src/handle-deep-loc.ts ================================================ import { extname } from "path" import { getJSONSourceLocation } from "@parcel/diagnostic" import { vLog } from "@plasmo/utils/logging" import { getState } from "./state" const DEEP_LOCS = [ ["icons"], ["browser_action", "default_icon"], ["browser_action", "default_popup"], ["page_action", "default_icon"], ["page_action", "default_popup"], ["action", "default_icon"], ["action", "default_popup"], ["chrome_url_overrides"], ["devtools_page"], ["options_ui", "page"], ["sidebar_action", "default_icon"], ["sidebar_action", "default_panel"], ["side_panel", "default_path"], ["storage", "managed_schema"], ["theme", "images", "theme_frame"], ["theme", "images", "additional_backgrounds"], ["user_scripts", "api_script"] ] export const handleDeepLOC = () => { const { program, filePath, ptrs, asset } = getState() const relevantLocs = DEEP_LOCS.map( (loc) => [loc, "/" + loc.join("/")] as const ).filter(([_, location]) => !!ptrs[location]) for (const [loc, location] of relevantLocs) { const lastIndex = loc.length - 1 const lastLoc = loc[lastIndex] // Reduce it right before the last loc const programPtr = loc.reduce( (acc, key, index) => (index === lastIndex ? acc : acc[key]), program ) const obj = programPtr[lastLoc] vLog(`Adding ${lastLoc}`) if (typeof obj === "string") { const ext = extname(obj) programPtr[lastLoc] = asset.addURLDependency(obj, { bundleBehavior: "isolated", loc: { filePath, ...getJSONSourceLocation(ptrs[location], "value") }, needsStableName: ext === ".html", pipeline: ext === ".json" ? "raw-env" : undefined }) } else { for (const k of Object.keys(obj)) { const ext = extname(obj[k]) obj[k] = asset.addURLDependency(obj[k], { bundleBehavior: "isolated", loc: { filePath, ...getJSONSourceLocation(ptrs[location + "/" + k], "value") }, needsStableName: ext === ".html", pipeline: ext === ".json" ? "raw-env" : undefined }) } } } } ================================================ FILE: core/parcel-transformer-manifest/src/handle-dictionaries.ts ================================================ import path from "path" import ThrowableDiagnostic, { getJSONSourceLocation } from "@parcel/diagnostic" import { getState } from "./state" export function handleDictionaries() { const { program, ptrs, filePath, asset } = getState() if (!program.dictionaries) { return } for (const dict in program.dictionaries) { const sourceLoc = getJSONSourceLocation( ptrs[`/dictionaries/${dict}`], "value" ) const loc = { filePath, ...sourceLoc } const dictFile = program.dictionaries[dict] if (path.extname(dictFile) !== ".dic") { throw new ThrowableDiagnostic({ diagnostic: [ { message: "Invalid Web Extension manifest", origin: "@plasmohq/parcel-transformer-manifest", codeFrames: [ { filePath, codeHighlights: [ { ...sourceLoc, message: "Dictionaries must be .dic files" } ] } ] } ] }) } program.dictionaries[dict] = asset.addURLDependency(dictFile, { needsStableName: true, loc }) asset.addURLDependency(dictFile.slice(0, -4) + ".aff", { needsStableName: true, loc }) } } ================================================ FILE: core/parcel-transformer-manifest/src/handle-locales.ts ================================================ import { resolve } from "path" import { vLog, wLog } from "@plasmo/utils/logging" import { getState } from "./state" import { addExtraAssets, wLogOnce } from "./utils" export async function handleLocales() { const { program, asset, assetsDir, projectDir } = getState() const localesDir = [ resolve(projectDir, "locales"), resolve(assetsDir, "locales"), resolve(assetsDir, "_locales") ].find((dir) => asset.fs.existsSync(dir)) if (!localesDir) { return } const localeEntries = await asset.fs.readdir(localesDir) if (localeEntries.length === 0) { vLog("No locale found, skipping") return } if (!program.default_locale) { program.default_locale = localeEntries[0] wLogOnce(`default_locale not set, fallback to ${localeEntries[0]}`) } const defaultLocaleMessageExists = await asset.fs.exists( resolve(localesDir, program.default_locale, "messages.json") ) if (!defaultLocaleMessageExists) { wLog("Default locale message.json not found, skipping locale!") delete program.default_locale return } await Promise.all( localeEntries.map(async (locale) => { vLog(`Adding locale ${locale}`) const localeFilePath = resolve(localesDir, locale, "messages.json") if (await asset.fs.exists(localeFilePath)) { const bundlePath = `_locales/${locale}/messages.json` asset.invalidateOnFileChange(localeFilePath) await addExtraAssets(localeFilePath, bundlePath) } }) ) } ================================================ FILE: core/parcel-transformer-manifest/src/handle-sandboxes.ts ================================================ import { resolve } from "path" import { vLog } from "@plasmo/utils/logging" import { getState } from "./state" export async function handleSandboxes() { const { asset, srcDir, dotPlasmoDir, program, env } = getState() // firefox does not support sandbox if (env.PLASMO_BROWSER === "firefox" || env.PLASMO_BROWSER === "gecko") { return } const srcPaths = [ "sandboxes", "sandbox.ts", "sandbox.tsx", "sandbox.svelte", "sandbox.vue" ].map((file) => resolve(srcDir, file)) const dotSandboxesDir = resolve(dotPlasmoDir, "sandboxes") const [ srcSandboxesDirExists, srcSandboxTsFileExists, srcSandboxTsxFileExists, srcSandboxSvelteFileExists, srcSandboxVueFileExists, dotSandboxesDirExists ] = await Promise.all( [...srcPaths, dotSandboxesDir].map((p) => asset.fs.exists(p)) ) const [srcSandboxesDir] = srcPaths const sandboxPages = [] if (srcSandboxesDirExists && dotSandboxesDirExists) { const sandboxEntries = await asset.fs.readdir(srcSandboxesDir) if (sandboxEntries.length > 0) { sandboxEntries.forEach((entry) => { const token = entry.split(".") token.pop() sandboxPages.push([entry, `${token.join(".")}.html`]) }) } } const hasSandboxFile = srcSandboxTsFileExists || srcSandboxTsxFileExists || srcSandboxSvelteFileExists || srcSandboxVueFileExists if (!hasSandboxFile && sandboxPages.length === 0) { return } if (!program.sandbox) { program.sandbox = {} } if (!program.sandbox.pages) { program.sandbox.pages = [] } if (hasSandboxFile) { program.sandbox.pages.push( asset.addURLDependency("sandbox.html", { needsStableName: true }) ) } await Promise.all( sandboxPages.map(async ([entry, htmlEntry]) => { const srcEntryPath = resolve(srcSandboxesDir, entry) const entryPath = resolve(dotSandboxesDir, htmlEntry) if ( (await asset.fs.exists(srcEntryPath)) && (await asset.fs.exists(entryPath)) ) { vLog(`Adding sandbox page: ${entry}`) program.sandbox.pages.push( asset.addURLDependency(`sandboxes/${htmlEntry}`, { needsStableName: true }) ) } }) ) } ================================================ FILE: core/parcel-transformer-manifest/src/handle-tabs.ts ================================================ import { resolve } from "path" import { vLog } from "@plasmo/utils/logging" import { getState } from "./state" export async function handleTabs() { const { asset, dotPlasmoDir, srcDir } = getState() const srcTabsDir = resolve(srcDir, "tabs") const dotTabsDir = resolve(dotPlasmoDir, "tabs") const [dotTabsDirExists, srcTabsDirExists] = await Promise.all([ asset.fs.exists(dotTabsDir), asset.fs.exists(srcTabsDir) ]) if (!dotTabsDirExists || !srcTabsDirExists) { return } const tabsEntries = await asset.fs.readdir(srcTabsDir) if (tabsEntries.length === 0) { vLog(`No tab found in ${srcTabsDir}, skipping`) return } const entryNames = tabsEntries.map((entry) => { const token = entry.split(".") token.pop() return [entry, `${token.join(".")}.html`] }) await Promise.all( entryNames.map(async ([entry, htmlEntry]) => { const entryPath = resolve(dotTabsDir, htmlEntry) const srcEntryPath = resolve(srcTabsDir, entry) if ( (await asset.fs.exists(entryPath)) && (await asset.fs.exists(srcEntryPath)) ) { vLog(`Adding tab ${entry}`) asset.addURLDependency(`./tabs/${htmlEntry}`, { bundleBehavior: "isolated", needsStableName: true }) } }) ) } ================================================ FILE: core/parcel-transformer-manifest/src/index.ts ================================================ /** * Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors * MIT License * * Based on: https://github.com/parcel-bundler/parcel/tree/v2/packages/transformers/webextension * MIT License */ import { parse } from "@mischnic/json-sourcemap" import { Transformer } from "@parcel/plugin" import type { TargetSourceMapOptions } from "@parcel/types" import { validateSchema } from "@parcel/utils" import { vLog } from "@plasmo/utils/logging" import { handleAction } from "./handle-action" import { handleBackground } from "./handle-background" import { handleContentScripts } from "./handle-content-scripts" import { handleDeclarativeNetRequest } from "./handle-declarative-net-request" import { handleDeepLOC } from "./handle-deep-loc" import { handleDictionaries } from "./handle-dictionaries" import { handleLocales } from "./handle-locales" import { handleSandboxes } from "./handle-sandboxes" import { handleTabs } from "./handle-tabs" import { normalizeManifest } from "./normalize-manifest" import { MV2Schema, MV3Schema } from "./schema" import { getState, initState } from "./state" async function collectDependencies() { normalizeManifest() await Promise.all([ handleTabs(), handleSandboxes(), handleLocales(), handleAction(), handleDeclarativeNetRequest() ]) handleContentScripts() handleDictionaries() handleDeepLOC() handleBackground() } const getSourceMapConfig = (): TargetSourceMapOptions => { switch (process.env.__PLASMO_FRAMEWORK_INTERNAL_SOURCE_MAPS) { case "inline": { return { inline: true, inlineSources: true } } case "external": { return { inline: false, inlineSources: false } } default: { return undefined } } } export default new Transformer({ async transform({ asset, options }) { vLog("@plasmohq/parcel-transformer-manifest") // Set environment to browser, since web extensions are always used in // browsers, and because it avoids delegating extra config to the user asset.setEnvironment({ context: "browser", outputFormat: asset.env.outputFormat === "commonjs" ? "global" : asset.env.outputFormat, engines: { browsers: asset.env.engines.browsers }, sourceMap: asset.env.sourceMap && getSourceMapConfig(), includeNodeModules: asset.env.includeNodeModules, sourceType: asset.env.sourceType, isLibrary: asset.env.isLibrary, shouldOptimize: asset.env.shouldOptimize, shouldScopeHoist: asset.env.shouldScopeHoist }) const code = await asset.getCode() const parsed = parse(code) const data = parsed.data const schema = data.manifest_version === 3 ? MV3Schema : MV2Schema validateSchema.diagnostic( schema, { data, source: code, filePath: asset.filePath }, "@plasmohq/parcel-transformer-manifest", "Invalid Web Extension manifest" ) await initState(asset, data, parsed.pointers, options) const state = getState() await collectDependencies() state.asset.setCode(JSON.stringify(data, null, 2)) state.asset.meta.webextEntry = true vLog("+ Finished transforming manifest") return state.getAssets() } }) ================================================ FILE: core/parcel-transformer-manifest/src/normalize-manifest.ts ================================================ import { getState } from "./state" export const normalizeManifest = () => { const { program } = getState() delete program.$schema } ================================================ FILE: core/parcel-transformer-manifest/src/schema.ts ================================================ /** * Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors * MIT License * * Based on: https://github.com/parcel-bundler/parcel/blob/v2/packages/transformers/webextension/src/schema.js * MIT License */ import type { FromSchema } from "json-schema-to-ts" import { validateBrowserVersion, validateSemanticVersion } from "./validate-version" const stringSchema = { type: "string" } as const const booleanSchema = { type: "boolean" } as const const iconsSchema = { type: "object", properties: {}, additionalProperties: stringSchema } as const const actionProps = { // FF only browser_style: booleanSchema, chrome_style: booleanSchema, // You can also have a raw string, but not in Edge, apparently... default_icon: { oneOf: [iconsSchema, stringSchema] }, default_popup: stringSchema, default_title: stringSchema } as const const arraySchema = { type: "array", items: stringSchema } as const const browserAction = { type: "object", properties: { ...actionProps, // rest are FF only default_area: { type: "string", enum: ["navbar", "menupanel", "tabstrip", "personaltoolbar"] }, theme_icons: { type: "array", items: { type: "object", properties: { light: stringSchema, dark: stringSchema, size: { type: "number" } }, additionalProperties: false, required: ["light", "dark", "size"] } } }, additionalProperties: false } as const const warBase = { type: "object", properties: { resources: arraySchema, matches: arraySchema, extension_ids: arraySchema, use_dynamic_url: booleanSchema }, additionalProperties: false } as const const commonProps = { $schema: stringSchema, name: stringSchema, version: { type: "string", __validate: validateSemanticVersion }, default_locale: stringSchema, description: stringSchema, icons: iconsSchema, author: stringSchema, browser_specific_settings: { type: "object", properties: {}, additionalProperties: { type: "object", properties: {} } }, chrome_settings_overrides: { type: "object", properties: { homepage: stringSchema, search_provider: { type: "object", properties: { name: stringSchema, keyword: stringSchema, favicon_url: stringSchema, search_url: stringSchema, encoding: stringSchema, suggest_url: stringSchema, image_url: stringSchema, instant_url: stringSchema, search_url_post_params: stringSchema, suggest_url_post_params: stringSchema, image_url_post_params: stringSchema, instant_url_post_params: stringSchema, alternate_urls: arraySchema, prepopulated_id: { type: "number" }, is_default: booleanSchema }, additionalProperties: false, required: ["name", "search_url"] }, startup_pages: arraySchema }, additionalProperties: false }, chrome_url_overrides: { type: "object", properties: { bookmarks: stringSchema, history: stringSchema, newtab: stringSchema }, additionalProperties: false }, commands: { type: "object", properties: {}, additionalProperties: { type: "object", properties: { suggested_key: { type: "object", properties: { default: stringSchema, mac: stringSchema, linux: stringSchema, windows: stringSchema, chromeos: stringSchema, android: stringSchema, ios: stringSchema }, additionalProperties: false }, description: stringSchema, global: booleanSchema }, additionalProperties: false } }, content_scripts: { type: "array", items: { type: "object", properties: { matches: arraySchema, css: arraySchema, js: arraySchema, match_about_blank: booleanSchema, match_origin_as_fallback: booleanSchema, exclude_matches: arraySchema, include_globs: arraySchema, exclude_globs: arraySchema, run_at: { type: "string", enum: ["document_idle", "document_start", "document_end"] }, all_frames: booleanSchema }, additionalProperties: false, required: ["matches"] } }, declarative_net_request: { type: "object", properties: { rule_resources: { type: "array", items: { type: "object", properties: { id: stringSchema, enabled: booleanSchema, path: stringSchema }, additionalProperties: false, required: ["id", "enabled", "path"] } } }, additionalProperties: false, required: ["rule_resources"] }, devtools_page: stringSchema, // looks to be FF only dictionaries: { type: "object", properties: {}, additionalProperties: stringSchema }, externally_connectable: { type: "object", properties: { ids: arraySchema, matches: arraySchema, accept_tls_channel_id: booleanSchema }, additionalProperties: false }, // These next two are where it gets a bit Chrome-y // (we don't include all because some have next to no actual use) file_browser_handlers: { type: "array", items: { type: "object", properties: { id: stringSchema, default_title: stringSchema, file_filters: arraySchema }, additionalProperties: false, required: ["id", "default_title", "file_filters"] } }, file_system_provider_capabilities: { type: "object", properties: { configurable: booleanSchema, multiple_mounts: booleanSchema, watchable: booleanSchema, source: { type: "string", enum: ["file", "device", "network"] } }, additionalProperties: false, required: ["source"] }, homepage_url: stringSchema, incognito: { type: "string", enum: ["spanning", "split", "not_allowed"] }, key: stringSchema, minimum_chrome_version: { type: "string", __validate: validateBrowserVersion }, // No NaCl modules because deprecated oauth2: { type: "object", properties: { client_id: stringSchema, scopes: arraySchema }, additionalProperties: false }, offline_enabled: booleanSchema, omnibox: { type: "object", properties: {}, additionalProperties: stringSchema }, optional_host_permissions: arraySchema, optional_permissions: arraySchema, // options_page is deprecated options_ui: { type: "object", properties: { browser_style: booleanSchema, chrome_style: booleanSchema, open_in_tab: booleanSchema, page: stringSchema }, additionalProperties: false, required: ["page"] }, permissions: arraySchema, // FF only, but has some use protocol_handlers: { type: "array", items: { type: "object", properties: { protocol: stringSchema, name: stringSchema, uriTemplate: stringSchema }, additionalProperties: false, required: ["protocol", "name", "uriTemplate"] } }, // Chrome only requirements: { type: "object", properties: { "3D": { type: "object", properties: { features: arraySchema }, additionalProperties: false } } }, short_name: stringSchema, // FF only, but has some use sidebar_action: { type: "object", properties: { browser_style: actionProps.browser_style, default_icon: actionProps.default_icon, default_panel: stringSchema, default_title: stringSchema, open_at_install: booleanSchema }, additionalProperties: false, required: ["default_panel"] }, storage: { type: "object", properties: { managed_schema: stringSchema }, additionalProperties: false }, theme: { type: "object", properties: { images: { type: "object", properties: { theme_frame: stringSchema, additional_backgrounds: arraySchema }, additionalProperties: false }, colors: { type: "object", properties: { bookmark_text: stringSchema, button_background_active: stringSchema, button_background_hover: stringSchema, icons: stringSchema, icons_attention: stringSchema, frame: stringSchema, frame_inactive: stringSchema, ntp_background: stringSchema, ntp_text: stringSchema, popup: stringSchema, popup_border: stringSchema, popup_highlight: stringSchema, popup_highlight_text: stringSchema, popup_text: stringSchema, sidebar: stringSchema, sidebar_border: stringSchema, sidebar_highlight: stringSchema, sidebar_highlight_text: stringSchema, sidebar_text: stringSchema, tab_background_separator: stringSchema, tab_background_text: stringSchema, tab_line: stringSchema, tab_loading: stringSchema, tab_selected: stringSchema, tab_text: stringSchema, toolbar: stringSchema, toolbar_bottom_separator: stringSchema, toolbar_field: stringSchema, toolbar_field_border: stringSchema, toolbar_field_border_focus: stringSchema, toolbar_field_focus: stringSchema, toolbar_field_highlight: stringSchema, toolbar_field_highlight_text: stringSchema, toolbar_field_separator: stringSchema, toolbar_field_text: stringSchema, toolbar_field_text_focus: stringSchema, toolbar_text: stringSchema, toolbar_top_separator: stringSchema, toolbar_vertical_separator: stringSchema }, additionalProperties: false }, properties: { type: "object", properties: { additional_backgrounds_alignment: arraySchema, additional_backgrounds_tiling: { type: "array", items: { type: "string", enum: ["no-repeat", "repeat", "repeat-x", "repeat-y"] } } }, additionalProperties: false } }, additionalProperties: false, required: ["colors"] }, tts_engine: { type: "object", properties: { voices: { type: "array", items: { type: "object", properties: { voice_name: stringSchema, lang: stringSchema, event_type: { type: "string", enum: ["start", "word", "sentence", "marker", "end", "error"] } }, additionalProperties: false, required: ["voice_name", "event_type"] } } }, additionalProperties: false }, update_url: stringSchema, user_scripts: { type: "object", properties: { api_script: stringSchema }, additionalProperties: false }, version_name: stringSchema } as const export const MV3Schema = { type: "object", properties: { ...commonProps, manifest_version: { type: "number", enum: [3] }, action: browserAction, background: { type: "object", properties: { service_worker: stringSchema, type: { type: "string", enum: ["classic", "module"] } }, additionalProperties: false, required: ["service_worker"] }, content_security_policy: { type: "object", properties: { extension_pages: stringSchema, sandbox: stringSchema }, additionalProperties: false }, host_permissions: arraySchema, sandbox: { type: "object", properties: { pages: arraySchema }, additionalProperties: false }, web_accessible_resources: { type: "array", items: { oneOf: [ { ...warBase, required: ["resources", "matches"] }, { ...warBase, required: ["resources", "extension_ids"] } ] } } }, required: ["manifest_version", "name", "version"] } as const export type MV3Data = FromSchema export const MV2Schema = { type: "object", properties: { ...commonProps, manifest_version: { type: "number", enum: [2] }, background: { type: "object", properties: { scripts: arraySchema, page: stringSchema, persistent: booleanSchema }, additionalProperties: false }, browser_action: browserAction, content_security_policy: stringSchema, page_action: { type: "object", properties: { ...actionProps, // rest are FF only hide_matches: arraySchema, show_matches: arraySchema, pinned: booleanSchema }, additionalProperties: false }, sandbox: { type: "object", properties: { pages: arraySchema, content_security_policy: stringSchema }, additionalProperties: false }, web_accessible_resources: arraySchema }, required: ["manifest_version", "name", "version"] } as const export type MV2Data = FromSchema export type ManifestData = MV2Data | MV3Data ================================================ FILE: core/parcel-transformer-manifest/src/state.ts ================================================ import { dirname, resolve } from "path" import type { Mapping } from "@mischnic/json-sourcemap" import type { MutableAsset, PluginOptions, TransformerResult } from "@parcel/types" import type { ManifestData, MV2Data } from "./schema" type ExtraAsset = TransformerResult export const storeState = ( asset: MutableAsset, program: ManifestData, ptrs: Record, options: PluginOptions ) => { const base = { extraAssets: [] as ExtraAsset[], program, hmrOptions: options.hmrOptions, hot: Boolean(options.hmrOptions), fs: asset.fs, filePath: asset.filePath, ptrs, asset, env: options.env, _isMV2: program.manifest_version === 2 } const dotPlasmoDir = dirname(asset.filePath) const projectDir = resolve(dotPlasmoDir, "..") const assetsDir = resolve(projectDir, "assets") return { ...base, srcDir: process.env.PLASMO_SRC_DIR, dotPlasmoDir, projectDir, assetsDir, getAssets: () => [...base.extraAssets, asset] } } type StateParams = Parameters type State = Partial>> let state: State = {} export const getState = () => state export const initState = async (...props: StateParams) => { state = storeState(...props) } export const checkMV2 = (program: ManifestData): program is MV2Data => state._isMV2 ================================================ FILE: core/parcel-transformer-manifest/src/utils.ts ================================================ import type { DependencyOptions } from "@parcel/types" import { injectEnv } from "@plasmo/utils/env" import { wLog } from "@plasmo/utils/logging" import { getState } from "./state" export const addExtraAssets = async ( filePath: string, bundlePath: string, type = "json", dependencies = [] as DependencyOptions[] ) => { const { asset, fs, extraAssets } = getState() const rawContent = await fs.readFile(filePath, "utf8") const parsedContent = injectEnv(rawContent) extraAssets.push({ type, uniqueKey: bundlePath, content: parsedContent, bundleBehavior: "isolated", isBundleSplittable: type !== "json", env: asset.env, dependencies, meta: { bundlePath, webextEntry: false } }) } export const wLogOnce = (msg: string) => { if (!!process.env.__PLASMO_FRAMEWORK_INTERNAL_WATCHER_STARTED) { return } wLog(msg) } ================================================ FILE: core/parcel-transformer-manifest/src/validate-version.ts ================================================ const MIN_VERSION = 0 const MAX_VERSION = 65535 const isInvalidVersion = (parts: string[]) => parts.some((part) => { const num = Number(part) return !isNaN(num) && num < MIN_VERSION && num > MAX_VERSION + 1 }) const validateVersion = (ver: string, maxSize = 3, name = "Browser") => { const parts = ver.split(".") if (parts.length > maxSize) { return `${name} versions to have at most ${maxSize} dots` } if (isInvalidVersion(parts)) { return `${name} versions must be dot-separated integers between ${MIN_VERSION} and ${MAX_VERSION}` } return undefined } export const validateSemanticVersion = (ver: string) => validateVersion(ver, 3, "Semantic") export const validateBrowserVersion = (ver: string) => validateVersion(ver, 4, "Browser") ================================================ FILE: core/parcel-transformer-manifest/tsconfig.json ================================================ { "extends": "@plasmo/config/ts/framework", "include": ["src/**/*.ts", "../../cli/plasmo/templates/plasmo.d.ts"], "exclude": ["dist", "node_modules"] } ================================================ FILE: core/parcel-transformer-svelte/.gitignore ================================================ node_modules dist/ ================================================ FILE: core/parcel-transformer-svelte/package.json ================================================ { "name": "@plasmohq/parcel-transformer-svelte", "version": "0.6.1", "description": "Plasmo Parcel Transformer for Svelte", "files": [ "dist" ], "main": "dist/index.js", "scripts": { "prepublishOnly": "pnpm build", "build": "tsup src/index.ts --minify --clean", "dev": "tsup src/index.ts --sourcemap --watch" }, "author": "Plasmo Corp. ", "homepage": "https://docs.plasmo.com/", "engines": { "parcel": ">= 2.7.0" }, "license": "MIT", "repository": { "type": "git", "url": "https://github.com/PlasmoHQ/plasmo.git" }, "devDependencies": { "@plasmo/config": "workspace:*", "tsup": "8.4.0" }, "dependencies": { "@parcel/core": "2.9.3", "@parcel/diagnostic": "2.9.3", "@parcel/plugin": "2.9.3", "@parcel/source-map": "2.1.1", "@parcel/utils": "2.9.3", "svelte": "4.2.19" } } ================================================ FILE: core/parcel-transformer-svelte/src/convert-error.ts ================================================ /** * Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors * MIT License * * Based on: https://github.com/HellButcher/parcel-transformer-svelte3-plus * Copyright (c) 2023 Christoph Hommelsheim * MIT License */ import type { Diagnostic } from "@parcel/diagnostic" import type SourceMap from "@parcel/source-map" import type { Warning } from "svelte/types/compiler/interfaces" import { convertLOC } from "./convert-loc" import type { MutableAsset } from "./types" export function convertError( asset: MutableAsset, originalMap: SourceMap, code: string, diagnostic: Warning ) { let message = diagnostic.message || "Unknown error" if (diagnostic.code) { message = `${message} (${diagnostic.code})` } const res: Diagnostic = { message } if (diagnostic.frame) { res.hints = [diagnostic.frame] } if (diagnostic.start !== undefined && diagnostic.end !== undefined) { const { start, end } = convertLOC(asset, originalMap, diagnostic) res.codeFrames = [ { filePath: asset.filePath, code, language: "svelte", codeHighlights: [ { start, end } ] } ] } return res } ================================================ FILE: core/parcel-transformer-svelte/src/convert-loc.ts ================================================ /** * Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors * MIT License * * Based on: https://github.com/HellButcher/parcel-transformer-svelte3-plus * Copyright (c) 2023 Christoph Hommelsheim * MIT License */ import type SourceMap from "@parcel/source-map" import { remapSourceLocation } from "@parcel/utils" import type { Warning } from "svelte/types/compiler/interfaces" import type { MutableAsset } from "./types" export function convertLOC( asset: MutableAsset, originalMap: SourceMap, loc: Warning ) { let location = { filePath: asset.filePath, start: { line: loc.start.line + Number(asset.meta.startLine || 1) - 1, column: loc.start.column + 1 }, end: { line: loc.end.line + Number(asset.meta.startLine || 1) - 1, column: loc.end.column + 1 } } if (originalMap) { location = remapSourceLocation(location, originalMap) } return location } ================================================ FILE: core/parcel-transformer-svelte/src/index.ts ================================================ /** * Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors * MIT License * * Based on: https://github.com/HellButcher/parcel-transformer-svelte3-plus * Copyright (c) 2023 Christoph Hommelsheim * MIT License */ import ThrowableDiagnostic from "@parcel/diagnostic" import { Transformer } from "@parcel/plugin" import { relativeUrl } from "@parcel/utils" import { compile, preprocess, type CompileOptions } from "svelte/compiler" import { convertError } from "./convert-error" import { extendSourceMap } from "./source-map" export default new Transformer({ async loadConfig({ config, options }) { const conf = await config.getConfig( [ ".svelterc", "svelte.config.js", "svelte.config.cjs", "svelte.config.mjs" ], { packageKey: "svelte" } ) let contents = {} as any if (conf && typeof conf.contents === "object") { contents = conf.contents if (conf.filePath.endsWith(".js") || conf.filePath.endsWith(".cjs")) { config.invalidateOnStartup() } } const compilerOptions = contents.compilerOptions || contents.compiler || {} return { compilerOptions: { dev: options.mode !== "production", css: "injected", ...compilerOptions } as CompileOptions, preprocess: contents.preprocess, filePath: conf && conf.filePath } }, async transform({ asset, config, options, logger }) { const [code, originalMap] = await Promise.all([ asset.getCode(), asset.getMap() ]) let finalCode = code try { // Retrieve the asset's source code and source map. const filename = relativeUrl( options.projectRoot, asset.filePath ) as string const compilerOptions = { filename, ...(config.compilerOptions || {}) } if (config.preprocess) { const preprocessed = await preprocess( code, config.preprocess, compilerOptions ) if (preprocessed.map) compilerOptions.sourcemap = preprocessed.map if (preprocessed.dependencies) { for (const dependency of preprocessed.dependencies) { asset.invalidateOnFileChange(dependency) } } finalCode = preprocessed.code } const compiled = compile(finalCode, compilerOptions) compiled.warnings?.forEach((warning) => { if (compilerOptions.css && warning.code === "css-unused-selector") return logger.warn(convertError(asset, originalMap, finalCode, warning)) }) const results = [ { type: "js", content: compiled.js.code, uniqueKey: `${asset.id}-js`, map: extendSourceMap( options, asset.filePath, originalMap, compiled.js.map ) } ] if (compiled.css && compiled.css.code) { results.push({ type: "css", content: compiled.css.code, uniqueKey: `${asset.id}-css`, map: extendSourceMap( options, asset.filePath, originalMap, compiled.css.map ) }) } return results } catch (error) { throw new ThrowableDiagnostic({ diagnostic: convertError(asset, originalMap, finalCode, error) }) } } }) ================================================ FILE: core/parcel-transformer-svelte/src/source-map.ts ================================================ /** * Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors * MIT License * * Based on: https://github.com/HellButcher/parcel-transformer-svelte3-plus * Copyright (c) 2023 Christoph Hommelsheim * MIT License */ import { dirname, isAbsolute, join } from "path" import SourceMap from "@parcel/source-map" import type { Options } from "./types" export function mapSourceMapPath(mapSourceRoot: string, sourcePath: string) { if (sourcePath.startsWith("file://")) { sourcePath = sourcePath.substring(7) } if (isAbsolute(sourcePath)) { return sourcePath } else { return join(mapSourceRoot, sourcePath) } } export function extendSourceMap( options: Options, filePath: string, originalMap: SourceMap, sourceMap: any ): SourceMap | null { if (!sourceMap) return originalMap let mapSourceRoot = dirname(filePath) let map = new SourceMap(options.projectRoot) map.addVLQMap({ ...sourceMap, sources: sourceMap.sources.map((s) => mapSourceMapPath(mapSourceRoot, s)) }) if (originalMap) { map.extends(originalMap.toBuffer()) } return map } ================================================ FILE: core/parcel-transformer-svelte/src/types.ts ================================================ /** * Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors * MIT License */ import type { Transformer } from "@parcel/plugin" type Transform = ConstructorParameters[0]["transform"] export type MutableAsset = Parameters[0]["asset"] export type Options = Parameters[0]["options"] ================================================ FILE: core/parcel-transformer-svelte/tsconfig.json ================================================ { "extends": "@plasmo/config/ts/cli", "include": ["src/**/*.ts"], "exclude": ["dist", "node_modules"] } ================================================ FILE: core/parcel-transformer-vue/.gitignore ================================================ node_modules dist/ ================================================ FILE: core/parcel-transformer-vue/package.json ================================================ { "name": "@plasmohq/parcel-transformer-vue", "version": "0.5.1", "description": "Plasmo Parcel Transformer for Vue", "files": [ "dist" ], "main": "dist/index.js", "scripts": { "prepublishOnly": "pnpm build", "build": "tsup src/index.ts --minify --clean", "dev": "tsup src/index.ts --sourcemap --watch" }, "author": "Plasmo Corp. ", "homepage": "https://docs.plasmo.com/", "engines": { "parcel": ">= 2.7.0" }, "license": "MIT", "repository": { "type": "git", "url": "https://github.com/PlasmoHQ/plasmo.git" }, "devDependencies": { "@plasmo/config": "workspace:*", "tsup": "8.4.0" }, "dependencies": { "@parcel/core": "2.9.3", "@parcel/diagnostic": "2.9.3", "@parcel/plugin": "2.9.3", "@parcel/source-map": "2.1.1", "@parcel/types": "2.9.3", "@parcel/utils": "2.9.3", "@plasmohq/consolidate": "0.17.0", "@vue/compiler-sfc": "3.5.13", "nullthrows": "1.1.1", "semver": "7.7.1", "vue": "3.5.13" } } ================================================ FILE: core/parcel-transformer-vue/src/index.ts ================================================ /** * Copyright (c) 2023 Plasmo Corp. (https://www.plasmo.com) and contributors * MIT License * * Based on: https://github.com/parcel-bundler/parcel/tree/v2/packages/transformers/vue * MIT License */ import { basename, dirname, extname, relative } from "path" import ThrowableDiagnostic, { escapeMarkdown, md, type Diagnostic } from "@parcel/diagnostic" import { Transformer } from "@parcel/plugin" import SourceMap from "@parcel/source-map" import type { TransformerResult } from "@parcel/types" import { hashObject } from "@parcel/utils" import * as compiler from "@vue/compiler-sfc" import nullthrows from "nullthrows" import semver from "semver" import consolidate from "@plasmohq/consolidate" const MODULE_BY_NAME_RE = /\.module\./ // TODO: Use language-specific config files during preprocessing export default new Transformer({ async loadConfig({ config }) { let conf = await config.getConfig( [".vuerc", ".vuerc.json", ".vuerc.js", "vue.config.js"], { packageKey: "vue" } ) let contents = {} as any if (conf) { config.invalidateOnStartup() contents = conf.contents if (typeof contents !== "object") { // TODO: codeframe throw new ThrowableDiagnostic({ diagnostic: { message: "Vue config should be an object.", origin: "@parcel/transformer-vue" } }) } } return { customBlocks: contents.customBlocks || {}, filePath: conf && conf.filePath, compilerOptions: contents.compilerOptions || {} } }, canReuseAST({ ast }) { return ast.type === "vue" && semver.satisfies(ast.version, "^3.0.0") }, async parse({ asset, options }) { // TODO: This parses the vue component multiple times. Fix? let code = await asset.getCode() let parsed = compiler.parse(code, { sourceMap: true, filename: asset.filePath }) if (parsed.errors.length) { throw new ThrowableDiagnostic({ diagnostic: parsed.errors.map((err) => { return createDiagnostic(err, asset.filePath) }) }) } const descriptor = parsed.descriptor let id = hashObject({ filePath: asset.filePath, source: options.mode === "production" ? code : null }).slice(-6) return { type: "vue", version: "3.0.0", program: { ...descriptor, script: descriptor.script != null || descriptor.scriptSetup != null ? compiler.compileScript(descriptor, { id, isProd: options.mode === "production" }) : null, id } } }, async transform({ asset, options, resolve, config }) { let { template, script, styles, customBlocks, id } = nullthrows( await asset.getAST() ).program let scopeId = "data-v-" + id let hmrId = id + "-hmr" let basePath = basename(asset.filePath) if (asset.pipeline != null) { return processPipeline({ asset, template, script, styles, customBlocks, config, basePath, options, resolve, id, hmrId }) } return [ { type: "js", uniqueKey: asset.id + "-glue", content: ` let script; let initialize = () => { script = ${ script != null ? `require('script:./${basePath}'); if (script.__esModule) script = script.default` : "{}" }; ${ template != null ? `script.render = require('template:./${basePath}').render;` : "" } ${ styles.length !== 0 ? `script.__cssModules = require('style:./${basePath}').default;` : "" } ${ customBlocks != null ? `require('custom:./${basePath}').default(script);` : "" } script.__scopeId = '${scopeId}'; script.__file = ${JSON.stringify( options.mode === "production" ? basePath : asset.filePath )}; }; initialize(); ${ options.hmrOptions ? `if (module.hot) { script.__hmrId = '${hmrId}'; module.hot.accept(() => { setTimeout(() => { initialize(); if (!__VUE_HMR_RUNTIME__.createRecord('${hmrId}', script)) { __VUE_HMR_RUNTIME__.reload('${hmrId}', script); } }, 0); }); }` : "" } export default script;` } ] } }) function createDiagnostic(err, filePath) { if (typeof err === "string") { return { message: err, origin: "@parcel/transformer-vue", filePath } } // TODO: codeframe let diagnostic: Diagnostic = { message: escapeMarkdown(err.message), origin: "@parcel/transformer-vue", name: err.name, stack: err.stack } if (err.loc) { diagnostic.codeFrames = [ { codeHighlights: [ { start: { line: err.loc.start.line + err.loc.start.offset, column: err.loc.start.column }, end: { line: err.loc.end.line + err.loc.end.offset, column: err.loc.end.column } } ] } ] } return diagnostic } async function processPipeline({ asset, template, script, styles, customBlocks, config, basePath, options, resolve, id, hmrId }) { switch (asset.pipeline) { case "template": { if (template.src) { template.content = ( await options.inputFS.readFile( await resolve(asset.filePath, template.src) ) ).toString() template.lang = extname(template.src).slice(1) } let content = template.content if (template.lang && !["htm", "html"].includes(template.lang)) { let options = {} as any let preprocessor = consolidate[template.lang] // Pug doctype fix (fixes #7756) switch (template.lang) { case "pug": options.doctype = "html" break } if (!preprocessor) { // TODO: codeframe throw new ThrowableDiagnostic({ diagnostic: { message: md([`Unknown template language: "${template.lang}"`]), origin: "@parcel/transformer-vue" } }) } content = await preprocessor.render(content, options) } let templateComp = compiler.compileTemplate({ filename: asset.filePath, source: content, inMap: template.src ? undefined : template.map, scoped: styles.some((style) => style.scoped), compilerOptions: { ...config.compilerOptions, bindingMetadata: script ? script.bindings : undefined }, isProd: options.mode === "production", id }) if (templateComp.errors.length) { throw new ThrowableDiagnostic({ diagnostic: templateComp.errors.map((err) => { return createDiagnostic(err, asset.filePath) }) }) } let templateAsset: TransformerResult = { type: "js", uniqueKey: asset.id + "-template", ...(!template.src && asset.env.sourceMap && { map: createMap(templateComp.map, options.projectRoot) }), content: templateComp.code + ` ${ options.hmrOptions ? `if (module.hot) { module.hot.accept(() => { __VUE_HMR_RUNTIME__.rerender('${hmrId}', render); }) }` : "" }` } return [templateAsset] } case "script": { if (script.src) { script.content = ( await options.inputFS.readFile( await resolve(asset.filePath, script.src) ) ).toString() script.lang = extname(script.src).slice(1) } let type switch (script.lang || "js") { case "javascript": case "js": type = "js" break case "jsx": type = "jsx" break case "typescript": case "ts": type = "ts" break case "tsx": type = "tsx" break case "coffeescript": case "coffee": type = "coffee" break default: // TODO: codeframe throw new ThrowableDiagnostic({ diagnostic: { message: md([`Unknown script language: "${script.lang}"`]), origin: "@parcel/transformer-vue" } }) } let scriptAsset = { type, uniqueKey: asset.id + "-script", content: script.content, ...(!script.src && asset.env.sourceMap && { map: createMap(script.map, options.projectRoot) }) } return [scriptAsset] } case "style": case "style-raw": { let cssModules = {} let assets = await Promise.all( styles.map(async (style, i) => { if (style.src) { style.content = ( await options.inputFS.readFile( await resolve(asset.filePath, style.src) ) ).toString() if (!style.module) { style.module = MODULE_BY_NAME_RE.test(style.src) } style.lang = extname(style.src).slice(1) } switch (style.lang) { case "less": case "stylus": case "styl": case "scss": case "sass": case "css": case undefined: break default: // TODO: codeframe throw new ThrowableDiagnostic({ diagnostic: { message: md([`Unknown style language: "${style.lang}"`]), origin: "@parcel/transformer-vue" } }) } let styleComp = await compiler.compileStyleAsync({ filename: asset.filePath, source: style.content, modules: style.module, preprocessLang: style.lang || "css", scoped: style.scoped, inMap: style.src ? undefined : style.map, isProd: options.mode === "production", id }) if (styleComp.errors.length) { throw new ThrowableDiagnostic({ diagnostic: styleComp.errors.map((err) => { return createDiagnostic(err, asset.filePath) }) }) } let styleAsset = { type: "css", content: styleComp.code, sideEffects: true, ...(!style.src && asset.env.sourceMap && { map: createMap(style.map, options.projectRoot) }), uniqueKey: asset.id + "-style" + i } if (styleComp.modules) { if (typeof style.module === "boolean") style.module = "$style" cssModules[style.module] = { ...cssModules[style.module], ...styleComp.modules } } return styleAsset }) ) if (asset.pipeline == "style") { if (Object.keys(cssModules).length !== 0) { assets.push({ type: "js", uniqueKey: asset.id + "-cssModules", content: ` import {render} from 'template:./${basePath}'; let cssModules = ${JSON.stringify(cssModules)}; ${ options.hmrOptions ? `if (module.hot) { module.hot.accept(() => { __VUE_HMR_RUNTIME__.rerender('${hmrId}', render); }); };` : "" } export default cssModules;` }) } return assets } else if (asset.pipeline == "style-raw") { const styleRawString = assets.map((a) => a.content).join("\n") return [ { type: "js", uniqueKey: asset.id + "-cssRawString", content: ` const styleRawString = \`${styleRawString}\` export default styleRawString ` } ] } } case "custom": { let toCall = [] // To satisfy flow if (!config) return [] const types = new Set() for (let block of customBlocks) { let { type, src, content, attrs } = block if (!config.customBlocks[type]) { // TODO: codeframe throw new ThrowableDiagnostic({ diagnostic: { message: md([`No preprocessor found for block type ${type}`]), origin: "@parcel/transformer-vue" } }) } if (src) { content = ( await options.inputFS.readFile(await resolve(asset.filePath, src)) ).toString() } toCall.push([type, content, attrs]) types.add(type) } return [ { type: "js", uniqueKey: asset.id + "-custom", content: ` let NOOP = () => {}; ${( await Promise.all( [...types].map( async (type: any) => `import p${type} from './${relative( dirname(asset.filePath), await resolve(nullthrows(config.filePath), config.customBlocks[type]) )}'; if (typeof p${type} !== 'function') { p${type} = NOOP; }` ) ) ).join("\n")} export default script => { ${toCall .map( ([type, content, attrs]) => ` p${type}(script, ${JSON.stringify(content)}, ${JSON.stringify( attrs )});` ) .join("\n")} }` } ] } default: { return [] } } } function createMap(rawMap, projectRoot: string) { let newMap = new SourceMap(projectRoot) newMap.addVLQMap(rawMap) return newMap } ================================================ FILE: core/parcel-transformer-vue/tsconfig.json ================================================ { "extends": "@plasmo/config/ts/cli", "include": ["src/**/*.ts"], "exclude": ["dist", "node_modules"] } ================================================ FILE: eslint.config.mjs ================================================ import globals from "globals"; import tseslint from "typescript-eslint"; /** @type {import('eslint').Linter.Config[]} */ export default [ {files: ["**/*.{js,mjs,cjs,ts}"]}, {languageOptions: { globals: {...globals.browser, ...globals.node} }}, ...tseslint.configs.recommended, ]; ================================================ FILE: package.json ================================================ { "name": "p1asm0", "private": true, "workspaces": [ "cli/*", "api/*", "core/*", "packages/*", "examples/*" ], "scripts": { "dev:cli": "turbo run dev --filter=plasmo", "build": "turbo run build", "build:cli": "turbo run build --filter=plasmo", "build:packages": "turbo run build --filter \"./packages/**\"", "build:api": "turbo run build --filter \"./api/**\"", "build:core": "turbo run build --filter \"./core/**\"", "build:examples": "pnpm --filter \"./examples/**\" -r build", "test:examples": "pnpm --filter \"./examples/**\" -r test", "publish:packages": "pnpm --filter \"./packages/**\" publish", "publish:api": "pnpm --filter \"./api/**\" publish", "publish:core": "pnpm --filter \"./core/**\" publish", "publish:cli": "pnpm --filter \"./cli/*\" publish", "publish:cli:lab": "pnpm --filter \"./cli/*\" publish --no-git-checks --tag lab", "publish:lab": "run-s publish:packages publish:cli:lab", "format": "prettier --write \"**/*.{ts,tsx,md,mjs}\"", "### version script usage example": "pnpm v:cli patch", "v:packages": "pnpm --filter \"./packages/**\" --parallel -r exec pnpm version --commit-hooks false --git-tag-version false --workspaces-update", "v:core": "pnpm --filter \"./core/**\" --parallel -r exec pnpm version --commit-hooks false --git-tag-version false --workspaces-update", "v:api": "pnpm --filter \"./api/**\" --parallel -r exec pnpm version --commit-hooks false --git-tag-version false --workspaces-update", "v:cli": "pnpm --filter \"./cli/**\" --parallel -r exec pnpm version --commit-hooks false --git-tag-version false --workspaces-update" }, "pnpm": { "overrides": { "@parcel/source-map": "2.1.1", "react-refresh": "0.16.0" }, "onlyBuiltDependencies": [ "@parcel/watcher", "@swc/core", "esbuild", "svelte-preprocess", "canvas" ] }, "devDependencies": { "@ianvs/prettier-plugin-sort-imports": "4.4.1", "@plasmohq/rps": "workspace:*", "@types/fs-extra": "11.0.4", "@types/inquirer": "9.0.7", "@types/node": "22.13.13", "@types/node-rsa": "1.1.4", "@types/react": "19.0.12", "@types/react-dom": "19.0.4", "@types/semver": "7.7.0", "@types/uuid": "10.0.0", "@types/ws": "8.18.0", "esbuild": "0.25.1", "eslint": "9.23.0", "eslint-config-prettier": "10.1.1", "eslint-plugin-react": "7.37.4", "fs-extra": "11.3.0", "globals": "16.0.0", "prettier": "3.5.3", "tsup": "8.4.0", "turbo": "2.4.4", "typescript-eslint": "8.28.0" }, "engines": { "node": ">=20.0.0" }, "packageManager": "pnpm@10.11.0" } ================================================ FILE: packages/framework-shared/build-socket/event.ts ================================================ export enum BuildSocketEvent { BuildReady = "build_ready", CsChanged = "cs_changed" } ================================================ FILE: packages/framework-shared/build-socket/index.ts ================================================ import { WebSocket, WebSocketServer } from "ws" import { BuildSocketEvent } from "./event" export { BuildSocketEvent } const createBuildSocket = (hmrHost: string, hmrPort: number) => { const wss = new WebSocketServer({ host: hmrHost, port: hmrPort + 1 }) const broadcast = (type: BuildSocketEvent) => { for (const client of wss.clients) { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify({ type })) } } } return { broadcast } } let _buildSocket: Awaited> export const getBuildSocket = (hmrHost = "localhost", hmrPort?: number) => { if (process.env.NODE_ENV === "production") { return null } if (!!_buildSocket) { return _buildSocket } if (!hmrPort) { throw new Error("HMR port is not provided") } _buildSocket = createBuildSocket(hmrHost, hmrPort) return _buildSocket } export const buildBroadcast = (type: BuildSocketEvent) => { if (process.env.NODE_ENV === "production") { return } const buildSocket = getBuildSocket() if (buildSocket) { buildSocket.broadcast(type) } } ================================================ FILE: packages/framework-shared/package.json ================================================ { "name": "@plasmo/framework-shared", "version": "0.0.2", "private": true, "license": "MIT", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { "ws": "8.18.1" }, "devDependencies": { "@plasmo/config": "workspace:*", "@plasmo/utils": "workspace:*" } } ================================================ FILE: packages/framework-shared/tsconfig.json ================================================ { "extends": "@plasmo/config/ts/utils", "include": ["./**/*.ts"], "exclude": ["node_modules"] } ================================================ FILE: packages/init/.gitignore ================================================ node_modules ================================================ FILE: packages/init/bpp.yml ================================================ name: "Submit to Web Store" on: workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Cache pnpm modules uses: actions/cache@v3 with: path: ~/.pnpm-store key: ${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}- - uses: pnpm/action-setup@v2.4.1 with: version: latest run_install: true - name: Use Node.js 20.x uses: actions/setup-node@v3.5.1 with: node-version: 20.x cache: "pnpm" - name: Build the extension run: pnpm build - name: Package the extension into a zip artifact run: pnpm package - name: Browser Platform Publish uses: PlasmoHQ/bpp@v3 with: keys: ${{ secrets.SUBMIT_KEYS }} artifact: build/chrome-mv3-prod.zip ================================================ FILE: packages/init/entries/background.ts ================================================ export {} console.log("Hello from background script!") ================================================ FILE: packages/init/entries/content.ts ================================================ import type { PlasmoCSConfig } from "plasmo" export const config: PlasmoCSConfig = { matches: ["https://www.plasmo.com/*"] } window.addEventListener("load", () => { console.log("content script loaded") document.body.style.background = "pink" }) ================================================ FILE: packages/init/entries/contents/inline.tsx ================================================ import type { PlasmoCSConfig, PlasmoGetInlineAnchor } from "plasmo" export const config: PlasmoCSConfig = { matches: ["https://www.plasmo.com/*"] } export const getInlineAnchor: PlasmoGetInlineAnchor = () => document.querySelector("#supercharge > h2 > span") // Use this to optimize unmount lookups export const getShadowHostId = () => "plasmo-inline-example-unique-id" const PlasmoInline = () => { return } export default PlasmoInline ================================================ FILE: packages/init/entries/contents/overlay.tsx ================================================ import type { PlasmoCSConfig, PlasmoGetOverlayAnchor } from "plasmo" export const config: PlasmoCSConfig = { matches: ["https://www.plasmo.com/*"] } export const getOverlayAnchor: PlasmoGetOverlayAnchor = async () => document.querySelector("#pricing") const PlasmoPricingExtra = () => { return ( HELLO WORLD ) } export default PlasmoPricingExtra ================================================ FILE: packages/init/entries/newtab.tsx ================================================ import { useState } from "react" function IndexNewtab() { const [data, setData] = useState("") return (

Welcome to your{" "} Plasmo {" "} Extension!

setData(e.target.value)} value={data} /> View Docs
) } export default IndexNewtab ================================================ FILE: packages/init/entries/options.tsx ================================================ import { useState } from "react" function IndexOptions() { const [data, setData] = useState("") return (

Welcome to your{" "} Plasmo {" "} Extension!

setData(e.target.value)} value={data} /> View Docs
) } export default IndexOptions ================================================ FILE: packages/init/entries/popup.tsx ================================================ import { useState } from "react" function IndexPopup() { const [data, setData] = useState("") return (

Welcome to your{" "} Plasmo {" "} Extension!

setData(e.target.value)} value={data} /> View Docs
) } export default IndexPopup ================================================ FILE: packages/init/index.json ================================================ {} ================================================ FILE: packages/init/package.json ================================================ { "name": "@plasmohq/init", "version": "0.7.0", "description": "Plasmo init template files", "files": [ "entries", "templates", "bpp.yml" ], "main": "index.json", "author": "Plasmo Corp. ", "homepage": "https://docs.plasmo.com/", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/PlasmoHQ/plasmo.git" } } ================================================ FILE: packages/init/templates/README.md ================================================ This is a [Plasmo extension](https://docs.plasmo.com/) project bootstrapped with [`plasmo init`](https://www.npmjs.com/package/plasmo). ## Getting Started First, run the development server: ```bash pnpm dev # or npm run dev ``` Open your browser and load the appropriate development build. For example, if you are developing for the chrome browser, using manifest v3, use: `build/chrome-mv3-dev`. You can start editing the popup by modifying `popup.tsx`. It should auto-update as you make changes. To add an options page, simply add a `options.tsx` file to the root of the project, with a react component default exported. Likewise to add a content page, add a `content.ts` file to the root of the project, importing some module and do some logic, then reload the extension on your browser. For further guidance, [visit our Documentation](https://docs.plasmo.com/) ## Making production build Run the following: ```bash pnpm build # or npm run build ``` This should create a production bundle for your extension, ready to be zipped and published to the stores. ## Submit to the webstores The easiest way to deploy your Plasmo extension is to use the built-in [bpp](https://github.com/marketplace/actions/browser-platform-publisher) GitHub action. Prior to using this action however, make sure to build your extension and upload the first version to the store to establish the basic credentials. Then, simply follow [this setup instruction](https://docs.plasmo.com/framework/workflows/submit) and you should be on your way for automated submission! ================================================ FILE: packages/init/templates/tsconfig.json ================================================ { "extends": "plasmo/templates/tsconfig.base", "exclude": ["node_modules"], "include": [".plasmo/index.d.ts", "./**/*.ts", "./**/*.tsx"], "compilerOptions": { "paths": { "~*": ["./*"] }, "baseUrl": "." } } ================================================ FILE: packages/init/tsconfig.json ================================================ { "include": ["./**/*.ts", "./**/*.tsx"], "compilerOptions": { "strict": false, "jsx": "react-jsx", "paths": { "plasmo": ["../../cli/plasmo/src/type.ts"] } } } ================================================ FILE: pnpm-workspace.yaml ================================================ packages: - "cli/*" - "packages/*" - "examples/*" - "api/*" - "core/*" ================================================ FILE: renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:recommended", "group:allNonMajor" ], "git-submodules": { "enabled": true }, "cloneSubmodules": true } ================================================ FILE: scripts/move-prettier-cjs-to-mjs.bash ================================================ #!/usr/bin/env bash # a bash script that traverse the examples directory and mv all prettier cjs files to mjs dir="examples" # Use a for loop to traverse the directory for subdir in $(find $dir -type d); do # Check if the file exists if [ -f "$subdir/.prettierrc.cjs" ]; then # If the file exists, rename it mv "$subdir/.prettierrc.cjs" "$subdir/.prettierrc.mjs" echo "Renamed .prettierrc.cjs to .prettierrc.mjs in directory $subdir" fi done ================================================ FILE: turbo.json ================================================ { "$schema": "https://turborepo.org/schema.json", "tasks": { "build": { "dependsOn": [ "^build" ], "outputs": [ "dist/**" ] }, "publish": { "dependsOn": [ "^build" ], "outputs": [ "dist/**" ] }, "lint": { "outputs": [] }, "dev": { "dependsOn": [ "^build" ], "cache": false }, "clean": { "dependsOn": [ "^clean" ], "cache": false } } }