@dbx-tools/projen 0.9.154 → 0.9.158
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 +36 -47
- package/index.ts +42 -20
- package/package.json +8 -8
- package/src/_task-command.ts +7 -7
- package/src/appkit-workspace.ts +278 -104
- package/src/barrels.ts +147 -259
- package/src/bun-app.ts +54 -10
- package/src/clean.ts +6 -6
- package/src/engine-root.ts +4 -4
- package/src/mixin.ts +0 -2
- package/src/module-exports.ts +4 -5
- package/src/npm-packaging.ts +128 -0
- package/src/npm-release.ts +200 -0
- package/src/packages.ts +11 -11
- package/src/project-js.ts +209 -103
- package/src/project-predicate.ts +5 -12
- package/src/project-py.ts +6 -6
- package/src/project.ts +7 -14
- package/src/publish.ts +3 -3
- package/src/python-node-bindings.ts +16 -12
- package/src/python-packaging.ts +177 -0
- package/src/release-options.ts +2 -2
- package/src/release.ts +3 -3
- package/src/shared-core-usage.ts +17 -4
- package/src/tags.ts +10 -9
- package/src/watch.ts +7 -9
- package/src/workspace-lock.ts +2 -2
- package/src/workspace-manifest.ts +2 -2
- package/tasks/appkit-deploy.ts +35 -15
- package/tasks/barrels.ts +6 -9
- package/tasks/bump.ts +1 -1
- package/tasks/clean.ts +3 -3
- package/tasks/cli.ts +3 -3
- package/tasks/compile-workspace.ts +8 -8
- package/tasks/dev-watch.ts +11 -11
- package/tasks/local-publish.ts +6 -6
- package/tasks/publish-npm.ts +11 -319
- package/tasks/publish-python.ts +2 -180
- package/tasks/publish.ts +6 -11
- package/tasks/python-node-bindings.ts +2 -2
- package/tasks/python-registry.ts +6 -6
- package/tasks/python-sync.ts +2 -3
- package/tasks/release-version.ts +1 -1
- package/tasks/release.ts +11 -11
- package/tasks/sync.ts +2 -2
- package/tasks/test-workspace.ts +9 -9
- package/tasks/verify-publications.ts +9 -5
- package/tasks/version-check.ts +5 -5
- /package/{tasks → src}/python-release.ts +0 -0
package/src/project.ts
CHANGED
|
@@ -3,18 +3,13 @@
|
|
|
3
3
|
* dbx-tools project implementations.
|
|
4
4
|
*/
|
|
5
5
|
import { type PathMatchInput } from "@dbx-tools/path";
|
|
6
|
-
import {
|
|
6
|
+
import { type OneOrMany, toOneOrMany } from "@dbx-tools/shared-core";
|
|
7
7
|
import { type IConstruct } from "constructs";
|
|
8
8
|
import { Project, type ProjectOptions } from "projen";
|
|
9
9
|
import * as mixin from "./mixin.ts";
|
|
10
10
|
import type { DBXToolsJavaScriptProject } from "./project-js.ts";
|
|
11
11
|
import * as projectPredicate from "./project-predicate.ts";
|
|
12
12
|
|
|
13
|
-
export * from "./project-js.ts";
|
|
14
|
-
export * from "./project-py.ts";
|
|
15
|
-
export * from "./python-node-bindings-component.ts";
|
|
16
|
-
export * from "./python-node-runtime.ts";
|
|
17
|
-
|
|
18
13
|
/** Runtime family implemented by a dbx-tools project. */
|
|
19
14
|
export type DBXToolsProjectLanguage = "javascript" | "python";
|
|
20
15
|
|
|
@@ -91,29 +86,27 @@ export function applyToProjects<P extends Project>(
|
|
|
91
86
|
if (!options?.includeRoots) predicate = predicate.and((project) => project.parent != null);
|
|
92
87
|
if (options?.identifierPackageName) {
|
|
93
88
|
predicate = predicate.and(
|
|
94
|
-
projectPredicate.hasIdentifierPackageName(
|
|
95
|
-
...object.toOneOrMany(options.identifierPackageName),
|
|
96
|
-
),
|
|
89
|
+
projectPredicate.hasIdentifierPackageName(...toOneOrMany(options.identifierPackageName)),
|
|
97
90
|
);
|
|
98
91
|
}
|
|
99
92
|
if (options?.name) {
|
|
100
|
-
predicate = predicate.and(projectPredicate.hasName(...
|
|
93
|
+
predicate = predicate.and(projectPredicate.hasName(...toOneOrMany(options.name)));
|
|
101
94
|
}
|
|
102
95
|
if (options?.identifierScope) {
|
|
103
96
|
predicate = predicate.and(
|
|
104
|
-
projectPredicate.hasIdentifierScope(...
|
|
97
|
+
projectPredicate.hasIdentifierScope(...toOneOrMany(options.identifierScope)),
|
|
105
98
|
);
|
|
106
99
|
}
|
|
107
100
|
if (options?.identifierName) {
|
|
108
101
|
predicate = predicate.and(
|
|
109
|
-
projectPredicate.hasIdentifierName(...
|
|
102
|
+
projectPredicate.hasIdentifierName(...toOneOrMany(options.identifierName)),
|
|
110
103
|
);
|
|
111
104
|
}
|
|
112
105
|
if (options?.tags) {
|
|
113
|
-
predicate = predicate.and(projectPredicate.hasTag(...
|
|
106
|
+
predicate = predicate.and(projectPredicate.hasTag(...toOneOrMany(options.tags)));
|
|
114
107
|
}
|
|
115
108
|
if (options?.path) {
|
|
116
|
-
predicate = predicate.and(projectPredicate.hasPath(...
|
|
109
|
+
predicate = predicate.and(projectPredicate.hasPath(...toOneOrMany(options.path)));
|
|
117
110
|
}
|
|
118
111
|
const projectMixin = mixin.create(predicate, (project) => {
|
|
119
112
|
callbacks.forEach((callback) => callback(project as Project));
|
package/src/publish.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* Two compiler options make the emitted tree loadable, both native `tsc`:
|
|
18
18
|
*
|
|
19
|
-
* - `rootDir: "."` so
|
|
19
|
+
* - `rootDir: "."` so package-root `index*.ts` barrels are compiled at all.
|
|
20
20
|
* projen's default `rootDir: "src"` puts the barrel outside the compilation,
|
|
21
21
|
* which is why `lib/` would otherwise have no `index.js`.
|
|
22
22
|
* - `rewriteRelativeImportExtensions`, which turns the `./x.ts` specifiers this
|
|
@@ -80,7 +80,7 @@ function compiledStem(target: string): string | undefined {
|
|
|
80
80
|
* target that ships as-is (`./package.json`, a stylesheet, an SVG asset).
|
|
81
81
|
*
|
|
82
82
|
* `rootDir: "."` means the emitted tree mirrors the package layout, so the
|
|
83
|
-
* mapping is positional: `./
|
|
83
|
+
* mapping is positional: `./index.react.ts` -> `./lib/index.react.js`.
|
|
84
84
|
*/
|
|
85
85
|
function compiledTarget(target: string): { types: string; default: string } | undefined {
|
|
86
86
|
const stem = compiledStem(target);
|
|
@@ -145,7 +145,7 @@ export function applyCompiledPublish(pkg: javascript.NodeProject): void {
|
|
|
145
145
|
if (!publishesCompiled(pkg)) return;
|
|
146
146
|
|
|
147
147
|
applyCompilerOptions(pkg, COMPILED_COMPILER_OPTIONS);
|
|
148
|
-
applyIncludes(pkg, "index
|
|
148
|
+
applyIncludes(pkg, "index*.ts");
|
|
149
149
|
addPackageFiles(pkg, COMPILED_DIR);
|
|
150
150
|
|
|
151
151
|
const config = publishConfig(pkg);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/** Shared pyproject-backed configuration for PythonMonkey Node bindings. */
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { existsSync, readFileSync, readdirSync, realpathSync, rmSync } from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
4
5
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
7
|
+
import { findFilesSync } from "@dbx-tools/path";
|
|
8
|
+
import { isRecord, toIdentifierWithOptions } from "@dbx-tools/shared-core";
|
|
8
9
|
import { parse } from "smol-toml";
|
|
9
10
|
import ts from "typescript";
|
|
10
11
|
import { runTaskCommand } from "./_task-command.ts";
|
|
@@ -137,7 +138,7 @@ export function resolvePythonNodeBindingModules(
|
|
|
137
138
|
config: ResolvedPythonNodeBindings,
|
|
138
139
|
): ResolvedPythonNodeBindings {
|
|
139
140
|
if (config.modules.length > 0) return config;
|
|
140
|
-
const packageEntrypoint =
|
|
141
|
+
const packageEntrypoint = resolveModule(config.entrypoint, config.projectDirectory);
|
|
141
142
|
const modules = publicNamespaceExports(packageEntrypoint).filter(
|
|
142
143
|
(module) =>
|
|
143
144
|
publicFunctionExports(resolvePythonNodeBindingModule(packageEntrypoint, module)).length > 0,
|
|
@@ -147,7 +148,7 @@ export function resolvePythonNodeBindingModules(
|
|
|
147
148
|
|
|
148
149
|
/** Map a JavaScript export or namespace to its generated Python identifier. */
|
|
149
150
|
export function pythonNodeBindingFunctionName(javascriptName: string): string {
|
|
150
|
-
const name =
|
|
151
|
+
const name = toIdentifierWithOptions({ delimiter: "_" }, javascriptName);
|
|
151
152
|
if (!isPythonNodeBindingIdentifier(name)) {
|
|
152
153
|
throw new Error(
|
|
153
154
|
`JavaScript export ${javascriptName} does not map to a valid Python function name`,
|
|
@@ -184,7 +185,7 @@ export function pythonNodeBindingInputFingerprint(
|
|
|
184
185
|
for (const input of config.watchInputs) files.add(canonicalPath(resolve(root, input)));
|
|
185
186
|
for (const { handlerFile } of config.functionOverrides) files.add(canonicalPath(handlerFile));
|
|
186
187
|
const packageEntrypoint = canonicalPath(
|
|
187
|
-
|
|
188
|
+
resolveModule(config.entrypoint, config.projectDirectory),
|
|
188
189
|
);
|
|
189
190
|
files.add(packageEntrypoint);
|
|
190
191
|
const packageManifest = nearestPackageManifest(packageEntrypoint);
|
|
@@ -327,8 +328,7 @@ export function resolveAllPythonNodeBindings(
|
|
|
327
328
|
/** Repository-relative Python projects that configure generated Node bindings. */
|
|
328
329
|
export function pythonNodeBindingProjects(projectRoot: string): string[] {
|
|
329
330
|
return [
|
|
330
|
-
...
|
|
331
|
-
.findFilesSync("**/pyproject.toml", { cwd: projectRoot })
|
|
331
|
+
...findFilesSync("**/pyproject.toml", { cwd: projectRoot })
|
|
332
332
|
.filter((file) => {
|
|
333
333
|
const manifest = parse(readFileSync(resolve(projectRoot, file), "utf8")) as {
|
|
334
334
|
tool?: { dbx_tools?: { node_bindings?: unknown } };
|
|
@@ -443,7 +443,7 @@ type BindingSourceConfig = Omit<ResolvedPythonNodeBindings, "watchInputs">;
|
|
|
443
443
|
|
|
444
444
|
/** Transitive workspace source files reachable from configured binding modules. */
|
|
445
445
|
function bindingSourceInputs(root: string, config: BindingSourceConfig): string[] {
|
|
446
|
-
const packageEntrypoint =
|
|
446
|
+
const packageEntrypoint = resolveModule(config.entrypoint, config.projectDirectory);
|
|
447
447
|
const entrypoints =
|
|
448
448
|
config.modules.length > 0
|
|
449
449
|
? config.modules.map((module) => resolvePythonNodeBindingModule(packageEntrypoint, module))
|
|
@@ -479,7 +479,7 @@ function transitiveSourceFiles(
|
|
|
479
479
|
const dependencies = ts.preProcessFile(source, true, true).importedFiles;
|
|
480
480
|
for (const dependency of dependencies) {
|
|
481
481
|
try {
|
|
482
|
-
pending.push(
|
|
482
|
+
pending.push(resolveModule(dependency.fileName, dirname(candidate)));
|
|
483
483
|
} catch {
|
|
484
484
|
// Built-ins and external packages are outside the workspace source graph.
|
|
485
485
|
}
|
|
@@ -501,7 +501,11 @@ export function resolvePythonNodeBindingModule(entrypoint: string, module: strin
|
|
|
501
501
|
if (!match?.[1]) {
|
|
502
502
|
throw new Error(`${entrypoint} does not export namespace ${module}`);
|
|
503
503
|
}
|
|
504
|
-
return
|
|
504
|
+
return resolveModule(match[1], dirname(entrypoint));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function resolveModule(specifier: string, directory: string): string {
|
|
508
|
+
return createRequire(join(directory, "package.json")).resolve(specifier);
|
|
505
509
|
}
|
|
506
510
|
|
|
507
511
|
function canonicalPath(path: string): string {
|
|
@@ -578,11 +582,11 @@ function validateBindings(configs: readonly ResolvedPythonNodeBindings[]): void
|
|
|
578
582
|
|
|
579
583
|
function nodePackageName(packageName: string): string {
|
|
580
584
|
const unscoped = packageName.split("/").at(-1) ?? packageName;
|
|
581
|
-
return
|
|
585
|
+
return toIdentifierWithOptions({ delimiter: "_" }, unscoped);
|
|
582
586
|
}
|
|
583
587
|
|
|
584
588
|
function record(value: unknown, path: string): Record<string, unknown> {
|
|
585
|
-
if (!
|
|
589
|
+
if (!isRecord(value)) throw new Error(`${path} must be a table`);
|
|
586
590
|
return value;
|
|
587
591
|
}
|
|
588
592
|
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/** Side-effect-free Python workspace distribution packaging. */
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
cpSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
15
|
+
import { projectUtils } from "@dbx-tools/core";
|
|
16
|
+
import { parse, stringify } from "smol-toml";
|
|
17
|
+
import { runTaskCommand } from "./_task-command.ts";
|
|
18
|
+
import { preparePythonProjectForPublication, pythonProjectInfo } from "./python-release.ts";
|
|
19
|
+
|
|
20
|
+
interface PythonProjectFile {
|
|
21
|
+
readonly directory: string;
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly version: string;
|
|
24
|
+
readonly path: string;
|
|
25
|
+
readonly private: boolean;
|
|
26
|
+
readonly source: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Python wheel and source archives accepted by Twine and package indexes. */
|
|
30
|
+
export function pythonDistributionPaths(directory: string): string[] {
|
|
31
|
+
return readdirSync(directory)
|
|
32
|
+
.filter((file) => file.endsWith(".whl") || file.endsWith(".tar.gz"))
|
|
33
|
+
.sort()
|
|
34
|
+
.map((file) => join(directory, file));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function removeTemporaryTree(root: string): void {
|
|
38
|
+
if (!existsSync(root)) return;
|
|
39
|
+
const makeWritable = (directory: string): void => {
|
|
40
|
+
chmodSync(directory, 0o700);
|
|
41
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
42
|
+
if (entry.isDirectory()) makeWritable(join(directory, entry.name));
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
makeWritable(root);
|
|
46
|
+
rmSync(root, { recursive: true, force: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function pythonProjects(root: string): PythonProjectFile[] {
|
|
50
|
+
const packageFiles = readdirSync(root, { withFileTypes: true })
|
|
51
|
+
.filter((entry) => entry.isDirectory())
|
|
52
|
+
.map((entry) => resolve(root, entry.name, "pyproject.toml"))
|
|
53
|
+
.filter(existsSync)
|
|
54
|
+
.sort();
|
|
55
|
+
const allProjects: PythonProjectFile[] = packageFiles.map((path) => {
|
|
56
|
+
const source = readFileSync(path, "utf8");
|
|
57
|
+
const info = pythonProjectInfo(source, { parse });
|
|
58
|
+
return {
|
|
59
|
+
directory: basename(resolve(path, "..")),
|
|
60
|
+
name: info.name,
|
|
61
|
+
version: info.version,
|
|
62
|
+
path,
|
|
63
|
+
private: info.private,
|
|
64
|
+
source,
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
return allProjects;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Build and upload every selected public Python workspace distribution. */
|
|
71
|
+
export function publishPythonProjects(options: {
|
|
72
|
+
readonly dryRun?: boolean;
|
|
73
|
+
readonly indexUrl: string;
|
|
74
|
+
readonly publishUrl: string;
|
|
75
|
+
readonly root: string;
|
|
76
|
+
readonly version: string;
|
|
77
|
+
}): void {
|
|
78
|
+
const output = mkdtempSync(join(tmpdir(), "projen-python-publish-"));
|
|
79
|
+
try {
|
|
80
|
+
buildPythonProjectsInternal({ ...options, output }, true);
|
|
81
|
+
runTaskCommand(
|
|
82
|
+
process.cwd(),
|
|
83
|
+
"uvx",
|
|
84
|
+
[
|
|
85
|
+
"--from",
|
|
86
|
+
"devpi-client",
|
|
87
|
+
"devpi",
|
|
88
|
+
"upload",
|
|
89
|
+
"--index",
|
|
90
|
+
options.publishUrl,
|
|
91
|
+
"--from-dir",
|
|
92
|
+
...(options.dryRun ? ["--dry-run"] : []),
|
|
93
|
+
output,
|
|
94
|
+
],
|
|
95
|
+
{ env: { ...process.env, UV_DEFAULT_INDEX: options.indexUrl } },
|
|
96
|
+
);
|
|
97
|
+
} finally {
|
|
98
|
+
removeTemporaryTree(output);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Build the exact Python release distributions without publishing them. */
|
|
103
|
+
export function buildPythonProjects(options: {
|
|
104
|
+
readonly allowEmpty?: boolean;
|
|
105
|
+
readonly output: string;
|
|
106
|
+
readonly packages?: readonly string[];
|
|
107
|
+
/** Place each package's artifacts in its directory beneath output. */
|
|
108
|
+
readonly packageDirectories?: boolean;
|
|
109
|
+
readonly root: string;
|
|
110
|
+
readonly version: string;
|
|
111
|
+
}): void {
|
|
112
|
+
buildPythonProjectsInternal(options, false);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildPythonProjectsInternal(
|
|
116
|
+
options: Parameters<typeof buildPythonProjects>[0],
|
|
117
|
+
allowOutputOutsideWorkspace: boolean,
|
|
118
|
+
): void {
|
|
119
|
+
const root = resolve(options.root);
|
|
120
|
+
const output = resolve(options.output);
|
|
121
|
+
const workspace = projectUtils.root(root) ?? dirname(root);
|
|
122
|
+
const outputPath = relative(workspace, output);
|
|
123
|
+
if (
|
|
124
|
+
!allowOutputOutsideWorkspace &&
|
|
125
|
+
(!outputPath || outputPath.startsWith("..") || isAbsolute(outputPath))
|
|
126
|
+
) {
|
|
127
|
+
throw new Error("Python release output must be a non-root directory inside the workspace");
|
|
128
|
+
}
|
|
129
|
+
rmSync(output, { recursive: true, force: true });
|
|
130
|
+
const allProjects = pythonProjects(root);
|
|
131
|
+
const requested = new Set(options.packages ?? []);
|
|
132
|
+
const missing = [...requested].filter(
|
|
133
|
+
(directory) => !allProjects.some((project) => project.directory === directory),
|
|
134
|
+
);
|
|
135
|
+
if (missing.length > 0) throw new Error(`Unknown Python packages: ${missing.join(", ")}`);
|
|
136
|
+
const projects = allProjects.filter(
|
|
137
|
+
(project) => !project.private && (requested.size === 0 || requested.has(project.directory)),
|
|
138
|
+
);
|
|
139
|
+
if (projects.length === 0 && options.allowEmpty) {
|
|
140
|
+
mkdirSync(output, { recursive: true });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (projects.length === 0) throw new Error(`No Python packages found under ${root}`);
|
|
144
|
+
|
|
145
|
+
const temporaryRoot = mkdtempSync(join(tmpdir(), "projen-python-release-"));
|
|
146
|
+
try {
|
|
147
|
+
mkdirSync(output, { recursive: true });
|
|
148
|
+
for (const project of projects) {
|
|
149
|
+
const packageRoot = join(temporaryRoot, project.directory);
|
|
150
|
+
cpSync(dirname(project.path), packageRoot, { recursive: true });
|
|
151
|
+
const manifestPath = join(packageRoot, "pyproject.toml");
|
|
152
|
+
chmodSync(manifestPath, 0o644);
|
|
153
|
+
writeFileSync(
|
|
154
|
+
manifestPath,
|
|
155
|
+
preparePythonProjectForPublication(project.source, {
|
|
156
|
+
packages: allProjects,
|
|
157
|
+
toml: { parse, stringify },
|
|
158
|
+
version: options.version,
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
const packageOutput = options.packageDirectories ? join(output, project.directory) : output;
|
|
162
|
+
runTaskCommand(root, "uv", ["build", "--out-dir", packageOutput, packageRoot]);
|
|
163
|
+
if (pythonDistributionPaths(packageOutput).length === 0) {
|
|
164
|
+
throw new Error(`No Python distributions found for ${project.name}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const distributions = options.packageDirectories
|
|
168
|
+
? projects.flatMap((project) => pythonDistributionPaths(join(output, project.directory)))
|
|
169
|
+
: pythonDistributionPaths(output);
|
|
170
|
+
if (distributions.length === 0) {
|
|
171
|
+
throw new Error(`No Python distributions found in ${output}`);
|
|
172
|
+
}
|
|
173
|
+
runTaskCommand(process.cwd(), "uvx", ["twine", "check", ...distributions]);
|
|
174
|
+
} finally {
|
|
175
|
+
removeTemporaryTree(temporaryRoot);
|
|
176
|
+
}
|
|
177
|
+
}
|
package/src/release-options.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { parseRecord } from "@dbx-tools/shared-core";
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
|
|
13
13
|
/** Publication scopes accepted by the release task; auto preserves normal publication. */
|
|
@@ -70,7 +70,7 @@ export function parseReleaseTagAnnotation(annotation: string): ReleaseStepSelect
|
|
|
70
70
|
const selections = annotation.split("\n").filter((line) => line.startsWith(ANNOTATION_MARKER));
|
|
71
71
|
if (selections.length === 0) return releaseStepSelection();
|
|
72
72
|
if (selections.length !== 1) throw new Error("Release tag contains multiple step selections");
|
|
73
|
-
const value =
|
|
73
|
+
const value = parseRecord(selections[0]!.slice(ANNOTATION_MARKER.length));
|
|
74
74
|
const parsed = ReleaseStepSelectionSchema.safeParse(value);
|
|
75
75
|
if (!parsed.success) throw new Error("Invalid release tag step selection");
|
|
76
76
|
return parsed.data;
|
package/src/release.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* jobs download them. `DBXToolsRelease` therefore remains separate while native
|
|
11
11
|
* workflow and project primitives own the YAML model and setup steps.
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
13
|
+
import { dedent } from "@dbx-tools/shared-core";
|
|
14
14
|
import { Component, github } from "projen";
|
|
15
15
|
import { GithubWorkflow } from "projen/lib/github";
|
|
16
16
|
import { JobPermission, type Job, type JobStep } from "projen/lib/github/workflows-model";
|
|
@@ -216,7 +216,7 @@ export function refreshPublicationVerification(project: DBXToolsJavaScriptProjec
|
|
|
216
216
|
VERIFY_PYPI: hasPypi ? "${{ needs.build-release.outputs.pypi }}" : "false",
|
|
217
217
|
},
|
|
218
218
|
// prettier-ignore
|
|
219
|
-
run:
|
|
219
|
+
run: dedent(
|
|
220
220
|
// ============================================================================
|
|
221
221
|
/*bash*/`
|
|
222
222
|
args=(--version "$RELEASE_VERSION")
|
|
@@ -282,7 +282,7 @@ function releaseBuildJob(
|
|
|
282
282
|
RELEASE_TAG: "${{ github.ref_name }}",
|
|
283
283
|
},
|
|
284
284
|
// prettier-ignore
|
|
285
|
-
run:
|
|
285
|
+
run: dedent(
|
|
286
286
|
// ============================================================================
|
|
287
287
|
/*bash*/`
|
|
288
288
|
case "$RELEASE_TAG" in ${tagPrefix}*) ;; *) exit 1 ;; esac
|
package/src/shared-core-usage.ts
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { readFileSync } from "node:fs";
|
|
9
9
|
import { join, relative, resolve } from "node:path";
|
|
10
|
-
import {
|
|
10
|
+
import { findFiles } from "@dbx-tools/path";
|
|
11
11
|
import * as core from "@dbx-tools/shared-core";
|
|
12
|
-
import { PACKAGE_IDENTIFIER,
|
|
12
|
+
import { PACKAGE_IDENTIFIER, sequence } from "@dbx-tools/shared-core";
|
|
13
13
|
import { PYTHON_GENERATED_PACKAGE } from "./generated.ts";
|
|
14
14
|
import { readPackageManifest, recordedPackages, repoRoot } from "./packages.ts";
|
|
15
15
|
|
|
@@ -33,6 +33,7 @@ export const SHARED_CORE_BINDING_EXPORTS = [
|
|
|
33
33
|
|
|
34
34
|
const SOURCE_PATTERN = "**/*.{ts,tsx,js,mjs}";
|
|
35
35
|
const NAMESPACE_EXPORT = /^export \* as (\w+) from "\.\/src\/([^"]+)\.ts";$/gm;
|
|
36
|
+
const ROOT_NAMED_IMPORT = /import\s*\{([\s\S]*?)\}\s*from\s*["']@dbx-tools\/shared-core["']/g;
|
|
36
37
|
const LOCAL_OWNED_HELPER =
|
|
37
38
|
/(?: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
|
|
|
@@ -70,7 +71,7 @@ async function sourceFiles(root: string, includeGenerated: boolean): Promise<str
|
|
|
70
71
|
];
|
|
71
72
|
if (!includeGenerated) ignore.push(`**/${PYTHON_GENERATED_PACKAGE}/**`);
|
|
72
73
|
const files: string[] = [];
|
|
73
|
-
for await (const file of
|
|
74
|
+
for await (const file of findFiles(SOURCE_PATTERN, { cwd: root, ignore })) {
|
|
74
75
|
files.push(resolve(root, file));
|
|
75
76
|
}
|
|
76
77
|
return files.sort();
|
|
@@ -109,6 +110,18 @@ function mentionsExport(
|
|
|
109
110
|
moduleFile: string,
|
|
110
111
|
): boolean {
|
|
111
112
|
if (source.includes(`${namespace}.${name}`)) return true;
|
|
113
|
+
ROOT_NAMED_IMPORT.lastIndex = 0;
|
|
114
|
+
for (const match of source.matchAll(ROOT_NAMED_IMPORT)) {
|
|
115
|
+
const specifiers = match[1]!.split(",").map((specifier) => specifier.trim());
|
|
116
|
+
if (
|
|
117
|
+
specifiers.some((specifier) => {
|
|
118
|
+
if (!specifier || specifier.startsWith("type ")) return false;
|
|
119
|
+
return specifier.split(/\s+as\s+/u, 1)[0] === name;
|
|
120
|
+
})
|
|
121
|
+
) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
112
125
|
const imported =
|
|
113
126
|
source.includes(`from "@dbx-tools/shared-core/${moduleFile}"`) ||
|
|
114
127
|
source.includes(`from "./${moduleFile}.ts"`) ||
|
|
@@ -173,7 +186,7 @@ export async function duplicatedOwnedHelpers(root: string = repoRoot): Promise<s
|
|
|
173
186
|
|
|
174
187
|
/** Render a compact usage table for `shared-core:usage`. */
|
|
175
188
|
export function formatSharedCoreUsage(usage: readonly SharedCoreExportUsage[]): string {
|
|
176
|
-
const counts =
|
|
189
|
+
const counts = sequence(usage).group({
|
|
177
190
|
production: (row) => row.kind === "production",
|
|
178
191
|
binding: (row) => row.kind === "binding",
|
|
179
192
|
internal: (row) => row.kind === "internal",
|
package/src/tags.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* across the subtree; the root applies built-in tag mixins during construction and
|
|
14
14
|
* callers add their own afterward.
|
|
15
15
|
*/
|
|
16
|
+
import { relative } from "node:path";
|
|
16
17
|
import type { IMixin as ConstructsMixin } from "constructs";
|
|
17
18
|
import { DependencyType, javascript } from "projen";
|
|
18
19
|
import { BunBuildFile, BunDevServerFile, BunfigFile } from "./bun-app.ts";
|
|
@@ -23,7 +24,7 @@ import {
|
|
|
23
24
|
applyCompilerOptions,
|
|
24
25
|
applyExports,
|
|
25
26
|
applyIncludes,
|
|
26
|
-
|
|
27
|
+
srcFolderExports,
|
|
27
28
|
} from "./project-js.ts";
|
|
28
29
|
import * as projectPredicate from "./project-predicate.ts";
|
|
29
30
|
|
|
@@ -83,11 +84,13 @@ export const PACKAGE_TAG_MIXINS = {
|
|
|
83
84
|
applyCompilerOptions(p, {
|
|
84
85
|
target: "ES2022",
|
|
85
86
|
lib: [...DOM_LIB],
|
|
87
|
+
noEmit: true,
|
|
88
|
+
rootDir: relative(p.outdir, p.root.outdir).replaceAll("\\", "/") || ".",
|
|
86
89
|
});
|
|
87
|
-
// UI libraries
|
|
88
|
-
// and any other public source modules are discovered from the source tree.
|
|
90
|
+
// UI libraries expose their package barrel, folder barrels, and public CSS.
|
|
89
91
|
applyExports(p, {
|
|
90
|
-
|
|
92
|
+
".": "./index.ts",
|
|
93
|
+
...srcFolderExports(p),
|
|
91
94
|
"./package.json": "./package.json",
|
|
92
95
|
});
|
|
93
96
|
}),
|
|
@@ -149,13 +152,11 @@ export const PACKAGE_TAG_MIXINS = {
|
|
|
149
152
|
// A CLI compiles code OUTSIDE `src/` - its root `index.ts` barrel and the
|
|
150
153
|
// `bin/` entries - which the src-only tag default doesn't reach.
|
|
151
154
|
applyCompilerOptions(p, NODE_COMPILER_OPTIONS);
|
|
152
|
-
applyIncludes(p, "index
|
|
153
|
-
// A CLI's standard surface: the
|
|
154
|
-
// top-level `src` module, and `./package.json`. Derived rather than declared
|
|
155
|
-
// so no CLI has to hand-list its own modules.
|
|
155
|
+
applyIncludes(p, "index*.ts", "bin/**/*.ts");
|
|
156
|
+
// A CLI's standard surface: the root barrel, folder barrels, and package metadata.
|
|
156
157
|
applyExports(p, {
|
|
157
158
|
".": "./index.ts",
|
|
158
|
-
...
|
|
159
|
+
...srcFolderExports(p),
|
|
159
160
|
"./package.json": "./package.json",
|
|
160
161
|
});
|
|
161
162
|
// A CLI is the one shape whose entry point lives outside `src`, so its
|
package/src/watch.ts
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* is thin glue.
|
|
17
17
|
*/
|
|
18
18
|
import { isAbsolute, resolve } from "node:path";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { watchFiles } from "@dbx-tools/path";
|
|
20
|
+
import { log, sleep, toError } from "@dbx-tools/shared-core";
|
|
21
21
|
import { isGeneratedFile, recordedRoots, repoRoot } from "./packages.ts";
|
|
22
22
|
import { withWorkspaceMutationLock } from "./workspace-lock.ts";
|
|
23
23
|
|
|
@@ -49,9 +49,7 @@ const CLOSE_GRACE_MS = 2_000;
|
|
|
49
49
|
const FATAL_WATCH_ERROR_CODES = new Set(["EMFILE", "ENFILE", "ENOSPC"]);
|
|
50
50
|
|
|
51
51
|
/** node-path's built-in ignore-group toggles (`{ dot, temp, test, lock, defaults }`). */
|
|
52
|
-
export type IgnoreGroupOptions = NonNullable<
|
|
53
|
-
Parameters<typeof pathWatch.watchFiles>[1]
|
|
54
|
-
>["ignoreOptions"];
|
|
52
|
+
export type IgnoreGroupOptions = NonNullable<Parameters<typeof watchFiles>[1]>["ignoreOptions"];
|
|
55
53
|
|
|
56
54
|
/** The package roots (absolute), where every watchable source file lives. */
|
|
57
55
|
export function watchRoots(): string[] {
|
|
@@ -119,7 +117,7 @@ export async function watchLoop(
|
|
|
119
117
|
});
|
|
120
118
|
}
|
|
121
119
|
} catch (err) {
|
|
122
|
-
logger.error(`${tag} cycle failed`, { error:
|
|
120
|
+
logger.error(`${tag} cycle failed`, { error: toError(err) });
|
|
123
121
|
} finally {
|
|
124
122
|
running = false;
|
|
125
123
|
if (rerun) {
|
|
@@ -129,7 +127,7 @@ export async function watchLoop(
|
|
|
129
127
|
}
|
|
130
128
|
}
|
|
131
129
|
|
|
132
|
-
const watcher = await
|
|
130
|
+
const watcher = await watchFiles(paths, {
|
|
133
131
|
cwd: repoRoot,
|
|
134
132
|
ignoreInitial: true,
|
|
135
133
|
ignore: (path) => ignoredPath(path),
|
|
@@ -146,7 +144,7 @@ export async function watchLoop(
|
|
|
146
144
|
if (closing) return;
|
|
147
145
|
closing = true;
|
|
148
146
|
clearTimeout(timer);
|
|
149
|
-
await Promise.race([watcher.close().catch(() => {}),
|
|
147
|
+
await Promise.race([watcher.close().catch(() => {}), sleep(CLOSE_GRACE_MS)]);
|
|
150
148
|
process.exit(code);
|
|
151
149
|
}
|
|
152
150
|
|
|
@@ -156,7 +154,7 @@ export async function watchLoop(
|
|
|
156
154
|
timer = setTimeout(() => void flush(), DEBOUNCE_MS);
|
|
157
155
|
});
|
|
158
156
|
watcher.on("error", (err) => {
|
|
159
|
-
logger.error(`${tag} watcher error`, { error:
|
|
157
|
+
logger.error(`${tag} watcher error`, { error: toError(err) });
|
|
160
158
|
const code = (err as NodeJS.ErrnoException | undefined)?.code;
|
|
161
159
|
if (code === undefined || !FATAL_WATCH_ERROR_CODES.has(code)) return;
|
|
162
160
|
logger.error(`${tag}: ${code} leaves the watcher blind - exiting so it is restarted`);
|
package/src/workspace-lock.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Cross-process coordination for generated workspace mutations. */
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import
|
|
3
|
+
import { withFileLock } from "@dbx-tools/core";
|
|
4
4
|
import { log } from "@dbx-tools/shared-core";
|
|
5
5
|
|
|
6
6
|
const MUTATION_LOCK_SCOPE = "dbx-tools-workspace-mutation";
|
|
@@ -36,7 +36,7 @@ export async function withWorkspaceMutationLock<T>(
|
|
|
36
36
|
}
|
|
37
37
|
let waitLogTimer: ReturnType<typeof setInterval> | undefined;
|
|
38
38
|
try {
|
|
39
|
-
return await
|
|
39
|
+
return await withFileLock(
|
|
40
40
|
[MUTATION_LOCK_SCOPE, repository],
|
|
41
41
|
async () => {
|
|
42
42
|
if (check && !(await check())) {
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
} from "node:fs";
|
|
18
18
|
import { tmpdir } from "node:os";
|
|
19
19
|
import { dirname, join, resolve } from "node:path";
|
|
20
|
-
import
|
|
20
|
+
import { spawnSync } from "@dbx-tools/core";
|
|
21
21
|
|
|
22
22
|
const DEPENDENCY_FIELDS = [
|
|
23
23
|
"dependencies",
|
|
@@ -102,7 +102,7 @@ export async function withStagedWorkspaceManifests<T>(
|
|
|
102
102
|
): Promise<T> {
|
|
103
103
|
const stage = mkdtempSync(join(tmpdir(), "dbx-tools-app-deploy-"));
|
|
104
104
|
try {
|
|
105
|
-
const listed =
|
|
105
|
+
const listed = spawnSync(
|
|
106
106
|
"git",
|
|
107
107
|
["ls-files", "--cached", "--others", "--exclude-standard", "-z"],
|
|
108
108
|
{
|