anypack-plugin-serve 0.1.0-alpha.1

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.
@@ -0,0 +1,146 @@
1
+ /*
2
+ Copyright © 2019 Andrew Powell
3
+
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+
8
+ The above copyright notice and this permission notice shall be
9
+ included in all copies or substantial portions of this Source Code Form.
10
+ */
11
+ const fs = require('node:fs');
12
+ const childProcess = require('node:child_process');
13
+
14
+ const colors = require('ansi-colors');
15
+
16
+ const defaults = {
17
+ blockSize: 512,
18
+ // 256 mb
19
+ bytes: 2.56e8,
20
+ name: 'wpr',
21
+ };
22
+ const name = 'WebpackPluginRamdisk';
23
+
24
+ function getCommands() {
25
+ const { platform } = process;
26
+
27
+ if (platform === 'darwin') {
28
+ return {
29
+ root: ({ name }) => `/Volumes/${name}`,
30
+ mount: (options) => {
31
+ const result = run(
32
+ 'hdiutil',
33
+ 'attach',
34
+ '-nomount',
35
+ `ram://${options.blocks}`,
36
+ );
37
+
38
+ options.devicePath = result.stdout.toString().trim();
39
+
40
+ run('diskutil', 'erasevolume', 'HFS+', name, options.devicePath);
41
+ },
42
+ umount: (options) => {
43
+ // If the disk wasn't unmounted properly on previous run we don't know its device id
44
+ if (!options.devicePath) {
45
+ const all = run('hdiutil', 'info');
46
+
47
+ const output = all.stdout
48
+ .toString()
49
+ .split(/\n\r?/g)
50
+ .find((line) => line.endsWith(options.diskPath));
51
+
52
+ if (output) {
53
+ options.devicePath = output.split(/\s+/g)[0];
54
+ }
55
+ }
56
+
57
+ if (options.devicePath) {
58
+ run('hdiutil', 'detach', options.devicePath);
59
+ }
60
+ },
61
+ };
62
+ }
63
+
64
+ if (platform === 'linux') {
65
+ return {
66
+ root: ({ name }) => `/dev/shm/${name}`,
67
+ mount: ({ diskPath }) => {
68
+ run('sudo', 'mkdir', '-p', diskPath);
69
+ run('sudo', 'chmod', '777', diskPath);
70
+ },
71
+ umount: ({ diskPath }) => {
72
+ run('sudo', 'rm', '-rf', diskPath);
73
+ },
74
+ };
75
+ }
76
+
77
+ return null;
78
+ }
79
+
80
+ function run(command, ...args) {
81
+ if (!command) {
82
+ return;
83
+ }
84
+
85
+ const result = childProcess.spawnSync(command, args);
86
+
87
+ if (result.status !== 0) {
88
+ throw new Error(
89
+ `Failed command [${command} ${args.join(' ')}]: ${result.stdout} / ${
90
+ result.stderr
91
+ }`,
92
+ );
93
+ }
94
+
95
+ return result;
96
+ }
97
+
98
+ class Ramdisk {
99
+ constructor(opts = {}) {
100
+ const partialOpts = Object.assign({}, defaults, opts);
101
+ const options = Object.assign({}, partialOpts, {
102
+ blocks: partialOpts.bytes / partialOpts.blockSize,
103
+ });
104
+
105
+ this.options = options;
106
+
107
+ this.commands = getCommands();
108
+ this.options.diskPath = this.commands?.root(this.options);
109
+
110
+ // const options = Object.assign({}, opts, { root: commands.root });
111
+
112
+ this.init();
113
+ }
114
+
115
+ init() {
116
+ if (!fs.existsSync(this.options.diskPath)) {
117
+ console.info(
118
+ colors.blue(`⬡ aps:`),
119
+ `Initializing RAMdisk at ${this.options.diskPath}. You may be prompted for credentials`,
120
+ );
121
+ this.commands?.mount(this.options);
122
+ }
123
+
124
+ process.on('SIGINT', () => {
125
+ this.cleanup();
126
+ });
127
+
128
+ process.on('SIGTERM', () => {
129
+ this.cleanup();
130
+ });
131
+
132
+ process.on('exit', () => {
133
+ this.cleanup();
134
+ });
135
+ }
136
+
137
+ get diskPath() {
138
+ return this.options.diskPath;
139
+ }
140
+
141
+ cleanup() {
142
+ this.commands?.umount(this.options);
143
+ }
144
+ }
145
+
146
+ module.exports = { defaults, Ramdisk };
package/lib/routes.js ADDED
@@ -0,0 +1,118 @@
1
+ /*
2
+ Copyright © 2018 Andrew Powell
3
+
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+
8
+ The above copyright notice and this permission notice shall be
9
+ included in all copies or substantial portions of this Source Code Form.
10
+ */
11
+ const { unstyle } = require('ansi-colors');
12
+ const router = require('koa-route');
13
+ const stringify = require('json-stringify-safe');
14
+
15
+ const prep = (data) => stringify(data);
16
+
17
+ const statsOptions = {
18
+ all: false,
19
+ cached: true,
20
+ children: true,
21
+ hash: true,
22
+ modules: true,
23
+ timings: true,
24
+ exclude: ['node_modules', 'bower_components', 'components'],
25
+ };
26
+
27
+ const setupRoutes = function setupRoutes() {
28
+ const { app, options } = this;
29
+ const events = ['build', 'done', 'invalid', 'progress'];
30
+ const connect = async (ctx) => {
31
+ if (ctx.ws) {
32
+ const socket = await ctx.ws();
33
+ const send = (data) => {
34
+ if (socket.readyState !== 1) {
35
+ return;
36
+ }
37
+ socket.send(data);
38
+ };
39
+
40
+ socket.build = (compilerName = '<unknown>', { wpsId }) => {
41
+ send(prep({ action: 'build', data: { compilerName, wpsId } }));
42
+ };
43
+
44
+ socket.done = (stats, { wpsId }) => {
45
+ const { hash } = stats;
46
+
47
+ if (socket.lastHash === hash) {
48
+ return;
49
+ }
50
+
51
+ send(prep({ action: 'done', data: { hash, wpsId } }));
52
+
53
+ socket.lastHash = hash;
54
+
55
+ const { errors = [], warnings = [] } = stats.toJson(statsOptions);
56
+
57
+ if (errors.length || warnings.length) {
58
+ send(
59
+ prep({
60
+ action: 'problems',
61
+ data: {
62
+ errors: errors.slice(0).map((e) => unstyle(e)),
63
+ hash,
64
+ warnings: warnings.slice(0).map((e) => unstyle(e)),
65
+ wpsId,
66
+ },
67
+ }),
68
+ );
69
+
70
+ if (errors.length) {
71
+ return;
72
+ }
73
+ }
74
+
75
+ if (options.hmr || options.liveReload) {
76
+ const action = options.liveReload ? 'reload' : 'replace';
77
+ send(prep({ action, data: { hash, wpsId } }));
78
+ }
79
+ };
80
+
81
+ socket.invalid = (filePath = '<unknown>', compiler) => {
82
+ const context =
83
+ compiler.context || compiler.options.context || process.cwd();
84
+ const fileName = filePath?.replace?.(context, '') || filePath;
85
+ const { wpsId } = compiler;
86
+
87
+ send(prep({ action: 'invalid', data: { fileName, wpsId } }));
88
+ };
89
+
90
+ socket.progress = (data) => {
91
+ send(prep({ action: 'progress', data }));
92
+ };
93
+
94
+ for (const event of events) {
95
+ this.on(event, socket[event]);
96
+
97
+ socket.on('close', () => {
98
+ this.removeListener(event, socket[event]);
99
+ });
100
+ }
101
+
102
+ // #138. handle emitted events that don't have a listener registered, and forward the message
103
+ // onto the client via the socket
104
+ const unhandled = ({ eventName, data }) =>
105
+ send(prep({ action: eventName, data }));
106
+ this.on('unhandled', unhandled);
107
+ socket.on('close', () => {
108
+ this.off('unhandled', unhandled);
109
+ });
110
+
111
+ send(prep({ action: 'connected' }));
112
+ }
113
+ };
114
+
115
+ app.use(router.get('/wps', connect));
116
+ };
117
+
118
+ module.exports = { setupRoutes };
package/lib/server.js ADDED
@@ -0,0 +1,129 @@
1
+ /*
2
+ Copyright © 2018 Andrew Powell
3
+
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+
8
+ The above copyright notice and this permission notice shall be
9
+ included in all copies or substantial portions of this Source Code Form.
10
+ */
11
+ const url = require('node:url');
12
+ const { createServer } = require('node:http');
13
+
14
+ const open = require('open').default;
15
+
16
+ const { getBuiltins } = require('./middleware');
17
+ const { setupRoutes } = require('./routes');
18
+
19
+ const select = (options, callback) => {
20
+ const types = {
21
+ http: createServer,
22
+ https: require('node:https').createServer,
23
+ http2: require('node:http2').createServer,
24
+ http2Secure: require('node:http2').createSecureServer,
25
+ };
26
+ const { http2, https } = options;
27
+ let server;
28
+ let secure = false;
29
+
30
+ if (http2) {
31
+ if (http2 === true) {
32
+ server = types.http2({}, callback);
33
+ } else if (http2.cert || http2.pfx) {
34
+ secure = true;
35
+ server = types.http2Secure(http2, callback);
36
+ } else {
37
+ server = types.http2(http2, callback);
38
+ }
39
+ } else if (https) {
40
+ secure = true;
41
+ server = types.https(https === true ? {} : https, callback);
42
+ } else {
43
+ server = types.http(callback);
44
+ }
45
+
46
+ return { secure, server };
47
+ };
48
+
49
+ const start = async function start() {
50
+ if (this.listening) {
51
+ return;
52
+ }
53
+
54
+ const { app } = this;
55
+ const { host, middleware, port, waitForBuild } = this.options;
56
+ const builtins = getBuiltins(app, this.options);
57
+
58
+ this.options.host = await host;
59
+ this.options.port = await port;
60
+
61
+ if (waitForBuild) {
62
+ app.use(async (_ctx, next) => {
63
+ await this.state.compiling;
64
+ await next();
65
+ });
66
+ }
67
+
68
+ // allow users to add and manipulate middleware in the config
69
+ await middleware(app, builtins);
70
+
71
+ // call each of the builtin middleware. methods are once'd so this has no ill-effects.
72
+ for (const fn of Object.values(builtins)) {
73
+ if (!fn.skip) {
74
+ fn();
75
+ }
76
+ }
77
+
78
+ setupRoutes.bind(this)();
79
+
80
+ const { secure, server } = select(this.options, app.callback());
81
+ const emitter = this;
82
+
83
+ this.options.secure = secure;
84
+
85
+ server.listen({ host: this.options.host, port: this.options.port });
86
+
87
+ // wait for the server to fully spin up before asking it for details
88
+ await {
89
+ // biome-ignore lint/suspicious/noThenProperty: legacy
90
+ then(r, f) {
91
+ server.on('listening', () => {
92
+ emitter.emit('listening', server);
93
+ r();
94
+ });
95
+ server.on('error', f);
96
+ },
97
+ };
98
+
99
+ this.listening = true;
100
+
101
+ const protocol = secure ? 'https' : 'http';
102
+ const address = server.address();
103
+
104
+ address.hostname = address.address;
105
+
106
+ // fix #131 - server address reported as 127.0.0.1 for localhost
107
+ if (
108
+ address.hostname !== this.options.host &&
109
+ this.options.host === 'localhost'
110
+ ) {
111
+ address.hostname = this.options.host;
112
+ }
113
+
114
+ // we set this so the client can use the actual hostname of the server. sometimes the net
115
+ // will mutate the actual hostname value (e.g. :: -> [::])
116
+ this.options.address = url.format(address);
117
+
118
+ const uri = `${protocol}://${this.options.address}`;
119
+
120
+ this.log.info('Server Listening on:', uri);
121
+
122
+ this.once('done', () => {
123
+ if (this.options.open) {
124
+ open(uri, this.options.open === true ? {} : this.options.open);
125
+ }
126
+ });
127
+ };
128
+
129
+ module.exports = { start };
@@ -0,0 +1,77 @@
1
+ /*
2
+ Copyright © 2018 Andrew Powell
3
+
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+
8
+ The above copyright notice and this permission notice shall be
9
+ included in all copies or substantial portions of this Source Code Form.
10
+ */
11
+ const {
12
+ refine,
13
+ any,
14
+ nullable,
15
+ type,
16
+ partial,
17
+ array,
18
+ union,
19
+ integer,
20
+ max,
21
+ boolean,
22
+ string,
23
+ func,
24
+ literal,
25
+ never,
26
+ validate,
27
+ } = require('superstruct');
28
+
29
+ const promise = refine(any(), 'promise-like', (value) => {
30
+ return (
31
+ !!value &&
32
+ (typeof value === 'object' || typeof value === 'function') &&
33
+ typeof value.then === 'function'
34
+ );
35
+ });
36
+
37
+ module.exports = {
38
+ validate(options) {
39
+ const schema = partial({
40
+ allowMany: boolean(),
41
+ client: partial({
42
+ address: string(),
43
+ protocol: union([literal('ws'), literal('wss')]),
44
+ retry: boolean(),
45
+ silent: boolean(),
46
+ }),
47
+ compress: nullable(boolean()),
48
+ headers: nullable(type({})),
49
+ historyFallback: union([boolean(), type({})]),
50
+ hmr: union([boolean(), literal('refresh-on-failure')]),
51
+ host: nullable(union([promise, string()])),
52
+ http2: union([boolean(), type({})]),
53
+ https: nullable(type({})),
54
+ liveReload: boolean(),
55
+ log: partial({ level: string(), timestamp: boolean() }),
56
+ middleware: func(),
57
+ open: union([boolean(), type({})]),
58
+ port: union([max(integer(), 65535), promise]),
59
+ progress: union([boolean(), literal('minimal')]),
60
+ publicPath: nullable(string()),
61
+ ramdisk: union([boolean(), type({})]),
62
+ secure: never(),
63
+ static: nullable(
64
+ union([
65
+ string(),
66
+ array(string()),
67
+ partial({ glob: array(string()), options: type({}) }),
68
+ ]),
69
+ ),
70
+ status: boolean(),
71
+ waitForBuild: boolean(),
72
+ });
73
+ const [error, value] = validate(options, schema);
74
+
75
+ return { error, value };
76
+ },
77
+ };
package/lib/ws.js ADDED
@@ -0,0 +1,41 @@
1
+ /*
2
+ Copyright © 2018 Andrew Powell
3
+
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
7
+
8
+ The above copyright notice and this permission notice shall be
9
+ included in all copies or substantial portions of this Source Code Form.
10
+ */
11
+ const defer = require('./helpers.js').defer;
12
+ const WebSocket = require('ws');
13
+
14
+ const socketServer = new WebSocket.Server({ noServer: true });
15
+
16
+ const middleware = async (ctx, next) => {
17
+ const deferred = defer();
18
+ const { upgrade = '' } = ctx.request.headers;
19
+ const upgradable = upgrade
20
+ .split(',')
21
+ .map((header) => header.trim())
22
+ .includes('websocket');
23
+
24
+ if (upgradable) {
25
+ ctx.ws = async () => {
26
+ socketServer.handleUpgrade(
27
+ ctx.req,
28
+ ctx.request.socket,
29
+ Buffer.alloc(0),
30
+ deferred.resolve,
31
+ );
32
+ ctx.respond = false;
33
+
34
+ return deferred.promise;
35
+ };
36
+ }
37
+
38
+ await next();
39
+ };
40
+
41
+ module.exports = { middleware };
package/package.json ADDED
@@ -0,0 +1,94 @@
1
+ {
2
+ "name": "anypack-plugin-serve",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "A Development Server in a Webpack Plugin",
5
+ "license": "MPL-2.0",
6
+ "repository": "onigoetz/anypack-plugin-serve",
7
+ "author": "onigoetz",
8
+ "homepage": "https://github.com/onigoetz/anypack-plugin-serve",
9
+ "bugs": "https://github.com/onigoetz/anypack-plugin-serve/issues",
10
+ "bin": "",
11
+ "main": "lib/index.js",
12
+ "engines": {
13
+ "node": ">= 20.0.0"
14
+ },
15
+ "scripts": {
16
+ "ci:lint": "biome check lib test",
17
+ "ci:test": "npm run test --coverage",
18
+ "dev": "npm run dev:clean && node node_modules/webpack-nano/bin/wp --config test/fixtures/simple/webpack.config.js",
19
+ "dev:clean": "del test/fixtures/multi/output/* test/fixtures/simple/output/*",
20
+ "dev:multi": "npm run dev:clean && node node_modules/webpack-nano/bin/wp --config test/fixtures/multi/webpack.config.js",
21
+ "faq:toc": "echo 'temporarily disabled' # markdown-toc -i .github/FAQ.md",
22
+ "format": "biome format lib test",
23
+ "lint": "biome check --write lib test",
24
+ "lint-staged": "lint-staged",
25
+ "test": "rstest"
26
+ },
27
+ "files": [
28
+ "client.js",
29
+ "lib/",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
33
+ "peerDependencies": {
34
+ "webpack": "^5.0.0"
35
+ },
36
+ "dependencies": {
37
+ "ansi-colors": "^4.1.3",
38
+ "connect-history-api-fallback": "^2.0.0",
39
+ "empathic": "^2.0.0",
40
+ "http-proxy-middleware": "^1.0.3",
41
+ "json-stringify-safe": "^5.0.1",
42
+ "koa": "^2.5.3",
43
+ "koa-compress": "^5.0.1",
44
+ "koa-connect": "^2.0.1",
45
+ "koa-route": "^3.2.0",
46
+ "koa-static": "^5.0.0",
47
+ "loglevelnext": "^4.0.1",
48
+ "nanoid": "^5.1.6",
49
+ "onetime": "^7.0.0",
50
+ "open": "^11.0.0",
51
+ "superstruct": "^2.0.2",
52
+ "tinyglobby": "^0.2.15",
53
+ "ws": "^8.18.3"
54
+ },
55
+ "devDependencies": {
56
+ "@biomejs/biome": "2.3.8",
57
+ "@rstest/core": "^0.7.0",
58
+ "@rstest/coverage-istanbul": "^0.1.0",
59
+ "del-cli": "7.0.0",
60
+ "execa": "^9.0.0",
61
+ "get-port": "^7.1.0",
62
+ "line-by-line": "^0.1.6",
63
+ "lint-staged": "^16.2.7",
64
+ "node-fetch": "^3.3.2",
65
+ "pre-commit": "^1.2.2",
66
+ "puppeteer": "^24.32.0",
67
+ "random-js": "^2.1.0",
68
+ "webpack": "^5.0.0",
69
+ "webpack-nano": "^1.0.0"
70
+ },
71
+ "keywords": [
72
+ "dev",
73
+ "development",
74
+ "devserver",
75
+ "serve",
76
+ "server",
77
+ "webpack"
78
+ ],
79
+ "lint-staged": {
80
+ "*.{js,mjs}": [
81
+ "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off"
82
+ ]
83
+ },
84
+ "nyc": {
85
+ "include": [
86
+ "lib/**/*.js"
87
+ ],
88
+ "exclude": [
89
+ "lib/client*.js",
90
+ "test/"
91
+ ]
92
+ },
93
+ "pre-commit": "lint-staged"
94
+ }