@protobase/cli 0.10.0 → 0.11.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,48 @@
|
|
|
1
|
+
import { ts } from "ts-morph";
|
|
2
|
+
//#region src/build/component-names.ts
|
|
3
|
+
const layoutModule = "@protobase/layout";
|
|
4
|
+
const componentImports = (source) => {
|
|
5
|
+
const named = /* @__PURE__ */ new Set();
|
|
6
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
7
|
+
for (const statement of source.statements) {
|
|
8
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== layoutModule) continue;
|
|
9
|
+
const bindings = statement.importClause?.namedBindings;
|
|
10
|
+
if (bindings && ts.isNamespaceImport(bindings)) namespaces.add(bindings.name.text);
|
|
11
|
+
if (bindings && ts.isNamedImports(bindings)) {
|
|
12
|
+
for (const element of bindings.elements) if ((element.propertyName ?? element.name).text === "component") named.add(element.name.text);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
named,
|
|
17
|
+
namespaces
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The names a module declares with `component('Name')` from @protobase/layout, read from its source: the config does
|
|
22
|
+
* not run at build time. A name that is not a string literal is not seen.
|
|
23
|
+
*/
|
|
24
|
+
const declaredComponents = (code, file) => {
|
|
25
|
+
if (!code.includes(layoutModule)) return [];
|
|
26
|
+
const source = ts.createSourceFile(file, code, ts.ScriptTarget.Latest, true, /\.[jt]sx$/.test(file) ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
27
|
+
const { named, namespaces } = componentImports(source);
|
|
28
|
+
if (named.size === 0 && namespaces.size === 0) return [];
|
|
29
|
+
const isComponent = (callee) => ts.isIdentifier(callee) && named.has(callee.text) || ts.isPropertyAccessExpression(callee) && callee.name.text === "component" && ts.isIdentifier(callee.expression) && namespaces.has(callee.expression.text);
|
|
30
|
+
const names = [];
|
|
31
|
+
const visit = (node) => {
|
|
32
|
+
if (ts.isCallExpression(node) && isComponent(node.expression) && node.arguments[0] && ts.isStringLiteralLike(node.arguments[0])) names.push(node.arguments[0].text);
|
|
33
|
+
ts.forEachChild(node, visit);
|
|
34
|
+
};
|
|
35
|
+
visit(source);
|
|
36
|
+
return names;
|
|
37
|
+
};
|
|
38
|
+
/** Adds the components every bundled module declares to `into`, for the check that the project's UI exports each. */
|
|
39
|
+
const collectComponents = (into) => ({
|
|
40
|
+
name: "protobase-component-names",
|
|
41
|
+
enforce: "pre",
|
|
42
|
+
transform(code, id) {
|
|
43
|
+
if (!/\.[cm]?[jt]sx?$/.test(id.replace(/\?.*$/, ""))) return void 0;
|
|
44
|
+
for (const name of declaredComponents(code, id)) into.add(name);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
//#endregion
|
|
48
|
+
export { collectComponents, declaredComponents };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { hostModuleIds } from "../serve/host-module-ids.js";
|
|
2
2
|
import { extensionImports } from "../project/extensions.js";
|
|
3
|
+
import { collectComponents } from "./component-names.js";
|
|
3
4
|
import { bundleEntryCode } from "./entry-code.js";
|
|
4
5
|
import { layoutCheck } from "./layout-check.js";
|
|
5
6
|
import { findPackageDir, packageName } from "./packages/package-dir.js";
|
|
@@ -52,11 +53,12 @@ const servedImports = (native) => {
|
|
|
52
53
|
}
|
|
53
54
|
};
|
|
54
55
|
};
|
|
55
|
-
const buildConfigBundle = async ({ projectDir, outFile, bun, extensions = [] }) => {
|
|
56
|
+
const buildConfigBundle = async ({ projectDir, outFile, bun, extensions = [], components = /* @__PURE__ */ new Set() }) => {
|
|
56
57
|
const file = path.join(projectDir, "protobase-bundle-entry.js");
|
|
57
58
|
const native = /* @__PURE__ */ new Map();
|
|
58
59
|
const plugins = [
|
|
59
60
|
layoutCheck(),
|
|
61
|
+
collectComponents(components),
|
|
60
62
|
extensionImports(projectDir, extensions),
|
|
61
63
|
servedImports(native),
|
|
62
64
|
entry(file, bundleEntryCode(projectDir, extensions))
|
|
@@ -3,7 +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 {
|
|
6
|
+
import { checkUi } from "./ui-check.js";
|
|
7
7
|
import { existsSync } from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { readFile, rm, writeFile } from "node:fs/promises";
|
|
@@ -20,13 +20,15 @@ const removeOwnNodeModules = async (outDir) => {
|
|
|
20
20
|
};
|
|
21
21
|
const buildDeployBundle = async ({ projectDir, outDir, bun, extensions = [] }) => {
|
|
22
22
|
await removeOwnNodeModules(outDir);
|
|
23
|
-
|
|
23
|
+
const components = /* @__PURE__ */ new Set();
|
|
24
24
|
const native = await buildConfigBundle({
|
|
25
25
|
projectDir,
|
|
26
26
|
outFile: path.join(outDir, bundleLayout.server),
|
|
27
27
|
bun,
|
|
28
|
-
extensions
|
|
28
|
+
extensions,
|
|
29
|
+
components
|
|
29
30
|
});
|
|
31
|
+
await checkUi(projectDir, extensions, components);
|
|
30
32
|
const packages = native.length > 0 ? await copyPackages(native, path.join(outDir, bundleNodeModules)) : [];
|
|
31
33
|
await buildUiBundle({
|
|
32
34
|
outDir: path.join(outDir, bundleLayout.public),
|
package/dist/build/ui-check.js
CHANGED
|
@@ -1,24 +1,42 @@
|
|
|
1
1
|
import { extensionImports } from "../project/extensions.js";
|
|
2
2
|
import { viteBundle } from "./vite-bundle.js";
|
|
3
|
-
import {
|
|
3
|
+
import { projectUiFile, projectUiId, projectUiPlugin } from "../project/project-ui.js";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
6
7
|
import { pathToFileURL } from "node:url";
|
|
7
8
|
import { tmpdir } from "node:os";
|
|
8
9
|
//#region src/build/ui-check.ts
|
|
9
|
-
const
|
|
10
|
+
const checkFile = (projectDir) => path.join(projectDir, "protobase-ui-check.js");
|
|
11
|
+
const checkCode = [
|
|
12
|
+
`import * as ui from '${projectUiId}'`,
|
|
13
|
+
`import { mergeUi, uiFromModule } from '@protobase/ui'`,
|
|
14
|
+
"export default Object.keys(mergeUi(uiFromModule(ui)).components ?? {})",
|
|
15
|
+
""
|
|
16
|
+
].join("\n");
|
|
17
|
+
const checkEntry = (file) => ({
|
|
18
|
+
name: "protobase-ui-check",
|
|
19
|
+
resolveId: (id) => id === file ? `\0${file}` : void 0,
|
|
20
|
+
load: (id) => id === `\0${file}` ? checkCode : void 0
|
|
21
|
+
});
|
|
22
|
+
const registeredComponents = async (projectDir, extensions) => {
|
|
23
|
+
if (!existsSync(projectUiFile(projectDir)) && !extensions.some((extension) => extension.ui)) return [];
|
|
10
24
|
const scratch = await mkdtemp(path.join(tmpdir(), "protobase-ui-check-"));
|
|
11
|
-
const outFile = path.join(scratch, "ui-check.
|
|
25
|
+
const outFile = path.join(scratch, "ui-check.mjs");
|
|
12
26
|
try {
|
|
13
|
-
const plugins = [
|
|
27
|
+
const plugins = [
|
|
28
|
+
checkEntry(checkFile(projectDir)),
|
|
29
|
+
projectUiPlugin(projectDir, extensions),
|
|
30
|
+
extensionImports(projectDir, extensions)
|
|
31
|
+
];
|
|
14
32
|
await viteBundle({
|
|
15
33
|
root: projectDir,
|
|
16
|
-
entry:
|
|
34
|
+
entry: checkFile(projectDir),
|
|
17
35
|
outFile,
|
|
18
36
|
plugins,
|
|
19
37
|
logLevel: "error"
|
|
20
38
|
});
|
|
21
|
-
await import(pathToFileURL(outFile).href);
|
|
39
|
+
return (await import(pathToFileURL(outFile).href)).default;
|
|
22
40
|
} finally {
|
|
23
41
|
await rm(scratch, {
|
|
24
42
|
recursive: true,
|
|
@@ -26,5 +44,17 @@ const checkUiMerge = async (projectDir, extensions) => {
|
|
|
26
44
|
});
|
|
27
45
|
}
|
|
28
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* Fails the build, instead of the app in the browser, when two UI configs define the same component or action
|
|
49
|
+
* handler, or when the config declares a component with `component('Name')` that protobase.ui.tsx does not export.
|
|
50
|
+
* It loads only browser code, which needs no secrets; nothing is rendered.
|
|
51
|
+
*/
|
|
52
|
+
const checkUi = async (projectDir, extensions, declared) => {
|
|
53
|
+
if (declared.size === 0 && !extensions.some((extension) => extension.ui)) return;
|
|
54
|
+
const registered = new Set(await registeredComponents(projectDir, extensions));
|
|
55
|
+
const missing = [...declared].filter((name) => !registered.has(name)).sort();
|
|
56
|
+
if (missing.length === 0) return;
|
|
57
|
+
throw new Error(`The config declares ${missing.map((name) => `component('${name}')`).join(", ")}, but protobase.ui.tsx exports no component by ${missing.length === 1 ? "that name" : "those names"}. Export each from its own file and re-export it, such as: export { ${missing[0]} } from './ui/${missing[0]}'`);
|
|
58
|
+
};
|
|
29
59
|
//#endregion
|
|
30
|
-
export {
|
|
60
|
+
export { checkUi };
|
package/dist/package.js
CHANGED
|
@@ -10,11 +10,11 @@ const mergedUiCode = (projectDir, extensions) => {
|
|
|
10
10
|
const own = projectUiFile(projectDir);
|
|
11
11
|
const hasOwn = existsSync(own);
|
|
12
12
|
return [
|
|
13
|
-
`import { mergeUi } from '@protobase/ui'`,
|
|
13
|
+
`import { mergeUi, uiFromModule } from '@protobase/ui'`,
|
|
14
14
|
...files.map((file, i) => `import * as extension${i} from ${JSON.stringify(file)}`),
|
|
15
15
|
...hasOwn ? [`import * as project from ${JSON.stringify(own)}`] : [],
|
|
16
16
|
"const named = (ui, file) => ({ ...ui, name: ui.name ?? file })",
|
|
17
|
-
`export default mergeUi(${[...files.map((file, i) => `named(extension${i}
|
|
17
|
+
`export default mergeUi(${[...files.map((file, i) => `named(uiFromModule(extension${i}), ${JSON.stringify(file)})`), ...hasOwn ? [`named(uiFromModule(project), "protobase.ui.tsx")`] : []].join(", ")})`,
|
|
18
18
|
""
|
|
19
19
|
].join("\n");
|
|
20
20
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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.11.0",
|
|
38
|
+
"@protobase/query": "0.11.0",
|
|
39
|
+
"@protobase/schema": "0.11.0",
|
|
40
|
+
"@protobase/server": "0.11.0",
|
|
41
|
+
"@protobase/ui": "0.11.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",
|