Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/vite-3/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { onCleanup, onMount } from "solid-js";
import { CounterProvider, useCounter } from "./CounterContext";
import { Label } from "./Label.jsx";

const title = 'Count';

Expand Down Expand Up @@ -56,6 +57,7 @@ export default function App() {

return (
<CounterProvider>
<Label text="solid" />
<Count />
<Increment />
<Decrement />
Expand Down
3 changes: 3 additions & 0 deletions examples/vite-3/src/Label.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function Label(props) {
return <span data-testid="label">{props.text}</span>;
}
2 changes: 2 additions & 0 deletions examples/vite-4/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { onCleanup, onMount } from "solid-js";
import { CounterProvider, useCounter } from "./CounterContext";
import { Label } from "./Label.jsx";

const title = 'Count';

Expand Down Expand Up @@ -56,6 +57,7 @@ export default function App() {

return (
<CounterProvider>
<Label text="solid" />
<Count />
<Increment />
<Decrement />
Expand Down
3 changes: 3 additions & 0 deletions examples/vite-4/src/Label.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function Label(props) {
return <span data-testid="label">{props.text}</span>;
}
2 changes: 2 additions & 0 deletions examples/vite-5/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { onCleanup, onMount } from "solid-js";
import { CounterProvider, useCounter } from "./CounterContext";
import { Label } from "./Label.jsx";

const title = 'Count';

Expand Down Expand Up @@ -56,6 +57,7 @@ export default function App() {

return (
<CounterProvider>
<Label text="solid" />
<Count />
<Increment />
<Decrement />
Expand Down
3 changes: 3 additions & 0 deletions examples/vite-5/src/Label.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function Label(props) {
return <span data-testid="label">{props.text}</span>;
}
2 changes: 2 additions & 0 deletions examples/vite-6/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { onCleanup, onMount } from "solid-js";
import { CounterProvider, useCounter } from "./CounterContext";
import { Label } from "./Label.jsx";

const title = 'Count';

Expand Down Expand Up @@ -56,6 +57,7 @@ export default function App() {

return (
<CounterProvider>
<Label text="solid" />
<Count />
<Increment />
<Decrement />
Expand Down
3 changes: 3 additions & 0 deletions examples/vite-6/src/Label.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function Label(props) {
return <span data-testid="label">{props.text}</span>;
}
4 changes: 4 additions & 0 deletions examples/vite-6/tests/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import App from '../src/App.jsx';
test('App', async () => {
const root = page.elementLocator(render(() => <App />).baseElement);

// Label is a .jsx component — verifies the plugin processes .jsx files correctly
const label = root.getByTestId('label');
await expect.element(label).toHaveTextContent('solid');

const count = root.getByText('Count:');
await expect.element(count).toHaveTextContent('Count: 0');

Expand Down
2 changes: 2 additions & 0 deletions examples/vite-7/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { onCleanup, onMount } from "solid-js";
import { CounterProvider, useCounter } from "./CounterContext";
import { Label } from "./Label.jsx";

const title = 'Count';

Expand Down Expand Up @@ -56,6 +57,7 @@ export default function App() {

return (
<CounterProvider>
<Label text="solid" />
<Count />
<Increment />
<Decrement />
Expand Down
3 changes: 3 additions & 0 deletions examples/vite-7/src/Label.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function Label(props) {
return <span data-testid="label">{props.text}</span>;
}
4 changes: 4 additions & 0 deletions examples/vite-7/tests/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import App from '../src/App.jsx';
test('App', async () => {
const root = page.elementLocator(render(() => <App />).baseElement);

// Label is a .jsx component — verifies the plugin processes .jsx files correctly
const label = root.getByTestId('label');
await expect.element(label).toHaveTextContent('solid');

const count = root.getByText('Count:');
await expect.element(count).toHaveTextContent('Count: 0');

Expand Down
2 changes: 2 additions & 0 deletions examples/vite-8/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { onCleanup, onMount } from "solid-js";
import { CounterProvider, useCounter } from "./CounterContext";
import { Label } from "./Label.jsx";

const title = 'Count';

Expand Down Expand Up @@ -56,6 +57,7 @@ export default function App() {

return (
<CounterProvider>
<Label text="solid" />
<Count />
<Increment />
<Decrement />
Expand Down
3 changes: 3 additions & 0 deletions examples/vite-8/src/Label.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function Label(props) {
return <span data-testid="label">{props.text}</span>;
}
4 changes: 4 additions & 0 deletions examples/vite-8/tests/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import App from '../src/App.jsx';
test('App', async () => {
const root = page.elementLocator(render(() => <App />).baseElement);

// Label is a .jsx component — verifies the plugin processes .jsx files correctly
const label = root.getByTestId('label');
await expect.element(label).toHaveTextContent('solid');

const count = root.getByText('Count:');
await expect.element(count).toHaveTextContent('Count: 0');

Expand Down
6 changes: 5 additions & 1 deletion pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
packages:
- '.'
- "examples/*"
allowBuilds:
cypress: set this to true or false
esbuild: set this to true or false
msw: set this to true or false
catalog:
"solid-js": ^1.9.4
"solid-js": ^1.9.4
183 changes: 103 additions & 80 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { crawlFrameworkPkgs } from 'vitefu';
const require = createRequire(import.meta.url);

const runtimePublicPath = '/@solid-refresh';
const runtimePublicPathFilter = /^\/@solid-refresh$/;
const runtimeFilePath = require.resolve('solid-refresh/dist/solid-refresh.mjs');
const runtimeCode = readFileSync(runtimeFilePath, 'utf-8');

Expand Down Expand Up @@ -332,103 +333,125 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin {
needHmr = config.command === 'serve' && config.mode !== 'production' && options.hot !== false;
},

resolveId(id) {
if (id === runtimePublicPath) return id;
resolveId: {
// Filter is used by Rolldown and newer Rollup/Vite versions (Vite >= 6.3.0) to skip
// unnecessary Rust-to-JS calls. The inner check is kept for backward compatibility
// with older versions that ignore hook filters and call the handler for every module.
// @ts-ignore filter is not yet in Vite's type definitions but is supported at runtime
filter: { id: runtimePublicPathFilter },
handler(id) {
if (id === runtimePublicPath) return id;
},
},

load(id) {
if (id === runtimePublicPath) return runtimeCode;
load: {
// Same backward-compatibility strategy as resolveId above.
// @ts-ignore filter is not yet in Vite's type definitions but is supported at runtime
filter: { id: runtimePublicPathFilter },
handler(id) {
if (id === runtimePublicPath) return runtimeCode;
},
},

async transform(source, id, transformOptions) {
const isSsr = this.environment
? this.environment.config.consumer === 'server'
: Boolean(transformOptions?.ssr);
const currentFileExtension = getExtension(id);

const extensionsToWatch = options.extensions || [];
const allExtensions = extensionsToWatch.map((extension) =>
// An extension can be a string or a tuple [extension, options]
typeof extension === 'string' ? extension : extension[0],
);

if (!filter(id)) {
return null;
}
transform: {
// Limit invocations to files that could contain Solid JSX. Custom extensions
// (options.extensions) cannot be represented as a static regex, so they are
// still handled by the inner extension check below — this filter covers the
// common case and is the source of the largest performance win.
// The inner checks (filter(id) / extension regex) are kept as-is so that the
// hook behaves correctly on older Vite/Rollup versions that ignore hook filters.
// @ts-ignore filter is not yet in Vite's type definitions but is supported at runtime
filter: { id: /\.[mc]?[tj]sx$/i },
async handler(source, id, transformOptions) {
const isSsr = this.environment
? this.environment.config.consumer === 'server'
: Boolean(transformOptions?.ssr);
const currentFileExtension = getExtension(id);

const extensionsToWatch = options.extensions || [];
const allExtensions = extensionsToWatch.map((extension) =>
// An extension can be a string or a tuple [extension, options]
typeof extension === 'string' ? extension : extension[0],
);

if (!filter(id)) {
return null;
}

id = id.replace(/\?.*$/, '');
id = id.replace(/\?.*$/, '');

if (!(/\.[mc]?[tj]sx$/i.test(id) || allExtensions.includes(currentFileExtension))) {
return null;
}
if (!(/\.[mc]?[tj]sx$/i.test(id) || allExtensions.includes(currentFileExtension))) {
return null;
}

const inNodeModules = /node_modules/.test(id);
const inNodeModules = /node_modules/.test(id);

let solidOptions: { generate: 'ssr' | 'dom'; hydratable: boolean };
let solidOptions: { generate: 'ssr' | 'dom'; hydratable: boolean };

if (options.ssr) {
if (isSsr) {
solidOptions = { generate: 'ssr', hydratable: true };
if (options.ssr) {
if (isSsr) {
solidOptions = { generate: 'ssr', hydratable: true };
} else {
solidOptions = { generate: 'dom', hydratable: true };
}
} else {
solidOptions = { generate: 'dom', hydratable: true };
solidOptions = { generate: 'dom', hydratable: false };
}
} else {
solidOptions = { generate: 'dom', hydratable: false };
}

// We need to know if the current file extension has a typescript options tied to it
const shouldBeProcessedWithTypescript =
/\.[mc]?tsx$/i.test(id) ||
extensionsToWatch.some((extension) => {
if (typeof extension === 'string') {
return extension.includes('tsx');
}

const [extensionName, extensionOptions] = extension;
if (extensionName !== currentFileExtension) return false;

return extensionOptions.typescript;
});
const plugins: NonNullable<NonNullable<babel.TransformOptions['parserOpts']>['plugins']> = [
'jsx',
];

if (shouldBeProcessedWithTypescript) {
plugins.push('typescript');
}

const opts: babel.TransformOptions = {
root: projectRoot,
filename: id,
sourceFileName: id,
presets: [[solid, { ...solidOptions, ...(options.solid || {}) }]],
plugins: needHmr && !isSsr && !inNodeModules ? [[solidRefresh, { bundler: 'vite' }]] : [],
ast: false,
sourceMaps: true,
configFile: false,
babelrc: false,
parserOpts: {
plugins,
},
};

// Default value for babel user options
let babelUserOptions: babel.TransformOptions = {};
// We need to know if the current file extension has a typescript options tied to it
const shouldBeProcessedWithTypescript =
/\.[mc]?tsx$/i.test(id) ||
extensionsToWatch.some((extension) => {
if (typeof extension === 'string') {
return extension.includes('tsx');
}

const [extensionName, extensionOptions] = extension;
if (extensionName !== currentFileExtension) return false;

return extensionOptions.typescript;
});
const plugins: NonNullable<NonNullable<babel.TransformOptions['parserOpts']>['plugins']> = [
'jsx',
];

if (shouldBeProcessedWithTypescript) {
plugins.push('typescript');
}

if (options.babel) {
if (typeof options.babel === 'function') {
const babelOptions = options.babel(source, id, isSsr);
babelUserOptions = babelOptions instanceof Promise ? await babelOptions : babelOptions;
} else {
babelUserOptions = options.babel;
const opts: babel.TransformOptions = {
root: projectRoot,
filename: id,
sourceFileName: id,
presets: [[solid, { ...solidOptions, ...(options.solid || {}) }]],
plugins: needHmr && !isSsr && !inNodeModules ? [[solidRefresh, { bundler: 'vite' }]] : [],
ast: false,
sourceMaps: true,
configFile: false,
babelrc: false,
parserOpts: {
plugins,
},
};

// Default value for babel user options
let babelUserOptions: babel.TransformOptions = {};

if (options.babel) {
if (typeof options.babel === 'function') {
const babelOptions = options.babel(source, id, isSsr);
babelUserOptions = babelOptions instanceof Promise ? await babelOptions : babelOptions;
} else {
babelUserOptions = options.babel;
}
}
}

const babelOptions = mergeAndConcat(babelUserOptions, opts) as babel.TransformOptions;
const babelOptions = mergeAndConcat(babelUserOptions, opts) as babel.TransformOptions;

const { code, map } = await babel.transformAsync(source, babelOptions);
const { code, map } = await babel.transformAsync(source, babelOptions);

return { code, map };
return { code, map };
},
},
};
}
Expand Down