Skip to content

Commit b3af9b8

Browse files
authored
add esm.sh fallback (#302)
1 parent bd459ce commit b3af9b8

File tree

7 files changed

+162
-23
lines changed

7 files changed

+162
-23
lines changed

packages/editor/src/runtime/executor/resolver/LocalResolver.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ async function resolveNestedModule(id: string, mode?: string) {
1818
// Any import if React and related libraries, we want to resolve to the
1919
// local imported React. Otherwise we get multiple instances of React, which breaks things
2020
// (plus, it's inefficient to load the library from a CDN)
21-
if (id === "react" && (!mode || mode === "imports/optimized/react.js")) {
21+
if (
22+
id === "react" &&
23+
(!mode ||
24+
mode === "imports/optimized/react.js" ||
25+
mode === "es2021/react.js")
26+
) {
2227
return react;
2328
}
2429

packages/editor/src/runtime/executor/resolver/resolver.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@ import {
44
ImportShimResolver,
55
ResolvedImport,
66
SkypackResolver,
7+
// JSPMResolver,
8+
ESMshResolver,
79
} from "@typecell-org/engine";
810
import getExposeGlobalVariables from "../lib/exports";
911
import { LocalResolver } from "./LocalResolver";
1012
import { TypeCellCompiledCodeProvider } from "./typecell/TypeCellCompiledCodeProvider";
1113

1214
// Used for resolving NPM imports
15+
const esmshResolver = new ESMshResolver();
1316
const skypackResolver = new SkypackResolver();
17+
// const jspmResolver = new JSPMResolver();
1418
const importShimResolver = new ImportShimResolver(
15-
[skypackResolver],
19+
[skypackResolver, esmshResolver],
1620
LocalResolver
1721
);
1822

packages/engine/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export * from "./Engine";
22
export * from "./CodeModel";
3-
export * from "./resolvers/SkypackResolver";
3+
export * from "./resolvers/cdns/SkypackResolver";
4+
export * from "./resolvers/cdns/JSPMResolver";
5+
export * from "./resolvers/cdns/ESMshResolver";
46
export * from "./resolvers/LocalModuleResolver";
57
export * from "./resolvers/ImportShimResolver";
68
export * from "./reactView";

packages/engine/src/resolvers/ImportShimResolver.ts

Lines changed: 71 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,42 @@ export class ImportShimResolver {
4141
dispose: () => {},
4242
};
4343
}
44-
return {
45-
module: await this.importShim(moduleName),
46-
dispose: () => {},
47-
};
44+
45+
/*
46+
Let's try different resolvers one by one.
47+
Because there is only one global importShim,
48+
we use a hack to pass the resolver we're trying to importShim()
49+
as part of the module name, e.g.: use$SkypackResolver$lodash
50+
51+
This is decoded again in onImportShimResolve below
52+
*/
53+
for (let resolver of this.resolvers) {
54+
try {
55+
const module = await this.importShim(
56+
"use$" + resolver.constructor.name + "$" + moduleName
57+
);
58+
console.log(
59+
"loaded module",
60+
moduleName,
61+
"using",
62+
resolver.constructor.name
63+
);
64+
return {
65+
module,
66+
dispose: () => {},
67+
};
68+
} catch (e) {
69+
console.error("failed loading module", resolver.constructor.name, e);
70+
}
71+
}
72+
throw new Error("couldn't resolve module" + moduleName);
4873
}
4974

5075
/**
5176
* This is called by es-module-shims whenever it wants to resolve an Import.
5277
*/
5378
private async doResolveImportURL(
79+
resolver: ExternalModuleResolver,
5480
moduleName: string, // can be a relative URL, absolute URL, or "package name"
5581
parent: string, // the parent URL the package is loaded from
5682
importShimResolve: any // the original resolve function from es-module-shims
@@ -72,8 +98,8 @@ export class ImportShimResolver {
7298
const defaultURL = await importShimResolve(moduleName, parent);
7399

74100
// Try the registered resolvers
75-
for (let resolver of this.resolvers) {
76-
if (defaultURL) {
101+
if (defaultURL) {
102+
for (let resolver of this.resolvers) {
77103
// Does the URL we're trying to load match with the resolver?
78104
const parsedModule = await resolver.getModuleInfoFromURL(defaultURL);
79105
if (parsedModule) {
@@ -92,21 +118,22 @@ export class ImportShimResolver {
92118
const parsedParent = await resolver.getModuleInfoFromURL(parent);
93119
if (parsedParent?.module === "react-map-gl") {
94120
return this.doResolveImportURL(
121+
resolver,
95122
"maplibre-gl",
96123
parent,
97124
importShimResolve
98125
);
99126
}
100127
}
101128
}
102-
} else {
103-
// moduleName + parent combination couldn't be resolved by es-module-shims
104-
// (i.e.: it's not an absolute URL, but just a package name like "lodash")
105-
// Try to get a CDN URL from our resolver
106-
const resolverURL = await resolver.getURLForModule(moduleName, parent);
107-
if (resolverURL) {
108-
return resolverURL;
109-
}
129+
}
130+
} else {
131+
// moduleName + parent combination couldn't be resolved by es-module-shims
132+
// (i.e.: it's not an absolute URL, but just a package name like "lodash")
133+
// Try to get a CDN URL from our resolver
134+
const resolverURL = await resolver.getURLForModule(moduleName, parent);
135+
if (resolverURL) {
136+
return resolverURL;
110137
}
111138
}
112139

@@ -169,12 +196,41 @@ export class ImportShimResolver {
169196
return url;
170197
}
171198

199+
/**
200+
* The hook called by es-module-shims
201+
*/
172202
private onImportShimResolve = async (
173203
id: string,
174204
parent: string,
175205
importShimResolve: any
176206
) => {
177-
const ret = await this.doResolveImportURL(id, parent, importShimResolve);
207+
// by default, try the first resolver
208+
let resolver = this.resolvers[0];
209+
210+
if (id.startsWith("use$")) {
211+
// an explicit resolver was passed in (see doResolveImport)
212+
const parts = id.split("$");
213+
if (parts.length !== 3) {
214+
throw new Error("expected resolver name in import" + id);
215+
}
216+
resolver = this.resolvers.find((r) => r.constructor.name === parts[1])!;
217+
id = parts[2];
218+
} else {
219+
if (!parent) {
220+
// when there is a parent, this is expected (because it's a nested module).
221+
console.warn(
222+
"no explicit resolver detected in import, falling back to default",
223+
id
224+
);
225+
}
226+
}
227+
228+
const ret = await this.doResolveImportURL(
229+
resolver,
230+
id,
231+
parent,
232+
importShimResolve
233+
);
178234
return ret;
179235
};
180236

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { ExternalModuleResolver } from "../ExternalModuleResolver";
2+
3+
export class ESMshResolver extends ExternalModuleResolver {
4+
public async getModuleInfoFromURL(url: string) {
5+
// https://cdn.esm.sh/v66/@tldraw/[email protected]/es2021/core.js
6+
7+
const prefix = "https://cdn.esm.sh/";
8+
if (url.startsWith(prefix)) {
9+
url = url.substring(prefix.length - 1);
10+
let mode: string | undefined;
11+
let library = url.match(/^\/v\d+\/[^\/]+\.js$/);
12+
if (library) {
13+
return undefined;
14+
}
15+
let matches = url.match(/^\/v\d+\/(.*)@[.\d]+\/(.*)$/);
16+
if (!matches || !matches[1]) {
17+
throw new Error("couldn't match url");
18+
}
19+
const matchedModuleName = matches[1];
20+
21+
// mode is necessary for jsx-runtime, e.g.: @yousef/use-p2
22+
mode = matches[2];
23+
24+
return {
25+
module: matchedModuleName,
26+
mode,
27+
};
28+
}
29+
return undefined;
30+
}
31+
32+
public async getURLForModule(moduleName: string, parent: string) {
33+
if (moduleName.startsWith("https://") || moduleName.startsWith("../")) {
34+
throw new Error("unexpected modulename");
35+
}
36+
return `https://esm.sh/${moduleName}`;
37+
}
38+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { ExternalModuleResolver } from "../ExternalModuleResolver";
2+
3+
export class JSPMResolver extends ExternalModuleResolver {
4+
public async getModuleInfoFromURL(url: string) {
5+
// TODO: should also pass version identifier (@xx)
6+
7+
const prefix = "https://jspm.dev/";
8+
if (url.startsWith(prefix)) {
9+
url = url.substring(prefix.length - 1);
10+
let mode: string | undefined;
11+
let matches = url.match(/^\/npm:(.*)$/);
12+
if (!matches || !matches[1]) {
13+
throw new Error("couldn't match url");
14+
}
15+
const matchedModuleName = matches[1];
16+
17+
// mode is necessary for jsx-runtime, e.g.: @yousef/use-p2
18+
mode = undefined; //matches[3];
19+
20+
return {
21+
module: matchedModuleName,
22+
mode,
23+
};
24+
}
25+
return undefined;
26+
}
27+
28+
public async getURLForModule(moduleName: string, parent: string) {
29+
if (moduleName.startsWith("https://") || moduleName.startsWith("../")) {
30+
throw new Error("unexpected modulename");
31+
}
32+
return `https://jspm.dev/npm:${moduleName}`;
33+
}
34+
}

packages/engine/src/resolvers/SkypackResolver.ts renamed to packages/engine/src/resolvers/cdns/SkypackResolver.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ExternalModuleResolver } from "./ExternalModuleResolver";
1+
import { ExternalModuleResolver } from "../ExternalModuleResolver";
22

33
export class SkypackResolver extends ExternalModuleResolver {
44
public async getModuleInfoFromURL(url: string) {
@@ -9,14 +9,14 @@ export class SkypackResolver extends ExternalModuleResolver {
99
if (url.startsWith(prefix)) {
1010
url = url.substring(prefix.length - 1);
1111
let mode: string | undefined;
12-
let matches = url.match(/^\/-\/(.+)@v[\d.]+-.*mode=(.*)$/);
13-
if (!matches || !matches[1]) {
12+
let matches = url.match(/^\/(new|-)\/(.+)@v[\d.]+[-\/].*mode=(.*)$/);
13+
if (!matches || !matches[2]) {
1414
throw new Error("couldn't match url");
1515
}
16-
const matchedModuleName = matches[1];
16+
const matchedModuleName = matches[2];
1717

1818
// mode is necessary for jsx-runtime, e.g.: @yousef/use-p2
19-
mode = matches[2];
19+
mode = matches[3];
2020

2121
return {
2222
module: matchedModuleName,

0 commit comments

Comments
 (0)