@dbx-tools/projen 0.9.54 → 0.9.60
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/index.ts +4 -1
- package/package.json +4 -4
- package/src/project-js.ts +39 -10
- package/src/shared-core-usage.ts +190 -0
- package/src/vscode.ts +17 -1
- package/tasks/publish.ts +4 -2
- package/tasks/python-node-bindings-watch.ts +2 -2
- package/tasks/python-node-bindings.ts +3 -1
- package/tasks/python-registry.ts +5 -11
- package/tasks/release.ts +2 -1
- package/tasks/shared-core-usage.ts +5 -0
- package/tasks/test-workspace.ts +253 -0
- package/tasks/ui-bundle-sizes.ts +22 -0
package/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
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.60";
|
|
7
7
|
export * as barrels from "./src/barrels.ts";
|
|
8
8
|
export * as bunApp from "./src/bun-app.ts";
|
|
9
9
|
export * as bunWorkflow from "./src/bun-workflow.ts";
|
|
@@ -29,6 +29,7 @@ export * as release from "./src/release.ts";
|
|
|
29
29
|
export * as releaseContext from "./src/release-context.ts";
|
|
30
30
|
export * as releaseOptions from "./src/release-options.ts";
|
|
31
31
|
export * as scaffold from "./src/scaffold.ts";
|
|
32
|
+
export * as sharedCoreUsage from "./src/shared-core-usage.ts";
|
|
32
33
|
export * as tags from "./src/tags.ts";
|
|
33
34
|
export * as tsconfig from "./src/tsconfig.ts";
|
|
34
35
|
export * as vscode from "./src/vscode.ts";
|
|
@@ -65,6 +66,8 @@ export type { ReleaseDocsOptions, DBXToolsReleaseOptions } from "./src/release.t
|
|
|
65
66
|
export { RELEASE_VERSION } from "./src/release-context.ts";
|
|
66
67
|
export { RELEASE_PUBLISH_TARGETS, RELEASE_INSTALL_MODES, ReleaseStepSelectionSchema } from "./src/release-options.ts";
|
|
67
68
|
export type { ReleasePublishTarget, ReleaseInstallMode, ReleaseStepSelection, ReleaseSelectionOptions } from "./src/release-options.ts";
|
|
69
|
+
export { SHARED_CORE_BINDING_EXPORTS } from "./src/shared-core-usage.ts";
|
|
70
|
+
export type { SharedCoreUsageKind, SharedCoreExportUsage } from "./src/shared-core-usage.ts";
|
|
68
71
|
export { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS } from "./src/tags.ts";
|
|
69
72
|
export type { PackageTag } from "./src/tags.ts";
|
|
70
73
|
export { DBXToolsRootTsconfig } from "./src/tsconfig.ts";
|
package/package.json
CHANGED
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@clack/prompts": "^1.7.0",
|
|
33
|
-
"@dbx-tools/core": "^0.9.
|
|
34
|
-
"@dbx-tools/path": "^0.9.
|
|
35
|
-
"@dbx-tools/shared-core": "^0.9.
|
|
33
|
+
"@dbx-tools/core": "^0.9.60",
|
|
34
|
+
"@dbx-tools/path": "^0.9.60",
|
|
35
|
+
"@dbx-tools/shared-core": "^0.9.60",
|
|
36
36
|
"commander": "^15.0.0",
|
|
37
37
|
"concurrently": "^10.0.3",
|
|
38
38
|
"constructs": "^10.6.0",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
},
|
|
55
55
|
"main": "index.ts",
|
|
56
56
|
"license": "Apache-2.0",
|
|
57
|
-
"version": "0.9.
|
|
57
|
+
"version": "0.9.60",
|
|
58
58
|
"types": "index.ts",
|
|
59
59
|
"type": "module",
|
|
60
60
|
"exports": {
|
package/src/project-js.ts
CHANGED
|
@@ -275,7 +275,7 @@ export function projectReleaseBranch(project: DBXToolsJavaScriptProject): string
|
|
|
275
275
|
*/
|
|
276
276
|
export function addPackageFiles(pkg: javascript.NodeProject, ...entries: string[]): void {
|
|
277
277
|
const current = (pkg.package.manifest.files ?? []) as string[];
|
|
278
|
-
pkg.package.addField("files", [...
|
|
278
|
+
pkg.package.addField("files", [...object.sequence(current, entries).distinct()]);
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
/**
|
|
@@ -859,16 +859,45 @@ class GeneratedSource extends Component {
|
|
|
859
859
|
* are the child's business - so `bun run build` at the root type-checked nothing
|
|
860
860
|
* and ran no package tests. Compilation groups ordinary package tsconfigs into
|
|
861
861
|
* a few TypeScript processes and runs custom package lifecycles alongside them.
|
|
862
|
-
* Tests
|
|
863
|
-
*
|
|
864
|
-
*
|
|
865
|
-
*
|
|
866
|
-
* task delegating to it cannot recurse. Members declared outside the scanned
|
|
867
|
-
* package roots (`extraWorkspaceMembers`) are covered by both tasks.
|
|
862
|
+
* Tests use a small number of root runner groups, skip packages without tests,
|
|
863
|
+
* and expose focused, changed, unit, integration, and full tiers. The test task
|
|
864
|
+
* reads the current workspace list from `package.json`, so a newly added member
|
|
865
|
+
* needs no re-synth.
|
|
868
866
|
*/
|
|
869
867
|
class WorkspaceValidationTasks extends Component {
|
|
870
868
|
private configured = false;
|
|
871
869
|
|
|
870
|
+
public constructor(project: DBXToolsNodeProject | DBXToolsTypeScriptProject) {
|
|
871
|
+
super(project);
|
|
872
|
+
if (project.parent) return;
|
|
873
|
+
project.addTask("workspace:graph", {
|
|
874
|
+
description: "Write the workspace dependency and reverse-dependency graph",
|
|
875
|
+
execArgs: taskCommand("test-workspace.ts", "graph"),
|
|
876
|
+
});
|
|
877
|
+
project.addTask("test:focused", {
|
|
878
|
+
description: "Run tests for explicit workspace packages or paths",
|
|
879
|
+
execArgs: taskCommand("test-workspace.ts", "focused"),
|
|
880
|
+
receiveArgs: true,
|
|
881
|
+
});
|
|
882
|
+
project.addTask("test:changed", {
|
|
883
|
+
description: "Run tests for changed packages and their dependents",
|
|
884
|
+
execArgs: taskCommand("test-workspace.ts", "changed"),
|
|
885
|
+
receiveArgs: true,
|
|
886
|
+
});
|
|
887
|
+
project.addTask("test:unit", {
|
|
888
|
+
description: "Run unit tests in grouped root runners",
|
|
889
|
+
execArgs: taskCommand("test-workspace.ts", "unit"),
|
|
890
|
+
});
|
|
891
|
+
project.addTask("test:integration", {
|
|
892
|
+
description: "Run integration and packed-consumer tests",
|
|
893
|
+
execArgs: taskCommand("test-workspace.ts", "integration"),
|
|
894
|
+
});
|
|
895
|
+
project.addTask("test:all", {
|
|
896
|
+
description: "Run all JavaScript, TypeScript, and Python tests",
|
|
897
|
+
execArgs: taskCommand("test-workspace.ts", "all"),
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
872
901
|
public override preSynthesize(): void {
|
|
873
902
|
if (this.configured) return;
|
|
874
903
|
this.configured = true;
|
|
@@ -877,7 +906,7 @@ class WorkspaceValidationTasks extends Component {
|
|
|
877
906
|
return;
|
|
878
907
|
}
|
|
879
908
|
project.compileTask.execArgs(taskCommand("compile-workspace.ts"));
|
|
880
|
-
project.testTask.
|
|
909
|
+
project.testTask.spawn(project.tasks.tryFind("test:all")!);
|
|
881
910
|
}
|
|
882
911
|
}
|
|
883
912
|
|
|
@@ -1253,7 +1282,7 @@ function initProject(
|
|
|
1253
1282
|
// agnostic floor is set in the child's constructor; per-tag deps/tsconfig come from
|
|
1254
1283
|
// the PACKAGE_TAG_MIXINS applied across the subtree below.
|
|
1255
1284
|
for (const p of scanPackages(rootAbs, roots)) {
|
|
1256
|
-
const tags = [...
|
|
1285
|
+
const tags = [...object.sequence(p.tagCandidates, resolveTags(p, tagPaths)).distinct()];
|
|
1257
1286
|
const found = existing.get(p.memberPath);
|
|
1258
1287
|
if (found) {
|
|
1259
1288
|
found.dbxToolsConfig.tags.push(...tags);
|
|
@@ -1272,7 +1301,7 @@ function initProject(
|
|
|
1272
1301
|
}
|
|
1273
1302
|
|
|
1274
1303
|
// The root project may itself carry tags (via a `""`/`"."` tag-path key).
|
|
1275
|
-
const rootTags = [...
|
|
1304
|
+
const rootTags = [...object.sequence(tagPaths[""], tagPaths["."]).distinct()];
|
|
1276
1305
|
if (rootTags.length) project.dbxToolsConfig.tags.push(...rootTags);
|
|
1277
1306
|
|
|
1278
1307
|
// Apply per-tag mixins across the whole subtree now that every child exists
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-export usage for `@dbx-tools/shared-core`.
|
|
3
|
+
*
|
|
4
|
+
* Classifies each live export as production, test, package-internal, generated
|
|
5
|
+
* binding, or unused. The report is informational; architecture tests fail only
|
|
6
|
+
* unused exports and duplicated owned helpers.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { join, relative } from "node:path";
|
|
10
|
+
import { find } from "@dbx-tools/path";
|
|
11
|
+
import * as core from "@dbx-tools/shared-core";
|
|
12
|
+
import { PACKAGE_IDENTIFIER, object } from "@dbx-tools/shared-core";
|
|
13
|
+
import { PYTHON_GENERATED_PACKAGE } from "./generated.ts";
|
|
14
|
+
import { readPackageManifest, recordedPackages, repoRoot } from "./packages.ts";
|
|
15
|
+
|
|
16
|
+
/** How one runtime export is referenced in this repository. */
|
|
17
|
+
export type SharedCoreUsageKind = "production" | "test" | "internal" | "binding" | "unused";
|
|
18
|
+
|
|
19
|
+
/** One classified shared-core runtime export. */
|
|
20
|
+
export interface SharedCoreExportUsage {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly namespace: string;
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly kind: SharedCoreUsageKind;
|
|
25
|
+
readonly files: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Binding-safe wrappers called from generated Python even without TS imports. */
|
|
29
|
+
export const SHARED_CORE_BINDING_EXPORTS = [
|
|
30
|
+
"bindings.logActiveLevel",
|
|
31
|
+
"bindings.logLevelEnabled",
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
const SOURCE_PATTERN = "**/*.{ts,tsx,js,mjs}";
|
|
35
|
+
const NAMESPACE_EXPORT = /^export \* as (\w+) from "\.\/src\/([^"]+)\.ts";$/gm;
|
|
36
|
+
const LOCAL_OWNED_HELPER =
|
|
37
|
+
/(?:export\s+)?(?:async\s+)?function(?:\s*\*)?\s+(isRecord|sleep|escapeRegExp|trimToNull|trimToUndefined|trimToEmpty)\s*\(|(?:export\s+)?const\s+(isRecord|sleep|escapeRegExp|trimToNull|trimToUndefined|trimToEmpty)\s*=/g;
|
|
38
|
+
|
|
39
|
+
/** Owners of the shared helpers; other files must import them. */
|
|
40
|
+
const OWNED_HELPER_FILES = new Set([
|
|
41
|
+
"packages/js/shared/core/src/object.ts",
|
|
42
|
+
"packages/js/shared/core/src/async-utils.ts",
|
|
43
|
+
"packages/js/shared/core/src/pattern.ts",
|
|
44
|
+
"packages/js/shared/core/src/string-utils.ts",
|
|
45
|
+
"docs/scripts/repository-docs.mjs",
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
export function sharedCorePackageDir(root: string = repoRoot): string {
|
|
49
|
+
for (const pkg of recordedPackages(root)) {
|
|
50
|
+
if (readPackageManifest(pkg.dir)?.name === PACKAGE_IDENTIFIER) return pkg.dir;
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`workspace package ${PACKAGE_IDENTIFIER} is missing`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function namespaceMap(coreDir: string): Readonly<Record<string, string>> {
|
|
56
|
+
const barrel = readFileSync(join(coreDir, "index.ts"), "utf8");
|
|
57
|
+
return Object.fromEntries(
|
|
58
|
+
[...barrel.matchAll(NAMESPACE_EXPORT)].map((match) => [match[1]!, match[2]!]),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function sourceFiles(root: string, includeGenerated: boolean): string[] {
|
|
63
|
+
const ignore = [
|
|
64
|
+
"**/node_modules/**",
|
|
65
|
+
"**/lib/**",
|
|
66
|
+
"**/.docs-build/**",
|
|
67
|
+
// Bundled PythonMonkey runtimes inline shared-core; they are not local copies.
|
|
68
|
+
"**/node_runtime/runtime.js",
|
|
69
|
+
"**/_runtime.js",
|
|
70
|
+
];
|
|
71
|
+
if (!includeGenerated) ignore.push(`**/${PYTHON_GENERATED_PACKAGE}/**`);
|
|
72
|
+
return [...find.findFiles(SOURCE_PATTERN, { cwd: root, ignore })].sort();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function classifyFile(
|
|
76
|
+
relativePath: string,
|
|
77
|
+
coreDirRel: string,
|
|
78
|
+
): Exclude<SharedCoreUsageKind, "unused"> {
|
|
79
|
+
if (relativePath.includes(`/${PYTHON_GENERATED_PACKAGE}/`)) return "binding";
|
|
80
|
+
if (relativePath.startsWith(`${coreDirRel}/src/`)) return "internal";
|
|
81
|
+
if (/(?:^|\/)(?:test|tests)\//.test(relativePath) || relativePath.includes(".test.")) {
|
|
82
|
+
return "test";
|
|
83
|
+
}
|
|
84
|
+
return "production";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function preferKind(
|
|
88
|
+
current: SharedCoreUsageKind,
|
|
89
|
+
next: Exclude<SharedCoreUsageKind, "unused">,
|
|
90
|
+
): SharedCoreUsageKind {
|
|
91
|
+
const rank: Record<SharedCoreUsageKind, number> = {
|
|
92
|
+
production: 0,
|
|
93
|
+
binding: 1,
|
|
94
|
+
internal: 2,
|
|
95
|
+
test: 3,
|
|
96
|
+
unused: 4,
|
|
97
|
+
};
|
|
98
|
+
return rank[next] < rank[current] ? next : current;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function mentionsExport(
|
|
102
|
+
source: string,
|
|
103
|
+
namespace: string,
|
|
104
|
+
name: string,
|
|
105
|
+
moduleFile: string,
|
|
106
|
+
): boolean {
|
|
107
|
+
if (source.includes(`${namespace}.${name}`)) return true;
|
|
108
|
+
const imported =
|
|
109
|
+
source.includes(`from "@dbx-tools/shared-core/${moduleFile}"`) ||
|
|
110
|
+
source.includes(`from "./${moduleFile}.ts"`) ||
|
|
111
|
+
source.includes(`from "../src/${moduleFile}.ts"`);
|
|
112
|
+
if (!imported) return false;
|
|
113
|
+
return new RegExp(`\\b${name}\\b`).test(source);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Classify every namespace runtime export against repository callers. */
|
|
117
|
+
export function sharedCoreExportUsage(root: string = repoRoot): SharedCoreExportUsage[] {
|
|
118
|
+
const coreDir = sharedCorePackageDir(root);
|
|
119
|
+
const coreDirRel = relative(root, coreDir);
|
|
120
|
+
const namespaces = namespaceMap(coreDir);
|
|
121
|
+
const files = sourceFiles(root, true).map((file) => ({
|
|
122
|
+
path: relative(root, file),
|
|
123
|
+
text: readFileSync(file, "utf8"),
|
|
124
|
+
}));
|
|
125
|
+
const usage: SharedCoreExportUsage[] = [];
|
|
126
|
+
for (const [namespace, moduleFile] of Object.entries(namespaces)) {
|
|
127
|
+
const exported = core[namespace as keyof typeof core];
|
|
128
|
+
if (!exported || typeof exported !== "object") continue;
|
|
129
|
+
for (const name of Object.keys(exported).sort()) {
|
|
130
|
+
if (name === "__esModule" || name === "default") continue;
|
|
131
|
+
const id = `${namespace}.${name}`;
|
|
132
|
+
const defining = `${coreDirRel}/src/${moduleFile}.ts`;
|
|
133
|
+
const matches = files
|
|
134
|
+
.filter(({ path, text }) => {
|
|
135
|
+
if (path === defining) {
|
|
136
|
+
return (text.match(new RegExp(`\\b${name}\\b`, "g"))?.length ?? 0) > 1;
|
|
137
|
+
}
|
|
138
|
+
return mentionsExport(text, namespace, name, moduleFile);
|
|
139
|
+
})
|
|
140
|
+
.map(({ path }) => path);
|
|
141
|
+
let kind: SharedCoreUsageKind = "unused";
|
|
142
|
+
if (
|
|
143
|
+
SHARED_CORE_BINDING_EXPORTS.includes(id as (typeof SHARED_CORE_BINDING_EXPORTS)[number])
|
|
144
|
+
) {
|
|
145
|
+
kind = "binding";
|
|
146
|
+
}
|
|
147
|
+
for (const file of matches) kind = preferKind(kind, classifyFile(file, coreDirRel));
|
|
148
|
+
usage.push({ id, namespace, name, kind, files: matches });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return usage.sort((left, right) => left.id.localeCompare(right.id));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Local copies of helpers owned by shared-core. */
|
|
155
|
+
export function duplicatedOwnedHelpers(root: string = repoRoot): string[] {
|
|
156
|
+
const duplicates: string[] = [];
|
|
157
|
+
for (const file of sourceFiles(root, false)) {
|
|
158
|
+
const relativePath = relative(root, file);
|
|
159
|
+
if (OWNED_HELPER_FILES.has(relativePath)) continue;
|
|
160
|
+
if (relativePath.startsWith("packages/js/shared/core/test/")) continue;
|
|
161
|
+
const text = readFileSync(file, "utf8");
|
|
162
|
+
LOCAL_OWNED_HELPER.lastIndex = 0;
|
|
163
|
+
if (LOCAL_OWNED_HELPER.test(text)) duplicates.push(relativePath);
|
|
164
|
+
}
|
|
165
|
+
return duplicates.sort();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Render a compact usage table for `shared-core:usage`. */
|
|
169
|
+
export function formatSharedCoreUsage(usage: readonly SharedCoreExportUsage[]): string {
|
|
170
|
+
const counts = object.sequence(usage).group({
|
|
171
|
+
production: (row) => row.kind === "production",
|
|
172
|
+
binding: (row) => row.kind === "binding",
|
|
173
|
+
internal: (row) => row.kind === "internal",
|
|
174
|
+
test: (row) => row.kind === "test",
|
|
175
|
+
unused: (row) => row.kind === "unused",
|
|
176
|
+
});
|
|
177
|
+
const lines = [
|
|
178
|
+
`shared-core runtime exports: ${usage.length}`,
|
|
179
|
+
` production ${counts.production.length}`,
|
|
180
|
+
` binding ${counts.binding.length}`,
|
|
181
|
+
` internal ${counts.internal.length}`,
|
|
182
|
+
` test ${counts.test.length}`,
|
|
183
|
+
` unused ${counts.unused.length}`,
|
|
184
|
+
];
|
|
185
|
+
if (counts.unused.length > 0) {
|
|
186
|
+
lines.push("unused:");
|
|
187
|
+
for (const row of counts.unused) lines.push(` ${row.id}`);
|
|
188
|
+
}
|
|
189
|
+
return `${lines.join("\n")}\n`;
|
|
190
|
+
}
|
package/src/vscode.ts
CHANGED
|
@@ -17,12 +17,28 @@ export function configureVsCode(scope: javascript.NodeProject): void {
|
|
|
17
17
|
"javascript.preferences.importModuleSpecifier": "non-relative",
|
|
18
18
|
"editor.formatOnSave": true,
|
|
19
19
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
20
|
+
"editor.codeActionsOnSave": {
|
|
21
|
+
"source.fixAll.eslint": "explicit",
|
|
22
|
+
},
|
|
23
|
+
"[python]": {
|
|
24
|
+
"editor.defaultFormatter": "charliermarsh.ruff",
|
|
25
|
+
"editor.formatOnSave": true,
|
|
26
|
+
"editor.codeActionsOnSave": {
|
|
27
|
+
"source.fixAll.ruff": "explicit",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
"notebook.formatOnSave.enabled": true,
|
|
31
|
+
"notebook.defaultFormatter": "charliermarsh.ruff",
|
|
20
32
|
"files.watcherExclude": {
|
|
21
33
|
"**/node_modules/**": true,
|
|
22
34
|
"**/dist/**": true,
|
|
23
35
|
},
|
|
24
36
|
});
|
|
25
|
-
editor.extensions.addRecommendations(
|
|
37
|
+
editor.extensions.addRecommendations(
|
|
38
|
+
"esbenp.prettier-vscode",
|
|
39
|
+
"dbaeumer.vscode-eslint",
|
|
40
|
+
"charliermarsh.ruff",
|
|
41
|
+
);
|
|
26
42
|
|
|
27
43
|
new JsonFile(scope, ".vscode/tasks.json", {
|
|
28
44
|
marker: false,
|
package/tasks/publish.ts
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
47
47
|
import { tmpdir } from "node:os";
|
|
48
48
|
import { delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
49
|
-
import { asyncUtils, log } from "@dbx-tools/shared-core";
|
|
49
|
+
import { asyncUtils, log, object } from "@dbx-tools/shared-core";
|
|
50
50
|
import {
|
|
51
51
|
applyPublishConfig,
|
|
52
52
|
npmReleaseMatches,
|
|
@@ -169,7 +169,9 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<void
|
|
|
169
169
|
`${pkg.name ?? dirname(dir)} has invalid publishConfig.access ${String(access)}`,
|
|
170
170
|
);
|
|
171
171
|
}
|
|
172
|
-
const compiledTargets = [
|
|
172
|
+
const compiledTargets = [
|
|
173
|
+
...object.sequence(compiledPublishTargets(pkg.publishConfig)).distinct(),
|
|
174
|
+
];
|
|
173
175
|
publishable.push({
|
|
174
176
|
dir,
|
|
175
177
|
name: pkg.name ?? dirname(dir),
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/** Keep every configured Python Node bridge current with one repository watcher. */
|
|
3
3
|
import { dirname, resolve, sep } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { log } from "@dbx-tools/shared-core";
|
|
5
|
+
import { log, object } from "@dbx-tools/shared-core";
|
|
6
6
|
import { runTaskCommand } from "../src/_task-command.ts";
|
|
7
7
|
import { repoRoot } from "../src/packages.ts";
|
|
8
8
|
import {
|
|
@@ -41,7 +41,7 @@ function watchPaths(configs: readonly ResolvedPythonNodeBindings[]): string[] {
|
|
|
41
41
|
const explicitInputs = configs
|
|
42
42
|
.flatMap(pythonNodeBindingWatchInputs)
|
|
43
43
|
.filter((input) => !covered.some((root) => containsPath(root, input)));
|
|
44
|
-
return [...
|
|
44
|
+
return [...object.sequence(covered, explicitInputs).distinct()].sort();
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
function selection(
|
|
@@ -463,7 +463,9 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
|
|
|
463
463
|
"",
|
|
464
464
|
...(usesCallable ? ["from collections.abc import Callable"] : []),
|
|
465
465
|
"from dataclasses import dataclass, field",
|
|
466
|
-
"from typing import Any,
|
|
466
|
+
"from typing import Any, Protocol, TypedDict",
|
|
467
|
+
"",
|
|
468
|
+
"from typing_extensions import NotRequired",
|
|
467
469
|
"",
|
|
468
470
|
"from .._runtime import MISSING as _MISSING",
|
|
469
471
|
"from .._runtime import invoke_positioned as _invoke_positioned",
|
package/tasks/python-registry.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
|
-
import { net } from "@dbx-tools/shared-core";
|
|
5
|
+
import { net, stringUtils } from "@dbx-tools/shared-core";
|
|
6
6
|
import { parse } from "smol-toml";
|
|
7
7
|
import { captureTaskCommand } from "../src/_task-command.ts";
|
|
8
8
|
|
|
@@ -120,12 +120,6 @@ export function devpiRegistry(index: string): LocalPythonRegistry | undefined {
|
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
/** Split a whitespace-separated index list (the pip/uv env-var form). */
|
|
124
|
-
function splitIndexList(value: string | undefined): string[] {
|
|
125
|
-
const trimmed = value?.trim();
|
|
126
|
-
return trimmed ? trimmed.split(/\s+/) : [];
|
|
127
|
-
}
|
|
128
|
-
|
|
129
123
|
/** Read a `pip config get <key>`, treating pip's literal "undefined" as unset. */
|
|
130
124
|
function pipConfig(key: string): string | undefined {
|
|
131
125
|
const out = captureTaskCommand(process.cwd(), "python", ["-m", "pip", "config", "get", key]);
|
|
@@ -153,8 +147,8 @@ export function activePythonIndexes(): string[] {
|
|
|
153
147
|
// uv: env vars first (primary, then extras), then uv.toml's index blocks.
|
|
154
148
|
add(process.env.UV_DEFAULT_INDEX);
|
|
155
149
|
add(process.env.UV_INDEX_URL);
|
|
156
|
-
for (const url of
|
|
157
|
-
for (const url of
|
|
150
|
+
for (const url of stringUtils.parseList(process.env.UV_INDEX)) add(url);
|
|
151
|
+
for (const url of stringUtils.parseList(process.env.UV_EXTRA_INDEX_URL)) add(url);
|
|
158
152
|
const uvConfig = process.env.UV_CONFIG_FILE ?? resolve(homedir(), ".config/uv/uv.toml");
|
|
159
153
|
if (existsSync(uvConfig)) {
|
|
160
154
|
for (const url of parseUvIndexes(readFileSync(uvConfig, "utf8"))) add(url);
|
|
@@ -162,9 +156,9 @@ export function activePythonIndexes(): string[] {
|
|
|
162
156
|
|
|
163
157
|
// pip: env vars (primary + extras), then `pip config` (index-url + extra-index-url).
|
|
164
158
|
add(process.env.PIP_INDEX_URL);
|
|
165
|
-
for (const url of
|
|
159
|
+
for (const url of stringUtils.parseList(process.env.PIP_EXTRA_INDEX_URL)) add(url);
|
|
166
160
|
add(pipConfig("global.index-url"));
|
|
167
|
-
for (const url of
|
|
161
|
+
for (const url of stringUtils.parseList(pipConfig("global.extra-index-url"))) add(url);
|
|
168
162
|
|
|
169
163
|
return out;
|
|
170
164
|
}
|
package/tasks/release.ts
CHANGED
|
@@ -161,6 +161,7 @@ export async function runRelease(
|
|
|
161
161
|
);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
runTaskCommand(root, "git", ["push", remote, `HEAD:${branch}`]);
|
|
164
165
|
runTaskCommand(root, "git", [
|
|
165
166
|
"tag",
|
|
166
167
|
"--annotate",
|
|
@@ -169,7 +170,7 @@ export async function runRelease(
|
|
|
169
170
|
releaseTagAnnotation(tag, selection),
|
|
170
171
|
]);
|
|
171
172
|
try {
|
|
172
|
-
runTaskCommand(root, "git", ["push",
|
|
173
|
+
runTaskCommand(root, "git", ["push", remote, `refs/tags/${tag}`]);
|
|
173
174
|
} catch (error) {
|
|
174
175
|
runTaskCommand(root, "git", ["tag", "--delete", tag]);
|
|
175
176
|
throw error;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import * as exec from "@dbx-tools/core/exec";
|
|
4
|
+
import { log, object } from "@dbx-tools/shared-core";
|
|
5
|
+
|
|
6
|
+
const logger = log.logger("projen:test");
|
|
7
|
+
|
|
8
|
+
interface PackageManifest {
|
|
9
|
+
readonly name?: string;
|
|
10
|
+
readonly workspaces?: readonly string[];
|
|
11
|
+
readonly dependencies?: Readonly<Record<string, string>>;
|
|
12
|
+
readonly optionalDependencies?: Readonly<Record<string, string>>;
|
|
13
|
+
readonly peerDependencies?: Readonly<Record<string, string>>;
|
|
14
|
+
readonly devDependencies?: Readonly<Record<string, string>>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface WorkspaceGraphPackage {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly path: string;
|
|
20
|
+
readonly dependencies: readonly string[];
|
|
21
|
+
readonly reverseDependencies: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface WorkspaceGraph {
|
|
25
|
+
readonly packages: readonly WorkspaceGraphPackage[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type TestMode = "focused" | "changed" | "unit" | "integration" | "all" | "graph";
|
|
29
|
+
|
|
30
|
+
const PROJEN_INTEGRATION_TEST =
|
|
31
|
+
/(?:local-publish|npm-release|packed-consumer|project-py|publish|python-sync|release|root-install|sdk-boundary).*\.test\.ts$/;
|
|
32
|
+
|
|
33
|
+
function readJson<T>(path: string): T {
|
|
34
|
+
return JSON.parse(readFileSync(path, "utf8")) as T;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function toPosix(path: string): string {
|
|
38
|
+
return path.split(sep).join("/");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function workspaceGraph(root: string): WorkspaceGraph {
|
|
42
|
+
const workspace = readJson<PackageManifest>(join(root, "package.json"));
|
|
43
|
+
const packages = new Map<string, { path: string; manifest: PackageManifest }>();
|
|
44
|
+
for (const member of workspace.workspaces ?? []) {
|
|
45
|
+
const manifest = readJson<PackageManifest>(join(root, member, "package.json"));
|
|
46
|
+
if (manifest.name) packages.set(manifest.name, { path: toPosix(member), manifest });
|
|
47
|
+
}
|
|
48
|
+
const reverse = new Map<string, Set<string>>();
|
|
49
|
+
const rows = [...packages].map(([name, pkg]) => {
|
|
50
|
+
const dependencies = new Set<string>();
|
|
51
|
+
for (const field of [
|
|
52
|
+
"dependencies",
|
|
53
|
+
"optionalDependencies",
|
|
54
|
+
"peerDependencies",
|
|
55
|
+
"devDependencies",
|
|
56
|
+
] as const) {
|
|
57
|
+
for (const dependency of Object.keys(pkg.manifest[field] ?? {})) {
|
|
58
|
+
if (!packages.has(dependency)) continue;
|
|
59
|
+
dependencies.add(dependency);
|
|
60
|
+
const dependents = reverse.get(dependency) ?? new Set<string>();
|
|
61
|
+
dependents.add(name);
|
|
62
|
+
reverse.set(dependency, dependents);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { name, path: pkg.path, dependencies: [...dependencies].sort() };
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
packages: rows
|
|
69
|
+
.map((pkg) => ({
|
|
70
|
+
...pkg,
|
|
71
|
+
reverseDependencies: [...(reverse.get(pkg.name) ?? [])].sort(),
|
|
72
|
+
}))
|
|
73
|
+
.sort((left, right) => left.name.localeCompare(right.name)),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function writeGraph(root: string, graph: WorkspaceGraph): void {
|
|
78
|
+
const path = join(root, ".projen", "workspace-graph.json");
|
|
79
|
+
writeFileSync(path, `${JSON.stringify(graph, null, 2)}\n`);
|
|
80
|
+
logger.info(`wrote ${relative(root, path)}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function testFiles(directory: string): string[] {
|
|
84
|
+
if (!existsSync(directory)) return [];
|
|
85
|
+
const files: string[] = [];
|
|
86
|
+
const visit = (current: string): void => {
|
|
87
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
88
|
+
const path = join(current, entry.name);
|
|
89
|
+
if (entry.isDirectory()) visit(path);
|
|
90
|
+
else if (/\.(?:test|spec)\.tsx?$/.test(entry.name)) files.push(path);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
visit(directory);
|
|
94
|
+
return files.sort();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function integrationTest(path: string): boolean {
|
|
98
|
+
return (
|
|
99
|
+
path.includes(".integration.test.") ||
|
|
100
|
+
(toPosix(path).includes("/projen/test/") && PROJEN_INTEGRATION_TEST.test(basename(path)))
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function packageForPath(graph: WorkspaceGraph, path: string): WorkspaceGraphPackage | undefined {
|
|
105
|
+
const normalized = toPosix(path);
|
|
106
|
+
return [...graph.packages]
|
|
107
|
+
.sort((left, right) => right.path.length - left.path.length)
|
|
108
|
+
.find((pkg) => normalized === pkg.path || normalized.startsWith(`${pkg.path}/`));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function affectedPackages(graph: WorkspaceGraph, changed: readonly string[]): Set<string> {
|
|
112
|
+
const byName = new Map(graph.packages.map((pkg) => [pkg.name, pkg]));
|
|
113
|
+
const affected = new Set<string>();
|
|
114
|
+
const pending: string[] = [];
|
|
115
|
+
for (const path of changed) {
|
|
116
|
+
const pkg = packageForPath(graph, path);
|
|
117
|
+
if (!pkg) return new Set(graph.packages.map((entry) => entry.name));
|
|
118
|
+
if (!affected.has(pkg.name)) pending.push(pkg.name);
|
|
119
|
+
}
|
|
120
|
+
while (pending.length > 0) {
|
|
121
|
+
const name = pending.pop()!;
|
|
122
|
+
if (affected.has(name)) continue;
|
|
123
|
+
affected.add(name);
|
|
124
|
+
pending.push(...(byName.get(name)?.reverseDependencies ?? []));
|
|
125
|
+
}
|
|
126
|
+
return affected;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function changedFiles(root: string, base?: string): string[] {
|
|
130
|
+
const candidates = base ? [base] : ["origin/main", "main", "HEAD~1"];
|
|
131
|
+
let committed: string[] = [];
|
|
132
|
+
for (const candidate of candidates) {
|
|
133
|
+
const result = exec.spawnSync("git", ["diff", "--name-only", `${candidate}...HEAD`], {
|
|
134
|
+
cwd: root,
|
|
135
|
+
stdin: "ignore",
|
|
136
|
+
stdout: "capture",
|
|
137
|
+
stderr: "ignore",
|
|
138
|
+
});
|
|
139
|
+
if (result.exitCode === 0) {
|
|
140
|
+
committed = result.stdoutLines.filter(Boolean);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const working = exec.spawnSync("git", ["diff", "--name-only", "HEAD"], {
|
|
145
|
+
cwd: root,
|
|
146
|
+
stdin: "ignore",
|
|
147
|
+
stdout: "capture",
|
|
148
|
+
stderr: "ignore",
|
|
149
|
+
check: true,
|
|
150
|
+
}).stdoutLines;
|
|
151
|
+
return [...object.sequence(committed, working).filter(Boolean).distinct()].sort();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function selectedTests(
|
|
155
|
+
root: string,
|
|
156
|
+
graph: WorkspaceGraph,
|
|
157
|
+
mode: TestMode,
|
|
158
|
+
args: readonly string[],
|
|
159
|
+
): string[] {
|
|
160
|
+
const all = [
|
|
161
|
+
...graph.packages.flatMap((pkg) => testFiles(join(root, pkg.path, "test"))),
|
|
162
|
+
...testFiles(join(root, "docs/scripts")),
|
|
163
|
+
];
|
|
164
|
+
if (mode === "unit") return all.filter((path) => !integrationTest(path));
|
|
165
|
+
if (mode === "integration") return all.filter(integrationTest);
|
|
166
|
+
if (mode === "all") return all;
|
|
167
|
+
|
|
168
|
+
let names: Set<string>;
|
|
169
|
+
if (mode === "changed") {
|
|
170
|
+
const baseIndex = args.indexOf("--base");
|
|
171
|
+
const changed = changedFiles(root, baseIndex >= 0 ? args[baseIndex + 1] : undefined);
|
|
172
|
+
if (changed.length === 0) return [];
|
|
173
|
+
names = affectedPackages(graph, changed);
|
|
174
|
+
} else {
|
|
175
|
+
if (args.length === 0) throw new Error("test:focused requires a package name or path");
|
|
176
|
+
names = new Set<string>();
|
|
177
|
+
for (const value of args) {
|
|
178
|
+
const direct = graph.packages.find((pkg) => pkg.name === value);
|
|
179
|
+
const byPath = direct ?? packageForPath(graph, toPosix(relative(root, resolve(root, value))));
|
|
180
|
+
if (!byPath) throw new Error(`Unknown workspace package or path: ${value}`);
|
|
181
|
+
names.add(byPath.name);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const paths = new Set(graph.packages.filter((pkg) => names.has(pkg.name)).map((pkg) => pkg.path));
|
|
185
|
+
return all.filter((path) => {
|
|
186
|
+
const relativePath = toPosix(relative(root, path));
|
|
187
|
+
return [...paths].some((pkg) => relativePath.startsWith(`${pkg}/`));
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function runnerGroup(root: string, path: string): string {
|
|
192
|
+
const relativePath = toPosix(relative(root, path));
|
|
193
|
+
if (relativePath.startsWith("packages/js/shared/")) return "browser-shared";
|
|
194
|
+
if (
|
|
195
|
+
relativePath.startsWith("packages/js/ui/") ||
|
|
196
|
+
relativePath.startsWith("packages/example/app/")
|
|
197
|
+
) {
|
|
198
|
+
return "react-ui";
|
|
199
|
+
}
|
|
200
|
+
if (relativePath.startsWith("projen/")) return "projen";
|
|
201
|
+
if (relativePath.startsWith("docs/scripts/")) return "docs";
|
|
202
|
+
if (relativePath.startsWith("packages/example/")) return "examples";
|
|
203
|
+
return "node";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function runTests(root: string, files: readonly string[]): Promise<void> {
|
|
207
|
+
if (files.length === 0) {
|
|
208
|
+
logger.info("no tests selected");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const groups = new Map<string, string[]>();
|
|
212
|
+
for (const file of files) {
|
|
213
|
+
const name = runnerGroup(root, file);
|
|
214
|
+
const paths = groups.get(name) ?? [];
|
|
215
|
+
paths.push(relative(root, file));
|
|
216
|
+
groups.set(name, paths);
|
|
217
|
+
}
|
|
218
|
+
const started = performance.now();
|
|
219
|
+
const jobs = [...groups].map(async ([name, paths]) => {
|
|
220
|
+
const groupStarted = performance.now();
|
|
221
|
+
const result = await exec.spawn("bun", ["test", ...paths], {
|
|
222
|
+
cwd: root,
|
|
223
|
+
stdin: "ignore",
|
|
224
|
+
stdout: "inherit",
|
|
225
|
+
stderr: "inherit",
|
|
226
|
+
});
|
|
227
|
+
const seconds = ((performance.now() - groupStarted) / 1000).toFixed(2);
|
|
228
|
+
logger.info(`${name}: ${paths.length} files in ${seconds}s`);
|
|
229
|
+
return { name, result };
|
|
230
|
+
});
|
|
231
|
+
const results = await Promise.all(jobs);
|
|
232
|
+
logger.info(
|
|
233
|
+
`selected tests: ${files.length} files in ${((performance.now() - started) / 1000).toFixed(2)}s`,
|
|
234
|
+
);
|
|
235
|
+
const failed = results.filter(({ result }) => result.exitCode !== 0);
|
|
236
|
+
if (failed.length > 0) {
|
|
237
|
+
throw new Error(`Test groups failed: ${failed.map(({ name }) => name).join(", ")}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function main(argv: readonly string[] = process.argv.slice(2)): Promise<void> {
|
|
242
|
+
const root = process.cwd();
|
|
243
|
+
const mode = (argv[0] ?? "all") as TestMode;
|
|
244
|
+
if (!["focused", "changed", "unit", "integration", "all", "graph"].includes(mode)) {
|
|
245
|
+
throw new Error(`Unknown test mode: ${mode}`);
|
|
246
|
+
}
|
|
247
|
+
const graph = workspaceGraph(root);
|
|
248
|
+
writeGraph(root, graph);
|
|
249
|
+
if (mode === "graph") return;
|
|
250
|
+
await runTests(root, selectedTests(root, graph, mode, argv.slice(1)));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (import.meta.main) await main();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
const entries = {
|
|
4
|
+
auth: "packages/js/ui/appkit/src/auth/react/index.ts",
|
|
5
|
+
email: "packages/js/ui/appkit/src/email/react/index.ts",
|
|
6
|
+
search: "packages/js/ui/appkit/src/search/react/index.ts",
|
|
7
|
+
} as const;
|
|
8
|
+
|
|
9
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
10
|
+
const result = await Bun.build({
|
|
11
|
+
entrypoints: [resolve(entry)],
|
|
12
|
+
target: "browser",
|
|
13
|
+
minify: true,
|
|
14
|
+
sourcemap: "none",
|
|
15
|
+
external: ["react", "react-dom", "@databricks/*", "@dbx-tools/shared-*", "lucide-react"],
|
|
16
|
+
});
|
|
17
|
+
if (!result.success) {
|
|
18
|
+
throw new AggregateError(result.logs, `Could not build the ${name} UI subpath`);
|
|
19
|
+
}
|
|
20
|
+
const bytes = result.outputs.reduce((total, output) => total + output.size, 0);
|
|
21
|
+
console.log(`${name}: ${bytes} bytes`);
|
|
22
|
+
}
|