sst 2.3.1 → 2.3.2
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/bootstrap.js +3 -1
- package/cli/colors.js +1 -1
- package/cli/commands/bind.js +3 -0
- package/cli/commands/dev.js +24 -2
- package/constructs/AppSyncApi.d.ts +34 -3
- package/constructs/AppSyncApi.js +8 -1
- package/constructs/SsrSite.d.ts +19 -8
- package/constructs/SsrSite.js +13 -0
- package/constructs/Stack.d.ts +1 -1
- package/constructs/Stack.js +3 -5
- package/package.json +1 -1
- package/project.d.ts +1 -0
- package/sst.mjs +20 -4
package/bootstrap.js
CHANGED
|
@@ -208,7 +208,6 @@ async function bootstrapCDK() {
|
|
|
208
208
|
const identity = await useSTSIdentity();
|
|
209
209
|
const credentials = await useAWSCredentials();
|
|
210
210
|
const { region, profile, cdk } = useProject().config;
|
|
211
|
-
cdk || {};
|
|
212
211
|
await new Promise((resolve, reject) => {
|
|
213
212
|
const proc = spawn([
|
|
214
213
|
"npx",
|
|
@@ -228,6 +227,9 @@ async function bootstrapCDK() {
|
|
|
228
227
|
...(cdk?.fileAssetsBucketName
|
|
229
228
|
? ["--toolkit-bucket-name", cdk.fileAssetsBucketName]
|
|
230
229
|
: []),
|
|
230
|
+
...(cdk?.customPermissionsBoundary
|
|
231
|
+
? ["--custom-permissions-boundary", cdk.customPermissionsBoundary]
|
|
232
|
+
: []),
|
|
231
233
|
].join(" "), {
|
|
232
234
|
env: {
|
|
233
235
|
...process.env,
|
package/cli/colors.js
CHANGED
package/cli/commands/bind.js
CHANGED
|
@@ -194,6 +194,9 @@ export const bind = (program) => program
|
|
|
194
194
|
stdio: "inherit",
|
|
195
195
|
shell: true,
|
|
196
196
|
});
|
|
197
|
+
p.on("exit", (code) => {
|
|
198
|
+
process.exit();
|
|
199
|
+
});
|
|
197
200
|
}
|
|
198
201
|
function areEnvsSame(envs1, envs2) {
|
|
199
202
|
return (Object.keys(envs1).length === Object.keys(envs2).length &&
|
package/cli/commands/dev.js
CHANGED
|
@@ -23,6 +23,7 @@ export const dev = (program) => program.command(["dev", "start"], "Work on your
|
|
|
23
23
|
const fs = await import("fs/promises");
|
|
24
24
|
const crypto = await import("crypto");
|
|
25
25
|
const { useFunctions } = await import("../../constructs/Function.js");
|
|
26
|
+
const { useSites } = await import("../../constructs/SsrSite.js");
|
|
26
27
|
const { usePothosBuilder } = await import("./plugins/pothos.js");
|
|
27
28
|
const { useKyselyTypeGenerator } = await import("./plugins/kysely.js");
|
|
28
29
|
const { useRDSWarmer } = await import("./plugins/warmer.js");
|
|
@@ -172,9 +173,29 @@ export const dev = (program) => program.command(["dev", "start"], "Work on your
|
|
|
172
173
|
component.clear();
|
|
173
174
|
component.unmount();
|
|
174
175
|
printDeploymentResults(assembly, results);
|
|
175
|
-
//
|
|
176
|
+
// Run after initial deploy
|
|
176
177
|
if (!lastDeployed) {
|
|
177
178
|
await saveAppMetadata({ mode: "dev" });
|
|
179
|
+
// print start frontend commands
|
|
180
|
+
useSites()
|
|
181
|
+
.all.filter(({ props }) => props.dev?.deploy !== true)
|
|
182
|
+
.forEach(({ type, props }) => {
|
|
183
|
+
const framework = type === "AstroSite"
|
|
184
|
+
? "Astro"
|
|
185
|
+
: type === "NextjsSite"
|
|
186
|
+
? "Next.js"
|
|
187
|
+
: type === "RemixSite"
|
|
188
|
+
? "Remix"
|
|
189
|
+
: type === "SolidStartSite"
|
|
190
|
+
? "SolidStart"
|
|
191
|
+
: undefined;
|
|
192
|
+
if (framework) {
|
|
193
|
+
const cdCmd = path.resolve(props.path) === process.cwd()
|
|
194
|
+
? ""
|
|
195
|
+
: `cd ${props.path} && `;
|
|
196
|
+
Colors.line(Colors.danger(`➜ `), Colors.bold(`Start ${framework}:`), `${cdCmd}npm run dev`);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
178
199
|
}
|
|
179
200
|
lastDeployed = nextChecksum;
|
|
180
201
|
// Write outputs.json
|
|
@@ -226,7 +247,8 @@ export const dev = (program) => program.command(["dev", "start"], "Work on your
|
|
|
226
247
|
return;
|
|
227
248
|
if (evt.properties.app !== project.config.name)
|
|
228
249
|
return;
|
|
229
|
-
Colors.
|
|
250
|
+
Colors.gap();
|
|
251
|
+
Colors.line(Colors.danger(`➜ `), "Another `sst dev` session has been started for this stage. Exiting...");
|
|
230
252
|
process.exit(0);
|
|
231
253
|
});
|
|
232
254
|
});
|
|
@@ -12,6 +12,7 @@ import { IServerlessCluster } from "aws-cdk-lib/aws-rds";
|
|
|
12
12
|
import { ISecret } from "aws-cdk-lib/aws-secretsmanager";
|
|
13
13
|
import { AwsIamConfig, BaseDataSource, CfnDomainName, GraphqlApi, GraphqlApiProps, IGraphqlApi, Resolver, ResolverProps } from "aws-cdk-lib/aws-appsync";
|
|
14
14
|
import { ICertificate } from "aws-cdk-lib/aws-certificatemanager";
|
|
15
|
+
import { IDomain } from "aws-cdk-lib/aws-opensearchservice";
|
|
15
16
|
export interface AppSyncApiDomainProps extends appSyncApiDomain.CustomDomainProps {
|
|
16
17
|
}
|
|
17
18
|
interface AppSyncApiBaseDataSourceProps {
|
|
@@ -89,7 +90,7 @@ export interface AppSyncApiDynamoDbDataSourceProps extends AppSyncApiBaseDataSou
|
|
|
89
90
|
* dataSources: {
|
|
90
91
|
* rds: {
|
|
91
92
|
* type: "rds",
|
|
92
|
-
* rds:
|
|
93
|
+
* rds: myRDSCluster
|
|
93
94
|
* },
|
|
94
95
|
* },
|
|
95
96
|
* });
|
|
@@ -116,6 +117,36 @@ export interface AppSyncApiRdsDataSourceProps extends AppSyncApiBaseDataSourcePr
|
|
|
116
117
|
};
|
|
117
118
|
};
|
|
118
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Used to define a OpenSearch data source
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```js
|
|
125
|
+
* new AppSyncApi(stack, "AppSync", {
|
|
126
|
+
* dataSources: {
|
|
127
|
+
* search: {
|
|
128
|
+
* type: "open_search",
|
|
129
|
+
* cdk: {
|
|
130
|
+
* dataSource: {
|
|
131
|
+
* domain: myOpenSearchDomain,
|
|
132
|
+
* }
|
|
133
|
+
* }
|
|
134
|
+
* }
|
|
135
|
+
* }
|
|
136
|
+
* });
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
export interface AppSyncApiOpenSearchDataSourceProps extends AppSyncApiBaseDataSourceProps {
|
|
140
|
+
/**
|
|
141
|
+
* String literal to signify that this data source is an OpenSearch domain
|
|
142
|
+
*/
|
|
143
|
+
type: "open_search";
|
|
144
|
+
cdk: {
|
|
145
|
+
dataSource: {
|
|
146
|
+
domain: IDomain;
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
}
|
|
119
150
|
/**
|
|
120
151
|
* Used to define an http data source
|
|
121
152
|
*
|
|
@@ -283,7 +314,7 @@ export interface AppSyncApiProps {
|
|
|
283
314
|
* });
|
|
284
315
|
* ```
|
|
285
316
|
*/
|
|
286
|
-
dataSources?: Record<string, FunctionInlineDefinition | AppSyncApiLambdaDataSourceProps | AppSyncApiDynamoDbDataSourceProps | AppSyncApiRdsDataSourceProps | AppSyncApiHttpDataSourceProps | AppSyncApiNoneDataSourceProps>;
|
|
317
|
+
dataSources?: Record<string, FunctionInlineDefinition | AppSyncApiLambdaDataSourceProps | AppSyncApiDynamoDbDataSourceProps | AppSyncApiRdsDataSourceProps | AppSyncApiOpenSearchDataSourceProps | AppSyncApiHttpDataSourceProps | AppSyncApiNoneDataSourceProps>;
|
|
287
318
|
/**
|
|
288
319
|
* The resolvers for this API. Takes an object, with the key being the type name and field name as a string and the value is either a string with the name of existing data source.
|
|
289
320
|
*
|
|
@@ -411,7 +442,7 @@ export declare class AppSyncApi extends Construct implements SSTConstruct {
|
|
|
411
442
|
* ```
|
|
412
443
|
*/
|
|
413
444
|
addDataSources(scope: Construct, dataSources: {
|
|
414
|
-
[key: string]: FunctionInlineDefinition | AppSyncApiLambdaDataSourceProps | AppSyncApiDynamoDbDataSourceProps | AppSyncApiRdsDataSourceProps | AppSyncApiHttpDataSourceProps | AppSyncApiNoneDataSourceProps;
|
|
445
|
+
[key: string]: FunctionInlineDefinition | AppSyncApiLambdaDataSourceProps | AppSyncApiDynamoDbDataSourceProps | AppSyncApiRdsDataSourceProps | AppSyncApiOpenSearchDataSourceProps | AppSyncApiHttpDataSourceProps | AppSyncApiNoneDataSourceProps;
|
|
415
446
|
}): void;
|
|
416
447
|
/**
|
|
417
448
|
* Add resolvers the construct has been created
|
package/constructs/AppSyncApi.js
CHANGED
|
@@ -346,7 +346,7 @@ export class AppSyncApi extends Construct {
|
|
|
346
346
|
description: dsValue.description,
|
|
347
347
|
});
|
|
348
348
|
}
|
|
349
|
-
//
|
|
349
|
+
// RDS ds
|
|
350
350
|
else if (dsValue.type === "rds") {
|
|
351
351
|
dataSource = this.cdk.graphqlApi.addRdsDataSource(dsKey, dsValue.rds
|
|
352
352
|
? dsValue.rds.cdk.cluster
|
|
@@ -359,6 +359,13 @@ export class AppSyncApi extends Construct {
|
|
|
359
359
|
description: dsValue.description,
|
|
360
360
|
});
|
|
361
361
|
}
|
|
362
|
+
// OpenSearch ds
|
|
363
|
+
else if (dsValue.type === "open_search") {
|
|
364
|
+
dataSource = this.cdk.graphqlApi.addOpenSearchDataSource(dsKey, dsValue.cdk?.dataSource?.domain, {
|
|
365
|
+
name: dsValue.name,
|
|
366
|
+
description: dsValue.description,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
362
369
|
// Http ds
|
|
363
370
|
else if (dsValue.type === "http") {
|
|
364
371
|
dataSource = this.cdk.graphqlApi.addHttpDataSource(dsKey, dsValue.endpoint, {
|
package/constructs/SsrSite.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { Size } from "./util/size.js";
|
|
|
13
13
|
import { Duration } from "./util/duration.js";
|
|
14
14
|
import { Permissions } from "./util/permission.js";
|
|
15
15
|
import { FunctionBindingProps } from "./util/functionBinding.js";
|
|
16
|
+
type SsrSiteType = "NextjsSite" | "RemixSite" | "AstroSite" | "SolidStartSite";
|
|
16
17
|
export type SsrBuildConfig = {
|
|
17
18
|
typesPath: string;
|
|
18
19
|
serverBuildOutputFile: string;
|
|
@@ -174,6 +175,13 @@ export interface SsrSiteProps {
|
|
|
174
175
|
server?: Pick<FunctionProps, "vpc" | "vpcSubnets" | "securityGroups" | "allowAllOutbound" | "allowPublicSubnet" | "architecture">;
|
|
175
176
|
};
|
|
176
177
|
}
|
|
178
|
+
type SsrSiteNormalizedProps = SsrSiteProps & {
|
|
179
|
+
path: Exclude<SsrSiteProps["path"], undefined>;
|
|
180
|
+
runtime: Exclude<SsrSiteProps["runtime"], undefined>;
|
|
181
|
+
timeout: Exclude<SsrSiteProps["timeout"], undefined>;
|
|
182
|
+
memorySize: Exclude<SsrSiteProps["memorySize"], undefined>;
|
|
183
|
+
waitForInvalidation: Exclude<SsrSiteProps["waitForInvalidation"], undefined>;
|
|
184
|
+
};
|
|
177
185
|
/**
|
|
178
186
|
* The `SsrSite` construct is a higher level CDK construct that makes it easy to create modern web apps with Server Side Rendering capabilities.
|
|
179
187
|
* @example
|
|
@@ -187,13 +195,7 @@ export interface SsrSiteProps {
|
|
|
187
195
|
*/
|
|
188
196
|
export declare class SsrSite extends Construct implements SSTConstruct {
|
|
189
197
|
readonly id: string;
|
|
190
|
-
protected props:
|
|
191
|
-
path: Exclude<SsrSiteProps["path"], undefined>;
|
|
192
|
-
runtime: Exclude<SsrSiteProps["runtime"], undefined>;
|
|
193
|
-
timeout: Exclude<SsrSiteProps["timeout"], undefined>;
|
|
194
|
-
memorySize: Exclude<SsrSiteProps["memorySize"], undefined>;
|
|
195
|
-
waitForInvalidation: Exclude<SsrSiteProps["waitForInvalidation"], undefined>;
|
|
196
|
-
};
|
|
198
|
+
protected props: SsrSiteNormalizedProps;
|
|
197
199
|
private doNotDeploy;
|
|
198
200
|
protected buildConfig: SsrBuildConfig;
|
|
199
201
|
private serverLambdaForEdge?;
|
|
@@ -235,7 +237,7 @@ export declare class SsrSite extends Construct implements SSTConstruct {
|
|
|
235
237
|
attachPermissions(permissions: Permissions): void;
|
|
236
238
|
/** @internal */
|
|
237
239
|
getConstructMetadata(): {
|
|
238
|
-
type:
|
|
240
|
+
type: SsrSiteType;
|
|
239
241
|
data: {
|
|
240
242
|
mode: "placeholder" | "deployed";
|
|
241
243
|
path: string;
|
|
@@ -280,3 +282,12 @@ export declare class SsrSite extends Construct implements SSTConstruct {
|
|
|
280
282
|
private writeTypesFile;
|
|
281
283
|
protected generateBuildId(): string;
|
|
282
284
|
}
|
|
285
|
+
export declare const useSites: () => {
|
|
286
|
+
add(name: string, type: SsrSiteType, props: SsrSiteNormalizedProps): void;
|
|
287
|
+
readonly all: {
|
|
288
|
+
name: string;
|
|
289
|
+
type: SsrSiteType;
|
|
290
|
+
props: SsrSiteNormalizedProps;
|
|
291
|
+
}[];
|
|
292
|
+
};
|
|
293
|
+
export {};
|
package/constructs/SsrSite.js
CHANGED
|
@@ -18,6 +18,7 @@ import { S3Origin, HttpOrigin } from "aws-cdk-lib/aws-cloudfront-origins";
|
|
|
18
18
|
import { CloudFrontTarget } from "aws-cdk-lib/aws-route53-targets";
|
|
19
19
|
import { Stack } from "./Stack.js";
|
|
20
20
|
import { Logger } from "../logger.js";
|
|
21
|
+
import { createAppContext } from "./context.js";
|
|
21
22
|
import { isCDKConstruct } from "./Construct.js";
|
|
22
23
|
import { Function } from "./Function.js";
|
|
23
24
|
import { Secret } from "./Secret.js";
|
|
@@ -69,6 +70,7 @@ export class SsrSite extends Construct {
|
|
|
69
70
|
this.buildConfig = this.initBuildConfig();
|
|
70
71
|
this.validateSiteExists();
|
|
71
72
|
this.writeTypesFile();
|
|
73
|
+
useSites().add(id, this.constructor.name, this.props);
|
|
72
74
|
if (this.doNotDeploy) {
|
|
73
75
|
// @ts-ignore
|
|
74
76
|
this.bucket = this.distribution = null;
|
|
@@ -820,3 +822,14 @@ function handler(event) {
|
|
|
820
822
|
return buildId;
|
|
821
823
|
}
|
|
822
824
|
}
|
|
825
|
+
export const useSites = createAppContext(() => {
|
|
826
|
+
const sites = [];
|
|
827
|
+
return {
|
|
828
|
+
add(name, type, props) {
|
|
829
|
+
sites.push({ name, type, props });
|
|
830
|
+
},
|
|
831
|
+
get all() {
|
|
832
|
+
return sites;
|
|
833
|
+
},
|
|
834
|
+
};
|
|
835
|
+
});
|
package/constructs/Stack.d.ts
CHANGED
|
@@ -119,7 +119,7 @@ export declare class Stack extends cdk.Stack {
|
|
|
119
119
|
* });
|
|
120
120
|
* ```
|
|
121
121
|
*/
|
|
122
|
-
addOutputs(outputs: Record<string, string | cdk.CfnOutputProps>): void;
|
|
122
|
+
addOutputs(outputs: Record<string, string | cdk.CfnOutputProps | undefined>): void;
|
|
123
123
|
private createCustomResourceHandler;
|
|
124
124
|
private static buildSynthesizer;
|
|
125
125
|
private static checkForPropsIsConstruct;
|
package/constructs/Stack.js
CHANGED
|
@@ -170,11 +170,9 @@ export class Stack extends cdk.Stack {
|
|
|
170
170
|
* ```
|
|
171
171
|
*/
|
|
172
172
|
addOutputs(outputs) {
|
|
173
|
-
Object.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
throw new Error(`The stack output "${key}" is undefined`);
|
|
177
|
-
}
|
|
173
|
+
Object.entries(outputs)
|
|
174
|
+
.filter((e) => e[1] !== undefined)
|
|
175
|
+
.forEach(([key, value]) => {
|
|
178
176
|
// Note: add "SSTStackOutput" prefix to the CfnOutput id to ensure the id
|
|
179
177
|
// does not thrash w/ construct ids in the stack. So users can do this:
|
|
180
178
|
// ```
|
package/package.json
CHANGED
package/project.d.ts
CHANGED
package/sst.mjs
CHANGED
|
@@ -713,7 +713,7 @@ var init_colors = __esm({
|
|
|
713
713
|
}
|
|
714
714
|
},
|
|
715
715
|
hex: chalk.hex,
|
|
716
|
-
primary: chalk.hex("#
|
|
716
|
+
primary: chalk.hex("#FF9000"),
|
|
717
717
|
link: chalk.cyan,
|
|
718
718
|
success: chalk.green,
|
|
719
719
|
danger: chalk.red,
|
|
@@ -5271,7 +5271,6 @@ async function bootstrapCDK() {
|
|
|
5271
5271
|
const identity = await useSTSIdentity();
|
|
5272
5272
|
const credentials = await useAWSCredentials();
|
|
5273
5273
|
const { region, profile, cdk } = useProject().config;
|
|
5274
|
-
cdk || {};
|
|
5275
5274
|
await new Promise((resolve, reject) => {
|
|
5276
5275
|
const proc = spawn6(
|
|
5277
5276
|
[
|
|
@@ -5283,7 +5282,8 @@ async function bootstrapCDK() {
|
|
|
5283
5282
|
...cdk?.publicAccessBlockConfiguration === false ? ["--public-access-block-configuration", "false"] : cdk?.publicAccessBlockConfiguration === true ? ["--public-access-block-configuration", "false"] : [],
|
|
5284
5283
|
...cdk?.toolkitStackName ? ["--toolkit-stack-name", cdk.toolkitStackName] : [],
|
|
5285
5284
|
...cdk?.qualifier ? ["--qualifier", cdk.qualifier] : [],
|
|
5286
|
-
...cdk?.fileAssetsBucketName ? ["--toolkit-bucket-name", cdk.fileAssetsBucketName] : []
|
|
5285
|
+
...cdk?.fileAssetsBucketName ? ["--toolkit-bucket-name", cdk.fileAssetsBucketName] : [],
|
|
5286
|
+
...cdk?.customPermissionsBoundary ? ["--custom-permissions-boundary", cdk.customPermissionsBoundary] : []
|
|
5287
5287
|
].join(" "),
|
|
5288
5288
|
{
|
|
5289
5289
|
env: {
|
|
@@ -6776,6 +6776,7 @@ var dev = (program2) => program2.command(
|
|
|
6776
6776
|
const fs18 = await import("fs/promises");
|
|
6777
6777
|
const crypto2 = await import("crypto");
|
|
6778
6778
|
const { useFunctions: useFunctions3 } = await import("../src/constructs/Function.js");
|
|
6779
|
+
const { useSites } = await import("../src/constructs/SsrSite.js");
|
|
6779
6780
|
const { usePothosBuilder: usePothosBuilder2 } = await Promise.resolve().then(() => (init_pothos2(), pothos_exports2));
|
|
6780
6781
|
const { useKyselyTypeGenerator: useKyselyTypeGenerator2 } = await Promise.resolve().then(() => (init_kysely(), kysely_exports));
|
|
6781
6782
|
const { useRDSWarmer: useRDSWarmer2 } = await Promise.resolve().then(() => (init_warmer(), warmer_exports));
|
|
@@ -6945,6 +6946,17 @@ var dev = (program2) => program2.command(
|
|
|
6945
6946
|
printDeploymentResults2(assembly, results);
|
|
6946
6947
|
if (!lastDeployed) {
|
|
6947
6948
|
await saveAppMetadata2({ mode: "dev" });
|
|
6949
|
+
useSites().all.filter(({ props }) => props.dev?.deploy !== true).forEach(({ type, props }) => {
|
|
6950
|
+
const framework = type === "AstroSite" ? "Astro" : type === "NextjsSite" ? "Next.js" : type === "RemixSite" ? "Remix" : type === "SolidStartSite" ? "SolidStart" : void 0;
|
|
6951
|
+
if (framework) {
|
|
6952
|
+
const cdCmd = path20.resolve(props.path) === process.cwd() ? "" : `cd ${props.path} && `;
|
|
6953
|
+
Colors2.line(
|
|
6954
|
+
Colors2.danger(`\u279C `),
|
|
6955
|
+
Colors2.bold(`Start ${framework}:`),
|
|
6956
|
+
`${cdCmd}npm run dev`
|
|
6957
|
+
);
|
|
6958
|
+
}
|
|
6959
|
+
});
|
|
6948
6960
|
}
|
|
6949
6961
|
lastDeployed = nextChecksum;
|
|
6950
6962
|
fs18.writeFile(
|
|
@@ -7003,9 +7015,10 @@ var dev = (program2) => program2.command(
|
|
|
7003
7015
|
return;
|
|
7004
7016
|
if (evt.properties.app !== project.config.name)
|
|
7005
7017
|
return;
|
|
7018
|
+
Colors2.gap();
|
|
7006
7019
|
Colors2.line(
|
|
7007
7020
|
Colors2.danger(`\u279C `),
|
|
7008
|
-
"Another sst dev session has been started
|
|
7021
|
+
"Another `sst dev` session has been started for this stage. Exiting..."
|
|
7009
7022
|
);
|
|
7010
7023
|
process.exit(0);
|
|
7011
7024
|
});
|
|
@@ -7282,6 +7295,9 @@ var bind = (program2) => program2.command(
|
|
|
7282
7295
|
stdio: "inherit",
|
|
7283
7296
|
shell: true
|
|
7284
7297
|
});
|
|
7298
|
+
p.on("exit", (code) => {
|
|
7299
|
+
process.exit();
|
|
7300
|
+
});
|
|
7285
7301
|
}
|
|
7286
7302
|
function areEnvsSame(envs1, envs2) {
|
|
7287
7303
|
return Object.keys(envs1).length === Object.keys(envs2).length && Object.keys(envs1).every((key) => envs1[key] === envs2[key]);
|