-
Notifications
You must be signed in to change notification settings - Fork 9
/
index.ts
190 lines (181 loc) · 6.35 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
import fs from "fs";
import url from "url";
import assert from "assert";
import path from "path";
import CopyPlugin from "copy-webpack-plugin";
import webpack from "webpack";
import * as patterns from "./lib/patterns";
import { createRequire } from "node:module";
function noop(_) {
return _;
}
let dirname;
try {
// @ts-ignore import.meta is only available in esm...
dirname = path.dirname(url.fileURLToPath(import.meta.url));
} catch (e) {
noop(e);
}
interface PyodideOptions extends Partial<CopyPlugin.PluginOptions> {
/**
* 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.
*
* @default https://cdn.jsdelivr.net/pyodide/v${installedPyodideVersion}/full/
*/
packageIndexUrl?: string;
/**
* 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).
*
* @default false
*/
globalLoadPyodide?: boolean;
/**
* Relative path to webpack root where you want to output the pyodide files.
* Defaults to pyodide
*/
outDirectory?: string;
/**
* Pyodide package version to use when resolving the default pyodide package index url. Default
* version is whatever version is installed in {pyodideDependencyPath}
*/
version?: string;
/**
* Path on disk to the pyodide module. By default the plugin will attempt to look
* in ./node_modules for pyodide.
*/
pyodideDependencyPath?: string;
}
export class PyodidePlugin extends CopyPlugin {
readonly globalLoadPyodide: boolean;
constructor(options: PyodideOptions = {}) {
let outDirectory = options.outDirectory || "pyodide";
if (outDirectory.startsWith("/")) {
outDirectory = outDirectory.slice(1);
}
const globalLoadPyodide = options.globalLoadPyodide || false;
const pyodidePackagePath = tryGetPyodidePath(options.pyodideDependencyPath);
const pkg = tryResolvePyodidePackage(pyodidePackagePath, options.version);
options.patterns = patterns.chooseAndTransform(pkg, options.packageIndexUrl).map((pattern) => {
return {
from: path.resolve(pyodidePackagePath, pattern.from),
to: path.join(outDirectory, pattern.to),
transform: pattern.transform,
};
});
assert.ok(options.patterns.length > 0, `Unsupported version of pyodide. Must use >=${patterns.versions[0]}`);
// we have to delete all pyodide plugin options before calling super. Rest of options passed to copy webpack plugin
delete options.packageIndexUrl;
delete options.globalLoadPyodide;
delete options.outDirectory;
delete options.version;
delete options.pyodideDependencyPath;
super(options as Required<PyodideOptions>);
this.globalLoadPyodide = globalLoadPyodide;
}
apply(compiler: webpack.Compiler) {
super.apply(compiler);
compiler.hooks.compilation.tap(this.constructor.name, (compilation) => {
const compilationHooks = webpack.NormalModule.getCompilationHooks(compilation);
compilationHooks.beforeLoaders.tap(this.constructor.name, (loaders, normalModule) => {
const matches = normalModule.userRequest.match(/pyodide\.m?js$/);
if (matches) {
// add a new loader specifically to handle pyodide.m?js. See loader.ts for functionalidy
loaders.push({
loader: path.resolve(dirname, "loader.cjs"),
options: {
globalLoadPyodide: this.globalLoadPyodide,
isModule: matches[0].endsWith(".mjs"),
},
ident: "pyodide",
type: null,
});
}
});
});
}
}
/**
* Try to find the pyodide path. Can't use require.resolve because it is not supported in
* module builds. Nodes import.meta.resolve is experimental and still very new as of node 19.x
* This method is works universally under the assumption of an install in node_modules/pyodide
* @param pyodidePath
* @returns
*/
function tryGetPyodidePath(pyodidePath?: string) {
if (pyodidePath) {
return path.resolve(pyodidePath);
}
let pyodideEntrypoint = "";
if (typeof require) {
try {
pyodideEntrypoint = __non_webpack_require__.resolve("pyodide");
} catch (e) {
noop(e);
}
} else {
try {
// @ts-ignore import.meta is only available in esm...
const r = createRequire(import.meta.url);
pyodideEntrypoint = r.resolve("pyodide");
} catch (e) {
noop(e);
}
}
const walk = (p: string) => {
const stat = fs.statSync(p);
if (stat.isFile()) {
return walk(path.dirname(p));
}
if (stat.isDirectory()) {
if (path.basename(p) === "node_modules") {
throw new Error(
"unable to locate pyodide package. You can define it manually with pyodidePath if you're trying to test something novel"
);
}
for (const dirent of fs.readdirSync(p, { withFileTypes: true })) {
if (dirent.name !== "package.json" || dirent.isDirectory()) {
continue;
}
try {
const pkg = fs.readFileSync(path.join(p, dirent.name), "utf-8");
const pkgJson = JSON.parse(pkg);
if (pkgJson.name === "pyodide") {
// found pyodide package root. Exit this thing
return p;
}
} catch (e) {
throw new Error(
"unable to locate and parse pyodide package.json. You can define it manually with pyodidePath if you're trying to test something novel"
);
}
}
return walk(path.dirname(p));
}
};
return walk(pyodideEntrypoint);
}
/**
* Read the pyodide package dependency package.json to return necessary metadata
* @param version
* @returns
*/
function tryResolvePyodidePackage(pyodidePath: string, version?: string) {
if (version) {
return { version };
}
const pkgPath = path.resolve(pyodidePath, "package.json");
try {
const pkg = fs.readFileSync(pkgPath, "utf-8");
return JSON.parse(pkg);
} catch (e) {
throw new Error(`unable to read package.json from pyodide dependency in ${pkgPath}`);
}
}
export default PyodidePlugin;