{"html_url": "https://github.com/simonw/datasette/issues/1165#issuecomment-752828851", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1165", "id": 752828851, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MjgyODg1MQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-31T03:19:38Z", "updated_at": "2020-12-31T03:19:38Z", "author_association": "OWNER", "body": "I got Cypress working! I added the `datasette.plugins` code to the table template and ran a test called `plugins.spec.js` using the following:\r\n```javascript\r\ncontext('datasette.plugins API', () => {\r\n beforeEach(() => {\r\n cy.visit('/fixtures/compound_three_primary_keys')\r\n });\r\n it('should exist', () => {\r\n let datasette;\r\n cy.window().then(win => {\r\n datasette = win.datasette;\r\n }).then(() => {\r\n expect(datasette).to.exist;\r\n expect(datasette.plugins).to.exist;\r\n });\r\n });\r\n it('should register and execute plugins', () => {\r\n let datasette;\r\n cy.window().then(win => {\r\n datasette = win.datasette;\r\n }).then(() => {\r\n expect(datasette.plugins.call('numbers')).to.deep.equal([]);\r\n // Register a plugin\r\n datasette.plugins.register(\"numbers\", (a, b) => a + b, ['a', 'b']);\r\n var result = datasette.plugins.call(\"numbers\", {a: 1, b: 2});\r\n expect(result).to.deep.equal([3]);\r\n // Second plugin\r\n datasette.plugins.register(\"numbers\", (a, b) => a * b, ['a', 'b']);\r\n var result2 = datasette.plugins.call(\"numbers\", {a: 1, b: 2});\r\n expect(result2).to.deep.equal([3, 2]);\r\n });\r\n });\r\n});\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 776635426, "label": "Mechanism for executing JavaScript unit tests"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1165#issuecomment-752839433", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1165", "id": 752839433, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MjgzOTQzMw==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-31T04:29:40Z", "updated_at": "2020-12-31T04:29:40Z", "author_association": "OWNER", "body": "Important to absorb the slightly bizarre assertion syntax from Chai - docs here https://www.chaijs.com/api/bdd/", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 776635426, "label": "Mechanism for executing JavaScript unit tests"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1165#issuecomment-752846267", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1165", "id": 752846267, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjg0NjI2Nw==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-31T05:10:41Z", "updated_at": "2020-12-31T05:13:14Z", "author_association": "OWNER", "body": "https://github.com/PostHog/posthog/tree/master/cypress/integration has some useful examples, linked from this article: https://posthog.com/blog/cypress-end-to-end-tests\r\n\r\nAlso useful: their workflow https://github.com/PostHog/posthog/blob/master/.github/workflows/e2e.yml", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 776635426, "label": "Mechanism for executing JavaScript unit tests"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/983#issuecomment-752882797", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752882797, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjg4Mjc5Nw==", "user": {"value": 154364, "label": "dracos"}, "created_at": "2020-12-31T08:07:59Z", "updated_at": "2020-12-31T15:04:32Z", "author_association": "NONE", "body": "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.\r\n\r\nYour 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:\r\n\r\n`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}}}();`\r\n\r\nSource 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`:\r\n\r\n```js\r\nvar datasette = datasette || {};\r\ndatasette.plugins = (() => {\r\n var registry = {};\r\n return {\r\n register: (hook, fn, parameters) => {\r\n if (!registry[hook]) {\r\n registry[hook] = [];\r\n }\r\n registry[hook].push([fn, parameters]);\r\n },\r\n call: (hook, args) => {\r\n args = args || {};\r\n var results = [];\r\n (registry[hook] || []).forEach((data) => {\r\n /* Call with the correct arguments */\r\n var result = data[0].apply(data[0], data[1].map(parameter => args[parameter]));\r\n if (result !== undefined) {\r\n results.push(result);\r\n }\r\n });\r\n return results;\r\n }\r\n };\r\n})();\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 712260429, "label": "JavaScript plugin hooks mechanism similar to pluggy"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/983#issuecomment-752888552", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752888552, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjg4ODU1Mg==", "user": {"value": 154364, "label": "dracos"}, "created_at": "2020-12-31T08:33:11Z", "updated_at": "2020-12-31T08:34:27Z", "author_association": "NONE", "body": "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:\r\n\r\n```js\r\nvar datasette = datasette || {};\r\ndatasette.plugins = (() => {\r\n var registry = {};\r\n return {\r\n register: (hook, fn) => {\r\n registry[hook] = registry[hook] || [];\r\n registry[hook].push(fn);\r\n },\r\n call: (hook, args) => {\r\n var results = (registry[hook] || []).map(fn => fn(args||{}));\r\n return results;\r\n }\r\n };\r\n})();\r\n```\r\n\r\n`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||{})})}}}();`\r\n\r\nCalled the same, definitions tiny bit different:\r\n\r\n```js\r\ndatasette.plugins.register('numbers', ({a, b}) => a + b)\r\ndatasette.plugins.register('numbers', o => o.a * o.b)\r\ndatasette.plugins.call('numbers', {a: 4, b: 6})\r\n```", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 712260429, "label": "JavaScript plugin hooks mechanism similar to pluggy"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1165#issuecomment-753033121", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1165", "id": 753033121, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MzAzMzEyMQ==", "user": {"value": 154364, "label": "dracos"}, "created_at": "2020-12-31T19:33:47Z", "updated_at": "2020-12-31T19:33:47Z", "author_association": "NONE", "body": "Sorry to go on about it, but it's my only example ;) And thought it might be of interest/use. Here is FixMyStreet's Cypress workflow https://github.com/mysociety/fixmystreet/blob/master/.github/workflows/cypress.yml with the master script that sets up server etc at https://github.com/mysociety/fixmystreet/blob/master/bin/browser-tests (that has features such as working inside/outside Vagrant, and can do JS code coverage) and then the tests are at https://github.com/mysociety/fixmystreet/tree/master/.cypress/cypress/integration", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 776635426, "label": "Mechanism for executing JavaScript unit tests"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1166#issuecomment-753193475", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1166", "id": 753193475, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MzE5MzQ3NQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-31T21:33:00Z", "updated_at": "2020-12-31T21:33:00Z", "author_association": "OWNER", "body": "I want a CI check that confirms that files conform to prettier - but only `datasette/static/*.js` files that are not already minified.\r\n\r\nThis seems to do the job:\r\n\r\n npx prettier --check 'datasette/static/*[!.min].js'\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 777140799, "label": "Adopt Prettier for JavaScript code formatting"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1166#issuecomment-753195905", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1166", "id": 753195905, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MzE5NTkwNQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-31T21:34:46Z", "updated_at": "2020-12-31T21:34:46Z", "author_association": "OWNER", "body": "This action looks good - tag 3.2 is equivalent to this commit hash: https://github.com/creyD/prettier_action/tree/bb361e2979cff283ca7684908deac8f95400e779", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 777140799, "label": "Adopt Prettier for JavaScript code formatting"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1166#issuecomment-753197957", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1166", "id": 753197957, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MzE5Nzk1Nw==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-31T21:36:14Z", "updated_at": "2020-12-31T21:36:14Z", "author_association": "OWNER", "body": "Maybe not that action actually - I wanted to use a pre-built action to avoid installing Prettier every time, but that's what it seems to do: https://github.com/creyD/prettier_action/blob/bb361e2979cff283ca7684908deac8f95400e779/entrypoint.sh#L28-L37", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 777140799, "label": "Adopt Prettier for JavaScript code formatting"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1166#issuecomment-753200580", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1166", "id": 753200580, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MzIwMDU4MA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-31T21:38:06Z", "updated_at": "2020-12-31T21:38:06Z", "author_association": "OWNER", "body": "I think this should work:\r\n```\r\n- uses: actions/cache@v2\r\n with:\r\n path: ~/.npm\r\n key: ${{ runner.os }}-node-${{ hashFiles('**/prettier.yml' }}\r\n```\r\nI'll use the `prettier.yml` workflow that I'm about to create as the cache key for the NPM cache.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 777140799, "label": "Adopt Prettier for JavaScript code formatting"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1166#issuecomment-753209192", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1166", "id": 753209192, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MzIwOTE5Mg==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-31T21:44:22Z", "updated_at": "2020-12-31T21:44:22Z", "author_association": "OWNER", "body": "Tests passed in https://github.com/simonw/datasette/runs/1631677726?check_suite_focus=true\r\n\r\nI'm going to try submitting a pull request with badly formatted JavaScript to see if it gets caught.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 777140799, "label": "Adopt Prettier for JavaScript code formatting"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1166#issuecomment-753210536", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1166", "id": 753210536, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MzIxMDUzNg==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-31T21:45:19Z", "updated_at": "2020-12-31T21:45:19Z", "author_association": "OWNER", "body": "Oops, committed that bad formatting test to `main` instead of a branch!", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 777140799, "label": "Adopt Prettier for JavaScript code formatting"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1166#issuecomment-753211535", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1166", "id": 753211535, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MzIxMTUzNQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-31T21:46:04Z", "updated_at": "2020-12-31T21:46:04Z", "author_association": "OWNER", "body": "https://github.com/simonw/datasette/runs/1631682372?check_suite_focus=true failed!\r\n\r\n\"Trying_out_bad_formatting__refs__1166_\u00b7_simonw_datasette_8087091\"\r\n", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 777140799, "label": "Adopt Prettier for JavaScript code formatting"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1166#issuecomment-753214664", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1166", "id": 753214664, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MzIxNDY2NA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-31T21:58:04Z", "updated_at": "2020-12-31T21:58:04Z", "author_association": "OWNER", "body": "Wrote a TIL about this: https://til.simonwillison.net/github-actions/prettier-github-actions", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 777140799, "label": "Adopt Prettier for JavaScript code formatting"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/983#issuecomment-753215545", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 753215545, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MzIxNTU0NQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-31T22:05:41Z", "updated_at": "2020-12-31T22:05:41Z", "author_association": "OWNER", "body": "Using object destructuring like that is a great idea. I'm going to play with your version - it's delightfully succinct.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 712260429, "label": "JavaScript plugin hooks mechanism similar to pluggy"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/983#issuecomment-753215761", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 753215761, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MzIxNTc2MQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-31T22:07:31Z", "updated_at": "2020-12-31T22:07:31Z", "author_association": "OWNER", "body": "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.\r\n\r\nI'll write some example plugins to help me decide if the filtering-out-of-undefined mechanism is needed or not.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 712260429, "label": "JavaScript plugin hooks mechanism similar to pluggy"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/987#issuecomment-753217127", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/987", "id": 753217127, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MzIxNzEyNw==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-31T22:16:46Z", "updated_at": "2020-12-31T22:16:46Z", "author_association": "OWNER", "body": "I'm going to use `class=\"plugin-content-pre-table\"` rather than `id=` - just because I still want to be able to display all of this stuff on the single https://latest.datasette.io/-/patterns page so duplicate IDs are best avoided.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 712984738, "label": "Documented HTML hooks for JavaScript plugin authors"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/983#issuecomment-753217714", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 753217714, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MzIxNzcxNA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-31T22:21:33Z", "updated_at": "2020-12-31T22:21:33Z", "author_association": "OWNER", "body": "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.\r\n\r\nBut I don't want to inline those into the page.\r\n\r\nSo... 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.\r\n\r\nIf 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.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 712260429, "label": "JavaScript plugin hooks mechanism similar to pluggy"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/983#issuecomment-753217917", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 753217917, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MzIxNzkxNw==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-31T22:23:29Z", "updated_at": "2020-12-31T22:23:36Z", "author_association": "OWNER", "body": "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.\r\n\r\nMaybe 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.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 712260429, "label": "JavaScript plugin hooks mechanism similar to pluggy"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/983#issuecomment-753218817", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 753218817, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MzIxODgxNw==", "user": {"value": 173848, "label": "yozlet"}, "created_at": "2020-12-31T22:32:25Z", "updated_at": "2020-12-31T22:32:25Z", "author_association": "NONE", "body": "Amazing work! And you've put in far more work than I'd expect to reduce the payload (which is admirable).\r\n\r\nSo, to add a plugin with the current design, it goes in (a) the template or (b) a bookmarklet, right?", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 712260429, "label": "JavaScript plugin hooks mechanism similar to pluggy"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/983#issuecomment-753219407", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 753219407, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MzIxOTQwNw==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-31T22:38:45Z", "updated_at": "2020-12-31T22:39:10Z", "author_association": "OWNER", "body": "You'll be able to add JavaScript plugins using a bunch of different mechanisms:\r\n\r\n- In a custom template, dropping the code in to a `