Skip to content

Commit 71270e1

Browse files
authored
feat: adding defineConfig and reroute function exports (#66)
* feat: adding defineConfig and reroute function exports * chore: add changset * chore: update README.md * chore: update readme
1 parent 743d41f commit 71270e1

File tree

7 files changed

+346
-15
lines changed

7 files changed

+346
-15
lines changed

.changeset/lovely-rats-glow.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"@heymp/scratchpad": minor
3+
---
4+
5+
Add config and playwright rerouting helpers
6+
7+
Export `defineConfig`, `rerouteUrl`, `rerouteDocument`.
8+
9+
Example:
10+
11+
```ts
12+
import * as Scratchpad from '@heymp/scratchpad';
13+
14+
export default Scratchpad.defineConfig({
15+
devtools: true,
16+
url: 'https://www.redhat.com/en',
17+
playwright: async (args) => {
18+
const { page } = args;
19+
await Scratchpad.rerouteDocument(page, './pages');
20+
await Scratchpad.rerouteUrl(page, {
21+
type: 'path',
22+
target: '**/rh-cta/rh-cta.js',
23+
source: './rh-cta.js'
24+
})
25+
}
26+
});
27+
```

.gitignore

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
node_modules
1+
# Dependencies
2+
node_modules/
23

3-
src/**.js
4-
src/**.d.ts
5-
src/**.d.ts.map
4+
# Compiled JavaScript and TypeScript definition files from the src directory
5+
src/**/*.js
6+
src/**/*.d.ts
7+
src/**/*.d.ts.map

README.md

Lines changed: 112 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ https://github.com/heyMP/scratchpad/assets/3428964/2a58d587-510d-418f-bd8a-99958
1111
npx @heymp/scratchpad@next run ./my-test-file.js
1212
```
1313

14+
Note: This is still in development. Use the `next` tag until 1.0 release is ready.
15+
1416
## Commands
1517

1618
```bash
@@ -47,15 +49,116 @@ This allows you to interact with the Playwright API to perform actions like bloc
4749
network requests or navigating to different urls.
4850

4951
```js
50-
export default /** @type {import('@heymp/scratchpad/src/config').Config} */ ({
52+
export default ({
53+
devtools: true,
54+
url: 'https://google.com',
55+
headless: true,
56+
});
57+
```
58+
59+
#### Define Config
60+
61+
While you can create a `scratchpad.config.js` file above without installing this package,
62+
it's recommended to install `@heymp/scratchpad` and use the `defineConfig` helper to access
63+
the correct types.
64+
65+
**Usage:**
66+
67+
`scratchpad.config.js`
68+
```ts
69+
import { defineConfig } from '@heymp/scratchpad';
70+
71+
export default defineConfig({
5172
devtools: true,
73+
url: 'https://google.com',
74+
headless: true,
75+
});
76+
```
77+
78+
#### Reroute Documents
79+
80+
The `rerouteDocument` function allows you to replace the HTML content of any webpage with a local HTML file from your system. This is incredibly useful for testing changes or developing components in the context of a live site without deploying your code.
81+
82+
When used, `rerouteDocument` also watches your local HTML file for changes. If you save an update to the file, the Playwright page will automatically reload to reflect your latest edits, providing a fast feedback loop.
83+
84+
**How it works:**
85+
86+
* It intercepts requests made by the Playwright `page` that are for HTML documents.
87+
* It maps the URL's path to a local file structure. For instance, if you are on `http://example.com/user/profile` and you've set your local directory to `'./my-pages'`, the function will look for `'./my-pages/user/profile/index.html'`.
88+
89+
**Usage:**
90+
91+
You would typically use this function within the `playwright` async method in your `scratchpad.config.js`:
92+
93+
`scratchpad.config.js`
94+
```javascript
95+
import { defineConfig, rerouteDocument } from '@heymp/scratchpad';
96+
97+
export default defineConfig({
98+
url: 'https://example.com', // The initial URL you are working with
5299
playwright: async (args) => {
53-
const { context, page } = args;
54-
// block esmodule shims
55-
await context.route(/es-module-shims\.js/, async route => {
56-
await route.abort();
100+
const { page } = args;
101+
102+
// Tell Scratchpad to serve local HTML files from the './pages' directory
103+
// whenever a document is requested.
104+
await rerouteDocument(page, './pages');
105+
106+
// Now, if you navigate to https://example.com/some/path,
107+
// Scratchpad will try to serve './pages/some/path/index.html'.
108+
// If that file doesn't exist, it will load the original page from the web.
109+
}
110+
});
111+
```
112+
113+
#### Reroute URLs
114+
115+
The `rerouteUrl` function gives you the power to intercept network requests for specific assets (like JavaScript files, CSS, images, or API calls) and redirect them. You can reroute a `target` URL to either another live `source` URL or to a `source` local file from your system. This is incredibly useful for testing local versions of assets against a live site, mocking API responses, or redirecting to different service endpoints without deploying code.
116+
117+
If you save an update to this file, the Playwright page will automatically reload to show your latest edits, creating a very fast development feedback loop.
118+
119+
**How it works:**
120+
121+
* It uses Playwright's `page.route()` to intercept network requests that match the `target` URL or pattern you specify.
122+
* If you set `type: 'url'`, it fetches the content from your specified `source` URL instead of the original `target`.
123+
* If you set `type: 'path'`, it serves the content from your local `source` file. It also starts watching this file, and if any changes are detected, it reloads the page.
124+
125+
**Usage:**
126+
127+
You typically use `rerouteUrl` within the `playwright` async method in your configuration file (e.g., `scratchpad.config.js`):
128+
129+
`scratchpad.config.js`
130+
```javascript
131+
import * as Scratchpad from '@heymp/scratchpad';
132+
133+
export default Scratchpad.defineConfig({
134+
url: 'https://www.your-website.com',
135+
playwright: async (args) => {
136+
const { page } = args;
137+
138+
// Example 1: Reroute a specific JavaScript file to a local version
139+
await Scratchpad.rerouteUrl(page, {
140+
type: 'path',
141+
target: '**/scripts/main-app.js', // Intercept requests for this JS file
142+
source: './local-dev/main-app.js' // Serve your local version instead
143+
});
144+
145+
// Example 2: Reroute an API call to a different endpoint
146+
await Scratchpad.rerouteUrl(page, {
147+
type: 'url',
148+
target: 'https://api.production.com/data', // Original API endpoint
149+
source: 'https://api.staging.com/data' // Reroute to staging API
150+
});
151+
152+
// Example 3: Reroute an API call to a local mock JSON file
153+
await Scratchpad.rerouteUrl(page, {
154+
type: 'path',
155+
target: 'https://api.production.com/user/settings',
156+
source: './mocks/user-settings.json' // Serve local mock data
57157
});
58-
await page.goto('https://ux.redhat.com');
158+
159+
// Now, when the page requests '**/scripts/main-app.js',
160+
// your local './local-dev/main-app.js' will be served and auto-reloaded on change.
161+
// Requests to 'https://api.production.com/data' will go to the staging API.
59162
}
60163
});
61164
```
@@ -106,14 +209,15 @@ log(undefined);
106209
You can provide your own custom exposed functions using the `scratchpad.config.js` file.
107210

108211
```.js
212+
import * as Scratchpad from '@heymp/scratchpad';
109213
import { join } from 'node:path'
110214
import fs from 'node:fs/promises';
111215

112216
function loadFile(path) {
113217
return fs.readFile(join(process.cwd(), path), 'utf8');
114218
}
115219

116-
export default /** @type {import('@heymp/scratchpad/src/config').Config} */ ({
220+
export default defineConfig({
117221
playwright: async (args) => {
118222
const { context, page } = args;
119223
await context.exposeFunction('loadFile', loadFile)
@@ -149,14 +253,13 @@ yourself.
149253
}
150254
```
151255

152-
An alternatice to the `tsconfig.json` file is to use the following triple-slash comment
256+
An alternative to the `tsconfig.json` file is to use the following triple-slash comment
153257
in your `.ts` files:
154258

155259
```ts
156260
/// <reference path="./node_modules/@heymp/scratchpad/types.d.ts" />
157261
```
158262

159-
160263
## Development
161264

162265
```bash

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,18 @@
22
"name": "@heymp/scratchpad",
33
"description": "Run TS/JS snippets in a locally",
44
"version": "1.0.0-next.13",
5-
"main": "bin/cli.js",
5+
"main": "src/lib/index.js",
6+
"module": "src/lib/index.js",
67
"type": "module",
78
"bin": {
89
"scratchpad": "bin/cli.js"
910
},
11+
"exports": {
12+
".": {
13+
"types": "./src/lib/index.d.ts.js",
14+
"default": "./src/lib/index.js"
15+
}
16+
},
1017
"files": [
1118
"bin/cli.js",
1219
"src/**",

src/lib/index.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { readFile } from 'node:fs/promises';
2+
import fs from 'node:fs';
3+
import { join } from 'node:path';
4+
import type { Page } from 'playwright';
5+
import type { RerouteUrlParams } from './types.js';
6+
import { Config } from '../config.js';
7+
import type { PathLike } from 'node:fs';
8+
9+
/**
10+
* Defines the configuration for the application.
11+
* This function simply returns the configuration object passed to it,
12+
* often used for type-checking and providing a clear entry point for configuration.
13+
* @param {Config} config - The configuration object.
14+
* @returns {Config} The configuration object.
15+
*/
16+
export function defineConfig(config: Config) {
17+
return config;
18+
}
19+
20+
/**
21+
* Reroutes document requests to local files within a specified directory.
22+
* It intercepts 'document' type requests and attempts to serve a corresponding
23+
* 'index.html' file from the local file system. It also watches the local file
24+
* for changes and reloads the page if a change is detected.
25+
* @async
26+
* @param {Page} context - The Playwright Page object to apply the rerouting to.
27+
* @param {string} dir - The root directory where the local page files (e.g., 'about/index.html') are stored.
28+
* @returns {Promise<void>} A promise that resolves when the routing has been set up.
29+
*/
30+
export async function rerouteDocument(context: Page, dir: string) {
31+
// reload the page if the document has been changed
32+
const watchCallback = () => {
33+
context.reload();
34+
}
35+
36+
await context.route('**', async route => {
37+
const resourceType = route.request().resourceType();
38+
if (resourceType !== 'document') {
39+
await route.fallback();
40+
return;
41+
}
42+
const pageName = new URL(route.request().url())
43+
.pathname
44+
.replace(/^\//, '');
45+
const pagePath = join(dir, `${pageName}/index.html`);
46+
47+
// watch file for changes
48+
watchFile(pagePath, watchCallback);
49+
50+
const dashboardHtml = await readFile(join(process.cwd(), pagePath), 'utf8')
51+
.catch(() => null);
52+
53+
if (dashboardHtml) {
54+
const response = await route.fetch();
55+
await route.fulfill({
56+
response,
57+
body: dashboardHtml
58+
});
59+
console.log(`\x1b[33m 🚸 Page override:\x1b[0m ${pagePath}`);
60+
} else {
61+
await route.fallback();
62+
}
63+
});
64+
}
65+
66+
/**
67+
* Reroutes network requests for a given Playwright Page context.
68+
* It can reroute a target URL to another URL or to a local file path.
69+
* If rerouting to a local file path, it watches the file for changes and
70+
* reloads the page if the file is modified.
71+
* @async
72+
* @param {Page} context - The Playwright Page object to apply the rerouting to.
73+
* @param {RerouteUrlParams} options - An object containing the rerouting parameters.
74+
* @param {('url' | 'path')} options.type - The type of rerouting: 'url' to reroute to another URL,
75+
* 'path' to reroute to a local file.
76+
* @param {string | RegExp} options.target - The URL or pattern to intercept and reroute.
77+
* @param {string} options.source - The source URL or local file path to reroute to.
78+
* @returns {Promise<void>} A promise that resolves when the routing has been set up.
79+
*/
80+
export async function rerouteUrl(context: Page, options: RerouteUrlParams) {
81+
const { type, target, source } = options;
82+
// reload the page if the document has been changed
83+
const watchCallback = () => {
84+
context.reload();
85+
}
86+
87+
if (type === 'url') {
88+
await context.route(target, async route => {
89+
const response = await route.fetch({
90+
url: source,
91+
});
92+
await route.fulfill({ response })
93+
console.log(`\x1b[33m 🚸 Reroute url:\x1b[0m ${target}${source}`);
94+
});
95+
return;
96+
}
97+
if (type === 'path') {
98+
const path = join(process.cwd(), source);
99+
100+
// watch the file for changes
101+
watchFile(path, watchCallback);
102+
103+
await context.route(target, async route => {
104+
const file = await readFile(path, 'utf8')
105+
.catch(() => null);
106+
if (!file) {
107+
console.warn(`\x1b[33m ⚠️ Could not read file for reroute:\x1b[0m ${path}`);
108+
await route.abort(); // Abort the request if the file is not found
109+
return;
110+
}
111+
112+
const response = await route.fetch(); // Fetch original to get headers, status etc. if needed
113+
await route.fulfill({
114+
body: file,
115+
response // Pass the original response to inherit headers, status (unless overridden)
116+
});
117+
console.log(`\x1b[33m 🚸 Reroute url:\x1b[0m ${target}${source}`);
118+
});
119+
}
120+
}
121+
122+
/**
123+
* Watches a file for changes and executes a callback function when the file is modified.
124+
* It ensures that a file is not watched multiple times by attempting to unwatch it first.
125+
* Handles cases where the file might not exist.
126+
* @async
127+
* @param {PathLike} path - The path to the file to watch.
128+
* @param {() => void} callback - The function to call when the file changes.
129+
* @returns {Promise<void>} A promise that resolves when the file watching is set up or if an error occurs during setup.
130+
* It does not wait for file changes themselves.
131+
*/
132+
async function watchFile(path: PathLike, callback: () => void): Promise<void> {
133+
try {
134+
// Check if file exists first
135+
if (!fs.existsSync(path)) {
136+
return;
137+
}
138+
139+
// Try to unwatch, but don't throw if it fails
140+
try {
141+
fs.unwatchFile(path, callback);
142+
} catch (e) {
143+
// Ignore unwatch errors, e.g., if the file was not previously watched with the same callback
144+
}
145+
146+
// Set up new watch
147+
fs.watchFile(path, { interval: 100 }, callback);
148+
} catch (e: unknown) {
149+
console.error(`watchFile: Failed to watch file ${path.toString()}:`, e);
150+
}
151+
}

0 commit comments

Comments
 (0)