Skip to content

Commit 5c23a22

Browse files
Add plugin to fix fast refresh with vite 7
1 parent c2878d5 commit 5c23a22

10 files changed

Lines changed: 464 additions & 11 deletions

File tree

‎package-lock.json‎

Lines changed: 19 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"scripts": {
77
"start": "node src/Main.res.mjs",
88
"prepack": "rescript && rollup -c",
9-
"test": "rescript && node --test test/*Test.res.mjs",
9+
"test": "rescript && node --test test/*Test.res.mjs test/templates/*/*Test.res.mjs",
1010
"format": "rescript format",
1111
"dev": "rescript -w"
1212
},
@@ -42,6 +42,7 @@
4242
"@rollup/plugin-node-resolve": "^16.0.3",
4343
"@rollup/plugin-terser": "^0.4.4",
4444
"compare-versions": "^6.1.0",
45+
"prettier": "^3.8.3",
4546
"rescript": "^12.0.0",
4647
"rollup": "^4.53.3"
4748
}

‎rescript.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
"dir": "src",
66
"subdirs": true
77
},
8+
{
9+
"dir": "src-fast-refresh-plugin",
10+
"subdirs": true
11+
},
812
{
913
"dir": "test",
1014
"subdirs": true,

‎rollup.config.mjs‎

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,48 @@
11
import { nodeResolve } from "@rollup/plugin-node-resolve";
22
import commonjs from "@rollup/plugin-commonjs";
33
import terser from "@rollup/plugin-terser";
4+
import prettier from "prettier";
45

5-
export default {
6-
input: `src/Main.res.mjs`,
7-
output: {
8-
file: `out/create-rescript-app.cjs`,
9-
format: "cjs",
10-
banner: "#!/usr/bin/env node",
6+
function formatViteTemplateConfig() {
7+
return {
8+
name: "format-vite-template-config",
9+
async renderChunk(code, chunk) {
10+
if (chunk.fileName !== "vite.config.js") {
11+
return null;
12+
}
13+
14+
let formatted = code
15+
.replace(/\/\/ Generated by ReScript, PLEASE EDIT WITH CARE\n\s*/g, "")
16+
.replace(/\n\/\* No side effect \*\/\n/g, "\n")
17+
.replace(/\nvar ViteConfig = defineConfig\(/, "\nexport default defineConfig(")
18+
.replace(/\n\nexport \{ ViteConfig as default \};\s*$/, "\n")
19+
.replace(/from '([^']+)';/g, 'from "$1";');
20+
21+
return await prettier.format(formatted, {
22+
parser: "babel",
23+
printWidth: 100,
24+
});
25+
},
26+
};
27+
}
28+
29+
export default [
30+
{
31+
input: `src/Main.res.mjs`,
32+
output: {
33+
file: `out/create-rescript-app.cjs`,
34+
format: "cjs",
35+
banner: "#!/usr/bin/env node",
36+
},
37+
plugins: [terser(), nodeResolve({ preferBuiltins: true }), commonjs()],
38+
},
39+
{
40+
input: `src-fast-refresh-plugin/ViteConfig.mjs`,
41+
output: {
42+
file: `templates/rescript-template-vite/vite.config.js`,
43+
format: "esm",
44+
},
45+
external: ["vite", "@vitejs/plugin-react", "@tailwindcss/vite"],
46+
plugins: [nodeResolve(), commonjs(), formatViteTemplateConfig()],
1147
},
12-
plugins: [terser(), nodeResolve({ preferBuiltins: true }), commonjs()],
13-
};
48+
];
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// This module is bundled into the Vite template config, so keep the generated JavaScript
2+
// as close to runtime-free as practical.
3+
type transformResult = {
4+
code: string,
5+
map: Null.t<unit>,
6+
}
7+
8+
type hotUpdateContext = {
9+
file: string,
10+
read: unit => promise<string>,
11+
}
12+
13+
type plugin = {
14+
name: string,
15+
enforce: string,
16+
handleHotUpdate: hotUpdateContext => promise<Nullable.t<array<string>>>,
17+
transform: (string, string) => Nullable.t<transformResult>,
18+
}
19+
20+
module JsArray = {
21+
@get external length: array<'a> => int = "length"
22+
@get_index external unsafeGet: (array<'a>, int) => 'a = ""
23+
@send external filter: (array<'a>, 'a => bool) => array<'a> = "filter"
24+
@send external map: (array<'a>, 'a => 'b) => array<'b> = "map"
25+
@send external join: (array<string>, string) => string = "join"
26+
}
27+
28+
@send external splitByRegExp: (string, RegExp.t) => array<string> = "split"
29+
30+
let rescriptReactFastRefresh = () => {
31+
let previousCode = Dict.make()
32+
33+
let componentName = id => {
34+
let pathParts = id->String.split("/")
35+
36+
if pathParts->JsArray.length === 0 {
37+
Nullable.undefined
38+
} else {
39+
let fileName = pathParts->JsArray.unsafeGet(pathParts->JsArray.length - 1)
40+
let fileName = fileName->String.replaceRegExp(/\.res\.mjs$/, "")
41+
42+
if fileName->String.length === 0 {
43+
Nullable.undefined
44+
} else {
45+
let name =
46+
fileName
47+
->splitByRegExp(/[^A-Za-z0-9_$]+/)
48+
->JsArray.filter(part => part->String.length > 0)
49+
->JsArray.map(part =>
50+
part->String.charAt(0)->String.toUpperCase ++ part->String.slice(~start=1)
51+
)
52+
->JsArray.join("")
53+
54+
Nullable.make(/^[A-Za-z_$]/->RegExp.test(name) ? name : `ReScript${name}`)
55+
}
56+
}
57+
}
58+
59+
{
60+
name: "rescript-react-fast-refresh",
61+
enforce: "pre",
62+
handleHotUpdate: async ({file, read}) => {
63+
if file->String.endsWith(".res") || file->String.endsWith(".resi") {
64+
Nullable.make([])
65+
} else if !(file->String.endsWith(".res.mjs")) {
66+
Nullable.undefined
67+
} else {
68+
let code = await read()
69+
let previous = previousCode->Dict.get(file)
70+
previousCode->Dict.set(file, code)
71+
72+
switch previous {
73+
| Some(previous) if previous === code => Nullable.make([])
74+
| _ => Nullable.undefined
75+
}
76+
}
77+
},
78+
transform: (code, id) => {
79+
let idParts = id->String.split("?")
80+
let file = idParts->JsArray.unsafeGet(0)
81+
82+
if !(file->String.endsWith(".res.mjs")) {
83+
Nullable.undefined
84+
} else {
85+
previousCode->Dict.set(file, code)
86+
87+
if (
88+
!(
89+
code->String.includes("react/jsx-runtime") &&
90+
!(code->String.includes("react-dom/client")) &&
91+
/\bfunction\s+make\s*\(/->RegExp.test(code) &&
92+
/\bexport\s*\{[\s\S]*\bmake\b[\s\S]*\}/m->RegExp.test(code)
93+
)
94+
) {
95+
Nullable.undefined
96+
} else {
97+
switch componentName(file) {
98+
| Null | Undefined | Value("Main") => Nullable.undefined
99+
| Value(name) =>
100+
Nullable.make({
101+
code: code
102+
->String.replaceRegExp(/\bfunction\s+make\s*\(/, `function ${name}(`)
103+
->String.replaceRegExp(/\nexport\s*\{/, `\nlet make = ${name};\n\nexport {`),
104+
map: Null.null,
105+
})
106+
}
107+
}
108+
}
109+
},
110+
}
111+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
type transformResult = {
2+
code: string,
3+
map: Null.t<unit>,
4+
}
5+
6+
type hotUpdateContext = {
7+
file: string,
8+
read: unit => promise<string>,
9+
}
10+
11+
type plugin = {
12+
name: string,
13+
enforce: string,
14+
handleHotUpdate: hotUpdateContext => promise<Nullable.t<array<string>>>,
15+
transform: (string, string) => Nullable.t<transformResult>,
16+
}
17+
18+
let rescriptReactFastRefresh: unit => plugin
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { defineConfig } from "vite";
2+
import react from "@vitejs/plugin-react";
3+
import tailwindcss from "@tailwindcss/vite";
4+
import { rescriptReactFastRefresh } from "./RescriptReactFastRefresh.res.mjs";
5+
6+
// https://vitejs.dev/config/
7+
export default defineConfig({
8+
plugins: [
9+
tailwindcss(),
10+
rescriptReactFastRefresh(),
11+
react({
12+
include: ["**/*.res.mjs"],
13+
}),
14+
],
15+
server: {
16+
watch: {
17+
// We ignore ReScript build artifacts to avoid unnecessarily triggering HMR on incremental compilation
18+
ignored: ["**/lib/bs/**", "**/lib/ocaml/**", "**/lib/rescript.lock"],
19+
},
20+
},
21+
});

‎src/bindings/Node.res‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ module Test = {
7777

7878
@module("node:test")
7979
external test: (string, unit => unit) => unit = "test"
80+
81+
@module("node:test")
82+
external testAsync: (string, unit => promise<unit>) => unit = "test"
8083
}
8184

8285
module Url = {

‎templates/rescript-template-vite/vite.config.js‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,84 @@ import { defineConfig } from "vite";
22
import react from "@vitejs/plugin-react";
33
import tailwindcss from "@tailwindcss/vite";
44

5+
function rescriptReactFastRefresh() {
6+
let previousCode = {};
7+
let componentName = (id) => {
8+
let pathParts = id.split("/");
9+
if (pathParts.length === 0) {
10+
return;
11+
}
12+
let fileName = pathParts[(pathParts.length - 1) | 0];
13+
let fileName$1 = fileName.replace(/\.res\.mjs$/, "");
14+
if (fileName$1.length === 0) {
15+
return;
16+
}
17+
let name = fileName$1
18+
.split(/[^A-Za-z0-9_$]+/)
19+
.filter((part) => part.length > 0)
20+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
21+
.join("");
22+
if (/^[A-Za-z_$]/.test(name)) {
23+
return name;
24+
} else {
25+
return `ReScript` + name;
26+
}
27+
};
28+
return {
29+
name: "rescript-react-fast-refresh",
30+
enforce: "pre",
31+
handleHotUpdate: async (param) => {
32+
let file = param.file;
33+
if (file.endsWith(".res") || file.endsWith(".resi")) {
34+
return [];
35+
}
36+
if (!file.endsWith(".res.mjs")) {
37+
return;
38+
}
39+
let code = await param.read();
40+
let previous = previousCode[file];
41+
previousCode[file] = code;
42+
if (previous !== undefined && previous === code) {
43+
return [];
44+
}
45+
},
46+
transform: (code, id) => {
47+
let idParts = id.split("?");
48+
let file = idParts[0];
49+
if (!file.endsWith(".res.mjs")) {
50+
return;
51+
}
52+
previousCode[file] = code;
53+
if (
54+
!(
55+
code.includes("react/jsx-runtime") &&
56+
!code.includes("react-dom/client") &&
57+
/\bfunction\s+make\s*\(/.test(code) &&
58+
/\bexport\s*\{[\s\S]*\bmake\b[\s\S]*\}/m.test(code)
59+
)
60+
) {
61+
return;
62+
}
63+
let name = componentName(file);
64+
if (name == null || name === "Main") {
65+
return;
66+
} else {
67+
return {
68+
code: code
69+
.replace(/\bfunction\s+make\s*\(/, `function ` + name + `(`)
70+
.replace(/\nexport\s*\{/, `\nlet make = ` + name + `;\n\nexport {`),
71+
map: null,
72+
};
73+
}
74+
},
75+
};
76+
}
77+
578
// https://vitejs.dev/config/
679
export default defineConfig({
780
plugins: [
881
tailwindcss(),
82+
rescriptReactFastRefresh(),
983
react({
1084
include: ["**/*.res.mjs"],
1185
}),

0 commit comments

Comments
 (0)