@rspack/dev-server 0.0.0-02df4322c5-20221125030212

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/config.ts ADDED
@@ -0,0 +1,68 @@
1
+ import type {
2
+ Dev,
3
+ WebSocketServerOptions,
4
+ RspackOptionsNormalized
5
+ } from "@rspack/core";
6
+ import type { WatchOptions } from "chokidar";
7
+ import path from "node:path";
8
+ import { resolveWatchOption } from "@rspack/core";
9
+
10
+ export interface ResolvedDev {
11
+ host: string;
12
+ port: number;
13
+ static: {
14
+ directory: string;
15
+ watch: false | WatchOptions;
16
+ };
17
+ devMiddleware: {};
18
+ hot: boolean;
19
+ open: boolean;
20
+ liveReload: boolean;
21
+ webSocketServer: false | WebSocketServerOptions;
22
+ }
23
+
24
+ export function resolveDevOptions(
25
+ devConfig: Dev,
26
+ compilerOptions: RspackOptionsNormalized
27
+ ): ResolvedDev {
28
+ const open = true;
29
+ const hot = devConfig.hot ?? true;
30
+ // --- static
31
+ const directory =
32
+ devConfig.static?.directory ??
33
+ path.resolve(compilerOptions.context, "dist");
34
+ let watch: false | WatchOptions = {};
35
+ if (devConfig.static?.watch === false) {
36
+ watch = false;
37
+ } else if (devConfig.static?.watch === true) {
38
+ watch = resolveWatchOption({});
39
+ } else if (devConfig.static?.watch) {
40
+ watch = devConfig.static?.watch;
41
+ }
42
+ // ---
43
+ const devMiddleware = devConfig.devMiddleware ?? {};
44
+ const liveReload = devConfig.liveReload ?? true;
45
+
46
+ let webSocketServer: false | WebSocketServerOptions = {};
47
+ if (devConfig.webSocketServer === false) {
48
+ webSocketServer = false;
49
+ } else if (devConfig.webSocketServer === true) {
50
+ webSocketServer = {};
51
+ } else if (devConfig.webSocketServer) {
52
+ webSocketServer = devConfig.webSocketServer;
53
+ }
54
+
55
+ return {
56
+ host: devConfig.host,
57
+ port: devConfig.port ? Number(devConfig.port) : undefined,
58
+ static: {
59
+ directory,
60
+ watch
61
+ },
62
+ devMiddleware,
63
+ open,
64
+ hot,
65
+ liveReload,
66
+ webSocketServer
67
+ };
68
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { RspackDevServer } from "./server";
package/src/logger.ts ADDED
@@ -0,0 +1,30 @@
1
+ export interface Logger {
2
+ error(...args: any[]): void;
3
+ warn(...args: any[]): void;
4
+ info(...args: any[]): void;
5
+ }
6
+
7
+ const COLOR = {
8
+ RESET: "\x1b[0m",
9
+ //
10
+ RED: "\x1b[31m",
11
+ GREEN: "\x1b[32m",
12
+ YELLOW: "\x1b[33m"
13
+ };
14
+
15
+ export function createLogger(name: string): Logger {
16
+ if (!name.length) {
17
+ throw Error();
18
+ }
19
+ return {
20
+ error(...msgs: any[]) {
21
+ console.log("[", name, "]:", COLOR.RED, msgs, COLOR.RESET);
22
+ },
23
+ warn(...msgs: any[]) {
24
+ console.log("[", name, "]:", COLOR.YELLOW, msgs, COLOR.RESET);
25
+ },
26
+ info(...msgs: any[]) {
27
+ console.log("[", name, "]:", COLOR.GREEN, msgs, COLOR.RESET);
28
+ }
29
+ };
30
+ }
package/src/reload.ts ADDED
@@ -0,0 +1,31 @@
1
+ import type { RspackOptionsNormalized } from "@rspack/core";
2
+
3
+ interface Status {
4
+ isUnloading: boolean;
5
+ currentHash: string;
6
+ previousHash: string[];
7
+ }
8
+
9
+ export function reloadApp(
10
+ { liveReload, hot }: RspackOptionsNormalized["devServer"],
11
+ status: Status
12
+ ) {
13
+ if (status.isUnloading) {
14
+ return;
15
+ }
16
+
17
+ function applyReload(rootWindow: Window, intervalId: number) {
18
+ clearInterval(intervalId);
19
+ console.log("App update, Reloading...");
20
+ rootWindow.location.reload();
21
+ }
22
+
23
+ if (liveReload) {
24
+ let rootWindow = self;
25
+ const intervalId = self.setInterval(() => {
26
+ if (rootWindow.location.protocol !== "about:") {
27
+ applyReload(rootWindow, intervalId);
28
+ }
29
+ });
30
+ }
31
+ }
package/src/server.ts ADDED
@@ -0,0 +1,307 @@
1
+ import type { Compiler, Dev, RspackOptionsNormalized } from "@rspack/core";
2
+ import type { Logger } from "./logger";
3
+ import type { Socket } from "net";
4
+ import type { FSWatcher, WatchOptions } from "chokidar";
5
+ import type { WebSocketServer, ClientConnection } from "./ws";
6
+ import type {
7
+ Application,
8
+ RequestHandler as ExpressRequestHandler,
9
+ ErrorRequestHandler as ExpressErrorRequestHandler
10
+ } from "express";
11
+ import type { DevMiddleware } from "@rspack/dev-middleware";
12
+ import type { Server } from "http";
13
+ import type { ResolvedDev } from "./config";
14
+
15
+ import chokidar from "chokidar";
16
+ import http from "http";
17
+ import { createLogger } from "./logger";
18
+ import WebpackDevServer from "webpack-dev-server";
19
+ import express from "express";
20
+
21
+ import rdm from "@rspack/dev-middleware";
22
+ import { createWebsocketServer } from "./ws";
23
+ import { resolveDevOptions } from "./config";
24
+
25
+ interface Middleware {
26
+ name?: string;
27
+ path?: string;
28
+ middleware: ExpressErrorRequestHandler | ExpressRequestHandler;
29
+ }
30
+ interface Listener {
31
+ name: string | Symbol;
32
+ listener: (...args: any) => void;
33
+ }
34
+ type Host = "local-ip" | "local-ipv4" | "local-ipv6" | string;
35
+ type Port = number | string | "auto";
36
+
37
+ // copy from webpack-dev-server
38
+ export class RspackDevServer {
39
+ options: ResolvedDev;
40
+ logger: Logger;
41
+ staticWatchers: FSWatcher[];
42
+ sockets: Socket[];
43
+ app: Application;
44
+ server: Server;
45
+ private listeners: Listener[];
46
+ private currentHash: string;
47
+ private middleware: DevMiddleware | undefined;
48
+ // TODO: now only support 'ws'
49
+ webSocketServer: WebSocketServer | undefined;
50
+
51
+ constructor(public compiler: Compiler) {
52
+ this.logger = createLogger("rspack-dev-server");
53
+ this.staticWatchers = [];
54
+ this.listeners = [];
55
+ this.sockets = [];
56
+ this.currentHash = "";
57
+ this.options = this.normalizeOptions(compiler.options.devServer);
58
+ this.rewriteCompilerOptions();
59
+ this.addAdditionEntires();
60
+ }
61
+
62
+ normalizeOptions(dev: Dev = {}) {
63
+ return resolveDevOptions(dev, this.compiler.options);
64
+ }
65
+
66
+ rewriteCompilerOptions() {
67
+ if (!this.compiler.options.builtins.react) {
68
+ this.compiler.options.builtins.react = {};
69
+ }
70
+ this.compiler.options.builtins.react.development =
71
+ this.compiler.options.builtins.react.development ?? true;
72
+ if (this.options.hot) {
73
+ this.compiler.options.builtins.react.refresh =
74
+ this.compiler.options.builtins.react.refresh ?? true;
75
+ }
76
+ }
77
+
78
+ addAdditionEntires() {
79
+ const entries: string[] = [];
80
+
81
+ if (this.options.hot) {
82
+ const hotUpdateEntryPath = require.resolve(
83
+ "@rspack/dev-client/devServer"
84
+ );
85
+ entries.push(hotUpdateEntryPath);
86
+
87
+ const cssHotEntryPath = require.resolve("@rspack/dev-client/css");
88
+
89
+ entries.push(cssHotEntryPath);
90
+
91
+ if (this.compiler.options.builtins.react?.refresh) {
92
+ const reactRefreshEntryPath = require.resolve(
93
+ "@rspack/dev-client/react-refresh"
94
+ );
95
+ entries.push(reactRefreshEntryPath);
96
+ }
97
+ }
98
+
99
+ const devClientEntryPath = require.resolve("@rspack/dev-client");
100
+ entries.push(devClientEntryPath);
101
+ for (const key in this.compiler.options.entry) {
102
+ this.compiler.options.entry[key].unshift(...entries);
103
+ }
104
+ }
105
+
106
+ static isAbsoluteURL(URL: string): boolean {
107
+ return WebpackDevServer.isAbsoluteURL(URL);
108
+ }
109
+
110
+ static findIp(gateway: string): string | undefined {
111
+ return WebpackDevServer.findIp(gateway);
112
+ }
113
+
114
+ static async internalIP(family: "v6" | "v4"): Promise<string | undefined> {
115
+ return WebpackDevServer.internalIP(family);
116
+ }
117
+
118
+ static async internalIPSync(
119
+ family: "v6" | "v4"
120
+ ): Promise<string | undefined> {
121
+ return WebpackDevServer.internalIPSync(family);
122
+ }
123
+
124
+ static async getHostname(hostname?: Host): Promise<string> {
125
+ return WebpackDevServer.getHostname(hostname);
126
+ }
127
+
128
+ static async getFreePort(port: Port, host: string): Promise<string | number> {
129
+ return WebpackDevServer.getFreePort(port, host);
130
+ }
131
+
132
+ static findCacheDir(): string {
133
+ // TODO: we need remove the `webpack-dev-server` tag in WebpackDevServer;
134
+ return "";
135
+ }
136
+
137
+ private getCompilerOptions(): RspackOptionsNormalized {
138
+ return this.compiler.options;
139
+ }
140
+
141
+ sendMessage(
142
+ clients: ClientConnection[],
143
+ type: string,
144
+ data?: any,
145
+ params?: any
146
+ ) {
147
+ for (const client of clients) {
148
+ if (client.readyState === 1) {
149
+ client.send(JSON.stringify({ type, data, params }));
150
+ }
151
+ }
152
+ }
153
+
154
+ watchFiles(watchPath: string | string[], watchOptions?: WatchOptions): void {
155
+ const watcher = chokidar.watch(watchPath, watchOptions);
156
+
157
+ // disabling refreshing on changing the content
158
+ if (this.options.liveReload) {
159
+ // TODO: remove this after we had memory filesystem
160
+ if (this.options.hot) {
161
+ return;
162
+ }
163
+
164
+ watcher.on("change", item => {
165
+ if (this.webSocketServer) {
166
+ this.sendMessage(
167
+ this.webSocketServer.clients,
168
+ "static-changed",
169
+ item
170
+ );
171
+ }
172
+ });
173
+ }
174
+
175
+ this.staticWatchers.push(watcher);
176
+ }
177
+
178
+ invalidate(callback = () => {}): void {
179
+ if (this.middleware) {
180
+ this.middleware.invalidate(callback);
181
+ }
182
+ }
183
+
184
+ async start(): Promise<void> {
185
+ this.setupHooks();
186
+ this.setupApp();
187
+ this.createServer();
188
+ this.setupWatchStaticFiles();
189
+ this.createWebsocketServer();
190
+ this.setupDevMiddleware();
191
+ this.setupMiddlewares();
192
+ const host = await RspackDevServer.getHostname(this.options.host);
193
+ const port = await RspackDevServer.getFreePort(this.options.port, host);
194
+ await new Promise(resolve =>
195
+ this.server.listen(
196
+ {
197
+ port,
198
+ host
199
+ },
200
+ () => {
201
+ this.logger.info(`Loopback: http://localhost:${port}`);
202
+ let internalIPv4 = WebpackDevServer.internalIPSync("v4");
203
+ this.logger.info(
204
+ `Your Network (IPV4) http://${internalIPv4}:${port}`
205
+ );
206
+ resolve({});
207
+ }
208
+ )
209
+ );
210
+ }
211
+
212
+ startCallback(callback?: (err?: Error) => void): void {
213
+ throw new Error("Method not implemented.");
214
+ }
215
+ stopCallback(callback?: (err?: Error) => void): void {
216
+ throw new Error("Method not implemented.");
217
+ }
218
+ listen(port: Port, hostname: string, fn: (err?: Error) => void): void {
219
+ throw new Error("Method not implemented.");
220
+ }
221
+ close(callback?: (err?: Error) => void): void {
222
+ throw new Error("Method not implemented.");
223
+ }
224
+
225
+ async stop(): Promise<void> {
226
+ await Promise.all(this.staticWatchers.map(watcher => watcher.close()));
227
+ this.middleware = null;
228
+ this.staticWatchers = [];
229
+ if (this.server) {
230
+ this.server.close();
231
+ }
232
+ if (this.webSocketServer) {
233
+ await new Promise(resolve => {
234
+ this.webSocketServer.implementation.close(() => {
235
+ resolve(void 0);
236
+ });
237
+ for (const client of this.webSocketServer.clients) client.terminate();
238
+ });
239
+ }
240
+ }
241
+
242
+ private setupApp() {
243
+ this.app = express();
244
+ }
245
+
246
+ private setupWatchStaticFiles() {
247
+ if (this.options.static.watch === false) {
248
+ return;
249
+ }
250
+ this.watchFiles(this.options.static.directory, this.options.static.watch);
251
+ }
252
+
253
+ private setupDevMiddleware() {
254
+ // @ts-ignored
255
+ this.middleware = rdm(this.compiler, this.options.devMiddleware);
256
+ }
257
+
258
+ private createWebsocketServer() {
259
+ if (this.options.webSocketServer !== false) {
260
+ this.webSocketServer = createWebsocketServer(this);
261
+ }
262
+ }
263
+
264
+ private setupMiddlewares() {
265
+ const middlewares: Middleware[] = [];
266
+ middlewares.push({
267
+ name: "rdm",
268
+ middleware: this.middleware
269
+ });
270
+
271
+ // Todo Add options
272
+ const connectHistoryApiFallback = require("connect-history-api-fallback");
273
+ middlewares.push({
274
+ name: "[connect-history-api-fallback]",
275
+ middleware: connectHistoryApiFallback({
276
+ verbose: true,
277
+ logger: console.log.bind(console)
278
+ })
279
+ });
280
+ middlewares.push({
281
+ name: "express-static",
282
+ path: this.compiler.options.output.publicPath ?? "/",
283
+ middleware: express.static(this.options.static.directory)
284
+ });
285
+
286
+ middlewares.forEach(m => {
287
+ if (m.path) {
288
+ this.app.use(m.path, m.middleware);
289
+ } else {
290
+ this.app.use(m.middleware);
291
+ }
292
+ });
293
+ }
294
+
295
+ private createServer() {
296
+ this.server = http.createServer(this.app);
297
+ }
298
+
299
+ private setupHooks() {
300
+ this.compiler.hooks.done.tap("dev-server", stats => {
301
+ // send Message
302
+ if (this.webSocketServer) {
303
+ this.sendMessage(this.webSocketServer.clients, "ok"); // TODO: send hash
304
+ }
305
+ });
306
+ }
307
+ }
package/src/ws.ts ADDED
@@ -0,0 +1,18 @@
1
+ import type { RspackDevServer } from "./server";
2
+ import WebSocket from "ws";
3
+ import WebpackWsServer from "webpack-dev-server/lib/servers/WebsocketServer";
4
+
5
+ export type ClientConnection = WebSocket & { isAlive?: boolean };
6
+
7
+ export interface WebSocketServer {
8
+ heartbeatInterval: number;
9
+ implementation: WebSocket.Server<WebSocket.WebSocket>;
10
+ server: RspackDevServer;
11
+ clients: ClientConnection[];
12
+ }
13
+
14
+ export function createWebsocketServer(
15
+ server: RspackDevServer
16
+ ): WebSocketServer {
17
+ return new WebpackWsServer(server);
18
+ }
@@ -0,0 +1,80 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`normalize options snapshot additional entires should added 1`] = `
4
+ {
5
+ "main": [
6
+ "<prefix>/rspack-dev-client/dist/devServer.js",
7
+ "<prefix>/rspack-dev-client/dist/css.js",
8
+ "<prefix>/rspack-dev-client/dist/reactRefresh.js",
9
+ "<prefix>/rspack-dev-client/dist/index.js",
10
+ "<prefix>/something",
11
+ ],
12
+ }
13
+ `;
14
+
15
+ exports[`normalize options snapshot no options 1`] = `
16
+ {
17
+ "devMiddleware": {},
18
+ "host": undefined,
19
+ "hot": true,
20
+ "liveReload": true,
21
+ "open": true,
22
+ "port": undefined,
23
+ "static": {
24
+ "directory": "<PROJECT_ROOT>/dist",
25
+ "watch": {},
26
+ },
27
+ "webSocketServer": {},
28
+ }
29
+ `;
30
+
31
+ exports[`normalize options snapshot port string 1`] = `
32
+ {
33
+ "devMiddleware": {},
34
+ "host": undefined,
35
+ "hot": true,
36
+ "liveReload": true,
37
+ "open": true,
38
+ "port": 9000,
39
+ "static": {
40
+ "directory": "<PROJECT_ROOT>/dist",
41
+ "watch": {},
42
+ },
43
+ "webSocketServer": {},
44
+ }
45
+ `;
46
+
47
+ exports[`normalize options snapshot react.development and react.refresh should be true in default when hot enabled 1`] = `
48
+ {
49
+ "builtins": {
50
+ "browserslist": [],
51
+ "decorator": {
52
+ "emitMetadata": true,
53
+ "legacy": true,
54
+ "useDefineForClassFields": true,
55
+ },
56
+ "define": {},
57
+ "html": [],
58
+ "minify": true,
59
+ "react": {
60
+ "development": true,
61
+ "refresh": true,
62
+ },
63
+ },
64
+ "devServer": {
65
+ "hot": true,
66
+ },
67
+ }
68
+ `;
69
+
70
+ exports[`normalize options snapshot react-refresh client added when react/refresh enabled 1`] = `
71
+ {
72
+ "main": [
73
+ "<prefix>/rspack-dev-client/dist/devServer.js",
74
+ "<prefix>/rspack-dev-client/dist/css.js",
75
+ "<prefix>/rspack-dev-client/dist/reactRefresh.js",
76
+ "<prefix>/rspack-dev-client/dist/index.js",
77
+ "<prefix>/something",
78
+ ],
79
+ }
80
+ `;
@@ -0,0 +1,73 @@
1
+ import type { RspackOptions } from "@rspack/core";
2
+ import { RspackDevServer } from "@rspack/dev-server";
3
+ import { createCompiler } from "@rspack/core";
4
+ import serializer from "jest-serializer-path";
5
+ import os from "os";
6
+ expect.addSnapshotSerializer(serializer);
7
+
8
+ describe("normalize options snapshot", () => {
9
+ it("no options", () => {
10
+ match({});
11
+ });
12
+
13
+ it("port string", () => {
14
+ match({
15
+ devServer: {
16
+ port: "9000"
17
+ }
18
+ });
19
+ });
20
+
21
+ it("additional entires should added", () => {
22
+ matchAdditionEntries({
23
+ entry: ["something"]
24
+ });
25
+ });
26
+
27
+ it("react-refresh client added when react/refresh enabled", () => {
28
+ matchAdditionEntries({
29
+ entry: ["something"],
30
+ builtins: {
31
+ react: {
32
+ refresh: true
33
+ }
34
+ }
35
+ });
36
+ });
37
+
38
+ it("react.development and react.refresh should be true in default when hot enabled", () => {
39
+ const compiler = createCompiler({
40
+ devServer: {
41
+ hot: true
42
+ }
43
+ });
44
+ new RspackDevServer(compiler);
45
+ expect({
46
+ builtins: compiler.options.builtins,
47
+ devServer: compiler.options.devServer
48
+ }).toMatchSnapshot();
49
+ });
50
+ });
51
+
52
+ function match(config: RspackOptions) {
53
+ const compiler = createCompiler(config);
54
+ const server = new RspackDevServer(compiler);
55
+ expect(server.options).toMatchSnapshot();
56
+ }
57
+
58
+ function matchAdditionEntries(config: RspackOptions) {
59
+ const compiler = createCompiler(config);
60
+ const server = new RspackDevServer(compiler);
61
+ const entires = Object.entries(compiler.options.entry);
62
+ // some hack for snapshot
63
+ const value = Object.fromEntries(
64
+ entires.map(([key, item]) => {
65
+ const replaced = item.map(entry => {
66
+ const array = entry.replace(/\\/g, "/").split("/");
67
+ return "<prefix>" + "/" + array.slice(-3).join("/");
68
+ });
69
+ return [key, replaced];
70
+ })
71
+ );
72
+ expect(value).toMatchSnapshot();
73
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "include": [
4
+ "src"
5
+ ],
6
+ "compilerOptions": {
7
+ "outDir": "dist",
8
+ "rootDir": "src"
9
+ },
10
+ "references": [
11
+ {
12
+ "path": "../rspack-dev-middleware"
13
+ }
14
+ ]
15
+ }