@terraforge/core 0.0.5 → 0.0.7

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;
@@ -58,12 +58,13 @@ declare const isDataSource: (obj: object) => obj is DataSource;
58
58
  type ResourceConfig = Config & {
59
59
  /** Import an existing resource instead of creating a new resource. */
60
60
  import?: string;
61
- /** If true the resource will be retained in the backing cloud provider during a Pulumi delete operation. */
61
+ /** If true the resource will be retained in the backing cloud provider during a delete operation. */
62
62
  retainOnDelete?: boolean;
63
- /** Override the default create-after-delete behavior when replacing a resource. */
64
63
  /** If set, the provider’s Delete method will not be called for this resource if the specified resource is being deleted as well. */
65
64
  /** Declare that changes to certain properties should be treated as forcing a replacement. */
66
65
  replaceOnChanges?: string[];
66
+ /** If true, create the replacement before deleting the existing resource. */
67
+ createBeforeReplace?: boolean;
67
68
  };
68
69
  type ResourceMeta<I extends State = State, O extends State = State> = Meta<'resource', I, O, ResourceConfig>;
69
70
  type Resource<I extends State = State, O extends State = State> = O & {
@@ -187,6 +188,11 @@ type DeleteProps<T = State> = {
187
188
  state: T;
188
189
  idempotantToken?: string;
189
190
  };
191
+ type PlanProps<T = State> = {
192
+ type: string;
193
+ priorState: T;
194
+ proposedState: T;
195
+ };
190
196
  type GetProps<T = State> = {
191
197
  type: string;
192
198
  state: T;
@@ -210,12 +216,60 @@ interface Provider {
210
216
  state: State;
211
217
  }>;
212
218
  deleteResource(props: DeleteProps): Promise<void>;
219
+ planResourceChange?(props: PlanProps): Promise<{
220
+ version: number;
221
+ state: State;
222
+ requiresReplacement: boolean;
223
+ }>;
213
224
  getData?(props: GetDataProps): Promise<{
214
225
  state: State;
215
226
  }>;
216
227
  destroy?(): Promise<void>;
217
228
  }
218
229
 
230
+ type ResourceEvent = {
231
+ urn: URN;
232
+ type: string;
233
+ };
234
+ type BeforeResourceCreateEvent = ResourceEvent & {
235
+ resource: Resource;
236
+ newInput: State;
237
+ };
238
+ type AfterResourceCreateEvent = ResourceEvent & {
239
+ resource: Resource;
240
+ newInput: State;
241
+ newOutput: State;
242
+ };
243
+ type BeforeResourceUpdateEvent = ResourceEvent & {
244
+ resource: Resource;
245
+ oldInput: State;
246
+ newInput: State;
247
+ oldOutput: State;
248
+ };
249
+ type AfterResourceUpdateEvent = ResourceEvent & {
250
+ resource: Resource;
251
+ oldInput: State;
252
+ newInput: State;
253
+ oldOutput: State;
254
+ newOutput: State;
255
+ };
256
+ type BeforeResourceDeleteEvent = ResourceEvent & {
257
+ oldInput: State;
258
+ oldOutput: State;
259
+ };
260
+ type AfterResourceDeleteEvent = ResourceEvent & {
261
+ oldInput: State;
262
+ oldOutput: State;
263
+ };
264
+ type Hooks = {
265
+ beforeResourceCreate?: (event: BeforeResourceCreateEvent) => Promise<void> | void;
266
+ beforeResourceUpdate?: (event: BeforeResourceUpdateEvent) => Promise<void> | void;
267
+ beforeResourceDelete?: (event: BeforeResourceDeleteEvent) => Promise<void> | void;
268
+ afterResourceCreate?: (event: AfterResourceCreateEvent) => Promise<void> | void;
269
+ afterResourceUpdate?: (event: AfterResourceUpdateEvent) => Promise<void> | void;
270
+ afterResourceDelete?: (event: AfterResourceDeleteEvent) => Promise<void> | void;
271
+ };
272
+
219
273
  type ProcedureOptions = {
220
274
  filters?: string[];
221
275
  idempotentToken?: UUID;
@@ -227,6 +281,7 @@ type WorkSpaceOptions = {
227
281
  state: StateBackend;
228
282
  lock: LockBackend;
229
283
  };
284
+ hooks?: Hooks;
230
285
  };
231
286
  declare class WorkSpace {
232
287
  protected props: WorkSpaceOptions;
@@ -356,7 +411,11 @@ type CustomResourceProvider = Partial<{
356
411
  createResource?(props: Omit<CreateProps, 'type'>): Promise<State>;
357
412
  deleteResource?(props: Omit<DeleteProps, 'type'>): Promise<void>;
358
413
  getData?(props: Omit<GetDataProps, 'type'>): Promise<State>;
414
+ planResourceChange?(props: Omit<PlanProps, 'type'>): Promise<{
415
+ state: State;
416
+ requiresReplacement: boolean;
417
+ }>;
359
418
  }>;
360
419
  declare const createCustomProvider: (providerId: string, resourceProviders: Record<string, CustomResourceProvider>) => Provider;
361
420
 
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 };
421
+ 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
@@ -420,6 +420,9 @@ var DependencyGraph = class {
420
420
  this.callbacks.set(urn, callback);
421
421
  this.graph.mergeNode(urn);
422
422
  for (const dep of deps) {
423
+ if (!dep) {
424
+ throw new Error(`Resource ${urn} has an undefined dependency.`);
425
+ }
423
426
  if (willCreateCycle(this.graph, dep, urn)) {
424
427
  throw new Error(`There is a circular dependency between ${urn} -> ${dep}`);
425
428
  }
@@ -610,11 +613,23 @@ var deleteResource = async (appToken, urn, state, opt) => {
610
613
  const idempotantToken = createIdempotantToken(appToken, urn, "delete");
611
614
  const provider = findProvider(opt.providers, state.provider);
612
615
  try {
616
+ await opt.hooks?.beforeResourceDelete?.({
617
+ urn,
618
+ type: state.type,
619
+ oldInput: state.input,
620
+ oldOutput: state.output
621
+ });
613
622
  await provider.deleteResource({
614
623
  type: state.type,
615
624
  state: state.output,
616
625
  idempotantToken
617
626
  });
627
+ await opt.hooks?.afterResourceDelete?.({
628
+ urn,
629
+ type: state.type,
630
+ oldInput: state.input,
631
+ oldOutput: state.output
632
+ });
618
633
  } catch (error) {
619
634
  if (error instanceof ResourceNotFound) {
620
635
  debug(state.type, "already deleted");
@@ -699,11 +714,24 @@ var createResource = async (resource, appToken, input, opt) => {
699
714
  debug2(input);
700
715
  let result;
701
716
  try {
717
+ await opt.hooks?.beforeResourceCreate?.({
718
+ urn: resource.urn,
719
+ type: meta.type,
720
+ resource,
721
+ newInput: input
722
+ });
702
723
  result = await provider.createResource({
703
724
  type: meta.type,
704
725
  state: input,
705
726
  idempotantToken
706
727
  });
728
+ await opt.hooks?.afterResourceCreate?.({
729
+ urn: resource.urn,
730
+ type: meta.type,
731
+ resource,
732
+ newInput: input,
733
+ newOutput: result.state
734
+ });
707
735
  } catch (error) {
708
736
  throw ResourceError.wrap(meta.urn, meta.type, "create", error);
709
737
  }
@@ -774,7 +802,7 @@ var importResource = async (resource, input, opt) => {
774
802
 
775
803
  // src/workspace/procedure/replace-resource.ts
776
804
  var debug5 = createDebugger("Replace");
777
- var replaceResource = async (resource, appToken, priorState, proposedState, opt) => {
805
+ var replaceResource = async (resource, appToken, priorInputState, priorOutputState, proposedState, opt) => {
778
806
  const meta = getMeta(resource);
779
807
  const urn = meta.urn;
780
808
  const type = meta.type;
@@ -786,11 +814,23 @@ var replaceResource = async (resource, appToken, priorState, proposedState, opt)
786
814
  debug5("retain", type);
787
815
  } else {
788
816
  try {
817
+ await opt.hooks?.beforeResourceDelete?.({
818
+ urn,
819
+ type,
820
+ oldInput: priorInputState,
821
+ oldOutput: priorOutputState
822
+ });
789
823
  await provider.deleteResource({
790
824
  type,
791
- state: priorState,
825
+ state: priorOutputState,
792
826
  idempotantToken
793
827
  });
828
+ await opt.hooks?.afterResourceDelete?.({
829
+ urn,
830
+ type,
831
+ oldInput: priorInputState,
832
+ oldOutput: priorOutputState
833
+ });
794
834
  } catch (error) {
795
835
  if (error instanceof ResourceNotFound) {
796
836
  debug5(type, "already deleted");
@@ -801,11 +841,24 @@ var replaceResource = async (resource, appToken, priorState, proposedState, opt)
801
841
  }
802
842
  let result;
803
843
  try {
844
+ await opt.hooks?.beforeResourceCreate?.({
845
+ urn,
846
+ type,
847
+ resource,
848
+ newInput: proposedState
849
+ });
804
850
  result = await provider.createResource({
805
851
  type,
806
852
  state: proposedState,
807
853
  idempotantToken
808
854
  });
855
+ await opt.hooks?.afterResourceCreate?.({
856
+ urn,
857
+ type,
858
+ resource,
859
+ newInput: proposedState,
860
+ newOutput: result.state
861
+ });
809
862
  } catch (error) {
810
863
  throw ResourceError.wrap(urn, type, "replace", error);
811
864
  }
@@ -817,7 +870,7 @@ var replaceResource = async (resource, appToken, priorState, proposedState, opt)
817
870
 
818
871
  // src/workspace/procedure/update-resource.ts
819
872
  var debug6 = createDebugger("Update");
820
- var updateResource = async (resource, appToken, priorState, proposedState, opt) => {
873
+ var updateResource = async (resource, appToken, priorInputState, priorOutputState, proposedState, opt) => {
821
874
  const meta = getMeta(resource);
822
875
  const provider = findProvider(opt.providers, meta.provider);
823
876
  const idempotantToken = createIdempotantToken(appToken, meta.urn, "update");
@@ -825,12 +878,29 @@ var updateResource = async (resource, appToken, priorState, proposedState, opt)
825
878
  debug6(meta.type);
826
879
  debug6(proposedState);
827
880
  try {
881
+ await opt.hooks?.beforeResourceUpdate?.({
882
+ urn: resource.urn,
883
+ type: meta.type,
884
+ resource,
885
+ newInput: proposedState,
886
+ oldInput: priorInputState,
887
+ oldOutput: priorOutputState
888
+ });
828
889
  result = await provider.updateResource({
829
890
  type: meta.type,
830
- priorState,
891
+ priorState: priorOutputState,
831
892
  proposedState,
832
893
  idempotantToken
833
894
  });
895
+ await opt.hooks?.afterResourceUpdate?.({
896
+ urn: resource.urn,
897
+ type: meta.type,
898
+ resource,
899
+ newInput: proposedState,
900
+ oldInput: priorInputState,
901
+ newOutput: result.state,
902
+ oldOutput: priorOutputState
903
+ });
834
904
  } catch (error) {
835
905
  throw ResourceError.wrap(meta.urn, meta.type, "update", error);
836
906
  }
@@ -842,6 +912,89 @@ var updateResource = async (resource, appToken, priorState, proposedState, opt)
842
912
 
843
913
  // src/workspace/procedure/deploy-app.ts
844
914
  var debug7 = createDebugger("Deploy App");
915
+ var findDependencyPaths = (value, dependencyUrn, path = []) => {
916
+ const paths = [];
917
+ const visit = (current, currentPath) => {
918
+ if (current instanceof Output) {
919
+ for (const dep of current.dependencies) {
920
+ if (dep.urn === dependencyUrn) {
921
+ paths.push(currentPath);
922
+ return;
923
+ }
924
+ }
925
+ return;
926
+ }
927
+ if (Array.isArray(current)) {
928
+ current.forEach((item, index) => {
929
+ visit(item, [...currentPath, index]);
930
+ });
931
+ return;
932
+ }
933
+ if (current && typeof current === "object") {
934
+ for (const [key, item] of Object.entries(current)) {
935
+ visit(item, [...currentPath, key]);
936
+ }
937
+ }
938
+ };
939
+ visit(value, path);
940
+ return paths;
941
+ };
942
+ var cloneState = (value) => JSON.parse(JSON.stringify(value));
943
+ var removeAtPath = (target, path) => {
944
+ if (path.length === 0) return;
945
+ let parent = target;
946
+ for (let i = 0; i < path.length - 1; i++) {
947
+ if (parent == null) return;
948
+ parent = parent[path[i]];
949
+ }
950
+ const last = path[path.length - 1];
951
+ if (Array.isArray(parent) && typeof last === "number") {
952
+ if (last >= 0 && last < parent.length) {
953
+ parent.splice(last, 1);
954
+ }
955
+ return;
956
+ }
957
+ if (parent && typeof parent === "object") {
958
+ delete parent[last];
959
+ }
960
+ };
961
+ var stripDependencyInputs = (input, metaInput, dependencyUrn) => {
962
+ const paths = findDependencyPaths(metaInput, dependencyUrn);
963
+ if (paths.length === 0) {
964
+ return input;
965
+ }
966
+ const detached = cloneState(input);
967
+ const sortedPaths = [...paths].sort((a, b) => {
968
+ if (a.length !== b.length) return b.length - a.length;
969
+ const aLast = a[a.length - 1];
970
+ const bLast = b[b.length - 1];
971
+ if (typeof aLast === "number" && typeof bLast === "number") {
972
+ return bLast - aLast;
973
+ }
974
+ return 0;
975
+ });
976
+ for (const path of sortedPaths) {
977
+ removeAtPath(detached, path);
978
+ }
979
+ return detached;
980
+ };
981
+ var allowsDependentReplace = (replaceOnChanges, dependencyPaths) => {
982
+ if (!replaceOnChanges || replaceOnChanges.length === 0) {
983
+ return false;
984
+ }
985
+ for (const path of dependencyPaths) {
986
+ const base = typeof path[0] === "string" ? path[0] : void 0;
987
+ if (!base) {
988
+ continue;
989
+ }
990
+ for (const replacePath of replaceOnChanges) {
991
+ if (replacePath === base || replacePath.startsWith(`${base}.`) || replacePath.startsWith(`${base}[`) || replacePath.startsWith(`${base}.*`)) {
992
+ return true;
993
+ }
994
+ }
995
+ }
996
+ return false;
997
+ };
845
998
  var deployApp = async (app, opt) => {
846
999
  debug7(app.name, "start");
847
1000
  const latestState = await opt.backend.state.get(app.urn);
@@ -864,8 +1017,23 @@ var deployApp = async (app, opt) => {
864
1017
  stacks = app.stacks.filter((stack) => opt.filters.includes(stack.name));
865
1018
  filteredOutStacks = app.stacks.filter((stack) => !opt.filters.includes(stack.name));
866
1019
  }
1020
+ const nodeByUrn = /* @__PURE__ */ new Map();
1021
+ const stackStates = /* @__PURE__ */ new Map();
1022
+ const plannedDependents = /* @__PURE__ */ new Set();
1023
+ const forcedUpdateDependents = /* @__PURE__ */ new Set();
1024
+ for (const stack of stacks) {
1025
+ const stackState = appState.stacks[stack.urn] = appState.stacks[stack.urn] ?? {
1026
+ name: stack.name,
1027
+ nodes: {}
1028
+ };
1029
+ stackStates.set(stack.urn, stackState);
1030
+ for (const node of stack.nodes) {
1031
+ nodeByUrn.set(getMeta(node).urn, node);
1032
+ }
1033
+ }
867
1034
  const queue = createConcurrencyQueue(opt.concurrency ?? 10);
868
1035
  const graph = new DependencyGraph();
1036
+ const replacementDeletes = /* @__PURE__ */ new Map();
869
1037
  const allNodes = {};
870
1038
  for (const stackState of Object.values(appState.stacks)) {
871
1039
  for (const [urn, nodeState] of entries(stackState.nodes)) {
@@ -912,10 +1080,7 @@ var deployApp = async (app, opt) => {
912
1080
  }
913
1081
  }
914
1082
  for (const stack of stacks) {
915
- const stackState = appState.stacks[stack.urn] = appState.stacks[stack.urn] ?? {
916
- name: stack.name,
917
- nodes: {}
918
- };
1083
+ const stackState = stackStates.get(stack.urn);
919
1084
  for (const [urn, nodeState] of entries(stackState.nodes)) {
920
1085
  const resource = stack.nodes.find((r) => getMeta(r).urn === urn);
921
1086
  if (!resource) {
@@ -986,6 +1151,7 @@ var deployApp = async (app, opt) => {
986
1151
  const newResourceState = await updateResource(
987
1152
  node,
988
1153
  appState.idempotentToken,
1154
+ importedState.input,
989
1155
  importedState.output,
990
1156
  input,
991
1157
  opt
@@ -1013,22 +1179,121 @@ var deployApp = async (app, opt) => {
1013
1179
  !compareState(nodeState.input, input)
1014
1180
  ) {
1015
1181
  let newResourceState;
1016
- if (requiresReplacement(nodeState.input, input, meta2.config?.replaceOnChanges ?? [])) {
1017
- newResourceState = await replaceResource(
1018
- node,
1019
- appState.idempotentToken,
1020
- nodeState.output,
1021
- input,
1022
- opt
1023
- );
1182
+ const ignoreReplace = forcedUpdateDependents.has(meta2.urn);
1183
+ if (!ignoreReplace && requiresReplacement(nodeState.input, input, meta2.config?.replaceOnChanges ?? [])) {
1184
+ if (meta2.config?.createBeforeReplace) {
1185
+ const priorState = { ...nodeState };
1186
+ newResourceState = await createResource(node, appState.idempotentToken, input, opt);
1187
+ if (!meta2.config?.retainOnDelete) {
1188
+ replacementDeletes.set(meta2.urn, priorState);
1189
+ }
1190
+ } else {
1191
+ for (const [dependentUrn, dependentNode] of nodeByUrn.entries()) {
1192
+ if (!isResource(dependentNode)) {
1193
+ continue;
1194
+ }
1195
+ const dependentMeta = getMeta(dependentNode);
1196
+ if (!dependentMeta.dependencies.has(meta2.urn)) {
1197
+ continue;
1198
+ }
1199
+ if (plannedDependents.has(dependentUrn)) {
1200
+ continue;
1201
+ }
1202
+ const dependentStackState = stackStates.get(dependentMeta.stack.urn);
1203
+ const dependentState = dependentStackState?.nodes[dependentUrn];
1204
+ if (!dependentStackState || !dependentState) {
1205
+ continue;
1206
+ }
1207
+ const dependencyPaths = findDependencyPaths(dependentMeta.input, meta2.urn);
1208
+ if (dependencyPaths.length === 0) {
1209
+ continue;
1210
+ }
1211
+ const detachedInput = stripDependencyInputs(
1212
+ dependentState.input,
1213
+ dependentMeta.input,
1214
+ meta2.urn
1215
+ );
1216
+ if (compareState(dependentState.input, detachedInput)) {
1217
+ continue;
1218
+ }
1219
+ plannedDependents.add(dependentUrn);
1220
+ let dependentRequiresReplacement = false;
1221
+ const dependentProvider = findProvider(opt.providers, dependentMeta.provider);
1222
+ if (dependentProvider.planResourceChange) {
1223
+ try {
1224
+ const dependentPlan = await dependentProvider.planResourceChange({
1225
+ type: dependentMeta.type,
1226
+ priorState: dependentState.output,
1227
+ proposedState: detachedInput
1228
+ });
1229
+ dependentRequiresReplacement = dependentPlan.requiresReplacement;
1230
+ } catch (error) {
1231
+ throw ResourceError.wrap(
1232
+ dependentMeta.urn,
1233
+ dependentMeta.type,
1234
+ "update",
1235
+ error
1236
+ );
1237
+ }
1238
+ }
1239
+ if (dependentRequiresReplacement) {
1240
+ if (!allowsDependentReplace(
1241
+ dependentMeta.config?.replaceOnChanges,
1242
+ dependencyPaths
1243
+ )) {
1244
+ throw ResourceError.wrap(
1245
+ dependentMeta.urn,
1246
+ dependentMeta.type,
1247
+ "update",
1248
+ new Error(
1249
+ `Replacing ${meta2.urn} requires ${dependentMeta.urn} to set replaceOnChanges for its dependency fields.`
1250
+ )
1251
+ );
1252
+ }
1253
+ await deleteResource(
1254
+ appState.idempotentToken,
1255
+ dependentUrn,
1256
+ dependentState,
1257
+ opt
1258
+ );
1259
+ delete dependentStackState.nodes[dependentUrn];
1260
+ } else {
1261
+ const updated = await updateResource(
1262
+ dependentNode,
1263
+ appState.idempotentToken,
1264
+ dependentState.input,
1265
+ dependentState.output,
1266
+ detachedInput,
1267
+ opt
1268
+ );
1269
+ Object.assign(dependentState, {
1270
+ input: detachedInput,
1271
+ ...updated
1272
+ });
1273
+ forcedUpdateDependents.add(dependentUrn);
1274
+ }
1275
+ }
1276
+ newResourceState = await replaceResource(
1277
+ node,
1278
+ appState.idempotentToken,
1279
+ nodeState.input,
1280
+ nodeState.output,
1281
+ input,
1282
+ opt
1283
+ );
1284
+ }
1024
1285
  } else {
1025
1286
  newResourceState = await updateResource(
1026
1287
  node,
1027
1288
  appState.idempotentToken,
1289
+ nodeState.input,
1028
1290
  nodeState.output,
1029
1291
  input,
1030
1292
  opt
1031
1293
  );
1294
+ if (ignoreReplace) {
1295
+ forcedUpdateDependents.delete(meta2.urn);
1296
+ }
1032
1297
  }
1033
1298
  Object.assign(nodeState, {
1034
1299
  input,
@@ -1047,6 +1312,19 @@ var deployApp = async (app, opt) => {
1047
1312
  }
1048
1313
  }
1049
1314
  const errors = await graph.run();
1315
+ if (errors.length === 0 && replacementDeletes.size > 0) {
1316
+ for (const [urn, nodeState] of replacementDeletes.entries()) {
1317
+ try {
1318
+ await deleteResource(appState.idempotentToken, urn, nodeState, opt);
1319
+ } catch (error) {
1320
+ if (error instanceof Error) {
1321
+ errors.push(error);
1322
+ } else {
1323
+ errors.push(new Error(`${error}`));
1324
+ }
1325
+ }
1326
+ }
1327
+ }
1050
1328
  removeEmptyStackStates(appState);
1051
1329
  delete appState.idempotentToken;
1052
1330
  await opt.backend.state.update(app.urn, appState);
@@ -1514,6 +1792,21 @@ var createCustomProvider = (providerId, resourceProviders) => {
1514
1792
  async deleteResource({ type, ...props }) {
1515
1793
  await getProvider(type).deleteResource?.(props);
1516
1794
  },
1795
+ async planResourceChange({ type, ...props }) {
1796
+ const provider = getProvider(type);
1797
+ if (!provider.planResourceChange) {
1798
+ return {
1799
+ version,
1800
+ state: props.proposedState,
1801
+ requiresReplacement: false
1802
+ };
1803
+ }
1804
+ const result = await provider.planResourceChange(props);
1805
+ return {
1806
+ version,
1807
+ ...result
1808
+ };
1809
+ },
1517
1810
  async getData({ type, ...props }) {
1518
1811
  return {
1519
1812
  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.7",
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": {