diff --git a/.gitignore b/.gitignore index 0c63d28..c1d318e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ packages/*/index.d.ts !packages/solid-layouts-oxc/index.d.ts !packages/rsbuild-plugin-solid-layouts/index.js !packages/rsbuild-plugin-solid-layouts/index.d.ts +packages/solid-layouts/solid-2-staging/ diff --git a/SOLID-2-PLAN.md b/SOLID-2-PLAN.md new file mode 100644 index 0000000..46f5b16 --- /dev/null +++ b/SOLID-2-PLAN.md @@ -0,0 +1,169 @@ +# Solid 2 support for the layouts toolchain + +Scoped 2026-08-15 on `next/solid-2`, branched from `master` at `a107bf1`, which +is exactly the published state: solid-layouts 0.1.3, solid-layouts-oxc 0.1.7, +rsbuild-plugin-solid-layouts 0.1.4. Tagged `solid-1-baseline`. + +**Status: built, tested, unpublished.** The toolchain now serves both majors. +Steps 1 to 5 below are done; step 6, publishing, is not. + +## It is one file, and inside that file one import + +I had this down as "three packages need Solid 2 support", which was true but +useless. Measured, the whole dependence was: + +| where | Solid sites | note | +| --- | ---: | --- | +| `packages/solid-layouts/src/component.ts` | **32** | 437 lines. This was the job. | +| `packages/solid-layouts/src/component.test.ts` | 34 | follows the source | +| `packages/solid-layouts/src/ids.test.ts` | 3 | `createRoot` only | +| `packages/solid-layouts-oxc` (3,850 lines of Rust) | **2 real** | an emitted import header and a builtins list | +| `packages/rsbuild-plugin-solid-layouts` | **0** | a thin wrapper, no Solid usage | + +Then it got smaller again. Of the four things `component.ts` used that 2.0 +changed, three can be told apart at runtime from the module object itself, so +they needed no fork at all: + +| what | how it is handled | +| --- | --- | +| `splitProps` became `omit` | resolved once at load: `"omit" in solid` | +| `Context.Provider` became the context | `context.Provider ?? context` | +| `useContext` throws instead of returning `undefined` | the internal defaults context now carries `{}`, which both majors read the same way | +| **`Dynamic` and `createComponent` moved module** | **the only real fork** | + +The fourth cannot be detected, because it is a module specifier and those are +resolved before any code runs. 1.9 serves them from `solid-js/web`; 2.0 moved +them to `@solidjs/web` **and dropped the `solid-js/web` subpath entirely**, so +no single import statement resolves under both. That is now `src/renderer.ts`, +twenty lines, with a twin in `src/renderer.solid-2.ts`. `scripts/build.mjs` +emits the tree twice with the twin swapped in. `dist/component.js` and +`dist/solid-2/component.js` are identical files. + +## Three things the migration guide did not tell me + +Read out of `solid-js@2.0.0-rc.0` and `@solidjs/signals@2.0.0-rc.0` rather than +out of the guide, and all three changed the plan: + +1. **`Dynamic` survived unchanged.** It is still a component taking a + `component` prop, exported from `@solidjs/web`. The plan predicted a rewrite + to a `dynamic(source)` factory. `dynamic()` exists, but `Dynamic` does too, + and `createComponent` is re-exported by `@solidjs/web` from `solid-js`, so + the two names the renderer needs are the same two names in both majors. +2. **`JSX` is gone from `solid-js`.** Core no longer declares the namespace at + all, because the shape of an element is the renderer's business. It comes + from `@solidjs/web` now. The plan missed this entirely, and it is the reason + the renderer module has to carry the type as well as the two values. +3. **`useContext` throws in the implementation, not just the docs.** + `getContext` throws `ContextNotFoundError` when the resolved value is + `undefined` and `NoOwnerError` when there is no owner. A provider supplying + `undefined` counts as having provided, so an empty provider **shadows a real + one above it with a throw**. `defineComponent` now skips the wrapper when the + setup returned no context, which is a correctness fix under 2.0 and a no-op + under 1.9. + +## `splitProps` was the part that changed shape + +Four buckets came out of one call, and the call made them disjoint by +construction: a key named in two lists landed in the earlier one only. + +```js +const [presentation, escape, behaviour, passthrough] = + splitProps(props, presentationKeys, ["class", "className", "style", "children"], behaviourKeys); +``` + +`omit` returns only the remainder, and neither major ships the subset half. So +the three routed buckets are picked by name, the fourth still falls out of one +call, and which bucket each declared key belongs to is decided **once per +component** rather than once per render. + +## The compiler: an option, not a fork + +`solid: 1 | 2` on `TransformOptions`. It decides one thing, the specifier a +generated component imports its boundary from: + +``` +solid-layouts/application-boundary -> solid-layouts/solid-2/application-boundary +``` + +Told rather than sniffed: the compiler sees one source file, and which Solid an +application runs on is not written in it. Default is 1 and the emission is +byte-identical to before, asserted by a test rather than by inspection. + +The builtins list gained 2.0's renames (`Suspense` to `Loading`, `SuspenseList` +to `Reveal`, `ErrorBoundary` to `Errored`, plus `Repeat`) and the source check +admits `@solidjs/web`. Neither is gated behind the option: the check tests a +name against a module, and no 1.9 build can import `Loading` from a Solid module +that does not export one. + +## Why the specifier and not an export condition + +Both work. A `solid-2` export condition would keep the generated files +byte-identical between majors and cost no compiler change at all. It was built +that way first and then removed, for one reason: a specifier is a line you can +read. `grep` answers which runtime a build is on, and a mismatch reads as a +wrong-looking import rather than as a resolve failure two layers down. The +condition also needed `customConditions` in every consumer's tsconfig to keep +tsc agreeing with the bundler. + +The cost of the explicit form is one edit in `@pathscale/ui`, because the +library funnels every hand-written runtime import through `src/lib/layouts/index.ts`. + +## The plugins + +Two more exported names rather than an option on the existing two: + +```js +pluginSolid2LayoutsLibrary(), +pluginBabel({ include: /\.(?:jsx|tsx)$/ }), +pluginSolid2(), +``` + +The choice is not independent of the rest of the build. It has to agree with +`pluginSolid2()` and with the installed `solid-js`, and a name that must match +its neighbour in the plugin list is easier to get right than a flag that must. +`pluginSolidLayoutsLibrary`/`Application` are untouched. + +Asking a runtime that predates this for `./solid-2` now fails by name rather +than quietly falling back to `main` and serving the wrong major. + +## Order of work + +1. ~~**`component.ts` prop routing.**~~ Done. `pick` plus one `rest`, buckets + resolved per component. +2. **The context decision.** Half done. The runtime's own context carries a + default now, which settles `solid-layouts`. The library question is open: + `@pathscale/ui` calls `useContext` at **57 sites with only 17 guarded**, and + every compound component that currently works standalone throws under 2.0 + unless each context gets a default. That is an API decision about what 3.0 + *is*, and it does not belong in this repository. +3. ~~**`@solidjs/web` imports.**~~ Done, and smaller than expected: `Dynamic` + did not change shape. +4. ~~**Rust: builtins list, source check, boundary option.**~~ Done. 60 Rust + tests, 22 JS tests. +5. ~~**`rsbuild-plugin-solid-layouts`.**~~ Done. Still true that it sits beside + `@rsbuild/plugin-solid@1.2.2`, which *depends on* `babel-preset-solid: + ^1.9.12`. That is a hard pin on Solid 1's JSX transform and needs an override + to `babel-preset-solid@next` or a patched plugin. Untested. +6. **Publish under a prerelease tag** (`0.2.0-rc.0` on `next`) so + `@pathscale/ui` on `next/solid-2` can consume it without touching `latest`. + Not done. + +## What is verified, and what is not + +Verified by running it: 145 runtime tests under 1.9 unchanged; both builds +emitted; 60 Rust tests; 22 compiler JS tests; the boundary specifier checked +end to end through the rebuilt native binding in both bundle and source mode. + +**Not verified:** the 2.0 build has never run against an installed `solid-js@2`. +It typechecks with `skipLibCheck`, so its declarations bind to `@solidjs/web` +but nothing has checked `@solidjs/web` against the Solid 2 it expects. The first +real test is a `@pathscale/ui` build on Solid 2, and that is the next thing to +do. + +## What is NOT blocked on this + +The 11 `@solid-primitives/*` packages `@pathscale/ui` depends on peer-pin +`solid-js: ^1.6.12` and call `createEffect`/`onCleanup` internally, both of +which change signature. They break at runtime, not at install, and no work here +fixes that. Either upstream ships Solid 2 releases, or the four or five we +actually use get vendored. diff --git a/packages/rsbuild-plugin-solid-layouts/index.d.ts b/packages/rsbuild-plugin-solid-layouts/index.d.ts index d141d92..d52affc 100644 --- a/packages/rsbuild-plugin-solid-layouts/index.d.ts +++ b/packages/rsbuild-plugin-solid-layouts/index.d.ts @@ -1,9 +1,11 @@ export { + pluginSolid2LayoutsApplication, pluginSolidLayoutsApplication, type ApplicationLayoutSource, type SolidLayoutsApplicationOptions, } from "solid-layouts-oxc/application"; export { + pluginSolid2LayoutsLibrary, pluginSolidLayoutsLibrary, type SolidLayoutsLibraryOptions, } from "solid-layouts-oxc/library"; diff --git a/packages/rsbuild-plugin-solid-layouts/index.js b/packages/rsbuild-plugin-solid-layouts/index.js index e74b277..0e887be 100644 --- a/packages/rsbuild-plugin-solid-layouts/index.js +++ b/packages/rsbuild-plugin-solid-layouts/index.js @@ -3,3 +3,8 @@ import library from "solid-layouts-oxc/library"; export const pluginSolidLayoutsApplication = application.pluginSolidLayoutsApplication; export const pluginSolidLayoutsLibrary = library.pluginSolidLayoutsLibrary; + +// The Solid 2.0 pair. Separate names rather than an option, so the plugin list +// reads as one choice: `pluginSolid2LayoutsLibrary()` beside `pluginSolid2()`. +export const pluginSolid2LayoutsApplication = application.pluginSolid2LayoutsApplication; +export const pluginSolid2LayoutsLibrary = library.pluginSolid2LayoutsLibrary; diff --git a/packages/rsbuild-plugin-solid-layouts/package.json b/packages/rsbuild-plugin-solid-layouts/package.json index b321a9d..d9d8b35 100644 --- a/packages/rsbuild-plugin-solid-layouts/package.json +++ b/packages/rsbuild-plugin-solid-layouts/package.json @@ -1,6 +1,6 @@ { "name": "rsbuild-plugin-solid-layouts", - "version": "0.1.4", + "version": "0.2.0", "description": "Rsbuild integration for the Solid Layouts library and application compilers", "license": "MIT", "type": "module", @@ -19,7 +19,7 @@ "index.d.ts" ], "dependencies": { - "solid-layouts-oxc": "0.1.7" + "solid-layouts-oxc": "0.2.0" }, "peerDependencies": { "@rsbuild/core": ">=1.3.0" diff --git a/packages/solid-layouts-oxc/application.d.ts b/packages/solid-layouts-oxc/application.d.ts index 74e7616..d675477 100644 --- a/packages/solid-layouts-oxc/application.d.ts +++ b/packages/solid-layouts-oxc/application.d.ts @@ -10,6 +10,11 @@ export type SolidLayoutsApplicationOptions = { layouts?: ApplicationLayoutSource[]; runtime?: string; include?: string; + /** + * Which major of Solid the build targets. Defaults to 1. Prefer + * `pluginSolid2LayoutsApplication`, which sets it. + */ + solid?: 1 | 2; }; export type ResolvedLayoutSource = { @@ -30,6 +35,11 @@ export type CompiledApplication = { }; export declare const APPLICATION_BOUNDARY: "solid-layouts/application-boundary"; +export declare const SOLID_2_APPLICATION_BOUNDARY: "solid-layouts/solid-2/application-boundary"; +export declare function boundaryFor(solid?: 1 | 2): { + specifier: string; + subpath: "." | "./solid-2"; +}; export declare const FORMAT: "solid-layouts-library-v2"; export declare function compileApplication( options?: SolidLayoutsApplicationOptions, @@ -46,3 +56,6 @@ export declare function resolveLayoutSource( export declare function pluginSolidLayoutsApplication( options?: SolidLayoutsApplicationOptions, ): { name: string; enforce: "post"; setup(api: unknown): void }; +export declare function pluginSolid2LayoutsApplication( + options?: Omit, +): { name: string; enforce: "post"; setup(api: unknown): void }; diff --git a/packages/solid-layouts-oxc/application.js b/packages/solid-layouts-oxc/application.js index 4e12716..e3e026f 100644 --- a/packages/solid-layouts-oxc/application.js +++ b/packages/solid-layouts-oxc/application.js @@ -7,6 +7,28 @@ const { transform } = require("./index.js"); const FORMAT = "solid-layouts-library-v2"; const APPLICATION_BOUNDARY = "solid-layouts/application-boundary"; +const SOLID_2_APPLICATION_BOUNDARY = "solid-layouts/solid-2/application-boundary"; + +/** + * The specifier a generated component imports its boundary from, and the + * `solid-layouts` subpath that specifier has to resolve to. + * + * Two entries because the runtime is published twice: Solid 2.0 moved + * `Dynamic` and `createComponent` out of `solid-js/web` and dropped that + * subpath, so one build cannot serve both majors. The specifier is spelled out + * in the generated file rather than resolved by a bundler condition, which + * means `grep` answers which runtime a build is on and a mismatch reads as a + * wrong-looking import rather than as a resolve failure two layers down. + */ +function boundaryFor(solid) { + if (solid === undefined || solid === 1) { + return { specifier: APPLICATION_BOUNDARY, subpath: "." }; + } + if (solid === 2) { + return { specifier: SOLID_2_APPLICATION_BOUNDARY, subpath: "./solid-2" }; + } + throw new Error(`unknown solid major: ${solid}`); +} function readJson(path, label) { try { @@ -43,14 +65,23 @@ function requiredFile(packageRoot, path, label) { return absolute; } -function publicEntryFrom(packageJson) { - const rootExport = packageJson.exports?.["."]; +function publicEntryFrom(packageJson, subpath = ".") { + const rootExport = packageJson.exports?.[subpath]; if (typeof rootExport === "string") return rootExport; if (rootExport && typeof rootExport === "object") { for (const condition of ["import", "default"]) { if (typeof rootExport[condition] === "string") return rootExport[condition]; } } + // Only the root entry has the legacy fields to fall back on. A named subpath + // that is missing is a version of the runtime the installed package does not + // publish, which is worth saying plainly rather than silently serving the + // wrong major from `main`. + if (subpath !== ".") { + throw new Error( + `${packageJson.name}@${packageJson.version} does not export ${subpath}; it predates Solid 2 support`, + ); + } for (const field of ["module", "main"]) { if (typeof packageJson[field] === "string") return packageJson[field]; } @@ -141,14 +172,18 @@ function publicSubpathSources(source) { .sort((a, b) => a.module.localeCompare(b.module)); } -function resolvePublicPackageEntry(root, module) { +function resolvePublicPackageEntry(root, module, subpath = ".") { const packageJsonPath = resolvePackageJson(root, module); const packageRoot = dirname(packageJsonPath); const packageJson = readJson(packageJsonPath, `${module} package metadata`); if (packageJson.name !== module) { throw new Error(`resolved package ${packageJson.name} does not match ${module}`); } - return requiredFile(packageRoot, publicEntryFrom(packageJson), `${module} public entry`); + return requiredFile( + packageRoot, + publicEntryFrom(packageJson, subpath), + `${module} public entry`, + ); } function validateComponent(module, packageRoot, name, component) { @@ -286,8 +321,9 @@ function compileApplicationFile(source, filename, application) { } function pluginSolidLayoutsApplication(options = {}) { + const boundary = boundaryFor(options.solid); return { - name: "solid-layouts:application", + name: options.solid === 2 ? "solid-layouts:application:solid-2" : "solid-layouts:application", enforce: "post", setup(api) { const application = compileApplication({ @@ -296,13 +332,13 @@ function pluginSolidLayoutsApplication(options = {}) { }); const runtime = options.runtime ? resolve(application.root, options.runtime) - : resolvePublicPackageEntry(application.root, "solid-layouts"); + : resolvePublicPackageEntry(application.root, "solid-layouts", boundary.subpath); if (!existsSync(runtime)) throw new Error(`solid-layouts runtime not found: ${runtime}`); api.modifyBundlerChain({ order: "post", handler(chain) { - chain.resolve.alias.set(APPLICATION_BOUNDARY, runtime); + chain.resolve.alias.set(boundary.specifier, runtime); chain.module .rule("solid-layouts-application") .after("babel-js") @@ -321,11 +357,27 @@ function pluginSolidLayoutsApplication(options = {}) { }; } +/** + * The Solid 2.0 form of the application plugin. + * + * A separate exported name rather than an option the caller passes, because + * the choice is not independent of the rest of the build: it has to agree with + * `pluginSolid2()` and with the installed `solid-js`. A name that must match + * its neighbour in the plugin list is easier to get right, and easier to read + * back later, than a flag that must. + */ +function pluginSolid2LayoutsApplication(options = {}) { + return pluginSolidLayoutsApplication({ ...options, solid: 2 }); +} + module.exports = { APPLICATION_BOUNDARY, FORMAT, + SOLID_2_APPLICATION_BOUNDARY, + boundaryFor, compileApplication, compileApplicationFile, + pluginSolid2LayoutsApplication, pluginSolidLayoutsApplication, resolveLayoutSource, }; diff --git a/packages/solid-layouts-oxc/crates/common/src/lib.rs b/packages/solid-layouts-oxc/crates/common/src/lib.rs index 8120fbd..450e865 100644 --- a/packages/solid-layouts-oxc/crates/common/src/lib.rs +++ b/packages/solid-layouts-oxc/crates/common/src/lib.rs @@ -41,6 +41,34 @@ pub enum LibraryOutput { Component, } +/// Which major of Solid the emitted code targets. +/// +/// It decides one thing: which `solid-layouts` entry a generated component +/// imports its boundary from. The runtime is published twice because Solid 2.0 +/// moved `Dynamic` and `createComponent` out of `solid-js/web` into +/// `@solidjs/web` and dropped the old subpath, so one specifier cannot resolve +/// under both. +/// +/// Configured rather than sniffed. The compiler sees a source file, and the +/// answer is a fact about the application being built. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum SolidVersion { + #[default] + V1, + V2, +} + +impl SolidVersion { + /// The module a generated component imports `defineComponent` from. + pub fn application_boundary(self) -> &'static str { + match self { + Self::V1 => "solid-layouts/application-boundary", + Self::V2 => "solid-layouts/solid-2/application-boundary", + } + } +} + /// One resolved Layout package available to an application build. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -93,6 +121,8 @@ pub struct TransformOptions { #[serde(default)] pub library_output: LibraryOutput, #[serde(default)] + pub solid: SolidVersion, + #[serde(default)] pub config: LayoutsConfig, /// Off by default. With it set the pass parses and returns the source /// unchanged, which is how a host proves the pipeline is wired before any @@ -107,6 +137,7 @@ impl TransformOptions { filename: filename.into(), mode, library_output: LibraryOutput::default(), + solid: SolidVersion::default(), config: LayoutsConfig::default(), parse_only: false, } diff --git a/packages/solid-layouts-oxc/crates/transform/src/lib.rs b/packages/solid-layouts-oxc/crates/transform/src/lib.rs index 9a537e4..65f7300 100644 --- a/packages/solid-layouts-oxc/crates/transform/src/lib.rs +++ b/packages/solid-layouts-oxc/crates/transform/src/lib.rs @@ -17,7 +17,8 @@ pub mod linter; pub mod match_layouts; use layouts_common::{ - CompilerMode, Diagnostic, FileKind, LibraryOutput, Severity, TransformOptions, TransformResult, + CompilerMode, Diagnostic, FileKind, LibraryOutput, Severity, SolidVersion, TransformOptions, + TransformResult, }; use oxc_allocator::Allocator; use oxc_ast::ast::{ @@ -123,9 +124,13 @@ pub fn transform(source: &str, options: &TransformOptions) -> TransformResult { } let code = match options.mode { - CompilerMode::Library => { - compile_library_source(source, &parsed.program, &layouts, options.library_output) - } + CompilerMode::Library => compile_library_source( + source, + &parsed.program, + &layouts, + options.library_output, + options.solid, + ), CompilerMode::Application => compile_application_source(source, &parsed.program, options), }; let changed = code != source; @@ -215,6 +220,7 @@ fn compile_library_source( program: &Program<'_>, layouts: &[FoundLayout], output: LibraryOutput, + solid: SolidVersion, ) -> String { let recipes = compile_recipe::find_recipes(program); let index = compile_recipe::SlotIndex::build(&recipes); @@ -252,9 +258,15 @@ fn compile_library_source( let unresolved = semantic.scoping().root_unresolved_references(); if output == LibraryOutput::Component && !layouts.is_empty() { + // The boundary specifier is the only part of this header a major of + // Solid changes. `Component` is still exported from `solid-js` in 2.0, + // so the type import is the same line either way. edits.push(SourceEdit::Insert { at: 0, - text: "import { defineComponent as __defineLayoutComponent } from \"solid-layouts/application-boundary\";\nimport type { Component as __LayoutComponent } from \"solid-js\";\n".to_owned(), + text: format!( + "import {{ defineComponent as __defineLayoutComponent }} from \"{}\";\nimport type {{ Component as __LayoutComponent }} from \"solid-js\";\n", + solid.application_boundary(), + ), }); } @@ -777,6 +789,54 @@ export const Button: Layout = () => { ); } + #[test] + fn the_solid_major_picks_which_runtime_entry_the_boundary_imports() { + let source = r#"import type { Layout } from "solid-layouts"; +import { button } from "./Button.recipe"; +export const Button: Layout = () => { + return ; +}; +"#; + let boundary_of = |solid| { + let mut options = TransformOptions::new("Button.layout.tsx", CompilerMode::Library); + options.library_output = LibraryOutput::Component; + options.solid = solid; + transform(source, &options).code + }; + + // Default and explicit 1 are the same output, which is the point: a + // host that never sets the option keeps emitting what it always did. + let default = boundary_of(SolidVersion::default()); + assert_eq!(default, boundary_of(SolidVersion::V1)); + assert!( + default.contains("from \"solid-layouts/application-boundary\""), + "{default}" + ); + + let v2 = boundary_of(SolidVersion::V2); + assert!( + v2.contains("from \"solid-layouts/solid-2/application-boundary\""), + "{v2}" + ); + // The type import is not version-specific: 2.0 still exports + // `Component` from `solid-js`. + assert!( + v2.contains("import type { Component as __LayoutComponent } from \"solid-js\";"), + "{v2}" + ); + } + + #[test] + fn solid_2_renderer_builtins_need_no_layout() { + let source = r#"import { Loading, Errored } from "solid-js"; +import { Portal } from "@solidjs/web"; +export const View = () => ; +"#; + let options = TransformOptions::new("View.tsx", CompilerMode::Application); + let result = transform(source, &options); + assert!(result.diagnostics.is_empty(), "{:?}", result.diagnostics); + } + #[test] fn application_mode_rewrites_a_validated_package_import_to_c() { let source = r#"import { Icon as StatusIcon } from "@pathscale/test-ui"; diff --git a/packages/solid-layouts-oxc/crates/transform/src/match_layouts.rs b/packages/solid-layouts-oxc/crates/transform/src/match_layouts.rs index cbe8795..b196071 100644 --- a/packages/solid-layouts-oxc/crates/transform/src/match_layouts.rs +++ b/packages/solid-layouts-oxc/crates/transform/src/match_layouts.rs @@ -224,6 +224,12 @@ fn declared_name(declaration: &Declaration<'_>, origins: &mut HashMap`. const SOLID_BUILTINS: &[&str] = &[ "For", "Show", @@ -235,6 +241,11 @@ const SOLID_BUILTINS: &[&str] = &[ "Index", "Dynamic", "ErrorBoundary", + // 2.0 + "Loading", + "Reveal", + "Repeat", + "Errored", ]; /// Whether a name is a Solid built-in imported from Solid itself. @@ -242,8 +253,14 @@ const SOLID_BUILTINS: &[&str] = &[ /// Both halves matter. A user's own component called `Show` imported from /// their library still needs a Layout, and a built-in imported from anywhere /// else is not the built-in. +/// +/// `@solidjs/web` is where 2.0 serves the renderer's half of the list, and it +/// is Solid itself, so it belongs beside the two 1.9 modules rather than being +/// gated behind the version option. `solid-js/web` does not exist under 2.0 and +/// `@solidjs/web` does not exist under 1.9, so admitting all three cannot make +/// one major accept the other's import. fn is_solid_builtin(name: &str, source: &str) -> bool { - SOLID_BUILTINS.contains(&name) && (source == "solid-js" || source == "solid-js/web") + SOLID_BUILTINS.contains(&name) && matches!(source, "solid-js" | "solid-js/web" | "@solidjs/web") } /// Checks every component reference against the configured Layout sources. diff --git a/packages/solid-layouts-oxc/index.d.ts b/packages/solid-layouts-oxc/index.d.ts index 0954b23..7ea2910 100644 --- a/packages/solid-layouts-oxc/index.d.ts +++ b/packages/solid-layouts-oxc/index.d.ts @@ -34,6 +34,12 @@ export interface JsLayoutSource { export interface JsOptions { mode: string libraryOutput?: string + /** + * Which major of Solid the output targets: `1` or `2`. Defaults to 1, + * so a host that has never heard of this option keeps emitting what it + * always emitted. + */ + solid?: number layoutSources?: Array parseOnly?: boolean } diff --git a/packages/solid-layouts-oxc/index.js b/packages/solid-layouts-oxc/index.js index cf4c747..17ba664 100644 --- a/packages/solid-layouts-oxc/index.js +++ b/packages/solid-layouts-oxc/index.js @@ -77,8 +77,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-android-arm64') const bindingPackageVersion = require('solid-layouts-oxc-android-arm64/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -93,8 +93,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-android-arm-eabi') const bindingPackageVersion = require('solid-layouts-oxc-android-arm-eabi/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -114,8 +114,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-win32-x64-gnu') const bindingPackageVersion = require('solid-layouts-oxc-win32-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -130,8 +130,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-win32-x64-msvc') const bindingPackageVersion = require('solid-layouts-oxc-win32-x64-msvc/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -147,8 +147,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-win32-ia32-msvc') const bindingPackageVersion = require('solid-layouts-oxc-win32-ia32-msvc/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -163,8 +163,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-win32-arm64-msvc') const bindingPackageVersion = require('solid-layouts-oxc-win32-arm64-msvc/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -182,8 +182,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-darwin-universal') const bindingPackageVersion = require('solid-layouts-oxc-darwin-universal/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -198,8 +198,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-darwin-x64') const bindingPackageVersion = require('solid-layouts-oxc-darwin-x64/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -214,8 +214,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-darwin-arm64') const bindingPackageVersion = require('solid-layouts-oxc-darwin-arm64/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -234,8 +234,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-freebsd-x64') const bindingPackageVersion = require('solid-layouts-oxc-freebsd-x64/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -250,8 +250,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-freebsd-arm64') const bindingPackageVersion = require('solid-layouts-oxc-freebsd-arm64/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -271,8 +271,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-linux-x64-musl') const bindingPackageVersion = require('solid-layouts-oxc-linux-x64-musl/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -287,8 +287,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-linux-x64-gnu') const bindingPackageVersion = require('solid-layouts-oxc-linux-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -305,8 +305,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-linux-arm64-musl') const bindingPackageVersion = require('solid-layouts-oxc-linux-arm64-musl/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -321,8 +321,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-linux-arm64-gnu') const bindingPackageVersion = require('solid-layouts-oxc-linux-arm64-gnu/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -339,8 +339,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-linux-arm-musleabihf') const bindingPackageVersion = require('solid-layouts-oxc-linux-arm-musleabihf/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -355,8 +355,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-linux-arm-gnueabihf') const bindingPackageVersion = require('solid-layouts-oxc-linux-arm-gnueabihf/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -373,8 +373,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-linux-loong64-musl') const bindingPackageVersion = require('solid-layouts-oxc-linux-loong64-musl/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -389,8 +389,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-linux-loong64-gnu') const bindingPackageVersion = require('solid-layouts-oxc-linux-loong64-gnu/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -407,8 +407,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-linux-riscv64-musl') const bindingPackageVersion = require('solid-layouts-oxc-linux-riscv64-musl/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -423,8 +423,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-linux-riscv64-gnu') const bindingPackageVersion = require('solid-layouts-oxc-linux-riscv64-gnu/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -440,8 +440,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-linux-ppc64-gnu') const bindingPackageVersion = require('solid-layouts-oxc-linux-ppc64-gnu/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -456,8 +456,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-linux-s390x-gnu') const bindingPackageVersion = require('solid-layouts-oxc-linux-s390x-gnu/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -476,8 +476,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-openharmony-arm64') const bindingPackageVersion = require('solid-layouts-oxc-openharmony-arm64/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -492,8 +492,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-openharmony-x64') const bindingPackageVersion = require('solid-layouts-oxc-openharmony-x64/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -508,8 +508,8 @@ function requireNative() { try { const binding = require('solid-layouts-oxc-openharmony-arm') const bindingPackageVersion = require('solid-layouts-oxc-openharmony-arm/package.json').version - if (bindingPackageVersion !== '0.1.6' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -648,8 +648,8 @@ if (!nativeBinding || forceWasi) { if (!candidateFailed) { if (process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { const bindingPackageVersion = require('solid-layouts-oxc-wasm32-wasi/package.json').version - if (bindingPackageVersion !== '0.1.6') { - throw new Error(`WASI binding package version mismatch, expected 0.1.6 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.2.0') { + throw new Error(`WASI binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } } wasiBinding = require('solid-layouts-oxc-wasm32-wasi') diff --git a/packages/solid-layouts-oxc/library.d.ts b/packages/solid-layouts-oxc/library.d.ts index 69a2a51..3bc612e 100644 --- a/packages/solid-layouts-oxc/library.d.ts +++ b/packages/solid-layouts-oxc/library.d.ts @@ -5,6 +5,12 @@ export type SolidLayoutsLibraryOptions = { output?: string; check?: boolean; updateBaseline?: boolean; + /** + * Which major of Solid the emitted boundary import targets. Defaults to 1, + * or to `solid` in `layouts.library.json`. Prefer + * `pluginSolid2LayoutsLibrary`, which sets it. + */ + solid?: 1 | 2; }; export type LibraryDiagnostic = { @@ -67,3 +73,6 @@ export declare function lintLibrary( export declare function pluginSolidLayoutsLibrary( options?: SolidLayoutsLibraryOptions, ): { name: string; setup(api: unknown): void }; +export declare function pluginSolid2LayoutsLibrary( + options?: Omit, +): { name: string; setup(api: unknown): void }; diff --git a/packages/solid-layouts-oxc/library.js b/packages/solid-layouts-oxc/library.js index 053007f..5a9f6ea 100644 --- a/packages/solid-layouts-oxc/library.js +++ b/packages/solid-layouts-oxc/library.js @@ -12,6 +12,7 @@ const { } = require("node:fs"); const { basename, dirname, relative, resolve, sep } = require("node:path"); const { lintProject, transform } = require("./index.js"); +const { boundaryFor } = require("./application.js"); const FORMAT = "solid-layouts-library-v2"; @@ -157,8 +158,8 @@ function formatDiagnostics(filename, diagnostics) { .join("\n"); } -function compileFile(source, filename, libraryOutput = "layout") { - const result = transform(source, filename, { mode: "library", libraryOutput }); +function compileFile(source, filename, libraryOutput = "layout", solid = 1) { + const result = transform(source, filename, { mode: "library", libraryOutput, solid }); if (result.failed) throw new Error(formatDiagnostics(filename, result.diagnostics)); return result.code; } @@ -219,14 +220,48 @@ function lintLibrary(options = {}) { }; } +/** + * Why source mode asks for a list when nothing else does. + * + * Convention-based discovery reads every `*.layout.tsx` and emits a `generated` + * manifest entry carrying the recipe, the Layout and their exports, so the + * application compiler can verify the join. Source mode generates alongside an + * existing package instead, where the public surface is whatever that package's + * own barrel exports, including parts like `AccordionItem` that share a parent's + * layout file and have no file of their own. The compiler cannot infer that, so + * source mode is told. + * + * The old message said only "must declare its public Layout exports", which + * reads as a missing field rather than as a consequence of the mode, and sends + * you to write the list rather than to ask whether you wanted the mode. The + * documentation says no authored component manifest is required, and for + * convention-based discovery that is true. + */ +function sourceModeExportsMessage(configPath) { + return [ + `${configPath}: source mode needs an "exports" array naming this package's public components.`, + "", + 'Only `mode: "source"` needs one. Convention-based discovery derives the manifest from', + "every `*.layout.tsx` it finds, and emits richer entries the application compiler can", + "verify; source mode generates alongside a package whose public surface it cannot see,", + "so the list is how it learns the names.", + "", + "If this package does not need adjacent generation, delete the config and let discovery", + "do it. If it does, the list has to be kept in step with the barrel by hand: nothing", + "compares them, and a rename that misses it leaves the manifest naming components that", + "no longer exist while rejecting the ones that do.", + ].join("\n"); +} + function generateLibrarySource(options = {}) { const root = resolve(options.root || process.cwd()); const { configPath, config } = readLibraryConfig(root, options); + const solid = options.solid ?? config.solid; if (config.mode !== "source") { throw new Error(`${configPath || root} must set mode to "source" for adjacent generation`); } if (!Array.isArray(config.exports) || config.exports.length === 0) { - throw new Error(`${configPath} must declare its public Layout exports`); + throw new Error(sourceModeExportsMessage(configPath)); } const lint = lintLibrary({ ...options, root }); if (lint.failed) { @@ -239,7 +274,7 @@ function generateLibrarySource(options = {}) { for (const input of filesBelow(lint.sourceRoot)) { if (!/\.layout\.(tsx|jsx)$/.test(input)) continue; const output = input.replace(/\.layout\.(tsx|jsx)$/, ".generated.$1"); - const compiled = `${compileFile(readFileSync(input, "utf8"), input, "component").trimEnd()}\n`; + const compiled = `${compileFile(readFileSync(input, "utf8"), input, "component", solid).trimEnd()}\n`; const current = existsSync(output) ? readFileSync(output, "utf8") : ""; if (current === compiled) continue; if (options.check) { @@ -256,7 +291,7 @@ function emitSourceManifest(options = {}) { const { configPath, config } = readLibraryConfig(root, options); if (config.mode !== "source") throw new Error(`${configPath || root} is not a source library config`); const exports = config.exports || []; - if (exports.length === 0) throw new Error(`${configPath} must declare its public Layout exports`); + if (exports.length === 0) throw new Error(sourceModeExportsMessage(configPath)); const duplicates = exports.filter((name, index) => exports.indexOf(name) !== index); if (duplicates.length) throw new Error(`${configPath} repeats Layout export ${duplicates[0]}`); const sourcePackage = readJson(resolve(root, "package.json")); @@ -281,7 +316,7 @@ function modulePath(fromFile, toFile) { return specifier.startsWith(".") ? specifier : `./${specifier}`; } -function generateEntries(components, outputRoot) { +function generateEntries(components, outputRoot, solid) { const entries = new Map(); for (const component of components) { const entry = resolve(outputRoot, component.entry); @@ -291,7 +326,7 @@ function generateEntries(components, outputRoot) { ); const recipe = resolve(outputRoot, component.recipe); const lines = entries.get(entry) || [ - 'import { defineComponent as __defineLayoutComponent } from "solid-layouts/application-boundary";', + `import { defineComponent as __defineLayoutComponent } from "${boundaryFor(solid).specifier}";`, 'import type { Component as __LayoutComponent } from "solid-js";', ]; const componentExpression = component.propsType @@ -318,7 +353,7 @@ function generateEntries(components, outputRoot) { } } -function assertComponent(component, sourceRoot, outputRoot) { +function assertComponent(component, sourceRoot, outputRoot, solid) { for (const key of ["name", "entry", "recipe", "recipeExport", "layout", "layoutExport"]) { if (!component[key]) throw new Error(`component entry is missing ${key}`); } @@ -385,7 +420,7 @@ function assertComponent(component, sourceRoot, outputRoot) { `${component.name}: generated layout does not export ${component.layoutExport}`, ); } - if (!entryOutput.includes("solid-layouts/application-boundary")) { + if (!entryOutput.includes(boundaryFor(solid).specifier)) { throw new Error(`${component.name}: ${component.entry} has no application compiler boundary`); } @@ -432,6 +467,7 @@ function compileLibrary(options = {}) { const root = resolve(options.root || process.cwd()); const { configPath, config } = readLibraryConfig(root, options); if (config.mode === "source") return generateLibrarySource({ ...options, root }); + const solid = options.solid ?? config.solid; const sourceRoot = resolve(root, config.source || "src"); const outputRoot = resolve(root, config.output || "bundle"); const sourcePackage = readJson(resolve(root, "package.json")); @@ -457,7 +493,7 @@ function compileLibrary(options = {}) { mkdirSync(dirname(output), { recursive: true }); if (isLayout || /\.recipe\.(ts|tsx|js|jsx)$/.test(fromSource)) { - const compiled = compileFile(readFileSync(input, "utf8"), input); + const compiled = compileFile(readFileSync(input, "utf8"), input, "layout", solid); writeFileSync(output, compiled); const parsed = transform(compiled, output, { mode: "library", parseOnly: true }); if (parsed.failed) throw new Error(formatDiagnostics(output, parsed.diagnostics)); @@ -467,11 +503,11 @@ function compileLibrary(options = {}) { } const configuredComponents = config.components || discoverComponents(sourceRoot); - generateEntries(configuredComponents, outputRoot); + generateEntries(configuredComponents, outputRoot, solid); const components = {}; for (const component of configuredComponents) { - components[component.name] = assertComponent(component, sourceRoot, outputRoot); + components[component.name] = assertComponent(component, sourceRoot, outputRoot, solid); } if (Object.keys(components).length === 0) { throw new Error(`${configPath || sourceRoot} must contain at least one Layout component`); @@ -514,10 +550,11 @@ function compileLibrary(options = {}) { function pluginSolidLayoutsLibrary(options = {}) { return { - name: "solid-layouts:library", + name: options.solid === 2 ? "solid-layouts:library:solid-2" : "solid-layouts:library", setup(api) { const root = resolve(options.root || api.context.rootPath); const { config } = readLibraryConfig(root, options); + const solid = options.solid ?? config.solid; const compile = () => compileLibrary({ ...options, @@ -537,7 +574,7 @@ function pluginSolidLayoutsLibrary(options = {}) { .end() .use("solid-layouts-library") .loader(require.resolve("./loader.js")) - .options({ mode: "library", libraryOutput: "layout" }); + .options({ mode: "library", libraryOutput: "layout", solid }); }, }); api.onAfterBuild(() => emitSourceManifest({ ...options, root })); @@ -546,11 +583,20 @@ function pluginSolidLayoutsLibrary(options = {}) { }; } +/** + * The Solid 2.0 form of the library plugin. See its application twin for why + * this is a second exported name rather than an option on the first. + */ +function pluginSolid2LayoutsLibrary(options = {}) { + return pluginSolidLayoutsLibrary({ ...options, solid: 2 }); +} + module.exports = { FORMAT, compileLibrary, generateLibrarySource, emitSourceManifest, lintLibrary, + pluginSolid2LayoutsLibrary, pluginSolidLayoutsLibrary, }; diff --git a/packages/solid-layouts-oxc/library.test.js b/packages/solid-layouts-oxc/library.test.js index f298297..748bcef 100644 --- a/packages/solid-layouts-oxc/library.test.js +++ b/packages/solid-layouts-oxc/library.test.js @@ -78,6 +78,26 @@ test("builds valid generated Layout source and a package manifest", () => { ); }); +test("the solid major picks which runtime entry the generated entry imports", () => { + // The whole point of naming the runtime in the emitted source rather than + // resolving it through a bundler condition: which major a build is on is a + // line you can read, in a file you can grep. + const one = readFileSync( + join(compileLibrary({ root: fixture() }).outputRoot, "index.ts"), + "utf8", + ); + const two = readFileSync( + join(compileLibrary({ root: fixture(), solid: 2 }).outputRoot, "index.ts"), + "utf8", + ); + + expect(one).toContain('from "solid-layouts/application-boundary"'); + expect(one).not.toContain("solid-2"); + expect(two).toContain('from "solid-layouts/solid-2/application-boundary"'); + // Everything else about the two entries is the same file. + expect(two.replace("/solid-2/application-boundary", "/application-boundary")).toBe(one); +}); + test("lints the library with the native project checker", () => { const root = fixture(); const result = lintLibrary({ root }); diff --git a/packages/solid-layouts-oxc/loader.js b/packages/solid-layouts-oxc/loader.js index 8341f1c..3cb625c 100644 --- a/packages/solid-layouts-oxc/loader.js +++ b/packages/solid-layouts-oxc/loader.js @@ -31,6 +31,7 @@ module.exports = function layoutsLoader(source) { result = transform(source, filename, { mode: options.mode, libraryOutput: options.libraryOutput, + solid: options.solid, layoutSources: options.layoutSources, parseOnly: options.parseOnly, }); diff --git a/packages/solid-layouts-oxc/package.json b/packages/solid-layouts-oxc/package.json index 328d9a6..01f35f6 100644 --- a/packages/solid-layouts-oxc/package.json +++ b/packages/solid-layouts-oxc/package.json @@ -1,6 +1,6 @@ { "name": "solid-layouts-oxc", - "version": "0.1.7", + "version": "0.2.0", "description": "The Layouts pre-pass for SolidJS, built on oxc", "license": "MIT", "type": "commonjs", diff --git a/packages/solid-layouts-oxc/src/lib.rs b/packages/solid-layouts-oxc/src/lib.rs index 468d8b6..aa3cc8d 100644 --- a/packages/solid-layouts-oxc/src/lib.rs +++ b/packages/solid-layouts-oxc/src/lib.rs @@ -53,6 +53,10 @@ mod binding { pub struct JsOptions { pub mode: String, pub library_output: Option, + /// Which major of Solid the output targets: `1` or `2`. Defaults to 1, + /// so a host that has never heard of this option keeps emitting what it + /// always emitted. + pub solid: Option, pub layout_sources: Option>, pub parse_only: Option, } @@ -169,6 +173,11 @@ mod binding { Some("component") => layouts_common::LibraryOutput::Component, Some(other) => panic!("unknown solid-layouts library output: {other}"), }; + options_inner.solid = match given.solid { + None | Some(1) => layouts_common::SolidVersion::V1, + Some(2) => layouts_common::SolidVersion::V2, + Some(other) => panic!("unknown solid major: {other}"), + }; if let Some(layout_sources) = given.layout_sources { options_inner.config.sources = layout_sources .into_iter() diff --git a/packages/solid-layouts/bun.lock b/packages/solid-layouts/bun.lock index 5effad8..8f93859 100644 --- a/packages/solid-layouts/bun.lock +++ b/packages/solid-layouts/bun.lock @@ -5,16 +5,23 @@ "": { "name": "solid-layouts", "devDependencies": { + "@solidjs/web": "2.0.0-rc.0", "@types/bun": "^1.3.14", "happy-dom": "^20.11.2", "typescript": "^7.0.2", }, "peerDependencies": { - "solid-js": "^1.9.14", + "@solidjs/web": "^2.0.0-rc.0", + "solid-js": "^1.9.14 || ^2.0.0-rc.0", }, + "optionalPeers": [ + "@solidjs/web", + ], }, }, "packages": { + "@solidjs/web": ["@solidjs/web@2.0.0-rc.0", "", { "dependencies": { "seroval": "~1.5.4", "seroval-plugins": "~1.5.4" }, "peerDependencies": { "solid-js": "^2.0.0-rc.0" } }, "sha512-pYSaA9+dH8H1h/d/ZF/P2kR6omfzFGNcdzKhWTcg9fJghXhn8+5UrXUr2iYxDdYNOXZzxxFQhYHSJ7P4HKDqgw=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="], diff --git a/packages/solid-layouts/package.json b/packages/solid-layouts/package.json index 3e4ae42..2b398f4 100644 --- a/packages/solid-layouts/package.json +++ b/packages/solid-layouts/package.json @@ -1,6 +1,6 @@ { "name": "solid-layouts", - "version": "0.1.3", + "version": "0.2.0", "description": "Logic in a .ts file, markup in a .layout.tsx file, presentation declared at the call site", "license": "MIT", "type": "module", @@ -23,21 +23,44 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js" }, + "./solid-2": { + "types": "./dist/solid-2/index.d.ts", + "import": "./dist/solid-2/index.js" + }, + "./solid-2/recipe": { + "types": "./dist/solid-2/recipe.d.ts", + "import": "./dist/solid-2/recipe.js" + }, + "./solid-2/cx": { + "types": "./dist/solid-2/cx.d.ts", + "import": "./dist/solid-2/cx.js" + }, + "./solid-2/application-boundary": { + "types": "./dist/solid-2/index.d.ts", + "import": "./dist/solid-2/index.js" + }, "./package.json": "./package.json" }, "files": [ "dist" ], "scripts": { - "build": "rm -rf dist && tsc -p tsconfig.build.json", + "build": "node scripts/build.mjs", "prepack": "bun run build", "test": "bun test --conditions=browser", "typecheck": "tsc --noEmit" }, "peerDependencies": { - "solid-js": "^1.9.14" + "@solidjs/web": "^2.0.0-rc.0", + "solid-js": "^1.9.14 || ^2.0.0-rc.0" + }, + "peerDependenciesMeta": { + "@solidjs/web": { + "optional": true + } }, "devDependencies": { + "@solidjs/web": "2.0.0-rc.0", "@types/bun": "^1.3.14", "happy-dom": "^20.11.2", "typescript": "^7.0.2" diff --git a/packages/solid-layouts/scripts/build.mjs b/packages/solid-layouts/scripts/build.mjs new file mode 100644 index 0000000..3461bca --- /dev/null +++ b/packages/solid-layouts/scripts/build.mjs @@ -0,0 +1,45 @@ +/** + * Emits the runtime twice: once for Solid 1.9, once for Solid 2.0. + * + * The two builds share every source file but one. `renderer.ts` is the only + * module a major of Solid can move rather than rename - 1.9 serves `Dynamic` + * and `createComponent` from `solid-js/web`, 2.0 from `@solidjs/web` and drops + * the old subpath - so the 2.0 build is the same tree with `renderer.ts` + * replaced by `renderer.solid-2.ts`. + * + * Copying the tree rather than pointing a second tsconfig at a different file + * is deliberate: TypeScript's `paths` does not remap relative specifiers, and + * `component.ts` imports `./renderer.js` relatively because at runtime it must. + * + * `dist/` keeps its path and a 1.9 consumer importing `solid-layouts` gets a + * build that imports no 2.0 package: `renderer.solid-2.ts` is excluded from + * this pass rather than merely unused by it, so nothing resolves `@solidjs/web` + * in a tree that has no reason to have installed it. + */ +import { execFileSync } from "node:child_process"; +import { cpSync, rmSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +// Not dot-prefixed: TypeScript skips hidden directories when resolving +// `include`, so a `.solid-2` staging tree compiles to "no inputs were found". +const staging = resolve(root, "solid-2-staging"); +const tsc = resolve(root, "node_modules/.bin/tsc"); + +const run = (...args) => + execFileSync(tsc, args, { cwd: root, stdio: "inherit" }); + +rmSync(resolve(root, "dist"), { recursive: true, force: true }); +rmSync(staging, { recursive: true, force: true }); + +run("-p", "tsconfig.build.json"); + +try { + cpSync(resolve(root, "src"), staging, { recursive: true }); + cpSync(resolve(staging, "renderer.solid-2.ts"), resolve(staging, "renderer.ts")); + rmSync(resolve(staging, "renderer.solid-2.ts")); + run("-p", "tsconfig.solid-2.json"); +} finally { + rmSync(staging, { recursive: true, force: true }); +} diff --git a/packages/solid-layouts/src/component.ts b/packages/solid-layouts/src/component.ts index 0ff34ad..c7626a6 100644 --- a/packages/solid-layouts/src/component.ts +++ b/packages/solid-layouts/src/component.ts @@ -1,19 +1,95 @@ +import * as solid from "solid-js"; import { type Context, - type JSX, children as resolveChildren, createContext, createMemo, - splitProps, useContext, } from "solid-js"; -import { Dynamic, createComponent } from "solid-js/web"; import type { ComponentDefaults, UIConfig } from "./defaults.js"; import { globalDefaultsFor } from "./defaults.js"; import { __nextInstance, __slotId } from "./ids.js"; import type { Recipe } from "./recipe.js"; +import { Dynamic, type JSX, createComponent } from "./renderer.js"; import type { PropsOf, SlotAttrs, SlotsOf, StateOf } from "./types.js"; +/** + * `props` without `keys`, still tracked. + * + * 1.9 spells this `splitProps(props, keys)[1]`. 2.0 renamed it to `omit`, made + * it variadic, and took `splitProps` away. + * + * Detected from the module object rather than configured, and resolved once at + * load rather than branched per call. Which one is there is a fact about the + * `solid-js` that got installed, and no build flag can be more right about that + * than the module itself; a flag can only disagree with it. + */ +type Solid1Props = { + splitProps( + props: Record, + keys: string[], + ): [Record, Record]; +}; +type Solid2Props = { + omit( + props: Record, + ...keys: string[] + ): Record; +}; + +const rest: ( + props: Record, + keys: readonly string[], +) => Record = + "omit" in solid + ? (props, keys) => + (solid as unknown as Solid2Props).omit(props, ...keys) + : (props, keys) => + (solid as unknown as Solid1Props).splitProps(props, keys as string[])[1]; + +/** + * The component that provides a context's value. + * + * 1.9 hangs it off the context as `.Provider`. In 2.0 the context *is* the + * provider and `.Provider` is gone, so asking for it and falling back covers + * both without knowing which is running. + */ +type Provider = (props: { value: T; children: JSX.Element }) => JSX.Element; + +const providerOf = (context: unknown): Provider => + ((context as { Provider?: unknown }).Provider ?? context) as Provider; + +/** The four props every consumer may set, whatever the component declares. */ +const ESCAPE_KEYS = ["class", "className", "style", "children"] as const; + +/** + * The half of `splitProps` neither major ships: both give back a remainder, + * neither gives back a subset. + * + * Each bucket is a plain object of getters over `props`, which is what + * `splitProps` returned anyway, so reads stay tracked at the point of use. + * + * A key absent from `props` is skipped rather than defined as `undefined`, + * matching `splitProps`. `setup` receives the behaviour bucket, and some read + * their own keys with `in` or `Object.keys`, which a defined-but-undefined key + * would answer wrongly. + */ +function pick( + props: Record, + keys: readonly string[], +): Record { + const out: Record = {}; + for (const key of keys) { + if (!(key in props)) continue; + Object.defineProperty(out, key, { + get: () => props[key], + enumerable: true, + configurable: true, + }); + } + return out; +} + /** * What a layout receives. * @@ -37,23 +113,34 @@ export type Layout> = ( props: PropsOf & StateOf & Model & Record, ) => JSX.Element; -/** Subtree-scoped defaults. The layer `configureUI` cannot express. */ -const UIDefaultsContext = createContext(); +/** + * Subtree-scoped defaults. The layer `configureUI` cannot express. + * + * The empty default is load-bearing under 2.0, where `useContext` throws + * `ContextNotFoundError` instead of returning `undefined` when nothing above + * has provided a value. Every component built here reads this context and most + * trees never mount a ``, so a default-less context would make an + * unconfigured application throw on its first render. 1.9 reads the same + * default the same way, so this is one spelling rather than a branch. + */ +const UIDefaultsContext = createContext({}); export function UIDefaults( props: { children: JSX.Element } & UIConfig, ): JSX.Element { - const [, overrides] = splitProps(props, ["children"]); + const overrides = rest(props as unknown as Record, [ + "children", + ]); const inherited = useContext(UIDefaultsContext); // Merged with anything above rather than replacing it, so nesting two // providers configuring different components composes instead of clobbering. const value = createMemo(() => ({ - ...(inherited ?? {}), + ...inherited, ...(overrides as UIConfig), })); - return createComponent(UIDefaultsContext.Provider, { + return createComponent(providerOf(UIDefaultsContext), { get value() { return value(); }, @@ -145,6 +232,29 @@ export function defineComponent< /** The slot this component renders as itself. See `slot` on the config. */ const rootSlot = (config.slot ?? "root") as string; + // Which bucket each declared key belongs to, decided once per component + // rather than once per render. `splitProps` made the buckets disjoint by + // construction: a key named in two lists landed in the earlier one only. + // Picking by name does not, so first claim wins here instead. The order + // decides real cases - a recipe that declares `size` and a component whose + // logic also wants `size` must not hand the logic a prop the presentation + // cascade has already resolved. + const claimed = new Set(); + const claim = (keys: readonly string[]): string[] => { + const own: string[] = []; + for (const key of keys) { + if (claimed.has(key)) continue; + claimed.add(key); + own.push(key); + } + return own; + }; + const presentationOwn = claim(presentationKeys); + const escapeOwn = claim(ESCAPE_KEYS); + const behaviourOwn = claim(behaviourKeys); + /** Everything routed somewhere. What is left over is plain HTML. */ + const routedKeys = [...presentationOwn, ...escapeOwn, ...behaviourOwn]; + const compiled = recipe.config._layouts; return function LayoutComponent(outer: ComponentProps) { @@ -177,12 +287,14 @@ export function defineComponent< // The fourth bucket is not optional. Without it `id`, `onClick`, // `aria-label` and `data-testid` were swallowed as behaviour and never // reached the DOM at all. - const [presentation, escape, behaviour, passthrough] = splitProps( - props, - presentationKeys, - ["class", "className", "style", "children"], - behaviourKeys as string[], - ); + // + // One `splitProps` call used to return all four. Solid 2 has only the + // remainder half of it, so the three routed buckets are picked by name and + // the fourth still falls out of one call, as before. + const presentation = pick(props, presentationOwn); + const escape = pick(props, escapeOwn); + const behaviour = pick(props, behaviourOwn); + const passthrough = rest(props, routedKeys); // `slotId` reaches the logic as well as the layout: an accordion item has // to hand its trigger's id to `aria-controls` on the panel, and that is a @@ -201,7 +313,7 @@ export function defineComponent< const atCallSite = (presentation as Record)[key]; if (atCallSite !== undefined) return atCallSite; return ( - subtree?.[componentName]?.[key] ?? + subtree[componentName]?.[key] ?? globalDefaultsFor(componentName)?.[key] ?? config.defaults?.[key] ?? // The recipe's own, so a default can live beside the axis it defaults @@ -345,11 +457,16 @@ export function defineComponent< ...spreadable(() => resolved()[rootSlot] as SlotAttrs), }); - if (!provide) return rendered; + // Nothing to provide means no wrapper, and under 2.0 that is a correctness + // rule rather than an optimisation: a provider counts as having provided + // even when its value is `undefined`, and `useContext` throws on an + // undefined value, so an empty provider would shadow a real one above it + // with a throw. Under 1.9 the two spellings are indistinguishable. + if (!provide || !model || !("context" in model)) return rendered; - return createComponent(provide.Provider, { + return createComponent(providerOf(provide), { get value() { - return model?.context; + return model.context; }, get children() { return rendered; diff --git a/packages/solid-layouts/src/renderer.solid-2.ts b/packages/solid-layouts/src/renderer.solid-2.ts new file mode 100644 index 0000000..a22cd21 --- /dev/null +++ b/packages/solid-layouts/src/renderer.solid-2.ts @@ -0,0 +1,16 @@ +/** + * The renderer, for Solid 2.0. See `renderer.ts` for why this file is the only + * one that has a twin. + * + * `Dynamic` survived the major with the same props, and `createComponent` is + * re-exported by `@solidjs/web` from `solid-js`, so the two names this module + * carries are the same two names its 1.9 twin carries. The `JSX` namespace did + * not survive: `solid-js` no longer declares one, because the shape of an + * element is the renderer's business, so it comes from `@solidjs/web` here. + * + * The build swaps this file in as `renderer.ts` when emitting the + * `solid-layouts/solid-2` entry. It is never part of the 1.9 output, which is + * what lets it import a package a 1.9 consumer has no reason to install. + */ +export type { JSX } from "@solidjs/web"; +export { Dynamic, createComponent } from "@solidjs/web"; diff --git a/packages/solid-layouts/src/renderer.ts b/packages/solid-layouts/src/renderer.ts new file mode 100644 index 0000000..e69e7e2 --- /dev/null +++ b/packages/solid-layouts/src/renderer.ts @@ -0,0 +1,20 @@ +/** + * The renderer, for Solid 1.9. Its Solid 2.0 twin is `renderer.solid-2.ts`. + * + * This file exists because it is the *only* thing in the runtime that a major + * of Solid can move rather than rename. Everything else either kept its name + * and module (`createContext`, `useContext`, `createMemo`, `children`, the + * `Context` type) or can be told apart at runtime from the module object + * itself, which is what `component.ts` does for `splitProps` / `omit` and for + * `Context.Provider`. Rendering cannot: 1.9 puts `Dynamic` and + * `createComponent` under `solid-js/web`, 2.0 moved them to `@solidjs/web` and + * dropped the `solid-js/web` subpath entirely, so no single import statement + * resolves under both. + * + * The build therefore emits the same sources twice, swapping this file for its + * twin: `solid-layouts` is the 1.9 package entry, `solid-layouts/solid-2` the + * 2.0 one. This module is the only difference between the two outputs - + * `dist/component.js` and `dist/solid-2/component.js` are identical files. + */ +export type { JSX } from "solid-js"; +export { Dynamic, createComponent } from "solid-js/web"; diff --git a/packages/solid-layouts/tsconfig.build.json b/packages/solid-layouts/tsconfig.build.json index dcee450..afd5d19 100644 --- a/packages/solid-layouts/tsconfig.build.json +++ b/packages/solid-layouts/tsconfig.build.json @@ -9,5 +9,9 @@ "rootDir": "src" }, "include": ["src"], - "exclude": ["src/**/*.test.ts", "src/__parity__"] + "exclude": [ + "src/**/*.test.ts", + "src/__parity__", + "src/renderer.solid-2.ts" + ] } diff --git a/packages/solid-layouts/tsconfig.json b/packages/solid-layouts/tsconfig.json index 2ceaa88..fbd99c5 100644 --- a/packages/solid-layouts/tsconfig.json +++ b/packages/solid-layouts/tsconfig.json @@ -12,5 +12,9 @@ "skipLibCheck": true, "types": ["bun"] }, - "include": ["src"] + "include": ["src"], + // The Solid 2.0 renderer is checked by `tsconfig.solid-2.json`, against the + // Solid it targets. Checking it here would mean checking `@solidjs/web` + // against 1.9, which answers nothing. + "exclude": ["src/renderer.solid-2.ts"] } diff --git a/packages/solid-layouts/tsconfig.solid-2.json b/packages/solid-layouts/tsconfig.solid-2.json new file mode 100644 index 0000000..49c08e5 --- /dev/null +++ b/packages/solid-layouts/tsconfig.solid-2.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "declarationMap": true, + "outDir": "dist/solid-2", + "rootDir": "solid-2-staging", + "jsxImportSource": "@solidjs/web" + }, + "include": ["solid-2-staging"], + "exclude": ["solid-2-staging/**/*.test.ts", "solid-2-staging/__parity__"] +}