@protobase/cli 0.11.1 → 0.12.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.
@@ -0,0 +1,55 @@
1
+ import { projectUiFile } from "../project/project-ui.js";
2
+ import { readPackageJson } from "./packages/package-dir.js";
3
+ import { buildPlugin, pluginManifestFile } from "./plugin-build.js";
4
+ import { existsSync } from "node:fs";
5
+ import path from "node:path";
6
+ /** Where a bundle's plugins go, inside its `public` folder. */
7
+ const pluginsFolder = "plugins";
8
+ const extensionName = async (extension) => {
9
+ return (existsSync(path.join(extension.dir, "package.json")) ? await readPackageJson(extension.dir) : {}).name ?? path.basename(extension.dir);
10
+ };
11
+ const folderOf = (name) => name.replace(/^@/, "").replace(/[^\w.-]+/g, "-");
12
+ const sources = async (projectDir, extensions) => {
13
+ const own = existsSync(projectUiFile(projectDir)) ? [{
14
+ dir: projectDir,
15
+ name: "app"
16
+ }] : [];
17
+ const all = [...await Promise.all(extensions.filter((extension) => extension.ui).map(async (extension) => ({
18
+ dir: extension.dir,
19
+ name: await extensionName(extension)
20
+ }))), ...own];
21
+ const seen = /* @__PURE__ */ new Map();
22
+ for (const source of all) {
23
+ const other = seen.get(folderOf(source.name));
24
+ if (other) throw new Error(`${other} and ${source.dir} would both be the plugin ${folderOf(source.name)}; give the extension's package.json another name`);
25
+ seen.set(folderOf(source.name), source.dir);
26
+ }
27
+ return all;
28
+ };
29
+ /**
30
+ * Builds the project's protobase.ui.tsx and each extension's as a plugin under `publicDir`/plugins/<name>/, as
31
+ * `protobase build-plugin` builds one: the app loads them like any other, from what `/meta` names.
32
+ */
33
+ const buildBundlePlugins = async ({ projectDir, publicDir, extensions, ignore }) => {
34
+ const plugins = [];
35
+ for (const source of await sources(projectDir, extensions)) {
36
+ const folder = path.join(pluginsFolder, folderOf(source.name));
37
+ const manifest = await buildPlugin({
38
+ pluginDir: source.dir,
39
+ outDir: path.join(publicDir, folder),
40
+ name: source.name,
41
+ projectDir,
42
+ ignore
43
+ });
44
+ plugins.push({
45
+ name: manifest.name,
46
+ path: `${folder}/${pluginManifestFile}`,
47
+ api: manifest.api,
48
+ protobase: manifest.protobase,
49
+ ...manifest.fields && { fields: manifest.fields }
50
+ });
51
+ }
52
+ return plugins;
53
+ };
54
+ //#endregion
55
+ export { buildBundlePlugins, pluginsFolder };
@@ -1,12 +1,12 @@
1
- import { hostModuleIds } from "../serve/host-module-ids.js";
1
+ import { findPackageDir, packageName } from "./packages/package-dir.js";
2
2
  import { extensionImports } from "../project/extensions.js";
3
+ import { viteBundle } from "./vite-bundle.js";
4
+ import { hostModuleIds } from "../serve/host-module-ids.js";
3
5
  import { collectComponents } from "./component-names.js";
4
6
  import { bundleEntryCode } from "./entry-code.js";
5
7
  import { layoutCheck } from "./layout-check.js";
6
- import { findPackageDir, packageName } from "./packages/package-dir.js";
7
8
  import { hostPackages, hostVersionProblem } from "./packages/host-packages.js";
8
9
  import { isNativePackage } from "./packages/native-package.js";
9
- import { viteBundle } from "./vite-bundle.js";
10
10
  import path from "node:path";
11
11
  //#region src/build/config-bundle.ts
12
12
  const isHostModule = (id) => hostModuleIds.includes(id);
@@ -1,12 +1,14 @@
1
1
  import { bundleLayout, bundleNodeModules, manifestFile } from "../bundle/manifest.js";
2
+ import { buildBundlePlugins } from "./bundle-plugins.js";
2
3
  import { buildConfigBundle } from "./config-bundle.js";
3
4
  import { protobaseVersion } from "../version/version.js";
4
5
  import { copyPackages } from "./packages/copy-packages.js";
5
- import { buildUiBundle } from "./ui-bundle.js";
6
+ import { buildTheme } from "./theme.js";
7
+ import { shellForBuild, shellManifestFile } from "./shell.js";
6
8
  import { checkUi } from "./ui-check.js";
7
9
  import { existsSync } from "node:fs";
8
10
  import path from "node:path";
9
- import { readFile, rm, writeFile } from "node:fs/promises";
11
+ import { cp, readFile, rm, writeFile } from "node:fs/promises";
10
12
  //#region src/build/deploy-bundle.ts
11
13
  const removeOwnNodeModules = async (outDir) => {
12
14
  const dir = path.join(outDir, bundleNodeModules);
@@ -18,7 +20,26 @@ const removeOwnNodeModules = async (outDir) => {
18
20
  force: true
19
21
  });
20
22
  };
21
- const buildDeployBundle = async ({ projectDir, outDir, bun, extensions = [] }) => {
23
+ const copyShell = async (publicDir) => {
24
+ const { dir, shell, scratch } = await shellForBuild();
25
+ try {
26
+ await rm(publicDir, {
27
+ recursive: true,
28
+ force: true
29
+ });
30
+ await cp(dir, publicDir, {
31
+ recursive: true,
32
+ filter: (file) => path.basename(file) !== shellManifestFile
33
+ });
34
+ } finally {
35
+ if (scratch) await rm(dir, {
36
+ recursive: true,
37
+ force: true
38
+ });
39
+ }
40
+ return shell;
41
+ };
42
+ const buildDeployBundle = async ({ projectDir, outDir, bun, extensions = [], onWarning }) => {
22
43
  await removeOwnNodeModules(outDir);
23
44
  const components = /* @__PURE__ */ new Set();
24
45
  const native = await buildConfigBundle({
@@ -30,16 +51,25 @@ const buildDeployBundle = async ({ projectDir, outDir, bun, extensions = [] }) =
30
51
  });
31
52
  await checkUi(projectDir, extensions, components);
32
53
  const packages = native.length > 0 ? await copyPackages(native, path.join(outDir, bundleNodeModules)) : [];
33
- await buildUiBundle({
34
- outDir: path.join(outDir, bundleLayout.public),
54
+ const publicDir = path.join(outDir, bundleLayout.public);
55
+ const shell = await copyShell(publicDir);
56
+ const plugins = await buildBundlePlugins({
35
57
  projectDir,
36
- extensions
58
+ publicDir,
59
+ extensions,
60
+ ignore: [outDir]
37
61
  });
62
+ for (const warning of await buildTheme({
63
+ projectDir,
64
+ outFile: path.join(publicDir, bundleLayout.theme)
65
+ })) onWarning?.(warning);
38
66
  const manifest = {
39
- version: 2,
67
+ version: 3,
40
68
  protobase: protobaseVersion,
41
69
  ...bundleLayout,
42
70
  api: [...bundleLayout.api],
71
+ plugins,
72
+ shell,
43
73
  ...packages.length > 0 ? { nodeModules: bundleNodeModules } : {}
44
74
  };
45
75
  await writeFile(path.join(outDir, manifestFile), `${JSON.stringify(manifest, null, 2)}\n`);
@@ -1,6 +1,8 @@
1
- import { uiPackageDir } from "../project/admin-app.js";
2
1
  import { findPackageDirSync, readPackageJson } from "./packages/package-dir.js";
2
+ import { uiPackageDir } from "../project/admin-app.js";
3
+ import { extensionImports } from "../project/extensions.js";
3
4
  import { sharedModules } from "../project/shared-modules.js";
5
+ import { pluginFieldTypes } from "./plugin-fields.js";
4
6
  import { createRequire } from "node:module";
5
7
  import { existsSync } from "node:fs";
6
8
  import path from "node:path";
@@ -16,12 +18,12 @@ const pluginEntryFile = "protobase.ui.tsx";
16
18
  const pluginManifestFile = "plugin.json";
17
19
  const tailwindDir = path.dirname(createRequire(path.join(uiPackageDir, "package.json")).resolve("tailwindcss/package.json"));
18
20
  /**
19
- * What the plugin is built for, from the @protobase/ui installed for it: its plugin API, and Protobase from its version
20
- * on, unless the plugin's package.json names other versions in `engines.protobase`. Also that version's theme, which
21
- * the plugin's stylesheet draws with.
21
+ * What the plugin is built for, from the @protobase/ui installed for it (else for the project it extends): its plugin
22
+ * API, and Protobase from its version on, unless the plugin's package.json names other versions in `engines.protobase`.
23
+ * Also that version's theme, which the plugin's stylesheet draws with, and its folder.
22
24
  */
23
- const pluginTarget = async (pluginDir, own) => {
24
- const uiDir = findPackageDirSync("@protobase/ui", pluginDir);
25
+ const pluginTarget = async (pluginDir, own, projectDir) => {
26
+ const uiDir = findPackageDirSync("@protobase/ui", pluginDir) ?? (projectDir === void 0 ? void 0 : findPackageDirSync("@protobase/ui", projectDir));
25
27
  if (!uiDir) throw new Error(`@protobase/ui is not installed for ${pluginDir}; add it to the plugin's dependencies, as the version it is built for`);
26
28
  const ui = await readPackageJson(uiDir);
27
29
  const api = ui.protobase?.pluginApi;
@@ -34,7 +36,8 @@ const pluginTarget = async (pluginDir, own) => {
34
36
  return {
35
37
  api,
36
38
  protobase,
37
- theme: path.join(uiDir, theme)
39
+ theme: path.join(uiDir, theme),
40
+ uiDir
38
41
  };
39
42
  };
40
43
  const pluginImports = () => ({
@@ -48,12 +51,17 @@ const pluginImports = () => ({
48
51
  if (sharedModules.some((name) => id.startsWith(`${name}/`))) this.error(`${id} is not shared with plugins, and a copy of its own would not work with the app's; a plugin imports ${sharedModules.join(", ")} by those names alone`);
49
52
  }
50
53
  });
51
- const styleCode = (pluginDir, theme) => [
54
+ const inside = (dir, parent) => {
55
+ const relative = path.relative(parent, dir);
56
+ return !relative.startsWith("..") && !path.isAbsolute(relative);
57
+ };
58
+ const styleCode = (pluginDir, theme, outDirs) => [
52
59
  "@layer theme, base, components, utilities;",
53
60
  `@import ${JSON.stringify(path.join(tailwindDir, "theme.css"))} layer(theme) theme(reference);`,
54
61
  `@reference ${JSON.stringify(theme)};`,
55
62
  `@import ${JSON.stringify(path.join(tailwindDir, "utilities.css"))} layer(utilities) source(none);`,
56
63
  `@source ${JSON.stringify(pluginDir)};`,
64
+ ...outDirs.filter((dir) => inside(dir, pluginDir)).map((dir) => `@source not ${JSON.stringify(dir)};`),
57
65
  ""
58
66
  ].join("\n");
59
67
  const pluginStyles = (file, code) => ({
@@ -68,21 +76,21 @@ const outputOf = (result) => {
68
76
  };
69
77
  /** The output folder must not hold the plugin's own files, which emptying it would delete. */
70
78
  const checkOutDir = (pluginDir, outDir) => {
71
- const relative = path.relative(outDir, pluginDir);
72
- if (!relative.startsWith("..") && !path.isAbsolute(relative)) throw new Error(`--out ${outDir} holds the plugin's own files; build into a folder inside it or elsewhere, such as ${path.join(pluginDir, "dist")}`);
79
+ if (inside(pluginDir, outDir)) throw new Error(`--out ${outDir} holds the plugin's own files; build into a folder inside it or elsewhere, such as ${path.join(pluginDir, "dist")}`);
73
80
  };
74
81
  /**
75
82
  * Builds the plugin in `pluginDir` into `outDir`: its protobase.ui.tsx as an ES module (and its chunks) that imports the
76
- * shared modules by name, its stylesheet, and plugin.json naming both. The app loads it from the plugin.json's URL.
83
+ * shared modules by name, its stylesheet, and plugin.json naming both and the field types it registers. The app loads
84
+ * it from the plugin.json's URL.
77
85
  */
78
- const buildPlugin = async ({ pluginDir, outDir, name }) => {
86
+ const buildPlugin = async ({ pluginDir, outDir, name, projectDir, ignore = [] }) => {
79
87
  const entry = path.join(pluginDir, pluginEntryFile);
80
88
  if (!existsSync(entry)) throw new Error(`There is no ${pluginEntryFile} in ${pluginDir}; a plugin exports its components and action handlers from it`);
81
89
  checkOutDir(pluginDir, outDir);
82
90
  const own = existsSync(path.join(pluginDir, "package.json")) ? await readPackageJson(pluginDir) : {};
83
91
  const pluginName = (name ?? own.name ?? path.basename(pluginDir)).trim();
84
92
  if (pluginName === "") throw new Error("The plugin needs a name: give its package.json a name, or pass --name");
85
- const target = await pluginTarget(pluginDir, own);
93
+ const target = await pluginTarget(pluginDir, own, projectDir);
86
94
  const styles = path.join(pluginDir, "protobase-plugin.css");
87
95
  const nodeEnv = process.env.NODE_ENV;
88
96
  process.env.NODE_ENV = "production";
@@ -95,7 +103,8 @@ const buildPlugin = async ({ pluginDir, outDir, name }) => {
95
103
  logLevel: "warn",
96
104
  plugins: [
97
105
  pluginImports(),
98
- pluginStyles(styles, styleCode(pluginDir, target.theme)),
106
+ ...projectDir === void 0 ? [] : [extensionImports(projectDir, [{ dir: pluginDir }])],
107
+ pluginStyles(styles, styleCode(pluginDir, target.theme, [outDir, ...ignore])),
99
108
  react(),
100
109
  tailwindcss()
101
110
  ],
@@ -124,13 +133,20 @@ const buildPlugin = async ({ pluginDir, outDir, name }) => {
124
133
  const module = output.find((file) => file.type === "chunk" && file.isEntry && file.facadeModuleId === entry);
125
134
  const css = output.find((file) => file.type === "asset" && file.fileName.endsWith(".css"));
126
135
  if (!module || !css) throw new Error("The plugin build wrote no module or no stylesheet");
136
+ const fields = await pluginFieldTypes({
137
+ pluginDir,
138
+ entry,
139
+ uiDir: target.uiDir,
140
+ projectDir
141
+ });
127
142
  const manifest = {
128
143
  name: pluginName,
129
144
  ...own.version && { version: own.version },
130
145
  api: target.api,
131
146
  protobase: target.protobase,
132
147
  entry: module.fileName,
133
- css: css.fileName
148
+ css: css.fileName,
149
+ ...Object.keys(fields).length > 0 && { fields }
134
150
  };
135
151
  await writeFile(path.join(outDir, pluginManifestFile), `${JSON.stringify(manifest, null, 2)}\n`);
136
152
  return manifest;
@@ -0,0 +1,67 @@
1
+ import { extensionImports } from "../project/extensions.js";
2
+ import { sharedModules } from "../project/shared-modules.js";
3
+ import { viteBundle } from "./vite-bundle.js";
4
+ import path from "node:path";
5
+ import { mkdtemp, rm } from "node:fs/promises";
6
+ import { tmpdir } from "node:os";
7
+ import { pathToFileURL } from "node:url";
8
+ //#region src/build/plugin-fields.ts
9
+ const fieldsCode = (entry) => [
10
+ `import * as ui from ${JSON.stringify(entry)}`,
11
+ `import { mergeUi, uiFromModule } from '@protobase/ui'`,
12
+ "const { fields = {} } = mergeUi(uiFromModule(ui))",
13
+ "export default Object.fromEntries(Object.entries(fields).map(([name, type]) => [name, type.of]))",
14
+ ""
15
+ ].join("\n");
16
+ const fieldsEntry = (file, code) => ({
17
+ name: "protobase-plugin-fields",
18
+ resolveId: (id) => id === file ? `\0${file}` : void 0,
19
+ load: (id) => id === `\0${file}` ? code : void 0
20
+ });
21
+ const sharedFromUi = (uiDir) => ({
22
+ name: "protobase-plugin-fields-shared",
23
+ enforce: "pre",
24
+ resolveId(id, _importer, options) {
25
+ if (id === "@protobase/ui" || !sharedModules.includes(id)) return void 0;
26
+ return this.resolve(id, path.join(uiDir, "package.json"), {
27
+ ...options,
28
+ skipSelf: true
29
+ });
30
+ }
31
+ });
32
+ const isStorageTypes = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
33
+ /**
34
+ * The field types a plugin registers (`definePlugin({ fields })`), each with the storage types it shows (`of`), which
35
+ * the server checks the views against at startup. The plugin's module is loaded once in Node, everything bundled into
36
+ * a scratch file, as `protobase build` loads a project's UI to check its names; nothing is rendered.
37
+ */
38
+ const pluginFieldTypes = async ({ pluginDir, entry, uiDir, projectDir }) => {
39
+ const file = path.join(pluginDir, "protobase-plugin-fields.js");
40
+ const scratch = await mkdtemp(path.join(tmpdir(), "protobase-plugin-fields-"));
41
+ const outFile = path.join(scratch, "fields.mjs");
42
+ try {
43
+ const plugins = [
44
+ fieldsEntry(file, fieldsCode(entry)),
45
+ sharedFromUi(uiDir),
46
+ ...projectDir === void 0 ? [] : [extensionImports(projectDir, [{ dir: pluginDir }])]
47
+ ];
48
+ await viteBundle({
49
+ root: pluginDir,
50
+ entry: file,
51
+ outFile,
52
+ plugins,
53
+ logLevel: "error"
54
+ });
55
+ const fields = (await import(pathToFileURL(outFile).href)).default;
56
+ const wrong = Object.entries(fields).find(([, of]) => !isStorageTypes(of));
57
+ if (wrong) throw new Error(`The field type "${wrong[0]}" names no storage types it shows; give it of: ['integer'] or the like`);
58
+ return fields;
59
+ } finally {
60
+ await rm(scratch, {
61
+ recursive: true,
62
+ force: true
63
+ });
64
+ }
65
+ };
66
+ //#endregion
67
+ export { pluginFieldTypes };
@@ -1,5 +1,5 @@
1
- import { moduleFile } from "../module-file.js";
2
1
  import { viteBundle } from "./vite-bundle.js";
2
+ import { moduleFile } from "../module-file.js";
3
3
  import { fileURLToPath } from "node:url";
4
4
  //#region src/build/serve-runtime.ts
5
5
  const cliRoot = fileURLToPath(new URL("../..", import.meta.url));
@@ -0,0 +1,91 @@
1
+ import { projectUiPlugin } from "../project/project-ui.js";
2
+ import { readPackageJson } from "./packages/package-dir.js";
3
+ import { adminAppDir, uiEntry, uiPackageDir } from "../project/admin-app.js";
4
+ import { sharedModulesPlugin } from "../project/shared-modules.js";
5
+ import { themeLink } from "../project/theme-link.js";
6
+ import { existsSync } from "node:fs";
7
+ import path from "node:path";
8
+ import { build } from "vite";
9
+ import { mkdtemp, readFile, writeFile } from "node:fs/promises";
10
+ import tailwindcss from "@tailwindcss/vite";
11
+ import react from "@vitejs/plugin-react";
12
+ import { tmpdir } from "node:os";
13
+ //#region src/build/shell.ts
14
+ /** What a shell folder holds beside its files: the `BundleShell` a bundle's manifest copies. */
15
+ const shellManifestFile = "protobase.shell.json";
16
+ /** The shell's files, as URL paths: its page and its content-hashed assets. */
17
+ const shellPaths = ["/index.html", "/assets"];
18
+ /** Where @protobase/ui's package ships its prebuilt shell, which `protobase build` copies and `protobase serve` serves. */
19
+ const prebuiltShellDir = path.join(uiPackageDir, "dist", "shell");
20
+ const shellIdentity = async () => {
21
+ const ui = await readPackageJson(uiPackageDir);
22
+ if (!ui.version || ui.protobase?.pluginApi === void 0) throw new Error(`${uiPackageDir} names no version or plugin API`);
23
+ return {
24
+ protobase: ui.version,
25
+ pluginApi: ui.protobase.pluginApi,
26
+ paths: shellPaths
27
+ };
28
+ };
29
+ /**
30
+ * The admin app as a production build, the same for every project: index.html and content-hashed files under assets/,
31
+ * with protobase.shell.json saying which version it is. Asset URLs are absolute, so index.html works as the answer to
32
+ * any deep link. It calls /api on its own origin and loads the project's UI, like every plugin, from what `/meta`
33
+ * names, and links the project's theme at /protobase.theme.css. The modules plugins share with it have entries of
34
+ * their own, which index.html's import map names.
35
+ */
36
+ const buildShell = async (outDir) => {
37
+ const nodeEnv = process.env.NODE_ENV;
38
+ process.env.NODE_ENV = "production";
39
+ await build({
40
+ configFile: false,
41
+ root: adminAppDir,
42
+ base: "/",
43
+ mode: "production",
44
+ publicDir: false,
45
+ logLevel: "warn",
46
+ plugins: [
47
+ sharedModulesPlugin(),
48
+ projectUiPlugin(void 0),
49
+ themeLink(),
50
+ react(),
51
+ tailwindcss()
52
+ ],
53
+ build: {
54
+ outDir,
55
+ emptyOutDir: true,
56
+ chunkSizeWarningLimit: 4096
57
+ }
58
+ }).finally(() => {
59
+ if (nodeEnv === void 0) delete process.env.NODE_ENV;
60
+ else process.env.NODE_ENV = nodeEnv;
61
+ });
62
+ const shell = await shellIdentity();
63
+ await writeFile(path.join(outDir, shellManifestFile), `${JSON.stringify(shell, null, 2)}\n`);
64
+ return shell;
65
+ };
66
+ /** The shell in `dir`, as its protobase.shell.json describes it. */
67
+ const readShell = async (dir) => {
68
+ const file = path.join(dir, shellManifestFile);
69
+ if (!existsSync(file)) throw new Error(`${dir} holds no shell: it has no ${shellManifestFile}`);
70
+ return JSON.parse(await readFile(file, "utf8"));
71
+ };
72
+ /** The shell `protobase serve` has of its own: the installed @protobase/ui's, none when the CLI runs from sources. */
73
+ const installedShellDir = path.extname(uiEntry) === ".js" ? prebuiltShellDir : void 0;
74
+ /**
75
+ * The shell `protobase build` copies into a bundle: the prebuilt one of the installed @protobase/ui, or, when the CLI
76
+ * runs from this repository's sources, one built from them into a scratch folder, so it is never older than the code.
77
+ */
78
+ const shellForBuild = async () => {
79
+ if (installedShellDir) return {
80
+ dir: installedShellDir,
81
+ shell: await readShell(installedShellDir)
82
+ };
83
+ const dir = await mkdtemp(path.join(tmpdir(), "protobase-shell-"));
84
+ return {
85
+ dir,
86
+ shell: await buildShell(dir),
87
+ scratch: true
88
+ };
89
+ };
90
+ //#endregion
91
+ export { buildShell, installedShellDir, prebuiltShellDir, readShell, shellForBuild, shellManifestFile, shellPaths };
@@ -0,0 +1,87 @@
1
+ import { uiEntry } from "../project/admin-app.js";
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { readFile, writeFile } from "node:fs/promises";
5
+ import { transform } from "lightningcss";
6
+ //#region src/build/theme.ts
7
+ /** The project's theme, at its root, and where a bundle's `public` folder has it: the app links it at `/protobase.theme.css`. */
8
+ const themeFile = "protobase.theme.css";
9
+ const tokensFile = path.join(path.dirname(uiEntry), "tokens.css");
10
+ /** The names of the tokens the installed @protobase/ui reads, such as `--primary` and `--radius`. */
11
+ const themeTokens = async (file = tokensFile) => new Set([...(await readFile(file, "utf8")).matchAll(/(--[\w-]+)\s*:/g)].map(([, name]) => name));
12
+ const isThemeSelector = (selector) => {
13
+ if (selector.length !== 1) return false;
14
+ const [part] = selector;
15
+ return part.type === "pseudo-class" && part.kind === "root" || part.type === "attribute" && part.name === "data-theme" && part.operation?.value === "dark";
16
+ };
17
+ const where = (file, line) => `${file}:${line + 1}`;
18
+ const checkRule = (rule, file) => {
19
+ if (rule.type === "font-face") return {
20
+ problems: [],
21
+ tokens: []
22
+ };
23
+ if (rule.type !== "style") {
24
+ const line = ("value" in rule ? rule.value.loc?.line : void 0) ?? 0;
25
+ return {
26
+ problems: [`${where(file, line)}: @${rule.type} is not allowed; a theme holds token values under :root and [data-theme="dark"], and @font-face`],
27
+ tokens: []
28
+ };
29
+ }
30
+ const { selectors, declarations, rules, loc } = rule.value;
31
+ const problems = [];
32
+ if (!selectors.every(isThemeSelector)) problems.push(`${where(file, loc.line)}: a theme sets tokens on :root and [data-theme="dark"] only, not on other selectors`);
33
+ if ((rules ?? []).length > 0) problems.push(`${where(file, loc.line)}: a theme holds no nested rules`);
34
+ const all = [...declarations?.declarations ?? [], ...declarations?.importantDeclarations ?? []];
35
+ const properties = all.filter((declaration) => declaration.property !== "custom").map((declaration) => declaration.property);
36
+ if (properties.length > 0) problems.push(`${where(file, loc.line)}: ${properties.join(", ")} ${properties.length === 1 ? "is no token" : "are no tokens"}; a theme sets only tokens such as --primary`);
37
+ return {
38
+ problems,
39
+ tokens: all.flatMap((declaration) => declaration.property === "custom" ? [{
40
+ name: declaration.value.name,
41
+ line: loc.line
42
+ }] : [])
43
+ };
44
+ };
45
+ /**
46
+ * The theme in `code` minified, once it holds only token values, under :root (light, and dark unless dark sets its
47
+ * own) and [data-theme="dark"], and @font-face rules: no selector into the app, so it works on every version of it.
48
+ * Throws naming each place that holds something else; a token the app does not read is a warning, a likely typo.
49
+ */
50
+ const compileTheme = (code, file, known) => {
51
+ const problems = [];
52
+ const warnings = [];
53
+ const css = transform({
54
+ filename: file,
55
+ code: Buffer.from(code),
56
+ minify: true,
57
+ visitor: { StyleSheet(sheet) {
58
+ for (const rule of sheet.rules) {
59
+ const checked = checkRule(rule, file);
60
+ problems.push(...checked.problems);
61
+ for (const token of checked.tokens) if (!known.has(token.name)) warnings.push(`${where(file, token.line)}: ${token.name} is not a token the app reads; check its name`);
62
+ }
63
+ return sheet;
64
+ } }
65
+ }).code.toString();
66
+ if (problems.length > 0) throw new Error(`${file} is not a theme:\n${problems.map((problem) => ` - ${problem}`).join("\n")}`);
67
+ return {
68
+ css,
69
+ warnings
70
+ };
71
+ };
72
+ /**
73
+ * Writes the project's protobase.theme.css, checked and minified, to `outFile`, and an empty file when it has none, so
74
+ * the stylesheet the app links is always there. Returns the warnings about it.
75
+ */
76
+ const buildTheme = async ({ projectDir, outFile }) => {
77
+ const source = path.join(projectDir, themeFile);
78
+ if (!existsSync(source)) {
79
+ await writeFile(outFile, "");
80
+ return [];
81
+ }
82
+ const { css, warnings } = compileTheme(await readFile(source, "utf8"), themeFile, await themeTokens());
83
+ await writeFile(outFile, css);
84
+ return warnings;
85
+ };
86
+ //#endregion
87
+ export { buildTheme, compileTheme, themeFile, themeTokens };
@@ -1,11 +1,11 @@
1
+ import { projectUiFile, projectUiId, projectUiPlugin } from "../project/project-ui.js";
1
2
  import { extensionImports } from "../project/extensions.js";
2
3
  import { viteBundle } from "./vite-bundle.js";
3
- import { projectUiFile, projectUiId, projectUiPlugin } from "../project/project-ui.js";
4
4
  import { existsSync } from "node:fs";
5
5
  import path from "node:path";
6
6
  import { mkdtemp, rm } from "node:fs/promises";
7
- import { pathToFileURL } from "node:url";
8
7
  import { tmpdir } from "node:os";
8
+ import { pathToFileURL } from "node:url";
9
9
  //#region src/build/ui-check.ts
10
10
  const checkFile = (projectDir) => path.join(projectDir, "protobase-ui-check.js");
11
11
  const checkCode = [
@@ -8,22 +8,32 @@ const bundleLayout = {
8
8
  server: "protobase.config.js",
9
9
  public: "public",
10
10
  spa: "index.html",
11
- api: ["/api"]
11
+ api: ["/api"],
12
+ theme: "protobase.theme.css"
12
13
  };
13
14
  const bundleNodeModules = "node_modules";
14
15
  const isStringList = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
16
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
17
+ const isFieldTypes = (value) => isObject(value) && Object.values(value).every(isStringList);
18
+ const isPlugin = (value) => isObject(value) && typeof value.name === "string" && typeof value.path === "string" && Number.isInteger(value.api) && typeof value.protobase === "string" && (value.fields === void 0 || isFieldTypes(value.fields));
19
+ const isShell = (value) => isObject(value) && typeof value.protobase === "string" && Number.isInteger(value.pluginApi) && isStringList(value.paths);
15
20
  const parseManifest = (value, file) => {
16
21
  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`);
22
+ if (manifest?.version !== 3) throw new Error(`${file} is not a version 3 bundle manifest; rebuild it with \`protobase build\``);
23
+ const { server, public: publicDir, api, spa, plugins, theme, shell, nodeModules } = manifest;
24
+ if (typeof server !== "string" || typeof publicDir !== "string" || typeof spa !== "string" || typeof theme !== "string" || !isStringList(api)) throw new Error(`${file} needs server, public, spa and theme paths and an api list`);
25
+ if (!Array.isArray(plugins) || !plugins.every(isPlugin)) throw new Error(`${file} needs a plugins list, each with a name, path, api and protobase`);
26
+ if (!isShell(shell)) throw new Error(`${file} needs a shell with its protobase version, pluginApi and paths`);
20
27
  if (nodeModules !== void 0 && typeof nodeModules !== "string") throw new Error(`${file} has a nodeModules that is no path`);
21
28
  return {
22
- version: 2,
29
+ version: 3,
23
30
  server,
24
31
  public: publicDir,
25
32
  api,
26
33
  spa,
34
+ plugins,
35
+ theme,
36
+ shell,
27
37
  ...nodeModules === void 0 ? {} : { nodeModules }
28
38
  };
29
39
  };
package/dist/dev/run.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { loadNearestEnvFile } from "./env.js";
2
+ import { projectUiPlugin } from "../project/project-ui.js";
2
3
  import { adminAppDir, adminAppMain, uiPackageDir } from "../project/admin-app.js";
3
4
  import { extensionImports, extensionStyles } from "../project/extensions.js";
5
+ import { sharedModulesPlugin } from "../project/shared-modules.js";
4
6
  import { moduleFile } from "../module-file.js";
5
7
  import { layoutCheck } from "../build/layout-check.js";
6
- import { projectUiPlugin } from "../project/project-ui.js";
7
- import { sharedModulesPlugin } from "../project/shared-modules.js";
8
+ import { themeLink } from "../project/theme-link.js";
8
9
  import { checkDatabase } from "./check-database.js";
9
10
  import { stopDevJobs } from "./dev-jobs.js";
10
11
  import { existsSync } from "node:fs";
@@ -73,6 +74,7 @@ const runDev = async (options, out) => {
73
74
  reloadOnProjectChange([options.projectDir, ...extensions.map((extension) => extension.dir)]),
74
75
  stopJobsOnClose,
75
76
  sharedModulesPlugin(),
77
+ themeLink(options.projectDir),
76
78
  projectUiPlugin(options.projectDir, extensions),
77
79
  extensionImports(options.projectDir, extensions),
78
80
  extensionStyles(extensions),
package/dist/main.js CHANGED
@@ -2,8 +2,9 @@ import { loadNearestEnvFile } from "./dev/env.js";
2
2
  import { registerAuth } from "./auth/register.js";
3
3
  import { manifestFile } from "./bundle/manifest.js";
4
4
  import { resolveExtensions } from "./project/extensions.js";
5
- import { buildDeployBundle } from "./build/deploy-bundle.js";
6
5
  import { buildPlugin, pluginManifestFile } from "./build/plugin-build.js";
6
+ import { buildShell } from "./build/shell.js";
7
+ import { buildDeployBundle } from "./build/deploy-bundle.js";
7
8
  import { buildServeRuntime } from "./build/serve-runtime.js";
8
9
  import { resolveConnectionUrl } from "./db/connect.js";
9
10
  import { parseHosts } from "./dev/hosts.js";
@@ -66,16 +67,19 @@ program.command("dev").description("Serve the admin app and API for the project
66
67
  process.once("SIGINT", () => void server.close().then(() => process.exit(0)));
67
68
  process.once("SIGTERM", () => void server.close().then(() => process.exit(0)));
68
69
  });
69
- 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) => {
70
+ program.command("build").description("Build the deploy bundle of the project in the current directory: its config module, the admin app with the project's UI as a plugin, and the 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) => {
70
71
  const extensions = resolveExtensions(opts.extend);
72
+ const onWarning = (message) => process.stderr.write(`warning: ${message}\n`);
71
73
  const manifest = await buildDeployBundle({
72
74
  projectDir: process.cwd(),
73
75
  outDir: path.resolve(opts.out),
74
76
  bun: opts.bun,
75
- extensions
77
+ extensions,
78
+ onWarning
76
79
  });
77
80
  const packages = manifest.nodeModules ? `, ${manifest.nodeModules}/` : "";
78
- out(`built ${opts.out}: ${manifest.server}${packages}, ${manifest.public}/ and ${manifestFile} (api ${manifest.api.join(", ")})\n`);
81
+ const plugins = manifest.plugins.length > 0 ? ` with plugins ${manifest.plugins.map((plugin) => plugin.name).join(", ")}` : "";
82
+ out(`built ${opts.out}: ${manifest.server}${packages}, ${manifest.public}/${plugins} and ${manifestFile} (api ${manifest.api.join(", ")})\n`);
79
83
  });
80
84
  program.command("build-plugin").description("Build a plugin the admin app loads at runtime: its protobase.ui.tsx as one ES module, its stylesheet and plugin.json").argument("[dir]", "the plugin folder, holding protobase.ui.tsx", ".").option("--out <dir>", "output folder (default: dist in the plugin folder)").option("--name <name>", "the plugin's name (default: its package.json name)").action(async (dir, opts) => {
81
85
  const pluginDir = path.resolve(dir);
@@ -88,6 +92,10 @@ program.command("build-plugin").description("Build a plugin the admin app loads
88
92
  const shown = path.relative(process.cwd(), outDir);
89
93
  out(`built ${shown.startsWith("..") ? outDir : shown || "."}: ${pluginManifestFile}, ${manifest.entry} and ${manifest.css} (${manifest.name}, plugin API ${manifest.api}, Protobase ${manifest.protobase})\n`);
90
94
  });
95
+ program.command("build-shell", { hidden: true }).description("Build the admin app every bundle shares, as @protobase/ui's package and the protobase image ship it").argument("<out>", "output folder").action(async (outDir) => {
96
+ const shell = await buildShell(path.resolve(outDir));
97
+ out(`built ${outDir}: the shell of Protobase ${shell.protobase} (plugin API ${shell.pluginApi})\n`);
98
+ });
91
99
  program.command("build-serve").description("Bundle the serve runtime into one file for Bun: bun --no-install protobase-serve.js <bundle.js>").option("--out <file>", "output file", "dist/protobase-serve.js").option("--bun", "have Bun's bundler write the file (needs bun on PATH)", false).action(async (opts) => {
92
100
  await buildServeRuntime({
93
101
  outFile: path.resolve(opts.out),
package/dist/package.js CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.11.1";
2
+ var version = "0.12.0";
3
3
  //#endregion
4
4
  export { version };
@@ -2,9 +2,10 @@ import { configExports, createAdmin, jobDefinitions } from "@protobase/server";
2
2
  //#region src/project/project-admin.ts
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
- * pages and subscriptions, and the rest of its config. `jobs` are the project's own and the delivery of its events.
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
7
  */
7
- const projectAdmin = (project, config, db) => {
8
+ const projectAdmin = (project, config, db, ui) => {
8
9
  const { resources, views, pages, userMenu, bottomBars, subscriptions } = configExports(config);
9
10
  const own = jobDefinitions(project.jobs ?? {});
10
11
  const admin = createAdmin({
@@ -22,6 +23,10 @@ const projectAdmin = (project, config, db) => {
22
23
  ...project.webhooks && { webhooks: project.webhooks },
23
24
  jobs: own,
24
25
  subscriptions,
26
+ ...ui && {
27
+ bundlePlugins: ui.plugins,
28
+ fieldTypes: ui.fieldTypes
29
+ },
25
30
  options: {
26
31
  onUnhandledError: (error) => console.error(error),
27
32
  ...project.options
@@ -0,0 +1,43 @@
1
+ import { compileTheme, themeFile, themeTokens } from "../build/theme.js";
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { readFile } from "node:fs/promises";
5
+ //#region src/project/theme-link.ts
6
+ /**
7
+ * Links the project's theme at `/protobase.theme.css` in the app's page, after its own stylesheets, so it themes the
8
+ * sign-in page too. A bundle always has the file (see `buildTheme`); with `projectDir`, the dev server answers it from
9
+ * the project's protobase.theme.css, checked as the build checks it, and empty without one.
10
+ */
11
+ const themeLink = (projectDir) => {
12
+ let base = "/";
13
+ return {
14
+ name: "protobase-theme-link",
15
+ configResolved: (config) => {
16
+ base = config.base;
17
+ },
18
+ configureServer: (server) => {
19
+ if (projectDir === void 0) return;
20
+ server.middlewares.use(`/${themeFile}`, (_request, response, next) => {
21
+ const file = path.join(projectDir, themeFile);
22
+ (existsSync(file) ? Promise.all([readFile(file, "utf8"), themeTokens()]).then(([code, tokens]) => compileTheme(code, themeFile, tokens).css) : Promise.resolve("")).then((body) => {
23
+ response.setHeader("content-type", "text/css");
24
+ response.setHeader("cache-control", "no-cache");
25
+ response.end(body);
26
+ }, next);
27
+ });
28
+ },
29
+ transformIndexHtml: {
30
+ order: "post",
31
+ handler: () => [{
32
+ tag: "link",
33
+ attrs: {
34
+ rel: "stylesheet",
35
+ href: `${base}${themeFile}`
36
+ },
37
+ injectTo: "head"
38
+ }]
39
+ }
40
+ };
41
+ };
42
+ //#endregion
43
+ export { themeLink };
@@ -0,0 +1,7 @@
1
+ //#region src/serve/bundle-ui.ts
2
+ const bundleUi = (manifest) => ({
3
+ plugins: manifest.plugins.map((plugin) => `/${plugin.path}`),
4
+ fieldTypes: Object.fromEntries(manifest.plugins.flatMap((plugin) => Object.entries(plugin.fields ?? {})))
5
+ });
6
+ //#endregion
7
+ export { bundleUi };
@@ -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 }) => {
17
+ const startServe = async ({ project, env, createDb = (url) => createPgDb(pg, url), site, ui }) => {
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);
26
+ const { admin, jobs: definitions } = projectAdmin(project, project.config, db, ui);
27
27
  if (definitions.length > 0) await checkJobsSchema(db);
28
28
  const jobs = definitions.length > 0 ? await startJobs({
29
29
  db,
@@ -1,17 +1,22 @@
1
+ import { readManifest } from "../bundle/manifest.js";
2
+ import { bundleUi } from "./bundle-ui.js";
1
3
  import { checkBundleVersion } from "./bundle-version.js";
2
4
  import { readServeEnv } from "./env.js";
3
5
  import { startServe } from "./lifecycle.js";
4
6
  import { loadBundle } from "./load-bundle.js";
5
7
  import { closeOnSignals } from "./signals.js";
8
+ import path from "node:path";
6
9
  //#region src/serve/serve-bundle.ts
7
10
  const serveBundle = async (file, env, out, site) => {
8
11
  const serveEnv = readServeEnv(env);
9
12
  await checkBundleVersion(file);
13
+ const manifest = await readManifest(path.dirname(file));
10
14
  const project = await loadBundle(file);
11
15
  const server = await startServe({
12
16
  project,
13
17
  env: serveEnv,
14
- site
18
+ site,
19
+ ui: bundleUi(manifest)
15
20
  });
16
21
  closeOnSignals(server.close);
17
22
  out(`protobase serve listening on port ${server.port}\n`);
@@ -1,15 +1,22 @@
1
1
  import { readManifest } from "../bundle/manifest.js";
2
+ import { installedShellDir } from "../build/shell.js";
2
3
  import { serveBundle } from "./serve-bundle.js";
4
+ import { servedShell, shellVariable } from "./served-shell.js";
3
5
  import path from "node:path";
4
6
  //#region src/serve/serve-dir.ts
5
7
  const serveBundleDir = async (dir, env, out) => {
6
8
  const manifest = await readManifest(dir);
9
+ const own = env["PROTOBASE_SHELL"] ? path.resolve(env[shellVariable]) : installedShellDir;
10
+ const { shell, note } = await servedShell(manifest, own);
7
11
  const site = {
8
12
  publicDir: path.resolve(dir, manifest.public),
9
13
  spa: manifest.spa,
10
- api: manifest.api
14
+ api: manifest.api,
15
+ ...shell && { shell }
11
16
  };
12
- return serveBundle(path.join(dir, manifest.server), env, out, site);
17
+ const server = await serveBundle(path.join(dir, manifest.server), env, out, site);
18
+ out(` app ${note}\n`);
19
+ return server;
13
20
  };
14
21
  //#endregion
15
22
  export { serveBundleDir };
@@ -0,0 +1,30 @@
1
+ import { readShell } from "../build/shell.js";
2
+ import { pluginProblem } from "@protobase/schema";
3
+ //#region src/serve/served-shell.ts
4
+ /** The environment variable naming a shell folder for `protobase serve`, instead of the installed @protobase/ui's. */
5
+ const shellVariable = "PROTOBASE_SHELL";
6
+ /**
7
+ * The shell a server of its own shell `ownDir` serves a bundle with: its own, when every plugin of the bundle loads in
8
+ * it, so the app follows the server's version without a rebuild; otherwise none, and the bundle's copy serves, with
9
+ * the reason in `note`.
10
+ */
11
+ const servedShell = async (manifest, ownDir) => {
12
+ if (ownDir === void 0) return { note: "the bundle's copy of the app" };
13
+ const own = await readShell(ownDir);
14
+ for (const plugin of manifest.plugins) {
15
+ const problem = pluginProblem(plugin, {
16
+ api: own.pluginApi,
17
+ version: own.protobase
18
+ });
19
+ if (problem) return { note: `the bundle's copy of the app: its plugin ${plugin.name} would be off in the app of Protobase ${own.protobase}, since ${problem}` };
20
+ }
21
+ return {
22
+ shell: {
23
+ dir: ownDir,
24
+ paths: own.paths
25
+ },
26
+ note: `the app of Protobase ${own.protobase} from ${ownDir}`
27
+ };
28
+ };
29
+ //#endregion
30
+ export { servedShell, shellVariable };
@@ -26,20 +26,24 @@ const fileFor = async (publicDir, pathname) => {
26
26
  const index = path.join(file, "index.html");
27
27
  return (await statOrUndefined(index))?.isFile() ? index : void 0;
28
28
  };
29
- const cacheControl = (publicDir, file) => file.startsWith(path.join(publicDir, "assets", path.sep)) ? "public, max-age=31536000, immutable" : "no-cache";
30
- const fileResponse = async (publicDir, file) => new Response(await readFile(file), { headers: {
29
+ const cacheControl = (root, file) => path.relative(root, file).split(path.sep).slice(0, -1).includes("assets") ? "public, max-age=31536000, immutable" : "no-cache";
30
+ const fileResponse = async (root, file) => new Response(await readFile(file), { headers: {
31
31
  "content-type": getMimeType(file) ?? "application/octet-stream",
32
- "cache-control": cacheControl(publicDir, file)
32
+ "cache-control": cacheControl(root, file)
33
33
  } });
34
34
  const isNavigation = (request) => (request.headers.get("accept") ?? "").includes("text/html");
35
- const serveStaticSite = async ({ publicDir, spa }, request) => {
35
+ const isUnder = (paths, pathname) => paths.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`));
36
+ const serveStaticSite = async ({ publicDir, spa, shell }, request) => {
36
37
  if (request.method !== "GET" && request.method !== "HEAD") return new Response("Method Not Allowed", {
37
38
  status: 405,
38
39
  headers: { allow: "GET, HEAD" }
39
40
  });
40
- const file = await fileFor(publicDir, new URL(request.url).pathname);
41
- if (file) return fileResponse(publicDir, file);
42
- if (isNavigation(request)) return fileResponse(publicDir, path.join(publicDir, spa));
41
+ const pathname = new URL(request.url).pathname;
42
+ const appDir = shell?.dir ?? publicDir;
43
+ const root = shell && (pathname === "/" || isUnder(shell.paths, pathname)) ? shell.dir : publicDir;
44
+ const file = await fileFor(root, pathname);
45
+ if (file) return fileResponse(root, file);
46
+ if (isNavigation(request)) return fileResponse(appDir, path.join(appDir, spa));
43
47
  return new Response("Not Found", { status: 404 });
44
48
  };
45
49
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/cli",
3
- "version": "0.11.1",
3
+ "version": "0.12.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.11.1",
38
- "@protobase/query": "0.11.1",
39
- "@protobase/schema": "0.11.1",
40
- "@protobase/server": "0.11.1",
41
- "@protobase/ui": "0.11.1",
37
+ "@protobase/layout": "0.12.0",
38
+ "@protobase/query": "0.12.0",
39
+ "@protobase/schema": "0.12.0",
40
+ "@protobase/server": "0.12.0",
41
+ "@protobase/ui": "0.12.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",
@@ -49,6 +49,7 @@
49
49
  "hono": "^4.13.13",
50
50
  "jose": "^6.2.12",
51
51
  "kysely": "^0.29.6",
52
+ "lightningcss": "^1.33.0",
52
53
  "pg": "^8.23.1",
53
54
  "postgres": "^3.4.9",
54
55
  "ts-morph": "^28.0.0",
@@ -1,38 +0,0 @@
1
- import { adminAppDir } from "../project/admin-app.js";
2
- import { extensionImports, extensionStyles } from "../project/extensions.js";
3
- import { projectUiPlugin } from "../project/project-ui.js";
4
- import { sharedModulesPlugin } from "../project/shared-modules.js";
5
- import { build } from "vite";
6
- import tailwindcss from "@tailwindcss/vite";
7
- import react from "@vitejs/plugin-react";
8
- //#region src/build/ui-bundle.ts
9
- const buildUiBundle = async ({ outDir, projectDir, extensions = [] }) => {
10
- const nodeEnv = process.env.NODE_ENV;
11
- process.env.NODE_ENV = "production";
12
- await build({
13
- configFile: false,
14
- root: adminAppDir,
15
- base: "/",
16
- mode: "production",
17
- publicDir: false,
18
- logLevel: "warn",
19
- plugins: [
20
- sharedModulesPlugin(),
21
- projectUiPlugin(projectDir, extensions),
22
- ...projectDir === void 0 ? [] : [extensionImports(projectDir, extensions)],
23
- extensionStyles(extensions),
24
- react(),
25
- tailwindcss()
26
- ],
27
- build: {
28
- outDir,
29
- emptyOutDir: true,
30
- chunkSizeWarningLimit: 4096
31
- }
32
- }).finally(() => {
33
- if (nodeEnv === void 0) delete process.env.NODE_ENV;
34
- else process.env.NODE_ENV = nodeEnv;
35
- });
36
- };
37
- //#endregion
38
- export { buildUiBundle };