fluxion-ts 0.0.4

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.
Files changed (43) hide show
  1. package/.oxlintrc.json +64 -0
  2. package/.prettierrc +6 -0
  3. package/AGENTS.md +3 -0
  4. package/Dockerfile +13 -0
  5. package/LICENSE +21 -0
  6. package/README.md +11 -0
  7. package/document/index.html +16 -0
  8. package/document/src/main.tsx +8 -0
  9. package/document/src/styles.css +321 -0
  10. package/document/src/view/App.tsx +304 -0
  11. package/document/src/view/CodeBlock.tsx +11 -0
  12. package/document/src/view/Section.tsx +18 -0
  13. package/document/vite.config.ts +23 -0
  14. package/draft/vibe.md +50 -0
  15. package/package.json +66 -0
  16. package/rollup.config.mjs +102 -0
  17. package/scripts/build-image.ts +13 -0
  18. package/scripts/bump-version.ts +12 -0
  19. package/scripts/configs.ts +79 -0
  20. package/scripts/lines.ts +54 -0
  21. package/scripts/publish.ts +6 -0
  22. package/src/common/consts.ts +30 -0
  23. package/src/common/dtm.ts +10 -0
  24. package/src/common/logger.ts +145 -0
  25. package/src/core/meta-api.ts +48 -0
  26. package/src/core/server.ts +447 -0
  27. package/src/core/types.d.ts +6 -0
  28. package/src/core/utils/headers.ts +34 -0
  29. package/src/core/utils/request.ts +145 -0
  30. package/src/core/utils/send-json.ts +21 -0
  31. package/src/index.ts +11 -0
  32. package/src/workers/file-runtime.ts +1071 -0
  33. package/src/workers/handler-worker-pool.ts +754 -0
  34. package/src/workers/handler-worker.ts +1029 -0
  35. package/src/workers/options.ts +77 -0
  36. package/src/workers/protocol.d.ts +186 -0
  37. package/tests/core/dynamic-directory.test.ts +48 -0
  38. package/tests/core/file-runtime.test.ts +347 -0
  39. package/tests/core/server-options.test.ts +204 -0
  40. package/tests/e2e/fluxion-server.e2e-spec.ts +225 -0
  41. package/tests/helpers/test-utils.ts +81 -0
  42. package/tsconfig.json +22 -0
  43. package/vitest.config.ts +24 -0
@@ -0,0 +1,79 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+
4
+ import type { RollupReplaceOptions } from '@rollup/plugin-replace';
5
+
6
+ export const getTSConfig = () => {
7
+ const tsconfigBuildPath = path.join(import.meta.dirname, '..', 'tsconfig.build.json');
8
+ const tsconfigPath = path.join(import.meta.dirname, '..', 'tsconfig.json');
9
+ return fs.existsSync(tsconfigBuildPath) ? tsconfigBuildPath : tsconfigPath;
10
+ };
11
+
12
+ // #region replace options
13
+
14
+ interface CommonPackageJson {
15
+ name: string;
16
+ version: string;
17
+ description: string;
18
+ description_zh: string;
19
+ author: {
20
+ name: string;
21
+ email: string;
22
+ };
23
+ license: string;
24
+ repository: {
25
+ type: string;
26
+ url: string;
27
+ };
28
+ }
29
+
30
+ export function replaceOpts(): RollupReplaceOptions {
31
+ const pkg = JSON.parse(
32
+ fs.readFileSync(path.join(import.meta.dirname, '..', 'package.json'), 'utf-8'),
33
+ ) as CommonPackageJson;
34
+ function formatDateFull(dt = new Date()) {
35
+ const y = dt.getFullYear();
36
+ const m = String(dt.getMonth() + 1).padStart(2, '0');
37
+ const d = String(dt.getDate()).padStart(2, '0');
38
+ const hh = String(dt.getHours()).padStart(2, '0');
39
+ const mm = String(dt.getMinutes()).padStart(2, '0');
40
+ const ss = String(dt.getSeconds()).padStart(2, '0');
41
+ const ms = String(dt.getMilliseconds()).padStart(3, '0');
42
+ return `${y}.${m}.${d} ${hh}:${mm}:${ss}.${ms}`;
43
+ }
44
+
45
+ const __KEBAB_NAME__ = pkg.name.replace('rollup-plugin-', '');
46
+ const __VERSION__ = pkg.version;
47
+ const __NAME__ = __KEBAB_NAME__.replace(/(^|-)(\w)/g, (_, __, c) => c.toUpperCase());
48
+
49
+ const __PKG_INFO__ = `## About
50
+ * @package ${__NAME__}
51
+ * @author ${pkg.author.name} <${pkg.author.email}>
52
+ * @version ${pkg.version} (Last Update: ${formatDateFull()})
53
+ * @license ${pkg.license}
54
+ * @link ${pkg.repository.url}
55
+ * @link https://baendlorel.github.io/ Welcome to my site!
56
+ * @description ${pkg.description.replace(/\n/g, '\n * \n * ')}
57
+ * @copyright Copyright (c) ${new Date().getFullYear()} ${pkg.author.name}. All rights reserved.`;
58
+
59
+ return {
60
+ preventAssignment: true,
61
+ delimiters: ['', ''],
62
+ values: {
63
+ __IS_DEV__: 'false',
64
+ __NAME__,
65
+ __KEBAB_NAME__,
66
+ __PKG_INFO__,
67
+ __VERSION__,
68
+
69
+ // global error/warn/debug
70
+ "$throw('": `throw new Error('[${__NAME__} error] `,
71
+ '$throw(`': `throw new Error(\`[${__NAME__} error] `,
72
+ '$throw("': `throw new Error("[${__NAME__} error] `,
73
+ '$warn(': `console.warn('[${__NAME__} warn]',`,
74
+ '$error(': `console.error('[${__NAME__} error]',`,
75
+ '$debug(': `console.debug('[${__NAME__} debug]',`,
76
+ },
77
+ };
78
+ }
79
+ // #endregion
@@ -0,0 +1,54 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const ROOT = path.resolve('./packages');
5
+
6
+ const INCLUDE = ['**/*.{ts,tsx,css}'];
7
+ const EXCLUDE = ['**/node_modules/**', '**/dist/**', '**/*.test.{ts,tsx}'];
8
+
9
+ let totalLines = 0;
10
+ let fileCount = 0;
11
+
12
+ function countLines(filePath: string): number {
13
+ const content = fs.readFileSync(filePath, 'utf8');
14
+ return content.split(/\r\n|\n|\r/).length;
15
+ }
16
+
17
+ function resolveFiles(rootDir: string): string[] {
18
+ const matched = fs
19
+ .globSync(INCLUDE, {
20
+ cwd: rootDir,
21
+ exclude: EXCLUDE,
22
+ })
23
+ .map((relativePath) => path.relative(ROOT, path.join(rootDir, relativePath)).replaceAll(path.sep, '/'));
24
+
25
+ const unique = Array.from(new Set(matched));
26
+
27
+ return unique
28
+ .filter((relativePath) => fs.existsSync(path.join(ROOT, relativePath)))
29
+ .sort((a, b) => a.localeCompare(b));
30
+ }
31
+
32
+ // "lines": "clear && find ./packages -type f \\( -name \"*.ts\" -o -name \"*.tsx\" -o -name \"*.js\" -o -name \"*.mjs\" -o -name \"*.cjs\" \\) | xargs wc -l",
33
+
34
+ function main() {
35
+ const subPackage = process.argv[2];
36
+ const rootDir = subPackage ? path.join(ROOT, subPackage) : ROOT;
37
+ if (!fs.existsSync(rootDir)) {
38
+ console.error(`Sub package not found: ${subPackage}`);
39
+ process.exit(1);
40
+ }
41
+
42
+ const files = resolveFiles(rootDir);
43
+ for (const relativePath of files) {
44
+ const fullPath = path.join(ROOT, relativePath);
45
+ const lines = countLines(fullPath);
46
+ totalLines += lines;
47
+ fileCount++;
48
+ console.log(`${lines.toString().padStart(7)} ${relativePath}`);
49
+ }
50
+
51
+ console.log(`Files: ${fileCount} for ${INCLUDE.join(', ')}`);
52
+ console.log(`Total lines: ${totalLines}`);
53
+ }
54
+ main();
@@ -0,0 +1,6 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { bumpVersion } from './bump-version';
3
+
4
+ bumpVersion();
5
+ execSync('pnpm build', { stdio: 'inherit' });
6
+ execSync('pnpm publish --access public --no-git-checks', { stdio: 'inherit' });
@@ -0,0 +1,30 @@
1
+ export const DUMMY_BASE_URL = 'http://fluxion.local';
2
+ export const META_PREFIX = '/_fluxion';
3
+
4
+ export const STATIC_CONTENT_TYPES: Record<string, string> = {
5
+ '.css': 'text/css; charset=utf-8',
6
+ '.html': 'text/html; charset=utf-8',
7
+ '.ico': 'image/x-icon',
8
+ '.js': 'text/javascript; charset=utf-8',
9
+ '.json': 'application/json; charset=utf-8',
10
+ '.map': 'application/json; charset=utf-8',
11
+ '.png': 'image/png',
12
+ '.jpg': 'image/jpeg',
13
+ '.jpeg': 'image/jpeg',
14
+ '.svg': 'image/svg+xml',
15
+ '.txt': 'text/plain; charset=utf-8',
16
+ '.webp': 'image/webp',
17
+ };
18
+
19
+ export const enum HttpCode {
20
+ Ok = 200,
21
+ BadRequest = 400,
22
+ PayloadTooLarge = 413,
23
+ NotFound = 404,
24
+ InternalServerError = 500,
25
+ }
26
+
27
+ export const enum HandlerResult {
28
+ NotFound,
29
+ Handled,
30
+ }
@@ -0,0 +1,10 @@
1
+ export function dtm(dt = new Date()) {
2
+ const y = dt.getFullYear();
3
+ const m = String(dt.getMonth() + 1).padStart(2, '0');
4
+ const d = String(dt.getDate()).padStart(2, '0');
5
+ const hh = String(dt.getHours()).padStart(2, '0');
6
+ const mm = String(dt.getMinutes()).padStart(2, '0');
7
+ const ss = String(dt.getSeconds()).padStart(2, '0');
8
+ const ms = String(dt.getMilliseconds()).padStart(3, '0');
9
+ return `${y}.${m}.${d} ${hh}:${mm}:${ss}.${ms}`;
10
+ }
@@ -0,0 +1,145 @@
1
+ import chalk from 'chalk';
2
+ import { dtm } from './dtm.js';
3
+
4
+ export type LogLevel = 'INFO' | 'WARN' | 'ERROR' | 'SUCC';
5
+
6
+ export interface LogEntry {
7
+ timestamp: string;
8
+ level: LogLevel;
9
+ event: string;
10
+ message?: string;
11
+ [key: string]: unknown;
12
+ }
13
+
14
+ export type LoggerMode = 'one-line' | 'json-line';
15
+ export type LoggerSink = (entry: LogEntry) => void;
16
+ export type LoggerOption = LoggerMode | LoggerSink;
17
+
18
+ export interface FluxionLogger {
19
+ write(level: LogLevel, event: string, fields?: Record<string, unknown>): void;
20
+ }
21
+
22
+ function safeStringify(value: unknown): string {
23
+ try {
24
+ return JSON.stringify(value);
25
+ } catch {
26
+ return '[unserializable]';
27
+ }
28
+ }
29
+
30
+ function omitReservedFields(entry: LogEntry): Record<string, unknown> {
31
+ const fields: Record<string, unknown> = {};
32
+ const keys = Object.keys(entry);
33
+
34
+ for (let i = 0; i < keys.length; i++) {
35
+ const key = keys[i];
36
+ if (key === 'timestamp' || key === 'level' || key === 'event' || key === 'message') {
37
+ continue;
38
+ }
39
+
40
+ fields[key] = entry[key];
41
+ }
42
+
43
+ return fields;
44
+ }
45
+
46
+ function writeOneLine(entry: LogEntry): void {
47
+ const fields = omitReservedFields(entry);
48
+ const hasFields = Object.keys(fields).length > 0;
49
+ const timestampText = chalk.hex('#166534')(`[${entry.timestamp}]`);
50
+ const levelText = formatLevel(entry.level);
51
+ const body = entry.message ?? entry.event;
52
+ const bodyText = chalk.white(body);
53
+ const fieldsText = hasFields ? ` ${chalk.dim(safeStringify(fields))}` : '';
54
+
55
+ console.log(`${timestampText} ${levelText} ${bodyText}${fieldsText}`);
56
+ }
57
+
58
+ function writeJsonLine(entry: LogEntry): void {
59
+ console.log(safeStringify(entry));
60
+ }
61
+
62
+ export function resolveLoggerSink(option: LoggerOption | undefined): LoggerSink {
63
+ if (option === undefined || option === 'one-line') {
64
+ return writeOneLine;
65
+ }
66
+
67
+ if (option === 'json-line') {
68
+ return writeJsonLine;
69
+ }
70
+
71
+ if (typeof option === 'function') {
72
+ return option;
73
+ }
74
+
75
+ throw new Error('Invalid logger option: expected function | "one-line" | "json-line"');
76
+ }
77
+
78
+ function formatLevel(level: LogLevel): string {
79
+ const label = level;
80
+
81
+ switch (level) {
82
+ case 'INFO':
83
+ return chalk.hex('#38bdf8')(label);
84
+ case 'WARN':
85
+ return chalk.hex('#fb923c')(label);
86
+ case 'ERROR':
87
+ return chalk.hex('#ef4444')(label);
88
+ case 'SUCC':
89
+ return chalk.hex('#22c55e')(label);
90
+ default:
91
+ return label;
92
+ }
93
+ }
94
+
95
+ export function createLogger(option: LoggerOption | undefined = 'one-line'): FluxionLogger {
96
+ const sink = resolveLoggerSink(option);
97
+
98
+ return {
99
+ write(level: LogLevel, event: string, fields: Record<string, unknown> = {}): void {
100
+ const entry: LogEntry = {
101
+ timestamp: dtm(),
102
+ level,
103
+ event,
104
+ };
105
+
106
+ const keys = Object.keys(fields);
107
+ for (let i = 0; i < keys.length; i++) {
108
+ const key = keys[i];
109
+
110
+ if (key === 'timestamp' || key === 'level' || key === 'event') {
111
+ continue;
112
+ }
113
+
114
+ entry[key] = fields[key];
115
+ }
116
+
117
+ try {
118
+ sink(entry);
119
+ } catch {
120
+ // Ignore logger sink failures to avoid breaking request handling.
121
+ }
122
+ },
123
+ };
124
+ }
125
+
126
+ export function getErrorMessage(error: unknown): string {
127
+ // ! Error.isError needs Node.js 24
128
+ return Error.isError(error) ? error.message : String(error);
129
+ }
130
+
131
+ const defaultLogger = createLogger('one-line');
132
+
133
+ /**
134
+ * Compatibility helper for existing call sites. Prefer `createLogger().write`.
135
+ */
136
+ export function logJsonl(level: LogLevel, event: string, fields: Record<string, unknown> = {}): void {
137
+ defaultLogger.write(level, event, fields);
138
+ }
139
+
140
+ /**
141
+ * Compatibility helper for existing call sites. Prefer structured events.
142
+ */
143
+ export function log(level: LogLevel, message: string, fields: Record<string, unknown> = {}): void {
144
+ defaultLogger.write(level, 'Message', { message, ...fields });
145
+ }
@@ -0,0 +1,48 @@
1
+ import type http from 'node:http';
2
+
3
+ import type { FileRouteSnapshot, FileWorkerSnapshot } from '@/workers/file-runtime.js';
4
+ import { sendJson } from './utils/send-json.js';
5
+ import type { NormalizedRequest } from './types.js';
6
+ import { META_PREFIX } from '@/common/consts.js';
7
+
8
+ interface CreateMetaApiOptions {
9
+ /**
10
+ * Same as `FluxionOptions.dir`.
11
+ */
12
+ dir: string;
13
+ getRouteSnapshot: () => Promise<FileRouteSnapshot> | FileRouteSnapshot;
14
+ getWorkerSnapshot: () => Promise<FileWorkerSnapshot> | FileWorkerSnapshot;
15
+ }
16
+
17
+ export function createMetaApi(options: CreateMetaApiOptions) {
18
+ return {
19
+ handleRequest: async (
20
+ _req: http.IncomingMessage,
21
+ res: http.ServerResponse,
22
+ normalized: NormalizedRequest,
23
+ ): Promise<boolean> => {
24
+ const pathname = normalized.url.pathname;
25
+
26
+ if (normalized.method === 'GET') {
27
+ if (pathname === META_PREFIX + '/routes') {
28
+ const routes = await options.getRouteSnapshot();
29
+ sendJson(res, { routes });
30
+ return true;
31
+ }
32
+
33
+ if (pathname === META_PREFIX + '/healthz') {
34
+ sendJson(res, { ok: true, now: Date.now() });
35
+ return true;
36
+ }
37
+
38
+ if (pathname === META_PREFIX + '/workers') {
39
+ const workers = await options.getWorkerSnapshot();
40
+ sendJson(res, { workers });
41
+ return true;
42
+ }
43
+ }
44
+
45
+ return false;
46
+ },
47
+ };
48
+ }