@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/bun-app.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* `src/`), they are outside the package's `tsconfig` include, so their `Bun.*`
|
|
23
23
|
* usage never trips the `app` package's DOM-only `compile`.
|
|
24
24
|
*/
|
|
25
|
-
import {
|
|
25
|
+
import { dedentLines } from "@dbx-tools/shared-core";
|
|
26
26
|
import { type Project, TextFile } from "projen";
|
|
27
27
|
import { header, headerLines } from "./generated.ts";
|
|
28
28
|
|
|
@@ -35,6 +35,17 @@ export const BUN_APP_OVERRIDES = [BUN_DEV_OVERRIDE, BUN_BUILD_OVERRIDE];
|
|
|
35
35
|
const PROJEN_HEADER = { tool: "projen", source: ".projenrc.ts" } as const;
|
|
36
36
|
const PROJEN_TS_HEADER = header(PROJEN_HEADER).trimEnd().replaceAll("\n", "\n ");
|
|
37
37
|
|
|
38
|
+
/** Paths owned by a generated Bun browser build. */
|
|
39
|
+
export interface BunBuildFileOptions {
|
|
40
|
+
readonly entrypoint?: string;
|
|
41
|
+
readonly outdir?: string;
|
|
42
|
+
readonly publicDir?: string;
|
|
43
|
+
/** Root resolved by the conventional `@/` source import alias. */
|
|
44
|
+
readonly sourceRoot?: string;
|
|
45
|
+
/** TypeScript configuration used by Bun for path aliases and JSX settings. */
|
|
46
|
+
readonly tsconfig?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
38
49
|
/**
|
|
39
50
|
* Root `bunfig.toml`: pin bun's HOISTED linker workspace-wide.
|
|
40
51
|
*
|
|
@@ -126,14 +137,17 @@ export class BunDevServerFile extends TextFile {
|
|
|
126
137
|
super(project, "dev.ts", {
|
|
127
138
|
marker: true,
|
|
128
139
|
readonly: true,
|
|
129
|
-
lines:
|
|
140
|
+
lines: dedentLines(source, { trimEnd: false }),
|
|
130
141
|
});
|
|
131
142
|
}
|
|
132
143
|
}
|
|
133
144
|
|
|
134
145
|
/** `build.ts`: a `Bun.build()` production bundle (Tailwind via the plugin API). */
|
|
135
146
|
export class BunBuildFile extends TextFile {
|
|
136
|
-
constructor(project: Project) {
|
|
147
|
+
constructor(project: Project, options: BunBuildFileOptions = {}) {
|
|
148
|
+
const entrypoint = options.entrypoint ?? "index.html";
|
|
149
|
+
const outdir = options.outdir ?? "dist";
|
|
150
|
+
const publicDir = options.publicDir ?? "public";
|
|
137
151
|
// Raw template preserves backslashes in generated regular expression literals.
|
|
138
152
|
// prettier-ignore
|
|
139
153
|
const source = (
|
|
@@ -143,21 +157,51 @@ export class BunBuildFile extends TextFile {
|
|
|
143
157
|
${PROJEN_TS_HEADER}
|
|
144
158
|
import { existsSync } from "node:fs";
|
|
145
159
|
import { cp, rm } from "node:fs/promises";
|
|
146
|
-
import {
|
|
160
|
+
import { createRequire } from "node:module";
|
|
161
|
+
import { basename${options.sourceRoot ? ", resolve" : ""} } from "node:path";
|
|
162
|
+
import type { BunPlugin } from "bun";
|
|
147
163
|
import tailwind from "bun-plugin-tailwind";
|
|
148
164
|
|
|
165
|
+
const require = createRequire(import.meta.url);
|
|
166
|
+
const reactEntries = new Map(
|
|
167
|
+
["react", "react/jsx-runtime", "react/jsx-dev-runtime", "react-dom", "react-dom/client"].map(
|
|
168
|
+
(specifier) => [specifier, require.resolve(specifier)],
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
const dedupeReact: BunPlugin = {
|
|
172
|
+
name: "dedupe-react",
|
|
173
|
+
setup(build) {
|
|
174
|
+
build.onResolve({ filter: /^react(?:-dom)?(?:\/.*)?$/ }, ({ path }) => {
|
|
175
|
+
const resolved = reactEntries.get(path);
|
|
176
|
+
return resolved ? { path: resolved } : undefined;
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
${options.sourceRoot ? /*ts*/ `
|
|
181
|
+
const sourceRoot = resolve(import.meta.dirname, ${JSON.stringify(options.sourceRoot)});
|
|
182
|
+
const sourceAlias: BunPlugin = {
|
|
183
|
+
name: "source-alias",
|
|
184
|
+
setup(build) {
|
|
185
|
+
build.onResolve({ filter: new RegExp("^@/") }, ({ path }) => ({
|
|
186
|
+
path: Bun.resolveSync(\`./\${path.slice(2)}\`, sourceRoot),
|
|
187
|
+
}));
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
` : ""}
|
|
191
|
+
|
|
149
192
|
// Unmanaged override (${BUN_BUILD_OVERRIDE}): its default export is merged over
|
|
150
193
|
// the generated build options - later wins.
|
|
151
194
|
let options: Bun.BuildConfig = {
|
|
152
|
-
entrypoints: [
|
|
153
|
-
outdir:
|
|
195
|
+
entrypoints: [${JSON.stringify(entrypoint)}],
|
|
196
|
+
outdir: ${JSON.stringify(outdir)},
|
|
154
197
|
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
|
155
198
|
minify: true,
|
|
156
199
|
splitting: true,
|
|
157
200
|
publicPath: "/",
|
|
158
201
|
external: ["/fonts/*"],
|
|
159
202
|
sourcemap: "none",
|
|
160
|
-
plugins: [tailwind],
|
|
203
|
+
plugins: [tailwind, dedupeReact${options.sourceRoot ? ", sourceAlias" : ""}],
|
|
204
|
+
${options.tsconfig ? ` tsconfig: ${JSON.stringify(options.tsconfig)},` : ""}
|
|
161
205
|
};
|
|
162
206
|
|
|
163
207
|
const overrideUrl = new URL("./${BUN_BUILD_OVERRIDE}", import.meta.url);
|
|
@@ -167,7 +211,7 @@ export class BunBuildFile extends TextFile {
|
|
|
167
211
|
options = { ...options, ...override, plugins: [...(options.plugins ?? []), ...(override.plugins ?? [])] };
|
|
168
212
|
}
|
|
169
213
|
|
|
170
|
-
const outdir = options.outdir ??
|
|
214
|
+
const outdir = options.outdir ?? ${JSON.stringify(outdir)};
|
|
171
215
|
await rm(outdir, { recursive: true, force: true });
|
|
172
216
|
|
|
173
217
|
const result = await Bun.build(options);
|
|
@@ -195,7 +239,7 @@ export class BunBuildFile extends TextFile {
|
|
|
195
239
|
await Bun.write(htmlOut.path, html);
|
|
196
240
|
}
|
|
197
241
|
|
|
198
|
-
const publicDir =
|
|
242
|
+
const publicDir = ${JSON.stringify(publicDir)};
|
|
199
243
|
if (existsSync(publicDir)) {
|
|
200
244
|
await cp(publicDir, outdir, { recursive: true });
|
|
201
245
|
}
|
|
@@ -207,7 +251,7 @@ export class BunBuildFile extends TextFile {
|
|
|
207
251
|
super(project, "build.ts", {
|
|
208
252
|
marker: true,
|
|
209
253
|
readonly: true,
|
|
210
|
-
lines:
|
|
254
|
+
lines: dedentLines(source, { trimEnd: false }),
|
|
211
255
|
});
|
|
212
256
|
}
|
|
213
257
|
}
|
package/src/clean.ts
CHANGED
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { existsSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
21
21
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
22
|
+
import { findFiles } from "@dbx-tools/path";
|
|
23
|
+
import { parseRecord } from "@dbx-tools/shared-core";
|
|
24
24
|
import { isGenerated, makeWritable } from "./generated.ts";
|
|
25
25
|
import { resolveRepoRoot, toPosix } from "./packages.ts";
|
|
26
26
|
|
|
@@ -39,11 +39,11 @@ const CLEAN_SKIP_FILES: ReadonlySet<string> = new Set([".gitignore"]);
|
|
|
39
39
|
*/
|
|
40
40
|
async function nativeGeneratedFiles(root: string): Promise<string[]> {
|
|
41
41
|
const files = new Set<string>();
|
|
42
|
-
for await (const manifestPath of
|
|
42
|
+
for await (const manifestPath of findFiles("**/.projen/files.json", {
|
|
43
43
|
cwd: root,
|
|
44
44
|
ignoreOptions: { dot: false },
|
|
45
45
|
})) {
|
|
46
|
-
const manifest =
|
|
46
|
+
const manifest = parseRecord(readFileSync(resolve(root, manifestPath), "utf8"));
|
|
47
47
|
const projectRoot = dirname(dirname(resolve(root, manifestPath)));
|
|
48
48
|
const entries = manifest?.files;
|
|
49
49
|
if (!Array.isArray(entries)) continue;
|
|
@@ -57,7 +57,7 @@ async function nativeGeneratedFiles(root: string): Promise<string[]> {
|
|
|
57
57
|
export async function listGeneratedFiles(root: string = resolveRepoRoot()): Promise<string[]> {
|
|
58
58
|
const rel = (f: string): string => toPosix(relative(root, f));
|
|
59
59
|
const custom: string[] = [];
|
|
60
|
-
for await (const file of
|
|
60
|
+
for await (const file of findFiles("**/*", { cwd: root })) {
|
|
61
61
|
const absolute = join(root, file);
|
|
62
62
|
if (isGenerated(absolute)) custom.push(absolute);
|
|
63
63
|
}
|
|
@@ -85,7 +85,7 @@ export async function listNodeModulesDirs(root: string = resolveRepoRoot()): Pro
|
|
|
85
85
|
if (!existsSync(root)) return [];
|
|
86
86
|
const rel = (f: string): string => toPosix(relative(root, f));
|
|
87
87
|
const directories: string[] = [];
|
|
88
|
-
for await (const match of
|
|
88
|
+
for await (const match of findFiles("**/node_modules", {
|
|
89
89
|
cwd: root,
|
|
90
90
|
ignoreOptions: { dot: false },
|
|
91
91
|
})) {
|
package/src/engine-root.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { dirname } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
9
|
+
import { root } from "@dbx-tools/core";
|
|
10
|
+
import { memoize } from "@dbx-tools/shared-core";
|
|
11
11
|
|
|
12
12
|
const ENGINE_PKG = "@dbx-tools/projen";
|
|
13
13
|
|
|
@@ -20,8 +20,8 @@ const ENGINE_PKG = "@dbx-tools/projen";
|
|
|
20
20
|
* {@link functionUtils.memoize}, which caches only a successful result - a
|
|
21
21
|
* throw is retried on the next call.
|
|
22
22
|
*/
|
|
23
|
-
export const resolvePkgRoot =
|
|
24
|
-
const found =
|
|
23
|
+
export const resolvePkgRoot = memoize((): string => {
|
|
24
|
+
const found = root(dirname(fileURLToPath(import.meta.url)));
|
|
25
25
|
if (!found) throw new Error(`${ENGINE_PKG} package root not found`);
|
|
26
26
|
return found;
|
|
27
27
|
});
|
package/src/mixin.ts
CHANGED
|
@@ -9,8 +9,6 @@
|
|
|
9
9
|
import type { Predicate } from "@dbx-tools/shared-core";
|
|
10
10
|
import type { IConstruct, IMixin as ConstructsMixin } from "constructs";
|
|
11
11
|
|
|
12
|
-
export type { ConstructsMixin };
|
|
13
|
-
|
|
14
12
|
/**
|
|
15
13
|
* Builds a {@link ConstructsMixin} from `supports` and `applyTo` callbacks.
|
|
16
14
|
*
|
package/src/module-exports.ts
CHANGED
|
@@ -86,13 +86,12 @@ const FUNCTION_DECLARATIONS = new Set(["FunctionDeclaration", "TSDeclareFunction
|
|
|
86
86
|
export function moduleExports(file: string): ModuleExport[] {
|
|
87
87
|
const body = moduleStatements(file);
|
|
88
88
|
|
|
89
|
-
// Dedupe within the module
|
|
90
|
-
//
|
|
91
|
-
// wins (a value declaration and a same-named type would be unusual and are
|
|
92
|
-
// collapsed to whichever appears first).
|
|
89
|
+
// Dedupe within the module. When TypeScript merges a type and value with the
|
|
90
|
+
// same name, the value export carries both namespaces and must win.
|
|
93
91
|
const byName = new Map<string, ModuleExport>();
|
|
94
92
|
const push = (e: ModuleExport): void => {
|
|
95
|
-
|
|
93
|
+
const existing = byName.get(e.name);
|
|
94
|
+
if (!existing || (existing.isType && !e.isType)) byName.set(e.name, e);
|
|
96
95
|
};
|
|
97
96
|
for (const stmt of body) {
|
|
98
97
|
if (stmt.type !== "ExportNamedDeclaration") continue;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/** Side-effect-free npm archive packing helpers. */
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
import { spawnSync, root } from "@dbx-tools/core";
|
|
15
|
+
|
|
16
|
+
import type { NpmArchiveManifest } from "./npm-release.ts";
|
|
17
|
+
import { materializeWorkspaceManifest } from "./workspace-manifest.ts";
|
|
18
|
+
|
|
19
|
+
/** Transform applied to a packed npm manifest before archive verification. */
|
|
20
|
+
export type NpmArchiveManifestTransform = (
|
|
21
|
+
manifest: NpmArchiveManifest,
|
|
22
|
+
) => Readonly<Record<string, unknown>>;
|
|
23
|
+
|
|
24
|
+
const PUBLISH_CONFIG_ENTRY_FIELDS = ["main", "types", "bin", "exports"] as const;
|
|
25
|
+
|
|
26
|
+
/** Fold compiled publish entry points onto an archive manifest. */
|
|
27
|
+
export function applyPublishConfig(
|
|
28
|
+
source: Readonly<Record<string, unknown>>,
|
|
29
|
+
): Readonly<Record<string, unknown>> {
|
|
30
|
+
const manifest = { ...source };
|
|
31
|
+
const publishConfig = manifest.publishConfig as Record<string, unknown> | undefined;
|
|
32
|
+
if (!publishConfig) return manifest;
|
|
33
|
+
for (const field of PUBLISH_CONFIG_ENTRY_FIELDS) {
|
|
34
|
+
if (field in publishConfig) manifest[field] = publishConfig[field];
|
|
35
|
+
}
|
|
36
|
+
return manifest;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function archiveFileEntries(directory: string, prefix = ""): string[] {
|
|
40
|
+
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
41
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
42
|
+
return entry.isDirectory()
|
|
43
|
+
? archiveFileEntries(join(directory, entry.name), relativePath)
|
|
44
|
+
: [relativePath];
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Pack one workspace package and optionally transform only its archived manifest. */
|
|
49
|
+
export function packNpmPackage(
|
|
50
|
+
directory: string,
|
|
51
|
+
destination: string,
|
|
52
|
+
path = process.env.PATH ?? "",
|
|
53
|
+
transformManifest?: NpmArchiveManifestTransform,
|
|
54
|
+
): string {
|
|
55
|
+
const executable = process.versions.bun ? process.execPath : "bun";
|
|
56
|
+
const manifest = join(directory, "package.json");
|
|
57
|
+
const originalManifest = readFileSync(manifest, "utf8");
|
|
58
|
+
const workspaceRoot = root(directory) ?? directory;
|
|
59
|
+
const workspaceManifest = JSON.parse(
|
|
60
|
+
readFileSync(join(workspaceRoot, "package.json"), "utf8"),
|
|
61
|
+
) as Record<string, unknown>;
|
|
62
|
+
const packedManifest = `${JSON.stringify(
|
|
63
|
+
materializeWorkspaceManifest(
|
|
64
|
+
JSON.parse(originalManifest) as Record<string, unknown>,
|
|
65
|
+
workspaceManifest,
|
|
66
|
+
),
|
|
67
|
+
null,
|
|
68
|
+
2,
|
|
69
|
+
)}\n`;
|
|
70
|
+
const manifestMode = lstatSync(manifest).mode & 0o777;
|
|
71
|
+
const restoreManifestMode = (manifestMode & 0o200) === 0;
|
|
72
|
+
const existingArchives = new Set(readdirSync(destination));
|
|
73
|
+
if (restoreManifestMode) chmodSync(manifest, manifestMode | 0o200);
|
|
74
|
+
try {
|
|
75
|
+
writeFileSync(manifest, packedManifest);
|
|
76
|
+
spawnSync(
|
|
77
|
+
executable,
|
|
78
|
+
["pm", "pack", "--destination", destination, "--ignore-scripts", "--quiet"],
|
|
79
|
+
{
|
|
80
|
+
cwd: directory,
|
|
81
|
+
env: { ...process.env, PATH: path },
|
|
82
|
+
stdout: "inherit",
|
|
83
|
+
stderr: "inherit",
|
|
84
|
+
stdin: "ignore",
|
|
85
|
+
check: true,
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
} finally {
|
|
89
|
+
writeFileSync(manifest, originalManifest);
|
|
90
|
+
if (restoreManifestMode) chmodSync(manifest, manifestMode);
|
|
91
|
+
}
|
|
92
|
+
const archives = readdirSync(destination).filter(
|
|
93
|
+
(file) => file.endsWith(".tgz") && !existingArchives.has(file),
|
|
94
|
+
);
|
|
95
|
+
if (archives.length !== 1) {
|
|
96
|
+
throw new Error(`Expected one packed npm archive, found ${archives.length}`);
|
|
97
|
+
}
|
|
98
|
+
const archive = join(destination, archives[0]!);
|
|
99
|
+
if (!transformManifest) return archive;
|
|
100
|
+
|
|
101
|
+
const temp = mkdtempSync(join(tmpdir(), "projen-npm-project-"));
|
|
102
|
+
try {
|
|
103
|
+
spawnSync("tar", ["-xzf", archive, "-C", temp], {
|
|
104
|
+
cwd: process.cwd(),
|
|
105
|
+
stdout: "ignore",
|
|
106
|
+
stderr: "inherit",
|
|
107
|
+
stdin: "ignore",
|
|
108
|
+
check: true,
|
|
109
|
+
});
|
|
110
|
+
const manifestPath = join(temp, "package", "package.json");
|
|
111
|
+
const archivedManifest = JSON.parse(readFileSync(manifestPath, "utf8")) as NpmArchiveManifest;
|
|
112
|
+
writeFileSync(
|
|
113
|
+
manifestPath,
|
|
114
|
+
`${JSON.stringify(transformManifest(archivedManifest), null, 2)}\n`,
|
|
115
|
+
);
|
|
116
|
+
rmSync(archive, { force: true });
|
|
117
|
+
spawnSync("tar", ["-czf", archive, "-C", temp, ...archiveFileEntries(temp)], {
|
|
118
|
+
cwd: process.cwd(),
|
|
119
|
+
stdout: "ignore",
|
|
120
|
+
stderr: "inherit",
|
|
121
|
+
stdin: "ignore",
|
|
122
|
+
check: true,
|
|
123
|
+
});
|
|
124
|
+
} finally {
|
|
125
|
+
rmSync(temp, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
return archive;
|
|
128
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/** Side-effect-free npm release identity and public registry helpers. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
readlinkSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join, relative } from "node:path";
|
|
14
|
+
import { spawnSync } from "@dbx-tools/core";
|
|
15
|
+
import { PUBLIC_NPM_REGISTRY_URL } from "./release-context.ts";
|
|
16
|
+
|
|
17
|
+
/** Registry and archive identity used to verify idempotent npm publication. */
|
|
18
|
+
export interface NpmReleaseIdentity {
|
|
19
|
+
readonly access?: "public" | "restricted";
|
|
20
|
+
readonly contentDigest?: string;
|
|
21
|
+
readonly integrity?: string;
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly repository?: unknown;
|
|
24
|
+
readonly version: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** npm package fields read directly from one release archive. */
|
|
28
|
+
export interface NpmArchiveManifest {
|
|
29
|
+
readonly [key: string]: unknown;
|
|
30
|
+
readonly dependencies?: Readonly<Record<string, string>>;
|
|
31
|
+
readonly optionalDependencies?: Readonly<Record<string, string>>;
|
|
32
|
+
readonly name?: string;
|
|
33
|
+
readonly publishConfig?: { readonly access?: unknown };
|
|
34
|
+
readonly repository?: unknown;
|
|
35
|
+
readonly version?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Verify that registry metadata represents the exact locally packed release. */
|
|
39
|
+
export function npmReleaseMatches(
|
|
40
|
+
local: NpmReleaseIdentity,
|
|
41
|
+
published: NpmReleaseIdentity | undefined,
|
|
42
|
+
): boolean {
|
|
43
|
+
if (!published) return false;
|
|
44
|
+
if (published.name !== local.name || published.version !== local.version) {
|
|
45
|
+
throw new Error(`Published npm identity does not match ${local.name}@${local.version}`);
|
|
46
|
+
}
|
|
47
|
+
const localRepository = normalizedRepository(local.repository);
|
|
48
|
+
const publishedRepository = normalizedRepository(published.repository);
|
|
49
|
+
if (localRepository && publishedRepository && localRepository !== publishedRepository) {
|
|
50
|
+
throw new Error(`Published npm repository does not match ${local.name}@${local.version}`);
|
|
51
|
+
}
|
|
52
|
+
if (
|
|
53
|
+
local.contentDigest &&
|
|
54
|
+
published.contentDigest &&
|
|
55
|
+
local.contentDigest !== published.contentDigest
|
|
56
|
+
) {
|
|
57
|
+
throw new Error(`Published npm content does not match ${local.name}@${local.version}`);
|
|
58
|
+
}
|
|
59
|
+
if (
|
|
60
|
+
!(local.contentDigest && published.contentDigest) &&
|
|
61
|
+
local.integrity &&
|
|
62
|
+
published.integrity !== local.integrity
|
|
63
|
+
) {
|
|
64
|
+
throw new Error(`Published npm integrity does not match ${local.name}@${local.version}`);
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Read one published npm release identity, or `undefined` when it is absent. */
|
|
70
|
+
export async function publishedNpmRelease(
|
|
71
|
+
name: string,
|
|
72
|
+
version: string,
|
|
73
|
+
registry = process.env.NPM_CONFIG_REGISTRY ?? PUBLIC_NPM_REGISTRY_URL,
|
|
74
|
+
signal?: AbortSignal,
|
|
75
|
+
): Promise<NpmReleaseIdentity | undefined> {
|
|
76
|
+
const response = await fetch(registryUrl(registry, name, version), {
|
|
77
|
+
headers: { accept: "application/json" },
|
|
78
|
+
signal,
|
|
79
|
+
});
|
|
80
|
+
if (response.status === 404) return undefined;
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
throw new Error(`npm registry lookup failed for ${name}@${version}: ${response.status}`);
|
|
83
|
+
}
|
|
84
|
+
const metadata = (await response.json()) as {
|
|
85
|
+
dist?: { integrity?: string; tarball?: string };
|
|
86
|
+
name?: string;
|
|
87
|
+
repository?: unknown;
|
|
88
|
+
version?: string;
|
|
89
|
+
};
|
|
90
|
+
if (!metadata.name || !metadata.version) {
|
|
91
|
+
throw new Error(`npm registry returned an incomplete identity for ${name}@${version}`);
|
|
92
|
+
}
|
|
93
|
+
let contentDigest: string | undefined;
|
|
94
|
+
if (metadata.dist?.tarball) {
|
|
95
|
+
const archive = await fetch(metadata.dist.tarball, { signal });
|
|
96
|
+
if (!archive.ok) {
|
|
97
|
+
throw new Error(`npm tarball lookup failed for ${name}@${version}: ${archive.status}`);
|
|
98
|
+
}
|
|
99
|
+
const temp = mkdtempSync(join(tmpdir(), "projen-published-npm-"));
|
|
100
|
+
const path = join(temp, "package.tgz");
|
|
101
|
+
try {
|
|
102
|
+
writeFileSync(path, Buffer.from(await archive.arrayBuffer()));
|
|
103
|
+
contentDigest = npmArchiveContentDigest(path);
|
|
104
|
+
} finally {
|
|
105
|
+
rmSync(temp, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
...(contentDigest ? { contentDigest } : {}),
|
|
110
|
+
integrity: metadata.dist?.integrity,
|
|
111
|
+
name: metadata.name,
|
|
112
|
+
repository: metadata.repository,
|
|
113
|
+
version: metadata.version,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Read one npm package manifest directly from its archive. */
|
|
118
|
+
export function readNpmArchiveManifest(path: string): NpmArchiveManifest {
|
|
119
|
+
const result = spawnSync("tar", ["-xOf", path, "package/package.json"], {
|
|
120
|
+
cwd: process.cwd(),
|
|
121
|
+
stdout: "capture",
|
|
122
|
+
stderr: "capture",
|
|
123
|
+
stdin: "ignore",
|
|
124
|
+
check: false,
|
|
125
|
+
});
|
|
126
|
+
if (result.exitCode !== 0 || !result.stdout) {
|
|
127
|
+
throw new Error(`Cannot read npm package manifest from ${path}: ${result.stderr}`);
|
|
128
|
+
}
|
|
129
|
+
return JSON.parse(result.stdout) as NpmArchiveManifest;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Read and hash the release identity embedded in an npm archive. */
|
|
133
|
+
export function readNpmArchiveIdentity(path: string): NpmReleaseIdentity {
|
|
134
|
+
const manifest = readNpmArchiveManifest(path);
|
|
135
|
+
if (!manifest.name || !manifest.version) {
|
|
136
|
+
throw new Error(`npm archive has no package name or version: ${path}`);
|
|
137
|
+
}
|
|
138
|
+
const access = manifest.publishConfig?.access;
|
|
139
|
+
if (access !== undefined && access !== "public" && access !== "restricted") {
|
|
140
|
+
throw new Error(`npm archive has invalid publishConfig.access: ${String(access)}`);
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
...(access ? { access } : {}),
|
|
144
|
+
contentDigest: npmArchiveContentDigest(path),
|
|
145
|
+
integrity: `sha512-${createHash("sha512").update(readFileSync(path)).digest("base64")}`,
|
|
146
|
+
name: manifest.name,
|
|
147
|
+
repository: manifest.repository,
|
|
148
|
+
version: manifest.version,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Hash paths, executable bits, symlink targets, and bytes while ignoring tar metadata. */
|
|
153
|
+
export function npmArchiveContentDigest(path: string): string {
|
|
154
|
+
const temp = mkdtempSync(join(tmpdir(), "projen-npm-content-"));
|
|
155
|
+
try {
|
|
156
|
+
spawnSync("tar", ["-xzf", path, "-C", temp], {
|
|
157
|
+
cwd: process.cwd(),
|
|
158
|
+
stdout: "ignore",
|
|
159
|
+
stderr: "capture",
|
|
160
|
+
stdin: "ignore",
|
|
161
|
+
check: true,
|
|
162
|
+
});
|
|
163
|
+
const hash = createHash("sha512");
|
|
164
|
+
const visit = (directory: string): void => {
|
|
165
|
+
for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) =>
|
|
166
|
+
a.name.localeCompare(b.name),
|
|
167
|
+
)) {
|
|
168
|
+
const entryPath = join(directory, entry.name);
|
|
169
|
+
const name = relative(temp, entryPath).split("\\").join("/");
|
|
170
|
+
const stat = lstatSync(entryPath);
|
|
171
|
+
hash.update(`${entry.isDirectory() ? "D" : entry.isSymbolicLink() ? "L" : "F"}\0`);
|
|
172
|
+
hash.update(`${name}\0${stat.mode & 0o111}\0`);
|
|
173
|
+
if (entry.isDirectory()) visit(entryPath);
|
|
174
|
+
else if (entry.isSymbolicLink()) hash.update(readlinkSync(entryPath));
|
|
175
|
+
else hash.update(readFileSync(entryPath));
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
visit(temp);
|
|
179
|
+
return `sha512-${hash.digest("base64")}`;
|
|
180
|
+
} finally {
|
|
181
|
+
rmSync(temp, { recursive: true, force: true });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizedRepository(value: unknown): string | undefined {
|
|
186
|
+
const url =
|
|
187
|
+
typeof value === "string"
|
|
188
|
+
? value
|
|
189
|
+
: value && typeof value === "object" && "url" in value
|
|
190
|
+
? String(value.url)
|
|
191
|
+
: undefined;
|
|
192
|
+
return url
|
|
193
|
+
?.replace(/^git\+/, "")
|
|
194
|
+
.replace(/\.git$/, "")
|
|
195
|
+
.replace(/\/$/, "");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function registryUrl(registry: string, name: string, version: string): string {
|
|
199
|
+
return `${registry.replace(/\/$/, "")}/${encodeURIComponent(name)}/${encodeURIComponent(version)}`;
|
|
200
|
+
}
|
package/src/packages.ts
CHANGED
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import { existsSync, readFileSync } from "node:fs";
|
|
27
27
|
import { extname, relative, resolve, sep } from "node:path";
|
|
28
|
-
import
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
28
|
+
import { projectUtils } from "@dbx-tools/core";
|
|
29
|
+
import { findFilesSync } from "@dbx-tools/path";
|
|
30
|
+
import { parseRecord, isRecord, toSlug, parseList } from "@dbx-tools/shared-core";
|
|
31
31
|
import { parse } from "yaml";
|
|
32
32
|
import { DBXToolsConfigDataSchema, type DBXToolsConfigData } from "./dbx-tools-config.ts";
|
|
33
33
|
import { isGenerated } from "./generated.ts";
|
|
@@ -77,7 +77,7 @@ function nestingTagsFromSegments(segments: readonly string[]): string[] {
|
|
|
77
77
|
const out: string[] = [];
|
|
78
78
|
let acc = "";
|
|
79
79
|
for (const segment of prefix) {
|
|
80
|
-
const token =
|
|
80
|
+
const token = toSlug(segment);
|
|
81
81
|
if (!token) continue;
|
|
82
82
|
acc = acc ? `${acc}-${token}` : token;
|
|
83
83
|
out.push(acc);
|
|
@@ -154,7 +154,7 @@ export class DiscoveredPackage {
|
|
|
154
154
|
* Matched against `packageTagPaths` to resolve applied mixin tags.
|
|
155
155
|
*/
|
|
156
156
|
get tagCandidates(): string[] {
|
|
157
|
-
const leaf =
|
|
157
|
+
const leaf = toSlug(this.name);
|
|
158
158
|
if (leaf === "app" || leaf === "server") return [leaf];
|
|
159
159
|
return nestingTagsFromSegments(this.relSegments);
|
|
160
160
|
}
|
|
@@ -189,7 +189,7 @@ function packageOfMember(projectRoot: string, member: string): DiscoveredPackage
|
|
|
189
189
|
*/
|
|
190
190
|
function collectPackageDirs(rootAbs: string): string[] {
|
|
191
191
|
const owners = new Set<string>();
|
|
192
|
-
for (const file of
|
|
192
|
+
for (const file of findFilesSync(SRC_MODULE_GLOB, { cwd: rootAbs })) {
|
|
193
193
|
if (!isModuleFile(file)) continue;
|
|
194
194
|
const segs = toPosix(file).split("/");
|
|
195
195
|
const srcIdx = segs.indexOf("src");
|
|
@@ -255,7 +255,7 @@ export function readPackageManifest(dir: string): Record<string, unknown> | unde
|
|
|
255
255
|
const path = resolve(dir, "package.json");
|
|
256
256
|
if (!existsSync(path)) return undefined;
|
|
257
257
|
try {
|
|
258
|
-
return
|
|
258
|
+
return parseRecord(readFileSync(path, "utf8"));
|
|
259
259
|
} catch {
|
|
260
260
|
return undefined; // unreadable (permissions, race with a concurrent write)
|
|
261
261
|
}
|
|
@@ -282,13 +282,13 @@ function readManifestTags(dir: string): string[] | undefined {
|
|
|
282
282
|
*/
|
|
283
283
|
export function syncResynthPaths(projectRoot: string = resolveRepoRoot()): string[] {
|
|
284
284
|
const paths = readDbxToolsConfig(projectRoot)?.syncResynthPaths;
|
|
285
|
-
return Array.isArray(paths) ?
|
|
285
|
+
return Array.isArray(paths) ? parseList(paths.map((p) => String(p))) : [];
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
/** Projen task names supervised as focused processes by `sync --watch`. */
|
|
289
289
|
export function syncWatchTasks(projectRoot: string = resolveRepoRoot()): string[] {
|
|
290
290
|
const tasks = readDbxToolsConfig(projectRoot)?.syncWatchTasks;
|
|
291
|
-
return Array.isArray(tasks) ?
|
|
291
|
+
return Array.isArray(tasks) ? parseList(tasks.map((task) => String(task))) : [];
|
|
292
292
|
}
|
|
293
293
|
|
|
294
294
|
/**
|
|
@@ -338,7 +338,7 @@ export function workspaceDependencyDirectories(
|
|
|
338
338
|
directories.add(pkg.dir);
|
|
339
339
|
for (const field of ["dependencies", "optionalDependencies", "peerDependencies"] as const) {
|
|
340
340
|
const dependencies = pkg.manifest[field];
|
|
341
|
-
if (!
|
|
341
|
+
if (!isRecord(dependencies)) continue;
|
|
342
342
|
for (const dependency of Object.keys(dependencies)) visit(dependency);
|
|
343
343
|
}
|
|
344
344
|
};
|
|
@@ -354,7 +354,7 @@ export function workspaceDependencyDirectories(
|
|
|
354
354
|
export function recordedRoots(projectRoot: string = resolveRepoRoot()): string[] {
|
|
355
355
|
const configured = readDbxToolsConfig(projectRoot)?.packageRoots;
|
|
356
356
|
const configuredRoots = Array.isArray(configured)
|
|
357
|
-
?
|
|
357
|
+
? parseList(configured.map((root) => String(root)))
|
|
358
358
|
: [];
|
|
359
359
|
const roots = new Set<string>(configuredRoots.length ? configuredRoots : DEFAULT_PACKAGE_ROOTS);
|
|
360
360
|
for (const member of readRecordedMembers(projectRoot)) {
|