Skip to content

Commit 96cefea

Browse files
authored
feat: add clone command (#73)
* feat: add clone command * feat: adding more generic rerouteLocal function to handle both document and asset type files * chore: update readme * chore: add changest
1 parent d666e75 commit 96cefea

File tree

9 files changed

+170
-16
lines changed

9 files changed

+170
-16
lines changed

.changeset/large-suns-buy.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
"@heymp/scratchpad": minor
3+
---
4+
5+
Add clone command to copy remote files locally
6+
7+
```bash
8+
npx @heymp/scratchpad@next clone https://www.example.com/scripts.js
9+
```
10+
11+
Added new generic helper to reroute both document and asset file types.
12+
13+
scratchpad.config.ts
14+
```ts
15+
export default Scratchpad.defineConfig({
16+
playwright: async (args) => {
17+
const { page } = args;
18+
Scratchpad.rerouteLocal(page, 'pages');
19+
}
20+
});
21+
```

README.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,48 @@ You can then reuse the session by using the `login` option when using the `run`
9292
npx @heymp/scratchpad@next run --login
9393
```
9494

95+
#### Reroute Local Files
96+
97+
The `rerouteLocal` function allows you to replace the contents of a document or asset file with a local 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.
98+
99+
When used, `rerouteLocal` also watches for 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.
100+
101+
**How it works:**
102+
103+
* It intercepts requests made by the Playwright `page.
104+
* If the request is not of type `document` it will URL's path to a local file structure.
105+
* If the request is of type `document` (e.g. `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'`.
106+
107+
**Usage:**
108+
109+
You would typically use this function within the `playwright` async method in your `scratchpad.config.js`:
110+
111+
`scratchpad.config.js`
112+
```javascript
113+
import { defineConfig, rerouteDocument } from '@heymp/scratchpad';
114+
115+
export default defineConfig({
116+
url: 'https://example.com', // The initial URL you are working with
117+
playwright: async (args) => {
118+
const { page } = args;
119+
120+
// Tell Scratchpad to serve local HTML files from the './pages' directory
121+
// whenever a document is requested.
122+
await rerouteLocal(page, './pages');
123+
124+
// Now, if you navigate to https://example.com/some/path,
125+
// Scratchpad will try to serve './pages/some/path/index.html'.
126+
// If that file doesn't exist, it will load the original page from the web.
127+
128+
// If there is a local file of `./assets/scripts.js` it will serve the contents
129+
// of that local file.
130+
}
131+
});
132+
```
133+
95134
🚨 It is highly recommended to add the `.scratchpad` directory to your .gitignore file. Never commit or share your session `login.json` file!
96135

97-
#### Reroute Documents
136+
#### Reroute Documents [deprecated]
98137

99138
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.
100139

src/Processor.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
11
import fs from 'node:fs';
22
import { join } from 'node:path';
33
import { build } from 'esbuild';
4+
import type { Config } from './config.js';
45

56
export class ProcessorChangeEvent extends Event {
67
constructor() {
78
super('change');
89
}
910
}
1011

11-
export type ProcessorOpts = {
12-
headless?: boolean;
13-
devtools?: boolean;
14-
tsWrite?: boolean;
15-
login?: boolean;
16-
url?: string;
17-
playwright?: any;
12+
export type ProcessorOpts = Config & {
1813
file: string;
1914
}
2015

src/browser.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { join } from 'node:path'
44
import fs from 'node:fs/promises';
55
import type { Processor } from './Processor.js';
66
import { getSession } from './login.js';
7+
import { rerouteLocal } from './lib/index.js';
78
util.inspect.defaultOptions.maxArrayLength = null;
89
util.inspect.defaultOptions.depth = null;
910

@@ -36,6 +37,10 @@ export async function browser(processor: Processor) {
3637
const page = await context.newPage();
3738
const playwrightConfig = processor.opts.playwright;
3839

40+
if (processor.opts.rerouteDir) {
41+
await rerouteLocal(page, processor.opts.rerouteDir);
42+
}
43+
3944
// Exposed functions
4045
await context.exposeFunction('writeFile', writeFile);
4146
await context.exposeFunction('appendFile', appendFile);

src/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
66
import { Command } from '@commander-js/extra-typings';
77
import { runCommand } from './runCommand.js';
88
import { generateCommand } from './generateCommand.js';
9+
import { cloneCommand } from './cloneCommand.js';
910

1011
// Get pkg info
1112
const __filename = fileURLToPath(import.meta.url);
@@ -18,4 +19,5 @@ program
1819
.version(pkg.version);
1920
program.addCommand(runCommand);
2021
program.addCommand(generateCommand);
22+
program.addCommand(cloneCommand);
2123
program.parse(process.argv);

src/cloneCommand.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Command } from '@commander-js/extra-typings';
2+
import { writeFile, mkdir } from 'node:fs/promises';
3+
import { dirname, join, extname, basename } from 'node:path';
4+
import { URL } from 'node:url';
5+
import assert from 'node:assert';
6+
import { getConfig } from './config.js';
7+
8+
export const cloneCommand = new Command('clone')
9+
.description('Generates a local copy of a file from a url.')
10+
.argument('<url>', 'url to copy.')
11+
.option('--dir <string>', 'source directory where you want to save the document.')
12+
.action(async (_url, opts) => {
13+
const config = await getConfig();
14+
const dir = opts.dir ?? config.rerouteDir;
15+
16+
// verify that we have a path
17+
if (!dir) {
18+
assert.fail(`Source directory not specified`)
19+
}
20+
21+
const outputDir = join(process.cwd(), dir);
22+
const url = new URL(_url);
23+
const isDocument = !extname(url.pathname);
24+
25+
// get content
26+
const res = await fetch(url);
27+
if (!res.ok) {
28+
assert.fail(`HTTP error! Status: ${res.status} - ${res.statusText} for URL: ${url}`)
29+
}
30+
const html = await res.text();
31+
32+
if (isDocument) {
33+
const fileDir = join(outputDir, url.pathname.replace(/^\//, ''));
34+
const filePath = join(fileDir, 'index.html');
35+
await mkdir(fileDir, { recursive: true });
36+
await writeFile(filePath, html, 'utf8');
37+
}
38+
else {
39+
const fileDir = join(outputDir, dirname(url.pathname));
40+
const filePath = join(fileDir, basename(url.pathname));
41+
await mkdir(fileDir, { recursive: true });
42+
await writeFile(filePath, html, 'utf8');
43+
}
44+
});

src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type Config = {
1515
tsWrite?: boolean;
1616
url?: string;
1717
login?: boolean;
18+
rerouteDir?: string;
1819
playwright?: (page: PlaywrightConfig) => Promise<void>
1920
}
2021

src/lib/index.ts

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { readFile } from 'node:fs/promises';
22
import fs from 'node:fs';
3-
import { join } from 'node:path';
3+
import { extname, join, basename } from 'node:path';
44
import type { Page } from 'playwright';
55
import type { RerouteUrlParams } from './types.js';
66
import { Config } from '../config.js';
@@ -18,6 +18,51 @@ export function defineConfig(config: Config) {
1818
}
1919

2020
/**
21+
* Reroutes requests to local files within a specified directory. If the file exists locally,
22+
* the contents of the local file with be used.
23+
* @async
24+
* @param {Page} context - The Playwright Page object to apply the rerouting to.
25+
* @param {string} dir - The root directory where the local page or file (e.g., 'about/index.html', 'assets/script.js') are stored.
26+
* @returns {Promise<void>} A promise that resolves when the routing has been set up.
27+
*/
28+
export async function rerouteLocal(context: Page, dir: string) {
29+
// reload the page if the document has been changed
30+
const watchCallback = () => {
31+
context.reload();
32+
}
33+
34+
await context.route('**', async route => {
35+
const resourceType = route.request().resourceType();
36+
const url = new URL(route.request().url())
37+
const basepath = resourceType === 'document'
38+
? !extname(url.pathname)
39+
? join(url.pathname, 'index.html')
40+
: url.pathname
41+
: basename(url.pathname);
42+
const pagePath = join(dir, basepath);
43+
44+
const content = await readFile(join(process.cwd(), pagePath), 'utf8')
45+
.catch(() => null);
46+
47+
// watch the file for changes
48+
watchFile(pagePath, watchCallback);
49+
50+
if (content) {
51+
const response = await route.fetch();
52+
await route.fulfill({
53+
response,
54+
body: content
55+
});
56+
console.log(`\x1b[33m 🚸 Page override:\x1b[0m ${pagePath}`);
57+
} else {
58+
await route.fallback();
59+
}
60+
});
61+
}
62+
63+
/**
64+
* @deprecated Use rerouteLocal
65+
*
2166
* Reroutes document requests to local files within a specified directory.
2267
* It intercepts 'document' type requests and attempts to serve a corresponding
2368
* 'index.html' file from the local file system. It also watches the local file
@@ -39,22 +84,23 @@ export async function rerouteDocument(context: Page, dir: string) {
3984
await route.fallback();
4085
return;
4186
}
42-
const pageName = new URL(route.request().url())
43-
.pathname
44-
.replace(/^\//, '');
45-
const pagePath = join(dir, `${pageName}/index.html`);
87+
const url = new URL(route.request().url())
88+
const basepath = !extname(url.pathname)
89+
? join(url.pathname, 'index.html')
90+
: url.pathname;
91+
const pagePath = join(dir, basepath);
4692

4793
// watch file for changes
4894
watchFile(pagePath, watchCallback);
4995

50-
const dashboardHtml = await readFile(join(process.cwd(), pagePath), 'utf8')
96+
const content = await readFile(join(process.cwd(), pagePath), 'utf8')
5197
.catch(() => null);
5298

53-
if (dashboardHtml) {
99+
if (content) {
54100
const response = await route.fetch();
55101
await route.fulfill({
56102
response,
57-
body: dashboardHtml
103+
body: content
58104
});
59105
console.log(`\x1b[33m 🚸 Page override:\x1b[0m ${pagePath}`);
60106
} else {

src/runCommand.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const runCommand = new Command('run')
2222
url: typeof opts['url'] === 'string' ? opts['url'] : undefined,
2323
playwright: opts['playwright'],
2424
login: !!opts['login'],
25+
rerouteDir: opts['rerouteDir'],
2526
file,
2627
});
2728
browser(processor);

0 commit comments

Comments
 (0)