mintiljs 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.
- package/README.md +192 -0
- package/auth/index.ts +1 -0
- package/i18n/index.ts +1 -0
- package/index.ts +1 -0
- package/package.json +68 -0
- package/src/auth/index.ts +5 -0
- package/src/auth/jwt.ts +101 -0
- package/src/auth/middleware.ts +150 -0
- package/src/auth/session.ts +46 -0
- package/src/auth/store.ts +39 -0
- package/src/auth/types.ts +55 -0
- package/src/cli/cli.ts +142 -0
- package/src/cli/generate.ts +142 -0
- package/src/core/client.ts +157 -0
- package/src/core/config.ts +16 -0
- package/src/core/css.ts +72 -0
- package/src/core/islands.ts +90 -0
- package/src/core/logger.ts +37 -0
- package/src/core/routes.ts +247 -0
- package/src/core/runtime.ts +131 -0
- package/src/core/watcher.ts +43 -0
- package/src/i18n/index.ts +57 -0
- package/src/i18n/loader.ts +125 -0
- package/src/i18n/translate.ts +89 -0
- package/src/i18n/types.ts +10 -0
- package/src/index.ts +10 -0
- package/src/render/island.tsx +78 -0
- package/src/render/ssr.tsx +198 -0
- package/src/render/stream.tsx +144 -0
- package/src/router/api.ts +111 -0
- package/src/router/index.ts +9 -0
- package/src/router/middleware.ts +16 -0
- package/src/router/pages.ts +97 -0
- package/src/router/shared.ts +3 -0
- package/src/types/api.ts +106 -0
- package/src/types/config.ts +35 -0
- package/src/types/index.ts +4 -0
- package/src/types/middleware.ts +21 -0
- package/src/types/page.ts +51 -0
- package/src/types/plugin.ts +29 -0
- package/src/utils/fs.ts +46 -0
- package/src/utils/logger.ts +21 -0
- package/src/utils/network.ts +14 -0
- package/templates/default/api/hello.ts +9 -0
- package/templates/default/components/card.tsx +10 -0
- package/templates/default/components/layouts/base.tsx +26 -0
- package/templates/default/lib/greeting.ts +3 -0
- package/templates/default/mintil.config.ts +10 -0
- package/templates/default/package.json +17 -0
- package/templates/default/pages/(*).tsx +11 -0
- package/templates/default/pages/about.tsx +13 -0
- package/templates/default/pages/blog/(:slug).tsx +12 -0
- package/templates/default/pages/counter.tsx +36 -0
- package/templates/default/pages/index.tsx +14 -0
- package/templates/default/public/readme.txt +1 -0
- package/templates/default/styles.css +1 -0
- package/templates/default/tsconfig.json +21 -0
package/src/cli/cli.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import { start } from "../core/runtime.ts";
|
|
5
|
+
import { resolveConfig } from "../core/config.ts";
|
|
6
|
+
import { generate } from "./generate.ts";
|
|
7
|
+
import type { MintilConfig } from "../types/config.ts";
|
|
8
|
+
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
const cmd = args[0] ?? "dev";
|
|
11
|
+
|
|
12
|
+
async function main() {
|
|
13
|
+
switch (cmd) {
|
|
14
|
+
case "init":
|
|
15
|
+
await initProject(args[1]);
|
|
16
|
+
break;
|
|
17
|
+
case "dev":
|
|
18
|
+
case "start":
|
|
19
|
+
await runProject(cmd);
|
|
20
|
+
break;
|
|
21
|
+
case "generate":
|
|
22
|
+
case "g":
|
|
23
|
+
await generate(args[1], args[2]);
|
|
24
|
+
break;
|
|
25
|
+
case "--help":
|
|
26
|
+
case "-h":
|
|
27
|
+
case "help":
|
|
28
|
+
showHelp();
|
|
29
|
+
break;
|
|
30
|
+
default:
|
|
31
|
+
console.error(`Unknown command: ${cmd}`);
|
|
32
|
+
showHelp();
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function showHelp() {
|
|
38
|
+
console.log(`Usage: mintil <command>
|
|
39
|
+
|
|
40
|
+
Commands:
|
|
41
|
+
init <name> Create a new Mintil project in the <name> directory
|
|
42
|
+
dev Run the project in development mode (forces mode: development)
|
|
43
|
+
start Run the project (uses config mode or defaults to production)
|
|
44
|
+
generate <t> <n> Scaffold a page, api, component, or island (alias: g)
|
|
45
|
+
help Show this help message
|
|
46
|
+
|
|
47
|
+
Run \`mintil generate help\` for scaffold type options.
|
|
48
|
+
`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function initProject(name?: string) {
|
|
52
|
+
if (!name) {
|
|
53
|
+
console.error("Please provide a project name: mintil init <name>");
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const target = path.resolve(process.cwd(), name);
|
|
58
|
+
const template = path.resolve(import.meta.dir, "..", "..", "templates", "default");
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
await fs.access(target);
|
|
62
|
+
console.error(`Directory already exists: ${target}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
} catch {
|
|
65
|
+
// expected: directory does not exist
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await fs.cp(template, target, { recursive: true });
|
|
69
|
+
|
|
70
|
+
// Update package.json project name
|
|
71
|
+
const pkgPath = path.join(target, "package.json");
|
|
72
|
+
try {
|
|
73
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8")) as Record<string, unknown>;
|
|
74
|
+
pkg.name = name;
|
|
75
|
+
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
76
|
+
} catch {
|
|
77
|
+
// ignore if no package.json
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(`Created Mintil project at ${target}`);
|
|
81
|
+
console.log(`\nNext steps:\n cd ${name}\n bun install\n mintil dev`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function runProject(command: "dev" | "start") {
|
|
85
|
+
const root = process.cwd();
|
|
86
|
+
const userConfig = await loadEntryConfig(root);
|
|
87
|
+
|
|
88
|
+
const config: MintilConfig = {
|
|
89
|
+
...userConfig,
|
|
90
|
+
root: userConfig?.root ?? root,
|
|
91
|
+
mode:
|
|
92
|
+
command === "dev"
|
|
93
|
+
? "development"
|
|
94
|
+
: (userConfig?.mode ?? (process.env.NODE_ENV as MintilConfig["mode"]) ?? "production"),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const resolved = resolveConfig(config.root as string, config);
|
|
98
|
+
console.log(
|
|
99
|
+
`[mintil] running in ${resolved.mode} mode from ${resolved.root} on port ${resolved.port}`,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const server = await start(config);
|
|
103
|
+
|
|
104
|
+
for (const signal of ["SIGINT", "SIGTERM"] as const) {
|
|
105
|
+
process.on(signal, () => {
|
|
106
|
+
console.log(`\n[mintil] received ${signal}, shutting down...`);
|
|
107
|
+
try {
|
|
108
|
+
server.stop();
|
|
109
|
+
} catch {
|
|
110
|
+
// ignore
|
|
111
|
+
}
|
|
112
|
+
// Bun can keep the process alive when a local tsconfig.json is present,
|
|
113
|
+
// so we force an immediate exit with SIGKILL.
|
|
114
|
+
console.log("[mintil] force kill");
|
|
115
|
+
process.kill(process.pid, "SIGKILL");
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function loadEntryConfig(root: string): Promise<Partial<MintilConfig>> {
|
|
121
|
+
// Prefer mintil.config.ts, fall back to index.ts for legacy projects
|
|
122
|
+
const candidates = ["mintil.config.ts", "mintil.config.tsx", "mintil.config.js", "index.ts", "index.tsx", "index.js"];
|
|
123
|
+
for (const file of candidates) {
|
|
124
|
+
const entry = path.join(root, file);
|
|
125
|
+
try {
|
|
126
|
+
await fs.access(entry);
|
|
127
|
+
const mod: any = await import(entry);
|
|
128
|
+
if (mod.default && typeof mod.default === "object" && !Array.isArray(mod.default)) {
|
|
129
|
+
return mod.default as Partial<MintilConfig>;
|
|
130
|
+
}
|
|
131
|
+
return {};
|
|
132
|
+
} catch {
|
|
133
|
+
// try next candidate
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return {};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
main().catch((err) => {
|
|
140
|
+
console.error(err);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
const TEMPLATES: Record<string, (name: string) => string> = {
|
|
5
|
+
page(name) {
|
|
6
|
+
const comp = pascal(name);
|
|
7
|
+
return `import React from "react";
|
|
8
|
+
|
|
9
|
+
export default function ${comp}() {
|
|
10
|
+
return (
|
|
11
|
+
<div>
|
|
12
|
+
<h1>${comp}</h1>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
`;
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
"page:client"(name) {
|
|
20
|
+
const comp = pascal(name);
|
|
21
|
+
return `import React from "react";
|
|
22
|
+
|
|
23
|
+
export const useClient = true;
|
|
24
|
+
|
|
25
|
+
export default function ${comp}() {
|
|
26
|
+
const [count, setCount] = React.useState(0);
|
|
27
|
+
return (
|
|
28
|
+
<div>
|
|
29
|
+
<h1>${comp}</h1>
|
|
30
|
+
<p>Count: {count}</p>
|
|
31
|
+
<button onClick={() => setCount(c => c + 1)}>+</button>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
api(name) {
|
|
39
|
+
const route = name.toLowerCase().replace(/\s+/g, "-");
|
|
40
|
+
return `import type { ApiMethodHandler } from "mintiljs";
|
|
41
|
+
|
|
42
|
+
export const GET: ApiMethodHandler = (request, response) => {
|
|
43
|
+
return response.json({ message: "Hello from ${route}" });
|
|
44
|
+
};
|
|
45
|
+
`;
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
component(name) {
|
|
49
|
+
const comp = pascal(name);
|
|
50
|
+
return `import React from "react";
|
|
51
|
+
|
|
52
|
+
interface ${comp}Props {
|
|
53
|
+
children?: React.ReactNode;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default function ${comp}({ children }: ${comp}Props) {
|
|
57
|
+
return <div className="mintil-component">{children}</div>;
|
|
58
|
+
}
|
|
59
|
+
`;
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
island(name) {
|
|
63
|
+
const comp = pascal(name);
|
|
64
|
+
return `import React from "react";
|
|
65
|
+
|
|
66
|
+
export default function ${comp}({ greeting = "Hello" }: { greeting?: string }) {
|
|
67
|
+
const [count, setCount] = React.useState(0);
|
|
68
|
+
return (
|
|
69
|
+
<div>
|
|
70
|
+
<p>{greeting} from ${comp}! Count: {count}</p>
|
|
71
|
+
<button onClick={() => setCount(c => c + 1)}>+</button>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function pascal(name: string): string {
|
|
80
|
+
return name
|
|
81
|
+
.split(/[-_\s]+/)
|
|
82
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
83
|
+
.join("");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function fileName(name: string, ext: string = ".tsx"): string {
|
|
87
|
+
return name.toLowerCase().replace(/\s+/g, "-") + ext;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function help(): string {
|
|
91
|
+
return `Usage: mintil generate <type> <name>
|
|
92
|
+
|
|
93
|
+
Types:
|
|
94
|
+
page <name> Scaffold a new page in pages/
|
|
95
|
+
page:client <name> Scaffold a client-side page in pages/
|
|
96
|
+
api <name> Scaffold a new API route in api/
|
|
97
|
+
component <name> Scaffold a new component in components/
|
|
98
|
+
island <name> Scaffold a new island in islands/
|
|
99
|
+
`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function generate(type: string, name?: string) {
|
|
103
|
+
if (!type || type === "help" || !name) {
|
|
104
|
+
console.log(help());
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const root = process.cwd();
|
|
109
|
+
const template = TEMPLATES[type];
|
|
110
|
+
if (!template) {
|
|
111
|
+
console.error(`Unknown type: ${type}`);
|
|
112
|
+
console.log(help());
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const dirMap: Record<string, string> = {
|
|
117
|
+
page: "pages",
|
|
118
|
+
"page:client": "pages",
|
|
119
|
+
api: "api",
|
|
120
|
+
component: "components",
|
|
121
|
+
island: "islands",
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const subdir = dirMap[type];
|
|
125
|
+
const targetDir = path.join(root, subdir);
|
|
126
|
+
const fname = fileName(name);
|
|
127
|
+
const targetFile = path.join(targetDir, fname);
|
|
128
|
+
|
|
129
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
await fs.access(targetFile);
|
|
133
|
+
console.error(`File already exists: ${targetFile}`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
} catch {
|
|
136
|
+
// ok
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const content = template(name);
|
|
140
|
+
await fs.writeFile(targetFile, content);
|
|
141
|
+
console.log(`Created ${type}: ${targetFile}`);
|
|
142
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
const tmpDir = path.join(process.cwd(), "node_modules", ".mintil-client");
|
|
5
|
+
let entryCounter = 0;
|
|
6
|
+
const bundleCache = new Map<string, string>();
|
|
7
|
+
let frameworkCache: string | null = null;
|
|
8
|
+
const FRAMEWORK_ROUTE = "/_mintil/framework.js";
|
|
9
|
+
|
|
10
|
+
async function ensureTmp() {
|
|
11
|
+
await fs.mkdir(tmpDir, { recursive: true }).catch(() => {});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function clearClientCache() {
|
|
15
|
+
bundleCache.clear();
|
|
16
|
+
frameworkCache = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function getFrameworkRoute(): Promise<string> {
|
|
20
|
+
await ensureFrameworkBundle();
|
|
21
|
+
return FRAMEWORK_ROUTE;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function getFrameworkBundle(): Promise<string | null> {
|
|
25
|
+
await ensureFrameworkBundle();
|
|
26
|
+
return frameworkCache;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function ensureFrameworkBundle() {
|
|
30
|
+
if (frameworkCache) return;
|
|
31
|
+
frameworkCache = await buildFrameworkBundle();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function buildFrameworkBundle(): Promise<string | null> {
|
|
35
|
+
await ensureTmp();
|
|
36
|
+
entryCounter++;
|
|
37
|
+
|
|
38
|
+
const frameworkEntry = [
|
|
39
|
+
`import React from "react";`,
|
|
40
|
+
`import { hydrateRoot } from "react-dom/client";`,
|
|
41
|
+
`import { jsxDEV } from "react/jsx-dev-runtime";`,
|
|
42
|
+
``,
|
|
43
|
+
`export default React;`,
|
|
44
|
+
`export { hydrateRoot, jsxDEV };`,
|
|
45
|
+
].join("\n");
|
|
46
|
+
|
|
47
|
+
const entryFile = path.join(tmpDir, `framework-${entryCounter}.tsx`);
|
|
48
|
+
await fs.writeFile(entryFile, frameworkEntry);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const result = await Bun.build({
|
|
52
|
+
entrypoints: [entryFile],
|
|
53
|
+
target: "browser",
|
|
54
|
+
format: "esm",
|
|
55
|
+
jsx: { runtime: "automatic" } as any,
|
|
56
|
+
jsxImportSource: "react",
|
|
57
|
+
minify: true,
|
|
58
|
+
} as any);
|
|
59
|
+
|
|
60
|
+
if (!result.success || !result.outputs[0]) {
|
|
61
|
+
for (const log of result.logs) {
|
|
62
|
+
console.error("[mintil] framework build:", log);
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const js = await result.outputs[0].text();
|
|
68
|
+
await fs.rm(entryFile).catch(() => {});
|
|
69
|
+
return js;
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error("[mintil] framework build error:", err);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface BuildOptions {
|
|
77
|
+
mode?: "development" | "production";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function buildClientBundle(
|
|
81
|
+
pageFile: string,
|
|
82
|
+
pageRoute: string,
|
|
83
|
+
buildOpts?: BuildOptions,
|
|
84
|
+
): Promise<string | null> {
|
|
85
|
+
await ensureTmp();
|
|
86
|
+
entryCounter++;
|
|
87
|
+
|
|
88
|
+
const encodedRoute = encodeURIComponent(pageRoute).replace(/%2F/g, "_").replace(/%3A/g, "_");
|
|
89
|
+
const cacheKey = `${encodedRoute}-${entryCounter}`;
|
|
90
|
+
|
|
91
|
+
if (bundleCache.has(cacheKey)) {
|
|
92
|
+
return bundleCache.get(cacheKey)!;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const isDev = buildOpts?.mode !== "production";
|
|
96
|
+
|
|
97
|
+
const entryContent = [
|
|
98
|
+
`import { hydrateRoot } from "react-dom/client";`,
|
|
99
|
+
`import Page from "${pageFile}";`,
|
|
100
|
+
``,
|
|
101
|
+
`const el = document.getElementById("__mintil-root");`,
|
|
102
|
+
`if (el) {`,
|
|
103
|
+
` const props = JSON.parse(document.getElementById("__PAGE_PROPS__")?.textContent ?? "{}");`,
|
|
104
|
+
` hydrateRoot(el, <Page {...props} />);`,
|
|
105
|
+
`}`,
|
|
106
|
+
].join("\n");
|
|
107
|
+
|
|
108
|
+
const entryFile = path.join(tmpDir, `${cacheKey}.tsx`);
|
|
109
|
+
await fs.writeFile(entryFile, entryContent);
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const result = await Bun.build({
|
|
113
|
+
entrypoints: [entryFile],
|
|
114
|
+
target: "browser",
|
|
115
|
+
format: "esm",
|
|
116
|
+
external: ["react", "react-dom"],
|
|
117
|
+
jsx: { runtime: "automatic" } as any,
|
|
118
|
+
jsxImportSource: "react",
|
|
119
|
+
minify: !isDev,
|
|
120
|
+
sourcemap: isDev ? "external" : undefined,
|
|
121
|
+
} as any);
|
|
122
|
+
|
|
123
|
+
if (!result.success || !result.outputs[0]) {
|
|
124
|
+
for (const log of result.logs) {
|
|
125
|
+
console.error("[mintil] client build:", log);
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const js = await result.outputs[0].text();
|
|
131
|
+
bundleCache.set(cacheKey, js);
|
|
132
|
+
|
|
133
|
+
await fs.rm(entryFile).catch(() => {});
|
|
134
|
+
|
|
135
|
+
// Store source map reference in a separate map for serving
|
|
136
|
+
if (isDev && result.outputs[0].sourcemap) {
|
|
137
|
+
const sm = await result.outputs[0].sourcemap.text().catch(() => null);
|
|
138
|
+
if (sm) sourceMapCache.set(cacheKey, sm);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return js;
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error("[mintil] client build error:", err);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const sourceMapCache = new Map<string, string>();
|
|
149
|
+
|
|
150
|
+
export function getSourceMap(cacheKey: string): string | undefined {
|
|
151
|
+
return sourceMapCache.get(cacheKey);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function getClientCacheKey(pageRoute: string): string {
|
|
155
|
+
const encodedRoute = encodeURIComponent(pageRoute).replace(/%2F/g, "_").replace(/%3A/g, "_");
|
|
156
|
+
return `${encodedRoute}-${entryCounter}`;
|
|
157
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { MintilConfig, ResolvedConfig } from "../types/config.ts";
|
|
2
|
+
import type { MintilPlugin } from "../types/plugin.ts";
|
|
3
|
+
|
|
4
|
+
export function resolveConfig(root: string, userConfig?: MintilConfig): ResolvedConfig {
|
|
5
|
+
const envPort = process.env.PORT ? Number(process.env.PORT) : undefined;
|
|
6
|
+
const envMode = process.env.NODE_ENV === "production" ? "production" : "development";
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
root,
|
|
10
|
+
port: userConfig?.port ?? envPort ?? 3456,
|
|
11
|
+
hosts: userConfig?.hosts ?? [],
|
|
12
|
+
mode: userConfig?.mode ?? (envMode as ResolvedConfig["mode"]),
|
|
13
|
+
host: userConfig?.host ?? false,
|
|
14
|
+
plugins: userConfig?.plugins ?? [],
|
|
15
|
+
};
|
|
16
|
+
}
|
package/src/core/css.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
let cached: { root: string; css: string; mode: string } | null = null;
|
|
5
|
+
|
|
6
|
+
export function clearCssCache() {
|
|
7
|
+
cached = null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function minifyCss(css: string): string {
|
|
11
|
+
return css
|
|
12
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
13
|
+
.replace(/\s*([{}:;,])\s*/g, "$1")
|
|
14
|
+
.replace(/\s+/g, " ")
|
|
15
|
+
.replace(/;\}/g, "}")
|
|
16
|
+
.trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function processCss(
|
|
20
|
+
root: string,
|
|
21
|
+
mode?: "development" | "production",
|
|
22
|
+
): Promise<string> {
|
|
23
|
+
if (cached?.root === root && cached?.mode === mode) return cached.css;
|
|
24
|
+
|
|
25
|
+
const cssPath = path.join(root, "styles.css");
|
|
26
|
+
let rawCss: string;
|
|
27
|
+
try {
|
|
28
|
+
rawCss = await fs.readFile(cssPath, "utf-8");
|
|
29
|
+
} catch {
|
|
30
|
+
cached = { root, css: "", mode: mode ?? "development" };
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const postcssMod: any = await import("postcss");
|
|
36
|
+
const tailwindMod: any = await import("@tailwindcss/postcss");
|
|
37
|
+
const postcss = postcssMod.default ?? postcssMod;
|
|
38
|
+
const tailwind = tailwindMod.default ?? tailwindMod;
|
|
39
|
+
const result = await postcss([tailwind()]).process(rawCss, {
|
|
40
|
+
from: cssPath,
|
|
41
|
+
});
|
|
42
|
+
let css = result.css;
|
|
43
|
+
|
|
44
|
+
if (mode === "production") {
|
|
45
|
+
try {
|
|
46
|
+
const cssnanoMod: any = await import("cssnano");
|
|
47
|
+
const cssnano = cssnanoMod.default ?? cssnanoMod;
|
|
48
|
+
const nanoResult = await postcss([cssnano()]).process(css, { from: cssPath });
|
|
49
|
+
css = nanoResult.css;
|
|
50
|
+
} catch {
|
|
51
|
+
css = minifyCss(css);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
cached = { root, css, mode: mode ?? "development" };
|
|
56
|
+
return css;
|
|
57
|
+
} catch {
|
|
58
|
+
let css = rawCss;
|
|
59
|
+
if (mode === "production") {
|
|
60
|
+
css = minifyCss(css);
|
|
61
|
+
}
|
|
62
|
+
cached = { root, css, mode: mode ?? "development" };
|
|
63
|
+
return css;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function getCss(
|
|
68
|
+
root: string,
|
|
69
|
+
mode?: "development" | "production",
|
|
70
|
+
): Promise<string> {
|
|
71
|
+
return processCss(root, mode);
|
|
72
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { pathExists, walkFiles, fileToRoutePath } from "../utils/fs.ts";
|
|
4
|
+
import { registerIsland } from "../render/island.tsx";
|
|
5
|
+
|
|
6
|
+
const tmpDir = path.join(process.cwd(), "node_modules", ".mintil-client");
|
|
7
|
+
let buildCounter = 0;
|
|
8
|
+
const bundleCache = new Map<string, string>();
|
|
9
|
+
|
|
10
|
+
async function ensureTmp() {
|
|
11
|
+
await fs.mkdir(tmpDir, { recursive: true }).catch(() => {});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function clearIslandCache() {
|
|
15
|
+
bundleCache.clear();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function loadAndRegisterIslands(root: string) {
|
|
19
|
+
const islandsDir = path.join(root, "islands");
|
|
20
|
+
if (!(await pathExists(islandsDir))) return;
|
|
21
|
+
|
|
22
|
+
for await (const file of walkFiles(islandsDir)) {
|
|
23
|
+
const ext = path.extname(file);
|
|
24
|
+
if (ext !== ".tsx" && ext !== ".jsx" && ext !== ".ts" && ext !== ".js") continue;
|
|
25
|
+
|
|
26
|
+
const name = path.basename(file, ext);
|
|
27
|
+
const mod: any = await import(file);
|
|
28
|
+
const component = mod.default ?? null;
|
|
29
|
+
if (typeof component !== "function") continue;
|
|
30
|
+
|
|
31
|
+
registerIsland(name, { component, file });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function buildIslandBundle(
|
|
36
|
+
name: string,
|
|
37
|
+
islandFile: string,
|
|
38
|
+
buildOpts?: { mode?: "development" | "production" },
|
|
39
|
+
): Promise<string | null> {
|
|
40
|
+
await ensureTmp();
|
|
41
|
+
buildCounter++;
|
|
42
|
+
|
|
43
|
+
const cacheKey = `island-${name}`;
|
|
44
|
+
if (bundleCache.has(cacheKey)) return bundleCache.get(cacheKey)!;
|
|
45
|
+
|
|
46
|
+
const isDev = buildOpts?.mode !== "production";
|
|
47
|
+
|
|
48
|
+
const entryContent = [
|
|
49
|
+
`import React from "react";`,
|
|
50
|
+
`import { hydrateRoot } from "react-dom/client";`,
|
|
51
|
+
`import Component from "${islandFile}";`,
|
|
52
|
+
``,
|
|
53
|
+
`const el = document.querySelector('[data-island="${name}"]');`,
|
|
54
|
+
`if (el) {`,
|
|
55
|
+
` const props = JSON.parse(el.getAttribute("data-props") || "{}");`,
|
|
56
|
+
` hydrateRoot(el, React.createElement(Component, props));`,
|
|
57
|
+
`}`,
|
|
58
|
+
].join("\n");
|
|
59
|
+
|
|
60
|
+
const entryFile = path.join(tmpDir, `${cacheKey}-${buildCounter}.tsx`);
|
|
61
|
+
await fs.writeFile(entryFile, entryContent);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const result = await Bun.build({
|
|
65
|
+
entrypoints: [entryFile],
|
|
66
|
+
target: "browser",
|
|
67
|
+
format: "esm",
|
|
68
|
+
external: ["react", "react-dom"],
|
|
69
|
+
jsx: { runtime: "automatic" } as any,
|
|
70
|
+
jsxImportSource: "react",
|
|
71
|
+
minify: !isDev,
|
|
72
|
+
sourcemap: isDev ? "external" : undefined,
|
|
73
|
+
} as any);
|
|
74
|
+
|
|
75
|
+
if (!result.success || !result.outputs[0]) {
|
|
76
|
+
for (const log of result.logs) {
|
|
77
|
+
console.error("[mintil] island build:", log);
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const js = await result.outputs[0].text();
|
|
83
|
+
bundleCache.set(cacheKey, js);
|
|
84
|
+
await fs.rm(entryFile).catch(() => {});
|
|
85
|
+
return js;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.error("[mintil] island build error:", err);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { color, formatDuration, statusColorFor } from "../utils/logger.ts";
|
|
2
|
+
|
|
3
|
+
export function logMiddleware(serverRef?: { current: ReturnType<typeof Bun.serve> | null }) {
|
|
4
|
+
return async function (c: any, next: () => Promise<void>) {
|
|
5
|
+
const startTime = performance.now();
|
|
6
|
+
const method = c.req.method;
|
|
7
|
+
const pathname = c.req.path;
|
|
8
|
+
|
|
9
|
+
await next();
|
|
10
|
+
|
|
11
|
+
const status = c.res.status;
|
|
12
|
+
const durationMs = performance.now() - startTime;
|
|
13
|
+
const duration = formatDuration(durationMs);
|
|
14
|
+
const ip = getClientIp(c, serverRef);
|
|
15
|
+
const date = new Date().toLocaleString("pt-BR");
|
|
16
|
+
const reset = color.reset;
|
|
17
|
+
|
|
18
|
+
console.log(
|
|
19
|
+
`${date} ${color.green}${method.padEnd(7)}${reset} ${pathname} ${statusColorFor(status)}${status}${reset} - ${duration} - ${ip}`,
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getClientIp(
|
|
25
|
+
c: any,
|
|
26
|
+
serverRef?: { current: ReturnType<typeof Bun.serve> | null },
|
|
27
|
+
): string {
|
|
28
|
+
const forwarded = c.req.header("x-forwarded-for") || c.req.header("x-real-ip");
|
|
29
|
+
if (forwarded) {
|
|
30
|
+
return String(forwarded).split(",")[0]?.trim() ?? "-";
|
|
31
|
+
}
|
|
32
|
+
if (serverRef?.current) {
|
|
33
|
+
const info = (serverRef.current as any).requestIP?.(c.req.raw);
|
|
34
|
+
if (info) return info.address;
|
|
35
|
+
}
|
|
36
|
+
return "-";
|
|
37
|
+
}
|