@fluixi/cli 0.1.0-alpha.82 → 0.1.0-alpha.84
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/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @fluixi/cli v0.1.0-alpha.
|
|
1
|
+
/*! @fluixi/cli v0.1.0-alpha.84 | (c) 2026 Ibrahima Touré and Fluixi contributors | MIT */
|
|
2
2
|
"use strict";
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
package/dist/index.mjs
CHANGED
package/dist/report.cjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/report.ts
|
|
21
|
+
var report_exports = {};
|
|
22
|
+
__export(report_exports, {
|
|
23
|
+
report: () => report,
|
|
24
|
+
reportAuth: () => reportAuth
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(report_exports);
|
|
27
|
+
var import_node_path = require("node:path");
|
|
28
|
+
var import_ui = require("@fluixi/start/ui");
|
|
29
|
+
var rel = (file) => (0, import_node_path.relative)(process.cwd(), file) || ".";
|
|
30
|
+
function report(steps) {
|
|
31
|
+
for (const s of steps) {
|
|
32
|
+
if (s.kind === "hint") {
|
|
33
|
+
console.log(` ${import_ui.c.yellow("!")} ${import_ui.c.gray("add to")} ${import_ui.c.cyan(rel(s.file))}${import_ui.c.gray(":")}`);
|
|
34
|
+
for (const l of s.lines) console.log(` ${l}`);
|
|
35
|
+
} else if (s.kind === "kept") {
|
|
36
|
+
console.log(` ${import_ui.c.gray("-")} ${import_ui.c.cyan(rel(s.file))} ${import_ui.c.gray("exists, kept")}`);
|
|
37
|
+
} else if (s.kind === "listed") {
|
|
38
|
+
console.log(` ${import_ui.c.gray("-")} ${import_ui.c.cyan(rel(s.file))} ${import_ui.c.gray("already lists it")}`);
|
|
39
|
+
} else {
|
|
40
|
+
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}`) : ""}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function reportAuth(result, installHint) {
|
|
45
|
+
for (const n of result.notes) console.log(` ${import_ui.c.gray(n)}`);
|
|
46
|
+
report(result.steps);
|
|
47
|
+
const next = [...installHint ? [installHint] : [], ...result.next];
|
|
48
|
+
if (next.length) console.log(` ${import_ui.c.gray("next:")} ${next.map((n) => import_ui.c.gray(n)).join(import_ui.c.gray("; "))}`);
|
|
49
|
+
}
|
|
50
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
51
|
+
0 && (module.exports = {
|
|
52
|
+
report,
|
|
53
|
+
reportAuth
|
|
54
|
+
});
|
package/dist/report.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AuthResult, Step } from './scaffold/index.js';
|
|
2
|
+
export declare function report(steps: Step[]): void;
|
|
3
|
+
/** Steps, then notes and what is left, for an auth setup. */
|
|
4
|
+
export declare function reportAuth(result: AuthResult, installHint?: string): void;
|
|
5
|
+
//# sourceMappingURL=report.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAI5D,wBAAgB,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI,CAa1C;AAED,6DAA6D;AAC7D,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAKzE"}
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** How the CLI shows what a scaffold step did. */
|
|
2
|
+
import { relative } from 'node:path';
|
|
3
|
+
import { c } from '@fluixi/start/ui';
|
|
4
|
+
const rel = (file) => relative(process.cwd(), file) || '.';
|
|
5
|
+
export function report(steps) {
|
|
6
|
+
for (const s of steps) {
|
|
7
|
+
if (s.kind === 'hint') {
|
|
8
|
+
console.log(` ${c.yellow('!')} ${c.gray('add to')} ${c.cyan(rel(s.file))}${c.gray(':')}`);
|
|
9
|
+
for (const l of s.lines)
|
|
10
|
+
console.log(` ${l}`);
|
|
11
|
+
}
|
|
12
|
+
else if (s.kind === 'kept') {
|
|
13
|
+
console.log(` ${c.gray('-')} ${c.cyan(rel(s.file))} ${c.gray('exists, kept')}`);
|
|
14
|
+
}
|
|
15
|
+
else if (s.kind === 'listed') {
|
|
16
|
+
console.log(` ${c.gray('-')} ${c.cyan(rel(s.file))} ${c.gray('already lists it')}`);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
console.log(` ${c.green('✓')} ${c.gray(s.kind)} ${c.cyan(rel(s.file))}${s.detail ? c.gray(`: ${s.detail}`) : ''}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Steps, then notes and what is left, for an auth setup. */
|
|
24
|
+
export function reportAuth(result, installHint) {
|
|
25
|
+
for (const n of result.notes)
|
|
26
|
+
console.log(` ${c.gray(n)}`);
|
|
27
|
+
report(result.steps);
|
|
28
|
+
const next = [...(installHint ? [installHint] : []), ...result.next];
|
|
29
|
+
if (next.length)
|
|
30
|
+
console.log(` ${c.gray('next:')} ${next.map((n) => c.gray(n)).join(c.gray('; '))}`);
|
|
31
|
+
}
|
package/dist/report.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// src/report.ts
|
|
2
|
+
import { relative } from "node:path";
|
|
3
|
+
import { c } from "@fluixi/start/ui";
|
|
4
|
+
var rel = (file) => relative(process.cwd(), file) || ".";
|
|
5
|
+
function report(steps) {
|
|
6
|
+
for (const s of steps) {
|
|
7
|
+
if (s.kind === "hint") {
|
|
8
|
+
console.log(` ${c.yellow("!")} ${c.gray("add to")} ${c.cyan(rel(s.file))}${c.gray(":")}`);
|
|
9
|
+
for (const l of s.lines) console.log(` ${l}`);
|
|
10
|
+
} else if (s.kind === "kept") {
|
|
11
|
+
console.log(` ${c.gray("-")} ${c.cyan(rel(s.file))} ${c.gray("exists, kept")}`);
|
|
12
|
+
} else if (s.kind === "listed") {
|
|
13
|
+
console.log(` ${c.gray("-")} ${c.cyan(rel(s.file))} ${c.gray("already lists it")}`);
|
|
14
|
+
} else {
|
|
15
|
+
console.log(` ${c.green("✓")} ${c.gray(s.kind)} ${c.cyan(rel(s.file))}${s.detail ? c.gray(`: ${s.detail}`) : ""}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function reportAuth(result, installHint) {
|
|
20
|
+
for (const n of result.notes) console.log(` ${c.gray(n)}`);
|
|
21
|
+
report(result.steps);
|
|
22
|
+
const next = [...installHint ? [installHint] : [], ...result.next];
|
|
23
|
+
if (next.length) console.log(` ${c.gray("next:")} ${next.map((n) => c.gray(n)).join(c.gray("; "))}`);
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
report,
|
|
27
|
+
reportAuth
|
|
28
|
+
};
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/scaffold/auth.ts
|
|
21
|
+
var auth_exports = {};
|
|
22
|
+
__export(auth_exports, {
|
|
23
|
+
ScaffoldError: () => ScaffoldError,
|
|
24
|
+
ensureAuthEnv: () => ensureAuthEnv,
|
|
25
|
+
importPath: () => importPath,
|
|
26
|
+
projectFormat: () => projectFormat,
|
|
27
|
+
registerModule: () => registerModule,
|
|
28
|
+
resolveTokens: () => resolveTokens,
|
|
29
|
+
setupBetterAuth: () => setupBetterAuth,
|
|
30
|
+
setupOAuth2: () => setupOAuth2,
|
|
31
|
+
writeOnce: () => writeOnce
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(auth_exports);
|
|
34
|
+
var import_node_fs2 = require("node:fs");
|
|
35
|
+
var import_node_path2 = require("node:path");
|
|
36
|
+
|
|
37
|
+
// src/scaffold/edit.ts
|
|
38
|
+
var import_node_fs = require("node:fs");
|
|
39
|
+
var import_node_path = require("node:path");
|
|
40
|
+
function closing(text, open) {
|
|
41
|
+
let depth = 0;
|
|
42
|
+
for (let i = open; i < text.length; i++) {
|
|
43
|
+
const ch = text[i];
|
|
44
|
+
if (ch === "/" && text[i + 1] === "/") i = text.indexOf("\n", i) === -1 ? text.length : text.indexOf("\n", i);
|
|
45
|
+
else if (ch === "/" && text[i + 1] === "*") i = text.indexOf("*/", i + 2) + 1;
|
|
46
|
+
else if (ch === "'" || ch === '"' || ch === "`") {
|
|
47
|
+
for (i++; i < text.length && text[i] !== ch; i++) if (text[i] === "\\") i++;
|
|
48
|
+
} else if ("([{".includes(ch)) depth++;
|
|
49
|
+
else if (")]}".includes(ch) && --depth === 0) return i;
|
|
50
|
+
}
|
|
51
|
+
return -1;
|
|
52
|
+
}
|
|
53
|
+
function appendToArray(text, open, items) {
|
|
54
|
+
const close = closing(text, open);
|
|
55
|
+
const inner = text.slice(open + 1, close);
|
|
56
|
+
if (!inner.trim()) return text.slice(0, open + 1) + items.join(", ") + text.slice(close);
|
|
57
|
+
if (inner.includes("\n")) {
|
|
58
|
+
const last = inner.trimEnd();
|
|
59
|
+
const indent = last.slice(last.lastIndexOf("\n") + 1).match(/^\s*/)[0];
|
|
60
|
+
const comma = last.endsWith(",") ? "" : ",";
|
|
61
|
+
const tail = inner.slice(last.length);
|
|
62
|
+
return text.slice(0, open + 1) + last + comma + items.map((i) => `
|
|
63
|
+
${indent}${i},`).join("") + tail + text.slice(close);
|
|
64
|
+
}
|
|
65
|
+
const body = inner.replace(/,?\s*$/, "");
|
|
66
|
+
const trailing = inner.slice(body.length).replace(/^,/, "");
|
|
67
|
+
return text.slice(0, open + 1) + body + ", " + items.join(", ") + trailing + text.slice(close);
|
|
68
|
+
}
|
|
69
|
+
function addImport(text, line) {
|
|
70
|
+
if (text.includes(line)) return text;
|
|
71
|
+
const imports = [...text.matchAll(/^import[\s\S]*?from\s+['"][^'"]+['"];?[^\n]*\n/gm)];
|
|
72
|
+
const at = imports.length ? imports[imports.length - 1].index + imports[imports.length - 1][0].length : 0;
|
|
73
|
+
return text.slice(0, at) + line + "\n" + text.slice(at);
|
|
74
|
+
}
|
|
75
|
+
function register(log, file, header, r) {
|
|
76
|
+
const imports = [...r.extraImports ?? [], r.importLine];
|
|
77
|
+
if (!(0, import_node_fs.existsSync)(file)) {
|
|
78
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(file), { recursive: true });
|
|
79
|
+
(0, import_node_fs.writeFileSync)(
|
|
80
|
+
file,
|
|
81
|
+
`${header}import { defineApp } from '@fluixi/start/app';
|
|
82
|
+
${imports.join("\n")}
|
|
83
|
+
|
|
84
|
+
export default defineApp({ imports: [${r.entry}] });
|
|
85
|
+
`
|
|
86
|
+
);
|
|
87
|
+
log.push({ kind: "created", file });
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
let text = (0, import_node_fs.readFileSync)(file, "utf8");
|
|
91
|
+
if (text.includes(r.importLine)) {
|
|
92
|
+
log.push({ kind: "listed", file, detail: r.entry });
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
const m = text.match(/\bimports\s*:\s*\[/);
|
|
96
|
+
if (!m) {
|
|
97
|
+
log.push({ kind: "hint", file, lines: [...imports, `imports: [${r.entry}]`] });
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
text = appendToArray(text, m.index + m[0].length - 1, [r.entry]);
|
|
101
|
+
for (const line of imports) text = addImport(text, line);
|
|
102
|
+
(0, import_node_fs.writeFileSync)(file, text);
|
|
103
|
+
log.push({ kind: "updated", file, detail: `imports ${r.entry}` });
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
function envPublicNames(configFile) {
|
|
107
|
+
if (!(0, import_node_fs.existsSync)(configFile)) return null;
|
|
108
|
+
const text = (0, import_node_fs.readFileSync)(configFile, "utf8");
|
|
109
|
+
const m = text.match(/\bpublic\s*:\s*\[/);
|
|
110
|
+
if (!m) return null;
|
|
111
|
+
const open = m.index + m[0].length - 1;
|
|
112
|
+
const list = text.slice(open, closing(text, open) + 1);
|
|
113
|
+
return [...list.matchAll(/['"]([A-Za-z_][A-Za-z0-9_]*)['"]/g)].map((x) => x[1]);
|
|
114
|
+
}
|
|
115
|
+
function addEnvPublic(log, configFile, names) {
|
|
116
|
+
const list = (indent) => `${indent}public: [
|
|
117
|
+
${names.map((n) => `${indent} '${n}',
|
|
118
|
+
`).join("")}${indent}],
|
|
119
|
+
`;
|
|
120
|
+
const have = envPublicNames(configFile);
|
|
121
|
+
if (have === null) {
|
|
122
|
+
const text2 = (0, import_node_fs.existsSync)(configFile) ? (0, import_node_fs.readFileSync)(configFile, "utf8") : "";
|
|
123
|
+
const call = text2.match(/defineConfig\(\{\n/);
|
|
124
|
+
if (call && !/\benv\s*:/.test(text2)) {
|
|
125
|
+
const at = call.index + call[0].length;
|
|
126
|
+
(0, import_node_fs.writeFileSync)(configFile, text2.slice(0, at) + ` env: {
|
|
127
|
+
${list(" ")} },
|
|
128
|
+
` + text2.slice(at));
|
|
129
|
+
log.push({ kind: "updated", file: configFile, detail: `env.public ${names.join(", ")}` });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
log.push({ kind: "hint", file: configFile, lines: [`env: { public: [${names.map((n) => `'${n}'`).join(", ")}] },`] });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const missing = names.filter((n) => !have.includes(n));
|
|
136
|
+
if (!missing.length) return;
|
|
137
|
+
const text = (0, import_node_fs.readFileSync)(configFile, "utf8");
|
|
138
|
+
const m = text.match(/\bpublic\s*:\s*\[/);
|
|
139
|
+
(0, import_node_fs.writeFileSync)(configFile, appendToArray(text, m.index + m[0].length - 1, missing.map((n) => `'${n}'`)));
|
|
140
|
+
log.push({ kind: "updated", file: configFile, detail: `env.public + ${missing.join(", ")}` });
|
|
141
|
+
}
|
|
142
|
+
function addEnvExample(log, file, comment, vars) {
|
|
143
|
+
const text = (0, import_node_fs.existsSync)(file) ? (0, import_node_fs.readFileSync)(file, "utf8") : "";
|
|
144
|
+
const have = new Set([...text.matchAll(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/gm)].map((m) => m[1]));
|
|
145
|
+
const add = Object.entries(vars).filter(([k]) => !have.has(k) && !have.has(`VITE_${k}`));
|
|
146
|
+
if (!add.length) return;
|
|
147
|
+
const block = `${comment.split("\n").map((l) => `# ${l}`).join("\n")}
|
|
148
|
+
${add.map(([k, v]) => `${k}=${v}`).join("\n")}
|
|
149
|
+
`;
|
|
150
|
+
(0, import_node_fs.writeFileSync)(file, text ? `${text.replace(/\n*$/, "\n")}
|
|
151
|
+
${block}` : block);
|
|
152
|
+
log.push({ kind: text ? "updated" : "created", file, detail: add.map(([k]) => k).join(", ") });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/scaffold/templates.ts
|
|
156
|
+
var PROVIDERS = ["oidc", "keycloak", "auth0", "okta", "google", "dex", "oauth2"];
|
|
157
|
+
var SERVER_ONLY = {
|
|
158
|
+
google: "Google answers a token request without a client secret with `client_secret is missing`",
|
|
159
|
+
dex: "Dex sends no CORS headers on discovery or the token endpoint"
|
|
160
|
+
};
|
|
161
|
+
var PROVIDER_VARS = {
|
|
162
|
+
oidc: { AUTH_ISSUER: "https://id.example.com" },
|
|
163
|
+
keycloak: { AUTH_BASE_URL: "http://localhost:8080", AUTH_REALM: "master" },
|
|
164
|
+
auth0: { AUTH_DOMAIN: "example.eu.auth0.com", AUTH_AUDIENCE: "https://api.example.com" },
|
|
165
|
+
okta: { AUTH_DOMAIN: "example.okta.com" },
|
|
166
|
+
google: {},
|
|
167
|
+
dex: { AUTH_BASE_URL: "http://localhost:5556/dex" },
|
|
168
|
+
oauth2: {
|
|
169
|
+
AUTH_ISSUER: "https://id.example.com",
|
|
170
|
+
AUTH_AUTHORIZATION_ENDPOINT: "https://id.example.com/authorize",
|
|
171
|
+
AUTH_TOKEN_ENDPOINT: "https://id.example.com/token"
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
var AUTH_READER = `// Provider, issuer, client and token mode from the AUTH_* variables in env.public. Moving to
|
|
175
|
+
// another provider is a change to .env.
|
|
176
|
+
export const auth = authFromEnv(publicEnv(), { clientPrefix: '' });
|
|
177
|
+
`;
|
|
178
|
+
var authEnvConfig = () => `import { publicEnv } from '@fluixi/start/env';
|
|
179
|
+
import { authFromEnv } from '@fluixi/oauth2/env';
|
|
180
|
+
|
|
181
|
+
${AUTH_READER}`;
|
|
182
|
+
var authServiceFile = (tokens) => `import { createAuthService, authModule } from '@fluixi/oauth2';
|
|
183
|
+
import { env } from '@fluixi/start/env';
|
|
184
|
+
${tokens === "browser" ? `import { auth as authEnv } from '../config/env.js';
|
|
185
|
+
` : ""}
|
|
186
|
+
export interface User {
|
|
187
|
+
sub: string;
|
|
188
|
+
email?: string;
|
|
189
|
+
name?: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const appUrl = env('APP_URL') ?? 'http://localhost:3000';
|
|
193
|
+
|
|
194
|
+
export const auth = createAuthService<User>({
|
|
195
|
+
${tokens === "browser" ? ` config: {
|
|
196
|
+
...authEnv.client,
|
|
197
|
+
tokens: 'browser',
|
|
198
|
+
redirectUri: \`\${appUrl}/callback\`,
|
|
199
|
+
postLogoutRedirectUri: \`\${appUrl}/login\`,
|
|
200
|
+
},` : ` // The tokens stay on the server behind an httpOnly cookie. The browser calls the routes
|
|
201
|
+
// auth.server.ts mounts under basePath and never sees a token.
|
|
202
|
+
config: {
|
|
203
|
+
tokens: 'server',
|
|
204
|
+
redirectUri: \`\${appUrl}/auth/callback\`,
|
|
205
|
+
postLogoutRedirectUri: \`\${appUrl}/login\`,
|
|
206
|
+
basePath: '/auth',
|
|
207
|
+
},`}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
export const AuthModule = authModule(auth);
|
|
211
|
+
export const { protectedRoute, Protect, Can, useUser } = auth;
|
|
212
|
+
`;
|
|
213
|
+
var authServerFile = () => `import { authServerModule } from '@fluixi/oauth2/server';
|
|
214
|
+
import { publicEnv, serverEnv } from '@fluixi/start/env';
|
|
215
|
+
import { auth } from '../config/env.js';
|
|
216
|
+
|
|
217
|
+
// A function: serverEnv with required throws when a secret is unset, and it is only read
|
|
218
|
+
// when app.config.server.ts registers this.
|
|
219
|
+
export function authServer() {
|
|
220
|
+
const appUrl = publicEnv().APP_URL ?? 'http://localhost:3000';
|
|
221
|
+
return authServerModule({
|
|
222
|
+
...auth.client,
|
|
223
|
+
clientSecret: serverEnv('OIDC_CLIENT_SECRET', { required: true }),
|
|
224
|
+
cookieSecret: serverEnv('SESSION_SECRET', { required: true }),
|
|
225
|
+
redirectUri: \`\${appUrl}/auth/callback\`,
|
|
226
|
+
defaultReturnTo: '/',
|
|
227
|
+
secureCookie: appUrl.startsWith('https://'),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
`;
|
|
231
|
+
var callbackRoute = (html) => html ? `import { html } from '@fluixi/core';
|
|
232
|
+
import { OAuthCallback } from '@fluixi/oauth2/callback';
|
|
233
|
+
|
|
234
|
+
export default OAuthCallback({
|
|
235
|
+
pending: () => html\`<p>Signing you in</p>\`,
|
|
236
|
+
failed: (reason) => html\`<p>\${reason}</p><a href="/login">Try again</a>\`,
|
|
237
|
+
});
|
|
238
|
+
` : `import { OAuthCallback } from '@fluixi/oauth2/callback';
|
|
239
|
+
|
|
240
|
+
export default OAuthCallback({
|
|
241
|
+
pending: () => <p>Signing you in</p>,
|
|
242
|
+
failed: (reason) => (
|
|
243
|
+
<>
|
|
244
|
+
<p>{reason}</p>
|
|
245
|
+
<a href="/login">Try again</a>
|
|
246
|
+
</>
|
|
247
|
+
),
|
|
248
|
+
});
|
|
249
|
+
`;
|
|
250
|
+
var loginRoute = (html) => html ? `import { html } from '@fluixi/core';
|
|
251
|
+
import { auth } from '../services/auth.js';
|
|
252
|
+
|
|
253
|
+
export default function Login() {
|
|
254
|
+
return html\`<button @click=\${() => auth.signIn({ returnTo: '/' })}>Sign in</button>\`;
|
|
255
|
+
}
|
|
256
|
+
` : `import { auth } from '../services/auth.js';
|
|
257
|
+
|
|
258
|
+
export default function Login() {
|
|
259
|
+
return <button onClick={() => auth.signIn({ returnTo: '/' })}>Sign in</button>;
|
|
260
|
+
}
|
|
261
|
+
`;
|
|
262
|
+
var betterAuthConfig = () => `import { betterAuth } from 'better-auth';
|
|
263
|
+
import { defineAuth } from '@fluixi/auth/server';
|
|
264
|
+
|
|
265
|
+
export type AppUser = { id: string; email: string; name?: string; role?: string };
|
|
266
|
+
|
|
267
|
+
// Better Auth owns the audited primitives; defineAuth registers it with @fluixi/auth.
|
|
268
|
+
export const auth = defineAuth<AppUser>(
|
|
269
|
+
betterAuth({
|
|
270
|
+
// database: <your adapter>,
|
|
271
|
+
emailAndPassword: { enabled: true },
|
|
272
|
+
// socialProviders: { google: { … }, github: { … } },
|
|
273
|
+
} as Parameters<typeof betterAuth>[0]),
|
|
274
|
+
);
|
|
275
|
+
`;
|
|
276
|
+
var betterAuthClient = () => `import { createAuthClient } from 'better-auth/client';
|
|
277
|
+
import { createAuthHooks } from '@fluixi/auth/client';
|
|
278
|
+
import type { AppUser } from './auth.config.js';
|
|
279
|
+
|
|
280
|
+
const client = createAuthClient({ baseURL: '/api/auth' });
|
|
281
|
+
|
|
282
|
+
// Or, to reuse an existing reactive session: createAuthHooks({ session: yourSessionStore }).
|
|
283
|
+
export const { useUser, useSession, useAuth, useSignIn, useSignOut, protectedRoute } =
|
|
284
|
+
createAuthHooks<AppUser>({ client, loginPath: '/login' });
|
|
285
|
+
`;
|
|
286
|
+
var betterAuthMiddleware = () => `import { defineMiddleware } from '@fluixi/start';
|
|
287
|
+
import { authMiddleware } from '@fluixi/auth/server';
|
|
288
|
+
import './auth.config.js';
|
|
289
|
+
|
|
290
|
+
export default defineMiddleware([authMiddleware()]);
|
|
291
|
+
`;
|
|
292
|
+
var betterAuthRoute = () => `import { mountAuthRoutes } from '@fluixi/auth/server';
|
|
293
|
+
import '../../auth.config.js';
|
|
294
|
+
|
|
295
|
+
const handler = mountAuthRoutes();
|
|
296
|
+
export const GET = handler;
|
|
297
|
+
export const POST = handler;
|
|
298
|
+
export default handler;
|
|
299
|
+
`;
|
|
300
|
+
|
|
301
|
+
// src/scaffold/auth.ts
|
|
302
|
+
var ScaffoldError = class extends Error {
|
|
303
|
+
};
|
|
304
|
+
function projectFormat(root) {
|
|
305
|
+
try {
|
|
306
|
+
const ts = JSON.parse((0, import_node_fs2.readFileSync)((0, import_node_path2.join)(root, "tsconfig.json"), "utf8"));
|
|
307
|
+
if (ts?.compilerOptions && !ts.compilerOptions.jsx) return "html";
|
|
308
|
+
} catch {
|
|
309
|
+
}
|
|
310
|
+
return "jsx";
|
|
311
|
+
}
|
|
312
|
+
function importPath(from, to) {
|
|
313
|
+
const p = (0, import_node_path2.relative)((0, import_node_path2.dirname)(from), to).replace(/\\/g, "/").replace(/\.tsx?$/, ".js");
|
|
314
|
+
return p.startsWith(".") ? p : `./${p}`;
|
|
315
|
+
}
|
|
316
|
+
function writeOnce(log, file, content) {
|
|
317
|
+
if ((0, import_node_fs2.existsSync)(file)) {
|
|
318
|
+
log.push({ kind: "kept", file });
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
(0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(file), { recursive: true });
|
|
322
|
+
(0, import_node_fs2.writeFileSync)(file, content);
|
|
323
|
+
log.push({ kind: "created", file });
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
var APP_HEADER = "// Registered by @fluixi/start ahead of both entries.\n";
|
|
327
|
+
var APP_SERVER_HEADER = "// Only the server build imports this file, so what it lists never reaches the browser.\n";
|
|
328
|
+
function registerModule(log, root, server, file, name, entry = name) {
|
|
329
|
+
const config = (0, import_node_path2.join)(root, server ? "src/app.config.server.ts" : "src/app.config.ts");
|
|
330
|
+
register(log, config, server ? APP_SERVER_HEADER : APP_HEADER, {
|
|
331
|
+
importLine: `import { ${name} } from '${importPath(config, file)}';`,
|
|
332
|
+
entry
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
function ensureAuthEnv(log, root) {
|
|
336
|
+
const file = (0, import_node_path2.join)(root, "src/config/env.ts");
|
|
337
|
+
if (writeOnce(log, file, authEnvConfig())) return;
|
|
338
|
+
let text = (0, import_node_fs2.readFileSync)(file, "utf8");
|
|
339
|
+
if (/export\s+const\s+auth\b/.test(text)) return;
|
|
340
|
+
if (!/\bpublicEnv\b/.test(text)) text = addImport(text, `import { publicEnv } from '@fluixi/start/env';`);
|
|
341
|
+
text = addImport(text, `import { authFromEnv } from '@fluixi/oauth2/env';`);
|
|
342
|
+
(0, import_node_fs2.writeFileSync)(file, `${text.replace(/\n*$/, "\n")}
|
|
343
|
+
${AUTH_READER}`);
|
|
344
|
+
log.push({ kind: "updated", file, detail: "auth" });
|
|
345
|
+
}
|
|
346
|
+
function resolveTokens(provider, tokens) {
|
|
347
|
+
const refusal = SERVER_ONLY[provider];
|
|
348
|
+
if (refusal && tokens === "browser") throw new ScaffoldError(`${provider} needs server tokens: ${refusal}`);
|
|
349
|
+
return { tokens: tokens ?? (refusal ? "server" : "browser"), forced: !!refusal && !tokens };
|
|
350
|
+
}
|
|
351
|
+
function setupOAuth2(root, options, dependencySpec = "alpha") {
|
|
352
|
+
const provider = options.provider ?? "oidc";
|
|
353
|
+
if (!PROVIDERS.includes(provider)) throw new ScaffoldError(`unknown provider ${provider}: ${PROVIDERS.join(", ")}`);
|
|
354
|
+
const { tokens, forced } = resolveTokens(provider, options.tokens);
|
|
355
|
+
const html = options.format === "html";
|
|
356
|
+
const ext = html ? "ts" : "tsx";
|
|
357
|
+
const steps = [];
|
|
358
|
+
const at = (rel) => (0, import_node_path2.join)(root, rel);
|
|
359
|
+
ensureAuthEnv(steps, root);
|
|
360
|
+
const service = at("src/services/auth.ts");
|
|
361
|
+
if (writeOnce(steps, service, authServiceFile(tokens))) registerModule(steps, root, false, service, "AuthModule");
|
|
362
|
+
if (tokens === "browser") writeOnce(steps, at(`src/routes/callback.${ext}`), callbackRoute(html));
|
|
363
|
+
writeOnce(steps, at(`src/routes/login.${ext}`), loginRoute(html));
|
|
364
|
+
if (tokens === "server") {
|
|
365
|
+
const server = at("src/services/auth.server.ts");
|
|
366
|
+
if (writeOnce(steps, server, authServerFile())) registerModule(steps, root, true, server, "authServer", "authServer()");
|
|
367
|
+
}
|
|
368
|
+
const vars = {
|
|
369
|
+
AUTH_PROVIDER: provider,
|
|
370
|
+
AUTH_CLIENT_ID: "my-app",
|
|
371
|
+
AUTH_TOKENS: tokens,
|
|
372
|
+
...PROVIDER_VARS[provider],
|
|
373
|
+
APP_URL: "http://localhost:3000"
|
|
374
|
+
};
|
|
375
|
+
addEnvPublic(steps, at("fluixi.config.ts"), Object.keys(vars));
|
|
376
|
+
addEnvExample(steps, at(".env.example"), `@fluixi/oauth2 (${provider}). Listed in env.public: the browser sees them.`, vars);
|
|
377
|
+
if (tokens === "server") {
|
|
378
|
+
addEnvExample(steps, at(".env.example"), "Server only, never in env.public. SESSION_SECRET: 32 bytes or more.", {
|
|
379
|
+
OIDC_CLIENT_SECRET: "",
|
|
380
|
+
SESSION_SECRET: ""
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
const redirect = tokens === "browser" ? "/callback" : "/auth/callback";
|
|
384
|
+
return {
|
|
385
|
+
steps,
|
|
386
|
+
dependencies: { "@fluixi/oauth2": dependencySpec },
|
|
387
|
+
next: ["copy .env.example to .env and fill it in", `register <APP_URL>${redirect} as the redirect URI at the provider`],
|
|
388
|
+
notes: forced ? [`${provider} refuses a public client, so the tokens stay on the server`] : []
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function setupBetterAuth(root, dependencySpec = "alpha") {
|
|
392
|
+
const steps = [];
|
|
393
|
+
const at = (rel) => (0, import_node_path2.join)(root, rel);
|
|
394
|
+
writeOnce(steps, at("src/auth.config.ts"), betterAuthConfig());
|
|
395
|
+
writeOnce(steps, at("src/auth.client.ts"), betterAuthClient());
|
|
396
|
+
if (!writeOnce(steps, at("src/middleware.ts"), betterAuthMiddleware())) {
|
|
397
|
+
steps.push({
|
|
398
|
+
kind: "hint",
|
|
399
|
+
file: at("src/middleware.ts"),
|
|
400
|
+
lines: [`import { authMiddleware } from '@fluixi/auth/server';`, `import './auth.config.js';`, "authMiddleware() in the middleware list"]
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
writeOnce(steps, at("src/api/auth/[...all].ts"), betterAuthRoute());
|
|
404
|
+
return {
|
|
405
|
+
steps,
|
|
406
|
+
dependencies: { "@fluixi/auth": dependencySpec, "better-auth": "^1" },
|
|
407
|
+
next: ["set your database adapter in src/auth.config.ts"],
|
|
408
|
+
notes: []
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
412
|
+
0 && (module.exports = {
|
|
413
|
+
ScaffoldError,
|
|
414
|
+
ensureAuthEnv,
|
|
415
|
+
importPath,
|
|
416
|
+
projectFormat,
|
|
417
|
+
registerModule,
|
|
418
|
+
resolveTokens,
|
|
419
|
+
setupBetterAuth,
|
|
420
|
+
setupOAuth2,
|
|
421
|
+
writeOnce
|
|
422
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type Step } from './edit.js';
|
|
2
|
+
import * as t from './templates.js';
|
|
3
|
+
export type Format = 'jsx' | 'html';
|
|
4
|
+
export type Tokens = 'browser' | 'server';
|
|
5
|
+
export interface OAuth2Options {
|
|
6
|
+
provider?: t.ProviderName;
|
|
7
|
+
tokens?: Tokens;
|
|
8
|
+
format: Format;
|
|
9
|
+
}
|
|
10
|
+
export interface AuthResult {
|
|
11
|
+
steps: Step[];
|
|
12
|
+
/** Packages the wiring imports, with the spec to install. */
|
|
13
|
+
dependencies: Record<string, string>;
|
|
14
|
+
/** What is left to the developer, one line each. */
|
|
15
|
+
next: string[];
|
|
16
|
+
/** Decided rather than asked, worth saying (a provider forcing server tokens). */
|
|
17
|
+
notes: string[];
|
|
18
|
+
}
|
|
19
|
+
/** A refusal the caller reports as it reports any bad input. */
|
|
20
|
+
export declare class ScaffoldError extends Error {
|
|
21
|
+
}
|
|
22
|
+
/** `jsx` when the project's tsconfig configures it, `html` for an html``-authored one. */
|
|
23
|
+
export declare function projectFormat(root: string): Format;
|
|
24
|
+
/** A path for an import between two files, with the .js extension ESM needs. */
|
|
25
|
+
export declare function importPath(from: string, to: string): string;
|
|
26
|
+
/** Write a file the app may already have, keeping theirs. Returns whether it wrote. */
|
|
27
|
+
export declare function writeOnce(log: Step[], file: string, content: string): boolean;
|
|
28
|
+
/** List a module in `src/app.config.ts`, or `app.config.server.ts` for a server one. */
|
|
29
|
+
export declare function registerModule(log: Step[], root: string, server: boolean, file: string, name: string, entry?: string): void;
|
|
30
|
+
/** `src/config/env.ts` with the `auth` reader: created when missing, appended when not there. */
|
|
31
|
+
export declare function ensureAuthEnv(log: Step[], root: string): void;
|
|
32
|
+
/** The token mode for a provider: what was asked, checked, or the provider's only option. */
|
|
33
|
+
export declare function resolveTokens(provider: t.ProviderName, tokens: Tokens | undefined): {
|
|
34
|
+
tokens: Tokens;
|
|
35
|
+
forced: boolean;
|
|
36
|
+
};
|
|
37
|
+
export declare function setupOAuth2(root: string, options: OAuth2Options, dependencySpec?: string): AuthResult;
|
|
38
|
+
export declare function setupBetterAuth(root: string, dependencySpec?: string): AuthResult;
|
|
39
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/scaffold/auth.ts"],"names":[],"mappings":"AASA,OAAO,EAAoD,KAAK,IAAI,EAAE,MAAM,WAAW,CAAC;AACxF,OAAO,KAAK,CAAC,MAAM,gBAAgB,CAAC;AAEpC,MAAM,MAAM,MAAM,GAAG,KAAK,GAAG,MAAM,CAAC;AACpC,MAAM,MAAM,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE1C,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,CAAC,CAAC,YAAY,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,6DAA6D;IAC7D,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,oDAAoD;IACpD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,kFAAkF;IAClF,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,gEAAgE;AAChE,qBAAa,aAAc,SAAQ,KAAK;CAAG;AAE3C,0FAA0F;AAC1F,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQlD;AAED,gFAAgF;AAChF,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAG3D;AAED,uFAAuF;AACvF,wBAAgB,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAS7E;AAKD,wFAAwF;AACxF,wBAAgB,cAAc,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,SAAO,GAAG,IAAI,CAMzH;AAED,iGAAiG;AACjG,wBAAgB,aAAa,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAS7D;AAED,6FAA6F;AAC7F,wBAAgB,aAAa,CAAC,QAAQ,EAAE,CAAC,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAIvH;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,cAAc,SAAU,GAAG,UAAU,CA0CtG;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,cAAc,SAAU,GAAG,UAAU,CAmBlF"}
|