Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: hot reload on file changes (fix #485) #486

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ file like the following and adapt to your needs:
"version": "1.0.0",
"license": "MIT",
"scripts": {
"dev": "techradar dev",
"build": "techradar build",
"serve": "techradar serve"
},
Expand Down Expand Up @@ -214,6 +215,10 @@ Run `npm run build` to build the radar and upload the files of the `./build` fol
You can view a development version of the radar by running `npm run serve` and open the radar in
your browser at `http://localhost:3000/techradar` or the path you specified via `basePath`.

As an alternative to `npm run build`, you can start the dev server via `npm run dev`, which will watch for changes in
the `radar` and `public` folder and rebuild the radar automatically. The dev server serves on
`http://localhost:3000/techradar` or the path you specified via `basePath`.

## Advanced styling with `custom.css`

If you need to customize the radar's styles, you can add custom CSS rules to the `custom.css` file.
Expand Down
233 changes: 168 additions & 65 deletions bin/techradar.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,55 @@

const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");
const { execSync, spawn } = require("child_process");
const crypto = require("crypto");
const chokidar = require("chokidar");

const CWD = process.cwd();
const BUILDER_DIR = path.join(CWD, ".techradar");
const SOURCE_DIR = path.join(CWD, "node_modules", "aoe_technology_radar");
const HASH_FILE = path.join(BUILDER_DIR, "hash");

const PARAMETER = process.argv[2]; // "build" or "serve"
const PARAMETER = process.argv[2]; // "build", "serve" or "dev"

// We need this mapping on several places, so let's maintain it centrally
const fileMapping = {
"radar-dir": {
isDirectory: true,
bootstrappingSource: path.join(SOURCE_DIR, "data", "radar"),
buildSource: path.join(CWD, "radar"),
buildTarget: path.join(BUILDER_DIR, "data", "radar"),
rebuildOnChange: true,
},
"public-dir": {
isDirectory: true,
bootstrappingSource: path.join(SOURCE_DIR, "public"),
buildSource: path.join(CWD, "public"),
buildTarget: path.join(BUILDER_DIR, "public"),
rebuildOnChange: true,
},
"config-file": {
isDirectory: false,
bootstrappingSource: path.join(SOURCE_DIR, "data", "config.default.json"),
buildSource: path.join(CWD, "config.json"),
buildTarget: path.join(BUILDER_DIR, "data", "config.json"),
rebuildOnChange: true,
},
"about-file": {
isDirectory: false,
bootstrappingSource: path.join(SOURCE_DIR, "data", "about.md"),
buildSource: path.join(CWD, "about.md"),
buildTarget: path.join(BUILDER_DIR, "data", "about.md"),
rebuildOnChange: true,
},
"customcss-file": {
isDirectory: false,
bootstrappingSource: path.join(SOURCE_DIR, "src", "styles", "custom.css"),
buildSource: path.join(CWD, "custom.css"),
buildTarget: path.join(BUILDER_DIR, "src", "styles", "custom.css"),
rebuildOnChange: true,
},
};

function info(message) {
console.log(`\x1b[32m${message}\x1b[0m`);
Expand All @@ -25,51 +65,35 @@ function error(message) {
process.exit(1);
}

function bootstrap() {
if (!fs.existsSync(path.join(CWD, "radar"))) {
warn(
"Could not find radar directory. Created a bootstrap radar directory in your current working directory. Feel free to customize it.",
);
fs.cpSync(path.join(SOURCE_DIR, "data", "radar"), path.join(CWD, "radar"), {
recursive: true,
});
}

if (!fs.existsSync(path.join(CWD, "public"))) {
warn(
"Could not find public directory. Created a public radar directory in your current working directory.",
);
fs.cpSync(path.join(SOURCE_DIR, "public"), path.join(CWD, "public"), {
recursive: true,
});
}

if (!fs.existsSync(path.join(CWD, "config.json"))) {
warn(
"Could not find a config.json. Created a bootstrap config.json in your current working directory. Customize it to your needs.",
);
fs.copyFileSync(
path.join(SOURCE_DIR, "data", "config.default.json"),
path.join(CWD, "config.json"),
);
}
function debounce(func, wait) {
let timeout;
return function (...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}

if (!fs.existsSync(path.join(CWD, "about.md"))) {
warn(
"Could not find a about.md. Created a bootstrap about.md in your current working directory. Customize it to your needs.",
);
fs.copyFileSync(
path.join(SOURCE_DIR, "data", "about.md"),
path.join(CWD, "about.md"),
);
}
function bootstrap() {
for (const file of Object.values(fileMapping)) {
const relativeBuildPath = `./${path.relative(CWD, file.buildSource)}`;

if (!fs.existsSync(path.join(CWD, "custom.css"))) {
warn("Created a bootstrap custom.css in your current working directory.");
fs.copyFileSync(
path.join(SOURCE_DIR, "src", "styles", "custom.css"),
path.join(CWD, "custom.css"),
);
if (!fs.existsSync(file.buildSource) && file.isDirectory) {
warn(
`Could not find the directory ${relativeBuildPath}. Created a bootstrap directory in your current working directory. Feel free to customize it.`,
);
fs.cpSync(file.bootstrappingSource, file.buildSource, {
recursive: true,
});
} else if (!fs.existsSync(file.buildSource) && !file.isDirectory) {
warn(
`Could not find ${relativeBuildPath}. Created a bootstrap file in your current working directory. Feel free to customize it. Customize it to your needs.`,
);
fs.copyFileSync(file.bootstrappingSource, file.buildSource);
}
}
}

Expand Down Expand Up @@ -119,27 +143,20 @@ if (RECREATE_DIR) {
bootstrap();

try {
if (fs.existsSync(path.join(BUILDER_DIR, "data", "radar"))) {
fs.rmSync(path.join(BUILDER_DIR, "data", "radar"), { recursive: true });
for (const file of Object.values(fileMapping)) {
// Clean up a directory if it exists
if (fs.existsSync(file.buildTarget) && file.isDirectory) {
fs.rmSync(file.buildTarget, { recursive: true });
}

if (file.isDirectory) {
fs.cpSync(file.buildSource, file.buildTarget, {
recursive: true,
});
} else if (!file.isDirectory) {
fs.copyFileSync(file.buildSource, file.buildTarget);
}
}
fs.cpSync(path.join(CWD, "radar"), path.join(BUILDER_DIR, "data", "radar"), {
recursive: true,
});
fs.cpSync(path.join(CWD, "public"), path.join(BUILDER_DIR, "public"), {
recursive: true,
});
fs.copyFileSync(
path.join(CWD, "about.md"),
path.join(BUILDER_DIR, "data", "about.md"),
);
fs.copyFileSync(
path.join(CWD, "custom.css"),
path.join(BUILDER_DIR, "src", "styles", "custom.css"),
);
fs.copyFileSync(
path.join(CWD, "config.json"),
path.join(BUILDER_DIR, "data", "config.json"),
);
process.chdir(BUILDER_DIR);
} catch (e) {
error(e.message);
Expand All @@ -162,3 +179,89 @@ if (PARAMETER === "build") {
info(`Copying techradar to ${path.join(CWD, "build")}`);
fs.renameSync(path.join(BUILDER_DIR, "out"), path.join(CWD, "build"));
}

if (PARAMETER === "dev") {
info("Developing techradar");

// Let's spawn a child process to run the dev server, so that it doesn't block our main thread
// The process has to be killed whenever the main thread is killed
const NODEDEV_CHILD_PROCESS = spawn("npm", ["run", "dev"], {
stdio: "inherit",
detached: false,
});
process.on("exit", () => {
NODEDEV_CHILD_PROCESS.kill();
});
// Initialize watching of source directory and files with chokidar
const filesToWatch = Object.values(fileMapping).filter(
(fileConfig) => fileConfig.rebuildOnChange && !fileConfig.isDirectory,
);
const dirsToWatch = Object.values(fileMapping).filter(
(fileConfig) => fileConfig.rebuildOnChange && fileConfig.isDirectory,
);

const srcFilesToWatch = [...filesToWatch, ...dirsToWatch].map(
(fileConfig) => fileConfig.buildSource,
);
const watcher = chokidar.watch(srcFilesToWatch, {
ignored: /^\./, // Ignore dotfiles
persistent: true,
ignoreInitial: true,
depth: 5,
});

// Rebuild the data when a change is detected
// Debounce the function to avoid multiple rebuilds at the same time
const rebuildData = debounce(({ path: changedPath }) => {
try {
const fileConfig = Object.values(fileMapping).find(
(fileConfig) =>
fileConfig.buildSource === changedPath && !fileConfig.isDirectory,
);
const dirConfig = Object.values(fileMapping).find(
(fileConfig) =>
changedPath.startsWith(fileConfig.buildSource) &&
fileConfig.isDirectory,
);

// First copy over the changed file to the build directory

// Is it a file that has been changed?
if (fileConfig) {
const relativeBuildSrc = `./${path.relative(CWD, changedPath)}`;

info(`${relativeBuildSrc} changed`);
fs.copyFileSync(fileConfig.buildSource, fileConfig.buildTarget);
} else if (dirConfig) {
// Is it a directory that has been changed?
const relativeBuildSrc = `./${path.relative(CWD, changedPath)}`;
const relativeTargetPath = `./${path.relative(CWD, dirConfig.buildTarget)}`;
info(
`${relativeBuildSrc} changed, going to update all files in ${relativeTargetPath}.`,
);
if (fs.existsSync(dirConfig.buildTarget)) {
fs.rmSync(dirConfig.buildTarget, { recursive: true });
}
fs.cpSync(dirConfig.buildSource, dirConfig.buildTarget, {
recursive: true,
});
} else {
info(
"Unknown file changed. Won't be copied.",
`./${path.relative(CWD, changedPath)}`,
);
}

// Then rebuild the json files, so that the next.js dev-server picks up the changes
execSync("npm run build:data", { stdio: "inherit" });
} catch (e) {
error("Unable to reload updated data. Please restart the server.", e);
}
}, 1000);

// Event handlers
watcher
.on("add", (path) => rebuildData({ path }))
.on("change", (path) => rebuildData({ path }))
.on("unlink", (path) => rebuildData({ path }));
}
33 changes: 31 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"chokidar": "4.0.1",
"clsx": "^2.1.1",
"eslint": "^8.57.0",
"eslint-config-next": "14.2.4",
Expand Down