Skip to content

Commit

Permalink
fix: handles multiple resets and better autocomplete
Browse files Browse the repository at this point in the history
  • Loading branch information
AutoSponge committed Mar 29, 2020
1 parent 18bfecb commit 2f569df
Show file tree
Hide file tree
Showing 15 changed files with 3,608 additions and 182 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,7 @@ dist
.tern-port

# IDE
.vscode/
.vscode/

# docs
docs/
13 changes: 0 additions & 13 deletions CHANGELOG.md

This file was deleted.

35 changes: 17 additions & 18 deletions bin/cli.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
#!/usr/bin/env node --experimental-repl-await
// --inspect
// --inspect-brk=28189

'use strict';
const { resolve } = require('path');
const meow = require('meow');
const dlv = require('dlv');
const Scriptwriter = require('../');
const cli = meow(
`
Expand Down Expand Up @@ -56,24 +59,20 @@ const cli = meow(
}
);

const config = {
browserType: 'chromium',
const { config, browser, headless, csp, js, device } = cli.flags;
const file = config ? require(resolve(config)) : {};
const use = (path, fallback) => dlv(file, path, fallback);

const scriptwriter = new Scriptwriter({
browserType: use('browserType', browser),
launch: {
headless: true,
args: [],
headless: use('launch.headless', headless),
args: use('launch.args', []),
},
context: {},
device: null,
};
if (cli.flags.config) {
const file = require(resolve(cli.flags.config));
Object.assign(config, file);
}
config.browserType = cli.flags.browser;
config.launch.headless = cli.flags.headless;
config.context.bypassCSP = cli.flags.csp;
config.context.javaScriptEnabled = cli.flags.js;
config.device = cli.flags.device || config.device;

const scriptwriter = new Scriptwriter(config);
context: {
bypassCSP: use('context.bypassCSP', csp),
javaScriptEnabled: use('context.javaScriptEnabled', js),
},
device: use('device', device),
});
scriptwriter.init();
10 changes: 5 additions & 5 deletions lib/events/browser.enter.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
const dlv = require('dlv')
module.exports = async function(scriptwriter) {
const { company, config } = scriptwriter
const dlv = require('dlv');
module.exports = async function (scriptwriter) {
const { company, config } = scriptwriter;
const { browser, playwright } = company;
const contextConfig = {
viewport: null,
...dlv(playwright.devices, [config.device], {}),
...dlv(config, 'context', {})
}
...dlv(config, 'context', {}),
};
const context = await browser.newContext(contextConfig);
scriptwriter.assign({ context });
};
10 changes: 5 additions & 5 deletions lib/events/client.enter.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
module.exports = async function(scriptwriter) {
module.exports = async function (scriptwriter) {
const { client } = scriptwriter.company;
await Promise.all([
client.send('Accessibility.enable'),
client.send('Runtime.enable'),
client.send('DOM.enable'),
]);
client.send('Accessibility.enable'),
client.send('Runtime.enable'),
client.send('DOM.enable'),
]);
};
14 changes: 9 additions & 5 deletions lib/events/context.enter.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
module.exports = async function(scriptwriter) {
const { context } = scriptwriter.company;
const page = await context.newPage();
scriptwriter.assign({ page });
if (context.newCDPSession) {
const client = await context.newCDPSession(page);
scriptwriter.assign({ client });
try {
const page = await context.newPage();
scriptwriter.assign({ page });
if (context.newCDPSession) {
const client = await context.newCDPSession(page);
scriptwriter.assign({ client });
}
} catch (err) {
scriptwriter.log(err);
}
};
14 changes: 8 additions & 6 deletions lib/events/director.enter.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
const { magenta } = require('kleur');
const PLAYBILL = 'playbill'
const COMMAND_PLAYBILL = `.${PLAYBILL}`
const dlv = require('dlv')
const PLAYBILL = 'playbill';
const COMMAND_PLAYBILL = `.${PLAYBILL}`;

module.exports = function(scriptwriter) {
module.exports = function (scriptwriter) {
const { director } = scriptwriter.company;
scriptwriter.assign({ scriptwriter });
director.displayPrompt();
if (scriptwriter.completions.includes(COMMAND_PLAYBILL)) return;
console.log(magenta('.help for help. Tab twice for hints.'));
/* istanbul ignore if */
if (dlv(scriptwriter, `director.commands.${PLAYBILL}`)) return;
scriptwriter.log(magenta('.help for help. Tab twice for hints.'));
scriptwriter.completion = COMMAND_PLAYBILL;
director.defineCommand(PLAYBILL, {
help: `List the scriptwriter's ${PLAYBILL}`,
action() {
const credits = Object.keys(scriptwriter.company).sort();
console.log(credits.join('\n'));
scriptwriter.log(credits.join('\n'));
director.displayPrompt();
},
});
Expand Down
7 changes: 4 additions & 3 deletions lib/events/director.exit.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { magenta } = require('kleur');

module.exports = function() {
console.log(magenta('repl session ended.'));
process.exit();
module.exports = function(scriptwriter) {
scriptwriter.log(magenta('repl session ended.'));
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'test') process.exit();
};
19 changes: 11 additions & 8 deletions lib/events/director.reset.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
const { magenta } = require('kleur');

module.exports = async function(scriptwriter) {
const { browser, director } = scriptwriter.company;
await browser.close();
await scriptwriter.init();
console.log(magenta('repl session reset.'));
director.setPrompt('> ');
director.displayPrompt();
};
const handler = async function (scriptwriter) {
const { browser, director } = scriptwriter.company;
await browser.close();
await scriptwriter.init();
scriptwriter.log(magenta('repl session reset.'));
director.setPrompt('> ');
director.displayPrompt();
}
handler.once = true

module.exports = handler;
2 changes: 1 addition & 1 deletion lib/events/page.load.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { green, yellow } = require('kleur');

module.exports = async function(scriptwriter) {
module.exports = async function (scriptwriter) {
const { page, director } = scriptwriter.company;
const url = await page.url();
const { host } = new URL(url);
Expand Down
107 changes: 86 additions & 21 deletions lib/scriptwriter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const assert = require('assert');
const dlv = require('dlv');
const { resolve } = require('path');
const { readdir } = require('fs').promises;
const { isString } = require('./utils');
const importGlobal = require('import-global');
const playwright = importGlobal('playwright');

Expand All @@ -17,30 +16,63 @@ const config = new Map();
const company = new Map();
const completions = new Set();

/**
* @typedef {Object} config
* @property {string} browserType
* @property {string} device
* @property {Object} launch
* @property {boolean} launch.headless
* @property {string[]} launch.args
* @property {Object} context
* @property {boolean} context.bypassCSP
* @property {boolean} context.javaScriptEnabled
*/
const defaultConfig = { browserType: 'chromium', launch: {} };

/**
* @extends EventEmitter
*/
module.exports = class Scriptwriter extends EventEmitter {
constructor(initialConfig = {}) {
/**
* @param {config} config
*/
constructor(initialConfig = defaultConfig) {
super();
Object.entries(initialConfig).map(([k, v]) => config.set(k, v));
this.on(EVENT_ASSIGN, this.register.bind(this));
this.replServer = null;
}

/**
* @return {Object} company
*/
get company() {
return Object.fromEntries(company);
}

/**
* @return {string[]} completions
*/
get completions() {
return Array.from(completions).sort();
}

set completion(str) {
completions.add(str);
/**
* Adds a completion to the internal Set.
* @param {string} completion
*/
set completion(completion) {
completions.add(completion);
return completion;
}

/**
* @return {config} config
*/
get config() {
return Object.fromEntries(config);
}

/**
* Assigns the properties of the parameter object
* to the replServer.context (and the "playbill").
* @param {Object} obj
*/
assign(obj) {
Object.entries(obj).forEach(([name, value]) => {
company.set(name, value);
Expand All @@ -51,24 +83,34 @@ module.exports = class Scriptwriter extends EventEmitter {
}
});
}

/**
* Subscribes relevant event handlers from ./events.
* @param {string} assignment
* @fires EventEmitter#enter
*/
async register(assignment) {
const assigned = company.get(assignment);
assert.ok(assigned, `${assigned} not loaded.`);
this.completion = assignment;
if (!assigned.emit) return;
const listings = await EVENTS_DIR;
for (const list of listings) {
/* istanbul ignore if */
if (list.endsWith('.test.js')) continue;
const [role, event] = list.replace(/\.js$/, '').split('.');
if (assignment !== role) continue;
const file = resolve(EVENTS_PATH, list);
const handle = require(file).bind(null, this);
assigned.on(event, handle);
const handler = require(file);
const handle = handler.bind(null, this);
assigned[handler.once ? 'once' : 'on'](event, handle);
}
assigned.emit(EVENT_ENTER);
}

/**
* Resets company and completions.
* Recycles the replServer.
* Assigns the director (replServer), playwright, and browser
*/
async init() {
company.clear();
completions.clear();
Expand All @@ -90,29 +132,52 @@ module.exports = class Scriptwriter extends EventEmitter {
await this.assign({ playwright, browser });
director.displayPrompt();
}

/**
* Parses the incoming repl line for expandable namespaces
* known to the company.
* @param {string} line
*/
completer(line) {
let completions = this.completions;
// line has a space or . in it
complex: if (line.substring(1).match(/[\s\.]/)) {
const jsonPath = dlv(line.match(/\S+$/), [0]);
completions = [];
const jsonPath = dlv(line.match(/\S+$/), [0]);
// the last chunk of syntax started an invocation
if (!jsonPath || jsonPath.match(/\(/)) break complex;
const chunks = jsonPath.split('.');
completions = this.completions;
chunks.pop();
if (!chunks.length) break complex;
const obj = dlv(this.company, chunks);
const last = chunks.pop();
// there's only one token
if (!chunks.length) {
completions = this.completions.flatMap(c => {
return c.startsWith(last) ? `${line}${c.substring(last.length)}` : [];
});
break complex;
}
const obj = dlv(this.replServer.context, chunks);
completions = [];
// not an object in scope
if (!obj) break complex;
completions = Reflect.ownKeys(obj)
.concat(Reflect.ownKeys(Reflect.getPrototypeOf(obj)))
.concat(Reflect.ownKeys(Reflect.getPrototypeOf(obj) || {}))
.flatMap(c => {
if (!isString(c)) return [];
if (c === 'constructor') return [];
if (typeof c !== 'string') return [];
return [`${line.replace(/\.[^\.]*$/, '')}.${c}`];
})
.sort();
}
const hits = completions.filter(c => c.startsWith(line));
if (hits.length === 1 && hits[0] === line) return [[], line];
return [hits.length ? hits : completions, line];
}
/**
* @param {...any} args
*/
log(...args) {
this.emit('log', args);
if (process.env.NODE_ENV === 'test') return;
/* istanbul ignore next */
console.log(...args);
}
};
Loading

0 comments on commit 2f569df

Please sign in to comment.