stoix 0.1.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.
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "stoix-app",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=18"
8
+ },
9
+ "scripts": {
10
+ "dev": "cross-env NODE_ENV=development tsx server/server.ts",
11
+ "build": "tsc --noEmit && vite build && tsc -p tsconfig.server.json",
12
+ "start": "cross-env NODE_ENV=production node dist/server/server/server.js",
13
+ "typecheck": "tsc --noEmit"
14
+ },
15
+ "dependencies": {
16
+ "cors": "^2.8.5",
17
+ "dotenv": "^16.4.7",
18
+ "express": "^4.18.2",
19
+ "helmet": "^7.1.0",
20
+ "react": "^18.2.0",
21
+ "react-dom": "^18.2.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/cors": "^2.8.17",
25
+ "@types/express": "^4.17.21",
26
+ "@types/react": "^18.2.48",
27
+ "@types/react-dom": "^18.2.18",
28
+ "@vitejs/plugin-react": "^4.2.1",
29
+ "cross-env": "^7.0.3",
30
+ "tsx": "^4.7.0",
31
+ "typescript": "^5.3.3",
32
+ "vite": "^5.0.12"
33
+ }
34
+ }
@@ -0,0 +1,61 @@
1
+ import { useState, useEffect } from 'react';
2
+
3
+ interface StatusCheck {
4
+ react: boolean;
5
+ api: boolean;
6
+ }
7
+
8
+ function HomePage() {
9
+ const [status, setStatus] = useState<StatusCheck>({ react: true, api: false });
10
+
11
+ useEffect(() => {
12
+ fetch('/api/example')
13
+ .then((res) => res.json())
14
+ .then(() => setStatus((s) => ({ ...s, api: true })))
15
+ .catch(() => setStatus((s) => ({ ...s, api: false })));
16
+ }, []);
17
+
18
+ return (
19
+ <div className="stoix-page">
20
+ <header className="stoix-hero">
21
+ <h1 className="stoix-title">Stoix</h1>
22
+ <p className="stoix-subtitle">
23
+ A disciplined TypeScript framework built on React and Express.
24
+ </p>
25
+ </header>
26
+
27
+ <div className="stoix-divider" />
28
+
29
+ <section className="stoix-status">
30
+ <h2 className="stoix-section-heading">System Status</h2>
31
+ <div className="stoix-status-grid">
32
+ <div className="stoix-status-item">
33
+ <span className="stoix-status-label">React Rendering</span>
34
+ <span className={`stoix-status-value ${status.react ? 'ok' : ''}`}>
35
+ {status.react ? '✓' : '—'}
36
+ </span>
37
+ </div>
38
+ <div className="stoix-status-item">
39
+ <span className="stoix-status-label">API Connected</span>
40
+ <span className={`stoix-status-value ${status.api ? 'ok' : ''}`}>
41
+ {status.api ? '✓' : '—'}
42
+ </span>
43
+ </div>
44
+ </div>
45
+ </section>
46
+
47
+ <div className="stoix-divider" />
48
+
49
+ <blockquote className="stoix-quote">
50
+ <p>
51
+ "If it is not right, do not do it; if it is not true, do not say it."
52
+ </p>
53
+ <cite>— Marcus Aurelius</cite>
54
+ </blockquote>
55
+
56
+ <p className="stoix-principle">Structure before scale.</p>
57
+ </div>
58
+ );
59
+ }
60
+
61
+ export default HomePage;
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
2
+ <rect width="100" height="100" rx="16" fill="#111111"/>
3
+ <text x="50" y="68" font-family="Georgia, 'Times New Roman', serif" font-size="52" font-weight="400" fill="#e5e5e5" text-anchor="middle" letter-spacing="2">S</text>
4
+ </svg>
@@ -0,0 +1,28 @@
1
+ import { Router, type Request, type Response } from 'express';
2
+
3
+ const router = Router();
4
+
5
+ /**
6
+ * GET /api/example
7
+ * A sample API route to demonstrate Stoix auto-loading.
8
+ *
9
+ * Add more routes in server/routes/ — each file auto-mounts
10
+ * at /api/<filename>.
11
+ */
12
+ router.get('/', (_req: Request, res: Response) => {
13
+ res.json({
14
+ message: 'Hello from Stoix!',
15
+ framework: 'Stoix v0.1',
16
+ timestamp: new Date().toISOString(),
17
+ });
18
+ });
19
+
20
+ /**
21
+ * GET /api/example/health
22
+ * A simple health check endpoint.
23
+ */
24
+ router.get('/health', (_req: Request, res: Response) => {
25
+ res.json({ status: 'ok' });
26
+ });
27
+
28
+ export default router;
@@ -0,0 +1,78 @@
1
+ import 'dotenv/config';
2
+ import express, { type Request, type Response, type NextFunction } from 'express';
3
+ import helmet from 'helmet';
4
+ import cors from 'cors';
5
+ import path from 'node:path';
6
+ import fs from 'node:fs';
7
+ import { fileURLToPath, pathToFileURL } from 'node:url';
8
+ import config from '../stoix.config.js';
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const isProduction = process.env.NODE_ENV === 'production';
12
+
13
+ function getRouteFiles(dir: string): string[] {
14
+ const files: string[] = [];
15
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
16
+ const fullPath = path.join(dir, entry.name);
17
+ if (entry.isDirectory()) {
18
+ files.push(...getRouteFiles(fullPath));
19
+ continue;
20
+ }
21
+
22
+ const isRouteModule = /\.(ts|js)$/.test(entry.name) && !entry.name.includes('.d.');
23
+ if (isRouteModule) files.push(fullPath);
24
+ }
25
+ return files;
26
+ }
27
+
28
+ async function start() {
29
+ const app = express();
30
+ const port = Number(process.env.PORT) || config.port;
31
+
32
+ app.use(helmet(isProduction ? undefined : { contentSecurityPolicy: false }));
33
+ if (config.server.cors) app.use(cors({ origin: config.server.cors }));
34
+ app.use(express.json());
35
+ app.use(express.urlencoded({ extended: true }));
36
+
37
+ // Auto-mount route files in server/routes recursively at /api/<path-to-file>.
38
+ const routesDir = path.join(__dirname, 'routes');
39
+ if (fs.existsSync(routesDir)) {
40
+ for (const filePath of getRouteFiles(routesDir).sort()) {
41
+ const relativeFilePath = path.relative(routesDir, filePath);
42
+ const routeFromFile = relativeFilePath
43
+ .replace(/\.(ts|js)$/, '')
44
+ .split(path.sep)
45
+ .join('/')
46
+ .replace(/\/index$/, '');
47
+ const routePath = routeFromFile ? `${config.server.apiPrefix}/${routeFromFile}` : config.server.apiPrefix;
48
+
49
+ const { default: router } = await import(pathToFileURL(filePath).href);
50
+ app.use(routePath, router);
51
+ }
52
+ }
53
+
54
+ app.use(config.server.apiPrefix, (_req: Request, res: Response) => {
55
+ res.status(404).json({ error: 'Not Found' });
56
+ });
57
+
58
+ if (isProduction) {
59
+ const dist = path.join(process.cwd(), 'dist', 'client');
60
+ app.use(express.static(dist));
61
+ app.get('*', (_req: Request, res: Response) => res.sendFile(path.join(dist, 'index.html')));
62
+ } else {
63
+ const { createServer } = await import('vite');
64
+ app.use((await createServer({ server: { middlewareMode: true }, appType: 'spa' })).middlewares);
65
+ }
66
+
67
+ app.use((err: Error & { status?: number }, _req: Request, res: Response, _next: NextFunction) => {
68
+ res.status(err.status || 500).json({ error: err.message });
69
+ });
70
+
71
+ const server = app.listen(port, () => console.log(`Stoix running on http://localhost:${port}`));
72
+
73
+ const shutdown = () => server.close(() => process.exit(0));
74
+ process.on('SIGTERM', shutdown);
75
+ process.on('SIGINT', shutdown);
76
+ }
77
+
78
+ start();
@@ -0,0 +1,18 @@
1
+ import HomePage from '../pages/index';
2
+
3
+ function App() {
4
+ return (
5
+ <div className="stoix-app">
6
+ <main className="stoix-main">
7
+ <HomePage />
8
+ </main>
9
+
10
+ <footer className="stoix-footer">
11
+ <div className="stoix-footer-line" />
12
+ <p>Stoix v0.1</p>
13
+ </footer>
14
+ </div>
15
+ );
16
+ }
17
+
18
+ export default App;
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+ import './styles.css';
5
+
6
+ const rootElement = document.getElementById('root');
7
+
8
+ if (!rootElement) {
9
+ throw new Error('Root element not found. Make sure <div id="root"></div> exists in index.html.');
10
+ }
11
+
12
+ // Client-side rendering (SSR-ready: swap with hydrateRoot for SSR)
13
+ ReactDOM.createRoot(rootElement).render(
14
+ <React.StrictMode>
15
+ <App />
16
+ </React.StrictMode>
17
+ );
@@ -0,0 +1,198 @@
1
+ /* ── Stoix ── Discipline over decoration ────────── */
2
+
3
+ :root {
4
+ --stoix-bg: #111111;
5
+ --stoix-surface: #1a1a1a;
6
+ --stoix-border: #333333;
7
+ --stoix-text: #ffffff;
8
+ --stoix-text-muted: #888888;
9
+ --stoix-marble: #e5e5e5;
10
+ --stoix-stone: #888888;
11
+ --stoix-font-body: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
12
+ --stoix-font-heading: Georgia, 'Times New Roman', serif;
13
+ --stoix-mono: 'SF Mono', 'Fira Code', 'Fira Mono', monospace;
14
+ }
15
+
16
+ *,
17
+ *::before,
18
+ *::after {
19
+ margin: 0;
20
+ padding: 0;
21
+ box-sizing: border-box;
22
+ }
23
+
24
+ body {
25
+ font-family: var(--stoix-font-body);
26
+ background-color: var(--stoix-bg);
27
+ color: var(--stoix-text);
28
+ line-height: 1.7;
29
+ -webkit-font-smoothing: antialiased;
30
+ -moz-osx-font-smoothing: grayscale;
31
+ }
32
+
33
+ a {
34
+ color: var(--stoix-marble);
35
+ text-decoration: none;
36
+ transition: color 0.2s;
37
+ }
38
+
39
+ a:hover {
40
+ color: var(--stoix-text);
41
+ }
42
+
43
+ /* ── Layout ──────────────────────────────────────── */
44
+
45
+ .stoix-app {
46
+ min-height: 100vh;
47
+ display: flex;
48
+ flex-direction: column;
49
+ }
50
+
51
+ .stoix-main {
52
+ flex: 1;
53
+ display: flex;
54
+ align-items: center;
55
+ justify-content: center;
56
+ padding: 4rem 2rem;
57
+ }
58
+
59
+ /* ── Page ────────────────────────────────────────── */
60
+
61
+ .stoix-page {
62
+ max-width: 600px;
63
+ width: 100%;
64
+ text-align: center;
65
+ }
66
+
67
+ /* ── Hero ────────────────────────────────────────── */
68
+
69
+ .stoix-hero {
70
+ padding: 2rem 0;
71
+ }
72
+
73
+ .stoix-title {
74
+ font-family: var(--stoix-font-heading);
75
+ font-size: 3.5rem;
76
+ font-weight: 400;
77
+ letter-spacing: 0.12em;
78
+ color: var(--stoix-text);
79
+ margin-bottom: 1.25rem;
80
+ text-transform: uppercase;
81
+ }
82
+
83
+ .stoix-subtitle {
84
+ font-size: 1rem;
85
+ color: var(--stoix-text-muted);
86
+ font-weight: 400;
87
+ line-height: 1.6;
88
+ max-width: 440px;
89
+ margin: 0 auto;
90
+ }
91
+
92
+ /* ── Divider (temple foundation line) ────────────── */
93
+
94
+ .stoix-divider {
95
+ width: 80px;
96
+ height: 1px;
97
+ background: var(--stoix-border);
98
+ margin: 2.5rem auto;
99
+ }
100
+
101
+ /* ── System Status ───────────────────────────────── */
102
+
103
+ .stoix-section-heading {
104
+ font-family: var(--stoix-font-body);
105
+ font-size: 0.75rem;
106
+ font-weight: 500;
107
+ text-transform: uppercase;
108
+ letter-spacing: 0.15em;
109
+ color: var(--stoix-text-muted);
110
+ margin-bottom: 1.25rem;
111
+ }
112
+
113
+ .stoix-status-grid {
114
+ display: flex;
115
+ justify-content: center;
116
+ gap: 3rem;
117
+ }
118
+
119
+ .stoix-status-item {
120
+ display: flex;
121
+ flex-direction: column;
122
+ align-items: center;
123
+ gap: 0.4rem;
124
+ }
125
+
126
+ .stoix-status-label {
127
+ font-size: 0.8rem;
128
+ color: var(--stoix-text-muted);
129
+ letter-spacing: 0.04em;
130
+ }
131
+
132
+ .stoix-status-value {
133
+ font-size: 1rem;
134
+ color: var(--stoix-text-muted);
135
+ font-family: var(--stoix-mono);
136
+ }
137
+
138
+ .stoix-status-value.ok {
139
+ color: var(--stoix-marble);
140
+ }
141
+
142
+ /* ── Quote ───────────────────────────────────────── */
143
+
144
+ .stoix-quote {
145
+ padding: 0;
146
+ border: none;
147
+ margin: 0 auto;
148
+ max-width: 480px;
149
+ }
150
+
151
+ .stoix-quote p {
152
+ font-family: var(--stoix-font-heading);
153
+ font-size: 1.05rem;
154
+ font-style: italic;
155
+ color: var(--stoix-marble);
156
+ line-height: 1.8;
157
+ margin-bottom: 0.75rem;
158
+ }
159
+
160
+ .stoix-quote cite {
161
+ font-family: var(--stoix-font-body);
162
+ font-size: 0.8rem;
163
+ font-style: normal;
164
+ color: var(--stoix-text-muted);
165
+ letter-spacing: 0.05em;
166
+ }
167
+
168
+ /* ── Principle ───────────────────────────────────── */
169
+
170
+ .stoix-principle {
171
+ margin-top: 2.5rem;
172
+ font-size: 0.85rem;
173
+ font-weight: 500;
174
+ letter-spacing: 0.1em;
175
+ text-transform: uppercase;
176
+ color: var(--stoix-stone);
177
+ }
178
+
179
+ /* ── Footer ──────────────────────────────────────── */
180
+
181
+ .stoix-footer {
182
+ text-align: center;
183
+ padding: 2rem;
184
+ }
185
+
186
+ .stoix-footer-line {
187
+ width: 40px;
188
+ height: 1px;
189
+ background: var(--stoix-border);
190
+ margin: 0 auto 1rem;
191
+ }
192
+
193
+ .stoix-footer p {
194
+ font-size: 0.75rem;
195
+ color: var(--stoix-text-muted);
196
+ letter-spacing: 0.08em;
197
+ text-transform: uppercase;
198
+ }
@@ -0,0 +1,19 @@
1
+ export interface StoixConfig {
2
+ port: number;
3
+ framework: 'react';
4
+ server: {
5
+ apiPrefix: string;
6
+ cors: string | string[] | false;
7
+ };
8
+ }
9
+
10
+ const config: StoixConfig = {
11
+ port: 3000,
12
+ framework: 'react',
13
+ server: {
14
+ apiPrefix: '/api',
15
+ cors: false,
16
+ },
17
+ };
18
+
19
+ export default config;
@@ -0,0 +1,31 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "resolveJsonModule": true,
12
+ "isolatedModules": true,
13
+ "noEmit": true,
14
+ "allowImportingTsExtensions": true,
15
+ "outDir": "dist",
16
+ "rootDir": ".",
17
+ "baseUrl": ".",
18
+ "paths": {
19
+ "@/*": ["./*"]
20
+ }
21
+ },
22
+ "include": [
23
+ "src",
24
+ "pages",
25
+ "server",
26
+ "stoix.config.ts",
27
+ "vite.config.ts",
28
+ "vite-env.d.ts"
29
+ ],
30
+ "exclude": ["node_modules", "dist"]
31
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "resolveJsonModule": true,
11
+ "outDir": "dist/server",
12
+ "rootDir": "."
13
+ },
14
+ "include": ["server", "stoix.config.ts"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import path from 'node:path';
4
+
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ root: '.',
8
+ publicDir: 'public',
9
+ build: {
10
+ outDir: 'dist/client',
11
+ emptyOutDir: true,
12
+ },
13
+ resolve: {
14
+ alias: {
15
+ '@': path.resolve(__dirname, '.'),
16
+ },
17
+ },
18
+ });