From 051a2766f5859f8ed0071829d0e4928586fd702b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=83=A5=EB=83=90=EC=B1=A0?= Date: Thu, 17 Oct 2024 11:11:14 +0900 Subject: [PATCH] feat: support specifying npm registries via .npmrc (#427) * chore(sb_module_loader): add a dependency * chore: update `Cargo.lock` * feat: support specifying npm registries via .npmrc * chore: add examples related to the private npm registry import * chore: update examples * stamp: polishing * chore: add an integration test * stamp: polishing * stamp: oops * stamp: meow * fix: find .npmrc by traversing parent directories * chore: add an example * fix: make npm resolver respect `noModuleCache` option * fix: add `--disable-module-cache` flag to bundle command * chore: update an integration test and test cases --- Cargo.lock | 1 + crates/base/src/deno_runtime.rs | 10 +- crates/base/src/lib.rs | 1 + crates/base/src/rt_worker/utils.rs | 14 +- crates/base/src/rt_worker/worker.rs | 6 +- crates/base/src/rt_worker/worker_ctx.rs | 2 +- crates/base/src/server.rs | 3 + crates/base/src/utils.rs | 14 -- crates/base/src/utils/mod.rs | 2 + crates/base/src/utils/path.rs | 23 +++ crates/base/test_cases/main_eszip/index.ts | 2 +- .../test_cases/main_with_registry/index.ts | 74 ++++++++ .../main_with_registry/registry/mock.ts | 1 + .../main_with_registry/registry/mod.ts | 3 + .../registry/registry-handler.ts | 81 +++++++++ .../main_with_registry/registry/template.json | 49 ++++++ .../private-npm-package-import-2/.npmrc | 2 + .../inner/index.js | 13 ++ .../private-npm-package-import/.npmrc | 2 + .../private-npm-package-import/index.js | 13 ++ crates/base/tests/integration_tests.rs | 159 +++++++++++++++++- crates/cli/src/flags.rs | 6 + crates/cli/src/main.rs | 20 ++- crates/npm/cache_dir.rs | 3 +- crates/sb_core/npm.rs | 27 ++- crates/sb_eszip_shared/lib.rs | 1 + crates/sb_fs/lib.rs | 4 +- crates/sb_graph/emitter.rs | 50 ++++-- crates/sb_graph/lib.rs | 42 ++++- crates/sb_module_loader/Cargo.toml | 1 + crates/sb_module_loader/standalone/mod.rs | 38 ++++- examples/.npmrc | 2 + examples/main/index.ts | 7 + examples/main/registry/mock.ts | 1 + examples/main/registry/mod.ts | 3 + examples/main/registry/registry-handler.ts | 81 +++++++++ examples/main/registry/template.json | 49 ++++++ .../private-npm-package-import-2/index.js | 14 ++ examples/private-npm-package-import/.npmrc | 2 + examples/private-npm-package-import/index.js | 13 ++ 40 files changed, 795 insertions(+), 44 deletions(-) delete mode 100644 crates/base/src/utils.rs create mode 100644 crates/base/src/utils/mod.rs create mode 100644 crates/base/src/utils/path.rs create mode 100644 crates/base/test_cases/main_with_registry/index.ts create mode 100644 crates/base/test_cases/main_with_registry/registry/mock.ts create mode 100644 crates/base/test_cases/main_with_registry/registry/mod.ts create mode 100644 crates/base/test_cases/main_with_registry/registry/registry-handler.ts create mode 100644 crates/base/test_cases/main_with_registry/registry/template.json create mode 100644 crates/base/test_cases/private-npm-package-import-2/.npmrc create mode 100644 crates/base/test_cases/private-npm-package-import-2/inner/index.js create mode 100644 crates/base/test_cases/private-npm-package-import/.npmrc create mode 100644 crates/base/test_cases/private-npm-package-import/index.js create mode 100644 examples/.npmrc create mode 100644 examples/main/registry/mock.ts create mode 100644 examples/main/registry/mod.ts create mode 100644 examples/main/registry/registry-handler.ts create mode 100644 examples/main/registry/template.json create mode 100644 examples/private-npm-package-import-2/index.js create mode 100644 examples/private-npm-package-import/.npmrc create mode 100644 examples/private-npm-package-import/index.js diff --git a/Cargo.lock b/Cargo.lock index 8c0dce776..22ad6cefe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5144,6 +5144,7 @@ dependencies = [ "log", "monch", "once_cell", + "rkyv", "sb_core", "sb_eszip_shared", "sb_fs", diff --git a/crates/base/src/deno_runtime.rs b/crates/base/src/deno_runtime.rs index 971732c35..5053a2f39 100644 --- a/crates/base/src/deno_runtime.rs +++ b/crates/base/src/deno_runtime.rs @@ -1,6 +1,7 @@ use crate::inspector_server::Inspector; use crate::rt_worker::supervisor::{CPUUsage, CPUUsageMetrics}; use crate::rt_worker::worker::DuplexStreamEntry; +use crate::utils::path::find_up; use crate::utils::units::{bytes_to_display, mib_to_bytes}; use anyhow::{anyhow, bail, Context, Error}; @@ -333,8 +334,15 @@ where CacheSetting::Use }; + if let Some(npmrc_path) = find_up(".npmrc", &base_dir_path) { + if npmrc_path.exists() && npmrc_path.is_file() { + emitter_factory.set_npmrc_path(npmrc_path); + emitter_factory.set_npmrc_env_vars(env_vars.clone()); + } + } + emitter_factory.set_file_fetcher_allow_remote(allow_remote_modules); - emitter_factory.set_file_fetcher_cache_strategy(cache_strategy); + emitter_factory.set_cache_strategy(cache_strategy); emitter_factory.set_decorator_type(maybe_decorator); if let Some(jsx_import_source_config) = maybe_jsx_import_source_config.clone() { diff --git a/crates/base/src/lib.rs b/crates/base/src/lib.rs index 116391b07..0cbabc7a6 100644 --- a/crates/base/src/lib.rs +++ b/crates/base/src/lib.rs @@ -12,6 +12,7 @@ mod inspector_server; mod timeout; pub use inspector_server::InspectorOption; +pub use sb_core::cache::CacheSetting; pub use sb_graph::DecoratorType; #[cfg(test)] diff --git a/crates/base/src/rt_worker/utils.rs b/crates/base/src/rt_worker/utils.rs index 3e9848d97..a7dbe0492 100644 --- a/crates/base/src/rt_worker/utils.rs +++ b/crates/base/src/rt_worker/utils.rs @@ -1,6 +1,6 @@ -use event_worker::events::{EventMetadata, WorkerEventWithMetadata}; +use event_worker::events::{EventMetadata, WorkerEventWithMetadata, WorkerEvents}; use sb_workers::context::{UserWorkerMsgs, WorkerRuntimeOpts}; -use tokio::sync::mpsc::UnboundedSender; +use tokio::sync::mpsc::{self, UnboundedSender}; use tokio_util::sync::CancellationToken; use uuid::Uuid; @@ -46,3 +46,13 @@ pub fn get_event_metadata(conf: &WorkerRuntimeOpts) -> EventMetadata { event_metadata } + +pub fn send_event_if_event_worker_available( + maybe_event_worker: Option<&mpsc::UnboundedSender>, + event: WorkerEvents, + metadata: EventMetadata, +) { + if let Some(event_worker) = maybe_event_worker { + let _ = event_worker.send(WorkerEventWithMetadata { event, metadata }); + } +} diff --git a/crates/base/src/rt_worker/worker.rs b/crates/base/src/rt_worker/worker.rs index ef5915829..281e931e0 100644 --- a/crates/base/src/rt_worker/worker.rs +++ b/crates/base/src/rt_worker/worker.rs @@ -1,9 +1,11 @@ use crate::deno_runtime::DenoRuntime; use crate::inspector_server::Inspector; use crate::rt_worker::supervisor; -use crate::rt_worker::utils::{get_event_metadata, parse_worker_conf}; +use crate::rt_worker::utils::{ + get_event_metadata, parse_worker_conf, send_event_if_event_worker_available, +}; use crate::rt_worker::worker_ctx::create_supervisor; -use crate::utils::send_event_if_event_worker_available; + use anyhow::Error; use base_mem_check::MemCheckState; use base_rt::error::CloneableError; diff --git a/crates/base/src/rt_worker/worker_ctx.rs b/crates/base/src/rt_worker/worker_ctx.rs index a0b048904..0b9f36ce5 100644 --- a/crates/base/src/rt_worker/worker_ctx.rs +++ b/crates/base/src/rt_worker/worker_ctx.rs @@ -1,8 +1,8 @@ +use super::utils::send_event_if_event_worker_available; use crate::deno_runtime::DenoRuntime; use crate::inspector_server::Inspector; use crate::server::ServerFlags; use crate::timeout::{self, CancelOnWriteTimeout, ReadTimeoutStream}; -use crate::utils::send_event_if_event_worker_available; use crate::rt_worker::worker::{Worker, WorkerHandler}; use crate::rt_worker::worker_pool::WorkerPool; diff --git a/crates/base/src/server.rs b/crates/base/src/server.rs index f1e6e5c78..9a48c6675 100644 --- a/crates/base/src/server.rs +++ b/crates/base/src/server.rs @@ -6,6 +6,7 @@ use crate::rt_worker::worker_pool::WorkerPoolPolicy; use crate::InspectorOption; use anyhow::{anyhow, bail, Context, Error}; use deno_config::JsxImportSourceConfig; +use enum_as_inner::EnumAsInner; use futures_util::future::{poll_fn, BoxFuture}; use futures_util::{FutureExt, Stream}; use hyper_v014::{server::conn::Http, service::Service, Body, Request, Response}; @@ -52,6 +53,7 @@ pub enum ServerEvent { Draining, } +#[derive(Debug, EnumAsInner)] pub enum ServerHealth { Listening(mpsc::UnboundedReceiver, SharedMetricSource), Failure, @@ -233,6 +235,7 @@ impl Service> for WorkerService { } } +#[derive(Default)] pub struct WorkerEntrypoints { pub main: Option, pub events: Option, diff --git a/crates/base/src/utils.rs b/crates/base/src/utils.rs deleted file mode 100644 index 8eec09a31..000000000 --- a/crates/base/src/utils.rs +++ /dev/null @@ -1,14 +0,0 @@ -use event_worker::events::{EventMetadata, WorkerEventWithMetadata, WorkerEvents}; -use tokio::sync::mpsc; - -pub mod units; - -pub fn send_event_if_event_worker_available( - maybe_event_worker: Option<&mpsc::UnboundedSender>, - event: WorkerEvents, - metadata: EventMetadata, -) { - if let Some(event_worker) = maybe_event_worker { - let _ = event_worker.send(WorkerEventWithMetadata { event, metadata }); - } -} diff --git a/crates/base/src/utils/mod.rs b/crates/base/src/utils/mod.rs new file mode 100644 index 000000000..ba80d6f0f --- /dev/null +++ b/crates/base/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod path; +pub mod units; diff --git a/crates/base/src/utils/path.rs b/crates/base/src/utils/path.rs new file mode 100644 index 000000000..dd34f4ad5 --- /dev/null +++ b/crates/base/src/utils/path.rs @@ -0,0 +1,23 @@ +use std::path::{Path, PathBuf}; + +pub fn find_up(filename: A, start: B) -> Option +where + A: AsRef, + B: AsRef, +{ + let mut current = start.as_ref().to_path_buf(); + + loop { + if current.join(&filename).exists() { + return Some(current.join(filename)); + } + + if let Some(parent) = current.parent() { + current = parent.to_path_buf(); + } else { + break; + } + } + + None +} diff --git a/crates/base/test_cases/main_eszip/index.ts b/crates/base/test_cases/main_eszip/index.ts index 49c70a820..5014912b6 100644 --- a/crates/base/test_cases/main_eszip/index.ts +++ b/crates/base/test_cases/main_eszip/index.ts @@ -24,7 +24,7 @@ Deno.serve(async (req: Request) => { const workerTimeoutMs = 10 * 60 * 1000; const cpuTimeSoftLimitMs = 10 * 60 * 1000; const cpuTimeHardLimitMs = 10 * 60 * 1000; - const noModuleCache = false; + const noModuleCache = true; const importMapPath = null; const envVarsObj = Deno.env.toObject(); const envVars = Object.keys(envVarsObj).map(k => [k, envVarsObj[k]]); diff --git a/crates/base/test_cases/main_with_registry/index.ts b/crates/base/test_cases/main_with_registry/index.ts new file mode 100644 index 000000000..f2044643b --- /dev/null +++ b/crates/base/test_cases/main_with_registry/index.ts @@ -0,0 +1,74 @@ +import { handleRegistryRequest } from "./registry/mod.ts"; + +console.log('main function started'); + +Deno.serve(async (req: Request) => { + console.log(req.url); + const url = new URL(req.url); + const { pathname } = url; + const path_parts = pathname.split("/"); + let service_name = path_parts[1]; + + if (req.headers.has('x-service-path')) { + service_name = req.headers.get('x-service-path')!; + } + + const REGISTRY_PREFIX = '/_internal/registry'; + if (pathname.startsWith(REGISTRY_PREFIX)) { + return await handleRegistryRequest(REGISTRY_PREFIX, req); + } + + if (!service_name || service_name === "") { + const error = { msg: "missing function name in request" } + return new Response( + JSON.stringify(error), + { status: 400, headers: { "Content-Type": "application/json" } }, + ) + } + + const servicePath = `./test_cases/${service_name}`; + console.error(`serving the request with ${servicePath}`); + + const createWorker = async () => { + const memoryLimitMb = 150; + const workerTimeoutMs = 10 * 60 * 1000; + const cpuTimeSoftLimitMs = 10 * 60 * 1000; + const cpuTimeHardLimitMs = 10 * 60 * 1000; + const noModuleCache = true; + const importMapPath = null; + const envVarsObj = Deno.env.toObject(); + const envVars = Object.keys(envVarsObj).map(k => [k, envVarsObj[k]]); + + return await EdgeRuntime.userWorkers.create({ + servicePath, + memoryLimitMb, + workerTimeoutMs, + cpuTimeSoftLimitMs, + cpuTimeHardLimitMs, + noModuleCache, + importMapPath, + envVars + }); + } + + const callWorker = async () => { + try { + const worker = await createWorker(); + return await worker.fetch(req); + } catch (e) { + console.error(e); + + // if (e instanceof Deno.errors.WorkerRequestCancelled) { + // return await callWorker(); + // } + + const error = { msg: e.toString() } + return new Response( + JSON.stringify(error), + { status: 500, headers: { "Content-Type": "application/json" } }, + ); + } + } + + return callWorker(); +}) diff --git a/crates/base/test_cases/main_with_registry/registry/mock.ts b/crates/base/test_cases/main_with_registry/registry/mock.ts new file mode 100644 index 000000000..e4fb7a79f --- /dev/null +++ b/crates/base/test_cases/main_with_registry/registry/mock.ts @@ -0,0 +1 @@ +export default "H4sIAMaFtVcAA+2Vz2rDMAzGc85TiJw2GKmT2g6MbfQ02GG3vYDnuG2axjZ2Mgpl7z7nXzcKXS/Nxph/F4Ek8ikR+qIZL9lKzIIJQQhlhICLSUbQ13gAktS1zFFKKQKUzFNKAiBTDjXS2JoZN4qwWsj16b5z9eE9DvGPoIf9FzIXu3hjp9Bw34NS+s3+8ef+cbd/nKU0ADTFMMf88/1XKm+2IhY7rUxt4R6WjeR1oeTVNezfw/C35/NMy3j/Q3QOoOSlNdr7x/j0/acEH/k/IQT7+/8J9iFAJFkloluIFpXipeVKi9lSqVdmopu2/CaMdY7QdiQxilGfzYXlptD1UHlkpYDeTfp6xYquMP5Z+uy24ELaTu356aXPsaZeK9M9pFOFu159IXW1sbEyq4co9F7k8Xg8F+UDnayJaQAOAAA="; \ No newline at end of file diff --git a/crates/base/test_cases/main_with_registry/registry/mod.ts b/crates/base/test_cases/main_with_registry/registry/mod.ts new file mode 100644 index 000000000..f15450b50 --- /dev/null +++ b/crates/base/test_cases/main_with_registry/registry/mod.ts @@ -0,0 +1,3 @@ +import registryHandler from './registry-handler.ts'; + +export { registryHandler as handleRegistryRequest }; \ No newline at end of file diff --git a/crates/base/test_cases/main_with_registry/registry/registry-handler.ts b/crates/base/test_cases/main_with_registry/registry/registry-handler.ts new file mode 100644 index 000000000..12d01be53 --- /dev/null +++ b/crates/base/test_cases/main_with_registry/registry/registry-handler.ts @@ -0,0 +1,81 @@ +import crypto from "node:crypto"; +import assign from "npm:object-assign"; + +import mock from "./mock.ts"; + +const PKG_NAME = "@meowmeow/foobar"; +const TOKEN = "MeowMeowToken"; +const RESPONSE_TEMPLATE = await import("./template.json"); + +export default async function (prefix: string, req: Request) { + if (req.method !== "GET") { + return Response.json({ error: "Method Not Allowed" }, { status: 405 }); + } + + if (!req.headers.get("authorization")) { + return Response.json({ error: "Missing authorization header" }, { status: 403 }); + } + + const token = req.headers.get("authorization")?.split(" ", 2); + const isValidTokenType = token?.[0] === "Bearer"; + const isValidToken = token?.[1] === TOKEN; + + if (!isValidTokenType || !isValidToken) { + return Response.json({ error: "Incorrect token" }, { status: 403 }); + } + + + const url = new URL(req.url); + const { pathname } = url; + + const moduleName = PKG_NAME.split("/", 2)[1]; + const buf = new Uint8Array(await ((await fetch(`data:application/octet;base64,${mock}`)).arrayBuffer())); + const sum = sha1(buf); + + if (ucEnc(pathname) === `${prefix}/${softEncode(PKG_NAME)}` || ucEnc(pathname) === `${prefix}/${PKG_NAME}`) { + return Response.json(generatePackageResponse("localhost", prefix, moduleName, sum)); + } + + + if (pathname === `${prefix}/${PKG_NAME}/-/${moduleName}-1.0.0.tgz`) { + return new Response(buf); + } + + return Response.json({ error: "File Not Found" }, { status: 404 }); +} + +function ucEnc(str: string) { + return str.replace(/(%[a-f0-9]{2})/g, function (match) { + return match.toUpperCase() + }) +} + +function softEncode(pkg: string) { + return encodeURIComponent(pkg).replace(/^%40/, '@') +} + +function generatePackageResponse(hostname: string, prefix: string, moduleName: string, sum: string) { + const port = Deno.env.get("EDGE_RUNTIME_PORT") || "9998"; + const tpl = assign({}, RESPONSE_TEMPLATE.default, { + _id: PKG_NAME, + name: PKG_NAME, + }); + + tpl.versions["1.0.0"] = assign({}, tpl.versions["1.0.0"], { + _id: PKG_NAME + "@1.0.0", + name: PKG_NAME, + dist: assign({}, tpl.versions["1.0.0"].dist, { + shasum: sum, + tarball: [ + "http://" + hostname + ":" + port + prefix, + PKG_NAME, "-", moduleName + "-1.0.0.tgz" + ].join("/") + }) + }) + + return tpl +} + +function sha1(data: Uint8Array) { + return crypto.createHash("sha1").update(data).digest('hex') +} diff --git a/crates/base/test_cases/main_with_registry/registry/template.json b/crates/base/test_cases/main_with_registry/registry/template.json new file mode 100644 index 000000000..68fed2551 --- /dev/null +++ b/crates/base/test_cases/main_with_registry/registry/template.json @@ -0,0 +1,49 @@ +{ + "_id": "@mockscope/foobar", + "_rev": "1-c30105564f195a3038d3348840c9e080", + "name": "@mockscope/foobar", + "description": "Fake module", + "dist-tags": { + "latest": "1.0.0" + }, + "versions": { + "1.0.0": { + "name": "@mockscope/foobar", + "version": "1.0.0", + "description": "Fake module", + "main": "index.js", + "license": "MIT", + "gitHead": "2666d6874aaebbaf7188e14d94eb8488079d6c2c", + "_id": "@mockscope/foobar@1.0.0", + "_shasum": "19d13344e0b701cae3374fa6f03d2d3f90847c1c", + "_from": ".", + "_npmVersion": "2.5.1", + "_nodeVersion": "1.2.0", + "_npmUser": { + "name": "foobar", + "email": "foobar@npmjs.org" + }, + "maintainers": [{ + "name": "foobar", + "email": "foobar@npmjs.org" + }], + "dist": { + "shasum": "cd2e97011c99721c5f0a6d677c50a144ec790a2d" + }, + "directories": {} + } + }, + "readme": "Mock module!\n", + "maintainers": [{ + "name": "foobar", + "email": "foobar@npmjs.org" + }], + "time": { + "modified": "2015-05-05T10:32:06.793Z", + "created": "2015-05-05T10:32:06.793Z", + "1.0.0": "2015-05-05T10:32:06.793Z" + }, + "license": "MIT", + "readmeFilename": "README.md", + "_attachments": {} + } \ No newline at end of file diff --git a/crates/base/test_cases/private-npm-package-import-2/.npmrc b/crates/base/test_cases/private-npm-package-import-2/.npmrc new file mode 100644 index 000000000..1dae32a7d --- /dev/null +++ b/crates/base/test_cases/private-npm-package-import-2/.npmrc @@ -0,0 +1,2 @@ +@meowmeow:registry=http://localhost:8498/_internal/registry/ +//localhost:8498/_internal/registry/:_authToken=MeowMeowToken \ No newline at end of file diff --git a/crates/base/test_cases/private-npm-package-import-2/inner/index.js b/crates/base/test_cases/private-npm-package-import-2/inner/index.js new file mode 100644 index 000000000..17fe46afd --- /dev/null +++ b/crates/base/test_cases/private-npm-package-import-2/inner/index.js @@ -0,0 +1,13 @@ +import foobar from "npm:@meowmeow/foobar"; +import isOdd from "npm:is-odd"; + +console.log(foobar()); + +export default { + fetch() { + return Response.json({ + meow: typeof foobar, + odd: isOdd(1), + }); + } +} \ No newline at end of file diff --git a/crates/base/test_cases/private-npm-package-import/.npmrc b/crates/base/test_cases/private-npm-package-import/.npmrc new file mode 100644 index 000000000..1dae32a7d --- /dev/null +++ b/crates/base/test_cases/private-npm-package-import/.npmrc @@ -0,0 +1,2 @@ +@meowmeow:registry=http://localhost:8498/_internal/registry/ +//localhost:8498/_internal/registry/:_authToken=MeowMeowToken \ No newline at end of file diff --git a/crates/base/test_cases/private-npm-package-import/index.js b/crates/base/test_cases/private-npm-package-import/index.js new file mode 100644 index 000000000..17fe46afd --- /dev/null +++ b/crates/base/test_cases/private-npm-package-import/index.js @@ -0,0 +1,13 @@ +import foobar from "npm:@meowmeow/foobar"; +import isOdd from "npm:is-odd"; + +console.log(foobar()); + +export default { + fetch() { + return Response.json({ + meow: typeof foobar, + odd: isOdd(1), + }); + } +} \ No newline at end of file diff --git a/crates/base/tests/integration_tests.rs b/crates/base/tests/integration_tests.rs index 4427ea457..6569a67c0 100644 --- a/crates/base/tests/integration_tests.rs +++ b/crates/base/tests/integration_tests.rs @@ -1,3 +1,6 @@ +#![allow(clippy::arc_with_non_send_sync)] +#![allow(clippy::async_yields_async)] + #[path = "../src/utils/integration_test_helper.rs"] mod integration_test_helper; @@ -23,10 +26,10 @@ use async_tungstenite::WebSocketStream; use base::{ integration_test, integration_test_listen_fut, integration_test_with_server_flag, rt_worker::worker_ctx::{create_user_worker_pool, create_worker, TerminationToken}, - server::{ServerEvent, ServerFlags, ServerHealth, Tls}, + server::{Server, ServerEvent, ServerFlags, ServerHealth, Tls}, DecoratorType, }; -use deno_core::serde_json; +use deno_core::serde_json::{self, json}; use futures_util::{future::BoxFuture, Future, FutureExt, SinkExt, StreamExt}; use http::{Method, Request, Response as HttpResponse, StatusCode}; use http_utils::utils::get_upgrade_type; @@ -2443,7 +2446,6 @@ async fn test_should_be_able_to_bundle_against_various_exts() { async { generate_binary_eszip( PathBuf::from(path), - #[allow(clippy::arc_with_non_send_sync)] Arc::new(emitter_factory), None, None, @@ -2532,6 +2534,157 @@ async fn test_should_be_able_to_bundle_against_various_exts() { test_serve_simple_fn("tsx", REACT_RESULT.as_bytes()).await; } +#[tokio::test] +#[serial] +async fn test_private_npm_package_import() { + // Required because test_cases/main_with_registry/registry/registry-handler.ts:58 + std::env::set_var("EDGE_RUNTIME_PORT", NON_SECURE_PORT.to_string()); + let _guard = scopeguard::guard((), |_| { + std::env::remove_var("EDGE_RUNTIME_PORT"); + }); + + let client = Client::new(); + let run_server_fn = |main: &'static str, token| async move { + let (tx, mut rx) = mpsc::channel(1); + let handle = tokio::task::spawn({ + async move { + Server::new( + "127.0.0.1", + NON_SECURE_PORT, + None, + main.to_string(), + None, + None, + None, + None, + Default::default(), + Some(tx), + Default::default(), + Some(token), + vec![], + None, + None, + None, + ) + .await + .unwrap() + .listen() + .await + .unwrap(); + } + }); + + let _ev = loop { + match rx.recv().await { + Some(health) => break health.into_listening().unwrap(), + _ => continue, + } + }; + + handle + }; + + { + let token = TerminationToken::new(); + let handle = run_server_fn("./test_cases/main_with_registry", token.clone()).await; + + let resp = client + .request( + Method::GET, + format!( + "http://localhost:{}/private-npm-package-import", + NON_SECURE_PORT + ), + ) + .send() + .await + .unwrap(); + + assert_eq!(resp.status().as_u16(), 200); + + let body = resp.json::().await.unwrap(); + let body = body.as_object().unwrap(); + + assert_eq!(body.len(), 2); + assert_eq!(body.get("meow"), Some(&json!("function"))); + assert_eq!(body.get("odd"), Some(&json!(true))); + + token.cancel(); + handle.await.unwrap(); + } + + { + let token = TerminationToken::new(); + let handle = run_server_fn("./test_cases/main_with_registry", token.clone()).await; + + let resp = client + .request( + Method::GET, + format!("http://localhost:{}/meow", NON_SECURE_PORT), + ) + .header("x-service-path", "private-npm-package-import-2/inner") + .send() + .await + .unwrap(); + + assert_eq!(resp.status().as_u16(), 200); + + let body = resp.json::().await.unwrap(); + let body = body.as_object().unwrap(); + + assert_eq!(body.len(), 2); + assert_eq!(body.get("meow"), Some(&json!("function"))); + assert_eq!(body.get("odd"), Some(&json!(true))); + + token.cancel(); + handle.await.unwrap(); + } + + { + let token = TerminationToken::new(); + let handle = run_server_fn("./test_cases/main_eszip", token.clone()).await; + + let buf = { + let mut emitter_factory = EmitterFactory::new(); + + emitter_factory.set_npmrc_path("./test_cases/private-npm-package-import/.npmrc"); + + generate_binary_eszip( + PathBuf::from("./test_cases/private-npm-package-import/index.js"), + Arc::new(emitter_factory), + None, + None, + None, + ) + .await + .unwrap() + .into_bytes() + }; + + let resp = client + .request( + Method::POST, + format!("http://localhost:{}/meow", NON_SECURE_PORT), + ) + .body(buf) + .send() + .await + .unwrap(); + + assert_eq!(resp.status().as_u16(), 200); + + let body = resp.json::().await.unwrap(); + let body = body.as_object().unwrap(); + + assert_eq!(body.len(), 2); + assert_eq!(body.get("meow"), Some(&json!("function"))); + assert_eq!(body.get("odd"), Some(&json!(true))); + + token.cancel(); + handle.await.unwrap(); + } +} + #[derive(Deserialize)] struct ErrorResponsePayload { msg: String, diff --git a/crates/cli/src/flags.rs b/crates/cli/src/flags.rs index 3a6b37e1c..76f79bd32 100644 --- a/crates/cli/src/flags.rs +++ b/crates/cli/src/flags.rs @@ -249,6 +249,12 @@ fn get_bundle_command() -> Command { .help("Hash function to use when checksum the contents") .value_parser(value_parser!(EszipV2ChecksumKind)) ) + .arg( + arg!(--"disable-module-cache") + .help("Disable using module cache") + .default_value("false") + .value_parser(FalseyValueParser::new()) + ) } fn get_unbundle_command() -> Command { diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index f0c8c2659..af23720bd 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -9,7 +9,8 @@ use base::commands::start_server; use base::rt_worker::worker_pool::{SupervisorPolicy, WorkerPoolPolicy}; use base::server::{ServerFlags, Tls, WorkerEntrypoints}; -use base::{DecoratorType, InspectorOption}; +use base::utils::path::find_up; +use base::{CacheSetting, DecoratorType, InspectorOption}; use clap::ArgMatches; use deno_core::url::Url; use env::resolve_deno_runtime_env; @@ -265,6 +266,7 @@ fn main() -> Result<(), anyhow::Error> { let entrypoint_dir_path = entrypoint_script_path.parent().unwrap(); let mut emitter_factory = EmitterFactory::new(); + let maybe_import_map = load_import_map(import_map_path.clone()) .map_err(|e| anyhow!("import map path is invalid ({})", e))?; let mut maybe_import_map_url = None; @@ -277,6 +279,22 @@ fn main() -> Result<(), anyhow::Error> { .to_string(), ); } + + if sub_matches + .get_one::("disable-module-cache") + .cloned() + .unwrap() + { + emitter_factory.set_cache_strategy(CacheSetting::ReloadAll); + } + + if let Some(npmrc_path) = find_up(".npmrc", entrypoint_dir_path) { + if npmrc_path.exists() && npmrc_path.is_file() { + emitter_factory.set_npmrc_path(npmrc_path); + emitter_factory.set_npmrc_env_vars(std::env::vars().collect()); + } + } + let maybe_checksum_kind = sub_matches .get_one::("checksum") .copied() diff --git a/crates/npm/cache_dir.rs b/crates/npm/cache_dir.rs index 57cf0d6f2..0c9804109 100644 --- a/crates/npm/cache_dir.rs +++ b/crates/npm/cache_dir.rs @@ -69,7 +69,8 @@ impl NpmCacheDir { registry_url: &Url, ) -> PathBuf { if folder_id.copy_index == 0 { - self.package_folder_for_nv(&folder_id.nv, registry_url) + self.package_name_folder(&folder_id.nv.name, registry_url) + .join(folder_id.nv.version.to_string()) } else { self.package_name_folder(&folder_id.nv.name, registry_url) .join(format!("{}_{}", folder_id.nv.version, folder_id.copy_index)) diff --git a/crates/sb_core/npm.rs b/crates/sb_core/npm.rs index 9711f159e..8fb698113 100644 --- a/crates/sb_core/npm.rs +++ b/crates/sb_core/npm.rs @@ -1,9 +1,11 @@ -use std::sync::Arc; +use std::{collections::HashMap, path::Path, sync::Arc}; use deno_core::url::Url; +use deno_npm::npm_rc::{NpmRc, ResolvedNpmRc}; -use deno_npm::npm_rc::ResolvedNpmRc; +use anyhow::Context; use once_cell::sync::Lazy; +use tokio::fs; pub fn npm_registry_url() -> &'static Url { static NPM_REGISTRY_DEFAULT_URL: Lazy = Lazy::new(|| { @@ -37,3 +39,24 @@ pub fn create_default_npmrc() -> Arc { registry_configs: Default::default(), }) } + +pub async fn create_npmrc

( + path: P, + maybe_env_vars: Option<&HashMap>, +) -> Result, anyhow::Error> +where + P: AsRef, +{ + let get_env_fn = |k: &str| maybe_env_vars.as_ref().and_then(|it| it.get(k).cloned()); + NpmRc::parse( + fs::read_to_string(path) + .await + .context("failed to read path")? + .as_str(), + &get_env_fn, + ) + .context("failed to parse .npmrc file")? + .as_resolved(npm_registry_url()) + .context("failed to resolve .npmrc file") + .map(Arc::new) +} diff --git a/crates/sb_eszip_shared/lib.rs b/crates/sb_eszip_shared/lib.rs index 77f26f60d..f36ba96dd 100644 --- a/crates/sb_eszip_shared/lib.rs +++ b/crates/sb_eszip_shared/lib.rs @@ -6,6 +6,7 @@ pub static SUPABASE_ESZIP_VERSION_KEY: &str = "---SUPABASE-ESZIP-VERSION-ESZIP-- pub static VFS_ESZIP_KEY: &str = "---SUPABASE-VFS-DATA-ESZIP---"; pub static SOURCE_CODE_ESZIP_KEY: &str = "---SUPABASE-SOURCE-CODE-ESZIP---"; pub static STATIC_FILES_ESZIP_KEY: &str = "---SUPABASE-STATIC-FILES-ESZIP---"; +pub static NPM_RC_SCOPES_KEY: &str = "---SUPABASE-NPM-RC-SCOPES---"; pub trait AsyncEszipDataRead: std::fmt::Debug + Send + Sync { fn ensure_module(&self, specifier: &str) -> Option; diff --git a/crates/sb_fs/lib.rs b/crates/sb_fs/lib.rs index 2c36dd8bc..5614ba76e 100644 --- a/crates/sb_fs/lib.rs +++ b/crates/sb_fs/lib.rs @@ -136,7 +136,9 @@ where // but also don't make this dependent on the registry url let root_path = npm_resolver.global_cache_root_folder(); let mut builder = VfsBuilder::new(root_path, add_content_callback_fn)?; - for package in npm_resolver.all_system_packages(&NpmSystemInfo::default()) { + let mut packages = npm_resolver.all_system_packages(&NpmSystemInfo::default()); + packages.sort_by(|a, b| a.id.cmp(&b.id)); // determinism + for package in packages { let folder = npm_resolver.resolve_pkg_folder_from_pkg_id(&package.id)?; builder.add_dir_recursive(&folder)?; } diff --git a/crates/sb_graph/emitter.rs b/crates/sb_graph/emitter.rs index ffd977c81..260e3313f 100644 --- a/crates/sb_graph/emitter.rs +++ b/crates/sb_graph/emitter.rs @@ -11,6 +11,7 @@ use deno_core::error::AnyError; use deno_core::futures::FutureExt; use deno_core::parking_lot::Mutex; use deno_lockfile::Lockfile; +use deno_npm::npm_rc::ResolvedNpmRc; use deno_npm::resolution::ValidSerializedNpmResolutionSnapshot; use eszip::deno_graph::source::Loader; use import_map::ImportMap; @@ -34,7 +35,7 @@ use sb_npm::{ }; use std::collections::HashMap; use std::future::Future; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; struct Deferred(once_cell::unsync::OnceCell); @@ -91,13 +92,16 @@ pub struct EmitterFactory { node_resolver: Deferred>, cli_node_resolver: Deferred>, npm_resolver: Deferred>, + resolved_npm_rc: Deferred>, workspace_resolver: Deferred>, resolver: Deferred>, + cache_strategy: Option, file_fetcher: Deferred>, - file_fetcher_cache_strategy: Option, - jsx_import_source_config: Option, file_fetcher_allow_remote: bool, + jsx_import_source_config: Option, pub maybe_import_map: Option, + maybe_npmrc_path: Option, + maybe_npmrc_env_vars: Option>, module_info_cache: Deferred>, } @@ -123,18 +127,21 @@ impl EmitterFactory { node_resolver: Default::default(), cli_node_resolver: Default::default(), npm_resolver: Default::default(), + resolved_npm_rc: Default::default(), workspace_resolver: Default::default(), resolver: Default::default(), + cache_strategy: None, file_fetcher: Default::default(), - file_fetcher_cache_strategy: None, file_fetcher_allow_remote: true, - maybe_import_map: None, jsx_import_source_config: None, + maybe_import_map: None, + maybe_npmrc_path: None, + maybe_npmrc_env_vars: None, } } - pub fn set_file_fetcher_cache_strategy(&mut self, strategy: CacheSetting) { - self.file_fetcher_cache_strategy = Some(strategy); + pub fn set_cache_strategy(&mut self, strategy: CacheSetting) { + self.cache_strategy = Some(strategy); } pub fn set_file_fetcher_allow_remote(&mut self, allow_remote: bool) { @@ -145,6 +152,17 @@ impl EmitterFactory { self.maybe_import_map = import_map; } + pub fn set_npmrc_path

(&mut self, path: P) + where + P: AsRef, + { + self.maybe_npmrc_path = Some(path.as_ref().to_path_buf()); + } + + pub fn set_npmrc_env_vars(&mut self, vars: HashMap) { + self.maybe_npmrc_env_vars = Some(vars); + } + pub fn set_decorator_type(&mut self, decorator_type: Option) { self.maybe_decorator = decorator_type; } @@ -282,17 +300,29 @@ impl EmitterFactory { fs: self.real_fs(), http_client_provider: self.http_client_provider().clone(), npm_global_cache_dir: self.deno_dir.npm_folder_path().clone(), - cache_setting: CacheSetting::Use, + cache_setting: self.cache_strategy.clone().unwrap_or(CacheSetting::Use), maybe_node_modules_path: None, npm_system_info: Default::default(), package_json_deps_provider: Default::default(), - npmrc: npm::create_default_npmrc(), + npmrc: self.resolved_npm_rc().await?.clone(), }) .await }) .await } + pub async fn resolved_npm_rc(&self) -> Result<&Arc, AnyError> { + self.resolved_npm_rc + .get_or_try_init_async(async { + if let Some(path) = self.maybe_npmrc_path.clone() { + npm::create_npmrc(path, self.maybe_npmrc_env_vars.as_ref()).await + } else { + Ok(npm::create_default_npmrc()) + } + }) + .await + } + pub async fn node_resolver(&self) -> Result<&Arc, AnyError> { self.node_resolver .get_or_try_init_async( @@ -366,7 +396,7 @@ impl EmitterFactory { Ok(Arc::new(FileFetcher::new( global_cache.clone(), - self.file_fetcher_cache_strategy + self.cache_strategy .clone() .unwrap_or(CacheSetting::ReloadAll), self.file_fetcher_allow_remote, diff --git a/crates/sb_graph/lib.rs b/crates/sb_graph/lib.rs index 409ab27f8..a9968bbaf 100644 --- a/crates/sb_graph/lib.rs +++ b/crates/sb_graph/lib.rs @@ -15,8 +15,8 @@ use futures::{AsyncReadExt, AsyncSeekExt}; use glob::glob; use log::error; use sb_eszip_shared::{ - AsyncEszipDataRead, SOURCE_CODE_ESZIP_KEY, STATIC_FILES_ESZIP_KEY, SUPABASE_ESZIP_VERSION, - SUPABASE_ESZIP_VERSION_KEY, VFS_ESZIP_KEY, + AsyncEszipDataRead, NPM_RC_SCOPES_KEY, SOURCE_CODE_ESZIP_KEY, STATIC_FILES_ESZIP_KEY, + SUPABASE_ESZIP_VERSION, SUPABASE_ESZIP_VERSION_KEY, VFS_ESZIP_KEY, }; use sb_fs::{build_vfs, VfsOpts}; use sb_npm::InnerCliNpmResolverRef; @@ -734,6 +734,44 @@ where ); }; + let resolved_npm_rc = emitter_factory.resolved_npm_rc().await?; + let modified_scopes = resolved_npm_rc + .scopes + .iter() + .filter_map(|(k, v)| { + Some((k.clone(), { + let mut url = v.registry_url.clone(); + + if url.scheme() != "http" && url.scheme() != "https" { + return None; + } + if url.port().is_none() && url.path() == "/" { + return None; + } + if url.set_port(None).is_err() { + return None; + } + if url.set_host(Some("localhost")).is_err() { + return None; + } + if url.set_scheme("https").is_err() { + return None; + } + + url.to_string() + })) + }) + .collect::>(); + + eszip.add_opaque_data( + String::from(NPM_RC_SCOPES_KEY), + Arc::from( + rkyv::to_bytes::<_, 1024>(&modified_scopes) + .with_context(|| "cannot serialize vfs data")? + .into_boxed_slice(), + ), + ); + Ok(eszip) } diff --git a/crates/sb_module_loader/Cargo.toml b/crates/sb_module_loader/Cargo.toml index b5d025732..d31c40f25 100644 --- a/crates/sb_module_loader/Cargo.toml +++ b/crates/sb_module_loader/Cargo.toml @@ -36,3 +36,4 @@ base64.workspace = true tracing.workspace = true eszip.workspace = true futures-util.workspace = true +rkyv.workspace = true diff --git a/crates/sb_module_loader/standalone/mod.rs b/crates/sb_module_loader/standalone/mod.rs index ff7aad284..2145cd841 100644 --- a/crates/sb_module_loader/standalone/mod.rs +++ b/crates/sb_module_loader/standalone/mod.rs @@ -6,7 +6,7 @@ use deno_config::workspace::{PackageJsonDepResolution, WorkspaceResolver}; use deno_core::error::AnyError; use deno_core::url::Url; use deno_core::{FastString, ModuleSpecifier}; -use deno_npm::npm_rc::ResolvedNpmRc; +use deno_npm::npm_rc::{RegistryConfigWithUrl, ResolvedNpmRc}; use deno_tls::rustls::RootCertStore; use deno_tls::RootCertStoreProvider; use futures_util::future::OptionFuture; @@ -18,7 +18,9 @@ use sb_core::cache::CacheSetting; use sb_core::cert::{get_root_cert_store, CaData}; use sb_core::node::CliCjsCodeAnalyzer; use sb_core::util::http_util::HttpClientProvider; -use sb_eszip_shared::{AsyncEszipDataRead, SOURCE_CODE_ESZIP_KEY, VFS_ESZIP_KEY}; +use sb_eszip_shared::{ + AsyncEszipDataRead, NPM_RC_SCOPES_KEY, SOURCE_CODE_ESZIP_KEY, VFS_ESZIP_KEY, +}; use sb_fs::file_system::DenoCompileFileSystem; use sb_fs::{extract_static_files_from_eszip, load_npm_vfs}; use sb_graph::resolver::{CjsResolutionStore, CliNodeResolver, NpmModuleLoader}; @@ -33,6 +35,7 @@ use sb_npm::{ }; use standalone_module_loader::WorkspaceEszip; +use std::collections::HashMap; use std::path::Path; use std::rc::Rc; use std::sync::Arc; @@ -93,7 +96,36 @@ where root_node_modules_path.clone(), vec![npm_registry_url.clone()], ); + let npm_global_cache_dir = npm_cache_dir.get_cache_location(); + let scopes = if let Some(maybe_scopes) = OptionFuture::<_>::from( + eszip + .ensure_module(NPM_RC_SCOPES_KEY) + .map(|it| async move { it.take_source().await }), + ) + .await + .flatten() + .map(|it| { + rkyv::from_bytes::>(it.as_ref()) + .context("failed to deserialize npm scopes data from eszip") + }) { + maybe_scopes? + .into_iter() + .map( + |(k, v)| -> Result<(String, RegistryConfigWithUrl), AnyError> { + Ok(( + k, + RegistryConfigWithUrl { + registry_url: Url::parse(&v).context("failed to parse registry url")?, + config: Default::default(), + }, + )) + }, + ) + .collect::, _>>()? + } else { + Default::default() + }; let entry_module_source = OptionFuture::<_>::from( eszip @@ -147,7 +179,7 @@ where registry_url: npm_registry_url.clone(), config: Default::default(), }, - scopes: Default::default(), + scopes, registry_configs: Default::default(), }), }) diff --git a/examples/.npmrc b/examples/.npmrc new file mode 100644 index 000000000..00a0f3fa7 --- /dev/null +++ b/examples/.npmrc @@ -0,0 +1,2 @@ +@meowmeow:registry=http://localhost:9998/_internal/registry/ +//localhost:9998/_internal/registry/:_authToken=MeowMeowToken \ No newline at end of file diff --git a/examples/main/index.ts b/examples/main/index.ts index 991406327..5bc361c11 100644 --- a/examples/main/index.ts +++ b/examples/main/index.ts @@ -1,6 +1,8 @@ // @ts-ignore import { STATUS_CODE } from 'https://deno.land/std/http/status.ts'; +import { handleRegistryRequest } from './registry/mod.ts'; + console.log('main function started'); // log system memory usage every 30s @@ -63,6 +65,11 @@ Deno.serve(async (req: Request) => { // return response; // 101 (Switching Protocols) // } + const REGISTRY_PREFIX = '/_internal/registry'; + if (pathname.startsWith(REGISTRY_PREFIX)) { + return await handleRegistryRequest(REGISTRY_PREFIX, req); + } + const path_parts = pathname.split('/'); const service_name = path_parts[1]; diff --git a/examples/main/registry/mock.ts b/examples/main/registry/mock.ts new file mode 100644 index 000000000..e4fb7a79f --- /dev/null +++ b/examples/main/registry/mock.ts @@ -0,0 +1 @@ +export default "H4sIAMaFtVcAA+2Vz2rDMAzGc85TiJw2GKmT2g6MbfQ02GG3vYDnuG2axjZ2Mgpl7z7nXzcKXS/Nxph/F4Ek8ikR+qIZL9lKzIIJQQhlhICLSUbQ13gAktS1zFFKKQKUzFNKAiBTDjXS2JoZN4qwWsj16b5z9eE9DvGPoIf9FzIXu3hjp9Bw34NS+s3+8ef+cbd/nKU0ADTFMMf88/1XKm+2IhY7rUxt4R6WjeR1oeTVNezfw/C35/NMy3j/Q3QOoOSlNdr7x/j0/acEH/k/IQT7+/8J9iFAJFkloluIFpXipeVKi9lSqVdmopu2/CaMdY7QdiQxilGfzYXlptD1UHlkpYDeTfp6xYquMP5Z+uy24ELaTu356aXPsaZeK9M9pFOFu159IXW1sbEyq4co9F7k8Xg8F+UDnayJaQAOAAA="; \ No newline at end of file diff --git a/examples/main/registry/mod.ts b/examples/main/registry/mod.ts new file mode 100644 index 000000000..f15450b50 --- /dev/null +++ b/examples/main/registry/mod.ts @@ -0,0 +1,3 @@ +import registryHandler from './registry-handler.ts'; + +export { registryHandler as handleRegistryRequest }; \ No newline at end of file diff --git a/examples/main/registry/registry-handler.ts b/examples/main/registry/registry-handler.ts new file mode 100644 index 000000000..12d01be53 --- /dev/null +++ b/examples/main/registry/registry-handler.ts @@ -0,0 +1,81 @@ +import crypto from "node:crypto"; +import assign from "npm:object-assign"; + +import mock from "./mock.ts"; + +const PKG_NAME = "@meowmeow/foobar"; +const TOKEN = "MeowMeowToken"; +const RESPONSE_TEMPLATE = await import("./template.json"); + +export default async function (prefix: string, req: Request) { + if (req.method !== "GET") { + return Response.json({ error: "Method Not Allowed" }, { status: 405 }); + } + + if (!req.headers.get("authorization")) { + return Response.json({ error: "Missing authorization header" }, { status: 403 }); + } + + const token = req.headers.get("authorization")?.split(" ", 2); + const isValidTokenType = token?.[0] === "Bearer"; + const isValidToken = token?.[1] === TOKEN; + + if (!isValidTokenType || !isValidToken) { + return Response.json({ error: "Incorrect token" }, { status: 403 }); + } + + + const url = new URL(req.url); + const { pathname } = url; + + const moduleName = PKG_NAME.split("/", 2)[1]; + const buf = new Uint8Array(await ((await fetch(`data:application/octet;base64,${mock}`)).arrayBuffer())); + const sum = sha1(buf); + + if (ucEnc(pathname) === `${prefix}/${softEncode(PKG_NAME)}` || ucEnc(pathname) === `${prefix}/${PKG_NAME}`) { + return Response.json(generatePackageResponse("localhost", prefix, moduleName, sum)); + } + + + if (pathname === `${prefix}/${PKG_NAME}/-/${moduleName}-1.0.0.tgz`) { + return new Response(buf); + } + + return Response.json({ error: "File Not Found" }, { status: 404 }); +} + +function ucEnc(str: string) { + return str.replace(/(%[a-f0-9]{2})/g, function (match) { + return match.toUpperCase() + }) +} + +function softEncode(pkg: string) { + return encodeURIComponent(pkg).replace(/^%40/, '@') +} + +function generatePackageResponse(hostname: string, prefix: string, moduleName: string, sum: string) { + const port = Deno.env.get("EDGE_RUNTIME_PORT") || "9998"; + const tpl = assign({}, RESPONSE_TEMPLATE.default, { + _id: PKG_NAME, + name: PKG_NAME, + }); + + tpl.versions["1.0.0"] = assign({}, tpl.versions["1.0.0"], { + _id: PKG_NAME + "@1.0.0", + name: PKG_NAME, + dist: assign({}, tpl.versions["1.0.0"].dist, { + shasum: sum, + tarball: [ + "http://" + hostname + ":" + port + prefix, + PKG_NAME, "-", moduleName + "-1.0.0.tgz" + ].join("/") + }) + }) + + return tpl +} + +function sha1(data: Uint8Array) { + return crypto.createHash("sha1").update(data).digest('hex') +} diff --git a/examples/main/registry/template.json b/examples/main/registry/template.json new file mode 100644 index 000000000..68fed2551 --- /dev/null +++ b/examples/main/registry/template.json @@ -0,0 +1,49 @@ +{ + "_id": "@mockscope/foobar", + "_rev": "1-c30105564f195a3038d3348840c9e080", + "name": "@mockscope/foobar", + "description": "Fake module", + "dist-tags": { + "latest": "1.0.0" + }, + "versions": { + "1.0.0": { + "name": "@mockscope/foobar", + "version": "1.0.0", + "description": "Fake module", + "main": "index.js", + "license": "MIT", + "gitHead": "2666d6874aaebbaf7188e14d94eb8488079d6c2c", + "_id": "@mockscope/foobar@1.0.0", + "_shasum": "19d13344e0b701cae3374fa6f03d2d3f90847c1c", + "_from": ".", + "_npmVersion": "2.5.1", + "_nodeVersion": "1.2.0", + "_npmUser": { + "name": "foobar", + "email": "foobar@npmjs.org" + }, + "maintainers": [{ + "name": "foobar", + "email": "foobar@npmjs.org" + }], + "dist": { + "shasum": "cd2e97011c99721c5f0a6d677c50a144ec790a2d" + }, + "directories": {} + } + }, + "readme": "Mock module!\n", + "maintainers": [{ + "name": "foobar", + "email": "foobar@npmjs.org" + }], + "time": { + "modified": "2015-05-05T10:32:06.793Z", + "created": "2015-05-05T10:32:06.793Z", + "1.0.0": "2015-05-05T10:32:06.793Z" + }, + "license": "MIT", + "readmeFilename": "README.md", + "_attachments": {} + } \ No newline at end of file diff --git a/examples/private-npm-package-import-2/index.js b/examples/private-npm-package-import-2/index.js new file mode 100644 index 000000000..949cf527c --- /dev/null +++ b/examples/private-npm-package-import-2/index.js @@ -0,0 +1,14 @@ +// ./examples/.npmrc +import foobar from "npm:@meowmeow/foobar"; +import isOdd from "npm:is-odd"; + +console.log(foobar()); + +export default { + fetch() { + return Response.json({ + meow: typeof foobar, + odd: isOdd(1), + }); + } +} \ No newline at end of file diff --git a/examples/private-npm-package-import/.npmrc b/examples/private-npm-package-import/.npmrc new file mode 100644 index 000000000..00a0f3fa7 --- /dev/null +++ b/examples/private-npm-package-import/.npmrc @@ -0,0 +1,2 @@ +@meowmeow:registry=http://localhost:9998/_internal/registry/ +//localhost:9998/_internal/registry/:_authToken=MeowMeowToken \ No newline at end of file diff --git a/examples/private-npm-package-import/index.js b/examples/private-npm-package-import/index.js new file mode 100644 index 000000000..17fe46afd --- /dev/null +++ b/examples/private-npm-package-import/index.js @@ -0,0 +1,13 @@ +import foobar from "npm:@meowmeow/foobar"; +import isOdd from "npm:is-odd"; + +console.log(foobar()); + +export default { + fetch() { + return Response.json({ + meow: typeof foobar, + odd: isOdd(1), + }); + } +} \ No newline at end of file