costrix 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/costrix.js ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { main } = require("../src/cli");
4
+
5
+ main().catch((error) => {
6
+ const message = error && error.message ? error.message : String(error);
7
+ console.error(`[ERROR] Failed to scaffold project: ${message}`);
8
+ process.exit(1);
9
+ });
package/package.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "costrix",
3
+ "version": "1.0.0",
4
+ "description": "npx initializer for a TypeScript + Webpack + Jest project",
5
+ "bin": {
6
+ "costrix": "./bin/costrix.js"
7
+ },
8
+ "type": "commonjs",
9
+ "license": "MIT"
10
+ }
package/src/args.js ADDED
@@ -0,0 +1,42 @@
1
+ function parseCliArgs(argv) {
2
+ const flags = {
3
+ install: true,
4
+ dev: true,
5
+ help: false
6
+ };
7
+ let projectArg = "";
8
+
9
+ argv.forEach((arg) => {
10
+ if (arg === "--no-install") {
11
+ flags.install = false;
12
+ return;
13
+ }
14
+ if (arg === "--no-dev") {
15
+ flags.dev = false;
16
+ return;
17
+ }
18
+ if (arg === "--help" || arg === "-h") {
19
+ flags.help = true;
20
+ return;
21
+ }
22
+ if (!arg.startsWith("-") && projectArg === "") {
23
+ projectArg = arg;
24
+ }
25
+ });
26
+
27
+ return { projectArg, flags };
28
+ }
29
+
30
+ function printHelp() {
31
+ console.log("\nUsage:");
32
+ console.log(" costrix [app-name] [--no-install] [--no-dev]");
33
+ console.log("\nFlags:");
34
+ console.log(" --no-install Scaffold only, skip npm install");
35
+ console.log(" --no-dev Skip auto-starting dev server");
36
+ console.log(" -h, --help Show this help message\n");
37
+ }
38
+
39
+ module.exports = {
40
+ parseCliArgs,
41
+ printHelp
42
+ };
package/src/cli.js ADDED
@@ -0,0 +1,53 @@
1
+ const path = require("path");
2
+ const { parseCliArgs, printHelp } = require("./args");
3
+ const { askProjectArg } = require("./prompt");
4
+ const { scaffoldProject } = require("./scaffold");
5
+ const { runCommand } = require("./run");
6
+ const { printBanner, logInfo, logWarn, logSuccess, logError } = require("./logger");
7
+
8
+ async function main() {
9
+ printBanner();
10
+
11
+ const { projectArg: parsedProjectArg, flags } = parseCliArgs(process.argv.slice(2));
12
+
13
+ if (flags.help) {
14
+ printHelp();
15
+ return;
16
+ }
17
+
18
+ let projectArg = parsedProjectArg;
19
+ if (!projectArg) {
20
+ projectArg = await askProjectArg();
21
+ }
22
+
23
+ if (!projectArg) {
24
+ logError("No app name provided.");
25
+ process.exit(1);
26
+ }
27
+
28
+ const projectDir = path.resolve(process.cwd(), projectArg);
29
+ logInfo(`Preparing folder ${projectDir}`);
30
+ logInfo("Generating project files...");
31
+ scaffoldProject(projectDir, projectArg);
32
+
33
+ logSuccess(`Scaffold complete for ${projectArg}`);
34
+ logInfo(`Options: install=${String(flags.install)} dev=${String(flags.dev)}`);
35
+
36
+ const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
37
+
38
+ if (flags.install) {
39
+ await runCommand(npmCmd, ["install"], projectDir, "Installing dependencies");
40
+ } else {
41
+ logWarn("Skipping dependency installation (--no-install)");
42
+ }
43
+
44
+ if (flags.dev) {
45
+ await runCommand(npmCmd, ["run", "dev"], projectDir, "Starting development server");
46
+ } else {
47
+ logWarn("Skipping development server startup (--no-dev)");
48
+ }
49
+ }
50
+
51
+ module.exports = {
52
+ main
53
+ };
package/src/logger.js ADDED
@@ -0,0 +1,66 @@
1
+ const color = {
2
+ reset: "\x1b[0m",
3
+ cyan: "\x1b[36m",
4
+ blue: "\x1b[34m",
5
+ green: "\x1b[32m",
6
+ yellow: "\x1b[33m",
7
+ red: "\x1b[31m",
8
+ magenta: "\x1b[35m",
9
+ gray: "\x1b[90m"
10
+ };
11
+
12
+ const paint = (tone, text) => `${color[tone]}${text}${color.reset}`;
13
+
14
+ function printBanner() {
15
+ const lines = [
16
+ "",
17
+ paint("cyan", " ██████╗ ██████╗ ███████╗████████╗██████╗ ██╗██╗ ██╗"),
18
+ paint("cyan", "██╔════╝██╔═══██╗██╔════╝╚══██╔══╝██╔══██╗██║╚██╗██╔╝"),
19
+ paint("cyan", "██║ ██║ ██║███████╗ ██║ ██████╔╝██║ ╚███╔╝ "),
20
+ paint("cyan", "██║ ██║ ██║╚════██║ ██║ ██╔══██╗██║ ██╔██╗ "),
21
+ paint("cyan", "╚██████╗╚██████╔╝███████║ ██║ ██║ ██║██║██╔╝ ██╗"),
22
+ paint("cyan", " ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝"),
23
+ paint("gray", "Scaffold TypeScript projects with guardrails"),
24
+ paint("gray", "Created with care by Kaustubh"),
25
+ ""
26
+ ];
27
+ console.log(lines.join("\n"));
28
+ }
29
+
30
+ const logInfo = (text) => console.log(`${paint("cyan", "ℹ")} ${text}`);
31
+ const logStep = (text) => console.log(`${paint("blue", "🧩")} ${text}`);
32
+ const logSuccess = (text) => console.log(`${paint("green", "✅")} ${text}`);
33
+ const logWarn = (text) => console.log(`${paint("yellow", "⚠️")} ${text}`);
34
+ const logError = (text) => console.error(`${paint("red", "❌")} ${text}`);
35
+
36
+ function startLoader(label) {
37
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
38
+ let index = 0;
39
+ process.stdout.write(`${paint("cyan", frames[index])} ${label}`);
40
+
41
+ const timer = setInterval(() => {
42
+ index = (index + 1) % frames.length;
43
+ process.stdout.write(`\r${paint("cyan", frames[index])} ${label}`);
44
+ }, 90);
45
+
46
+ const stop = (icon, tone, text) => {
47
+ clearInterval(timer);
48
+ process.stdout.write(`\r${paint(tone, icon)} ${text}\n`);
49
+ };
50
+
51
+ return {
52
+ done: (text) => stop("✅", "green", text),
53
+ fail: (text) => stop("❌", "red", text),
54
+ info: (text) => stop("ℹ", "cyan", text)
55
+ };
56
+ }
57
+
58
+ module.exports = {
59
+ printBanner,
60
+ logInfo,
61
+ logStep,
62
+ logSuccess,
63
+ logWarn,
64
+ logError,
65
+ startLoader
66
+ };
package/src/prompt.js ADDED
@@ -0,0 +1,19 @@
1
+ const readline = require("readline");
2
+
3
+ function askProjectArg() {
4
+ return new Promise((resolve) => {
5
+ const rl = readline.createInterface({
6
+ input: process.stdin,
7
+ output: process.stdout
8
+ });
9
+
10
+ rl.question("Enter app name (folder): ", (answer) => {
11
+ rl.close();
12
+ resolve(answer.trim());
13
+ });
14
+ });
15
+ }
16
+
17
+ module.exports = {
18
+ askProjectArg
19
+ };
package/src/run.js ADDED
@@ -0,0 +1,55 @@
1
+ const { spawn } = require("child_process");
2
+ const { logInfo, logError, startLoader } = require("./logger");
3
+
4
+ function spawnWithOptions(command, args, cwd, useShell) {
5
+ return spawn(command, args, {
6
+ cwd,
7
+ stdio: "inherit",
8
+ shell: useShell
9
+ });
10
+ }
11
+
12
+ function runCommand(command, args, cwd, label) {
13
+ const loader = startLoader(label);
14
+
15
+ const execute = (useShell) =>
16
+ new Promise((resolve, reject) => {
17
+ const child = spawnWithOptions(command, args, cwd, useShell);
18
+
19
+ child.on("error", (error) => {
20
+ reject(error);
21
+ });
22
+
23
+ child.on("close", (code) => {
24
+ if (code === 0) {
25
+ resolve();
26
+ return;
27
+ }
28
+ reject(new Error(`${label} failed with exit code ${String(code)}`));
29
+ });
30
+ });
31
+
32
+ return execute(false)
33
+ .then(() => {
34
+ loader.done(`${label} complete`);
35
+ })
36
+ .catch((error) => {
37
+ if (process.platform === "win32" && error && error.code === "EINVAL") {
38
+ loader.info(`${label}: retrying with Windows shell fallback...`);
39
+ logInfo(`${label}: retrying with Windows shell fallback...`);
40
+ return execute(true).then(() => {
41
+ loader.done(`${label} complete`);
42
+ });
43
+ }
44
+ throw error;
45
+ })
46
+ .catch((error) => {
47
+ loader.fail(`${label} failed`);
48
+ logError(`${label} failed: ${error.message}`);
49
+ throw error;
50
+ });
51
+ }
52
+
53
+ module.exports = {
54
+ runCommand
55
+ };
@@ -0,0 +1,83 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { logWarn, logStep } = require("./logger");
4
+ const { toPackageName } = require("./utils");
5
+
6
+ const TEMPLATE_ROOT = path.resolve(__dirname, "..", "templates");
7
+
8
+ function ensureDirForFile(filePath) {
9
+ const dir = path.dirname(filePath);
10
+ fs.mkdirSync(dir, { recursive: true });
11
+ }
12
+
13
+ function writeTextFileSafe(targetPath, content) {
14
+ if (fs.existsSync(targetPath)) {
15
+ return false;
16
+ }
17
+
18
+ ensureDirForFile(targetPath);
19
+ fs.writeFileSync(targetPath, content, "utf8");
20
+ return true;
21
+ }
22
+
23
+ function copyFileSafe(sourcePath, targetPath) {
24
+ if (fs.existsSync(targetPath)) {
25
+ return false;
26
+ }
27
+
28
+ ensureDirForFile(targetPath);
29
+ fs.copyFileSync(sourcePath, targetPath);
30
+ return true;
31
+ }
32
+
33
+ function renderFile(relativePath, sourceContent, appName) {
34
+ if (relativePath === "package.json") {
35
+ const parsed = JSON.parse(sourceContent);
36
+ parsed.name = toPackageName(appName);
37
+ return `${JSON.stringify(parsed, null, 2)}\n`;
38
+ }
39
+
40
+ return sourceContent;
41
+ }
42
+
43
+ function copyTemplateDir(templateDir, projectDir, appName, rootTemplateDir) {
44
+ const entries = fs.readdirSync(templateDir, { withFileTypes: true });
45
+
46
+ entries.forEach((entry) => {
47
+ const sourcePath = path.join(templateDir, entry.name);
48
+ const relativePath = path.relative(rootTemplateDir, sourcePath);
49
+ const targetPath = path.join(projectDir, relativePath);
50
+
51
+ if (entry.isDirectory()) {
52
+ fs.mkdirSync(targetPath, { recursive: true });
53
+ copyTemplateDir(sourcePath, projectDir, appName, rootTemplateDir);
54
+ return;
55
+ }
56
+
57
+ const normalizedRelativePath = relativePath.replace(/\\/g, "/");
58
+ let created = false;
59
+ if (normalizedRelativePath === "package.json") {
60
+ const rawContent = fs.readFileSync(sourcePath, "utf8");
61
+ const content = renderFile(normalizedRelativePath, rawContent, appName);
62
+ created = writeTextFileSafe(targetPath, content);
63
+ } else {
64
+ created = copyFileSafe(sourcePath, targetPath);
65
+ }
66
+
67
+ if (created) {
68
+ logStep(`Created ${normalizedRelativePath}`);
69
+ return;
70
+ }
71
+
72
+ logWarn(`Skipped ${normalizedRelativePath} (already exists)`);
73
+ });
74
+ }
75
+
76
+ function scaffoldProject(projectDir, appName) {
77
+ fs.mkdirSync(projectDir, { recursive: true });
78
+ copyTemplateDir(TEMPLATE_ROOT, projectDir, appName, TEMPLATE_ROOT);
79
+ }
80
+
81
+ module.exports = {
82
+ scaffoldProject
83
+ };
package/src/utils.js ADDED
@@ -0,0 +1,16 @@
1
+ function toPackageName(input) {
2
+ const normalized = String(input || "")
3
+ .trim()
4
+ .toLowerCase()
5
+ .replace(/[_\s]+/g, "-")
6
+ .replace(/[^a-z0-9.-]/g, "-")
7
+ .replace(/-+/g, "-")
8
+ .replace(/^-+/, "")
9
+ .replace(/-+$/, "");
10
+
11
+ return normalized || "my-costrix-app";
12
+ }
13
+
14
+ module.exports = {
15
+ toPackageName
16
+ };
@@ -0,0 +1,69 @@
1
+ const js = require("@eslint/js");
2
+ const globals = require("globals");
3
+ const tsParser = require("@typescript-eslint/parser");
4
+ const tsPlugin = require("@typescript-eslint/eslint-plugin");
5
+
6
+ module.exports = [
7
+ {
8
+ ignores: ["dist/**", "coverage/**", "node_modules/**"]
9
+ },
10
+ js.configs.recommended,
11
+ {
12
+ files: ["src/**/*.ts", "tests/**/*.ts"],
13
+ languageOptions: {
14
+ parser: tsParser,
15
+ parserOptions: {
16
+ project: ["./tsconfig.eslint.json"],
17
+ tsconfigRootDir: __dirname,
18
+ ecmaVersion: "latest",
19
+ sourceType: "module"
20
+ },
21
+ globals: {
22
+ ...globals.browser,
23
+ ...globals.node,
24
+ ...globals.jest
25
+ }
26
+ },
27
+ plugins: {
28
+ "@typescript-eslint": tsPlugin
29
+ },
30
+ rules: {
31
+ ...tsPlugin.configs.recommended.rules,
32
+ ...tsPlugin.configs["recommended-type-checked"].rules,
33
+ "@typescript-eslint/no-explicit-any": "error",
34
+ "@typescript-eslint/no-unsafe-assignment": "error",
35
+ "@typescript-eslint/no-unsafe-argument": "error",
36
+ "@typescript-eslint/no-unsafe-member-access": "error",
37
+ "@typescript-eslint/no-unsafe-call": "error",
38
+ "@typescript-eslint/no-unsafe-return": "error",
39
+ "@typescript-eslint/explicit-function-return-type": [
40
+ "error",
41
+ { allowExpressions: true, allowTypedFunctionExpressions: true }
42
+ ],
43
+ "@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
44
+ "@typescript-eslint/no-floating-promises": "error",
45
+ "@typescript-eslint/no-misused-promises": "error",
46
+ "@typescript-eslint/strict-boolean-expressions": "error",
47
+ "@typescript-eslint/switch-exhaustiveness-check": "error",
48
+ "@typescript-eslint/no-unused-vars": [
49
+ "error",
50
+ { argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" }
51
+ ],
52
+ "@typescript-eslint/require-await": "error",
53
+ "no-console": ["error", { allow: ["warn", "error"] }]
54
+ }
55
+ },
56
+ {
57
+ files: ["*.js", "*.cjs", "scripts/**/*.cjs"],
58
+ languageOptions: {
59
+ ecmaVersion: "latest",
60
+ sourceType: "commonjs",
61
+ globals: {
62
+ ...globals.node
63
+ }
64
+ },
65
+ rules: {
66
+ "no-console": "off"
67
+ }
68
+ }
69
+ ];
@@ -0,0 +1,20 @@
1
+ /** @type {import('jest').Config} */
2
+ module.exports = {
3
+ preset: "ts-jest",
4
+ testEnvironment: "jsdom",
5
+ roots: ["<rootDir>/tests"],
6
+ collectCoverage: true,
7
+ collectCoverageFrom: ["src/**/*.ts", "!src/**/*.d.ts"],
8
+ coverageDirectory: "coverage",
9
+ coverageThreshold: {
10
+ global: {
11
+ branches: 100,
12
+ functions: 100,
13
+ lines: 100,
14
+ statements: 100
15
+ }
16
+ },
17
+ moduleNameMapper: {
18
+ "\\.module\\.css$": "<rootDir>/tests/styleMock.ts"
19
+ }
20
+ };
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "my-costrix-app",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "predev": "node scripts/banner.cjs dev",
7
+ "dev": "webpack serve --mode development",
8
+ "prebuild": "node scripts/banner.cjs build",
9
+ "build": "npm run prod",
10
+ "preprod": "node scripts/banner.cjs prod",
11
+ "prod": "npm run lint && npm run typecheck && npm run test:ci && webpack --mode production",
12
+ "test": "jest",
13
+ "test:ci": "jest --coverage",
14
+ "lint": "eslint . --ext .ts,.js,.cjs",
15
+ "typecheck": "tsc --noEmit"
16
+ },
17
+ "devDependencies": {
18
+ "@eslint/js": "^9.0.0",
19
+ "@types/jest": "^30.0.0",
20
+ "@types/node": "^22.10.2",
21
+ "@typescript-eslint/eslint-plugin": "^8.18.1",
22
+ "@typescript-eslint/parser": "^8.18.1",
23
+ "copy-webpack-plugin": "^13.0.0",
24
+ "css-loader": "^7.1.2",
25
+ "eslint": "^9.0.0",
26
+ "eslint-config-prettier": "^9.1.0",
27
+ "globals": "^15.0.0",
28
+ "html-webpack-plugin": "^5.6.3",
29
+ "jest": "^30.0.0",
30
+ "jest-environment-jsdom": "^30.0.0",
31
+ "mini-css-extract-plugin": "^2.9.2",
32
+ "style-loader": "^4.0.0",
33
+ "ts-jest": "^29.4.6",
34
+ "ts-loader": "^9.5.1",
35
+ "typescript": "^5.7.2",
36
+ "webpack": "^5.97.1",
37
+ "webpack-cli": "^6.0.1",
38
+ "webpack-dev-server": "^5.2.0"
39
+ }
40
+ }
Binary file
@@ -0,0 +1,28 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="description" content="Costrix TypeScript starter template with strict guardrails for linting, testing, and production builds." />
7
+ <meta name="robots" content="index,follow" />
8
+ <meta name="theme-color" content="#0b1020" />
9
+ <meta name="application-name" content="Costrix App" />
10
+
11
+ <link rel="icon" href="favicon.ico" sizes="any" />
12
+ <link rel="apple-touch-icon" href="favicon.ico" />
13
+
14
+ <meta property="og:type" content="website" />
15
+ <meta property="og:title" content="Costrix TypeScript Starter" />
16
+ <meta property="og:description" content="Bootstrap a strict TypeScript project with production-ready guardrails." />
17
+ <meta property="og:image" content="ogimage.jpg" />
18
+ <meta property="og:image:alt" content="Costrix TypeScript starter preview" />
19
+
20
+ <meta name="twitter:card" content="summary_large_image" />
21
+ <meta name="twitter:title" content="Costrix TypeScript Starter" />
22
+ <meta name="twitter:description" content="Bootstrap a strict TypeScript project with production-ready guardrails." />
23
+ <meta name="twitter:image" content="ogimage.jpg" />
24
+
25
+ <title>Costrix App</title>
26
+ </head>
27
+ <body></body>
28
+ </html>
Binary file
@@ -0,0 +1,26 @@
1
+ const mode = process.argv[2] || "run";
2
+
3
+ const color = {
4
+ reset: "\x1b[0m",
5
+ cyan: "\x1b[36m",
6
+ magenta: "\x1b[35m",
7
+ gray: "\x1b[90m"
8
+ };
9
+
10
+ const paint = (tone, text) => `${color[tone]}${text}${color.reset}`;
11
+
12
+ const lines = [
13
+ "",
14
+ paint("cyan", " ██████╗ ██████╗ ███████╗████████╗██████╗ ██╗██╗ ██╗"),
15
+ paint("cyan", "██╔════╝██╔═══██╗██╔════╝╚══██╔══╝██╔══██╗██║╚██╗██╔╝"),
16
+ paint("cyan", "██║ ██║ ██║███████╗ ██║ ██████╔╝██║ ╚███╔╝ "),
17
+ paint("cyan", "██║ ██║ ██║╚════██║ ██║ ██╔══██╗██║ ██╔██╗ "),
18
+ paint("cyan", "╚██████╗╚██████╔╝███████║ ██║ ██║ ██║██║██╔╝ ██╗"),
19
+ paint("cyan", " ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝"),
20
+ paint("magenta", `Running: ${mode.toUpperCase()}`),
21
+ paint("gray", "TypeScript + Guardrails + Webpack"),
22
+ paint("gray", "Crafted with care by Kaustubh"),
23
+ ""
24
+ ];
25
+
26
+ console.log(lines.join("\n"));
@@ -0,0 +1,339 @@
1
+ :global(*) {
2
+ -webkit-box-sizing: border-box;
3
+ -moz-box-sizing: border-box;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ :global(body) {
8
+ margin: 0;
9
+ -webkit-font-smoothing: antialiased;
10
+ -moz-osx-font-smoothing: grayscale;
11
+ font-family: "Segoe UI", "Trebuchet MS", sans-serif;
12
+ color: #e7ecff;
13
+ background: radial-gradient(circle at 20% 10%, #1c2f66 0%, #0b1020 42%, #070a14 100%);
14
+ overflow-x: hidden;
15
+ }
16
+
17
+ .app {
18
+ min-height: 100vh;
19
+ display: -webkit-box;
20
+ display: -moz-box;
21
+ display: -ms-flexbox;
22
+ display: flex;
23
+ -webkit-box-align: center;
24
+ -moz-box-align: center;
25
+ -ms-flex-align: center;
26
+ align-items: center;
27
+ -webkit-box-pack: center;
28
+ -moz-box-pack: center;
29
+ -ms-flex-pack: center;
30
+ justify-content: center;
31
+ padding: 1.2rem;
32
+ }
33
+
34
+ @supports (display: grid) and (place-items: center) {
35
+ .app {
36
+ display: grid;
37
+ place-items: center;
38
+ }
39
+ }
40
+
41
+ .page {
42
+ position: relative;
43
+ width: 100%;
44
+ max-width: 980px;
45
+ overflow: hidden;
46
+ border: 1px solid rgba(163, 179, 255, 0.28);
47
+ -webkit-border-radius: 20px;
48
+ -moz-border-radius: 20px;
49
+ border-radius: 20px;
50
+ background: linear-gradient(155deg, rgba(19, 29, 59, 0.9), rgba(16, 20, 40, 0.92));
51
+ -webkit-box-shadow: 0 26px 60px rgba(0, 0, 0, 0.45);
52
+ -moz-box-shadow: 0 26px 60px rgba(0, 0, 0, 0.45);
53
+ box-shadow: 0 26px 60px rgba(0, 0, 0, 0.45);
54
+ }
55
+
56
+ .ambientGlowOne,
57
+ .ambientGlowTwo {
58
+ position: absolute;
59
+ border-radius: 999px;
60
+ filter: blur(2px);
61
+ pointer-events: none;
62
+ }
63
+
64
+ .ambientGlowOne {
65
+ top: -90px;
66
+ left: -100px;
67
+ width: 260px;
68
+ height: 260px;
69
+ background: radial-gradient(circle, rgba(90, 231, 255, 0.48), rgba(90, 231, 255, 0));
70
+ animation: floatA 7s ease-in-out infinite;
71
+ }
72
+
73
+ .ambientGlowTwo {
74
+ right: -120px;
75
+ bottom: -120px;
76
+ width: 300px;
77
+ height: 300px;
78
+ background: radial-gradient(circle, rgba(255, 143, 112, 0.42), rgba(255, 143, 112, 0));
79
+ animation: floatB 8s ease-in-out infinite;
80
+ }
81
+
82
+ .gridOverlay {
83
+ position: absolute;
84
+ inset: 0;
85
+ opacity: 0.22;
86
+ background-image:
87
+ linear-gradient(rgba(130, 145, 213, 0.3) 1px, transparent 1px),
88
+ linear-gradient(90deg, rgba(130, 145, 213, 0.3) 1px, transparent 1px);
89
+ background-size: 34px 34px;
90
+ pointer-events: none;
91
+ }
92
+
93
+ @supports ((-webkit-mask-image: radial-gradient(circle at center, black 40%, transparent 100%)) or
94
+ (mask-image: radial-gradient(circle at center, black 40%, transparent 100%))) {
95
+ .gridOverlay {
96
+ -webkit-mask-image: radial-gradient(circle at center, black 40%, transparent 100%);
97
+ mask-image: radial-gradient(circle at center, black 40%, transparent 100%);
98
+ }
99
+ }
100
+
101
+ .contentShell {
102
+ position: relative;
103
+ z-index: 1;
104
+ display: grid;
105
+ grid-template-columns: 1.1fr 0.9fr;
106
+ gap: 1.2rem;
107
+ padding: 1.4rem;
108
+ }
109
+
110
+ .leftCol,
111
+ .rightCol {
112
+ border: 1px solid rgba(180, 190, 255, 0.2);
113
+ -webkit-border-radius: 14px;
114
+ -moz-border-radius: 14px;
115
+ border-radius: 14px;
116
+ background: rgba(7, 12, 30, 0.55);
117
+ padding: 1rem;
118
+ }
119
+
120
+ @supports ((-webkit-backdrop-filter: blur(8px)) or (backdrop-filter: blur(8px))) {
121
+ .leftCol,
122
+ .rightCol {
123
+ -webkit-backdrop-filter: blur(8px);
124
+ backdrop-filter: blur(8px);
125
+ }
126
+ }
127
+
128
+ .leftCol {
129
+ animation: rise 650ms ease-out both;
130
+ }
131
+
132
+ .rightCol {
133
+ animation: rise 650ms 120ms ease-out both;
134
+ }
135
+
136
+ .header {
137
+ margin-bottom: 1rem;
138
+ }
139
+
140
+ .pill {
141
+ margin: 0;
142
+ width: fit-content;
143
+ padding: 0.26rem 0.7rem;
144
+ border: 1px solid rgba(134, 223, 255, 0.6);
145
+ -webkit-border-radius: 999px;
146
+ -moz-border-radius: 999px;
147
+ border-radius: 999px;
148
+ color: #8fe4ff;
149
+ font-size: 0.78rem;
150
+ letter-spacing: 0.06em;
151
+ text-transform: uppercase;
152
+ }
153
+
154
+ .title {
155
+ margin: 0.75rem 0 0;
156
+ color: #f4f6ff;
157
+ font-size: clamp(1.45rem, 3.2vw, 2.2rem);
158
+ line-height: 1.12;
159
+ }
160
+
161
+ .subtitle {
162
+ margin: 0.75rem 0 0;
163
+ color: #cfd8ff;
164
+ line-height: 1.55;
165
+ max-width: 58ch;
166
+ }
167
+
168
+ .sectionTitle {
169
+ margin: 0;
170
+ margin-top: 0.9rem;
171
+ color: #e5eaff;
172
+ font-size: 1rem;
173
+ }
174
+
175
+ .guardrailList,
176
+ .commandList {
177
+ margin: 0.8rem 0 0;
178
+ padding: 0;
179
+ list-style: none;
180
+ display: grid;
181
+ gap: 0.62rem;
182
+ }
183
+
184
+ .guardrailItem {
185
+ border: 1px solid rgba(179, 189, 255, 0.24);
186
+ -webkit-border-radius: 10px;
187
+ -moz-border-radius: 10px;
188
+ border-radius: 10px;
189
+ padding: 0.8rem;
190
+ background: linear-gradient(145deg, rgba(17, 25, 50, 0.8), rgba(13, 18, 37, 0.86));
191
+ animation: reveal 550ms ease both;
192
+ }
193
+
194
+ .guardrailItem:nth-child(2) {
195
+ animation-delay: 120ms;
196
+ }
197
+
198
+ .guardrailItem:nth-child(3) {
199
+ animation-delay: 220ms;
200
+ }
201
+
202
+ .guardrailTitle {
203
+ margin: 0;
204
+ color: #f0f4ff;
205
+ font-size: 0.95rem;
206
+ }
207
+
208
+ .guardrailDetail {
209
+ margin: 0.35rem 0 0;
210
+ color: #bec8ef;
211
+ line-height: 1.45;
212
+ font-size: 0.92rem;
213
+ }
214
+
215
+ .commandItem {
216
+ border: 1px solid rgba(130, 151, 255, 0.35);
217
+ -webkit-border-radius: 10px;
218
+ -moz-border-radius: 10px;
219
+ border-radius: 10px;
220
+ padding: 0.58rem 0.72rem;
221
+ font-family: "Consolas", "Courier New", monospace;
222
+ font-size: 0.92rem;
223
+ color: #d8e0ff;
224
+ background: rgba(24, 33, 67, 0.72);
225
+ }
226
+
227
+ .docsButton {
228
+ display: inline-block;
229
+ margin-top: 0.9rem;
230
+ padding: 0.6rem 0.78rem;
231
+ border: 1px solid rgba(116, 228, 255, 0.48);
232
+ -webkit-border-radius: 10px;
233
+ -moz-border-radius: 10px;
234
+ border-radius: 10px;
235
+ color: #9feeff;
236
+ background: rgba(9, 24, 44, 0.6);
237
+ text-decoration: none;
238
+ font-weight: 600;
239
+ transition: transform 180ms ease, background-color 180ms ease, border-color 180ms ease;
240
+ }
241
+
242
+ .docsButton:hover {
243
+ transform: translateY(-1px);
244
+ border-color: rgba(116, 228, 255, 0.8);
245
+ background: rgba(12, 34, 61, 0.85);
246
+ }
247
+
248
+ .noteCard {
249
+ margin-top: 1rem;
250
+ border: 1px solid rgba(160, 177, 255, 0.24);
251
+ -webkit-border-radius: 10px;
252
+ -moz-border-radius: 10px;
253
+ border-radius: 10px;
254
+ padding: 0.85rem;
255
+ background: linear-gradient(170deg, rgba(22, 27, 53, 0.88), rgba(15, 18, 35, 0.86));
256
+ }
257
+
258
+ .noteTitle {
259
+ margin: 0;
260
+ color: #f0f4ff;
261
+ font-size: 0.95rem;
262
+ }
263
+
264
+ .noteText {
265
+ margin: 0.4rem 0 0;
266
+ color: #c2cbef;
267
+ line-height: 1.5;
268
+ font-size: 0.9rem;
269
+ }
270
+
271
+ .footer {
272
+ margin: 1rem 0 0;
273
+ color: #9cabde;
274
+ font-size: 0.9rem;
275
+ }
276
+
277
+ @keyframes rise {
278
+ from {
279
+ opacity: 0;
280
+ transform: translateY(12px);
281
+ }
282
+
283
+ to {
284
+ opacity: 1;
285
+ transform: translateY(0);
286
+ }
287
+ }
288
+
289
+ @keyframes reveal {
290
+ from {
291
+ opacity: 0;
292
+ transform: translateY(8px) scale(0.98);
293
+ }
294
+
295
+ to {
296
+ opacity: 1;
297
+ transform: translateY(0) scale(1);
298
+ }
299
+ }
300
+
301
+ @keyframes floatA {
302
+ 0%,
303
+ 100% {
304
+ transform: translate(0, 0);
305
+ }
306
+
307
+ 50% {
308
+ transform: translate(14px, 10px);
309
+ }
310
+ }
311
+
312
+ @keyframes floatB {
313
+ 0%,
314
+ 100% {
315
+ transform: translate(0, 0);
316
+ }
317
+
318
+ 50% {
319
+ transform: translate(-12px, -10px);
320
+ }
321
+ }
322
+
323
+ @media (max-width: 860px) {
324
+ .contentShell {
325
+ grid-template-columns: 1fr;
326
+ }
327
+ }
328
+
329
+ @media (max-width: 640px) {
330
+ .page {
331
+ -webkit-border-radius: 14px;
332
+ -moz-border-radius: 14px;
333
+ border-radius: 14px;
334
+ }
335
+
336
+ .contentShell {
337
+ padding: 0.9rem;
338
+ }
339
+ }
@@ -0,0 +1,4 @@
1
+ declare module "*.module.css" {
2
+ const classes: Readonly<Record<string, string>>;
3
+ export default classes;
4
+ }
@@ -0,0 +1,155 @@
1
+ import * as stylesImport from "./App.module.css";
2
+
3
+ export type GuardrailItem = {
4
+ readonly title: string;
5
+ readonly detail: string;
6
+ };
7
+
8
+ type StylesMap = Readonly<Record<string, string>>;
9
+
10
+ export const DEFAULT_GUARDRAILS: ReadonlyArray<GuardrailItem> = [
11
+ { title: "Strict TypeScript", detail: "Compiler options are strict and tuned for safer refactors." },
12
+ { title: "Strict ESLint", detail: "Linting blocks unsafe patterns and explicit any usage." },
13
+ { title: "Quality Build", detail: "Production build runs lint, typecheck, tests, and coverage gates." }
14
+ ];
15
+
16
+ const toStringMap = (source: Record<string, unknown>): StylesMap => {
17
+ const entries = Object.entries(source).filter((entry): entry is [string, string] => typeof entry[1] === "string");
18
+ return Object.fromEntries(entries);
19
+ };
20
+
21
+ export const resolveStyles = (source: unknown): StylesMap => {
22
+ if (source === null || typeof source !== "object") {
23
+ return {};
24
+ }
25
+
26
+ const directMap = toStringMap(source as Record<string, unknown>);
27
+ if (Object.keys(directMap).length > 0) {
28
+ return directMap;
29
+ }
30
+
31
+ const wrappedDefault = (source as { default?: unknown }).default;
32
+ if (wrappedDefault !== null && typeof wrappedDefault === "object") {
33
+ return toStringMap(wrappedDefault as Record<string, unknown>);
34
+ }
35
+
36
+ return {};
37
+ };
38
+
39
+ export const cls = (styles: StylesMap, key: string): string => styles[key] ?? key;
40
+
41
+ export const createEl = <K extends keyof HTMLElementTagNameMap>(
42
+ doc: Document,
43
+ tag: K,
44
+ className?: string,
45
+ text?: string
46
+ ): HTMLElementTagNameMap[K] => {
47
+ const element = doc.createElement(tag);
48
+ if (className !== undefined && className.length > 0) {
49
+ element.className = className;
50
+ }
51
+ if (text !== undefined && text.length > 0) {
52
+ element.textContent = text;
53
+ }
54
+ return element;
55
+ };
56
+
57
+ export const buildGuardrailItem = (doc: Document, styles: StylesMap, item: GuardrailItem): HTMLElement => {
58
+ const card = createEl(doc, "li", cls(styles, "guardrailItem"));
59
+ const title = createEl(doc, "h3", cls(styles, "guardrailTitle"), item.title);
60
+ const detail = createEl(doc, "p", cls(styles, "guardrailDetail"), item.detail);
61
+ card.append(title, detail);
62
+ return card;
63
+ };
64
+
65
+ export const buildGuardrailList = (
66
+ doc: Document,
67
+ styles: StylesMap,
68
+ items: ReadonlyArray<GuardrailItem>
69
+ ): HTMLElement => {
70
+ const section = createEl(doc, "ul", cls(styles, "guardrailList"));
71
+ const cards = items.map((item) => buildGuardrailItem(doc, styles, item));
72
+ section.append(...cards);
73
+ return section;
74
+ };
75
+
76
+ export const buildCommandList = (doc: Document, styles: StylesMap): HTMLElement => {
77
+ const list = createEl(doc, "ul", cls(styles, "commandList"));
78
+ const commands = ["npm run dev", "npm run test", "npm run build"];
79
+ const items = commands.map((command) => createEl(doc, "li", cls(styles, "commandItem"), command));
80
+ list.append(...items);
81
+ return list;
82
+ };
83
+
84
+ export const buildPage = (
85
+ doc: Document,
86
+ styles: StylesMap,
87
+ guardrails: ReadonlyArray<GuardrailItem> = DEFAULT_GUARDRAILS
88
+ ): HTMLElement => {
89
+ const page = createEl(doc, "div", cls(styles, "page"));
90
+ const ambientGlowOne = createEl(doc, "div", cls(styles, "ambientGlowOne"));
91
+ const ambientGlowTwo = createEl(doc, "div", cls(styles, "ambientGlowTwo"));
92
+ const gridOverlay = createEl(doc, "div", cls(styles, "gridOverlay"));
93
+
94
+ const shell = createEl(doc, "div", cls(styles, "contentShell"));
95
+ const leftCol = createEl(doc, "section", cls(styles, "leftCol"));
96
+ const rightCol = createEl(doc, "section", cls(styles, "rightCol"));
97
+
98
+ const header = createEl(doc, "header", cls(styles, "header"));
99
+ const pill = createEl(doc, "p", cls(styles, "pill"), "Costrix Starter");
100
+ const title = createEl(doc, "h1", cls(styles, "title"), "Costrix TypeScript Starter");
101
+ const subtitle = createEl(
102
+ doc,
103
+ "p",
104
+ cls(styles, "subtitle"),
105
+ "This template sets up a minimal TypeScript codebase that compiles to vanilla JavaScript with strict quality gates."
106
+ );
107
+ header.append(pill, title, subtitle);
108
+
109
+ const guardrailHeading = createEl(doc, "h2", cls(styles, "sectionTitle"), "Build Guardrails");
110
+ const guardrailList = buildGuardrailList(doc, styles, guardrails);
111
+ leftCol.append(header, guardrailHeading, guardrailList);
112
+
113
+ const commandsHeading = createEl(doc, "h2", cls(styles, "sectionTitle"), "Available Commands");
114
+ const commandList = buildCommandList(doc, styles);
115
+ const docsButton = createEl(doc, "a", cls(styles, "docsButton"), "Read Costrix Documentation");
116
+ docsButton.setAttribute("href", "https://costrix.kaustubhvats.in");
117
+ docsButton.setAttribute("target", "_blank");
118
+ docsButton.setAttribute("rel", "noopener noreferrer");
119
+
120
+ const noteCard = createEl(doc, "div", cls(styles, "noteCard"));
121
+ const noteTitle = createEl(doc, "h3", cls(styles, "noteTitle"), "Ready for real app code");
122
+ const noteText = createEl(
123
+ doc,
124
+ "p",
125
+ cls(styles, "noteText"),
126
+ "Edit src/index.ts, add modules in src/, and keep guardrails on while shipping to dist/."
127
+ );
128
+ noteCard.append(noteTitle, noteText);
129
+
130
+ const footer = createEl(
131
+ doc,
132
+ "p",
133
+ cls(styles, "footer"),
134
+ "Start with src/index.ts and build your app. Crafted with care by Kaustubh."
135
+ );
136
+ rightCol.append(commandsHeading, commandList, docsButton, noteCard, footer);
137
+
138
+ shell.append(leftCol, rightCol);
139
+ page.append(ambientGlowOne, ambientGlowTwo, gridOverlay, shell);
140
+ return page;
141
+ };
142
+
143
+ export const mountApp = (
144
+ doc: Document = document,
145
+ stylesSource: unknown = stylesImport
146
+ ): HTMLElement => {
147
+ const styles = resolveStyles(stylesSource);
148
+ const root = createEl(doc, "main", cls(styles, "app"));
149
+ root.setAttribute("data-costrix-root", "true");
150
+ root.append(buildPage(doc, styles));
151
+ doc.body.append(root);
152
+ return root;
153
+ };
154
+
155
+ mountApp();
@@ -0,0 +1,103 @@
1
+ import type { GuardrailItem } from "../src/index";
2
+ import type * as IndexModule from "../src/index";
3
+ import { beforeEach, describe, expect, it, jest } from "@jest/globals";
4
+
5
+ const loadModule = async (): Promise<typeof IndexModule> => import("../src/index");
6
+
7
+ describe("app module", () => {
8
+ beforeEach(() => {
9
+ document.body.innerHTML = "";
10
+ jest.resetModules();
11
+ });
12
+
13
+ it("mounts by default on import", async () => {
14
+ await loadModule();
15
+ const root = document.querySelector('[data-costrix-root="true"]');
16
+ expect(root).not.toBeNull();
17
+ });
18
+
19
+ it("resolves direct style objects and class fallback", async () => {
20
+ const mod = await loadModule();
21
+ const resolved = mod.resolveStyles({ app: "app_hash" });
22
+ expect(mod.cls(resolved, "app")).toBe("app_hash");
23
+ expect(mod.cls(resolved, "missing")).toBe("missing");
24
+ });
25
+
26
+ it("resolves wrapped default style objects and invalid input", async () => {
27
+ const mod = await loadModule();
28
+ const wrapped = mod.resolveStyles({ default: { page: "page_hash" } });
29
+ const empty = mod.resolveStyles(null);
30
+ const fallbackEmpty = mod.resolveStyles({ default: 7, page: 1 });
31
+ expect(mod.cls(wrapped, "page")).toBe("page_hash");
32
+ expect(mod.cls(empty, "page")).toBe("page");
33
+ expect(mod.cls(fallbackEmpty, "page")).toBe("page");
34
+ });
35
+
36
+ it("creates elements with optional class and text", async () => {
37
+ const mod = await loadModule();
38
+ const withAll = mod.createEl(document, "button", "cta", "Launch");
39
+ const bare = mod.createEl(document, "div");
40
+ expect(withAll.className).toBe("cta");
41
+ expect(withAll.textContent).toBe("Launch");
42
+ expect(bare.className).toBe("");
43
+ expect(bare.textContent).toBe("");
44
+ });
45
+
46
+ it("builds guardrail item and list", async () => {
47
+ const mod = await loadModule();
48
+ const styles = mod.resolveStyles({
49
+ guardrailItem: "card",
50
+ guardrailTitle: "cardTitle",
51
+ guardrailDetail: "cardDetail",
52
+ guardrailList: "grid"
53
+ });
54
+ const item: GuardrailItem = { title: "Rule", detail: "Detail" };
55
+ const card = mod.buildGuardrailItem(document, styles, item);
56
+ const grid = mod.buildGuardrailList(document, styles, [item]);
57
+ expect(card.className).toBe("card");
58
+ expect(card.querySelector("h3")?.className).toBe("cardTitle");
59
+ expect(card.querySelector("p")?.className).toBe("cardDetail");
60
+ expect(grid.className).toBe("grid");
61
+ expect(grid.querySelectorAll("li").length).toBe(1);
62
+ });
63
+
64
+ it("builds command list", async () => {
65
+ const mod = await loadModule();
66
+ const styles = mod.resolveStyles({ commandList: "commands", commandItem: "command" });
67
+ const list = mod.buildCommandList(document, styles);
68
+ expect(list.className).toBe("commands");
69
+ expect(list.querySelectorAll("li").length).toBe(3);
70
+ });
71
+
72
+ it("builds page and mounts with provided styles", async () => {
73
+ const mod = await loadModule();
74
+ const styles = mod.resolveStyles({
75
+ app: "appHash",
76
+ page: "pageHash",
77
+ header: "headerHash",
78
+ title: "titleHash",
79
+ subtitle: "subtitleHash",
80
+ sectionTitle: "sectionTitleHash",
81
+ guardrailList: "guardrailListHash",
82
+ guardrailItem: "guardrailItemHash",
83
+ guardrailTitle: "guardrailTitleHash",
84
+ guardrailDetail: "guardrailDetailHash",
85
+ commandList: "commandListHash",
86
+ commandItem: "commandItemHash",
87
+ docsButton: "docsButtonHash",
88
+ footer: "footerHash"
89
+ });
90
+ const page = mod.buildPage(document, styles);
91
+ expect(page.className).toBe("pageHash");
92
+ expect(page.querySelector("h1")?.textContent).toBe("Costrix TypeScript Starter");
93
+ expect(page.querySelector('a[href="https://costrix.kaustubhvats.in"]')?.className).toBe("docsButtonHash");
94
+
95
+ const defaultPage = mod.buildPage(document, styles);
96
+ expect(defaultPage.querySelectorAll("li").length).toBeGreaterThan(3);
97
+
98
+ document.body.innerHTML = "";
99
+ const mounted = mod.mountApp(document, styles);
100
+ expect(mounted.className).toBe("appHash");
101
+ expect(document.querySelectorAll('[data-costrix-root="true"]').length).toBe(1);
102
+ });
103
+ });
@@ -0,0 +1,8 @@
1
+ const styles = new Proxy<Record<string, string>>(
2
+ {},
3
+ {
4
+ get: (_target, prop: string | symbol): string => String(prop)
5
+ }
6
+ );
7
+
8
+ export default styles;
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "include": ["src/**/*.ts", "tests/**/*.ts"]
4
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "strict": true,
8
+ "noImplicitAny": true,
9
+ "noImplicitReturns": true,
10
+ "noImplicitOverride": true,
11
+ "noUnusedLocals": true,
12
+ "noUnusedParameters": true,
13
+ "noFallthroughCasesInSwitch": true,
14
+ "noUncheckedIndexedAccess": true,
15
+ "noPropertyAccessFromIndexSignature": true,
16
+ "exactOptionalPropertyTypes": true,
17
+ "useUnknownInCatchVariables": true,
18
+ "esModuleInterop": true,
19
+ "forceConsistentCasingInFileNames": true,
20
+ "skipLibCheck": true,
21
+ "resolveJsonModule": true,
22
+ "isolatedModules": true,
23
+ "sourceMap": true,
24
+ "outDir": "dist"
25
+ },
26
+ "include": ["src", "tests"]
27
+ }
@@ -0,0 +1,72 @@
1
+ const path = require("path");
2
+ const HtmlWebpackPlugin = require("html-webpack-plugin");
3
+ const MiniCssExtractPlugin = require("mini-css-extract-plugin");
4
+ const CopyWebpackPlugin = require("copy-webpack-plugin");
5
+
6
+ module.exports = (_env, argv) => {
7
+ const isProduction = argv.mode === "production";
8
+
9
+ return {
10
+ entry: "./src/index.ts",
11
+ output: {
12
+ path: path.resolve(__dirname, "dist"),
13
+ filename: "bundle.[contenthash].js",
14
+ clean: true
15
+ },
16
+ devtool: isProduction ? "source-map" : "eval-cheap-module-source-map",
17
+ resolve: {
18
+ extensions: [".ts", ".js"]
19
+ },
20
+ module: {
21
+ rules: [
22
+ {
23
+ test: /\.ts$/,
24
+ exclude: /node_modules/,
25
+ use: "ts-loader"
26
+ },
27
+ {
28
+ test: /\.module\.css$/,
29
+ use: [
30
+ isProduction ? MiniCssExtractPlugin.loader : "style-loader",
31
+ {
32
+ loader: "css-loader",
33
+ options: {
34
+ modules: true
35
+ }
36
+ }
37
+ ]
38
+ }
39
+ ]
40
+ },
41
+ plugins: [
42
+ new HtmlWebpackPlugin({
43
+ template: "./public/index.html"
44
+ }),
45
+ new CopyWebpackPlugin({
46
+ patterns: [
47
+ {
48
+ from: "public",
49
+ to: "public",
50
+ globOptions: {
51
+ ignore: ["**/index.html"]
52
+ }
53
+ }
54
+ ]
55
+ }),
56
+ ...(isProduction ? [new MiniCssExtractPlugin({ filename: "styles.[contenthash].css" })] : [])
57
+ ],
58
+ devServer: {
59
+ static: [
60
+ {
61
+ directory: path.join(__dirname, "public"),
62
+ publicPath: "/",
63
+ watch: true
64
+ }
65
+ ],
66
+ watchFiles: ["public/**/*"],
67
+ hot: true,
68
+ port: 3000,
69
+ open: true
70
+ }
71
+ };
72
+ };