sst 2.4.0 → 2.4.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/cli/commands/bind.js +61 -48
- package/cli/commands/plugins/kysely.js +12 -4
- package/constructs/NextjsSite.js +1 -1
- package/node/actor/index.d.ts +29 -0
- package/node/actor/index.js +17 -0
- package/node/util/index.js +12 -1
- package/package.json +1 -1
- package/runtime/handlers/go.js +7 -4
- package/sst.mjs +95 -70
- package/support/bootstrap-metadata-function/index.mjs +238 -238
- package/support/custom-resources/index.mjs +238 -238
- package/util/error.d.ts +3 -0
- package/util/error.js +6 -0
package/cli/commands/bind.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import { VisibleError } from "../../error.js";
|
|
3
|
-
const
|
|
3
|
+
const SSR_SITE_CONFIG = {
|
|
4
4
|
NextjsSite: "next.config",
|
|
5
5
|
AstroSite: "astro.config",
|
|
6
6
|
RemixSite: "remix.config",
|
|
7
7
|
SolidStartSite: "vite.config",
|
|
8
|
-
StaticSite: "vite.config",
|
|
9
8
|
SlsNextjsSite: "next.config",
|
|
10
9
|
};
|
|
11
10
|
export const bind = (program) => program
|
|
@@ -18,6 +17,7 @@ export const bind = (program) => program
|
|
|
18
17
|
const { useBus } = await import("../../bus.js");
|
|
19
18
|
const { useIOT } = await import("../../iot.js");
|
|
20
19
|
const { Colors } = await import("../colors.js");
|
|
20
|
+
const { Logger } = await import("../../logger.js");
|
|
21
21
|
if (args._[0] === "env") {
|
|
22
22
|
Colors.line(Colors.warning(`Warning: ${Colors.bold(`sst env`)} has been renamed to ${Colors.bold(`sst bind`)}`));
|
|
23
23
|
}
|
|
@@ -25,17 +25,18 @@ export const bind = (program) => program
|
|
|
25
25
|
const bus = useBus();
|
|
26
26
|
const project = useProject();
|
|
27
27
|
const command = args.command?.join(" ");
|
|
28
|
-
const
|
|
28
|
+
const isSsrSite = await isRunningInSsrSite();
|
|
29
29
|
let p;
|
|
30
30
|
let timer;
|
|
31
|
-
let
|
|
31
|
+
let siteConfigCache;
|
|
32
32
|
// Handle missing command
|
|
33
33
|
if (!command) {
|
|
34
|
-
throw new VisibleError(`Command is required, e.g. sst bind ${
|
|
34
|
+
throw new VisibleError(`Command is required, e.g. sst bind ${isSsrSite ? "next dev" : "env"}`);
|
|
35
35
|
}
|
|
36
36
|
// Bind script
|
|
37
37
|
const initialMetadata = await getSiteMetadata();
|
|
38
|
-
if (!initialMetadata) {
|
|
38
|
+
if (!initialMetadata && !isSsrSite) {
|
|
39
|
+
Logger.debug("Running in script mode.");
|
|
39
40
|
return await bindScript();
|
|
40
41
|
}
|
|
41
42
|
// Bind site
|
|
@@ -44,33 +45,44 @@ export const bind = (program) => program
|
|
|
44
45
|
bus.subscribe("stacks.metadata.deleted", () => bindSite("metadata_updated"));
|
|
45
46
|
bus.subscribe("config.secret.updated", (payload) => {
|
|
46
47
|
const secretName = payload.properties.name;
|
|
47
|
-
if (
|
|
48
|
+
if (siteConfigCache?.secrets === undefined)
|
|
48
49
|
return;
|
|
49
|
-
if (!
|
|
50
|
+
if (!siteConfigCache.secrets.includes(secretName))
|
|
50
51
|
return;
|
|
51
52
|
Colors.line(`\n`, `SST secrets have been updated. Restarting \`${command}\`...`);
|
|
52
53
|
bindSite("secrets_updated");
|
|
53
54
|
});
|
|
54
|
-
async function
|
|
55
|
+
async function isRunningInSsrSite() {
|
|
55
56
|
const { existsAsync } = await import("../../util/fs.js");
|
|
56
|
-
const
|
|
57
|
-
|
|
57
|
+
const { readFile } = await import("fs/promises");
|
|
58
|
+
const results = await Promise.all(Object.values(SSR_SITE_CONFIG)
|
|
59
|
+
.map((config) => [".js", ".cjs", ".mjs", ".ts"].map(async (ext) => {
|
|
60
|
+
const exists = await existsAsync(`${config}${ext}`);
|
|
61
|
+
if (exists && config === "vite.config") {
|
|
62
|
+
const content = await readFile(`${config}${ext}`);
|
|
63
|
+
return content.includes("solid-start");
|
|
64
|
+
}
|
|
65
|
+
return exists;
|
|
66
|
+
}))
|
|
58
67
|
.flat());
|
|
59
68
|
return results.some(Boolean);
|
|
60
69
|
}
|
|
61
70
|
async function bindSite(reason) {
|
|
62
71
|
// Get metadata
|
|
63
|
-
const
|
|
72
|
+
const siteMetadata = (reason === "init"
|
|
73
|
+
? initialMetadata
|
|
74
|
+
: await getSiteMetadataUntilAvailable());
|
|
75
|
+
const siteConfig = await parseSiteConfig(siteMetadata);
|
|
64
76
|
// Handle rebind due to metadata updated
|
|
65
77
|
if (reason === "metadata_updated") {
|
|
66
|
-
if (areEnvsSame(
|
|
78
|
+
if (areEnvsSame(siteConfig.envs, siteConfigCache?.envs || {}))
|
|
67
79
|
return;
|
|
68
80
|
Colors.line(`\n`, `SST resources have been updated. Restarting \`${command}\`...`);
|
|
69
81
|
}
|
|
70
|
-
|
|
82
|
+
siteConfigCache = siteConfig;
|
|
71
83
|
// Assume function's role credentials
|
|
72
|
-
if (
|
|
73
|
-
const credentials = await assumeSsrRole(
|
|
84
|
+
if (siteConfig.role) {
|
|
85
|
+
const credentials = await assumeSsrRole(siteConfig.role);
|
|
74
86
|
if (credentials) {
|
|
75
87
|
// refresh crecentials 1 minute before expiration
|
|
76
88
|
const expireAt = credentials.Expiration.getTime() - 60000;
|
|
@@ -80,7 +92,7 @@ export const bind = (program) => program
|
|
|
80
92
|
bindSite("iam_expired");
|
|
81
93
|
}, expireAt - Date.now());
|
|
82
94
|
runCommand({
|
|
83
|
-
...
|
|
95
|
+
...siteConfig.envs,
|
|
84
96
|
AWS_ACCESS_KEY_ID: credentials.AccessKeyId,
|
|
85
97
|
AWS_SECRET_ACCESS_KEY: credentials.SecretAccessKey,
|
|
86
98
|
AWS_SESSION_TOKEN: credentials.SessionToken,
|
|
@@ -90,7 +102,7 @@ export const bind = (program) => program
|
|
|
90
102
|
}
|
|
91
103
|
// Fallback to use local IAM credentials
|
|
92
104
|
runCommand({
|
|
93
|
-
...
|
|
105
|
+
...siteConfig.envs,
|
|
94
106
|
...(await localIamCredentials()),
|
|
95
107
|
});
|
|
96
108
|
}
|
|
@@ -101,24 +113,30 @@ export const bind = (program) => program
|
|
|
101
113
|
...(await localIamCredentials()),
|
|
102
114
|
});
|
|
103
115
|
}
|
|
104
|
-
async function
|
|
105
|
-
const { metadata } = await import("../../stacks/metadata.js");
|
|
106
|
-
const { createSpinner } = await import("../spinner.js");
|
|
116
|
+
async function parseSiteConfig(metadata) {
|
|
107
117
|
const { LambdaClient, GetFunctionCommand } = await import("@aws-sdk/client-lambda");
|
|
108
118
|
const { useAWSClient } = await import("../../credentials.js");
|
|
119
|
+
const isBindSupported = metadata.type !== "StaticSite" && metadata.type !== "SlsNextjsSite";
|
|
120
|
+
// Handle StaticSite
|
|
121
|
+
if (!isBindSupported) {
|
|
122
|
+
return { envs: metadata.data.environment };
|
|
123
|
+
}
|
|
124
|
+
// Get function details
|
|
125
|
+
const lambda = useAWSClient(LambdaClient);
|
|
126
|
+
const { Configuration: functionConfig } = await lambda.send(new GetFunctionCommand({
|
|
127
|
+
FunctionName: metadata.data.server,
|
|
128
|
+
}));
|
|
129
|
+
return {
|
|
130
|
+
role: functionConfig?.Role,
|
|
131
|
+
envs: functionConfig?.Environment?.Variables || {},
|
|
132
|
+
secrets: metadata.data.secrets,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async function getSiteMetadataUntilAvailable() {
|
|
136
|
+
const { createSpinner } = await import("../spinner.js");
|
|
109
137
|
const spinner = createSpinner({});
|
|
110
138
|
while (true) {
|
|
111
|
-
const
|
|
112
|
-
const data = Object.values(metadataData)
|
|
113
|
-
.flat()
|
|
114
|
-
.filter((c) => Boolean(c))
|
|
115
|
-
.filter((c) => Boolean(SITE_CONFIG[c.type]))
|
|
116
|
-
.find((c) => path.resolve(project.paths.root, c.data.path) ===
|
|
117
|
-
process.cwd());
|
|
118
|
-
// Do not retry if not running in frontend
|
|
119
|
-
if (!data && !isFrontend) {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
139
|
+
const data = await getSiteMetadata();
|
|
122
140
|
// Handle site metadata not found
|
|
123
141
|
if (!data) {
|
|
124
142
|
spinner.start(
|
|
@@ -137,25 +155,20 @@ export const bind = (program) => program
|
|
|
137
155
|
continue;
|
|
138
156
|
}
|
|
139
157
|
spinner.isSpinning && spinner.stop().clear();
|
|
140
|
-
|
|
141
|
-
if (!isBindSupported) {
|
|
142
|
-
return { envs: data.data.environment };
|
|
143
|
-
}
|
|
144
|
-
// Get function details
|
|
145
|
-
const lambda = useAWSClient(LambdaClient);
|
|
146
|
-
const { Configuration: functionConfig } = await lambda.send(new GetFunctionCommand({
|
|
147
|
-
FunctionName: data.data.server,
|
|
148
|
-
}));
|
|
149
|
-
return {
|
|
150
|
-
role: functionConfig?.Role,
|
|
151
|
-
envs: functionConfig?.Environment?.Variables || {},
|
|
152
|
-
secrets: data.data.secrets,
|
|
153
|
-
};
|
|
158
|
+
return data;
|
|
154
159
|
}
|
|
155
160
|
}
|
|
161
|
+
async function getSiteMetadata() {
|
|
162
|
+
const { metadata } = await import("../../stacks/metadata.js");
|
|
163
|
+
const metadataData = await metadata();
|
|
164
|
+
return Object.values(metadataData)
|
|
165
|
+
.flat()
|
|
166
|
+
.filter((c) => Boolean(c))
|
|
167
|
+
.filter((c) => c.type === "StaticSite" || Boolean(SSR_SITE_CONFIG[c.type]))
|
|
168
|
+
.find((c) => path.resolve(project.paths.root, c.data.path) === process.cwd());
|
|
169
|
+
}
|
|
156
170
|
async function assumeSsrRole(roleArn) {
|
|
157
171
|
const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
|
|
158
|
-
const { Logger } = await import("../../logger.js");
|
|
159
172
|
const { useAWSClient } = await import("../../credentials.js");
|
|
160
173
|
const sts = useAWSClient(STSClient);
|
|
161
174
|
const assumeRole = async (duration) => {
|
|
@@ -184,7 +197,7 @@ export const bind = (program) => program
|
|
|
184
197
|
err = e;
|
|
185
198
|
}
|
|
186
199
|
}
|
|
187
|
-
Colors.line(
|
|
200
|
+
Colors.line("Using local IAM credentials since `sst dev` is not running.");
|
|
188
201
|
Logger.debug(`Failed to assume ${roleArn}.`, err);
|
|
189
202
|
}
|
|
190
203
|
async function localIamCredentials() {
|
|
@@ -10,10 +10,11 @@ import { useAWSClient, } from "../../../credentials.js";
|
|
|
10
10
|
export const useKyselyTypeGenerator = Context.memo(async () => {
|
|
11
11
|
let databases = [];
|
|
12
12
|
const bus = useBus();
|
|
13
|
+
const logger = Logger.debug.bind(null, "[kysely-codegen]");
|
|
13
14
|
async function generate(db) {
|
|
14
15
|
if (!db.types)
|
|
15
16
|
return;
|
|
16
|
-
|
|
17
|
+
logger("generating types for", db.migratorID);
|
|
17
18
|
const k = new Kysely({
|
|
18
19
|
dialect: new DataApiDialect({
|
|
19
20
|
mode: db.engine.includes("postgres") ? "postgres" : "mysql",
|
|
@@ -26,6 +27,7 @@ export const useKyselyTypeGenerator = Context.memo(async () => {
|
|
|
26
27
|
}),
|
|
27
28
|
});
|
|
28
29
|
const tables = await k.introspection.getTables();
|
|
30
|
+
logger("introspected tables");
|
|
29
31
|
const metadata = db.engine.includes("postgres")
|
|
30
32
|
? tables.map((table) => ({
|
|
31
33
|
...table,
|
|
@@ -46,6 +48,7 @@ export const useKyselyTypeGenerator = Context.memo(async () => {
|
|
|
46
48
|
enumValues: null,
|
|
47
49
|
})),
|
|
48
50
|
}));
|
|
51
|
+
logger("generated metadata", metadata.length);
|
|
49
52
|
const transformer = new Transformer();
|
|
50
53
|
const Dialect = db.engine.includes("postgres")
|
|
51
54
|
? new PostgresDialect()
|
|
@@ -55,6 +58,7 @@ export const useKyselyTypeGenerator = Context.memo(async () => {
|
|
|
55
58
|
camelCase: db.types.camelCase === true,
|
|
56
59
|
metadata: new DatabaseMetadata(metadata, new EnumCollection()),
|
|
57
60
|
});
|
|
61
|
+
logger("transformed nodes", nodes.length);
|
|
58
62
|
const lastIndex = nodes.length - 1;
|
|
59
63
|
const last = nodes[lastIndex];
|
|
60
64
|
nodes[lastIndex] = {
|
|
@@ -82,7 +86,9 @@ export const useKyselyTypeGenerator = Context.memo(async () => {
|
|
|
82
86
|
defaultDatabaseName: c.data.defaultDatabaseName,
|
|
83
87
|
secretArn: c.data.secretArn,
|
|
84
88
|
}));
|
|
85
|
-
databases.map((db) => generate(db).catch(() => {
|
|
89
|
+
databases.map((db) => generate(db).catch((err) => {
|
|
90
|
+
logger(err);
|
|
91
|
+
}));
|
|
86
92
|
});
|
|
87
93
|
bus.subscribe("function.success", async (evt) => {
|
|
88
94
|
if (!evt.properties.body?.results)
|
|
@@ -90,7 +96,9 @@ export const useKyselyTypeGenerator = Context.memo(async () => {
|
|
|
90
96
|
const db = databases.find((db) => db.migratorID === evt.properties.functionID);
|
|
91
97
|
if (!db)
|
|
92
98
|
return;
|
|
93
|
-
generate(db).catch(() => {
|
|
99
|
+
generate(db).catch((err) => {
|
|
100
|
+
logger(err);
|
|
101
|
+
});
|
|
94
102
|
});
|
|
95
|
-
|
|
103
|
+
logger("Loaded kyseley type generator");
|
|
96
104
|
});
|
package/constructs/NextjsSite.js
CHANGED
|
@@ -26,7 +26,7 @@ const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
|
|
|
26
26
|
export class NextjsSite extends SsrSite {
|
|
27
27
|
constructor(scope, id, props) {
|
|
28
28
|
super(scope, id, {
|
|
29
|
-
buildCommand: "npx --yes open-next
|
|
29
|
+
buildCommand: "npx --yes open-next@^0.8.0 build",
|
|
30
30
|
...props,
|
|
31
31
|
});
|
|
32
32
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { SSTError } from "../../util/error.js";
|
|
2
|
+
interface Definition {
|
|
3
|
+
type: string;
|
|
4
|
+
properties: Record<string, any>;
|
|
5
|
+
}
|
|
6
|
+
export declare class WrongActorError extends SSTError {
|
|
7
|
+
}
|
|
8
|
+
export declare function createActors<T extends Definition>(): {
|
|
9
|
+
useActor: () => T | {
|
|
10
|
+
type: "public";
|
|
11
|
+
properties: {};
|
|
12
|
+
};
|
|
13
|
+
provideActor: (value: T | {
|
|
14
|
+
type: "public";
|
|
15
|
+
properties: {};
|
|
16
|
+
}) => void;
|
|
17
|
+
assertActor<T_1 extends (T | {
|
|
18
|
+
type: "public";
|
|
19
|
+
properties: {};
|
|
20
|
+
})["type"]>(type: T_1): (Extract<T, {
|
|
21
|
+
type: T_1;
|
|
22
|
+
}> | Extract<{
|
|
23
|
+
type: "public";
|
|
24
|
+
properties: {};
|
|
25
|
+
}, {
|
|
26
|
+
type: T_1;
|
|
27
|
+
}>)["properties"];
|
|
28
|
+
};
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Context } from "../../context/context.js";
|
|
2
|
+
import { SSTError } from "../../util/error.js";
|
|
3
|
+
export class WrongActorError extends SSTError {
|
|
4
|
+
}
|
|
5
|
+
export function createActors() {
|
|
6
|
+
const ctx = Context.create();
|
|
7
|
+
return {
|
|
8
|
+
useActor: ctx.use,
|
|
9
|
+
provideActor: ctx.provide,
|
|
10
|
+
assertActor(type) {
|
|
11
|
+
const actor = ctx.use();
|
|
12
|
+
if (actor.type === type)
|
|
13
|
+
return actor.properties;
|
|
14
|
+
throw new WrongActorError(`Expected actor type "${type} but got "${actor.type}"`);
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
package/node/util/index.js
CHANGED
|
@@ -9,7 +9,10 @@ const ssm = new SSMClient({ region: process.env.SST_REGION });
|
|
|
9
9
|
// }
|
|
10
10
|
// }
|
|
11
11
|
let allVariables = {};
|
|
12
|
-
await
|
|
12
|
+
// NOTE: in some setups, top level await must be assigned to a variable,
|
|
13
|
+
// otherwise it would throw a top level await error.
|
|
14
|
+
// https://discord.com/channels/983865673656705025/1089184080534446110
|
|
15
|
+
const _placeholder = await parseEnvironment();
|
|
13
16
|
export function createProxy(constructName) {
|
|
14
17
|
return new Proxy({}, {
|
|
15
18
|
get(target, prop) {
|
|
@@ -87,6 +90,14 @@ async function fetchValuesFromSSM(variablesFromSsm) {
|
|
|
87
90
|
const variable = parseSsmFallbackPath(item.Name);
|
|
88
91
|
storeVariable(variable, item.Value);
|
|
89
92
|
});
|
|
93
|
+
// Throw error if any values are missing
|
|
94
|
+
const missingSecrets = fallbackResults.invalidParams
|
|
95
|
+
.map((name) => parseSsmFallbackPath(name))
|
|
96
|
+
.filter((variable) => variable.constructName === "Secret")
|
|
97
|
+
.map((variable) => variable.constructId);
|
|
98
|
+
if (missingSecrets.length > 0) {
|
|
99
|
+
throw new Error(`The following secrets were not found: ${missingSecrets.join(", ")}`);
|
|
100
|
+
}
|
|
90
101
|
}
|
|
91
102
|
async function loadSecrets(paths) {
|
|
92
103
|
// Split paths into chunks of 10
|
package/package.json
CHANGED
package/runtime/handlers/go.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs/promises";
|
|
3
|
+
import os from "os";
|
|
3
4
|
import { useRuntimeHandlers } from "../handlers.js";
|
|
4
5
|
import { useRuntimeWorkers } from "../workers.js";
|
|
5
6
|
import { Context } from "../../context/context.js";
|
|
@@ -57,7 +58,8 @@ export const useGoHandler = Context.memo(async () => {
|
|
|
57
58
|
if (input.mode === "start") {
|
|
58
59
|
try {
|
|
59
60
|
const target = path.join(input.out, handlerName);
|
|
60
|
-
const
|
|
61
|
+
const srcPath = os.platform() === "win32" ? src.replaceAll("\\", "\\\\") : src;
|
|
62
|
+
const result = await execAsync(`go build -ldflags "-s -w" -o "${target}" ./${srcPath}`, {
|
|
61
63
|
cwd: project,
|
|
62
64
|
env: {
|
|
63
65
|
...process.env,
|
|
@@ -71,7 +73,8 @@ export const useGoHandler = Context.memo(async () => {
|
|
|
71
73
|
if (input.mode === "deploy") {
|
|
72
74
|
try {
|
|
73
75
|
const target = path.join(input.out, "bootstrap");
|
|
74
|
-
|
|
76
|
+
const srcPath = os.platform() === "win32" ? src.replaceAll("\\", "\\\\") : src;
|
|
77
|
+
await execAsync(`go build -ldflags "-s -w" -o "${target}" ./${srcPath}`, {
|
|
75
78
|
cwd: project,
|
|
76
79
|
env: {
|
|
77
80
|
...process.env,
|
|
@@ -81,8 +84,8 @@ export const useGoHandler = Context.memo(async () => {
|
|
|
81
84
|
},
|
|
82
85
|
});
|
|
83
86
|
}
|
|
84
|
-
catch {
|
|
85
|
-
throw new VisibleError(
|
|
87
|
+
catch (ex) {
|
|
88
|
+
throw new VisibleError(`Failed to build ${ex}`);
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
return {
|