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 +9 -0
- package/package.json +10 -0
- package/src/args.js +42 -0
- package/src/cli.js +53 -0
- package/src/logger.js +66 -0
- package/src/prompt.js +19 -0
- package/src/run.js +55 -0
- package/src/scaffold.js +83 -0
- package/src/utils.js +16 -0
- package/templates/eslint.config.cjs +69 -0
- package/templates/jest.config.cjs +20 -0
- package/templates/package.json +40 -0
- package/templates/public/favicon.ico +0 -0
- package/templates/public/index.html +28 -0
- package/templates/public/ogimage.jpg +0 -0
- package/templates/scripts/banner.cjs +26 -0
- package/templates/src/App.module.css +339 -0
- package/templates/src/global.d.ts +4 -0
- package/templates/src/index.ts +155 -0
- package/templates/tests/index.test.ts +103 -0
- package/templates/tests/styleMock.ts +8 -0
- package/templates/tsconfig.eslint.json +4 -0
- package/templates/tsconfig.json +27 -0
- package/templates/webpack.config.js +72 -0
package/bin/costrix.js
ADDED
package/package.json
ADDED
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
|
+
};
|
package/src/scaffold.js
ADDED
|
@@ -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,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,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
|
+
};
|