@classytic/arc 2.20.0 → 2.21.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/dist/{BaseController-D-MHZ27_.mjs → BaseController-BnLYgE9A.mjs} +2 -2
- package/dist/{EventTransport-IEn7Tn-Q.d.mts → EventTransport-QmY7A258.d.mts} +0 -1
- package/dist/{QueryCache-TRfXau3W.d.mts → QueryCache-Z1B38koh.d.mts} +0 -1
- package/dist/{actionPermissions-C9RMiIpc.mjs → actionPermissions-ze70xXJi.mjs} +1 -1
- package/dist/audit/index.d.mts +9 -5
- package/dist/auth/audit.d.mts +7 -4
- package/dist/auth/audit.mjs +5 -0
- package/dist/auth/index.d.mts +7 -14
- package/dist/auth/index.mjs +1 -1
- package/dist/auth/redis-session.d.mts +1 -2
- package/dist/cache/index.d.mts +2 -3
- package/dist/cli/commands/describe.d.mts +1 -2
- package/dist/cli/commands/docs.mjs +1 -1
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/context/index.d.mts +2 -2
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +4 -4
- package/dist/{core-CWhyGkiM.mjs → core-DZ6Xe1jR.mjs} +65 -10
- package/dist/{createActionRouter-HlQso0jO.mjs → createActionRouter-BWMDW-dL.mjs} +2 -2
- package/dist/{createAggregationRouter-5LzPzgw1.mjs → createAggregationRouter-l5mNdYrZ.mjs} +1 -1
- package/dist/{createApp-D6uwzCKu.mjs → createApp-Bi61XxqU.mjs} +149 -137
- package/dist/discovery/index.d.mts +0 -1
- package/dist/docs/index.d.mts +1 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/{elevation-xGUBq6p_.d.mts → elevation-C9s5hFP_.d.mts} +0 -1
- package/dist/encryption/index.d.mts +2 -144
- package/dist/{errorHandler-BtUUDPEa.d.mts → errorHandler-B0e-bq-O.d.mts} +0 -1
- package/dist/{errors-DOHkjQlD.d.mts → errors-DHcHVAgk.d.mts} +0 -1
- package/dist/{eventPlugin-DbBFd3x_.d.mts → eventPlugin-A6EbthXR.d.mts} +10 -6
- package/dist/events/index.d.mts +9 -6
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -2
- package/dist/factory/index.d.mts +4 -4
- package/dist/factory/index.mjs +2 -2
- package/dist/{fastify-augmentation-CCQJEDgh.d.mts → fastify-augmentation-DafigGQp.d.mts} +2 -3
- package/dist/{fields-C7ahdGYf.d.mts → fields-BGqrAa_u.d.mts} +1 -2
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.d.mts +4 -3
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-DUa8WHz3.d.mts → index-B5N-NaHs.d.mts} +88 -7
- package/dist/{index-CdFkIMEd.d.mts → index-B5PoNbfu.d.mts} +36 -7
- package/dist/{index-_--GyzcO.d.mts → index-Dtrj6XZk.d.mts} +62 -28
- package/dist/index.d.mts +7 -7
- package/dist/index.mjs +6 -6
- package/dist/{init-YH_nslQR.mjs → init-0YhHkXGk.mjs} +154 -137
- package/dist/integrations/event-gateway.d.mts +2 -3
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/jobs.d.mts +0 -1
- package/dist/integrations/mcp/index.d.mts +6 -7
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -2
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/streamline.d.mts +2 -2
- package/dist/integrations/webhooks.d.mts +0 -1
- package/dist/integrations/websocket-pushref-redis.d.mts +1 -2
- package/dist/integrations/websocket-redis.d.mts +1 -2
- package/dist/integrations/websocket.d.mts +2 -2
- package/dist/{loadResources-B8UmpITt.mjs → loadResources-DpU8pfjV.mjs} +4 -3
- package/dist/{mcpPlugin-CuW6L_uD.mjs → mcpPlugin-B9XjjNVG.mjs} +3 -3
- package/dist/middleware/index.d.mts +1 -2
- package/dist/{module-JYH9V0I8.d.mts → module-WeuU5jfd.d.mts} +46 -1
- package/dist/{openapi-Dq6RbEGK.mjs → openapi-Kyod_di6.mjs} +2 -2
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +2 -2
- package/dist/{permissions-CgbOLJZG.mjs → permissions-BJMuKMl7.mjs} +33 -1
- package/dist/pipeline/index.d.mts +1 -2
- package/dist/plugins/index.d.mts +79 -7
- package/dist/plugins/index.mjs +88 -1
- package/dist/plugins/response-cache.d.mts +5 -3
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/filesUpload.d.mts +4 -5
- package/dist/presets/filesUpload.mjs +1 -1
- package/dist/presets/index.d.mts +1 -2
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -2
- package/dist/presets/search.d.mts +2 -3
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-DWsYQdbV.mjs → presets-gw0ciIUD.mjs} +1 -1
- package/dist/{pushref-registry-DnCqL3aa.d.mts → pushref-registry-BbcnpNTm.d.mts} +4 -2
- package/dist/{queryCachePlugin-xzFyKAjT.d.mts → queryCachePlugin-CfYWq-Ak.d.mts} +1 -2
- package/dist/{redis-C3W0qbz8.d.mts → redis-UDtLaNj6.d.mts} +0 -1
- package/dist/{redis-stream-DsP1vqZB.d.mts → redis-stream-iVR2l7aS.d.mts} +1 -2
- package/dist/{registerRoute-DGSREwPw.mjs → registerRoute-oGAl0oA8.mjs} +1 -1
- package/dist/registry/index.d.mts +2 -2
- package/dist/schemas/index.d.mts +0 -1
- package/dist/scim/index.d.mts +0 -1
- package/dist/scope/index.d.mts +2 -3
- package/dist/{sessionManager-BXG-HGwG.d.mts → sessionManager-BHOM3N_0.d.mts} +10 -6
- package/dist/{storage-Co_PQpeQ.d.mts → storage-DLUZ04kK.d.mts} +8 -4
- package/dist/testing/index.d.mts +2 -3
- package/dist/testing/index.mjs +5 -2
- package/dist/testing/storageContract.d.mts +1 -2
- package/dist/{tracing-D8jlGXP7.d.mts → tracing-CsGKQJGg.d.mts} +0 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/types/storage.d.mts +1 -1
- package/dist/types-Cc91nXuA.d.mts +132 -0
- package/dist/{types-LNebCoJx.d.mts → types-DH0GupI7.d.mts} +8 -4
- package/dist/{types-C9b2HR5W.d.mts → types-DRj-Ut4n.d.mts} +1 -2
- package/dist/{types-BHMKlMBU.d.mts → types-DpxoNX5v.d.mts} +61 -21
- package/dist/utils/index.d.mts +182 -128
- package/dist/utils/index.mjs +2 -2
- package/dist/{utils-ChjYQpjn.mjs → utils-CO8a3B_1.mjs} +70 -26
- package/dist/{versioning-D_eBCfNr.d.mts → versioning-DQ4mCvg0.d.mts} +1 -2
- package/dist/{websocket-MlYlrYq0.d.mts → websocket-uuWqM6UY.d.mts} +5 -4
- package/package.json +7 -7
- package/skills/arc-module-publishing/SKILL.md +242 -0
- package/skills/arc-module-publishing/references/authoring-modules.md +272 -0
- package/skills/arc-module-publishing/references/package-manifest.md +155 -0
- package/skills/arc-module-publishing/references/publishing-workflow.md +182 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { m as RESERVED_QUERY_PARAMS, t as CRUD_OPERATIONS } from "./constants-DNre0wRg.mjs";
|
|
2
2
|
import { arcLog } from "./logger/index.mjs";
|
|
3
3
|
import { t as ArcError } from "./errors-DkO8Nk6-.mjs";
|
|
4
|
-
import { r as getAvailablePresets } from "./presets-
|
|
4
|
+
import { r as getAvailablePresets } from "./presets-gw0ciIUD.mjs";
|
|
5
5
|
import { errorContractSchema as errorContractSchema$1, errorDetailSchema as errorDetailSchema$1 } from "@classytic/repo-core/errors";
|
|
6
6
|
//#region src/utils/simpleEqualityMatcher.ts
|
|
7
7
|
/**
|
|
@@ -752,6 +752,55 @@ function defineGuard(config) {
|
|
|
752
752
|
};
|
|
753
753
|
}
|
|
754
754
|
//#endregion
|
|
755
|
+
//#region src/utils/filter-dialect.ts
|
|
756
|
+
/**
|
|
757
|
+
* Default dialect — MongoDB / MongoKit operator shape. Emits `$`-prefixed
|
|
758
|
+
* tokens and the `$options: 'i'` case-insensitive sibling. This is what
|
|
759
|
+
* `ArcQueryParser` uses when no dialect is supplied, so behaviour is identical
|
|
760
|
+
* to every prior arc version.
|
|
761
|
+
*/
|
|
762
|
+
const MONGO_DIALECT = {
|
|
763
|
+
operators: {
|
|
764
|
+
eq: "$eq",
|
|
765
|
+
ne: "$ne",
|
|
766
|
+
gt: "$gt",
|
|
767
|
+
gte: "$gte",
|
|
768
|
+
lt: "$lt",
|
|
769
|
+
lte: "$lte",
|
|
770
|
+
in: "$in",
|
|
771
|
+
nin: "$nin",
|
|
772
|
+
like: "$regex",
|
|
773
|
+
contains: "$regex",
|
|
774
|
+
regex: "$regex",
|
|
775
|
+
exists: "$exists"
|
|
776
|
+
},
|
|
777
|
+
caseInsensitiveModifier: () => ({ $options: "i" })
|
|
778
|
+
};
|
|
779
|
+
/**
|
|
780
|
+
* Store-neutral reference dialect — emits UN-prefixed operator tokens as a
|
|
781
|
+
* translation AST (`?price[gte]=40` → `{ price: { gte: 40 } }`, with
|
|
782
|
+
* `{ caseInsensitive: true }` for `like`/`contains`). A non-Mongo adapter can
|
|
783
|
+
* consume this directly instead of writing a full `QueryParserInterface`. Use
|
|
784
|
+
* as a starting point; adjust the token names to your store's vocabulary.
|
|
785
|
+
*/
|
|
786
|
+
const NEUTRAL_DIALECT = {
|
|
787
|
+
operators: {
|
|
788
|
+
eq: "eq",
|
|
789
|
+
ne: "ne",
|
|
790
|
+
gt: "gt",
|
|
791
|
+
gte: "gte",
|
|
792
|
+
lt: "lt",
|
|
793
|
+
lte: "lte",
|
|
794
|
+
in: "in",
|
|
795
|
+
nin: "nin",
|
|
796
|
+
like: "like",
|
|
797
|
+
contains: "contains",
|
|
798
|
+
regex: "regex",
|
|
799
|
+
exists: "exists"
|
|
800
|
+
},
|
|
801
|
+
caseInsensitiveModifier: () => ({ caseInsensitive: true })
|
|
802
|
+
};
|
|
803
|
+
//#endregion
|
|
755
804
|
//#region src/utils/handleRaw.ts
|
|
756
805
|
/**
|
|
757
806
|
* Wrap a raw Fastify handler with Arc's response shape and error handling.
|
|
@@ -868,22 +917,17 @@ var ArcQueryParser = class {
|
|
|
868
917
|
allowedSortFields;
|
|
869
918
|
/** Allowed operators (used by MCP for operator descriptions) */
|
|
870
919
|
allowedOperators;
|
|
871
|
-
/**
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
nin: "$nin",
|
|
881
|
-
like: "$regex",
|
|
882
|
-
contains: "$regex",
|
|
883
|
-
regex: "$regex",
|
|
884
|
-
exists: "$exists"
|
|
885
|
-
};
|
|
920
|
+
/**
|
|
921
|
+
* Datastore dialect — owns the store-specific operator tokens + the
|
|
922
|
+
* case-insensitive modifier. Defaults to Mongo (backward-compatible).
|
|
923
|
+
*/
|
|
924
|
+
dialect;
|
|
925
|
+
/** Supported filter operators (neutral name → emitted token), from the dialect. */
|
|
926
|
+
get operators() {
|
|
927
|
+
return this.dialect.operators;
|
|
928
|
+
}
|
|
886
929
|
constructor(options = {}) {
|
|
930
|
+
this.dialect = options.dialect ?? MONGO_DIALECT;
|
|
887
931
|
this.maxLimit = options.maxLimit ?? 1e3;
|
|
888
932
|
this.defaultLimit = options.defaultLimit ?? 20;
|
|
889
933
|
this.maxRegexLength = options.maxRegexLength ?? 200;
|
|
@@ -1063,17 +1107,17 @@ var ArcQueryParser = class {
|
|
|
1063
1107
|
const allOperators = operatorKeys.every((op) => this.operators[op] && (!this._allowedOperators || this._allowedOperators.has(op)));
|
|
1064
1108
|
const allKnownOperators = operatorKeys.every((op) => this.operators[op]);
|
|
1065
1109
|
if (allOperators && operatorKeys.length > 0) {
|
|
1066
|
-
const
|
|
1110
|
+
const fieldOps = {};
|
|
1067
1111
|
let needsCaseInsensitive = false;
|
|
1068
1112
|
for (const [op, opValue] of Object.entries(operatorObj)) {
|
|
1069
|
-
const
|
|
1070
|
-
if (
|
|
1071
|
-
|
|
1113
|
+
const token = this.operators[op];
|
|
1114
|
+
if (token) {
|
|
1115
|
+
fieldOps[token] = this.parseFilterValue(opValue, op);
|
|
1072
1116
|
if (op === "contains" || op === "like") needsCaseInsensitive = true;
|
|
1073
1117
|
}
|
|
1074
1118
|
}
|
|
1075
|
-
if (needsCaseInsensitive)
|
|
1076
|
-
filters[key] =
|
|
1119
|
+
if (needsCaseInsensitive) Object.assign(fieldOps, this.dialect.caseInsensitiveModifier());
|
|
1120
|
+
filters[key] = fieldOps;
|
|
1077
1121
|
return;
|
|
1078
1122
|
}
|
|
1079
1123
|
if (allKnownOperators && this._allowedOperators) return;
|
|
@@ -1083,12 +1127,12 @@ var ArcQueryParser = class {
|
|
|
1083
1127
|
const [, fieldName, operator] = match;
|
|
1084
1128
|
if (!fieldName) return;
|
|
1085
1129
|
if (operator && this.operators[operator] && (!this._allowedOperators || this._allowedOperators.has(operator))) {
|
|
1086
|
-
const
|
|
1130
|
+
const token = this.operators[operator];
|
|
1087
1131
|
const parsedValue = this.parseFilterValue(value, operator);
|
|
1088
1132
|
if (!filters[fieldName]) filters[fieldName] = {};
|
|
1089
1133
|
const fieldFilter = filters[fieldName];
|
|
1090
|
-
fieldFilter[
|
|
1091
|
-
if (operator === "contains" || operator === "like") fieldFilter
|
|
1134
|
+
fieldFilter[token] = parsedValue;
|
|
1135
|
+
if (operator === "contains" || operator === "like") Object.assign(fieldFilter, this.dialect.caseInsensitiveModifier());
|
|
1092
1136
|
} else if (!operator) filters[fieldName] = this.parseFilterValue(value);
|
|
1093
1137
|
}
|
|
1094
1138
|
parseFilterValue(value, operator) {
|
|
@@ -1688,4 +1732,4 @@ function getUserId(user) {
|
|
|
1688
1732
|
return id ? String(id) : void 0;
|
|
1689
1733
|
}
|
|
1690
1734
|
//#endregion
|
|
1691
|
-
export {
|
|
1735
|
+
export { createCircuitBreaker as A, defineErrorMapper as C, CircuitBreakerError as D, CircuitBreaker as E, simpleEqualityMatcher as F, assertValidConfig as M, formatValidationErrors as N, CircuitBreakerRegistry as O, validateResourceConfig as P, defineGuard as S, withCompensation as T, ArcQueryParser as _, bareListResponse as a, MONGO_DIALECT as b, errorDetailSchema$1 as c, keysetListResponse as d, listResponse as f, responses as g, queryParams as h, aggregateListResponse as i, createCircuitBreakerRegistry as j, CircuitState as k, getDefaultCrudSchemas as l, paginationSchema as m, createStateMachine as n, deleteResponse as o, offsetListResponse as p, scheduleBackground as r, errorContractSchema$1 as s, getUserId as t, getListQueryParams as u, createQueryParser as v, defineCompensation as w, NEUTRAL_DIALECT as x, handleRaw as y };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { n as DomainEvent } from "./EventTransport-
|
|
1
|
+
import { n as DomainEvent } from "./EventTransport-QmY7A258.mjs";
|
|
2
2
|
import { FastifyPluginAsync, FastifyRequest } from "fastify";
|
|
3
|
-
|
|
4
3
|
//#region src/plugins/health.d.ts
|
|
5
4
|
declare module "fastify" {
|
|
6
5
|
interface FastifyRequest {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { a as PushRefRegistry, l as EnvelopeWriter, s as PushRefStore } from "./pushref-registry-
|
|
1
|
+
import { a as PushRefRegistry, l as EnvelopeWriter, s as PushRefStore } from "./pushref-registry-BbcnpNTm.mjs";
|
|
2
2
|
import { FastifyBaseLogger, FastifyPluginAsync } from "fastify";
|
|
3
|
-
|
|
4
3
|
//#region src/integrations/websocket/adapter.d.ts
|
|
5
4
|
/**
|
|
6
5
|
* WebSocket cross-instance adapter contract.
|
|
@@ -135,8 +134,10 @@ interface WebSocketClient {
|
|
|
135
134
|
send(data: string): void;
|
|
136
135
|
close(code?: number, reason?: string): void;
|
|
137
136
|
readyState: number;
|
|
138
|
-
bufferedAmount?: number;
|
|
139
|
-
|
|
137
|
+
bufferedAmount?: number;
|
|
138
|
+
/** RFC 6455 §5.5.2 ping control frame. Browsers auto-respond at the protocol layer. */
|
|
139
|
+
ping?(data?: unknown): void;
|
|
140
|
+
/** Immediate socket destroy — no Close frame, no flush. Use for zombies. */
|
|
140
141
|
terminate?(): void;
|
|
141
142
|
};
|
|
142
143
|
subscriptions: Set<string>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/arc",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.21.0",
|
|
4
4
|
"description": "Resource-oriented backend framework for Fastify - clean, minimal, powerful, tree-shakable",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -231,8 +231,8 @@
|
|
|
231
231
|
"lint:fix": "biome check --fix src/",
|
|
232
232
|
"lint:all": "biome check src/ tests/",
|
|
233
233
|
"test": "vitest run",
|
|
234
|
-
"test:main": "vitest run
|
|
235
|
-
"test:
|
|
234
|
+
"test:main": "vitest run",
|
|
235
|
+
"test:main:serial": "vitest run --no-file-parallelism",
|
|
236
236
|
"test:perf": "node --expose-gc ./node_modules/vitest/vitest.mjs run --config vitest.perf.config.ts --no-file-parallelism",
|
|
237
237
|
"test:ci": "npm run test:main && npm run test:perf",
|
|
238
238
|
"test:watch": "vitest",
|
|
@@ -373,9 +373,9 @@
|
|
|
373
373
|
"@better-auth/mongo-adapter": "^1.6.19",
|
|
374
374
|
"@biomejs/biome": "^2.5.0",
|
|
375
375
|
"@classytic/dev-tools": "^0.2.0",
|
|
376
|
-
"@classytic/mongokit": "^3.
|
|
376
|
+
"@classytic/mongokit": "^3.21.0",
|
|
377
377
|
"@classytic/primitives": "^0.9.1",
|
|
378
|
-
"@classytic/repo-core": "^0.
|
|
378
|
+
"@classytic/repo-core": "^0.9.0",
|
|
379
379
|
"@classytic/sqlitekit": "^0.6.0",
|
|
380
380
|
"@classytic/streamline": "^2.7.0",
|
|
381
381
|
"@fastify/cors": "^11.2.0",
|
|
@@ -410,8 +410,8 @@
|
|
|
410
410
|
"mongodb": "^7.3.0",
|
|
411
411
|
"mongodb-memory-server": "^11.2.0",
|
|
412
412
|
"mongoose": "^9.7.1",
|
|
413
|
-
"tsdown": "^0.22.
|
|
414
|
-
"typescript": "^
|
|
413
|
+
"tsdown": "^0.22.5",
|
|
414
|
+
"typescript": "^7.0.2",
|
|
415
415
|
"vitest": "^4.1.9",
|
|
416
416
|
"ws": "^8.21.0",
|
|
417
417
|
"zod": "^4.4.3"
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: arc-module-publishing
|
|
3
|
+
description: |
|
|
4
|
+
Author and publish an ecosystem package for @classytic/arc the way arc's own packages
|
|
5
|
+
are built — modules, action presets, and plugins that hosts install and compose.
|
|
6
|
+
Codifies the non-negotiable conventions: no hardcoded dependencies (host duty), peers
|
|
7
|
+
as `>=` ranges (stable API contract), bundle-nothing tsdown, public-subpath imports only,
|
|
8
|
+
one-way dependency (arc never depends on you), and the classytic release gate.
|
|
9
|
+
Use when creating a new @classytic/arc-<domain> package, extracting a domain out of an
|
|
10
|
+
app into a reusable module, deciding a package's package.json / tsdown / peer setup,
|
|
11
|
+
writing a defineModule() or an action preset, or preparing an arc module for npm publish.
|
|
12
|
+
Triggers: publish arc module, arc package, arc ecosystem, defineModule, ArcModule, arc
|
|
13
|
+
plugin package, arc action preset, extract domain to package, arc-approval, arc-involvement,
|
|
14
|
+
peer dependencies arc, tsdown arc package, arc module conventions, classytic publish.
|
|
15
|
+
license: MIT
|
|
16
|
+
metadata:
|
|
17
|
+
author: Classytic
|
|
18
|
+
tags:
|
|
19
|
+
- arc
|
|
20
|
+
- packaging
|
|
21
|
+
- publishing
|
|
22
|
+
- modules
|
|
23
|
+
- ecosystem
|
|
24
|
+
- tsdown
|
|
25
|
+
- peer-dependencies
|
|
26
|
+
- monorepo
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
# Publishing @classytic/arc ecosystem packages
|
|
30
|
+
|
|
31
|
+
How to author and ship a reusable package **on top of** arc — a domain module, an action
|
|
32
|
+
preset, or a plugin — so it composes cleanly into any host and follows the same discipline
|
|
33
|
+
as arc's own first-party packages (`@classytic/arc-approval`, `@classytic/arc-involvement`,
|
|
34
|
+
`@classytic/arc-notifications`).
|
|
35
|
+
|
|
36
|
+
**Node ≥22 · ESM only · TypeScript strict · tsdown build · Biome lint.**
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## The one rule everything else follows
|
|
41
|
+
|
|
42
|
+
> **The package declares a contract; the host owns the installation.**
|
|
43
|
+
|
|
44
|
+
A published arc package is a *contribution* to someone else's app, not an app. That single
|
|
45
|
+
idea produces every convention below: you never bundle what the host provides, you never
|
|
46
|
+
pin what the host must choose, you never reach past arc's public surface, and you never
|
|
47
|
+
make arc depend on you.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Decision: which shape are you shipping?
|
|
52
|
+
|
|
53
|
+
Three shapes, smallest surface first. Pick the smallest that does the job.
|
|
54
|
+
|
|
55
|
+
| Shape | You export | Host uses it via | Example |
|
|
56
|
+
|---|---|---|---|
|
|
57
|
+
| **Action preset** | a function returning an `actions:` slice | spread into `defineResource({ actions })` | `withApprovalChain()` (arc-approval) |
|
|
58
|
+
| **Plugin** | an `fp()`-wrapped Fastify plugin (+ named export) | `app.register()` / `plugins:` slot | any cross-cutting decorator/hook |
|
|
59
|
+
| **Module** | `createXModule(deps) → ArcModule` | `createApp({ modules: [...] })` | a whole domain (engine + resources + wiring) |
|
|
60
|
+
|
|
61
|
+
- A preset adds **verbs to one resource**. No lifecycle, no engine — just a function that
|
|
62
|
+
returns `Record<string, ActionDefinition>`. Storage-agnostic via a **structural repository
|
|
63
|
+
port** (an interface with the two or three methods you actually call), never a kit import.
|
|
64
|
+
- A module owns a **whole domain**: it initializes an engine in `bootstrap`, registers its
|
|
65
|
+
resources, wires `afterResources`, and tears down in `onClose`. Use `defineModule` and let
|
|
66
|
+
the host list it in `modules:`. See [references/authoring-modules.md](references/authoring-modules.md).
|
|
67
|
+
|
|
68
|
+
When unsure, ship a **preset or plugin**. A module is the right call only when there is
|
|
69
|
+
engine state to boot and multiple resources to register as a unit.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Dependency discipline (the part reviewers keep catching)
|
|
74
|
+
|
|
75
|
+
This is where packages go wrong. Get the `package.json` dependency fields exactly right:
|
|
76
|
+
|
|
77
|
+
- **`dependencies`: usually empty.** Everything arc-shaped is a *peer*. Only add a real
|
|
78
|
+
`dependency` for a genuinely private, bundled-in helper the host should never see or
|
|
79
|
+
control — which is rare. If you're tempted to add `@classytic/*` or `fastify` here, stop:
|
|
80
|
+
that's a peer.
|
|
81
|
+
- **`peerDependencies`: `>=` ranges, never `^`.** A peer is a *contract*: "I work against
|
|
82
|
+
arc 2.20 and up." `^2.20.0` would wrongly forbid a host on arc 3.x that still satisfies the
|
|
83
|
+
API you use. Use `">=2.20.0"`. This keeps the API contract stable and lets hosts upgrade arc
|
|
84
|
+
without your package blocking them.
|
|
85
|
+
- **`devDependencies`: pin with `^`.** These are the versions your *test suite* runs against —
|
|
86
|
+
a concrete, current version (`"@classytic/arc": "^2.20.0"`). The floor of every devDep must
|
|
87
|
+
be `>=` the peer floor, or your tests never exercise the version you claim to support.
|
|
88
|
+
- **Optional peers** (zod, ioredis, a kit) go in `peerDependencies` **and**
|
|
89
|
+
`peerDependenciesMeta: { "<name>": { optional: true } }`. Never let their types leak into
|
|
90
|
+
your public `.d.mts` — use structural typing so a host that doesn't install them still
|
|
91
|
+
compiles. See [references/package-manifest.md](references/package-manifest.md).
|
|
92
|
+
|
|
93
|
+
**Why `>=` and not `^` for peers:** the host, not you, resolves the actual installed version.
|
|
94
|
+
Your job is to state the *floor* of the API you depend on and stay out of the host's upgrade
|
|
95
|
+
path. A caret peer turns every host major-bump into a forced wait on your release.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Import discipline: public surface only
|
|
100
|
+
|
|
101
|
+
Import arc through its **published subpaths**, exactly as a third-party host would. Never
|
|
102
|
+
reach into `@classytic/arc/src/...` or a deep `dist/` path.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import type { PermissionCheck } from "@classytic/arc/permissions";
|
|
106
|
+
import { getOrgId, getUserId, requireOrgId } from "@classytic/arc/scope";
|
|
107
|
+
import type { ActionDefinition, RequestWithExtras } from "@classytic/arc/types";
|
|
108
|
+
import { createDomainError, NotFoundError, ValidationError } from "@classytic/arc/utils";
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The canonical entry points: `@classytic/arc` (core), `/permissions`, `/scope`, `/types`,
|
|
112
|
+
`/utils`, `/factory` (for `defineModule`, `getModuleExports`), `/testing`. If something you
|
|
113
|
+
need isn't on a public subpath, that's a signal — file it against arc rather than deep-import
|
|
114
|
+
around it. Deep imports break the moment arc reorganizes internals, which it's free to do
|
|
115
|
+
because they were never public.
|
|
116
|
+
|
|
117
|
+
**One-way dependency:** your package depends on arc; **arc never depends on your package.**
|
|
118
|
+
If arc would need something from you, the abstraction belongs *in arc* (a port, a hook, a
|
|
119
|
+
preset seam), not as a back-edge. This keeps the dependency graph a DAG and arc publishable
|
|
120
|
+
on its own.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Build: bundle nothing
|
|
125
|
+
|
|
126
|
+
tsdown, ESM only, and **bundle nothing that's a peer**. Every `@classytic/*` and `fastify`
|
|
127
|
+
stays external so the host's single installed copy is the one that runs (two copies of arc =
|
|
128
|
+
two plugin registries = subtle breakage).
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
// tsdown.config.ts
|
|
132
|
+
import { defineConfig } from "tsdown";
|
|
133
|
+
|
|
134
|
+
export default defineConfig({
|
|
135
|
+
entry: ["src/index.ts"],
|
|
136
|
+
format: ["esm"],
|
|
137
|
+
dts: true,
|
|
138
|
+
sourcemap: false,
|
|
139
|
+
clean: true,
|
|
140
|
+
treeshake: true,
|
|
141
|
+
target: "node22",
|
|
142
|
+
outDir: "dist",
|
|
143
|
+
deps: {
|
|
144
|
+
skipNodeModulesBundle: true, // don't inline node_modules
|
|
145
|
+
neverBundle: [/^@classytic\//], // arc + primitives + kits stay external, always
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`package.json` build wiring: `sideEffects: false`, `files: ["dist"]`, an `exports` map with
|
|
151
|
+
`types` + `default`, `type: "module"`, `engines.node: ">=22"`, `publishConfig.access:
|
|
152
|
+
"public"`. Full template in [references/package-manifest.md](references/package-manifest.md).
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Bridge onto arc's primitives — don't reinvent them
|
|
157
|
+
|
|
158
|
+
A hard-won lesson: when a host already gets a capability from arc, **bridge onto it** instead
|
|
159
|
+
of shipping a second path.
|
|
160
|
+
|
|
161
|
+
- Need background work? Use `fastify.jobs` (arc's BullMQ bridge) — don't add your own BullMQ
|
|
162
|
+
dependency and stand up a parallel queue. Two job systems in one app is a footgun, not a
|
|
163
|
+
feature.
|
|
164
|
+
- Need guaranteed event delivery? Use arc's **outbox** and the `@classytic/primitives` event
|
|
165
|
+
contract (`createEvent`, `DomainEvent`, `EventTransport`) — don't invent an event shape.
|
|
166
|
+
- Need permissions/scope? Use `@classytic/arc/permissions` + `@classytic/arc/scope`
|
|
167
|
+
accessors (`getOrgId`, `requireUserId`) — don't re-derive tenant/user from `request.user`.
|
|
168
|
+
|
|
169
|
+
The test: *"Does arc already bless a primitive for this?"* If yes, your package composes onto
|
|
170
|
+
it. Shipping a competing path breaks the host's single-source-of-truth and doubles their ops
|
|
171
|
+
surface. Reinvent only when arc genuinely has no primitive — and then consider whether it
|
|
172
|
+
should live in arc.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Naming & versioning
|
|
177
|
+
|
|
178
|
+
- **Package name:** `@classytic/arc-<domain>` for first-party; third parties use their own
|
|
179
|
+
scope (`@acme/arc-billing`). The `arc-` prefix signals "arc ecosystem package."
|
|
180
|
+
- **Module `name`** (the `defineModule({ name })` identifier): namespace it so it can't
|
|
181
|
+
collide in a host that composes many modules — `"accounting"`, `"order"`, or a
|
|
182
|
+
vendor-qualified `"acme.billing"`. This is the key hosts use in `dependsOn` and
|
|
183
|
+
`getModuleExports`, and arc rejects duplicate module names at boot.
|
|
184
|
+
- **SemVer the public export**, not the internals. Your exported function signatures, the
|
|
185
|
+
`ArcModule` shape you return, and the action/schema surface are the contract. Bumping a peer
|
|
186
|
+
floor (e.g. requiring a newer arc) is at least a **minor**; changing an exported signature is
|
|
187
|
+
a **major**.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Release gate
|
|
192
|
+
|
|
193
|
+
Every publish runs a gate — no `--no-verify`, ever. In a workspace, a shared script; in a
|
|
194
|
+
solo package, `prepublishOnly`:
|
|
195
|
+
|
|
196
|
+
```jsonc
|
|
197
|
+
"scripts": {
|
|
198
|
+
"build": "tsdown",
|
|
199
|
+
"typecheck": "tsc --noEmit",
|
|
200
|
+
"lint": "biome check src/",
|
|
201
|
+
"prepublishOnly": "npm run typecheck && npm run lint && npm run test && npm run build"
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
For a **monorepo of packages**, add a **peer-skew gate** that fails if any package's devDep
|
|
206
|
+
floor is below its declared peer floor — it's the mechanical guard that stops your suite from
|
|
207
|
+
silently testing against a version older than you promise to support. Full script and workspace
|
|
208
|
+
layout in [references/publishing-workflow.md](references/publishing-workflow.md).
|
|
209
|
+
|
|
210
|
+
**classytic packages** additionally: commit as `classytic-bot[bot]` (per-invocation git env
|
|
211
|
+
vars, never `git config --global`, no `Co-Authored-By`), push via `classytic-push`, publish to
|
|
212
|
+
public npm. See the workspace `CLAUDE.md` and [references/publishing-workflow.md](references/publishing-workflow.md).
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Pre-publish checklist
|
|
217
|
+
|
|
218
|
+
- [ ] `dependencies` is empty (or only truly-private bundled helpers)
|
|
219
|
+
- [ ] every arc/fastify/kit dep is a `peerDependency` with a `>=` range
|
|
220
|
+
- [ ] every peer floor is covered by a `^`-pinned devDep at or above it (peer-skew clean)
|
|
221
|
+
- [ ] optional peers are marked `optional: true` and their types don't leak into `dist/*.d.mts`
|
|
222
|
+
- [ ] tsdown `neverBundle: [/^@classytic\//]` + `skipNodeModulesBundle: true`
|
|
223
|
+
- [ ] imports use public subpaths only — no `@classytic/arc/src` or deep `dist` paths
|
|
224
|
+
- [ ] arc has **no** dependency on this package (grep arc for the package name → nothing)
|
|
225
|
+
- [ ] `sideEffects: false`, `files: ["dist"]`, `exports` map with `types`+`default`, `type: "module"`
|
|
226
|
+
- [ ] action schemas shipped as **JSON Schema** (zod stays an optional peer, never required)
|
|
227
|
+
- [ ] `prepublishOnly` gate green (typecheck + lint + test + build), no `--no-verify`
|
|
228
|
+
- [ ] module `name` is namespaced and can't collide across a multi-module host
|
|
229
|
+
- [ ] boots green in a **kit-backed testkit** (real in-memory store, prod-fidelity — not a fake repo)
|
|
230
|
+
- [ ] `.npmrc`: `auto-install-peers=false` + an install-script allowlist for any dep with a `postinstall`
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## References
|
|
235
|
+
|
|
236
|
+
- [references/package-manifest.md](references/package-manifest.md) — copy-paste `package.json`
|
|
237
|
+
+ `tsdown.config.ts`, optional-peer structural-typing pattern, the dependency-field cheat sheet.
|
|
238
|
+
- [references/authoring-modules.md](references/authoring-modules.md) — `defineModule` lifecycle,
|
|
239
|
+
`dependsOn` composition order, `getModuleExports` cross-module wiring, thunk lazy/region packs,
|
|
240
|
+
action-preset and structural-port patterns.
|
|
241
|
+
- [references/publishing-workflow.md](references/publishing-workflow.md) — monorepo layout, the
|
|
242
|
+
peer-skew + prepublish gates, classytic bot-identity release steps, versioning a public export.
|