@xano/sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +788 -0
- package/LICENSE +21 -0
- package/README.md +541 -0
- package/dist/.build-fingerprint +1 -0
- package/dist/agent-file-refresh-ZEMK33AX.js +38 -0
- package/dist/answer-shape-K67TAAPY.js +33 -0
- package/dist/backend-dir-2U53CIAL.js +39 -0
- package/dist/backend-slot-7QXW2QYC.js +28 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +255 -0
- package/dist/branch-NCRNWQBQ.js +61 -0
- package/dist/branch-commands-ACIRCEQJ.js +79 -0
- package/dist/bundle.d.ts +223 -0
- package/dist/bundle.js +153 -0
- package/dist/capture-WAXPD7DR.js +41 -0
- package/dist/chunk-27GWNP2I.js +23 -0
- package/dist/chunk-2CMJZDCL.js +177 -0
- package/dist/chunk-2EZ3ZJZZ.js +302 -0
- package/dist/chunk-2GNFDZA2.js +390 -0
- package/dist/chunk-2MNLRIOX.js +11 -0
- package/dist/chunk-2TFRR6JV.js +104 -0
- package/dist/chunk-2ZUEE4EK.js +408 -0
- package/dist/chunk-36JUQZVQ.js +22 -0
- package/dist/chunk-3DRXWLJ5.js +83 -0
- package/dist/chunk-3JDU4VRL.js +137 -0
- package/dist/chunk-3KZCLN4Z.js +241 -0
- package/dist/chunk-3LDULPRE.js +168 -0
- package/dist/chunk-3M4LFYNQ.js +111 -0
- package/dist/chunk-3XI5CTUE.js +22 -0
- package/dist/chunk-3YW23UFH.js +50 -0
- package/dist/chunk-45UIT5T2.js +862 -0
- package/dist/chunk-4BW2SG2N.js +46 -0
- package/dist/chunk-4KHEMC32.js +79 -0
- package/dist/chunk-4POWHT2K.js +321 -0
- package/dist/chunk-4Q2GN7NW.js +192 -0
- package/dist/chunk-5A2PLKZB.js +70 -0
- package/dist/chunk-5FBYXK5P.js +159 -0
- package/dist/chunk-5HVXPTU4.js +323 -0
- package/dist/chunk-5QU2P5CA.js +304 -0
- package/dist/chunk-5V5XU32D.js +191 -0
- package/dist/chunk-5ZSO4T2G.js +10 -0
- package/dist/chunk-622HUCZT.js +10 -0
- package/dist/chunk-65HWOMGV.js +177 -0
- package/dist/chunk-6ELFRN3P.js +365 -0
- package/dist/chunk-6HO3NCSJ.js +1055 -0
- package/dist/chunk-6IUMKYWI.js +457 -0
- package/dist/chunk-6KEB3C3U.js +65 -0
- package/dist/chunk-6MR5TI5V.js +987 -0
- package/dist/chunk-6NNTNXN7.js +157 -0
- package/dist/chunk-6PBR37LN.js +97 -0
- package/dist/chunk-6QXLMMT3.js +155 -0
- package/dist/chunk-6ZGJCBZS.js +224 -0
- package/dist/chunk-73IVBKMA.js +250 -0
- package/dist/chunk-7G3CTU7I.js +286 -0
- package/dist/chunk-A3UDOZB6.js +222 -0
- package/dist/chunk-A6E77GDW.js +85 -0
- package/dist/chunk-AHJWCYQO.js +660 -0
- package/dist/chunk-AKNCFVKZ.js +36 -0
- package/dist/chunk-AM4FKL4J.js +237 -0
- package/dist/chunk-AN6BL47T.js +26 -0
- package/dist/chunk-AOSYVMUS.js +147 -0
- package/dist/chunk-APWXY5SL.js +182 -0
- package/dist/chunk-AWEIIR2U.js +218 -0
- package/dist/chunk-AWQ7BQTQ.js +73 -0
- package/dist/chunk-BAUZOWGX.js +393 -0
- package/dist/chunk-BHACRS6R.js +940 -0
- package/dist/chunk-BWPNT55T.js +680 -0
- package/dist/chunk-C6RIMRU4.js +187 -0
- package/dist/chunk-CGVQZXYG.js +263 -0
- package/dist/chunk-CIMQZRRE.js +162 -0
- package/dist/chunk-CJHQKJU4.js +113 -0
- package/dist/chunk-CTMO46PK.js +1897 -0
- package/dist/chunk-D53J55C5.js +106 -0
- package/dist/chunk-D7VGG3WS.js +90 -0
- package/dist/chunk-DAD3ONIS.js +245 -0
- package/dist/chunk-DGWHAVRG.js +162 -0
- package/dist/chunk-DX4QR4B2.js +483 -0
- package/dist/chunk-EHP3WPEG.js +21 -0
- package/dist/chunk-ELP2HMXC.js +432 -0
- package/dist/chunk-ERVIASGL.js +21 -0
- package/dist/chunk-ET4Y4XCB.js +865 -0
- package/dist/chunk-ETO6LGLD.js +4750 -0
- package/dist/chunk-EUUUMY7J.js +24 -0
- package/dist/chunk-FQJ2TTHP.js +218 -0
- package/dist/chunk-FQOF7E2E.js +133 -0
- package/dist/chunk-FWD7AL6N.js +21 -0
- package/dist/chunk-FXFV3KU6.js +138 -0
- package/dist/chunk-FXRQOWTE.js +171 -0
- package/dist/chunk-G554NZNI.js +210 -0
- package/dist/chunk-GSGDWJB5.js +13 -0
- package/dist/chunk-H2V3N23D.js +66 -0
- package/dist/chunk-H5C7MBCR.js +159 -0
- package/dist/chunk-HHN734BU.js +371 -0
- package/dist/chunk-HRVBLCVJ.js +243 -0
- package/dist/chunk-HVRUQXGD.js +1264 -0
- package/dist/chunk-I4F5TCJH.js +130 -0
- package/dist/chunk-IDOTCMDV.js +57 -0
- package/dist/chunk-IGOLHKWF.js +427 -0
- package/dist/chunk-JGE7CAWB.js +148 -0
- package/dist/chunk-JTUKLCXS.js +92 -0
- package/dist/chunk-JZGVVJSI.js +66 -0
- package/dist/chunk-K4WX6UUU.js +923 -0
- package/dist/chunk-K5BBGTYZ.js +217 -0
- package/dist/chunk-K6RXBGUB.js +427 -0
- package/dist/chunk-KTSDSD36.js +165 -0
- package/dist/chunk-L5JYN3NM.js +1038 -0
- package/dist/chunk-LIEOKXKD.js +354 -0
- package/dist/chunk-LLNZGEFK.js +51 -0
- package/dist/chunk-LPECSJ2J.js +55 -0
- package/dist/chunk-LQCMKAJ7.js +413 -0
- package/dist/chunk-LRN4ZJCQ.js +1145 -0
- package/dist/chunk-MJ4ZVHKN.js +17 -0
- package/dist/chunk-MXHSQOC4.js +3061 -0
- package/dist/chunk-N5GKCWE5.js +367 -0
- package/dist/chunk-NA3PXMEF.js +10 -0
- package/dist/chunk-NAQDMEHV.js +76 -0
- package/dist/chunk-NWKXM5RD.js +109 -0
- package/dist/chunk-NZOWWHU5.js +196 -0
- package/dist/chunk-OHX6MIUZ.js +184 -0
- package/dist/chunk-OOXWYAA4.js +15 -0
- package/dist/chunk-OPHB5ORL.js +1990 -0
- package/dist/chunk-OXA6PSZT.js +1725 -0
- package/dist/chunk-P2HAGF6H.js +73 -0
- package/dist/chunk-PTHVMXZI.js +3036 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-Q325JKYB.js +189 -0
- package/dist/chunk-Q7DGTQ5G.js +255 -0
- package/dist/chunk-QCNYHBL7.js +601 -0
- package/dist/chunk-QO6BVGS5.js +145 -0
- package/dist/chunk-QXTKSOFI.js +76 -0
- package/dist/chunk-ROY4YAEQ.js +66 -0
- package/dist/chunk-RTJRZFFY.js +447 -0
- package/dist/chunk-SAVJUS2X.js +128 -0
- package/dist/chunk-SFLHVCKX.js +173 -0
- package/dist/chunk-STSFNCY4.js +127 -0
- package/dist/chunk-SWBF2LTK.js +170 -0
- package/dist/chunk-T5KELPAR.js +109 -0
- package/dist/chunk-TFFGOFVZ.js +1481 -0
- package/dist/chunk-TLHIFK4M.js +256 -0
- package/dist/chunk-TLQE7ZN3.js +156 -0
- package/dist/chunk-TPHWJ7UG.js +1981 -0
- package/dist/chunk-TRJCALW3.js +291 -0
- package/dist/chunk-TTEIYD5P.js +133 -0
- package/dist/chunk-U64O2MXY.js +407 -0
- package/dist/chunk-UCQ2VUTG.js +630 -0
- package/dist/chunk-UTI5G6OJ.js +44 -0
- package/dist/chunk-UVHDMMIP.js +188 -0
- package/dist/chunk-UZ6QQWJT.js +120 -0
- package/dist/chunk-VAX3UZOY.js +686 -0
- package/dist/chunk-W3HBUKFO.js +57 -0
- package/dist/chunk-WKQLGKCL.js +2823 -0
- package/dist/chunk-WQFNASK4.js +560 -0
- package/dist/chunk-WRBKLYSJ.js +224 -0
- package/dist/chunk-WTPZRR4I.js +17 -0
- package/dist/chunk-WZDJ2LLX.js +2385 -0
- package/dist/chunk-X2NL4KUO.js +14 -0
- package/dist/chunk-X4DVXBFY.js +38 -0
- package/dist/chunk-X677DMZM.js +7050 -0
- package/dist/chunk-XBKMWY7F.js +612 -0
- package/dist/chunk-XNCP4KYO.js +78 -0
- package/dist/chunk-XVBGLISP.js +35 -0
- package/dist/chunk-XW6KISBB.js +1192 -0
- package/dist/chunk-YGZHHAAB.js +8156 -0
- package/dist/chunk-YHEFQUQB.js +186 -0
- package/dist/chunk-YLDALTWZ.js +653 -0
- package/dist/chunk-YU4HSQW7.js +229 -0
- package/dist/chunk-ZDPP6RKB.js +27 -0
- package/dist/chunk-ZFDZKNGG.js +194 -0
- package/dist/chunk-ZIY7RNUT.js +49 -0
- package/dist/chunk-ZYJP6BOO.js +30 -0
- package/dist/cli.d.ts +1111 -0
- package/dist/cli.js +165 -0
- package/dist/codegen-command-VR2GWR6C.js +173 -0
- package/dist/codegen.d.ts +115 -0
- package/dist/codegen.js +19 -0
- package/dist/commands-GJMJ4XYZ.js +76 -0
- package/dist/completion-ZQLO6PB6.js +738 -0
- package/dist/config-MBYGCAFK.js +136 -0
- package/dist/deploy-command-JVZPWHMK.js +180 -0
- package/dist/deploy-source-3W45CXMP.js +81 -0
- package/dist/dev-env-sync-HN44K4D5.js +188 -0
- package/dist/diagnostics-UEAAHWVF.js +30 -0
- package/dist/display-name-hint-ABBPVQIR.js +46 -0
- package/dist/emit-CyhBDxli.d.ts +7007 -0
- package/dist/env-example-refresh-ET5HMMDW.js +126 -0
- package/dist/env-pull-command-TNMNB3CV.js +323 -0
- package/dist/env-target-O4HRKTGZ.js +40 -0
- package/dist/env-var-command-ZIRDCAZA.js +427 -0
- package/dist/ephemeral-P7F3PMAD.js +54 -0
- package/dist/ephemeral-command-CLJJKOFT.js +592 -0
- package/dist/ephemeral-state-PYCBSOGX.js +61 -0
- package/dist/file-lock-PT74GWYW.js +10 -0
- package/dist/generate-command-AYEOLA26.js +510 -0
- package/dist/gitignore-3SJ3WNRG.js +51 -0
- package/dist/guards-4EBNCZDC.js +162 -0
- package/dist/hosted-file-PSHJB3HX.js +28 -0
- package/dist/impersonate-command-5JAS3WSV.js +152 -0
- package/dist/import-EQSFU6JC.js +19 -0
- package/dist/index.d.ts +5470 -0
- package/dist/index.js +371 -0
- package/dist/init-command-X2FR4UAK.js +125 -0
- package/dist/init-modules-RY7UHAOL.js +50 -0
- package/dist/init-web-M4YZSZK6.js +42 -0
- package/dist/internal.d.ts +179 -0
- package/dist/internal.js +2634 -0
- package/dist/io-RC56QSN5.js +26 -0
- package/dist/keep-data-merge-7MPLVAVI.js +1119 -0
- package/dist/lambda-config-reconcile-OUZ4OYAB.js +116 -0
- package/dist/landed-753YLJ5H.js +40 -0
- package/dist/landing-record-SU52SKXU.js +61 -0
- package/dist/live-diff-YONW2VI3.js +65 -0
- package/dist/local-engine-choice-CH5X3W7E.js +79 -0
- package/dist/local-engine-command-Z3OL7VOD.js +766 -0
- package/dist/local-engine-config-5U3SC434.js +39 -0
- package/dist/local-engine-deploy-PCQ4SNPB.js +39 -0
- package/dist/local-engine-process-EHTE5LLU.js +63 -0
- package/dist/local-engine-state-A4FTRFAF.js +32 -0
- package/dist/local-engine-update-check-6L645VBL.js +162 -0
- package/dist/lock-MRRHW5GS.js +87 -0
- package/dist/lock-commands-2QG25HL7.js +963 -0
- package/dist/login-command-QPYRWOXZ.js +538 -0
- package/dist/logout-command-WOIQLDP6.js +56 -0
- package/dist/loop-ALIZLOSX.js +191 -0
- package/dist/marketplace-command-K5YL42NO.js +977 -0
- package/dist/marketplace-resolve-LY32JES5.js +40 -0
- package/dist/meta-client-YXGPW53C.js +244 -0
- package/dist/module-manifest-JFGHU2TC.js +38 -0
- package/dist/near-key.d.ts +13 -0
- package/dist/near-key.js +8 -0
- package/dist/node.d.ts +95 -0
- package/dist/node.js +969 -0
- package/dist/operation-outcome-TZSESNC7.js +51 -0
- package/dist/output-target-AL3I44GG.js +47 -0
- package/dist/package-manager-PRAHUSCN.js +34 -0
- package/dist/plan-disclosure-6G4MLZKD.js +114 -0
- package/dist/plugin-contributions-WRNY5ABG.js +28 -0
- package/dist/plugin-prompt-4VA3B4FF.js +20 -0
- package/dist/plugin-questions-JTSYA74Q.js +24 -0
- package/dist/plugin.d.ts +396 -0
- package/dist/plugin.js +1 -0
- package/dist/preflight-command-2T5Z36MI.js +321 -0
- package/dist/profile-command-AH3PIM4X.js +800 -0
- package/dist/profile-pointer-GRWJU3YD.js +31 -0
- package/dist/profile-select-74TRDEPD.js +23 -0
- package/dist/project-dependency-KSH63ODT.js +35 -0
- package/dist/project-sdk-REPPOIBD.js +24 -0
- package/dist/promote-command-PCWT5N6S.js +140 -0
- package/dist/prompt-II3F3LND.js +27 -0
- package/dist/publish-command-TTDUUJI5.js +414 -0
- package/dist/pull-command-VBO7C4S3.js +161 -0
- package/dist/pull-superseded-GJWLG7XO.js +20 -0
- package/dist/release-command-N4ZPKMHR.js +146 -0
- package/dist/release-ns-command-3KFDMZ6G.js +1745 -0
- package/dist/response-HlXahv4K.d.ts +925 -0
- package/dist/retry-command-AS5EVREX.js +34 -0
- package/dist/routes-manifest-SNWUVBT6.js +43 -0
- package/dist/runtime-XETHVF33.js +14 -0
- package/dist/scaffold.d.ts +1911 -0
- package/dist/scaffold.js +107 -0
- package/dist/secrets-file-LVZD6IIM.js +56 -0
- package/dist/secrets-fill-command-TTCPSVXN.js +218 -0
- package/dist/seed-VU7C3QYS.js +46 -0
- package/dist/sent-writes-N7X4OVXT.js +22 -0
- package/dist/source-resolve-S2XA3XZF.js +83 -0
- package/dist/static-host-B2Y6AG3W.js +352 -0
- package/dist/status-command-RUDXTF7I.js +554 -0
- package/dist/store-6VNCD4D7.js +20 -0
- package/dist/store-ARM6VMO5.js +98 -0
- package/dist/store-DuYfzKlW.d.ts +3144 -0
- package/dist/suggest-6FOOYKHG.js +12 -0
- package/dist/sveltekit.d.ts +154 -0
- package/dist/sveltekit.js +109 -0
- package/dist/sync-contributions-UCEROO26.js +415 -0
- package/dist/table-ENAVMITR.js +42 -0
- package/dist/tables-command-GUPXXSB5.js +156 -0
- package/dist/tenant-IIHPOHGP.js +42 -0
- package/dist/tenant-command-D5HGI2X4.js +965 -0
- package/dist/test-command-TIKZMDPZ.js +476 -0
- package/dist/tests-BGGIOK3L.js +26 -0
- package/dist/token-NICJUTMT.js +72 -0
- package/dist/toolchain-hooks-GHKLUJOH.js +96 -0
- package/dist/toolchain-modules-5EAZUONB.js +24 -0
- package/dist/tracked-backend-EIBUMDZA.js +80 -0
- package/dist/update-check-2W2NLDTC.js +136 -0
- package/dist/upgrade-command-QHNFWM7O.js +531 -0
- package/dist/verify-rollout-DRCK5TDD.js +61 -0
- package/dist/whoami-command-STGINTXH.js +51 -0
- package/dist/workspace-2CLHU2LK.js +93 -0
- package/dist/workspace-binding-56PIIPCP.js +54 -0
- package/dist/workspace-command-JEXQETJD.js +535 -0
- package/dist/workspace-env-F2RFMSC5.js +70 -0
- package/dist/workspace-export-OJSH2HIW.js +21 -0
- package/dist/writable-VA7OISSK.js +25 -0
- package/dist/xano-ICFUSPFR.js +42 -0
- package/dist/xanosdk-import-GN42TO2B.js +56 -0
- package/dist/xanosdk-project-S6DXJONQ.js +41 -0
- package/dist/xdo-CxDr2Q1C.d.ts +1371 -0
- package/guides/README.md +33 -0
- package/guides/authoring.md +921 -0
- package/guides/cli.md +401 -0
- package/guides/codegen.md +177 -0
- package/guides/codes.md +404 -0
- package/guides/coverage.md +74 -0
- package/guides/deploying.md +1525 -0
- package/guides/environment.md +400 -0
- package/guides/marketplace.md +415 -0
- package/guides/module-contract.md +558 -0
- package/guides/object-kinds.md +433 -0
- package/guides/project-structure.md +43 -0
- package/guides/scaffold.md +351 -0
- package/guides/typed-frontend.md +242 -0
- package/llms/client.md +63 -0
- package/llms/errors.md +56 -0
- package/llms/fields.md +176 -0
- package/llms/filters.md +183 -0
- package/llms/kinds-agent-mcp.md +30 -0
- package/llms/kinds-core.md +100 -0
- package/llms/kinds-knowledge.md +20 -0
- package/llms/kinds-realtime.md +76 -0
- package/llms/lambda.md +94 -0
- package/llms/legacy.md +44 -0
- package/llms/lock.md +40 -0
- package/llms/object-kinds.md +34 -0
- package/llms/statements-calls.md +62 -0
- package/llms/statements-catalog.md +320 -0
- package/llms/statements-data.md +66 -0
- package/llms/statements-runtime.md +68 -0
- package/llms/tests.md +59 -0
- package/llms/toolchain.md +16 -0
- package/llms/triggers.md +36 -0
- package/llms/values.md +29 -0
- package/llms-full.txt +2085 -0
- package/llms.txt +493 -0
- package/manifest.json +11224 -0
- package/package.json +162 -0
- package/types/lambda-globals.d.ts +129 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
# The module contract
|
|
2
|
+
|
|
3
|
+
How to build a Xano SDK add-on: the manifest fields both kinds of module declare, the `@xano/sdk/plugin` types a toolchain module compiles against, every hook and when it fires, and the shapes the SDK refuses rather than absorbs.
|
|
4
|
+
|
|
5
|
+
An add-on is an ordinary npm package. It becomes a Xano SDK module by describing itself in a
|
|
6
|
+
`"xanosdk"` field in its own `package.json`, which is the only description of a package that
|
|
7
|
+
ships with the package. Everything below is that field plus, for a toolchain module, the
|
|
8
|
+
default export of one file.
|
|
9
|
+
|
|
10
|
+
Read [The marketplace](marketplace.md) first for the user-facing side: what `install`,
|
|
11
|
+
`reinstall` and `remove` do to a project, and what your module's settings look like once they
|
|
12
|
+
land in it.
|
|
13
|
+
|
|
14
|
+
## Which kind are you writing?
|
|
15
|
+
|
|
16
|
+
| | Workspace module | Toolchain module |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| Extends | the deployed workspace | the CLI |
|
|
19
|
+
| Declares | `"register"`, `"returns"`, `"options"` | `"kind": "toolchain"`, `"plugin"` |
|
|
20
|
+
| Ships | tables, endpoints, functions, agents | questions, file contributions, hooks |
|
|
21
|
+
| Wired by | an import and a call in `xano/index.ts` | nothing: it is configured, not registered |
|
|
22
|
+
| Examples | `@xano-sdk/auth`, `@xano-sdk/vector` | `@xano-sdk/xanoscript` |
|
|
23
|
+
|
|
24
|
+
The `kind` field is what routes a package to one loader or the other. It defaults to
|
|
25
|
+
`workspace`, because every module published before the toolchain kind existed declares none
|
|
26
|
+
and defaulting the other way would strand them. A `kind` the SDK does not recognize is never
|
|
27
|
+
defaulted to one kind or the other: the failure a default would produce is a module reported
|
|
28
|
+
as unwired when its only fault is a typo in one string.
|
|
29
|
+
|
|
30
|
+
What that costs you depends on where the typo is read. `xanosdk init` refuses by name, with
|
|
31
|
+
the valid kinds listed, because a scaffold that silently drops your module is worse than one
|
|
32
|
+
that stops. Every other path, the marketplace verbs and the CLI's own module loading
|
|
33
|
+
included, warns and reads the package as an ordinary dependency rather than taking a whole
|
|
34
|
+
command down over one unrecognized string. Either way it is reported: nothing guesses which
|
|
35
|
+
loader you meant.
|
|
36
|
+
|
|
37
|
+
## Workspace modules
|
|
38
|
+
|
|
39
|
+
A workspace module exports a `register*` function that mutates a workspace and is called from
|
|
40
|
+
the user's `xano/index.ts`. It says how in its manifest:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"xanosdk": {
|
|
45
|
+
"register": "registerAuth",
|
|
46
|
+
"returns": "workspace",
|
|
47
|
+
"options": { "canonical": { "value": "authn" } }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
| Field | Meaning |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `register` | The exported function to call. Required for the declarative path. |
|
|
55
|
+
| `returns` | `"workspace"` or `"handle"`. Decides whether the call's result is bound to a name. |
|
|
56
|
+
| `options` | What the call's second argument needs, by option name. |
|
|
57
|
+
|
|
58
|
+
`options` entries take one of two forms. A **literal** is written into the generated file as
|
|
59
|
+
JSON, so nothing is imported for it:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{ "canonical": { "value": "authn" } }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
An **import** names another package's export, for a module that cannot be registered bare:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{ "authTable": { "package": "@xano-sdk/auth", "export": "userTable" } }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Declaring `options` is all or nothing
|
|
72
|
+
|
|
73
|
+
The field states what the **whole** argument is, and is taken at its word without your
|
|
74
|
+
register function being read. Declare `"options": {}` and the call is written bare.
|
|
75
|
+
|
|
76
|
+
**Omit the key and your module has said nothing either way**, so the SDK imports your entry
|
|
77
|
+
and measures `register`'s arity. A function that takes a second parameter nobody declared is
|
|
78
|
+
left **out** of the generated `xano/index.ts` rather than called without it, with a commented
|
|
79
|
+
call and a pointer to `xanosdk marketplace details <pkg> --prompt`. The call would not compile,
|
|
80
|
+
and a fresh scaffold that passes `npm run typecheck` is worth more than one that has your
|
|
81
|
+
module in it.
|
|
82
|
+
|
|
83
|
+
So: a module that needs an option only the user can answer (a URL into their own frontend,
|
|
84
|
+
their own role model) omits the key rather than declaring the rest. That is what lets the CLI
|
|
85
|
+
find the argument and leave the module out honestly.
|
|
86
|
+
|
|
87
|
+
Arity is a signal rather than a proof, because the published artifact is not the source.
|
|
88
|
+
`options?: Opts` and a default parameter downleveled to ES5 both report 2, and
|
|
89
|
+
`(xano, ...rest)` reports 1. Declaring the field is what settles it; nothing measured from
|
|
90
|
+
outside your module can.
|
|
91
|
+
|
|
92
|
+
### Registration must be a mutation
|
|
93
|
+
|
|
94
|
+
The generated file writes `registerX(app);` as a statement and default-exports `app`, in any
|
|
95
|
+
order. That is only correct because registration **mutates** the workspace it is handed.
|
|
96
|
+
Write your register function so it registers onto the instance it receives; `returns` only
|
|
97
|
+
decides whether the result is worth binding to a name, and is never load-bearing for
|
|
98
|
+
correctness.
|
|
99
|
+
|
|
100
|
+
### If you declare nothing
|
|
101
|
+
|
|
102
|
+
A module with no `"xanosdk"` field still works. Its installed entry is imported and searched
|
|
103
|
+
for a single export matching `/^register[A-Z]/`. Real exports are read, not prose, so nothing
|
|
104
|
+
can mistake a comment for code. Two matching exports is a refusal rather than a guess, and
|
|
105
|
+
the refusal says the manifest field is what settles it.
|
|
106
|
+
|
|
107
|
+
### Point somewhere useful
|
|
108
|
+
|
|
109
|
+
Declare a `homepage` in your `package.json`. It is where a refusal sends the reader when the
|
|
110
|
+
CLI cannot wire your module, and the installed manifest is the one place that says where
|
|
111
|
+
without the CLI knowing your package.
|
|
112
|
+
|
|
113
|
+
## Toolchain modules
|
|
114
|
+
|
|
115
|
+
A toolchain module declares its kind and the file whose default export is the plugin:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"name": "@acme/reviewable",
|
|
120
|
+
"version": "1.0.0",
|
|
121
|
+
"xanosdk": { "kind": "toolchain", "plugin": "./dist/plugin.js" },
|
|
122
|
+
"peerDependencies": { "@xano/sdk": ">=1.0.0 <2.0.0" }
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`@xano/sdk` is a **peer**, never a dependency. Your hooks are handed real compiled data, so
|
|
127
|
+
your module is only correct against the SDK versions whose shapes it understands. Write that
|
|
128
|
+
range out with a floor and a ceiling, as above: the ceiling is the next major, so a module
|
|
129
|
+
built for 1.x loads on every 1.x SDK and is refused on 2.0.
|
|
130
|
+
|
|
131
|
+
That range is **enforced at load**, on every command that discovers modules. An SDK outside it
|
|
132
|
+
is refused by name before your plugin file is imported, naming the range you asked for and the
|
|
133
|
+
version it got. So the floor is the version whose shapes your hooks understand, not an
|
|
134
|
+
aspiration — declare it accurately and the SDK will never quietly run your module against a
|
|
135
|
+
contract it was not built for. Ranges are read as `>=` / `>` / `<=` / `<` / `=` comparators,
|
|
136
|
+
`^`, `~`, exact pins and `||` unions of those; a shape outside that subset is not refused, it
|
|
137
|
+
is simply not checked.
|
|
138
|
+
|
|
139
|
+
### The import erases
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import type { ToolchainPlugin, BundleContext } from "@xano/sdk/plugin";
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`@xano/sdk/plugin` is a **types-only** entry. Nothing in it has a runtime representation, so
|
|
146
|
+
the import erases at compile time and your published bundle neither carries the SDK nor
|
|
147
|
+
resolves it at runtime. That matters because the SDK is the thing importing your plugin: a
|
|
148
|
+
plugin that pulled the SDK back in would load a second copy of the compiler into the process
|
|
149
|
+
already running one.
|
|
150
|
+
|
|
151
|
+
A plugin that wants the precise per-kind payload shapes can depend on `@xano/sdk/internal`
|
|
152
|
+
for them.
|
|
153
|
+
|
|
154
|
+
### The plugin
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import type { ToolchainPlugin } from "@xano/sdk/plugin";
|
|
158
|
+
|
|
159
|
+
const plugin: ToolchainPlugin = {
|
|
160
|
+
kind: "toolchain",
|
|
161
|
+
questions: [...],
|
|
162
|
+
contributes: (answers) => ({ ... }),
|
|
163
|
+
answersFromConfig: (config) => ({ ... }),
|
|
164
|
+
onBundle: async (ctx) => ({ ... }),
|
|
165
|
+
onPreflight: async (ctx) => ({ ... }),
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export default plugin;
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`kind: "toolchain"` is repeated from the manifest so the two must agree. A plugin whose
|
|
172
|
+
default export does not say it is refused: the mismatch means one of the two is stale, and
|
|
173
|
+
guessing which would run code the manifest did not describe.
|
|
174
|
+
|
|
175
|
+
Every hook is optional. A module that only adds a `.gitattributes` line declares
|
|
176
|
+
`contributes` alone.
|
|
177
|
+
|
|
178
|
+
Five hooks, and deliberately no more. This contract was designed from one example, and a
|
|
179
|
+
contract designed from one example generalizes badly, so it covers exactly what that first
|
|
180
|
+
module needed and stops. The second toolchain module is the one that should widen it, with a
|
|
181
|
+
real second set of requirements in hand.
|
|
182
|
+
|
|
183
|
+
## Questions
|
|
184
|
+
|
|
185
|
+
A module declares questions as **data**, never as a prompt callback:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
questions: [
|
|
189
|
+
{
|
|
190
|
+
id: "xanoscript",
|
|
191
|
+
label: "Render the compiled workspace to a committed tree",
|
|
192
|
+
type: "boolean",
|
|
193
|
+
default: true,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: "dir",
|
|
197
|
+
label: "Directory to render into",
|
|
198
|
+
type: "string",
|
|
199
|
+
default: "xanoscript",
|
|
200
|
+
flag: "xanoscript-dir",
|
|
201
|
+
when: (answers) => answers.xanoscript === true,
|
|
202
|
+
},
|
|
203
|
+
]
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
| Field | Meaning |
|
|
207
|
+
|---|---|
|
|
208
|
+
| `id` | Stable key the answer is stored under. |
|
|
209
|
+
| `label` | One line shown to a human. No trailing punctuation. |
|
|
210
|
+
| `type` | `"boolean"`, `"string"` or `"choice"`. |
|
|
211
|
+
| `choices` | The allowed values, for `type: "choice"`. |
|
|
212
|
+
| `default` | Used when the question is not asked and nothing is stored. |
|
|
213
|
+
| `flag` | The long flag that answers it, without dashes. Derived from `id` when omitted. |
|
|
214
|
+
| `when` | Whether to ask at all, given the answers so far. |
|
|
215
|
+
|
|
216
|
+
Two reasons the callback form does not exist, and both are hard. A callback would let a
|
|
217
|
+
module hang a command on a TTY read in CI, where there is no one to answer it, and the SDK
|
|
218
|
+
could not even impose a timeout because it cannot tell a slow prompt from a slow computation.
|
|
219
|
+
And `xanosdk init --web` renders the questionnaire as a browser form, which can display a list
|
|
220
|
+
of choices but cannot run your callback at all. Declaring the question as data means the SDK
|
|
221
|
+
owns every way of answering it.
|
|
222
|
+
|
|
223
|
+
### Precedence
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
flag > the project's stored answer > your declared default
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The middle rank is what makes a re-ask safe. A stored answer is both the prompt's default and
|
|
230
|
+
the non-interactive fallback, so re-running the questionnaire never quietly resets a
|
|
231
|
+
directory the user chose back to what you shipped. The machine output records which rank each
|
|
232
|
+
answer came from: `flag`, `prompt`, `config`, or `default`.
|
|
233
|
+
|
|
234
|
+
### The derived flag
|
|
235
|
+
|
|
236
|
+
Each question gets a long flag, from `flag` when you name one and from `id` otherwise.
|
|
237
|
+
Booleans get a `--no-` counterpart, because a question whose default is `true` is otherwise
|
|
238
|
+
unanswerable without a TTY. A `string` or `choice` question is answered with `--flag=value`
|
|
239
|
+
and never `--flag value`: your module is not loaded when the arguments are parsed, so the CLI
|
|
240
|
+
cannot know your flag takes a value and never consumes the following token.
|
|
241
|
+
|
|
242
|
+
**A collision is refused, not absorbed.** A flag matching one the CLI already owns
|
|
243
|
+
(`--name`, `--force`, `--framework`) would never reach you: the parser binds the real flag
|
|
244
|
+
first, so the SDK acts on a value meant for your module and your module quietly records its
|
|
245
|
+
default. Two wrong values, no message. Two modules deriving the same flag fail the same way
|
|
246
|
+
between them. Both are refused by name, naming your module and the command that hit it.
|
|
247
|
+
|
|
248
|
+
Declare an explicit `flag` when the derived name would read badly across several modules
|
|
249
|
+
(`--reviewable-dir` rather than `--dir`) or would collide.
|
|
250
|
+
|
|
251
|
+
### `when`, and what happens to a stale answer
|
|
252
|
+
|
|
253
|
+
A question `when` excludes is not asked **and no answer is recorded for it**. An unasked
|
|
254
|
+
question has no answer, and recording one would make a later `deploy` act on a choice nobody
|
|
255
|
+
made. On a re-ask that also deletes a stale key a previous run stored for it, and the
|
|
256
|
+
deletion is reported:
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
! @acme/reviewable: `dir` is no longer asked, so its stored setting was dropped.
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
A stored value the question no longer accepts (a choice you retired in an upgrade) is
|
|
263
|
+
replaced by your default and reported, rather than offered back as the prompt default.
|
|
264
|
+
|
|
265
|
+
`when` is evaluated against the answers so far, so a question can depend on one asked before
|
|
266
|
+
it in your own list.
|
|
267
|
+
|
|
268
|
+
## `contributes`
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
contributes: (answers) => ({
|
|
272
|
+
gitattributes: answers.xanoscript ? [`${answers.dir}/** linguist-generated`] : [],
|
|
273
|
+
config: { enabled: answers.xanoscript === true, dir: answers.dir },
|
|
274
|
+
}),
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
`contributes` is a **pure function from answers to contributions**. It has no knowledge of
|
|
278
|
+
which command is running and no access to the filesystem, and it may be called several times
|
|
279
|
+
in one run and must return the same thing for the same answers.
|
|
280
|
+
|
|
281
|
+
That is what makes the reconcile idempotent. A module is configured whenever its project is
|
|
282
|
+
reconciled, and `xanosdk init` is only the first of those moments: installing into a project
|
|
283
|
+
that already exists, re-answering later, and removing all run the same pass. The correct
|
|
284
|
+
state is a function of the project's dependencies and its stored answers, so it can be
|
|
285
|
+
rebuilt from scratch at any moment, however your module arrived.
|
|
286
|
+
|
|
287
|
+
`contributes` is **synchronous**. An `async contributes` returns a Promise, which is a
|
|
288
|
+
refusal: a pending Promise has no `gitattributes` and no `config`, which would read as the
|
|
289
|
+
empty contribution that switches your module off.
|
|
290
|
+
|
|
291
|
+
### The two slots
|
|
292
|
+
|
|
293
|
+
| Slot | What it is |
|
|
294
|
+
|---|---|
|
|
295
|
+
| `gitattributes` | Lines added to the project's `.gitattributes`, in your own marked block. One line per entry, no trailing newline. |
|
|
296
|
+
| `config` | Your module's block in the project's `package.json` `"xanosdk"` object, stored under your package name. |
|
|
297
|
+
|
|
298
|
+
Contributions are **additive against named slots the SDK owns**. You return lines and a
|
|
299
|
+
config object; the SDK composes them into the files it maintains. You never hand back a whole
|
|
300
|
+
file, never rewrite a script string, and never touch the disk yourself. That constraint is
|
|
301
|
+
what makes two installed modules safe together, and it moves the failure earlier: a malformed
|
|
302
|
+
contribution is refused where it was written rather than producing a broken file the user
|
|
303
|
+
meets later.
|
|
304
|
+
|
|
305
|
+
Your lines land in a block naming your package and stamped with your version:
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
# xanosdk:begin @acme/reviewable
|
|
309
|
+
# xanosdk 1.0.0 — generated; edits inside this block are overwritten
|
|
310
|
+
rendered/** linguist-generated
|
|
311
|
+
# xanosdk:end @acme/reviewable
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**An empty contribution is meaningful.** Returning no `gitattributes` lines removes your
|
|
315
|
+
block rather than leaving an empty one, which is how a module whose answers turned it off
|
|
316
|
+
stops applying rules the user declined. Returning nothing at all is the SDK's signal that
|
|
317
|
+
your module reported itself switched off, and it is recorded as `{ "enabled": false }`.
|
|
318
|
+
|
|
319
|
+
### What you may not contribute
|
|
320
|
+
|
|
321
|
+
A `.gitattributes` rule is refused when it:
|
|
322
|
+
|
|
323
|
+
- **matches `*`.** That applies to every path in the repository, and a module able to set it
|
|
324
|
+
from an add-on install could renormalize line endings repo-wide on the next checkout.
|
|
325
|
+
- **sets `text` or `eol`, including a bare `-text`.** `.gitattributes` is last-match-wins, so
|
|
326
|
+
an appended `* -text` or `*.gen eol=crlf` silently defeats the `* text=auto eol=lf` rule the
|
|
327
|
+
scaffold sets so a byte comparison cannot fail over a contributor's `core.autocrlf`.
|
|
328
|
+
- **spells a xanosdk block marker.** Blocks are located by counting markers, so one forged
|
|
329
|
+
line leaves a file no run can balance, including another package's block.
|
|
330
|
+
|
|
331
|
+
Contribute display rules (`linguist-*`, `diff`) for the paths you own instead. Each check runs
|
|
332
|
+
**per physical line**, because the composer splits every entry on newlines before writing it:
|
|
333
|
+
`["docs/*.md linguist-documentation\n* binary"]` would otherwise pass and then land `* binary`
|
|
334
|
+
on every path in the repository.
|
|
335
|
+
|
|
336
|
+
A `config` that is not a plain object is refused too. The reconciler writes it into the
|
|
337
|
+
`"xanosdk"` block, and the reader refuses any entry that is not an object, so one run would
|
|
338
|
+
author a manifest that every later reconcile then refuses to read. It must also survive
|
|
339
|
+
`JSON.stringify`, so no BigInt and no cycles.
|
|
340
|
+
|
|
341
|
+
## `answersFromConfig`
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
answersFromConfig: (config) => ({
|
|
345
|
+
xanoscript: config.enabled === true,
|
|
346
|
+
dir: typeof config.dir === "string" ? config.dir : "xanoscript",
|
|
347
|
+
}),
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
The inverse of `contributes`: read your stored config block back into the answers that
|
|
351
|
+
produced it.
|
|
352
|
+
|
|
353
|
+
`contributes` is a **module-private transform**. Only you know that `{ enabled: true, dir:
|
|
354
|
+
"xanoscript" }` came from `{ xanoscript: true, dir: "xanoscript" }`. The SDK will not guess,
|
|
355
|
+
and matching question ids to same-named config keys would be a convention this contract does
|
|
356
|
+
not state.
|
|
357
|
+
|
|
358
|
+
**Declare it whenever your config keys are not your question ids.** Without it, a re-ask
|
|
359
|
+
offers your shipped defaults and, non-interactively, answers with them, silently resetting a
|
|
360
|
+
directory the user chose. A module that omits it is left as it is rather than re-derived, the
|
|
361
|
+
run reports it as carried, and `reinstall` tells the user what happened before the prompts
|
|
362
|
+
start.
|
|
363
|
+
|
|
364
|
+
It is the one hook that can never take a command down. Keys matching no declared question are
|
|
365
|
+
ignored, and a throw is reported and degrades to your defaults.
|
|
366
|
+
|
|
367
|
+
## `onBundle`
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
onBundle: async (ctx) => {
|
|
371
|
+
if (ctx.entry === undefined) return { message: "no entry, nothing rendered" };
|
|
372
|
+
if (ctx.frozen) {
|
|
373
|
+
const stale = compare(render(ctx.bundle.payload), readFromDisk(ctx.cwd));
|
|
374
|
+
return stale.length === 0
|
|
375
|
+
? { message: "rendered tree is current" }
|
|
376
|
+
: { message: "rendered tree is stale", warnings: stale, failed: true };
|
|
377
|
+
}
|
|
378
|
+
write(ctx.cwd, render(ctx.bundle.payload));
|
|
379
|
+
return { message: "rendered tree written" };
|
|
380
|
+
},
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Fires on `export` and every `deploy` (`--to` included), **after the bundle compiles and before the network call**.
|
|
384
|
+
That placement is deliberate: a tree describes the **source**, so writing it before the deploy
|
|
385
|
+
means a failed deploy still leaves a correct tree, and a tree never claims something shipped
|
|
386
|
+
that did not.
|
|
387
|
+
|
|
388
|
+
Plugins run in sequence rather than concurrently. They write to the user's tree and to the
|
|
389
|
+
terminal, and interleaved output from two modules writing adjacent directories is harder to
|
|
390
|
+
read than it is slow.
|
|
391
|
+
|
|
392
|
+
| `BundleContext` | What it carries |
|
|
393
|
+
|---|---|
|
|
394
|
+
| `bundle.payload` | The compiled payload, keyed by payload key (`table`, `query`, `app`, ...). |
|
|
395
|
+
| `entry` | The entry file the bundle came from. **Absent** on the `--bundle <path>` branch. |
|
|
396
|
+
| `cwd` | The project root every relative path resolves against. |
|
|
397
|
+
| `command` | `"export"` or `"deploy"`. |
|
|
398
|
+
| `frozen` | The run is `--frozen-lock` (or `export --check` / `deploy --to … --dry-run`, which imply it): a verification, not a write. |
|
|
399
|
+
| `sdkVersion` | The running `@xano/sdk` version, for your own peer-range check. |
|
|
400
|
+
| `config` | Your block from the project's `package.json` `"xanosdk"` object. |
|
|
401
|
+
|
|
402
|
+
**`entry` is optional rather than a made-up path** so a plugin may decline the
|
|
403
|
+
`--bundle <path>` branch, where the input is already-serialized text with no entry and no
|
|
404
|
+
registry behind it. A tree rendered from a bundle the project did not compile describes
|
|
405
|
+
something other than the source beside it.
|
|
406
|
+
|
|
407
|
+
**Under `frozen` a plugin must compare and report, never write.** It is also the mode in which
|
|
408
|
+
a plugin failing to load is fatal rather than a warning.
|
|
409
|
+
|
|
410
|
+
### Env values are blanked before you see them
|
|
411
|
+
|
|
412
|
+
`payload.env` entries keep their `name` and carry `value: ""`. A hook's output is typically
|
|
413
|
+
committed, and the real values come from a gitignored file, so handing them to a renderer
|
|
414
|
+
would be handing them to git. Your obvious implementation, rendering the payload faithfully,
|
|
415
|
+
would put production credentials into a repository and nothing in the contract would have
|
|
416
|
+
warned you. The declarations survive because they are structure; the secrets do not.
|
|
417
|
+
|
|
418
|
+
## `onPreflight`
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
onPreflight: async (ctx) => {
|
|
422
|
+
if (ctx.engineRendering.kind === "error") {
|
|
423
|
+
return { message: `could not read the engine's rendering: ${ctx.engineRendering.why}` };
|
|
424
|
+
}
|
|
425
|
+
...
|
|
426
|
+
},
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
Fires on `xanosdk preflight`, contributing a comparison to the report.
|
|
430
|
+
|
|
431
|
+
| `PreflightContext` | What it carries |
|
|
432
|
+
|---|---|
|
|
433
|
+
| `engineRendering` | The engine's own rendering of what it has, **or why it could not be had**. |
|
|
434
|
+
| `exportedPayload` | The payload as the engine exported it after import. |
|
|
435
|
+
| `remappedPayload` | The compiled payload with its guids translated to the engine's re-minted ones. |
|
|
436
|
+
| `verbose` | The command was run with `--verbose`: report at length. |
|
|
437
|
+
| `cwd`, `sdkVersion`, `config` | As on `BundleContext`. |
|
|
438
|
+
|
|
439
|
+
The SDK owns the fetch and nothing else. Every derivation past that point is yours: you have
|
|
440
|
+
both payloads, so you render both local sides and compute your own exclusions, and no
|
|
441
|
+
rendering knowledge stays behind in the SDK.
|
|
442
|
+
|
|
443
|
+
`engineRendering` is tagged because the difference is load-bearing. **A missing text route
|
|
444
|
+
must report as unavailable, never as drift.** Treating a failed fetch as an empty rendering
|
|
445
|
+
would diff every object in the workspace against nothing and report a catastrophic mismatch
|
|
446
|
+
that is really a network error.
|
|
447
|
+
|
|
448
|
+
Both payloads are blanked identically, so a comparison between them is unaffected. A
|
|
449
|
+
comparison against `engineRendering` should exclude env values, which the engine has no
|
|
450
|
+
reason to blank.
|
|
451
|
+
|
|
452
|
+
A project whose plugins declare no `onPreflight` never pays for the engine's rendering at all,
|
|
453
|
+
so declaring the hook is what turns that fetch on.
|
|
454
|
+
|
|
455
|
+
## `HookResult`: `failed` is not a throw
|
|
456
|
+
|
|
457
|
+
```ts
|
|
458
|
+
interface HookResult {
|
|
459
|
+
message?: string; // one line for the command's normal output
|
|
460
|
+
warnings?: string[]; // per-item notes shown under the message
|
|
461
|
+
failed?: boolean; // the check this hook performed did not pass
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
This distinction is the whole reason the field exists.
|
|
466
|
+
|
|
467
|
+
- **A thrown error is a broken plugin.** It could not do its job. On `export` and `deploy` the
|
|
468
|
+
command warns and carries on, and under `--frozen-lock` it is fatal: a check that did not
|
|
469
|
+
run must not read as a check that passed. A throw from `onPreflight` is never fatal, because
|
|
470
|
+
`preflight` is itself a check and its own report is worth printing, but it does fail the
|
|
471
|
+
gate for the same reason.
|
|
472
|
+
- **`failed: true` is a working plugin reporting that what it checked does not pass.** A
|
|
473
|
+
committed tree that no longer matches the source, say. It is never swallowed and reaches the
|
|
474
|
+
command's exit code on every run.
|
|
475
|
+
|
|
476
|
+
Collapsing the two would break both directions: a bug in a plugin would fail a deploy that is
|
|
477
|
+
fine, and a real staleness finding would be swallowed as a plugin bug. `preflight` maps
|
|
478
|
+
`failed` onto the same non-zero exit code it uses for its own findings, which is what lets CI
|
|
479
|
+
keep gating on it.
|
|
480
|
+
|
|
481
|
+
Set `failed` only when your plugin ran correctly and the answer is no.
|
|
482
|
+
|
|
483
|
+
## Contract skew is refused, never absorbed
|
|
484
|
+
|
|
485
|
+
Because every hook is optional, a module written against a shape of this contract the running
|
|
486
|
+
SDK does not have would load, register, and contribute **nothing**, silently, since "a module
|
|
487
|
+
with no contributions" is a legitimate outcome. So the SDK detects each known skew structurally
|
|
488
|
+
and refuses it by name, with the upgrade to make.
|
|
489
|
+
|
|
490
|
+
| Shape | Why it is refused | The fix |
|
|
491
|
+
|---|---|---|
|
|
492
|
+
| A `files` hook and no `contributes` | `files` was renamed once the hook stopped running only at scaffold time. | Rename it. `contributes(answers)` returns a `ProjectContributions`. |
|
|
493
|
+
| A `ciSteps` entry in what `contributes` returns | The slot is gone. Your check already rides on `xanosdk export ./xano/index.ts --frozen-lock` through `onBundle` with `frozen` set. | Drop it. No workflow step of your own is needed. |
|
|
494
|
+
| `contributes` returning a string, a number, an array or a Promise | Indistinguishable from "contributed nothing", which is recorded as switched off. | Return an object, or nothing at all. `contributes` is synchronous. |
|
|
495
|
+
| A default export whose `kind` is not `"toolchain"` | The manifest and the module disagree, so one of the two is stale. | Make them agree. |
|
|
496
|
+
| An SDK outside your declared `peerDependencies["@xano/sdk"]` range | Your hooks were written against shapes this SDK does not have, so it would apply nothing. | Upgrade the SDK, or install a build of your module made for it. |
|
|
497
|
+
|
|
498
|
+
The peer-range row is the one that catches the skew **in the other direction** — your module
|
|
499
|
+
newer than the SDK running it. Nothing about the plugin object can reveal that: the check that
|
|
500
|
+
would recognize a hook name you have not yet invented would have to ship in the SDK that
|
|
501
|
+
predates it. Your declared range is the only fact both sides hold, which is why it is read
|
|
502
|
+
rather than trusted to npm's install-time warning — that warning is routinely bypassed by
|
|
503
|
+
`--legacy-peer-deps`, by pnpm and yarn peer handling, and by an SDK downgraded after the
|
|
504
|
+
install. It is also the only version check that reaches a **reconcile**: `sdkVersion` is handed
|
|
505
|
+
to `onBundle` and `onPreflight` only, so a guard of your own inside a hook first fires on the
|
|
506
|
+
next `export` or `deploy`, well after the `marketplace install` that quietly wrote nothing.
|
|
507
|
+
|
|
508
|
+
Someone pinned to an older SDK is never locked out by this: `"enabled": false` is read before
|
|
509
|
+
the check runs, and `marketplace remove` uninstalls before it reconciles.
|
|
510
|
+
|
|
511
|
+
The installers ask the same question, so a module of yours that cannot run on the SDK in front
|
|
512
|
+
of it never gets left on disk waiting to be refused: `marketplace install` undoes an install it
|
|
513
|
+
just made, `reinstall` refuses without touching the module you already had, and
|
|
514
|
+
`init --marketplace` reports it and takes it back off. See
|
|
515
|
+
[Peer ranges](marketplace.md#peer-ranges).
|
|
516
|
+
|
|
517
|
+
The last row in the table above is the pattern: silently dropping a contributed CI step would
|
|
518
|
+
be a guard that stops running, and a wrong return type would switch a working module off and
|
|
519
|
+
keep it off while `marketplace remove` refused it for being deliberately disabled. Neither
|
|
520
|
+
failure would ever reach you.
|
|
521
|
+
|
|
522
|
+
## What a throwing hook costs
|
|
523
|
+
|
|
524
|
+
A module that cannot describe its contributions loses its contributions, not the command.
|
|
525
|
+
`contributes` is third-party code running after an install has already written to the target
|
|
526
|
+
directory; letting it take the run down would leave a half-populated project every re-run then
|
|
527
|
+
refuses. Your module is reported and every byte it owns is left exactly as it was, including
|
|
528
|
+
its config, whose presence is what tells the next run the questionnaire already completed.
|
|
529
|
+
|
|
530
|
+
A **refusal** (the rows in the table above) does propagate. Those are deterministic, so
|
|
531
|
+
re-running cannot fix them, and carrying on would ship a project that does not do what your
|
|
532
|
+
module says it does.
|
|
533
|
+
|
|
534
|
+
## Testing your module
|
|
535
|
+
|
|
536
|
+
The SDK's own suite drives a generated fixture plugin through the real load path rather than
|
|
537
|
+
mocking it, which is the shape worth copying: write your plugin into a temp project's
|
|
538
|
+
`node_modules` with a real `package.json`, and run the CLI against it.
|
|
539
|
+
|
|
540
|
+
Things worth proving about your own module:
|
|
541
|
+
|
|
542
|
+
- `contributes` returns the same thing for the same answers, called twice.
|
|
543
|
+
- `answersFromConfig(contributes(a).config)` round-trips back to `a` for every answer set.
|
|
544
|
+
- Your `onBundle` writes nothing when `ctx.frozen` is true.
|
|
545
|
+
- Your `onBundle` handles `ctx.entry === undefined`.
|
|
546
|
+
- Your hooks never read `payload.env` values, because they are blanked.
|
|
547
|
+
|
|
548
|
+
## Publishing
|
|
549
|
+
|
|
550
|
+
Publish to npm like any package. To appear in `xanosdk marketplace list` and `search`, the
|
|
551
|
+
module also needs a catalogue entry, which carries its title, tagline, description, tags, the
|
|
552
|
+
objects it installs, what the user supplies, the registration snippet, and the agent prompt
|
|
553
|
+
`details --prompt` emits. Write that prompt for a machine: it is what `init --marketplace`
|
|
554
|
+
and `marketplace install` point at whenever they cannot wire your module themselves.
|
|
555
|
+
|
|
556
|
+
Nothing about the marketplace is required to use a module. Any package declaring a `"xanosdk"`
|
|
557
|
+
field works from a `file:` path, a tarball, a git spec or a private registry, and the CLI
|
|
558
|
+
passes the specifier to npm exactly as typed.
|