Skip to content

Commit

Permalink
fix: dont require config file
Browse files Browse the repository at this point in the history
Fixes a couple of issues with config files in websnacks projects.

First, config files are no longer required and the dev and build
commands will no longer error out if a websnacks.ts/js file doesn't
exist.

Second, all optional user config params are now actually optional -
before some parameters were assumed to exist and would error out if not
present (e.g. the "watch" parameter).

Finally, e2e tests were added to prevent regressions on these issues
and test helpers were extracted to a separate file.
  • Loading branch information
mgeorgehansen authored Jun 10, 2020
1 parent 98f761a commit 5520bb3
Show file tree
Hide file tree
Showing 5 changed files with 410 additions and 120 deletions.
57 changes: 43 additions & 14 deletions src/cli/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,21 @@

import { existsSync, promises as fs, watch } from "fs";
import * as http from "http";
import * as net from "net";
import * as path from "path";

import { renderSite } from "../../build";
import { Config, loadConfig } from "../../config";
import { Command, UsageError } from "../types";

const SERVER_PORT = 8080;
const DEFAULT_SERVER_PORT = 8080;

const injectLiveReloadScript = (htmlContents: string): string =>
const injectLiveReloadScript = (htmlContents: string, port: number): string =>
htmlContents.replace(
"</html>",
`
<script>
const ws = new WebSocket("ws://127.0.0.1:${SERVER_PORT}");
const ws = new WebSocket("ws://127.0.0.1:${port}");
ws.onmessage = function() {
console.log('dev server requested reload, reloading...');
location.reload();
Expand Down Expand Up @@ -118,8 +119,21 @@ const guessMimeType = (ext: string): string => {
return mimeType;
};

const serve = (publicDir: string): http.Server => {
const server = http.createServer(async (req, res) => {
const portFromServer = (server: Pick<net.Server, "address">): number => {
const addrInfo = server.address();
if (addrInfo == null) {
throw new Error(`server address is null (this should never happen!)`);
}
if (typeof addrInfo === "string") {
throw new Error(
`server address is a string (this should never happen!)`
);
}
return addrInfo.port;
};

const startHttpServer = async (publicDir: string): Promise<http.Server> => {
const httpServer = http.createServer(async (req, res) => {
if (req.url == null) {
res.writeHead(404);
res.end();
Expand All @@ -144,18 +158,36 @@ const serve = (publicDir: string): http.Server => {
}
const mimeType = guessMimeType(reqExt);
if (mimeType === "text/html") {
contents = injectLiveReloadScript(contents.toString("utf8"));
const port = portFromServer(req.socket);
contents = injectLiveReloadScript(contents.toString("utf8"), port);
}
res.writeHead(200, {
"Content-Type": mimeType,
});
res.end(contents);
});
return server;
const listen = async (port?: number): Promise<string> =>
new Promise((resolve, reject) => {
httpServer
.once("error", (error) => reject(error))
.once("listening", () => resolve())
.listen(port);
});
try {
await listen(DEFAULT_SERVER_PORT);
} catch (error) {
if (error.code !== "EADDRINUSE") {
throw error;
}
await listen();
}
const port = portFromServer(httpServer);
console.log(`Listening at http://127.0.0.1:${port}`);
return httpServer;
};

const startWebSocketServer = async (
server: http.Server
httpServer: http.Server
): Promise<import("ws").Server | undefined> => {
// Attempt to load the ws module, aborting if it isn't available.
let ws;
Expand All @@ -168,7 +200,7 @@ const startWebSocketServer = async (
console.warn(`'ws' module not found, live-reloading will be disabled`);
return;
}
const wsServer = new ws.Server({ server });
const wsServer = new ws.Server({ server: httpServer });
wsServer.on("connection", () => {
console.log("connected to dev site");
});
Expand Down Expand Up @@ -244,15 +276,12 @@ const devCommand: Command = {
};
const config = await rebuild();
const { outDir } = config.paths;
const httpServer = serve(outDir);
const httpServer = await startHttpServer(outDir);
const wsServer = await startWebSocketServer(httpServer);
httpServer.listen(SERVER_PORT, () => {
console.log(`Listening at http://127.0.0.1:${SERVER_PORT}`);
});
const watchedFolders = config.watch.filter((filePath) =>
existsSync(filePath)
);
watchFolders(watchedFolders, async (event, filePath) => {
await watchFolders(watchedFolders, async (event, filePath) => {
console.log(`${filePath}:${event} triggering rebuild...`);
await rebuild();
if (wsServer != null) {
Expand Down
33 changes: 23 additions & 10 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,31 @@ const noop = () => {};
* @return Fully-realized configuration.
*/
export const loadConfig = async (rootDir: string): Promise<Config> => {
const configPath = require.resolve(path.resolve(rootDir, "websnacks"));
purgeModuleAndDepsFromCache(configPath);
// TODO: validate user config.
const userConfig = await import(configPath);
let configPath;
let userConfig: UserConfig = {};
// Attempt to load a websnacks.ts/js file in rootDir.
try {
configPath = require.resolve(path.resolve(rootDir, "websnacks"));
purgeModuleAndDepsFromCache(configPath);
// TODO: validate user config.
userConfig = await import(configPath);
} catch (error) {
// Use default config;
}
const outDir = path.join(rootDir, "public");
const pagesDir = path.join(rootDir, "pages");
const staticAssetsDir = path.join(rootDir, "static");

const watch = [pagesDir, staticAssetsDir];
if (configPath != null) {
watch.push(path.relative(rootDir, configPath));
}
if (userConfig.watch != null) {
for (const userWatch of userConfig.watch) {
watch.push(path.relative(rootDir, userWatch));
}
}

return {
paths: {
rootDir,
Expand All @@ -76,11 +94,6 @@ export const loadConfig = async (rootDir: string): Promise<Config> => {
afterSiteRender: noop,
...userConfig.hooks,
},
watch: [
...userConfig.watch.map((p: string) => path.relative(rootDir, p)),
path.relative(rootDir, configPath),
pagesDir,
staticAssetsDir,
],
watch,
};
};
121 changes: 121 additions & 0 deletions test/e2e/build.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

import { promises as fs } from "fs";
import * as path from "path";

import { runCommand, WEBSNACKS_BIN_PATH, WEBSNACKS_REPO_ROOT, withTempDir } from "../helpers/e2e";
import { testSuite } from "../lib";

testSuite("build command", ({ test }) => {
test("runs without throwing error", async () => {
await withTempDir(async (tempDirPath) => {
await fs.writeFile(
path.join(tempDirPath, "tsconfig.json"),
JSON.stringify({
compilerOptions: {
esModuleInterop: true,
module: "CommonJS",
moduleResolution: "node",
jsx: "react",
jsxFactory: "createElement",
target: "ES2018",
lib: ["ES2018"],
strict: true,
noUnusedLocals: true,
noUnusedParameters: true,
noImplicitReturns: true,
noFallthroughCasesInSwitch: true,
},
include: ["components/**/*", "pages/**/*"],
}),
{
encoding: "utf8",
}
);
await fs.writeFile(
path.join(tempDirPath, "websnacks.ts"),
`
import { Config } from "${WEBSNACKS_REPO_ROOT}";
const config: Config = {
watch: [],
};
export = config;
`,
{
encoding: "utf8",
}
);
const pagesPath = path.join(tempDirPath, "pages");
await fs.mkdir(pagesPath);
await fs.writeFile(
path.join(pagesPath, "index.tsx"),
`
import { createElement } from "${WEBSNACKS_REPO_ROOT}";
export const page = () => <html />;
`,
{
encoding: "utf8",
}
);
const cmd = runCommand(
"node",
[WEBSNACKS_BIN_PATH, "-r", "ts-node/register", "build"],
{
cwd: tempDirPath,
}
);
await cmd.complete;
});
});

test("works without config file", async () => {
await withTempDir(async (tempDirPath) => {
await fs.writeFile(
path.join(tempDirPath, "tsconfig.json"),
JSON.stringify({
compilerOptions: {
esModuleInterop: true,
module: "CommonJS",
moduleResolution: "node",
jsx: "react",
jsxFactory: "createElement",
target: "ES2018",
lib: ["ES2018"],
strict: true,
noUnusedLocals: true,
noUnusedParameters: true,
noImplicitReturns: true,
noFallthroughCasesInSwitch: true,
},
include: ["components/**/*", "pages/**/*"],
}),
{
encoding: "utf8",
}
);
const pagesPath = path.join(tempDirPath, "pages");
await fs.mkdir(pagesPath);
await fs.writeFile(
path.join(pagesPath, "index.tsx"),
`
import { createElement } from "${WEBSNACKS_REPO_ROOT}";
export const page = () => <html />;
`,
{
encoding: "utf8",
}
);
const cmd = runCommand(
"node",
[WEBSNACKS_BIN_PATH, "-r", "ts-node/register", "build"],
{
cwd: tempDirPath,
}
);
await cmd.complete;
});
});
});
Loading

0 comments on commit 5520bb3

Please sign in to comment.