{"html_url": "https://github.com/simonw/datasette/issues/983#issuecomment-752729035", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752729035, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MjcyOTAzNQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T19:15:56Z", "updated_at": "2020-12-30T19:16:44Z", "author_association": "OWNER", "body": "The `column_actions` hook is the obvious first place to try this out. What are some demo plugins I could build for it?\r\n\r\n- Word cloud for this column\r\n- 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.\r\n- Extract this column into a separate table\r\n- Add an index to this column", "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-752722863", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752722863, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MjcyMjg2Mw==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T18:52:39Z", "updated_at": "2020-12-30T18:52:39Z", "author_association": "OWNER", "body": "Then to call the plugins:\r\n```javascript\r\ndatasette.plugins.call('column_actions', {database: 'database', table: 'table'})\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-752721840", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752721840, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MjcyMTg0MA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T18:48:53Z", "updated_at": "2020-12-30T18:51:51Z", "author_association": "OWNER", "body": "Potential design:\r\n```javascript\r\ndatasette.plugins.register('column_actions', function(database, table, column, actor) {\r\n /* ... *l\r\n})\r\n```\r\nOr if you want to be explicit to survive minification:\r\n```javascript\r\ndatasette.plugins.register('column_actions', function(database, table, column, actor) {\r\n /* ... *l\r\n}, ['database', 'table', 'column', 'actor'])\r\n```\r\nI'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.\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-752721069", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752721069, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MjcyMTA2OQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T18:46:10Z", "updated_at": "2020-12-30T18:46:10Z", "author_association": "OWNER", "body": "Pluggy does dependency injection by introspecting the named arguments to the Python function, which I really like.\r\n\r\nThat'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.\r\n\r\nEven more challenging: JavaScript developers love minifying their code, and minification can shorten the function parameter names.\r\n\r\nFrom 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:\r\n\r\n```javascript\r\n angular.module('DemoApp', [])\r\n .controller('DemoController', ['$scope', '$log', function($scope, $log) {\r\n $scope.message = \"Hello World\";\r\n $log.debug('logging hello');\r\n }]);\r\n```\r\n\r\nI 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.", "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-752715412", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752715412, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MjcxNTQxMg==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T18:25:31Z", "updated_at": "2020-12-30T18:25:31Z", "author_association": "OWNER", "body": "I'm going to introduce a global `datasette` object which holds all the documented JavaScript API for plugin authors.", "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-752715236", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752715236, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MjcxNTIzNg==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T18:24:54Z", "updated_at": "2020-12-30T18:24:54Z", "author_association": "OWNER", "body": "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.", "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-752714747", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/987", "id": 752714747, "node_id": "MDEyOklzc3VlQ29tbWVudDc1MjcxNDc0Nw==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T18:23:08Z", "updated_at": "2020-12-30T18:23:20Z", "author_association": "OWNER", "body": "In terms of \"places to put your plugin content\", the simplest solution I can think of is something like this:\r\n```html\r\n
\r\n```\r\nAlternative designs:\r\n\r\n- A documented JavaScript function that returns the CSS selector where plugins should put their content\r\n- A documented JavaScript function that returns a DOM node where plugins should put their content. This would allow the JavaScript to create the element if it does not already exist (though it wouldn't be obvious WHERE that element should be created)\r\n- Documented JavaScript functions for things like \"append this node/HTML to the place-where-plugins-go\"\r\n\r\nI think the original option - an empty `
` with a known `id` attribute - is the right one to go with here. It's the simplest, it's very easy for custom template authors to understand and it acknowledges that plugins may have all kinds of extra crazy stuff they want to do - like checking in that div to see if another plugin has written to it already, for example.", "reactions": "{\"total_count\": 1, \"+1\": 1, \"-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/1165#issuecomment-752780000", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1165", "id": 752780000, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc4MDAwMA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T22:41:25Z", "updated_at": "2020-12-30T22:41:25Z", "author_association": "OWNER", "body": "Jest works with Puppeteer: https://jestjs.io/docs/en/puppeteer", "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-752779820", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1165", "id": 752779820, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc3OTgyMA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T22:40:28Z", "updated_at": "2020-12-30T22:40:28Z", "author_association": "OWNER", "body": "I don't know if Jest on the command-line is the right tool for this. It works for the `plugins.js` script but I'm increasingly going to want to start adding tests for browser JavaScript features - like the https://github.com/simonw/datasette/blob/0.53/datasette/static/table.js script - which will need to run in a browser.\r\n\r\nSo maybe I should just find a browser testing solution and figure out how to run that under CI in GitHub Actions. Maybe https://www.cypress.io/ ?", "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-752779490", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1165", "id": 752779490, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc3OTQ5MA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T22:38:43Z", "updated_at": "2020-12-30T22:38:43Z", "author_association": "OWNER", "body": "Turned that into a TIL: https://til.simonwillison.net/javascript/jest-without-package-json", "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-752777744", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1165", "id": 752777744, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc3Nzc0NA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T22:30:24Z", "updated_at": "2020-12-30T22:30:24Z", "author_association": "OWNER", "body": "https://www.valentinog.com/blog/jest/ was useful.\r\n\r\nI created a `static/__tests__` folder and added this file as `plugins.spec.js`:\r\n\r\n```javascript\r\nconst datasette = require(\"../plugins.js\");\r\n\r\ndescribe(\"Datasette Plugins\", () => {\r\n test(\"it should have datasette.plugins\", () => {\r\n expect(!!datasette.plugins).toEqual(true);\r\n });\r\n test(\"registering a plugin should work\", () => {\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).toEqual([3]);\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).toEqual([3, 2]);\r\n });\r\n});\r\n```\r\n\r\nIn `static/plugins.js` I put this:\r\n```javascript\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(([fn, parameters]) => {\r\n /* Call with the correct arguments */\r\n var result = fn.apply(fn, parameters.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\r\nmodule.exports = datasette;\r\n```\r\nNote the `module.exports` line at the end.\r\n\r\nThen inside `static/` I ran the following command:\r\n\r\n```\r\n% npx jest -c '{}'\r\n PASS __tests__/plugins.spec.js\r\n Datasette Plugins\r\n \u2713 it should have datasette.plugins (3 ms)\r\n \u2713 registering a plugin should work (1 ms)\r\n\r\nTest Suites: 1 passed, 1 total\r\nTests: 2 passed, 2 total\r\nSnapshots: 0 total\r\nTime: 1.163 s\r\nRan all test suites.\r\n```\r\nThe `-c {}` was necessary because I didn't have a Jest configuration or a `package.json`.", "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-752773508", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752773508, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc3MzUwOA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T22:10:08Z", "updated_at": "2020-12-30T22:11:34Z", "author_association": "OWNER", "body": "https://twitter.com/dracos/status/1344402639476424706 points out that plugins returning 0 will be ignored.\r\n\r\nThis should probably check for `result !== undefined` instead - knocks the size up to 250.", "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-752770488", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752770488, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc3MDQ4OA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T21:55:35Z", "updated_at": "2020-12-30T21:58:26Z", "author_association": "OWNER", "body": "This one minifies to 241:\r\n```javascript\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(([fn, parameters]) => {\r\n /* Call with the correct arguments */\r\n var result = fn.apply(fn, parameters.map(parameter => args[parameter]));\r\n if (result) {\r\n results.push(result);\r\n }\r\n });\r\n return results;\r\n }\r\n };\r\n})();\r\n```\r\n`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}}})();`", "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-752770133", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752770133, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc3MDEzMw==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T21:53:45Z", "updated_at": "2020-12-30T21:54:22Z", "author_association": "OWNER", "body": "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\r\n\r\nNote `var fixmystreet=fixmystreet||{};` which is shorter - https://twitter.com/dracos/status/1344399909794045954", "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/1164#issuecomment-752769452", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1164", "id": 752769452, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc2OTQ1Mg==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T21:50:16Z", "updated_at": "2020-12-30T21:50:16Z", "author_association": "OWNER", "body": "If I implement this I can automate the CodeMirror minification and remove the bit about running `uglify-js` against it from the documentation here: https://docs.datasette.io/en/0.53/contributing.html#upgrading-codemirror", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 776634318, "label": "Mechanism for minifying JavaScript that ships with Datasette"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1164#issuecomment-752768785", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1164", "id": 752768785, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc2ODc4NQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T21:47:06Z", "updated_at": "2020-12-30T21:47:06Z", "author_association": "OWNER", "body": "If I'm going to minify `table.js` I'd like to offer a source map for it.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 776634318, "label": "Mechanism for minifying JavaScript that ships with Datasette"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1164#issuecomment-752768652", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1164", "id": 752768652, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc2ODY1Mg==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T21:46:29Z", "updated_at": "2020-12-30T21:46:29Z", "author_association": "OWNER", "body": "Running https://skalman.github.io/UglifyJS-online/ against https://github.com/simonw/datasette/blob/0.53/datasette/static/table.js knocks it down from 7810 characters to 4643.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 776634318, "label": "Mechanism for minifying JavaScript that ships with Datasette"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/983#issuecomment-752767500", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752767500, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc2NzUwMA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T21:42:07Z", "updated_at": "2020-12-30T21:42:07Z", "author_association": "OWNER", "body": "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.", "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-752767174", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752767174, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc2NzE3NA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T21:40:44Z", "updated_at": "2020-12-30T21:40:44Z", "author_association": "OWNER", "body": "Started a Twitter thread about this here: https://twitter.com/simonw/status/1344392603794477056", "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-752760815", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752760815, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc2MDgxNQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T21:15:41Z", "updated_at": "2020-12-30T21:15:41Z", "author_association": "OWNER", "body": "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.", "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-752760054", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752760054, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc2MDA1NA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T21:12:36Z", "updated_at": "2020-12-30T21:14:05Z", "author_association": "OWNER", "body": "I gotta admit that 262 byte version is pretty tempting, if it's going to end up in the `` of every single page.", "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-752759885", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752759885, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc1OTg4NQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T21:11:52Z", "updated_at": "2020-12-30T21:14:00Z", "author_association": "OWNER", "body": "262 bytes if I remove the parameter introspection code, instead requiring plugin authors to specify the arguments they take:\r\n```javascript\r\nwindow.datasette = window.datasette || {};\r\nwindow.datasette.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(([fn, parameters]) => {\r\n /* Call with the correct arguments */\r\n var callWith = parameters.map(parameter => args[parameter]);\r\n var result = fn.apply(fn, callWith);\r\n if (result) {\r\n results.push(result);\r\n }\r\n });\r\n return results;\r\n }\r\n };\r\n})();\r\n```\r\n`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}}})();`", "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-752758802", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752758802, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc1ODgwMg==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T21:07:33Z", "updated_at": "2020-12-30T21:10:10Z", "author_association": "OWNER", "body": "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.\r\n```javascript\r\nwindow.datasette = window.datasette || {};\r\nwindow.datasette.plugins = (() => {\r\n var registry = {};\r\n\r\n function extractParameters(fn) {\r\n var match = /\\((.*)\\)/.exec(fn.toString());\r\n if (match && match[1].trim()) {\r\n return match[1].split(',').map(s => s.trim());\r\n } else {\r\n return [];\r\n }\r\n }\r\n return {\r\n register: (hook, fn, parameters) => {\r\n parameters = parameters || extractParameters(fn);\r\n if (!registry[hook]) {\r\n registry[hook] = [];\r\n }\r\n registry[hook].push([fn, parameters]);\r\n },\r\n \r\n call: (hook, args) => {\r\n args = args || {};\r\n var implementations = registry[hook] || [];\r\n var results = [];\r\n implementations.forEach(([fn, parameters]) => {\r\n /* Call with the correct arguments */\r\n var callWith = parameters.map(parameter => args[parameter]);\r\n var result = fn.apply(fn, callWith);\r\n if (result) {\r\n results.push(result);\r\n }\r\n });\r\n return results;\r\n }\r\n };\r\n})();\r\n```\r\n`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}}})();`", "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-752757910", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1165", "id": 752757910, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc1NzkxMA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T21:04:18Z", "updated_at": "2020-12-30T21:04:18Z", "author_association": "OWNER", "body": "https://jestjs.io/ looks worth trying here.", "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-752757289", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752757289, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc1NzI4OQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T21:02:20Z", "updated_at": "2020-12-30T21:02:20Z", "author_association": "OWNER", "body": "I'm going to need to add JavaScript unit tests for this new plugin system.", "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/1164#issuecomment-752757075", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1164", "id": 752757075, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc1NzA3NQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T21:01:27Z", "updated_at": "2020-12-30T21:01:27Z", "author_association": "OWNER", "body": "I don't want Datasette contributors to need a working Node.js install to run the tests or work on Datasette unless they are explicitly working on the JavaScript.\r\n\r\nI think I'm going to do this with a unit test that runs only if `upglify-js` is available on the path and confirms that the `*.min.js` version of each script in the repository correctly matches the results from running `uglify-js` against it.\r\n\r\nThat way if anyone checks in a change to JavaScript but forgets to run the minifier the tests will fail in CI.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 776634318, "label": "Mechanism for minifying JavaScript that ships with Datasette"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/1164#issuecomment-752756612", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/1164", "id": 752756612, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc1NjYxMg==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T20:59:54Z", "updated_at": "2020-12-30T20:59:54Z", "author_association": "OWNER", "body": "I tried a few different pure-Python JavaScript minifying libraries and none of them produced results as good as https://www.npmjs.com/package/uglify-js for the plugin code I'm considering in #983.\r\n\r\nSo I think I'll need to rely on a Node.js tool for this.", "reactions": "{\"total_count\": 0, \"+1\": 0, \"-1\": 0, \"laugh\": 0, \"hooray\": 0, \"confused\": 0, \"heart\": 0, \"rocket\": 0, \"eyes\": 0}", "issue": {"value": 776634318, "label": "Mechanism for minifying JavaScript that ships with Datasette"}, "performed_via_github_app": null} {"html_url": "https://github.com/simonw/datasette/issues/983#issuecomment-752751490", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752751490, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc1MTQ5MA==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T20:40:04Z", "updated_at": "2020-12-30T21:34:22Z", "author_association": "OWNER", "body": "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:\r\n```javascript\r\nwindow.datasette = window.datasette || {};\r\nwindow.datasette.plugins = (() => {\r\n var registry = {};\r\n var definitions = {};\r\n var stringify = JSON.stringify;\r\n\r\n function extractParameters(fn) {\r\n var match = /\\((.*)\\)/.exec(fn.toString());\r\n if (match && match[1].trim()) {\r\n return match[1].split(',').map(s => s.trim());\r\n } else {\r\n return [];\r\n }\r\n }\r\n\r\n function isSubSet(a, b) {\r\n return a.every(parameter => b.includes(parameter))\r\n }\r\n\r\n return {\r\n _r: registry,\r\n define: (hook, parameters) => {\r\n definitions[hook] = parameters || [];\r\n },\r\n register: (hook, fn, parameters) => {\r\n parameters = parameters || extractParameters(fn);\r\n if (!definitions[hook]) {\r\n throw 'Hook \"' + hook + '\" not defined';\r\n }\r\n /* Check parameters is a subset of definitions[hook] */\r\n var validParameters = definitions[hook];\r\n if (!isSubSet(parameters, validParameters)) {\r\n throw '\"' + hook + '\" valid args: ' + stringify(validParameters);\r\n }\r\n if (!registry[hook]) {\r\n registry[hook] = [];\r\n }\r\n registry[hook].push([fn, parameters]);\r\n },\r\n \r\n call: (hook, args) => {\r\n args = args || {};\r\n if (!definitions[hook]) {\r\n throw '\"' + hook + '\" hook not defined';\r\n }\r\n if (!isSubSet(Object.keys(args), definitions[hook])) {\r\n throw '\"' + hook + '\" valid args: ' + stringify(definitions[hook]);\r\n }\r\n \r\n var implementations = registry[hook] || [];\r\n var results = [];\r\n implementations.forEach(([fn, parameters]) => {\r\n /* Call with the correct arguments */\r\n var callWith = parameters.map(parameter => args[parameter]);\r\n var result = fn.apply(fn, callWith);\r\n if (result) {\r\n results.push(result);\r\n }\r\n });\r\n return results;\r\n } \r\n };\r\n})();\r\n```\r\n`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}}})();`", "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-752750551", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752750551, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc1MDU1MQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T20:36:38Z", "updated_at": "2020-12-30T20:37:48Z", "author_association": "OWNER", "body": "This version minifies to 702 characters:\r\n```javascript\r\nwindow.datasette = window.datasette || {};\r\nwindow.datasette.plugins = (() => {\r\n var registry = {};\r\n var definitions = {};\r\n var stringify = JSON.stringify;\r\n\r\n function extractParameters(fn) {\r\n var match = /\\((.*)\\)/.exec(fn.toString());\r\n if (match && match[1].trim()) {\r\n return match[1].split(',').map(s => s.trim());\r\n } else {\r\n return [];\r\n }\r\n }\r\n\r\n function isSubSet(a, b) {\r\n return a.every(parameter => b.includes(parameter))\r\n }\r\n\r\n return {\r\n _registry: registry,\r\n define: (hook, parameters) => {\r\n definitions[hook] = parameters || [];\r\n },\r\n register: (hook, fn, parameters) => {\r\n parameters = parameters || extractParameters(fn);\r\n if (!definitions[hook]) {\r\n throw '\"' + hook + '\" is not a defined hook';\r\n }\r\n /* Check parameters is a subset of definitions[hook] */\r\n var validParameters = definitions[hook];\r\n if (!isSubSet(parameters, validParameters)) {\r\n throw '\"' + hook + '\" valid args are ' + stringify(validParameters);\r\n }\r\n if (!registry[hook]) {\r\n registry[hook] = [];\r\n }\r\n registry[hook].push([fn, parameters]);\r\n },\r\n \r\n call: (hook, args) => {\r\n args = args || {};\r\n if (!definitions[hook]) {\r\n throw '\"' + hook + '\" hook is not defined';\r\n }\r\n if (!isSubSet(Object.keys(args), definitions[hook])) {\r\n throw '\"' + hook + '\" valid args: ' + stringify(definitions[hook]);\r\n }\r\n \r\n var implementations = registry[hook] || [];\r\n var results = [];\r\n implementations.forEach(([fn, parameters]) => {\r\n /* Call with the correct arguments */\r\n var callWith = parameters.map(parameter => args[parameter]);\r\n var result = fn.apply(fn, callWith);\r\n if (result) {\r\n results.push(result);\r\n }\r\n });\r\n return results;\r\n } \r\n };\r\n})();\r\n```\r\nOr 701 characters using https://skalman.github.io/UglifyJS-online/", "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-752749189", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752749189, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc0OTE4OQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T20:31:28Z", "updated_at": "2020-12-30T20:31:28Z", "author_association": "OWNER", "body": "Using raw string exceptions, `throw '\"' + hook + '\" hook has not been defined';`, knocks it down to 795 characters.", "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-752748496", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752748496, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc0ODQ5Ng==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T20:28:48Z", "updated_at": "2020-12-30T20:28:48Z", "author_association": "OWNER", "body": "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.", "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-752747999", "issue_url": "https://api.github.com/repos/simonw/datasette/issues/983", "id": 752747999, "node_id": "MDEyOklzc3VlQ29tbWVudDc1Mjc0Nzk5OQ==", "user": {"value": 9599, "label": "simonw"}, "created_at": "2020-12-30T20:27:00Z", "updated_at": "2020-12-30T20:27:00Z", "author_association": "OWNER", "body": "I need to decide how this code is going to be loaded. Putting it in a blocking `