@protobase/cli 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/LICENSE +202 -0
  2. package/bin/protobase.mjs +4 -0
  3. package/dist/build/config-bundle.js +70 -0
  4. package/dist/build/deploy-bundle.js +42 -0
  5. package/dist/build/entry-code.js +23 -0
  6. package/dist/build/layout-check.js +50 -0
  7. package/dist/build/packages/binary-platform.js +81 -0
  8. package/dist/build/packages/copy-packages.js +59 -0
  9. package/dist/build/packages/host-packages.js +37 -0
  10. package/dist/build/packages/native-package.js +22 -0
  11. package/dist/build/packages/package-dir.js +32 -0
  12. package/dist/build/packages/package-tree.js +53 -0
  13. package/dist/build/packages/targets.js +38 -0
  14. package/dist/build/serve-runtime.js +16 -0
  15. package/dist/build/ui-bundle.js +33 -0
  16. package/dist/build/vite-bundle.js +36 -0
  17. package/dist/bundle/manifest.js +35 -0
  18. package/dist/db/connect.js +18 -0
  19. package/dist/db/introspect.js +47 -0
  20. package/dist/db/keys.js +15 -0
  21. package/dist/db/model.js +4 -0
  22. package/dist/db/queries.js +62 -0
  23. package/dist/dev/check-database.js +38 -0
  24. package/dist/dev/create-db.js +13 -0
  25. package/dist/dev/entry.d.ts +4 -0
  26. package/dist/dev/entry.js +32 -0
  27. package/dist/dev/env.js +15 -0
  28. package/dist/dev/hosts.js +4 -0
  29. package/dist/dev/project.js +39 -0
  30. package/dist/dev/run.js +88 -0
  31. package/dist/doctor/check.js +76 -0
  32. package/dist/doctor/load-config.js +10 -0
  33. package/dist/doctor/run.js +22 -0
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.js +2 -0
  36. package/dist/infer/check-values.js +14 -0
  37. package/dist/infer/compat.js +17 -0
  38. package/dist/infer/default-column.js +12 -0
  39. package/dist/infer/defaults.js +28 -0
  40. package/dist/infer/names.js +28 -0
  41. package/dist/infer/proposal.js +68 -0
  42. package/dist/infer/render-field.js +26 -0
  43. package/dist/infer/tenant.js +5 -0
  44. package/dist/infer/type-map.js +52 -0
  45. package/dist/main.d.ts +4 -0
  46. package/dist/main.js +84 -0
  47. package/dist/module-file.js +6 -0
  48. package/dist/package.js +4 -0
  49. package/dist/project/admin-app.js +9 -0
  50. package/dist/project/convention.js +8 -0
  51. package/dist/project/import-project-file.js +6 -0
  52. package/dist/project/pg-db.js +11 -0
  53. package/dist/project/project-ui.js +17 -0
  54. package/dist/scaffold/data-file.js +44 -0
  55. package/dist/scaffold/drift.js +30 -0
  56. package/dist/scaffold/existing-config.js +95 -0
  57. package/dist/scaffold/index-file.js +18 -0
  58. package/dist/scaffold/plan.js +128 -0
  59. package/dist/scaffold/project.js +13 -0
  60. package/dist/scaffold/prompt.js +70 -0
  61. package/dist/scaffold/resource-names.js +19 -0
  62. package/dist/scaffold/resource-plan.js +33 -0
  63. package/dist/scaffold/run.js +53 -0
  64. package/dist/scaffold/targets.js +15 -0
  65. package/dist/scaffold/ui-fields.js +30 -0
  66. package/dist/scaffold/ui-file.js +40 -0
  67. package/dist/serve/app.js +30 -0
  68. package/dist/serve/bundle-version.js +16 -0
  69. package/dist/serve/env.js +13 -0
  70. package/dist/serve/host-module-ids.js +21 -0
  71. package/dist/serve/host-modules.js +49 -0
  72. package/dist/serve/lifecycle.js +39 -0
  73. package/dist/serve/load-bundle.js +17 -0
  74. package/dist/serve/main.d.ts +1 -0
  75. package/dist/serve/main.js +13 -0
  76. package/dist/serve/serve-bundle.js +21 -0
  77. package/dist/serve/serve-dir.js +15 -0
  78. package/dist/serve/signals.js +11 -0
  79. package/dist/serve/static-site.js +46 -0
  80. package/dist/users/commands.js +31 -0
  81. package/dist/users/load-auth.js +15 -0
  82. package/dist/users/manage.js +21 -0
  83. package/dist/users/password.js +64 -0
  84. package/dist/users/register.js +94 -0
  85. package/dist/users/ttl.js +16 -0
  86. package/dist/version/compatibility.js +31 -0
  87. package/dist/version/version.js +5 -0
  88. package/package.json +55 -3
  89. package/README.md +0 -3
@@ -0,0 +1,53 @@
1
+ import { findPackageDir, readPackageJson } from "./package-dir.js";
2
+ import { isPlatformPackage, packageTargets } from "./targets.js";
3
+ import { realpath } from "node:fs/promises";
4
+ //#region src/build/packages/package-tree.ts
5
+ const lookupPath = (dest, name) => {
6
+ const parts = dest.split("/node_modules/");
7
+ return [...parts.map((_, i) => [...parts.slice(0, parts.length - i), name].join("/node_modules/")), name];
8
+ };
9
+ const runsOnTarget = (json) => !isPlatformPackage(json) || packageTargets(json).length > 0;
10
+ const packageTree = async (imports) => {
11
+ const placed = /* @__PURE__ */ new Map();
12
+ const tree = [];
13
+ const place = (pkg) => {
14
+ placed.set(pkg.dest, pkg.dir);
15
+ tree.push(pkg);
16
+ };
17
+ for (const imported of imports) {
18
+ const { name } = imported;
19
+ const dir = await realpath(imported.dir);
20
+ const there = placed.get(name);
21
+ if (there !== void 0 && there !== dir) throw new Error(`The config imports two installs of ${name}: ${there} and ${dir}`);
22
+ const json = await readPackageJson(dir);
23
+ if (there === void 0 && runsOnTarget(json)) place({
24
+ name,
25
+ dir,
26
+ dest: name,
27
+ json
28
+ });
29
+ }
30
+ for (let i = 0; i < tree.length; i++) {
31
+ const parent = tree[i];
32
+ const optional = parent.json.optionalDependencies ?? {};
33
+ for (const name of /* @__PURE__ */ new Set([...Object.keys(parent.json.dependencies ?? {}), ...Object.keys(optional)])) {
34
+ const dir = await findPackageDir(name, parent.dir);
35
+ if (!dir && name in optional) continue;
36
+ if (!dir) throw new Error(`${name}, which ${parent.name} needs, is not installed`);
37
+ const json = await readPackageJson(dir);
38
+ if (!runsOnTarget(json)) continue;
39
+ const candidates = lookupPath(parent.dest, name);
40
+ const found = candidates.find((dest) => placed.has(dest));
41
+ if (found !== void 0 && placed.get(found) === dir) continue;
42
+ place({
43
+ name,
44
+ dir,
45
+ dest: found === void 0 ? name : candidates[0],
46
+ json
47
+ });
48
+ }
49
+ }
50
+ return tree;
51
+ };
52
+ //#endregion
53
+ export { packageTree };
@@ -0,0 +1,38 @@
1
+ //#region src/build/packages/targets.ts
2
+ const bundleTargets = ["linux-x64", "linux-arm64"];
3
+ const isTarget = (platform) => bundleTargets.includes(platform);
4
+ const runImageLibraries = /* @__PURE__ */ new Set([
5
+ "ld-linux-x86-64.so.2",
6
+ "ld-linux-aarch64.so.1",
7
+ "libc.so.6",
8
+ "libm.so.6",
9
+ "libmvec.so.1",
10
+ "libdl.so.2",
11
+ "libpthread.so.0",
12
+ "librt.so.1",
13
+ "libresolv.so.2",
14
+ "libutil.so.1",
15
+ "libanl.so.1",
16
+ "libnsl.so.1",
17
+ "libBrokenLocale.so.1",
18
+ "libthread_db.so.1",
19
+ "libc_malloc_debug.so.0",
20
+ "libnss_compat.so.2",
21
+ "libnss_dns.so.2",
22
+ "libnss_files.so.2",
23
+ "libnss_hesiod.so.2",
24
+ "libstdc++.so.6",
25
+ "libgcc_s.so.1"
26
+ ]);
27
+ const allows = (list, value) => {
28
+ if (!list?.length) return true;
29
+ if (list.includes(`!${value}`)) return false;
30
+ return list.includes(value) || list.every((item) => item.startsWith("!"));
31
+ };
32
+ const isPlatformPackage = (json) => Boolean(json.os?.length || json.cpu?.length || json.libc?.length);
33
+ const packageTargets = (json) => bundleTargets.filter((target) => {
34
+ const [os, cpu] = target.split("-");
35
+ return allows(json.os, os) && allows(json.cpu, cpu) && allows(json.libc, "glibc");
36
+ });
37
+ //#endregion
38
+ export { bundleTargets, isPlatformPackage, isTarget, packageTargets, runImageLibraries };
@@ -0,0 +1,16 @@
1
+ import { moduleFile } from "../module-file.js";
2
+ import { viteBundle } from "./vite-bundle.js";
3
+ import { fileURLToPath } from "node:url";
4
+ //#region src/build/serve-runtime.ts
5
+ const cliRoot = fileURLToPath(new URL("../..", import.meta.url));
6
+ const serveMain = moduleFile("../serve/main", import.meta.url);
7
+ const buildServeRuntime = async ({ outFile }) => {
8
+ await viteBundle({
9
+ root: cliRoot,
10
+ entry: serveMain,
11
+ outFile,
12
+ plugins: []
13
+ });
14
+ };
15
+ //#endregion
16
+ export { buildServeRuntime };
@@ -0,0 +1,33 @@
1
+ import { adminAppDir } from "../project/admin-app.js";
2
+ import { projectUiPlugin } from "../project/project-ui.js";
3
+ import { build } from "vite";
4
+ import tailwindcss from "@tailwindcss/vite";
5
+ import react from "@vitejs/plugin-react";
6
+ //#region src/build/ui-bundle.ts
7
+ const buildUiBundle = async ({ outDir, projectDir }) => {
8
+ const nodeEnv = process.env.NODE_ENV;
9
+ process.env.NODE_ENV = "production";
10
+ await build({
11
+ configFile: false,
12
+ root: adminAppDir,
13
+ base: "/",
14
+ mode: "production",
15
+ publicDir: false,
16
+ logLevel: "warn",
17
+ plugins: [
18
+ projectUiPlugin(projectDir),
19
+ react(),
20
+ tailwindcss()
21
+ ],
22
+ build: {
23
+ outDir,
24
+ emptyOutDir: true,
25
+ chunkSizeWarningLimit: 4096
26
+ }
27
+ }).finally(() => {
28
+ if (nodeEnv === void 0) delete process.env.NODE_ENV;
29
+ else process.env.NODE_ENV = nodeEnv;
30
+ });
31
+ };
32
+ //#endregion
33
+ export { buildUiBundle };
@@ -0,0 +1,36 @@
1
+ import path from "node:path";
2
+ import { build } from "vite";
3
+ //#region src/build/vite-bundle.ts
4
+ const viteBundle = async ({ root, entry, outFile, plugins }) => {
5
+ await build({
6
+ configFile: false,
7
+ root,
8
+ publicDir: false,
9
+ logLevel: "warn",
10
+ plugins,
11
+ oxc: { jsx: { development: false } },
12
+ ssr: {
13
+ noExternal: true,
14
+ target: "node"
15
+ },
16
+ build: {
17
+ ssr: entry,
18
+ outDir: path.dirname(outFile),
19
+ emptyOutDir: false,
20
+ copyPublicDir: false,
21
+ minify: false,
22
+ target: "esnext",
23
+ rolldownOptions: {
24
+ external: ["pg-native"],
25
+ output: {
26
+ entryFileNames: path.basename(outFile),
27
+ format: "es",
28
+ codeSplitting: false,
29
+ banner: "// @bun"
30
+ }
31
+ }
32
+ }
33
+ });
34
+ };
35
+ //#endregion
36
+ export { viteBundle };
@@ -0,0 +1,35 @@
1
+ import path from "node:path";
2
+ import { readFile } from "node:fs/promises";
3
+ //#region src/bundle/manifest.ts
4
+ const manifestFile = "protobase.bundle.json";
5
+ const isApiPath = (api, pathname) => api.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`));
6
+ const bundleBasePath = "/api/v1";
7
+ const bundleLayout = {
8
+ server: "protobase.config.js",
9
+ public: "public",
10
+ spa: "index.html",
11
+ api: ["/api"]
12
+ };
13
+ const bundleNodeModules = "node_modules";
14
+ const isStringList = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
15
+ const parseManifest = (value, file) => {
16
+ const manifest = value;
17
+ if (manifest?.version !== 2) throw new Error(`${file} is not a version 2 bundle manifest; rebuild it with \`protobase build\``);
18
+ const { server, public: publicDir, api, spa, nodeModules } = manifest;
19
+ if (typeof server !== "string" || typeof publicDir !== "string" || typeof spa !== "string" || !isStringList(api)) throw new Error(`${file} needs server, public and spa paths and an api list`);
20
+ if (nodeModules !== void 0 && typeof nodeModules !== "string") throw new Error(`${file} has a nodeModules that is no path`);
21
+ return {
22
+ version: 2,
23
+ server,
24
+ public: publicDir,
25
+ api,
26
+ spa,
27
+ ...nodeModules === void 0 ? {} : { nodeModules }
28
+ };
29
+ };
30
+ const readManifest = async (bundleDir) => {
31
+ const file = path.join(bundleDir, manifestFile);
32
+ return parseManifest(JSON.parse(await readFile(file, "utf8")), file);
33
+ };
34
+ //#endregion
35
+ export { bundleBasePath, bundleLayout, bundleNodeModules, isApiPath, manifestFile, parseManifest, readManifest };
@@ -0,0 +1,18 @@
1
+ import postgres from "postgres";
2
+ //#region src/db/connect.ts
3
+ const connect = (url, options = {}) => postgres(url, {
4
+ max: 1,
5
+ ...options.connectTimeout && { connect_timeout: options.connectTimeout },
6
+ onnotice: () => {},
7
+ connection: { default_transaction_read_only: true }
8
+ });
9
+ const resolveConnectionUrl = (arg, envName) => {
10
+ if (arg && envName) throw new Error("Pass either a connection string or --env, not both");
11
+ if (arg) return arg;
12
+ if (!envName) throw new Error("Pass a connection string or --env <VARIABLE>");
13
+ const value = process.env[envName];
14
+ if (!value) throw new Error(`Environment variable ${envName} is not set`);
15
+ return value;
16
+ };
17
+ //#endregion
18
+ export { connect, resolveConnectionUrl };
@@ -0,0 +1,47 @@
1
+ import { tableKey } from "./model.js";
2
+ import { selectChecks, selectColumns, selectForeignKeys, selectIndexes } from "./queries.js";
3
+ //#region src/db/introspect.ts
4
+ const byTable = (rows) => {
5
+ const groups = /* @__PURE__ */ new Map();
6
+ for (const row of rows) {
7
+ const key = tableKey({
8
+ schema: row.schema,
9
+ name: row.table
10
+ });
11
+ groups.set(key, [...groups.get(key) ?? [], row]);
12
+ }
13
+ return groups;
14
+ };
15
+ const introspect = async (sql) => {
16
+ const [columns, foreignKeys, checks, indexes] = await Promise.all([
17
+ selectColumns(sql),
18
+ selectForeignKeys(sql),
19
+ selectChecks(sql),
20
+ selectIndexes(sql)
21
+ ]);
22
+ const fks = byTable(foreignKeys);
23
+ const chks = byTable(checks);
24
+ const idxs = byTable(indexes);
25
+ return [...byTable(columns)].map(([key, rows]) => ({
26
+ schema: rows[0].schema,
27
+ name: rows[0].table,
28
+ columns: rows.map(({ schema, table, enumValues, defaultExpr, ...column }) => ({
29
+ ...column,
30
+ enumValues: enumValues ?? [],
31
+ ...defaultExpr !== null && { defaultExpr }
32
+ })),
33
+ foreignKeys: (fks.get(key) ?? []).map(({ columns, refSchema, refTable, refColumns }) => ({
34
+ columns,
35
+ refSchema,
36
+ refTable,
37
+ refColumns
38
+ })),
39
+ checks: (chks.get(key) ?? []).map(({ columns, definition }) => ({
40
+ columns,
41
+ definition
42
+ })),
43
+ indexes: (idxs.get(key) ?? []).map(({ schema, table, ...index }) => index)
44
+ }));
45
+ };
46
+ //#endregion
47
+ export { introspect };
@@ -0,0 +1,15 @@
1
+ //#region src/db/keys.ts
2
+ const plainColumns = (columns) => columns.every((column) => column !== null) ? columns : void 0;
3
+ const keyColumns = (table) => {
4
+ const primary = table.indexes.find((index) => index.primary);
5
+ if (primary) return plainColumns(primary.columns);
6
+ const notNull = new Set(table.columns.filter((column) => column.notNull).map((column) => column.name));
7
+ return table.indexes.filter((index) => index.unique && !index.partial).map((index) => plainColumns(index.columns)).find((columns) => columns && columns.every((column) => notNull.has(column)));
8
+ };
9
+ const isUniqueSet = (table, columns) => table.indexes.some((index) => {
10
+ const indexed = plainColumns(index.columns);
11
+ return index.unique && !index.partial && indexed && indexed.every((column) => columns.includes(column));
12
+ });
13
+ const leadingIndex = (table, column, tenantColumn) => table.indexes.find((index) => !index.partial && (index.columns[0] === column || tenantColumn !== void 0 && index.columns[0] === tenantColumn && index.columns[1] === column));
14
+ //#endregion
15
+ export { isUniqueSet, keyColumns, leadingIndex };
@@ -0,0 +1,4 @@
1
+ //#region src/db/model.ts
2
+ const tableKey = (table) => `${table.schema}.${table.name}`;
3
+ //#endregion
4
+ export { tableKey };
@@ -0,0 +1,62 @@
1
+ //#region src/db/queries.ts
2
+ const userTables = `
3
+ c.relkind in ('r', 'p')
4
+ and n.nspname not in ('pg_catalog', 'information_schema', 'pg_toast')
5
+ and n.nspname not like 'pg_temp%'
6
+ `;
7
+ const selectColumns = (sql) => sql`
8
+ select n.nspname as schema, c.relname as table, a.attname as name, a.attnum::int as position,
9
+ bt.typname as "typeName", format_type(a.atttypid, a.atttypmod) as formatted,
10
+ (bt.typtype = 'e') as "isEnum",
11
+ (select array_agg(e.enumlabel::text order by e.enumsortorder) from pg_enum e where e.enumtypid = bt.oid) as "enumValues",
12
+ a.attnotnull as "notNull", (a.attidentity <> '') as identity, (a.attidentity = 'a') as "identityAlways", (a.attgenerated <> '') as generated,
13
+ pg_get_expr(d.adbin, d.adrelid) as "defaultExpr"
14
+ from pg_attribute a
15
+ join pg_class c on c.oid = a.attrelid
16
+ join pg_namespace n on n.oid = c.relnamespace
17
+ join pg_type t on t.oid = a.atttypid
18
+ join pg_type bt on bt.oid = case when t.typtype = 'd' then t.typbasetype else t.oid end
19
+ left join pg_attrdef d on d.adrelid = a.attrelid and d.adnum = a.attnum
20
+ where ${sql.unsafe(userTables)} and a.attnum > 0 and not a.attisdropped
21
+ order by n.nspname, c.relname, a.attnum
22
+ `;
23
+ const selectForeignKeys = (sql) => sql`
24
+ select n.nspname as schema, c.relname as table,
25
+ (select array_agg(a.attname::text order by k.ord) from unnest(con.conkey) with ordinality k(attnum, ord)
26
+ join pg_attribute a on a.attrelid = con.conrelid and a.attnum = k.attnum) as columns,
27
+ rn.nspname as "refSchema", rc.relname as "refTable",
28
+ (select array_agg(a.attname::text order by k.ord) from unnest(con.confkey) with ordinality k(attnum, ord)
29
+ join pg_attribute a on a.attrelid = con.confrelid and a.attnum = k.attnum) as "refColumns"
30
+ from pg_constraint con
31
+ join pg_class c on c.oid = con.conrelid
32
+ join pg_namespace n on n.oid = c.relnamespace
33
+ join pg_class rc on rc.oid = con.confrelid
34
+ join pg_namespace rn on rn.oid = rc.relnamespace
35
+ where con.contype = 'f' and ${sql.unsafe(userTables)}
36
+ `;
37
+ const selectChecks = (sql) => sql`
38
+ select n.nspname as schema, c.relname as table,
39
+ (select array_agg(a.attname::text order by k.ord) from unnest(con.conkey) with ordinality k(attnum, ord)
40
+ join pg_attribute a on a.attrelid = con.conrelid and a.attnum = k.attnum) as columns,
41
+ pg_get_constraintdef(con.oid) as definition
42
+ from pg_constraint con
43
+ join pg_class c on c.oid = con.conrelid
44
+ join pg_namespace n on n.oid = c.relnamespace
45
+ where con.contype = 'c' and con.conkey is not null and ${sql.unsafe(userTables)}
46
+ `;
47
+ const selectIndexes = (sql) => sql`
48
+ select n.nspname as schema, c.relname as table, ic.relname as name,
49
+ (select array_agg(case when k.attnum = 0 then null else a.attname::text end order by k.ord)
50
+ from unnest(i.indkey::int2[]) with ordinality k(attnum, ord)
51
+ left join pg_attribute a on a.attrelid = i.indrelid and a.attnum = k.attnum
52
+ where k.ord <= i.indnkeyatts) as columns,
53
+ i.indisunique as unique, i.indisprimary as primary, (i.indpred is not null) as partial, am.amname as method
54
+ from pg_index i
55
+ join pg_class c on c.oid = i.indrelid
56
+ join pg_namespace n on n.oid = c.relnamespace
57
+ join pg_class ic on ic.oid = i.indexrelid
58
+ join pg_am am on am.oid = ic.relam
59
+ where i.indisvalid and ${sql.unsafe(userTables)}
60
+ `;
61
+ //#endregion
62
+ export { selectChecks, selectColumns, selectForeignKeys, selectIndexes };
@@ -0,0 +1,38 @@
1
+ import { connect } from "../db/connect.js";
2
+ import path from "node:path";
3
+ import { readFile } from "node:fs/promises";
4
+ //#region src/dev/check-database.ts
5
+ const describeTarget = (url) => {
6
+ const { hostname, port, pathname } = new URL(url);
7
+ return `${hostname}:${port || 5432}${pathname}`;
8
+ };
9
+ const startHint = async (projectDir) => {
10
+ const manifest = await readFile(path.join(projectDir, "package.json"), "utf8").catch((error) => {
11
+ if (error.code === "ENOENT") return void 0;
12
+ throw error;
13
+ });
14
+ if (!manifest) return "";
15
+ const { name, scripts } = JSON.parse(manifest);
16
+ if (!scripts?.["db:up"]) return "";
17
+ return `\nStart it with: pnpm --filter ${name} db:up`;
18
+ };
19
+ const reasons = {
20
+ ECONNREFUSED: "the connection was refused, so no Postgres is listening there",
21
+ ENOTFOUND: "the host name does not resolve",
22
+ CONNECT_TIMEOUT: "the connection timed out",
23
+ ETIMEDOUT: "the connection timed out",
24
+ "28P01": "the password was rejected",
25
+ "28000": "the user was rejected",
26
+ "3D000": "the database does not exist"
27
+ };
28
+ const checkDatabase = async (url, projectDir) => {
29
+ const sql = connect(url, { connectTimeout: 3 });
30
+ const failure = await sql`select 1`.then(() => void 0, (error) => {
31
+ if (!error.code || !(error.code in reasons)) throw error;
32
+ return reasons[error.code];
33
+ });
34
+ await sql.end();
35
+ if (failure) throw new Error(`Cannot reach the database at ${describeTarget(url)}: ${failure}.${await startHint(projectDir)}`);
36
+ };
37
+ //#endregion
38
+ export { checkDatabase };
@@ -0,0 +1,13 @@
1
+ import { createPgDb } from "../project/pg-db.js";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ //#region src/dev/create-db.ts
5
+ const sharedDb = (projectDir, url) => {
6
+ const store = globalThis;
7
+ if (store.__protobaseDb) return store.__protobaseDb;
8
+ const load = createRequire(path.join(projectDir, "package.json"));
9
+ store.__protobaseDb = createPgDb(load("pg"), url);
10
+ return store.__protobaseDb;
11
+ };
12
+ //#endregion
13
+ export { sharedDb };
@@ -0,0 +1,4 @@
1
+ //#region src/dev/entry.d.ts
2
+ declare const _default: import("hono/hono-base").HonoBase<import("hono/types").BlankEnv, import("hono/types").BlankSchema | import("hono/types").MergeSchemaPath<import("hono/types").BlankSchema, "/">, "/", "/">;
3
+ //#endregion
4
+ export { _default as default };
@@ -0,0 +1,32 @@
1
+ import { loadProject, projectDb } from "./project.js";
2
+ import { Hono } from "hono";
3
+ import { configExports, createAdmin, hasUsers } from "@protobase/server";
4
+ //#region src/dev/entry.ts
5
+ const projectDir = process.env.PROTOBASE_PROJECT;
6
+ const project = await loadProject(projectDir);
7
+ const db = projectDb(project, projectDir);
8
+ const { resources, views, pages, userMenu } = configExports(project.exports);
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 admin = createAdmin({
11
+ resources,
12
+ views,
13
+ pages,
14
+ ...userMenu && { userMenu },
15
+ db,
16
+ authenticate: project.authenticate,
17
+ ...project.auth && { auth: project.auth },
18
+ options: {
19
+ onUnhandledError: (error) => console.error(error),
20
+ ...project.options
21
+ }
22
+ });
23
+ var entry_default = new Hono().route("/", admin);
24
+ const store = globalThis;
25
+ if (project.auth && !store.__protobaseNoUsersShown) {
26
+ store.__protobaseNoUsersShown = true;
27
+ hasUsers(project.auth).then((any) => {
28
+ if (!any) console.log("No users yet. Create the first admin with: protobase users create you@example.com");
29
+ }, (error) => console.error(`Could not check for users, is the auth schema migrated? ${error.message}`));
30
+ }
31
+ //#endregion
32
+ export { entry_default as default };
@@ -0,0 +1,15 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ //#region src/dev/env.ts
4
+ const loadNearestEnvFile = (from) => {
5
+ for (let dir = from;; dir = path.dirname(dir)) {
6
+ const file = path.join(dir, ".env");
7
+ if (existsSync(file)) {
8
+ process.loadEnvFile(file);
9
+ return file;
10
+ }
11
+ if (path.dirname(dir) === dir) return void 0;
12
+ }
13
+ };
14
+ //#endregion
15
+ export { loadNearestEnvFile };
@@ -0,0 +1,4 @@
1
+ //#region src/dev/hosts.ts
2
+ const parseHosts = (value) => (value ?? "").split(",").map((host) => host.trim()).filter(Boolean);
3
+ //#endregion
4
+ export { parseHosts };
@@ -0,0 +1,39 @@
1
+ import { conventionConfig } from "../project/convention.js";
2
+ import { sharedDb } from "./create-db.js";
3
+ import { existsSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { readdir } from "node:fs/promises";
6
+ //#region src/dev/project.ts
7
+ const load = (file) => import(
8
+ /* @vite-ignore */
9
+ `/@fs${file}`
10
+ );
11
+ const conventionExports = async (projectDir) => {
12
+ const configDir = path.join(projectDir, "config");
13
+ const index = path.join(configDir, "index.ts");
14
+ if (!existsSync(index)) throw new Error(`No config/index.ts in ${projectDir}; run \`protobase scaffold\` or add a protobase.config.ts`);
15
+ const indexExports = await load(index);
16
+ const dirs = (await readdir(configDir, { withFileTypes: true })).filter((entry) => entry.isDirectory());
17
+ const uiModules = [];
18
+ for (const { name } of dirs) {
19
+ const ui = path.join(configDir, name, "ui.ts");
20
+ if (existsSync(ui)) uiModules.push([name, await load(ui)]);
21
+ }
22
+ return conventionConfig(indexExports, uiModules);
23
+ };
24
+ const loadProject = async (projectDir) => {
25
+ const file = path.join(projectDir, "protobase.config.ts");
26
+ const custom = existsSync(file) ? (await load(file)).default ?? {} : {};
27
+ return {
28
+ ...custom,
29
+ exports: custom.config ?? await conventionExports(projectDir)
30
+ };
31
+ };
32
+ const projectDb = (project, projectDir) => {
33
+ if (project.db) return project.db;
34
+ const url = process.env.PROTOBASE_DATABASE_URL;
35
+ if (!url) throw new Error("No database: set DATABASE_URL (or pass --env) or export `db` from protobase.config.ts");
36
+ return sharedDb(projectDir, url);
37
+ };
38
+ //#endregion
39
+ export { loadProject, projectDb };
@@ -0,0 +1,88 @@
1
+ import { moduleFile } from "../module-file.js";
2
+ import { layoutCheck } from "../build/layout-check.js";
3
+ import { adminAppDir, adminAppMain, uiPackageDir } from "../project/admin-app.js";
4
+ import { projectUiPlugin } from "../project/project-ui.js";
5
+ import { loadNearestEnvFile } from "./env.js";
6
+ import { checkDatabase } from "./check-database.js";
7
+ import { existsSync } from "node:fs";
8
+ import path from "node:path";
9
+ import { createServer, searchForWorkspaceRoot } from "vite";
10
+ import tailwindcss from "@tailwindcss/vite";
11
+ import react from "@vitejs/plugin-react";
12
+ import devServer from "@hono/vite-dev-server";
13
+ //#region src/dev/run.ts
14
+ const entry = moduleFile("./entry", import.meta.url);
15
+ const requireAppFiles = (appDir) => {
16
+ const missing = ["index.html", adminAppMain].filter((file) => !existsSync(path.join(appDir, file)));
17
+ 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
+ const reloadOnProjectChange = (projectDir) => ({
20
+ name: "protobase-project-reload",
21
+ handleHotUpdate: ({ file, server }) => {
22
+ if (file.startsWith(`${projectDir}${path.sep}`) && !file.includes(`${path.sep}node_modules${path.sep}`)) server.environments.ssr.moduleGraph.invalidateAll();
23
+ }
24
+ });
25
+ const appDependencies = [
26
+ "react",
27
+ "react/jsx-runtime",
28
+ "react/jsx-dev-runtime",
29
+ "react-dom",
30
+ "react-dom/client",
31
+ "lucide-react",
32
+ "zod",
33
+ "@tanstack/react-query",
34
+ "@tanstack/react-table",
35
+ "@tanstack/react-virtual",
36
+ "recharts"
37
+ ];
38
+ const runDev = async (options, out) => {
39
+ loadNearestEnvFile(options.projectDir);
40
+ const url = process.env[options.envVar];
41
+ if (!url) throw new Error(`${options.envVar} is not set; add it to .env or pass --env <VARIABLE>`);
42
+ const appDir = options.appDir ?? adminAppDir;
43
+ requireAppFiles(appDir);
44
+ await checkDatabase(url, options.projectDir);
45
+ process.env.PROTOBASE_PROJECT = options.projectDir;
46
+ process.env.PROTOBASE_DATABASE_URL = url;
47
+ const server = await createServer({
48
+ root: appDir,
49
+ configFile: false,
50
+ ...options.cacheDir && { cacheDir: options.cacheDir },
51
+ optimizeDeps: { include: appDependencies },
52
+ server: {
53
+ port: options.port,
54
+ strictPort: true,
55
+ ...options.allowedHosts.length > 0 && { allowedHosts: options.allowedHosts },
56
+ fs: { allow: [
57
+ searchForWorkspaceRoot(options.projectDir),
58
+ uiPackageDir,
59
+ options.projectDir
60
+ ] }
61
+ },
62
+ plugins: [
63
+ reloadOnProjectChange(options.projectDir),
64
+ projectUiPlugin(options.projectDir),
65
+ layoutCheck(),
66
+ react(),
67
+ tailwindcss(),
68
+ devServer({
69
+ entry,
70
+ exclude: [/^(?!\/api(\/|\?|$)).*/],
71
+ injectClientScript: false
72
+ })
73
+ ]
74
+ });
75
+ await server.listen();
76
+ await server.ssrLoadModule(entry).catch(async (error) => {
77
+ await server.close();
78
+ throw error;
79
+ });
80
+ const address = server.httpServer?.address();
81
+ const port = typeof address === "object" && address ? address.port : options.port;
82
+ out(`protobase dev ready at http://localhost:${port}\n`);
83
+ out(` project ${options.projectDir}\n api http://localhost:${port}/api/v1 (docs at /api/docs)\n`);
84
+ out(" login sign in with a user created by `protobase users create`\n");
85
+ return server;
86
+ };
87
+ //#endregion
88
+ export { runDev };