Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ packages/desktop/out
packages/desktop/.stage
packages/desktop/bundle.cjs
packages/desktop/public
packages/desktop/icon.png
packages/frontend/public/*.png

# Environment variables
.env
Expand Down
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ COPY pnpm-workspace.yaml /app/localstack-explorer-builder/.
COPY package.json /app/localstack-explorer-builder/.
COPY tsconfig.base.json /app/localstack-explorer-builder/.
COPY packages /app/localstack-explorer-builder/packages
COPY icons /app/localstack-explorer-builder/icons

FROM base AS build
WORKDIR /app/localstack-explorer-builder
Expand All @@ -28,4 +29,4 @@ WORKDIR /app/localstack-explorer
COPY --from=deps /app/localstack-explorer/node_modules ./node_modules
COPY --from=build /app/localstack-explorer-builder/packages/backend/dist/. .

ENTRYPOINT ["node", "index.js"]
ENTRYPOINT ["node", "server.js"]
Binary file added icons/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/favicon-16x16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/favicon-32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/icon-192x192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added icons/icon-512x512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
Binary file added icons/icon-desktop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 6 additions & 4 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
"type": "module",
"main": "dist/index.js",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"dev": "tsx watch src/server.ts",
"build": "tsc -p tsconfig.build.json",
"build:bundle": "tsup",
"start": "node dist/index.js",
"start": "node dist/server.js",
"test": "vitest run"
},
"dependencies": {
Expand All @@ -24,15 +24,17 @@
"@aws-sdk/util-dynamodb": "^3.996.2",
"@fastify/autoload": "^6.3.1",
"@fastify/cors": "^10.1.0",
"@fastify/static": "^8.1.0",
"@fastify/multipart": "^9.4.0",
"@fastify/static": "^8.1.0",
"env-schema": "^7.0.0",
"fastify": "^5.8.4",
"fastify-plugin": "^5.1.0",
"typebox": "^1.1.7"
},
"devDependencies": {
"@types/node": "^22.19.15",
"@vitest/coverage-v8": "3.2.4",
"testcontainers": "^11.11.0",
"tsup": "^8.5.0",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
Expand Down
56 changes: 0 additions & 56 deletions packages/backend/src/aws/clients.ts

This file was deleted.

27 changes: 25 additions & 2 deletions packages/backend/src/health.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import { config } from "./config.js";

interface LocalStackHealthResponse {
services?: Record<string, string>;
}

export async function checkLocalstackHealth(endpoint: string, region: string) {
try {
const controller = new AbortController();
Expand All @@ -13,13 +19,30 @@ export async function checkLocalstackHealth(endpoint: string, region: string) {
connected: false,
endpoint,
region,
services: [] as string[],
error: `HTTP ${response.status}`,
};
}

return { connected: true, endpoint, region };
const body = (await response.json()) as LocalStackHealthResponse;
const enabledSet = new Set<string>(config.enabledServices);
const activeServices = Object.entries(body.services ?? {})
.filter(
([name, status]) =>
enabledSet.has(name) &&
(status === "running" || status === "available"),
)
.map(([name]) => name);

return { connected: true, endpoint, region, services: activeServices };
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
return { connected: false, endpoint, region, error: message };
return {
connected: false,
endpoint,
region,
services: [] as string[],
error: message,
};
}
}
19 changes: 6 additions & 13 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
import autoload from "@fastify/autoload";
import cors from "@fastify/cors";
import fastifyStatic from "@fastify/static";
import Fastify from "fastify";
import Fastify, { type FastifyInstance } from "fastify";
import { config } from "./config.js";
import { checkLocalstackHealth } from "./health.js";
import clientCachePlugin from "./plugins/client-cache.js";
Expand All @@ -14,9 +14,11 @@ import { registerErrorHandler } from "./shared/errors.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

async function main() {
export async function buildApp(
options: { logger?: boolean } = {},
): Promise<FastifyInstance> {
const app = Fastify({
logger: true,
logger: options.logger ?? false,
});

// Register CORS
Expand Down Expand Up @@ -73,14 +75,5 @@ async function main() {
});
}

try {
await app.listen({ port: config.port, host: "0.0.0.0" });
app.log.info(`Server running on http://localhost:${config.port}`);
app.log.info(`Enabled services: ${config.enabledServices.join(", ")}`);
} catch (err) {
app.log.error(err);
process.exit(1);
}
return app;
}

main();
4 changes: 2 additions & 2 deletions packages/backend/src/plugins/iam/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ export async function iamRoutes(app: FastifyInstance) {
app.delete("/users/:userName/attached-policies/:policyArn", {
schema: {
response: {
200: DeleteResponseSchema,
200: MessageResponseSchema,
404: ErrorResponseSchema,
},
},
Expand Down Expand Up @@ -482,7 +482,7 @@ export async function iamRoutes(app: FastifyInstance) {
app.delete("/groups/:groupName/members/:userName", {
schema: {
response: {
200: DeleteResponseSchema,
200: MessageResponseSchema,
404: ErrorResponseSchema,
},
},
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/plugins/iam/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,7 @@ export class IAMService {

return {
versionId: resolvedVersionId ?? "",
isDefaultVersion: response.PolicyVersion?.IsDefaultVersion ?? false,
document,
};
} catch (err) {
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/plugins/s3/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ export async function s3Routes(app: FastifyInstance) {
const { bucketName } = request.params as { bucketName: string };
const { key } = request.query as { key: string };
const result = await service.downloadObject(bucketName, key);
/* v8 ignore next */
const filename = key.split("/").pop() ?? key;
return reply
.header("Content-Type", result.contentType)
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/plugins/sns/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export class SNSService {
const topics = (response.Topics ?? []).map((topic) => {
const topicArn = topic.TopicArn ?? "";
const parts = topicArn.split(":");
/* v8 ignore next */
const name = parts[parts.length - 1] ?? "";
return { topicArn, name };
});
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/plugins/sqs/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export class SQSService {
const queueUrls = response.QueueUrls ?? [];
const queues = queueUrls.map((url) => {
const parts = url.split("/");
/* v8 ignore next */
const queueName = parts[parts.length - 1] ?? "";
return { queueUrl: url, queueName };
});
Expand Down
22 changes: 22 additions & 0 deletions packages/backend/src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* v8 ignore start */
import { config } from "./config";
import { buildApp } from "./index";

async function main() {
const app = await buildApp({ logger: true });

try {
await app.listen({ port: config.port, host: "0.0.0.0" });
app.log.info(`Server running on http://localhost:${config.port}`);
app.log.info(`Enabled services: ${config.enabledServices.join(", ")}`);
} catch (err) {
app.log.error(err);
process.exit(1);
}
}

main().catch((err) => {
console.error("Error starting server:", err);
process.exit(1);
});
/* v8 ignore stop */
79 changes: 79 additions & 0 deletions packages/backend/test/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

const ALL_SERVICES = ["s3", "sqs", "sns", "iam", "cloudformation", "dynamodb"];

let mockEnabledServices = "s3,sqs,sns,iam,cloudformation,dynamodb";

vi.mock("env-schema", () => ({
envSchema: () => ({
PORT: 3001,
LOCALSTACK_ENDPOINT: "http://localhost:4566",
LOCALSTACK_REGION: "us-east-1",
ENABLED_SERVICES: mockEnabledServices,
}),
}));

beforeEach(() => {
vi.resetModules();
});

describe("config shape", () => {
it("exposes port, endpoint, region with correct types", async () => {
mockEnabledServices = "s3,sqs,sns,iam,cloudformation,dynamodb";
const { config } = await import("../src/config.js");

expect(config.port).toBe(3001);
expect(config.localstackEndpoint).toBe("http://localhost:4566");
expect(config.localstackRegion).toBe("us-east-1");
});

it("exposes all enabled services", async () => {
mockEnabledServices = "s3,sqs,sns,iam,cloudformation,dynamodb";
const { config } = await import("../src/config.js");

expect(config.enabledServices).toHaveLength(6);
for (const svc of config.enabledServices) {
expect(ALL_SERVICES).toContain(svc);
}
});
});

describe("parseEnabledServices", () => {
it("returns all services when ENABLED_SERVICES is empty", async () => {
mockEnabledServices = "";
const { config } = await import("../src/config.js");

expect(config.enabledServices).toHaveLength(ALL_SERVICES.length);
expect(config.enabledServices).toEqual(
expect.arrayContaining(ALL_SERVICES),
);
});

it("returns all services when ENABLED_SERVICES is whitespace only", async () => {
mockEnabledServices = " ";
const { config } = await import("../src/config.js");

expect(config.enabledServices).toHaveLength(ALL_SERVICES.length);
});

it("filters out unknown service names", async () => {
mockEnabledServices = "s3,unknown-service,dynamodb";
const { config } = await import("../src/config.js");

expect(config.enabledServices).toEqual(["s3", "dynamodb"]);
});

it("returns only listed services", async () => {
mockEnabledServices = "s3,sqs";
const { config } = await import("../src/config.js");

expect(config.enabledServices).toEqual(["s3", "sqs"]);
});

it("trims whitespace around service names", async () => {
mockEnabledServices = " s3 , sqs ";
const { config } = await import("../src/config.js");

expect(config.enabledServices).toEqual(["s3", "sqs"]);
});
});
Loading
Loading