@ideasonpurpose/build-tools-wordpress 1.1.5 → 1.1.6

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/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ #### v1.1.5
8
+
9
+ > 25 January 2024
10
+
11
+ - move the localPort stuff into devserver-proxy
12
+
7
13
  #### v1.1.4
8
14
 
9
15
  > 25 January 2024
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @ideasonpurpose/build-tools-wordpress
2
2
 
3
- #### Version 1.1.5
3
+ #### Version 1.1.6
4
4
 
5
5
  ![NPM Version](https://img.shields.io/npm/v/%40ideasonpurpose%2Fbuild-tools-wordpress?logo=npm)
6
6
  ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/ideasonpurpose/build-tools-wordpress/npm-publish.yml?logo=github&logoColor=white)
@@ -15,7 +15,7 @@ const main = async () => {
15
15
  "\n",
16
16
  " 🚀 ",
17
17
  chalk.bold("Local WP site:"),
18
- chalk.magenta(`http://${info.hostname}:${chalk.bold(info.port)}`),
18
+ chalk.yellow(`http://${info.hostname}:${chalk.bold(info.port)}`),
19
19
  "\n",
20
20
  );
21
21
  } else {
package/bin/zip.js ADDED
@@ -0,0 +1,231 @@
1
+ import fs from "fs-extra";
2
+ import { posix as path } from "path";
3
+ import url from "url";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
+
6
+ import archiver from "archiver";
7
+ import chalk from "chalk";
8
+ import { cosmiconfig, cosmiconfigSync } from "cosmiconfig";
9
+ import { filesize } from "filesize";
10
+ import { globby } from "globby";
11
+ import isTextPath from "is-text-path";
12
+ import cliTruncate from "cli-truncate";
13
+ import stringLength from "string-length";
14
+ import replaceStream from "replacestream";
15
+
16
+ import buildConfig from "./lib/buildConfig.js";
17
+ import { prettierHrtime } from "./lib/prettier-hrtime.js";
18
+
19
+
20
+
21
+ export async function zip() {
22
+ // const siteDir = new URL("../site", import.meta.url).pathname;
23
+ const siteDir = new URL(import.meta.url).pathname;
24
+ // const explorerSync = cosmiconfigSync("ideasonpurpose");
25
+ const explorer = cosmiconfig("ideasonpurpose");
26
+ // const configFile = explorerSync.search(siteDir);
27
+ const configFile = await explorer.search(siteDir);
28
+ }
29
+
30
+ zip()
31
+
32
+ /**
33
+ * This script expects to the site to live in /usr/src/site/ to
34
+ * match the webpack config. It can also be called with a single
35
+ * path argument which will be evaluated relative to the script.
36
+ * This can be used for testing, or to bundle any directory while
37
+ * excluding src, node_modules, etc.
38
+ */
39
+ let siteDirBase = process.argv[2] || "/usr/src/site/";
40
+ siteDirBase += siteDirBase.endsWith("/") ? "" : "/";
41
+ // const siteDir = new URL(siteDirBase, import.meta.url);
42
+
43
+ // const explorerSync = cosmiconfigSync("ideasonpurpose");
44
+ // const configFile = explorerSync.search(siteDir.pathname) || {
45
+ // config: {},
46
+ // filepath: siteDir.pathname,
47
+ // };
48
+ // const configFileUrl = url.pathToFileURL(configFile.filepath);
49
+ // const config = buildConfig(configFile);
50
+
51
+ // const explorerSync = cosmiconfigSync("ideasonpurpose");
52
+ // const configFile = explorerSync.search(siteDir);
53
+
54
+ const configFile = {
55
+ filepath: fileURLToPath(
56
+ new URL("../site/ideasonpurpose.config.js", import.meta.url)
57
+ ),
58
+ };
59
+ const configFileUrl = pathToFileURL(configFile.filepath);
60
+
61
+ // import buildConfig from "./lib/buildConfig.js";
62
+ import siteConfig from "../site/ideasonpurpose.config.js";
63
+
64
+ console.log({ configFile, configFileUrl });
65
+
66
+
67
+ return ;
68
+ // const config = buildConfig(configFile ?? siteConfig);
69
+ const config = buildConfig({ config: siteConfig });
70
+
71
+ const packageJson = fs.readJsonSync(new URL("package.json", configFileUrl));
72
+ packageJson.version ??= "";
73
+
74
+ const archiveName = packageJson.name || "archive";
75
+
76
+ const versionDirName = packageJson.version
77
+ ? `${archiveName}-${packageJson.version}`.replace(/[ .]/g, "_")
78
+ : archiveName;
79
+
80
+ const zipFileName = `${versionDirName}.zip`;
81
+ const zipFile = new URL(`_builds/${zipFileName}`, configFileUrl).pathname;
82
+
83
+ fs.ensureFileSync(zipFile);
84
+ const output = fs.createWriteStream(zipFile);
85
+
86
+ output.on("finish", finishReporter);
87
+
88
+ const archive = archiver("zip", { zlib: { level: 9 } });
89
+ archive.pipe(output);
90
+
91
+ console.log(chalk.bold("Bundling Project for Deployment"));
92
+ const start = process.hrtime();
93
+
94
+ /**
95
+ * Set projectDir to the parent directory of config.src, all bundled
96
+ * files will be found relative to this.
97
+ */
98
+ const projectDir = new URL(`${config.src}/../`, configFileUrl);
99
+
100
+ /**
101
+ * Counters for total uncompressed size and number of files
102
+ * and a re-usable scoped continuer for file info
103
+ */
104
+ let inBytes = 0;
105
+ let fileCount = 0;
106
+
107
+ const globOpts = { cwd: projectDir.pathname, nodir: false };
108
+ globby(["**/*", "!src", "!_builds", "!**/*.sql", "!**/node_modules"], globOpts)
109
+ .then((fileList) => {
110
+ /**
111
+ * Throw an error and bail out if there are no files to zip
112
+ */
113
+ if (!fileList.length) {
114
+ throw new Error("No files found.");
115
+ }
116
+ return fileList;
117
+ })
118
+ .then((fileList) =>
119
+ fileList.map((f) => {
120
+ const file = {
121
+ path: f,
122
+ stat: fs.statSync(new URL(f, projectDir)),
123
+ contents: fs.createReadStream(path.join(globOpts.cwd, f)),
124
+ };
125
+
126
+ /**
127
+ * Replace the dev folder name with the versioned folder name in hard-coded
128
+ * include paths. These replacements are only run against webpack's compiled
129
+ * assets and Composer's generated autoloaders.
130
+ */
131
+ if (isTextPath(f)) {
132
+ const devPath = new RegExp(`wp-content/themes/${archiveName}/`, "gi");
133
+
134
+ file.contents = file.contents.pipe(
135
+ replaceStream(devPath, `wp-content/themes/${versionDirName}/`)
136
+ );
137
+ }
138
+
139
+ file.contents.on("data", (chunk) => {
140
+ inBytes += chunk.length;
141
+ });
142
+
143
+ file.contents.on("end", () => foundReporter(file));
144
+
145
+ /**
146
+ * Adding a data handler changes a stream's mode from paused to flowing
147
+ * so we need to change it back or the streams will be truncated
148
+ */
149
+ file.contents.pause();
150
+ return file;
151
+ })
152
+ )
153
+ .then((fileList) =>
154
+ fileList.map((f) =>
155
+ archive.append(f.contents, {
156
+ name: f.path,
157
+ prefix: versionDirName,
158
+ })
159
+ )
160
+ )
161
+ .then(() => archive.finalize())
162
+ .catch(console.error);
163
+
164
+ function foundReporter(file) {
165
+ fileCount += 1;
166
+ let outString = [
167
+ "🔍 ",
168
+ chalk.yellow("Found"),
169
+ chalk.magenta(fileCount),
170
+ chalk.yellow("files..."),
171
+ chalk.gray(`(Uncompressed: ${filesize(inBytes)}) `),
172
+ ].join(" ");
173
+
174
+ /**
175
+ * calculate width of terminal then shorten paths
176
+ */
177
+ const cols = process.stdout.columns - stringLength(outString) - 1;
178
+ outString += chalk.blue(cliTruncate(file.path, cols, { position: "middle" }));
179
+
180
+ if (fileCount % 25 == 0) {
181
+ process.stdout.clearLine();
182
+ process.stdout.cursorTo(0);
183
+ process.stdout.write(outString);
184
+ }
185
+ }
186
+
187
+ function finishReporter() {
188
+ const outBytes = archive.pointer();
189
+ const end = process.hrtime(start);
190
+ const duration = prettierHrtime(end);
191
+ const savedBytes = inBytes - outBytes;
192
+ const savedPercent = ((1 - outBytes / inBytes) * 100).toFixed(2);
193
+
194
+ process.stdout.clearLine();
195
+ process.stdout.cursorTo(0);
196
+
197
+ console.log(
198
+ "🔍 ",
199
+ chalk.yellow("Found"),
200
+ chalk.magenta(fileCount),
201
+ chalk.yellow("files"),
202
+ chalk.gray(`(Uncompressed: ${filesize(inBytes)})`)
203
+ );
204
+ console.log(
205
+ "👀 ",
206
+ chalk.yellow("Webpack Bundle Analyzer report:"),
207
+ chalk.magenta("webpack/stats/index.html")
208
+ );
209
+ console.log(
210
+ "📦 ",
211
+ chalk.yellow("Created"),
212
+ chalk.magenta(filesize(outBytes)),
213
+ chalk.yellow("Zip archive"),
214
+ chalk.gray(`(Saved ${filesize(savedBytes)}, ${savedPercent}%)`)
215
+ );
216
+ console.log(
217
+ "🎁 ",
218
+ chalk.yellow("Theme archive"),
219
+ chalk.magenta(
220
+ `${path.basename(path.dirname(zipFile))}/${chalk.bold(zipFileName)}`
221
+ ),
222
+ chalk.yellow("created in"),
223
+ chalk.magenta(duration)
224
+ );
225
+ console.log("⏳");
226
+ console.log(
227
+ "🚀 ",
228
+ chalk.bold(`Remember to push to ${chalk.cyan("GitHub!")}`)
229
+ );
230
+ console.log("✨");
231
+ }
@@ -23,7 +23,16 @@ export class AfterDoneReporterPlugin {
23
23
  apply(compiler) {
24
24
  compiler.hooks.done.tapPromise(this.name, async (stats) => {
25
25
  if (this.config.echo) {
26
- // console.log(compiler.options, stats);
26
+ /**
27
+ * NOTE: As of webpack-dev-server vXX.XX.XXX, auto-assigned ports are
28
+ * not propagated into the compiler.options.devServer object.
29
+ *
30
+ * As a workaround, this relies on the follining line being added to
31
+ * the webpack.config file's devServer.setupMiddlewares method:
32
+ *
33
+ * devServer.compiler.options.devServer.port = devServer.options.port;
34
+ *
35
+ */
27
36
  const { host, port } = compiler.options.devServer;
28
37
  const { startTime, endTime, modules, assetsInfo } = stats.compilation;
29
38
  const hostname = host === "0.0.0.0" ? "localhost" : host;
@@ -45,7 +54,23 @@ export class AfterDoneReporterPlugin {
45
54
  setTimeout(() =>
46
55
  console.log("\n" + this.config.prefix + " " + messages),
47
56
  );
57
+
58
+ setTimeout(() => {
59
+ // console.log(Object.keys(compiler));
60
+ // console.log(compiler.options);
61
+ });
48
62
  }
49
63
  });
64
+
65
+
66
+ // compiler.hooks.done.tapPromise("webpack-dev-server", async (stats) => {
67
+ // await stats;
68
+ // setTimeout(() => {
69
+ // console.log(chalk.bold.magenta("in webpack-dev-server hook"), {
70
+ // stats, thing: Object.keys( compiler.options),
71
+ // });
72
+ // });
73
+ // });
74
+
50
75
  }
51
76
  }
@@ -12,7 +12,7 @@ export class WatchRunReporterPlugin {
12
12
  const defaults = {
13
13
  echo: true,
14
14
  prefix: `🟢${chalk.gray("(iop)")}:`,
15
- message: chalk.bold("Compiling..."),
15
+ message: chalk.bold("Compiling...", ""),
16
16
  };
17
17
  this.config = { ...defaults, ...options };
18
18
  this.name = "IOP Reporter Plugin";
@@ -14,7 +14,7 @@ const defaultConfig = {
14
14
  contentBase: "/dist/", // TODO: Should this be false?
15
15
  manifestFile: "./dependency-manifest.json",
16
16
  sass: "sass-embedded",
17
- // port: 8080,
17
+ devtool: false,
18
18
  transpileDependencies: ["ansi-regex", "normalize-url"],
19
19
  proxy: "wordpress",
20
20
  };
@@ -2,101 +2,64 @@
2
2
 
3
3
  // DO NOT destructure this or we won't be able to mock it
4
4
  // import {promises as dnsPromises} from "dns";
5
- import dns from "dns";
5
+ // import dns from "dns";
6
6
  import chalk from "chalk";
7
7
  import { isIP } from "net";
8
8
 
9
+ import { findLocalPort } from "./find-local-docker-port.js";
9
10
  /**
10
11
  * If config.proxy is explicitly not false, return a {proxy} object
11
12
  * otherwise return an empty object.
12
13
  */
13
14
  export async function devserverProxy(config) {
14
- let target;
15
+ let target = config.proxy;
15
16
 
16
- // console.log({config});
17
-
18
- // if (
19
- // config.proxy ===
20
- // "http://devserver-proxy-token--d939bef2a41c4aa154ddb8db903ce19fff338b61"
21
- // ) {
22
- // /**
23
- // * Deal with the legacy service hostname, re-map to 'wordpress' which will
24
- // * be resolved to an IP address.
25
- // *
26
- // * TODO: Remove this warning after 2023-02 (extended another year, not hurting anything)
27
- // */
28
-
29
- // console.log(
30
- // "⚠️ ",
31
- // `The ${chalk.magenta(
32
- // "devserver-proxy-token"
33
- // )} value is no longer necessary`,
34
- // "and can be safely removed from docker-compose"
35
- // );
36
-
37
- // /**
38
- // * 'wordpress' is the internal hostname used by docker
39
- // */
40
- // target = await dns.promises.resolve("wordpress").catch(() => false);
41
- // if (target) {
42
- // target = "http://" + target.pop();
43
- // }
44
- // } else if (!!isIP(config.proxy)) {
45
- if (!!isIP(config.proxy)) {
17
+ if (config.proxy === true) {
18
+ const localPort = await findLocalPort();
19
+ // console.log({ localPort });
20
+ if (localPort && localPort.hostname && localPort.port) {
21
+ target = new URL(`http://${localPort.hostname}:${localPort.port}`);
22
+ console.log(
23
+ chalk.cyan.bold(
24
+ `PORT FROM DEVSERVER-PROXY: TRUE ${localPort.port}`,
25
+ target,
26
+ ),
27
+ );
28
+ }
29
+ } else if (!!isIP(config.proxy)) {
46
30
  /**
47
31
  * Bare IP addresses
48
32
  * net.isIP() returns 4, 6 or 0(IPv4, IPv6 or not-an-IP address)
49
33
  */
50
- target = `http://${config.proxy}`;
51
- } else if (
52
- typeof config.proxy == "string" &&
53
- !config.proxy.match(/^https?:\/\//i)
54
- ) {
34
+ target = new URL(`http://${config.proxy}`);
35
+ } else if (typeof config.proxy === "string") {
55
36
  /**
56
- * Handle plain strings that don't start with http:// or https://
57
- * Attempt to resolve these to IP addresses
37
+ * It's a string, first see if it can be made into a URL, then try it as
38
+ * a docker container name, then fail.
58
39
  */
59
- target = await dns.promises.resolve(config.proxy).catch(() => false);
60
-
61
- if (target) {
62
- target = "http://" + target.pop();
40
+ try {
41
+ target = new URL(config.proxy);
42
+ } catch (err) {
43
+ const localPort = await findLocalPort(config.proxy);
44
+ if (localPort && localPort.hostname && localPort.port) {
45
+ target = new URL(`http://${localPort.hostname}:${localPort.port}`);
46
+ console.log(
47
+ chalk.cyan.bold(
48
+ `PORT FROM DEVSERVER-PROXY: "${config.proxy} ${localPort.port}`,
49
+ target,
50
+ ),
51
+ );
52
+ }
63
53
  }
64
- } else {
65
- /**
66
- * Pass anything else straight through
67
- */
68
- target = config.proxy;
69
-
70
- // TODO: This obliterates any default values, sort out this setting.
71
-
72
- const localPort = await findLocalPort();
73
-
74
- target = `http://${localPort.hostname}:${localPort.port}`;
75
- console.log(
76
- chalk.cyan.bold(
77
- `FOUND THE PORT IN BUILDCONFIG ${localPort.port}`,
78
- target,
79
- ),
80
- );
81
54
  }
82
55
 
83
- /**
84
- * Note: This is a sticky bit of code. Everything above gets bottlenecked here, and
85
- * if aa previous value of target can't be converted to a URL, the whole proxy will
86
- * return an empty object.
87
- */
88
- try {
89
- config.proxyUrl = new URL(target);
90
- } catch (err) {
91
- // Invalid URL, unable to configure proxy.
56
+ if (!target) {
92
57
  return {};
93
58
  }
94
59
 
95
- // console.log({ target , proxyUrl: config.proxyUrl});
96
-
97
60
  const proxy = {
98
61
  "**": {
99
- target,
62
+ target: target.origin,
100
63
  secure: false,
101
64
  autoRewrite: true,
102
65
  selfHandleResponse: true, // necessary to avoid res.end being called automatically
@@ -108,7 +71,7 @@ export async function devserverProxy(config) {
108
71
  if (err.code === "ECONNRESET") {
109
72
  console.log(chalk.yellow("Caught ECONNRESET error, ignoring..."));
110
73
  } else {
111
- console.log("PROXY ERROR: ", req.url, err, err.stack);
74
+ console.log("Devserver Proxy Error: ", req.url, err, err.stack);
112
75
  res.writeHead(500, { "Content-Type": "text-plain" });
113
76
  res.end("Webpack DevServer Proxy Error: " + err);
114
77
  }
@@ -141,7 +104,7 @@ export async function devserverProxy(config) {
141
104
  let header = proxyRes.headers[key];
142
105
  if (typeof header == "string") {
143
106
  header = header.replace(
144
- new RegExp(config.proxyUrl.host, "gi"),
107
+ new RegExp(target.host, "gi"),
145
108
  req.headers.host,
146
109
  );
147
110
  }
@@ -155,7 +118,7 @@ export async function devserverProxy(config) {
155
118
  "^/wp-(?:admin|includes|content/plugins).*(?:css|js|map)$",
156
119
  );
157
120
  const originRegExp = new RegExp(
158
- `(http:\\\\/\\\\/|http://)${config.proxyUrl.host}`,
121
+ `(http:\\\\/\\\\/|http://)${target.host}`,
159
122
  "gi",
160
123
  );
161
124
 
@@ -1,19 +1,38 @@
1
1
  import { exec } from "node:child_process";
2
2
 
3
- export function findLocalPort() {
4
- // export default () => {
5
- return new Promise((resolve, reject) => {
6
- exec("docker compose port wordpress 80", (error, stdout, stderr) => {
3
+ /**
4
+ * Export this separately so it can be tested. The exec method
5
+ * in findLocalPort will be replaced with a mock.
6
+ */
7
+ export function successHandler(execResponse) {
8
+ const [addr, port] = execResponse.trim().split(":");
9
+ const hostname = addr === "0.0.0.0" ? "localhost" : addr;
10
+ return { hostname, port };
11
+ }
12
+
13
+ export function findLocalPort(serviceName = "wordpress", timeout = 3000) {
14
+ /* v8 ignore start */
15
+ return new Promise((resolve, reject) => {
16
+ exec(`docker compose port ${serviceName} 80`, (error, stdout, stderr) => {
7
17
  if (error) {
8
18
  reject(error);
9
19
  return;
10
20
  }
11
21
 
12
22
  if (stdout) {
13
- const [addr, port] = stdout.trim().split(":");
14
- const hostname = addr === "0.0.0.0" ? "localhost" : addr;
15
- resolve({ hostname, port, stdout: stdout.trim() });
23
+ const { hostname, port } = successHandler(stdout);
24
+ resolve({
25
+ hostname,
26
+ port,
27
+ stdout: stdout.trim(),
28
+ stderr: stderr.trim(),
29
+ });
16
30
  }
31
+ /* v8 ignore stop */
17
32
  });
33
+ setTimeout(() => {
34
+ const timeoutError = new Error("The `exec` command timed out.");
35
+ reject(timeoutError);
36
+ }, timeout);
18
37
  });
19
38
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ideasonpurpose/build-tools-wordpress",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "description": "Build scripts and dependencies for IOP's WordPress development environments.",
5
5
  "homepage": "https://github.com/ideasonpurpose/build-tools-wordpress#readme",
6
6
  "bugs": {
@@ -19,7 +19,8 @@
19
19
  "type": "module",
20
20
  "main": "index.js",
21
21
  "bin": {
22
- "iop-build-port-reporter": "./bin/port-reporter.js"
22
+ "iop-build-port-reporter": "./bin/port-reporter.js",
23
+ "iop-build-zip-archive": "./bin/zip.js"
23
24
  },
24
25
  "directories": {
25
26
  "lib": "lib"
@@ -99,5 +100,9 @@
99
100
  "files": [
100
101
  "README.md"
101
102
  ]
103
+ },
104
+ "devDependencies": {
105
+ "@vitest/coverage-v8": "^1.2.2",
106
+ "vitest": "^1.2.1"
102
107
  }
103
108
  }
@@ -0,0 +1,297 @@
1
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
2
+
3
+ import fs from "fs";
4
+ import dns from "dns";
5
+
6
+ import { devserverProxy } from "../lib/devserver-proxy.js";
7
+ import { findLocalPort } from "../lib/find-local-docker-port.js";
8
+
9
+ const expected = "11.22.33.44";
10
+
11
+ const expectedPort = 56789;
12
+ const expectedHostName = "stella.dog";
13
+ const expectedTarget = `http://${expectedHostName}:${expectedPort}`;
14
+
15
+ vi.mock("../lib/find-local-docker-port.js");
16
+
17
+ let localPort;
18
+
19
+ beforeEach(() => {
20
+ localPort = new Promise((resolve) => {
21
+ resolve({
22
+ port: expectedPort,
23
+ hostname: expectedHostName,
24
+ });
25
+ });
26
+ vi.mocked(findLocalPort).mockReturnValue(localPort);
27
+ });
28
+
29
+ afterEach(() => {
30
+ vi.restoreAllMocks();
31
+ });
32
+ // beforeEach(() => {
33
+ // jest.spyOn(dns, "promises", "get").mockImplementation(() => {
34
+ // return { resolve: async () => [expected] };
35
+ // });
36
+
37
+ // console.log = jest.fn();
38
+ // });
39
+
40
+ // afterEach(() => {
41
+ // jest.clearAllMocks();
42
+ // jest.resetAllMocks();
43
+ // // mockResolve.mockResolvedValue(["11.22.33.44"]);
44
+ // });
45
+
46
+ // disabled because we're now mocking the library
47
+ // test.skip("dns works normally", async () => {
48
+ // const actual = await dns.promises.resolve("apple.com");
49
+ // expect(actual[0]).toMatch(/^17\.253/);
50
+ // });
51
+
52
+ // test("mock dns.promises.resolve", async () => {
53
+ // const actual = await dns.promises.resolve("hello");
54
+ // expect(actual).toBe(expected);
55
+ // });
56
+
57
+ // test("resolve from file", async () => {
58
+ // const actual = await resolveFromFile("wordpress");
59
+ // expect(actual).toBe(expected);
60
+ // });
61
+
62
+
63
+ // test("Send legacy token where there's no wordpress service", async () => {
64
+ // jest.spyOn(dns, "promises", "get").mockImplementation(() => {
65
+ // // console.log("ONLY ONCE");
66
+ // return { resolve: () => new Promise((resolve, reject) => reject()) };
67
+ // });
68
+
69
+ // let proxy =
70
+ // "http://devserver-proxy-token--d939bef2a41c4aa154ddb8db903ce19fff338b61";
71
+
72
+ // const logSpy = jest.spyOn(console, "log");
73
+ // const actual = await devserverProxy({ proxy });
74
+
75
+ // expect(actual).toStrictEqual({});
76
+ // // expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("ONCE"));
77
+ // expect(logSpy).toHaveBeenCalledWith(
78
+ // expect.any(String),
79
+ // expect.stringContaining("devserver-proxy-token"),
80
+ // expect.any(String)
81
+ // );
82
+ // });
83
+
84
+ // test("prefix http onto string", async () => {
85
+ // jest.spyOn(dns, "promises", "get").mockImplementation(() => {
86
+ // return {
87
+ // resolve: () => new Promise((resolve, reject) => resolve(["fake-url"])),
88
+ // };
89
+ // });
90
+
91
+ // let proxy = "placeholder string";
92
+ // const actual = await devserverProxy({ proxy });
93
+ // expect(actual).toHaveProperty("proxy.**.target", "http://fake-url");
94
+ // });
95
+
96
+ // test("fail to prefix http onto string", async () => {
97
+ // jest.spyOn(dns, "promises", "get").mockImplementation(() => {
98
+ // return {
99
+ // resolve: () => new Promise((resolve, reject) => reject()),
100
+ // };
101
+ // });
102
+
103
+ // let proxy = "placeholder string";
104
+ // const actual = await devserverProxy({ proxy });
105
+ // expect(actual).toStrictEqual({});
106
+ // });
107
+
108
+ test("Test proxy settings", async () => {
109
+ let proxy = true;
110
+ const actual = await devserverProxy({ proxy });
111
+ console.log(actual);
112
+ expect(actual).toHaveProperty("proxy.**.target");
113
+ expect(actual.proxy["**"].target).toMatch("stella");
114
+ });
115
+
116
+ test("proxy is bare IP address", async () => {
117
+ let proxy = "4.3.2.1";
118
+ const actual = await devserverProxy({ proxy });
119
+ expect(actual).toHaveProperty("proxy.**.target", "http://4.3.2.1");
120
+ });
121
+
122
+ test("proxy is a url", async () => {
123
+ let proxy = "https://example.com/";
124
+ const actual = await devserverProxy({ proxy });
125
+ expect(actual).toHaveProperty("proxy.**.target", "https://example.com");
126
+ });
127
+
128
+ test("proxy is a plain string", async () => {
129
+ let proxy = "sandwich";
130
+ const actual = await devserverProxy({ proxy });
131
+ expect(actual).toHaveProperty("proxy.**.target", expectedTarget);
132
+ expect(findLocalPort).toHaveBeenCalledWith(proxy);
133
+ });
134
+
135
+ test("Proxy boolean true", async () => {
136
+ vi.mocked(findLocalPort).mockReturnValue(localPort);
137
+ const proxy = true;
138
+ const actual = await devserverProxy({ proxy });
139
+ expect(actual).toHaveProperty("proxy.**.target", expectedTarget);
140
+ });
141
+
142
+ test("Proxy boolean false", async () => {
143
+ const proxy = false;
144
+ expect(await devserverProxy({ proxy })).toStrictEqual({});
145
+ });
146
+
147
+ test("test the returned proxy onError handler", async () => {
148
+ let proxy = "wordpress";
149
+ const logSpy = vi.spyOn(console, "log");
150
+ const actual = await devserverProxy({ proxy: true });
151
+
152
+ // console.log(actual);
153
+ expect(actual).toHaveProperty("proxy.**");
154
+
155
+ const route = actual.proxy["**"];
156
+ let err = new Error("boom");
157
+ err.code = "ECONNRESET";
158
+ route.onError(err);
159
+
160
+ expect(route).toHaveProperty("onError");
161
+ expect(logSpy).toHaveBeenLastCalledWith(
162
+ expect.stringContaining("ECONNRESET"),
163
+ );
164
+
165
+ const writeHead = vi.fn();
166
+ const end = vi.fn();
167
+ const res = { writeHead, end };
168
+ const req = { url: "url" };
169
+
170
+ err = new Error("boom-again ");
171
+ err.code = "Unknown Error Code";
172
+ route.onError(err, req, res);
173
+
174
+ // expect(logSpy).toHaveBeenCalledWith('PROXY ERROR');
175
+ // expect(logSpy.mock.calls[1][0]).toMatch(/^PROXY ERROR/);
176
+ // expect(end.mock.calls[0][0]).toMatch(/^Webpack DevServer/);
177
+ });
178
+
179
+ // test("test proxy's onProxyRes handler", async () => {
180
+ // let proxy = "https://example.com";
181
+ // const logSpy = jest.spyOn(console, "log");
182
+ // const actual = await devserverProxy({ proxy });
183
+
184
+ // const route = actual.proxy["**"];
185
+ // const mockProxyRes = fs.createReadStream(new URL(import.meta.url));
186
+
187
+ // mockProxyRes.headers = {
188
+ // headerKey: "value",
189
+ // host: "example.com",
190
+ // "content-type": "text/html; charset=utf-8",
191
+ // };
192
+ // mockProxyRes.statusCode = "statusCode";
193
+ // mockProxyRes.statusMessage = "statusMessage";
194
+
195
+ // const events = {};
196
+ // jest.spyOn(mockProxyRes, "on").mockImplementation((event, handler) => {
197
+ // events[event] = handler;
198
+ // return mockProxyRes;
199
+ // });
200
+
201
+ // const setHeader = jest.fn();
202
+ // const end = jest.fn();
203
+
204
+ // const res = { setHeader, end };
205
+ // const req = {
206
+ // headers: { host: "req.headers.host" },
207
+ // path: "path",
208
+ // };
209
+
210
+ // route.onProxyRes(mockProxyRes, req, res);
211
+ // events.data(Buffer.from("A string with 28 characters."));
212
+ // events.end();
213
+
214
+ // expect(res.statusCode).toBe(mockProxyRes.statusCode);
215
+ // expect(res.statusMessage).toBe(mockProxyRes.statusMessage);
216
+ // expect(req.headers.host).toBe("req.headers.host");
217
+ // expect(setHeader).toHaveBeenLastCalledWith("Content-Length", 28);
218
+ // });
219
+
220
+ // test("test proxy's onProxyRes handler onEnd passthrough", async () => {
221
+ // let proxy = "https://example.com";
222
+ // // const logSpy = jest.spyOn(console, "log");
223
+ // const actual = await devserverProxy({ proxy });
224
+
225
+ // const route = actual.proxy["**"];
226
+ // const mockProxyRes = fs.createReadStream(new URL(import.meta.url));
227
+
228
+ // console.log("hello");
229
+ // mockProxyRes.headers = {
230
+ // headerKey: "value",
231
+ // host: "example.com",
232
+ // "Content-Length": 123,
233
+ // };
234
+
235
+ // const events = {};
236
+ // jest.spyOn(mockProxyRes, "on").mockImplementation((event, handler) => {
237
+ // events[event] = handler;
238
+ // return mockProxyRes;
239
+ // });
240
+
241
+ // const setHeader = jest.fn();
242
+ // const end = jest.fn();
243
+
244
+ // const res = { setHeader, end };
245
+ // const req = {
246
+ // headers: { host: "req.headers.host" },
247
+ // path: "/wp-admin/fake.css",
248
+ // };
249
+
250
+ // route.onProxyRes(mockProxyRes, req, res);
251
+ // events.end();
252
+
253
+ // expect(end).toHaveBeenCalled();
254
+ // });
255
+
256
+ // test("proxy should rewrite http:// and http:\\/\\/", async () => {
257
+ // let proxy = "wordpress";
258
+
259
+ // const logSpy = jest.spyOn(console, "log");
260
+ // const actual = await devserverProxy({ proxy });
261
+ // const route = actual.proxy["**"];
262
+ // const mockProxyRes = fs.createReadStream(new URL(import.meta.url));
263
+
264
+ // mockProxyRes.headers = {
265
+ // headerKey: "value",
266
+ // host: "example.com",
267
+ // "content-type": "text/html; charset=utf-8",
268
+ // };
269
+
270
+ // const events = {};
271
+ // jest.spyOn(mockProxyRes, "on").mockImplementation((event, handler) => {
272
+ // events[event] = handler;
273
+ // return mockProxyRes;
274
+ // });
275
+
276
+ // const setHeader = jest.fn();
277
+ // const end = jest.fn();
278
+
279
+ // const res = { setHeader, end };
280
+ // const req = {
281
+ // headers: { host: "req.headers.host" },
282
+ // path: "path",
283
+ // };
284
+
285
+ // route.onProxyRes(mockProxyRes, req, res);
286
+ // events.data(Buffer.from("http://11.22.33.44\n"));
287
+ // events.data(Buffer.from("http:\\/\\/11.22.33.44\n"));
288
+ // events.end();
289
+
290
+ // expect(end.mock.calls[0][0].toString("utf8")).toMatch(
291
+ // /http:\/\/req.headers.host/
292
+ // );
293
+
294
+ // expect(end.mock.calls[0][0].toString("utf8")).toMatch(
295
+ // /http:\\\/\\\/req.headers.host/
296
+ // );
297
+ // });
@@ -0,0 +1,39 @@
1
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
2
+
3
+ import { exec } from "node:child_process";
4
+
5
+ import {
6
+ findLocalPort,
7
+ successHandler,
8
+ } from "../lib/find-local-docker-port.js";
9
+
10
+ vi.mock("node:child_process");
11
+
12
+ afterEach(() => {
13
+ vi.restoreAllMocks();
14
+ });
15
+
16
+ // Proof of concept: None of the tests matter if this doesn't work
17
+ test("Mock child_process.exec", async () => {
18
+ const serviceName = "wp";
19
+ await findLocalPort(serviceName, 200).catch((err) => {
20
+ expect(err).toBeInstanceOf(Error);
21
+ });
22
+
23
+ expect(exec).toHaveBeenCalledWith(
24
+ `docker compose port ${serviceName} 80`,
25
+ expect.any(Function),
26
+ );
27
+
28
+ expect(exec).toHaveBeenCalledTimes(1);
29
+ });
30
+
31
+ test("successHandler returns", () => {
32
+ let actual;
33
+ actual = successHandler("0.0.0.0:55555\n");
34
+ expect(actual).toHaveProperty("port", "55555");
35
+ expect(actual).toHaveProperty("hostname", "localhost");
36
+
37
+ actual = successHandler("example.com:55555\n");
38
+ expect(actual).toHaveProperty("hostname", "example.com");
39
+ });
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ threads: false,
6
+ coverage: {
7
+ enabled: true,
8
+ reportOnFailure: true,
9
+ reporter: ["lcov", "clover", "json", "text"],
10
+ },
11
+ // This project doesn't use a bundler, so disable
12
+ // deps.interopDefault. See note at the end of docs:
13
+ // @link https://vitest.dev/config/#deps-interopdefault
14
+ deps: {
15
+ interopDefault: false,
16
+ },
17
+ },
18
+ });