Skip to content

Commit

Permalink
Add generate config command (#47)
Browse files Browse the repository at this point in the history
* feat: add run command

* feat: add generate config command

* chore: add changeset and update Readme

* chore: update yarn lock

* chore: add run command changeset

* chore: update changeset
  • Loading branch information
heyMP authored Aug 3, 2024
1 parent a242c0f commit 4aa2817
Show file tree
Hide file tree
Showing 12 changed files with 225 additions and 105 deletions.
20 changes: 20 additions & 0 deletions .changeset/chilly-bobcats-reflect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@heymp/scratchpad": major
---

Add `generate config` command [#38](https://github.com/heyMP/scratchpad/issues/38)

```bash
npx @heymp/scratchpad generate --help

Usage: scratchpad generate [options] [command]

Generate files from templates.

Options:
-h, --help display help for command

Commands:
config Generates an example config file.
help [command] display help for command
```
25 changes: 25 additions & 0 deletions .changeset/loud-rivers-pretend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
"@heymp/scratchpad": major
---

Add `run` command

```bash
npx @heymp/scratchpad run --help

Usage: cli run [options] <file>

Execute a file in a browser.

Arguments:
file file to execute in the browser.

Options:
--headless [boolean] specify running the browser in headless
mode.
--devtools [boolean] open browser devtools automatically.
--ts-write [boolean] write the js output of the target ts file.
--url [string] specify a specific url to execute the code
in.
-h, --help display help for command
```
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,24 @@ https://github.com/heyMP/scratchpad/assets/3428964/2a58d587-510d-418f-bd8a-99958
## Usage

```bash
npx @heymp/scratchpad@next ./my-test-file.js
npx @heymp/scratchpad@next run ./my-test-file.js
```

## Options
## Commands

```bash
Usage: @heymp/scratchpad@next <file> [options]
Usage: @heymp/scratchpad@next run <file> [options]

Arguments:
file file to execute in the browser.
Run TS/JS snippets in a locally

Options:
--headless [boolean] specify running the browser in headless mode.
--devtools [boolean] open browser devtools automatically.
--ts-write [boolean] write the js output of the target ts file.
--url [string] specify a specific url to execute the code in.
-V, --version output the version number
-h, --help display help for command

Commands:
run [options] <file> Execute a file in a browser.
generate Generate files from templates.
help [command] display help for command
```


Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "@heymp/scratchpad",
"description": "Run TS/JS snippets in a locally",
"version": "1.0.0-next.7",
"main": "bin/cli.js",
"type": "module",
Expand All @@ -12,7 +13,7 @@
"types.d.ts"
],
"dependencies": {
"commander": "^11.0.0",
"commander": "^12.0.0",
"esbuild": "^0.23.0",
"playwright": "^1.44.1"
},
Expand All @@ -23,6 +24,7 @@
},
"devDependencies": {
"@changesets/cli": "^2.27.1",
"@commander-js/extra-typings": "^12.1.0",
"@types/node": "^22.1.0",
"typescript": "^5.5.4"
},
Expand Down
83 changes: 83 additions & 0 deletions src/Processor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import fs from 'node:fs';
import { join } from 'node:path';
import * as esbuild from 'esbuild';

export class ProcessorChangeEvent extends Event {
constructor() {
super('change');
}
}

export type ProcessorOpts = {
headless?: boolean;
devtools?: boolean;
tsWrite?: boolean;
url?: string;
playwright?: any;
file: string;
}

/**
* Event Bus that monitors the target file for
* changes and emits stringified javascript to
* throught the 'change' event.
* */
export class Processor extends EventTarget {
_func = '';

constructor(public opts: ProcessorOpts) {
super();
this.watcher();
}

get func() {
return this._func;
}

set func(func) {
this._func = func;
this.dispatchEvent(new ProcessorChangeEvent());
}

watcher() {
const file = this.opts.file;
if (!fs.existsSync(file)) {
throw new Error(`${file} file not found.`);
}
// execute it immediately then start watcher
this.build();
fs.watchFile(join(file), { interval: 100 }, () => {
this.build();
});
}

async build() {
const file = this.opts.file;
try {
if (!file) {
throw new Error(`${file} file not found.`);
}
if (file.endsWith('.ts')) {
const { outputFiles: [stdout]} = await esbuild.build({
entryPoints: [file],
format: 'esm',
bundle: true,
write: false,
});
this.func = new TextDecoder().decode(stdout.contents);
if (this.opts.tsWrite) {
fs.writeFile(join(process.cwd(), file.replace(/\.ts$/g, '.js')), this.func, 'utf8', () => {});
}
}
else {
this.func = fs.readFileSync(file, 'utf8');
}
} catch (e) {
console.error(e);
}
}

start() {
this.dispatchEvent(new ProcessorChangeEvent());
}
}
15 changes: 8 additions & 7 deletions src/browser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import playwright from 'playwright';
import util from 'node:util';
import type { Processor } from './cli.js';
import type { Processor } from './Processor.js';
util.inspect.defaultOptions.maxArrayLength = null;
util.inspect.defaultOptions.depth = null;

Expand All @@ -11,20 +11,21 @@ function nodelog(value:any) {
export async function browser(processor: Processor) {
// Launch the browser
const browser = await playwright['chromium'].launch({
headless: !!processor.headless,
devtools: !!processor.devtools
headless: !!processor.opts.headless,
devtools: !!processor.opts.devtools
});
const context = await browser.newContext();
const page = await context.newPage();
const playwrightConfig = processor.opts.playwright;

// Allow playwright config override
if (processor.playwrightConfig && typeof processor.playwrightConfig === 'function') {
await processor.playwrightConfig({ browser, context, page });
if (playwrightConfig && typeof playwrightConfig === 'function') {
await playwrightConfig({ browser, context, page });
}

// Create a page
if (processor.url) {
await page.goto(processor.url);
if (processor.opts.url) {
await page.goto(processor.opts.url);
}

await page.exposeFunction('clear', () => {
Expand Down
90 changes: 7 additions & 83 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
import fs from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { Command } from 'commander';
import { browser } from './browser.js';
import { getConfig } from './config.js';
import * as esbuild from 'esbuild';
import { Command } from '@commander-js/extra-typings';
import { runCommand } from './runCommand.js';
import { generateCommand } from './generateCommand.js';

// Get pkg info
const __filename = fileURLToPath(import.meta.url);
Expand All @@ -15,83 +14,8 @@ const pkg = JSON.parse(fs.readFileSync(join(__dirname, '../package.json'), 'utf8

const program = new Command();
program
.argument('<file>', 'file to execute in the browser.')
.option('--headless [boolean]', 'specify running the browser in headless mode.')
.option('--devtools [boolean]', 'open browser devtools automatically.')
.option('--ts-write [boolean]', 'write the js output of the target ts file.')
.option('--url [string]', 'specify a specific url to execute the code in.')
.version(pkg.version)
.description(pkg.description)
.version(pkg.version);
program.addCommand(runCommand);
program.addCommand(generateCommand);
program.parse(process.argv);

const file = program.args.at(0);
const config = await getConfig();
const opts = { ...config, ...program.opts()};

export class Processor extends EventTarget {
url = opts['url'];
headless = opts['headless'];
devtools = opts['devtools'];
tsWrite = opts['tsWrite'];
playwrightConfig = opts['playwright'];
_func = '';

constructor() {
super();
this.watcher();
browser(this);
}

get func() {
return this._func;
}

set func(func) {
this._func = func;
this.dispatchEvent(new Event('change'));
}

watcher() {
if (!file) {
throw new Error(`${file} file not found.`);
}
if (!fs.existsSync(file)) {
throw new Error(`${file} file not found.`);
}
// execute it immediately then start watcher
this.build();
fs.watchFile(join(file), { interval: 100 }, () => {
this.build();
});
}

async build() {
try {
if (!file) {
throw new Error(`${file} file not found.`);
}
if (file.endsWith('.ts')) {
const { outputFiles: [stdout]} = await esbuild.build({
entryPoints: [file],
format: 'esm',
bundle: true,
write: false,
});
this.func = new TextDecoder().decode(stdout.contents);
if (this.tsWrite) {
fs.writeFile(join(process.cwd(), file.replace(/\.ts$/g, '.js')), this.func, 'utf8', () => {});
}
}
else {
this.func = fs.readFileSync(file, 'utf8');
}
} catch (e) {
console.error(e);
}
}

start() {
this.dispatchEvent(new Event('change'));
}
}

new Processor();
2 changes: 1 addition & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ async function importConfig(rootDir:string) {
}
}

export async function getConfig() {
export async function getConfig(): Promise<Config> {
const rootDir = process.cwd();
return {
...await importConfig(rootDir),
Expand Down
32 changes: 32 additions & 0 deletions src/generateCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Command } from '@commander-js/extra-typings';
import * as readline from 'node:readline/promises';
import { stdin, stdout } from 'node:process';
import { writeFile, readFile, stat } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const configCommand = new Command('config')
.description('Generates an example config file.')
.action(async () => {
const rl = readline.createInterface({ input: stdin, output: stdout });
const template = await readFile(join(__dirname, '../templates/scratchpad.config.js'), 'utf8');
const outpath = join(process.cwd(), 'scratchpad.config.js');
const confirmation = await rl.question(`Are you sure you want to create the following
${outpath} [Y/n]: `);

// if the user answers no then abort
if (['n', 'N'].includes(confirmation)) {
rl.close();
return;
}

// write the template file to the users directory
writeFile(outpath, template, 'utf8');
rl.close();
});

export const generateCommand = new Command('generate')
.description('Generate files from templates.')
.addCommand(configCommand);
26 changes: 26 additions & 0 deletions src/runCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Command } from '@commander-js/extra-typings';
import { getConfig } from './config.js';
import { Processor } from './Processor.js';
import { browser } from './browser.js';

export const runCommand = new Command('run')
.description('Execute a file in a browser.')
.argument('<file>', 'file to execute in the browser.')
.option('--headless [boolean]', 'specify running the browser in headless mode.')
.option('--devtools [boolean]', 'open browser devtools automatically.')
.option('--ts-write [boolean]', 'write the js output of the target ts file.')
.option('--url [string]', 'specify a specific url to execute the code in.')
.action(async (file, options, command) => {
const config = await getConfig();
const opts = { ...config, ...options};
const processor = new Processor({
// type narrow the options
headless: !!opts['headless'],
devtools: !!opts['devtools'],
tsWrite: !!opts['tsWrite'],
url: typeof opts['url'] === 'string' ? opts['url'] : undefined,
playwright: opts['playwright'],
file,
});
browser(processor);
});
2 changes: 2 additions & 0 deletions templates/scratchpad.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export default /** @type {import('@heymp/scratchpad/src/config').Config} */ ({
});
Loading

0 comments on commit 4aa2817

Please sign in to comment.