From 0605c9f229c82d3de259a544c1ea3b4dcc9e4006 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 14 Mar 2026 12:07:04 -0500 Subject: [PATCH] Tighten plugin dev file watching --- pnpm-lock.yaml | 352 ++++++++++++++++++ server/package.json | 1 + .../src/__tests__/plugin-dev-watcher.test.ts | 20 +- server/src/app.ts | 14 +- server/src/services/plugin-dev-watcher.ts | 153 ++++---- 5 files changed, 464 insertions(+), 76 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6820f52..32db336f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -246,6 +246,180 @@ importers: specifier: ^3.0.5 version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + packages/plugins/create-paperclip-plugin: + dependencies: + '@paperclipai/plugin-sdk': + specifier: workspace:* + version: link:../sdk + devDependencies: + '@types/node': + specifier: ^24.6.0 + version: 24.12.0 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + + packages/plugins/examples/plugin-authoring-smoke-example: + dependencies: + '@paperclipai/plugin-sdk': + specifier: workspace:* + version: link:../../sdk + react: + specifier: '>=18' + version: 19.2.4 + devDependencies: + '@rollup/plugin-node-resolve': + specifier: ^16.0.1 + version: 16.0.3(rollup@4.57.1) + '@rollup/plugin-typescript': + specifier: ^12.1.2 + version: 12.3.0(rollup@4.57.1)(tslib@2.8.1)(typescript@5.9.3) + '@types/node': + specifier: ^24.6.0 + version: 24.12.0 + '@types/react': + specifier: ^19.0.8 + version: 19.2.14 + esbuild: + specifier: ^0.27.3 + version: 0.27.3 + rollup: + specifier: ^4.38.0 + version: 4.57.1 + tslib: + specifier: ^2.8.1 + version: 2.8.1 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^3.0.5 + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + + packages/plugins/examples/plugin-file-browser-example: + dependencies: + '@codemirror/lang-javascript': + specifier: ^6.2.2 + version: 6.2.4 + '@codemirror/language': + specifier: ^6.11.0 + version: 6.12.1 + '@codemirror/state': + specifier: ^6.4.0 + version: 6.5.4 + '@codemirror/view': + specifier: ^6.28.0 + version: 6.39.15 + '@lezer/highlight': + specifier: ^1.2.1 + version: 1.2.3 + '@paperclipai/plugin-sdk': + specifier: workspace:* + version: link:../../sdk + codemirror: + specifier: ^6.0.1 + version: 6.0.2 + devDependencies: + '@types/node': + specifier: ^24.6.0 + version: 24.12.0 + '@types/react': + specifier: ^19.0.8 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.0.3 + version: 19.2.3(@types/react@19.2.14) + esbuild: + specifier: ^0.27.3 + version: 0.27.3 + react: + specifier: ^19.0.0 + version: 19.2.4 + react-dom: + specifier: ^19.0.0 + version: 19.2.4(react@19.2.4) + typescript: + specifier: ^5.7.3 + version: 5.9.3 + + packages/plugins/examples/plugin-hello-world-example: + dependencies: + '@paperclipai/plugin-sdk': + specifier: workspace:* + version: link:../../sdk + devDependencies: + '@types/node': + specifier: ^24.6.0 + version: 24.12.0 + '@types/react': + specifier: ^19.0.8 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.0.3 + version: 19.2.3(@types/react@19.2.14) + react: + specifier: ^19.0.0 + version: 19.2.4 + react-dom: + specifier: ^19.0.0 + version: 19.2.4(react@19.2.4) + typescript: + specifier: ^5.7.3 + version: 5.9.3 + + packages/plugins/examples/plugin-kitchen-sink-example: + dependencies: + '@paperclipai/plugin-sdk': + specifier: workspace:* + version: link:../../sdk + '@paperclipai/shared': + specifier: workspace:* + version: link:../../../shared + devDependencies: + '@types/node': + specifier: ^24.6.0 + version: 24.12.0 + '@types/react': + specifier: ^19.0.8 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.0.3 + version: 19.2.3(@types/react@19.2.14) + esbuild: + specifier: ^0.27.3 + version: 0.27.3 + react: + specifier: ^19.0.0 + version: 19.2.4 + react-dom: + specifier: ^19.0.0 + version: 19.2.4(react@19.2.4) + typescript: + specifier: ^5.7.3 + version: 5.9.3 + + packages/plugins/sdk: + dependencies: + '@paperclipai/shared': + specifier: workspace:* + version: link:../../shared + react: + specifier: '>=18' + version: 19.2.4 + zod: + specifier: ^3.24.2 + version: 3.25.76 + devDependencies: + '@types/node': + specifier: ^24.6.0 + version: 24.12.0 + '@types/react': + specifier: ^19.0.8 + version: 19.2.14 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + packages/shared: dependencies: zod: @@ -288,12 +462,24 @@ importers: '@paperclipai/db': specifier: workspace:* version: link:../packages/db + '@paperclipai/plugin-sdk': + specifier: workspace:* + version: link:../packages/plugins/sdk '@paperclipai/shared': specifier: workspace:* version: link:../packages/shared + ajv: + specifier: ^8.18.0 + version: 8.18.0 + ajv-formats: + specifier: ^3.0.1 + version: 3.0.1(ajv@8.18.0) better-auth: specifier: 1.4.18 version: 1.4.18(drizzle-kit@0.31.9)(drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4))(pg@8.18.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.12.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + chokidar: + specifier: ^4.0.3 + version: 4.0.3 detect-port: specifier: ^2.1.0 version: 2.1.0 @@ -2436,6 +2622,37 @@ packages: '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rollup/plugin-node-resolve@16.0.3': + resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-typescript@12.3.0': + resolution: {integrity: sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.14.0||^3.0.0||^4.0.0 + tslib: '*' + typescript: '>=3.7.0' + peerDependenciesMeta: + rollup: + optional: true + tslib: + optional: true + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.57.1': resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} cpu: [arm] @@ -3068,6 +3285,9 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/resolve@1.20.2': + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/send@1.2.1': resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} @@ -3148,6 +3368,17 @@ packages: resolution: {integrity: sha512-XNAb/a6TCqou+TufU8/u11HCu9x1gYvOoxLwtlXgIqmkrYQADVv6ljyW2zwiPhHz9R1gItAWpuDrdJMmrOBFEA==} engines: {node: '>= 16.0.0'} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + anser@2.3.5: resolution: {integrity: sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ==} @@ -3361,6 +3592,10 @@ packages: chevrotain@11.1.2: resolution: {integrity: sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -3660,6 +3895,10 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + default-browser-id@5.0.1: resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} engines: {node: '>=18'} @@ -3941,6 +4180,9 @@ packages: estree-util-visit@2.0.0: resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -3971,6 +4213,9 @@ packages: fast-copy@4.0.2: resolution: {integrity: sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -3978,6 +4223,9 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-xml-parser@5.3.6: resolution: {integrity: sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==} hasBin: true @@ -4165,6 +4413,10 @@ packages: is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} @@ -4193,6 +4445,9 @@ packages: engines: {node: '>=14.16'} hasBin: true + is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -4252,6 +4507,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -4742,6 +5000,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} @@ -5030,6 +5291,10 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + real-require@0.2.0: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} @@ -5046,6 +5311,10 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -5053,6 +5322,11 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -5257,6 +5531,10 @@ packages: resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==} engines: {node: '>=14.18.0'} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + tabbable@6.4.0: resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} @@ -8212,6 +8490,33 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rollup/plugin-node-resolve@16.0.3(rollup@4.57.1)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.57.1) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.11 + optionalDependencies: + rollup: 4.57.1 + + '@rollup/plugin-typescript@12.3.0(rollup@4.57.1)(tslib@2.8.1)(typescript@5.9.3)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.57.1) + resolve: 1.22.11 + typescript: 5.9.3 + optionalDependencies: + rollup: 4.57.1 + tslib: 2.8.1 + + '@rollup/pluginutils@5.3.0(rollup@4.57.1)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.57.1 + '@rollup/rollup-android-arm-eabi@4.57.1': optional: true @@ -8934,6 +9239,8 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/resolve@1.20.2': {} + '@types/send@1.2.1': dependencies: '@types/node': 25.2.3 @@ -9043,6 +9350,17 @@ snapshots: address@2.0.3: {} + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + anser@2.3.5: {} ansi-colors@4.1.3: {} @@ -9209,6 +9527,10 @@ snapshots: '@chevrotain/utils': 11.1.2 lodash-es: 4.17.23 + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -9517,6 +9839,8 @@ snapshots: deep-eql@5.0.2: {} + deepmerge@4.3.1: {} + default-browser-id@5.0.1: {} default-browser@5.5.0: @@ -9789,6 +10113,8 @@ snapshots: '@types/estree-jsx': 1.0.5 '@types/unist': 3.0.3 + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 @@ -9845,6 +10171,8 @@ snapshots: fast-copy@4.0.2: {} + fast-deep-equal@3.1.3: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -9855,6 +10183,8 @@ snapshots: fast-safe-stringify@2.1.1: {} + fast-uri@3.1.0: {} + fast-xml-parser@5.3.6: dependencies: strnum: 2.1.2 @@ -10057,6 +10387,10 @@ snapshots: is-alphabetical: 2.0.1 is-decimal: 2.0.1 + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + is-decimal@2.0.1: {} is-docker@3.0.0: {} @@ -10075,6 +10409,8 @@ snapshots: dependencies: is-docker: 3.0.0 + is-module@1.0.0: {} + is-number@7.0.0: {} is-plain-obj@4.1.0: {} @@ -10116,6 +10452,8 @@ snapshots: jsesc@3.1.0: {} + json-schema-traverse@1.0.0: {} + json5@2.2.3: {} jsonfile@4.0.0: @@ -10873,6 +11211,8 @@ snapshots: path-key@3.1.1: {} + path-parse@1.0.7: {} + path-to-regexp@8.3.0: {} path-type@4.0.0: {} @@ -11221,6 +11561,8 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + readdirp@4.1.2: {} + real-require@0.2.0: {} remark-gfm@4.0.1: @@ -11257,10 +11599,18 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 + require-from-string@2.0.2: {} + resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + reusify@1.1.0: {} robust-predicates@3.0.2: {} @@ -11510,6 +11860,8 @@ snapshots: transitivePeerDependencies: - supports-color + supports-preserve-symlinks-flag@1.0.0: {} + tabbable@6.4.0: {} tailwind-merge@3.4.1: {} diff --git a/server/package.json b/server/package.json index 2e56ae5d..d6ad8dad 100644 --- a/server/package.json +++ b/server/package.json @@ -48,6 +48,7 @@ "ajv": "^8.18.0", "ajv-formats": "^3.0.1", "better-auth": "1.4.18", + "chokidar": "^4.0.3", "detect-port": "^2.1.0", "dotenv": "^17.0.1", "drizzle-orm": "^0.38.4", diff --git a/server/src/__tests__/plugin-dev-watcher.test.ts b/server/src/__tests__/plugin-dev-watcher.test.ts index b21aaeae..75df3105 100644 --- a/server/src/__tests__/plugin-dev-watcher.test.ts +++ b/server/src/__tests__/plugin-dev-watcher.test.ts @@ -20,7 +20,7 @@ function makeTempPluginDir(): string { } describe("resolvePluginWatchTargets", () => { - it("watches the package root plus declared build output directories", () => { + it("watches package metadata plus concrete declared runtime files", () => { const pluginDir = makeTempPluginDir(); mkdirSync(path.join(pluginDir, "dist", "ui"), { recursive: true }); writeFileSync( @@ -37,26 +37,32 @@ describe("resolvePluginWatchTargets", () => { writeFileSync(path.join(pluginDir, "dist", "manifest.js"), "export default {};\n"); writeFileSync(path.join(pluginDir, "dist", "worker.js"), "export default {};\n"); writeFileSync(path.join(pluginDir, "dist", "ui", "index.js"), "export default {};\n"); + writeFileSync(path.join(pluginDir, "dist", "ui", "index.css"), "body {}\n"); const targets = resolvePluginWatchTargets(pluginDir); expect(targets).toEqual([ - { path: pluginDir, recursive: false }, - { path: path.join(pluginDir, "dist"), recursive: true }, - { path: path.join(pluginDir, "dist", "ui"), recursive: true }, + { path: path.join(pluginDir, "dist", "manifest.js"), recursive: false, kind: "file" }, + { path: path.join(pluginDir, "dist", "ui", "index.css"), recursive: false, kind: "file" }, + { path: path.join(pluginDir, "dist", "ui", "index.js"), recursive: false, kind: "file" }, + { path: path.join(pluginDir, "dist", "worker.js"), recursive: false, kind: "file" }, + { path: path.join(pluginDir, "package.json"), recursive: false, kind: "file" }, ]); }); it("falls back to dist when package metadata does not declare entrypoints", () => { const pluginDir = makeTempPluginDir(); - mkdirSync(path.join(pluginDir, "dist"), { recursive: true }); + mkdirSync(path.join(pluginDir, "dist", "nested"), { recursive: true }); writeFileSync(path.join(pluginDir, "package.json"), JSON.stringify({ name: "@acme/example" })); + writeFileSync(path.join(pluginDir, "dist", "manifest.js"), "export default {};\n"); + writeFileSync(path.join(pluginDir, "dist", "nested", "chunk.js"), "export default {};\n"); const targets = resolvePluginWatchTargets(pluginDir); expect(targets).toEqual([ - { path: pluginDir, recursive: false }, - { path: path.join(pluginDir, "dist"), recursive: true }, + { path: path.join(pluginDir, "package.json"), recursive: false, kind: "file" }, + { path: path.join(pluginDir, "dist", "manifest.js"), recursive: false, kind: "file" }, + { path: path.join(pluginDir, "dist", "nested", "chunk.js"), recursive: false, kind: "file" }, ]); }); }); diff --git a/server/src/app.ts b/server/src/app.ts index 8dfcec7c..22183848 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -272,14 +272,16 @@ export async function createApp( void toolDispatcher.initialize().catch((err) => { logger.error({ err }, "Failed to initialize plugin tool dispatcher"); }); - const devWatcher = createPluginDevWatcher( - lifecycle, - async (pluginId) => (await pluginRegistry.getById(pluginId))?.packagePath ?? null, - ); + const devWatcher = opts.uiMode === "vite-dev" + ? createPluginDevWatcher( + lifecycle, + async (pluginId) => (await pluginRegistry.getById(pluginId))?.packagePath ?? null, + ) + : null; void loader.loadAll().then((result) => { if (!result) return; for (const loaded of result.results) { - if (loaded.success && loaded.plugin.packagePath) { + if (devWatcher && loaded.success && loaded.plugin.packagePath) { devWatcher.watch(loaded.plugin.id, loaded.plugin.packagePath); } } @@ -287,7 +289,7 @@ export async function createApp( logger.error({ err }, "Failed to load ready plugins on startup"); }); process.once("exit", () => { - devWatcher.close(); + devWatcher?.close(); hostServiceCleanup.disposeAll(); hostServiceCleanup.teardown(); }); diff --git a/server/src/services/plugin-dev-watcher.ts b/server/src/services/plugin-dev-watcher.ts index f556adba..76b258c4 100644 --- a/server/src/services/plugin-dev-watcher.ts +++ b/server/src/services/plugin-dev-watcher.ts @@ -7,10 +7,14 @@ * `packagePath` in the DB) are watched. File changes in the plugin's package * directory trigger a debounced worker restart via the lifecycle manager. * + * Uses chokidar rather than raw fs.watch so we get a production-grade watcher + * backend across platforms and avoid exhausting file descriptors as quickly in + * large dev workspaces. + * * @see PLUGIN_SPEC.md §27.2 — Local Development Workflow */ -import { watch, type FSWatcher } from "node:fs"; -import { existsSync, readFileSync, statSync } from "node:fs"; +import chokidar, { type FSWatcher } from "chokidar"; +import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; import path from "node:path"; import { logger } from "../middleware/logger.js"; import type { PluginLifecycleManager } from "./plugin-lifecycle.js"; @@ -35,14 +39,15 @@ export type ResolvePluginPackagePath = ( export interface PluginDevWatcherFsDeps { existsSync?: typeof existsSync; - watch?: typeof watch; readFileSync?: typeof readFileSync; + readdirSync?: typeof readdirSync; statSync?: typeof statSync; } type PluginWatchTarget = { path: string; recursive: boolean; + kind: "file" | "dir"; }; type PluginPackageJson = { @@ -69,17 +74,19 @@ function shouldIgnorePath(filename: string | null | undefined): boolean { export function resolvePluginWatchTargets( packagePath: string, - fsDeps?: Pick, + fsDeps?: Pick, ): PluginWatchTarget[] { const fileExists = fsDeps?.existsSync ?? existsSync; const readFile = fsDeps?.readFileSync ?? readFileSync; + const readDir = fsDeps?.readdirSync ?? readdirSync; const statFile = fsDeps?.statSync ?? statSync; const absPath = path.resolve(packagePath); const targets = new Map(); - function addWatchTarget(targetPath: string, recursive: boolean): void { + function addWatchTarget(targetPath: string, recursive: boolean, kind?: "file" | "dir"): void { const resolved = path.resolve(targetPath); if (!fileExists(resolved)) return; + const inferredKind = kind ?? (statFile(resolved).isDirectory() ? "dir" : "file"); const existing = targets.get(resolved); if (existing) { @@ -87,14 +94,27 @@ export function resolvePluginWatchTargets( return; } - targets.set(resolved, { path: resolved, recursive }); + targets.set(resolved, { path: resolved, recursive, kind: inferredKind }); } - // Watch the package root non-recursively so top-level files like package.json - // can trigger reloads without traversing node_modules or other deep trees. - addWatchTarget(absPath, false); + function addRuntimeFilesFromDir(dirPath: string): void { + if (!fileExists(dirPath)) return; + + for (const entry of readDir(dirPath, { withFileTypes: true })) { + const entryPath = path.join(dirPath, entry.name); + if (entry.isDirectory()) { + addRuntimeFilesFromDir(entryPath); + continue; + } + + if (!entry.isFile()) continue; + if (!entry.name.endsWith(".js") && !entry.name.endsWith(".css")) continue; + addWatchTarget(entryPath, false, "file"); + } + } const packageJsonPath = path.join(absPath, "package.json"); + addWatchTarget(packageJsonPath, false, "file"); if (!fileExists(packageJsonPath)) { return [...targets.values()]; } @@ -113,7 +133,7 @@ export function resolvePluginWatchTargets( ].filter((value): value is string => typeof value === "string" && value.length > 0); if (entrypointPaths.length === 0) { - addWatchTarget(path.join(absPath, "dist"), true); + addRuntimeFilesFromDir(path.join(absPath, "dist")); return [...targets.values()]; } @@ -123,13 +143,13 @@ export function resolvePluginWatchTargets( const stat = statFile(resolvedEntrypoint); if (stat.isDirectory()) { - addWatchTarget(resolvedEntrypoint, true); + addRuntimeFilesFromDir(resolvedEntrypoint); } else { - addWatchTarget(path.dirname(resolvedEntrypoint), true); + addWatchTarget(resolvedEntrypoint, false, "file"); } } - return [...targets.values()]; + return [...targets.values()].sort((a, b) => a.path.localeCompare(b.path)); } /** @@ -141,10 +161,9 @@ export function createPluginDevWatcher( resolvePluginPackagePath?: ResolvePluginPackagePath, fsDeps?: PluginDevWatcherFsDeps, ): PluginDevWatcher { - const watchers = new Map(); + const watchers = new Map(); const debounceTimers = new Map>(); const fileExists = fsDeps?.existsSync ?? existsSync; - const watchFs = fsDeps?.watch ?? watch; function watchPlugin(pluginId: string, packagePath: string): void { // Don't double-watch @@ -169,60 +188,70 @@ export function createPluginDevWatcher( return; } - const activeWatchers = watcherTargets.map((target) => { - const watcher = watchFs(target.path, { recursive: target.recursive }, (_event, filename) => { - if (shouldIgnorePath(filename)) return; + const watcher = chokidar.watch( + watcherTargets.map((target) => target.path), + { + ignoreInitial: true, + awaitWriteFinish: { + stabilityThreshold: 200, + pollInterval: 100, + }, + ignored: (watchedPath) => { + const relativePath = path.relative(absPath, watchedPath); + return shouldIgnorePath(relativePath); + }, + }, + ); - // Debounce: multiple rapid file changes collapse into one restart - const existing = debounceTimers.get(pluginId); - if (existing) clearTimeout(existing); + watcher.on("all", (_eventName, changedPath) => { + const relativePath = path.relative(absPath, changedPath); + if (shouldIgnorePath(relativePath)) return; - debounceTimers.set( - pluginId, - setTimeout(() => { - debounceTimers.delete(pluginId); - log.info( - { pluginId, changedFile: filename, watchTarget: target.path }, - "plugin-dev-watcher: file change detected, restarting worker", + const existing = debounceTimers.get(pluginId); + if (existing) clearTimeout(existing); + + debounceTimers.set( + pluginId, + setTimeout(() => { + debounceTimers.delete(pluginId); + log.info( + { pluginId, changedFile: relativePath || path.basename(changedPath) }, + "plugin-dev-watcher: file change detected, restarting worker", + ); + + lifecycle.restartWorker(pluginId).catch((err) => { + log.warn( + { + pluginId, + err: err instanceof Error ? err.message : String(err), + }, + "plugin-dev-watcher: failed to restart worker after file change", ); - - lifecycle.restartWorker(pluginId).catch((err) => { - log.warn( - { - pluginId, - err: err instanceof Error ? err.message : String(err), - }, - "plugin-dev-watcher: failed to restart worker after file change", - ); - }); - }, DEBOUNCE_MS), - ); - }); - - watcher.on("error", (err) => { - log.warn( - { - pluginId, - packagePath: absPath, - watchTarget: target.path, - err: err instanceof Error ? err.message : String(err), - }, - "plugin-dev-watcher: watcher error, stopping watch for this plugin", - ); - unwatchPlugin(pluginId); - }); - - return watcher; + }); + }, DEBOUNCE_MS), + ); }); - watchers.set(pluginId, activeWatchers); + watcher.on("error", (err) => { + log.warn( + { + pluginId, + packagePath: absPath, + err: err instanceof Error ? err.message : String(err), + }, + "plugin-dev-watcher: watcher error, stopping watch for this plugin", + ); + unwatchPlugin(pluginId); + }); + + watchers.set(pluginId, watcher); log.info( { pluginId, packagePath: absPath, watchTargets: watcherTargets.map((target) => ({ path: target.path, - recursive: target.recursive, + kind: target.kind, })), }, "plugin-dev-watcher: watching local plugin for changes", @@ -240,11 +269,9 @@ export function createPluginDevWatcher( } function unwatchPlugin(pluginId: string): void { - const pluginWatchers = watchers.get(pluginId); - if (pluginWatchers) { - for (const watcher of pluginWatchers) { - watcher.close(); - } + const pluginWatcher = watchers.get(pluginId); + if (pluginWatcher) { + void pluginWatcher.close(); watchers.delete(pluginId); } const timer = debounceTimers.get(pluginId);