@protobase/cli 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/migration-file.js +18 -0
- package/dist/auth/register.js +18 -0
- package/dist/auth/write-migration.js +20 -0
- package/dist/build/config-bundle.js +9 -6
- package/dist/build/deploy-bundle.js +7 -3
- package/dist/build/entry-code.js +3 -2
- package/dist/build/ui-bundle.js +5 -2
- package/dist/build/ui-check.js +30 -0
- package/dist/build/vite-bundle.js +3 -3
- package/dist/dev/entry.js +7 -3
- package/dist/dev/project.js +15 -3
- package/dist/dev/run.js +13 -7
- package/dist/main.js +10 -4
- package/dist/package.js +1 -1
- package/dist/project/extensions.js +46 -0
- package/dist/project/project-ui.js +31 -10
- package/dist/serve/app.js +1 -1
- package/dist/serve/host-modules.js +1 -1
- package/dist/serve/lifecycle.js +2 -0
- package/dist/users/commands.js +8 -2
- package/dist/users/register.js +5 -4
- package/package.json +6 -6
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/auth/migration-file.ts
|
|
2
|
+
const numbered = /^(\d+)_.*\.sql$/;
|
|
3
|
+
const namePart = /^[a-z0-9][a-z0-9_-]*$/;
|
|
4
|
+
/**
|
|
5
|
+
* The name of the next migration in a folder holding `files`: one past the highest number, as wide as the widest
|
|
6
|
+
* (`009_…` after `008_…`; three digits in an empty folder), then `name`.
|
|
7
|
+
*/
|
|
8
|
+
const nextMigrationName = (files, name) => {
|
|
9
|
+
if (!namePart.test(name)) throw new Error(`Invalid migration name "${name}": use lowercase letters, digits, "_" or "-"`);
|
|
10
|
+
const numbers = files.map((file) => numbered.exec(file)?.[1]).filter((found) => found !== void 0);
|
|
11
|
+
const next = Math.max(0, ...numbers.map(Number)) + 1;
|
|
12
|
+
const width = Math.max(3, ...numbers.map((found) => found.length));
|
|
13
|
+
return `${String(next).padStart(width, "0")}_${name}.sql`;
|
|
14
|
+
};
|
|
15
|
+
/** The file's contents: what wrote it and why, then Better Auth's statements, one per paragraph. */
|
|
16
|
+
const migrationText = (sql) => `-- The auth schema changes this version of Protobase needs, written by \`protobase auth migration\`.\n-- Applied with the project's other migrations; edit or remove statements the project handles differently.\n\n${sql}\n`;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { migrationText, nextMigrationName };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { loadProjectAuth } from "../users/load-auth.js";
|
|
2
|
+
import { writeAuthMigration } from "./write-migration.js";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
//#region src/auth/register.ts
|
|
5
|
+
const out = (text) => process.stdout.write(text);
|
|
6
|
+
const registerAuth = (program) => {
|
|
7
|
+
program.command("auth").description("The auth schema of the project in the current directory").command("migration").description("Write the auth schema changes this Protobase version needs as the project's next SQL migration, compared with its database").option("--dir <dir>", "migrations folder", "db/migrations").option("--name <name>", "file name after the number", "auth").action(async (opts) => {
|
|
8
|
+
const file = await writeAuthMigration({
|
|
9
|
+
auth: await loadProjectAuth(process.cwd()),
|
|
10
|
+
dir: path.resolve(opts.dir),
|
|
11
|
+
name: opts.name
|
|
12
|
+
});
|
|
13
|
+
out(file ? `wrote ${path.relative(process.cwd(), file)}; apply it with the project's migrations\n` : "the auth schema is up to date; nothing to write\n");
|
|
14
|
+
process.exit(0);
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { registerAuth };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { migrationText, nextMigrationName } from "./migration-file.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { mkdir, readdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import { authSchemaMigration } from "@protobase/server";
|
|
5
|
+
//#region src/auth/write-migration.ts
|
|
6
|
+
/**
|
|
7
|
+
* Writes the auth schema changes the database still needs as the next migration in `dir`, and returns its path;
|
|
8
|
+
* `undefined` when the database is current. The changes are found by comparing the database with what Better Auth
|
|
9
|
+
* needs, so the project's earlier migrations must be applied to it first.
|
|
10
|
+
*/
|
|
11
|
+
const writeAuthMigration = async ({ auth, dir, name }) => {
|
|
12
|
+
const sql = await authSchemaMigration(auth);
|
|
13
|
+
if (sql === void 0) return void 0;
|
|
14
|
+
await mkdir(dir, { recursive: true });
|
|
15
|
+
const file = path.join(dir, nextMigrationName(await readdir(dir), name));
|
|
16
|
+
await writeFile(file, migrationText(sql), { flag: "wx" });
|
|
17
|
+
return file;
|
|
18
|
+
};
|
|
19
|
+
//#endregion
|
|
20
|
+
export { writeAuthMigration };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hostModuleIds } from "../serve/host-module-ids.js";
|
|
2
|
+
import { extensionImports } from "../project/extensions.js";
|
|
2
3
|
import { bundleEntryCode } from "./entry-code.js";
|
|
3
4
|
import { layoutCheck } from "./layout-check.js";
|
|
4
5
|
import { findPackageDir, packageName } from "./packages/package-dir.js";
|
|
@@ -51,19 +52,21 @@ const servedImports = (native) => {
|
|
|
51
52
|
}
|
|
52
53
|
};
|
|
53
54
|
};
|
|
54
|
-
const buildConfigBundle = async ({ projectDir, outFile, bun }) => {
|
|
55
|
+
const buildConfigBundle = async ({ projectDir, outFile, bun, extensions = [] }) => {
|
|
55
56
|
const file = path.join(projectDir, "protobase-bundle-entry.js");
|
|
56
57
|
const native = /* @__PURE__ */ new Map();
|
|
58
|
+
const plugins = [
|
|
59
|
+
layoutCheck(),
|
|
60
|
+
extensionImports(projectDir, extensions),
|
|
61
|
+
servedImports(native),
|
|
62
|
+
entry(file, bundleEntryCode(projectDir, extensions))
|
|
63
|
+
];
|
|
57
64
|
await viteBundle({
|
|
58
65
|
root: projectDir,
|
|
59
66
|
entry: file,
|
|
60
67
|
outFile,
|
|
61
68
|
bun,
|
|
62
|
-
plugins
|
|
63
|
-
layoutCheck(),
|
|
64
|
-
servedImports(native),
|
|
65
|
-
entry(file, bundleEntryCode(projectDir))
|
|
66
|
-
]
|
|
69
|
+
plugins
|
|
67
70
|
});
|
|
68
71
|
return [...native.values()];
|
|
69
72
|
};
|
|
@@ -3,6 +3,7 @@ import { buildConfigBundle } from "./config-bundle.js";
|
|
|
3
3
|
import { protobaseVersion } from "../version/version.js";
|
|
4
4
|
import { copyPackages } from "./packages/copy-packages.js";
|
|
5
5
|
import { buildUiBundle } from "./ui-bundle.js";
|
|
6
|
+
import { checkUiMerge } from "./ui-check.js";
|
|
6
7
|
import { existsSync } from "node:fs";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
import { readFile, rm, writeFile } from "node:fs/promises";
|
|
@@ -17,17 +18,20 @@ const removeOwnNodeModules = async (outDir) => {
|
|
|
17
18
|
force: true
|
|
18
19
|
});
|
|
19
20
|
};
|
|
20
|
-
const buildDeployBundle = async ({ projectDir, outDir, bun }) => {
|
|
21
|
+
const buildDeployBundle = async ({ projectDir, outDir, bun, extensions = [] }) => {
|
|
21
22
|
await removeOwnNodeModules(outDir);
|
|
23
|
+
if (extensions.some((extension) => extension.ui)) await checkUiMerge(projectDir, extensions);
|
|
22
24
|
const native = await buildConfigBundle({
|
|
23
25
|
projectDir,
|
|
24
26
|
outFile: path.join(outDir, bundleLayout.server),
|
|
25
|
-
bun
|
|
27
|
+
bun,
|
|
28
|
+
extensions
|
|
26
29
|
});
|
|
27
30
|
const packages = native.length > 0 ? await copyPackages(native, path.join(outDir, bundleNodeModules)) : [];
|
|
28
31
|
await buildUiBundle({
|
|
29
32
|
outDir: path.join(outDir, bundleLayout.public),
|
|
30
|
-
projectDir
|
|
33
|
+
projectDir,
|
|
34
|
+
extensions
|
|
31
35
|
});
|
|
32
36
|
const manifest = {
|
|
33
37
|
version: 2,
|
package/dist/build/entry-code.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
//#region src/build/entry-code.ts
|
|
5
5
|
const conventionModule = moduleFile("../project/convention", import.meta.url);
|
|
6
6
|
const uiDirs = (configDir) => readdirSync(configDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && existsSync(path.join(configDir, entry.name, "ui.ts"))).map((entry) => entry.name).sort();
|
|
7
|
-
const bundleEntryCode = (projectDir) => {
|
|
7
|
+
const bundleEntryCode = (projectDir, extensions = []) => {
|
|
8
8
|
const custom = path.join(projectDir, "protobase.config.ts");
|
|
9
9
|
const configDir = path.join(projectDir, "config");
|
|
10
10
|
const index = path.join(configDir, "index.ts");
|
|
@@ -16,7 +16,8 @@ const bundleEntryCode = (projectDir) => {
|
|
|
16
16
|
const dirs = uiDirs(configDir);
|
|
17
17
|
lines.push(`import { conventionConfig } from ${JSON.stringify(conventionModule)}`, `import * as index from ${JSON.stringify(index)}`, ...dirs.map((dir, i) => `import * as ui${i} from ${JSON.stringify(path.join(configDir, dir, "ui.ts"))}`), `const convention = conventionConfig(index, [${dirs.map((dir, i) => `[${JSON.stringify(dir)}, ui${i}]`).join(", ")}])`);
|
|
18
18
|
} else lines.push("const convention = undefined");
|
|
19
|
-
|
|
19
|
+
const configs = extensions.flatMap((extension) => extension.config ? [extension.config] : []);
|
|
20
|
+
lines.push(`import { mergeConfig } from '@protobase/server'`, ...configs.map((file, i) => `import * as extension${i} from ${JSON.stringify(file)}`), "const named = (config, file) => ({ ...config, name: config.name ?? file })", `export default mergeConfig(${[...configs.map((file, i) => `named(extension${i}.default ?? {}, ${JSON.stringify(file)})`), `named({ ...project, config: project.config ?? convention }, ${JSON.stringify(hasCustom ? "protobase.config.ts" : "config/index.ts")})`].join(", ")})`);
|
|
20
21
|
return `${lines.join("\n")}\n`;
|
|
21
22
|
};
|
|
22
23
|
//#endregion
|
package/dist/build/ui-bundle.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { adminAppDir } from "../project/admin-app.js";
|
|
2
|
+
import { extensionImports, extensionStyles } from "../project/extensions.js";
|
|
2
3
|
import { projectUiPlugin } from "../project/project-ui.js";
|
|
3
4
|
import { build } from "vite";
|
|
4
5
|
import tailwindcss from "@tailwindcss/vite";
|
|
5
6
|
import react from "@vitejs/plugin-react";
|
|
6
7
|
//#region src/build/ui-bundle.ts
|
|
7
|
-
const buildUiBundle = async ({ outDir, projectDir }) => {
|
|
8
|
+
const buildUiBundle = async ({ outDir, projectDir, extensions = [] }) => {
|
|
8
9
|
const nodeEnv = process.env.NODE_ENV;
|
|
9
10
|
process.env.NODE_ENV = "production";
|
|
10
11
|
await build({
|
|
@@ -15,7 +16,9 @@ const buildUiBundle = async ({ outDir, projectDir }) => {
|
|
|
15
16
|
publicDir: false,
|
|
16
17
|
logLevel: "warn",
|
|
17
18
|
plugins: [
|
|
18
|
-
projectUiPlugin(projectDir),
|
|
19
|
+
projectUiPlugin(projectDir, extensions),
|
|
20
|
+
...projectDir === void 0 ? [] : [extensionImports(projectDir, extensions)],
|
|
21
|
+
extensionStyles(extensions),
|
|
19
22
|
react(),
|
|
20
23
|
tailwindcss()
|
|
21
24
|
],
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { extensionImports } from "../project/extensions.js";
|
|
2
|
+
import { viteBundle } from "./vite-bundle.js";
|
|
3
|
+
import { mergedUiFile, projectUiPlugin } from "../project/project-ui.js";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
//#region src/build/ui-check.ts
|
|
9
|
+
const checkUiMerge = async (projectDir, extensions) => {
|
|
10
|
+
const scratch = await mkdtemp(path.join(tmpdir(), "protobase-ui-check-"));
|
|
11
|
+
const outFile = path.join(scratch, "ui-check.js");
|
|
12
|
+
try {
|
|
13
|
+
const plugins = [projectUiPlugin(projectDir, extensions), extensionImports(projectDir, extensions)];
|
|
14
|
+
await viteBundle({
|
|
15
|
+
root: projectDir,
|
|
16
|
+
entry: mergedUiFile(projectDir),
|
|
17
|
+
outFile,
|
|
18
|
+
plugins,
|
|
19
|
+
logLevel: "error"
|
|
20
|
+
});
|
|
21
|
+
await import(pathToFileURL(outFile).href);
|
|
22
|
+
} finally {
|
|
23
|
+
await rm(scratch, {
|
|
24
|
+
recursive: true,
|
|
25
|
+
force: true
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
30
|
+
export { checkUiMerge };
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { bunBuild } from "./bun-build.js";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { build } from "vite";
|
|
3
4
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
5
|
import { tmpdir } from "node:os";
|
|
5
|
-
import { build } from "vite";
|
|
6
6
|
//#region src/build/vite-bundle.ts
|
|
7
|
-
const viteBuild = async ({ root, entry, outFile, plugins }) => {
|
|
7
|
+
const viteBuild = async ({ root, entry, outFile, plugins, logLevel = "warn" }) => {
|
|
8
8
|
await build({
|
|
9
9
|
configFile: false,
|
|
10
10
|
root,
|
|
11
11
|
publicDir: false,
|
|
12
|
-
logLevel
|
|
12
|
+
logLevel,
|
|
13
13
|
plugins,
|
|
14
14
|
oxc: { jsx: { development: false } },
|
|
15
15
|
ssr: {
|
package/dist/dev/entry.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { loadProject, projectDb } from "./project.js";
|
|
2
|
+
import { checkAuthSchema, configExports, createAdmin, hasUsers } from "@protobase/server";
|
|
2
3
|
import { Hono } from "hono";
|
|
3
|
-
import { configExports, createAdmin, hasUsers } from "@protobase/server";
|
|
4
4
|
//#region src/dev/entry.ts
|
|
5
5
|
const projectDir = process.env.PROTOBASE_PROJECT;
|
|
6
|
-
const project = await loadProject(projectDir);
|
|
6
|
+
const project = await loadProject(projectDir, JSON.parse(process.env.PROTOBASE_EXTENSIONS ?? "[]"));
|
|
7
7
|
const db = projectDb(project, projectDir);
|
|
8
8
|
const { resources, views, pages, userMenu } = configExports(project.exports);
|
|
9
9
|
if (!project.authenticate) throw new Error("No authenticator: export `authenticate` (and `auth`) from protobase.config.ts; see https://docs.protobase.net/reference/auth/");
|
|
10
|
+
const store = globalThis;
|
|
11
|
+
if (project.auth && !store.__protobaseAuthSchemaChecked) {
|
|
12
|
+
await checkAuthSchema(project.auth);
|
|
13
|
+
store.__protobaseAuthSchemaChecked = true;
|
|
14
|
+
}
|
|
10
15
|
const admin = createAdmin({
|
|
11
16
|
resources,
|
|
12
17
|
views,
|
|
@@ -21,7 +26,6 @@ const admin = createAdmin({
|
|
|
21
26
|
}
|
|
22
27
|
});
|
|
23
28
|
var entry_default = new Hono().route("/", admin);
|
|
24
|
-
const store = globalThis;
|
|
25
29
|
if (project.auth && !store.__protobaseNoUsersShown) {
|
|
26
30
|
store.__protobaseNoUsersShown = true;
|
|
27
31
|
hasUsers(project.auth).then((any) => {
|
package/dist/dev/project.js
CHANGED
|
@@ -3,6 +3,7 @@ import { sharedDb } from "./create-db.js";
|
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { readdir } from "node:fs/promises";
|
|
6
|
+
import { mergeConfig } from "@protobase/server";
|
|
6
7
|
//#region src/dev/project.ts
|
|
7
8
|
const load = (file) => import(
|
|
8
9
|
/* @vite-ignore */
|
|
@@ -21,12 +22,23 @@ const conventionExports = async (projectDir) => {
|
|
|
21
22
|
}
|
|
22
23
|
return conventionConfig(indexExports, uiModules);
|
|
23
24
|
};
|
|
24
|
-
const
|
|
25
|
+
const named = (config, file) => ({
|
|
26
|
+
...config,
|
|
27
|
+
name: config.name ?? file
|
|
28
|
+
});
|
|
29
|
+
const loadProject = async (projectDir, extensions = []) => {
|
|
25
30
|
const file = path.join(projectDir, "protobase.config.ts");
|
|
26
31
|
const custom = existsSync(file) ? (await load(file)).default ?? {} : {};
|
|
27
|
-
|
|
32
|
+
const extended = [];
|
|
33
|
+
for (const extension of extensions) if (extension.config) extended.push(named((await load(extension.config)).default ?? {}, extension.config));
|
|
34
|
+
const own = named({
|
|
28
35
|
...custom,
|
|
29
|
-
|
|
36
|
+
config: custom.config ?? await conventionExports(projectDir)
|
|
37
|
+
}, existsSync(file) ? "protobase.config.ts" : "config/index.ts");
|
|
38
|
+
const project = mergeConfig(...extended, own);
|
|
39
|
+
return {
|
|
40
|
+
...project,
|
|
41
|
+
exports: project.config ?? {}
|
|
30
42
|
};
|
|
31
43
|
};
|
|
32
44
|
const projectDb = (project, projectDir) => {
|
package/dist/dev/run.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { loadNearestEnvFile } from "./env.js";
|
|
2
|
+
import { adminAppDir, adminAppMain, uiPackageDir } from "../project/admin-app.js";
|
|
3
|
+
import { extensionImports, extensionStyles } from "../project/extensions.js";
|
|
1
4
|
import { moduleFile } from "../module-file.js";
|
|
2
5
|
import { layoutCheck } from "../build/layout-check.js";
|
|
3
|
-
import { adminAppDir, adminAppMain, uiPackageDir } from "../project/admin-app.js";
|
|
4
6
|
import { projectUiPlugin } from "../project/project-ui.js";
|
|
5
|
-
import { loadNearestEnvFile } from "./env.js";
|
|
6
7
|
import { checkDatabase } from "./check-database.js";
|
|
7
8
|
import { existsSync } from "node:fs";
|
|
8
9
|
import path from "node:path";
|
|
@@ -16,10 +17,10 @@ const requireAppFiles = (appDir) => {
|
|
|
16
17
|
const missing = ["index.html", adminAppMain].filter((file) => !existsSync(path.join(appDir, file)));
|
|
17
18
|
if (missing.length > 0) throw new Error(`The admin app is missing ${missing.join(", ")} in ${appDir}; \`protobase dev\` serves the app from there`);
|
|
18
19
|
};
|
|
19
|
-
const reloadOnProjectChange = (
|
|
20
|
+
const reloadOnProjectChange = (dirs) => ({
|
|
20
21
|
name: "protobase-project-reload",
|
|
21
22
|
handleHotUpdate: ({ file, server }) => {
|
|
22
|
-
if (file.startsWith(`${
|
|
23
|
+
if (dirs.some((dir) => file.startsWith(`${dir}${path.sep}`)) && !file.includes(`${path.sep}node_modules${path.sep}`)) server.environments.ssr.moduleGraph.invalidateAll();
|
|
23
24
|
}
|
|
24
25
|
});
|
|
25
26
|
const appDependencies = [
|
|
@@ -42,7 +43,9 @@ const runDev = async (options, out) => {
|
|
|
42
43
|
const appDir = options.appDir ?? adminAppDir;
|
|
43
44
|
requireAppFiles(appDir);
|
|
44
45
|
await checkDatabase(url, options.projectDir);
|
|
46
|
+
const extensions = options.extensions ?? [];
|
|
45
47
|
process.env.PROTOBASE_PROJECT = options.projectDir;
|
|
48
|
+
process.env.PROTOBASE_EXTENSIONS = JSON.stringify(extensions);
|
|
46
49
|
process.env.PROTOBASE_DATABASE_URL = url;
|
|
47
50
|
const server = await createServer({
|
|
48
51
|
root: appDir,
|
|
@@ -56,12 +59,15 @@ const runDev = async (options, out) => {
|
|
|
56
59
|
fs: { allow: [
|
|
57
60
|
searchForWorkspaceRoot(options.projectDir),
|
|
58
61
|
uiPackageDir,
|
|
59
|
-
options.projectDir
|
|
62
|
+
options.projectDir,
|
|
63
|
+
...extensions.map((extension) => extension.dir)
|
|
60
64
|
] }
|
|
61
65
|
},
|
|
62
66
|
plugins: [
|
|
63
|
-
reloadOnProjectChange(options.projectDir),
|
|
64
|
-
projectUiPlugin(options.projectDir),
|
|
67
|
+
reloadOnProjectChange([options.projectDir, ...extensions.map((extension) => extension.dir)]),
|
|
68
|
+
projectUiPlugin(options.projectDir, extensions),
|
|
69
|
+
extensionImports(options.projectDir, extensions),
|
|
70
|
+
extensionStyles(extensions),
|
|
65
71
|
layoutCheck(),
|
|
66
72
|
react(),
|
|
67
73
|
tailwindcss(),
|
package/dist/main.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { loadNearestEnvFile } from "./dev/env.js";
|
|
2
|
+
import { registerAuth } from "./auth/register.js";
|
|
1
3
|
import { manifestFile } from "./bundle/manifest.js";
|
|
4
|
+
import { resolveExtensions } from "./project/extensions.js";
|
|
2
5
|
import { buildDeployBundle } from "./build/deploy-bundle.js";
|
|
3
6
|
import { buildServeRuntime } from "./build/serve-runtime.js";
|
|
4
7
|
import { resolveConnectionUrl } from "./db/connect.js";
|
|
5
8
|
import { parseHosts } from "./dev/hosts.js";
|
|
6
|
-
import { loadNearestEnvFile } from "./dev/env.js";
|
|
7
9
|
import { runDev } from "./dev/run.js";
|
|
8
10
|
import { runDoctor } from "./doctor/run.js";
|
|
9
11
|
import { runScaffold } from "./scaffold/run.js";
|
|
@@ -46,8 +48,9 @@ program.command("doctor").description("Check a config folder against the databas
|
|
|
46
48
|
}, out);
|
|
47
49
|
if (errors > 0) process.exitCode = 1;
|
|
48
50
|
});
|
|
49
|
-
program.command("dev").description("Serve the admin app and API for the project in the current directory").option("--port <port>", "port for the app and API", "5173").option("--env <variable>", "environment variable holding the database URL", "DATABASE_URL").option("--allowed-hosts <hosts>", "extra Host headers to accept, comma separated; a leading dot matches subdomains (.trycloudflare.com)").option("--cache-dir <dir>", "Vite's dependency cache, so parallel dev servers do not share one").action(async (opts) => {
|
|
51
|
+
program.command("dev").description("Serve the admin app and API for the project in the current directory").option("--port <port>", "port for the app and API", "5173").option("--env <variable>", "environment variable holding the database URL", "DATABASE_URL").option("--allowed-hosts <hosts>", "extra Host headers to accept, comma separated; a leading dot matches subdomains (.trycloudflare.com)").option("--cache-dir <dir>", "Vite's dependency cache, so parallel dev servers do not share one").option("--extend <dir>", "merge the protobase.config.ts and protobase.ui.tsx in this folder before the project's, repeatable", collect, []).action(async (opts) => {
|
|
50
52
|
const server = await runDev({
|
|
53
|
+
extensions: resolveExtensions(opts.extend),
|
|
51
54
|
port: Number(opts.port),
|
|
52
55
|
envVar: opts.env,
|
|
53
56
|
projectDir: process.cwd(),
|
|
@@ -57,11 +60,13 @@ program.command("dev").description("Serve the admin app and API for the project
|
|
|
57
60
|
process.once("SIGINT", () => void server.close().then(() => process.exit(0)));
|
|
58
61
|
process.once("SIGTERM", () => void server.close().then(() => process.exit(0)));
|
|
59
62
|
});
|
|
60
|
-
program.command("build").description("Build the deploy bundle of the project in the current directory: its config module, admin UI and manifest").option("--out <dir>", "output folder", "dist").option("--bun", "have Bun's bundler write the config module, for a bundle only Bun serves (needs bun on PATH)", false).action(async (opts) => {
|
|
63
|
+
program.command("build").description("Build the deploy bundle of the project in the current directory: its config module, admin UI and manifest").option("--out <dir>", "output folder", "dist").option("--bun", "have Bun's bundler write the config module, for a bundle only Bun serves (needs bun on PATH)", false).option("--extend <dir>", "merge the protobase.config.ts and protobase.ui.tsx in this folder before the project's, repeatable", collect, []).action(async (opts) => {
|
|
64
|
+
const extensions = resolveExtensions(opts.extend);
|
|
61
65
|
const manifest = await buildDeployBundle({
|
|
62
66
|
projectDir: process.cwd(),
|
|
63
67
|
outDir: path.resolve(opts.out),
|
|
64
|
-
bun: opts.bun
|
|
68
|
+
bun: opts.bun,
|
|
69
|
+
extensions
|
|
65
70
|
});
|
|
66
71
|
const packages = manifest.nodeModules ? `, ${manifest.nodeModules}/` : "";
|
|
67
72
|
out(`built ${opts.out}: ${manifest.server}${packages}, ${manifest.public}/ and ${manifestFile} (api ${manifest.api.join(", ")})\n`);
|
|
@@ -80,6 +85,7 @@ program.command("serve").description("Serve a bundle written by `protobase build
|
|
|
80
85
|
});
|
|
81
86
|
registerUsers(program);
|
|
82
87
|
registerToken(program);
|
|
88
|
+
registerAuth(program);
|
|
83
89
|
const run = (argv) => program.parseAsync(argv).catch((error) => {
|
|
84
90
|
process.stderr.write(`error: ${error.message}\n`);
|
|
85
91
|
process.exitCode = 1;
|
package/dist/package.js
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { adminAppDir } from "./admin-app.js";
|
|
2
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
//#region src/project/extensions.ts
|
|
5
|
+
const extensionConfigFile = "protobase.config.ts";
|
|
6
|
+
const extensionUiFile = "protobase.ui.tsx";
|
|
7
|
+
const resolveExtensions = (dirs) => dirs.map((given) => {
|
|
8
|
+
const resolved = path.resolve(given);
|
|
9
|
+
if (!existsSync(resolved)) throw new Error(`--extend ${given}: there is no folder ${resolved}`);
|
|
10
|
+
const dir = realpathSync(resolved);
|
|
11
|
+
const config = path.join(dir, extensionConfigFile);
|
|
12
|
+
const ui = path.join(dir, extensionUiFile);
|
|
13
|
+
const extension = {
|
|
14
|
+
dir,
|
|
15
|
+
...existsSync(config) && { config },
|
|
16
|
+
...existsSync(ui) && { ui }
|
|
17
|
+
};
|
|
18
|
+
if (!extension.config && !extension.ui) throw new Error(`--extend ${given}: ${dir} has neither ${extensionConfigFile} nor ${extensionUiFile}`);
|
|
19
|
+
return extension;
|
|
20
|
+
});
|
|
21
|
+
const shared = /^(@protobase\/[^/]+|react|react-dom)(\/.*)?$/;
|
|
22
|
+
const inside = (file, dir) => file === dir || file.startsWith(`${dir}${path.sep}`);
|
|
23
|
+
const extensionImports = (projectDir, extensions) => ({
|
|
24
|
+
name: "protobase-extension-imports",
|
|
25
|
+
enforce: "pre",
|
|
26
|
+
async resolveId(id, importer, options) {
|
|
27
|
+
if (!importer || !shared.test(id)) return void 0;
|
|
28
|
+
const from = importer.replace(/^\0/, "").replace(/\?.*$/, "");
|
|
29
|
+
if (!extensions.some((extension) => inside(from, extension.dir))) return void 0;
|
|
30
|
+
return this.resolve(id, path.join(projectDir, extensionConfigFile), {
|
|
31
|
+
...options,
|
|
32
|
+
skipSelf: true
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
const appStyles = path.join(path.dirname(adminAppDir), "styles.css");
|
|
37
|
+
const extensionStyles = (extensions) => ({
|
|
38
|
+
name: "protobase-extension-styles",
|
|
39
|
+
enforce: "pre",
|
|
40
|
+
transform: (code, id) => {
|
|
41
|
+
if (extensions.length === 0 || id.replace(/\?.*$/, "") !== appStyles) return void 0;
|
|
42
|
+
return `${code}\n${extensions.map((extension) => `@source ${JSON.stringify(extension.dir)};`).join("\n")}\n`;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
//#endregion
|
|
46
|
+
export { extensionConfigFile, extensionImports, extensionStyles, extensionUiFile, resolveExtensions };
|
|
@@ -4,14 +4,35 @@ import path from "node:path";
|
|
|
4
4
|
const projectUiId = "virtual:protobase-project-ui";
|
|
5
5
|
const empty = `\0${projectUiId}`;
|
|
6
6
|
const projectUiFile = (projectDir) => path.join(projectDir, "protobase.ui.tsx");
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
7
|
+
const mergedUiFile = (projectDir) => path.join(projectDir, "protobase-ui-entry.js");
|
|
8
|
+
const mergedUiCode = (projectDir, extensions) => {
|
|
9
|
+
const files = extensions.flatMap((extension) => extension.ui ? [extension.ui] : []);
|
|
10
|
+
const own = projectUiFile(projectDir);
|
|
11
|
+
const hasOwn = existsSync(own);
|
|
12
|
+
return [
|
|
13
|
+
`import { mergeUi } from '@protobase/ui'`,
|
|
14
|
+
...files.map((file, i) => `import * as extension${i} from ${JSON.stringify(file)}`),
|
|
15
|
+
...hasOwn ? [`import * as project from ${JSON.stringify(own)}`] : [],
|
|
16
|
+
"const named = (ui, file) => ({ ...ui, name: ui.name ?? file })",
|
|
17
|
+
`export default mergeUi(${[...files.map((file, i) => `named(extension${i}.default ?? {}, ${JSON.stringify(file)})`), ...hasOwn ? [`named(project.default ?? {}, "protobase.ui.tsx")`] : []].join(", ")})`,
|
|
18
|
+
""
|
|
19
|
+
].join("\n");
|
|
20
|
+
};
|
|
21
|
+
const projectUiPlugin = (projectDir, extensions = []) => {
|
|
22
|
+
const merged = projectDir !== void 0 && extensions.some((extension) => extension.ui) ? `\0${mergedUiFile(projectDir)}` : void 0;
|
|
23
|
+
return {
|
|
24
|
+
name: "protobase-project-ui",
|
|
25
|
+
resolveId: (id) => {
|
|
26
|
+
if (merged && (id === "virtual:protobase-project-ui" || id === mergedUiFile(projectDir))) return merged;
|
|
27
|
+
if (id !== "virtual:protobase-project-ui") return void 0;
|
|
28
|
+
const file = projectDir === void 0 ? void 0 : projectUiFile(projectDir);
|
|
29
|
+
return file && existsSync(file) ? file : empty;
|
|
30
|
+
},
|
|
31
|
+
load: (id) => {
|
|
32
|
+
if (id === empty) return "export default {}\n";
|
|
33
|
+
if (id === merged) return mergedUiCode(projectDir, extensions);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
};
|
|
16
37
|
//#endregion
|
|
17
|
-
export { projectUiFile, projectUiId, projectUiPlugin };
|
|
38
|
+
export { mergedUiCode, mergedUiFile, projectUiFile, projectUiId, projectUiPlugin };
|
package/dist/serve/app.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { isApiPath } from "../bundle/manifest.js";
|
|
2
2
|
import { serveStaticSite } from "./static-site.js";
|
|
3
|
+
import { configExports, createAdmin } from "@protobase/server";
|
|
3
4
|
import { Hono } from "hono";
|
|
4
5
|
import { logger } from "hono/logger";
|
|
5
|
-
import { configExports, createAdmin } from "@protobase/server";
|
|
6
6
|
//#region src/serve/app.ts
|
|
7
7
|
const createServeApp = ({ project, db, requestLog, site }) => {
|
|
8
8
|
const { resources, views, pages, userMenu } = configExports(project.config);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import * as server from "@protobase/server";
|
|
1
2
|
import * as postgres$1 from "postgres";
|
|
2
3
|
import * as schema from "@protobase/schema";
|
|
3
4
|
import * as nodeServer from "@hono/node-server";
|
|
4
5
|
import * as pg$1 from "pg";
|
|
5
6
|
import * as kysely from "kysely";
|
|
6
7
|
import * as hono from "hono";
|
|
7
|
-
import * as server from "@protobase/server";
|
|
8
8
|
import * as apiReference from "@scalar/hono-api-reference";
|
|
9
9
|
import * as aipParsers from "aip-parsers";
|
|
10
10
|
import * as betterAuth from "better-auth";
|
package/dist/serve/lifecycle.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createPgDb } from "../project/pg-db.js";
|
|
2
2
|
import { createServeApp } from "./app.js";
|
|
3
|
+
import { checkAuthSchema } from "@protobase/server";
|
|
3
4
|
import { createAdaptorServer } from "@hono/node-server";
|
|
4
5
|
import pg from "pg";
|
|
5
6
|
//#region src/serve/lifecycle.ts
|
|
@@ -13,6 +14,7 @@ const listen = (server, port) => new Promise((resolve, reject) => {
|
|
|
13
14
|
const closeServer = (server) => new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
14
15
|
const startServe = async ({ project, env, createDb = (url) => createPgDb(pg, url), site }) => {
|
|
15
16
|
if (!project.db && !env.databaseUrl) throw new Error("No database: set DATABASE_URL or export `db` from protobase.config.ts");
|
|
17
|
+
if (project.auth) await checkAuthSchema(project.auth);
|
|
16
18
|
const ownDb = project.db ? void 0 : createDb(env.databaseUrl);
|
|
17
19
|
const db = project.db ?? ownDb;
|
|
18
20
|
const app = createServeApp({
|
package/dist/users/commands.js
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { resolvePassword } from "./password.js";
|
|
2
2
|
//#region src/users/commands.ts
|
|
3
|
+
const githubId = /^[1-9][0-9]*$/;
|
|
3
4
|
const createUserCommand = async (api, options, deps, out) => {
|
|
4
5
|
if (!options.email.includes("@")) throw new Error(`"${options.email}" is not an email address`);
|
|
6
|
+
if (options.githubId !== void 0 && !githubId.test(options.githubId)) throw new Error(`"${options.githubId}" is not a GitHub user id, a number such as 583231`);
|
|
5
7
|
const { password, generated } = await resolvePassword(options, deps);
|
|
6
8
|
const role = !await api.hasUsers() ? "admin" : options.role;
|
|
7
9
|
const user = await api.createUser({
|
|
8
10
|
email: options.email,
|
|
9
11
|
password,
|
|
10
12
|
...role && { role },
|
|
11
|
-
...options.name && { name: options.name }
|
|
13
|
+
...options.name && { name: options.name },
|
|
14
|
+
...options.githubId && { accounts: [{
|
|
15
|
+
providerId: "github",
|
|
16
|
+
accountId: options.githubId
|
|
17
|
+
}] }
|
|
12
18
|
});
|
|
13
|
-
out(`Created ${user.role} ${user.email}\n`);
|
|
19
|
+
out(`Created ${user.role} ${user.email}${options.githubId ? `, linked to GitHub user ${options.githubId}` : ""}\n`);
|
|
14
20
|
if (generated) out(`Password (shown once): ${password}\n`);
|
|
15
21
|
};
|
|
16
22
|
const listUsersCommand = async (api, out) => {
|
package/dist/users/register.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { loadProjectAuth } from "./load-auth.js";
|
|
1
2
|
import { readHidden, rejectPasswordArgument } from "./password.js";
|
|
2
3
|
import { createUserCommand, listRolesCommand, listUsersCommand } from "./commands.js";
|
|
3
4
|
import { deleteUserCommand, setDisabledCommand, setRoleCommand } from "./manage.js";
|
|
4
|
-
import { loadProjectAuth } from "./load-auth.js";
|
|
5
5
|
import { parseTtl } from "./ttl.js";
|
|
6
6
|
import { Option } from "commander";
|
|
7
|
-
import { createInterface } from "node:readline/promises";
|
|
8
7
|
import { createUser, deleteUser, hasUsers, issueToken, listUsers, roleChoices, setUserBanned, setUserRole } from "@protobase/server";
|
|
8
|
+
import { createInterface } from "node:readline/promises";
|
|
9
9
|
//#region src/users/register.ts
|
|
10
10
|
const out = (text) => process.stdout.write(text);
|
|
11
11
|
const usersApi = (auth) => ({
|
|
@@ -34,14 +34,15 @@ const ask = async (question) => {
|
|
|
34
34
|
const finish = (code) => process.exit(code);
|
|
35
35
|
const registerUsers = (program) => {
|
|
36
36
|
const users = program.command("users").description("Manage the admin users of the project in the current directory");
|
|
37
|
-
users.command("create").description("Create a user; the first user is always an admin").argument("<email>").option("--name <name>", "display name").option("--role <role>", "role for users after the first (default: the project's default role; see `users roles`)").option("--password-stdin", "read the password from stdin", false).option("--generate-password", "generate a strong password and print it once", false).addOption(new Option("--password <value>").hideHelp()).action(async (email, opts) => {
|
|
37
|
+
users.command("create").description("Create a user; the first user is always an admin").argument("<email>").option("--name <name>", "display name").option("--role <role>", "role for users after the first (default: the project's default role; see `users roles`)").option("--password-stdin", "read the password from stdin", false).option("--generate-password", "generate a strong password and print it once", false).option("--github-id <id>", "link the user to this GitHub user id, so signing in with GitHub finds them whatever their address").addOption(new Option("--password <value>").hideHelp()).action(async (email, opts) => {
|
|
38
38
|
rejectPasswordArgument(opts.password);
|
|
39
39
|
await createUserCommand(await projectApi(), {
|
|
40
40
|
email,
|
|
41
41
|
name: opts.name,
|
|
42
42
|
role: opts.role,
|
|
43
43
|
passwordStdin: opts.passwordStdin,
|
|
44
|
-
generatePassword: opts.generatePassword
|
|
44
|
+
generatePassword: opts.generatePassword,
|
|
45
|
+
githubId: opts.githubId
|
|
45
46
|
}, {
|
|
46
47
|
stdin: process.stdin,
|
|
47
48
|
prompt: readHidden
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "The protobase command: scaffold, doctor, dev, build and serve a Protobase project.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -34,11 +34,11 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@hono/node-server": "^2.1.3",
|
|
36
36
|
"@hono/vite-dev-server": "^0.26.1",
|
|
37
|
-
"@protobase/layout": "0.
|
|
38
|
-
"@protobase/query": "0.
|
|
39
|
-
"@protobase/schema": "0.
|
|
40
|
-
"@protobase/server": "0.
|
|
41
|
-
"@protobase/ui": "0.
|
|
37
|
+
"@protobase/layout": "0.6.0",
|
|
38
|
+
"@protobase/query": "0.6.0",
|
|
39
|
+
"@protobase/schema": "0.6.0",
|
|
40
|
+
"@protobase/server": "0.6.0",
|
|
41
|
+
"@protobase/ui": "0.6.0",
|
|
42
42
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
43
43
|
"@tailwindcss/vite": "^4.3.3",
|
|
44
44
|
"@vitejs/plugin-react": "^6.1.2",
|