@terraforge/core 0.0.5 → 0.0.6

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/README.md CHANGED
@@ -12,7 +12,7 @@ The most used IaC solutions are slow & don't effectively leverage diffing to spe
12
12
  Install with (NPM):
13
13
 
14
14
  ```
15
- npm i @terraforge/core @terraforge/aws
15
+ npm i @terraforge/core @terraforge/terraform @terraforge/aws
16
16
  ```
17
17
 
18
18
  ## Example
@@ -52,12 +52,12 @@ This example illustrates how simple it is to define multi-stack resources withou
52
52
  ```ts
53
53
  const app = new App('todo-app')
54
54
  const storage = new Stack(app, 'storage')
55
- const list = new aws.s3.Bucket(storage, 'list', {
55
+ const list = new aws.s3.Bucket(storage, 'todo-list', {
56
56
  bucket: 'your-bucket-name'
57
57
  })
58
58
 
59
59
  const items = new Stack(app, 'items')
60
- const todo = new aws.s3.BucketObject(items, 'item', {
60
+ const todo = new aws.s3.BucketObject(items, 'todo-item', {
61
61
  bucket: list.bucket,
62
62
  key: 'item-1',
63
63
  content: JSON.stringify({
package/dist/index.d.ts CHANGED
@@ -42,7 +42,7 @@ declare const interpolate: (literals: TemplateStringsArray, ...placeholders: Inp
42
42
  type URN = `urn:${string}`;
43
43
 
44
44
  declare const nodeMetaSymbol: unique symbol;
45
- type Node<T extends Tag = Tag, I extends State = State, O extends State = any, C extends Config = Config> = {
45
+ type Node<T extends Tag = Tag, I extends State = State, O extends State = State, C extends Config = Config> = {
46
46
  readonly [nodeMetaSymbol]: Meta<T, I, O, C>;
47
47
  readonly urn: URN;
48
48
  } & O;
@@ -187,6 +187,11 @@ type DeleteProps<T = State> = {
187
187
  state: T;
188
188
  idempotantToken?: string;
189
189
  };
190
+ type PlanProps<T = State> = {
191
+ type: string;
192
+ priorState: T;
193
+ proposedState: T;
194
+ };
190
195
  type GetProps<T = State> = {
191
196
  type: string;
192
197
  state: T;
@@ -210,12 +215,60 @@ interface Provider {
210
215
  state: State;
211
216
  }>;
212
217
  deleteResource(props: DeleteProps): Promise<void>;
218
+ planResourceChange?(props: PlanProps): Promise<{
219
+ version: number;
220
+ state: State;
221
+ requiresReplacement: boolean;
222
+ }>;
213
223
  getData?(props: GetDataProps): Promise<{
214
224
  state: State;
215
225
  }>;
216
226
  destroy?(): Promise<void>;
217
227
  }
218
228
 
229
+ type ResourceEvent = {
230
+ urn: URN;
231
+ type: string;
232
+ };
233
+ type BeforeResourceCreateEvent = ResourceEvent & {
234
+ resource: Resource;
235
+ newInput: State;
236
+ };
237
+ type AfterResourceCreateEvent = ResourceEvent & {
238
+ resource: Resource;
239
+ newInput: State;
240
+ newOutput: State;
241
+ };
242
+ type BeforeResourceUpdateEvent = ResourceEvent & {
243
+ resource: Resource;
244
+ oldInput: State;
245
+ newInput: State;
246
+ oldOutput: State;
247
+ };
248
+ type AfterResourceUpdateEvent = ResourceEvent & {
249
+ resource: Resource;
250
+ oldInput: State;
251
+ newInput: State;
252
+ oldOutput: State;
253
+ newOutput: State;
254
+ };
255
+ type BeforeResourceDeleteEvent = ResourceEvent & {
256
+ oldInput: State;
257
+ oldOutput: State;
258
+ };
259
+ type AfterResourceDeleteEvent = ResourceEvent & {
260
+ oldInput: State;
261
+ oldOutput: State;
262
+ };
263
+ type Hooks = {
264
+ beforeResourceCreate?: (event: BeforeResourceCreateEvent) => Promise<void> | void;
265
+ beforeResourceUpdate?: (event: BeforeResourceUpdateEvent) => Promise<void> | void;
266
+ beforeResourceDelete?: (event: BeforeResourceDeleteEvent) => Promise<void> | void;
267
+ afterResourceCreate?: (event: AfterResourceCreateEvent) => Promise<void> | void;
268
+ afterResourceUpdate?: (event: AfterResourceUpdateEvent) => Promise<void> | void;
269
+ afterResourceDelete?: (event: AfterResourceDeleteEvent) => Promise<void> | void;
270
+ };
271
+
219
272
  type ProcedureOptions = {
220
273
  filters?: string[];
221
274
  idempotentToken?: UUID;
@@ -227,6 +280,7 @@ type WorkSpaceOptions = {
227
280
  state: StateBackend;
228
281
  lock: LockBackend;
229
282
  };
283
+ hooks?: Hooks;
230
284
  };
231
285
  declare class WorkSpace {
232
286
  protected props: WorkSpaceOptions;
@@ -356,7 +410,11 @@ type CustomResourceProvider = Partial<{
356
410
  createResource?(props: Omit<CreateProps, 'type'>): Promise<State>;
357
411
  deleteResource?(props: Omit<DeleteProps, 'type'>): Promise<void>;
358
412
  getData?(props: Omit<GetDataProps, 'type'>): Promise<State>;
413
+ planResourceChange?(props: Omit<PlanProps, 'type'>): Promise<{
414
+ state: State;
415
+ requiresReplacement: boolean;
416
+ }>;
359
417
  }>;
360
418
  declare const createCustomProvider: (providerId: string, resourceProviders: Record<string, CustomResourceProvider>) => Provider;
361
419
 
362
- export { App, AppError, type Config, type CreateProps, type CustomResourceProvider, type DataSource, type DataSourceFunction, type DataSourceMeta, type DeleteProps, DynamoLockBackend, FileLockBackend, FileStateBackend, Future, type GetDataProps, type GetProps, Group, type Input, type LockBackend, MemoryLockBackend, MemoryStateBackend, type Meta, type Node, type OptionalInput, type OptionalOutput, Output, type ProcedureOptions, type Provider, type Resource, ResourceAlreadyExists, type ResourceClass, type ResourceConfig, ResourceError, type ResourceMeta, ResourceNotFound, S3StateBackend, Stack, type State, type StateBackend, type Tag, type URN, type UpdateProps, WorkSpace, type WorkSpaceOptions, createCustomProvider, createCustomResourceClass, createDebugger, createMeta, deferredOutput, enableDebug, findInputDeps, getMeta, isDataSource, isNode, isResource, nodeMetaSymbol, output, resolveInputs };
420
+ export { App, AppError, type Config, type CreateProps, type CustomResourceProvider, type DataSource, type DataSourceFunction, type DataSourceMeta, type DeleteProps, DynamoLockBackend, FileLockBackend, FileStateBackend, Future, type GetDataProps, type GetProps, Group, type Input, type LockBackend, MemoryLockBackend, MemoryStateBackend, type Meta, type Node, type OptionalInput, type OptionalOutput, Output, type PlanProps, type ProcedureOptions, type Provider, type Resource, ResourceAlreadyExists, type ResourceClass, type ResourceConfig, ResourceError, type ResourceMeta, ResourceNotFound, S3StateBackend, Stack, type State, type StateBackend, type Tag, type URN, type UpdateProps, WorkSpace, type WorkSpaceOptions, createCustomProvider, createCustomResourceClass, createDebugger, createMeta, deferredOutput, enableDebug, findInputDeps, getMeta, isDataSource, isNode, isResource, nodeMetaSymbol, output, resolveInputs };
package/dist/index.js CHANGED
@@ -610,11 +610,23 @@ var deleteResource = async (appToken, urn, state, opt) => {
610
610
  const idempotantToken = createIdempotantToken(appToken, urn, "delete");
611
611
  const provider = findProvider(opt.providers, state.provider);
612
612
  try {
613
+ await opt.hooks?.beforeResourceDelete?.({
614
+ urn,
615
+ type: state.type,
616
+ oldInput: state.input,
617
+ oldOutput: state.output
618
+ });
613
619
  await provider.deleteResource({
614
620
  type: state.type,
615
621
  state: state.output,
616
622
  idempotantToken
617
623
  });
624
+ await opt.hooks?.afterResourceDelete?.({
625
+ urn,
626
+ type: state.type,
627
+ oldInput: state.input,
628
+ oldOutput: state.output
629
+ });
618
630
  } catch (error) {
619
631
  if (error instanceof ResourceNotFound) {
620
632
  debug(state.type, "already deleted");
@@ -699,11 +711,24 @@ var createResource = async (resource, appToken, input, opt) => {
699
711
  debug2(input);
700
712
  let result;
701
713
  try {
714
+ await opt.hooks?.beforeResourceCreate?.({
715
+ urn: resource.urn,
716
+ type: meta.type,
717
+ resource,
718
+ newInput: input
719
+ });
702
720
  result = await provider.createResource({
703
721
  type: meta.type,
704
722
  state: input,
705
723
  idempotantToken
706
724
  });
725
+ await opt.hooks?.afterResourceCreate?.({
726
+ urn: resource.urn,
727
+ type: meta.type,
728
+ resource,
729
+ newInput: input,
730
+ newOutput: result.state
731
+ });
707
732
  } catch (error) {
708
733
  throw ResourceError.wrap(meta.urn, meta.type, "create", error);
709
734
  }
@@ -774,7 +799,7 @@ var importResource = async (resource, input, opt) => {
774
799
 
775
800
  // src/workspace/procedure/replace-resource.ts
776
801
  var debug5 = createDebugger("Replace");
777
- var replaceResource = async (resource, appToken, priorState, proposedState, opt) => {
802
+ var replaceResource = async (resource, appToken, priorInputState, priorOutputState, proposedState, opt) => {
778
803
  const meta = getMeta(resource);
779
804
  const urn = meta.urn;
780
805
  const type = meta.type;
@@ -786,11 +811,23 @@ var replaceResource = async (resource, appToken, priorState, proposedState, opt)
786
811
  debug5("retain", type);
787
812
  } else {
788
813
  try {
814
+ await opt.hooks?.beforeResourceDelete?.({
815
+ urn,
816
+ type,
817
+ oldInput: priorInputState,
818
+ oldOutput: priorOutputState
819
+ });
789
820
  await provider.deleteResource({
790
821
  type,
791
- state: priorState,
822
+ state: priorOutputState,
792
823
  idempotantToken
793
824
  });
825
+ await opt.hooks?.afterResourceDelete?.({
826
+ urn,
827
+ type,
828
+ oldInput: priorInputState,
829
+ oldOutput: priorOutputState
830
+ });
794
831
  } catch (error) {
795
832
  if (error instanceof ResourceNotFound) {
796
833
  debug5(type, "already deleted");
@@ -801,11 +838,24 @@ var replaceResource = async (resource, appToken, priorState, proposedState, opt)
801
838
  }
802
839
  let result;
803
840
  try {
841
+ await opt.hooks?.beforeResourceCreate?.({
842
+ urn,
843
+ type,
844
+ resource,
845
+ newInput: proposedState
846
+ });
804
847
  result = await provider.createResource({
805
848
  type,
806
849
  state: proposedState,
807
850
  idempotantToken
808
851
  });
852
+ await opt.hooks?.afterResourceCreate?.({
853
+ urn,
854
+ type,
855
+ resource,
856
+ newInput: proposedState,
857
+ newOutput: result.state
858
+ });
809
859
  } catch (error) {
810
860
  throw ResourceError.wrap(urn, type, "replace", error);
811
861
  }
@@ -817,7 +867,7 @@ var replaceResource = async (resource, appToken, priorState, proposedState, opt)
817
867
 
818
868
  // src/workspace/procedure/update-resource.ts
819
869
  var debug6 = createDebugger("Update");
820
- var updateResource = async (resource, appToken, priorState, proposedState, opt) => {
870
+ var updateResource = async (resource, appToken, priorInputState, priorOutputState, proposedState, opt) => {
821
871
  const meta = getMeta(resource);
822
872
  const provider = findProvider(opt.providers, meta.provider);
823
873
  const idempotantToken = createIdempotantToken(appToken, meta.urn, "update");
@@ -825,12 +875,29 @@ var updateResource = async (resource, appToken, priorState, proposedState, opt)
825
875
  debug6(meta.type);
826
876
  debug6(proposedState);
827
877
  try {
878
+ await opt.hooks?.beforeResourceUpdate?.({
879
+ urn: resource.urn,
880
+ type: meta.type,
881
+ resource,
882
+ newInput: proposedState,
883
+ oldInput: priorInputState,
884
+ oldOutput: priorOutputState
885
+ });
828
886
  result = await provider.updateResource({
829
887
  type: meta.type,
830
- priorState,
888
+ priorState: priorOutputState,
831
889
  proposedState,
832
890
  idempotantToken
833
891
  });
892
+ await opt.hooks?.afterResourceUpdate?.({
893
+ urn: resource.urn,
894
+ type: meta.type,
895
+ resource,
896
+ newInput: proposedState,
897
+ oldInput: priorInputState,
898
+ newOutput: result.state,
899
+ oldOutput: priorOutputState
900
+ });
834
901
  } catch (error) {
835
902
  throw ResourceError.wrap(meta.urn, meta.type, "update", error);
836
903
  }
@@ -842,6 +909,89 @@ var updateResource = async (resource, appToken, priorState, proposedState, opt)
842
909
 
843
910
  // src/workspace/procedure/deploy-app.ts
844
911
  var debug7 = createDebugger("Deploy App");
912
+ var findDependencyPaths = (value, dependencyUrn, path = []) => {
913
+ const paths = [];
914
+ const visit = (current, currentPath) => {
915
+ if (current instanceof Output) {
916
+ for (const dep of current.dependencies) {
917
+ if (dep.urn === dependencyUrn) {
918
+ paths.push(currentPath);
919
+ return;
920
+ }
921
+ }
922
+ return;
923
+ }
924
+ if (Array.isArray(current)) {
925
+ current.forEach((item, index) => {
926
+ visit(item, [...currentPath, index]);
927
+ });
928
+ return;
929
+ }
930
+ if (current && typeof current === "object") {
931
+ for (const [key, item] of Object.entries(current)) {
932
+ visit(item, [...currentPath, key]);
933
+ }
934
+ }
935
+ };
936
+ visit(value, path);
937
+ return paths;
938
+ };
939
+ var cloneState = (value) => JSON.parse(JSON.stringify(value));
940
+ var removeAtPath = (target, path) => {
941
+ if (path.length === 0) return;
942
+ let parent = target;
943
+ for (let i = 0; i < path.length - 1; i++) {
944
+ if (parent == null) return;
945
+ parent = parent[path[i]];
946
+ }
947
+ const last = path[path.length - 1];
948
+ if (Array.isArray(parent) && typeof last === "number") {
949
+ if (last >= 0 && last < parent.length) {
950
+ parent.splice(last, 1);
951
+ }
952
+ return;
953
+ }
954
+ if (parent && typeof parent === "object") {
955
+ delete parent[last];
956
+ }
957
+ };
958
+ var stripDependencyInputs = (input, metaInput, dependencyUrn) => {
959
+ const paths = findDependencyPaths(metaInput, dependencyUrn);
960
+ if (paths.length === 0) {
961
+ return input;
962
+ }
963
+ const detached = cloneState(input);
964
+ const sortedPaths = [...paths].sort((a, b) => {
965
+ if (a.length !== b.length) return b.length - a.length;
966
+ const aLast = a[a.length - 1];
967
+ const bLast = b[b.length - 1];
968
+ if (typeof aLast === "number" && typeof bLast === "number") {
969
+ return bLast - aLast;
970
+ }
971
+ return 0;
972
+ });
973
+ for (const path of sortedPaths) {
974
+ removeAtPath(detached, path);
975
+ }
976
+ return detached;
977
+ };
978
+ var allowsDependentReplace = (replaceOnChanges, dependencyPaths) => {
979
+ if (!replaceOnChanges || replaceOnChanges.length === 0) {
980
+ return false;
981
+ }
982
+ for (const path of dependencyPaths) {
983
+ const base = typeof path[0] === "string" ? path[0] : void 0;
984
+ if (!base) {
985
+ continue;
986
+ }
987
+ for (const replacePath of replaceOnChanges) {
988
+ if (replacePath === base || replacePath.startsWith(`${base}.`) || replacePath.startsWith(`${base}[`) || replacePath.startsWith(`${base}.*`)) {
989
+ return true;
990
+ }
991
+ }
992
+ }
993
+ return false;
994
+ };
845
995
  var deployApp = async (app, opt) => {
846
996
  debug7(app.name, "start");
847
997
  const latestState = await opt.backend.state.get(app.urn);
@@ -864,6 +1014,20 @@ var deployApp = async (app, opt) => {
864
1014
  stacks = app.stacks.filter((stack) => opt.filters.includes(stack.name));
865
1015
  filteredOutStacks = app.stacks.filter((stack) => !opt.filters.includes(stack.name));
866
1016
  }
1017
+ const nodeByUrn = /* @__PURE__ */ new Map();
1018
+ const stackStates = /* @__PURE__ */ new Map();
1019
+ const plannedDependents = /* @__PURE__ */ new Set();
1020
+ const forcedUpdateDependents = /* @__PURE__ */ new Set();
1021
+ for (const stack of stacks) {
1022
+ const stackState = appState.stacks[stack.urn] = appState.stacks[stack.urn] ?? {
1023
+ name: stack.name,
1024
+ nodes: {}
1025
+ };
1026
+ stackStates.set(stack.urn, stackState);
1027
+ for (const node of stack.nodes) {
1028
+ nodeByUrn.set(getMeta(node).urn, node);
1029
+ }
1030
+ }
867
1031
  const queue = createConcurrencyQueue(opt.concurrency ?? 10);
868
1032
  const graph = new DependencyGraph();
869
1033
  const allNodes = {};
@@ -912,10 +1076,7 @@ var deployApp = async (app, opt) => {
912
1076
  }
913
1077
  }
914
1078
  for (const stack of stacks) {
915
- const stackState = appState.stacks[stack.urn] = appState.stacks[stack.urn] ?? {
916
- name: stack.name,
917
- nodes: {}
918
- };
1079
+ const stackState = stackStates.get(stack.urn);
919
1080
  for (const [urn, nodeState] of entries(stackState.nodes)) {
920
1081
  const resource = stack.nodes.find((r) => getMeta(r).urn === urn);
921
1082
  if (!resource) {
@@ -986,6 +1147,7 @@ var deployApp = async (app, opt) => {
986
1147
  const newResourceState = await updateResource(
987
1148
  node,
988
1149
  appState.idempotentToken,
1150
+ importedState.input,
989
1151
  importedState.output,
990
1152
  input,
991
1153
  opt
@@ -1013,10 +1175,97 @@ var deployApp = async (app, opt) => {
1013
1175
  !compareState(nodeState.input, input)
1014
1176
  ) {
1015
1177
  let newResourceState;
1016
- if (requiresReplacement(nodeState.input, input, meta2.config?.replaceOnChanges ?? [])) {
1178
+ const ignoreReplace = forcedUpdateDependents.has(meta2.urn);
1179
+ if (!ignoreReplace && requiresReplacement(nodeState.input, input, meta2.config?.replaceOnChanges ?? [])) {
1180
+ for (const [dependentUrn, dependentNode] of nodeByUrn.entries()) {
1181
+ if (!isResource(dependentNode)) {
1182
+ continue;
1183
+ }
1184
+ const dependentMeta = getMeta(dependentNode);
1185
+ if (!dependentMeta.dependencies.has(meta2.urn)) {
1186
+ continue;
1187
+ }
1188
+ if (plannedDependents.has(dependentUrn)) {
1189
+ continue;
1190
+ }
1191
+ const dependentStackState = stackStates.get(dependentMeta.stack.urn);
1192
+ const dependentState = dependentStackState?.nodes[dependentUrn];
1193
+ if (!dependentStackState || !dependentState) {
1194
+ continue;
1195
+ }
1196
+ const dependencyPaths = findDependencyPaths(dependentMeta.input, meta2.urn);
1197
+ if (dependencyPaths.length === 0) {
1198
+ continue;
1199
+ }
1200
+ const detachedInput = stripDependencyInputs(
1201
+ dependentState.input,
1202
+ dependentMeta.input,
1203
+ meta2.urn
1204
+ );
1205
+ if (compareState(dependentState.input, detachedInput)) {
1206
+ continue;
1207
+ }
1208
+ plannedDependents.add(dependentUrn);
1209
+ let dependentRequiresReplacement = false;
1210
+ const dependentProvider = findProvider(opt.providers, dependentMeta.provider);
1211
+ if (dependentProvider.planResourceChange) {
1212
+ try {
1213
+ const dependentPlan = await dependentProvider.planResourceChange({
1214
+ type: dependentMeta.type,
1215
+ priorState: dependentState.output,
1216
+ proposedState: detachedInput
1217
+ });
1218
+ dependentRequiresReplacement = dependentPlan.requiresReplacement;
1219
+ } catch (error) {
1220
+ throw ResourceError.wrap(
1221
+ dependentMeta.urn,
1222
+ dependentMeta.type,
1223
+ "update",
1224
+ error
1225
+ );
1226
+ }
1227
+ }
1228
+ if (dependentRequiresReplacement) {
1229
+ if (!allowsDependentReplace(
1230
+ dependentMeta.config?.replaceOnChanges,
1231
+ dependencyPaths
1232
+ )) {
1233
+ throw ResourceError.wrap(
1234
+ dependentMeta.urn,
1235
+ dependentMeta.type,
1236
+ "update",
1237
+ new Error(
1238
+ `Replacing ${meta2.urn} requires ${dependentMeta.urn} to set replaceOnChanges for its dependency fields.`
1239
+ )
1240
+ );
1241
+ }
1242
+ await deleteResource(
1243
+ appState.idempotentToken,
1244
+ dependentUrn,
1245
+ dependentState,
1246
+ opt
1247
+ );
1248
+ delete dependentStackState.nodes[dependentUrn];
1249
+ } else {
1250
+ const updated = await updateResource(
1251
+ dependentNode,
1252
+ appState.idempotentToken,
1253
+ dependentState.input,
1254
+ dependentState.output,
1255
+ detachedInput,
1256
+ opt
1257
+ );
1258
+ Object.assign(dependentState, {
1259
+ input: detachedInput,
1260
+ ...updated
1261
+ });
1262
+ forcedUpdateDependents.add(dependentUrn);
1263
+ }
1264
+ }
1017
1265
  newResourceState = await replaceResource(
1018
1266
  node,
1019
1267
  appState.idempotentToken,
1268
+ nodeState.input,
1020
1269
  nodeState.output,
1021
1270
  input,
1022
1271
  opt
@@ -1025,10 +1274,14 @@ var deployApp = async (app, opt) => {
1025
1274
  newResourceState = await updateResource(
1026
1275
  node,
1027
1276
  appState.idempotentToken,
1277
+ nodeState.input,
1028
1278
  nodeState.output,
1029
1279
  input,
1030
1280
  opt
1031
1281
  );
1282
+ if (ignoreReplace) {
1283
+ forcedUpdateDependents.delete(meta2.urn);
1284
+ }
1032
1285
  }
1033
1286
  Object.assign(nodeState, {
1034
1287
  input,
@@ -1514,6 +1767,21 @@ var createCustomProvider = (providerId, resourceProviders) => {
1514
1767
  async deleteResource({ type, ...props }) {
1515
1768
  await getProvider(type).deleteResource?.(props);
1516
1769
  },
1770
+ async planResourceChange({ type, ...props }) {
1771
+ const provider = getProvider(type);
1772
+ if (!provider.planResourceChange) {
1773
+ return {
1774
+ version,
1775
+ state: props.proposedState,
1776
+ requiresReplacement: false
1777
+ };
1778
+ }
1779
+ const result = await provider.planResourceChange(props);
1780
+ return {
1781
+ version,
1782
+ ...result
1783
+ };
1784
+ },
1517
1785
  async getData({ type, ...props }) {
1518
1786
  return {
1519
1787
  version,
package/package.json CHANGED
@@ -1,7 +1,14 @@
1
1
  {
2
2
  "name": "@terraforge/core",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "type": "module",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/terraforge-js/terraforge.git"
8
+ },
9
+ "bugs": {
10
+ "url": "https://github.com/terraforge-js/terraforge/issues"
11
+ },
5
12
  "module": "./dist/index.js",
6
13
  "types": "./dist/index.d.ts",
7
14
  "exports": {
@@ -12,7 +19,7 @@
12
19
  },
13
20
  "scripts": {
14
21
  "build": "tsup src/index.ts --format esm --dts --clean --out-dir ./dist",
15
- "prepublishOnly": "bun run build",
22
+ "prepublishOnly": "if bun run test; then bun run build; else exit; fi",
16
23
  "test": "bun test"
17
24
  },
18
25
  "dependencies": {