47 rows where issue = 712260429 sorted by updated_at descending

View and edit SQL

Suggested facets: reactions, created_at (date), updated_at (date)

author_association

issue

  • JavaScript plugin hooks mechanism similar to pluggy · 47
id html_url issue_url node_id user created_at updated_at ▲ author_association body reactions issue performed_via_github_app
766536076 https://github.com/simonw/datasette/issues/983#issuecomment-766536076 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc2NjUzNjA3Ng== simonw 9599 2021-01-25T04:43:53Z 2021-01-25T04:43:53Z OWNER

... actually not going to include this in 0.54, I need to write a couple of plugins myself using it before I even make it available in preview.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
766484915 https://github.com/simonw/datasette/issues/983#issuecomment-766484915 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc2NjQ4NDkxNQ== simonw 9599 2021-01-25T01:33:29Z 2021-01-25T01:33:29Z OWNER

I'm going to ship a version of this in Datasette 0.54 with a warning that the interface should be considered unstable (see #1202) so that we can start trying this out.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
754210356 https://github.com/simonw/datasette/issues/983#issuecomment-754210356 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1NDIxMDM1Ng== carlmjohnson 222245 2021-01-04T20:49:05Z 2021-01-04T20:49:05Z NONE

For reasons I've written about elsewhere, I'm in favor of modules. It has several beneficial effects. One, old browsers just ignore it all together. Two, if you include the same plain script on the page more than once, it will be executed twice, but if you include the same module script on a page twice, it will only execute once. Three, you get a module local namespace, instead of having to use the global window namespace or a function private namespace.

OTOH, if you are going to use an old style script, the code from before isn't ideal, because you wipe out your registry if the script it included more than once. Also you may as well use object methods and splat arguments.

The event based architecture probably makes more sense though. Just make up some event names prefixed with datasette: and listen for them on the root. The only concern with that approach is it can sometimes be tricky to make sure your plugins are run after datasette has run. Maybe

function mycallback(){
  // whatever
}

if (window.datasette) {
  window.datasette.init(mycallback);
} else {
  document.addEventListener('datasette:init', mycallback);
}
{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
754181647 https://github.com/simonw/datasette/issues/983#issuecomment-754181647 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1NDE4MTY0Nw== jussiarpalahti 11941245 2021-01-04T19:52:40Z 2021-01-04T19:52:40Z NONE

I was thinking JavaScript plugins going with server side template extensions custom HTML. Attach my own widgets on there and listen for Datasette events to refresh when user interacts with main UI. Like a map view or table that updates according to selected column. There's certainly other ways to look at this. Perhaps you could list possible hooks or high level design doc on what would be possible with the plugin system?

Re: modules. I would like to see modules supported at least in development. The developer experience is so much better than what JavaScript coding has been in the past. With large parts of NPM at your disposal I’d imagine even less experienced coder can whisk a custom plugin in no time. Proper production build system (like one you get with Pika or Parcel) could package everything up into bundles that older browsers can understand. Though that does come with performance and size penalties alongside the added complexity.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
753690280 https://github.com/simonw/datasette/issues/983#issuecomment-753690280 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1MzY5MDI4MA== simonw 9599 2021-01-03T23:13:30Z 2021-01-03T23:13:30Z OWNER

Oh that's interesting, I hadn't thought about plugins firing events - just responding to events fired by the rest of the application.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
753600999 https://github.com/simonw/datasette/issues/983#issuecomment-753600999 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1MzYwMDk5OQ== MarkusH 475613 2021-01-03T11:11:21Z 2021-01-03T11:11:21Z NONE

With regards to JS/Browser events, given your example of menu items that plugins could add, I could imagine this code to work:

// as part of datasette
datasette.events.AddMenuItem = 'DatasetteAddMenuItemEvent';
document.addEventListener(datasette.events.AddMenuItem, (e) => {
  // do whatever is needed to add the menu item. Data comes from `e`
  alert(e.title + ' ' + e.link);
});

// as part of a plugin
const event = new Event(datasette.events.AddMenuItem, {link: '/foo/bar', title: 'Go somewhere'});
Document.dispatchEvent(event)
{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
753587963 https://github.com/simonw/datasette/issues/983#issuecomment-753587963 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1MzU4Nzk2Mw== dracos 154364 2021-01-03T09:02:50Z 2021-01-03T10:00:05Z NONE

but I'm already commited to requiring support for () => {} arrow functions

Don't think you are :) (e.g. gzipped, using arrow functions in my example saves 2 bytes over spelling out function). On FMS, past month, looking at popular browsers, looks like we'd have 95.41% arrow support, 94.19% module support, and 4.58% (mostly IE9/IE11/Safari 9) supporting neither.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
753570710 https://github.com/simonw/datasette/issues/983#issuecomment-753570710 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1MzU3MDcxMA== simonw 9599 2021-01-03T05:29:56Z 2021-01-03T05:29:56Z OWNER

I thought about using browser events, but they don't quite match the API that I'm looking to provide. In particular, the great thing about Pluggy is that if you have multiple handlers registered for a specific plugin hook each of those handlers can return a value, and Pluggy will combine those values into a list of replies.

This is great for things like plugin hooks that add extra menu items - each plugin can return a menu item (maybe as a label/URL/click-callback object) and the calling code can then add all of those items to the menu. See https://docs.datasette.io/en/stable/plugin_hooks.html#table-actions-datasette-actor-database-table for a Python example.

I'm on the fence about relying on JavaScript modules. I need to think about browser compatibility for them - but I'm already commited to requiring support for () => {} arrow functions so maybe I'm committed to module support too already?

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
753224999 https://github.com/simonw/datasette/issues/983#issuecomment-753224999 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1MzIyNDk5OQ== jussiarpalahti 11941245 2020-12-31T23:29:36Z 2020-12-31T23:29:36Z NONE

I have yet to build Datasette plugin and am unfamiliar with Pluggy. Since browsers have event handling builtin Datasette could communicate with plugins through it. Handlers register as listeners for custom Datasette events and Datasette's JS can then trigger said events.

I was also wondering if you had looked at Javascript Modules for JS plugins? With services like Skypack (https://www.skypack.dev) NPM libraries can be loaded directly into browser, no build step needed. Same goes for local JS if you adhere to ES Module spec.

If minification is required then tools such as Snowpack (https://www.snowpack.dev) could fit better. It uses https://github.com/evanw/esbuild for bundling and minification.

On plugins you'd simply:

import {register} from '/assets/js/datasette'
register.on({'click' : my_func})

In Datasette HTML pages' head you'd merely import these files as modules one by one.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
753221646 https://github.com/simonw/datasette/issues/983#issuecomment-753221646 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1MzIyMTY0Ng== simonw 9599 2020-12-31T22:58:47Z 2020-12-31T22:58:47Z OWNER

https://github.com/mishoo/UglifyJS/issues/1905#issuecomment-300485490 says:

sourceMappingURL aren't added by default in 3.x due to one of the feature requests not to - some users are putting them within HTTP response headers instead.

So the command line for that would be:

js $ uglifyjs main.js -cmo main.min.js --source-map url=main.min.js.map

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
753219521 https://github.com/simonw/datasette/issues/983#issuecomment-753219521 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1MzIxOTUyMQ== simonw 9599 2020-12-31T22:39:52Z 2020-12-31T22:39:52Z OWNER

For inlining the plugins.min.js file into the Jinja templates I could use the trick described here: https://stackoverflow.com/a/41404611 - which adds a {{ include_file('file.txt') }} function to Jinja.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
753219407 https://github.com/simonw/datasette/issues/983#issuecomment-753219407 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1MzIxOTQwNw== simonw 9599 2020-12-31T22:38:45Z 2020-12-31T22:39:10Z OWNER

You'll be able to add JavaScript plugins using a bunch of different mechanisms:

{
    "total_count": 1,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 1,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
753218817 https://github.com/simonw/datasette/issues/983#issuecomment-753218817 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1MzIxODgxNw== yozlet 173848 2020-12-31T22:32:25Z 2020-12-31T22:32:25Z NONE

Amazing work! And you've put in far more work than I'd expect to reduce the payload (which is admirable).

So, to add a plugin with the current design, it goes in (a) the template or (b) a bookmarklet, right?

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
753217917 https://github.com/simonw/datasette/issues/983#issuecomment-753217917 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1MzIxNzkxNw== simonw 9599 2020-12-31T22:23:29Z 2020-12-31T22:23:36Z OWNER

If I'm going to do that, it would be good if subsequent plugins that register against the load event are executed straight away. That's a bit of a weird edge-case in plugin world - it would involve the bulkier code that gets loaded redefining how datasette.plugins.register works to special-case the 'load' hook.

Maybe the tiny bootstrap code could define a datasette.plugins.onload(callbackFunction) method which gets upgraded later into something that fires straight away? Would add more bytes though.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
753217714 https://github.com/simonw/datasette/issues/983#issuecomment-753217714 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1MzIxNzcxNA== simonw 9599 2020-12-31T22:21:33Z 2020-12-31T22:21:33Z OWNER

Eventually I'd like to provide a whole bunch of other datasette.X utility functions that plugins can use - things like datasette.addTabbedContentPane() or similar.

But I don't want to inline those into the page.

So... I think the basic plugin system remains inline - maybe from an inlined file called plugins-bootstrap.js. Then a separate plugins.js contains the rest of the API functionality.

If a plugin wants to take advantage of those APIs, maybe it registers itself using datasette.plugins.register('load', () => ...) - that load hook can then be fired once the bulkier plugin code has been loaded.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
753215761 https://github.com/simonw/datasette/issues/983#issuecomment-753215761 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1MzIxNTc2MQ== simonw 9599 2020-12-31T22:07:31Z 2020-12-31T22:07:31Z OWNER

I think I need to keep the mechanism whereby a plugin can return undefined in order to indicate that it has nothing to say for that specific item - that's borrowed from Pluggy and I've used it a bunch in my Python plugins. That makes the code a bit longer.

I'll write some example plugins to help me decide if the filtering-out-of-undefined mechanism is needed or not.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
753215545 https://github.com/simonw/datasette/issues/983#issuecomment-753215545 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1MzIxNTU0NQ== simonw 9599 2020-12-31T22:05:41Z 2020-12-31T22:05:41Z OWNER

Using object destructuring like that is a great idea. I'm going to play with your version - it's delightfully succinct.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752882797 https://github.com/simonw/datasette/issues/983#issuecomment-752882797 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1Mjg4Mjc5Nw== dracos 154364 2020-12-31T08:07:59Z 2020-12-31T15:04:32Z NONE

If you're using arrow functions, you can presumably use default parameters, not much difference in support. That would save you 9 bytes. But OTOH you need "use strict"; to use arrow functions etc, and that's 13 bytes.

Your latest 250-byte one, with use strict, gzips to 199 bytes. The following might be 292 bytes, but compresses to 204, basically the same, and works in any browser (well, IE9+) at all:

var datasette=datasette||{};datasette.plugins=function(){var d={};return{register:function(b,c,e){d[b]||(d[b]=[]);d[b].push([c,e])},call:function(b,c){c=c||{};var e=[];(d[b]||[]).forEach(function(a){a=a[0].apply(a[0],a[1].map(function(a){return c[a]}));void 0!==a&&e.push(a)});return e}}}();

Source for that is below; I replaced the [fn,parameters] because closure-compiler includes a polyfill for that, and I ran closure-compiler --language_out ECMASCRIPT3:

var datasette = datasette || {};
datasette.plugins = (() => {
    var registry = {};
    return {
        register: (hook, fn, parameters) => {
            if (!registry[hook]) {
                registry[hook] = [];
            }
            registry[hook].push([fn, parameters]);
        },
        call: (hook, args) => {
            args = args || {};
            var results = [];
            (registry[hook] || []).forEach((data) => {
                /* Call with the correct arguments */
                var result = data[0].apply(data[0], data[1].map(parameter => args[parameter]));
                if (result !== undefined) {
                    results.push(result);
                }
            });
            return results;
        }
    };
})();
{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752888552 https://github.com/simonw/datasette/issues/983#issuecomment-752888552 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1Mjg4ODU1Mg== dracos 154364 2020-12-31T08:33:11Z 2020-12-31T08:34:27Z NONE

If you could say that all hook functions had to accept one options parameter (and could use object destructuring if they wished to only see a subset), you could have this, which minifies (to all-browser-JS) to 200 bytes, gzips to 146, and works practically the same:

var datasette = datasette || {};
datasette.plugins = (() => {
    var registry = {};
    return {
        register: (hook, fn) => {
            registry[hook] = registry[hook] || [];
            registry[hook].push(fn);
        },
        call: (hook, args) => {
            var results = (registry[hook] || []).map(fn => fn(args||{}));
            return results;
        }
    };
})();

var datasette=datasette||{};datasette.plugins=function(){var b={};return{register:function(a,c){b[a]=b[a]||[];b[a].push(c)},call:function(a,c){return(b[a]||[]).map(function(a){return a(c||{})})}}}();

Called the same, definitions tiny bit different:

datasette.plugins.register('numbers', ({a, b}) => a + b)
datasette.plugins.register('numbers', o => o.a * o.b)
datasette.plugins.call('numbers', {a: 4, b: 6})
{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752773508 https://github.com/simonw/datasette/issues/983#issuecomment-752773508 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1Mjc3MzUwOA== simonw 9599 2020-12-30T22:10:08Z 2020-12-30T22:11:34Z OWNER

https://twitter.com/dracos/status/1344402639476424706 points out that plugins returning 0 will be ignored.

This should probably check for result !== undefined instead - knocks the size up to 250.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752770488 https://github.com/simonw/datasette/issues/983#issuecomment-752770488 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1Mjc3MDQ4OA== simonw 9599 2020-12-30T21:55:35Z 2020-12-30T21:58:26Z OWNER

This one minifies to 241:

var datasette = datasette || {};
datasette.plugins = (() => {
    var registry = {};
    return {
        register: (hook, fn, parameters) => {
            if (!registry[hook]) {
                registry[hook] = [];
            }
            registry[hook].push([fn, parameters]);
        },
        call: (hook, args) => {
            args = args || {};
            var results = [];
            (registry[hook] || []).forEach(([fn, parameters]) => {
                /* Call with the correct arguments */
                var result = fn.apply(fn, parameters.map(parameter => args[parameter]));
                if (result) {
                    results.push(result);
                }
            });
            return results;
        }
    };
})();

var datasette=datasette||{};datasette.plugins=(()=>{var a={};return{register:(t,r,e)=>{a[t]||(a[t]=[]),a[t].push([r,e])},call:(t,r)=>{r=r||{};var e=[];return(a[t]||[]).forEach(([a,t])=>{var s=a.apply(a,t.map(a=>r[a]));s&&e.push(s)}),e}}})();

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752770133 https://github.com/simonw/datasette/issues/983#issuecomment-752770133 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1Mjc3MDEzMw== simonw 9599 2020-12-30T21:53:45Z 2020-12-30T21:54:22Z OWNER

FixMyStreet inlines some JavaScript, and it's always a good idea to copy what they're doing when it comes to web performance: https://github.com/mysociety/fixmystreet/blob/23e9564b58a86b783ce47f3c0bf837cbd4fe7282/templates/web/base/common_header_tags.html#L19-L25

Note var fixmystreet=fixmystreet||{}; which is shorter - https://twitter.com/dracos/status/1344399909794045954

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752767500 https://github.com/simonw/datasette/issues/983#issuecomment-752767500 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1Mjc2NzUwMA== simonw 9599 2020-12-30T21:42:07Z 2020-12-30T21:42:07Z OWNER

Another option: have both "dev" and "production" versions of the plugin mechanism script. Make it easy to switch between the two. Build JavaScript unit tests that exercise the "production" APIs against the development version, and have extra tests that just work against the features in the development version.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752767174 https://github.com/simonw/datasette/issues/983#issuecomment-752767174 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1Mjc2NzE3NA== simonw 9599 2020-12-30T21:40:44Z 2020-12-30T21:40:44Z OWNER

Started a Twitter thread about this here: https://twitter.com/simonw/status/1344392603794477056

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752751490 https://github.com/simonw/datasette/issues/983#issuecomment-752751490 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1Mjc1MTQ5MA== simonw 9599 2020-12-30T20:40:04Z 2020-12-30T21:34:22Z OWNER

This one is 683 bytes with Uglify - I like how https://skalman.github.io/UglifyJS-online/ shows you the minified character count as you edit the script:

window.datasette = window.datasette || {};
window.datasette.plugins = (() => {
    var registry = {};
    var definitions = {};
    var stringify = JSON.stringify;

    function extractParameters(fn) {
        var match = /\((.*)\)/.exec(fn.toString());
        if (match && match[1].trim()) {
            return match[1].split(',').map(s => s.trim());
        } else {
            return [];
        }
    }

    function isSubSet(a, b) {
        return a.every(parameter => b.includes(parameter))
    }

    return {
        _r: registry,
        define: (hook, parameters) => {
            definitions[hook] = parameters || [];
        },
        register: (hook, fn, parameters) => {
            parameters = parameters || extractParameters(fn);
            if (!definitions[hook]) {
                throw 'Hook "' + hook + '" not defined';
            }
            /* Check parameters is a subset of definitions[hook] */
            var validParameters = definitions[hook];
            if (!isSubSet(parameters, validParameters)) {
                throw '"' + hook + '" valid args: ' + stringify(validParameters);
            }
            if (!registry[hook]) {
                registry[hook] = [];
            }
            registry[hook].push([fn, parameters]);
        },

        call: (hook, args) => {
            args = args || {};
            if (!definitions[hook]) {
                throw '"' + hook + '" hook not defined';
            }
            if (!isSubSet(Object.keys(args), definitions[hook])) {
                throw '"' + hook + '" valid args: ' + stringify(definitions[hook]);
            }

            var implementations = registry[hook] || [];
            var results = [];
            implementations.forEach(([fn, parameters]) => {
                /* Call with the correct arguments */
                var callWith = parameters.map(parameter => args[parameter]);
                var result = fn.apply(fn, callWith);
                if (result) {
                    results.push(result);
                }
            });
            return results;
        }       
    };
})();

window.datasette=window.datasette||{},window.datasette.plugins=(()=>{var t={},r={},e=JSON.stringify;function i(t,r){return t.every(t=>r.includes(t))}return{_r:t,define:(t,e)=>{r[t]=e||[]},register:(a,n,o)=>{if(o=o||function(t){var r=/\((.*)\)/.exec(t.toString());return r&&r[1].trim()?r[1].split(",").map(t=>t.trim()):[]}(n),!r[a])throw'Hook "'+a+'" not defined';var d=r[a];if(!i(o,d))throw'"'+a+'" valid args: '+e(d);t[a]||(t[a]=[]),t[a].push([n,o])},call:(a,n)=>{if(n=n||{},!r[a])throw'"'+a+'" hook not defined';if(!i(Object.keys(n),r[a]))throw'"'+a+'" valid args: '+e(r[a]);var o=t[a]||[],d=[];return o.forEach(([t,r])=>{var e=r.map(t=>n[t]),i=t.apply(t,e);i&&d.push(i)}),d}}})();

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752760815 https://github.com/simonw/datasette/issues/983#issuecomment-752760815 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1Mjc2MDgxNQ== simonw 9599 2020-12-30T21:15:41Z 2020-12-30T21:15:41Z OWNER

I'm going to write a few example plugins and try them out against the longer and shorter versions of the script, to get a better feel for how useful the longer versions with the error handling and explicit definition actually are.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752760054 https://github.com/simonw/datasette/issues/983#issuecomment-752760054 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1Mjc2MDA1NA== simonw 9599 2020-12-30T21:12:36Z 2020-12-30T21:14:05Z OWNER

I gotta admit that 262 byte version is pretty tempting, if it's going to end up in the <head> of every single page.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752759885 https://github.com/simonw/datasette/issues/983#issuecomment-752759885 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1Mjc1OTg4NQ== simonw 9599 2020-12-30T21:11:52Z 2020-12-30T21:14:00Z OWNER

262 bytes if I remove the parameter introspection code, instead requiring plugin authors to specify the arguments they take:

window.datasette = window.datasette || {};
window.datasette.plugins = (() => {
    var registry = {};
    return {
        register: (hook, fn, parameters) => {
            if (!registry[hook]) {
                registry[hook] = [];
            }
            registry[hook].push([fn, parameters]);
        },
        call: (hook, args) => {
            args = args || {};
            var results = [];
            (registry[hook] || []).forEach(([fn, parameters]) => {
                /* Call with the correct arguments */
                var callWith = parameters.map(parameter => args[parameter]);
                var result = fn.apply(fn, callWith);
                if (result) {
                    results.push(result);
                }
            });
            return results;
        }
    };
})();

window.datasette=window.datasette||{},window.datasette.plugins=(()=>{var a={};return{register:(t,e,r)=>{a[t]||(a[t]=[]),a[t].push([e,r])},call:(t,e)=>{e=e||{};var r=[];return(a[t]||[]).forEach(([a,t])=>{var s=t.map(a=>e[a]),d=a.apply(a,s);d&&r.push(d)}),r}}})();

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752758802 https://github.com/simonw/datasette/issues/983#issuecomment-752758802 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1Mjc1ODgwMg== simonw 9599 2020-12-30T21:07:33Z 2020-12-30T21:10:10Z OWNER

Removing the datasette.plugin.define() method and associated error handling reduces the uglified version from 683 bytes to 380 bytes. I think the error checking is worth the extra 303 bytes per page load, even if it's only really needed for a better developer experience.

window.datasette = window.datasette || {};
window.datasette.plugins = (() => {
    var registry = {};

    function extractParameters(fn) {
        var match = /\((.*)\)/.exec(fn.toString());
        if (match && match[1].trim()) {
            return match[1].split(',').map(s => s.trim());
        } else {
            return [];
        }
    }
    return {
        register: (hook, fn, parameters) => {
            parameters = parameters || extractParameters(fn);
            if (!registry[hook]) {
                registry[hook] = [];
            }
            registry[hook].push([fn, parameters]);
        },

        call: (hook, args) => {
            args = args || {};
            var implementations = registry[hook] || [];
            var results = [];
            implementations.forEach(([fn, parameters]) => {
                /* Call with the correct arguments */
                var callWith = parameters.map(parameter => args[parameter]);
                var result = fn.apply(fn, callWith);
                if (result) {
                    results.push(result);
                }
            });
            return results;
        }
    };
})();

window.datasette=window.datasette||{},window.datasette.plugins=(()=>{var t={};return{register:(r,a,e)=>{e=e||function(t){var r=/\((.*)\)/.exec(t.toString());return r&&r[1].trim()?r[1].split(",").map(t=>t.trim()):[]}(a),t[r]||(t[r]=[]),t[r].push([a,e])},call:(r,a)=>{a=a||{};var e=t[r]||[],i=[];return e.forEach(([t,r])=>{var e=r.map(t=>a[t]),n=t.apply(t,e);n&&i.push(n)}),i}}})();

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752757289 https://github.com/simonw/datasette/issues/983#issuecomment-752757289 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1Mjc1NzI4OQ== simonw 9599 2020-12-30T21:02:20Z 2020-12-30T21:02:20Z OWNER

I'm going to need to add JavaScript unit tests for this new plugin system.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752750551 https://github.com/simonw/datasette/issues/983#issuecomment-752750551 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1Mjc1MDU1MQ== simonw 9599 2020-12-30T20:36:38Z 2020-12-30T20:37:48Z OWNER

This version minifies to 702 characters:

window.datasette = window.datasette || {};
window.datasette.plugins = (() => {
    var registry = {};
    var definitions = {};
    var stringify = JSON.stringify;

    function extractParameters(fn) {
        var match = /\((.*)\)/.exec(fn.toString());
        if (match && match[1].trim()) {
            return match[1].split(',').map(s => s.trim());
        } else {
            return [];
        }
    }

    function isSubSet(a, b) {
        return a.every(parameter => b.includes(parameter))
    }

    return {
        _registry: registry,
        define: (hook, parameters) => {
            definitions[hook] = parameters || [];
        },
        register: (hook, fn, parameters) => {
            parameters = parameters || extractParameters(fn);
            if (!definitions[hook]) {
                throw '"' + hook + '" is not a defined hook';
            }
            /* Check parameters is a subset of definitions[hook] */
            var validParameters = definitions[hook];
            if (!isSubSet(parameters, validParameters)) {
                throw '"' + hook + '" valid args are ' + stringify(validParameters);
            }
            if (!registry[hook]) {
                registry[hook] = [];
            }
            registry[hook].push([fn, parameters]);
        },

        call: (hook, args) => {
            args = args || {};
            if (!definitions[hook]) {
                throw '"' + hook + '" hook is not defined';
            }
            if (!isSubSet(Object.keys(args), definitions[hook])) {
                throw '"' + hook + '" valid args: ' + stringify(definitions[hook]);
            }

            var implementations = registry[hook] || [];
            var results = [];
            implementations.forEach(([fn, parameters]) => {
                /* Call with the correct arguments */
                var callWith = parameters.map(parameter => args[parameter]);
                var result = fn.apply(fn, callWith);
                if (result) {
                    results.push(result);
                }
            });
            return results;
        }       
    };
})();

Or 701 characters using https://skalman.github.io/UglifyJS-online/

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752749189 https://github.com/simonw/datasette/issues/983#issuecomment-752749189 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1Mjc0OTE4OQ== simonw 9599 2020-12-30T20:31:28Z 2020-12-30T20:31:28Z OWNER

Using raw string exceptions, throw '"' + hook + '" hook has not been defined';, knocks it down to 795 characters.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752748496 https://github.com/simonw/datasette/issues/983#issuecomment-752748496 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1Mjc0ODQ5Ng== simonw 9599 2020-12-30T20:28:48Z 2020-12-30T20:28:48Z OWNER

If I'm going to minify it I'll need to figure out a build step in Datasette itself so that I can easily work on that minified version.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752747999 https://github.com/simonw/datasette/issues/983#issuecomment-752747999 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1Mjc0Nzk5OQ== simonw 9599 2020-12-30T20:27:00Z 2020-12-30T20:27:00Z OWNER

I need to decide how this code is going to be loaded. Putting it in a blocking <script> element in the head would work, but I'd rather not block loading of the rest of the page. Using a <script async> method would be nicer, but then I have to worry about plugins attempting to register themselves before the page has fully loaded.

Running it through https://javascript-minifier.com/ produces this, which is 855 characters - so maybe I could inline that into the header of the page?

window.datasette={},window.datasette.plugins=function(){var r={},n={};function e(r,n){return r.every(r=>n.includes(r))}return{define:function(r,e){n[r]=e||[]},register:function(t,i,o){if(o=o||function(r){var n=/\((.*)\)/.exec(r.toString());return n&&n[1].trim()?n[1].split(",").map(r=>r.trim()):[]}(i),!n[t])throw new Error('"'+t+'" is not a defined plugin hook');if(!n[t])throw new Error('"'+t+'" is not a defined plugin hook');var a=n[t];if(!e(o,a))throw new Error('"'+t+'" valid parameters are '+JSON.stringify(a));r[t]||(r[t]=[]),r[t].push([i,o])},_registry:r,call:function(t,i){if(i=i||{},!n[t])throw new Error('"'+t+'" hook has not been defined');if(!e(Object.keys(i),n[t]))throw new Error('"'+t+'" valid arguments are '+JSON.stringify(n[t]));var o=r[t]||[],a=[];return o.forEach(([r,n])=>{var e=n.map(r=>i[r]),t=r.apply(r,e);t&&a.push(t)}),a}}}();

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752747169 https://github.com/simonw/datasette/issues/983#issuecomment-752747169 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1Mjc0NzE2OQ== simonw 9599 2020-12-30T20:24:07Z 2020-12-30T20:24:07Z OWNER

This version adds datasette.plugins.define() plus extra validation of both .register() and .call():

window.datasette = {};
window.datasette.plugins = (function() {
    var registry = {};
    var definitions = {};

    function extractParameters(fn) {
        var match = /\((.*)\)/.exec(fn.toString());
        if (match && match[1].trim()) {
            return match[1].split(',').map(s => s.trim());
        } else {
            return [];
        }
    }

    function define(hook, parameters) {
        definitions[hook] = parameters || [];
    }

    function isSubSet(a, b) {
        return a.every(parameter => b.includes(parameter))
    }

    function register(hook, fn, parameters) {
        parameters = parameters || extractParameters(fn);
        if (!definitions[hook]) {
            throw new Error('"' + hook + '" is not a defined plugin hook');
        }
        if (!definitions[hook]) {
            throw new Error('"' + hook + '" is not a defined plugin hook');
        }
        /* Check parameters is a subset of definitions[hook] */
        var validParameters = definitions[hook];
        if (!isSubSet(parameters, validParameters)) {
            throw new Error('"' + hook + '" valid parameters are ' + JSON.stringify(validParameters));
        }
        if (!registry[hook]) {
            registry[hook] = [];
        }
        registry[hook].push([fn, parameters]);
    }

    function call(hook, args) {
        args = args || {};
        if (!definitions[hook]) {
            throw new Error('"' + hook + '" hook has not been defined');
        }
        if (!isSubSet(Object.keys(args), definitions[hook])) {
            throw new Error('"' + hook + '" valid arguments are ' + JSON.stringify(definitions[hook]));
        }

        var implementations = registry[hook] || [];
        var results = [];
        implementations.forEach(([fn, parameters]) => {
            /* Call with the correct arguments */
            var callWith = parameters.map(parameter => args[parameter]);
            var result = fn.apply(fn, callWith);
            if (result) {
                results.push(result);
            }
        });
        return results;
    }
    return {
        define: define,
        register: register,
        _registry: registry,
        call: call
    };
})();

Usage:

datasette.plugins.define('numbers', ['a', 'b'])
datasette.plugins.register('numbers', (a, b) => a + b)
datasette.plugins.register('numbers', (a, b) => a * b)
datasette.plugins.call('numbers', {a: 4, b: 6})
{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752744311 https://github.com/simonw/datasette/issues/983#issuecomment-752744311 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1Mjc0NDMxMQ== simonw 9599 2020-12-30T20:12:50Z 2020-12-30T20:13:02Z OWNER

This could work to define a plugin hook:

datasette.plugins.define('numbers', ['a' ,'b'])
{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752744195 https://github.com/simonw/datasette/issues/983#issuecomment-752744195 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1Mjc0NDE5NQ== simonw 9599 2020-12-30T20:12:26Z 2020-12-30T20:12:26Z OWNER

This implementation doesn't have an equivalent of "hookspecs" which can identify if a registered plugin implementation matches a known signature. I should add that, it will provide a better developer experience if someone has a typo.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752742669 https://github.com/simonw/datasette/issues/983#issuecomment-752742669 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1Mjc0MjY2OQ== simonw 9599 2020-12-30T20:07:05Z 2020-12-30T20:07:18Z OWNER

Initial prototype:

window.datasette = {};
window.datasette.plugins = (function() {
    var registry = {};

    function extractParameters(fn) {
        var match = /\((.*)\)/.exec(fn.toString());
        if (match && match[1].trim()) {
            return match[1].split(',').map(s => s.trim());
        } else {
            return [];
        }
    }

    function register(hook, fn, parameters) {
        parameters = parameters || extractParameters(fn);
        if (!registry[hook]) {
            registry[hook] = [];
        }
        registry[hook].push([fn, parameters]);
    }

    function call(hook, args) {
        args = args || {};
        var implementations = registry[hook] || [];
        var results = [];
        implementations.forEach(([fn, parameters]) => {
            /* Call with the correct arguments */
            var callWith = parameters.map(parameter => args[parameter]);
            var result = fn.apply(fn, callWith);
            if (result) {
                results.push(result);
            }
        });
        return results;
    }
    return {
        register: register,
        _registry: registry,
        call: call
    };
})();

Usage example:

datasette.plugins.register('numbers', (a, b) => a + b)
datasette.plugins.register('numbers', (a, b) => a * b)
datasette.plugins.call('numbers', {a: 4, b: 6})
/* Returns [10, 24] */
{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752729035 https://github.com/simonw/datasette/issues/983#issuecomment-752729035 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1MjcyOTAzNQ== simonw 9599 2020-12-30T19:15:56Z 2020-12-30T19:16:44Z OWNER

The column_actions hook is the obvious first place to try this out. What are some demo plugins I could build for it?

  • Word cloud for this column
  • Count values (essentially linking to the SQL query for that column, as an extended version of the facet counts) - would be great if this could include pagination somehow, via #856.
  • Extract this column into a separate table
  • Add an index to this column
{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752722863 https://github.com/simonw/datasette/issues/983#issuecomment-752722863 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1MjcyMjg2Mw== simonw 9599 2020-12-30T18:52:39Z 2020-12-30T18:52:39Z OWNER

Then to call the plugins:

datasette.plugins.call('column_actions', {database: 'database', table: 'table'})
{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752721840 https://github.com/simonw/datasette/issues/983#issuecomment-752721840 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1MjcyMTg0MA== simonw 9599 2020-12-30T18:48:53Z 2020-12-30T18:51:51Z OWNER

Potential design:

datasette.plugins.register('column_actions', function(database, table, column, actor) {
    /* ... *l
})

Or if you want to be explicit to survive minification:

datasette.plugins.register('column_actions', function(database, table, column, actor) {
    /* ... *l
}, ['database', 'table', 'column', 'actor'])

I'm making that list of parameter names an optional third argument to the register() function. If that argument isn't passed, introspection will be used to figure out the parameter names.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752721069 https://github.com/simonw/datasette/issues/983#issuecomment-752721069 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1MjcyMTA2OQ== simonw 9599 2020-12-30T18:46:10Z 2020-12-30T18:46:10Z OWNER

Pluggy does dependency injection by introspecting the named arguments to the Python function, which I really like.

That's tricker in JavaScript. It looks like the only way to introspect a function is to look at the .toString() representation of it and parse the (parameter, list) using a regular expression.

Even more challenging: JavaScript developers love minifying their code, and minification can shorten the function parameter names.

From https://code-maven.com/dependency-injection-in-angularjs it looks like Angular.js does dependency injection and solves this by letting you optionally provide a separate list of the arguments your function uses:

   angular.module('DemoApp', [])
   .controller('DemoController', ['$scope', '$log', function($scope, $log) {
       $scope.message = "Hello World";
       $log.debug('logging hello');
   }]);

I can copy that approach: I'll introspect by default, but provide a documented mechanism for explicitly listing your parameter names so that if you know your plugin code will be minified you can use that instead.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752715412 https://github.com/simonw/datasette/issues/983#issuecomment-752715412 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1MjcxNTQxMg== simonw 9599 2020-12-30T18:25:31Z 2020-12-30T18:25:31Z OWNER

I'm going to introduce a global datasette object which holds all the documented JavaScript API for plugin authors.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
752715236 https://github.com/simonw/datasette/issues/983#issuecomment-752715236 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc1MjcxNTIzNg== simonw 9599 2020-12-30T18:24:54Z 2020-12-30T18:24:54Z OWNER

I think I'm going to try building a very lightweight clone of the core API design of Pluggy - not the advanced features, just the idea that plugins can register and a call to plugin.nameOfHook() will return the concatenated results of all of the registered hooks.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
744066249 https://github.com/simonw/datasette/issues/983#issuecomment-744066249 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDc0NDA2NjI0OQ== simonw 9599 2020-12-13T20:47:52Z 2020-12-13T20:47:52Z OWNER

@yozlet just spotted this comment. Wow that is interesting!

With the right plugin hooks on the page (see also #987) one relatively simple way to do that could be with bookmarklets - users could install bookmarklets which, when executed against a Datasette page in their browser, use the existing JavaScript plugin integration points to add all kinds of functionality.

Doing full sandboxing is certainly daunting, but it looks like Figma figured it out so TIL it's technically feasible.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
706413753 https://github.com/simonw/datasette/issues/983#issuecomment-706413753 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDcwNjQxMzc1Mw== yozlet 173848 2020-10-09T21:41:12Z 2020-10-09T21:41:12Z NONE

If you don't mind a somewhat bonkers idea: how about a JS client-side plugin capability that allows any user looking at a Datasette site to pull in external plugins for data manipulation, even if the Datasette owner hasn't added them? (Yes, this may be much too ambitious. If you're remotely interested, maybe fork this discussion to a different issue.)

This is some fascinating reading about what JS sandboxing looks like these days:
https://www.figma.com/blog/how-we-built-the-figma-plugin-system/

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  
701629984 https://github.com/simonw/datasette/issues/983#issuecomment-701629984 https://api.github.com/repos/simonw/datasette/issues/983 MDEyOklzc3VlQ29tbWVudDcwMTYyOTk4NA== simonw 9599 2020-09-30T20:34:43Z 2020-09-30T20:34:43Z OWNER

I had a look around and there isn't an obvious pluggy equivalent in JavaScript world at the moment. Lots of frameworks like jQuery and Vue have their own custom plugin mechanisms.

https://github.com/rekit/js-plugin is a simple standalone plugin mechanism. Not quite as full-featured as Pluggy though - in particular I like how Pluggy supports multiple plugins returning results for the same hook that get concatenated into a list of results.

https://css-tricks.com/designing-a-javascript-plugin-system/ has some ideas.

{
    "total_count": 0,
    "+1": 0,
    "-1": 0,
    "laugh": 0,
    "hooray": 0,
    "confused": 0,
    "heart": 0,
    "rocket": 0,
    "eyes": 0
}
JavaScript plugin hooks mechanism similar to pluggy 712260429  

Advanced export

JSON shape: default, array, newline-delimited, object

CSV options:

CREATE TABLE [issue_comments] (
   [html_url] TEXT,
   [issue_url] TEXT,
   [id] INTEGER PRIMARY KEY,
   [node_id] TEXT,
   [user] INTEGER REFERENCES [users]([id]),
   [created_at] TEXT,
   [updated_at] TEXT,
   [author_association] TEXT,
   [body] TEXT,
   [reactions] TEXT,
   [issue] INTEGER REFERENCES [issues]([id])
, [performed_via_github_app] TEXT);
CREATE INDEX [idx_issue_comments_issue]
                ON [issue_comments] ([issue]);
CREATE INDEX [idx_issue_comments_user]
                ON [issue_comments] ([user]);