├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── build-and-test.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs └── changelog.md ├── examples ├── README.md ├── commonjs │ ├── .gitignore │ ├── index.js │ ├── package.json │ └── webpack.config.js └── esm │ ├── .gitignore │ ├── index.js │ ├── package.json │ └── webpack.config.js ├── index.ts ├── lib └── patterns.ts ├── loader.ts ├── package-lock.json ├── package.json ├── test └── pyodide-plugin.test.js ├── tsconfig.json ├── webpack.config.js └── webpack ├── after-build.js ├── examples.js ├── index.js ├── webpack.esm.js ├── webpack.loader.js └── webpack.umd.js /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 12, 5 | "sourceType": "module" 6 | }, 7 | "env": { 8 | "node": true, 9 | "es6": true, 10 | "browser": true 11 | }, 12 | "plugins": ["@typescript-eslint"], 13 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"], 14 | "rules": { 15 | "no-constant-condition": 0, 16 | "@typescript-eslint/no-var-requires": 0, 17 | "@typescript-eslint/no-unused-vars": [ 18 | "warn", // or "error" 19 | { 20 | "argsIgnorePattern": "^_{1,}$", 21 | "varsIgnorePattern": "^_{1,}$", 22 | "caughtErrorsIgnorePattern": "^_{1,}$" 23 | } 24 | ], 25 | "@typescript-eslint/ban-ts-comment": 0 26 | }, 27 | "globals": { 28 | "describe": "readonly", 29 | "beforeEach": "readonly", 30 | "afterEach": "readonly", 31 | "it": "readonly" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [18.x, 20.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm ci 24 | - run: npm test 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/macos,node,visualstudiocode 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,node,visualstudiocode 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### macOS Patch ### 34 | # iCloud generated files 35 | *.icloud 36 | 37 | ### Node ### 38 | # Logs 39 | logs 40 | *.log 41 | npm-debug.log* 42 | yarn-debug.log* 43 | yarn-error.log* 44 | lerna-debug.log* 45 | .pnpm-debug.log* 46 | 47 | # Diagnostic reports (https://nodejs.org/api/report.html) 48 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 49 | 50 | # Runtime data 51 | pids 52 | *.pid 53 | *.seed 54 | *.pid.lock 55 | 56 | # Directory for instrumented libs generated by jscoverage/JSCover 57 | lib-cov 58 | 59 | # Coverage directory used by tools like istanbul 60 | coverage 61 | *.lcov 62 | 63 | # nyc test coverage 64 | .nyc_output 65 | 66 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 67 | .grunt 68 | 69 | # Bower dependency directory (https://bower.io/) 70 | bower_components 71 | 72 | # node-waf configuration 73 | .lock-wscript 74 | 75 | # Compiled binary addons (https://nodejs.org/api/addons.html) 76 | build/Release 77 | 78 | # Dependency directories 79 | node_modules/ 80 | jspm_packages/ 81 | 82 | # Snowpack dependency directory (https://snowpack.dev/) 83 | web_modules/ 84 | 85 | # TypeScript cache 86 | *.tsbuildinfo 87 | 88 | # Optional npm cache directory 89 | .npm 90 | 91 | # Optional eslint cache 92 | .eslintcache 93 | 94 | # Optional stylelint cache 95 | .stylelintcache 96 | 97 | # Microbundle cache 98 | .rpt2_cache/ 99 | .rts2_cache_cjs/ 100 | .rts2_cache_es/ 101 | .rts2_cache_umd/ 102 | 103 | # Optional REPL history 104 | .node_repl_history 105 | 106 | # Output of 'npm pack' 107 | *.tgz 108 | 109 | # Yarn Integrity file 110 | .yarn-integrity 111 | 112 | # dotenv environment variable files 113 | .env 114 | .env.development.local 115 | .env.test.local 116 | .env.production.local 117 | .env.local 118 | 119 | # parcel-bundler cache (https://parceljs.org/) 120 | .cache 121 | .parcel-cache 122 | 123 | # Next.js build output 124 | .next 125 | out 126 | 127 | # Nuxt.js build / generate output 128 | .nuxt 129 | dist 130 | 131 | # Gatsby files 132 | .cache/ 133 | # Comment in the public line in if your project uses Gatsby and not Next.js 134 | # https://nextjs.org/blog/next-9-1#public-directory-support 135 | # public 136 | 137 | # vuepress build output 138 | .vuepress/dist 139 | 140 | # vuepress v2.x temp and cache directory 141 | .temp 142 | 143 | # Docusaurus cache and generated files 144 | .docusaurus 145 | 146 | # Serverless directories 147 | .serverless/ 148 | 149 | # FuseBox cache 150 | .fusebox/ 151 | 152 | # DynamoDB Local files 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | .tern-port 157 | 158 | # Stores VSCode versions used for testing VSCode extensions 159 | .vscode-test 160 | 161 | # yarn v2 162 | .yarn/cache 163 | .yarn/unplugged 164 | .yarn/build-state.yml 165 | .yarn/install-state.gz 166 | .pnp.* 167 | 168 | ### Node Patch ### 169 | # Serverless Webpack directories 170 | .webpack/ 171 | 172 | # Optional stylelint cache 173 | 174 | # SvelteKit build / generate output 175 | .svelte-kit 176 | 177 | ### VisualStudioCode ### 178 | .vscode/* 179 | !.vscode/settings.json 180 | !.vscode/tasks.json 181 | !.vscode/launch.json 182 | !.vscode/extensions.json 183 | !.vscode/*.code-snippets 184 | 185 | # Local History for Visual Studio Code 186 | .history/ 187 | 188 | # Built Visual Studio Code Extensions 189 | *.vsix 190 | 191 | ### VisualStudioCode Patch ### 192 | # Ignore all local history of files 193 | .history 194 | .ionide 195 | 196 | # Support for Project snippet scope 197 | .vscode/*.code-snippets 198 | 199 | # Ignore code-workspaces 200 | *.code-workspace 201 | 202 | # End of https://www.toptal.com/developers/gitignore/api/macos,node,visualstudiocode 203 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "ms-vscode.vscode-typescript-next", "dbaeumer.vscode-eslint"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Test: Current Test", 9 | "request": "launch", 10 | "runtimeArgs": ["run-script", "unit", "--", "--testPathPattern", "${fileBasenameNoExtension}"], 11 | "runtimeExecutable": "npm", 12 | "skipFiles": ["/**"], 13 | "type": "node" 14 | }, 15 | { 16 | "name": "Test: Unit Test", 17 | "request": "launch", 18 | "runtimeArgs": ["run-script", "unit"], 19 | "runtimeExecutable": "npm", 20 | "skipFiles": ["/**"], 21 | "type": "node" 22 | }, 23 | { 24 | "name": "Example: common:build", 25 | "request": "launch", 26 | "runtimeArgs": ["run-script", "build"], 27 | "runtimeExecutable": "npm", 28 | "cwd": "${workspaceFolder}/examples/commonjs", 29 | "skipFiles": ["/**"], 30 | "type": "node" 31 | }, 32 | { 33 | "name": "Example: esm:build", 34 | "request": "launch", 35 | "runtimeArgs": ["run-script", "build"], 36 | "runtimeExecutable": "npm", 37 | "cwd": "${workspaceFolder}/examples/esm", 38 | "skipFiles": ["/**"], 39 | "type": "node" 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing to Pyodide Webpack Plugin! There are many ways to contribute, and we appreciate all of them. Here are some guidelines & pointers for diving into it. 4 | 5 | ## Development Workflow 6 | 7 | To contribute code, see the following steps, 8 | 9 | 1. Fork the Pyodide repository https://github.com/pyodide/pyodide-webpack-plugin on Github. 10 | 2. Clone your fork of Pyodide Webpack Plugin\ 11 | `git clone https://github.com//pyodide-webpack-plugin.git`\ 12 | and add the upstream remote,\ 13 | `git remote add upstream https://github.com/pyodide/pyodide-webpack-plugin.git` 14 | 3. Nodejs [18.x+](https://nodejs.org/en/) 15 | 4. Install requirements\ 16 | `npm i` 17 | 5. See Testing and benchmarking documentation. 18 | 19 | ## Testing 20 | 21 | Run the full suite of tests including a fresh build, lint, formatting, and unit tests with: 22 | 23 | ``` 24 | npm test 25 | ``` 26 | 27 | You can run individual steps of the test by looking in [package.json](./package.json) for different scripts. `npm run unit` is useful for rerunning unit tests quickly. 28 | 29 | ### Unit Tests 30 | 31 | Unit testing currently tests against the [examples](./examples/). There are a few things to note: 32 | 33 | - Tests happen against the built plugin. You must run `npm run build` before you test if any of your code has changes. `npm test` runs the build as part of the test phase. 34 | - Your example must contain a `webpack.config.js` and must export webpack itself 35 | - New examples need to be initialized with npm workspaces `npm init -w examples/` 36 | - New examples are automatically picked up for the build targets and test phase 37 | 38 | > running `npx webpack --watch` can be faster than running `npm run build` if you are iterating over the plugin and testing. Use in conjunction with `npm run unit` for fast test iteration. 39 | 40 | ## Code of Conduct 41 | 42 | Pyodide has adopted a [Code of Conduct](https://pyodide.org/en/stable/project/code-of-conduct.html#code-of-conduct) that we expect all contributors and core members to adhere to. 43 | 44 | ## Development 45 | 46 | Work on Pyodide happens on GitHub. Core members and contributors can make Pull Requests to fix issues and add features, which all go through the same review process. We'll detail how you can start making PRs below. 47 | 48 | We'll do our best to keep `main` in a non-breaking state, ideally with tests always passing. The unfortunate reality of software development is sometimes things break. As such, `main` cannot be expected to remain reliable at all times. We recommend using the latest stable version of Pyodide. 49 | 50 | Pyodide follows [semantic versioning](http://semver.org/) - major versions for breaking changes (x.0.0), minor versions for new features (0.x.0), and patches for bug fixes (0.0.x). 51 | 52 | We keep a file, [docs/changelog.md](./docs/changelog.md), outlining changes to Pyodide in each release. We like to think of the audience for changelogs as non-developers who primarily run the latest stable. So the change log will primarily outline user-visible changes such as new features and deprecations, and will exclude things that might otherwise be inconsequential to the end user experience, such as infrastructure or refactoring. 53 | 54 | ## Bugs & Issues 55 | 56 | We use [Github Issues](https://github.com/pyodide/pyodide-webpack-plugin/issues) for announcing and discussing bugs and features. Use [this link](https://github.com/pyodide/pyodide-webpack-plugin/issues/new) to report a bug or issue. We provide a template to give you a guide for how to file optimally. If you have the chance, please search the existing issues before reporting a bug. It's possible that someone else has already reported your error. This doesn't always work, and sometimes it's hard to know what to search for, so consider this extra credit. We won't mind if you accidentally file a duplicate report. 57 | 58 | Core contributors are monitoring new issues & comments all the time, and will label & organize issues to align with development priorities. 59 | 60 | ## How to Contribute 61 | 62 | Pull requests are the primary mechanism we use to change Pyodide. GitHub itself has some [great documentation](https://help.github.com/articles/about-pull-requests/) on using the Pull Request feature. We use the "fork and pull" model [described here](https://help.github.com/articles/about-pull-requests/), where contributors push changes to their personal fork and create pull requests to bring those changes into the source repository. 63 | 64 | Please make pull requests against the `main` branch. 65 | 66 | If you're looking for a way to jump in and contribute, our list of [good first issues](https://github.com/pyodide/pyodide-webpack-plugin/labels/good%20first%20issue) is a great place to start. 67 | 68 | If you'd like to fix a currently-filed issue, please take a look at the comment thread on the issue to ensure no one is already working on it. If no one has claimed the issue, make a comment stating you'd like to tackle it in a PR. If someone has claimed the issue but has not worked on it in a few weeks, make a comment asking if you can take over, and we'll figure it out from there. 69 | 70 | We use [mocha](https://www.npmjs.com/package/mocha). Every PR will automatically run through our tests, and our test framework will alert you on GitHub if your PR doesn't pass all of them. If your PR fails a test, try to figure out whether or not you can update your code to make the test pass again, or ask for help. As a policy we will not accept a PR that fails any of our tests, and will likely ask you to add tests if your PR adds new functionality. Writing tests can be scary, but they make open-source contributions easier for everyone to assess. Take a moment and look through how we've written our tests, and try to make your tests match. If you are having trouble, we can help you get started on our test-writing journey. 71 | 72 | All code submissions should pass `npm run test`. TypeScript is checked with prettier. 73 | 74 | ## Documentation 75 | 76 | Documentation is a critical part of any open source project, and we are very welcome to any documentation improvements. Pyodide has a documentation written in Markdown in the `docs/` folder. We use the MyST for parsing Markdown in sphinx. You may want to have a look at the MyST syntax guide when contributing, in particular regarding cross-referencing sections. 77 | 78 | ## Migrating patches 79 | 80 | It often happens that patches need to be migrated between different versions of upstream packages. 81 | 82 | If patches fail to apply automatically, one solution can be to 83 | 84 | 1. Checkout the initial version of the upstream package in a separate repo, and create a branch from it. 85 | 2. Add existing patches with `git apply ` 86 | 3. Checkout the new version of the upstream package and create a branch from it. 87 | 4. Cherry-pick patches to the new version,\ 88 | `git cherry-pick `\ 89 | and resolve conflicts. 90 | 5. Re-export last `N` commits as patches e.g.\ 91 | `git format-patch - -N --no-stat HEAD -o ` 92 | 93 | ## Maintainer information 94 | 95 | For information about making releases see [Maintainer information](https://pyodide.org/en/stable/development/maintainers.html#maintainer-information). 96 | 97 | ## License 98 | 99 | All contributions to Pyodide will be licensed under the [Mozilla Public License 2.0 (MPL 2.0)](https://www.mozilla.org/en-US/MPL/2.0/). This is considered a "weak copyleft" license. Check out the [tl;drLegal entry]() for more information, as well as Mozilla's [MPL 2.0 FAQ](https://www.mozilla.org/en-US/MPL/2.0/FAQ/) if you need further clarification on what is and isn't permitted. 100 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Mozilla Public License Version 2.0 2 | 3 | 1. Definitions 4 | 5 | --- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | 88 | --- 89 | 90 | 2.1. Grants 91 | 92 | Each Contributor hereby grants You a world-wide, royalty-free, 93 | non-exclusive license: 94 | 95 | (a) under intellectual property rights (other than patent or trademark) 96 | Licensable by such Contributor to use, reproduce, make available, 97 | modify, display, perform, distribute, and otherwise exploit its 98 | Contributions, either on an unmodified basis, with Modifications, or 99 | as part of a Larger Work; and 100 | 101 | (b) under Patent Claims of such Contributor to make, use, sell, offer 102 | for sale, have made, import, and otherwise transfer either its 103 | Contributions or its Contributor Version. 104 | 105 | 2.2. Effective Date 106 | 107 | The licenses granted in Section 2.1 with respect to any Contribution 108 | become effective for each Contribution on the date the Contributor first 109 | distributes such Contribution. 110 | 111 | 2.3. Limitations on Grant Scope 112 | 113 | The licenses granted in this Section 2 are the only rights granted under 114 | this License. No additional rights or licenses will be implied from the 115 | distribution or licensing of Covered Software under this License. 116 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 117 | Contributor: 118 | 119 | (a) for any code that a Contributor has removed from Covered Software; 120 | or 121 | 122 | (b) for infringements caused by: (i) Your and any other third party's 123 | modifications of Covered Software, or (ii) the combination of its 124 | Contributions with other software (except as part of its Contributor 125 | Version); or 126 | 127 | (c) under Patent Claims infringed by Covered Software in the absence of 128 | its Contributions. 129 | 130 | This License does not grant any rights in the trademarks, service marks, 131 | or logos of any Contributor (except as may be necessary to comply with 132 | the notice requirements in Section 3.4). 133 | 134 | 2.4. Subsequent Licenses 135 | 136 | No Contributor makes additional grants as a result of Your choice to 137 | distribute the Covered Software under a subsequent version of this 138 | License (see Section 10.2) or under the terms of a Secondary License (if 139 | permitted under the terms of Section 3.3). 140 | 141 | 2.5. Representation 142 | 143 | Each Contributor represents that the Contributor believes its 144 | Contributions are its original creation(s) or it has sufficient rights 145 | to grant the rights to its Contributions conveyed by this License. 146 | 147 | 2.6. Fair Use 148 | 149 | This License is not intended to limit any rights You have under 150 | applicable copyright doctrines of fair use, fair dealing, or other 151 | equivalents. 152 | 153 | 2.7. Conditions 154 | 155 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 156 | in Section 2.1. 157 | 158 | 3. Responsibilities 159 | 160 | --- 161 | 162 | 3.1. Distribution of Source Form 163 | 164 | All distribution of Covered Software in Source Code Form, including any 165 | Modifications that You create or to which You contribute, must be under 166 | the terms of this License. You must inform recipients that the Source 167 | Code Form of the Covered Software is governed by the terms of this 168 | License, and how they can obtain a copy of this License. You may not 169 | attempt to alter or restrict the recipients' rights in the Source Code 170 | Form. 171 | 172 | 3.2. Distribution of Executable Form 173 | 174 | If You distribute Covered Software in Executable Form then: 175 | 176 | (a) such Covered Software must also be made available in Source Code 177 | Form, as described in Section 3.1, and You must inform recipients of 178 | the Executable Form how they can obtain a copy of such Source Code 179 | Form by reasonable means in a timely manner, at a charge no more 180 | than the cost of distribution to the recipient; and 181 | 182 | (b) You may distribute such Executable Form under the terms of this 183 | License, or sublicense it under different terms, provided that the 184 | license for the Executable Form does not attempt to limit or alter 185 | the recipients' rights in the Source Code Form under this License. 186 | 187 | 3.3. Distribution of a Larger Work 188 | 189 | You may create and distribute a Larger Work under terms of Your choice, 190 | provided that You also comply with the requirements of this License for 191 | the Covered Software. If the Larger Work is a combination of Covered 192 | Software with a work governed by one or more Secondary Licenses, and the 193 | Covered Software is not Incompatible With Secondary Licenses, this 194 | License permits You to additionally distribute such Covered Software 195 | under the terms of such Secondary License(s), so that the recipient of 196 | the Larger Work may, at their option, further distribute the Covered 197 | Software under the terms of either this License or such Secondary 198 | License(s). 199 | 200 | 3.4. Notices 201 | 202 | You may not remove or alter the substance of any license notices 203 | (including copyright notices, patent notices, disclaimers of warranty, 204 | or limitations of liability) contained within the Source Code Form of 205 | the Covered Software, except that You may alter any license notices to 206 | the extent required to remedy known factual inaccuracies. 207 | 208 | 3.5. Application of Additional Terms 209 | 210 | You may choose to offer, and to charge a fee for, warranty, support, 211 | indemnity or liability obligations to one or more recipients of Covered 212 | Software. However, You may do so only on Your own behalf, and not on 213 | behalf of any Contributor. You must make it absolutely clear that any 214 | such warranty, support, indemnity, or liability obligation is offered by 215 | You alone, and You hereby agree to indemnify every Contributor for any 216 | liability incurred by such Contributor as a result of warranty, support, 217 | indemnity or liability terms You offer. You may include additional 218 | disclaimers of warranty and limitations of liability specific to any 219 | jurisdiction. 220 | 221 | 4. Inability to Comply Due to Statute or Regulation 222 | 223 | --- 224 | 225 | If it is impossible for You to comply with any of the terms of this 226 | License with respect to some or all of the Covered Software due to 227 | statute, judicial order, or regulation then You must: (a) comply with 228 | the terms of this License to the maximum extent possible; and (b) 229 | describe the limitations and the code they affect. Such description must 230 | be placed in a text file included with all distributions of the Covered 231 | Software under this License. Except to the extent prohibited by statute 232 | or regulation, such description must be sufficiently detailed for a 233 | recipient of ordinary skill to be able to understand it. 234 | 235 | 5. Termination 236 | 237 | --- 238 | 239 | 5.1. The rights granted under this License will terminate automatically 240 | if You fail to comply with any of its terms. However, if You become 241 | compliant, then the rights granted under this License from a particular 242 | Contributor are reinstated (a) provisionally, unless and until such 243 | Contributor explicitly and finally terminates Your grants, and (b) on an 244 | ongoing basis, if such Contributor fails to notify You of the 245 | non-compliance by some reasonable means prior to 60 days after You have 246 | come back into compliance. Moreover, Your grants from a particular 247 | Contributor are reinstated on an ongoing basis if such Contributor 248 | notifies You of the non-compliance by some reasonable means, this is the 249 | first time You have received notice of non-compliance with this License 250 | from such Contributor, and You become compliant prior to 30 days after 251 | Your receipt of the notice. 252 | 253 | 5.2. If You initiate litigation against any entity by asserting a patent 254 | infringement claim (excluding declaratory judgment actions, 255 | counter-claims, and cross-claims) alleging that a Contributor Version 256 | directly or indirectly infringes any patent, then the rights granted to 257 | You by any and all Contributors for the Covered Software under Section 258 | 2.1 of this License shall terminate. 259 | 260 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 261 | end user license agreements (excluding distributors and resellers) which 262 | have been validly granted by You or Your distributors under this License 263 | prior to termination shall survive termination. 264 | 265 | --- 266 | 267 | - * 268 | - 6. Disclaimer of Warranty \* 269 | - ------------------------- \* 270 | - * 271 | - Covered Software is provided under this License on an "as is" \* 272 | - basis, without warranty of any kind, either expressed, implied, or \* 273 | - statutory, including, without limitation, warranties that the \* 274 | - Covered Software is free of defects, merchantable, fit for a \* 275 | - particular purpose or non-infringing. The entire risk as to the \* 276 | - quality and performance of the Covered Software is with You. \* 277 | - Should any Covered Software prove defective in any respect, You \* 278 | - (not any Contributor) assume the cost of any necessary servicing, \* 279 | - repair, or correction. This disclaimer of warranty constitutes an \* 280 | - essential part of this License. No use of any Covered Software is \* 281 | - authorized under this License except under this disclaimer. \* 282 | - * 283 | 284 | --- 285 | 286 | --- 287 | 288 | - * 289 | - 7. Limitation of Liability \* 290 | - -------------------------- \* 291 | - * 292 | - Under no circumstances and under no legal theory, whether tort \* 293 | - (including negligence), contract, or otherwise, shall any \* 294 | - Contributor, or anyone who distributes Covered Software as \* 295 | - permitted above, be liable to You for any direct, indirect, \* 296 | - special, incidental, or consequential damages of any character \* 297 | - including, without limitation, damages for lost profits, loss of \* 298 | - goodwill, work stoppage, computer failure or malfunction, or any \* 299 | - and all other commercial damages or losses, even if such party \* 300 | - shall have been informed of the possibility of such damages. This \* 301 | - limitation of liability shall not apply to liability for death or \* 302 | - personal injury resulting from such party's negligence to the \* 303 | - extent applicable law prohibits such limitation. Some \* 304 | - jurisdictions do not allow the exclusion or limitation of \* 305 | - incidental or consequential damages, so this exclusion and \* 306 | - limitation may not apply to You. \* 307 | - * 308 | 309 | --- 310 | 311 | 8. Litigation 312 | 313 | --- 314 | 315 | Any litigation relating to this License may be brought only in the 316 | courts of a jurisdiction where the defendant maintains its principal 317 | place of business and such litigation shall be governed by laws of that 318 | jurisdiction, without reference to its conflict-of-law provisions. 319 | Nothing in this Section shall prevent a party's ability to bring 320 | cross-claims or counter-claims. 321 | 322 | 9. Miscellaneous 323 | 324 | --- 325 | 326 | This License represents the complete agreement concerning the subject 327 | matter hereof. If any provision of this License is held to be 328 | unenforceable, such provision shall be reformed only to the extent 329 | necessary to make it enforceable. Any law or regulation which provides 330 | that the language of a contract shall be construed against the drafter 331 | shall not be used to construe this License against a Contributor. 332 | 333 | 10. Versions of the License 334 | 335 | --- 336 | 337 | 10.1. New Versions 338 | 339 | Mozilla Foundation is the license steward. Except as provided in Section 340 | 10.3, no one other than the license steward has the right to modify or 341 | publish new versions of this License. Each version will be given a 342 | distinguishing version number. 343 | 344 | 10.2. Effect of New Versions 345 | 346 | You may distribute the Covered Software under the terms of the version 347 | of the License under which You originally received the Covered Software, 348 | or under the terms of any subsequent version published by the license 349 | steward. 350 | 351 | 10.3. Modified Versions 352 | 353 | If you create software not governed by this License, and you want to 354 | create a new license for such software, you may create and use a 355 | modified version of this License if you rename the license and remove 356 | any references to the name of the license steward (except to note that 357 | such modified license differs from this License). 358 | 359 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 360 | Licenses 361 | 362 | If You choose to distribute Source Code Form that is Incompatible With 363 | Secondary Licenses under the terms of this version of the License, the 364 | notice described in Exhibit B of this License must be attached. 365 | 366 | ## Exhibit A - Source Code Form License Notice 367 | 368 | This Source Code Form is subject to the terms of the Mozilla Public 369 | License, v. 2.0. If a copy of the MPL was not distributed with this 370 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 371 | 372 | If it is not possible or desirable to put the notice in a particular 373 | file, then You may include the notice in a location (such as a LICENSE 374 | file in a relevant directory) where a recipient would be likely to look 375 | for such a notice. 376 | 377 | You may add additional accurate notices of copyright ownership. 378 | 379 | ## Exhibit B - "Incompatible With Secondary Licenses" Notice 380 | 381 | This Source Code Form is "Incompatible With Secondary Licenses", as 382 | defined by the Mozilla Public License, v. 2.0. 383 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Node.js CI](https://github.com/pyodide/pyodide-webpack-plugin/actions/workflows/build-and-test.yml/badge.svg?branch=main)](https://github.com/pyodide/pyodide-webpack-plugin/actions/workflows/build-and-test.yml) 2 | 3 | # Pyodide Webpack Plugin 4 | 5 | A Webpack plugin for integrating pyodide into your project. 6 | 7 | > works with pyodide >=0.21.3 8 | 9 | ## Getting Started 10 | 11 | Install pyodide and @pyodide/webpack-plugin 12 | 13 | ``` 14 | npm install --save-dev pyodide @pyodide/webpack-plugin 15 | ``` 16 | 17 | or 18 | 19 | ``` 20 | yarn add -D pyodide @pyodide/webpack-plugin 21 | ``` 22 | 23 | or 24 | 25 | ``` 26 | pnpm add -D pyodide @pyodide/webpack-plugin 27 | ``` 28 | 29 | Add the plugin to your webpack config 30 | 31 | ```js 32 | const { PyodidePlugin } = require("@pyodide/webpack-plugin"); 33 | 34 | module.exports = { 35 | plugins: [new PyodidePlugin()], 36 | }; 37 | ``` 38 | 39 | In your javascript application being bundled with webpack 40 | 41 | ```js 42 | async function main() { 43 | let pyodide = await loadPyodide({ indexURL: `${window.location.origin}/pyodide` }); 44 | // Pyodide is now ready to use... 45 | console.log( 46 | pyodide.runPython(` 47 | import sys 48 | sys.version 49 | `) 50 | ); 51 | } 52 | main(); 53 | ``` 54 | 55 | See [examples](./examples/). 56 | 57 | ## Options 58 | 59 | - [globalLoadPyodide](#globalLoadPyodide) 60 | - [outDirectory](#outDirectory) 61 | - [packageIndexUrl](#packageIndexUrl) 62 | 63 | ### globalLoadPyodide 64 | 65 | Type: `boolean`\ 66 | Default: `false`\ 67 | Required: false\ 68 | _Description_:Whether or not to expose loadPyodide method globally. A globalThis.loadPyodide is useful when using pyodide as a standalone script or in certain frameworks. With webpack we can scope the pyodide package locally to prevent leaks (default). 69 | 70 | ### outDirectory 71 | 72 | Type: `string`\ 73 | Default: `pyodide`\ 74 | Required: false\ 75 | _Description_: Relative path to webpack root where you want to output the pyodide files. 76 | 77 | ### packageIndexUrl 78 | 79 | Type: `string`\ 80 | Default: `https://cdn.jsdelivr.net/pyodide/v${installedPyodideVersion}/full/`\ 81 | Required: false\ 82 | _Description_: CDN endpoint for python packages. This option differs from [loadPyodide indexUrl](https://pyodide.org/en/stable/usage/api/js-api.html) in that it only impacts pip packages and _does not_ affect the location the main pyodide runtime location. Set this value to "" if you want to keep the pyodide default of accepting the indexUrl. 83 | 84 | ## Contributing 85 | 86 | Please view the [contributing guide](./CONTRIBUTING.md) for tips on filing issues, making changes, and submitting pull requests. Pyodide is an independent and community-driven open-source project. The decision-making process is outlined in the [Project governance](https://pyodide.org/en/stable/project/governance.html). 87 | 88 | https://github.com/pyodide/pyodide/blob/main/CODE-OF-CONDUCT.md 89 | 90 | ## License 91 | 92 | Pyodide Webpack Plugin uses the [Mozilla Public License Version 2.0](https://choosealicense.com/licenses/mpl-2.0/). 93 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pyodide/pyodide-webpack-plugin/6a32da3b4db68ba183b1e94c72fc96fc03465336/docs/changelog.md -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Example usage of pyodide webpack plugin. Download / copy the example folder you wish to try out and run the following commands: 4 | 5 | ``` 6 | npm i 7 | npm run serve 8 | ``` 9 | 10 | This will install the necessary dependencies and start webpack dev server. Open your browser to the URL output in the terminal and view the develop console to see the output of pyodide. 11 | -------------------------------------------------------------------------------- /examples/commonjs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /examples/commonjs/index.js: -------------------------------------------------------------------------------- 1 | const { loadPyodide, version } = require("pyodide"); 2 | 3 | async function main() { 4 | console.log("pyodide version", version); 5 | let pyodide = await loadPyodide({ 6 | indexURL: `${window.location.origin}/pyodide`, 7 | }); 8 | // Pyodide is now ready to use... 9 | console.log( 10 | pyodide.runPython(` 11 | import sys 12 | sys.version 13 | `) 14 | ); 15 | } 16 | main(); 17 | -------------------------------------------------------------------------------- /examples/commonjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pyodide-webpack-plugin-commonjs", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "build": "webpack", 7 | "serve": "webpack serve", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@pyodide/webpack-plugin": "^1.3.1", 14 | "html-webpack-plugin": "^5.5.3", 15 | "webpack": "5.88.x", 16 | "webpack-cli": "^5.1.4", 17 | "webpack-dev-server": "4.14.x" 18 | }, 19 | "dependencies": { 20 | "pyodide": "^0.24.1" 21 | }, 22 | "description": "" 23 | } 24 | -------------------------------------------------------------------------------- /examples/commonjs/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | const { PyodidePlugin } = require("@pyodide/webpack-plugin"); 4 | const webpack = require("webpack"); 5 | 6 | module.exports = (_, argv) => 7 | /** @type {import("webpack").Configuration} */ ({ 8 | target: "web", 9 | mode: argv.mode || "development", 10 | devtool: false, 11 | entry: path.resolve(__dirname, "index.js"), 12 | output: { 13 | path: path.resolve(__dirname, "dist"), 14 | filename: "example.js", 15 | }, 16 | devServer: { 17 | static: { 18 | directory: path.join(__dirname, "dist"), 19 | }, 20 | compress: true, 21 | port: 9000, 22 | }, 23 | plugins: [new PyodidePlugin(), new HtmlWebpackPlugin()], 24 | }); 25 | 26 | module.exports.webpack = webpack; 27 | -------------------------------------------------------------------------------- /examples/esm/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /examples/esm/index.js: -------------------------------------------------------------------------------- 1 | import { loadPyodide, version } from "pyodide"; 2 | 3 | export async function main() { 4 | console.log("pyodide version", version); 5 | let pyodide = await loadPyodide({ 6 | indexURL: `${window.location.origin}/pyodide`, 7 | }); 8 | // Pyodide is now ready to use... 9 | console.log( 10 | pyodide.runPython(` 11 | import sys 12 | sys.version 13 | `) 14 | ); 15 | } 16 | main(); 17 | -------------------------------------------------------------------------------- /examples/esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pyodide-webpack-plugin-esm", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "type": "module", 6 | "scripts": { 7 | "build": "webpack", 8 | "serve": "webpack serve", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@pyodide/webpack-plugin": "^1.3.1", 15 | "html-webpack-plugin": "^5.5.3", 16 | "webpack": "5.88.x", 17 | "webpack-cli": "^5.1.4", 18 | "webpack-dev-server": "4.14.x" 19 | }, 20 | "dependencies": { 21 | "pyodide": "^0.24.1" 22 | }, 23 | "description": "" 24 | } 25 | -------------------------------------------------------------------------------- /examples/esm/webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import HtmlWebpackPlugin from "html-webpack-plugin"; 3 | import { PyodidePlugin } from "@pyodide/webpack-plugin"; 4 | 5 | export default (_, argv) => 6 | /** @type {import("webpack").Configuration} */ ({ 7 | target: "web", 8 | mode: argv.mode || "development", 9 | devtool: false, 10 | entry: path.resolve("index.js"), 11 | output: { 12 | path: path.resolve("dist"), 13 | filename: "example.js", 14 | chunkFormat: "module", 15 | library: { 16 | type: "module", 17 | }, 18 | }, 19 | experiments: { 20 | outputModule: true, 21 | }, 22 | devServer: { 23 | static: { 24 | directory: path.resolve("dist"), 25 | }, 26 | compress: true, 27 | port: 9000, 28 | }, 29 | plugins: [ 30 | new PyodidePlugin(), 31 | new HtmlWebpackPlugin({ 32 | scriptLoading: "module", 33 | }), 34 | ], 35 | }); 36 | 37 | export { default as webpack } from "webpack"; 38 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import url from "url"; 3 | import assert from "assert"; 4 | import path from "path"; 5 | import CopyPlugin from "copy-webpack-plugin"; 6 | import webpack from "webpack"; 7 | import * as patterns from "./lib/patterns"; 8 | import { createRequire } from "node:module"; 9 | 10 | function noop(_) { 11 | return _; 12 | } 13 | 14 | let dirname; 15 | try { 16 | // @ts-ignore import.meta is only available in esm... 17 | dirname = path.dirname(url.fileURLToPath(import.meta.url)); 18 | } catch (e) { 19 | noop(e); 20 | } 21 | 22 | interface PyodideOptions extends Partial { 23 | /** 24 | * CDN endpoint for python packages 25 | * This option differs from 26 | * [loadPyodide indexUrl](https://pyodide.org/en/stable/usage/api/js-api.html) 27 | * in that it only impacts pip packages and _does not_ affect 28 | * the location the main pyodide runtime location. Set this value to "" if you want to keep 29 | * the pyodide default of accepting the indexUrl. 30 | * 31 | * @default https://cdn.jsdelivr.net/pyodide/v${installedPyodideVersion}/full/ 32 | */ 33 | packageIndexUrl?: string; 34 | /** 35 | * Whether or not to expose loadPyodide method globally. A globalThis.loadPyodide is useful when 36 | * using pyodide as a standalone script or in certain frameworks. With webpack we can scope the 37 | * pyodide package locally to prevent leaks (default). 38 | * 39 | * @default false 40 | */ 41 | globalLoadPyodide?: boolean; 42 | /** 43 | * Relative path to webpack root where you want to output the pyodide files. 44 | * Defaults to pyodide 45 | */ 46 | outDirectory?: string; 47 | /** 48 | * Pyodide package version to use when resolving the default pyodide package index url. Default 49 | * version is whatever version is installed in {pyodideDependencyPath} 50 | */ 51 | version?: string; 52 | /** 53 | * Path on disk to the pyodide module. By default the plugin will attempt to look 54 | * in ./node_modules for pyodide. 55 | */ 56 | pyodideDependencyPath?: string; 57 | } 58 | 59 | export class PyodidePlugin extends CopyPlugin { 60 | readonly globalLoadPyodide: boolean; 61 | 62 | constructor(options: PyodideOptions = {}) { 63 | let outDirectory = options.outDirectory || "pyodide"; 64 | if (outDirectory.startsWith("/")) { 65 | outDirectory = outDirectory.slice(1); 66 | } 67 | const globalLoadPyodide = options.globalLoadPyodide || false; 68 | const pyodidePackagePath = tryGetPyodidePath(options.pyodideDependencyPath); 69 | const pkg = tryResolvePyodidePackage(pyodidePackagePath, options.version); 70 | 71 | options.patterns = patterns.chooseAndTransform(pkg, options.packageIndexUrl).map((pattern) => { 72 | return { 73 | from: path.resolve(pyodidePackagePath, pattern.from), 74 | to: path.join(outDirectory, pattern.to), 75 | transform: pattern.transform, 76 | }; 77 | }); 78 | assert.ok(options.patterns.length > 0, `Unsupported version of pyodide. Must use >=${patterns.versions[0]}`); 79 | // we have to delete all pyodide plugin options before calling super. Rest of options passed to copy webpack plugin 80 | delete options.packageIndexUrl; 81 | delete options.globalLoadPyodide; 82 | delete options.outDirectory; 83 | delete options.version; 84 | delete options.pyodideDependencyPath; 85 | super(options as Required); 86 | this.globalLoadPyodide = globalLoadPyodide; 87 | } 88 | apply(compiler: webpack.Compiler) { 89 | super.apply(compiler); 90 | compiler.hooks.compilation.tap(this.constructor.name, (compilation) => { 91 | const compilationHooks = webpack.NormalModule.getCompilationHooks(compilation); 92 | compilationHooks.beforeLoaders.tap(this.constructor.name, (loaders, normalModule) => { 93 | const matches = normalModule.userRequest.match(/pyodide\.m?js$/); 94 | if (matches) { 95 | // add a new loader specifically to handle pyodide.m?js. See loader.ts for functionalidy 96 | loaders.push({ 97 | loader: path.resolve(dirname, "loader.cjs"), 98 | options: { 99 | globalLoadPyodide: this.globalLoadPyodide, 100 | isModule: matches[0].endsWith(".mjs"), 101 | }, 102 | ident: "pyodide", 103 | type: null, 104 | }); 105 | } 106 | }); 107 | }); 108 | } 109 | } 110 | /** 111 | * Try to find the pyodide path. Can't use require.resolve because it is not supported in 112 | * module builds. Nodes import.meta.resolve is experimental and still very new as of node 19.x 113 | * This method is works universally under the assumption of an install in node_modules/pyodide 114 | * @param pyodidePath 115 | * @returns 116 | */ 117 | function tryGetPyodidePath(pyodidePath?: string) { 118 | if (pyodidePath) { 119 | return path.resolve(pyodidePath); 120 | } 121 | 122 | let pyodideEntrypoint = ""; 123 | if (typeof require) { 124 | try { 125 | pyodideEntrypoint = __non_webpack_require__.resolve("pyodide"); 126 | } catch (e) { 127 | noop(e); 128 | } 129 | } else { 130 | try { 131 | // @ts-ignore import.meta is only available in esm... 132 | const r = createRequire(import.meta.url); 133 | pyodideEntrypoint = r.resolve("pyodide"); 134 | } catch (e) { 135 | noop(e); 136 | } 137 | } 138 | const walk = (p: string) => { 139 | const stat = fs.statSync(p); 140 | if (stat.isFile()) { 141 | return walk(path.dirname(p)); 142 | } 143 | if (stat.isDirectory()) { 144 | if (path.basename(p) === "node_modules") { 145 | throw new Error( 146 | "unable to locate pyodide package. You can define it manually with pyodidePath if you're trying to test something novel" 147 | ); 148 | } 149 | for (const dirent of fs.readdirSync(p, { withFileTypes: true })) { 150 | if (dirent.name !== "package.json" || dirent.isDirectory()) { 151 | continue; 152 | } 153 | try { 154 | const pkg = fs.readFileSync(path.join(p, dirent.name), "utf-8"); 155 | const pkgJson = JSON.parse(pkg); 156 | if (pkgJson.name === "pyodide") { 157 | // found pyodide package root. Exit this thing 158 | return p; 159 | } 160 | } catch (e) { 161 | throw new Error( 162 | "unable to locate and parse pyodide package.json. You can define it manually with pyodidePath if you're trying to test something novel" 163 | ); 164 | } 165 | } 166 | return walk(path.dirname(p)); 167 | } 168 | }; 169 | return walk(pyodideEntrypoint); 170 | } 171 | 172 | /** 173 | * Read the pyodide package dependency package.json to return necessary metadata 174 | * @param version 175 | * @returns 176 | */ 177 | function tryResolvePyodidePackage(pyodidePath: string, version?: string) { 178 | if (version) { 179 | return { version }; 180 | } 181 | const pkgPath = path.resolve(pyodidePath, "package.json"); 182 | try { 183 | const pkg = fs.readFileSync(pkgPath, "utf-8"); 184 | return JSON.parse(pkg); 185 | } catch (e) { 186 | throw new Error(`unable to read package.json from pyodide dependency in ${pkgPath}`); 187 | } 188 | } 189 | 190 | export default PyodidePlugin; 191 | -------------------------------------------------------------------------------- /lib/patterns.ts: -------------------------------------------------------------------------------- 1 | import { ObjectPattern, Transform } from "copy-webpack-plugin"; 2 | 3 | interface PyodideObjectPattern extends ObjectPattern { 4 | to: string; 5 | } 6 | 7 | type FileFunction = (pkg: Pkg) => string[]; 8 | 9 | interface Pkg { 10 | files?: string[]; 11 | } 12 | 13 | const files: { [key: string]: string[] | FileFunction } = { 14 | "0.21.3": [ 15 | "distutils.tar", 16 | "package.json", 17 | "pyodide_py.tar", 18 | "pyodide.asm.js", 19 | "pyodide.asm.js", 20 | "pyodide.asm.data", 21 | "pyodide.asm.wasm", 22 | "repodata.json", 23 | ], 24 | "0.22.1": [ 25 | "package.json", 26 | "pyodide_py.tar", 27 | "pyodide.asm.js", 28 | "pyodide.asm.data", 29 | "pyodide.asm.wasm", 30 | "repodata.json", 31 | ], 32 | "0.23.0": ["package.json", "pyodide.asm.js", "pyodide.asm.wasm", "repodata.json", "python_stdlib.zip"], 33 | "0.24.0": function (pkg: Pkg) { 34 | if (!pkg.files) { 35 | return []; 36 | } 37 | // list of files to ignore 38 | const ignore = [/^pyodide.m?js.*/, /.+\.d\.ts$/, /.+\.html$/]; 39 | // files to ensure are always included 40 | const always = ["package.json"]; 41 | const filtered = pkg.files.filter((file) => { 42 | return !ignore.some((v) => file.match(v)); 43 | }); 44 | always.forEach((f) => { 45 | if (!filtered.includes(f)) { 46 | filtered.push(f); 47 | } 48 | }); 49 | return filtered; 50 | }, 51 | }; 52 | export const versions = Object.keys(files); 53 | 54 | /** 55 | * Choose the set of files to match for copying out of pyodide. 56 | * Based on the version passed. If no version is available in files to match 57 | * that is great enough an empty array is returned. 58 | * @param version 59 | * @returns {string[]} 60 | */ 61 | export function choose(version = "0.0.0"): string[] | FileFunction { 62 | let chosen: string[] | FileFunction = []; 63 | for (let i = 0; i < versions.length; i++) { 64 | if (version >= versions[i]) { 65 | chosen = files[versions[i]]; 66 | } 67 | } 68 | return chosen; 69 | } 70 | /** 71 | * Choose the set of files to match for copying out of pyodide. 72 | * Based on the version passed. If no version is available in files to match 73 | * that is great enough an empty array is returned. 74 | * @param version 75 | * @param pattern 76 | * @param packageIndexUrl 77 | * @returns {PyodideObjectPattern[]} 78 | */ 79 | export function transform(version: string, pattern: string[], packageIndexUrl): PyodideObjectPattern[] { 80 | return pattern.map((name) => { 81 | let transform: Transform | undefined; 82 | if (packageIndexUrl && name == "pyodide.asm.js") { 83 | transform = { 84 | transformer: (input) => { 85 | return input 86 | .toString() 87 | .replace("resolvePath(file_name,API.config.indexURL)", `resolvePath(file_name,"${packageIndexUrl}")`); 88 | }, 89 | }; 90 | } 91 | return { from: name, to: name, transform }; 92 | }); 93 | } 94 | 95 | export function chooseAndTransform(pkg, packageIndexUrl?: string) { 96 | packageIndexUrl = packageIndexUrl ?? `https://cdn.jsdelivr.net/pyodide/v${pkg.version}/full/`; 97 | let files = choose(pkg.version); 98 | if (typeof files === "function") { 99 | files = files(pkg); 100 | } 101 | return transform(pkg.version, files, packageIndexUrl); 102 | } 103 | -------------------------------------------------------------------------------- /loader.ts: -------------------------------------------------------------------------------- 1 | import { Parser as AcornParser, Node } from "acorn"; 2 | import { importAssertions } from "acorn-import-assertions"; 3 | import esbuild from "esbuild"; 4 | import { LoaderContext } from "webpack"; 5 | const walk = require("acorn-walk"); 6 | const parser = AcornParser.extend(importAssertions as typeof importAssertions); 7 | 8 | interface LoaderOptions { 9 | isModule: boolean; 10 | globalLoadPyodide: boolean; 11 | } 12 | 13 | class PyodideParser { 14 | ast: Node; 15 | options: LoaderOptions; 16 | source: string; 17 | delta: number; 18 | constructor(source: string, options: LoaderOptions) { 19 | this.delta = 0; 20 | this.ast = parser.parse(source, { 21 | ecmaVersion: 2020, 22 | sourceType: options.isModule ? "module" : "script", 23 | }); 24 | this.options = options; 25 | this.source = source; 26 | } 27 | parse() { 28 | // eslint-disable-next-line 29 | const self = this; 30 | walk.simple(this.ast, { 31 | ExpressionStatement(node) { 32 | self.walkExpressionStatement(node); 33 | }, 34 | }); 35 | } 36 | replace(statement: Node, str: string) { 37 | const len = statement.end - statement.start; 38 | const start = this.source.slice(0, statement.start + this.delta); 39 | const end = this.source.slice(statement.end + this.delta); 40 | this.source = `${start}${str}${end}`; 41 | this.delta += str.length - len; 42 | return str; 43 | } 44 | walkExpressionStatement(statement) { 45 | // getting dumb here. Just want to do some quick things. 46 | if (this.options.globalLoadPyodide) { 47 | return; 48 | } 49 | const assignment = statement.expression?.left?.object; 50 | if (assignment?.type !== "Identifier" || assignment?.name !== "globalThis") { 51 | return; 52 | } 53 | // remove global load pyodide 54 | this.replace(statement, "({});"); 55 | } 56 | } 57 | 58 | function addNamedExports(source, options) { 59 | // convoluted way to inject exports. In the future if this 60 | // gets too complicated opt for a js compiler that can take in 61 | // estree AST and manipulate the AST tree directly instead. 62 | // for now though this works and keeps dependencies down to a minimum 63 | if (options.isModule) { 64 | // esm module already has exports like we expect 65 | return source; 66 | } 67 | const newSource = source.split("\n"); 68 | const commonExports = "module.exports = {loadPyodide: loadPyodide.loadPyodide, version: loadPyodide.version};"; 69 | for (let i = 0; i < newSource.length; i++) { 70 | if (!newSource[i].includes("sourceMappingURL")) continue; 71 | newSource.splice(i, 0, commonExports); 72 | break; 73 | } 74 | return newSource.join("\n"); 75 | } 76 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 77 | function fc(v: any) { 78 | return v as T; 79 | } 80 | 81 | export default function (source) { 82 | // @ts-expect-error this has a type any, but we know this is a loader context 83 | const self: LoaderContext = fc>(this); 84 | const options: LoaderOptions = self.getOptions(); 85 | let banner = "module.exports ="; 86 | let footer = ""; 87 | if (options.isModule) { 88 | source = esbuild.transformSync(source, { 89 | banner: "const module={exports:{}};", 90 | footer: "module.exports;", 91 | format: "cjs", 92 | }).code; 93 | banner = "const out ="; 94 | // not sure how to make this better. Need some way to dynamically export these but esm provides no way 95 | footer = "export const loadPyodide = out.loadPyodide;\nexport const version = out.version;"; 96 | } 97 | // this._module.parser.state.module = this._module; 98 | // parse with the original parser... causes errors because we do not want this to 99 | // actually be evaluated and added to webpack's tree 100 | // const ast = this._module.parser.parse(source, { 101 | // module: this._module, 102 | // current: this._module, 103 | // options: {}, 104 | // source: source 105 | // }); 106 | // parse with our own parser 107 | 108 | const p = new PyodideParser(source, options); 109 | p.parse(); 110 | const finalSource = addNamedExports(p.source, options); 111 | return `${banner} eval(${JSON.stringify(finalSource)});\n${footer}`; 112 | } 113 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pyodide/webpack-plugin", 3 | "version": "1.3.3", 4 | "description": "Webpack plugin for integrating pyodide into your project", 5 | "type": "module", 6 | "exports": { 7 | ".": { 8 | "import": { 9 | "types": "./types/esm/index.d.ts", 10 | "default": "./plugin.mjs" 11 | }, 12 | "require": { 13 | "types": "./types/umd/index.d.ts", 14 | "default": "./plugin.js" 15 | } 16 | } 17 | }, 18 | "types": "./types/umd/index.d.ts", 19 | "main": "./plugin.js", 20 | "files": [ 21 | "types", 22 | "plugin.*", 23 | "loader.cjs" 24 | ], 25 | "prettier": { 26 | "printWidth": 120 27 | }, 28 | "scripts": { 29 | "build": "webpack", 30 | "test": "npm-run-all lint format:check build unit", 31 | "lint": "eslint --ignore-path .eslintignore --ext .js,.ts .", 32 | "format": "prettier --ignore-path .gitignore --write \"**/*.+(js|ts|json)\"", 33 | "format:check": "npm run format -- --check --no-write", 34 | "unit": "mocha test/**/*.test.js" 35 | }, 36 | "author": "Michael Neil <@mneil>", 37 | "license": "MPL-2.0", 38 | "devDependencies": { 39 | "@types/node": "^18.7.18", 40 | "@types/webpack-env": "^1.18.0", 41 | "@typescript-eslint/eslint-plugin": "^5.38.0", 42 | "@typescript-eslint/parser": "^5.38.0", 43 | "eslint": "^8.23.1", 44 | "eslint-config-prettier": "^8.5.0", 45 | "extra-watch-webpack-plugin": "^1.0.3", 46 | "lodash": "^4.17.21", 47 | "mocha": "^10.2.0", 48 | "npm-run-all": "^4.1.5", 49 | "prettier": "^2.7.1", 50 | "pyodide": "^0.24.0", 51 | "string-replace-loader": "^3.1.0", 52 | "ts-loader": "^9.4.1", 53 | "typescript": "^4.8.3", 54 | "webpack": "^5.74.0", 55 | "webpack-cli": "^4.10.0", 56 | "webpack-node-externals": "^3.0.0" 57 | }, 58 | "dependencies": { 59 | "acorn": "^8.13.0", 60 | "acorn-import-assertions": "^1.9.0", 61 | "acorn-walk": "^8.2.0", 62 | "copy-webpack-plugin": "^11.0.0", 63 | "esbuild": "^0.19.5" 64 | }, 65 | "peerDependencies": { 66 | "pyodide": ">=0.21.3" 67 | }, 68 | "workspaces": [ 69 | "examples/esm", 70 | "examples/commonjs" 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /test/pyodide-plugin.test.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import os from "os"; 3 | import path from "path"; 4 | import url from "url"; 5 | import assert from "assert"; 6 | 7 | const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); 8 | 9 | describe("examples", () => { 10 | let tmpDir; 11 | const exampleDir = path.resolve(__dirname, "..", "examples"); 12 | const examples = fs.readdirSync(exampleDir, { withFileTypes: true }); 13 | // console.log(examples); 14 | beforeEach(async () => { 15 | tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pyodide-webpack-plugin")); 16 | }); 17 | afterEach(async () => { 18 | await fs.promises.rm(tmpDir, { recursive: true, force: true }); 19 | }); 20 | 21 | for (const example of examples) { 22 | if (!example.isDirectory()) { 23 | continue; 24 | } 25 | it(`should build ${example.name}`, async () => { 26 | // get the current directory 27 | const curDir = process.cwd(); 28 | // wrap the whole test to ensure we can teardown safely 29 | // eslint-disable-next-line no-async-promise-executor 30 | const err = await new Promise(async (resolve) => { 31 | try { 32 | // change into example directory. This is why the test setup is so ugly 33 | process.chdir(path.join(exampleDir, example.name)); 34 | // load our webpack config from the example directory 35 | const config = await import(path.join(exampleDir, example.name, "webpack.config.js")); 36 | // the config MUST export webpack itself. Have to or you get runtime type assertion errors 37 | config.webpack(config.default({}, {}), function (err, stats) { 38 | // wrap it all again in a try/catch because webpack requires a callback currently :( 39 | try { 40 | assert.ok(!err, err); 41 | assert.ok(stats, "no stats"); 42 | assert.ok(!stats.hasErrors(), stats.toString()); 43 | 44 | const files = stats.toJson().assets?.map((x) => x.name); 45 | assert.ok(files, "no files"); 46 | 47 | assert.ok(files.indexOf("pyodide/pyodide.asm.wasm") !== -1); 48 | assert.ok(files.indexOf("pyodide/pyodide.asm.js") !== -1); 49 | assert.ok(files.indexOf("pyodide/python_stdlib.zip") !== -1); 50 | assert.ok(files.indexOf("pyodide/pyodide-lock.json") !== -1); 51 | assert.ok(files.indexOf("pyodide/package.json") !== -1); 52 | } catch (e) { 53 | // resolve with an error. Avoid another try/catch 54 | return resolve(e); 55 | } 56 | // clean exit. Best case scenario, we passed 57 | return resolve(); 58 | }); 59 | } catch (e) { 60 | // something failed in setup 61 | return resolve(e); 62 | } 63 | }); 64 | // revert back to our original cwd directory 65 | process.chdir(curDir); 66 | // ensure the test didn't error 67 | assert.ok(!err, err); 68 | }); 69 | } 70 | }); 71 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "esModuleInterop": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "skipLibCheck": true, 7 | "checkJs": true, 8 | "allowJs": true, 9 | "declaration": true, 10 | "declarationMap": true, 11 | "allowSyntheticDefaultImports": true, 12 | "noImplicitAny": false, 13 | 14 | "lib": ["ES6", "DOM"], 15 | "target": "ES6", 16 | "module": "commonjs", 17 | "moduleResolution": "Node", 18 | "declarationDir": "./dist/types/umd" 19 | }, 20 | "files": ["./index.ts", "./loader.ts"], 21 | "include": ["./lib/**/*.ts"], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | import { umd, esm, loader } from "./webpack/index.js"; 2 | 3 | // webpack config 4 | export default (env, argv) => { 5 | return [esm(env, argv), umd(env, argv), loader(env, argv)]; 6 | }; 7 | -------------------------------------------------------------------------------- /webpack/after-build.js: -------------------------------------------------------------------------------- 1 | export class AfterBuild { 2 | constructor(callback) { 3 | if (typeof callback !== "function") { 4 | throw new Error("After Build Plugin requires a callback function"); 5 | } 6 | this.callback = callback; 7 | } 8 | apply(compiler) { 9 | if (process.env.WEBPACK_WATCH) { 10 | return compiler.hooks.watchClose.tap("AfterBuild", (stats) => { 11 | this.callback(compiler, stats); 12 | }); 13 | } 14 | return compiler.hooks.done.tap("AfterBuild", (stats) => { 15 | this.callback(compiler, stats); 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /webpack/examples.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs"; 3 | import { fileURLToPath } from "url"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = path.dirname(__filename); 7 | 8 | export const moveToExample = function (distDir, filename) { 9 | const exampleDir = path.resolve(__dirname, "..", "examples"); 10 | const examples = fs.readdirSync(exampleDir, { withFileTypes: true }); 11 | 12 | for (const example of examples) { 13 | if (!example.isDirectory()) { 14 | continue; 15 | } 16 | const outDir = path.join(exampleDir, example.name, "node_modules", "@pyodide", "webpack-plugin"); 17 | fs.mkdirSync(outDir, { recursive: true }); 18 | fs.writeFileSync(path.join(outDir, filename), fs.readFileSync(path.join(distDir, filename))); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /webpack/index.js: -------------------------------------------------------------------------------- 1 | export * from "./webpack.esm.js"; 2 | export * from "./webpack.loader.js"; 3 | export * from "./webpack.umd.js"; 4 | -------------------------------------------------------------------------------- /webpack/webpack.esm.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import _ from "lodash"; 3 | import nodeExternals from "webpack-node-externals"; 4 | import webpack from "webpack"; 5 | import { fileURLToPath } from "url"; 6 | import { AfterBuild } from "./after-build.js"; 7 | import { moveToExample } from "./examples.js"; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | // webpack config overloads when type is es6 12 | // npx webpack --env output='es6' 13 | 14 | export const esm = (_, argv) => 15 | /** @type {import("webpack").Configuration} */ ({ 16 | target: "node", 17 | mode: argv.mode || "development", 18 | entry: "./index.ts", 19 | experiments: { 20 | outputModule: true, 21 | }, 22 | optimization: { 23 | minimize: false, 24 | }, 25 | ...(argv.mode === "production" ? {} : { devtool: "inline-source-map" }), 26 | output: { 27 | path: path.join(__dirname, "..", "dist"), 28 | filename: "plugin.mjs", 29 | chunkFormat: "module", 30 | library: { 31 | type: "module", 32 | }, 33 | globalObject: `(typeof self !== 'undefined' ? self : this)`, 34 | }, 35 | performance: { 36 | hints: false, 37 | }, 38 | externalsPresets: { node: true }, 39 | externals: [nodeExternals({ importType: "module" })], 40 | plugins: [ 41 | new webpack.DefinePlugin({ 42 | MODULE: JSON.stringify(true), 43 | }), 44 | new AfterBuild((compiler) => { 45 | // copy the build plugin into the examples folder. This has to happen 46 | // because otherwise webpack will fail on the type of Configuration object not matching in memory 47 | // this is a static type check in the runtime that causing the issue. 48 | moveToExample(compiler.outputPath, "plugin.mjs"); 49 | }), 50 | ], 51 | resolve: { 52 | extensions: [".ts", ".js"], 53 | }, 54 | module: { 55 | parser: { 56 | javascript: { 57 | importMeta: false, 58 | }, 59 | }, 60 | rules: [ 61 | { 62 | test: /\.ts$/, 63 | use: [ 64 | { 65 | loader: "ts-loader", 66 | options: { 67 | compilerOptions: { 68 | lib: ["ES2022", "DOM"], 69 | target: "ES2022", 70 | module: "ES2022", 71 | declarationDir: "./dist/types/esm", 72 | }, 73 | }, 74 | }, 75 | ], 76 | exclude: /node_modules/, 77 | }, 78 | ], 79 | }, 80 | }); 81 | -------------------------------------------------------------------------------- /webpack/webpack.loader.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import _ from "lodash"; 3 | import fs from "fs"; 4 | import nodeExternals from "webpack-node-externals"; 5 | import { fileURLToPath } from "url"; 6 | import { AfterBuild } from "./after-build.js"; 7 | import { moveToExample } from "./examples.js"; 8 | import pkg from "../package.json" assert { type: "json" }; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = path.dirname(__filename); 12 | // webpack config overloads when type is es6 13 | // npx webpack --env output='es6' 14 | 15 | export const loader = (env, argv) => 16 | /** @type {import("webpack").Configuration} */ ({ 17 | target: "node", 18 | mode: argv.mode || "development", 19 | entry: "./loader.ts", 20 | optimization: { 21 | minimize: false, 22 | }, 23 | ...(argv.mode === "production" ? {} : { devtool: "inline-source-map" }), 24 | output: { 25 | path: path.join(__dirname, "..", "dist"), 26 | filename: "loader.cjs", 27 | library: { 28 | name: "pyodide-webpack-loader", 29 | type: "umd", 30 | }, 31 | umdNamedDefine: true, 32 | globalObject: `(typeof self !== 'undefined' ? self : this)`, 33 | }, 34 | performance: { 35 | hints: false, 36 | }, 37 | externalsPresets: { node: true }, 38 | externals: [nodeExternals()], 39 | plugins: [ 40 | new AfterBuild((compiler) => { 41 | // copy the build plugin into the examples folder. This has to happen 42 | // because otherwise webpack will fail on the type of Configuration object not matching in memory 43 | // this is a static type check in the runtime that causing the issue. 44 | delete pkg.scripts; 45 | delete pkg.devDependencies; 46 | delete pkg.overrides; 47 | delete pkg.type; 48 | delete pkg.prettier; 49 | delete pkg.workspaces; 50 | fs.writeFileSync(path.resolve(compiler.outputPath, "package.json"), JSON.stringify(pkg, undefined, 2)); 51 | moveToExample(compiler.outputPath, "loader.cjs"); 52 | moveToExample(compiler.outputPath, "package.json"); 53 | }), 54 | // new ExtraWatchWebpackPlugin({ 55 | // files: ["loader.cjs"], 56 | // }), 57 | ], 58 | resolve: { 59 | extensions: [".ts", ".js"], 60 | }, 61 | module: { 62 | rules: [ 63 | { 64 | test: /\.ts$/, 65 | use: [ 66 | { 67 | loader: "ts-loader", 68 | }, 69 | ], 70 | exclude: /node_modules/, 71 | }, 72 | ], 73 | }, 74 | }); 75 | -------------------------------------------------------------------------------- /webpack/webpack.umd.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import url from "url"; 3 | import _ from "lodash"; 4 | import nodeExternals from "webpack-node-externals"; 5 | import { AfterBuild } from "./after-build.js"; 6 | import { moveToExample } from "./examples.js"; 7 | 8 | const __filename = url.fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename); 10 | /** 11 | * webpack config overloads when type is undefined npm run build 12 | */ 13 | export const umd = (_, argv) => 14 | /**@type {import("webpack").Configuration}*/ ({ 15 | target: "node", 16 | mode: argv.mode || "development", 17 | entry: "./index.ts", 18 | optimization: { 19 | minimize: false, 20 | }, 21 | ...(argv.mode === "production" ? {} : { devtool: "inline-source-map" }), 22 | output: { 23 | path: path.join(__dirname, "..", "dist"), 24 | filename: "plugin.js", 25 | library: { 26 | name: "PyodidePlugin", 27 | type: "umd", 28 | }, 29 | umdNamedDefine: true, 30 | globalObject: `(typeof self !== 'undefined' ? self : this)`, 31 | }, 32 | performance: { 33 | hints: false, 34 | }, 35 | externalsPresets: { node: true }, 36 | externals: [nodeExternals()], 37 | plugins: [ 38 | new AfterBuild((compiler) => { 39 | // copy the build plugin into the examples folder. This has to happen 40 | // because otherwise webpack will fail on the type of Configuration object not matching in memory 41 | // this is a static type check in the runtime that causing the issue. 42 | moveToExample(compiler.outputPath, "plugin.js"); 43 | }), 44 | ], 45 | resolve: { 46 | extensions: [".ts", ".js"], 47 | }, 48 | module: { 49 | rules: [ 50 | { 51 | test: /\.ts$/, 52 | use: [ 53 | { 54 | loader: "ts-loader", 55 | }, 56 | ], 57 | exclude: /node_modules/, 58 | }, 59 | { 60 | test: /index\.ts$/, 61 | loader: "string-replace-loader", 62 | options: { 63 | search: "path.dirname(url.fileURLToPath(import.meta.url))", 64 | replace: "__dirname", 65 | }, 66 | }, 67 | ], 68 | }, 69 | }); 70 | --------------------------------------------------------------------------------