@dbx-tools/projen 0.3.44 → 0.4.0
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/package.json +1 -1
- package/src/project.ts +14 -4
- package/src/publish.ts +143 -0
- package/tasks/emit.ts +82 -0
package/package.json
CHANGED
package/src/project.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { generateCodegen } from "./codegen";
|
|
|
18
18
|
import { DBXToolsConfig, type DBXToolsConfigOptions } from "./dbx-tools-config";
|
|
19
19
|
import { resolvePkgRoot } from "./engine-root";
|
|
20
20
|
import { PnpmWorkspaceState, type DBXToolsPNPMWorkspaceOptions } from "./pnpm-workspace";
|
|
21
|
+
import { applyCompiledPublish } from "./publish";
|
|
21
22
|
import { DBXToolsRelease, type StandaloneRelease } from "./release";
|
|
22
23
|
import { AGNOSTIC_COMPILER_OPTIONS, PACKAGE_TAG_MIXINS, type PackageTag } from "./tags";
|
|
23
24
|
import { DBXToolsRootTsconfig } from "./tsconfig";
|
|
@@ -234,10 +235,15 @@ export function addExports(pkg: javascript.NodeProject, exports: Record<string,
|
|
|
234
235
|
* and `LICENSE` on top of whatever is listed, so those are never declared here.
|
|
235
236
|
*
|
|
236
237
|
* The baseline (`index.ts` + `src`, set at construction) is the source-first
|
|
237
|
-
* entry surface the `exports` map
|
|
238
|
-
* layout ships outside `src` - the `cli` tag its `bin/` launchers
|
|
239
|
-
*
|
|
240
|
-
*
|
|
238
|
+
* entry surface the workspace's own `exports` map resolves to. A tag adds what
|
|
239
|
+
* its layout ships outside `src` - the `cli` tag its `bin/` launchers - and
|
|
240
|
+
* {@link applyCompiledPublish} adds `lib/`, which is what the PUBLISHED
|
|
241
|
+
* `exports` resolves to. Source ships alongside the compiled output rather than
|
|
242
|
+
* instead of it: it costs little, and it keeps stack traces and go-to-definition
|
|
243
|
+
* landing on real code for consumers that want it.
|
|
244
|
+
*
|
|
245
|
+
* Everything else the build leaves behind (`test/`, `.projen/`, `tsconfig*`) is
|
|
246
|
+
* unreachable through either map and is deliberately withheld.
|
|
241
247
|
*/
|
|
242
248
|
export function addPackageFiles(pkg: javascript.NodeProject, ...entries: string[]): void {
|
|
243
249
|
const current = (pkg.package.manifest.files ?? []) as string[];
|
|
@@ -994,6 +1000,10 @@ function preSynthesizeProject(project: javascript.NodeProject): void {
|
|
|
994
1000
|
}
|
|
995
1001
|
for (const p of subtree) {
|
|
996
1002
|
if (!p.parent) continue;
|
|
1003
|
+
// Swap the source entry points for compiled ones in the PUBLISHED manifest
|
|
1004
|
+
// only. Runs here rather than in the constructor so the tags have already
|
|
1005
|
+
// installed their `exports` layouts for it to mirror.
|
|
1006
|
+
if (p instanceof javascript.NodeProject) applyCompiledPublish(p);
|
|
997
1007
|
// A child's `.gitignore` survives ONLY when it carries custom patterns (see
|
|
998
1008
|
// swapChildGitignore). `.gitattributes` is always dropped - the root's
|
|
999
1009
|
// annotateGenerated globs cover the children. Runs once from the root's
|
package/src/publish.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The compiled publish surface: what a package looks like on npm, as opposed to
|
|
3
|
+
* what it looks like inside this workspace.
|
|
4
|
+
*
|
|
5
|
+
* Packages resolve each other from SOURCE - every `exports` entry points at a
|
|
6
|
+
* `.ts` file, so a cross-package import type-checks with no build step and no
|
|
7
|
+
* `dist` to keep in sync. That property is worth keeping, but it cannot be what
|
|
8
|
+
* ships: Node refuses to strip types under `node_modules`
|
|
9
|
+
* (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`), so a published package whose
|
|
10
|
+
* entry point is `index.ts` is unloadable by anything that is not a bundler.
|
|
11
|
+
* Consumers papered over that by special-casing `@dbx-tools/*` into their own
|
|
12
|
+
* bundle, which is a tax this repo has no business charging.
|
|
13
|
+
*
|
|
14
|
+
* pnpm resolves both at once. It substitutes `publishConfig`'s `main`/`types`/
|
|
15
|
+
* `exports` into the manifest at pack time and drops `publishConfig` itself, so
|
|
16
|
+
* the workspace keeps its source entry points while the tarball advertises the
|
|
17
|
+
* compiled ones. Nothing here changes how the repo builds or type-checks; it
|
|
18
|
+
* only changes what `pnpm pack` writes.
|
|
19
|
+
*
|
|
20
|
+
* Two pieces make the emitted tree actually loadable:
|
|
21
|
+
*
|
|
22
|
+
* - `rootDir: "."` so the package-ROOT `index.ts` barrel is compiled at all.
|
|
23
|
+
* projen's default `rootDir: "src"` puts the barrel outside the compilation,
|
|
24
|
+
* which is why `lib/` has never had an `index.js`.
|
|
25
|
+
* - a specifier pass after `tsc`, because `tsc` never rewrites import paths.
|
|
26
|
+
* Sources are written for `moduleResolution: bundler` and so import `"./http"`,
|
|
27
|
+
* which Node cannot resolve; `tasks/emit.ts` appends the extension that the
|
|
28
|
+
* emitted file actually has. Doing it after the fact keeps 800-odd import
|
|
29
|
+
* sites free of the `.js` suffix that would otherwise have to be written - and
|
|
30
|
+
* maintained - by hand.
|
|
31
|
+
*
|
|
32
|
+
* UI packages are deliberately excluded (see {@link publishesCompiled}).
|
|
33
|
+
*/
|
|
34
|
+
import type { javascript } from "projen";
|
|
35
|
+
import { typescript } from "projen";
|
|
36
|
+
import { addPackageFiles, applyCompilerOptions, applyIncludes, taskScript } from "./project";
|
|
37
|
+
import { isDBXToolsProject } from "./project-predicate";
|
|
38
|
+
|
|
39
|
+
/** Directory `tsc` emits into, and the root of every published entry point. */
|
|
40
|
+
export const COMPILED_DIR = "lib";
|
|
41
|
+
|
|
42
|
+
/** An `exports` target written as TypeScript source, i.e. one with a compiled twin. */
|
|
43
|
+
const TS_SOURCE = /\.tsx?$/;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Whether a package publishes compiled output rather than source.
|
|
47
|
+
*
|
|
48
|
+
* Everything does EXCEPT the `ui` tag, and the exclusion is about consumers
|
|
49
|
+
* rather than convenience. The problem being solved is that Node cannot load raw
|
|
50
|
+
* TypeScript - but a browser package is never loaded by Node. UI packages reach
|
|
51
|
+
* their consumer through Vite, which reads their source happily, and they export
|
|
52
|
+
* `./styles.css` plus raw SVG assets that `tsc` does not copy and could not
|
|
53
|
+
* rewrite. Compiling them would mean a real asset pipeline (Vite library mode)
|
|
54
|
+
* to solve a problem they do not have.
|
|
55
|
+
*
|
|
56
|
+
* This is the same split the downstream app build already makes on its own: its
|
|
57
|
+
* client bundle resolves `@dbx-tools/ui-*` from source without complaint, while
|
|
58
|
+
* its SERVER bundle is the one that had to inline `@dbx-tools/*` to avoid
|
|
59
|
+
* handing Node a `.ts` entry point.
|
|
60
|
+
*/
|
|
61
|
+
export function publishesCompiled(pkg: javascript.NodeProject): boolean {
|
|
62
|
+
if (!(pkg instanceof typescript.TypeScriptProject) || !pkg.parent) return false;
|
|
63
|
+
return isDBXToolsProject(pkg) && !pkg.dbxToolsConfig.tags.includes("ui");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The compiled counterpart of a source `exports` target, or `undefined` for a
|
|
68
|
+
* target that ships as-is (`./package.json`, a stylesheet, an SVG asset).
|
|
69
|
+
*
|
|
70
|
+
* `rootDir: "."` means the emitted tree mirrors the package layout, so the
|
|
71
|
+
* mapping is positional: `./src/react/index.ts` -> `./lib/src/react/index.js`.
|
|
72
|
+
*/
|
|
73
|
+
function compiledTarget(target: string): { types: string; default: string } | undefined {
|
|
74
|
+
if (!TS_SOURCE.test(target)) return undefined;
|
|
75
|
+
const stem = `./${COMPILED_DIR}/${target.replace(/^\.\//, "").replace(TS_SOURCE, "")}`;
|
|
76
|
+
return { types: `${stem}.d.ts`, default: `${stem}.js` };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Derive `publishConfig` from the package's FINAL `exports` map.
|
|
81
|
+
*
|
|
82
|
+
* Runs at preSynthesize precisely so the tags have already installed their
|
|
83
|
+
* export layouts - deriving it any earlier would mirror the constructor's bare
|
|
84
|
+
* `.` entry and silently omit every subpath a tag added.
|
|
85
|
+
*/
|
|
86
|
+
function publishConfig(pkg: javascript.NodeProject): Record<string, unknown> | undefined {
|
|
87
|
+
const exports = (pkg.package.manifest.exports ?? {}) as Record<string, string>;
|
|
88
|
+
const compiled = Object.entries(exports).map(
|
|
89
|
+
([subpath, target]) => [subpath, compiledTarget(target) ?? target] as const,
|
|
90
|
+
);
|
|
91
|
+
// Nothing to swap means the package ships no TypeScript entry point at all;
|
|
92
|
+
// leave its manifest alone rather than writing an inert `publishConfig`.
|
|
93
|
+
if (!compiled.some(([, target]) => typeof target !== "string")) return undefined;
|
|
94
|
+
|
|
95
|
+
// Setting this field REPLACES whatever projen renders into it, and what projen
|
|
96
|
+
// renders is `access` - dropped, every scoped package here would publish as
|
|
97
|
+
// restricted. It is not readable from `manifest` at preSynthesize (projen
|
|
98
|
+
// emits it from `npmAccess` later), so carry it over from that source instead.
|
|
99
|
+
const root = compiled.find(([subpath]) => subpath === ".")?.[1];
|
|
100
|
+
return {
|
|
101
|
+
access: pkg.package.npmAccess,
|
|
102
|
+
...(typeof root === "object" ? { main: root.default, types: root.types } : {}),
|
|
103
|
+
exports: Object.fromEntries(compiled),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Give a package a compiled publish surface: emit the barrel, fix the emitted
|
|
109
|
+
* specifiers, ship `lib/`, and advertise it through `publishConfig`.
|
|
110
|
+
*
|
|
111
|
+
* Idempotent - `preSynthesizeProject` reaches every package twice (once from the
|
|
112
|
+
* root's walk, once from the package's own preSynthesize) and both passes must
|
|
113
|
+
* agree.
|
|
114
|
+
*/
|
|
115
|
+
export function applyCompiledPublish(pkg: javascript.NodeProject): void {
|
|
116
|
+
if (!publishesCompiled(pkg)) return;
|
|
117
|
+
|
|
118
|
+
// The barrel lives at the package root, so the compilation has to start there.
|
|
119
|
+
// The `cli` tag already does this for its `bin/` tree; for everything else it
|
|
120
|
+
// is what puts an `index.js` in the emitted output for the first time.
|
|
121
|
+
applyCompilerOptions(pkg, { rootDir: "." });
|
|
122
|
+
applyIncludes(pkg, "index.ts");
|
|
123
|
+
addPackageFiles(pkg, COMPILED_DIR);
|
|
124
|
+
|
|
125
|
+
const config = publishConfig(pkg);
|
|
126
|
+
if (config) pkg.package.addField("publishConfig", config);
|
|
127
|
+
|
|
128
|
+
// `tsc` emits extensionless relative specifiers; Node ESM cannot resolve them.
|
|
129
|
+
const fixSpecifiers = taskScript(pkg, "emit.ts", COMPILED_DIR);
|
|
130
|
+
if (!pkg.compileTask.steps.some((step) => step.exec === fixSpecifiers)) {
|
|
131
|
+
pkg.compileTask.exec(fixSpecifiers);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// The release workflow publishes straight after `pnpm install`, with no build
|
|
135
|
+
// in between, so the compiled output has to be produced by the pack itself
|
|
136
|
+
// rather than assumed present. This also covers a bare `pnpm pack` and the
|
|
137
|
+
// bump task's local-registry publish.
|
|
138
|
+
if (!pkg.tasks.tryFind("prepack")) {
|
|
139
|
+
pkg.addTask("prepack", { description: "Compile before packing the published tarball" });
|
|
140
|
+
}
|
|
141
|
+
const prepack = pkg.tasks.tryFind("prepack")!;
|
|
142
|
+
if (prepack.steps.length === 0) prepack.spawn(pkg.compileTask);
|
|
143
|
+
}
|
package/tasks/emit.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append the explicit file extension Node ESM requires to every relative
|
|
3
|
+
* specifier in a compiled tree.
|
|
4
|
+
*
|
|
5
|
+
* Sources are written for `moduleResolution: bundler`, so they import `"./http"`
|
|
6
|
+
* with no extension, and `tsc` copies specifiers through untouched. Node's ESM
|
|
7
|
+
* resolver does no extension probing, so that emitted output is unloadable -
|
|
8
|
+
* which is the whole reason these packages used to publish source instead. The
|
|
9
|
+
* alternative fix is to write `"./http.js"` at all 800-odd import sites and keep
|
|
10
|
+
* writing it forever; this pass makes it a build detail instead.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately filesystem-driven rather than clever: each specifier is resolved
|
|
13
|
+
* against what `tsc` actually emitted, so a module that became a file and a
|
|
14
|
+
* module that became a directory are told apart by looking, not by guessing.
|
|
15
|
+
* Anything that does not resolve is left exactly as it was - a bare package
|
|
16
|
+
* specifier, an asset, or a genuine mistake that should surface as itself.
|
|
17
|
+
*
|
|
18
|
+
* Usage: `tsx emit.ts <compiled-dir>` (relative to the package root).
|
|
19
|
+
*/
|
|
20
|
+
import { log } from "@dbx-tools/shared-core";
|
|
21
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { dirname, join, resolve } from "node:path";
|
|
23
|
+
|
|
24
|
+
const logger = log.logger("projen:emit");
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The specifier of a static import/export or a dynamic `import()`.
|
|
28
|
+
*
|
|
29
|
+
* Anchored on the keyword that precedes it so a relative path appearing inside a
|
|
30
|
+
* string literal or a comment is never touched.
|
|
31
|
+
*/
|
|
32
|
+
const SPECIFIER = /(\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*)(["'])(\.\.?\/[^"']*)\2/g;
|
|
33
|
+
|
|
34
|
+
/** Extensions that are already explicit, whether JavaScript or an asset. */
|
|
35
|
+
const EXPLICIT = /\.(js|mjs|cjs|json|css|svg|png|jpg|jpeg|gif|webp)$/;
|
|
36
|
+
|
|
37
|
+
/** Files whose specifiers matter: the emitted JavaScript and its declarations. */
|
|
38
|
+
const EMITTED = /\.(js|mjs|cjs|d\.ts)$/;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* What a relative specifier must become, or `undefined` to leave it alone.
|
|
42
|
+
*
|
|
43
|
+
* Declarations resolve against the emitted `.js` too: inside a `.d.ts`,
|
|
44
|
+
* TypeScript maps a `"./foo.js"` specifier onto the neighbouring `foo.d.ts`, so
|
|
45
|
+
* both file kinds want the same suffix.
|
|
46
|
+
*/
|
|
47
|
+
function retarget(fileDir: string, specifier: string): string | undefined {
|
|
48
|
+
if (EXPLICIT.test(specifier)) return undefined;
|
|
49
|
+
const base = resolve(fileDir, specifier);
|
|
50
|
+
if (existsSync(`${base}.js`)) return `${specifier}.js`;
|
|
51
|
+
if (existsSync(join(base, "index.js"))) return `${specifier}/index.js`;
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Every file under `dir`, recursively. */
|
|
56
|
+
function walk(dir: string): string[] {
|
|
57
|
+
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) =>
|
|
58
|
+
entry.isDirectory() ? walk(join(dir, entry.name)) : [join(dir, entry.name)],
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Rewrite one file in place; returns whether anything changed. */
|
|
63
|
+
function rewrite(file: string): boolean {
|
|
64
|
+
const before = readFileSync(file, "utf8");
|
|
65
|
+
const after = before.replace(SPECIFIER, (match, keyword, quote, specifier) => {
|
|
66
|
+
const next = retarget(dirname(file), specifier as string);
|
|
67
|
+
return next ? `${keyword}${quote}${next}${quote}` : match;
|
|
68
|
+
});
|
|
69
|
+
if (after === before) return false;
|
|
70
|
+
writeFileSync(file, after);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const target = resolve(process.argv[2] ?? "lib");
|
|
75
|
+
// A package whose compile produced nothing (no emit, or a tag that replaced the
|
|
76
|
+
// compile task) is not an error - there is simply nothing to fix up.
|
|
77
|
+
if (existsSync(target)) {
|
|
78
|
+
const changed = walk(target)
|
|
79
|
+
.filter((file) => EMITTED.test(file))
|
|
80
|
+
.filter(rewrite).length;
|
|
81
|
+
if (changed > 0) logger.debug(`emit: resolved specifiers in ${changed} files`);
|
|
82
|
+
}
|