Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

sst

Package Overview
Dependencies
Maintainers
2
Versions
1297
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

sst - npm Package Compare versions

Comparing version 0.0.0-20221121200755 to 0.0.0-20221121213401

cli/commands/remove.d.ts

4

app.d.ts

@@ -23,2 +23,6 @@ export interface Project {

};
export declare const ProjectContext: {
use(): ProjectWithDefaults;
provide(value: ProjectWithDefaults): void;
};
export declare function useProject(): ProjectWithDefaults;

@@ -25,0 +29,0 @@ interface GlobalOptions {

22

app.js

@@ -16,3 +16,3 @@ import fs from "fs/promises";

};
const ProjectContext = Context.create();
export const ProjectContext = Context.create();
export function useProject() {

@@ -24,3 +24,2 @@ return ProjectContext.use();

const root = globals.root || (await findRoot());
Logger.debug("Using project root", root);
const out = path.join(root, ".sst");

@@ -30,3 +29,2 @@ await fs.mkdir(out, {

});
Logger.debug("Using project out", out);
async function load() {

@@ -72,8 +70,13 @@ const base = await (async function () {

};
const packageJson = JSON.parse(await fs
.readFile(url.fileURLToPath(new URL("../package.json", import.meta.url)))
.then((x) => x.toString()));
project.version = packageJson.version;
try {
const packageJson = JSON.parse(await fs
.readFile(url.fileURLToPath(new URL("./package.json", import.meta.url)))
.then((x) => x.toString()));
project.version = packageJson.version;
}
catch {
project.version = "unknown";
}
ProjectContext.provide(project);
Logger.debug("Config loaded", project);
ProjectContext.provide(project);
}

@@ -105,3 +108,2 @@ async function usePersonalStage(out) {

async function findRoot() {
Logger.debug("Searching for project root...");
async function find(dir) {

@@ -112,5 +114,3 @@ if (dir === "/")

const configPath = path.join(dir, `sst${ext}`);
Logger.debug("Searching", configPath);
if (fsSync.existsSync(configPath)) {
Logger.debug("Found config", configPath);
return dir;

@@ -117,0 +117,0 @@ }

@@ -1,1 +0,39 @@

"use strict";
import { useProject } from "../../app.js";
import { createSpinner } from "../spinner.js";
import fs from "fs/promises";
import path from "path";
import { SiteEnv } from "../../site-env.js";
import { spawnSync } from "child_process";
export const env = (program) => program.command("env <command>", "description", (yargs) => yargs.positional("command", {
type: "string",
describe: "Command to run with environment variabels loaded",
demandOption: true,
}), async (args) => {
const project = useProject();
const spinner = createSpinner("Waiting for SST to start").start();
while (true) {
const exists = await fs
.access(SiteEnv.valuesFile())
.then(() => true)
.catch(() => false);
if (!exists) {
await new Promise((resolve) => setTimeout(resolve, 1000));
continue;
}
spinner.succeed();
const sites = await SiteEnv.values();
const current = path.relative(project.paths.root, process.cwd());
const env = sites[current] || {};
console.log(args.command);
const result = spawnSync(args.command, {
env: {
...process.env,
...env,
},
stdio: "inherit",
shell: process.env.SHELL || true,
});
process.exitCode = result.status || undefined;
break;
}
});

@@ -7,2 +7,4 @@ /// <reference types="yargs" />

profile: string | undefined;
} & {
fullscreen: boolean;
}>;

@@ -7,3 +7,9 @@ import path from "path";

import { dim, gray } from "colorette";
export const start = (program) => program.command("start", "Work on your SST app locally", (yargs) => yargs, async () => {
import { useProject } from "../../app.js";
import { SiteEnv } from "../../site-env.js";
export const start = (program) => program.command("start", "Work on your SST app locally", (yargs) => yargs.option("fullscreen", {
type: "boolean",
describe: "Disable full screen UI",
default: true,
}), async (args) => {
const { useRuntimeWorkers } = await import("../../runtime/workers.js");

@@ -46,3 +52,6 @@ const { useIOTBridge } = await import("../../runtime/iot.js");

bus.subscribe("function.error", async (evt) => {
console.log(bold(red(`Error `)), bold(useFunctions().fromID(evt.properties.functionID).handler));
console.log(bold(red(`Error `)), bold(useFunctions().fromID(evt.properties.functionID).handler), evt.properties.errorMessage);
for (const line of evt.properties.trace) {
console.log(` ${dim(line)}`);
}
});

@@ -53,2 +62,3 @@ });

const bus = useBus();
const project = useProject();
let lastDeployed;

@@ -86,9 +96,28 @@ let pending;

pending = undefined;
process.stdout.write("\x1b[?1049h");
const component = render(React.createElement(DeploymentUI, { stacks: assembly.stacks.map((s) => s.stackName) }));
let component = undefined;
if (args.fullscreen) {
process.stdout.write("\x1b[?1049h");
component = render(React.createElement(DeploymentUI, { stacks: assembly.stacks.map((s) => s.stackName) }));
}
const results = await Stacks.deployMany(assembly.stacks);
component.unmount();
if (component)
component.unmount();
process.stdout.write("\x1b[?1049l");
lastDeployed = nextChecksum;
printDeploymentResults(results);
const keys = await SiteEnv.keys();
if (keys.length) {
const result = {};
for (const key of keys) {
const stack = results[key.stack];
const value = stack.outputs[key.output];
let existing = result[key.path];
if (!existing) {
result[key.path] = existing;
existing = result[key.path] = {};
}
existing[key.environment] = value;
}
await SiteEnv.writeValues(result);
}
isDeploying = false;

@@ -95,0 +124,0 @@ deploy();

@@ -6,5 +6,5 @@ import { green, yellow } from "colorette";

const FIELDS = ["dependencies", "devDependencies"];
export const update = (program) => program.command("update [ver]", "Update SST and CDK packages to another version", yargs => yargs.positional("ver", {
export const update = (program) => program.command("update [ver]", "Update SST and CDK packages to another version", (yargs) => yargs.positional("ver", {
type: "string",
describe: "Optional SST version to update to"
describe: "Optional SST version to update to",
}), async (args) => {

@@ -15,5 +15,5 @@ const { fetch } = await import("undici");

const files = await find(project.paths.root);
const version = args.version ||
const version = args.ver ||
(await fetch(`https://registry.npmjs.org/sst/latest`)
.then(resp => resp.json())
.then((resp) => resp.json())
.then((resp) => resp.version));

@@ -24,3 +24,3 @@ const results = new Map();

.readFile(file)
.then(x => x.toString())
.then((x) => x.toString())
.then(JSON.parse);

@@ -27,0 +27,0 @@ for (const field of FIELDS) {

@@ -156,3 +156,2 @@ import { produceWithPatches, enablePatches } from "immer";

updateFunction(evt.properties.functionID, (draft) => {
console.log(evt.properties.context.awsRequestId);
if (draft.invocations.length >= 25)

@@ -159,0 +158,0 @@ draft.invocations.pop();

@@ -12,3 +12,6 @@ #!/usr/bin/env node

import { deploy } from "./commands/deploy.js";
import { remove } from "./commands/remove.js";
import dotenv from "dotenv";
import { env } from "./commands/env.js";
import { Logger } from "../logger.js";
dotenv.config();

@@ -21,4 +24,7 @@ secrets(program);

deploy(program);
remove(program);
env(program);
process.removeAllListeners("uncaughtException");
process.on("uncaughtException", (err) => {
Logger.debug(err);
const spinners = useSpinners();

@@ -25,0 +31,0 @@ for (const spinner of spinners) {

@@ -108,2 +108,3 @@ import React, { useState, useEffect } from "react";

export function printDeploymentResults(results) {
console.log();
console.log(`----------------------------`);

@@ -136,2 +137,3 @@ console.log(`| Stack deployment results |`);

}
console.log();
}

@@ -396,3 +396,3 @@ import { Construct } from "constructs";

}
export declare type ApiRouteProps<AuthorizerKeys> = FunctionInlineDefinition | ApiFunctionRouteProps<AuthorizerKeys> | ApiHttpRouteProps<AuthorizerKeys> | ApiAlbRouteProps<AuthorizerKeys> | ApiPothosRouteProps<AuthorizerKeys>;
export declare type ApiRouteProps<AuthorizerKeys> = FunctionInlineDefinition | ApiFunctionRouteProps<AuthorizerKeys> | ApiHttpRouteProps<AuthorizerKeys> | ApiAlbRouteProps<AuthorizerKeys> | ApiGraphQLRouteProps<AuthorizerKeys> | ApiPothosRouteProps<AuthorizerKeys>;
interface ApiBaseRouteProps<AuthorizerKeys = string> {

@@ -491,18 +491,36 @@ authorizer?: "none" | "iam" | (string extends AuthorizerKeys ? Omit<AuthorizerKeys, "none" | "iam"> : AuthorizerKeys);

* Specify a route handler that handles GraphQL queries using Pothos
*
* @deprecated "pothos" routes are deprecated for the Api construct, and will be removed in SST v2. Use "graphql" routes instead. Read more about how to upgrade here — https://docs.sst.dev/upgrade-guide#upgrade-to-v118
* @example
* ```js
* // Change
* api.addRoutes(stack, {
* "POST /graphql": {
* type: "pothos",
* schema: "backend/functions/graphql/schema.ts",
* output: "graphql/schema.graphql",
* function: {
* handler: "functions/graphql/graphql.ts",
* },
* commands: [
* "./genql graphql/graphql.schema graphql/
* ]
* type: "pothos",
* function: {
* handler: "functions/graphql/graphql.ts",
* },
* schema: "backend/functions/graphql/schema.ts",
* output: "graphql/schema.graphql",
* commands: [
* "./genql graphql/graphql.schema graphql/
* ]
* }
* })
*
* // To
* api.addRoutes(stack, {
* "POST /graphql": {
* type: "graphql",
* function: {
* handler: "functions/graphql/graphql.ts",
* },
* pothos: {
* schema: "backend/functions/graphql/schema.ts",
* output: "graphql/schema.graphql",
* commands: [
* "./genql graphql/graphql.schema graphql/
* ]
* }
* }
* })
* ```

@@ -530,2 +548,45 @@ */

/**
* Specify a route handler that handles GraphQL queries using Pothos
*
* @example
* ```js
* api.addRoutes(stack, {
* "POST /graphql": {
* type: "graphql",
* function: {
* handler: "functions/graphql/graphql.ts",
* },
* pothos: {
* schema: "backend/functions/graphql/schema.ts",
* output: "graphql/schema.graphql",
* commands: [
* "./genql graphql/graphql.schema graphql/
* ]
* }
* }
* })
* ```
*/
export interface ApiGraphQLRouteProps<AuthorizerKeys> extends ApiBaseRouteProps<AuthorizerKeys> {
type: "graphql";
/**
* The function definition used to create the function for this route. Must be a graphql handler
*/
function: FunctionDefinition;
pothos?: {
/**
* Path to pothos schema
*/
schema?: string;
/**
* File to write graphql schema to
*/
output?: string;
/**
* Commands to run after generating schema. Useful for code generation steps
*/
commands?: string[];
};
}
/**
* The Api construct is a higher level CDK construct that makes it easy to create an API.

@@ -708,2 +769,12 @@ *

} | {
type: "graphql";
route: string;
fn: {
node: string;
stack: string;
} | undefined;
schema: string | undefined;
output: string | undefined;
commands: string[] | undefined;
} | {
type: "lambda_function" | "url" | "alb";

@@ -735,2 +806,3 @@ route: string;

protected createPothosIntegration(scope: Construct, routeKey: string, routeProps: ApiPothosRouteProps<keyof Authorizers>, postfixName: string): apig.HttpRouteIntegration;
protected createGraphQLIntegration(scope: Construct, routeKey: string, routeProps: ApiGraphQLRouteProps<keyof Authorizers>, postfixName: string): apig.HttpRouteIntegration;
protected createCdkFunctionIntegration(scope: Construct, routeKey: string, routeProps: ApiFunctionRouteProps<keyof Authorizers>, postfixName: string): apig.HttpRouteIntegration;

@@ -740,3 +812,17 @@ protected createFunctionIntegration(scope: Construct, routeKey: string, routeProps: ApiFunctionRouteProps<keyof Authorizers>, postfixName: string): apig.HttpRouteIntegration;

private normalizeRouteKey;
/**
* Binds the given list of resources to a specific route.
*
* @example
* ```js
* const api = new Api(stack, "Api");
*
* api.setCors({
* allowMethods: ["GET"],
* });
* ```
*
*/
setCors(cors?: boolean | ApiCorsProps): void;
}
export {};
import { Construct } from "constructs";
import * as cdk from "aws-cdk-lib";
import * as cdkLib from "aws-cdk-lib";
import * as cognito from "aws-cdk-lib/aws-cognito";

@@ -124,3 +124,5 @@ import * as apig from "@aws-cdk/aws-apigatewayv2-alpha";

return;
if (route.type === "function" || route.type === "pothos") {
if (route.type === "function" ||
route.type === "pothos" ||
route.type === "graphql") {
return route.function;

@@ -140,3 +142,5 @@ }

for (const route of Object.values(this.routesData)) {
if (route.type === "function" || route.type === "pothos") {
if (route.type === "function" ||
route.type === "pothos" ||
route.type === "graphql") {
route.function.bind(constructs);

@@ -180,3 +184,5 @@ }

for (const route of Object.values(this.routesData)) {
if (route.type === "function" || route.type === "pothos") {
if (route.type === "function" ||
route.type === "pothos" ||
route.type === "graphql") {
route.function.attachPermissions(permissions);

@@ -233,2 +239,11 @@ }

};
if (data.type === "graphql")
return {
type: "graphql",
route: key,
fn: getFunctionRef(data.function),
schema: data.schema,
output: data.output,
commands: data.commands,
};
return { type: data.type, route: key };

@@ -383,3 +398,3 @@ }),

? toCdkDuration(value.resultsCacheTtl)
: cdk.Duration.seconds(0),
: cdkLib.Duration.seconds(0),
});

@@ -458,2 +473,8 @@ }

}
if (routeValue.type === "graphql") {
return [
routeValue,
this.createGraphQLIntegration(scope, routeKey, routeValue, postfixName),
];
}
if (routeValue.cdk?.function) {

@@ -541,2 +562,20 @@ return [

}
createGraphQLIntegration(scope, routeKey, routeProps, postfixName) {
const result = this.createFunctionIntegration(scope, routeKey, {
...routeProps,
type: "function",
payloadFormatVersion: "2.0",
}, postfixName);
const data = this.routesData[routeKey];
if (data.type === "function") {
this.routesData[routeKey] = {
...data,
type: "graphql",
output: routeProps.pothos?.output,
schema: routeProps.pothos?.schema,
commands: routeProps.pothos?.commands,
};
}
return result;
}
createCdkFunctionIntegration(scope, routeKey, routeProps, postfixName) {

@@ -643,2 +682,43 @@ ///////////////////

}
/**
* Binds the given list of resources to a specific route.
*
* @example
* ```js
* const api = new Api(stack, "Api");
*
* api.setCors({
* allowMethods: ["GET"],
* });
* ```
*
*/
setCors(cors) {
const { cdk } = this.props;
if (isCDKConstruct(cdk?.httpApi)) {
// Cannot set CORS if cdk.httpApi is a construct.
if (cors !== undefined) {
throw new Error(`Cannot configure the "cors" when "cdk.httpApi" is a construct`);
}
}
else {
// Cannot set CORS via cdk.httpApi. Always use Api.cors.
const httpApiProps = (cdk?.httpApi || {});
if (httpApiProps.corsPreflight !== undefined) {
throw new Error(`Cannot configure the "httpApi.corsPreflight" in the Api`);
}
const corsConfig = apigV2Cors.buildCorsConfig(cors);
if (corsConfig) {
const cfnApi = this.cdk.httpApi.node.defaultChild;
cfnApi.corsConfiguration = {
allowCredentials: corsConfig?.allowCredentials,
allowHeaders: corsConfig?.allowHeaders,
allowMethods: corsConfig?.allowMethods,
allowOrigins: corsConfig?.allowOrigins,
exposeHeaders: corsConfig?.exposeHeaders,
maxAge: corsConfig?.maxAge?.toSeconds(),
};
}
}
}
}

@@ -7,3 +7,2 @@ import * as cdk from "aws-cdk-lib";

import * as Config from "./Config.js";
import { BaseSiteEnvironmentOutputsInfo } from "./BaseSite.js";
import { Permissions } from "./util/permission.js";

@@ -100,3 +99,2 @@ import { StackProps } from "./Stack.js";

get defaultRemovalPolicy(): "destroy" | "retain" | "snapshot" | undefined;
private readonly siteEnvironments;
/**

@@ -204,3 +202,2 @@ * Skip building Function code

isRunningSSTTest(): boolean;
registerSiteEnvironment(environment: BaseSiteEnvironmentOutputsInfo): void;
getInputFilesFromEsbuildMetafile(file: string): Array<string>;

@@ -214,4 +211,2 @@ private createBindingSsmParameters;

private codegenTypes;
private codegenCreateIndexType;
private codegenCreateConstructTypes;
stack<T extends FunctionalStack<any>>(fn: T, props?: StackProps & {

@@ -218,0 +213,0 @@ id?: string;

@@ -19,2 +19,3 @@ import path from "path";

import { Logger } from "../logger.js";
import { SiteEnv } from "../site-env.js";
const require = createRequire(import.meta.url);

@@ -38,4 +39,4 @@ function exitWithMessage(message) {

this.local = false;
this.siteEnvironments = [];
AppContext.provide(this);
SiteEnv.reset();
this.bootstrap = deployProps.bootstrap;

@@ -208,5 +209,2 @@ this.appPath = process.cwd();

}
registerSiteEnvironment(environment) {
this.siteEnvironments.push(environment);
}
getInputFilesFromEsbuildMetafile(file) {

@@ -402,6 +400,2 @@ let metaJson;

Logger.debug(`Generating types in ${typesPath}`);
this.codegenCreateIndexType(typesPath);
this.codegenCreateConstructTypes(typesPath);
}
codegenCreateIndexType(typesPath) {
fs.rmSync(typesPath, {

@@ -414,28 +408,11 @@ recursive: true,

});
fs.writeFileSync(`${typesPath}/index.ts`, `
import "@serverless-stack/node/config";
declare module "@serverless-stack/node/config" {
export interface ConfigTypes {
APP: string;
STAGE: string;
}
}`);
}
codegenCreateConstructTypes(typesPath) {
//export function codegenTypes(typesPath: string) {
// fs.appendFileSync(`${typesPath}/index.d.ts`, `export * from "./config";`);
// fs.writeFileSync(`${typesPath}/config.d.ts`, `
// import "@serverless-stack/node/config";
// declare module "@serverless-stack/node/config" {
// export interface ConfigType {
// ${[
// "APP",
// "STAGE",
// ...Parameter.getAllNames(),
// ...Secret.getAllNames()
// ].map((p) => `${p}: string;`).join("\n")}
// }
// }
// `);
//}
fs.writeFileSync(`${typesPath}/index.ts`, [
`import "sst/node/config";`,
`declare module "sst/node/config" {`,
` export interface ConfigTypes {`,
` APP: string;`,
` STAGE: string;`,
` }`,
`}`,
].join("\n"));
class CodegenTypes {

@@ -455,22 +432,22 @@ visit(c) {

const id = c.id;
fs.appendFileSync(`${typesPath}/index.ts`, `export * from "./${className}-${id}";`);
// Case 1: variable does not have properties, ie. Secrets and Parameters
const typeContent = binding.variables[0] === "."
? `
import "@serverless-stack/node/${binding.clientPackage}";
declare module "@serverless-stack/node/${binding.clientPackage}" {
export interface ${className}Resources {
"${id}": string;
}
}`
: `
import "@serverless-stack/node/${binding.clientPackage}";
declare module "@serverless-stack/node/${binding.clientPackage}" {
export interface ${className}Resources {
"${id}": {
${binding.variables.map((p) => `${p}: string;`).join("\n")}
}
}
}`;
fs.writeFileSync(`${typesPath}/${className}-${id}.ts`, typeContent);
fs.writeFileSync(`${typesPath}/${className}-${id}.ts`, (binding.variables[0] === "."
? [
`import "sst/node/${binding.clientPackage}";`,
`declare module "sst/node/${binding.clientPackage}" {`,
` export interface ${className}Resources {`,
` "${id}": string;`,
` }`,
`}`,
]
: [
`import "sst/node/${binding.clientPackage}";`,
`declare module "sst/node/${binding.clientPackage}" {`,
` export interface ${className}Resources {`,
` "${id}": {`,
...binding.variables.map((p) => ` ${p}: string;`),
` }`,
` }`,
`}`,
]).join("\n"));
}

@@ -477,0 +454,0 @@ }

@@ -11,2 +11,3 @@ import { Construct, IConstruct } from "constructs";

permissions?: Permissions;
format?: "cjs" | "esm";
environment?: Record<string, string>;

@@ -13,0 +14,0 @@ /**

@@ -0,4 +1,4 @@

import fs from "fs";
import url from "url";
import path from "path";
import fs from "fs";
import crypto from "crypto";

@@ -46,7 +46,9 @@ import { Construct } from "constructs";

const handlerMethod = parts.slice(-1)[0];
const content = `
"use strict";
const index = require("./${handlerImportPath}");
const imports = this.props.format === "esm"
? `import * as index from "./${handlerImportPath}.mjs";`
: `"use strict"; const index = require("./${handlerImportPath}");`;
const exports = this.props.format === "esm"
? `export { handler };`
: `exports.handler = handler;`;
const content = `${imports}
const handler = async (event) => {

@@ -76,4 +78,4 @@ try {

exports.handler = handler;
`;
${exports}
`;
const { bundlePath } = this.props;

@@ -155,7 +157,3 @@ fs.writeFileSync(path.join(bundlePath, "index-wrapper.js"), content);

provider = new lambda.Function(stack, providerId, {
code: lambda.Code.fromAsset(
// TODO: Move this file into a shared folder
// This references a Nextjs directory, but the underlying
// code appears to be generic enough to utilise in this case.
path.join(__dirname, "../assets/NextjsSite/custom-resource")),
code: lambda.Code.fromAsset(path.join(__dirname, "../support/edge-function-code-replacer")),
layers: [this.createSingletonAwsCliLayer()],

@@ -203,3 +201,3 @@ runtime: lambda.Runtime.PYTHON_3_7,

const provider = new lambda.Function(stack, providerId, {
code: lambda.Code.fromAsset(path.join(__dirname, "nextjs-site", "custom-resource")),
code: lambda.Code.fromAsset(path.join(__dirname, "../support/edge-function")),
handler: "s3-bucket.handler",

@@ -236,3 +234,3 @@ runtime: lambda.Runtime.NODEJS_16_X,

provider = new lambda.Function(stack, providerId, {
code: lambda.Code.fromAsset(path.join(__dirname, "nextjs-site", "custom-resource")),
code: lambda.Code.fromAsset(path.join(__dirname, "../support/edge-function")),
handler: "edge-lambda.handler",

@@ -275,3 +273,3 @@ runtime: lambda.Runtime.NODEJS_16_X,

provider = new lambda.Function(stack, providerId, {
code: lambda.Code.fromAsset(path.join(__dirname, "nextjs-site", "custom-resource")),
code: lambda.Code.fromAsset(path.join(__dirname, "../support/edge-function")),
handler: "edge-lambda-version.handler",

@@ -278,0 +276,0 @@ runtime: lambda.Runtime.NODEJS_16_X,

@@ -186,2 +186,4 @@ /* eslint-disable @typescript-eslint/ban-types */

const result = await useRuntimeHandlers().build(this.node.addr, "deploy");
if (!result)
throw new Error(`Failed to build function "${props.handler}"`);
const code = lambda.AssetCode.fromAsset(result.out);

@@ -188,0 +190,0 @@ // Update function's code

@@ -78,2 +78,12 @@ import { HttpRouteIntegration } from "@aws-cdk/aws-apigatewayv2-alpha";

} | {
type: "graphql";
route: string;
fn: {
node: string;
stack: string;
} | undefined;
schema: string | undefined;
output: string | undefined;
commands: string[] | undefined;
} | {
type: "lambda_function" | "url" | "alb";

@@ -80,0 +90,0 @@ route: string;

export * from "./App.js";
export * from "./AstroSite.js";
export * from "./Auth.js";

@@ -24,2 +25,3 @@ export * from "./Api.js";

export * from "./RemixSite.js";
export * from "./SolidStartSite.js";
export * from "./StaticSite.js";

@@ -26,0 +28,0 @@ export * from "./ViteStaticSite.js";

export * from "./App.js";
export * from "./AstroSite.js";
export * from "./Auth.js";

@@ -24,2 +25,3 @@ export * from "./Api.js";

export * from "./RemixSite.js";
export * from "./SolidStartSite.js";
export * from "./StaticSite.js";

@@ -26,0 +28,0 @@ export * from "./ViteStaticSite.js";

@@ -201,2 +201,4 @@ // import path from "path";

// create wrapper that calls the handler
if (!bundle)
throw new Error(`Failed to build job "${this.props.handler}"`);
const parsed = path.parse(bundle.handler);

@@ -203,0 +205,0 @@ await fs.writeFile(path.join(bundle.out, "handler-wrapper.js"), [

@@ -19,3 +19,3 @@ import path from "path";

const provider = new lambda.Function(stack, providerId, {
code: lambda.Code.fromAsset(path.join(__dirname, "custom-resource")),
code: lambda.Code.fromAsset(path.join(__dirname, "../../support/edge-function")),
handler: "s3-bucket.handler",

@@ -52,3 +52,3 @@ runtime: lambda.Runtime.NODEJS_16_X,

provider = new lambda.Function(stack, providerId, {
code: lambda.Code.fromAsset(path.join(__dirname, "custom-resource")),
code: lambda.Code.fromAsset(path.join(__dirname, "../../support/edge-function")),
handler: "edge-lambda.handler",

@@ -91,3 +91,3 @@ runtime: lambda.Runtime.NODEJS_16_X,

provider = new lambda.Function(stack, providerId, {
code: lambda.Code.fromAsset(path.join(__dirname, "custom-resource")),
code: lambda.Code.fromAsset(path.join(__dirname, "../../support/edge-function")),
handler: "edge-lambda-version.handler",

@@ -94,0 +94,0 @@ runtime: lambda.Runtime.NODEJS_16_X,

@@ -30,2 +30,3 @@ import path from "path";

import { gray, red } from "colorette";
import { SiteEnv } from "../site-env.js";
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));

@@ -231,3 +232,3 @@ /////////////////////

new s3Assets.Asset(this, "Asset", {
path: path.resolve(__dirname, "../assets/NextjsSite/site-stub"),
path: path.resolve(__dirname, "../support/sls-nextjs-site-stub"),
}),

@@ -245,3 +246,3 @@ ];

? path.join(this.buildOutDir, handlerPath)
: path.join(__dirname, "../assets/NextjsSite/edge-lambda-stub");
: path.join(__dirname, "../support/ssr-site-function-stub");
const asset = new s3Assets.Asset(this, `${name}FunctionAsset`, {

@@ -399,3 +400,3 @@ path: assetPath,

provider = new lambda.Function(stack, providerId, {
code: lambda.Code.fromAsset(path.join(__dirname, "../assets/NextjsSite/custom-resource")),
code: lambda.Code.fromAsset(path.join(__dirname, "../support/edge-function-code-replacer")),
layers: [this.awsCliLayer],

@@ -448,3 +449,3 @@ runtime: lambda.Runtime.PYTHON_3_7,

const uploader = new lambda.Function(this, "S3Uploader", {
code: lambda.Code.fromAsset(path.join(__dirname, "../dist/base-site-custom-resource")),
code: lambda.Code.fromAsset(path.join(__dirname, "../support/base-site-custom-resource")),
layers: [this.awsCliLayer],

@@ -555,3 +556,3 @@ runtime: lambda.Runtime.PYTHON_3_7,

const result = spawn.sync("node", [
path.join(__dirname, "../assets/NextjsSite/build/build.cjs"),
path.join(__dirname, "../support/sls-nextjs-site-build-helper/build.cjs"),
"--path",

@@ -990,15 +991,12 @@ path.resolve(sitePath),

registerSiteEnvironment() {
const environmentOutputs = {};
for (const [key, value] of Object.entries(this.props.environment || {})) {
const outputId = `SstSiteEnv_${key}`;
const output = new CfnOutput(this, outputId, { value });
environmentOutputs[key] = Stack.of(this).getLogicalId(output);
SiteEnv.append({
path: this.props.path,
output: Stack.of(this).getLogicalId(output),
environment: key,
stack: Stack.of(this).stackName,
});
}
const app = this.node.root;
app.registerSiteEnvironment({
id: this.node.id,
path: this.props.path,
stack: Stack.of(this).node.id,
environmentOutputs,
});
}

@@ -1005,0 +1003,0 @@ normalizeRuntime(runtime) {

@@ -281,2 +281,3 @@ import path from "path";

});
fn._disableBind = true;
fn.attachPermissions([this.cdk.cluster]);

@@ -283,0 +284,0 @@ this.migratorFunction = fn;

@@ -1,134 +0,4 @@

import { Construct } from "constructs";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as route53 from "aws-cdk-lib/aws-route53";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as acm from "aws-cdk-lib/aws-certificatemanager";
import { SSTConstruct } from "./Construct.js";
import { BaseSiteDomainProps, BaseSiteCdkDistributionProps } from "./BaseSite.js";
import { Permissions } from "./util/permission.js";
import { FunctionBindingProps } from "./util/functionBinding.js";
export interface RemixDomainProps extends BaseSiteDomainProps {
}
export interface RemixCdkDistributionProps extends BaseSiteCdkDistributionProps {
}
export interface RemixSiteProps {
/**
* The Remix app server is deployed to a Lambda function in a single region. Alternatively, you can enable this option to deploy to Lambda@Edge.
*
* @default false
*/
edge?: boolean;
/**
* Path to the directory where the website source is located.
*/
path: string;
/**
* The customDomain for this website. SST supports domains that are hosted
* either on [Route 53](https://aws.amazon.com/route53/) or externally.
*
* Note that you can also migrate externally hosted domains to Route 53 by
* [following this guide](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/MigratingDNS.html).
*
* @example
* ```js {3}
* new RemixSite(stack, "RemixSite", {
* path: "path/to/site",
* customDomain: "domain.com",
* });
* ```
*
* ```js {3-6}
* new RemixSite(stack, "RemixSite", {
* path: "path/to/site",
* customDomain: {
* domainName: "domain.com",
* domainAlias: "www.domain.com",
* hostedZone: "domain.com"
* },
* });
* ```
*/
customDomain?: string | RemixDomainProps;
/**
* An object with the key being the environment variable name.
*
* @example
* ```js {3-6}
* new RemixSite(stack, "RemixSite", {
* path: "path/to/site",
* environment: {
* API_URL: api.url,
* USER_POOL_CLIENT: auth.cognitoUserPoolClient.userPoolClientId,
* },
* });
* ```
*/
environment?: Record<string, string>;
/**
* When running `sst start`, a placeholder site is deployed. This is to ensure
* that the site content remains unchanged, and subsequent `sst start` can
* start up quickly.
*
* @example
* ```js {3}
* new RemixSite(stack, "RemixSite", {
* path: "path/to/site",
* disablePlaceholder: true,
* });
* ```
*/
disablePlaceholder?: boolean;
defaults?: {
function?: {
timeout?: number;
memorySize?: number;
permissions?: Permissions;
};
};
/**
* While deploying, SST waits for the CloudFront cache invalidation process to finish. This ensures that the new content will be served once the deploy command finishes. However, this process can sometimes take more than 5 mins. For non-prod environments it might make sense to pass in `false`. That'll skip waiting for the cache to invalidate and speed up the deploy process.
*/
waitForInvalidation?: boolean;
cdk?: {
/**
* Allows you to override default id for this construct.
*/
id?: string;
/**
* Allows you to override default settings this construct uses internally to ceate the bucket
*/
bucket?: s3.BucketProps | s3.IBucket;
/**
* Pass in a value to override the default settings this construct uses to
* create the CDK `Distribution` internally.
*/
distribution?: RemixCdkDistributionProps;
/**
* Override the default CloudFront cache policies created internally.
*/
cachePolicies?: {
/**
* Override the CloudFront cache policy properties for browser build files.
*/
buildCachePolicy?: cloudfront.ICachePolicy;
/**
* Override the CloudFront cache policy properties for "public" folder
* static files.
*
* Note: This will not include the browser build files, which have a seperate
* cache policy; @see `buildCachePolicy`.
*/
staticsCachePolicy?: cloudfront.ICachePolicy;
/**
* Override the CloudFront cache policy properties for responses from the
* server rendering Lambda.
*
* @note The default cache policy that is used in the abscene of this property
* is one that performs no caching of the server response.
*/
serverCachePolicy?: cloudfront.ICachePolicy;
};
};
}
import { SsrSite } from "./SsrSite.js";
import { EdgeFunction } from "./EdgeFunction.js";
/**

@@ -147,147 +17,13 @@ * The `RemixSite` construct is a higher level CDK construct that makes it easy to create a Remix app.

*/
export declare class RemixSite extends Construct implements SSTConstruct {
readonly id: string;
/**
* The default CloudFront cache policy properties for browser build files.
*/
static buildCachePolicyProps: cloudfront.CachePolicyProps;
/**
* The default CloudFront cache policy properties for "public" folder
* static files.
*
* @note This policy is not applied to the browser build files; they have a seperate
* cache policy; @see `buildCachePolicyProps`.
*/
static staticsCachePolicyProps: cloudfront.CachePolicyProps;
/**
* The default CloudFront cache policy properties for responses from the
* server rendering Lambda.
*
* @note By default no caching is performed on the server rendering Lambda response.
*/
static serverCachePolicyProps: cloudfront.CachePolicyProps;
/**
* Exposes CDK instances created within the construct.
*/
readonly cdk: {
/**
* The internally created CDK `Function` instance. Not available in the "edge" mode.
*/
function?: lambda.Function;
/**
* The internally created CDK `Bucket` instance.
*/
bucket: s3.Bucket;
/**
* The internally created CDK `Distribution` instance.
*/
distribution: cloudfront.Distribution;
/**
* The Route 53 hosted zone for the custom domain.
*/
hostedZone?: route53.IHostedZone;
/**
* The AWS Certificate Manager certificate for the custom domain.
*/
certificate?: acm.ICertificate;
export declare class RemixSite extends SsrSite {
protected initBuildConfig(): {
serverBuildOutputFile: string;
clientBuildOutputDir: string;
clientBuildVersionedSubDir: string;
siteStub: string;
};
private props;
/**
* Determines if a placeholder site should be deployed instead. We will set
* this to `true` by default when performing local development, although the
* user can choose to override this value.
*/
private isPlaceholder;
/**
* The root SST directory used for builds.
*/
private sstBuildDir;
/**
* The remix site config. It contains user configuration overrides which we
* will need to consider when resolving Remix's build output.
*/
private remixConfig;
private serverLambdaForEdge?;
private serverLambdaForRegional?;
private awsCliLayer;
constructor(scope: Construct, id: string, props: RemixSiteProps);
/**
* The CloudFront URL of the website.
*/
get url(): string;
/**
* If the custom domain is enabled, this is the URL of the website with the
* custom domain.
*/
get customDomainUrl(): string | undefined;
/**
* The ARN of the internally created S3 Bucket.
*/
get bucketArn(): string;
/**
* The name of the internally created S3 Bucket.
*/
get bucketName(): string;
/**
* The ID of the internally created CloudFront Distribution.
*/
get distributionId(): string;
/**
* The domain name of the internally created CloudFront Distribution.
*/
get distributionDomain(): string;
/**
* Attaches the given list of permissions to allow the Remix server side
* rendering to access other AWS resources.
*
* @example
* ```js {5}
* const site = new RemixSite(stack, "Site", {
* path: "path/to/site",
* });
*
* site.attachPermissions(["sns"]);
* ```
*/
attachPermissions(permissions: Permissions): void;
getConstructMetadata(): {
type: "RemixSite";
data: {
distributionId: string;
customDomainUrl: string | undefined;
};
};
/** @internal */
getFunctionBinding(): FunctionBindingProps;
private buildApp;
private runNpmBuild;
private createStaticsS3Assets;
private createStaticsS3AssetsWithStub;
private createS3Bucket;
private createS3Deployment;
private createServerLambdaBundleForRegional;
private createServerLambdaBundleForEdge;
private createServerLambdaBundle;
private createServerLambdaBundleWithStub;
private createServerFunctionForRegional;
private createServerFunctionForEdge;
private validateCloudFrontDistributionSettings;
private createCloudFrontDistributionForRegional;
private createCloudFrontDistributionForEdge;
private createCloudFrontDistributionForStub;
private buildDistributionDomainNames;
private buildDistributionDefaultBehaviorForRegional;
private buildDistributionDefaultBehaviorForEdge;
private buildDistributionStaticBehaviors;
private createCloudFrontBuildAssetsCachePolicy;
private createCloudFrontStaticsCachePolicy;
private createCloudFrontServerCachePolicy;
private createCloudFrontInvalidation;
protected validateCustomDomainSettings(): void;
protected lookupHostedZone(): route53.IHostedZone | undefined;
private createCertificate;
protected createRoute53Records(): void;
private registerSiteEnvironment;
private readRemixConfig;
private generateBuildId;
protected createFunctionForRegional(): lambda.Function;
protected createFunctionForEdge(): EdgeFunction;
}

@@ -0,31 +1,13 @@

import fs from "fs";
import url from "url";
import path from "path";
import url from "url";
import fs from "fs";
import glob from "glob";
import crypto from "crypto";
import spawn from "cross-spawn";
import * as esbuild from "esbuild";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
import { Construct } from "constructs";
import { Fn, Duration, CfnOutput, RemovalPolicy, CustomResource, } from "aws-cdk-lib";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as iam from "aws-cdk-lib/aws-iam";
import { Duration, RemovalPolicy } from "aws-cdk-lib";
import * as logs from "aws-cdk-lib/aws-logs";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as route53 from "aws-cdk-lib/aws-route53";
import * as s3Assets from "aws-cdk-lib/aws-s3-assets";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as acm from "aws-cdk-lib/aws-certificatemanager";
import { AwsCliLayer } from "aws-cdk-lib/lambda-layer-awscli";
import * as origins from "aws-cdk-lib/aws-cloudfront-origins";
import * as route53Targets from "aws-cdk-lib/aws-route53-targets";
import * as route53Patterns from "aws-cdk-lib/aws-route53-patterns";
import { Stack } from "./Stack.js";
import { isCDKConstruct } from "./Construct.js";
import { Logger } from "../logger.js";
import { SsrSite } from "./SsrSite.js";
import { EdgeFunction } from "./EdgeFunction.js";
import { buildErrorResponsesForRedirectToIndex, } from "./BaseSite.js";
import { attachPermissionsToRole } from "./util/permission.js";
import { ENVIRONMENT_PLACEHOLDER, getParameterPath, } from "./util/functionBinding.js";
import { Logger } from "../logger.js";
const __dirname = url.fileURLToPath(new URL(".", import.meta.url));

@@ -45,366 +27,37 @@ /**

*/
export class RemixSite extends Construct {
constructor(scope, id, props) {
super(scope, props.cdk?.id || id);
this.id = id;
const app = scope.node.root;
try {
this.isPlaceholder =
(app.local || app.skipBuild) && !props.disablePlaceholder;
this.sstBuildDir = app.buildDir;
this.props = props;
this.cdk = {};
this.awsCliLayer = new AwsCliLayer(this, "AwsCliLayer");
this.registerSiteEnvironment();
// Prepare app
if (this.isPlaceholder) {
// Minimal configuration for the placeholder site
this.remixConfig = {};
}
else {
this.remixConfig = this.readRemixConfig();
this.buildApp();
}
// Create Bucket which will be utilised to contain the statics
this.cdk.bucket = this.createS3Bucket();
// Create Server functions
if (props.edge) {
const bundlePath = this.isPlaceholder
? this.createServerLambdaBundleWithStub()
: this.createServerLambdaBundleForEdge();
this.serverLambdaForEdge = this.createServerFunctionForEdge(bundlePath);
}
else {
const bundlePath = this.isPlaceholder
? this.createServerLambdaBundleWithStub()
: this.createServerLambdaBundleForRegional();
this.serverLambdaForRegional =
this.createServerFunctionForRegional(bundlePath);
this.cdk.function = this.serverLambdaForRegional;
}
// Create Custom Domain
this.validateCustomDomainSettings();
this.cdk.hostedZone = this.lookupHostedZone();
this.cdk.certificate = this.createCertificate();
// Create S3 Deployment
const assets = this.isPlaceholder
? this.createStaticsS3AssetsWithStub()
: this.createStaticsS3Assets();
const s3deployCR = this.createS3Deployment(assets);
// Create CloudFront
this.validateCloudFrontDistributionSettings();
if (props.edge) {
this.cdk.distribution = this.isPlaceholder
? this.createCloudFrontDistributionForStub()
: this.createCloudFrontDistributionForEdge();
}
else {
this.cdk.distribution = this.isPlaceholder
? this.createCloudFrontDistributionForStub()
: this.createCloudFrontDistributionForRegional();
}
this.cdk.distribution.node.addDependency(s3deployCR);
// Invalidate CloudFront
const invalidationCR = this.createCloudFrontInvalidation();
invalidationCR.node.addDependency(this.cdk.distribution);
// Connect Custom Domain to CloudFront Distribution
this.createRoute53Records();
export class RemixSite extends SsrSite {
initBuildConfig() {
const { path: sitePath } = this.props;
const configDefaults = {
assetsBuildDirectory: "public/build",
publicPath: "/build/",
serverBuildPath: "build/index.js",
serverBuildTarget: "node-cjs",
};
// Validate config path
const configPath = path.resolve(sitePath, "remix.config.js");
if (!fs.existsSync(configPath)) {
throw new Error(`Could not find "remix.config.js" at expected path "${configPath}".`);
}
catch (error) {
throw error;
}
}
/////////////////////
// Public Properties
/////////////////////
/**
* The CloudFront URL of the website.
*/
get url() {
return `https://${this.cdk.distribution.distributionDomainName}`;
}
/**
* If the custom domain is enabled, this is the URL of the website with the
* custom domain.
*/
get customDomainUrl() {
const { customDomain } = this.props;
if (!customDomain) {
return;
}
if (typeof customDomain === "string") {
return `https://${customDomain}`;
}
else {
return `https://${customDomain.domainName}`;
}
}
/**
* The ARN of the internally created S3 Bucket.
*/
get bucketArn() {
return this.cdk.bucket.bucketArn;
}
/**
* The name of the internally created S3 Bucket.
*/
get bucketName() {
return this.cdk.bucket.bucketName;
}
/**
* The ID of the internally created CloudFront Distribution.
*/
get distributionId() {
return this.cdk.distribution.distributionId;
}
/**
* The domain name of the internally created CloudFront Distribution.
*/
get distributionDomain() {
return this.cdk.distribution.distributionDomainName;
}
/////////////////////
// Public Methods
/////////////////////
/**
* Attaches the given list of permissions to allow the Remix server side
* rendering to access other AWS resources.
*
* @example
* ```js {5}
* const site = new RemixSite(stack, "Site", {
* path: "path/to/site",
* });
*
* site.attachPermissions(["sns"]);
* ```
*/
attachPermissions(permissions) {
if (this.serverLambdaForRegional) {
attachPermissionsToRole(this.serverLambdaForRegional.role, permissions);
}
this.serverLambdaForEdge?.attachPermissions(permissions);
}
getConstructMetadata() {
return {
type: "RemixSite",
data: {
distributionId: this.cdk.distribution.distributionId,
customDomainUrl: this.customDomainUrl,
},
// Load config
const userConfig = require(configPath);
const config = {
...configDefaults,
...userConfig,
};
}
/** @internal */
getFunctionBinding() {
const app = this.node.root;
// Validate config
Object.keys(configDefaults).forEach((key) => {
const k = key;
if (config[k] !== configDefaults[k]) {
throw new Error(`RemixSite: remix.config.js "${key}" must be "${configDefaults[k]}".`);
}
});
return {
clientPackage: "site",
variables: {
url: {
// Do not set real value b/c we don't want to make the Lambda function
// depend on the Site. B/c often the site depends on the Api, causing
// a CloudFormation circular dependency if the Api and the Site belong
// to different stacks.
environment: ENVIRONMENT_PLACEHOLDER,
parameter: this.customDomainUrl || this.url,
},
},
permissions: {
"ssm:GetParameters": [
`arn:aws:ssm:${app.region}:${app.account}:parameter${getParameterPath(this, "url")}`,
],
},
serverBuildOutputFile: "build/index.js",
clientBuildOutputDir: "public",
clientBuildVersionedSubDir: "build",
siteStub: path.resolve(__dirname, "../support/remix-site-html-stub"),
};
}
/////////////////////
// Build App
/////////////////////
buildApp() {
// Build
const app = this.node.root;
if (!app.isRunningSSTTest()) {
this.runNpmBuild();
}
// Validate build output exists
const serverBuildFile = path.join(this.props.path, this.remixConfig.serverBuildPath);
if (!fs.existsSync(serverBuildFile)) {
throw new Error(`No server build output found at "${serverBuildFile}"`);
}
}
runNpmBuild() {
// Given that Remix apps tend to involve concatenation of other commands
// such as Tailwind compilation, we feel that it is safest to target the
// "build" script for the app in order to ensure all outputs are generated.
const { path: sitePath } = this.props;
// validate site path exists
if (!fs.existsSync(sitePath)) {
throw new Error(`No path found at "${path.resolve(sitePath)}"`);
}
// Ensure that the site has a build script defined
if (!fs.existsSync(path.join(sitePath, "package.json"))) {
throw new Error(`No package.json found at "${sitePath}".`);
}
const packageJson = JSON.parse(fs.readFileSync(path.join(sitePath, "package.json")).toString());
if (!packageJson.scripts || !packageJson.scripts.build) {
throw new Error(`No "build" script found within package.json in "${sitePath}".`);
}
// Run build
Logger.debug(`Running "npm build" script`);
const buildResult = spawn.sync("npm", ["run", "build"], {
cwd: sitePath,
stdio: "inherit",
env: {
...process.env,
},
});
if (buildResult.status !== 0) {
throw new Error('The app "build" script failed.');
}
}
/////////////////////
// Bundle S3 Assets
/////////////////////
createStaticsS3Assets() {
const app = this.node.root;
const fileSizeLimit = app.isRunningSSTTest()
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: "sstTestFileSizeLimitOverride" not exposed in props
this.props.sstTestFileSizeLimitOverride || 200
: 200;
// First we need to create zip files containing the statics
const script = path.resolve(__dirname, "../support/base-site-archiver.cjs");
const zipOutDir = path.resolve(path.join(this.sstBuildDir, `RemixSite-${this.node.id}-${this.node.addr}`));
// Remove zip dir to ensure no partX.zip remain from previous build
fs.rmSync(zipOutDir, { recursive: true, force: true });
const result = spawn.sync("node", [
script,
path.join(this.props.path, "public"),
zipOutDir,
`${fileSizeLimit}`,
], {
stdio: "inherit",
});
if (result.status !== 0) {
throw new Error(`There was a problem generating the assets package.`);
}
// Create S3 Assets for each zip file
const assets = [];
for (let partId = 0;; partId++) {
const zipFilePath = path.join(zipOutDir, `part${partId}.zip`);
if (!fs.existsSync(zipFilePath)) {
break;
}
assets.push(new s3Assets.Asset(this, `Asset${partId}`, {
path: zipFilePath,
}));
}
return assets;
}
createStaticsS3AssetsWithStub() {
return [
new s3Assets.Asset(this, "Asset", {
path: path.resolve(__dirname, "../assets/RemixSite/site-sub"),
}),
];
}
createS3Bucket() {
const { cdk } = this.props;
// cdk.bucket is an imported construct
if (cdk?.bucket && isCDKConstruct(cdk?.bucket)) {
return cdk.bucket;
}
// cdk.bucket is a prop
else {
const bucketProps = cdk?.bucket;
return new s3.Bucket(this, "S3Bucket", {
publicReadAccess: true,
autoDeleteObjects: true,
removalPolicy: RemovalPolicy.DESTROY,
...bucketProps,
});
}
}
createS3Deployment(assets) {
// Create a Lambda function that will be doing the uploading
const uploader = new lambda.Function(this, "S3Uploader", {
code: lambda.Code.fromAsset(path.join(__dirname, "../support/base-site-custom-resource")),
layers: [this.awsCliLayer],
runtime: lambda.Runtime.PYTHON_3_7,
handler: "s3-upload.handler",
timeout: Duration.minutes(15),
memorySize: 1024,
});
this.cdk.bucket.grantReadWrite(uploader);
assets.forEach((asset) => asset.grantRead(uploader));
// Create the custom resource function
const handler = new lambda.Function(this, "S3Handler", {
code: lambda.Code.fromAsset(path.join(__dirname, "../support/base-site-custom-resource")),
layers: [this.awsCliLayer],
runtime: lambda.Runtime.PYTHON_3_7,
handler: "s3-handler.handler",
timeout: Duration.minutes(15),
memorySize: 1024,
environment: {
UPLOADER_FUNCTION_NAME: uploader.functionName,
},
});
this.cdk.bucket.grantReadWrite(handler);
uploader.grantInvoke(handler);
// Build file options
const fileOptions = [];
const publicPath = path.join(this.props.path, "public");
for (const item of fs.readdirSync(publicPath)) {
if (item === "build") {
fileOptions.push({
exclude: "*",
include: "build/*",
cacheControl: "public,max-age=31536000,immutable",
});
}
else {
const itemPath = path.join(publicPath, item);
fileOptions.push({
exclude: "*",
include: fs.statSync(itemPath).isDirectory()
? `${item}/*`
: `${item}`,
cacheControl: "public,max-age=3600,must-revalidate",
});
}
}
// Create custom resource
return new CustomResource(this, "S3Deployment", {
serviceToken: handler.functionArn,
resourceType: "Custom::SSTBucketDeployment",
properties: {
Sources: assets.map((asset) => ({
BucketName: asset.s3BucketName,
ObjectKey: asset.s3ObjectKey,
})),
DestinationBucketName: this.cdk.bucket.bucketName,
FileOptions: (fileOptions || []).map(({ exclude, include, cacheControl }) => {
return [
"--exclude",
exclude,
"--include",
include,
"--cache-control",
cacheControl,
];
}),
},
});
}
/////////////////////
// Bundle Lambda Server
/////////////////////
createServerLambdaBundleForRegional() {
const templatePath = path.resolve(__dirname, "../assets/RemixSite/server-lambda/regional-server.js");
return this.createServerLambdaBundle(templatePath);
}
createServerLambdaBundleForEdge() {
const templatePath = path.resolve(__dirname, "../assets/RemixSite/server-lambda/edge-server.js");
return this.createServerLambdaBundle(templatePath);
}
createServerLambdaBundle(templatePath) {
createServerLambdaBundle(wrapperFile) {
// Create a Lambda@Edge handler for the Remix server bundle.

@@ -418,42 +71,22 @@ //

// possibly consider this at a later date.
let serverPath;
if (this.remixConfig.server != null) {
// In this path we are using a user-specified server. We'll assume
// that they have built an appropriate CloudFront Lambda@Edge handler
// for the Remix "core server build".
//
// The Remix compiler will have bundled their server implementation into
// the server build ouput path. We therefore need to reference the
// serverBuildPath from the remix.config.js to determine our server build
// entry.
//
// Supporting this customisation of the server supports two cases:
// 1. It enables power users to override our own implementation with an
// implementation that meets their specific needs.
// 2. It provides us with the required stepping stone to enable a
// "Serverless Stack" template within the Remix repository (we would
// still need to reach out to the Remix team for this).
serverPath = this.remixConfig.serverBuildPath;
}
else {
// In this path we are assuming that the Remix build only outputs the
// "core server build". We can safely assume this as we have guarded the
// remix.config.js to ensure it matches our expectations for the build
// configuration.
// We need to ensure that the "core server build" is wrapped with an
// appropriate Lambda@Edge handler. We will utilise an internal asset
// template to create this wrapper within the "core server build" output
// directory.
Logger.debug(`Creating Lambda@Edge handler for server`);
// Resolve the path to create the server lambda handler at.
serverPath = path.join(this.props.path, "build/server.js");
// Write the server lambda
fs.copyFileSync(templatePath, serverPath);
}
// In this path we are assuming that the Remix build only outputs the
// "core server build". We can safely assume this as we have guarded the
// remix.config.js to ensure it matches our expectations for the build
// configuration.
// We need to ensure that the "core server build" is wrapped with an
// appropriate Lambda@Edge handler. We will utilise an internal asset
// template to create this wrapper within the "core server build" output
// directory.
Logger.debug(`Creating Lambda@Edge handler for server`);
// Resolve the path to create the server lambda handler at.
const serverPath = path.join(this.props.path, "build/server.js");
// Write the server lambda
const templatePath = path.resolve(__dirname, `../support/remix-site-function/${wrapperFile}`);
fs.copyFileSync(templatePath, serverPath);
Logger.debug(`Bundling server`);
// Create a directory that we will use to create the bundled version
// of the "core server build" along with our custom Lamba server handler.
const outputPath = path.resolve(path.join(this.sstBuildDir, `RemixSiteLambdaServer-${this.node.id}-${this.node.addr}`));
const outputPath = path.resolve(path.join(this.sstBuildDir, `RemixSiteFunction-${this.node.id}-${this.node.addr}`));
// Copy the Remix polyfil to the server build directory
const polyfillSource = path.resolve(__dirname, "../assets/RemixSite/server-lambda/polyfill.js");
const polyfillSource = path.resolve(__dirname, "../support/remix-site-function/polyfill.js");
const polyfillDest = path.join(this.props.path, "build/polyfill.js");

@@ -483,7 +116,10 @@ fs.copyFileSync(polyfillSource, polyfillDest);

// Use existing stub bundle in assets
return path.resolve(__dirname, "../assets/RemixSite/server-lambda-stub");
return path.resolve(__dirname, "../support/ssr-site-function-stub");
}
createServerFunctionForRegional(bundlePath) {
createFunctionForRegional() {
const { defaults, environment } = this.props;
const fn = new lambda.Function(this, `ServerFunction`, {
const bundlePath = this.isPlaceholder
? this.createServerLambdaBundleWithStub()
: this.createServerLambdaBundle("regional-server.js");
return new lambda.Function(this, `ServerFunction`, {
description: "Server handler for Remix",

@@ -501,12 +137,9 @@ handler: "server.handler",

});
// Attach permission
this.cdk.bucket.grantReadWrite(fn.role);
if (defaults?.function?.permissions) {
attachPermissionsToRole(fn.role, defaults.function.permissions);
}
return fn;
}
createServerFunctionForEdge(bundlePath) {
createFunctionForEdge() {
const { defaults, environment } = this.props;
const fn = new EdgeFunction(this, `Server`, {
const bundlePath = this.isPlaceholder
? this.createServerLambdaBundleWithStub()
: this.createServerLambdaBundle("edge-server.js");
return new EdgeFunction(this, `Server`, {
scopeOverride: this,

@@ -520,457 +153,3 @@ bundlePath,

});
// Attach permission
this.cdk.bucket.grantReadWrite(fn.role);
return fn;
}
/////////////////////
// CloudFront Distribution
/////////////////////
validateCloudFrontDistributionSettings() {
const { cdk } = this.props;
const cfDistributionProps = cdk?.distribution || {};
if (cfDistributionProps.certificate) {
throw new Error(`Do not configure the "cfDistribution.certificate". Use the "customDomain" to configure the RemixSite domain certificate.`);
}
if (cfDistributionProps.domainNames) {
throw new Error(`Do not configure the "cfDistribution.domainNames". Use the "customDomain" to configure the RemixSite domain.`);
}
}
createCloudFrontDistributionForRegional() {
const { cdk } = this.props;
const cfDistributionProps = cdk?.distribution || {};
const s3Origin = new origins.S3Origin(this.cdk.bucket);
return new cloudfront.Distribution(this, "Distribution", {
// these values can be overwritten by cfDistributionProps
defaultRootObject: "",
// Override props.
...cfDistributionProps,
// these values can NOT be overwritten by cfDistributionProps
domainNames: this.buildDistributionDomainNames(),
certificate: this.cdk.certificate,
defaultBehavior: this.buildDistributionDefaultBehaviorForRegional(),
additionalBehaviors: {
...this.buildDistributionStaticBehaviors(s3Origin),
...(cfDistributionProps.additionalBehaviors || {}),
},
});
}
createCloudFrontDistributionForEdge() {
const { cdk } = this.props;
const cfDistributionProps = cdk?.distribution || {};
const s3Origin = new origins.S3Origin(this.cdk.bucket);
return new cloudfront.Distribution(this, "Distribution", {
// these values can be overwritten by cfDistributionProps
defaultRootObject: "",
// Override props.
...cfDistributionProps,
// these values can NOT be overwritten by cfDistributionProps
domainNames: this.buildDistributionDomainNames(),
certificate: this.cdk.certificate,
defaultBehavior: this.buildDistributionDefaultBehaviorForEdge(s3Origin),
additionalBehaviors: {
...this.buildDistributionStaticBehaviors(s3Origin),
...(cfDistributionProps.additionalBehaviors || {}),
},
});
}
createCloudFrontDistributionForStub() {
return new cloudfront.Distribution(this, "Distribution", {
defaultRootObject: "index.html",
errorResponses: buildErrorResponsesForRedirectToIndex("index.html"),
domainNames: this.buildDistributionDomainNames(),
certificate: this.cdk.certificate,
defaultBehavior: {
origin: new origins.S3Origin(this.cdk.bucket),
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
});
}
buildDistributionDomainNames() {
const { customDomain } = this.props;
const domainNames = [];
if (!customDomain) {
// no domain
}
else if (typeof customDomain === "string") {
domainNames.push(customDomain);
}
else {
domainNames.push(customDomain.domainName);
}
return domainNames;
}
buildDistributionDefaultBehaviorForRegional() {
const { cdk } = this.props;
const cfDistributionProps = cdk?.distribution || {};
const fnUrl = this.serverLambdaForRegional.addFunctionUrl({
authType: lambda.FunctionUrlAuthType.NONE,
});
const serverCachePolicy = cdk?.cachePolicies?.serverCachePolicy ??
this.createCloudFrontServerCachePolicy();
return {
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
origin: new origins.HttpOrigin(Fn.parseDomainName(fnUrl.url)),
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS,
compress: true,
cachePolicy: serverCachePolicy,
...(cfDistributionProps.defaultBehavior || {}),
};
}
buildDistributionDefaultBehaviorForEdge(origin) {
const { cdk } = this.props;
const cfDistributionProps = cdk?.distribution || {};
const serverCachePolicy = cdk?.cachePolicies?.serverCachePolicy ??
this.createCloudFrontServerCachePolicy();
return {
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
origin,
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS,
compress: true,
cachePolicy: serverCachePolicy,
...(cfDistributionProps.defaultBehavior || {}),
// concatenate edgeLambdas
edgeLambdas: [
{
includeBody: true,
eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST,
functionVersion: this.serverLambdaForEdge.currentVersion,
},
...(cfDistributionProps.defaultBehavior?.edgeLambdas || []),
],
};
}
buildDistributionStaticBehaviors(origin) {
const { cdk } = this.props;
// Build cache policies
const buildCachePolicy = cdk?.cachePolicies?.buildCachePolicy ??
this.createCloudFrontBuildAssetsCachePolicy();
const staticsCachePolicy = cdk?.cachePolicies?.staticsCachePolicy ??
this.createCloudFrontStaticsCachePolicy();
// Create additional behaviours for statics
const publicPath = path.join(this.props.path, "public");
const staticsBehaviours = {};
const staticBehaviourOptions = {
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
origin,
allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS,
compress: true,
cachePolicy: staticsCachePolicy,
};
// Add behaviour for browser build
staticsBehaviours["build/*"] = {
...staticBehaviourOptions,
cachePolicy: buildCachePolicy,
};
// Add behaviour for public folder statics (excluding build)
const publicDir = path.join(this.props.path, "public");
for (const item of fs.readdirSync(publicDir)) {
if (item === "build") {
continue;
}
const itemPath = path.join(publicDir, item);
if (fs.statSync(itemPath).isDirectory()) {
staticsBehaviours[`${item}/*`] = staticBehaviourOptions;
}
else {
staticsBehaviours[item] = staticBehaviourOptions;
}
}
return staticsBehaviours;
}
createCloudFrontBuildAssetsCachePolicy() {
return new cloudfront.CachePolicy(this, "BuildCache", RemixSite.buildCachePolicyProps);
}
createCloudFrontStaticsCachePolicy() {
return new cloudfront.CachePolicy(this, "StaticsCache", RemixSite.staticsCachePolicyProps);
}
createCloudFrontServerCachePolicy() {
return new cloudfront.CachePolicy(this, "ServerCache", RemixSite.serverCachePolicyProps);
}
createCloudFrontInvalidation() {
// Create a Lambda function that will be doing the invalidation
const invalidator = new lambda.Function(this, "CloudFrontInvalidator", {
code: lambda.Code.fromAsset(path.join(__dirname, "../support/base-site-custom-resource")),
layers: [this.awsCliLayer],
runtime: lambda.Runtime.PYTHON_3_7,
handler: "cf-invalidate.handler",
timeout: Duration.minutes(15),
memorySize: 1024,
});
// Grant permissions to invalidate CF Distribution
invalidator.addToRolePolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
"cloudfront:GetInvalidation",
"cloudfront:CreateInvalidation",
],
resources: ["*"],
}));
const waitForInvalidation = this.isPlaceholder
? false
: this.props.waitForInvalidation === false
? false
: true;
return new CustomResource(this, "CloudFrontInvalidation", {
serviceToken: invalidator.functionArn,
resourceType: "Custom::SSTCloudFrontInvalidation",
properties: {
BuildId: this.isPlaceholder ? "live" : this.generateBuildId(),
DistributionId: this.cdk.distribution.distributionId,
// TODO: Ignore the browser build path as it may speed up invalidation
DistributionPaths: ["/*"],
WaitForInvalidation: waitForInvalidation,
},
});
}
/////////////////////
// Custom Domain
/////////////////////
validateCustomDomainSettings() {
const { customDomain } = this.props;
if (!customDomain) {
return;
}
if (typeof customDomain === "string") {
return;
}
if (customDomain.isExternalDomain === true) {
if (!customDomain.cdk?.certificate) {
throw new Error(`A valid certificate is required when "isExternalDomain" is set to "true".`);
}
if (customDomain.domainAlias) {
throw new Error(`Domain alias is only supported for domains hosted on Amazon Route 53. Do not set the "customDomain.domainAlias" when "isExternalDomain" is enabled.`);
}
if (customDomain.hostedZone) {
throw new Error(`Hosted zones can only be configured for domains hosted on Amazon Route 53. Do not set the "customDomain.hostedZone" when "isExternalDomain" is enabled.`);
}
}
}
lookupHostedZone() {
const { customDomain } = this.props;
// Skip if customDomain is not configured
if (!customDomain) {
return;
}
let hostedZone;
if (typeof customDomain === "string") {
hostedZone = route53.HostedZone.fromLookup(this, "HostedZone", {
domainName: customDomain,
});
}
else if (customDomain.cdk?.hostedZone) {
hostedZone = customDomain.cdk.hostedZone;
}
else if (typeof customDomain.hostedZone === "string") {
hostedZone = route53.HostedZone.fromLookup(this, "HostedZone", {
domainName: customDomain.hostedZone,
});
}
else if (typeof customDomain.domainName === "string") {
// Skip if domain is not a Route53 domain
if (customDomain.isExternalDomain === true) {
return;
}
hostedZone = route53.HostedZone.fromLookup(this, "HostedZone", {
domainName: customDomain.domainName,
});
}
else {
hostedZone = customDomain.hostedZone;
}
return hostedZone;
}
createCertificate() {
const { customDomain } = this.props;
if (!customDomain) {
return;
}
let acmCertificate;
// HostedZone is set for Route 53 domains
if (this.cdk.hostedZone) {
if (typeof customDomain === "string") {
acmCertificate = new acm.DnsValidatedCertificate(this, "Certificate", {
domainName: customDomain,
hostedZone: this.cdk.hostedZone,
region: "us-east-1",
});
}
else if (customDomain.cdk?.certificate) {
acmCertificate = customDomain.cdk.certificate;
}
else {
acmCertificate = new acm.DnsValidatedCertificate(this, "Certificate", {
domainName: customDomain.domainName,
hostedZone: this.cdk.hostedZone,
region: "us-east-1",
});
}
}
// HostedZone is NOT set for non-Route 53 domains
else {
if (typeof customDomain !== "string") {
acmCertificate = customDomain.cdk?.certificate;
}
}
return acmCertificate;
}
createRoute53Records() {
const { customDomain } = this.props;
if (!customDomain || !this.cdk.hostedZone) {
return;
}
let recordName;
let domainAlias;
if (typeof customDomain === "string") {
recordName = customDomain;
}
else {
recordName = customDomain.domainName;
domainAlias = customDomain.domainAlias;
}
// Create DNS record
const recordProps = {
recordName,
zone: this.cdk.hostedZone,
target: route53.RecordTarget.fromAlias(new route53Targets.CloudFrontTarget(this.cdk.distribution)),
};
new route53.ARecord(this, "AliasRecord", recordProps);
new route53.AaaaRecord(this, "AliasRecordAAAA", recordProps);
// Create Alias redirect record
if (domainAlias) {
new route53Patterns.HttpsRedirect(this, "Redirect", {
zone: this.cdk.hostedZone,
recordNames: [domainAlias],
targetDomain: recordName,
});
}
}
/////////////////////
// Helper Functions
/////////////////////
registerSiteEnvironment() {
const environmentOutputs = {};
for (const [key, value] of Object.entries(this.props.environment || {})) {
const outputId = `SstSiteEnv_${key}`;
const output = new CfnOutput(this, outputId, { value });
environmentOutputs[key] = Stack.of(this).getLogicalId(output);
}
const root = this.node.root;
root.registerSiteEnvironment({
id: this.node.id,
path: this.props.path,
stack: Stack.of(this).node.id,
environmentOutputs,
});
}
readRemixConfig() {
const { path: sitePath } = this.props;
const configDefaults = {
assetsBuildDirectory: "public/build",
publicPath: "/build/",
serverBuildPath: "build/index.js",
serverBuildTarget: "node-cjs",
server: undefined,
};
// Validate config path
const configPath = path.resolve(sitePath, "remix.config.js");
if (!fs.existsSync(configPath)) {
throw new Error(`Could not find "remix.config.js" at expected path "${configPath}".`);
}
// Load config
const userConfig = require(configPath);
const config = {
...configDefaults,
...userConfig,
};
// Validate config
Object.keys(configDefaults)
.filter((key) => key !== "server")
.forEach((key) => {
const k = key;
if (config[k] !== configDefaults[k]) {
throw new Error(`RemixSite: remix.config.js "${key}" must be "${configDefaults[k]}".`);
}
});
return config;
}
generateBuildId() {
// We will generate a hash based on the contents of the "public" folder
// which will be used to indicate if we need to invalidate our CloudFront
// cache. As the browser build files are always uniquely hash in their
// filenames according to their content we can ignore the browser build
// files.
// The below options are needed to support following symlinks when building zip files:
// - nodir: This will prevent symlinks themselves from being copied into the zip.
// - follow: This will follow symlinks and copy the files within.
const globOptions = {
dot: true,
nodir: true,
follow: true,
ignore: ["build/**"],
cwd: path.resolve(this.props.path, "public"),
};
const files = glob.sync("**", globOptions);
const hash = crypto.createHash("sha1");
for (const file of files) {
hash.update(file);
}
const buildId = hash.digest("hex");
Logger.debug(`Generated build ID ${buildId}`);
return buildId;
}
}
/**
* The default CloudFront cache policy properties for browser build files.
*/
RemixSite.buildCachePolicyProps = {
queryStringBehavior: cloudfront.CacheQueryStringBehavior.none(),
headerBehavior: cloudfront.CacheHeaderBehavior.none(),
cookieBehavior: cloudfront.CacheCookieBehavior.none(),
// The browser build file names all contain unique hashes based on their
// content, we can therefore aggressively cache them as we shouldn't hit
// unexpected collisions.
defaultTtl: Duration.days(365),
maxTtl: Duration.days(365),
minTtl: Duration.days(365),
enableAcceptEncodingBrotli: true,
enableAcceptEncodingGzip: true,
comment: "SST RemixSite Browser Build Default Cache Policy",
};
/**
* The default CloudFront cache policy properties for "public" folder
* static files.
*
* @note This policy is not applied to the browser build files; they have a seperate
* cache policy; @see `buildCachePolicyProps`.
*/
RemixSite.staticsCachePolicyProps = {
queryStringBehavior: cloudfront.CacheQueryStringBehavior.none(),
headerBehavior: cloudfront.CacheHeaderBehavior.none(),
cookieBehavior: cloudfront.CacheCookieBehavior.none(),
defaultTtl: Duration.hours(1),
maxTtl: Duration.hours(1),
minTtl: Duration.hours(1),
enableAcceptEncodingBrotli: true,
enableAcceptEncodingGzip: true,
comment: "SST RemixSite Public Folder Default Cache Policy",
};
/**
* The default CloudFront cache policy properties for responses from the
* server rendering Lambda.
*
* @note By default no caching is performed on the server rendering Lambda response.
*/
RemixSite.serverCachePolicyProps = {
queryStringBehavior: cloudfront.CacheQueryStringBehavior.all(),
headerBehavior: cloudfront.CacheHeaderBehavior.none(),
cookieBehavior: cloudfront.CacheCookieBehavior.all(),
defaultTtl: Duration.seconds(0),
maxTtl: Duration.days(365),
minTtl: Duration.seconds(0),
enableAcceptEncodingBrotli: true,
enableAcceptEncodingGzip: true,
comment: "SST RemixSite Server Response Default Cache Policy",
};

@@ -28,5 +28,3 @@ import { Construct } from "constructs";

* The name of the index page (e.g. "index.html") of the website.
*
* @default "index.html"
*
* @example

@@ -48,3 +46,3 @@ * ```js

* Note that, if the error pages are redirected to the index page, the HTTP status code is set to 200. This is necessary for single page apps, that handle 404 pages on the client side.
*
* @default redirect_to_index_page
* @example

@@ -60,3 +58,3 @@ * ```js

* The command for building the website
*
* @default no build command
* @example

@@ -72,7 +70,7 @@ * ```js

* The directory with the content that will be uploaded to the S3 bucket. If a `buildCommand` is provided, this is usually where the build output is generated. The path is relative to the [`path`](#path) where the website source is located.
*
* @default entire "path" directory
* @example
* ```js
* new StaticSite(stack, "Site", {
* buildOutput: "dist",
* buildOutput: "build",
* });

@@ -85,2 +83,17 @@ * ```

*
* Defaults to no cache control for HTML files, and a 1 year cache control for JS/CSS files.
* ```js
* [
* {
* exclude: "*",
* include: "*.html",
* cacheControl: "max-age=0,no-cache,no-store,must-revalidate",
* },
* {
* exclude: "*",
* include: ["*.js", "*.css"],
* cacheControl: "max-age=31536000,public,immutable",
* },
* ]
* ```
* @example

@@ -90,7 +103,7 @@ * ```js

* buildOutput: "dist",
* fileOptions: {
* fileOptions: [{
* exclude: "*",
* include: "*.js",
* cacheControl: "max-age=31536000,public,immutable",
* }
* }]
* });

@@ -105,3 +118,3 @@ * ```

* ```js
* new StaticSite(stack, "ReactSite", {
* new StaticSite(stack, "frontend", {
* replaceValues: [

@@ -130,3 +143,3 @@ * {

* ```js
* new StaticSite(stack, "Site", {
* new StaticSite(stack, "frontend", {
* path: "path/to/src",

@@ -139,3 +152,3 @@ * customDomain: "domain.com",

* ```js
* new StaticSite(stack, "Site", {
* new StaticSite(stack, "frontend", {
* path: "path/to/src",

@@ -156,3 +169,3 @@ * customDomain: {

* ```js
* new StaticSite(stack, "ReactSite", {
* new StaticSite(stack, "frontend", {
* environment: {

@@ -173,3 +186,3 @@ * REACT_APP_API_URL: api.url,

* ```js
* new StaticSite(stack, "ReactSite", {
* new StaticSite(stack, "frontend", {
* purge: false

@@ -187,3 +200,3 @@ * });

* ```js
* new StaticSite(stack, "ReactSite", {
* new StaticSite(stack, "frontend", {
* disablePlaceholder: true

@@ -194,10 +207,23 @@ * });

disablePlaceholder?: boolean;
vite?: {
/**
* The path where code-gen should place the type definition for environment variables
* @default "src/sst-env.d.ts"
* @example
* ```js
* new StaticSite(stack, "frontend", {
* vite: {
* types: "./other/path/sst-env.d.ts",
* }
* });
* ```
*/
types?: string;
};
/**
* While deploying, SST waits for the CloudFront cache invalidation process to finish. This ensures that the new content will be served once the deploy command finishes. However, this process can sometimes take more than 5 mins. For non-prod environments it might make sense to pass in `false`. That'll skip waiting for the cache to invalidate and speed up the deploy process.
*
* @default true
*
* @example
* ```js
* new StaticSite(stack, "ReactSite", {
* new StaticSite(stack, "frontend", {
* waitForInvalidation: false

@@ -324,2 +350,3 @@ * });

getFunctionBinding(): FunctionBindingProps;
private generateViteTypes;
private buildApp;

@@ -326,0 +353,0 @@ private bundleAssets;

@@ -25,2 +25,3 @@ import path from "path";

import { useProject } from "../app.js";
import { SiteEnv } from "../site-env.js";
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));

@@ -64,2 +65,4 @@ /////////////////////

this.validateCustomDomainSettings();
// Generate Vite types
this.generateViteTypes();
// Build app

@@ -161,2 +164,30 @@ this.buildApp();

}
generateViteTypes() {
const { path: sitePath, environment } = this.props;
// Build the path
let typesPath = this.props.vite?.types;
if (!typesPath) {
if (fs.existsSync(path.join(sitePath, "vite.config.js")) ||
fs.existsSync(path.join(sitePath, "vite.config.ts"))) {
typesPath = "src/sst-env.d.ts";
}
}
if (!typesPath) {
return;
}
// Create type file
const filePath = path.resolve(path.join(sitePath, typesPath));
const content = `/// <reference types="vite/client" />
interface ImportMetaEnv {
${Object.keys(environment || {})
.map((key) => ` readonly ${key}: string`)
.join("\n")}
}
interface ImportMeta {
readonly env: ImportMetaEnv
}`;
const fileDir = path.dirname(filePath);
fs.mkdirSync(fileDir, { recursive: true });
fs.writeFileSync(filePath, content);
}
buildApp() {

@@ -276,3 +307,14 @@ if (this.isPlaceholder) {

createS3Deployment() {
const { fileOptions } = this.props;
const fileOptions = this.props.fileOptions || [
{
exclude: "*",
include: "*.html",
cacheControl: "max-age=0,no-cache,no-store,must-revalidate",
},
{
exclude: "*",
include: ["*.js", "*.css"],
cacheControl: "max-age=31536000,public,immutable",
},
];
// Create a Lambda function that will be doing the uploading

@@ -349,2 +391,5 @@ const uploader = new lambda.Function(this, "S3Uploader", {

}
if (errorPage && cdk?.distribution?.errorResponses) {
throw new Error(`Cannot configure the "cfDistribution.errorResponses" when "errorPage" is passed in. Use one or the other to configure the behavior for error pages.`);
}
// Build domainNames

@@ -372,8 +417,5 @@ const domainNames = [];

}
else if (errorPage) {
if (cdk?.distribution?.errorResponses) {
throw new Error(`Cannot configure the "cfDistribution.errorResponses" when "errorPage" is passed in. Use one or the other to configure the behavior for error pages.`);
}
else {
errorResponses =
errorPage === "redirect_to_index_page"
errorPage === "redirect_to_index_page" || errorPage === undefined
? buildErrorResponsesForRedirectToIndex(indexPage)

@@ -583,16 +625,13 @@ : buildErrorResponsesFor404ErrorPage(errorPage);

registerSiteEnvironment() {
const environmentOutputs = {};
for (const [key, value] of Object.entries(this.props.environment || {})) {
const outputId = `SstSiteEnv_${key}`;
const output = new CfnOutput(this, outputId, { value });
environmentOutputs[key] = Stack.of(this).getLogicalId(output);
SiteEnv.append({
path: this.props.path,
output: Stack.of(this).getLogicalId(output),
environment: key,
stack: Stack.of(this).stackName,
});
}
const root = this.node.root;
root.registerSiteEnvironment({
id: this.node.id,
path: this.props.path,
stack: Stack.of(this).node.id,
environmentOutputs,
});
}
}

@@ -6,3 +6,3 @@ export declare const Context: {

};
declare function create<C>(cb?: () => C): {
declare function create<C>(cb?: (() => C) | string): {
use(): C;

@@ -9,0 +9,0 @@ provide(value: C): void;

@@ -12,3 +12,3 @@ export const Context = {

function create(cb) {
const id = Symbol();
const id = Symbol(cb?.toString());
return {

@@ -18,4 +18,4 @@ use() {

if (!result) {
if (!cb)
throw new Error(`"${String(id)}" context must be provided.`);
if (!cb || typeof cb === "string")
throw new Error(`"${String(id)}" context was not provided.`);
state.tracking.push(id);

@@ -22,0 +22,0 @@ const value = cb();

import { Context } from "./context.js";
const RequestContext = Context.create();
const RequestContext = Context.create("RequestContext");
export function useEvent(type) {

@@ -4,0 +4,0 @@ const ctx = RequestContext.use();

@@ -37,2 +37,5 @@ import "@aws-sdk/types";

retryStrategy: new StandardRetryStrategy(async () => 10000, {
retryDecider: (options) => {
return true;
},
delayDecider: (_, attempts) => {

@@ -39,0 +42,0 @@ return Math.min(1.5 ** attempts * 100, 5000);

@@ -35,2 +35,5 @@ import { IoTClient, DescribeEndpointCommand } from "@aws-sdk/client-iot";

const fragments = new Map();
device.on("error", (err) => {
Logger.debug("IoT error", err);
});
device.on("message", (_topic, buffer) => {

@@ -37,0 +40,0 @@ const fragment = JSON.parse(buffer.toString());

export declare const Logger: {
debug(...parts: any[]): Promise<void>;
debug(...parts: any[]): void;
};
import fs from "fs/promises";
import path from "path";
import { useProject } from "./app.js";
import { Context } from "./context/context.js";
let previous = new Date();
const filePath = path.join("debug.log");
const file = await fs.open(filePath, "w");
const useFile = Context.memo(async () => {
const project = useProject();
const filePath = path.join(project.paths.out, "debug.log");
const file = await fs.open(filePath, "w");
return file;
});
export const Logger = {
async debug(...parts) {
debug(...parts) {
const now = new Date();

@@ -21,4 +27,6 @@ const diff = now.getTime() - previous.getTime();

];
file.write(line.join(" ") + "\n");
useFile().then((file) => {
file.write(line.join(" ") + "\n");
});
},
};
{
"name": "sst",
"version": "0.0.0-20221121200755",
"version": "0.0.0-20221121213401",
"bin": {

@@ -16,3 +16,6 @@ "sst": "./cli/sst.js"

"exports": {
"./constructs": "./constructs/index.js"
"./constructs": "./constructs/index.js",
"./context": "./context/index.js",
"./node/*": "./node/*/index.js",
"./*": "./*"
},

@@ -56,6 +59,9 @@ "homepage": "https://sst.dev",

"express": "^4.18.2",
"fast-jwt": "^1.6.1",
"glob": "^8.0.3",
"graphql-helix": "^1.12.0",
"immer": "9",
"ink": "^3.2.0",
"ink-spinner": "^4.0.3",
"openid-client": "^5.1.8",
"ora": "^6.1.2",

@@ -88,12 +94,29 @@ "promptly": "^3.2.0",

"kysely-data-api": "^0.1.4",
"tsx": "^3.12.1"
"tsx": "^3.12.1",
"vitest": "^0.15.1",
"graphql": "^16.5.0",
"@sls-next/lambda-at-edge": "^3.7.0",
"@babel/core": "^7.0.0-0"
},
"optionalDependencies": {
"peerDependencies": {
"@babel/core": "^7.0.0-0",
"@sls-next/lambda-at-edge": "^3.7.0"
"@sls-next/lambda-at-edge": "^3.7.0",
"graphql": "^16.5.0"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"@sls-next/lambda-at-edge": {
"optional": true
},
"graphql": {
"optional": true
}
},
"gitHead": "8ac2d0abc11d5de721c87658bb445e3d6c211dcf",
"scripts": {
"build": "tsc && cp package.json ./dist"
"build": "tsc",
"watch": "tsc -w"
}
}

@@ -45,3 +45,3 @@ import { FunctionProps } from "../constructs/Function.js";

handler: string;
}>;
} | undefined>;
};

@@ -53,5 +53,5 @@ interface Artifact {

export declare const useFunctionBuilder: () => {
artifact: (functionID: string) => Artifact | Promise<Artifact>;
build: (functionID: string) => Promise<Artifact>;
artifact: (functionID: string) => Artifact | Promise<Artifact | undefined>;
build: (functionID: string) => Promise<Artifact | undefined>;
};
export {};

@@ -32,25 +32,28 @@ import { Context } from "../context/context.js";

await fs.mkdir(out, { recursive: true });
const built = await handler.build({
functionID,
out,
mode,
props: func,
});
if (mode === "deploy" && func.copyFiles) {
func.copyFiles.forEach((entry) => {
const fromPath = path.join(project.paths.root, entry.from);
const to = entry.to || entry.from;
if (path.isAbsolute(to))
throw new Error(`Copy destination path "${to}" must be relative`);
const toPath = path.join(out, to);
fs.cp(fromPath, toPath, {
recursive: true,
try {
const built = await handler.build({
functionID,
out,
mode,
props: func,
});
if (mode === "deploy" && func.copyFiles) {
func.copyFiles.forEach((entry) => {
const fromPath = path.join(project.paths.root, entry.from);
const to = entry.to || entry.from;
if (path.isAbsolute(to))
throw new Error(`Copy destination path "${to}" must be relative`);
const toPath = path.join(out, to);
fs.cp(fromPath, toPath, {
recursive: true,
});
});
});
}
bus.publish("function.built", { functionID });
return {
...built,
out,
};
}
bus.publish("function.built", { functionID });
return {
...built,
out,
};
catch { }
},

@@ -71,2 +74,4 @@ };

const result = await handlers.build(functionID, "start");
if (!result)
return;
artifacts.set(functionID, result);

@@ -73,0 +78,0 @@ return artifacts.get(functionID);

@@ -30,3 +30,8 @@ import path from "path";

const worker = new Worker(url.fileURLToPath(new URL("../support/nodejs-runtime/index.mjs", import.meta.url)), {
env: input.environment,
env: {
...process.env,
...input.environment,
IS_LOCAL: "true",
},
execArgv: ["--enable-source-maps"],
workerData: input,

@@ -40,2 +45,5 @@ stderr: true,

});
worker.stderr.on("data", (data) => {
workers.stdout(input.workerID, data.toString());
});
worker.on("exit", () => workers.exited(input.workerID));

@@ -93,2 +101,3 @@ threads.set(input.workerID, worker);

],
keepNames: true,
bundle: true,

@@ -119,3 +128,3 @@ metafile: true,

outfile: target,
sourcemap: nodejs.sourcemap,
sourcemap: input.mode === "start" ? "linked" : nodejs.sourcemap,
minify: nodejs.minify,

@@ -125,26 +134,32 @@ ...override,

// Install node_modules
if (input.mode === "deploy" && nodejs.install?.length) {
async function find(dir) {
if (nodejs.install?.length) {
async function find(dir, target) {
if (dir === "/")
throw new VisibleError("Could not found a package.json file");
if (await fs
.access(path.join(dir, "package.json"))
.access(path.join(dir, target))
.then(() => true)
.catch(() => false))
return dir;
return find(path.join(dir, ".."));
return find(path.join(dir, ".."), target);
}
const src = await find(parsed.dir);
const json = JSON.parse(await fs
.readFile(path.join(src, "package.json"))
.then((x) => x.toString()));
fs.writeFile(path.join(input.out, "package.json"), JSON.stringify({
dependencies: Object.fromEntries(nodejs.install?.map((x) => [x, json.dependencies?.[x] || "*"])),
}));
await new Promise((resolve) => {
const process = exec("npm install", {
cwd: input.out,
if (input.mode === "deploy") {
const src = await find(parsed.dir, "package.json");
const json = JSON.parse(await fs
.readFile(path.join(src, "package.json"))
.then((x) => x.toString()));
fs.writeFile(path.join(input.out, "package.json"), JSON.stringify({
dependencies: Object.fromEntries(nodejs.install?.map((x) => [x, json.dependencies?.[x] || "*"])),
}));
await new Promise((resolve) => {
const process = exec("npm install", {
cwd: input.out,
});
process.on("exit", () => resolve());
});
process.on("exit", () => resolve());
});
}
if (input.mode === "start") {
const dir = path.join(await find(parsed.dir, "package.json"), "node_modules");
await fs.symlink(path.resolve(dir), path.resolve(path.join(input.out, "node_modules")), "dir");
}
}

@@ -151,0 +166,0 @@ cache[input.functionID] = result;

@@ -30,2 +30,4 @@ import { Context } from "../context/context.js";

const build = await builder.artifact(evt.properties.functionID);
if (!build)
return;
await handler.startWorker({

@@ -32,0 +34,0 @@ ...build,

@@ -87,2 +87,6 @@ import { useBus } from "../bus.js";

catch (ex) {
Logger.debug("Failed to deploy stack", stack.id, ex);
if (ex.message === "No updates are to be performed.") {
return monitor(stack.stackName);
}
bus.publish("stack.status", {

@@ -89,0 +93,0 @@ stackID: stack.stackName,

@@ -56,38 +56,41 @@ import { CloudFormationClient, DescribeStackResourcesCommand, DescribeStacksCommand, } from "@aws-sdk/client-cloudformation";

while (true) {
const [describe, resources] = await Promise.all([
cfn.send(new DescribeStacksCommand({
StackName: stack,
})),
cfn.send(new DescribeStackResourcesCommand({
StackName: stack,
})),
]);
bus.publish("stack.resources", {
stackID: stack,
resources: resources.StackResources,
});
for (const resource of resources.StackResources || []) {
if (resource.ResourceStatusReason)
errors[resource.LogicalResourceId] = resource.ResourceStatusReason;
}
const [first] = describe.Stacks || [];
if (first) {
if (lastStatus !== first.StackStatus && first.StackStatus) {
lastStatus = first.StackStatus;
bus.publish("stack.status", {
stackID: stack,
status: first.StackStatus,
});
Logger.debug(first);
if (isFinal(first.StackStatus)) {
return {
try {
const [describe, resources] = await Promise.all([
cfn.send(new DescribeStacksCommand({
StackName: stack,
})),
cfn.send(new DescribeStackResourcesCommand({
StackName: stack,
})),
]);
bus.publish("stack.resources", {
stackID: stack,
resources: resources.StackResources,
});
for (const resource of resources.StackResources || []) {
if (resource.ResourceStatusReason)
errors[resource.LogicalResourceId] = resource.ResourceStatusReason;
}
const [first] = describe.Stacks || [];
if (first) {
if (lastStatus !== first.StackStatus && first.StackStatus) {
lastStatus = first.StackStatus;
bus.publish("stack.status", {
stackID: stack,
status: first.StackStatus,
outputs: Object.fromEntries(first.Outputs?.map((o) => [o.OutputKey, o.OutputValue]) || []),
errors: isFailed(first.StackStatus) ? errors : {},
};
});
Logger.debug(first);
if (isFinal(first.StackStatus)) {
return {
status: first.StackStatus,
outputs: Object.fromEntries(first.Outputs?.map((o) => [o.OutputKey, o.OutputValue]) || []),
errors: isFailed(first.StackStatus) ? errors : {},
};
}
}
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
await new Promise((resolve) => setTimeout(resolve, 1000));
catch (ex) { }
}
}
import type { CloudFormationStackArtifact } from "aws-cdk-lib/cx-api";
import { StackDeploymentResult } from "./monitor.js";
export declare function removeMany(stacks: CloudFormationStackArtifact[]): Promise<Record<string, {
status: "CREATE_IN_PROGRESS" | "DELETE_IN_PROGRESS" | "REVIEW_IN_PROGRESS" | "ROLLBACK_IN_PROGRESS" | "UPDATE_COMPLETE_CLEANUP_IN_PROGRESS" | "UPDATE_IN_PROGRESS" | "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS" | "UPDATE_ROLLBACK_IN_PROGRESS" | "CREATE_COMPLETE" | "UPDATE_COMPLETE" | "SKIPPED" | "CREATE_FAILED" | "DELETE_FAILED" | "ROLLBACK_FAILED" | "ROLLBACK_COMPLETE" | "UPDATE_FAILED" | "UPDATE_ROLLBACK_COMPLETE" | "UPDATE_ROLLBACK_FAILED" | "DEPENDENCY_FAILED";
outputs: {
[k: string]: string;
};
errors: Record<string, string>;
}>>;
export declare function remove(stack: CloudFormationStackArtifact): Promise<StackDeploymentResult>;
import { CloudFormationClient, DeleteStackCommand, } from "@aws-sdk/client-cloudformation";
import { useBus } from "../bus.js";
import { useAWSClient } from "../credentials.js";
import { useAWSClient, useAWSProvider } from "../credentials.js";
import { Logger } from "../logger.js";
import { monitor } from "./monitor.js";
import { monitor, isFailed } from "./monitor.js";
export async function removeMany(stacks) {
const { CloudFormationStackArtifact } = await import("aws-cdk-lib/cx-api");
await useAWSProvider();
const bus = useBus();
const complete = new Set();
const todo = new Set(stacks.map((s) => s.id));
const pending = new Set();
const results = {};
bus.subscribe("stack.updated", (evt) => {
pending.add(evt.properties.stackID);
});
return new Promise((resolve) => {
async function trigger() {
for (const stack of stacks) {
if (!todo.has(stack.id))
continue;
Logger.debug("Checking if", stack.id, "can be removed");
if (stacks.some((dependant) => {
if (complete.has(stack.id))
return false;
return dependant.dependencies?.map((d) => d.id).includes(stack.id);
}))
continue;
remove(stack).then((result) => {
results[stack.id] = result;
complete.add(stack.id);
if (isFailed(result.status))
stacks.forEach((s) => {
if (todo.delete(s.stackName)) {
complete.add(s.stackName);
results[s.id] = {
status: "DEPENDENCY_FAILED",
outputs: {},
errors: {},
};
bus.publish("stack.status", {
stackID: s.id,
status: "DEPENDENCY_FAILED",
});
}
});
if (complete.size === stacks.length) {
resolve(results);
}
trigger();
});
todo.delete(stack.id);
}
}
trigger();
});
}
export async function remove(stack) {

@@ -7,0 +59,0 @@ const bus = useBus();

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc