diff --git a/.changeset/rude-pets-hope.md b/.changeset/rude-pets-hope.md new file mode 100644 index 0000000..66aa96a --- /dev/null +++ b/.changeset/rude-pets-hope.md @@ -0,0 +1,34 @@ +--- +"@heymp/scratchpad": minor +--- + +Support ts config files + +`scratchpad.config.ts` +```ts +import { Config } from '@heymp/scratchpad/src/config.js'; + +export function hi(name: string) { + console.log(`Hi there ${name}`); +} + +declare global { + interface Window { + hi: typeof hi; + } +} + +export default ({ + playwright: async (args) => { + const { context } = args; + await context.exposeFunction('hi', hi); + } +}) satisfies Config; +``` + +`test.ts` +```.ts +/// + +window.hi('Bob'); +``` diff --git a/src/Processor.ts b/src/Processor.ts index e07089f..ae98327 100644 --- a/src/Processor.ts +++ b/src/Processor.ts @@ -1,6 +1,6 @@ import fs from 'node:fs'; import { join } from 'node:path'; -import * as esbuild from 'esbuild'; +import { build } from 'esbuild'; export class ProcessorChangeEvent extends Event { constructor() { @@ -58,7 +58,7 @@ export class Processor extends EventTarget { throw new Error(`${file} file not found.`); } if (file.endsWith('.ts')) { - const { outputFiles: [stdout]} = await esbuild.build({ + const { outputFiles: [stdout]} = await build({ entryPoints: [file], format: 'esm', bundle: true, diff --git a/src/config.ts b/src/config.ts index c37bf80..a356da5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,7 @@ import { join } from 'node:path'; -import { stat } from 'node:fs/promises'; import type { Page, BrowserContext, Browser } from 'playwright'; +import { build } from 'esbuild'; +import { exists, esm } from './utils.js'; export type PlaywrightConfig = { page: Page, @@ -16,26 +17,59 @@ export type Config = { playwright?: (page: PlaywrightConfig) => Promise } -const exists = (path:string) => stat(path).then(() => true, () => false); - -async function importConfig(rootDir:string) { - const path = join(rootDir, './scratchpad.config.js'); - if (await exists(path)) { - return import(path) - .then(x => x.default) - .catch(e => { - console.error(e); - return {}; - }); - } else { - return {}; +/** + * Looks at the users root path to see if there + * are any valid config files. + * @returns either the absolute path to the valid config + * or returns false if none have config + * has not been found. + */ +async function getConfigPath(): Promise { + if (await exists(join(process.cwd(), 'scratchpad.config.js'))) { + return join(process.cwd(), 'scratchpad.config.js'); + } + if (await exists(join(process.cwd(), 'scratchpad.config.ts'))) { + return join(process.cwd(), 'scratchpad.config.ts'); + } + return false; +} + +/** + * Get the import config object from the config file. + */ +async function importConfig(): Promise> { + // determine if there are any valid configs + const configPath = await getConfigPath(); + + // if there is no config path then return + // an empty object + if (!configPath) { + return {} + } + + try { + // use esbuild to load the .js or .ts file + const { outputFiles: [stdout] } = await build({ + entryPoints: [configPath], + format: 'esm', + bundle: true, + write: false, + }); + const contents = new TextDecoder().decode(stdout.contents); + const module = await import(esm`${contents}`); + return module.default ?? {}; + } catch (e) { + console.error(`An error occured parsing config file.${configPath}`); + console.error(``); + console.error(`Path: ${configPath}`); + console.error(``); + throw e; } } export async function getConfig(): Promise { - const rootDir = process.cwd(); return { - ...await importConfig(rootDir), + ...await importConfig(), } } diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..b061c88 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,23 @@ +import { stat } from 'node:fs/promises'; + +/** + * Helper function to check if a file exists + */ +export const exists = (path: string) => stat(path).then(() => true, () => false); + +/** + * Template Literal function that converts an string + * containing ESM javascript to data URI. + * + * @example + * const m1 = esm`export function f() { return 'Hello!' }`; + * const m2 = esm`import {f} from '${m1}'; export default f()+f();`; + * import(m1) + */ +export function esm(templateStrings: TemplateStringsArray, ...substitutions: any[]): string { + let js = templateStrings.raw[0]; + for (let i = 0; i < substitutions.length; i++) { + js += substitutions[i] + templateStrings.raw[i + 1]; + } + return 'data:text/javascript;base64,' + btoa(js); +}