@protobase/cli 0.12.4 → 0.12.6

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.
@@ -3,6 +3,7 @@ import { uiPackageDir } from "../project/admin-app.js";
3
3
  import { extensionImports } from "../project/extensions.js";
4
4
  import { sharedModules } from "../project/shared-modules.js";
5
5
  import { pluginFieldTypes } from "./plugin-fields.js";
6
+ import { layerPluginUtilities } from "./plugin-styles.js";
6
7
  import { createRequire } from "node:module";
7
8
  import { existsSync } from "node:fs";
8
9
  import path from "node:path";
@@ -69,6 +70,13 @@ const pluginStyles = (file, code) => ({
69
70
  resolveId: (id) => id === file ? id : void 0,
70
71
  load: (id) => id === file ? code : void 0
71
72
  });
73
+ const layeredStyles = () => ({
74
+ name: "protobase-plugin-layered-styles",
75
+ enforce: "post",
76
+ generateBundle(_, bundle) {
77
+ for (const file of Object.values(bundle)) if (file.type === "asset" && file.fileName.endsWith(".css")) file.source = layerPluginUtilities(String(file.source));
78
+ }
79
+ });
72
80
  const outputOf = (result) => {
73
81
  if (Array.isArray(result)) return result[0];
74
82
  if ("output" in result) return result;
@@ -106,7 +114,8 @@ const buildPlugin = async ({ pluginDir, outDir, name, projectDir, ignore = [] })
106
114
  ...projectDir === void 0 ? [] : [extensionImports(projectDir, [{ dir: pluginDir }])],
107
115
  pluginStyles(styles, styleCode(pluginDir, target.theme, [outDir, ...ignore])),
108
116
  react(),
109
- tailwindcss()
117
+ tailwindcss(),
118
+ layeredStyles()
110
119
  ],
111
120
  build: {
112
121
  outDir,
@@ -0,0 +1,70 @@
1
+ import postcss from "postcss";
2
+ //#region src/build/plugin-styles.ts
3
+ /**
4
+ * The layer, nested in `utilities`, that holds a plugin's utilities without a variant. Rules outside a nested layer win
5
+ * over the rules inside it, so the app's own utilities win over these, whatever the order of the stylesheets.
6
+ */
7
+ const pluginBaseLayer = "plugin";
8
+ const withoutBrackets = (name) => {
9
+ let depth = 0;
10
+ let out = "";
11
+ for (const char of name) if (char === "[") depth += 1;
12
+ else if (char === "]") depth -= 1;
13
+ else if (depth === 0) out += char;
14
+ return out;
15
+ };
16
+ /** Whether a utility's class name has a variant, such as `md:flex`, `hover:bg-muted` or `[&>*]:p-2`. */
17
+ const hasVariant = (name) => withoutBrackets(name).includes(":");
18
+ const unescape = (name) => name.replace(/\\([0-9a-fA-F]{1,6}) ?|\\(.)/g, (_, hex, char) => hex ? String.fromCodePoint(parseInt(hex, 16)) : char);
19
+ /** The class names in a selector, also those inside :is(), :where() and :not(), unescaped: `.md\:flex` is `md:flex`. */
20
+ const classNames = (selector) => [...selector.matchAll(/\.((?:\\[0-9a-fA-F]{1,6} ?|\\.|[\w-]|[^\x00-\x7f])+)/g)].map(([, name]) => unescape(name));
21
+ const around = (node, children) => {
22
+ const copy = node.clone();
23
+ copy.removeAll();
24
+ return copy.append(children.map((child) => child.clone()));
25
+ };
26
+ const partition = (nodes) => {
27
+ const parts = {
28
+ base: [],
29
+ variant: []
30
+ };
31
+ for (const node of nodes) if (node.type === "rule") {
32
+ const varies = (selector) => classNames(selector).some(hasVariant);
33
+ const base = node.selectors.filter((selector) => !varies(selector));
34
+ const variant = node.selectors.filter(varies);
35
+ if (base.length > 0) parts.base.push(variant.length === 0 ? node : node.clone({ selectors: base }));
36
+ if (variant.length > 0) parts.variant.push(base.length === 0 ? node : node.clone({ selectors: variant }));
37
+ } else if (node.type === "atrule" && [
38
+ "media",
39
+ "supports",
40
+ "container"
41
+ ].includes(node.name) && node.nodes) {
42
+ const inner = partition(node.nodes);
43
+ if (inner.base.length > 0) parts.base.push(around(node, inner.base));
44
+ if (inner.variant.length > 0) parts.variant.push(around(node, inner.variant));
45
+ } else parts.variant.push(node);
46
+ return parts;
47
+ };
48
+ const isUtilities = (node) => node.type === "atrule" && node.name === "layer" && node.params.trim() === "utilities" && node.nodes !== void 0;
49
+ /**
50
+ * A plugin's stylesheet with its utilities without a variant moved into a layer under the app's utilities. The app
51
+ * loads a plugin's stylesheet after its own, where a plugin's `.flex` would otherwise win over the app's `md:block` on
52
+ * the app's own elements. A plugin's variants stay after the app's utilities, so its `hidden md:flex` still works when
53
+ * only the app's stylesheet has `.hidden`.
54
+ */
55
+ const layerPluginUtilities = (css) => {
56
+ const root = postcss.parse(css);
57
+ for (const node of root.nodes.filter(isUtilities)) {
58
+ const { base, variant } = partition(node.nodes);
59
+ const kept = variant.map((child) => child.clone());
60
+ node.removeAll();
61
+ if (base.length > 0) node.append(postcss.atRule({
62
+ name: "layer",
63
+ params: pluginBaseLayer
64
+ }).append(base.map((child) => child.clone())));
65
+ node.append(kept);
66
+ }
67
+ return root.toString();
68
+ };
69
+ //#endregion
70
+ export { classNames, hasVariant, layerPluginUtilities, pluginBaseLayer };
package/dist/dev/entry.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { devJobs } from "./dev-jobs.js";
2
+ import { projectErrorReporter } from "../project/error-reporter.js";
2
3
  import { projectAdmin } from "../project/project-admin.js";
3
4
  import { loadProject, projectDb } from "./project.js";
4
5
  import { checkAuthSchema, checkChanges, checkWebhooksSchema, configExports, hasUsers } from "@protobase/server";
@@ -16,10 +17,11 @@ if (project.auth && !store.__protobaseAuthSchemaChecked) {
16
17
  }
17
18
  await checkChanges(db, resources.map((resource) => resource.toModel()), project.events);
18
19
  if (project.webhooks) await checkWebhooksSchema(db, project.webhooks);
20
+ const reporter = await projectErrorReporter(project, process.env);
19
21
  const { admin, jobs } = projectAdmin({
20
22
  ...project,
21
23
  authenticate: project.authenticate
22
- }, project.exports, db);
24
+ }, project.exports, db, void 0, reporter);
23
25
  await devJobs(db, jobs);
24
26
  var entry_default = new Hono().route("/", admin);
25
27
  if (project.auth && !store.__protobaseNoUsersShown) {
package/dist/dev/run.js CHANGED
@@ -41,7 +41,8 @@ const appDependencies = [
41
41
  "@tanstack/react-query",
42
42
  "@tanstack/react-table",
43
43
  "@tanstack/react-virtual",
44
- "recharts"
44
+ "recharts",
45
+ "@sentry/browser"
45
46
  ];
46
47
  const runDev = async (options, out) => {
47
48
  loadNearestEnvFile(options.projectDir);
package/dist/main.js CHANGED
@@ -10,7 +10,9 @@ import { resolveConnectionUrl } from "./db/connect.js";
10
10
  import { parseHosts } from "./dev/hosts.js";
11
11
  import { runDev } from "./dev/run.js";
12
12
  import { runDoctor } from "./doctor/run.js";
13
+ import { projectErrorReporter } from "./project/error-reporter.js";
13
14
  import { runScaffold } from "./scaffold/run.js";
15
+ import { reportCrashes } from "./serve/crash-reports.js";
14
16
  import { closeOnSignals } from "./serve/signals.js";
15
17
  import { serveBundle } from "./serve/serve-bundle.js";
16
18
  import { serveBundleDir } from "./serve/serve-dir.js";
@@ -110,8 +112,11 @@ program.command("serve").description("Serve a bundle written by `protobase build
110
112
  });
111
113
  program.command("worker").description("Run the jobs of a bundle from `protobase build` in a process of their own, until SIGTERM or SIGINT, as `serve` runs them beside the API").argument("<bundle>", "the bundle folder, e.g. dist, or its dist/protobase.config.js").action(async (bundle) => {
112
114
  loadNearestEnvFile(process.cwd());
115
+ const project = await loadWorkerBundle(bundle);
116
+ const reporter = await projectErrorReporter(project, process.env);
117
+ if (reporter) reportCrashes(reporter);
113
118
  const worker = await startWorker({
114
- project: await loadWorkerBundle(bundle),
119
+ project,
115
120
  databaseUrl: process.env.DATABASE_URL || void 0
116
121
  });
117
122
  closeOnSignals(worker.stop);
package/dist/package.js CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.12.4";
2
+ var version = "0.12.6";
3
3
  //#endregion
4
4
  export { version };
@@ -0,0 +1,9 @@
1
+ import { errorReportingSettings, startErrorReporting } from "@protobase/server";
2
+ //#region src/project/error-reporter.ts
3
+ /** The project's error reporter, when its `options.errorReporting` or `PROTOBASE_SENTRY_DSN` sets a DSN. */
4
+ const projectErrorReporter = async (project, env) => {
5
+ const settings = errorReportingSettings(project.options?.errorReporting, env);
6
+ return settings ? startErrorReporting(settings) : void 0;
7
+ };
8
+ //#endregion
9
+ export { projectErrorReporter };
@@ -3,11 +3,13 @@ import { configExports, createAdmin, jobDefinitions } from "@protobase/server";
3
3
  /**
4
4
  * The admin API of a project over `db`, as `dev`, `serve` and `worker` build it: its config module's resources, views,
5
5
  * pages and subscriptions, and the rest of its config; served from a bundle, the plugins it carries and their field
6
- * types. `jobs` are the project's own and the delivery of its events.
6
+ * types. `jobs` are the project's own and the delivery of its events. With a `reporter`, every error the API turns
7
+ * into a 500 goes to the error tracker too, besides the project's `onUnhandledError` or the console.
7
8
  */
8
- const projectAdmin = (project, config, db, ui) => {
9
+ const projectAdmin = (project, config, db, ui, reporter) => {
9
10
  const { resources, views, pages, userMenu, bottomBars, subscriptions } = configExports(config);
10
11
  const own = jobDefinitions(project.jobs ?? {});
12
+ const unhandled = project.options?.onUnhandledError ?? ((error) => console.error(error));
11
13
  const admin = createAdmin({
12
14
  resources,
13
15
  views,
@@ -28,8 +30,11 @@ const projectAdmin = (project, config, db, ui) => {
28
30
  fieldTypes: ui.fieldTypes
29
31
  },
30
32
  options: {
31
- onUnhandledError: (error) => console.error(error),
32
- ...project.options
33
+ ...project.options,
34
+ onUnhandledError: (error, request) => {
35
+ reporter?.report(error, request);
36
+ unhandled(error, request);
37
+ }
33
38
  }
34
39
  });
35
40
  return {
@@ -0,0 +1,16 @@
1
+ //#region src/serve/crash-reports.ts
2
+ /**
3
+ * Reports an exception or a rejection nothing handled, then ends the process as Node and Bun do without a listener:
4
+ * the error printed, exit code 1. The report has `flushMs` to reach the error tracker.
5
+ */
6
+ const reportCrashes = (reporter, target = process, flushMs = 2e3) => {
7
+ const crash = (error) => {
8
+ console.error(error);
9
+ reporter.report(error);
10
+ reporter.flush(flushMs).then(() => target.exit(1));
11
+ };
12
+ target.on("uncaughtException", crash);
13
+ target.on("unhandledRejection", crash);
14
+ };
15
+ //#endregion
16
+ export { reportCrashes };
@@ -14,7 +14,7 @@ const listen = (server, port) => new Promise((resolve, reject) => {
14
14
  });
15
15
  });
16
16
  const closeServer = (server) => new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
17
- const startServe = async ({ project, env, createDb = (url) => createPgDb(pg, url), site, ui }) => {
17
+ const startServe = async ({ project, env, createDb = (url) => createPgDb(pg, url), site, ui, reporter }) => {
18
18
  if (!project.db && !env.databaseUrl) throw new Error("No database: set DATABASE_URL or export `db` from protobase.config.ts");
19
19
  if (project.auth) await checkAuthSchema(project.auth);
20
20
  const ownDb = project.db ? void 0 : createDb(env.databaseUrl);
@@ -23,7 +23,7 @@ const startServe = async ({ project, env, createDb = (url) => createPgDb(pg, url
23
23
  const startJobsAndListen = async () => {
24
24
  await checkChanges(db, resources.map((resource) => resource.toModel()), project.events);
25
25
  if (project.webhooks) await checkWebhooksSchema(db, project.webhooks);
26
- const { admin, jobs: definitions } = projectAdmin(project, project.config, db, ui);
26
+ const { admin, jobs: definitions } = projectAdmin(project, project.config, db, ui, reporter);
27
27
  if (definitions.length > 0) await checkJobsSchema(db);
28
28
  const jobs = definitions.length > 0 ? await startJobs({
29
29
  db,
@@ -1,6 +1,8 @@
1
1
  import { readManifest } from "../bundle/manifest.js";
2
+ import { projectErrorReporter } from "../project/error-reporter.js";
2
3
  import { bundleUi } from "./bundle-ui.js";
3
4
  import { checkBundleVersion } from "./bundle-version.js";
5
+ import { reportCrashes } from "./crash-reports.js";
4
6
  import { readServeEnv } from "./env.js";
5
7
  import { startServe } from "./lifecycle.js";
6
8
  import { loadBundle } from "./load-bundle.js";
@@ -12,11 +14,14 @@ const serveBundle = async (file, env, out, site) => {
12
14
  await checkBundleVersion(file);
13
15
  const manifest = await readManifest(path.dirname(file));
14
16
  const project = await loadBundle(file);
17
+ const reporter = await projectErrorReporter(project, env);
18
+ if (reporter) reportCrashes(reporter);
15
19
  const server = await startServe({
16
20
  project,
17
21
  env: serveEnv,
18
22
  site,
19
- ui: bundleUi(manifest)
23
+ ui: bundleUi(manifest),
24
+ ...reporter && { reporter }
20
25
  });
21
26
  closeOnSignals(server.close);
22
27
  out(`protobase serve listening on port ${server.port}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/cli",
3
- "version": "0.12.4",
3
+ "version": "0.12.6",
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.12.4",
38
- "@protobase/query": "0.12.4",
39
- "@protobase/schema": "0.12.4",
40
- "@protobase/server": "0.12.4",
41
- "@protobase/ui": "0.12.4",
37
+ "@protobase/layout": "0.12.6",
38
+ "@protobase/query": "0.12.6",
39
+ "@protobase/schema": "0.12.6",
40
+ "@protobase/server": "0.12.6",
41
+ "@protobase/ui": "0.12.6",
42
42
  "@scalar/hono-api-reference": "^0.12.10",
43
43
  "@tailwindcss/vite": "^4.3.3",
44
44
  "@vitejs/plugin-react": "^6.1.2",
@@ -51,6 +51,7 @@
51
51
  "kysely": "^0.29.6",
52
52
  "lightningcss": "^1.33.0",
53
53
  "pg": "^8.23.1",
54
+ "postcss": "^8.5.29",
54
55
  "postgres": "^3.4.9",
55
56
  "ts-morph": "^28.0.0",
56
57
  "vite": "^8.3.3",