Skip to content

Commit ad95f7a

Browse files
committed
Take on dependencies to reduce chance of failure
1 parent 4ac2388 commit ad95f7a

8 files changed

Lines changed: 77 additions & 50 deletions

File tree

packages/dynamic-worker-bundler/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ await worker.getEntrypoint().fetch(request);
5050
| `externals` | `string[]` | `[]` | Additional modules to exclude (`cloudflare:*` always excluded) |
5151
| `minify` | `boolean` | `false` | Minify output |
5252
| `sourcemap` | `boolean` | `false` | Generate inline source maps |
53+
| `registry` | `string` | `'https://registry.npmjs.org'` | npm registry URL |
5354

5455
### Returns
5556

@@ -119,6 +120,10 @@ interface Env {
119120
}
120121
```
121122

123+
## Future Work
124+
125+
- **Lockfile support**: Read `package-lock.json` / `pnpm-lock.yaml` for deterministic installs
126+
122127
## License
123128

124129
MIT

packages/dynamic-worker-bundler/build.mjs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,14 @@ await esbuild.build({
4343
target: 'es2022',
4444
sourcemap: true,
4545
// Mark npm dependencies as external
46-
external: ['esbuild-wasm', 'sucrase', 'resolve.exports', 'smol-toml'],
46+
external: [
47+
'esbuild-wasm',
48+
'sucrase',
49+
'resolve.exports',
50+
'smol-toml',
51+
'es-module-lexer',
52+
'semver',
53+
],
4754
plugins: [
4855
{
4956
name: 'wasm-external',

packages/dynamic-worker-bundler/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@
1919
},
2020
"devDependencies": {
2121
"@types/node": "catalog:",
22+
"@types/semver": "^7.0.0",
2223
"esbuild": "^0.27.0",
2324
"typescript": "catalog:"
2425
},
2526
"dependencies": {
27+
"es-module-lexer": "^2.0.0",
2628
"esbuild-wasm": "^0.27.0",
2729
"resolve.exports": "^2.0.0",
30+
"semver": "^7.0.0",
2831
"smol-toml": "^1.0.0",
2932
"sucrase": "^3.0.0"
3033
},

packages/dynamic-worker-bundler/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export async function createWorker(options: CreateWorkerOptions): Promise<Create
4141
target = 'es2022',
4242
minify = false,
4343
sourcemap = false,
44+
registry,
4445
} = options;
4546

4647
// Always treat cloudflare:* modules as external (runtime-provided)
@@ -53,7 +54,7 @@ export async function createWorker(options: CreateWorkerOptions): Promise<Create
5354
// Auto-install dependencies if package.json has dependencies
5455
const installWarnings: string[] = [];
5556
if (hasDependencies(files)) {
56-
const installResult = await installDependencies(files);
57+
const installResult = await installDependencies(files, registry ? { registry } : {});
5758
files = installResult.files;
5859
installWarnings.push(...installResult.warnings);
5960
}

packages/dynamic-worker-bundler/src/installer.ts

Lines changed: 4 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* a virtual node_modules directory structure.
66
*/
77

8+
import * as semver from 'semver';
89
import type { Files } from './types.js';
910

1011
const NPM_REGISTRY = 'https://registry.npmjs.org';
@@ -259,54 +260,11 @@ function resolveVersion(range: string, metadata: NpmPackageMetadata): string | u
259260
return metadata['dist-tags'][range];
260261
}
261262

262-
// For ranges like ^1.0.0, ~1.0.0, >=1.0.0, we need to find the best match
263-
// Simple implementation: find the highest version that starts with the major version
263+
// Use semver.maxSatisfying to find the best matching version
264264
const versions = Object.keys(metadata.versions);
265+
const match = semver.maxSatisfying(versions, range);
265266

266-
// Parse the range to extract constraints
267-
const cleanRange = range.replace(/^[\^~>=<]+/, '');
268-
const [majorStr] = cleanRange.split('.');
269-
const major = parseInt(majorStr ?? '0', 10);
270-
271-
// Filter versions that match the major version (for ^ ranges)
272-
// This is a simplified semver matching - in production you'd use a proper semver library
273-
if (range.startsWith('^') || range.startsWith('~')) {
274-
const matchingVersions = versions
275-
.filter((v) => {
276-
const [vMajor] = v.split('.');
277-
return parseInt(vMajor ?? '0', 10) === major;
278-
})
279-
.sort(compareVersions)
280-
.reverse();
281-
282-
return matchingVersions[0];
283-
}
284-
285-
// For >= ranges, get the latest
286-
if (range.startsWith('>=')) {
287-
return metadata['dist-tags']['latest'];
288-
}
289-
290-
// Fallback: try to find any version that might work
291-
const sortedVersions = versions.sort(compareVersions).reverse();
292-
return sortedVersions[0];
293-
}
294-
295-
/**
296-
* Compare two semver versions.
297-
*/
298-
function compareVersions(a: string, b: string): number {
299-
const aParts = a.split('.').map((p) => parseInt(p.replace(/[^0-9]/g, ''), 10) || 0);
300-
const bParts = b.split('.').map((p) => parseInt(p.replace(/[^0-9]/g, ''), 10) || 0);
301-
302-
for (let i = 0; i < 3; i++) {
303-
const aVal = aParts[i] ?? 0;
304-
const bVal = bParts[i] ?? 0;
305-
if (aVal !== bVal) {
306-
return aVal - bVal;
307-
}
308-
}
309-
return 0;
267+
return match ?? undefined;
310268
}
311269

312270
/**

packages/dynamic-worker-bundler/src/resolver.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// Use the asm.js version to avoid WASM (works in workerd)
2+
import { parse } from 'es-module-lexer/js';
13
import * as resolveExports from 'resolve.exports';
24
import type { Files } from './types.js';
35

@@ -282,10 +284,36 @@ function normalizeRelativePath(path: string): string {
282284
/**
283285
* Parse imports from a JavaScript/TypeScript source file.
284286
*
285-
* This is a simple regex-based parser that handles common import patterns.
286-
* It doesn't handle all edge cases but works for most practical use cases.
287+
* Uses es-module-lexer for accurate parsing of ES module syntax.
288+
* Falls back to regex for JSX files since es-module-lexer doesn't
289+
* handle JSX syntax (e.g., `<div>` is not valid JavaScript).
287290
*/
288291
export function parseImports(code: string): string[] {
292+
try {
293+
const [imports] = parse(code);
294+
const specifiers: string[] = [];
295+
296+
for (const imp of imports) {
297+
// imp.n is the resolved module specifier (handles escape sequences)
298+
// imp.n is undefined for dynamic imports with non-string arguments
299+
if (imp.n !== undefined) {
300+
specifiers.push(imp.n);
301+
}
302+
}
303+
304+
return [...new Set(specifiers)]; // Deduplicate
305+
} catch {
306+
// es-module-lexer fails on JSX syntax (<Component />) and malformed code
307+
// Fall back to regex-based parsing
308+
return parseImportsRegex(code);
309+
}
310+
}
311+
312+
/**
313+
* Regex-based fallback for parsing imports.
314+
* Used when es-module-lexer fails (e.g., on JSX/TSX files).
315+
*/
316+
function parseImportsRegex(code: string): string[] {
289317
const imports: string[] = [];
290318

291319
// Match ES module imports

packages/dynamic-worker-bundler/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ export interface CreateWorkerOptions {
6767
* @default false
6868
*/
6969
sourcemap?: boolean;
70+
71+
/**
72+
* npm registry URL for fetching packages.
73+
* @default 'https://registry.npmjs.org'
74+
*/
75+
registry?: string;
7076
}
7177

7278
/**

pnpm-lock.yaml

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)