@dbarrett24/tsdown-config 1.0.2 → 1.1.1

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/dist/index.d.ts CHANGED
@@ -1,5 +1,43 @@
1
1
  import { UserConfig } from "tsdown";
2
2
 
3
+ //#region entryManifest.d.ts
4
+ /**
5
+ * Published-source support for the Evolve UI MCP.
6
+ *
7
+ * The MCP indexes design-system *sources* at runtime, so every publishable library ships
8
+ * `src/**` and an `evolve-ui.entries.json` manifest inside `dist/` (the tarball root). The
9
+ * manifest is generated from the same entry maps tsdown consumes, so the published subpaths
10
+ * it lists can never drift from the real build entries.
11
+ */
12
+ /** Manifest filename at the package (tarball) root. */
13
+ declare const ENTRY_MANIFEST_FILENAME = "evolve-ui.entries.json";
14
+ /** Which entry map an entry came from; the MCP scanner uses it to classify barrels. */
15
+ type EntryManifestSource = 'clientEntries' | 'entries';
16
+ type EntryManifestEntry = {
17
+ /** Entry key, which is also the published subpath. */exportName: string; /** Entry file as written in the build config, relative to the package root. */
18
+ filePath: string;
19
+ source: EntryManifestSource;
20
+ };
21
+ type EntryManifest = {
22
+ entries: EntryManifestEntry[];
23
+ package: string;
24
+ schemaVersion: 1;
25
+ };
26
+ /** Whether a path relative to `src/` belongs in the published source copy. */
27
+ declare const shouldShipSource: (relativePath: string) => boolean;
28
+ /**
29
+ * Copy the shippable subset of `sourceDir` into `targetDir`, preserving structure. A missing
30
+ * source directory is a no-op so packages without `src/` still build.
31
+ */
32
+ declare const copySources: (sourceDir: string, targetDir: string) => void;
33
+ /**
34
+ * Build the manifest from the same entry maps handed to tsdown. When a key appears in both
35
+ * maps, `clientEntries` wins — matching how the build merges them into one `entry` object.
36
+ */
37
+ declare const buildEntryManifest: (packageName: string, entries: Record<string, string>, clientEntries?: Record<string, string>) => EntryManifest;
38
+ /** Write the manifest into `targetDir` (the dist/tarball root). */
39
+ declare const writeEntryManifest: (packageName: string, entries: Record<string, string>, clientEntries?: Record<string, string>, targetDir?: string) => void;
40
+ //#endregion
3
41
  //#region index.d.ts
4
42
  declare const makeConfig: (entries: Record<string, string>, clientEntries?: Record<string, string>, optionOverrides?: Partial<UserConfig>) => UserConfig;
5
43
  declare const defaultConfigObject: {
@@ -16,5 +54,5 @@ declare const defaultConfigObject: {
16
54
  };
17
55
  };
18
56
  //#endregion
19
- export { defaultConfigObject, makeConfig };
57
+ export { ENTRY_MANIFEST_FILENAME, type EntryManifest, type EntryManifestEntry, type EntryManifestSource, buildEntryManifest, copySources, defaultConfigObject, makeConfig, shouldShipSource, writeEntryManifest };
20
58
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,8 +1,135 @@
1
1
  import { readWantedLockfile } from "@pnpm/lockfile-file";
2
2
  import { merge } from "es-toolkit";
3
+ import * as fs$1 from "fs";
3
4
  import fs from "fs";
4
5
  import path from "path";
5
6
  import { defineConfig } from "tsdown";
7
+ //#region entryManifest.ts
8
+ /**
9
+ * Published-source support for the Evolve UI MCP.
10
+ *
11
+ * The MCP indexes design-system *sources* at runtime, so every publishable library ships
12
+ * `src/**` and an `evolve-ui.entries.json` manifest inside `dist/` (the tarball root). The
13
+ * manifest is generated from the same entry maps tsdown consumes, so the published subpaths
14
+ * it lists can never drift from the real build entries.
15
+ */
16
+ /** Manifest filename at the package (tarball) root. */
17
+ const ENTRY_MANIFEST_FILENAME = "evolve-ui.entries.json";
18
+ /**
19
+ * Inclusion allowlist for the source copy: the MCP scanner only ever reads TS/TSX, and an
20
+ * allowlist cannot silently ship whatever new file type lands in `src/` later.
21
+ */
22
+ const SHIPPABLE_EXTENSIONS = [".ts", ".tsx"];
23
+ const EXCLUDED_NAME_INFIXES = [
24
+ ".spec.",
25
+ ".test.",
26
+ ".stories."
27
+ ];
28
+ const EXCLUDED_DIRECTORY_NAMES = /* @__PURE__ */ new Set(["__mocks__", "__snapshots__"]);
29
+ /** Whether a path relative to `src/` belongs in the published source copy. */
30
+ const shouldShipSource = (relativePath) => {
31
+ const segments = relativePath.split(/[\\/]/);
32
+ if (segments.some((segment) => EXCLUDED_DIRECTORY_NAMES.has(segment))) return false;
33
+ const fileName = segments[segments.length - 1] ?? "";
34
+ if (!SHIPPABLE_EXTENSIONS.some((extension) => fileName.endsWith(extension))) return false;
35
+ return !EXCLUDED_NAME_INFIXES.some((infix) => fileName.includes(infix));
36
+ };
37
+ const copyDirectory = (sourceDir, targetDir, relativePrefix) => {
38
+ for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
39
+ const relativePath = relativePrefix.length === 0 ? entry.name : `${relativePrefix}/${entry.name}`;
40
+ const sourcePath = path.join(sourceDir, entry.name);
41
+ const targetPath = path.join(targetDir, entry.name);
42
+ if (entry.isDirectory()) {
43
+ if (!EXCLUDED_DIRECTORY_NAMES.has(entry.name)) copyDirectory(sourcePath, targetPath, relativePath);
44
+ continue;
45
+ }
46
+ if (entry.isFile() && shouldShipSource(relativePath)) {
47
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
48
+ fs.copyFileSync(sourcePath, targetPath);
49
+ }
50
+ }
51
+ };
52
+ /**
53
+ * Copy the shippable subset of `sourceDir` into `targetDir`, preserving structure. A missing
54
+ * source directory is a no-op so packages without `src/` still build.
55
+ */
56
+ const copySources = (sourceDir, targetDir) => {
57
+ if (!fs.existsSync(sourceDir)) return;
58
+ copyDirectory(sourceDir, targetDir, "");
59
+ };
60
+ /**
61
+ * Build the manifest from the same entry maps handed to tsdown. When a key appears in both
62
+ * maps, `clientEntries` wins — matching how the build merges them into one `entry` object.
63
+ */
64
+ const buildEntryManifest = (packageName, entries, clientEntries = {}) => {
65
+ const manifestEntries = [];
66
+ for (const [exportName, filePath] of Object.entries(entries)) if (!(exportName in clientEntries)) manifestEntries.push({
67
+ exportName,
68
+ filePath,
69
+ source: "entries"
70
+ });
71
+ for (const [exportName, filePath] of Object.entries(clientEntries)) manifestEntries.push({
72
+ exportName,
73
+ filePath,
74
+ source: "clientEntries"
75
+ });
76
+ return {
77
+ entries: manifestEntries,
78
+ package: packageName,
79
+ schemaVersion: 1
80
+ };
81
+ };
82
+ /** Write the manifest into `targetDir` (the dist/tarball root). */
83
+ const writeEntryManifest = (packageName, entries, clientEntries = {}, targetDir = "dist") => {
84
+ const manifest = buildEntryManifest(packageName, entries, clientEntries);
85
+ fs.mkdirSync(targetDir, { recursive: true });
86
+ fs.writeFileSync(path.join(targetDir, ENTRY_MANIFEST_FILENAME), `${JSON.stringify(manifest, null, 4)}\n`);
87
+ };
88
+ //#endregion
89
+ //#region json-file.ts
90
+ /**
91
+ * The one way this repo turns JSON text into a typed value.
92
+ *
93
+ * A bare `JSON.parse(...)` returns `any`, so `JSON.parse(text).name` compiles whether or not the
94
+ * file has a `name` — which is how the entry manifest once shipped `package: undefined` (PR #55).
95
+ * The caller states the shape it relies on as a type guard, and a mismatch throws an error naming
96
+ * the file and the failed expectation.
97
+ *
98
+ * This is the single generic core (`isJsonObject`, `parseJson`, `readJsonFile`). `scripts/lib/json-file.ts`
99
+ * imports it and adds only the guards scripts/ itself needs; do not copy it. Kept in its own
100
+ * dependency-free module (like `workspace-specifier.ts`) so it unit-tests without loading tsdown.
101
+ *
102
+ * `pnpm check-json-parse` fails on a bare `JSON.parse` anywhere in scope, and exempts this file
103
+ * because it is where the one permitted `JSON.parse` lives.
104
+ */
105
+ const isJsonObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
106
+ const isString = (value) => typeof value === "string";
107
+ const isOptional = (value, guard) => value === void 0 || guard(value);
108
+ const isStringRecord = (value) => isJsonObject(value) && Object.values(value).every(isString);
109
+ const isSourceManifest = (value) => isJsonObject(value) && isString(value.name) && isString(value.version) && isOptional(value.dependencies, isStringRecord) && isOptional(value.peerDependencies, isStringRecord) && isOptional(value.publishConfig, (v) => isJsonObject(v) && isOptional(v.access, isString)) && isOptional(value.bin, isStringRecord) && isOptional(value.scripts, (v) => isJsonObject(v) && isOptional(v.postinstall, isString));
110
+ /** A manifest that declares a string `version`, which is all a workspace dependency lookup needs. */
111
+ const hasStringVersion = (value) => isJsonObject(value) && isString(value.version);
112
+ /**
113
+ * Parse JSON text and check it against `guard`.
114
+ *
115
+ * @param source what the text came from (a path, or a description such as `WebSocket frame`); it
116
+ * leads the error message
117
+ * @param expectation what the guard checks, for the error message (e.g. `a package.json object`)
118
+ */
119
+ const parseJson = (text, source, guard, expectation = "the expected shape") => {
120
+ let parsed;
121
+ try {
122
+ parsed = JSON.parse(text);
123
+ } catch (error) {
124
+ const reason = error instanceof Error ? error.message : String(error);
125
+ throw new Error(`${source}: not valid JSON (${reason})`);
126
+ }
127
+ if (!guard(parsed)) throw new Error(`${source}: parsed JSON is not ${expectation}`);
128
+ return parsed;
129
+ };
130
+ /** Read `filePath` as UTF-8 JSON and check it against `guard`; errors name `filePath`. */
131
+ const readJsonFile = (filePath, guard, expectation = "the expected shape") => parseJson(fs$1.readFileSync(filePath, "utf-8"), filePath, guard, expectation);
132
+ //#endregion
6
133
  //#region workspace-specifier.ts
7
134
  /**
8
135
  * Resolution of pnpm workspace protocol specifiers for the emitted `dist/package.json`.
@@ -38,7 +165,7 @@ const makeConfig = (entries, clientEntries, optionOverrides) => {
38
165
  return defineConfig(makeConfigObject(entries, clientEntries, optionOverrides));
39
166
  };
40
167
  const copyPackageJson = async () => {
41
- const packageJSON = JSON.parse(fs.readFileSync("package.json").toString());
168
+ const packageJSON = readJsonFile("package.json", isSourceManifest, "a package.json manifest");
42
169
  const newPackageJSON = {};
43
170
  newPackageJSON.name = packageJSON.name;
44
171
  newPackageJSON.dependencies = packageJSON.dependencies;
@@ -95,21 +222,13 @@ const copyPackageJson = async () => {
95
222
  const packageName = process.cwd().split(path.sep).slice(-2).join("/");
96
223
  const lockfile_package_list = (await readWantedLockfile("../..", { ignoreIncompatible: true }))?.importers?.[packageName];
97
224
  const merged_deps = merge(lockfile_package_list?.dependencies ?? {}, lockfile_package_list?.devDependencies ?? {});
98
- for (const item of Object.entries(newPackageJSON.peerDependencies)) {
99
- const [dep, version] = item;
100
- if (isWorkspaceSpecifier(version)) {
101
- const package_path = merged_deps?.[dep].version.replace(/(?:link:|\/dist)/g, "") + "/package.json";
102
- const packageJSON = JSON.parse(fs.readFileSync(package_path).toString());
103
- newPackageJSON.peerDependencies[dep] = resolveWorkspaceSpecifier(version, packageJSON.version);
104
- }
225
+ for (const [dep, version] of Object.entries(packageJSON.peerDependencies ?? {})) if (isWorkspaceSpecifier(version)) {
226
+ const packageJSON = readJsonFile(merged_deps?.[dep].version.replace(/(?:link:|\/dist)/g, "") + "/package.json", hasStringVersion, "a package.json with a string \"version\"");
227
+ newPackageJSON.peerDependencies[dep] = resolveWorkspaceSpecifier(version, packageJSON.version);
105
228
  }
106
- for (const item of Object.entries(newPackageJSON.dependencies ?? {})) {
107
- const [dep, version] = item;
108
- if (isWorkspaceSpecifier(version)) {
109
- const package_path = merged_deps?.[dep].version.replace(/(?:link:|\/dist)/g, "") + "/package.json";
110
- const packageJSON = JSON.parse(fs.readFileSync(package_path).toString());
111
- newPackageJSON.dependencies[dep] = resolveWorkspaceSpecifier(version, packageJSON.version);
112
- }
229
+ for (const [dep, version] of Object.entries(packageJSON.dependencies ?? {})) if (isWorkspaceSpecifier(version)) {
230
+ const packageJSON = readJsonFile(merged_deps?.[dep].version.replace(/(?:link:|\/dist)/g, "") + "/package.json", hasStringVersion, "a package.json with a string \"version\"");
231
+ newPackageJSON.dependencies[dep] = resolveWorkspaceSpecifier(version, packageJSON.version);
113
232
  }
114
233
  fs.writeFileSync("dist/package.json", formatEmittedPackageJson(newPackageJSON));
115
234
  };
@@ -163,7 +282,11 @@ const makeConfigObject = (entries, clientEntries = {}, optionOverrides = {}) =>
163
282
  ...optionOverrides,
164
283
  onSuccess: async (config, signal) => {
165
284
  if (typeof optionOverrides.onSuccess === "function") await optionOverrides.onSuccess(config, signal);
285
+ const packageName = readJsonFile("package.json", isJsonObject, "a package.json object").name;
286
+ if (typeof packageName !== "string" || packageName.length === 0) throw new Error("tsdown-config: package.json has no \"name\" to write into the entry manifest");
166
287
  await copyPackageJson();
288
+ copySources("src", "dist/src");
289
+ writeEntryManifest(packageName, entries, clientEntries, "dist");
167
290
  fillDuplicateDts({
168
291
  ...entries,
169
292
  ...clientEntries
@@ -173,6 +296,6 @@ const makeConfigObject = (entries, clientEntries = {}, optionOverrides = {}) =>
173
296
  };
174
297
  };
175
298
  //#endregion
176
- export { defaultConfigObject, makeConfig };
299
+ export { ENTRY_MANIFEST_FILENAME, buildEntryManifest, copySources, defaultConfigObject, makeConfig, shouldShipSource, writeEntryManifest };
177
300
 
178
301
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbarrett24/tsdown-config",
3
- "version": "1.0.2",
3
+ "version": "1.1.1",
4
4
  "description": "Shared tsdown configuration for building component libraries (publish-from-dist with generated wildcard exports)",
5
5
  "type": "module",
6
6
  "module": "dist/index.js",
@@ -55,8 +55,8 @@
55
55
  "lint-fix": "pnpm lint-oxlint-fix && pnpm lint-oxfmt-fix",
56
56
  "lint-oxfmt": "oxfmt --config ../../.oxfmtrc.json --check \"*.ts\"",
57
57
  "lint-oxfmt-fix": "oxfmt --config ../../.oxfmtrc.json --write \"*.ts\"",
58
- "lint-oxlint": "oxlint index.ts tsdown.config.ts workspace-specifier.ts",
59
- "lint-oxlint-fix": "oxlint --fix index.ts tsdown.config.ts workspace-specifier.ts",
58
+ "lint-oxlint": "oxlint index.ts json-file.ts tsdown.config.ts workspace-specifier.ts",
59
+ "lint-oxlint-fix": "oxlint --fix index.ts json-file.ts tsdown.config.ts workspace-specifier.ts",
60
60
  "ts-check": "tsc --noEmit",
61
61
  "test": "jest --passWithNoTests"
62
62
  }