diff --git a/.changeset/compiler-secondary-passes-any-extension.md b/.changeset/compiler-secondary-passes-any-extension.md new file mode 100644 index 0000000000..f4a0b5b35e --- /dev/null +++ b/.changeset/compiler-secondary-passes-any-extension.md @@ -0,0 +1,5 @@ +--- +"@solidjs/compiler": patch +--- + +The secondary passes (`transformDirectives`, the lazy and refresh passes, source names) accept a filename with any extension. They run on code that is already lowered to JavaScript, so the filename is metadata rather than a parser choice: a known extension still picks its dialect, and any other (`App.rx`, from another front end) is parsed as TSX instead of failing with "Unknown file extension". This matters for server functions, whose IDs hash the filename: a front end can now pass the module's real path, so the ID matches the one `moduleForFunctionId` computes for that module. The JSX transform itself is unchanged. diff --git a/packages/compiler/__tests__/secondary-passes-frontend.test.js b/packages/compiler/__tests__/secondary-passes-frontend.test.js new file mode 100644 index 0000000000..790f429872 --- /dev/null +++ b/packages/compiler/__tests__/secondary-passes-frontend.test.js @@ -0,0 +1,71 @@ +const path = require("path"); + +const compilerDir = path.resolve(__dirname, ".."); +const { transformDirectives, transformLazy, transformRefresh } = require(compilerDir); + +// Another front end (ReactiveScript's `.rx`, through a bundler's frontends hook) +// lowers its own modules to Solid's output, then hands that ordinary +// JavaScript to the secondary passes under the module's own filename — the +// path server-function ids hash, as the bundler's manifest keys it. +const ROOT = "/project"; +const FILENAME = `${ROOT}/src/routes/App.rx`; +const generated = ` + export async function save(value) { + "use server"; + return value; + } +`; + +describe("secondary passes for another front end's modules", () => { + it.each(["server", "client"])( + "transforms the generated JavaScript under the original path in %s mode", + mode => { + const result = transformDirectives(generated, { + filename: FILENAME, + root: ROOT, + mode, + env: "production" + }); + expect(result.valid).toBe(true); + expect(result.functions).toHaveLength(1); + expect(result.functions[0].name).toBe("save"); + } + ); + + it("hashes the module's own path, not a borrowed extension", () => { + const own = transformDirectives(generated, { + filename: FILENAME, + root: ROOT, + mode: "server", + env: "production" + }); + const client = transformDirectives(generated, { + filename: FILENAME, + root: ROOT, + mode: "client", + env: "production" + }); + const borrowed = transformDirectives(generated, { + filename: `${FILENAME}.tsx`, + root: ROOT, + mode: "server", + env: "production" + }); + expect(client.functions[0].id).toBe(own.functions[0].id); + expect(borrowed.functions[0].id).not.toBe(own.functions[0].id); + }); + + it("runs the lazy and refresh passes under that filename", () => { + const lazy = transformLazy( + `import { lazy } from "solid-js";\nexport const Page = lazy(() => import("./Page.rx"));\n`, + { filename: FILENAME } + ); + expect(lazy.code).toContain("lazy("); + const refresh = transformRefresh(`export function App() { return null; }\n`, { + filename: FILENAME, + bundler: "vite", + jsx: false + }); + expect(typeof refresh.code).toBe("string"); + }); +}); diff --git a/packages/compiler/src/config.rs b/packages/compiler/src/config.rs index d10f05baa1..ff090b7c23 100644 --- a/packages/compiler/src/config.rs +++ b/packages/compiler/src/config.rs @@ -114,6 +114,19 @@ pub struct TransformResult { pub css_hash: Option, } +/// The dialect a secondary pass (`transformDirectives`, `transformLazy`, +/// `transformRefresh`, `transformSourceNames`) parses its input in. Those +/// passes run on what a JSX transform — this crate's, or another front end's — +/// already produced, and keep the authored filename only for path-derived +/// metadata (server-function ids hash it). So a filename Oxc knows picks its +/// dialect, and any other (`.tsrx`, another front end's `.rx`) is ordinary +/// code, parsed as TSX. +pub(crate) fn secondary_source_type(filename: Option<&str>) -> SourceType { + filename + .and_then(|filename| SourceType::from_path(filename).ok()) + .unwrap_or_else(SourceType::tsx) +} + pub(crate) fn source_type_for_filename(filename: Option<&str>) -> Result { if filename.is_some_and(|filename| filename.ends_with(".tsrx")) { // Secondary passes receive already-projected ordinary code but retain diff --git a/packages/compiler/src/directives/mod.rs b/packages/compiler/src/directives/mod.rs index 01c5e883cc..13ffae009f 100644 --- a/packages/compiler/src/directives/mod.rs +++ b/packages/compiler/src/directives/mod.rs @@ -22,7 +22,7 @@ use oxc_allocator::Allocator; use oxc_codegen::{Codegen, CodegenOptions}; use oxc_parser::{ParseOptions, Parser}; -use crate::config::source_type_for_filename; +use crate::config::secondary_source_type; use transform::{DirectivesTransform, Env, ImportDef, ImportKind, Mode, UnsupportedExport}; const DEFAULT_RUNTIME: &str = "@solidjs/web/server-functions"; @@ -118,7 +118,7 @@ pub fn transform_directives( .clone() .unwrap_or_else(|| "use server".to_string()); - let source_type = source_type_for_filename(Some(filename))?; + let source_type = secondary_source_type(Some(filename)); let allocator = Allocator::default(); let parsed = Parser::new(&allocator, &code, source_type) .with_options(ParseOptions { diff --git a/packages/compiler/src/lazy.rs b/packages/compiler/src/lazy.rs index 0b3c8e62ea..63c9e8334b 100644 --- a/packages/compiler/src/lazy.rs +++ b/packages/compiler/src/lazy.rs @@ -32,7 +32,7 @@ use oxc_parser::{ParseOptions, Parser}; use oxc_semantic::SemanticBuilder; use oxc_span::{GetSpan, Span}; -use crate::config::{TransformResult, source_type_for_filename}; +use crate::config::{TransformResult, secondary_source_type}; pub const LAZY_PLACEHOLDER_PREFIX: &str = "__SOLID_LAZY_MODULE__:"; @@ -60,7 +60,7 @@ pub fn transform_lazy( }); }; - let source_type = source_type_for_filename(Some(filename))?; + let source_type = secondary_source_type(Some(filename)); let allocator = Allocator::default(); let parsed = Parser::new(&allocator, &code, source_type) .with_options(ParseOptions { diff --git a/packages/compiler/src/refresh/mod.rs b/packages/compiler/src/refresh/mod.rs index fcdc2fe4c4..5a1d8eda9b 100644 --- a/packages/compiler/src/refresh/mod.rs +++ b/packages/compiler/src/refresh/mod.rs @@ -25,7 +25,7 @@ use oxc_allocator::Allocator; use oxc_codegen::{Codegen, CodegenOptions}; use oxc_parser::{ParseOptions, Parser}; -use crate::config::{TransformResult, source_type_for_filename}; +use crate::config::{TransformResult, secondary_source_type}; use transform::{Bundler, RefreshConfig, RefreshTransform}; pub const DEFAULT_IMPORT_SOURCE: &str = "solid-refresh"; @@ -90,7 +90,7 @@ pub fn transform_refresh( .unwrap_or_else(|| DEFAULT_IMPORT_SOURCE.to_string()), }; - let source_type = source_type_for_filename(options.filename.as_deref())?; + let source_type = secondary_source_type(options.filename.as_deref()); let allocator = Allocator::default(); let parsed = Parser::new(&allocator, &code, source_type) .with_options(ParseOptions { diff --git a/packages/compiler/src/source_names.rs b/packages/compiler/src/source_names.rs index 8376a93aeb..71895204be 100644 --- a/packages/compiler/src/source_names.rs +++ b/packages/compiler/src/source_names.rs @@ -51,7 +51,7 @@ use oxc_semantic::{Scoping, SemanticBuilder, SymbolId}; use oxc_span::{GetSpan, Span}; use oxc_syntax::scope::ScopeFlags; -use crate::config::{TransformResult, source_type_for_filename}; +use crate::config::{TransformResult, secondary_source_type}; use crate::shared::ast::object_property; use crate::shared::ast_builder::AstBuilder; @@ -93,7 +93,7 @@ pub fn transform_source_names( return Ok(untouched(code)); } - let source_type = source_type_for_filename(filename)?; + let source_type = secondary_source_type(filename); let allocator = Allocator::default(); let parsed = Parser::new(&allocator, &code, source_type) .with_options(ParseOptions {