@fluixi/cli 0.1.0-alpha.82 → 0.1.0-alpha.83
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 +21 -3
- package/dist/add.cjs +590 -0
- package/dist/add.d.ts +10 -0
- package/dist/add.d.ts.map +1 -0
- package/dist/add.js +107 -0
- package/dist/add.mjs +562 -0
- package/dist/check.cjs +102 -0
- package/dist/check.d.ts +13 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +88 -0
- package/dist/check.mjs +76 -0
- package/dist/cli.cjs +977 -78
- package/dist/cli.js +44 -2
- package/dist/cli.mjs +976 -77
- package/dist/doctor.cjs +119 -0
- package/dist/doctor.d.ts +13 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +114 -0
- package/dist/doctor.mjs +92 -0
- package/dist/generate.cjs +614 -65
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +118 -48
- package/dist/generate.mjs +619 -64
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/report.cjs +54 -0
- package/dist/report.d.ts +5 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +31 -0
- package/dist/report.mjs +28 -0
- package/dist/scaffold/auth.cjs +422 -0
- package/dist/scaffold/auth.d.ts +39 -0
- package/dist/scaffold/auth.d.ts.map +1 -0
- package/dist/scaffold/auth.js +137 -0
- package/dist/scaffold/auth.mjs +389 -0
- package/dist/scaffold/edit.cjs +153 -0
- package/dist/scaffold/edit.d.ts +35 -0
- package/dist/scaffold/edit.d.ts.map +1 -0
- package/dist/scaffold/edit.js +140 -0
- package/dist/scaffold/edit.mjs +124 -0
- package/dist/scaffold/index.cjs +610 -0
- package/dist/scaffold/index.d.ts +10 -0
- package/dist/scaffold/index.d.ts.map +1 -0
- package/dist/scaffold/index.js +9 -0
- package/dist/scaffold/index.mjs +569 -0
- package/dist/scaffold/prompt.cjs +108 -0
- package/dist/scaffold/prompt.d.ts +9 -0
- package/dist/scaffold/prompt.d.ts.map +1 -0
- package/dist/scaffold/prompt.js +95 -0
- package/dist/scaffold/prompt.mjs +81 -0
- package/dist/scaffold/templates.cjs +274 -0
- package/dist/scaffold/templates.d.ts +26 -0
- package/dist/scaffold/templates.d.ts.map +1 -0
- package/dist/scaffold/templates.js +216 -0
- package/dist/scaffold/templates.mjs +229 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +19 -3
package/dist/cli.cjs
CHANGED
|
@@ -1,23 +1,596 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __export = (target, all) => {
|
|
5
|
+
for (var name in all)
|
|
6
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
7
|
+
};
|
|
3
8
|
|
|
4
9
|
// src/cli.ts
|
|
5
10
|
var import_config = require("@fluixi/start/config");
|
|
6
11
|
var import_commands = require("@fluixi/start/commands");
|
|
7
|
-
var
|
|
12
|
+
var import_ui5 = require("@fluixi/start/ui");
|
|
8
13
|
|
|
9
14
|
// src/generate.ts
|
|
15
|
+
var import_node_fs3 = require("node:fs");
|
|
16
|
+
var import_node_path4 = require("node:path");
|
|
17
|
+
var import_ui2 = require("@fluixi/start/ui");
|
|
18
|
+
|
|
19
|
+
// src/scaffold/auth.ts
|
|
20
|
+
var import_node_fs2 = require("node:fs");
|
|
21
|
+
var import_node_path2 = require("node:path");
|
|
22
|
+
|
|
23
|
+
// src/scaffold/edit.ts
|
|
10
24
|
var import_node_fs = require("node:fs");
|
|
11
25
|
var import_node_path = require("node:path");
|
|
26
|
+
function closing(text, open) {
|
|
27
|
+
let depth = 0;
|
|
28
|
+
for (let i = open; i < text.length; i++) {
|
|
29
|
+
const ch = text[i];
|
|
30
|
+
if (ch === "/" && text[i + 1] === "/") i = text.indexOf("\n", i) === -1 ? text.length : text.indexOf("\n", i);
|
|
31
|
+
else if (ch === "/" && text[i + 1] === "*") i = text.indexOf("*/", i + 2) + 1;
|
|
32
|
+
else if (ch === "'" || ch === '"' || ch === "`") {
|
|
33
|
+
for (i++; i < text.length && text[i] !== ch; i++) if (text[i] === "\\") i++;
|
|
34
|
+
} else if ("([{".includes(ch)) depth++;
|
|
35
|
+
else if (")]}".includes(ch) && --depth === 0) return i;
|
|
36
|
+
}
|
|
37
|
+
return -1;
|
|
38
|
+
}
|
|
39
|
+
function appendToArray(text, open, items) {
|
|
40
|
+
const close = closing(text, open);
|
|
41
|
+
const inner = text.slice(open + 1, close);
|
|
42
|
+
if (!inner.trim()) return text.slice(0, open + 1) + items.join(", ") + text.slice(close);
|
|
43
|
+
if (inner.includes("\n")) {
|
|
44
|
+
const last = inner.trimEnd();
|
|
45
|
+
const indent = last.slice(last.lastIndexOf("\n") + 1).match(/^\s*/)[0];
|
|
46
|
+
const comma = last.endsWith(",") ? "" : ",";
|
|
47
|
+
const tail = inner.slice(last.length);
|
|
48
|
+
return text.slice(0, open + 1) + last + comma + items.map((i) => `
|
|
49
|
+
${indent}${i},`).join("") + tail + text.slice(close);
|
|
50
|
+
}
|
|
51
|
+
const body = inner.replace(/,?\s*$/, "");
|
|
52
|
+
const trailing = inner.slice(body.length).replace(/^,/, "");
|
|
53
|
+
return text.slice(0, open + 1) + body + ", " + items.join(", ") + trailing + text.slice(close);
|
|
54
|
+
}
|
|
55
|
+
function addImport(text, line) {
|
|
56
|
+
if (text.includes(line)) return text;
|
|
57
|
+
const imports = [...text.matchAll(/^import[\s\S]*?from\s+['"][^'"]+['"];?[^\n]*\n/gm)];
|
|
58
|
+
const at = imports.length ? imports[imports.length - 1].index + imports[imports.length - 1][0].length : 0;
|
|
59
|
+
return text.slice(0, at) + line + "\n" + text.slice(at);
|
|
60
|
+
}
|
|
61
|
+
function register(log, file, header, r) {
|
|
62
|
+
const imports = [...r.extraImports ?? [], r.importLine];
|
|
63
|
+
if (!(0, import_node_fs.existsSync)(file)) {
|
|
64
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(file), { recursive: true });
|
|
65
|
+
(0, import_node_fs.writeFileSync)(
|
|
66
|
+
file,
|
|
67
|
+
`${header}import { defineApp } from '@fluixi/start/app';
|
|
68
|
+
${imports.join("\n")}
|
|
69
|
+
|
|
70
|
+
export default defineApp({ imports: [${r.entry}] });
|
|
71
|
+
`
|
|
72
|
+
);
|
|
73
|
+
log.push({ kind: "created", file });
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
let text = (0, import_node_fs.readFileSync)(file, "utf8");
|
|
77
|
+
if (text.includes(r.importLine)) {
|
|
78
|
+
log.push({ kind: "listed", file, detail: r.entry });
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
const m = text.match(/\bimports\s*:\s*\[/);
|
|
82
|
+
if (!m) {
|
|
83
|
+
log.push({ kind: "hint", file, lines: [...imports, `imports: [${r.entry}]`] });
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
text = appendToArray(text, m.index + m[0].length - 1, [r.entry]);
|
|
87
|
+
for (const line of imports) text = addImport(text, line);
|
|
88
|
+
(0, import_node_fs.writeFileSync)(file, text);
|
|
89
|
+
log.push({ kind: "updated", file, detail: `imports ${r.entry}` });
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
function envPublicNames(configFile) {
|
|
93
|
+
if (!(0, import_node_fs.existsSync)(configFile)) return null;
|
|
94
|
+
const text = (0, import_node_fs.readFileSync)(configFile, "utf8");
|
|
95
|
+
const m = text.match(/\bpublic\s*:\s*\[/);
|
|
96
|
+
if (!m) return null;
|
|
97
|
+
const open = m.index + m[0].length - 1;
|
|
98
|
+
const list = text.slice(open, closing(text, open) + 1);
|
|
99
|
+
return [...list.matchAll(/['"]([A-Za-z_][A-Za-z0-9_]*)['"]/g)].map((x) => x[1]);
|
|
100
|
+
}
|
|
101
|
+
function addEnvPublic(log, configFile, names) {
|
|
102
|
+
const list = (indent) => `${indent}public: [
|
|
103
|
+
${names.map((n) => `${indent} '${n}',
|
|
104
|
+
`).join("")}${indent}],
|
|
105
|
+
`;
|
|
106
|
+
const have = envPublicNames(configFile);
|
|
107
|
+
if (have === null) {
|
|
108
|
+
const text2 = (0, import_node_fs.existsSync)(configFile) ? (0, import_node_fs.readFileSync)(configFile, "utf8") : "";
|
|
109
|
+
const call = text2.match(/defineConfig\(\{\n/);
|
|
110
|
+
if (call && !/\benv\s*:/.test(text2)) {
|
|
111
|
+
const at = call.index + call[0].length;
|
|
112
|
+
(0, import_node_fs.writeFileSync)(configFile, text2.slice(0, at) + ` env: {
|
|
113
|
+
${list(" ")} },
|
|
114
|
+
` + text2.slice(at));
|
|
115
|
+
log.push({ kind: "updated", file: configFile, detail: `env.public ${names.join(", ")}` });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
log.push({ kind: "hint", file: configFile, lines: [`env: { public: [${names.map((n) => `'${n}'`).join(", ")}] },`] });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const missing = names.filter((n) => !have.includes(n));
|
|
122
|
+
if (!missing.length) return;
|
|
123
|
+
const text = (0, import_node_fs.readFileSync)(configFile, "utf8");
|
|
124
|
+
const m = text.match(/\bpublic\s*:\s*\[/);
|
|
125
|
+
(0, import_node_fs.writeFileSync)(configFile, appendToArray(text, m.index + m[0].length - 1, missing.map((n) => `'${n}'`)));
|
|
126
|
+
log.push({ kind: "updated", file: configFile, detail: `env.public + ${missing.join(", ")}` });
|
|
127
|
+
}
|
|
128
|
+
function addEnvExample(log, file, comment, vars) {
|
|
129
|
+
const text = (0, import_node_fs.existsSync)(file) ? (0, import_node_fs.readFileSync)(file, "utf8") : "";
|
|
130
|
+
const have = new Set([...text.matchAll(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/gm)].map((m) => m[1]));
|
|
131
|
+
const add2 = Object.entries(vars).filter(([k]) => !have.has(k) && !have.has(`VITE_${k}`));
|
|
132
|
+
if (!add2.length) return;
|
|
133
|
+
const block = `${comment.split("\n").map((l) => `# ${l}`).join("\n")}
|
|
134
|
+
${add2.map(([k, v]) => `${k}=${v}`).join("\n")}
|
|
135
|
+
`;
|
|
136
|
+
(0, import_node_fs.writeFileSync)(file, text ? `${text.replace(/\n*$/, "\n")}
|
|
137
|
+
${block}` : block);
|
|
138
|
+
log.push({ kind: text ? "updated" : "created", file, detail: add2.map(([k]) => k).join(", ") });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/scaffold/templates.ts
|
|
142
|
+
var templates_exports = {};
|
|
143
|
+
__export(templates_exports, {
|
|
144
|
+
AUTH_READER: () => AUTH_READER,
|
|
145
|
+
PROVIDERS: () => PROVIDERS,
|
|
146
|
+
PROVIDER_VARS: () => PROVIDER_VARS,
|
|
147
|
+
SERVER_ONLY: () => SERVER_ONLY,
|
|
148
|
+
apiRoute: () => apiRoute,
|
|
149
|
+
authEnvConfig: () => authEnvConfig,
|
|
150
|
+
authServerFile: () => authServerFile,
|
|
151
|
+
authServiceFile: () => authServiceFile,
|
|
152
|
+
betterAuthClient: () => betterAuthClient,
|
|
153
|
+
betterAuthConfig: () => betterAuthConfig,
|
|
154
|
+
betterAuthMiddleware: () => betterAuthMiddleware,
|
|
155
|
+
betterAuthRoute: () => betterAuthRoute,
|
|
156
|
+
callbackRoute: () => callbackRoute,
|
|
157
|
+
camel: () => camel,
|
|
158
|
+
envConfig: () => envConfig,
|
|
159
|
+
guardedApiRoute: () => guardedApiRoute,
|
|
160
|
+
loginRoute: () => loginRoute,
|
|
161
|
+
moduleFile: () => moduleFile,
|
|
162
|
+
serverFn: () => serverFn,
|
|
163
|
+
serviceFile: () => serviceFile,
|
|
164
|
+
verifyServer: () => verifyServer
|
|
165
|
+
});
|
|
166
|
+
var camel = (name) => name.toLowerCase().replace(/_+([a-z0-9])/g, (_, ch) => ch.toUpperCase());
|
|
167
|
+
var envConfig = (names) => {
|
|
168
|
+
const fields = names.length ? names.map((n) => ` ${camel(n)}: '${n}',`).join("\n") : ` // apiUrl: 'API_URL',`;
|
|
169
|
+
return `import { pickEnv } from '@fluixi/start/env';
|
|
170
|
+
|
|
171
|
+
// Every missing variable is reported at once, when this module loads. A field that may be
|
|
172
|
+
// unset takes { key, optional: true } or { key, default }; parse: asNumber and friends convert.
|
|
173
|
+
export const config = pickEnv({
|
|
174
|
+
${fields}
|
|
175
|
+
});
|
|
176
|
+
`;
|
|
177
|
+
};
|
|
178
|
+
var moduleFile = (Name, name) => `import { defineModule } from '@fluixi/start/app';
|
|
179
|
+
|
|
180
|
+
export const ${Name}Module = defineModule({
|
|
181
|
+
name: '${name}',
|
|
182
|
+
imports: [],
|
|
183
|
+
providers: [],
|
|
184
|
+
});
|
|
185
|
+
`;
|
|
186
|
+
var serviceFile = (Name, name, server) => `${server ? `// Server only: the browser build refuses a page that imports this file.
|
|
187
|
+
` : ""}import { injectionToken } from '@fluixi/start/di';
|
|
188
|
+
import { defineModule } from '@fluixi/start/app';
|
|
189
|
+
|
|
190
|
+
export class ${Name}Service {}
|
|
191
|
+
|
|
192
|
+
export const ${Name}Token = injectionToken<${Name}Service>('${Name}');
|
|
193
|
+
|
|
194
|
+
export const ${Name}Module = defineModule({
|
|
195
|
+
name: '${name}',
|
|
196
|
+
providers: [{ provide: ${Name}Token, useFactory: () => new ${Name}Service() }],
|
|
197
|
+
});
|
|
198
|
+
`;
|
|
199
|
+
var apiRoute = () => `export function GET(request: Request) {
|
|
200
|
+
return Response.json({ path: new URL(request.url).pathname });
|
|
201
|
+
}
|
|
202
|
+
`;
|
|
203
|
+
var guardedApiRoute = (guardImport, role) => `import { guard } from '${guardImport}';
|
|
204
|
+
|
|
205
|
+
export const GET = guard(
|
|
206
|
+
async (request, claims) => {
|
|
207
|
+
return Response.json({ sub: claims.sub });
|
|
208
|
+
},
|
|
209
|
+
{ role: '${role}' },
|
|
210
|
+
);
|
|
211
|
+
`;
|
|
212
|
+
var verifyServer = () => `import { createTokenVerifier, createApiGuard } from '@fluixi/oauth2/verify';
|
|
213
|
+
import { auth } from '../config/env.js';
|
|
214
|
+
|
|
215
|
+
// Checks a bearer token's signature, issuer, audience and expiry against the provider.
|
|
216
|
+
export const verify = createTokenVerifier(auth.provider.verifier(auth.clientId));
|
|
217
|
+
|
|
218
|
+
// Answers 401 without a valid token and 403 without the role.
|
|
219
|
+
export const guard = createApiGuard({ verify, roles: auth.provider.roles });
|
|
220
|
+
`;
|
|
221
|
+
var serverFn = (name) => `export async function ${name}(input: { id: string }) {
|
|
222
|
+
'use server';
|
|
223
|
+
// Runs on the server only. The browser calls it over RPC, so input and result are plain data.
|
|
224
|
+
return { id: input.id };
|
|
225
|
+
}
|
|
226
|
+
`;
|
|
227
|
+
var PROVIDERS = ["oidc", "keycloak", "auth0", "okta", "google", "dex", "oauth2"];
|
|
228
|
+
var SERVER_ONLY = {
|
|
229
|
+
google: "Google answers a token request without a client secret with `client_secret is missing`",
|
|
230
|
+
dex: "Dex sends no CORS headers on discovery or the token endpoint"
|
|
231
|
+
};
|
|
232
|
+
var PROVIDER_VARS = {
|
|
233
|
+
oidc: { AUTH_ISSUER: "https://id.example.com" },
|
|
234
|
+
keycloak: { AUTH_BASE_URL: "http://localhost:8080", AUTH_REALM: "master" },
|
|
235
|
+
auth0: { AUTH_DOMAIN: "example.eu.auth0.com", AUTH_AUDIENCE: "https://api.example.com" },
|
|
236
|
+
okta: { AUTH_DOMAIN: "example.okta.com" },
|
|
237
|
+
google: {},
|
|
238
|
+
dex: { AUTH_BASE_URL: "http://localhost:5556/dex" },
|
|
239
|
+
oauth2: {
|
|
240
|
+
AUTH_ISSUER: "https://id.example.com",
|
|
241
|
+
AUTH_AUTHORIZATION_ENDPOINT: "https://id.example.com/authorize",
|
|
242
|
+
AUTH_TOKEN_ENDPOINT: "https://id.example.com/token"
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
var AUTH_READER = `// Provider, issuer, client and token mode from the AUTH_* variables in env.public. Moving to
|
|
246
|
+
// another provider is a change to .env.
|
|
247
|
+
export const auth = authFromEnv(publicEnv(), { clientPrefix: '' });
|
|
248
|
+
`;
|
|
249
|
+
var authEnvConfig = () => `import { publicEnv } from '@fluixi/start/env';
|
|
250
|
+
import { authFromEnv } from '@fluixi/oauth2/env';
|
|
251
|
+
|
|
252
|
+
${AUTH_READER}`;
|
|
253
|
+
var authServiceFile = (tokens) => `import { createAuthService, authModule } from '@fluixi/oauth2';
|
|
254
|
+
import { env } from '@fluixi/start/env';
|
|
255
|
+
${tokens === "browser" ? `import { auth as authEnv } from '../config/env.js';
|
|
256
|
+
` : ""}
|
|
257
|
+
export interface User {
|
|
258
|
+
sub: string;
|
|
259
|
+
email?: string;
|
|
260
|
+
name?: string;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const appUrl = env('APP_URL') ?? 'http://localhost:3000';
|
|
264
|
+
|
|
265
|
+
export const auth = createAuthService<User>({
|
|
266
|
+
${tokens === "browser" ? ` config: {
|
|
267
|
+
...authEnv.client,
|
|
268
|
+
tokens: 'browser',
|
|
269
|
+
redirectUri: \`\${appUrl}/callback\`,
|
|
270
|
+
postLogoutRedirectUri: \`\${appUrl}/login\`,
|
|
271
|
+
},` : ` // The tokens stay on the server behind an httpOnly cookie. The browser calls the routes
|
|
272
|
+
// auth.server.ts mounts under basePath and never sees a token.
|
|
273
|
+
config: {
|
|
274
|
+
tokens: 'server',
|
|
275
|
+
redirectUri: \`\${appUrl}/auth/callback\`,
|
|
276
|
+
postLogoutRedirectUri: \`\${appUrl}/login\`,
|
|
277
|
+
basePath: '/auth',
|
|
278
|
+
},`}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
export const AuthModule = authModule(auth);
|
|
282
|
+
export const { protectedRoute, Protect, Can, useUser } = auth;
|
|
283
|
+
`;
|
|
284
|
+
var authServerFile = () => `import { authServerModule } from '@fluixi/oauth2/server';
|
|
285
|
+
import { publicEnv, serverEnv } from '@fluixi/start/env';
|
|
286
|
+
import { auth } from '../config/env.js';
|
|
287
|
+
|
|
288
|
+
// A function: serverEnv with required throws when a secret is unset, and it is only read
|
|
289
|
+
// when app.config.server.ts registers this.
|
|
290
|
+
export function authServer() {
|
|
291
|
+
const appUrl = publicEnv().APP_URL ?? 'http://localhost:3000';
|
|
292
|
+
return authServerModule({
|
|
293
|
+
...auth.client,
|
|
294
|
+
clientSecret: serverEnv('OIDC_CLIENT_SECRET', { required: true }),
|
|
295
|
+
cookieSecret: serverEnv('SESSION_SECRET', { required: true }),
|
|
296
|
+
redirectUri: \`\${appUrl}/auth/callback\`,
|
|
297
|
+
defaultReturnTo: '/',
|
|
298
|
+
secureCookie: appUrl.startsWith('https://'),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
`;
|
|
302
|
+
var callbackRoute = (html) => html ? `import { html } from '@fluixi/core';
|
|
303
|
+
import { OAuthCallback } from '@fluixi/oauth2/callback';
|
|
304
|
+
|
|
305
|
+
export default OAuthCallback({
|
|
306
|
+
pending: () => html\`<p>Signing you in</p>\`,
|
|
307
|
+
failed: (reason) => html\`<p>\${reason}</p><a href="/login">Try again</a>\`,
|
|
308
|
+
});
|
|
309
|
+
` : `import { OAuthCallback } from '@fluixi/oauth2/callback';
|
|
310
|
+
|
|
311
|
+
export default OAuthCallback({
|
|
312
|
+
pending: () => <p>Signing you in</p>,
|
|
313
|
+
failed: (reason) => (
|
|
314
|
+
<>
|
|
315
|
+
<p>{reason}</p>
|
|
316
|
+
<a href="/login">Try again</a>
|
|
317
|
+
</>
|
|
318
|
+
),
|
|
319
|
+
});
|
|
320
|
+
`;
|
|
321
|
+
var loginRoute = (html) => html ? `import { html } from '@fluixi/core';
|
|
322
|
+
import { auth } from '../services/auth.js';
|
|
323
|
+
|
|
324
|
+
export default function Login() {
|
|
325
|
+
return html\`<button @click=\${() => auth.signIn({ returnTo: '/' })}>Sign in</button>\`;
|
|
326
|
+
}
|
|
327
|
+
` : `import { auth } from '../services/auth.js';
|
|
328
|
+
|
|
329
|
+
export default function Login() {
|
|
330
|
+
return <button onClick={() => auth.signIn({ returnTo: '/' })}>Sign in</button>;
|
|
331
|
+
}
|
|
332
|
+
`;
|
|
333
|
+
var betterAuthConfig = () => `import { betterAuth } from 'better-auth';
|
|
334
|
+
import { defineAuth } from '@fluixi/auth/server';
|
|
335
|
+
|
|
336
|
+
export type AppUser = { id: string; email: string; name?: string; role?: string };
|
|
337
|
+
|
|
338
|
+
// Better Auth owns the audited primitives; defineAuth registers it with @fluixi/auth.
|
|
339
|
+
export const auth = defineAuth<AppUser>(
|
|
340
|
+
betterAuth({
|
|
341
|
+
// database: <your adapter>,
|
|
342
|
+
emailAndPassword: { enabled: true },
|
|
343
|
+
// socialProviders: { google: { … }, github: { … } },
|
|
344
|
+
} as Parameters<typeof betterAuth>[0]),
|
|
345
|
+
);
|
|
346
|
+
`;
|
|
347
|
+
var betterAuthClient = () => `import { createAuthClient } from 'better-auth/client';
|
|
348
|
+
import { createAuthHooks } from '@fluixi/auth/client';
|
|
349
|
+
import type { AppUser } from './auth.config.js';
|
|
350
|
+
|
|
351
|
+
const client = createAuthClient({ baseURL: '/api/auth' });
|
|
352
|
+
|
|
353
|
+
// Or, to reuse an existing reactive session: createAuthHooks({ session: yourSessionStore }).
|
|
354
|
+
export const { useUser, useSession, useAuth, useSignIn, useSignOut, protectedRoute } =
|
|
355
|
+
createAuthHooks<AppUser>({ client, loginPath: '/login' });
|
|
356
|
+
`;
|
|
357
|
+
var betterAuthMiddleware = () => `import { defineMiddleware } from '@fluixi/start';
|
|
358
|
+
import { authMiddleware } from '@fluixi/auth/server';
|
|
359
|
+
import './auth.config.js';
|
|
360
|
+
|
|
361
|
+
export default defineMiddleware([authMiddleware()]);
|
|
362
|
+
`;
|
|
363
|
+
var betterAuthRoute = () => `import { mountAuthRoutes } from '@fluixi/auth/server';
|
|
364
|
+
import '../../auth.config.js';
|
|
365
|
+
|
|
366
|
+
const handler = mountAuthRoutes();
|
|
367
|
+
export const GET = handler;
|
|
368
|
+
export const POST = handler;
|
|
369
|
+
export default handler;
|
|
370
|
+
`;
|
|
371
|
+
|
|
372
|
+
// src/scaffold/auth.ts
|
|
373
|
+
var ScaffoldError = class extends Error {
|
|
374
|
+
};
|
|
375
|
+
function projectFormat(root) {
|
|
376
|
+
try {
|
|
377
|
+
const ts = JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path2.join)(root, "tsconfig.json"), "utf8"));
|
|
378
|
+
if (ts?.compilerOptions && !ts.compilerOptions.jsx) return "html";
|
|
379
|
+
} catch {
|
|
380
|
+
}
|
|
381
|
+
return "jsx";
|
|
382
|
+
}
|
|
383
|
+
function importPath(from, to) {
|
|
384
|
+
const p = (0, import_node_path2.relative)((0, import_node_path2.dirname)(from), to).replace(/\\/g, "/").replace(/\.tsx?$/, ".js");
|
|
385
|
+
return p.startsWith(".") ? p : `./${p}`;
|
|
386
|
+
}
|
|
387
|
+
function writeOnce(log, file, content) {
|
|
388
|
+
if ((0, import_node_fs2.existsSync)(file)) {
|
|
389
|
+
log.push({ kind: "kept", file });
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
(0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(file), { recursive: true });
|
|
393
|
+
(0, import_node_fs2.writeFileSync)(file, content);
|
|
394
|
+
log.push({ kind: "created", file });
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
var APP_HEADER = "// Registered by @fluixi/start ahead of both entries.\n";
|
|
398
|
+
var APP_SERVER_HEADER = "// Only the server build imports this file, so what it lists never reaches the browser.\n";
|
|
399
|
+
function registerModule(log, root, server, file, name, entry = name) {
|
|
400
|
+
const config = (0, import_node_path2.join)(root, server ? "src/app.config.server.ts" : "src/app.config.ts");
|
|
401
|
+
register(log, config, server ? APP_SERVER_HEADER : APP_HEADER, {
|
|
402
|
+
importLine: `import { ${name} } from '${importPath(config, file)}';`,
|
|
403
|
+
entry
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
function ensureAuthEnv(log, root) {
|
|
407
|
+
const file = (0, import_node_path2.join)(root, "src/config/env.ts");
|
|
408
|
+
if (writeOnce(log, file, authEnvConfig())) return;
|
|
409
|
+
let text = (0, import_node_fs2.readFileSync)(file, "utf8");
|
|
410
|
+
if (/export\s+const\s+auth\b/.test(text)) return;
|
|
411
|
+
if (!/\bpublicEnv\b/.test(text)) text = addImport(text, `import { publicEnv } from '@fluixi/start/env';`);
|
|
412
|
+
text = addImport(text, `import { authFromEnv } from '@fluixi/oauth2/env';`);
|
|
413
|
+
(0, import_node_fs2.writeFileSync)(file, `${text.replace(/\n*$/, "\n")}
|
|
414
|
+
${AUTH_READER}`);
|
|
415
|
+
log.push({ kind: "updated", file, detail: "auth" });
|
|
416
|
+
}
|
|
417
|
+
function resolveTokens(provider, tokens) {
|
|
418
|
+
const refusal = SERVER_ONLY[provider];
|
|
419
|
+
if (refusal && tokens === "browser") throw new ScaffoldError(`${provider} needs server tokens: ${refusal}`);
|
|
420
|
+
return { tokens: tokens ?? (refusal ? "server" : "browser"), forced: !!refusal && !tokens };
|
|
421
|
+
}
|
|
422
|
+
function setupOAuth2(root, options, dependencySpec = "alpha") {
|
|
423
|
+
const provider = options.provider ?? "oidc";
|
|
424
|
+
if (!PROVIDERS.includes(provider)) throw new ScaffoldError(`unknown provider ${provider}: ${PROVIDERS.join(", ")}`);
|
|
425
|
+
const { tokens, forced } = resolveTokens(provider, options.tokens);
|
|
426
|
+
const html = options.format === "html";
|
|
427
|
+
const ext = html ? "ts" : "tsx";
|
|
428
|
+
const steps = [];
|
|
429
|
+
const at = (rel2) => (0, import_node_path2.join)(root, rel2);
|
|
430
|
+
ensureAuthEnv(steps, root);
|
|
431
|
+
const service = at("src/services/auth.ts");
|
|
432
|
+
if (writeOnce(steps, service, authServiceFile(tokens))) registerModule(steps, root, false, service, "AuthModule");
|
|
433
|
+
if (tokens === "browser") writeOnce(steps, at(`src/routes/callback.${ext}`), callbackRoute(html));
|
|
434
|
+
writeOnce(steps, at(`src/routes/login.${ext}`), loginRoute(html));
|
|
435
|
+
if (tokens === "server") {
|
|
436
|
+
const server = at("src/services/auth.server.ts");
|
|
437
|
+
if (writeOnce(steps, server, authServerFile())) registerModule(steps, root, true, server, "authServer", "authServer()");
|
|
438
|
+
}
|
|
439
|
+
const vars = {
|
|
440
|
+
AUTH_PROVIDER: provider,
|
|
441
|
+
AUTH_CLIENT_ID: "my-app",
|
|
442
|
+
AUTH_TOKENS: tokens,
|
|
443
|
+
...PROVIDER_VARS[provider],
|
|
444
|
+
APP_URL: "http://localhost:3000"
|
|
445
|
+
};
|
|
446
|
+
addEnvPublic(steps, at("fluixi.config.ts"), Object.keys(vars));
|
|
447
|
+
addEnvExample(steps, at(".env.example"), `@fluixi/oauth2 (${provider}). Listed in env.public: the browser sees them.`, vars);
|
|
448
|
+
if (tokens === "server") {
|
|
449
|
+
addEnvExample(steps, at(".env.example"), "Server only, never in env.public. SESSION_SECRET: 32 bytes or more.", {
|
|
450
|
+
OIDC_CLIENT_SECRET: "",
|
|
451
|
+
SESSION_SECRET: ""
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
const redirect = tokens === "browser" ? "/callback" : "/auth/callback";
|
|
455
|
+
return {
|
|
456
|
+
steps,
|
|
457
|
+
dependencies: { "@fluixi/oauth2": dependencySpec },
|
|
458
|
+
next: ["copy .env.example to .env and fill it in", `register <APP_URL>${redirect} as the redirect URI at the provider`],
|
|
459
|
+
notes: forced ? [`${provider} refuses a public client, so the tokens stay on the server`] : []
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function setupBetterAuth(root, dependencySpec = "alpha") {
|
|
463
|
+
const steps = [];
|
|
464
|
+
const at = (rel2) => (0, import_node_path2.join)(root, rel2);
|
|
465
|
+
writeOnce(steps, at("src/auth.config.ts"), betterAuthConfig());
|
|
466
|
+
writeOnce(steps, at("src/auth.client.ts"), betterAuthClient());
|
|
467
|
+
if (!writeOnce(steps, at("src/middleware.ts"), betterAuthMiddleware())) {
|
|
468
|
+
steps.push({
|
|
469
|
+
kind: "hint",
|
|
470
|
+
file: at("src/middleware.ts"),
|
|
471
|
+
lines: [`import { authMiddleware } from '@fluixi/auth/server';`, `import './auth.config.js';`, "authMiddleware() in the middleware list"]
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
writeOnce(steps, at("src/api/auth/[...all].ts"), betterAuthRoute());
|
|
475
|
+
return {
|
|
476
|
+
steps,
|
|
477
|
+
dependencies: { "@fluixi/auth": dependencySpec, "better-auth": "^1" },
|
|
478
|
+
next: ["set your database adapter in src/auth.config.ts"],
|
|
479
|
+
notes: []
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/scaffold/prompt.ts
|
|
484
|
+
var import_promises = require("node:readline/promises");
|
|
485
|
+
var import_node_readline = require("node:readline");
|
|
486
|
+
var import_node_process = require("node:process");
|
|
487
|
+
var COLOR = import_node_process.env.NO_COLOR || import_node_process.env.FORCE_COLOR === "0" ? false : (!!import_node_process.stdout.isTTY || !!import_node_process.env.FORCE_COLOR) && import_node_process.env.TERM !== "dumb";
|
|
488
|
+
var sgr = (o, x) => (s) => COLOR ? `\x1B[${o}m${s}\x1B[${x}m` : `${s}`;
|
|
489
|
+
var c = { bold: sgr(1, 22), green: sgr(32, 39), cyan: sgr(36, 39), gray: sgr(90, 39) };
|
|
490
|
+
var BAR = c.gray("│");
|
|
491
|
+
function askSelect(label, choices) {
|
|
492
|
+
return new Promise((done) => {
|
|
493
|
+
let idx = 0;
|
|
494
|
+
import_node_process.stdout.write(`${c.green("◆")} ${c.bold(label)}
|
|
495
|
+
`);
|
|
496
|
+
const draw = (first) => {
|
|
497
|
+
if (!first) import_node_process.stdout.write(`\x1B[${choices.length}A`);
|
|
498
|
+
for (let i = 0; i < choices.length; i++) {
|
|
499
|
+
const on = i === idx;
|
|
500
|
+
const dot = on ? c.green("●") : c.gray("○");
|
|
501
|
+
const name = on ? c.cyan(c.bold(choices[i].label)) : choices[i].label;
|
|
502
|
+
import_node_process.stdout.write(`\x1B[2K${BAR} ${dot} ${name} ${c.gray(choices[i].hint)}
|
|
503
|
+
`);
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
draw(true);
|
|
507
|
+
(0, import_node_readline.emitKeypressEvents)(import_node_process.stdin);
|
|
508
|
+
if (import_node_process.stdin.isTTY) import_node_process.stdin.setRawMode(true);
|
|
509
|
+
const onKey = (_s, k) => {
|
|
510
|
+
if (k.name === "up" || k.name === "k") {
|
|
511
|
+
idx = (idx - 1 + choices.length) % choices.length;
|
|
512
|
+
draw(false);
|
|
513
|
+
} else if (k.name === "down" || k.name === "j") {
|
|
514
|
+
idx = (idx + 1) % choices.length;
|
|
515
|
+
draw(false);
|
|
516
|
+
} else if (k.name === "return" || k.name === "enter") {
|
|
517
|
+
finish();
|
|
518
|
+
done(choices[idx].value);
|
|
519
|
+
} else if (k.name === "c" && k.ctrl) {
|
|
520
|
+
finish();
|
|
521
|
+
import_node_process.stdout.write("\n");
|
|
522
|
+
(0, import_node_process.exit)(1);
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
const onEnd = () => {
|
|
526
|
+
finish();
|
|
527
|
+
done(choices[idx].value);
|
|
528
|
+
};
|
|
529
|
+
const finish = () => {
|
|
530
|
+
import_node_process.stdin.off("keypress", onKey);
|
|
531
|
+
import_node_process.stdin.off("end", onEnd);
|
|
532
|
+
import_node_process.stdin.off("close", onEnd);
|
|
533
|
+
if (import_node_process.stdin.isTTY) import_node_process.stdin.setRawMode(false);
|
|
534
|
+
import_node_process.stdin.pause();
|
|
535
|
+
import_node_process.stdout.write(`${BAR}
|
|
536
|
+
`);
|
|
537
|
+
};
|
|
538
|
+
if (import_node_process.stdin.readableEnded) {
|
|
539
|
+
finish();
|
|
540
|
+
done(choices[idx].value);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
import_node_process.stdin.on("keypress", onKey);
|
|
544
|
+
import_node_process.stdin.on("end", onEnd);
|
|
545
|
+
import_node_process.stdin.on("close", onEnd);
|
|
546
|
+
import_node_process.stdin.resume();
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/report.ts
|
|
551
|
+
var import_node_path3 = require("node:path");
|
|
12
552
|
var import_ui = require("@fluixi/start/ui");
|
|
13
|
-
var
|
|
553
|
+
var rel = (file) => (0, import_node_path3.relative)(process.cwd(), file) || ".";
|
|
554
|
+
function report(steps) {
|
|
555
|
+
for (const s of steps) {
|
|
556
|
+
if (s.kind === "hint") {
|
|
557
|
+
console.log(` ${import_ui.c.yellow("!")} ${import_ui.c.gray("add to")} ${import_ui.c.cyan(rel(s.file))}${import_ui.c.gray(":")}`);
|
|
558
|
+
for (const l of s.lines) console.log(` ${l}`);
|
|
559
|
+
} else if (s.kind === "kept") {
|
|
560
|
+
console.log(` ${import_ui.c.gray("-")} ${import_ui.c.cyan(rel(s.file))} ${import_ui.c.gray("exists, kept")}`);
|
|
561
|
+
} else if (s.kind === "listed") {
|
|
562
|
+
console.log(` ${import_ui.c.gray("-")} ${import_ui.c.cyan(rel(s.file))} ${import_ui.c.gray("already lists it")}`);
|
|
563
|
+
} else {
|
|
564
|
+
console.log(` ${import_ui.c.green("✓")} ${import_ui.c.gray(s.kind)} ${import_ui.c.cyan(rel(s.file))}${s.detail ? import_ui.c.gray(`: ${s.detail}`) : ""}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
function reportAuth(result, installHint) {
|
|
569
|
+
for (const n of result.notes) console.log(` ${import_ui.c.gray(n)}`);
|
|
570
|
+
report(result.steps);
|
|
571
|
+
const next = [...installHint ? [installHint] : [], ...result.next];
|
|
572
|
+
if (next.length) console.log(` ${import_ui.c.gray("next:")} ${next.map((n) => import_ui.c.gray(n)).join(import_ui.c.gray("; "))}`);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/generate.ts
|
|
576
|
+
var GEN_USAGE = `Usage: fluixi generate <kind> [name] [options]
|
|
14
577
|
|
|
15
578
|
Kinds:
|
|
16
|
-
component <Name>
|
|
17
|
-
route <path>
|
|
18
|
-
middleware
|
|
19
|
-
|
|
579
|
+
component <Name> a reusable component → src/components/<Name>.{tsx,ts}
|
|
580
|
+
route <path> a file-based route page → src/routes/<path>.{tsx,ts}
|
|
581
|
+
middleware the request middleware file → src/middleware.ts
|
|
582
|
+
env typed config from env.public → src/config/env.ts
|
|
583
|
+
module <name> an app module → src/services/<name>/<name>.module.ts
|
|
584
|
+
service <Name> [--server] a service, token and module → src/services/<name>.ts (.server.ts)
|
|
585
|
+
api <path> [--role <role>] an API route, guarded by role → src/api/<path>.ts
|
|
586
|
+
server-fn <name> a "use server" function → src/server/<name>.ts
|
|
587
|
+
auth oauth2 [--provider <p>] [--tokens browser|server]
|
|
588
|
+
@fluixi/oauth2 wiring: env config, service, routes, .env.example
|
|
589
|
+
(fluixi add auth does the same, asks, and installs)
|
|
590
|
+
auth better-auth @fluixi/auth wiring → auth.config + auth.client + middleware + /api/auth
|
|
20
591
|
|
|
592
|
+
Modules and services are listed in src/app.config.ts, or app.config.server.ts for --server,
|
|
593
|
+
which is created when missing. Providers: ${templates_exports.PROVIDERS.join(", ")}.
|
|
21
594
|
Component/route syntax follows the project (JSX → .tsx, html\`\` → .ts); override with --jsx / --html.
|
|
22
595
|
`;
|
|
23
596
|
function detectFormat() {
|
|
@@ -25,7 +598,7 @@ function detectFormat() {
|
|
|
25
598
|
if (args.includes("--html")) return "html";
|
|
26
599
|
if (args.includes("--jsx")) return "jsx";
|
|
27
600
|
try {
|
|
28
|
-
const ts = JSON.parse((0,
|
|
601
|
+
const ts = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path4.resolve)("tsconfig.json"), "utf8"));
|
|
29
602
|
if (ts?.compilerOptions && !ts.compilerOptions.jsx) return "html";
|
|
30
603
|
} catch {
|
|
31
604
|
}
|
|
@@ -36,28 +609,46 @@ function pascal(s) {
|
|
|
36
609
|
}
|
|
37
610
|
function resolveHint(Name, ext) {
|
|
38
611
|
const configs = ["vite.config.ts", "vite.config.js", "fluixi.config.ts", "fluixi.config.js"];
|
|
39
|
-
const found = configs.map((f) => (0,
|
|
612
|
+
const found = configs.map((f) => (0, import_node_path4.resolve)(f)).find((f) => (0, import_node_fs3.existsSync)(f));
|
|
40
613
|
if (!found) return;
|
|
41
614
|
try {
|
|
42
|
-
if (/\bresolve\s*:\s*\[/.test((0,
|
|
615
|
+
if (/\bresolve\s*:\s*\[/.test((0, import_node_fs3.readFileSync)(found, "utf8"))) return;
|
|
43
616
|
} catch {
|
|
44
617
|
return;
|
|
45
618
|
}
|
|
46
619
|
console.log(
|
|
47
|
-
` ${
|
|
620
|
+
` ${import_ui2.c.gray("to use")} ${import_ui2.c.cyan(`<${Name} />`)} ${import_ui2.c.gray("without importing it, add to")} ${import_ui2.c.cyan((0, import_node_path4.relative)(process.cwd(), found))}:`
|
|
48
621
|
);
|
|
49
|
-
console.log(` ${
|
|
50
|
-
console.log(` ${
|
|
622
|
+
console.log(` ${import_ui2.c.gray(`resolve: [{ match: /^([A-Z]\\w+)$/, module: '/src/components/$1.${ext}' }]`)}`);
|
|
623
|
+
console.log(` ${import_ui2.c.gray("one pattern covers the whole folder. Importing it works too — including with")} ${import_ui2.c.cyan("load:")}`);
|
|
624
|
+
}
|
|
625
|
+
function option(name) {
|
|
626
|
+
const args = process.argv;
|
|
627
|
+
const i = args.indexOf(`--${name}`);
|
|
628
|
+
if (i !== -1 && args[i + 1] && !args[i + 1].startsWith("--")) return args[i + 1];
|
|
629
|
+
return args.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3);
|
|
630
|
+
}
|
|
631
|
+
var kebab = (s) => s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
632
|
+
function writeOnce2(file, content) {
|
|
633
|
+
const steps = [];
|
|
634
|
+
const wrote = writeOnce(steps, file, content);
|
|
635
|
+
report(steps);
|
|
636
|
+
return wrote;
|
|
637
|
+
}
|
|
638
|
+
function registerIn(server, file, name, entry = name) {
|
|
639
|
+
const steps = [];
|
|
640
|
+
registerModule(steps, process.cwd(), server, file, name, entry);
|
|
641
|
+
report(steps);
|
|
51
642
|
}
|
|
52
643
|
function write(file, content) {
|
|
53
|
-
const
|
|
54
|
-
if ((0,
|
|
55
|
-
console.error(` ${
|
|
644
|
+
const rel2 = (0, import_node_path4.relative)(process.cwd(), file);
|
|
645
|
+
if ((0, import_node_fs3.existsSync)(file)) {
|
|
646
|
+
console.error(` ${import_ui2.c.red("✗")} ${rel2} ${import_ui2.c.gray("already exists")}`);
|
|
56
647
|
process.exit(1);
|
|
57
648
|
}
|
|
58
|
-
(0,
|
|
59
|
-
(0,
|
|
60
|
-
console.log(` ${
|
|
649
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path4.dirname)(file), { recursive: true });
|
|
650
|
+
(0, import_node_fs3.writeFileSync)(file, content);
|
|
651
|
+
console.log(` ${import_ui2.c.green("✓")} ${import_ui2.c.gray("created")} ${import_ui2.c.cyan(rel2)}`);
|
|
61
652
|
}
|
|
62
653
|
var component = (Name) => `import type { Component, ComponentChild } from '@fluixi/core';
|
|
63
654
|
|
|
@@ -111,44 +702,6 @@ export default defineMiddleware([
|
|
|
111
702
|
},
|
|
112
703
|
]);
|
|
113
704
|
`;
|
|
114
|
-
var authConfig = () => `import { betterAuth } from 'better-auth';
|
|
115
|
-
import { defineAuth } from '@fluixi/auth/server';
|
|
116
|
-
|
|
117
|
-
export type AppUser = { id: string; email: string; name?: string; role?: string };
|
|
118
|
-
|
|
119
|
-
// Better Auth owns the audited primitives; defineAuth registers it with @fluixi/auth.
|
|
120
|
-
export const auth = defineAuth<AppUser>(
|
|
121
|
-
betterAuth({
|
|
122
|
-
// database: <your adapter>,
|
|
123
|
-
emailAndPassword: { enabled: true },
|
|
124
|
-
// socialProviders: { google: { … }, github: { … } },
|
|
125
|
-
} as Parameters<typeof betterAuth>[0]),
|
|
126
|
-
);
|
|
127
|
-
`;
|
|
128
|
-
var authClient = () => `import { createAuthClient } from 'better-auth/client';
|
|
129
|
-
import { createAuthHooks } from '@fluixi/auth/client';
|
|
130
|
-
import type { AppUser } from './auth.config.js';
|
|
131
|
-
|
|
132
|
-
const client = createAuthClient({ baseURL: '/api/auth' });
|
|
133
|
-
|
|
134
|
-
// Or, to reuse an existing reactive session: createAuthHooks({ session: yourSessionStore }).
|
|
135
|
-
export const { useUser, useSession, useAuth, useSignIn, useSignOut, protectedRoute } =
|
|
136
|
-
createAuthHooks<AppUser>({ client, loginPath: '/login' });
|
|
137
|
-
`;
|
|
138
|
-
var authMiddleware = () => `import { defineMiddleware } from '@fluixi/start';
|
|
139
|
-
import { authMiddleware } from '@fluixi/auth/server';
|
|
140
|
-
import './auth.config.js';
|
|
141
|
-
|
|
142
|
-
export default defineMiddleware([authMiddleware()]);
|
|
143
|
-
`;
|
|
144
|
-
var authRoute = () => `import { mountAuthRoutes } from '@fluixi/auth/server';
|
|
145
|
-
import '../../../auth.config.js';
|
|
146
|
-
|
|
147
|
-
const handler = mountAuthRoutes();
|
|
148
|
-
export const GET = handler;
|
|
149
|
-
export const POST = handler;
|
|
150
|
-
export default handler;
|
|
151
|
-
`;
|
|
152
705
|
function generate(kind, name) {
|
|
153
706
|
switch (kind) {
|
|
154
707
|
case "component":
|
|
@@ -157,7 +710,7 @@ function generate(kind, name) {
|
|
|
157
710
|
const Name = pascal(name);
|
|
158
711
|
const html = detectFormat() === "html";
|
|
159
712
|
const ext = html ? "ts" : "tsx";
|
|
160
|
-
write((0,
|
|
713
|
+
write((0, import_node_path4.resolve)("src/components", `${Name}.${ext}`), (html ? componentHtml : component)(Name));
|
|
161
714
|
resolveHint(Name, ext);
|
|
162
715
|
break;
|
|
163
716
|
}
|
|
@@ -165,67 +718,385 @@ function generate(kind, name) {
|
|
|
165
718
|
case "r": {
|
|
166
719
|
if (!name) return fail("route path required, e.g. `fluixi g route about`");
|
|
167
720
|
const html = detectFormat() === "html";
|
|
168
|
-
const Name = pascal((0,
|
|
169
|
-
write((0,
|
|
721
|
+
const Name = pascal((0, import_node_path4.basename)(name));
|
|
722
|
+
write((0, import_node_path4.resolve)("src/routes", `${name}.${html ? "ts" : "tsx"}`), (html ? routeHtml : route)(Name));
|
|
170
723
|
break;
|
|
171
724
|
}
|
|
172
725
|
case "middleware":
|
|
173
726
|
case "m":
|
|
174
|
-
write((0,
|
|
727
|
+
write((0, import_node_path4.resolve)("src/middleware.ts"), middleware());
|
|
728
|
+
break;
|
|
729
|
+
case "env": {
|
|
730
|
+
const names = (envPublicNames((0, import_node_path4.resolve)("fluixi.config.ts")) ?? []).filter((n) => !n.startsWith("AUTH_"));
|
|
731
|
+
write((0, import_node_path4.resolve)("src/config/env.ts"), templates_exports.envConfig(names));
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
case "module": {
|
|
735
|
+
if (!name) return fail("module name required, e.g. `fluixi g module billing`");
|
|
736
|
+
const slug = kebab(name);
|
|
737
|
+
const file = (0, import_node_path4.resolve)("src/services", slug, `${slug}.module.ts`);
|
|
738
|
+
write(file, templates_exports.moduleFile(pascal(name), slug));
|
|
739
|
+
registerIn(false, file, `${pascal(name)}Module`);
|
|
175
740
|
break;
|
|
741
|
+
}
|
|
742
|
+
case "service":
|
|
743
|
+
case "s": {
|
|
744
|
+
if (!name) return fail("service name required, e.g. `fluixi g service Orders`");
|
|
745
|
+
const Name = pascal(name).replace(/Service$/, "") || "App";
|
|
746
|
+
const server = process.argv.includes("--server");
|
|
747
|
+
const file = (0, import_node_path4.resolve)("src/services", `${kebab(Name)}${server ? ".server" : ""}.ts`);
|
|
748
|
+
write(file, templates_exports.serviceFile(Name, kebab(Name), server));
|
|
749
|
+
registerIn(server, file, `${Name}Module`);
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
case "api": {
|
|
753
|
+
if (!name) return fail("api path required, e.g. `fluixi g api users/[id]`");
|
|
754
|
+
const file = (0, import_node_path4.resolve)("src/api", `${name.replace(/\.ts$/, "")}.ts`);
|
|
755
|
+
const role = option("role");
|
|
756
|
+
if (!role) {
|
|
757
|
+
write(file, templates_exports.apiRoute());
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
const verify = (0, import_node_path4.resolve)("src/services/verify.server.ts");
|
|
761
|
+
write(file, templates_exports.guardedApiRoute(importPath(file, verify), role));
|
|
762
|
+
writeOnce2(verify, templates_exports.verifyServer());
|
|
763
|
+
{
|
|
764
|
+
const steps = [];
|
|
765
|
+
ensureAuthEnv(steps, process.cwd());
|
|
766
|
+
report(steps);
|
|
767
|
+
}
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
case "server-fn": {
|
|
771
|
+
if (!name) return fail("function name required, e.g. `fluixi g server-fn getUser`");
|
|
772
|
+
const fn = name.replace(/[^a-zA-Z0-9]+(.)/g, (_, ch) => ch.toUpperCase());
|
|
773
|
+
write((0, import_node_path4.resolve)("src/server", `${fn}.ts`), templates_exports.serverFn(fn));
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
176
776
|
case "auth":
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
777
|
+
try {
|
|
778
|
+
if (name === "oauth2") {
|
|
779
|
+
reportAuth(
|
|
780
|
+
setupOAuth2(process.cwd(), {
|
|
781
|
+
provider: option("provider"),
|
|
782
|
+
tokens: tokensOption(),
|
|
783
|
+
format: detectFormat()
|
|
784
|
+
}),
|
|
785
|
+
"install @fluixi/oauth2"
|
|
786
|
+
);
|
|
787
|
+
} else if (!name || name === "better-auth") {
|
|
788
|
+
reportAuth(setupBetterAuth(process.cwd()), "install @fluixi/auth and better-auth");
|
|
789
|
+
} else {
|
|
790
|
+
return fail(`unknown auth kind ${name}: oauth2 or better-auth`);
|
|
791
|
+
}
|
|
792
|
+
} catch (error) {
|
|
793
|
+
if (error instanceof ScaffoldError) return fail(error.message);
|
|
794
|
+
throw error;
|
|
795
|
+
}
|
|
182
796
|
break;
|
|
183
797
|
default:
|
|
184
798
|
console.error(GEN_USAGE);
|
|
185
799
|
process.exit(1);
|
|
186
800
|
}
|
|
187
801
|
}
|
|
802
|
+
function tokensOption() {
|
|
803
|
+
const tokens = option("tokens");
|
|
804
|
+
if (tokens !== void 0 && tokens !== "browser" && tokens !== "server") {
|
|
805
|
+
fail("--tokens is browser or server");
|
|
806
|
+
}
|
|
807
|
+
return tokens;
|
|
808
|
+
}
|
|
188
809
|
function fail(msg) {
|
|
189
|
-
console.error(` ${
|
|
810
|
+
console.error(` ${import_ui2.c.red("✗")} ${msg}`);
|
|
190
811
|
process.exit(1);
|
|
191
812
|
}
|
|
192
813
|
|
|
814
|
+
// src/add.ts
|
|
815
|
+
var import_node_child_process = require("node:child_process");
|
|
816
|
+
var import_node_fs4 = require("node:fs");
|
|
817
|
+
var import_node_path5 = require("node:path");
|
|
818
|
+
var import_node_process2 = require("node:process");
|
|
819
|
+
var import_ui3 = require("@fluixi/start/ui");
|
|
820
|
+
var ADD_USAGE = `Usage: fluixi add <feature> [options]
|
|
821
|
+
|
|
822
|
+
Features:
|
|
823
|
+
auth [oauth2|better-auth] authentication, asked for when not given
|
|
824
|
+
--provider <p> ${PROVIDERS.join(", ")}
|
|
825
|
+
--tokens browser|server where an OAuth2 app keeps its tokens
|
|
826
|
+
--no-install write the files, leave the packages to you
|
|
827
|
+
`;
|
|
828
|
+
function option2(args, name) {
|
|
829
|
+
const i = args.indexOf(`--${name}`);
|
|
830
|
+
if (i !== -1 && args[i + 1] && !args[i + 1].startsWith("-")) return args[i + 1];
|
|
831
|
+
return args.find((a) => a.startsWith(`--${name}=`))?.slice(name.length + 3);
|
|
832
|
+
}
|
|
833
|
+
function installCommand(root, specs) {
|
|
834
|
+
if ((0, import_node_fs4.existsSync)((0, import_node_path5.join)(root, "pnpm-lock.yaml"))) return ["pnpm", ["add", ...specs]];
|
|
835
|
+
if ((0, import_node_fs4.existsSync)((0, import_node_path5.join)(root, "yarn.lock"))) return ["yarn", ["add", ...specs]];
|
|
836
|
+
if ((0, import_node_fs4.existsSync)((0, import_node_path5.join)(root, "bun.lockb")) || (0, import_node_fs4.existsSync)((0, import_node_path5.join)(root, "bun.lock"))) return ["bun", ["add", ...specs]];
|
|
837
|
+
return ["npm", ["install", ...specs]];
|
|
838
|
+
}
|
|
839
|
+
function channelOf(root) {
|
|
840
|
+
try {
|
|
841
|
+
const version = JSON.parse((0, import_node_fs4.readFileSync)((0, import_node_path5.join)(root, "node_modules/@fluixi/start/package.json"), "utf8")).version;
|
|
842
|
+
return version.match(/-([a-z]+)\./)?.[1] ?? "latest";
|
|
843
|
+
} catch {
|
|
844
|
+
return "alpha";
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
async function addAuth(root, args) {
|
|
848
|
+
const ask = import_node_process2.stdin.isTTY;
|
|
849
|
+
let kind = args[1] && !args[1].startsWith("-") ? args[1] : void 0;
|
|
850
|
+
let provider = option2(args, "provider");
|
|
851
|
+
let tokens = option2(args, "tokens");
|
|
852
|
+
if (kind && kind !== "oauth2" && kind !== "better-auth") throw new ScaffoldError(`unknown auth kind ${kind}: oauth2 or better-auth`);
|
|
853
|
+
if (tokens && tokens !== "browser" && tokens !== "server") throw new ScaffoldError("--tokens is browser or server");
|
|
854
|
+
if (provider && !PROVIDERS.includes(provider)) throw new ScaffoldError(`unknown provider ${provider}: ${PROVIDERS.join(", ")}`);
|
|
855
|
+
if (!kind && ask) {
|
|
856
|
+
kind = await askSelect("Authentication", [
|
|
857
|
+
{ value: "oauth2", label: "OAuth2 / OIDC", hint: "@fluixi/oauth2 · Keycloak, Auth0, Okta, Google…" },
|
|
858
|
+
{ value: "better-auth", label: "Better Auth", hint: "@fluixi/auth · your own user table" }
|
|
859
|
+
]);
|
|
860
|
+
}
|
|
861
|
+
kind ??= "oauth2";
|
|
862
|
+
if (kind === "oauth2" && !provider && ask) {
|
|
863
|
+
provider = await askSelect("Identity provider", PROVIDERS.map((p) => ({ value: p, label: p, hint: "" })));
|
|
864
|
+
}
|
|
865
|
+
if (kind === "oauth2" && !tokens && ask && !SERVER_ONLY[provider ?? "oidc"]) {
|
|
866
|
+
tokens = await askSelect("Where the tokens live", [
|
|
867
|
+
{ value: "browser", label: "Browser", hint: "public client · PKCE" },
|
|
868
|
+
{ value: "server", label: "Server", hint: "httpOnly cookie · client secret" }
|
|
869
|
+
]);
|
|
870
|
+
}
|
|
871
|
+
const channel = channelOf(root);
|
|
872
|
+
const result = kind === "oauth2" ? setupOAuth2(root, { provider, tokens, format: projectFormat(root) }, channel) : setupBetterAuth(root, channel);
|
|
873
|
+
const specs = Object.entries(result.dependencies).map(([name, spec]) => `${name}@${spec}`);
|
|
874
|
+
if (args.includes("--no-install")) {
|
|
875
|
+
reportAuth(result, `install ${specs.join(" ")}`);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
reportAuth(result);
|
|
879
|
+
const [bin, binArgs] = installCommand(root, specs);
|
|
880
|
+
console.log(` ${import_ui3.c.gray("›")} ${import_ui3.c.cyan([bin, ...binArgs].join(" "))}`);
|
|
881
|
+
(0, import_node_child_process.execFileSync)(bin, binArgs, { cwd: root, stdio: "inherit" });
|
|
882
|
+
}
|
|
883
|
+
async function add(feature, args, root = process.cwd()) {
|
|
884
|
+
if (feature !== "auth") {
|
|
885
|
+
console.error(ADD_USAGE);
|
|
886
|
+
process.exitCode = 1;
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
try {
|
|
890
|
+
await addAuth(root, args);
|
|
891
|
+
} catch (error) {
|
|
892
|
+
if (!(error instanceof ScaffoldError)) throw error;
|
|
893
|
+
console.error(` ${import_ui3.c.red("✗")} ${error.message}`);
|
|
894
|
+
process.exitCode = 1;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
193
898
|
// src/start-copy.ts
|
|
194
|
-
var
|
|
899
|
+
var import_node_fs5 = require("node:fs");
|
|
195
900
|
var import_node_module = require("node:module");
|
|
196
|
-
var
|
|
901
|
+
var import_node_path6 = require("node:path");
|
|
197
902
|
function locate(from) {
|
|
198
903
|
try {
|
|
199
904
|
const file = (0, import_node_module.createRequire)(from).resolve("@fluixi/start/package.json");
|
|
200
|
-
return { dir: (0,
|
|
905
|
+
return { dir: (0, import_node_path6.dirname)((0, import_node_fs5.realpathSync)(file)), version: (0, import_node_module.createRequire)(from)(file).version };
|
|
201
906
|
} catch {
|
|
202
907
|
return null;
|
|
203
908
|
}
|
|
204
909
|
}
|
|
205
910
|
function startMismatch(appRoot, cliFile) {
|
|
206
|
-
const app = locate((0,
|
|
911
|
+
const app = locate((0, import_node_path6.join)(appRoot, "package.json"));
|
|
207
912
|
const cli = locate(cliFile);
|
|
208
913
|
if (!app || !cli || app.dir === cli.dir) return null;
|
|
209
914
|
return { app, cli };
|
|
210
915
|
}
|
|
211
916
|
|
|
917
|
+
// src/check.ts
|
|
918
|
+
var import_node_fs6 = require("node:fs");
|
|
919
|
+
var import_node_module2 = require("node:module");
|
|
920
|
+
var import_node_path7 = require("node:path");
|
|
921
|
+
var import_rules = require("@fluixi/ts-plugin/rules");
|
|
922
|
+
var import_ui4 = require("@fluixi/start/ui");
|
|
923
|
+
function loadTypeScript(root) {
|
|
924
|
+
try {
|
|
925
|
+
return (0, import_node_module2.createRequire)((0, import_node_path7.join)(root, "package.json"))("typescript");
|
|
926
|
+
} catch {
|
|
927
|
+
throw new Error("fluixi check needs typescript in the app: npm install -D typescript");
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
function rootFiles(ts, configPath, seen = /* @__PURE__ */ new Set()) {
|
|
931
|
+
seen.add(configPath);
|
|
932
|
+
const read = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
933
|
+
if (read.error) throw new Error(ts.flattenDiagnosticMessageText(read.error.messageText, "\n"));
|
|
934
|
+
const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, (0, import_node_path7.dirname)(configPath));
|
|
935
|
+
const files = [...parsed.fileNames];
|
|
936
|
+
let options = parsed.options;
|
|
937
|
+
for (const ref of parsed.projectReferences ?? []) {
|
|
938
|
+
const path = ts.resolveProjectReferencePath(ref);
|
|
939
|
+
if (seen.has(path) || !(0, import_node_fs6.existsSync)(path)) continue;
|
|
940
|
+
const child = rootFiles(ts, path, seen);
|
|
941
|
+
files.push(...child.files);
|
|
942
|
+
if (!parsed.fileNames.length) options = child.options;
|
|
943
|
+
}
|
|
944
|
+
return { files, options };
|
|
945
|
+
}
|
|
946
|
+
function checkProject(root = process.cwd(), ts = loadTypeScript(root)) {
|
|
947
|
+
const configPath = ts.findConfigFile(root, ts.sys.fileExists, "tsconfig.json");
|
|
948
|
+
if (!configPath) throw new Error(`no tsconfig.json in ${root}`);
|
|
949
|
+
const { files, options } = rootFiles(ts, configPath);
|
|
950
|
+
for (const name of ["fluixi.config.ts", "fluixi.config.mts", "fluixi.config.js"]) {
|
|
951
|
+
const path = (0, import_node_path7.resolve)(root, name);
|
|
952
|
+
if ((0, import_node_fs6.existsSync)(path) && !files.includes(path)) files.push(path);
|
|
953
|
+
}
|
|
954
|
+
const compilerOptions = { ...options, noEmit: true };
|
|
955
|
+
const host = ts.createCompilerHost(compilerOptions, true);
|
|
956
|
+
const program = ts.createProgram({ rootNames: [...new Set(files)], options: compilerOptions, host });
|
|
957
|
+
const out = [];
|
|
958
|
+
for (const sf of program.getSourceFiles()) {
|
|
959
|
+
if (sf.isDeclarationFile || /[\\/]node_modules[\\/]/.test(sf.fileName)) continue;
|
|
960
|
+
for (const d of (0, import_rules.envAuthDiagnostics)(ts, program, sf.fileName, ts.sys)) {
|
|
961
|
+
const at = sf.getLineAndCharacterOfPosition(d.start ?? 0);
|
|
962
|
+
out.push({
|
|
963
|
+
file: (0, import_node_path7.relative)(root, sf.fileName),
|
|
964
|
+
line: at.line + 1,
|
|
965
|
+
column: at.character + 1,
|
|
966
|
+
code: d.code,
|
|
967
|
+
severity: d.category === ts.DiagnosticCategory.Error ? "error" : d.category === ts.DiagnosticCategory.Warning ? "warning" : "suggestion",
|
|
968
|
+
message: ts.flattenDiagnosticMessageText(d.messageText, " ")
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return out.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column);
|
|
973
|
+
}
|
|
974
|
+
function check(root = process.cwd(), strict = false) {
|
|
975
|
+
const found = checkProject(root);
|
|
976
|
+
const shown = found.filter((f) => f.severity !== "suggestion");
|
|
977
|
+
const mark = { error: import_ui4.c.red("error"), warning: import_ui4.c.yellow("warning"), suggestion: import_ui4.c.gray("note") };
|
|
978
|
+
const lines = shown.map(
|
|
979
|
+
(f) => ` ${import_ui4.c.cyan(`${f.file}:${f.line}:${f.column}`)} ${mark[f.severity]} ${import_ui4.c.gray(`FX${f.code}`)} ${f.message}`
|
|
980
|
+
);
|
|
981
|
+
const errors = shown.filter((f) => f.severity === "error").length;
|
|
982
|
+
const warnings = shown.length - errors;
|
|
983
|
+
const notes = found.length - shown.length;
|
|
984
|
+
const tally = [`${errors} error${errors === 1 ? "" : "s"}`, `${warnings} warning${warnings === 1 ? "" : "s"}`];
|
|
985
|
+
if (notes) tally.push(`${notes} note${notes === 1 ? "" : "s"}`);
|
|
986
|
+
console.log(["", ...lines, ...lines.length ? [""] : [], ` ${shown.length ? import_ui4.c.bold(tally.join(", ")) : `${import_ui4.c.green("✓")} no env or auth findings`}`, ""].join("\n"));
|
|
987
|
+
return errors || strict && warnings ? 1 : 0;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// src/doctor.ts
|
|
991
|
+
var import_node_fs7 = require("node:fs");
|
|
992
|
+
var import_node_module3 = require("node:module");
|
|
993
|
+
var import_node_path8 = require("node:path");
|
|
994
|
+
var readJson = (file) => JSON.parse((0, import_node_fs7.readFileSync)(file, "utf8"));
|
|
995
|
+
function resolvedVersions(root) {
|
|
996
|
+
const out = {};
|
|
997
|
+
const scope = (0, import_node_path8.join)(root, "node_modules/@fluixi");
|
|
998
|
+
if (!(0, import_node_fs7.existsSync)(scope)) return out;
|
|
999
|
+
for (const name of (0, import_node_fs7.readdirSync)(scope).sort()) {
|
|
1000
|
+
const file = (0, import_node_path8.join)(scope, name, "package.json");
|
|
1001
|
+
if ((0, import_node_fs7.existsSync)(file)) out[`@fluixi/${name}`] = readJson(file).version;
|
|
1002
|
+
}
|
|
1003
|
+
return out;
|
|
1004
|
+
}
|
|
1005
|
+
function installedCopies(root) {
|
|
1006
|
+
const found = /* @__PURE__ */ new Map();
|
|
1007
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1008
|
+
const add2 = (dir) => {
|
|
1009
|
+
const file = (0, import_node_path8.join)(dir, "package.json");
|
|
1010
|
+
if (!(0, import_node_fs7.existsSync)(file)) return;
|
|
1011
|
+
const real = (0, import_node_fs7.realpathSync)(dir);
|
|
1012
|
+
if (seen.has(real)) return;
|
|
1013
|
+
seen.add(real);
|
|
1014
|
+
const { name, version } = readJson(file);
|
|
1015
|
+
if (!found.has(name)) found.set(name, /* @__PURE__ */ new Set());
|
|
1016
|
+
found.get(name).add(version);
|
|
1017
|
+
};
|
|
1018
|
+
const scan = (modules, depth) => {
|
|
1019
|
+
const scope = (0, import_node_path8.join)(modules, "@fluixi");
|
|
1020
|
+
if ((0, import_node_fs7.existsSync)(scope)) {
|
|
1021
|
+
for (const name of (0, import_node_fs7.readdirSync)(scope)) {
|
|
1022
|
+
add2((0, import_node_path8.join)(scope, name));
|
|
1023
|
+
if (depth > 0) scan((0, import_node_path8.join)(scope, name, "node_modules"), depth - 1);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
const top = (0, import_node_path8.join)(root, "node_modules");
|
|
1028
|
+
scan(top, 2);
|
|
1029
|
+
const store = (0, import_node_path8.join)(top, ".pnpm");
|
|
1030
|
+
if ((0, import_node_fs7.existsSync)(store)) {
|
|
1031
|
+
for (const entry of (0, import_node_fs7.readdirSync)(store)) {
|
|
1032
|
+
if (entry.startsWith("@fluixi+")) scan((0, import_node_path8.join)(store, entry, "node_modules"), 0);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
return Object.fromEntries([...found].map(([k, v]) => [k, [...v].sort()]));
|
|
1036
|
+
}
|
|
1037
|
+
function pluginConfigured(root) {
|
|
1038
|
+
const file = (0, import_node_path8.join)(root, "tsconfig.json");
|
|
1039
|
+
if (!(0, import_node_fs7.existsSync)(file)) return null;
|
|
1040
|
+
let config;
|
|
1041
|
+
try {
|
|
1042
|
+
const ts = (0, import_node_module3.createRequire)((0, import_node_path8.join)(root, "package.json"))("typescript");
|
|
1043
|
+
config = ts.readConfigFile(file, ts.sys.readFile).config;
|
|
1044
|
+
} catch {
|
|
1045
|
+
try {
|
|
1046
|
+
config = readJson(file);
|
|
1047
|
+
} catch {
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return (config?.compilerOptions?.plugins ?? []).some((p) => p.name === "@fluixi/ts-plugin");
|
|
1052
|
+
}
|
|
1053
|
+
function diagnose(root) {
|
|
1054
|
+
const problems = [];
|
|
1055
|
+
for (const [name, versions] of Object.entries(installedCopies(root))) {
|
|
1056
|
+
if (versions.length > 1) {
|
|
1057
|
+
problems.push({ severity: "error", message: `${name} is installed ${versions.length} times (${versions.join(", ")}); each copy keeps its own state` });
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
const pkg = (0, import_node_fs7.existsSync)((0, import_node_path8.join)(root, "package.json")) ? readJson((0, import_node_path8.join)(root, "package.json")) : {};
|
|
1061
|
+
const declared = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1062
|
+
const prerelease = Object.entries(declared).filter(([n]) => n.startsWith("@fluixi/")).map(([, v]) => v.match(/-(alpha|beta|rc)\.\d+/)?.[1]).filter(Boolean);
|
|
1063
|
+
if (new Set(prerelease).size > 1) {
|
|
1064
|
+
problems.push({ severity: "warning", message: `@fluixi/* dependencies mix prerelease tags (${[...new Set(prerelease)].join(", ")})` });
|
|
1065
|
+
}
|
|
1066
|
+
const plugin = pluginConfigured(root);
|
|
1067
|
+
if (plugin === false) {
|
|
1068
|
+
problems.push({
|
|
1069
|
+
severity: "warning",
|
|
1070
|
+
message: 'tsconfig.json does not load @fluixi/ts-plugin: the editor shows no env or auth warnings. Add { "name": "@fluixi/ts-plugin" } to compilerOptions.plugins'
|
|
1071
|
+
});
|
|
1072
|
+
} else if (plugin && !(0, import_node_fs7.existsSync)((0, import_node_path8.join)(root, "node_modules/@fluixi/ts-plugin"))) {
|
|
1073
|
+
problems.push({ severity: "warning", message: "tsconfig.json names @fluixi/ts-plugin, which is not installed" });
|
|
1074
|
+
}
|
|
1075
|
+
return problems;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
212
1078
|
// src/cli.ts
|
|
213
1079
|
var import_meta = {};
|
|
214
1080
|
function usage() {
|
|
215
|
-
const cmd = (name, desc) => ` ${
|
|
1081
|
+
const cmd = (name, desc) => ` ${import_ui5.c.cyan(name.padEnd(25))}${import_ui5.c.gray(desc)}`;
|
|
216
1082
|
return [
|
|
217
1083
|
"",
|
|
218
|
-
` ${(0,
|
|
1084
|
+
` ${(0, import_ui5.wordmark)()} ${import_ui5.c.gray("the Fluixi command-line tool")}`,
|
|
219
1085
|
"",
|
|
220
|
-
` ${
|
|
1086
|
+
` ${import_ui5.c.bold("Usage")} ${import_ui5.c.gray("fluixi <command>")}`,
|
|
221
1087
|
"",
|
|
222
1088
|
cmd("dev", "start the SSR dev server (Vite middleware + HMR)"),
|
|
223
1089
|
cmd("build", "build the client + server bundles for production"),
|
|
224
1090
|
cmd("build --format=web-component", "build custom elements as portable files"),
|
|
225
1091
|
cmd("start", "run the production SSR server (after build)"),
|
|
226
|
-
cmd("generate <kind> <name>", "scaffold
|
|
1092
|
+
cmd("generate <kind> <name>", "scaffold code, see fluixi g --help " + import_ui5.c.dim("(alias: g)")),
|
|
1093
|
+
cmd("add auth", "set up authentication: asks, writes, installs"),
|
|
1094
|
+
cmd("check [--strict]", "env and auth rules over the project, for CI"),
|
|
1095
|
+
cmd("env", "declared and set variables, secrets that would ship, env types"),
|
|
1096
|
+
cmd("doctor", "duplicate packages, editor plugin, env problems"),
|
|
1097
|
+
cmd("info", "versions, adapter, modules, page and API routes"),
|
|
227
1098
|
"",
|
|
228
|
-
` ${
|
|
1099
|
+
` ${import_ui5.c.gray("New project:")} ${import_ui5.c.cyan("npm create fluixi")} ${import_ui5.c.gray("<dir>")}`,
|
|
229
1100
|
""
|
|
230
1101
|
].join("\n");
|
|
231
1102
|
}
|
|
@@ -239,13 +1110,21 @@ async function main() {
|
|
|
239
1110
|
generate(process.argv[3], process.argv[4]);
|
|
240
1111
|
return;
|
|
241
1112
|
}
|
|
1113
|
+
if (cmd === "add") {
|
|
1114
|
+
await add(process.argv[3], process.argv.slice(3));
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
if (cmd === "check") {
|
|
1118
|
+
process.exitCode = check(process.cwd(), process.argv.includes("--strict"));
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
242
1121
|
const config = await (0, import_config.loadConfig)();
|
|
243
1122
|
const mismatch = startMismatch(process.cwd(), import_meta.url);
|
|
244
1123
|
if (mismatch) {
|
|
245
1124
|
const { app, cli } = mismatch;
|
|
246
1125
|
console.warn(
|
|
247
1126
|
`
|
|
248
|
-
${
|
|
1127
|
+
${import_ui5.c.yellow("!")} fluixi is running @fluixi/start ${cli.version} from ${cli.dir}
|
|
249
1128
|
but this app resolves ${app.version} from ${app.dir}.
|
|
250
1129
|
Changes to the app's copy will not take effect. With npm, add
|
|
251
1130
|
"overrides": { "@fluixi/start": "$@fluixi/start" } to package.json and reinstall.
|
|
@@ -265,16 +1144,36 @@ async function main() {
|
|
|
265
1144
|
case "start":
|
|
266
1145
|
await (0, import_commands.start)(config);
|
|
267
1146
|
break;
|
|
1147
|
+
case "env":
|
|
1148
|
+
process.exitCode = await (0, import_commands.env)(config);
|
|
1149
|
+
break;
|
|
1150
|
+
case "info":
|
|
1151
|
+
await (0, import_commands.info)(config, resolvedVersions(config.root ?? process.cwd()));
|
|
1152
|
+
break;
|
|
1153
|
+
case "doctor":
|
|
1154
|
+
process.exitCode = await doctor(config);
|
|
1155
|
+
break;
|
|
268
1156
|
default:
|
|
269
1157
|
console.error(`
|
|
270
|
-
${
|
|
1158
|
+
${import_ui5.c.red("✗")} Unknown command: ${import_ui5.c.bold(cmd)}`);
|
|
271
1159
|
console.log(usage());
|
|
272
1160
|
process.exit(1);
|
|
273
1161
|
}
|
|
274
1162
|
}
|
|
1163
|
+
async function doctor(config) {
|
|
1164
|
+
const problems = diagnose(config.root ?? process.cwd());
|
|
1165
|
+
const r = await (0, import_commands.envReport)(config);
|
|
1166
|
+
for (const n of r.secretPublic) problems.push({ severity: "error", message: `${n} is in env.public and reads like a secret` });
|
|
1167
|
+
for (const e of r.exposed) problems.push({ severity: "error", message: `${e.name} in ${e.file} carries the client prefix, so Vite ships it` });
|
|
1168
|
+
for (const p of r.public.filter((p2) => !p2.set)) problems.push({ severity: "warning", message: `${p.name} is in env.public and not set` });
|
|
1169
|
+
for (const n of r.missing) problems.push({ severity: "warning", message: `${n} is in .env.example and not set locally` });
|
|
1170
|
+
const lines = problems.map((p) => ` ${p.severity === "error" ? import_ui5.c.red("✗") : import_ui5.c.yellow("!")} ${p.message}`);
|
|
1171
|
+
console.log(["", ...lines.length ? lines : [` ${import_ui5.c.green("✓")} nothing to report`], ""].join("\n"));
|
|
1172
|
+
return problems.some((p) => p.severity === "error") ? 1 : 0;
|
|
1173
|
+
}
|
|
275
1174
|
main().catch((e) => {
|
|
276
1175
|
console.error(`
|
|
277
|
-
${
|
|
1176
|
+
${import_ui5.c.red("✗")} ${e?.message ?? e}
|
|
278
1177
|
`);
|
|
279
1178
|
process.exit(1);
|
|
280
1179
|
});
|