@dbx-tools/projen 0.9.150 → 0.9.152
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -2
- package/index.ts +5 -1
- package/package.json +6 -5
- package/src/appkit-workspace.ts +146 -0
- package/src/dbx-tools-config.ts +1 -0
- package/src/pnpm-workspace.ts +4 -0
- package/src/project-js.ts +45 -6
- package/src/tsconfig.ts +2 -1
- package/src/workspace-manifest.ts +173 -0
- package/tasks/appkit-deploy.ts +70 -0
- package/tasks/compile-workspace.ts +2 -1
- package/tasks/publish-npm.ts +3 -54
- package/tasks/test-workspace.ts +2 -0
package/README.md
CHANGED
|
@@ -25,6 +25,9 @@ generated-file cleanup and watch tasks.
|
|
|
25
25
|
|
|
26
26
|
Every `src`-bearing folder under the configured roots becomes a
|
|
27
27
|
`DBXToolsTypeScriptProject`. Folder path drives package name and runtime tags.
|
|
28
|
+
Use `packageDefaults` for native options shared by every discovered package,
|
|
29
|
+
such as `{ publishable: false }`; reserve `resolvePackageOptions` for genuine
|
|
30
|
+
per-package differences.
|
|
28
31
|
|
|
29
32
|
Use `DBXToolsTypeScriptProject` itself as the root when one package needs the
|
|
30
33
|
same Bun workspace, repository, task, and release defaults without a separate
|
|
@@ -310,8 +313,34 @@ parameter to `Project`). Drop to `mixin.create(predicate, fn)` +
|
|
|
310
313
|
`project.with(...)` only when you need a predicate the filters cannot express.
|
|
311
314
|
|
|
312
315
|
Built-in tag mixins set runtime defaults for `shared`, `node`, `cli`, `server`,
|
|
313
|
-
and `
|
|
314
|
-
scripts, and generated files on top.
|
|
316
|
+
`ui`, and `app`. Repo-specific mixins layer package-specific dependencies,
|
|
317
|
+
scripts, and generated files on top. `ui` is for reusable React component
|
|
318
|
+
libraries. `app` owns a Bun-built browser application.
|
|
319
|
+
|
|
320
|
+
AppKit mode defaults to `auto`: when the workspace has both `app` and `server`
|
|
321
|
+
packages, the engine adds the AppKit, Vite, React plugin, and Tailwind
|
|
322
|
+
dependencies to the package that uses each one. It also disables AppKit's
|
|
323
|
+
package migration, includes the Vite and generated-type inputs in the browser
|
|
324
|
+
tsconfig, adds `appkit:sync`, and ignores local Databricks state. Set
|
|
325
|
+
`appkit: false` to disable this ownership. Set `appkit: true` to require AppKit
|
|
326
|
+
and fail synthesis when either package role or its required entry files are
|
|
327
|
+
missing. Keep application-specific UI and server dependencies in the consuming
|
|
328
|
+
`.projenrc.ts`. A workspace with one `server` package also receives standard
|
|
329
|
+
`check`, `dev`, and `start` tasks unless the repository defines them.
|
|
330
|
+
|
|
331
|
+
The generated `deploy` task validates with the native Bun build, then delegates
|
|
332
|
+
to `appkit:deploy` unless the repository already owns `deploy`. The AppKit task
|
|
333
|
+
runs `databricks apps deploy --skip-validation` with all forwarded arguments
|
|
334
|
+
from an isolated source snapshot, so the Databricks CLI never runs npm locally.
|
|
335
|
+
Because the remote builder selects npm when no portable pnpm lockfile is
|
|
336
|
+
uploaded, the snapshot replaces `catalog:` and `workspace:` dependency protocols
|
|
337
|
+
with concrete versions and keeps every manifest stable through upload. The
|
|
338
|
+
generated working tree is never modified. The task passes an explicit profile,
|
|
339
|
+
using the supplied flag or resolving one through `@dbx-tools/auth`, and fails
|
|
340
|
+
before staging when neither profile nor complete environment credentials are
|
|
341
|
+
available. A repository-owned deployment flow can run its own Bun checks and
|
|
342
|
+
spawn `appkit:deploy` for the upload step instead of maintaining staging or
|
|
343
|
+
materialization code.
|
|
315
344
|
|
|
316
345
|
Every workspace root exposes a generic development watcher:
|
|
317
346
|
|
package/index.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// Hand edits are overwritten on the next watch; this file is read-only.
|
|
4
4
|
|
|
5
5
|
export const PACKAGE_IDENTIFIER = "@dbx-tools/projen";
|
|
6
|
-
export const PACKAGE_VERSION = "0.9.
|
|
6
|
+
export const PACKAGE_VERSION = "0.9.152";
|
|
7
|
+
export * as appkitWorkspace from "./src/appkit-workspace.ts";
|
|
7
8
|
export * as barrels from "./src/barrels.ts";
|
|
8
9
|
export * as bunApp from "./src/bun-app.ts";
|
|
9
10
|
export * as bunWorkflow from "./src/bun-workflow.ts";
|
|
@@ -36,7 +37,10 @@ export * as tsconfig from "./src/tsconfig.ts";
|
|
|
36
37
|
export * as vscode from "./src/vscode.ts";
|
|
37
38
|
export * as watch from "./src/watch.ts";
|
|
38
39
|
export * as workspaceLock from "./src/workspace-lock.ts";
|
|
40
|
+
export * as workspaceManifest from "./src/workspace-manifest.ts";
|
|
39
41
|
export * as workspaceVersion from "./src/workspace-version.ts";
|
|
42
|
+
export { AppKitWorkspace } from "./src/appkit-workspace.ts";
|
|
43
|
+
export type { AppKitWorkspaceMode, AppKitWorkspaceOptions } from "./src/appkit-workspace.ts";
|
|
40
44
|
export { BUN_DEV_OVERRIDE, BUN_BUILD_OVERRIDE, BUN_APP_OVERRIDES, RootBunfigFile, BunfigFile, BunDevServerFile, BunBuildFile } from "./src/bun-app.ts";
|
|
41
45
|
export { BUN_VERSION, MINIMUM_BUN_VERSION, BUN_VERSION_CHECK_SCRIPT } from "./src/bun-workflow.ts";
|
|
42
46
|
export { DBXToolsConfigDataSchema, DBXToolsConfig } from "./src/dbx-tools-config.ts";
|
package/package.json
CHANGED
|
@@ -30,10 +30,11 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@clack/prompts": "^1.7.0",
|
|
33
|
-
"@dbx-tools/
|
|
34
|
-
"@dbx-tools/
|
|
35
|
-
"@dbx-tools/
|
|
36
|
-
"@dbx-tools/
|
|
33
|
+
"@dbx-tools/auth": "^0.9.152",
|
|
34
|
+
"@dbx-tools/cli-args": "^0.9.152",
|
|
35
|
+
"@dbx-tools/core": "^0.9.152",
|
|
36
|
+
"@dbx-tools/path": "^0.9.152",
|
|
37
|
+
"@dbx-tools/shared-core": "^0.9.152",
|
|
37
38
|
"commander": "^15.0.0",
|
|
38
39
|
"concurrently": "^10.0.3",
|
|
39
40
|
"constructs": "^10.6.0",
|
|
@@ -55,7 +56,7 @@
|
|
|
55
56
|
},
|
|
56
57
|
"main": "index.ts",
|
|
57
58
|
"license": "Apache-2.0",
|
|
58
|
-
"version": "0.9.
|
|
59
|
+
"version": "0.9.152",
|
|
59
60
|
"types": "index.ts",
|
|
60
61
|
"type": "module",
|
|
61
62
|
"exports": {
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AppKit workspace conventions for paired browser and server packages.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { join, relative, sep } from "node:path";
|
|
8
|
+
import { Component, type Task, javascript, typescript } from "projen";
|
|
9
|
+
|
|
10
|
+
/** AppKit workspace discovery and validation mode. */
|
|
11
|
+
export type AppKitWorkspaceMode = "auto" | boolean;
|
|
12
|
+
|
|
13
|
+
/** Projects configured by the AppKit workspace owner. */
|
|
14
|
+
export interface AppKitWorkspaceOptions {
|
|
15
|
+
/** Browser application packages carrying the `app` tag. */
|
|
16
|
+
readonly apps: readonly javascript.NodeProject[];
|
|
17
|
+
/** AppKit backend packages carrying the `server` tag. */
|
|
18
|
+
readonly servers: readonly javascript.NodeProject[];
|
|
19
|
+
/** Fail synthesis when an AppKit package or required entry file is missing. */
|
|
20
|
+
readonly validate?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Own the dependencies and generated configuration shared by AppKit workspaces.
|
|
25
|
+
*/
|
|
26
|
+
export class AppKitWorkspace extends Component {
|
|
27
|
+
private readonly deployTask: Task;
|
|
28
|
+
private readonly serverEntrypoint?: string;
|
|
29
|
+
|
|
30
|
+
constructor(project: javascript.NodeProject, options: AppKitWorkspaceOptions) {
|
|
31
|
+
super(project);
|
|
32
|
+
|
|
33
|
+
if (options.validate) validateWorkspace(options);
|
|
34
|
+
project.gitignore.addPatterns(".databricks");
|
|
35
|
+
project.addTask("appkit:sync", {
|
|
36
|
+
description: "Synchronize installed AppKit plugin metadata",
|
|
37
|
+
exec: "appkit plugin sync --write --silent",
|
|
38
|
+
});
|
|
39
|
+
this.deployTask = project.addTask("appkit:deploy", {
|
|
40
|
+
description: "Deploy the AppKit workspace from an npm-compatible source snapshot",
|
|
41
|
+
execArgs: ["bun", "node_modules/@dbx-tools/projen/tasks/appkit-deploy.ts"],
|
|
42
|
+
receiveArgs: true,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
for (const app of options.apps) configureApp(app);
|
|
46
|
+
for (const server of options.servers) configureServer(server);
|
|
47
|
+
if (options.servers.length === 1) {
|
|
48
|
+
const server = options.servers[0]!;
|
|
49
|
+
const entrypoint = serverEntrypoint(server);
|
|
50
|
+
this.serverEntrypoint = relative(project.outdir, join(server.outdir, entrypoint))
|
|
51
|
+
.split(sep)
|
|
52
|
+
.join("/");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public override preSynthesize(): void {
|
|
57
|
+
let check = this.project.tasks.tryFind("check");
|
|
58
|
+
if (!check) {
|
|
59
|
+
check = this.project.addTask("check", {
|
|
60
|
+
description: "Validate the generated workspace with Bun",
|
|
61
|
+
});
|
|
62
|
+
check.spawn(this.project.buildTask);
|
|
63
|
+
}
|
|
64
|
+
if (!this.project.tasks.tryFind("deploy")) {
|
|
65
|
+
const deploy = this.project.addTask("deploy", {
|
|
66
|
+
description: "Validate with Bun and deploy the AppKit workspace",
|
|
67
|
+
});
|
|
68
|
+
deploy.spawn(check);
|
|
69
|
+
deploy.spawn(this.deployTask);
|
|
70
|
+
}
|
|
71
|
+
if (!this.serverEntrypoint) return;
|
|
72
|
+
if (!this.project.tasks.tryFind("dev")) {
|
|
73
|
+
this.project.addTask("dev", {
|
|
74
|
+
description: "Run the AppKit server in development mode",
|
|
75
|
+
env: { NODE_ENV: "development" },
|
|
76
|
+
exec: `bun --watch ${this.serverEntrypoint}`,
|
|
77
|
+
receiveArgs: true,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (!this.project.tasks.tryFind("start")) {
|
|
81
|
+
this.project.addTask("start", {
|
|
82
|
+
description: "Run the AppKit server in production mode",
|
|
83
|
+
env: { NODE_ENV: "production" },
|
|
84
|
+
exec: `bun ${this.serverEntrypoint}`,
|
|
85
|
+
receiveArgs: true,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Validate the explicit AppKit convention before generating dependent config. */
|
|
92
|
+
function validateWorkspace(options: AppKitWorkspaceOptions): void {
|
|
93
|
+
const missing: string[] = [];
|
|
94
|
+
if (options.apps.length === 0) missing.push("an `app` package");
|
|
95
|
+
if (options.servers.length === 0) missing.push("a `server` package");
|
|
96
|
+
for (const app of options.apps) {
|
|
97
|
+
for (const file of ["index.html", "vite.config.ts"]) {
|
|
98
|
+
if (!existsSync(join(app.outdir, file))) missing.push(`${app.name}/${file}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const server of options.servers) {
|
|
102
|
+
const entrypoint = serverEntrypoint(server);
|
|
103
|
+
if (!existsSync(join(server.outdir, entrypoint))) {
|
|
104
|
+
missing.push(`${server.name}/${entrypoint}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (missing.length > 0) {
|
|
108
|
+
throw new Error(`appkit: true requires ${missing.join(", ")}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Configure one AppKit browser application without broadening React libraries. */
|
|
113
|
+
function configureApp(project: javascript.NodeProject): void {
|
|
114
|
+
project.package.addField("appkit", { autoMigrate: false });
|
|
115
|
+
project.addDeps(
|
|
116
|
+
"@databricks/appkit-ui@catalog:",
|
|
117
|
+
"@tailwindcss/vite@catalog:",
|
|
118
|
+
"tailwindcss@catalog:",
|
|
119
|
+
);
|
|
120
|
+
project.addDevDeps("vite@catalog:");
|
|
121
|
+
|
|
122
|
+
if (!(project instanceof typescript.TypeScriptProject)) return;
|
|
123
|
+
project.tsconfig?.file.addOverride("compilerOptions.rootDir", ".");
|
|
124
|
+
project.tsconfig?.addInclude("vite.config.ts");
|
|
125
|
+
project.tsconfig?.addInclude("shared/appkit-types");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Configure one AppKit server with the runtime dependency its Vite loader imports. */
|
|
129
|
+
function configureServer(project: javascript.NodeProject): void {
|
|
130
|
+
const entrypoint = serverEntrypoint(project);
|
|
131
|
+
if (entrypoint !== project.package.manifest.main) {
|
|
132
|
+
project.package.addField("main", entrypoint);
|
|
133
|
+
project.package.addField("types", entrypoint);
|
|
134
|
+
}
|
|
135
|
+
project.addDeps("@databricks/appkit@catalog:", "@vitejs/plugin-react@catalog:");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Prefer the canonical AppKit server entry when a package still has its default barrel. */
|
|
139
|
+
function serverEntrypoint(project: javascript.NodeProject): string {
|
|
140
|
+
const configured =
|
|
141
|
+
typeof project.package.manifest.main === "string" ? project.package.manifest.main : "index.ts";
|
|
142
|
+
const canonical = "src/server.ts";
|
|
143
|
+
return configured === "index.ts" && existsSync(join(project.outdir, canonical))
|
|
144
|
+
? canonical
|
|
145
|
+
: configured;
|
|
146
|
+
}
|
package/src/dbx-tools-config.ts
CHANGED
|
@@ -73,6 +73,7 @@ export class DBXToolsConfig {
|
|
|
73
73
|
this.tags = [...(options.tags ?? [])];
|
|
74
74
|
this.codegenInputs = [...(options.codegenInputs ?? [])];
|
|
75
75
|
this.publishable = options.publishable ?? true;
|
|
76
|
+
project.package.addField("private", () => (this.publishable ? undefined : true));
|
|
76
77
|
project.package.addField(CONFIG_KEY, () => {
|
|
77
78
|
const data = this.data();
|
|
78
79
|
return Object.keys(data).length === 0 ? undefined : data;
|
package/src/pnpm-workspace.ts
CHANGED
|
@@ -62,6 +62,10 @@ const DEFAULT_CATALOG: Catalog = {
|
|
|
62
62
|
"react-dom": "~19.2.4",
|
|
63
63
|
"@types/react": "^19.2.2",
|
|
64
64
|
"@types/react-dom": "^19.2.2",
|
|
65
|
+
tailwindcss: "^4.3.2",
|
|
66
|
+
"@tailwindcss/vite": "^4.3.2",
|
|
67
|
+
vite: "^7.2.4",
|
|
68
|
+
"@vitejs/plugin-react": "^5.1.1",
|
|
65
69
|
// Tailwind v4 compiler for bun's dev server + `Bun.build` (the `app` tag).
|
|
66
70
|
"bun-plugin-tailwind": "^0.1.2",
|
|
67
71
|
"@types/node": "^24.6.0",
|
package/src/project-js.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { type IConstruct } from "constructs";
|
|
|
15
15
|
import { Component, LogLevel, Project, type Task, TextFile, javascript, typescript } from "projen";
|
|
16
16
|
import { JobPermission, type JobStep } from "projen/lib/github/workflows-model";
|
|
17
17
|
import type { ReleaseProjectOptions } from "projen/lib/release";
|
|
18
|
+
import { AppKitWorkspace, type AppKitWorkspaceMode } from "./appkit-workspace.ts";
|
|
18
19
|
import { generateBarrels, isGeneratedBarrel } from "./barrels.ts";
|
|
19
20
|
|
|
20
21
|
import { BUN_APP_OVERRIDES, RootBunfigFile } from "./bun-app.ts";
|
|
@@ -603,6 +604,8 @@ export type DBXToolsJavaScriptProjectOptions = CommonProjectOptions &
|
|
|
603
604
|
* disable. Defaults to `"node"`.
|
|
604
605
|
*/
|
|
605
606
|
readonly omitRelativePrefix?: OneOrMany<string>;
|
|
607
|
+
/** Native defaults merged into every filesystem-discovered package. */
|
|
608
|
+
readonly packageDefaults?: Omit<DiscoveredTypeScriptPackageOptions, "name">;
|
|
606
609
|
/** Resolve native constructor fields before a discovered package is constructed. */
|
|
607
610
|
readonly resolvePackageOptions?: (
|
|
608
611
|
pkg: DiscoveredPackage,
|
|
@@ -620,6 +623,11 @@ export type DBXToolsJavaScriptProjectOptions = CommonProjectOptions &
|
|
|
620
623
|
* a list = only those tags.
|
|
621
624
|
*/
|
|
622
625
|
readonly defaultTagMixins?: false | PackageTag[];
|
|
626
|
+
/**
|
|
627
|
+
* Apply AppKit conventions to `app` and `server` packages. Defaults to
|
|
628
|
+
* `auto`; `true` also validates the required package roles and entry files.
|
|
629
|
+
*/
|
|
630
|
+
readonly appkit?: AppKitWorkspaceMode;
|
|
623
631
|
/**
|
|
624
632
|
* Extra repo-root paths that trigger a full re-synth during `sync --watch`
|
|
625
633
|
* (alongside `.projenrc.ts`). Repo-relative, e.g. `".example.projenrc.ts"`.
|
|
@@ -679,7 +687,7 @@ export class DBXToolsNodeProject
|
|
|
679
687
|
readonly language = "javascript" as const;
|
|
680
688
|
readonly scope: string;
|
|
681
689
|
readonly dbxToolsConfig: DBXToolsConfig;
|
|
682
|
-
pnpmWorkspace
|
|
690
|
+
pnpmWorkspace: PnpmWorkspaceState;
|
|
683
691
|
rootTsconfig?: DBXToolsRootTsconfig;
|
|
684
692
|
readonly extraWorkspaceMembers: readonly string[];
|
|
685
693
|
readonly releaseBranch: string;
|
|
@@ -1007,7 +1015,10 @@ class WorkspaceValidationTasks extends Component {
|
|
|
1007
1015
|
return;
|
|
1008
1016
|
}
|
|
1009
1017
|
project.compileTask.execArgs(taskCommand("compile-workspace.ts"));
|
|
1010
|
-
project.
|
|
1018
|
+
const testAll = project.tasks.tryFind("test:all")!;
|
|
1019
|
+
const pythonTests = project.tasks.tryFind("py:test");
|
|
1020
|
+
if (pythonTests) testAll.spawn(pythonTests);
|
|
1021
|
+
project.testTask.spawn(testAll);
|
|
1011
1022
|
}
|
|
1012
1023
|
}
|
|
1013
1024
|
|
|
@@ -1281,7 +1292,14 @@ function initProject(
|
|
|
1281
1292
|
// What the dot group was actually earning here, named explicitly: secrets and
|
|
1282
1293
|
// local editor state. Both ignore CONTENTS (`.idea/*`) rather than the
|
|
1283
1294
|
// directory, so a later `!` negation can still reach a file inside.
|
|
1284
|
-
project.gitignore.addPatterns(
|
|
1295
|
+
project.gitignore.addPatterns(
|
|
1296
|
+
".env",
|
|
1297
|
+
".env.*",
|
|
1298
|
+
"!.env.example",
|
|
1299
|
+
"!.env.sample",
|
|
1300
|
+
".idea/*",
|
|
1301
|
+
".npmrc",
|
|
1302
|
+
);
|
|
1285
1303
|
for (const root of roots) {
|
|
1286
1304
|
project.annotateGenerated(`/${root}/**/index.ts`);
|
|
1287
1305
|
project.annotateGenerated(`/${root}/**/src/generated/**`);
|
|
@@ -1396,9 +1414,11 @@ function initProject(
|
|
|
1396
1414
|
continue;
|
|
1397
1415
|
}
|
|
1398
1416
|
const defaultPackageName = packageNameFor(project.scope, p.relPath, omitPrefixes);
|
|
1399
|
-
const
|
|
1400
|
-
|
|
1401
|
-
|
|
1417
|
+
const defaults = {
|
|
1418
|
+
name: defaultPackageName,
|
|
1419
|
+
...options.packageDefaults,
|
|
1420
|
+
} satisfies DiscoveredTypeScriptPackageOptions;
|
|
1421
|
+
const packageOptions = options.resolvePackageOptions?.(p, defaults) ?? defaults;
|
|
1402
1422
|
new DBXToolsTypeScriptProject({
|
|
1403
1423
|
parent: project,
|
|
1404
1424
|
outdir: p.memberPath,
|
|
@@ -1417,6 +1437,25 @@ function initProject(
|
|
|
1417
1437
|
if (enabledTagMixins.length) {
|
|
1418
1438
|
project.with(...enabledTagMixins.map((t) => PACKAGE_TAG_MIXINS[t]));
|
|
1419
1439
|
}
|
|
1440
|
+
const appkitMode = options.appkit ?? "auto";
|
|
1441
|
+
if (appkitMode !== false) {
|
|
1442
|
+
const projects = [project, ...project.subprojects].filter(
|
|
1443
|
+
(candidate): candidate is DBXToolsNodeProject | DBXToolsTypeScriptProject =>
|
|
1444
|
+
candidate instanceof DBXToolsNodeProject || candidate instanceof DBXToolsTypeScriptProject,
|
|
1445
|
+
);
|
|
1446
|
+
const apps = projects.filter((candidate) => candidate.dbxToolsConfig.tags.includes("app"));
|
|
1447
|
+
const servers = projects.filter((candidate) =>
|
|
1448
|
+
candidate.dbxToolsConfig.tags.includes("server"),
|
|
1449
|
+
);
|
|
1450
|
+
const enabled = appkitMode === true || (apps.length > 0 && servers.length > 0);
|
|
1451
|
+
if (enabled) {
|
|
1452
|
+
new AppKitWorkspace(project, {
|
|
1453
|
+
apps,
|
|
1454
|
+
servers,
|
|
1455
|
+
validate: appkitMode === true,
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1420
1459
|
|
|
1421
1460
|
new WorkspaceValidationTasks(project);
|
|
1422
1461
|
new PrettierIgnoreGenerated(project);
|
package/src/tsconfig.ts
CHANGED
|
@@ -56,8 +56,9 @@ export class DBXToolsRootTsconfig extends Component {
|
|
|
56
56
|
compilerOptions: {
|
|
57
57
|
lib: ["ESNext"],
|
|
58
58
|
types: ["node", "bun"],
|
|
59
|
+
jsx: javascript.TypeScriptJsxMode.REACT_JSX,
|
|
59
60
|
},
|
|
60
|
-
include: [".projenrc.ts"],
|
|
61
|
+
include: [".projenrc.ts", "test/**/*.ts", "test/**/*.tsx"],
|
|
61
62
|
exclude: ["node_modules", "**/dist", "**/node_modules"],
|
|
62
63
|
});
|
|
63
64
|
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace dependency protocol materialization for external npm consumers.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
chmodSync,
|
|
8
|
+
copyFileSync,
|
|
9
|
+
cpSync,
|
|
10
|
+
existsSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
mkdtempSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
statSync,
|
|
16
|
+
writeFileSync,
|
|
17
|
+
} from "node:fs";
|
|
18
|
+
import { tmpdir } from "node:os";
|
|
19
|
+
import { dirname, join, resolve } from "node:path";
|
|
20
|
+
import * as exec from "@dbx-tools/core/exec";
|
|
21
|
+
|
|
22
|
+
const DEPENDENCY_FIELDS = [
|
|
23
|
+
"dependencies",
|
|
24
|
+
"devDependencies",
|
|
25
|
+
"optionalDependencies",
|
|
26
|
+
"peerDependencies",
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
interface StoredManifest {
|
|
30
|
+
readonly path: string;
|
|
31
|
+
readonly mode: number;
|
|
32
|
+
readonly text: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Replace workspace and catalogue protocols with concrete registry ranges. */
|
|
36
|
+
export function materializeWorkspaceManifest(
|
|
37
|
+
source: Readonly<Record<string, unknown>>,
|
|
38
|
+
workspace: Readonly<Record<string, unknown>>,
|
|
39
|
+
): Readonly<Record<string, unknown>> {
|
|
40
|
+
const manifest = structuredClone(source) as Record<string, unknown>;
|
|
41
|
+
const version = workspace.version;
|
|
42
|
+
const catalog =
|
|
43
|
+
workspace.catalog && typeof workspace.catalog === "object"
|
|
44
|
+
? (workspace.catalog as Readonly<Record<string, unknown>>)
|
|
45
|
+
: {};
|
|
46
|
+
if (typeof version !== "string" || !version) {
|
|
47
|
+
throw new Error("Workspace manifest has no version");
|
|
48
|
+
}
|
|
49
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
50
|
+
const dependencies = manifest[field];
|
|
51
|
+
if (!dependencies || typeof dependencies !== "object") continue;
|
|
52
|
+
manifest[field] = Object.fromEntries(
|
|
53
|
+
Object.entries(dependencies).map(([name, value]) => [
|
|
54
|
+
name,
|
|
55
|
+
materializeDependency(name, value, version, catalog),
|
|
56
|
+
]),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return manifest;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Materialize every generated workspace manifest for one external command and
|
|
64
|
+
* restore exact bytes and permissions afterward.
|
|
65
|
+
*/
|
|
66
|
+
export async function withMaterializedWorkspaceManifests<T>(
|
|
67
|
+
root: string,
|
|
68
|
+
callback: () => Promise<T>,
|
|
69
|
+
): Promise<T> {
|
|
70
|
+
const rootManifestPath = resolve(root, "package.json");
|
|
71
|
+
const rootManifest = readManifest(rootManifestPath);
|
|
72
|
+
const workspaces = Array.isArray(rootManifest.workspaces)
|
|
73
|
+
? rootManifest.workspaces.filter((value): value is string => typeof value === "string")
|
|
74
|
+
: [];
|
|
75
|
+
const originals = [
|
|
76
|
+
rootManifestPath,
|
|
77
|
+
...workspaces.map((workspace) => resolve(root, workspace, "package.json")),
|
|
78
|
+
].map(storeManifest);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
for (const original of originals) {
|
|
82
|
+
const source = JSON.parse(original.text) as Record<string, unknown>;
|
|
83
|
+
writeManifest(original.path, materializeWorkspaceManifest(source, rootManifest));
|
|
84
|
+
}
|
|
85
|
+
return await callback();
|
|
86
|
+
} finally {
|
|
87
|
+
for (const original of originals) restoreManifest(original);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Copy tracked and non-ignored source into a temporary directory, materialize
|
|
93
|
+
* its manifests, and keep that snapshot stable for one external command.
|
|
94
|
+
*/
|
|
95
|
+
export async function withStagedWorkspaceManifests<T>(
|
|
96
|
+
root: string,
|
|
97
|
+
callback: (stage: string) => Promise<T>,
|
|
98
|
+
): Promise<T> {
|
|
99
|
+
const stage = mkdtempSync(join(tmpdir(), "dbx-tools-app-deploy-"));
|
|
100
|
+
try {
|
|
101
|
+
const listed = exec.spawnSync(
|
|
102
|
+
"git",
|
|
103
|
+
["ls-files", "--cached", "--others", "--exclude-standard", "-z"],
|
|
104
|
+
{
|
|
105
|
+
cwd: root,
|
|
106
|
+
stdin: "ignore",
|
|
107
|
+
stdout: "capture",
|
|
108
|
+
stderr: "capture",
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
if (listed.exitCode !== 0) {
|
|
112
|
+
throw new Error(`Could not enumerate app source: ${listed.stderr}`);
|
|
113
|
+
}
|
|
114
|
+
for (const relativePath of listed.stdout.split("\0").filter(Boolean)) {
|
|
115
|
+
const source = resolve(root, relativePath);
|
|
116
|
+
if (!existsSync(source)) continue;
|
|
117
|
+
const destination = resolve(stage, relativePath);
|
|
118
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
119
|
+
copyFileSync(source, destination);
|
|
120
|
+
chmodSync(destination, statSync(source).mode & 0o777);
|
|
121
|
+
}
|
|
122
|
+
const bundleState = resolve(root, ".databricks");
|
|
123
|
+
if (existsSync(bundleState)) {
|
|
124
|
+
cpSync(bundleState, resolve(stage, ".databricks"), { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
return await withMaterializedWorkspaceManifests(stage, () => callback(stage));
|
|
127
|
+
} finally {
|
|
128
|
+
rmSync(stage, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function materializeDependency(
|
|
133
|
+
name: string,
|
|
134
|
+
value: unknown,
|
|
135
|
+
version: string,
|
|
136
|
+
catalog: Readonly<Record<string, unknown>>,
|
|
137
|
+
): unknown {
|
|
138
|
+
if (value === "catalog:") {
|
|
139
|
+
const resolved = catalog[name];
|
|
140
|
+
if (typeof resolved !== "string" || !resolved) {
|
|
141
|
+
throw new Error(`Workspace catalog has no version for ${name}`);
|
|
142
|
+
}
|
|
143
|
+
return resolved;
|
|
144
|
+
}
|
|
145
|
+
if (typeof value !== "string" || !value.startsWith("workspace:")) return value;
|
|
146
|
+
const selector = value.slice("workspace:".length);
|
|
147
|
+
if (selector === "*" || !selector) return version;
|
|
148
|
+
if (selector === "^" || selector === "~") return `${selector}${version}`;
|
|
149
|
+
return selector;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function readManifest(path: string): Record<string, unknown> {
|
|
153
|
+
return JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function storeManifest(path: string): StoredManifest {
|
|
157
|
+
return {
|
|
158
|
+
path,
|
|
159
|
+
mode: statSync(path).mode & 0o777,
|
|
160
|
+
text: readFileSync(path, "utf8"),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function writeManifest(path: string, manifest: Readonly<Record<string, unknown>>): void {
|
|
165
|
+
chmodSync(path, 0o644);
|
|
166
|
+
writeFileSync(path, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function restoreManifest(manifest: StoredManifest): void {
|
|
170
|
+
chmodSync(manifest.path, 0o644);
|
|
171
|
+
writeFileSync(manifest.path, manifest.text);
|
|
172
|
+
chmodSync(manifest.path, manifest.mode);
|
|
173
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env -S bun
|
|
2
|
+
/** Deploy an AppKit workspace through npm-compatible temporary manifests. */
|
|
3
|
+
import { profile } from "@dbx-tools/auth";
|
|
4
|
+
import * as exec from "@dbx-tools/core/exec";
|
|
5
|
+
import { runTaskMain } from "./cli.ts";
|
|
6
|
+
import { withStagedWorkspaceManifests } from "../src/workspace-manifest.ts";
|
|
7
|
+
|
|
8
|
+
/** Effects accepted by the AppKit deploy task for focused tests. */
|
|
9
|
+
export interface AppKitDeployDependencies {
|
|
10
|
+
readonly environment?: NodeJS.ProcessEnv;
|
|
11
|
+
readonly root?: string;
|
|
12
|
+
readonly resolveProfile?: () => string | undefined;
|
|
13
|
+
readonly run?: (args: readonly string[], cwd: string) => Promise<{ readonly exitCode: number }>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Run Databricks Apps deploy while generated workspace manifests are materialized. */
|
|
17
|
+
export async function deployApp(
|
|
18
|
+
args: readonly string[],
|
|
19
|
+
dependencies: AppKitDeployDependencies = {},
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
const root = dependencies.root ?? process.cwd();
|
|
22
|
+
const deployArgs = resolveDeployArgs(args, dependencies);
|
|
23
|
+
const run =
|
|
24
|
+
dependencies.run ??
|
|
25
|
+
((deployArgs: readonly string[], cwd: string) =>
|
|
26
|
+
exec.spawn("databricks", ["apps", "deploy", ...deployArgs], {
|
|
27
|
+
cwd,
|
|
28
|
+
env: dependencies.environment ?? process.env,
|
|
29
|
+
stdin: "inherit",
|
|
30
|
+
stdout: "inherit",
|
|
31
|
+
stderr: "inherit",
|
|
32
|
+
}));
|
|
33
|
+
await withStagedWorkspaceManifests(root, async (stage) => {
|
|
34
|
+
const result = await run(deployArgs, stage);
|
|
35
|
+
if (result.exitCode !== 0) {
|
|
36
|
+
throw new Error(`databricks apps deploy exited with code ${result.exitCode}`);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveDeployArgs(
|
|
42
|
+
args: readonly string[],
|
|
43
|
+
dependencies: AppKitDeployDependencies,
|
|
44
|
+
): readonly string[] {
|
|
45
|
+
const environment = dependencies.environment ?? process.env;
|
|
46
|
+
const hasProfile = args.some(
|
|
47
|
+
(argument) =>
|
|
48
|
+
argument === "--profile" || argument === "-p" || argument.startsWith("--profile="),
|
|
49
|
+
);
|
|
50
|
+
const hasEnvironmentAuth =
|
|
51
|
+
Boolean(environment.DATABRICKS_HOST) &&
|
|
52
|
+
(Boolean(environment.DATABRICKS_TOKEN) ||
|
|
53
|
+
(Boolean(environment.DATABRICKS_CLIENT_ID) && Boolean(environment.DATABRICKS_CLIENT_SECRET)));
|
|
54
|
+
const resolvedProfile =
|
|
55
|
+
hasProfile || hasEnvironmentAuth
|
|
56
|
+
? undefined
|
|
57
|
+
: (dependencies.resolveProfile ?? (() => profile.resolveProfile()?.name))();
|
|
58
|
+
if (!hasProfile && !hasEnvironmentAuth && !resolvedProfile) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
"Select a Databricks profile with --profile or configure Databricks environment credentials",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return [
|
|
64
|
+
...(args.includes("--skip-validation") ? [] : ["--skip-validation"]),
|
|
65
|
+
...args,
|
|
66
|
+
...(resolvedProfile ? ["--profile", resolvedProfile] : []),
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await runTaskMain(import.meta, () => deployApp(process.argv.slice(2)));
|
|
@@ -50,7 +50,8 @@ function hasLifecycle(task: TaskSpec | undefined): boolean {
|
|
|
50
50
|
/** Read the root's concrete workspace paths on every run, including newly added members. */
|
|
51
51
|
export function compilePlan(root: string): CompilePlan {
|
|
52
52
|
const workspace = readJson<PackageManifest>(join(root, "package.json"));
|
|
53
|
-
const
|
|
53
|
+
const rootConfig = join(root, "tsconfig.json");
|
|
54
|
+
const typescriptConfigs: string[] = existsSync(rootConfig) ? [rootConfig] : [];
|
|
54
55
|
const customPackages: CompilePlan["customPackages"] = [];
|
|
55
56
|
for (const member of workspace.workspaces ?? []) {
|
|
56
57
|
const directory = resolve(root, member);
|
package/tasks/publish-npm.ts
CHANGED
|
@@ -20,6 +20,9 @@ import { z } from "zod";
|
|
|
20
20
|
import { runTaskMain, taskCommand, taskOptions } from "./cli.ts";
|
|
21
21
|
import { TaskConcurrencyOptionSchema, TaskDryRunOptionSchema } from "./options.ts";
|
|
22
22
|
import { runTaskCommandAsync } from "../src/_task-command.ts";
|
|
23
|
+
import { materializeWorkspaceManifest } from "../src/workspace-manifest.ts";
|
|
24
|
+
|
|
25
|
+
export { materializeWorkspaceManifest } from "../src/workspace-manifest.ts";
|
|
23
26
|
|
|
24
27
|
const DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
25
28
|
const DEFAULT_PUBLISH_CONCURRENCY = 4;
|
|
@@ -52,60 +55,6 @@ export type NpmArchiveManifestTransform = (
|
|
|
52
55
|
|
|
53
56
|
/** Entry-point fields projected from publishConfig into a packed manifest. */
|
|
54
57
|
const PUBLISH_CONFIG_ENTRY_FIELDS = ["main", "types", "bin", "exports"] as const;
|
|
55
|
-
const DEPENDENCY_FIELDS = [
|
|
56
|
-
"dependencies",
|
|
57
|
-
"devDependencies",
|
|
58
|
-
"optionalDependencies",
|
|
59
|
-
"peerDependencies",
|
|
60
|
-
] as const;
|
|
61
|
-
|
|
62
|
-
/** Replace workspace and catalogue protocols with publishable registry ranges. */
|
|
63
|
-
export function materializeWorkspaceManifest(
|
|
64
|
-
source: Readonly<Record<string, unknown>>,
|
|
65
|
-
workspace: Readonly<Record<string, unknown>>,
|
|
66
|
-
): Readonly<Record<string, unknown>> {
|
|
67
|
-
const manifest = structuredClone(source) as Record<string, unknown>;
|
|
68
|
-
const version = workspace.version;
|
|
69
|
-
const catalog =
|
|
70
|
-
workspace.catalog && typeof workspace.catalog === "object"
|
|
71
|
-
? (workspace.catalog as Readonly<Record<string, unknown>>)
|
|
72
|
-
: {};
|
|
73
|
-
if (typeof version !== "string" || !version) {
|
|
74
|
-
throw new Error("Workspace manifest has no version");
|
|
75
|
-
}
|
|
76
|
-
for (const field of DEPENDENCY_FIELDS) {
|
|
77
|
-
const dependencies = manifest[field];
|
|
78
|
-
if (!dependencies || typeof dependencies !== "object") continue;
|
|
79
|
-
manifest[field] = Object.fromEntries(
|
|
80
|
-
Object.entries(dependencies).map(([name, value]) => [
|
|
81
|
-
name,
|
|
82
|
-
materializeDependency(name, value, version, catalog),
|
|
83
|
-
]),
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
return manifest;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function materializeDependency(
|
|
90
|
-
name: string,
|
|
91
|
-
value: unknown,
|
|
92
|
-
version: string,
|
|
93
|
-
catalog: Readonly<Record<string, unknown>>,
|
|
94
|
-
): unknown {
|
|
95
|
-
if (value === "catalog:") {
|
|
96
|
-
const resolved = catalog[name];
|
|
97
|
-
if (typeof resolved !== "string" || !resolved) {
|
|
98
|
-
throw new Error(`Workspace catalog has no version for ${name}`);
|
|
99
|
-
}
|
|
100
|
-
return resolved;
|
|
101
|
-
}
|
|
102
|
-
if (typeof value !== "string" || !value.startsWith("workspace:")) return value;
|
|
103
|
-
const selector = value.slice("workspace:".length);
|
|
104
|
-
if (selector === "*" || !selector) return version;
|
|
105
|
-
if (selector === "^" || selector === "~") return `${selector}${version}`;
|
|
106
|
-
return selector;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
58
|
/**
|
|
110
59
|
* Fold compiled publish entry points onto an archive manifest without changing
|
|
111
60
|
* the source checkout.
|
package/tasks/test-workspace.ts
CHANGED
|
@@ -167,6 +167,8 @@ function selectedTests(
|
|
|
167
167
|
): string[] {
|
|
168
168
|
const all = [
|
|
169
169
|
...graph.packages.flatMap((pkg) => testFiles(join(root, pkg.path, "test"))),
|
|
170
|
+
...testFiles(join(root, "test")),
|
|
171
|
+
...testFiles(join(root, "scripts")),
|
|
170
172
|
...testFiles(join(root, "docs/scripts")),
|
|
171
173
|
];
|
|
172
174
|
if (mode === "unit") return all.filter((path) => !integrationTest(path));
|