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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules/
dist/
.vscode/
bun.lock
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@ yarn add -D solid-refresh
pnpm add -D solid-refresh
```

```bash
bun add -D solid-refresh
```

This project aims to provide HMR for Solid for various bundlers. It comes with a babel plugin and a runtime. Over time I hope to add different bundlers. Today it supports:

* Vite (with option `bundler: "vite"`)
* Bun (with option `bundler: "bun"`)
* Snowpack (with option `bundler: "esm"`)
* Webpack (for strict ESM, use option `bundler: "webpack5"`)
* Nollup
Expand All @@ -29,6 +34,29 @@ This project aims to provide HMR for Solid for various bundlers. It comes with a

`solid-refresh` is already built into [`vite-plugin-solid`](https://github.com/solidjs/vite-plugin-solid).

### Bun

When using Bun's built-in development server or bundler, add the following to `.babelrc`:

```json
{
"env": {
"development": {
"plugins": [["solid-refresh/babel", {
"bundler": "bun"
}]]
}
}
}
```

> [!NOTE]
> Bun requires direct calls to `import.meta.hot.*` methods and does not support passing `import.meta.hot` as an argument to functions. The `"bun"` bundler option generates inline HMR code that is compatible with Bun's requirements.
>
> Bun's HMR API does not currently support `import.meta.hot.invalidate()`. When a component cannot be hot-reloaded (e.g., due to structural changes), the page will perform a full reload using `window.location.reload()`.

For more information about Bun's HMR implementation, see the [Bun HMR documentation](https://bun.com/docs/bundler/hot-reloading).

### Webpack & Rspack

You can read the following guides first, respectively:
Expand Down
6 changes: 6 additions & 0 deletions src/babel/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ export const IMPORT_DECLINE: ImportDefinition = {
source: SOLID_REFRESH_MODULE,
};

export const IMPORT_PATCH_REGISTRY: ImportDefinition = {
kind: 'named',
name: '$$patchRegistry',
source: SOLID_REFRESH_MODULE,
};

export const IMPORT_SPECIFIERS: ImportIdentifierSpecifier[] = [
{
type: 'render',
Expand Down
116 changes: 99 additions & 17 deletions src/babel/core/create-registry.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,85 @@
import type * as babel from '@babel/core';
import * as t from '@babel/types';
import { IMPORT_REFRESH, IMPORT_REGISTRY } from './constants';
import {
IMPORT_PATCH_REGISTRY,
IMPORT_REFRESH,
IMPORT_REGISTRY,
} from './constants';
import { getHotIdentifier } from './get-hot-identifier';
import { getImportIdentifier } from './get-import-identifier';
import { getRootStatementPath } from './get-root-statement-path';
import type { StateContext } from './types';

const REGISTRY = 'REGISTRY';
const SOLID_REFRESH = 'solid-refresh';
const SOLID_REFRESH_PREV = 'solid-refresh-prev';

function createBunInlineHMR(
state: StateContext,
path: babel.NodePath,
registryId: t.Identifier,
): t.Statement[] {
const hotMeta = getHotIdentifier(state);
const patchRegistryId = getImportIdentifier(
state,
path,
IMPORT_PATCH_REGISTRY,
);
const hotData = t.memberExpression(hotMeta, t.identifier('data'));
const hotDataRefresh = t.memberExpression(
hotData,
t.stringLiteral(SOLID_REFRESH),
true,
);
const hotDataPrev = t.memberExpression(
hotData,
t.stringLiteral(SOLID_REFRESH_PREV),
true,
);
const assignRefresh = t.expressionStatement(
t.assignmentExpression(
'=',
hotDataRefresh,
t.logicalExpression('||', hotDataRefresh, registryId),
),
);
const assignPrev = t.expressionStatement(
t.assignmentExpression('=', hotDataPrev, registryId),
);
const modParam = t.identifier('mod');
const acceptCallback = t.arrowFunctionExpression(
[modParam],
t.blockStatement([
t.ifStatement(
t.logicalExpression(
'||',
t.binaryExpression('==', modParam, t.nullLiteral()),
t.callExpression(patchRegistryId, [hotDataRefresh, hotDataPrev]),
),
t.blockStatement([
t.expressionStatement(
t.callExpression(
t.memberExpression(
t.memberExpression(
t.identifier('window'),
t.identifier('location'),
),
t.identifier('reload'),
),
[],
),
),
]),
),
]),
);
const acceptCall = t.expressionStatement(
t.callExpression(t.memberExpression(hotMeta, t.identifier('accept')), [
acceptCallback,
]),
);
return [assignRefresh, assignPrev, acceptCall];
}

export function createRegistry(
state: StateContext,
Expand All @@ -33,22 +106,31 @@ export function createRegistry(
)[0],
);
const pathToHot = getHotIdentifier(state);
(
path.scope.getProgramParent().path as babel.NodePath<t.Program>
).pushContainer('body', [
t.ifStatement(
pathToHot,
t.blockStatement([
t.expressionStatement(
t.callExpression(getImportIdentifier(state, path, IMPORT_REFRESH), [
t.stringLiteral(state.bundler),
pathToHot,
identifier,
]),
),
]),
),
]);
const programPath = path.scope.getProgramParent()
.path as babel.NodePath<t.Program>;

if (state.bundler === 'bun') {
const bunHMRStatements = createBunInlineHMR(state, path, identifier);
programPath.pushContainer('body', [
t.ifStatement(pathToHot, t.blockStatement(bunHMRStatements)),
]);
} else {
programPath.pushContainer('body', [
t.ifStatement(
pathToHot,
t.blockStatement([
t.expressionStatement(
t.callExpression(getImportIdentifier(state, path, IMPORT_REFRESH), [
t.stringLiteral(state.bundler),
pathToHot,
identifier,
]),
),
]),
),
]);
}

state.imports.set(REGISTRY, identifier);
return identifier;
}
3 changes: 2 additions & 1 deletion src/babel/core/get-hot-identifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import * as t from '@babel/types';

export function getHotIdentifier(state: StateContext): t.MemberExpression {
switch (state.bundler) {
// vite/esm uses `import.meta.hot`
// vite/esm/bun uses `import.meta.hot`
case 'esm':
case 'vite':
case 'bun':
return t.memberExpression(
t.memberExpression(t.identifier('import'), t.identifier('meta')),
t.identifier('hot'),
Expand Down
2 changes: 2 additions & 0 deletions src/runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ function patchRegistry(oldRegistry: Registry, newRegistry: Registry) {
return shouldInvalidateByComponents || shouldInvalidateByContext;
}

export const $$patchRegistry = patchRegistry;

const SOLID_REFRESH = 'solid-refresh';
const SOLID_REFRESH_PREV = 'solid-refresh-prev';

Expand Down
2 changes: 1 addition & 1 deletion src/shared/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// For HMRs that follows Snowpack's spec
// https://github.com/FredKSchott/esm-hmr
export type ESMRuntimeType = 'esm' | 'vite';
export type ESMRuntimeType = 'esm' | 'vite' | 'bun';

// For HMRs that follow Webpack's design
export type StandardRuntimeType = 'standard' | 'webpack5' | 'rspack-esm';
Expand Down
Loading