@dbarrett24/tsdown-config 1.0.1 → 1.1.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/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
@@ -3,6 +3,117 @@ import { merge } from "es-toolkit";
3
3
  import fs from "fs";
4
4
  import path from "path";
5
5
  import { defineConfig } from "tsdown";
6
+ //#region entryManifest.ts
7
+ /**
8
+ * Published-source support for the Evolve UI MCP.
9
+ *
10
+ * The MCP indexes design-system *sources* at runtime, so every publishable library ships
11
+ * `src/**` and an `evolve-ui.entries.json` manifest inside `dist/` (the tarball root). The
12
+ * manifest is generated from the same entry maps tsdown consumes, so the published subpaths
13
+ * it lists can never drift from the real build entries.
14
+ */
15
+ /** Manifest filename at the package (tarball) root. */
16
+ const ENTRY_MANIFEST_FILENAME = "evolve-ui.entries.json";
17
+ /**
18
+ * Inclusion allowlist for the source copy: the MCP scanner only ever reads TS/TSX, and an
19
+ * allowlist cannot silently ship whatever new file type lands in `src/` later.
20
+ */
21
+ const SHIPPABLE_EXTENSIONS = [".ts", ".tsx"];
22
+ const EXCLUDED_NAME_INFIXES = [
23
+ ".spec.",
24
+ ".test.",
25
+ ".stories."
26
+ ];
27
+ const EXCLUDED_DIRECTORY_NAMES = /* @__PURE__ */ new Set(["__mocks__", "__snapshots__"]);
28
+ /** Whether a path relative to `src/` belongs in the published source copy. */
29
+ const shouldShipSource = (relativePath) => {
30
+ const segments = relativePath.split(/[\\/]/);
31
+ if (segments.some((segment) => EXCLUDED_DIRECTORY_NAMES.has(segment))) return false;
32
+ const fileName = segments[segments.length - 1] ?? "";
33
+ if (!SHIPPABLE_EXTENSIONS.some((extension) => fileName.endsWith(extension))) return false;
34
+ return !EXCLUDED_NAME_INFIXES.some((infix) => fileName.includes(infix));
35
+ };
36
+ const copyDirectory = (sourceDir, targetDir, relativePrefix) => {
37
+ for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
38
+ const relativePath = relativePrefix.length === 0 ? entry.name : `${relativePrefix}/${entry.name}`;
39
+ const sourcePath = path.join(sourceDir, entry.name);
40
+ const targetPath = path.join(targetDir, entry.name);
41
+ if (entry.isDirectory()) {
42
+ if (!EXCLUDED_DIRECTORY_NAMES.has(entry.name)) copyDirectory(sourcePath, targetPath, relativePath);
43
+ continue;
44
+ }
45
+ if (entry.isFile() && shouldShipSource(relativePath)) {
46
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
47
+ fs.copyFileSync(sourcePath, targetPath);
48
+ }
49
+ }
50
+ };
51
+ /**
52
+ * Copy the shippable subset of `sourceDir` into `targetDir`, preserving structure. A missing
53
+ * source directory is a no-op so packages without `src/` still build.
54
+ */
55
+ const copySources = (sourceDir, targetDir) => {
56
+ if (!fs.existsSync(sourceDir)) return;
57
+ copyDirectory(sourceDir, targetDir, "");
58
+ };
59
+ /**
60
+ * Build the manifest from the same entry maps handed to tsdown. When a key appears in both
61
+ * maps, `clientEntries` wins — matching how the build merges them into one `entry` object.
62
+ */
63
+ const buildEntryManifest = (packageName, entries, clientEntries = {}) => {
64
+ const manifestEntries = [];
65
+ for (const [exportName, filePath] of Object.entries(entries)) if (!(exportName in clientEntries)) manifestEntries.push({
66
+ exportName,
67
+ filePath,
68
+ source: "entries"
69
+ });
70
+ for (const [exportName, filePath] of Object.entries(clientEntries)) manifestEntries.push({
71
+ exportName,
72
+ filePath,
73
+ source: "clientEntries"
74
+ });
75
+ return {
76
+ entries: manifestEntries,
77
+ package: packageName,
78
+ schemaVersion: 1
79
+ };
80
+ };
81
+ /** Write the manifest into `targetDir` (the dist/tarball root). */
82
+ const writeEntryManifest = (packageName, entries, clientEntries = {}, targetDir = "dist") => {
83
+ const manifest = buildEntryManifest(packageName, entries, clientEntries);
84
+ fs.mkdirSync(targetDir, { recursive: true });
85
+ fs.writeFileSync(path.join(targetDir, ENTRY_MANIFEST_FILENAME), `${JSON.stringify(manifest, null, 4)}\n`);
86
+ };
87
+ //#endregion
88
+ //#region workspace-specifier.ts
89
+ /**
90
+ * Resolution of pnpm workspace protocol specifiers for the emitted `dist/package.json`.
91
+ *
92
+ * Kept in its own dependency-free module so it can be unit-tested without loading tsdown or
93
+ * the pnpm lockfile reader. Its correctness is load-bearing at publish time: a specifier that
94
+ * survives into the emitted manifest publishes successfully and is uninstallable for every
95
+ * consumer, which is the most expensive failure this package can produce.
96
+ */
97
+ /**
98
+ * Convert a `workspace:` specifier into a publishable semver range.
99
+ *
100
+ * `workspace:*` pins the dependency exactly, which is right for packages that must move in
101
+ * lockstep. `workspace:^` and `workspace:~` publish as the matching range, which is what a
102
+ * package wants when consumers should be able to pick up the dependency's patch releases —
103
+ * without it, a fix published to the dependency is unreachable behind an exact pin.
104
+ *
105
+ * @param specifier the raw dependency value, e.g. `workspace:^`
106
+ * @param resolvedVersion the dependency's actual version, e.g. `1.2.3`
107
+ */
108
+ const resolveWorkspaceSpecifier = (specifier, resolvedVersion) => {
109
+ if (specifier === "workspace:*") return resolvedVersion;
110
+ if (specifier === "workspace:^") return `^${resolvedVersion}`;
111
+ if (specifier === "workspace:~") return `~${resolvedVersion}`;
112
+ return specifier.replace(/^workspace:/, "");
113
+ };
114
+ /** True when a dependency value uses the pnpm workspace protocol. */
115
+ const isWorkspaceSpecifier = (specifier) => specifier.startsWith("workspace:");
116
+ //#endregion
6
117
  //#region index.ts
7
118
  const formatEmittedPackageJson = (obj) => `${JSON.stringify(obj, null, 4)}\n`;
8
119
  const makeConfig = (entries, clientEntries, optionOverrides) => {
@@ -68,18 +179,18 @@ const copyPackageJson = async () => {
68
179
  const merged_deps = merge(lockfile_package_list?.dependencies ?? {}, lockfile_package_list?.devDependencies ?? {});
69
180
  for (const item of Object.entries(newPackageJSON.peerDependencies)) {
70
181
  const [dep, version] = item;
71
- if (version.includes("workspace")) {
182
+ if (isWorkspaceSpecifier(version)) {
72
183
  const package_path = merged_deps?.[dep].version.replace(/(?:link:|\/dist)/g, "") + "/package.json";
73
184
  const packageJSON = JSON.parse(fs.readFileSync(package_path).toString());
74
- newPackageJSON.peerDependencies[dep] = version.replace("workspace:*", packageJSON.version);
185
+ newPackageJSON.peerDependencies[dep] = resolveWorkspaceSpecifier(version, packageJSON.version);
75
186
  }
76
187
  }
77
188
  for (const item of Object.entries(newPackageJSON.dependencies ?? {})) {
78
189
  const [dep, version] = item;
79
- if (version.includes("workspace")) {
190
+ if (isWorkspaceSpecifier(version)) {
80
191
  const package_path = merged_deps?.[dep].version.replace(/(?:link:|\/dist)/g, "") + "/package.json";
81
192
  const packageJSON = JSON.parse(fs.readFileSync(package_path).toString());
82
- newPackageJSON.dependencies[dep] = version.replace("workspace:*", packageJSON.version);
193
+ newPackageJSON.dependencies[dep] = resolveWorkspaceSpecifier(version, packageJSON.version);
83
194
  }
84
195
  }
85
196
  fs.writeFileSync("dist/package.json", formatEmittedPackageJson(newPackageJSON));
@@ -134,7 +245,11 @@ const makeConfigObject = (entries, clientEntries = {}, optionOverrides = {}) =>
134
245
  ...optionOverrides,
135
246
  onSuccess: async (config, signal) => {
136
247
  if (typeof optionOverrides.onSuccess === "function") await optionOverrides.onSuccess(config, signal);
248
+ const packageName = JSON.parse(fs.readFileSync("package.json").toString()).name;
249
+ if (typeof packageName !== "string" || packageName.length === 0) throw new Error("tsdown-config: package.json has no \"name\" to write into the entry manifest");
137
250
  await copyPackageJson();
251
+ copySources("src", "dist/src");
252
+ writeEntryManifest(packageName, entries, clientEntries, "dist");
138
253
  fillDuplicateDts({
139
254
  ...entries,
140
255
  ...clientEntries
@@ -144,6 +259,6 @@ const makeConfigObject = (entries, clientEntries = {}, optionOverrides = {}) =>
144
259
  };
145
260
  };
146
261
  //#endregion
147
- export { defaultConfigObject, makeConfig };
262
+ export { ENTRY_MANIFEST_FILENAME, buildEntryManifest, copySources, defaultConfigObject, makeConfig, shouldShipSource, writeEntryManifest };
148
263
 
149
264
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dbarrett24/tsdown-config",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
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",
@@ -23,7 +23,9 @@
23
23
  "typescript": "^6.0.3"
24
24
  },
25
25
  "devDependencies": {
26
+ "@swc/jest": "^0.2.39",
26
27
  "concurrently": "^8.2.2",
28
+ "jest": "^30.3.0",
27
29
  "@dbarrett24/typescript-config": "1.0.1"
28
30
  },
29
31
  "peerDependencies": {
@@ -53,9 +55,9 @@
53
55
  "lint-fix": "pnpm lint-oxlint-fix && pnpm lint-oxfmt-fix",
54
56
  "lint-oxfmt": "oxfmt --config ../../.oxfmtrc.json --check \"*.ts\"",
55
57
  "lint-oxfmt-fix": "oxfmt --config ../../.oxfmtrc.json --write \"*.ts\"",
56
- "lint-oxlint": "oxlint index.ts tsdown.config.ts",
57
- "lint-oxlint-fix": "oxlint --fix index.ts tsdown.config.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
60
  "ts-check": "tsc --noEmit",
59
- "test": "echo 'No tests for tsdown-config'"
61
+ "test": "jest --passWithNoTests"
60
62
  }
61
63
  }