http-modules 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## 0.0.1 - 2026-07-15
4
+
5
+ Initial release.
6
+
7
+ - Added Bun support for `https://` imports through a preloadable `Bun.plugin`.
8
+ - Added Node.js support through asynchronous module customization hooks and a `node --import` registration entrypoint.
9
+ - Added shared remote module fetching, HTTP error handling, runtime format detection, and relative import rewriting for nested remote modules.
10
+ - Added package entrypoints for the side-effect-free root API, Bun preload, Node hooks, and Node registration.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # http-modules
2
+
3
+ `http-modules` lets JavaScript runtimes import modules directly from `https://` URLs.
4
+
5
+ The package currently supports:
6
+
7
+ - Bun, through a preloadable `Bun.plugin`.
8
+ - Node.js, through module customization hooks.
9
+ - Remote JavaScript and TypeScript modules.
10
+ - Relative imports inside fetched remote modules.
11
+
12
+ ## Why this exists
13
+
14
+ Browsers and Deno can import URL-based modules as first-class module specifiers. Bun and Node.js normally expect package, file, or built-in specifiers. This package fills that gap for runtime code that wants to write normal static imports against HTTPS module URLs:
15
+
16
+ ```ts
17
+ import { nanoid } from "https://cdn.jsdelivr.net/npm/nanoid@5.0.7/index.browser.js";
18
+ ```
19
+
20
+ ## Install
21
+
22
+ ```sh
23
+ bun add http-modules
24
+ ```
25
+
26
+ ## Bun
27
+
28
+ Preload the Bun plugin before any static HTTPS imports are resolved:
29
+
30
+ ```toml
31
+ # bunfig.toml
32
+ preload = ["http-modules/bun"]
33
+ ```
34
+
35
+ Then import HTTPS modules directly:
36
+
37
+ ```ts
38
+ import { nanoid } from "https://cdn.jsdelivr.net/npm/nanoid@5.0.7/index.browser.js";
39
+
40
+ console.log(nanoid());
41
+ ```
42
+
43
+ For local development in this repo, `bunfig.toml` preloads `./src/bun.ts` so the source plugin is exercised directly.
44
+
45
+ ## Node.js
46
+
47
+ Preload the Node registration entrypoint:
48
+
49
+ ```sh
50
+ node --import http-modules/node/register ./app.mjs
51
+ ```
52
+
53
+ Then import HTTPS modules directly:
54
+
55
+ ```js
56
+ import { basename } from "https://deno.land/std@0.224.0/path/mod.ts";
57
+
58
+ console.log(basename("/tmp/example.ts"));
59
+ ```
60
+
61
+ Node.js support uses asynchronous module customization hooks. Remote TypeScript modules require Node.js `>=22.6`, because Node must understand the `module-typescript` loader format.
62
+
63
+ If you need to register hooks yourself, use:
64
+
65
+ ```js
66
+ import { register } from "node:module";
67
+
68
+ register("http-modules/node/hooks", import.meta.url);
69
+ ```
70
+
71
+ ## Package entrypoints
72
+
73
+ | Export | Purpose |
74
+ | --- | --- |
75
+ | `http-modules` | Shared helper exports. No runtime side effects. |
76
+ | `http-modules/bun` | Registers the Bun HTTPS import plugin. Use as a Bun preload. |
77
+ | `http-modules/node/register` | Registers Node customization hooks. Use with `node --import`. |
78
+ | `http-modules/node/hooks` | Raw Node hook module for manual registration. |
79
+
80
+ ## Behavior
81
+
82
+ - Only `https://` module specifiers are handled.
83
+ - HTTP errors fail module loading with the response status.
84
+ - Remote JavaScript and TypeScript sources have relative import specifiers rewritten to absolute HTTPS URLs before evaluation. This keeps nested remote imports working across both runtimes.
85
+ - JSON modules are loaded as JSON where the runtime supports that loader format.
86
+ - No cache is implemented yet. Every process fetches remote modules through the runtime loader path.
87
+
88
+ ## Development
89
+
90
+ This repo uses Bun for package management and build tasks, with mise pinning Bun and Node versions.
91
+
92
+ ```sh
93
+ mise install
94
+ bun install
95
+ bun run build
96
+ bun run test
97
+ bun run typecheck
98
+ ```
99
+
100
+ Equivalent mise tasks:
101
+
102
+ ```sh
103
+ mise run build
104
+ mise run test
105
+ mise run typecheck
106
+ ```
107
+
108
+ `bun run test` builds `dist/` and then runs the cross-runtime test suite. The tests start a local HTTPS server with fixture modules, then verify both Bun and Node can import the same remote module graph without depending on third-party networks.
109
+
110
+ ## Repository layout
111
+
112
+ ```txt
113
+ src/shared.ts Shared fetch, format detection, and import-rewrite logic.
114
+ src/bun.ts Bun plugin entrypoint.
115
+ src/node/hooks.ts Node async resolve/load hooks.
116
+ src/node/register.ts Node --import registration entrypoint.
117
+ src/index.ts Side-effect-free public helper exports.
118
+ ```
119
+
120
+ Build output is written to `dist/` and included in the published package.
package/dist/bun.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/bun.js ADDED
@@ -0,0 +1,116 @@
1
+ // src/bun.ts
2
+ import { plugin } from "bun";
3
+
4
+ // src/shared.ts
5
+ var RELATIVE_IMPORT_SPECIFIER = /(\b(?:from|import)\s*(?:\(\s*)?)(["'])(\.{1,2}\/[^"']+)\2/g;
6
+ function isHttpsSpecifier(specifier) {
7
+ return specifier.startsWith("https://");
8
+ }
9
+ function isRelativeSpecifier(specifier) {
10
+ return specifier.startsWith("./") || specifier.startsWith("../");
11
+ }
12
+ function toFetchUrl(namespacePath) {
13
+ if (namespacePath.startsWith("//")) {
14
+ return `https:${namespacePath}`;
15
+ }
16
+ if (namespacePath.startsWith("https://")) {
17
+ return namespacePath;
18
+ }
19
+ throw new Error(`Expected an HTTPS URL, received ${namespacePath}`);
20
+ }
21
+ function toNamespacePath(url) {
22
+ if (url.startsWith("https://")) {
23
+ return url.slice("https:".length);
24
+ }
25
+ if (url.startsWith("//")) {
26
+ return url;
27
+ }
28
+ throw new Error(`Expected an HTTPS URL, received ${url}`);
29
+ }
30
+ function resolveHttpsSpecifier(specifier, parentUrl) {
31
+ if (isHttpsSpecifier(specifier)) {
32
+ return specifier;
33
+ }
34
+ if (parentUrl?.startsWith("https://") && isRelativeSpecifier(specifier)) {
35
+ return new URL(specifier, parentUrl).href;
36
+ }
37
+ return;
38
+ }
39
+ function rewriteRelativeImportSpecifiers(source, importerUrl) {
40
+ return source.replace(RELATIVE_IMPORT_SPECIFIER, (_specifier, prefix, quote, path) => `${prefix}${quote}${new URL(path, importerUrl).href}${quote}`);
41
+ }
42
+ function bunLoaderFor(url, contentType) {
43
+ const pathname = new URL(url).pathname;
44
+ if (contentType?.includes("application/json") || pathname.endsWith(".json")) {
45
+ return "json";
46
+ }
47
+ if (pathname.endsWith(".ts") || pathname.endsWith(".mts") || contentType?.includes("typescript")) {
48
+ return "ts";
49
+ }
50
+ if (pathname.endsWith(".tsx")) {
51
+ return "tsx";
52
+ }
53
+ if (pathname.endsWith(".jsx")) {
54
+ return "jsx";
55
+ }
56
+ return "js";
57
+ }
58
+ function nodeFormatFor(url, contentType) {
59
+ const pathname = new URL(url).pathname;
60
+ if (contentType?.includes("application/json") || pathname.endsWith(".json")) {
61
+ return "json";
62
+ }
63
+ if (pathname.endsWith(".ts") || pathname.endsWith(".mts") || contentType?.includes("typescript")) {
64
+ return "module-typescript";
65
+ }
66
+ return "module";
67
+ }
68
+ function rewritesImports(loader) {
69
+ return loader === "js" || loader === "jsx" || loader === "ts" || loader === "tsx";
70
+ }
71
+ async function fetchRemoteModule(url) {
72
+ const response = await fetch(url);
73
+ if (!response.ok) {
74
+ throw new Error(`Failed to load ${url}: ${response.status} ${response.statusText}`);
75
+ }
76
+ const contentType = response.headers.get("content-type");
77
+ const bunLoader = bunLoaderFor(url, contentType);
78
+ const source = await response.text();
79
+ return {
80
+ bunLoader,
81
+ nodeFormat: nodeFormatFor(url, contentType),
82
+ source: rewritesImports(bunLoader) ? rewriteRelativeImportSpecifiers(source, url) : source
83
+ };
84
+ }
85
+
86
+ // src/bun.ts
87
+ var HTTPS_NAMESPACE = "https";
88
+ plugin({
89
+ name: "https-imports",
90
+ setup(build) {
91
+ build.onResolve({ filter: /^https:\/\// }, (args) => ({
92
+ path: toNamespacePath(args.path),
93
+ namespace: HTTPS_NAMESPACE
94
+ }));
95
+ build.onResolve({ filter: /.*/, namespace: HTTPS_NAMESPACE }, (args) => {
96
+ if (args.path.startsWith("https://") || args.path.startsWith("//")) {
97
+ return {
98
+ path: toNamespacePath(args.path),
99
+ namespace: HTTPS_NAMESPACE
100
+ };
101
+ }
102
+ const importer = toFetchUrl(args.importer);
103
+ return {
104
+ path: toNamespacePath(new URL(args.path, importer).href),
105
+ namespace: HTTPS_NAMESPACE
106
+ };
107
+ });
108
+ build.onLoad({ filter: /.*/, namespace: HTTPS_NAMESPACE }, async (args) => {
109
+ const remoteModule = await fetchRemoteModule(toFetchUrl(args.path));
110
+ return {
111
+ contents: remoteModule.source,
112
+ loader: remoteModule.bunLoader
113
+ };
114
+ });
115
+ }
116
+ });
@@ -0,0 +1,2 @@
1
+ export type { BunRemoteLoader, NodeRemoteFormat, RemoteModule } from "./shared";
2
+ export { fetchRemoteModule, resolveHttpsSpecifier } from "./shared";
package/dist/index.js ADDED
@@ -0,0 +1,85 @@
1
+ // src/shared.ts
2
+ var RELATIVE_IMPORT_SPECIFIER = /(\b(?:from|import)\s*(?:\(\s*)?)(["'])(\.{1,2}\/[^"']+)\2/g;
3
+ function isHttpsSpecifier(specifier) {
4
+ return specifier.startsWith("https://");
5
+ }
6
+ function isRelativeSpecifier(specifier) {
7
+ return specifier.startsWith("./") || specifier.startsWith("../");
8
+ }
9
+ function toFetchUrl(namespacePath) {
10
+ if (namespacePath.startsWith("//")) {
11
+ return `https:${namespacePath}`;
12
+ }
13
+ if (namespacePath.startsWith("https://")) {
14
+ return namespacePath;
15
+ }
16
+ throw new Error(`Expected an HTTPS URL, received ${namespacePath}`);
17
+ }
18
+ function toNamespacePath(url) {
19
+ if (url.startsWith("https://")) {
20
+ return url.slice("https:".length);
21
+ }
22
+ if (url.startsWith("//")) {
23
+ return url;
24
+ }
25
+ throw new Error(`Expected an HTTPS URL, received ${url}`);
26
+ }
27
+ function resolveHttpsSpecifier(specifier, parentUrl) {
28
+ if (isHttpsSpecifier(specifier)) {
29
+ return specifier;
30
+ }
31
+ if (parentUrl?.startsWith("https://") && isRelativeSpecifier(specifier)) {
32
+ return new URL(specifier, parentUrl).href;
33
+ }
34
+ return;
35
+ }
36
+ function rewriteRelativeImportSpecifiers(source, importerUrl) {
37
+ return source.replace(RELATIVE_IMPORT_SPECIFIER, (_specifier, prefix, quote, path) => `${prefix}${quote}${new URL(path, importerUrl).href}${quote}`);
38
+ }
39
+ function bunLoaderFor(url, contentType) {
40
+ const pathname = new URL(url).pathname;
41
+ if (contentType?.includes("application/json") || pathname.endsWith(".json")) {
42
+ return "json";
43
+ }
44
+ if (pathname.endsWith(".ts") || pathname.endsWith(".mts") || contentType?.includes("typescript")) {
45
+ return "ts";
46
+ }
47
+ if (pathname.endsWith(".tsx")) {
48
+ return "tsx";
49
+ }
50
+ if (pathname.endsWith(".jsx")) {
51
+ return "jsx";
52
+ }
53
+ return "js";
54
+ }
55
+ function nodeFormatFor(url, contentType) {
56
+ const pathname = new URL(url).pathname;
57
+ if (contentType?.includes("application/json") || pathname.endsWith(".json")) {
58
+ return "json";
59
+ }
60
+ if (pathname.endsWith(".ts") || pathname.endsWith(".mts") || contentType?.includes("typescript")) {
61
+ return "module-typescript";
62
+ }
63
+ return "module";
64
+ }
65
+ function rewritesImports(loader) {
66
+ return loader === "js" || loader === "jsx" || loader === "ts" || loader === "tsx";
67
+ }
68
+ async function fetchRemoteModule(url) {
69
+ const response = await fetch(url);
70
+ if (!response.ok) {
71
+ throw new Error(`Failed to load ${url}: ${response.status} ${response.statusText}`);
72
+ }
73
+ const contentType = response.headers.get("content-type");
74
+ const bunLoader = bunLoaderFor(url, contentType);
75
+ const source = await response.text();
76
+ return {
77
+ bunLoader,
78
+ nodeFormat: nodeFormatFor(url, contentType),
79
+ source: rewritesImports(bunLoader) ? rewriteRelativeImportSpecifiers(source, url) : source
80
+ };
81
+ }
82
+ export {
83
+ resolveHttpsSpecifier,
84
+ fetchRemoteModule
85
+ };
@@ -0,0 +1,21 @@
1
+ interface ResolveContext {
2
+ parentURL?: string;
3
+ }
4
+ interface ResolveResult {
5
+ format?: string;
6
+ shortCircuit?: boolean;
7
+ url: string;
8
+ }
9
+ interface LoadContext {
10
+ format?: string;
11
+ }
12
+ interface LoadResult {
13
+ format: string;
14
+ shortCircuit?: boolean;
15
+ source: string;
16
+ }
17
+ type NextResolve = (specifier: string, context: ResolveContext) => Promise<ResolveResult>;
18
+ type NextLoad = (url: string, context: LoadContext) => Promise<LoadResult>;
19
+ export declare function resolve(specifier: string, context: ResolveContext, nextResolve: NextResolve): Promise<ResolveResult>;
20
+ export declare function load(url: string, context: LoadContext, nextLoad: NextLoad): Promise<LoadResult>;
21
+ export {};
@@ -0,0 +1,109 @@
1
+ // src/shared.ts
2
+ var RELATIVE_IMPORT_SPECIFIER = /(\b(?:from|import)\s*(?:\(\s*)?)(["'])(\.{1,2}\/[^"']+)\2/g;
3
+ function isHttpsSpecifier(specifier) {
4
+ return specifier.startsWith("https://");
5
+ }
6
+ function isRelativeSpecifier(specifier) {
7
+ return specifier.startsWith("./") || specifier.startsWith("../");
8
+ }
9
+ function toFetchUrl(namespacePath) {
10
+ if (namespacePath.startsWith("//")) {
11
+ return `https:${namespacePath}`;
12
+ }
13
+ if (namespacePath.startsWith("https://")) {
14
+ return namespacePath;
15
+ }
16
+ throw new Error(`Expected an HTTPS URL, received ${namespacePath}`);
17
+ }
18
+ function toNamespacePath(url) {
19
+ if (url.startsWith("https://")) {
20
+ return url.slice("https:".length);
21
+ }
22
+ if (url.startsWith("//")) {
23
+ return url;
24
+ }
25
+ throw new Error(`Expected an HTTPS URL, received ${url}`);
26
+ }
27
+ function resolveHttpsSpecifier(specifier, parentUrl) {
28
+ if (isHttpsSpecifier(specifier)) {
29
+ return specifier;
30
+ }
31
+ if (parentUrl?.startsWith("https://") && isRelativeSpecifier(specifier)) {
32
+ return new URL(specifier, parentUrl).href;
33
+ }
34
+ return;
35
+ }
36
+ function rewriteRelativeImportSpecifiers(source, importerUrl) {
37
+ return source.replace(RELATIVE_IMPORT_SPECIFIER, (_specifier, prefix, quote, path) => `${prefix}${quote}${new URL(path, importerUrl).href}${quote}`);
38
+ }
39
+ function bunLoaderFor(url, contentType) {
40
+ const pathname = new URL(url).pathname;
41
+ if (contentType?.includes("application/json") || pathname.endsWith(".json")) {
42
+ return "json";
43
+ }
44
+ if (pathname.endsWith(".ts") || pathname.endsWith(".mts") || contentType?.includes("typescript")) {
45
+ return "ts";
46
+ }
47
+ if (pathname.endsWith(".tsx")) {
48
+ return "tsx";
49
+ }
50
+ if (pathname.endsWith(".jsx")) {
51
+ return "jsx";
52
+ }
53
+ return "js";
54
+ }
55
+ function nodeFormatFor(url, contentType) {
56
+ const pathname = new URL(url).pathname;
57
+ if (contentType?.includes("application/json") || pathname.endsWith(".json")) {
58
+ return "json";
59
+ }
60
+ if (pathname.endsWith(".ts") || pathname.endsWith(".mts") || contentType?.includes("typescript")) {
61
+ return "module-typescript";
62
+ }
63
+ return "module";
64
+ }
65
+ function rewritesImports(loader) {
66
+ return loader === "js" || loader === "jsx" || loader === "ts" || loader === "tsx";
67
+ }
68
+ async function fetchRemoteModule(url) {
69
+ const response = await fetch(url);
70
+ if (!response.ok) {
71
+ throw new Error(`Failed to load ${url}: ${response.status} ${response.statusText}`);
72
+ }
73
+ const contentType = response.headers.get("content-type");
74
+ const bunLoader = bunLoaderFor(url, contentType);
75
+ const source = await response.text();
76
+ return {
77
+ bunLoader,
78
+ nodeFormat: nodeFormatFor(url, contentType),
79
+ source: rewritesImports(bunLoader) ? rewriteRelativeImportSpecifiers(source, url) : source
80
+ };
81
+ }
82
+
83
+ // src/node/hooks.ts
84
+ async function resolve(specifier, context, nextResolve) {
85
+ const url = resolveHttpsSpecifier(specifier, context.parentURL);
86
+ if (url) {
87
+ return {
88
+ format: nodeFormatFor(url, null),
89
+ shortCircuit: true,
90
+ url
91
+ };
92
+ }
93
+ return nextResolve(specifier, context);
94
+ }
95
+ async function load(url, context, nextLoad) {
96
+ if (!url.startsWith("https://")) {
97
+ return nextLoad(url, context);
98
+ }
99
+ const remoteModule = await fetchRemoteModule(url);
100
+ return {
101
+ format: remoteModule.nodeFormat,
102
+ shortCircuit: true,
103
+ source: remoteModule.source
104
+ };
105
+ }
106
+ export {
107
+ resolve,
108
+ load
109
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ // src/node/register.ts
2
+ import { register } from "node:module";
3
+ register("./hooks.js", import.meta.url);
@@ -0,0 +1,17 @@
1
+ export type BunRemoteLoader = "js" | "jsx" | "ts" | "tsx" | "json";
2
+ export type NodeRemoteFormat = "module" | "module-typescript" | "json";
3
+ export interface RemoteModule {
4
+ bunLoader: BunRemoteLoader;
5
+ nodeFormat: NodeRemoteFormat;
6
+ source: string;
7
+ }
8
+ export declare function isHttpsSpecifier(specifier: string): boolean;
9
+ export declare function isRelativeSpecifier(specifier: string): boolean;
10
+ export declare function toFetchUrl(namespacePath: string): string;
11
+ export declare function toNamespacePath(url: string): string;
12
+ export declare function resolveHttpsSpecifier(specifier: string, parentUrl?: string): string | undefined;
13
+ export declare function rewriteRelativeImportSpecifiers(source: string, importerUrl: string): string;
14
+ export declare function bunLoaderFor(url: string, contentType: string | null): BunRemoteLoader;
15
+ export declare function nodeFormatFor(url: string, contentType: string | null): NodeRemoteFormat;
16
+ export declare function rewritesImports(loader: BunRemoteLoader): boolean;
17
+ export declare function fetchRemoteModule(url: string): Promise<RemoteModule>;
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "http-modules",
3
+ "version": "0.0.1",
4
+ "description": "Import HTTPS modules in Bun and Node.js.",
5
+ "type": "module",
6
+ "sideEffects": [
7
+ "./dist/bun.js",
8
+ "./dist/node/register.js"
9
+ ],
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ },
15
+ "./bun": {
16
+ "types": "./dist/bun.d.ts",
17
+ "import": "./dist/bun.js"
18
+ },
19
+ "./node/hooks": {
20
+ "types": "./dist/node/hooks.d.ts",
21
+ "import": "./dist/node/hooks.js"
22
+ },
23
+ "./node/register": {
24
+ "types": "./dist/node/register.d.ts",
25
+ "import": "./dist/node/register.js"
26
+ }
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md",
31
+ "CHANGELOG.md"
32
+ ],
33
+ "scripts": {
34
+ "build": "rm -rf ./dist && bun build ./src/index.ts ./src/bun.ts ./src/node/hooks.ts ./src/node/register.ts --outdir ./dist --target node --format esm --packages external && bun x tsc --project tsconfig.build.json",
35
+ "test": "bun run build && bun test",
36
+ "typecheck": "bun x tsc --noEmit"
37
+ },
38
+ "engines": {
39
+ "bun": ">=1.3.1",
40
+ "node": ">=22.6"
41
+ },
42
+ "devDependencies": {
43
+ "@types/bun": "latest",
44
+ "@types/node": "latest",
45
+ "typescript": "^5"
46
+ }
47
+ }