@dbx-tools/projen 0.9.12 → 0.9.18
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 +33 -25
- package/index.ts +5 -4
- package/package.json +4 -5
- package/src/_release-platform.ts +10 -0
- package/src/_rust-project.ts +40 -13
- package/src/_rust-release-workflow.ts +213 -344
- package/src/openapi.ts +191 -79
- package/src/project-js.ts +8 -13
- package/src/project-py.ts +22 -3
- package/src/project-rs.ts +50 -6
- package/src/release-github.ts +23 -0
- package/src/release-plan.ts +48 -15
- package/src/release.ts +17 -3
- package/src/watch.ts +19 -2
- package/src/workspace-lock.ts +32 -6
- package/tasks/barrels.ts +47 -30
- package/tasks/compile-workspace.ts +121 -0
- package/tasks/openapi.ts +15 -11
- package/tasks/release-assets.ts +476 -0
- package/tasks/release-pr.ts +123 -14
- package/tasks/rust.ts +62 -33
- package/tasks/uniffi-python.js +24 -12
- package/tasks/uniffi-release.mjs +40 -3
- package/tasks/uniffi.ts +2 -0
- package/src/_rust-release.ts +0 -605
- package/tasks/build-rust-release.ts +0 -68
- package/tasks/rust-release.d.ts +0 -41
- package/tasks/rust-release.mjs +0 -1717
package/src/release-plan.ts
CHANGED
|
@@ -47,13 +47,15 @@ export interface PlannedRustTarget {
|
|
|
47
47
|
readonly runner: string;
|
|
48
48
|
readonly python: string;
|
|
49
49
|
readonly libc: string;
|
|
50
|
-
readonly
|
|
50
|
+
readonly glibcVersion: string;
|
|
51
51
|
readonly packages: readonly string[];
|
|
52
|
+
readonly binaries: readonly string[];
|
|
53
|
+
readonly features: readonly string[];
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
/** Complete plan persisted as a workflow artifact and used for recovery. */
|
|
55
57
|
export interface ReleasePlan {
|
|
56
|
-
readonly schemaVersion:
|
|
58
|
+
readonly schemaVersion: 2;
|
|
57
59
|
readonly units: readonly PlannedReleaseUnit[];
|
|
58
60
|
readonly publishBatches: readonly (readonly string[])[];
|
|
59
61
|
readonly nodePackages: readonly PlannedPackage[];
|
|
@@ -130,18 +132,52 @@ export function buildReleasePlan(
|
|
|
130
132
|
]);
|
|
131
133
|
}
|
|
132
134
|
}
|
|
133
|
-
const
|
|
135
|
+
const rustTargets = new Map<string, PlannedRustTarget>();
|
|
134
136
|
for (const artifact of artifacts) {
|
|
135
137
|
const targets = artifact.data?.targets;
|
|
136
138
|
if (!Array.isArray(targets)) continue;
|
|
139
|
+
const binary = artifact.kind === "github-binary";
|
|
137
140
|
for (const value of targets) {
|
|
138
141
|
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
139
142
|
const target = value as Record<string, unknown>;
|
|
140
143
|
const required = ["os", "cpu", "node", "cargo", "runner", "python"] as const;
|
|
141
144
|
if (!required.every((field) => typeof target[field] === "string")) continue;
|
|
142
145
|
const key = String(target.node);
|
|
143
|
-
const currentTarget =
|
|
144
|
-
|
|
146
|
+
const currentTarget = rustTargets.get(key);
|
|
147
|
+
const packages = [
|
|
148
|
+
...new Set([
|
|
149
|
+
...(currentTarget?.packages ?? []),
|
|
150
|
+
...(rustPackagesByUnit.get(artifact.unit) ?? []),
|
|
151
|
+
]),
|
|
152
|
+
].sort();
|
|
153
|
+
const binaries = binary
|
|
154
|
+
? [
|
|
155
|
+
...new Set([
|
|
156
|
+
...(currentTarget?.binaries ?? []),
|
|
157
|
+
...(typeof artifact.data?.binary === "string" ? [artifact.data.binary] : []),
|
|
158
|
+
]),
|
|
159
|
+
].sort()
|
|
160
|
+
: (currentTarget?.binaries ?? []);
|
|
161
|
+
const configuredFeatures = Array.isArray(artifact.data?.features)
|
|
162
|
+
? artifact.data.features.filter((feature): feature is string => typeof feature === "string")
|
|
163
|
+
: [];
|
|
164
|
+
const crate = typeof artifact.data?.crate === "string" ? artifact.data.crate : undefined;
|
|
165
|
+
const features = binary
|
|
166
|
+
? [
|
|
167
|
+
...new Set([
|
|
168
|
+
...(currentTarget?.features ?? []),
|
|
169
|
+
...(crate
|
|
170
|
+
? configuredFeatures.map((feature) => `${crate}/${feature}`)
|
|
171
|
+
: configuredFeatures),
|
|
172
|
+
]),
|
|
173
|
+
].sort()
|
|
174
|
+
: [
|
|
175
|
+
...new Set([
|
|
176
|
+
...(currentTarget?.features ?? []),
|
|
177
|
+
...packages.map((pkg) => `${pkg}/uniffi-bindgen`),
|
|
178
|
+
]),
|
|
179
|
+
].sort();
|
|
180
|
+
rustTargets.set(key, {
|
|
145
181
|
os: String(target.os),
|
|
146
182
|
cpu: String(target.cpu),
|
|
147
183
|
node: String(target.node),
|
|
@@ -149,17 +185,14 @@ export function buildReleasePlan(
|
|
|
149
185
|
runner: String(target.runner),
|
|
150
186
|
python: String(target.python),
|
|
151
187
|
libc: typeof target.libc === "string" ? target.libc : "",
|
|
152
|
-
|
|
153
|
-
packages
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
...(rustPackagesByUnit.get(artifact.unit) ?? []),
|
|
157
|
-
]),
|
|
158
|
-
].sort(),
|
|
188
|
+
glibcVersion: typeof target.glibcVersion === "string" ? target.glibcVersion : "",
|
|
189
|
+
packages,
|
|
190
|
+
binaries,
|
|
191
|
+
features,
|
|
159
192
|
});
|
|
160
193
|
}
|
|
161
194
|
}
|
|
162
|
-
const
|
|
195
|
+
const plannedRustTargets = [...rustTargets.values()].sort((a, b) => a.node.localeCompare(b.node));
|
|
163
196
|
const publishBatches = current.publishBatches
|
|
164
197
|
.map((batch) => batch.filter((unit) => selected.has(unit)))
|
|
165
198
|
.filter((batch) => batch.length > 0);
|
|
@@ -179,14 +212,14 @@ export function buildReleasePlan(
|
|
|
179
212
|
.filter(([, enabled]) => !enabled)
|
|
180
213
|
.map(([name]) => name);
|
|
181
214
|
return {
|
|
182
|
-
schemaVersion:
|
|
215
|
+
schemaVersion: 2,
|
|
183
216
|
units,
|
|
184
217
|
publishBatches,
|
|
185
218
|
nodePackages,
|
|
186
219
|
pythonPackages,
|
|
187
220
|
rustPackages,
|
|
188
221
|
artifacts,
|
|
189
|
-
rustTargets,
|
|
222
|
+
rustTargets: plannedRustTargets,
|
|
190
223
|
stages,
|
|
191
224
|
omittedStages,
|
|
192
225
|
};
|
package/src/release.ts
CHANGED
|
@@ -117,6 +117,7 @@ export function releaseArtifactSteps(options: {
|
|
|
117
117
|
readonly recoveredName: string;
|
|
118
118
|
readonly pattern: string;
|
|
119
119
|
readonly path: string;
|
|
120
|
+
readonly condition?: string;
|
|
120
121
|
}): readonly JobStep[] {
|
|
121
122
|
const shared = {
|
|
122
123
|
pattern: options.pattern,
|
|
@@ -126,13 +127,13 @@ export function releaseArtifactSteps(options: {
|
|
|
126
127
|
return [
|
|
127
128
|
{
|
|
128
129
|
name: options.currentName,
|
|
129
|
-
if:
|
|
130
|
+
if: `\${{ ${options.condition ? `${options.condition} && ` : ""}inputs.source_run_id == '' }}`,
|
|
130
131
|
uses: "actions/download-artifact@v8",
|
|
131
132
|
with: shared,
|
|
132
133
|
},
|
|
133
134
|
{
|
|
134
135
|
name: options.recoveredName,
|
|
135
|
-
if:
|
|
136
|
+
if: `\${{ ${options.condition ? `${options.condition} && ` : ""}inputs.source_run_id != '' }}`,
|
|
136
137
|
uses: "actions/download-artifact@v8",
|
|
137
138
|
with: {
|
|
138
139
|
...shared,
|
|
@@ -179,6 +180,7 @@ function verifyContextJob(tagPrefix: string, releaseBranch: string): Job {
|
|
|
179
180
|
release_tag: { stepId: "release", outputName: "release_tag" },
|
|
180
181
|
expected_sha: { stepId: "release", outputName: "expected_sha" },
|
|
181
182
|
release_version: { stepId: "release", outputName: "release_version" },
|
|
183
|
+
build_mode: { stepId: "release", outputName: "build_mode" },
|
|
182
184
|
},
|
|
183
185
|
steps: [
|
|
184
186
|
{
|
|
@@ -186,7 +188,7 @@ function verifyContextJob(tagPrefix: string, releaseBranch: string): Job {
|
|
|
186
188
|
uses: "actions/checkout@v6",
|
|
187
189
|
with: {
|
|
188
190
|
ref: "${{ github.event_name == 'push' && github.sha || inputs.expected_sha }}",
|
|
189
|
-
"fetch-depth":
|
|
191
|
+
"fetch-depth": 0,
|
|
190
192
|
},
|
|
191
193
|
},
|
|
192
194
|
{
|
|
@@ -205,6 +207,7 @@ function verifyContextJob(tagPrefix: string, releaseBranch: string): Job {
|
|
|
205
207
|
EXPECTED_SHA:
|
|
206
208
|
"${{ github.event_name == 'workflow_dispatch' && inputs.expected_sha || '' }}",
|
|
207
209
|
DRY_RUN: "${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || false }}",
|
|
210
|
+
GH_TOKEN: "${{ github.token }}",
|
|
208
211
|
},
|
|
209
212
|
// prettier-ignore
|
|
210
213
|
run: stringUtils.dedent(
|
|
@@ -246,9 +249,15 @@ function verifyContextJob(tagPrefix: string, releaseBranch: string): Job {
|
|
|
246
249
|
case "\${{ inputs.source_run_id }}" in *[!0-9]*|"") exit 1 ;; esac
|
|
247
250
|
fi
|
|
248
251
|
fi
|
|
252
|
+
BUILD_MODE="$(git log -1 --format=%B -- VERSION | sed -n 's/^Release-Build: //p' | tail -1)"
|
|
253
|
+
case "$BUILD_MODE" in local|remote) ;; *) BUILD_MODE="remote" ;; esac
|
|
254
|
+
if [ "$BUILD_MODE" = "local" ] && ! gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
|
|
255
|
+
gh release create "$RELEASE_TAG" --draft --title "$RELEASE_TAG" --target "$RELEASE_SHA"
|
|
256
|
+
fi
|
|
249
257
|
echo "release_tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT"
|
|
250
258
|
echo "expected_sha=$RELEASE_SHA" >> "$GITHUB_OUTPUT"
|
|
251
259
|
echo "release_version=$RELEASE_VERSION" >> "$GITHUB_OUTPUT"
|
|
260
|
+
echo "build_mode=$BUILD_MODE" >> "$GITHUB_OUTPUT"
|
|
252
261
|
`
|
|
253
262
|
// ============================================================================
|
|
254
263
|
),
|
|
@@ -1021,6 +1030,11 @@ export class DBXToolsRelease extends Component {
|
|
|
1021
1030
|
description:
|
|
1022
1031
|
"Prepare, validate, locally publish, and automatically merge a release PR",
|
|
1023
1032
|
},
|
|
1033
|
+
"release:assets": {
|
|
1034
|
+
exec: taskScript(project, "release-assets.ts"),
|
|
1035
|
+
receiveArgs: true,
|
|
1036
|
+
description: "Build and upload native assets for an existing GitHub Release",
|
|
1037
|
+
},
|
|
1024
1038
|
}
|
|
1025
1039
|
: {}),
|
|
1026
1040
|
});
|
package/src/watch.ts
CHANGED
|
@@ -81,13 +81,28 @@ function ignoredPath(path: string): boolean {
|
|
|
81
81
|
* (e.g. the projenrc watcher passes `{ dot: false }` so its lone dotfile target,
|
|
82
82
|
* `.projenrc.ts`, isn't pruned by the default dotfile group and left with nothing to
|
|
83
83
|
* watch - which would let the process exit immediately).
|
|
84
|
+
*
|
|
85
|
+
* `check` is forwarded to the workspace mutation lock so a watcher that can tell
|
|
86
|
+
* the batch is a no-op never waits on an unrelated holder.
|
|
84
87
|
*/
|
|
88
|
+
export interface WatchLoopOptions {
|
|
89
|
+
ignoreOptions?: IgnoreGroupOptions;
|
|
90
|
+
check?: (changed: string[]) => boolean | Promise<boolean>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resolveWatchLoopOptions(config?: IgnoreGroupOptions | WatchLoopOptions): WatchLoopOptions {
|
|
94
|
+
if (!config) return {};
|
|
95
|
+
if ("check" in config || "ignoreOptions" in config) return config as WatchLoopOptions;
|
|
96
|
+
return { ignoreOptions: config as IgnoreGroupOptions };
|
|
97
|
+
}
|
|
98
|
+
|
|
85
99
|
export function watchLoop(
|
|
86
100
|
tag: string,
|
|
87
101
|
paths: string[],
|
|
88
102
|
onBatch: (changed: string[]) => void | Promise<void>,
|
|
89
|
-
|
|
103
|
+
config?: IgnoreGroupOptions | WatchLoopOptions,
|
|
90
104
|
): void {
|
|
105
|
+
const { ignoreOptions, check } = resolveWatchLoopOptions(config);
|
|
91
106
|
const pending = new Set<string>();
|
|
92
107
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
93
108
|
let running = false;
|
|
@@ -105,7 +120,9 @@ export function watchLoop(
|
|
|
105
120
|
pending.clear();
|
|
106
121
|
try {
|
|
107
122
|
if (relevant.length) {
|
|
108
|
-
await withWorkspaceMutationLock(repoRoot, () => onBatch(relevant)
|
|
123
|
+
await withWorkspaceMutationLock(repoRoot, () => onBatch(relevant), {
|
|
124
|
+
check: check ? () => check(relevant) : undefined,
|
|
125
|
+
});
|
|
109
126
|
}
|
|
110
127
|
} catch (err) {
|
|
111
128
|
logger.error(`${tag} cycle failed:`, err instanceof Error ? err.message : err);
|
package/src/workspace-lock.ts
CHANGED
|
@@ -6,17 +6,43 @@ import { log } from "@dbx-tools/shared-core";
|
|
|
6
6
|
const MUTATION_LOCK_SCOPE = "dbx-tools-workspace-mutation";
|
|
7
7
|
const logger = log.logger("projen:workspace-lock");
|
|
8
8
|
|
|
9
|
+
export interface WorkspaceMutationLockOptions {
|
|
10
|
+
/**
|
|
11
|
+
* When this returns false, skip both the lock and the callback. After the
|
|
12
|
+
* lock is held it runs again so a completed holder can make the work a no-op
|
|
13
|
+
* without taking the critical section only to return.
|
|
14
|
+
*/
|
|
15
|
+
check?: () => boolean | Promise<boolean>;
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
/**
|
|
10
19
|
* Serialize synthesis, generated-file watchers, and release preparation for one
|
|
11
20
|
* repository without coordinating unrelated worktrees or repositories.
|
|
21
|
+
*
|
|
22
|
+
* `check` is check-lock-check: a false result before acquire never waits, and a
|
|
23
|
+
* false result after acquire releases immediately without running `callback`.
|
|
12
24
|
*/
|
|
13
|
-
export function withWorkspaceMutationLock<T>(
|
|
25
|
+
export async function withWorkspaceMutationLock<T>(
|
|
14
26
|
root: string,
|
|
15
27
|
callback: () => T | Promise<T>,
|
|
16
|
-
|
|
28
|
+
options?: WorkspaceMutationLockOptions,
|
|
29
|
+
): Promise<T | undefined> {
|
|
17
30
|
const repository = resolve(root);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
31
|
+
const check = options?.check;
|
|
32
|
+
if (check && !(await check())) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return fileLock.withFileLock(
|
|
36
|
+
[MUTATION_LOCK_SCOPE, repository],
|
|
37
|
+
async () => {
|
|
38
|
+
if (check && !(await check())) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return callback();
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
backends: process.platform === "win32" ? ["file"] : ["flock", "file"],
|
|
45
|
+
onWait: () => logger.info("waiting for workspace mutation lock", { repository }),
|
|
46
|
+
},
|
|
47
|
+
);
|
|
22
48
|
}
|
package/tasks/barrels.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env -S bun
|
|
2
2
|
import { sep } from "node:path";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
|
-
import { log, stringUtils } from "@dbx-tools/shared-core";
|
|
4
|
+
import { log, object, stringUtils } from "@dbx-tools/shared-core";
|
|
5
5
|
import { generateBarrels } from "../src/barrels.ts";
|
|
6
6
|
import { recordedPackages } from "../src/packages.ts";
|
|
7
7
|
import { watchLoop, watchRoots } from "../src/watch.ts";
|
|
@@ -17,41 +17,58 @@ const { values } = parseArgs({
|
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
/** The recorded package dir that owns `abs`, if any (for a targeted barrel rebuild). */
|
|
20
|
-
function ownerPackageDir(
|
|
21
|
-
|
|
20
|
+
function ownerPackageDir(
|
|
21
|
+
abs: string,
|
|
22
|
+
packages: readonly { readonly dir: string; readonly tags: readonly string[] }[],
|
|
23
|
+
): string | undefined {
|
|
24
|
+
// The OpenAPI task rebuilds generated-client barrels after writing its files.
|
|
25
|
+
return packages.find(
|
|
26
|
+
({ dir, tags }) => !tags.includes("openapi") && (abs === dir || abs.startsWith(dir + sep)),
|
|
27
|
+
)?.dir;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function changedBarrelTargets(changed: readonly string[]): object.Sequence<string> {
|
|
31
|
+
const packages = recordedPackages();
|
|
32
|
+
return object
|
|
33
|
+
.sequence(changed)
|
|
34
|
+
.map((path) => ownerPackageDir(path, packages))
|
|
35
|
+
.nonNull()
|
|
36
|
+
.distinct();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function warnUnownedChanges(changed: readonly string[]): void {
|
|
40
|
+
if (changed.length) {
|
|
41
|
+
logger.warn(
|
|
42
|
+
`no recorded package owns ${stringUtils.pluralize(changed.length, "change")}; ` +
|
|
43
|
+
"run `bun run default` (or touch .projenrc.ts) to pick up a new package folder",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
22
46
|
}
|
|
23
47
|
|
|
24
48
|
if (values.watch) {
|
|
25
49
|
// Watch the package roots; a source edit inside a package rebuilds just that
|
|
26
50
|
// package's `index.ts` barrel (no re-synth - the projenrc watcher owns that).
|
|
27
51
|
// watchLoop already drops generated paths, so a barrel write never re-triggers us.
|
|
28
|
-
watchLoop(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
const n = generateBarrels({ dirs: [...dirs] });
|
|
53
|
-
if (n) logger.success(`rebuilt ${stringUtils.pluralize(n, "barrel")}`);
|
|
54
|
-
});
|
|
52
|
+
watchLoop(
|
|
53
|
+
"barrels",
|
|
54
|
+
watchRoots(),
|
|
55
|
+
(changed) => {
|
|
56
|
+
const n = generateBarrels({ dirs: [...changedBarrelTargets(changed)] });
|
|
57
|
+
if (n) logger.success(`rebuilt ${stringUtils.pluralize(n, "barrel")}`);
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
check: (changed) => {
|
|
61
|
+
const targets = changedBarrelTargets(changed);
|
|
62
|
+
// A change under a package root that no recorded package owns is a new
|
|
63
|
+
// folder: only a re-synth can create its barrel, so this watcher skips.
|
|
64
|
+
if (!targets.some(() => true)) {
|
|
65
|
+
warnUnownedChanges(changed);
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
);
|
|
55
72
|
} else {
|
|
56
73
|
const dirs = Array.isArray(values.dir)
|
|
57
74
|
? values.dir.filter((value): value is string => typeof value === "string")
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env -S bun
|
|
2
|
+
/** Compile source-first workspace packages in a few TypeScript processes. */
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { availableParallelism } from "node:os";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import * as exec from "@dbx-tools/core/exec";
|
|
7
|
+
|
|
8
|
+
interface PackageManifest {
|
|
9
|
+
readonly name?: string;
|
|
10
|
+
readonly scripts?: Record<string, string>;
|
|
11
|
+
readonly workspaces?: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface TaskManifest {
|
|
15
|
+
readonly tasks?: {
|
|
16
|
+
readonly compile?: {
|
|
17
|
+
readonly steps?: Array<{ readonly execArgs?: string[] }>;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CompilePlan {
|
|
23
|
+
readonly typescriptConfigs: string[];
|
|
24
|
+
readonly customPackages: Array<{ readonly name: string; readonly directory: string }>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readJson<T>(path: string): T {
|
|
28
|
+
return JSON.parse(readFileSync(path, "utf8")) as T;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function plainTypeScriptBuild(steps: unknown): boolean {
|
|
32
|
+
if (!Array.isArray(steps) || steps.length !== 1) return false;
|
|
33
|
+
const step = steps[0] as { execArgs?: string[] };
|
|
34
|
+
if (!step || typeof step !== "object") return false;
|
|
35
|
+
return (
|
|
36
|
+
Object.keys(step).length === 1 &&
|
|
37
|
+
step.execArgs?.length === 2 &&
|
|
38
|
+
step.execArgs[0] === "tsc" &&
|
|
39
|
+
step.execArgs[1] === "--build"
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Read the root's concrete workspace paths on every run, including newly added members. */
|
|
44
|
+
export function compilePlan(root: string): CompilePlan {
|
|
45
|
+
const workspace = readJson<PackageManifest>(join(root, "package.json"));
|
|
46
|
+
const typescriptConfigs: string[] = [];
|
|
47
|
+
const customPackages: CompilePlan["customPackages"] = [];
|
|
48
|
+
for (const member of workspace.workspaces ?? []) {
|
|
49
|
+
const directory = resolve(root, member);
|
|
50
|
+
const manifest = readJson<PackageManifest>(join(directory, "package.json"));
|
|
51
|
+
const script = manifest.scripts?.compile;
|
|
52
|
+
if (!script) continue;
|
|
53
|
+
const taskPath = join(directory, ".projen", "tasks.json");
|
|
54
|
+
const steps = existsSync(taskPath)
|
|
55
|
+
? readJson<TaskManifest>(taskPath).tasks?.compile?.steps
|
|
56
|
+
: undefined;
|
|
57
|
+
if (
|
|
58
|
+
script === "projen compile" &&
|
|
59
|
+
!manifest.scripts?.precompile &&
|
|
60
|
+
!manifest.scripts?.postcompile &&
|
|
61
|
+
plainTypeScriptBuild(steps)
|
|
62
|
+
) {
|
|
63
|
+
typescriptConfigs.push(join(directory, "tsconfig.json"));
|
|
64
|
+
} else {
|
|
65
|
+
customPackages.push({ name: manifest.name ?? member, directory });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { typescriptConfigs, customPackages };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const emoji = /[\p{Extended_Pictographic}\uFE0F\u200D]/gu;
|
|
72
|
+
|
|
73
|
+
async function main(): Promise<void> {
|
|
74
|
+
const root = process.cwd();
|
|
75
|
+
const plan = compilePlan(root);
|
|
76
|
+
const jobs: Array<{ name: string; result: Promise<exec.ExecResult> }> = [];
|
|
77
|
+
if (plan.typescriptConfigs.length > 0) {
|
|
78
|
+
const batchCount = Math.min(4, availableParallelism(), plan.typescriptConfigs.length);
|
|
79
|
+
const batches = Array.from({ length: batchCount }, () => [] as string[]);
|
|
80
|
+
plan.typescriptConfigs.forEach((config, index) => batches[index % batchCount]!.push(config));
|
|
81
|
+
console.log(
|
|
82
|
+
`Type-checking ${plan.typescriptConfigs.length} workspaces in ${batchCount} batches`,
|
|
83
|
+
);
|
|
84
|
+
for (const [index, batch] of batches.entries()) {
|
|
85
|
+
jobs.push({
|
|
86
|
+
name: `TypeScript batch ${index + 1}`,
|
|
87
|
+
result: exec.spawn("bunx", ["tsc", "--build", ...batch], {
|
|
88
|
+
cwd: root,
|
|
89
|
+
stdin: "ignore",
|
|
90
|
+
stdout: "inherit",
|
|
91
|
+
stderr: "inherit",
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const pkg of plan.customPackages) {
|
|
97
|
+
jobs.push({
|
|
98
|
+
name: pkg.name,
|
|
99
|
+
result: exec.spawn("bun", ["run", "compile"], {
|
|
100
|
+
cwd: pkg.directory,
|
|
101
|
+
stdin: "ignore",
|
|
102
|
+
stdout: (line) => console.log(`${pkg.name}: ${line.replace(emoji, "")}`),
|
|
103
|
+
stderr: (line) => console.error(`${pkg.name}: ${line.replace(emoji, "")}`),
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const results = await Promise.allSettled(jobs.map((job) => job.result));
|
|
108
|
+
let failed = false;
|
|
109
|
+
for (const [index, result] of results.entries()) {
|
|
110
|
+
if (result.status === "rejected") {
|
|
111
|
+
console.error(`${jobs[index]!.name} compile failed:`, result.reason);
|
|
112
|
+
failed = true;
|
|
113
|
+
} else if (result.value.exitCode !== 0) {
|
|
114
|
+
console.error(`${jobs[index]!.name} compile exited with ${result.value.exitCode}`);
|
|
115
|
+
failed = true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (failed) process.exitCode = 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (import.meta.main) await main();
|
package/tasks/openapi.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env -S bun
|
|
2
|
-
import { log, stringUtils } from "@dbx-tools/shared-core";
|
|
2
|
+
import { log, object, stringUtils } from "@dbx-tools/shared-core";
|
|
3
3
|
import { generateBarrels } from "../src/barrels.ts";
|
|
4
|
-
import { generateOpenapi,
|
|
4
|
+
import { generateOpenapi, isOpenapiSource, openapiWatchRoots } from "../src/openapi.ts";
|
|
5
5
|
import { runSynth } from "../src/scaffold.ts";
|
|
6
|
-
import { watchLoop
|
|
6
|
+
import { watchLoop } from "../src/watch.ts";
|
|
7
7
|
|
|
8
8
|
const logger = log.logger("projen:openapi");
|
|
9
9
|
|
|
@@ -11,14 +11,18 @@ if (process.argv.includes("--watch")) {
|
|
|
11
11
|
// Watch the package roots; a changed tsoa controller regenerates the openapi
|
|
12
12
|
// packages (spec + client) and rebuilds just their barrels. The projenrc watcher
|
|
13
13
|
// (alongside under `concurrently`) owns full re-synth, so no re-synth here.
|
|
14
|
-
watchLoop(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
watchLoop(
|
|
15
|
+
"openapi",
|
|
16
|
+
openapiWatchRoots(),
|
|
17
|
+
async () => {
|
|
18
|
+
const dirs = await generateOpenapi();
|
|
19
|
+
if (dirs.length) {
|
|
20
|
+
generateBarrels({ dirs });
|
|
21
|
+
logger.success(`regenerated openapi (${stringUtils.pluralize(dirs.length, "package")})`);
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{ check: (changed) => object.sequence(changed).some(isOpenapiSource) },
|
|
25
|
+
);
|
|
22
26
|
} else {
|
|
23
27
|
// One-shot: regenerate, then re-synth so any newly created openapi folder becomes a
|
|
24
28
|
// workspace member (a new package needs install + linking, which the watch path skips).
|