Skip to content

Commit f033396

Browse files
authored
Merge pull request #18 from BretHudson/v0.4.0
v0.4.0
2 parents e0dbb9e + d706942 commit f033396

File tree

8 files changed

+1065
-1515
lines changed

8 files changed

+1065
-1515
lines changed

.github/workflows/playwright.yml

Lines changed: 29 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,40 @@
11
name: Playwright Tests
22
on:
33
push:
4-
branches: [ main, master ]
4+
branches: [main, master]
55
pull_request:
6-
branches: [ main, master ]
6+
branches: [main, master]
77
jobs:
88
test:
99
env:
1010
NODE_ENV: development
1111
timeout-minutes: 60
1212
runs-on: ubuntu-latest
1313
steps:
14-
- uses: actions/checkout@v4
15-
- uses: actions/setup-node@v4
16-
with:
17-
node-version: lts/*
18-
- name: Install dependencies
19-
run: npm ci --include=dev
20-
# adapted from https://github.com/liam-hq/liam/pull/716/files
21-
- name: Cache Playwright Browsers
22-
id: playwright-cache
23-
uses: actions/cache@v4
24-
with:
25-
path: ~/.cache/ms-playwright
26-
key: playwright-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
27-
restore-keys: |
28-
playwright-${{ runner.os }}-
29-
- name: Install Playwright Browsers
30-
if: steps.playwright-cache.outputs.cache-hit != 'true'
31-
run: npx playwright install --with-deps
32-
- name: Run Playwright tests
33-
run: npx playwright test
34-
- uses: actions/upload-artifact@v4
35-
if: ${{ !cancelled() }}
36-
with:
37-
name: playwright-report
38-
path: playwright-report/
39-
retention-days: 30
14+
- uses: actions/checkout@v4
15+
- uses: actions/setup-node@v4
16+
with:
17+
node-version: lts/*
18+
- uses: pnpm/action-setup@v4
19+
- name: Install dependencies
20+
run: pnpm i
21+
# adapted from https://github.com/liam-hq/liam/pull/716/files
22+
- name: Cache Playwright Browsers
23+
id: playwright-cache
24+
uses: actions/cache@v4
25+
with:
26+
path: ~/.cache/ms-playwright
27+
key: playwright-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
28+
restore-keys: |
29+
playwright-${{ runner.os }}-
30+
- name: Install Playwright Browsers
31+
if: steps.playwright-cache.outputs.cache-hit != 'true'
32+
run: npx playwright install --with-deps
33+
- name: Run Playwright tests
34+
run: npx playwright test
35+
- uses: actions/upload-artifact@v4
36+
if: ${{ !cancelled() }}
37+
with:
38+
name: playwright-report
39+
path: playwright-report/
40+
retention-days: 30

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
node_modules
22
*.bat
33

4+
package-lock.json
5+
46
.env
57

68
# Playwright

README.md

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Web Hot Reloader
22

3-
Automatically update your CSS in real-time, without having to add a single line of code to your project's codebase.
3+
Automatically reload your HTML, CSS, and image resources within the browsre in real-time, without having to add a single line of code to your project's codebase.
44

55
- 🪶 Lightweight
66
- 🌻 Works with vanilla JS
@@ -12,7 +12,7 @@ Automatically update your CSS in real-time, without having to add a single line
1212

1313
Frameworks like [React](https://github.com/facebook/react) have spoiled us with features like HMR (hot module reload), reducing developer friction when making small, incremental changes. Wouldn't it be nice to have a lightweight solution for vanilla JS projects, without having to use a framework?
1414

15-
And so, Web Hot Reloader was born. It consists of two parts: a Node.js process that lists to a directory for CSS file changes, and a script to inject websocket code into your web page.
15+
And so, Web Hot Reloader was born. It consists of two parts: a Node.js process that watches to a directory for file changes, and a script to inject WebSocket code into your web page.
1616

1717
## Setup
1818

@@ -23,21 +23,23 @@ Prerequisites:
2323
- [Node.js](https://nodejs.org/en/download)
2424
- Clone the repo
2525

26-
### Running the program
26+
Ensure `node_modules` are installed with `pnpm i` (`npm i` will also work, but will generate a `package-lock.json` file)
2727

28-
In my project, there is a `/css` folder containing all the CSS files. (See: [Current limitations](#current-limitations))
28+
### Running the program
2929

30-
To initialize the hot reloader for this directory, we can type the following:
30+
To initialize the hot reloader for this project, we can type the following:
3131

3232
```cmd
33-
node app C:\xampp\app\brethudson\css
33+
node app C:\xampp\app\brethudson
3434
```
3535

3636
Tip: You can override `PORT` (default `3008`) and `NODE_ENV` (default `production`) in your environment variables.
3737

3838
### Getting your browser to listen
3939

40-
Now, while you _could_ copy/paste [public/reloader.js](public/reloader.js) into your project and include it in every page that you want to use it on, that doesn't scale well, and also means that you would need to ensure it gets removed for the production/live site.
40+
(Coming soon: built-in proxy server)
41+
42+
Now, while you _could_ copy/paste [public/reloader.js](public/reloader.js) into your project and include it in every page that you want to use it on, that doesn't scale well, and also means that you would need to ensure it gets removed in the production/live site.
4143

4244
What I suggest is grabbing a browser extension such as [Tampermonkey](https://www.tampermonkey.net/), which will allow us to run scripts on certain pages!
4345

@@ -47,16 +49,19 @@ Within this file, every `@match` line will correspond to a URL path to match. Fo
4749

4850
## Current limitations
4951

50-
- Only CSS files are supported
51-
- No recursive folder watching
52+
- Only HTML, CSS, and image files are supported
53+
- ~~No recursive folder watching~~
5254
- JS support is not currently on the roadmap
55+
- No automatic script injection
5356

5457
## Future plans
5558

5659
There really aren't any specific ones! Some things I would like to add:
5760

58-
- Recursive listening (`C:/project/css` would be able to listen to `C:/project/css/sub-folder` as well - meaning it would be possible to just use `C:/project` as the directory)
59-
- <img> support
60-
- JS support
61-
- Project configuration files (`reloader.json` or `reloader.config.js`)
62-
- Publish to `npm`
61+
- [x] ~~Recursive listening (`C:/project/css` would be able to listen to `C:/project/css/sub-folder` as well - meaning it would be possible to just use `C:/project` as the directory)~~
62+
- [x] ~~HTML reloading support~~
63+
- [x] ~~`<img>`/favicon reloading support~~
64+
- [ ] Proxy server with hot reloader injection magic
65+
- [ ] Project configuration files (`reloader.json` or `reloader.config.js`)
66+
- [ ] Publish to `npm`
67+
- [ ] JS support (maybe?)

app/index.js

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import http from 'node:http';
44
import path from 'node:path';
55
import { fileURLToPath } from 'node:url';
66

7+
import { default as ignore } from 'ignore';
78
import { Server } from 'socket.io';
89
import { instrument } from '@socket.io/admin-ui';
910

@@ -22,6 +23,93 @@ const [_nodePath, _scriptPath, ...args] = process.argv;
2223
const [_watchPath] = args;
2324
const watchPath = path.join(_watchPath);
2425

26+
const pathsToIgnore = [
27+
'.git',
28+
'.log',
29+
'.nyc_output',
30+
'.sass-cache',
31+
'.yarn',
32+
'bower_components',
33+
'coverage',
34+
'node_modules',
35+
];
36+
37+
const getKeyFromPath = (curPath) => {
38+
const dirPath = path.join(watchPath, path.relative(watchPath, curPath));
39+
return path.relative(watchPath, dirPath);
40+
};
41+
42+
const dirIgnoreMap = new Map();
43+
const dirGitignoreMap = new Map();
44+
const addGitignore = (curPath) => {
45+
const dirPath = path.join(watchPath, path.relative(watchPath, curPath));
46+
const filePath = path.join(dirPath, '.gitignore');
47+
48+
const key = getKeyFromPath(dirPath);
49+
if (dirIgnoreMap.has(key)) return;
50+
51+
let content = null;
52+
const ig = ignore();
53+
if (fs.existsSync(filePath)) {
54+
content = [...pathsToIgnore, fs.readFileSync(filePath, 'utf-8')].join('\n');
55+
ig.add(content);
56+
}
57+
dirIgnoreMap.set(key, ig);
58+
dirGitignoreMap.set(key, content);
59+
return Boolean(content);
60+
};
61+
62+
const ignores = (_filePath) => {
63+
const filePath = path.join(watchPath, path.relative(watchPath, _filePath));
64+
65+
const dirPath = path.dirname(filePath);
66+
const baseName = path.basename(filePath);
67+
const key = getKeyFromPath(dirPath + path.sep);
68+
69+
if (key === '') {
70+
const ig = dirIgnoreMap.get(key);
71+
const result = ig?.ignores(baseName) ?? false;
72+
return result;
73+
} else {
74+
const dirs = [''].concat(key.split(path.sep));
75+
for (let d = 0; d < dirs.length - 1; ++d) {
76+
const ig = dirIgnoreMap.get(dirs[d]);
77+
if (ig?.ignores(dirs[d + 1])) return true;
78+
}
79+
const ig = dirIgnoreMap.get(dirs.at(-1));
80+
if (ig?.ignores(baseName)) return true;
81+
}
82+
83+
return false;
84+
};
85+
86+
const scanForGitignore = (dir) => {
87+
const dirPath = path.join(watchPath, dir);
88+
89+
// scan for .gitignore first!
90+
if (addGitignore(dirPath)) {
91+
console.log(
92+
`\tParsed "${path.join(
93+
path.relative(watchPath, dirPath),
94+
'.gitignore',
95+
)}"`,
96+
);
97+
}
98+
99+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
100+
for (const entry of entries) {
101+
if (entry.name === '.gitignore') continue;
102+
103+
const parentPath = path.relative(watchPath, entry.parentPath);
104+
const filePath = path.join(parentPath, entry.name);
105+
if (ignores(path.join(watchPath, filePath))) continue;
106+
107+
if (!entry.isFile()) scanForGitignore(filePath);
108+
}
109+
};
110+
111+
scanForGitignore('');
112+
25113
const publicPath = path.join(__dirname, '../public');
26114
const server = http.createServer((req, res) => {
27115
res.setHeader('Access-Control-Allow-Origin', '*');
@@ -63,7 +151,7 @@ instrument(io, { auth: false });
63151

64152
let lastJsUpdate = Date.now();
65153
io.on('connection', (client) => {
66-
const { origin: clientOrigin, pathName, assets } = client.handshake.query;
154+
const { origin: clientOrigin, pathName } = client.handshake.query;
67155

68156
// TODO(bret): What about .php? or other files?
69157
const paths = [
@@ -123,16 +211,16 @@ const imageExtensions = [
123211
];
124212

125213
const fileToEventMap = {
126-
'.css': 'css-update',
127214
'.html': 'html-update',
128-
...Object.fromEntries(imageExtensions.map((e) => [e, 'image-update'])),
215+
'.css': 'asset-update',
216+
...Object.fromEntries(imageExtensions.map((e) => [e, 'asset-update'])),
129217
};
130218

131219
const supportedFileExt = Object.keys(fileToEventMap);
132220

133221
const sendUpdate = (eventType, fileName, contents) => {
134222
const ext = path.extname(fileName);
135-
const event = fileToEventMap[ext];
223+
let event = fileToEventMap[ext];
136224
if (!event) return;
137225
const room = path.join(fileName);
138226
io.sockets.to(room).emit(event, { fileName, contents });
@@ -177,14 +265,15 @@ const retry = async (callback) => {
177265
throw error;
178266
};
179267

180-
// TODO(bret): Do not commit recursive!!!
181268
fs.watch(watchPath, { recursive: true }, async (eventType, fileName) => {
182269
if (!fileName) return;
183270
if (eventType === 'rename') return;
184271

185272
if (!supportedFileExt.includes(path.extname(fileName))) return;
186273

187274
const filePath = path.join(watchPath, fileName);
275+
if (ignores(filePath)) return;
276+
188277
if (!fs.existsSync(filePath)) return;
189278

190279
await retry(async () => {

0 commit comments

Comments
 (0)