From f8928c3f07255f9b5a30446b9491a8f0b1a220d1 Mon Sep 17 00:00:00 2001 From: oglassdev <23040613+oglassdev@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:43:51 -0400 Subject: [PATCH 1/5] feat!: plugin subpath exports and plugin:/rpp: specifiers Plugins declare extra entry modules in rpp.json `exports`, imported as `plugin:/` next to `plugin:` for the config module. The SDK moves to `rpp`, `rpp:config` and `rpp:jsx`; internal virtual modules move under `rpp:internal/`. The `#rpp` and `#plugins/` specifiers no longer resolve, and bundle errors for them point at the new names. Closes #112 --- README.md | 6 +- .../rpp-cli/src/codegen/component_dts/mod.rs | 4 +- crates/rpp-cli/src/codegen/mod.rs | 18 +-- crates/rpp-cli/src/codegen/tsconfig.rs | 32 ++--- crates/rpp-cli/src/commands/dev/rebuild.rs | 2 +- crates/rpp-cli/src/commands/init.rs | 4 +- crates/rpp-cli/src/commands/plugin/pack.rs | 31 ++++- crates/rpp-cli/src/project.rs | 1 + crates/rpp-cli/tests/build_e2e.rs | 4 +- crates/rpp-cli/tests/codegen_cmd.rs | 8 +- crates/rpp-cli/tests/common/packed_plugin.rs | 4 +- crates/rpp-cli/tests/examples.rs | 6 +- crates/rpp-cli/tests/examples_wasm.rs | 2 +- crates/rpp-cli/tests/js_component_e2e.rs | 8 +- crates/rpp-cli/tests/plugin_pack.rs | 20 ++- crates/rpp-cli/tests/registry_install.rs | 4 +- crates/rpp-cli/tests/ts_config.rs | 27 ++-- crates/rpp-cli/tests/window_host_e2e.rs | 6 +- crates/rpp-js/src/bundle/mod.rs | 19 ++- crates/rpp-js/src/bundle/plugin.rs | 6 +- crates/rpp-js/src/pack.rs | 119 ++++++++++++------ crates/rpp-js/tests/bundle.rs | 15 ++- crates/rpp-js/tests/bundle_packages.rs | 51 ++++++-- crates/rpp-js/tests/pack.rs | 51 +++++++- crates/rpp/src/js/bundle.rs | 42 ++++--- crates/rpp/src/js/bundle_cache.rs | 11 +- crates/rpp/src/js/config.rs | 40 ++++-- crates/rpp/src/js/discover.rs | 4 +- crates/rpp/src/js/factory.rs | 2 +- crates/rpp/src/js/mod.rs | 40 ++++-- crates/rpp/src/js/sdk/components.ts | 2 +- crates/rpp/src/js/sdk/config.ts | 2 +- crates/rpp/src/js/sdk/index.ts | 2 +- crates/rpp/src/js/sdk/jsx.ts | 2 +- crates/rpp/src/js/sdk/runtime.ts | 4 +- crates/rpp/src/manifest.rs | 73 ++++++++++- crates/rpp/tests/js_config.rs | 11 +- crates/rpp/tests/js_discover.rs | 47 +++++-- crates/rpp/tests/js_generators.rs | 4 +- crates/rpp/tests/js_host.rs | 12 +- crates/rpp/tests/js_load.rs | 4 +- crates/rpp/tests/js_processors.rs | 10 +- docs/CODEGEN.md | 4 +- docs/MIGRATING.md | 6 +- docs/SPEC.md | 45 ++++--- docs/WASM_PLUGINS.md | 2 +- examples/pack/README.md | 2 +- examples/pack/plugins/catalog/src/config.ts | 2 +- examples/pack/plugins/catalog/src/plugin.ts | 2 +- examples/pack/rpp.config.ts | 8 +- examples/pack/src/items/ember_gem.ts | 2 +- examples/plugins/grayscale-wasm/src/plugin.ts | 2 +- examples/plugins/hash-rename/README.md | 2 +- examples/plugins/hash-rename/src/config.ts | 2 +- examples/plugins/hash-rename/src/plugin.ts | 2 +- examples/plugins/json-minify/README.md | 2 +- examples/plugins/json-minify/src/config.ts | 2 +- examples/plugins/json-minify/src/plugin.ts | 2 +- examples/plugins/mcmeta-validate/README.md | 2 +- .../plugins/mcmeta-validate/src/plugin.ts | 2 +- integrations/jvm/examples/pack/rpp.config.ts | 2 +- scripts/fixtures/check-plugin/src/plugin.ts | 2 +- scripts/fixtures/grayscale/rpp.config.ts | 2 +- 63 files changed, 581 insertions(+), 276 deletions(-) diff --git a/README.md b/README.md index 1695f49..3898969 100644 --- a/README.md +++ b/README.md @@ -81,8 +81,8 @@ A project has two files at its root. `rpp.config.ts` describes the pack and the plugins it runs: ```ts -import { defineConfig } from "#rpp/config"; -import jsonMinify from "#plugins/json-minify"; +import { defineConfig } from "rpp:config"; +import jsonMinify from "plugin:json-minify"; export default defineConfig({ pack: { name: "my-pack", description: "My resource pack", format: 34 }, @@ -106,7 +106,7 @@ version ranges or local directories: A plugin is a directory with an `rpp.json` manifest and a TypeScript entry point: ```ts -import { definePlugin } from "#rpp"; +import { definePlugin } from "rpp"; export default definePlugin<{ greeting?: string }>({ // Processors run on every matching file. diff --git a/crates/rpp-cli/src/codegen/component_dts/mod.rs b/crates/rpp-cli/src/codegen/component_dts/mod.rs index 0f416b8..a22c6ac 100644 --- a/crates/rpp-cli/src/codegen/component_dts/mod.rs +++ b/crates/rpp-cli/src/codegen/component_dts/mod.rs @@ -51,7 +51,7 @@ pub fn generate(name: &str, resolve: &Resolve, world: WorldId) -> String { } out.push_str(&format!("export interface {exports_name} {exports}\n")); out.push_str(&format!( - "declare module \"#rpp\" {{\n interface ComponentMap {{\n {}: {exports_name};\n }}\n}}\n", + "declare module \"rpp\" {{\n interface ComponentMap {{\n {}: {exports_name};\n }}\n}}\n", quote_key(name) )); out @@ -459,7 +459,7 @@ world w { export tools; export run: func(); }"#); assert!(out.contains("export interface Compiler {\n run: () => void;\n tools: {\n frob: (n: bigint) => bigint;\n };\n}"), "{out}"); assert!( out.ends_with( - "declare module \"#rpp\" {\n interface ComponentMap {\n compiler: Compiler;\n }\n}\n" + "declare module \"rpp\" {\n interface ComponentMap {\n compiler: Compiler;\n }\n}\n" ), "{out}" ); diff --git a/crates/rpp-cli/src/codegen/mod.rs b/crates/rpp-cli/src/codegen/mod.rs index c50a5db..33eba19 100644 --- a/crates/rpp-cli/src/codegen/mod.rs +++ b/crates/rpp-cli/src/codegen/mod.rs @@ -1,5 +1,5 @@ //! TypeScript editor and type-checker support: the SDK under `.rpp/sdk` and the -//! tsconfig files that map `#rpp` onto it. +//! tsconfig files that map `rpp` onto it. mod component_dts; mod tsconfig; @@ -9,7 +9,7 @@ use std::path::Path; use anyhow::{Context, Result}; -use rpp::manifest::PluginManifest; +use rpp::manifest::{plugin_modules, PluginManifest}; use rpp_fetch::registry::PACKAGE_MANIFEST; use self::tsconfig::{tsconfig, ROOT_TSCONFIG}; @@ -19,14 +19,16 @@ use crate::project::{is_plugin_manifest, resolve_ts_packages, CONFIG_FILE}; /// Returns whether any file changed. pub fn write(root: &Path) -> Result { let ts_project = root.join(CONFIG_FILE).is_file(); - let plugin_configs = if ts_project { + let plugin_modules = if ts_project { let packages = resolve_ts_packages(root)?; Some( packages - .into_iter() - .filter_map(|(name, package)| { - let config = package.manifest.config?; - Some((name, package.dir.join(config))) + .iter() + .flat_map(|(name, package)| { + let manifest = &package.manifest; + plugin_modules(name, manifest.config.as_deref(), &manifest.exports) + .into_iter() + .map(|(specifier, module)| (specifier, package.dir.join(module))) }) .collect(), ) @@ -43,7 +45,7 @@ pub fn write(root: &Path) -> Result { changed |= write_if_changed(&rpp_dir.join("sdk/globals.d.ts"), rpp_js::GLOBALS_DTS)?; changed |= write_if_changed( &rpp_dir.join("tsconfig.json"), - &tsconfig(plugin_configs.as_ref()), + &tsconfig(plugin_modules.as_ref()), )?; changed |= write_component_dts(root)?; diff --git a/crates/rpp-cli/src/codegen/tsconfig.rs b/crates/rpp-cli/src/codegen/tsconfig.rs index 129296c..e3485da 100644 --- a/crates/rpp-cli/src/codegen/tsconfig.rs +++ b/crates/rpp-cli/src/codegen/tsconfig.rs @@ -17,11 +17,11 @@ const TSCONFIG_HEAD: &str = r##"{ "skipLibCheck": true, "types": [], "jsx": "react-jsx", - "jsxImportSource": "#rpp/jsx", + "jsxImportSource": "rpp:jsx", "paths": { - "#rpp": ["./sdk/index.ts"], - "#rpp/jsx": ["./sdk/jsx.ts"], - "#rpp/jsx/jsx-runtime": ["./sdk/jsx.ts"]"##; + "rpp": ["./sdk/index.ts"], + "rpp:jsx": ["./sdk/jsx.ts"], + "rpp:jsx/jsx-runtime": ["./sdk/jsx.ts"]"##; const TSCONFIG_TAIL: &str = r#" } @@ -36,14 +36,14 @@ pub(super) const ROOT_TSCONFIG: &str = r#"{ } "#; -/// The `.rpp/tsconfig.json` contents. `plugin_configs` maps dependency names to their -/// config modules; it is `Some` for `rpp.config.ts` and plugin projects, which also map -/// `#rpp/config`. -pub(super) fn tsconfig(plugin_configs: Option<&BTreeMap>) -> String { +/// The `.rpp/tsconfig.json` contents. `plugin_modules` maps dependency module specifiers +/// (`plugin:[/]`) to their files; it is `Some` for `rpp.config.ts` and plugin +/// projects, which also map `rpp:config`. +pub(super) fn tsconfig(plugin_modules: Option<&BTreeMap>) -> String { let mut text = TSCONFIG_HEAD.to_string(); - if let Some(plugin_configs) = plugin_configs { - text.push_str(",\n \"#rpp/config\": [\"./sdk/config.ts\"]"); - for (name, path) in plugin_configs { + if let Some(plugin_modules) = plugin_modules { + text.push_str(",\n \"rpp:config\": [\"./sdk/config.ts\"]"); + for (specifier, path) in plugin_modules { let declaration = match path.extension().and_then(|extension| extension.to_str()) { Some("js") => path.with_extension("d.ts"), Some("mjs") => path.with_extension("d.mts"), @@ -61,7 +61,7 @@ pub(super) fn tsconfig(plugin_configs: Option<&BTreeMap>) -> St .strip_prefix(r"\\?\") .unwrap_or(&path) .replace('\\', "/"); - text.push_str(&format!(",\n \"#plugins/{name}\": [{path:?}]")); + text.push_str(&format!(",\n {specifier:?}: [{path:?}]")); } } text.push_str(TSCONFIG_TAIL); @@ -78,7 +78,7 @@ mod tests { let options = &generated["compilerOptions"]; assert_eq!(options["jsxImportSource"], rpp::js::JSX_IMPORT_SOURCE); assert_eq!( - options["paths"]["#rpp/jsx/jsx-runtime"], + options["paths"]["rpp:jsx/jsx-runtime"], serde_json::json!(["./sdk/jsx.ts"]) ); } @@ -88,7 +88,7 @@ mod tests { let root = tempfile::tempdir().unwrap(); let config = root.path().join("config.js"); std::fs::write(&config, "export default () => {};\n").unwrap(); - let configs = BTreeMap::from([("packed".to_string(), config.clone())]); + let configs = BTreeMap::from([("plugin:packed".to_string(), config.clone())]); assert!(tsconfig(Some(&configs)).contains("config.js")); std::fs::write( config.with_extension("d.ts"), @@ -103,10 +103,10 @@ mod tests { #[test] fn verbatim_windows_paths_are_mapped_as_drive_paths() { let configs = BTreeMap::from([( - "local".to_string(), + "plugin:local".to_string(), PathBuf::from(r"\\?\C:\plugins\local\config.ts"), )]); assert!(tsconfig(Some(&configs)) - .contains(r##""#plugins/local": ["C:/plugins/local/config.ts"]"##)); + .contains(r##""plugin:local": ["C:/plugins/local/config.ts"]"##)); } } diff --git a/crates/rpp-cli/src/commands/dev/rebuild.rs b/crates/rpp-cli/src/commands/dev/rebuild.rs index 166ef6e..dc98446 100644 --- a/crates/rpp-cli/src/commands/dev/rebuild.rs +++ b/crates/rpp-cli/src/commands/dev/rebuild.rs @@ -263,7 +263,7 @@ mod tests { use super::super::watch::spawn_watcher; use super::*; - const CONFIG: &str = r##"import { defineConfig } from "#rpp/config"; + const CONFIG: &str = r##"import { defineConfig } from "rpp:config"; export default defineConfig({ pack: { name: "test", format: 34 } }); "##; diff --git a/crates/rpp-cli/src/commands/init.rs b/crates/rpp-cli/src/commands/init.rs index 4b95e93..67eae81 100644 --- a/crates/rpp-cli/src/commands/init.rs +++ b/crates/rpp-cli/src/commands/init.rs @@ -142,7 +142,7 @@ fn rpp_config(name: &str, description: &str, pack_format: u32) -> String { let name = ts_string(name); let description = ts_string(description); format!( - r##"import {{ defineConfig, plugin }} from "#rpp/config"; + r##"import {{ defineConfig, plugin }} from "rpp:config"; export default defineConfig({{ pack: {{ @@ -180,7 +180,7 @@ const HELLO_MANIFEST: &str = r#"{ } "#; -const HELLO_PLUGIN: &str = r##"import { definePlugin } from "#rpp"; +const HELLO_PLUGIN: &str = r##"import { definePlugin } from "rpp"; // Processor: minify every JSON / mcmeta file in the pack. // Generator: emit a tiny build marker listing the pack name and greeting. diff --git a/crates/rpp-cli/src/commands/plugin/pack.rs b/crates/rpp-cli/src/commands/plugin/pack.rs index 8416089..b1c175e 100644 --- a/crates/rpp-cli/src/commands/plugin/pack.rs +++ b/crates/rpp-cli/src/commands/plugin/pack.rs @@ -51,7 +51,8 @@ pub fn pack(dir: &Path, out: &Path) -> Result { root: dir.to_path_buf(), plugin: manifest.entry.clone(), config: manifest.config.clone(), - self_specifier: Some(format!("#plugins/{}", manifest.id)), + exports: manifest.exports.clone(), + self_specifier: Some(format!("plugin:{}", manifest.id)), jsx_import_source: Some(JSX_IMPORT_SOURCE.to_string()), })?; let files = collect_files(dir, &text, &manifest, bundled)?; @@ -107,6 +108,14 @@ fn packed_manifest(text: &str, bundled: &PackOutput) -> Result> { if let Some(config) = &bundled.config { object.insert("config".into(), config.clone().into()); } + if !bundled.exports.is_empty() { + let exports = bundled + .exports + .iter() + .map(|(subpath, path)| (format!("./{subpath}"), Value::from(path.clone()))) + .collect(); + object.insert("exports".into(), Value::Object(exports)); + } Ok(format!("{}\n", serde_json::to_string_pretty(&raw)?).into_bytes()) } @@ -124,21 +133,31 @@ fn write_outputs(out: &Path, file: &str, archive: &[u8]) -> Result<(PathBuf, Str } /// Unpacks the archive as installs do and bundles each entry with only the SDK modules -/// its loader provides: `#rpp` for the plugin, `#rpp/config` for the config, and `#rpp/jsx`. +/// its loader provides: `rpp` for the plugin, `rpp:config` for the config, both for exports +/// (pack sources and `rpp.config.ts` import them), and `rpp:jsx`. fn self_check(archive: &[u8]) -> Result<()> { let dir = tempfile::tempdir()?; rpp_archive::unpack(archive, dir.path()).context("unpacking the archive")?; let manifest = PluginManifest::load(dir.path()).context("checking the packed manifest")?; - let mut entries = vec![(manifest.entry, "#rpp", SDK_INDEX)]; + let sdk = ("rpp", SDK_INDEX); + let config_sdk = ("rpp:config", SDK_CONFIG); + let mut entries = vec![(manifest.entry, vec![sdk])]; if let Some(config) = manifest.config { - entries.push((config, "#rpp/config", SDK_CONFIG)); + entries.push((config, vec![config_sdk])); } - for (entry, specifier, source) in entries { + entries.extend( + manifest + .exports + .into_values() + .map(|module| (module, vec![sdk, config_sdk])), + ); + for (entry, sdk_modules) in entries { let request = BundleRequest { root: dir.path().to_path_buf(), entry: entry.clone(), - virtual_modules: [(specifier.to_string(), source.to_string())] + virtual_modules: sdk_modules .into_iter() + .map(|(specifier, source)| (specifier.to_string(), source.to_string())) .chain(jsx_modules()) .collect(), jsx_import_source: Some(JSX_IMPORT_SOURCE.to_string()), diff --git a/crates/rpp-cli/src/project.rs b/crates/rpp-cli/src/project.rs index 70816e8..ceff15b 100644 --- a/crates/rpp-cli/src/project.rs +++ b/crates/rpp-cli/src/project.rs @@ -291,6 +291,7 @@ fn load_ts(root: &Path) -> Result<(Config, TsProject)> { ConfigPackage { dir: package.dir.clone(), config: package.manifest.config.clone(), + exports: package.manifest.exports.clone(), }, ) }) diff --git a/crates/rpp-cli/tests/build_e2e.rs b/crates/rpp-cli/tests/build_e2e.rs index 8ba4802..a3531f9 100644 --- a/crates/rpp-cli/tests/build_e2e.rs +++ b/crates/rpp-cli/tests/build_e2e.rs @@ -12,12 +12,12 @@ use common::{build, stderr, write}; /// An `rpp.config.ts` whose `defineConfig` object body is `body`. fn config_ts(body: &str) -> String { format!( - "import {{ defineConfig, plugin }} from \"#rpp/config\";\n\nexport default defineConfig({{\n{body}\n}});\n" + "import {{ defineConfig, plugin }} from \"rpp:config\";\n\nexport default defineConfig({{\n{body}\n}});\n" ) } /// A plugin that minifies every `data.json`. -const MINIFY_PLUGIN: &str = r##"import { definePlugin } from "#rpp"; +const MINIFY_PLUGIN: &str = r##"import { definePlugin } from "rpp"; export default definePlugin({ processors: { diff --git a/crates/rpp-cli/tests/codegen_cmd.rs b/crates/rpp-cli/tests/codegen_cmd.rs index af5bede..2a801c5 100644 --- a/crates/rpp-cli/tests/codegen_cmd.rs +++ b/crates/rpp-cli/tests/codegen_cmd.rs @@ -35,7 +35,7 @@ fn codegen_writes_sdk_and_tsconfig() { assert!(globals.contains("declare var crypto")); assert!(!globals.contains("structuredClone")); let tsconfig = std::fs::read_to_string(root.join(".rpp/tsconfig.json")).unwrap(); - assert!(tsconfig.contains("\"#rpp\": [\"./sdk/index.ts\"]")); + assert!(tsconfig.contains("\"rpp\": [\"./sdk/index.ts\"]")); let root_config = std::fs::read_to_string(root.join("tsconfig.json")).unwrap(); assert!(root_config.contains("./.rpp/tsconfig.json")); assert!(root_config.contains("**/*.mts")); @@ -71,7 +71,7 @@ fn codegen_works_in_json_plugin_dir() { ); let tsconfig = std::fs::read_to_string(plugin.join(".rpp/tsconfig.json")).unwrap(); assert!( - tsconfig.contains("\"#rpp/config\": [\"./sdk/config.ts\"]"), + tsconfig.contains("\"rpp:config\": [\"./sdk/config.ts\"]"), "{tsconfig}" ); } @@ -264,7 +264,7 @@ fn build_runs_typescript_plugin() { let root = dir.path(); std::fs::write( root.join("rpp.config.ts"), - "import { defineConfig, plugin } from \"#rpp/config\";\n\ + "import { defineConfig, plugin } from \"rpp:config\";\n\ export default defineConfig({ pack: { name: \"p\", format: 34 }, plugins: [plugin(\"upper\")] });\n", ) .unwrap(); @@ -284,7 +284,7 @@ fn build_runs_typescript_plugin() { .unwrap(); std::fs::write( plugin.join("src/plugin.ts"), - r##"import { definePlugin } from "#rpp"; + r##"import { definePlugin } from "rpp"; export default definePlugin({ processors: { diff --git a/crates/rpp-cli/tests/common/packed_plugin.rs b/crates/rpp-cli/tests/common/packed_plugin.rs index 919b753..46b2a32 100644 --- a/crates/rpp-cli/tests/common/packed_plugin.rs +++ b/crates/rpp-cli/tests/common/packed_plugin.rs @@ -41,7 +41,7 @@ pub fn plugin(root: &Path) { write( root, "src/config.ts", - r##"import { definePluginConfig, type Access, type PluginEntry } from "#rpp/config"; + r##"import { definePluginConfig, type Access, type PluginEntry } from "rpp:config"; type Options = { text: string }; const config: (options: Options, access?: Access) => PluginEntry = definePluginConfig( @@ -53,7 +53,7 @@ export default config; write( root, "src/plugin.ts", - r##"import { definePlugin } from "#rpp"; + r##"import { definePlugin } from "rpp"; import { mark } from "dep"; import { check } from "./helper"; diff --git a/crates/rpp-cli/tests/examples.rs b/crates/rpp-cli/tests/examples.rs index c221c52..d3bf4e6 100644 --- a/crates/rpp-cli/tests/examples.rs +++ b/crates/rpp-cli/tests/examples.rs @@ -230,8 +230,8 @@ fn hash_rename_uses_processed_bytes() { write( root, "rpp.config.ts", - r##"import { defineConfig, plugin } from "#rpp/config"; -import hashRename from "#plugins/hash-rename"; + r##"import { defineConfig, plugin } from "rpp:config"; +import hashRename from "plugin:hash-rename"; export default defineConfig({ pack: { name: "processed-hash", format: 34 }, @@ -248,7 +248,7 @@ export default defineConfig({ write( root, "plugins/modify/plugin.ts", - r##"import { definePlugin } from "#rpp"; + r##"import { definePlugin } from "rpp"; export default definePlugin({ processors: { diff --git a/crates/rpp-cli/tests/examples_wasm.rs b/crates/rpp-cli/tests/examples_wasm.rs index 9a8a751..f52b1a9 100644 --- a/crates/rpp-cli/tests/examples_wasm.rs +++ b/crates/rpp-cli/tests/examples_wasm.rs @@ -59,7 +59,7 @@ fn grayscale_project(tmp: &Path) -> PathBuf { write( &root, "rpp.config.ts", - br##"import { defineConfig, plugin } from "#rpp/config"; + br##"import { defineConfig, plugin } from "rpp:config"; export default defineConfig({ pack: { name: "gray", format: 34 }, diff --git a/crates/rpp-cli/tests/js_component_e2e.rs b/crates/rpp-cli/tests/js_component_e2e.rs index f9e0c7d..d1b1204 100644 --- a/crates/rpp-cli/tests/js_component_e2e.rs +++ b/crates/rpp-cli/tests/js_component_e2e.rs @@ -34,7 +34,7 @@ fn scaffold(root: &Path, plugin_ts: &str) { write( root, "rpp.config.ts", - r##"import { defineConfig, plugin } from "#rpp/config"; + r##"import { defineConfig, plugin } from "rpp:config"; export default defineConfig({ pack: { name: "js-component", format: 34 }, @@ -58,7 +58,7 @@ export default defineConfig({ /// Build a generator plugin whose `generate` body is `body`. fn generate(body: &str) -> anyhow::Result { let plugin = format!( - r##"import {{ components, definePlugin, ComponentError, ComponentTimeoutError }} from "#rpp"; + r##"import {{ components, definePlugin, ComponentError, ComponentTimeoutError }} from "rpp"; const json = (value: unknown) => JSON.stringify(value, (_key, v) => (typeof v === "bigint" ? `${{v}}n` : v)); export default definePlugin({{ @@ -212,7 +212,7 @@ fn spin_throws_component_timeout_error() { fn handle_released_between_processor_files() { require_wasip2!(); let error = build( - r##"import { components, definePlugin } from "#rpp"; + r##"import { components, definePlugin } from "rpp"; let c: ReturnType | undefined; export default definePlugin({ processors: { @@ -248,7 +248,7 @@ fn compiled_components_are_cached_outside_the_project() { let root = dir.path(); scaffold( root, - r##"import { components, definePlugin } from "#rpp"; + r##"import { components, definePlugin } from "rpp"; export default definePlugin({ generate(ctx) { ctx.emit("out.txt", String(components.load("c").exports.parse("1"))); } }); "##, ); diff --git a/crates/rpp-cli/tests/plugin_pack.rs b/crates/rpp-cli/tests/plugin_pack.rs index 7be7340..15dbb00 100644 --- a/crates/rpp-cli/tests/plugin_pack.rs +++ b/crates/rpp-cli/tests/plugin_pack.rs @@ -97,10 +97,23 @@ fn pack_requires_rpp_range() { } #[test] -fn pack_rewrites_manifest_entry_and_config() { +fn pack_rewrites_manifest_entries() { let dir = tempfile::tempdir().unwrap(); let root = dir.path(); plugin(root); + write( + root, + "rpp.json", + PLUGIN_MANIFEST.replace( + r#""config": "src/config.ts","#, + r#""config": "src/config.ts", "exports": { "./raw": "src/raw.ts" },"#, + ), + ); + write( + root, + "src/raw.ts", + "import { definePlugin } from \"rpp\";\nexport const raw: typeof definePlugin = definePlugin;\n", + ); pack(root, "out"); let files = entries(&std::fs::read(root.join("out/packed-1.2.3.rpp.tgz")).unwrap()); @@ -115,9 +128,12 @@ fn pack_rewrites_manifest_entry_and_config() { "rpp": ">=0.1", "entry": "dist/plugin.js", "config": "dist/config.js", + "exports": { "./raw": "dist/exports/raw.js" }, "components": { "tool": "tool.wasm" }, }) ); + assert!(files.contains_key("dist/exports/raw.js")); + assert!(files.contains_key("dist/exports/raw.d.ts")); } #[test] @@ -128,7 +144,7 @@ fn pack_rejects_config_importing_the_plugin_sdk() { write( root, "src/config.ts", - "import { definePlugin } from \"#rpp\";\nexport const config: typeof definePlugin = definePlugin;\n", + "import { definePlugin } from \"rpp\";\nexport const config: typeof definePlugin = definePlugin;\n", ); let out = run(root, &["plugin", "pack", "--out", "out"]); diff --git a/crates/rpp-cli/tests/registry_install.rs b/crates/rpp-cli/tests/registry_install.rs index 12617ec..850787c 100644 --- a/crates/rpp-cli/tests/registry_install.rs +++ b/crates/rpp-cli/tests/registry_install.rs @@ -120,8 +120,8 @@ fn consumer(dir: &Path, server: &Server, source: &str) { write( &project, "rpp.config.ts", - r##"import { defineConfig } from "#rpp/config"; -import packed from "#plugins/packed"; + r##"import { defineConfig } from "rpp:config"; +import packed from "plugin:packed"; export default defineConfig({ pack: { name: "p", format: 34 }, diff --git a/crates/rpp-cli/tests/ts_config.rs b/crates/rpp-cli/tests/ts_config.rs index a402434..1d31de7 100644 --- a/crates/rpp-cli/tests/ts_config.rs +++ b/crates/rpp-cli/tests/ts_config.rs @@ -21,7 +21,7 @@ fn project(root: &Path, validate: &str, plugin_call: &str) { write( root, "plugins/suffix/src/config.ts", - r##"import { definePluginConfig } from "#rpp/config"; + r##"import { definePluginConfig } from "rpp:config"; export default definePluginConfig<{ text: string }>("suffix", { validate: (options) => VALIDATE, @@ -32,7 +32,7 @@ export default definePluginConfig<{ text: string }>("suffix", { write( root, "plugins/suffix/src/plugin.ts", - r##"import { definePlugin } from "#rpp"; + r##"import { definePlugin } from "rpp"; export default definePlugin<{ text: string }>({ processors: { @@ -49,8 +49,8 @@ export default definePlugin<{ text: string }>({ write( root, "rpp.config.ts", - r##"import { defineConfig } from "#rpp/config"; -import suffix from "#plugins/suffix"; + r##"import { defineConfig } from "rpp:config"; +import suffix from "plugin:suffix"; export default defineConfig({ pack: { name: "p", format: 34 }, @@ -83,7 +83,7 @@ fn unknown_plugin_package_errors() { write( root, "rpp.config.ts", - r##"import { defineConfig, plugin } from "#rpp/config"; + r##"import { defineConfig, plugin } from "rpp:config"; export default defineConfig({ pack: { name: "p", format: 34 }, plugins: [plugin("missing")] }); "##, @@ -114,24 +114,35 @@ fn config_errors_point_at_rpp_config_ts() { } #[test] -fn codegen_maps_plugin_config_paths() { +fn codegen_maps_plugin_module_paths() { let dir = tempfile::tempdir().unwrap(); let root = dir.path(); project(root, "undefined", r#"suffix({ text: "!" })"#); + write( + root, + "plugins/suffix/rpp.json", + r#"{ "name": "suffix", "version": "0.1.0", "entry": "src/plugin.ts", "config": "src/config.ts", + "exports": { "./raw": "src/raw.ts" } }"#, + ); let out = run(root, &["codegen"]); assert!(out.status.success(), "{}", stderr(&out)); let tsconfig = std::fs::read_to_string(root.join(".rpp/tsconfig.json")).unwrap(); assert!( - tsconfig.contains("\"#rpp/config\": [\"./sdk/config.ts\"]"), + tsconfig.contains("\"rpp:config\": [\"./sdk/config.ts\"]"), "{tsconfig}" ); - assert!(tsconfig.contains("\"#plugins/suffix\""), "{tsconfig}"); + assert!(tsconfig.contains("\"plugin:suffix\""), "{tsconfig}"); assert!( tsconfig.contains("plugins/suffix/src/config.ts"), "{tsconfig}" ); + assert!( + tsconfig.contains("\"plugin:suffix/raw\": [") + && tsconfig.contains("plugins/suffix/src/raw.ts"), + "{tsconfig}" + ); assert!(root.join(".rpp/sdk/config.ts").is_file()); } diff --git a/crates/rpp-cli/tests/window_host_e2e.rs b/crates/rpp-cli/tests/window_host_e2e.rs index 36a58aa..4004561 100644 --- a/crates/rpp-cli/tests/window_host_e2e.rs +++ b/crates/rpp-cli/tests/window_host_e2e.rs @@ -68,7 +68,7 @@ fn build_component(target_dir: &Path, v2: bool) -> PathBuf { fn config(hud_shaders: bool) -> String { format!( - r##"import {{ defineConfig, plugin }} from "#rpp/config"; + r##"import {{ defineConfig, plugin }} from "rpp:config"; export default defineConfig({{ pack: {{ name: "window-host-fixture", format: 84 }}, @@ -86,7 +86,7 @@ export default defineConfig({{ } /// A plugin that compiles `window/**` sources through the `compiler` component. -const COMPILE_PLUGIN: &str = r##"import { components, definePlugin } from "#rpp"; +const COMPILE_PLUGIN: &str = r##"import { components, definePlugin } from "rpp"; interface Options { namespace: string; @@ -282,7 +282,7 @@ fn window_component_diagnostic_keeps_stable_plugin_context() { } /// A plugin that round-trips option-shaped values through the component twice. -const ROUND_TRIP_PLUGIN: &str = r##"import { components, definePlugin } from "#rpp"; +const ROUND_TRIP_PLUGIN: &str = r##"import { components, definePlugin } from "rpp"; const check = (condition: boolean, message: string): void => { if (!condition) throw new Error(message); diff --git a/crates/rpp-js/src/bundle/mod.rs b/crates/rpp-js/src/bundle/mod.rs index 9866615..0e80abc 100644 --- a/crates/rpp-js/src/bundle/mod.rs +++ b/crates/rpp-js/src/bundle/mod.rs @@ -33,12 +33,12 @@ pub struct BundleRequest { /// The entry specifier: a key of `virtual_modules`, or a path relative to `root`. pub entry: String, /// Modules that exist only in memory, keyed by the exact import specifier - /// (e.g. `#rpp`, `rpp:entry`). They take precedence over filesystem resolution, + /// (e.g. `rpp`, `rpp:internal/entry`). They take precedence over filesystem resolution, /// are parsed as TypeScript, and may import each other or files under `root` /// (relative imports from a virtual module resolve against `root`). pub virtual_modules: BTreeMap, /// Directories outside `root` that may also be bundled, keyed by an exact import - /// specifier (e.g. `#plugins/window`) that resolves to that package's entry file. + /// specifier (e.g. `plugin:window`) that resolves to that package's entry file. /// Files inside a package directory may import each other relatively; source maps /// show them as `/`. pub packages: BTreeMap, @@ -96,7 +96,9 @@ pub fn bundle(request: &BundleRequest) -> Result { }; Ok(Bundle { code: chunk.code, - source_map: chunk.source_map, + source_map: chunk + .source_map + .ok_or_else(|| Error::Bundle("bundling produced no source map".to_string()))?, inputs: built.inputs, input_hashes: built.input_hashes, }) @@ -105,7 +107,7 @@ pub fn bundle(request: &BundleRequest) -> Result { /// Behaviour that differs between [`bundle`] and [`crate::pack`]. #[derive(Debug, Default)] pub(crate) struct Settings { - /// Specifiers left as imports; one ending in `/` matches every specifier under it. + /// Specifiers left as imports; one ending in `/` or `:` matches every specifier under it. pub externals: Vec, /// Specifiers that resolve to a file relative to `root`. pub aliases: BTreeMap, @@ -120,7 +122,8 @@ pub(crate) struct Settings { pub(crate) struct Chunk { pub file_name: String, pub code: String, - pub source_map: String, + /// `None` for a facade chunk that only re-exports other chunks. + pub source_map: Option, } pub(crate) struct Built { @@ -325,11 +328,7 @@ fn collect_chunks(assets: &[Output]) -> Result> { let Output::Chunk(chunk) = asset else { continue; }; - let source_map = chunk - .map - .as_ref() - .ok_or_else(|| Error::Bundle("bundling produced no source map".to_string()))? - .to_json_string(); + let source_map = chunk.map.as_ref().map(|map| map.to_json_string()); chunks.push(Chunk { file_name: chunk.filename.to_string(), code: chunk.code.clone(), diff --git a/crates/rpp-js/src/bundle/plugin.rs b/crates/rpp-js/src/bundle/plugin.rs index cc7117b..220e33f 100644 --- a/crates/rpp-js/src/bundle/plugin.rs +++ b/crates/rpp-js/src/bundle/plugin.rs @@ -86,9 +86,9 @@ impl VirtualModules { } fn is_external(&self, specifier: &str) -> bool { - self.externals - .iter() - .any(|e| specifier == e || (e.ends_with('/') && specifier.starts_with(e.as_str()))) + self.externals.iter().any(|e| { + specifier == e || (e.ends_with(['/', ':']) && specifier.starts_with(e.as_str())) + }) } /// Specifiers the plugin answers itself, without Rolldown's resolver. diff --git a/crates/rpp-js/src/pack.rs b/crates/rpp-js/src/pack.rs index 14025fa..7e39a14 100644 --- a/crates/rpp-js/src/pack.rs +++ b/crates/rpp-js/src/pack.rs @@ -29,8 +29,10 @@ pub struct PackRequest { pub plugin: String, /// The config entry module, relative to `root`. pub config: Option, - /// The specifier other plugins use to import this plugin's config (e.g. - /// `#plugins/window`); it resolves to the `config` entry. + /// Extra entry modules by subpath, relative to `root`. + pub exports: BTreeMap, + /// The specifier other code uses to import this plugin's config (e.g. `plugin:window`); + /// it resolves to the `config` entry, and `/` to each export. pub self_specifier: Option, /// See [`BundleRequest::jsx_import_source`]; the import source stays an import. pub jsx_import_source: Option, @@ -43,17 +45,20 @@ pub struct PackOutput { pub plugin: String, /// The archive path of the bundled config entry, `dist/config.js`. pub config: Option, - /// `dist/*.js` chunks, their `.js.map` files and `dist/config.d.ts`. + /// The archive path of each bundled export by subpath, `dist/exports/.js`. + pub exports: BTreeMap, + /// `dist/**.js` chunks, their `.js.map` files, and a `.d.ts` stub beside each TypeScript + /// config or export entry. pub files: BTreeMap, - /// `types/**.d.ts`, one per TypeScript file of the config's import graph outside - /// `node_modules`. + /// `types/**.d.ts`, one per TypeScript file of the config's and exports' import graphs + /// outside `node_modules`. pub declarations: BTreeMap, } /// Bundle a plugin's entries into self-contained ES modules with npm dependencies /// inlined from `node_modules` (including hoisted ones outside `root`). /// -/// `#rpp` and `#rpp/*` stay as imports. Source map sources are relative to the map +/// `rpp` and `rpp:*` stay as imports. Source map sources are relative to the map /// file (`../src/plugin.ts`), with dependency files under `node_modules/`. /// /// # Errors @@ -70,31 +75,51 @@ pub fn pack(request: &PackRequest) -> Result { ..Default::default() }; let mut settings = settings(request); - let inputs = request + let modules: Vec<(String, &str)> = request .config .iter() - .map(|config| input(CONFIG, config)) + .map(|config| (CONFIG.to_string(), config.as_str())) + .chain( + request + .exports + .iter() + .map(|(subpath, module)| (format!("exports/{subpath}"), module.as_str())), + ) + .collect(); + let inputs = modules + .iter() + .map(|(name, module)| input(name, module)) .chain([input(PLUGIN, &request.plugin)]) .collect(); let built = build(&bundle_request, &settings, inputs)?; let mut files = chunk_files(built.chunks)?; - let mut declarations = BTreeMap::new(); - if let Some(config) = request.config.as_deref().filter(|c| is_typescript(c)) { - settings.split = false; - let Built { inputs, .. } = build(&bundle_request, &settings, vec![input(CONFIG, config)])?; - let declared = declare(&request.root, &inputs)?; - if let Some(stub) = config_stub(config, &declared) { - files.insert("dist/config.d.ts".to_string(), stub); + settings.split = false; + let mut declared_inputs = BTreeSet::new(); + let typed: Vec<_> = modules.iter().filter(|(_, m)| is_typescript(m)).collect(); + for (name, module) in &typed { + let Built { inputs, .. } = build(&bundle_request, &settings, vec![input(name, module)])?; + declared_inputs.extend(inputs); + } + let declared_inputs: Vec<_> = declared_inputs.into_iter().collect(); + let declared = declare(&request.root, &declared_inputs)?; + for (name, module) in typed { + if let Some(stub) = entry_stub(name, module, &declared) { + files.insert(format!("dist/{name}.d.ts"), stub); } - declarations = declared - .into_iter() - .map(|(path, declaration)| (path, declaration.text)) - .collect(); } + let declarations = declared + .into_iter() + .map(|(path, declaration)| (path, declaration.text)) + .collect(); Ok(PackOutput { plugin: entry_path(PLUGIN), config: request.config.as_ref().map(|_| entry_path(CONFIG)), + exports: request + .exports + .keys() + .map(|subpath| (subpath.clone(), entry_path(&format!("exports/{subpath}")))) + .collect(), files, declarations, }) @@ -102,14 +127,21 @@ pub fn pack(request: &PackRequest) -> Result { fn settings(request: &PackRequest) -> Settings { let mut settings = Settings { - externals: vec!["#rpp".to_string(), "#rpp/".to_string()], + externals: vec!["rpp".to_string(), "rpp:".to_string()], allow_node_modules: true, main_fields: Some(vec!["module".to_string(), "main".to_string()]), split: true, ..Default::default() }; - if let (Some(specifier), Some(config)) = (&request.self_specifier, &request.config) { - settings.aliases.insert(specifier.clone(), config.clone()); + if let Some(specifier) = &request.self_specifier { + if let Some(config) = &request.config { + settings.aliases.insert(specifier.clone(), config.clone()); + } + for (subpath, module) in &request.exports { + settings + .aliases + .insert(format!("{specifier}/{subpath}"), module.clone()); + } } settings } @@ -126,21 +158,24 @@ fn entry_path(name: &str) -> String { format!("dist/{name}.js") } -/// `dist/` files for each chunk: its code, linked to a `.js.map` beside it. +/// `dist/` files for each chunk: its code, linked to a `.js.map` beside it when it has one. fn chunk_files(chunks: Vec) -> Result> { let mut files = BTreeMap::new(); for chunk in chunks { - let map_name = format!("{}.map", chunk.file_name); let mut code = chunk.code; if !code.ends_with('\n') { code.push('\n'); } - code.push_str(&format!("//# sourceMappingURL={map_name}\n")); + if let Some(source_map) = &chunk.source_map { + let map_path = format!("{}.map", chunk.file_name); + let map_name = map_path.rsplit('/').next().unwrap_or(&map_path); + code.push_str(&format!("//# sourceMappingURL={map_name}\n")); + files.insert( + format!("dist/{map_path}"), + relative_to_dist(source_map, &chunk.file_name)?, + ); + } files.insert(format!("dist/{}", chunk.file_name), code); - files.insert( - format!("dist/{map_name}"), - relative_to_dist(&chunk.source_map)?, - ); } Ok(files) } @@ -166,13 +201,18 @@ fn declaration_path(relative: &str) -> String { } } -/// `dist/config.d.ts`, re-exporting the config's generated declaration. -fn config_stub(config: &str, declarations: &BTreeMap) -> Option { - let config = config.trim_start_matches("./"); - let declaration = declarations.get(&declaration_path(config))?; - let target = match config.strip_suffix(".mts") { - Some(stem) => format!("../types/{stem}.mjs"), - None => format!("../types/{}.js", ts_stem(config)), +/// `dist/.d.ts`, re-exporting the generated declaration of the entry `module`. +fn entry_stub( + name: &str, + module: &str, + declarations: &BTreeMap, +) -> Option { + let module = module.trim_start_matches("./"); + let declaration = declarations.get(&declaration_path(module))?; + let up = "../".repeat(name.matches('/').count() + 1); + let target = match module.strip_suffix(".mts") { + Some(stem) => format!("{up}types/{stem}.mjs"), + None => format!("{up}types/{}.js", ts_stem(module)), }; let mut stub = format!("export * from \"{target}\";\n"); if declaration.has_default { @@ -182,14 +222,15 @@ fn config_stub(config: &str, declarations: &BTreeMap) -> Op } /// Rewrites the map's sources (relative to the plugin directory) to be relative to -/// `dist/`. -fn relative_to_dist(source_map: &str) -> Result { +/// `dist/`. +fn relative_to_dist(source_map: &str, file_name: &str) -> Result { + let up = "../".repeat(file_name.matches('/').count() + 1); let mut map: serde_json::Value = serde_json::from_str(source_map) .map_err(|e| Error::Bundle(format!("invalid source map: {e}")))?; if let Some(sources) = map["sources"].as_array_mut() { for source in sources { if let Some(text) = source.as_str() { - *source = format!("../{text}").into(); + *source = format!("{up}{text}").into(); } } } diff --git a/crates/rpp-js/tests/bundle.rs b/crates/rpp-js/tests/bundle.rs index ede6d75..57d12e3 100644 --- a/crates/rpp-js/tests/bundle.rs +++ b/crates/rpp-js/tests/bundle.rs @@ -113,13 +113,13 @@ fn virtual_entry_imports_virtual_and_real_modules() { "src/real.ts", "export function real(): string { return 'real'; }\n", ); - let mut req = request(&root, "rpp:entry"); + let mut req = request(&root, "rpp:internal/entry"); req.virtual_modules.insert( - "rpp:entry".to_string(), - "import { host } from '#rpp';\nimport { real } from './src/real';\nexport const out: string = host() + real();\n".to_string(), + "rpp:internal/entry".to_string(), + "import { host } from 'rpp';\nimport { real } from './src/real';\nexport const out: string = host() + real();\n".to_string(), ); req.virtual_modules.insert( - "#rpp".to_string(), + "rpp".to_string(), "export function host(): string { return 'host'; }\n".to_string(), ); @@ -129,8 +129,11 @@ fn virtual_entry_imports_virtual_and_real_modules() { assert!(bundle.code.contains("'host'") || bundle.code.contains("\"host\"")); assert_eq!(bundle.inputs, vec![root.join("src/real.ts")]); let sources = source_list(&bundle.source_map); - assert!(sources.contains(&"#rpp".to_string()), "{sources:?}"); - assert!(sources.contains(&"rpp:entry".to_string()), "{sources:?}"); + assert!(sources.contains(&"rpp".to_string()), "{sources:?}"); + assert!( + sources.contains(&"rpp:internal/entry".to_string()), + "{sources:?}" + ); assert!(sources.contains(&"src/real.ts".to_string()), "{sources:?}"); } diff --git a/crates/rpp-js/tests/bundle_packages.rs b/crates/rpp-js/tests/bundle_packages.rs index 5a475ec..367a799 100644 --- a/crates/rpp-js/tests/bundle_packages.rs +++ b/crates/rpp-js/tests/bundle_packages.rs @@ -10,7 +10,7 @@ use common::{bundle_error, request, source_list, write}; fn package_request(root: &Path, pkg: &Path) -> BundleRequest { let mut req = request(root, "main.ts"); req.packages.insert( - "#plugins/window".to_string(), + "plugin:window".to_string(), BundlePackage { dir: pkg.to_path_buf(), entry: "index.ts".to_string(), @@ -28,12 +28,12 @@ fn package_specifier_resolves_outside_root() { write( &root, "main.ts", - "import { win } from '#plugins/window';\nexport const out: string = win();\n", + "import { win } from 'plugin:window';\nexport const out: string = win();\n", ); write( &pkg, "index.ts", - "import { host } from '#rpp';\nimport { name } from './lib/name';\nexport const win = (): string => host() + name;\n", + "import { host } from 'rpp';\nimport { name } from './lib/name';\nexport const win = (): string => host() + name;\n", ); write( &pkg, @@ -42,7 +42,7 @@ fn package_specifier_resolves_outside_root() { ); let mut req = package_request(&root, &pkg); req.virtual_modules.insert( - "#rpp".to_string(), + "rpp".to_string(), "export function host(): string { return 'host'; }\n".to_string(), ); @@ -68,7 +68,7 @@ fn package_files_cannot_escape_their_dir() { write( &root, "main.ts", - "import { win } from '#plugins/window';\nexport const out = win;\n", + "import { win } from 'plugin:window';\nexport const out = win;\n", ); write( &pkg, @@ -91,7 +91,7 @@ fn package_sources_are_labelled_by_specifier() { write( &root, "main.ts", - "import { win } from '#plugins/window';\nexport const out = win;\n", + "import { win } from 'plugin:window';\nexport const out = win;\n", ); write( &pkg, @@ -111,9 +111,9 @@ fn package_sources_are_labelled_by_specifier() { assert_eq!( sources, vec![ - "#plugins/window/index.ts", - "#plugins/window/lib/n.ts", - "main.ts" + "main.ts", + "plugin:window/index.ts", + "plugin:window/lib/n.ts" ] ); } @@ -157,3 +157,36 @@ fn bundle_records_source_map_inputs() { assert!(bundle.inputs.contains(&root.join("dist/lib.js.map"))); } + +#[test] +fn package_subpaths_share_modules() { + let root_dir = TempDir::new().unwrap(); + let pkg_dir = TempDir::new().unwrap(); + let root = root_dir.path().canonicalize().unwrap(); + let pkg = pkg_dir.path().canonicalize().unwrap(); + write( + &root, + "main.ts", + "import { a } from 'plugin:window';\nimport { b } from 'plugin:window/raw';\nexport const same: boolean = a === b;\n", + ); + write(&pkg, "state.ts", "export const state = {};\n"); + write(&pkg, "index.ts", "export { state as a } from './state';\n"); + write(&pkg, "raw.ts", "export { state as b } from './state';\n"); + let mut req = package_request(&root, &pkg); + req.packages.insert( + "plugin:window/raw".to_string(), + BundlePackage { + dir: pkg.clone(), + entry: "raw.ts".to_string(), + }, + ); + + let bundle = bundle(&req).unwrap(); + + assert_eq!( + bundle.code.matches("const state").count(), + 1, + "{}", + bundle.code + ); +} diff --git a/crates/rpp-js/tests/pack.rs b/crates/rpp-js/tests/pack.rs index d8d2a96..d3e5b4e 100644 --- a/crates/rpp-js/tests/pack.rs +++ b/crates/rpp-js/tests/pack.rs @@ -25,6 +25,7 @@ fn request(root: &Path, plugin: &str, config: Option<&str>) -> PackRequest { config: config.map(str::to_string), self_specifier: None, jsx_import_source: None, + ..Default::default() } } @@ -130,14 +131,14 @@ fn pack_keeps_rpp_external() { write( root, "plugin.ts", - "import { log } from '#rpp';\nimport { read } from '#rpp/fs';\nexport const run = (): void => { log(read()); };\n", + "import { log } from 'rpp';\nimport { read } from 'rpp:fs';\nexport const run = (): void => { log(read()); };\n", ); let output = packed(root, "plugin.ts", None); let code = &output.files["dist/plugin.js"]; - assert!(code.contains("from \"#rpp\""), "{code}"); - assert!(code.contains("from \"#rpp/fs\""), "{code}"); + assert!(code.contains("from \"rpp\""), "{code}"); + assert!(code.contains("from \"rpp:fs\""), "{code}"); } #[test] @@ -199,7 +200,7 @@ fn pack_emits_isolated_declarations() { write(root, "src/plugin.ts", "export const run = () => 1;\n"); let mut req = request(root, "src/plugin.ts", Some("src/config.ts")); - req.self_specifier = Some("#plugins/self".to_string()); + req.self_specifier = Some("plugin:self".to_string()); let output = pack(&req).unwrap(); let keys: Vec<_> = output.declarations.keys().map(String::as_str).collect(); @@ -336,3 +337,45 @@ fn pack_stub_reexports_default_declared_by_specifier() { "{stub}" ); } + +#[test] +fn pack_bundles_exports_beside_the_config() { + let dir = TempDir::new().unwrap(); + let root = dir.path(); + write( + root, + "src/shared.ts", + "export const shared = (): string => 'shared-marker';\n", + ); + write( + root, + "src/plugin.ts", + "export const run = (): number => 1;\n", + ); + write( + root, + "src/config.ts", + "import { raw } from 'plugin:window/raw';\nexport const name: string = raw();\n", + ); + write( + root, + "src/raw.ts", + "import { shared } from './shared';\nexport const raw = (): string => shared();\n", + ); + let mut request = request(root, "src/plugin.ts", Some("src/config.ts")); + request.self_specifier = Some("plugin:window".to_string()); + request + .exports + .insert("raw".to_string(), "src/raw.ts".to_string()); + + let output = pack(&request).unwrap(); + + assert_eq!(output.exports["raw"], "dist/exports/raw.js"); + assert!(output.files["dist/exports/raw.js"].contains("../chunk-")); + assert!(output.files["dist/config.js"].contains("./chunk-")); + assert_eq!( + output.files["dist/exports/raw.d.ts"], + "export * from \"../../types/src/raw.js\";\n" + ); + assert!(output.declarations.contains_key("types/src/raw.d.ts")); +} diff --git a/crates/rpp/src/js/bundle.rs b/crates/rpp/src/js/bundle.rs index 1587704..f97396b 100644 --- a/crates/rpp/src/js/bundle.rs +++ b/crates/rpp/src/js/bundle.rs @@ -8,9 +8,9 @@ use serde_json::Value; use super::bundle_cache::cached_bundle; use super::discover::{discovered_module, Discovery}; -use super::{jsx_modules, JSX_IMPORT_SOURCE, SDK_CONFIG, SDK_INDEX}; +use super::{hint_renamed_specifiers, jsx_modules, JSX_IMPORT_SOURCE, SDK_CONFIG, SDK_INDEX}; use crate::error::{Error, Result}; -use crate::manifest::PluginManifest; +use crate::manifest::{plugin_modules, PluginManifest}; use crate::util::path::to_forward_slash; const RUNTIME_SOURCE: &str = include_str!("sdk/runtime.ts"); @@ -40,13 +40,13 @@ pub(super) fn bundle_plugin( if discovery.is_empty() { let request = BundleRequest { root: root.to_path_buf(), - entry: "rpp:entry".into(), + entry: "rpp:internal/entry".into(), virtual_modules: virtual_modules(&format!("./{}", manifest.entry), false), jsx_import_source: Some(JSX_IMPORT_SOURCE.to_string()), ..Default::default() }; let bundle = cached_bundle(cache_dir, &manifest.id, &request, || { - rpp_js::bundle(&request).map_err(|e| e.to_string()) + rpp_js::bundle(&request).map_err(|e| hint_renamed_specifiers(e.to_string())) }) .map_err(load_error)?; return Ok(PluginBundle { @@ -77,20 +77,23 @@ pub(super) fn bundle_plugin( fn virtual_modules(entry: &str, discovered: bool) -> BTreeMap { let (import, register) = if discovered { ( - "import discovered from \"rpp:discovered\";\n", + "import discovered from \"rpp:internal/discovered\";\n", "register(plugin, discovered);", ) } else { ("", "register(plugin);") }; let entry_module = format!( - "import plugin from {};\nimport {{ register }} from \"rpp:runtime\";\n{import}{register}\nexport * from \"rpp:runtime\";\n", + "import plugin from {};\nimport {{ register }} from \"rpp:internal/runtime\";\n{import}{register}\nexport * from \"rpp:internal/runtime\";\n", Value::String(entry.to_string()) ); [ - ("#rpp".to_string(), SDK_INDEX.to_string()), - ("rpp:runtime".to_string(), RUNTIME_SOURCE.to_string()), - ("rpp:entry".to_string(), entry_module), + ("rpp".to_string(), SDK_INDEX.to_string()), + ( + "rpp:internal/runtime".to_string(), + RUNTIME_SOURCE.to_string(), + ), + ("rpp:internal/entry".to_string(), entry_module), ] .into_iter() .chain(jsx_modules()) @@ -98,7 +101,7 @@ fn virtual_modules(entry: &str, discovered: bool) -> BTreeMap { } /// Bundle the plugin together with the files its patterns match under `source`. The bundle -/// root is `source`; the plugin's own files are the package `#plugin`. +/// root is `source`; the plugin's own files are the package `rpp:internal/plugin`. fn bundle_discovered( manifest: &PluginManifest, plugin_root: &Path, @@ -108,29 +111,32 @@ fn bundle_discovered( ) -> std::result::Result<(Bundle, BTreeSet), String> { let entries = discovery.discover(source)?; - let mut virtual_modules = virtual_modules("#plugin", true); - virtual_modules.insert("#rpp/config".into(), SDK_CONFIG.into()); + let mut virtual_modules = virtual_modules("rpp:internal/plugin", true); + virtual_modules.insert("rpp:config".into(), SDK_CONFIG.into()); virtual_modules.insert( - "rpp:discovered".into(), + "rpp:internal/discovered".into(), discovered_module("", discovery, &entries), ); let package = |entry: &str| BundlePackage { dir: plugin_root.to_path_buf(), entry: entry.to_string(), }; - let mut packages = BTreeMap::from([("#plugin".to_string(), package(&manifest.entry))]); - if let Some(config) = &manifest.config { - packages.insert(format!("#plugins/{}", manifest.id), package(config)); + let mut packages = + BTreeMap::from([("rpp:internal/plugin".to_string(), package(&manifest.entry))]); + for (specifier, module) in + plugin_modules(&manifest.id, manifest.config.as_deref(), &manifest.exports) + { + packages.insert(specifier, package(module)); } let request = BundleRequest { root: source.to_path_buf(), - entry: "rpp:entry".into(), + entry: "rpp:internal/entry".into(), virtual_modules, packages, jsx_import_source: Some(JSX_IMPORT_SOURCE.to_string()), }; let bundle = cached_bundle(cache_dir, &manifest.id, &request, || { - rpp_js::bundle(&request).map_err(|e| e.to_string()) + rpp_js::bundle(&request).map_err(|e| hint_renamed_specifiers(e.to_string())) })?; let authoring = bundle diff --git a/crates/rpp/src/js/bundle_cache.rs b/crates/rpp/src/js/bundle_cache.rs index 2ec716f..704b45b 100644 --- a/crates/rpp/src/js/bundle_cache.rs +++ b/crates/rpp/src/js/bundle_cache.rs @@ -231,16 +231,17 @@ mod tests { let f = Fixture::new(); f.get(&f.cache, &f.request); let mut changed = f.request.clone(); - changed - .virtual_modules - .insert("rpp:discovered".into(), "export default {};".into()); + changed.virtual_modules.insert( + "rpp:internal/discovered".into(), + "export default {};".into(), + ); f.get(&f.cache, &changed); assert_eq!(f.builds.get(), 2); - changed.jsx_import_source = Some("#plugins/ui".into()); + changed.jsx_import_source = Some("plugin:ui".into()); f.get(&f.cache, &changed); f.get(&f.cache, &changed); assert_eq!(f.builds.get(), 3); - changed.jsx_import_source = Some("#plugins/other".into()); + changed.jsx_import_source = Some("plugin:other".into()); f.get(&f.cache, &changed); assert_eq!(f.builds.get(), 4); } diff --git a/crates/rpp/src/js/config.rs b/crates/rpp/src/js/config.rs index 5fd9f5a..6ceb32e 100644 --- a/crates/rpp/src/js/config.rs +++ b/crates/rpp/src/js/config.rs @@ -6,9 +6,13 @@ use std::path::{Path, PathBuf}; use rpp_js::{Bundle, BundlePackage, BundleRequest, Call, Cancellation, Clock, Host, HostReply}; use serde_json::Value; -use super::{instance, jsx_modules, log, runtime_limits, JSX_IMPORT_SOURCE, SDK_CONFIG}; +use super::{ + hint_renamed_specifiers, instance, jsx_modules, log, runtime_limits, JSX_IMPORT_SOURCE, + SDK_CONFIG, +}; use crate::config::{Config, LimitsConfig}; use crate::error::{Error, Result}; +use crate::manifest::plugin_modules; const ENTRY_MODULE: &str = "import config from \"./rpp.config.ts\";\nexport function main() { return config; }\n"; @@ -18,14 +22,16 @@ const EVALUATION_DEADLINE_SECONDS: u64 = 30; /// The project config file name. pub const CONFIG_FILE: &str = "rpp.config.ts"; -/// A resolved dependency whose config module `rpp.config.ts` may import as -/// `#plugins/`. +/// A resolved dependency whose modules `rpp.config.ts` may import as `plugin:` and +/// `plugin:/`. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ConfigPackage { /// The package root. pub dir: PathBuf, /// Its config module, relative to `dir` ([`crate::manifest::PluginManifest::config`]). pub config: Option, + /// Its extra modules by subpath ([`crate::manifest::PluginManifest::exports`]). + pub exports: BTreeMap, } /// An evaluated project config. @@ -37,8 +43,8 @@ pub struct EvaluatedConfig { pub inputs: Vec, } -/// Bundle `/rpp.config.ts` with `#rpp/config` (the embedded -/// `sdk/config.ts`) and `#plugins/` for each package that has a config module, +/// Bundle `/rpp.config.ts` with `rpp:config` (the embedded +/// `sdk/config.ts`) and each package's [`plugin_modules`], /// evaluate it with a fixed clock, the default `build.limits` (with a 30-second deadline) and no /// host functions, and /// convert its default export with [`Config::from_ts_json`]. @@ -58,7 +64,8 @@ pub fn evaluate_config( message: message.trim_end().to_string(), }; - let bundle = bundle_config(project_root, packages).map_err(|e| fail(e.to_string()))?; + let bundle = bundle_config(project_root, packages) + .map_err(|e| fail(hint_renamed_specifiers(e.to_string())))?; let limits = runtime_limits(&config_limits()); let clock = Clock::Fixed { @@ -111,20 +118,27 @@ fn bundle_config( ) -> rpp_js::Result { rpp_js::bundle(&BundleRequest { root: project_root.to_path_buf(), - entry: "rpp:config-entry".into(), + entry: "rpp:internal/config-entry".into(), virtual_modules: [ - ("rpp:config-entry".to_string(), ENTRY_MODULE.to_string()), - ("#rpp/config".to_string(), SDK_CONFIG.to_string()), + ( + "rpp:internal/config-entry".to_string(), + ENTRY_MODULE.to_string(), + ), + ("rpp:config".to_string(), SDK_CONFIG.to_string()), ] .into_iter() .chain(jsx_modules()) .collect(), packages: packages .iter() - .filter_map(|(name, package)| { - let entry = package.config.clone()?; - let dir = package.dir.clone(); - Some((format!("#plugins/{name}"), BundlePackage { dir, entry })) + .flat_map(|(name, package)| { + plugin_modules(name, package.config.as_deref(), &package.exports) + .into_iter() + .map(|(specifier, entry)| { + let dir = package.dir.clone(); + let entry = entry.to_string(); + (specifier, BundlePackage { dir, entry }) + }) }) .collect(), jsx_import_source: Some(JSX_IMPORT_SOURCE.to_string()), diff --git a/crates/rpp/src/js/discover.rs b/crates/rpp/src/js/discover.rs index 9606b86..832985a 100644 --- a/crates/rpp/src/js/discover.rs +++ b/crates/rpp/src/js/discover.rs @@ -1,4 +1,4 @@ -//! Entry discovery: plugin-declared source patterns and the generated `rpp:discovered` module. +//! Entry discovery: plugin-declared source patterns and the generated `rpp:internal/discovered` module. use std::collections::BTreeMap; use std::path::Path; @@ -102,7 +102,7 @@ fn namespace_segment(pattern: &str) -> Option { None } -/// The source of `rpp:discovered`: every declared name maps to its modules. +/// The source of `rpp:internal/discovered`: every declared name maps to its modules. /// `source_from_root` is the source directory relative to the bundle root, or empty. pub(crate) fn discovered_module( source_from_root: &str, diff --git a/crates/rpp/src/js/factory.rs b/crates/rpp/src/js/factory.rs index 6dae6fd..7ff411c 100644 --- a/crates/rpp/src/js/factory.rs +++ b/crates/rpp/src/js/factory.rs @@ -404,7 +404,7 @@ mod tests { write( root, "src/plugin.ts", - r##"import { definePlugin } from "#rpp"; + r##"import { definePlugin } from "rpp"; import { name } from "./name.ts"; export default definePlugin({ processors: { [name]: { files: "**/*", run() {} } } }); "##, diff --git a/crates/rpp/src/js/mod.rs b/crates/rpp/src/js/mod.rs index fa278e1..786af03 100644 --- a/crates/rpp/src/js/mod.rs +++ b/crates/rpp/src/js/mod.rs @@ -3,19 +3,20 @@ //! A plugin whose `rpp.json` `entry` ends in `.ts`, `.mts`, `.js` or `.mjs` is bundled with //! three virtual modules: //! -//! - `#rpp`: the SDK, `sdk/index.ts` followed by `sdk/components.ts`. [`SDK_FILES`] also +//! - `rpp`: the SDK, `sdk/index.ts` followed by `sdk/components.ts`. [`SDK_FILES`] also //! carries `sdk/bridge.d.ts`, the `__rpp` type declaration the sources reference; bundles //! never include it. -//! - `rpp:runtime`: `sdk/runtime.ts`, the dispatcher below. -//! - `rpp:entry`: `import plugin from "./"; import { register } from "rpp:runtime"; -//! register(plugin); export * from "rpp:runtime";` +//! - `rpp:internal/runtime`: `sdk/runtime.ts`, the dispatcher below. +//! - `rpp:internal/entry`: `import plugin from "./"; import { register } from "rpp:internal/runtime"; +//! register(plugin); export * from "rpp:internal/runtime";` //! -//! Every bundle also gets `#rpp/jsx` and `#rpp/jsx/jsx-runtime` (`sdk/jsx.ts`), the import +//! Every bundle also gets `rpp:jsx` and `rpp:jsx/jsx-runtime` (`sdk/jsx.ts`), the import //! source that `.tsx` files compile against. //! -//! A manifest with `discover` patterns also gets `#rpp/config`, a `#plugins/` package for -//! its config module, `#plugin` (the entry) and `rpp:discovered` (the matched files' namespace -//! objects); the bundle root is the source dir. `rpp:entry` then calls +//! A manifest with `discover` patterns also gets `rpp:config`, `plugin:` and +//! `plugin:/` packages for its config module and exports, `rpp:internal/plugin` +//! (the entry) and `rpp:internal/discovered` (the matched files' namespace objects); the +//! bundle root is the source dir. `rpp:internal/entry` then calls //! `register(plugin, discovered)`. Processors cannot read `ctx.discovered()`. //! //! Bundles are cached under `/bundles` and reused while the request and every @@ -87,18 +88,18 @@ use crate::config::LimitsConfig; pub use config::{evaluate_config, ConfigPackage, EvaluatedConfig, CONFIG_FILE}; pub use factory::{JsPluginFactory, JsPluginSpec}; -/// The plugin SDK imported as `#rpp`: `sdk/index.ts` with `sdk/components.ts` appended. +/// The plugin SDK imported as `rpp`: `sdk/index.ts` with `sdk/components.ts` appended. pub const SDK_INDEX: &str = concat!( include_str!("sdk/index.ts"), "\n", include_str!("sdk/components.ts") ); -/// `sdk/config.ts`, the config SDK imported as `#rpp/config`. +/// `sdk/config.ts`, the config SDK imported as `rpp:config`. pub const SDK_CONFIG: &str = include_str!("sdk/config.ts"); -/// `sdk/jsx.ts`, the JSX runtime imported as `#rpp/jsx` and `#rpp/jsx/jsx-runtime`. +/// `sdk/jsx.ts`, the JSX runtime imported as `rpp:jsx` and `rpp:jsx/jsx-runtime`. pub const SDK_JSX: &str = include_str!("sdk/jsx.ts"); /// The JSX import source of `.tsx` files. -pub const JSX_IMPORT_SOURCE: &str = "#rpp/jsx"; +pub const JSX_IMPORT_SOURCE: &str = "rpp:jsx"; /// `sdk/bridge.d.ts`, the `__rpp` declaration the SDK sources reference. Not a bundled module. const SDK_BRIDGE: &str = include_str!("sdk/bridge.d.ts"); @@ -112,7 +113,7 @@ pub const SDK_FILES: &[(&str, &str)] = &[ ]; /// The runtime limits `build.limits` sets for each plugin runtime and call. -/// The `#rpp/jsx` and `#rpp/jsx/jsx-runtime` virtual modules. +/// The `rpp:jsx` and `rpp:jsx/jsx-runtime` virtual modules. pub fn jsx_modules() -> [(String, String); 2] { [ (JSX_IMPORT_SOURCE.to_string(), SDK_JSX.to_string()), @@ -123,6 +124,19 @@ pub fn jsx_modules() -> [(String, String); 2] { ] } +/// `message` with a pointer to the current specifiers when it reports an unresolved `#rpp` +/// or `#plugins/` import. +pub(crate) fn hint_renamed_specifiers(message: String) -> String { + if message.contains("cannot import `#rpp") || message.contains("cannot import `#plugins/") { + format!( + "{message}\nthe SDK is imported as `rpp`, `rpp:config` and `rpp:jsx`, and plugin \ + modules as `plugin:`" + ) + } else { + message + } +} + fn runtime_limits(limits: &LimitsConfig) -> rpp_js::Limits { rpp_js::Limits { heap_bytes: limits.memory_limit_mb as usize * 1024 * 1024, diff --git a/crates/rpp/src/js/sdk/components.ts b/crates/rpp/src/js/sdk/components.ts index a8eb31f..e983143 100644 --- a/crates/rpp/src/js/sdk/components.ts +++ b/crates/rpp/src/js/sdk/components.ts @@ -1,6 +1,6 @@ /// // WASM components for plugins: `components.load` and the wire codec for component values. -// rpp appends this file to `index.ts` to form `#rpp`, so it shares that module's scope. +// rpp appends this file to `index.ts` to form `rpp`, so it shares that module's scope. /** Wire form of a component value type. */ type TypeDesc = diff --git a/crates/rpp/src/js/sdk/config.ts b/crates/rpp/src/js/sdk/config.ts index 507802e..b44b0c7 100644 --- a/crates/rpp/src/js/sdk/config.ts +++ b/crates/rpp/src/js/sdk/config.ts @@ -1,4 +1,4 @@ -// Project configuration types, imported as `#rpp/config` from `rpp.config.ts` and +// Project configuration types, imported as `rpp:config` from `rpp.config.ts` and // from plugins' config modules. /** A resource pack format, or an inclusive range of them. */ diff --git a/crates/rpp/src/js/sdk/index.ts b/crates/rpp/src/js/sdk/index.ts index 15809a0..c6487d6 100644 --- a/crates/rpp/src/js/sdk/index.ts +++ b/crates/rpp/src/js/sdk/index.ts @@ -1,5 +1,5 @@ /// -// The rpp plugin SDK, imported as `#rpp`. Embedded in rpp and written to +// The rpp plugin SDK, imported as `rpp`. Embedded in rpp and written to // `.rpp/sdk/index.ts` by `rpp codegen`, so it always matches the running rpp. const encoder = new TextEncoder(); diff --git a/crates/rpp/src/js/sdk/jsx.ts b/crates/rpp/src/js/sdk/jsx.ts index 302c15b..06d4625 100644 --- a/crates/rpp/src/js/sdk/jsx.ts +++ b/crates/rpp/src/js/sdk/jsx.ts @@ -1,4 +1,4 @@ -// The JSX runtime for `.tsx` sources, imported as `#rpp/jsx` and `#rpp/jsx/jsx-runtime`. +// The JSX runtime for `.tsx` sources, imported as `rpp:jsx` and `rpp:jsx/jsx-runtime`. // A tag is a component: a function called with its props whose result is the element. /** Anything JSX accepts as a child. */ diff --git a/crates/rpp/src/js/sdk/runtime.ts b/crates/rpp/src/js/sdk/runtime.ts index 2db81fc..d888d86 100644 --- a/crates/rpp/src/js/sdk/runtime.ts +++ b/crates/rpp/src/js/sdk/runtime.ts @@ -1,5 +1,5 @@ /// -// The dispatcher bundled with every plugin as `rpp:runtime`. Its exports are the functions rpp calls. +// The dispatcher bundled with every plugin as `rpp:internal/runtime`. Its exports are the functions rpp calls. import type { BuildStats, @@ -11,7 +11,7 @@ import type { Pack, Plugin, Processor, -} from "#rpp"; +} from "rpp"; const encoder = new TextEncoder(); const decoder = new TextDecoder(); diff --git a/crates/rpp/src/manifest.rs b/crates/rpp/src/manifest.rs index 4ffbea4..db5eda2 100644 --- a/crates/rpp/src/manifest.rs +++ b/crates/rpp/src/manifest.rs @@ -22,6 +22,24 @@ pub(crate) fn is_valid_id(name: &str) -> bool { && bytes.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'_' | b'-')) } +/// The modules a plugin exposes under `name`, as `(specifier, module)`: `plugin:` for +/// its `config` module and `plugin:/` for each of its `exports`. +pub fn plugin_modules<'a>( + name: &str, + config: Option<&'a str>, + exports: &'a BTreeMap, +) -> Vec<(String, &'a str)> { + config + .map(|config| (format!("plugin:{name}"), config)) + .into_iter() + .chain( + exports + .iter() + .map(|(subpath, module)| (format!("plugin:{name}/{subpath}"), module.as_str())), + ) + .collect() +} + /// A validated `rpp.json` manifest. #[derive(Debug, Clone)] pub struct PluginManifest { @@ -38,6 +56,8 @@ pub struct PluginManifest { /// Config module (relative to the plugin root) whose default export is the /// plugin's config factory. pub config: Option, + /// Extra modules by subpath (`raw` for the `./raw` key), relative to the plugin root. + pub exports: BTreeMap, /// Required rpp version range. pub rpp: Option, /// Entry discovery patterns by name, relative to the pack source directory. @@ -68,6 +88,8 @@ struct RawJsonManifest { #[serde(default)] config: Option, #[serde(default)] + exports: BTreeMap, + #[serde(default)] components: BTreeMap, #[serde(default)] discover: BTreeMap, @@ -135,6 +157,21 @@ fn parse_components(raw: BTreeMap) -> Check) -> Check> { + raw.into_iter() + .map(|(key, module)| { + let subpath = key + .strip_prefix("./") + .filter(|subpath| validate_relative(subpath).is_ok()) + .ok_or_else(|| { + format!("`exports` key `{key}` must be `./` followed by a relative path") + })?; + validate_relative(&module).map_err(|m| format!("invalid module for `{key}`: {m}"))?; + Ok((subpath.to_string(), module)) + }) + .collect() +} + fn validate_discover(discover: &BTreeMap) -> Check<()> { for (name, pattern) in discover { validate_id("discover name", name)?; @@ -165,6 +202,7 @@ impl PluginManifest { /// "rpp": ">=0.2", /// "entry": "src/plugin.ts", /// "config": "src/config.ts", + /// "exports": { "./raw": "src/raw.ts" }, /// "components": { "compiler": "window.wasm" }, /// "discover": { "windows": "*/window/**/window.ts" } /// } @@ -172,8 +210,8 @@ impl PluginManifest { /// /// `discover` maps names (same grammar) to one glob each, relative to the pack source /// directory. `name` becomes [`PluginManifest::id`] (same grammar). `entry` defaults to - /// `src/plugin.ts` and must be a JavaScript entry; `entry`, `config` and component - /// paths must be relative. `rpp` is a version requirement checked by + /// `src/plugin.ts` and must be a JavaScript entry; `entry`, `config`, `exports` and + /// component paths must be relative, and `exports` keys start with `./`. `rpp` is a version requirement checked by /// [`PluginManifest::load`]. Unknown keys are rejected, except `dependencies`, which is /// ignored. pub fn parse_json(text: &str, path: impl Into) -> Result { @@ -191,6 +229,7 @@ impl PluginManifest { if let Some(config) = &raw.config { validate_relative(config).map_err(|m| format!("invalid `config`: {m}"))?; } + let exports = parse_exports(raw.exports)?; let components = parse_components(raw.components)?; validate_discover(&raw.discover)?; validate_overrides(&raw.overrides)?; @@ -201,6 +240,7 @@ impl PluginManifest { entry, components, config: raw.config, + exports, rpp, discover: raw.discover, overrides: raw.overrides, @@ -266,6 +306,35 @@ mod tests { assert_eq!(m.components["compiler"].module, "window.wasm"); } + #[test] + fn exports_become_plugin_subpaths() { + let m = PluginManifest::parse_json( + r#"{"name":"window","version":"1.0.0","config":"src/config.ts", + "exports":{"./raw":"src/raw.ts","./a/b":"src/b.ts"}}"#, + "rpp.json", + ) + .unwrap(); + assert_eq!( + plugin_modules("window", m.config.as_deref(), &m.exports), + [ + ("plugin:window".to_string(), "src/config.ts"), + ("plugin:window/a/b".to_string(), "src/b.ts"), + ("plugin:window/raw".to_string(), "src/raw.ts"), + ] + ); + for exports in [ + r#"{"raw":"a.ts"}"#, + r#"{"./":"a.ts"}"#, + r#"{".":"a.ts"}"#, + r#"{"./../x":"a.ts"}"#, + r#"{"./raw":"../a.ts"}"#, + ] { + let text = format!(r#"{{"name":"x","version":"1.0.0","exports":{exports}}}"#); + let err = PluginManifest::parse_json(&text, "rpp.json").unwrap_err(); + assert!(matches!(err, Error::Manifest { .. }), "{exports}"); + } + } + #[test] fn parses_json_discover() { let m = PluginManifest::parse_json( diff --git a/crates/rpp/tests/js_config.rs b/crates/rpp/tests/js_config.rs index da0fd21..567e9be 100644 --- a/crates/rpp/tests/js_config.rs +++ b/crates/rpp/tests/js_config.rs @@ -27,7 +27,7 @@ fn demo_package(root: &Path, validate: &str) -> BTreeMap write_file( &dir, "src/config.ts", - &r##"import { definePluginConfig } from "#rpp/config"; + &r##"import { definePluginConfig } from "rpp:config"; export default definePluginConfig<{ level?: number }>("demo", { normalize: (options) => ({ level: 1, ...options }), validate: (options) => VALIDATE, @@ -40,6 +40,7 @@ export default definePluginConfig<{ level?: number }>("demo", { ConfigPackage { dir, config: Some("src/config.ts".into()), + exports: BTreeMap::new(), }, )]) } @@ -50,7 +51,7 @@ fn evaluates_minimal_config() { write_file( dir.path(), "rpp.config.ts", - r##"import { defineConfig } from "#rpp/config"; + r##"import { defineConfig } from "rpp:config"; export default defineConfig({ pack: { name: "mini", format: 34 } }); "##, ); @@ -70,8 +71,8 @@ fn plugin_config_factory_types_options() { write_file( dir.path(), "rpp.config.ts", - r##"import { defineConfig } from "#rpp/config"; -import demo from "#plugins/demo"; + r##"import { defineConfig } from "rpp:config"; +import demo from "plugin:demo"; export default defineConfig({ pack: { name: "p", format: 34 }, plugins: [demo({}), demo({ level: 5 })], @@ -97,7 +98,7 @@ fn factory_validation_errors_name_plugin() { write_file( dir.path(), "rpp.config.ts", - r##"import demo from "#plugins/demo"; + r##"import demo from "plugin:demo"; export default { pack: { name: "p", format: 34 }, plugins: [demo({ level: -1 })] }; "##, ); diff --git a/crates/rpp/tests/js_discover.rs b/crates/rpp/tests/js_discover.rs index 685ec7d..4f5b6d7 100644 --- a/crates/rpp/tests/js_discover.rs +++ b/crates/rpp/tests/js_discover.rs @@ -13,7 +13,7 @@ use rpp::model::{PackFile, PluginFactory, ProcessOutcome}; use tempfile::TempDir; const PLUGIN: &str = r##" -import { definePlugin } from "#rpp"; +import { definePlugin } from "rpp"; export default definePlugin({ generate(ctx) { const found = ctx.discovered("windows").map((d) => ({ @@ -171,11 +171,37 @@ fn definitions_import_plugin_api_via_plugins_specifier() { let project = Project::new(PLUGIN); project.write( "src/shop/window/window.ts", - "import { label } from \"#plugins/shop-ui\";\nexport const title = label(\"Api\");\n", + "import { label } from \"plugin:shop-ui\";\nexport const title = label(\"Api\");\n", ); assert_eq!(project.found().unwrap()[0]["title"], "Api"); } +#[test] +fn definitions_import_plugin_exports_by_subpath() { + let project = Project::new(PLUGIN); + project.write( + "plugin/rpp.json", + r#"{"name":"shop-ui","version":"1.0.0","config":"src/config.ts", + "exports":{"./raw":"src/raw.ts"},"discover":{"windows":"*/window/**/window.ts"}}"#, + ); + project.write( + "plugin/src/raw.ts", + "import { label } from \"./config.ts\";\nexport const raw = (t: string) => label(t) + \"!\";\n", + ); + project.write( + "src/shop/window/window.ts", + "import { raw } from \"plugin:shop-ui/raw\";\nexport const title = raw(\"Raw\");\n", + ); + assert_eq!(project.found().unwrap()[0]["title"], "Raw!"); + + project.write( + "src/shop/window/window.ts", + "import { label } from \"#plugins/shop-ui\";\nexport const title = label(\"Old\");\n", + ); + let err = project.load().err().unwrap().to_string(); + assert!(err.contains("`plugin:`"), "{err}"); +} + #[test] fn tsx_definitions_compile_against_the_sdk_jsx_runtime() { let project = Project::new(PLUGIN); @@ -190,25 +216,25 @@ fn tsx_definitions_compile_against_the_sdk_jsx_runtime() { ); project.write( "plugin/src/shop.tsx", - "import type { Child } from \"#rpp/jsx\";\n\ + "import type { Child } from \"rpp:jsx\";\n\ const Label = (props: { children?: Child }) => [props.children].flat().join(\",\");\n\ export const Shop = (props: { key?: string; children?: Child }): string =>\n\ `${\"key\" in props ? props.key : \"none\"}:${}`;\n", ); project.write( "src/shop/window/window.tsx", - "import { Shop } from \"#plugins/shop-ui\";\n\ + "import { Shop } from \"plugin:shop-ui\";\n\ const parts = <>{\"a\"}{false}{[\"b\", null]};\n\ export const title = {parts};\n", ); project.write( "src/spread/window/window.tsx", - "import { Shop } from \"#plugins/shop-ui\";\n\ + "import { Shop } from \"plugin:shop-ui\";\n\ export const title = x;\n", ); project.write( "src/undefined/window/window.tsx", - "import { Shop } from \"#plugins/shop-ui\";\n\ + "import { Shop } from \"plugin:shop-ui\";\n\ export const title = x;\n", ); let found = project.found().unwrap(); @@ -220,8 +246,9 @@ fn tsx_definitions_compile_against_the_sdk_jsx_runtime() { root: project.dir.path().join("plugin"), plugin: "src/plugin.ts".into(), config: Some("src/config.ts".into()), - self_specifier: Some("#plugins/shop-ui".into()), + self_specifier: Some("plugin:shop-ui".into()), jsx_import_source: Some(rpp::js::JSX_IMPORT_SOURCE.into()), + ..Default::default() }) .unwrap(); assert!(packed.declarations.contains_key("types/src/shop.d.ts")); @@ -257,7 +284,7 @@ fn intrinsic_jsx_tags_are_rejected() { #[test] fn undeclared_discover_name_throws() { let project = Project::new( - "import { definePlugin } from \"#rpp\";\n\ + "import { definePlugin } from \"rpp\";\n\ export default definePlugin({ generate(ctx) { ctx.discovered(\"doors\"); } });\n", ); let error = project.found().unwrap_err().to_string(); @@ -270,7 +297,7 @@ fn undeclared_discover_name_throws() { #[test] fn confined_to_source_dir_while_plugin_keeps_its_own_imports() { let project = Project::new( - "import { definePlugin } from \"#rpp\";\nimport { note } from \"./helper.ts\";\n\ + "import { definePlugin } from \"rpp\";\nimport { note } from \"./helper.ts\";\n\ export default definePlugin({ generate(ctx) { ctx.emit(\"found.json\", JSON.stringify([note])); } });\n", ); project.write("plugin/src/helper.ts", "export const note = \"helped\";\n"); @@ -342,7 +369,7 @@ fn helper_edit_changes_generator_key_only() { #[test] fn processor_cannot_read_discovered() { let project = Project::new( - "import { definePlugin } from \"#rpp\";\n\ + "import { definePlugin } from \"rpp\";\n\ export default definePlugin({ processors: { p: { files: \"**/*\", \ run(ctx: any) { ctx.discovered(\"windows\"); } } } });\n", ); diff --git a/crates/rpp/tests/js_generators.rs b/crates/rpp/tests/js_generators.rs index 1ca97ef..fe7bb20 100644 --- a/crates/rpp/tests/js_generators.rs +++ b/crates/rpp/tests/js_generators.rs @@ -9,7 +9,7 @@ use rpp::model::{BuildStats, PluginFactory}; use rpp::Error; const GENERATOR: &str = r##" -import { definePlugin } from "#rpp"; +import { definePlugin } from "rpp"; let calls = 0; export default definePlugin({ async generate(ctx) { @@ -73,7 +73,7 @@ fn generator_starts_from_fresh_module_state() { fn hooks_run() { let dir = write_plugin( r##" -import { definePlugin } from "#rpp"; +import { definePlugin } from "rpp"; export default definePlugin({ onStart(ctx) { throw new Error(`start ${ctx.plugin}`); }, async onFinish(ctx, stats) { throw new Error(`finish ${stats.processed}/${stats.cached}/${stats.generated}/${stats.dropped}`); }, diff --git a/crates/rpp/tests/js_host.rs b/crates/rpp/tests/js_host.rs index 831c6c1..d67f114 100644 --- a/crates/rpp/tests/js_host.rs +++ b/crates/rpp/tests/js_host.rs @@ -13,7 +13,7 @@ use rpp::Error; fn toml_and_hash_helpers() { let dir = write_plugin( r##" -import { definePlugin, hash, path, toml } from "#rpp"; +import { definePlugin, hash, path, toml } from "rpp"; export default definePlugin({ processors: { info: { @@ -53,7 +53,7 @@ export default definePlugin({ fn process_requires_trusted_permission() { let dir = write_plugin( r##" -import { definePlugin, process } from "#rpp"; +import { definePlugin, process } from "rpp"; export default definePlugin({ generate() { process.run({ program: "true" }); }, }); @@ -74,7 +74,7 @@ export default definePlugin({ fn process_cwd_must_stay_inside_project() { let dir = write_plugin( r##" -import { definePlugin, process } from "#rpp"; +import { definePlugin, process } from "rpp"; export default definePlugin({ generate() { process.run({ program: "true", cwd: "../outside" }); }, }); @@ -95,7 +95,7 @@ export default definePlugin({ fn deterministic_random_per_file() { let dir = write_plugin( r##" -import { definePlugin } from "#rpp"; +import { definePlugin } from "rpp"; export default definePlugin({ processors: { rand: { files: "**/*", run(ctx, file) { file.text = String(Math.random()); } } }, }); @@ -114,7 +114,7 @@ export default definePlugin({ fn module_level_randomness_validates_and_absent_description_is_empty() { let dir = write_plugin( r##" -import { definePlugin } from "#rpp"; +import { definePlugin } from "rpp"; export default definePlugin({ processors: { [String(Math.random())]: { @@ -136,7 +136,7 @@ export default definePlugin({ fn clocks_alone_keep_randomness_fixed() { let dir = write_plugin( r##" -import { definePlugin } from "#rpp"; +import { definePlugin } from "rpp"; export default definePlugin({ processors: { rand: { files: "**/*", run(ctx, file) { file.text = String(Math.random()); } } }, }); diff --git a/crates/rpp/tests/js_load.rs b/crates/rpp/tests/js_load.rs index 2b19f17..d782577 100644 --- a/crates/rpp/tests/js_load.rs +++ b/crates/rpp/tests/js_load.rs @@ -13,7 +13,7 @@ use tempfile::TempDir; fn options_reach_plugins_in_canonical_key_order() { let dir = write_plugin( r##" -import { definePlugin } from "#rpp"; +import { definePlugin } from "rpp"; export default definePlugin({ processors: { dump: { files: "**/*", run(ctx, file) { file.text = JSON.stringify(ctx.options); } }, @@ -77,7 +77,7 @@ fn missing_default_export_fails_to_load() { fn helper_plugin(constant: &str) -> TempDir { let dir = write_plugin( r##" -import { definePlugin } from "#rpp"; +import { definePlugin } from "rpp"; import { value } from "./helper"; export default definePlugin({ processors: { set: { files: "**/*", run(ctx, file) { file.text = value; } } }, diff --git a/crates/rpp/tests/js_processors.rs b/crates/rpp/tests/js_processors.rs index dac7a31..a805017 100644 --- a/crates/rpp/tests/js_processors.rs +++ b/crates/rpp/tests/js_processors.rs @@ -12,7 +12,7 @@ use rpp::Error; fn processor_mutates_text() { let dir = write_plugin( r##" -import { definePlugin } from "#rpp"; +import { definePlugin } from "rpp"; export default definePlugin({ processors: { up: { files: "**/*.txt", run(ctx, file) { file.text = file.text.toUpperCase(); } }, @@ -36,7 +36,7 @@ export default definePlugin({ fn processor_renames_and_drops() { let dir = write_plugin( r##" -import { definePlugin, path } from "#rpp"; +import { definePlugin, path } from "rpp"; export default definePlugin({ processors: { rename: { files: "**/*.txt", run(ctx, file) { file.path = path.withExt(file.path, "md"); } }, @@ -58,7 +58,7 @@ export default definePlugin({ fn processor_receives_options_and_pack() { let dir = write_plugin( r##" -import { definePlugin } from "#rpp"; +import { definePlugin } from "rpp"; export default definePlugin<{ suffix: string }>({ processors: { tag: { @@ -81,7 +81,7 @@ export default definePlugin<{ suffix: string }>({ fn module_state_persists_across_files_on_one_instance() { let dir = write_plugin( r##" -import { definePlugin } from "#rpp"; +import { definePlugin } from "rpp"; let count = 0; export default definePlugin({ processors: { count: { files: "**/*", run(ctx, file) { file.text = String(++count); } } }, @@ -100,7 +100,7 @@ export default definePlugin({ fn errors_show_typescript_locations() { let dir = write_plugin( r##" -import { definePlugin } from "#rpp"; +import { definePlugin } from "rpp"; export default definePlugin({ processors: { boom: { files: "**/*", run() { throw new Error("boom"); } } }, }); diff --git a/docs/CODEGEN.md b/docs/CODEGEN.md index 11bc63e..ee0d9a1 100644 --- a/docs/CODEGEN.md +++ b/docs/CODEGEN.md @@ -13,8 +13,8 @@ same build and invalidation story. Projects declare named project-relative output roots per plugin in `rpp.config.ts`: ```ts -import { defineConfig } from "#rpp/config"; -import window from "#plugins/window"; +import { defineConfig } from "rpp:config"; +import window from "plugin:window"; export default defineConfig({ pack: { name: "my-pack", format: 34 }, diff --git a/docs/MIGRATING.md b/docs/MIGRATING.md index ff02776..8a420be 100644 --- a/docs/MIGRATING.md +++ b/docs/MIGRATING.md @@ -27,7 +27,7 @@ Keys become camelCase and sections become objects: rejected. Plugin permissions need `security: "trusted"`. ```ts -import { defineConfig, plugin } from "#rpp/config"; +import { defineConfig, plugin } from "rpp:config"; export default defineConfig({ pack: { name: "my-pack", format: 34 }, @@ -40,7 +40,7 @@ rpp generates `pack.mcmeta` from `pack`. Move the fields of `src/pack.mcmeta` in `pack.mcmeta` fails the build. `pack.packFormat` is rejected in favor of `pack.format`. A plugin that ships a config module is configured through its factory instead, with typed and -validated options: `import minify from "#plugins/minify"` then `minify({ pretty: false })`. +validated options: `import minify from "plugin:minify"` then `minify({ pretty: false })`. ## Plugin sources to `rpp.json` @@ -72,7 +72,7 @@ factory module) and `discover` (globs of authoring modules, replacing `ctx:sourc ## Lua API to the TypeScript SDK -Import from `#rpp` and default-export `definePlugin({ ... })`. +Import from `rpp` and default-export `definePlugin({ ... })`. | Lua | TypeScript | | ------------------------------------------------- | -------------------------------------------------------------- | diff --git a/docs/SPEC.md b/docs/SPEC.md index f560ecb..fb18dc6 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -58,8 +58,8 @@ to the nearest `rpp.config.ts` (stopping at `rpp.toml`). User-global plugins are supported and `~/.rpp/plugins.toml` is ignored. ```ts -import { defineConfig, plugin } from "#rpp/config"; -import jsonMinify from "#plugins/json-minify"; +import { defineConfig, plugin } from "rpp:config"; +import jsonMinify from "plugin:json-minify"; export default defineConfig({ pack: { @@ -114,17 +114,17 @@ export default defineConfig({ - Keys are camelCase. `plugins` is an array of `{ plugin, options?, security?, permissions?, outputs? }`; `plugin` names an `rpp.json` dependency (`^[a-z0-9][a-z0-9_-]*$`). `plugin(name, options?, access?)` builds an entry, and a plugin's config factory (a `definePluginConfig` - default export, imported as `#plugins/`) validates and normalizes its options first. -- `#rpp/config` provides `defineConfig`, `plugin`, `definePluginConfig` and the config types. + default export, imported as `plugin:`) validates and normalizes its options first. +- `rpp:config` provides `defineConfig`, `plugin`, `definePluginConfig` and the config types. - Every `.tsx` file rpp bundles or packs (config imports, plugins, discovered files) compiles with - the automatic JSX runtime from `#rpp/jsx`; plugins cannot replace it, and JSX pragma comments + the automatic JSX runtime from `rpp:jsx`; plugins cannot replace it, and JSX pragma comments (`@jsx`, `@jsxFrag`, `@jsxRuntime`, `@jsxImportSource`) are rejected. A tag is a component: `x` calls `C({ a: 1, children: "x" })` (several children become an array) and evaluates to its result, even when the result is unused. `key` is an ordinary prop that takes precedence in source order, though its expression is evaluated after the other props; it type-checks only on components that declare it. Fragments evaluate to their children as a flat array without `null`, `undefined` or booleans. There are no intrinsic elements: a lowercase tag is - a type error and throws. `#rpp/jsx` exports `jsx`, `jsxs`, `createElement`, `Fragment`, the + a type error and throws. `rpp:jsx` exports `jsx`, `jsxs`, `createElement`, `Fragment`, the `Child` and `Component` types and the `JSX` namespace, whose `Element` accepts any component result. - Keys inside `options` and `outputs` are kept verbatim; `null` values are invalid. @@ -155,6 +155,7 @@ migration guide, as is an `entry` ending in `.lua`. "rpp": ">=0.2", "entry": "src/plugin.ts", "config": "src/config.ts", + "exports": { "./raw": "src/raw.ts" }, "components": { "compiler": "window.wasm" }, "discover": { "windows": "*/window/**/window.ts" }, "overrides": ["assets/*/textures/**"] @@ -164,7 +165,10 @@ migration guide, as is an `entry` ending in `.lua`. - `name` follows the plugin id grammar (`^[a-z0-9][a-z0-9_-]*$`); `version` is semver; `rpp` is a semver range checked during dependency resolution. - `entry` defaults to `src/plugin.ts` and must be a `.ts`, `.mts`, `.js` or `.mjs` file; - `entry`, `config` (the config-factory module) and component paths are relative. + `entry`, `config` (the config-factory module), `exports` and component paths are relative. +- A plugin's modules are imported as `plugin:` (its `config` module) and + `plugin:/` (each `exports` entry, keyed `./`). Undeclared subpaths do + not resolve. Modules shared between entries are bundled once. - `components` maps component names to WASIp2 binaries callable with `components.load` (§5). - `overrides` lists pack-path globs the plugin's generator may emit over or remove even when another source or plugin owns them. Entries are validated like other pack paths and globs. @@ -173,8 +177,8 @@ migration guide, as is an `entry` ending in `.lua`. no configuration change, and `ctx.discovered(name)` returns `{ path, namespace?, module }` for each, sorted by path (an undeclared name throws `TypeError`). `namespace` is the segment matched by the pattern's first whole `*` segment when all earlier segments are - literal, and must match `^[a-z0-9_.-]+$`. Authoring files import the plugin's `config` - module as `#plugins/`. Discovered files and the source files they import are + literal, and must match `^[a-z0-9_.-]+$`. Authoring files import the plugin's modules as + `plugin:` and `plugin:/`. Discovered files and the source files they import are authoring inputs: they are excluded from processors, `sourceFiles()` and pack output, as is every `.ts`, `.mts`, `.cts` or `.tsx` file under the source directory (TypeScript is never pack content). Discovered files are evaluated when the bundle loads, before any hook, so they have no @@ -254,11 +258,11 @@ Notes: Plugins are TypeScript (or JavaScript) modules bundled by Rolldown and run in a sandboxed V8 isolate (`crates/rpp-js`, wired into `rpp` by the `js` feature). The entry module default-exports -a plugin built with the SDK, imported as `#rpp` (written to `.rpp/sdk/index.ts` by `rpp codegen`, +a plugin built with the SDK, imported as `rpp` (written to `.rpp/sdk/index.ts` by `rpp codegen`, so it always matches the running rpp): ```ts -import { definePlugin, hash, path } from "#rpp"; +import { definePlugin, hash, path } from "rpp"; export default definePlugin<{ pretty?: boolean }>({ // Processors: parallel, per matching file. Pure: (ctx, file) only. @@ -322,8 +326,8 @@ stderr }`. Requires `security: "trusted"` with the program in `permissions.proce ### Module resolution & sandbox -- Imports resolve within the plugin package, to `#rpp`, `#rpp/config`, `#rpp/jsx`, - `#plugins/` for packages with a config module, and to npm dependencies inlined from `node_modules`. `node:` +- Imports resolve within the plugin package, to `rpp`, `rpp:config`, `rpp:jsx`, + `plugin:[/]` for packages' config modules and exports, and to npm dependencies inlined from `node_modules`. `node:` imports are rejected. There is no filesystem, network, clock or randomness access: the isolate gets deterministic host calls only, and trusted clock/random grants disable cache replay for that plugin. @@ -593,7 +597,7 @@ pub fn run_packsquash(binary: &str, pack_dir: &Path, zip_path: &Path, options_fi invalidate accordingly; `rpp.config.ts` and `rpp.json` changes do a full reload. - `rpp clean` — remove output + the project `.rpp` cache; the user-wide `` is kept. - `rpp codegen` — write the TypeScript SDK (`.rpp/sdk/`) and `.rpp/tsconfig.json` (which sets - `jsxImportSource` to `#rpp/jsx`), and a root `tsconfig.json` (including `.tsx` files) if missing, + `jsxImportSource` to `rpp:jsx`), and a root `tsconfig.json` (including `.tsx` files) if missing, in the nearest directory with `rpp.config.ts` or `rpp.json`, and a `.rpp/generated/.d.ts` for each built component a plugin manifest declares. Existing root tsconfigs are preserved; add `**/*.tsx` to their `include` to check `.tsx` files. @@ -609,15 +613,16 @@ pub fn run_packsquash(binary: &str, pack_dir: &Path, zip_path: &Path, options_fi `-.rpp.tgz` plus `.sha256` (` `), written to `--out` (default: the plugin directory). `--json` prints `{name, version, rpp, description, file, sha256}` on stdout. `rpp.json` must set `rpp`. The archive holds the manifest with - `entry: "dist/plugin.js"`, `config: "dist/config.js"` and `dependencies` removed; - `dist/*.js` with `.js.map` files (shared code in `dist/chunk-.js`); every - `components` module; and, for a TypeScript config, `types/**.d.ts` from isolated - declarations plus `dist/config.d.ts`. npm dependencies are inlined from `node_modules` + `entry: "dist/plugin.js"`, `config: "dist/config.js"`, each export at + `dist/exports/.js` and `dependencies` removed; `dist/**.js` with `.js.map` files + (shared code in `dist/chunk-.js`); every `components` module; and, for TypeScript config + and export modules, `types/**.d.ts` from isolated declarations plus a `.d.ts` stub beside each + entry. npm dependencies are inlined from `node_modules` (including hoisted ones outside the plugin directory, resolved through `module` then - `main`); `#rpp` and `#rpp/*` stay imports; `node:` imports are rejected. Source maps + `main`); `rpp` and `rpp:*` stay imports; `node:` imports are rejected. Source maps list the original files (`../src/plugin.ts`, `../node_modules/dep/index.js`), and a loaded file's `//# sourceMappingURL=` map is chained, so installed plugins report stacks - at their original sources. Config modules must support isolated declarations and their + at their original sources. Config and export modules must support isolated declarations and their public types must not reference dependency types. Each archive is checked by unpacking it and bundling its entries as an installing rpp does. diff --git a/docs/WASM_PLUGINS.md b/docs/WASM_PLUGINS.md index 827ae47..72f785f 100644 --- a/docs/WASM_PLUGINS.md +++ b/docs/WASM_PLUGINS.md @@ -14,7 +14,7 @@ A TypeScript plugin may ship named WASIp2 components and call them with ``` ```ts -import { components, definePlugin } from "#rpp"; +import { components, definePlugin } from "rpp"; export default definePlugin({ generate(ctx) { diff --git a/examples/pack/README.md b/examples/pack/README.md index 94c60a5..a0d777c 100644 --- a/examples/pack/README.md +++ b/examples/pack/README.md @@ -83,7 +83,7 @@ Edit `src/items/ember_gem.ts` to change its display name, or add `src/items/frost_gem.ts`: ```ts -import type { Item } from "#plugins/example-catalog"; +import type { Item } from "plugin:example-catalog"; export default { name: "Frost Gem", texture: "minecraft:custom/gem" } satisfies Item; ``` diff --git a/examples/pack/plugins/catalog/src/config.ts b/examples/pack/plugins/catalog/src/config.ts index 6677942..d8d2d9d 100644 --- a/examples/pack/plugins/catalog/src/config.ts +++ b/examples/pack/plugins/catalog/src/config.ts @@ -1,4 +1,4 @@ -import { definePluginConfig } from "#rpp/config"; +import { definePluginConfig } from "rpp:config"; export interface Options { /** Namespace of the generated models and translations. */ diff --git a/examples/pack/plugins/catalog/src/plugin.ts b/examples/pack/plugins/catalog/src/plugin.ts index e4cf3b8..213fbf1 100644 --- a/examples/pack/plugins/catalog/src/plugin.ts +++ b/examples/pack/plugins/catalog/src/plugin.ts @@ -1,4 +1,4 @@ -import { definePlugin } from "#rpp"; +import { definePlugin } from "rpp"; import type { Item, Options } from "./config.ts"; diff --git a/examples/pack/rpp.config.ts b/examples/pack/rpp.config.ts index b6ee7f3..73c8a89 100644 --- a/examples/pack/rpp.config.ts +++ b/examples/pack/rpp.config.ts @@ -1,7 +1,7 @@ -import { defineConfig, plugin } from "#rpp/config"; -import catalog from "#plugins/example-catalog"; -import hashRename from "#plugins/hash-rename"; -import jsonMinify from "#plugins/json-minify"; +import { defineConfig, plugin } from "rpp:config"; +import catalog from "plugin:example-catalog"; +import hashRename from "plugin:hash-rename"; +import jsonMinify from "plugin:json-minify"; export default defineConfig({ pack: { diff --git a/examples/pack/src/items/ember_gem.ts b/examples/pack/src/items/ember_gem.ts index 3c117f5..e59a709 100644 --- a/examples/pack/src/items/ember_gem.ts +++ b/examples/pack/src/items/ember_gem.ts @@ -1,3 +1,3 @@ -import type { Item } from "#plugins/example-catalog"; +import type { Item } from "plugin:example-catalog"; export default { name: "Ember Gem", texture: "minecraft:custom/gem" } satisfies Item; diff --git a/examples/plugins/grayscale-wasm/src/plugin.ts b/examples/plugins/grayscale-wasm/src/plugin.ts index ef9136a..adcdfac 100644 --- a/examples/plugins/grayscale-wasm/src/plugin.ts +++ b/examples/plugins/grayscale-wasm/src/plugin.ts @@ -1,4 +1,4 @@ -import { components, definePlugin } from "#rpp"; +import { components, definePlugin } from "rpp"; // Heavy per-file work (here, PNG decode/encode) lives in the component; TypeScript only // routes bytes. Processor component calls get a fresh guest instance per file, so the guest diff --git a/examples/plugins/hash-rename/README.md b/examples/plugins/hash-rename/README.md index f2bdbc2..93abb0a 100644 --- a/examples/plugins/hash-rename/README.md +++ b/examples/plugins/hash-rename/README.md @@ -30,7 +30,7 @@ renaming a vanilla texture would break the fixed name a model or blockstate points at, but `custom/` assets are referenced only through the rename map. ```ts -import hashRename from "#plugins/hash-rename"; +import hashRename from "plugin:hash-rename"; export default defineConfig({ pack: { name: "my-pack", format: 34 }, diff --git a/examples/plugins/hash-rename/src/config.ts b/examples/plugins/hash-rename/src/config.ts index be91df4..12eafe0 100644 --- a/examples/plugins/hash-rename/src/config.ts +++ b/examples/plugins/hash-rename/src/config.ts @@ -1,4 +1,4 @@ -import { definePluginConfig } from "#rpp/config"; +import { definePluginConfig } from "rpp:config"; export interface Options { /** diff --git a/examples/plugins/hash-rename/src/plugin.ts b/examples/plugins/hash-rename/src/plugin.ts index 3b8e7c3..8358bf2 100644 --- a/examples/plugins/hash-rename/src/plugin.ts +++ b/examples/plugins/hash-rename/src/plugin.ts @@ -1,4 +1,4 @@ -import { definePlugin, hash, path } from "#rpp"; +import { definePlugin, hash, path } from "rpp"; import type { Options } from "./config.ts"; diff --git a/examples/plugins/json-minify/README.md b/examples/plugins/json-minify/README.md index 70c448d..8e8421c 100644 --- a/examples/plugins/json-minify/README.md +++ b/examples/plugins/json-minify/README.md @@ -30,7 +30,7 @@ pack is as small as possible. `rpp.config.ts`: ```ts -import jsonMinify from "#plugins/json-minify"; +import jsonMinify from "plugin:json-minify"; export default defineConfig({ pack: { name: "my-pack", format: 34 }, diff --git a/examples/plugins/json-minify/src/config.ts b/examples/plugins/json-minify/src/config.ts index 1958088..a9481a8 100644 --- a/examples/plugins/json-minify/src/config.ts +++ b/examples/plugins/json-minify/src/config.ts @@ -1,4 +1,4 @@ -import { definePluginConfig } from "#rpp/config"; +import { definePluginConfig } from "rpp:config"; export interface Options { /** Re-indent instead of minifying; handy for diffing a built pack. */ diff --git a/examples/plugins/json-minify/src/plugin.ts b/examples/plugins/json-minify/src/plugin.ts index 3712d57..79d3bc2 100644 --- a/examples/plugins/json-minify/src/plugin.ts +++ b/examples/plugins/json-minify/src/plugin.ts @@ -1,4 +1,4 @@ -import { definePlugin } from "#rpp"; +import { definePlugin } from "rpp"; import type { Options } from "./config.ts"; diff --git a/examples/plugins/mcmeta-validate/README.md b/examples/plugins/mcmeta-validate/README.md index 3fa35f1..0efcb67 100644 --- a/examples/plugins/mcmeta-validate/README.md +++ b/examples/plugins/mcmeta-validate/README.md @@ -36,7 +36,7 @@ This plugin emits no output files; it is a pure correctness gate. `rpp.config.ts`: ```ts -import { defineConfig, plugin } from "#rpp/config"; +import { defineConfig, plugin } from "rpp:config"; export default defineConfig({ pack: { name: "my-pack", format: 34 }, diff --git a/examples/plugins/mcmeta-validate/src/plugin.ts b/examples/plugins/mcmeta-validate/src/plugin.ts index 8c04181..600bd59 100644 --- a/examples/plugins/mcmeta-validate/src/plugin.ts +++ b/examples/plugins/mcmeta-validate/src/plugin.ts @@ -1,4 +1,4 @@ -import { definePlugin } from "#rpp"; +import { definePlugin } from "rpp"; import { validateAnimation } from "./rules.ts"; diff --git a/integrations/jvm/examples/pack/rpp.config.ts b/integrations/jvm/examples/pack/rpp.config.ts index 0aed6f6..020e860 100644 --- a/integrations/jvm/examples/pack/rpp.config.ts +++ b/integrations/jvm/examples/pack/rpp.config.ts @@ -1,3 +1,3 @@ -import { defineConfig } from "#rpp/config"; +import { defineConfig } from "rpp:config"; export default defineConfig({ pack: { name: "jvm-example", format: 88 } }); diff --git a/scripts/fixtures/check-plugin/src/plugin.ts b/scripts/fixtures/check-plugin/src/plugin.ts index f8f55f4..b1d131e 100644 --- a/scripts/fixtures/check-plugin/src/plugin.ts +++ b/scripts/fixtures/check-plugin/src/plugin.ts @@ -1,4 +1,4 @@ -import { definePlugin, type Plugin } from "#rpp"; +import { definePlugin, type Plugin } from "rpp"; const plugin: Plugin = definePlugin({ processors: { diff --git a/scripts/fixtures/grayscale/rpp.config.ts b/scripts/fixtures/grayscale/rpp.config.ts index 08aaecd..c5a6b89 100644 --- a/scripts/fixtures/grayscale/rpp.config.ts +++ b/scripts/fixtures/grayscale/rpp.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, plugin } from "#rpp/config"; +import { defineConfig, plugin } from "rpp:config"; export default defineConfig({ pack: { name: "grayscale-fixture", description: "Release consumer", format: 34 }, From ac3361e86b1e96eab7f5472ee3b84a92a254a487 Mon Sep 17 00:00:00 2001 From: oglassdev <23040613+oglassdev@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:51:21 -0400 Subject: [PATCH 2/5] fix: address review of plugin subpath exports - Resolve chunk source map sources against the map's directory, so nested export maps point at the original files. - Bundle type-only entries, which produce no source map. - Resolve a plugin's own plugin:[/] modules when loading it without discovery, and map them in plugin-project codegen. --- crates/rpp-cli/src/codegen/mod.rs | 9 +++++++-- crates/rpp-js/src/bundle/mod.rs | 18 +++++++++++------ crates/rpp-js/src/bundle/paths.rs | 14 ++++++++----- crates/rpp-js/tests/bundle.rs | 11 +++++++++++ crates/rpp-js/tests/pack.rs | 31 +++++++++++++++++++++++++++++ crates/rpp/src/js/bundle.rs | 33 ++++++++++++++++++++----------- crates/rpp/tests/js_load.rs | 26 ++++++++++++++++++++++++ 7 files changed, 117 insertions(+), 25 deletions(-) diff --git a/crates/rpp-cli/src/codegen/mod.rs b/crates/rpp-cli/src/codegen/mod.rs index 33eba19..d7f5a60 100644 --- a/crates/rpp-cli/src/codegen/mod.rs +++ b/crates/rpp-cli/src/codegen/mod.rs @@ -4,7 +4,6 @@ mod component_dts; mod tsconfig; -use std::collections::BTreeMap; use std::path::Path; use anyhow::{Context, Result}; @@ -33,7 +32,13 @@ pub fn write(root: &Path) -> Result { .collect(), ) } else if is_plugin_manifest(&root.join(PACKAGE_MANIFEST)) { - Some(BTreeMap::new()) + let manifest = PluginManifest::load(root)?; + Some( + plugin_modules(&manifest.id, manifest.config.as_deref(), &manifest.exports) + .into_iter() + .map(|(specifier, module)| (specifier, root.join(module))) + .collect(), + ) } else { None }; diff --git a/crates/rpp-js/src/bundle/mod.rs b/crates/rpp-js/src/bundle/mod.rs index 0e80abc..0c0bbaf 100644 --- a/crates/rpp-js/src/bundle/mod.rs +++ b/crates/rpp-js/src/bundle/mod.rs @@ -40,7 +40,8 @@ pub struct BundleRequest { /// Directories outside `root` that may also be bundled, keyed by an exact import /// specifier (e.g. `plugin:window`) that resolves to that package's entry file. /// Files inside a package directory may import each other relatively; source maps - /// show them as `/`. + /// show them as `/`, unless the + /// directory is `root`. pub packages: BTreeMap, /// Compile JSX with the automatic runtime imported from `/jsx-runtime`. JSX /// pragma comments, which would replace it, are rejected. @@ -96,9 +97,10 @@ pub fn bundle(request: &BundleRequest) -> Result { }; Ok(Bundle { code: chunk.code, - source_map: chunk - .source_map - .ok_or_else(|| Error::Bundle("bundling produced no source map".to_string()))?, + // A module with nothing to run (e.g. only types) produces no map. + source_map: chunk.source_map.unwrap_or_else(|| { + r#"{"version":3,"sources":[],"names":[],"mappings":""}"#.to_string() + }), inputs: built.inputs, input_hashes: built.input_hashes, }) @@ -271,13 +273,17 @@ fn source_path_transform( ) -> SourceMapPathTransform { let root = root.to_path_buf(); let packages = Arc::clone(packages); - SourceMapPathTransform::new(Arc::new(move |sources, _| { + SourceMapPathTransform::new(Arc::new(move |sources, map_path| { let root = root.clone(); let packages = Arc::clone(&packages); + // Rolldown passes sources relative to the map's directory. + let map_dir = Path::new(map_path) + .parent() + .map_or_else(|| root.clone(), |dir| root.join(dir)); Box::pin(async move { Ok(sources .iter() - .map(|s| display_source(&root, &packages, allow_node_modules, s)) + .map(|s| display_source(&root, &map_dir, &packages, allow_node_modules, s)) .collect()) }) })) diff --git a/crates/rpp-js/src/bundle/paths.rs b/crates/rpp-js/src/bundle/paths.rs index 33303bf..491157e 100644 --- a/crates/rpp-js/src/bundle/paths.rs +++ b/crates/rpp-js/src/bundle/paths.rs @@ -4,10 +4,11 @@ use rolldown_sourcemap::{JSONSourceMap, SourceMap}; use super::{Package, NODE_MODULES, VIRTUAL_PREFIX}; -/// Turns a source path relative to `root` (as Rolldown reports it) into the form -/// listed in the source map. +/// Turns a source path relative to `map_dir` (as Rolldown reports it) into the form listed +/// in the source map: relative to `root`, under `node_modules/`, or under a package specifier. pub(super) fn display_source( root: &Path, + map_dir: &Path, packages: &[Package], allow_node_modules: bool, source: &str, @@ -15,7 +16,7 @@ pub(super) fn display_source( if let Some(at) = source.find(VIRTUAL_PREFIX) { return source[at + VIRTUAL_PREFIX.len()..].to_string(); } - let absolute = normalize(&root.join(source)); + let absolute = normalize(&map_dir.join(source)); if allow_node_modules { let components: Vec<_> = absolute.components().collect(); if let Some(at) = components @@ -26,13 +27,16 @@ pub(super) fn display_source( return tail.to_string_lossy().replace('\\', "/"); } } - for package in packages { + for package in packages.iter().filter(|package| package.dir != root) { if let Ok(relative) = absolute.strip_prefix(&package.dir) { let relative = relative.to_string_lossy().replace('\\', "/"); return format!("{}/{relative}", package.specifier); } } - source.replace('\\', "/") + match absolute.strip_prefix(root) { + Ok(relative) => relative.to_string_lossy().replace('\\', "/"), + Err(_) => source.replace('\\', "/"), + } } /// Resolves `.` and `..` components without touching the filesystem. diff --git a/crates/rpp-js/tests/bundle.rs b/crates/rpp-js/tests/bundle.rs index 57d12e3..b5cd59e 100644 --- a/crates/rpp-js/tests/bundle.rs +++ b/crates/rpp-js/tests/bundle.rs @@ -373,3 +373,14 @@ fn bundle_ignores_project_tsconfig() { ); } } + +#[test] +fn type_only_entry_bundles() { + let dir = TempDir::new().unwrap(); + write( + dir.path(), + "types.ts", + "export interface Options { size: number }\n", + ); + bundle(&request(dir.path(), "types.ts")).unwrap(); +} diff --git a/crates/rpp-js/tests/pack.rs b/crates/rpp-js/tests/pack.rs index d3e5b4e..53d6b1b 100644 --- a/crates/rpp-js/tests/pack.rs +++ b/crates/rpp-js/tests/pack.rs @@ -379,3 +379,34 @@ fn pack_bundles_exports_beside_the_config() { ); assert!(output.declarations.contains_key("types/src/raw.d.ts")); } + +#[test] +fn nested_export_maps_point_at_original_sources() { + let dir = TempDir::new().unwrap(); + let root = dir.path(); + write( + root, + "src/plugin.ts", + "export const run = (): number => 1;\n", + ); + write( + root, + "src/raw.ts", + "export const raw = (): string => 'raw-marker';\n", + ); + let mut request = request(root, "src/plugin.ts", None); + request + .exports + .insert("api/raw".to_string(), "src/raw.ts".to_string()); + + let output = pack(&request).unwrap(); + + let code = &output.files["dist/exports/api/raw.js"]; + assert!( + code.ends_with("//# sourceMappingURL=raw.js.map\n"), + "{code}" + ); + let map: serde_json::Value = + serde_json::from_str(&output.files["dist/exports/api/raw.js.map"]).unwrap(); + assert_eq!(map["sources"], serde_json::json!(["../../../src/raw.ts"])); +} diff --git a/crates/rpp/src/js/bundle.rs b/crates/rpp/src/js/bundle.rs index f97396b..278d18e 100644 --- a/crates/rpp/src/js/bundle.rs +++ b/crates/rpp/src/js/bundle.rs @@ -42,8 +42,8 @@ pub(super) fn bundle_plugin( root: root.to_path_buf(), entry: "rpp:internal/entry".into(), virtual_modules: virtual_modules(&format!("./{}", manifest.entry), false), + packages: self_packages(manifest, root), jsx_import_source: Some(JSX_IMPORT_SOURCE.to_string()), - ..Default::default() }; let bundle = cached_bundle(cache_dir, &manifest.id, &request, || { rpp_js::bundle(&request).map_err(|e| hint_renamed_specifiers(e.to_string())) @@ -100,6 +100,18 @@ fn virtual_modules(entry: &str, discovered: bool) -> BTreeMap { .collect() } +/// The plugin's own [`plugin_modules`], as packages rooted at `plugin_root`. +fn self_packages(manifest: &PluginManifest, plugin_root: &Path) -> BTreeMap { + plugin_modules(&manifest.id, manifest.config.as_deref(), &manifest.exports) + .into_iter() + .map(|(specifier, module)| { + let dir = plugin_root.to_path_buf(); + let entry = module.to_string(); + (specifier, BundlePackage { dir, entry }) + }) + .collect() +} + /// Bundle the plugin together with the files its patterns match under `source`. The bundle /// root is `source`; the plugin's own files are the package `rpp:internal/plugin`. fn bundle_discovered( @@ -117,17 +129,14 @@ fn bundle_discovered( "rpp:internal/discovered".into(), discovered_module("", discovery, &entries), ); - let package = |entry: &str| BundlePackage { - dir: plugin_root.to_path_buf(), - entry: entry.to_string(), - }; - let mut packages = - BTreeMap::from([("rpp:internal/plugin".to_string(), package(&manifest.entry))]); - for (specifier, module) in - plugin_modules(&manifest.id, manifest.config.as_deref(), &manifest.exports) - { - packages.insert(specifier, package(module)); - } + let mut packages = self_packages(manifest, plugin_root); + packages.insert( + "rpp:internal/plugin".to_string(), + BundlePackage { + dir: plugin_root.to_path_buf(), + entry: manifest.entry.clone(), + }, + ); let request = BundleRequest { root: source.to_path_buf(), entry: "rpp:internal/entry".into(), diff --git a/crates/rpp/tests/js_load.rs b/crates/rpp/tests/js_load.rs index d782577..43a5331 100644 --- a/crates/rpp/tests/js_load.rs +++ b/crates/rpp/tests/js_load.rs @@ -112,3 +112,29 @@ fn options_change_cache_key() { load(dir.path(), r#"{"a":2}"#).cache_key() ); } + +#[test] +fn plugin_imports_its_own_exports_by_specifier() { + let dir = write_plugin( + r##" +import { definePlugin } from "rpp"; +import { mark } from "plugin:ts-test/raw"; +export default definePlugin({ + processors: { mark: { files: "**/*", run(ctx, file) { file.text = mark(file.text); } } }, +}); +"##, + ); + write_file( + dir.path(), + "rpp.json", + r#"{ "name": "ts-test", "version": "1.0.0", "exports": { "./raw": "src/raw.ts" } }"#, + ); + write_file( + dir.path(), + "src/raw.ts", + "export const mark = (t: string) => t + \"!\";\n", + ); + let mut instance = load(dir.path(), "{}").instantiate().unwrap(); + let (file, _) = process(instance.as_mut(), "mark", "a.txt", "hi"); + assert_eq!(text(&file), "hi!"); +} From b4cfdf96bdd28a35e5fefa9a17d3fe0d0a6d81ab Mon Sep 17 00:00:00 2001 From: oglassdev <23040613+oglassdev@users.noreply.github.com> Date: Wed, 7 Oct 2026 23:53:52 -0400 Subject: [PATCH 3/5] fix: provide rpp:config to every plugin bundle A plugin without discovery that imports its own config module needs the config SDK the module imports. --- crates/rpp-cli/src/commands/plugin/pack.rs | 6 ++--- crates/rpp/src/js/bundle.rs | 2 +- crates/rpp/src/js/mod.rs | 6 +++-- crates/rpp/tests/js_load.rs | 28 ++++++++++++++++++++++ 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/crates/rpp-cli/src/commands/plugin/pack.rs b/crates/rpp-cli/src/commands/plugin/pack.rs index b1c175e..88dc619 100644 --- a/crates/rpp-cli/src/commands/plugin/pack.rs +++ b/crates/rpp-cli/src/commands/plugin/pack.rs @@ -133,15 +133,15 @@ fn write_outputs(out: &Path, file: &str, archive: &[u8]) -> Result<(PathBuf, Str } /// Unpacks the archive as installs do and bundles each entry with only the SDK modules -/// its loader provides: `rpp` for the plugin, `rpp:config` for the config, both for exports -/// (pack sources and `rpp.config.ts` import them), and `rpp:jsx`. +/// its loader provides: `rpp:config` for the config, and `rpp` too for the plugin and +/// exports (pack sources and `rpp.config.ts` import them), plus `rpp:jsx`. fn self_check(archive: &[u8]) -> Result<()> { let dir = tempfile::tempdir()?; rpp_archive::unpack(archive, dir.path()).context("unpacking the archive")?; let manifest = PluginManifest::load(dir.path()).context("checking the packed manifest")?; let sdk = ("rpp", SDK_INDEX); let config_sdk = ("rpp:config", SDK_CONFIG); - let mut entries = vec![(manifest.entry, vec![sdk])]; + let mut entries = vec![(manifest.entry, vec![sdk, config_sdk])]; if let Some(config) = manifest.config { entries.push((config, vec![config_sdk])); } diff --git a/crates/rpp/src/js/bundle.rs b/crates/rpp/src/js/bundle.rs index 278d18e..fb7e145 100644 --- a/crates/rpp/src/js/bundle.rs +++ b/crates/rpp/src/js/bundle.rs @@ -89,6 +89,7 @@ fn virtual_modules(entry: &str, discovered: bool) -> BTreeMap { ); [ ("rpp".to_string(), SDK_INDEX.to_string()), + ("rpp:config".to_string(), SDK_CONFIG.to_string()), ( "rpp:internal/runtime".to_string(), RUNTIME_SOURCE.to_string(), @@ -124,7 +125,6 @@ fn bundle_discovered( let entries = discovery.discover(source)?; let mut virtual_modules = virtual_modules("rpp:internal/plugin", true); - virtual_modules.insert("rpp:config".into(), SDK_CONFIG.into()); virtual_modules.insert( "rpp:internal/discovered".into(), discovered_module("", discovery, &entries), diff --git a/crates/rpp/src/js/mod.rs b/crates/rpp/src/js/mod.rs index 786af03..df76776 100644 --- a/crates/rpp/src/js/mod.rs +++ b/crates/rpp/src/js/mod.rs @@ -13,8 +13,10 @@ //! Every bundle also gets `rpp:jsx` and `rpp:jsx/jsx-runtime` (`sdk/jsx.ts`), the import //! source that `.tsx` files compile against. //! -//! A manifest with `discover` patterns also gets `rpp:config`, `plugin:` and -//! `plugin:/` packages for its config module and exports, `rpp:internal/plugin` +//! Every bundle also gets `rpp:config` and the plugin's own `plugin:` and +//! `plugin:/` packages for its config module and exports. +//! +//! A manifest with `discover` patterns also gets `rpp:internal/plugin` //! (the entry) and `rpp:internal/discovered` (the matched files' namespace objects); the //! bundle root is the source dir. `rpp:internal/entry` then calls //! `register(plugin, discovered)`. Processors cannot read `ctx.discovered()`. diff --git a/crates/rpp/tests/js_load.rs b/crates/rpp/tests/js_load.rs index 43a5331..b75d5ae 100644 --- a/crates/rpp/tests/js_load.rs +++ b/crates/rpp/tests/js_load.rs @@ -138,3 +138,31 @@ export default definePlugin({ let (file, _) = process(instance.as_mut(), "mark", "a.txt", "hi"); assert_eq!(text(&file), "hi!"); } + +#[test] +fn plugin_imports_its_own_config_factory() { + let dir = write_plugin( + r##" +import { definePlugin } from "rpp"; +import config from "plugin:ts-test"; +export default definePlugin({ + processors: { + id: { files: "**/*", run(ctx, file) { file.text = config({}).plugin; } }, + }, +}); +"##, + ); + write_file( + dir.path(), + "rpp.json", + r#"{ "name": "ts-test", "version": "1.0.0", "config": "src/config.ts" }"#, + ); + write_file( + dir.path(), + "src/config.ts", + "import { definePluginConfig } from \"rpp:config\";\nexport default definePluginConfig<{}>(\"ts-test\");\n", + ); + let mut instance = load(dir.path(), "{}").instantiate().unwrap(); + let (file, _) = process(instance.as_mut(), "id", "a.txt", ""); + assert_eq!(text(&file), "ts-test"); +} From 7dd2bc1f58d1ed0cc81962cb6bd366bf0781b569 Mon Sep 17 00:00:00 2001 From: oglassdev <23040613+oglassdev@users.noreply.github.com> Date: Thu, 8 Oct 2026 09:30:57 -0400 Subject: [PATCH 4/5] docs: update AGENTS.md for rpp: and plugin: specifiers --- AGENTS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index d041127..9a5fb73 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,7 +37,8 @@ Rust tooling for building Minecraft resource packs with incremental TypeScript a - Use `pub(crate)` for internals, `///` for public APIs, and `dep:` for optional dependencies. - Keep shared dependency versions in `[workspace.dependencies]`. - Write focused behavior tests. Keep comments sparse and about current behavior. -- TypeScript plugins run on V8 and import the SDK as `#rpp`; config uses `#rpp/config`. +- TypeScript plugins run on V8 and import the SDK as `rpp`; config uses `rpp:config`. + Plugin modules are imported as `plugin:` and `plugin:/`. ## Git From f673ff664ab595e58cd547f9f1d097efba8adf35 Mon Sep 17 00:00:00 2001 From: oglassdev <23040613+oglassdev@users.noreply.github.com> Date: Thu, 8 Oct 2026 09:33:38 -0400 Subject: [PATCH 5/5] docs: scope plugin exports to pack sources in self-check comment --- crates/rpp-cli/src/commands/plugin/pack.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpp-cli/src/commands/plugin/pack.rs b/crates/rpp-cli/src/commands/plugin/pack.rs index 88dc619..834c049 100644 --- a/crates/rpp-cli/src/commands/plugin/pack.rs +++ b/crates/rpp-cli/src/commands/plugin/pack.rs @@ -134,7 +134,7 @@ fn write_outputs(out: &Path, file: &str, archive: &[u8]) -> Result<(PathBuf, Str /// Unpacks the archive as installs do and bundles each entry with only the SDK modules /// its loader provides: `rpp:config` for the config, and `rpp` too for the plugin and -/// exports (pack sources and `rpp.config.ts` import them), plus `rpp:jsx`. +/// exports (pack sources import them), plus `rpp:jsx`. fn self_check(archive: &[u8]) -> Result<()> { let dir = tempfile::tempdir()?; rpp_archive::unpack(archive, dir.path()).context("unpacking the archive")?;