ts-repo-utils 10.3.2 → 10.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/README.md CHANGED
@@ -133,6 +133,10 @@ npm exec -- gen-index-ts ./src --target-ext .ts --index-ext .ts --export-ext .js
133
133
  # Skip generating index files at the root level (depth 0), only generate from depth 1 onwards
134
134
  npm exec -- gen-index-ts ./src --target-ext .mts --index-ext .mts --export-ext .mjs --min-depth 1
135
135
 
136
+ # Leave hand-written index files alone: an executable entry point at ./src/index.mts,
137
+ # and a curated list of named re-exports in every ./src/v*/index.mts
138
+ npm exec -- gen-index-ts ./src --target-ext .mts --index-ext .mts --export-ext .mjs --preserve index.mts --preserve 'v*/index.mts'
139
+
136
140
  # Example in npm scripts
137
141
  "gi": "gen-index-ts ./src --index-ext .mts --export-ext .mjs --target-ext .mts --target-ext .tsx --fmt 'npm run fmt'"
138
142
  ```
@@ -159,8 +163,20 @@ npm exec -- gen-index-ts ./src --target-ext .mts --index-ext .mts --export-ext .
159
163
  - `--exclude` - Glob patterns of files to exclude (optional, can be specified multiple times)
160
164
  - `--fmt` - Command to format after generating the index file (optional)
161
165
  - `--min-depth` - Minimum depth to start generating index files (default: 0)
166
+ - `--preserve` - Glob patterns of index files to leave untouched (optional, can be specified multiple times)
162
167
  - `--silent` - Suppress output messages (optional)
163
168
 
169
+ `--exclude` and `--preserve` answer different questions. `--exclude` says what an
170
+ index file may not _export_, and its patterns are matched against a bare file
171
+ name as well as a path relative to the target directory. `--preserve` says which
172
+ index files must not be _written_, and matches on the relative path alone — so
173
+ `--preserve index.mts` names the one at the root of the walk, where
174
+ `--exclude index.mts` would name every index file in the tree (and, index files
175
+ never being exported in the first place, does nothing at all).
176
+
177
+ A preserved directory is still walked, so index files below it are still
178
+ generated, and its parent still re-exports it.
179
+
164
180
  ### `check-should-run`
165
181
 
166
182
  Checks whether a CI step should run, based on which files differ from the base branch. Reports `false` when every changed file matches one of the `--paths-ignore` patterns, so a step can be skipped when nothing it checks has changed.
@@ -91,6 +91,11 @@ const cmdDef = cmd.command({
91
91
  long: "min-depth",
92
92
  type: cmd.optional(cmd.number),
93
93
  description: "Minimum depth to start generating index files (default: 0)"
94
+ }),
95
+ preserve: cmd.multioption({
96
+ long: "preserve",
97
+ type: cmd.optional(cmd.array(cmd.string)),
98
+ description: 'Glob patterns of index files to leave untouched, matched against the path relative to the target directory (e.g., "index.mts", "v*/index.mts")'
94
99
  })
95
100
  },
96
101
  handler: (args) => {
@@ -108,7 +113,8 @@ const main = async ({
108
113
  exclude,
109
114
  formatCommand,
110
115
  silent,
111
- minDepth
116
+ minDepth,
117
+ preserve
112
118
  }) => {
113
119
  await genIndex({
114
120
  targetDirectory: targetDirectory.includes(",") ? targetDirectory.split(",").map((dir) => dir.trim()) : targetDirectory,
@@ -118,7 +124,8 @@ const main = async ({
118
124
  indexFileExtension,
119
125
  formatCommand,
120
126
  silent,
121
- minDepth
127
+ minDepth,
128
+ preserve
122
129
  });
123
130
  };
124
131
  await cmd.run(cmdDef, Arr.skip(process.argv, 2));
@@ -1 +1 @@
1
- {"version":3,"file":"gen-index-ts.mjs","sources":["../../src/cmd/gen-index-ts.mts"],"sourcesContent":["#!/usr/bin/env node\n\n/* transformer-ignore convert-to-readonly */\n\nimport * as cmd from 'cmd-ts';\nimport { Arr, expectType } from 'ts-data-forge';\n// eslint-disable-next-line import-x/no-internal-modules, import-x/extensions\nimport { type InputOf, type OutputOf } from 'cmd-ts/dist/esm/from.js';\nimport { type NonEmptyArray } from 'ts-type-forge';\nimport { genIndex } from '../functions/index.mjs';\n\ntype Ext = `.${string}`;\n\nconst extensionType = cmd.extendType(cmd.string, {\n from: (s) => {\n if (!s.startsWith('.')) {\n throw new Error(`ext should start with '.'`);\n }\n\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n return Promise.resolve(s as Ext);\n },\n});\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst nonEmptyArray = <T extends cmd.Type<any, any>>(\n t: T,\n commandName: string,\n): cmd.Type<readonly InputOf<T>[], NonEmptyArray<OutputOf<T>>> =>\n cmd.extendType(\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n cmd.array(t) as cmd.Type<readonly InputOf<T>[], OutputOf<T>[]>,\n {\n from: (arr) => {\n if (Arr.isEmpty(arr)) {\n throw new Error(\n `No value provided for --${commandName}. At least one value is required.`,\n );\n }\n\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n return Promise.resolve(arr as unknown as NonEmptyArray<OutputOf<T>>);\n },\n },\n );\n\nconst cmdDef = cmd.command({\n name: 'gen-index-ts-cli',\n version: '10.1.8',\n args: {\n // required args\n targetDirectory: cmd.positional({\n type: cmd.string,\n displayName: 'target-directory',\n description:\n 'Directory where the index file will be generated (Comma-separated list can be used)',\n }),\n\n targetExtensions: cmd.multioption({\n long: 'target-ext',\n type: nonEmptyArray(extensionType, 'target-ext'),\n description: 'File extensions to include in the index file',\n }),\n indexFileExtension: cmd.option({\n long: 'index-ext',\n type: extensionType,\n description: 'Extension of the index file to be generated',\n }),\n exportStatementExtension: cmd.option({\n long: 'export-ext',\n type: cmd.union([\n extensionType,\n cmd.extendType(cmd.string, {\n from: (s) => {\n if (s !== 'none') {\n throw new Error(\n `export-ext should be 'none' or a valid extension`,\n );\n }\n\n return Promise.resolve('none' as const);\n },\n }),\n ]),\n description: 'Extension of the export statements in the index file',\n }),\n\n // optional args\n exclude: cmd.multioption({\n long: 'exclude',\n type: cmd.optional(cmd.array(cmd.string)),\n description:\n 'Glob patterns of files to exclude from the index file (e.g., \"*.d.mts\", \"*.test.mts\")',\n }),\n formatCommand: cmd.option({\n long: 'fmt',\n type: cmd.optional(cmd.string),\n description:\n 'Command to format after generating the index file (e.g., \"npm run fmt\")',\n }),\n silent: cmd.flag({\n long: 'silent',\n type: cmd.optional(cmd.boolean),\n description: 'If true, suppresses output messages (default: false)',\n }),\n minDepth: cmd.option({\n long: 'min-depth',\n type: cmd.optional(cmd.number),\n description: 'Minimum depth to start generating index files (default: 0)',\n }),\n },\n handler: (args) => {\n expectType<typeof args.targetDirectory, string>('=');\n\n expectType<typeof args.targetExtensions, NonEmptyArray<Ext>>('=');\n\n expectType<typeof args.exportStatementExtension, Ext | 'none'>('=');\n\n expectType<typeof args.indexFileExtension, Ext>('=');\n\n expectType<typeof args.exclude, string[] | undefined>('=');\n\n expectType<typeof args.formatCommand, string | undefined>('=');\n\n expectType<typeof args.silent, boolean | undefined>('=');\n\n expectType<typeof args.minDepth, number | undefined>('=');\n\n main(args).catch((error: unknown) => {\n console.error('An error occurred:', error);\n\n process.exit(1);\n });\n },\n});\n\nconst main = async ({\n targetDirectory,\n targetExtensions,\n exportStatementExtension,\n indexFileExtension,\n exclude,\n formatCommand,\n silent,\n minDepth,\n}: Readonly<{\n targetDirectory: string;\n targetExtensions: readonly Ext[];\n exportStatementExtension: Ext | 'none';\n indexFileExtension: Ext;\n exclude?: readonly string[] | undefined;\n formatCommand?: string | undefined;\n silent?: boolean | undefined;\n minDepth?: number | undefined;\n}>): Promise<void> => {\n await genIndex({\n targetDirectory: targetDirectory.includes(',')\n ? targetDirectory.split(',').map((dir) => dir.trim())\n : targetDirectory,\n exportStatementExtension,\n targetExtensions,\n exclude,\n indexFileExtension,\n formatCommand,\n silent,\n minDepth,\n });\n};\n\nawait cmd.run(cmdDef, Arr.skip(process.argv, 2));\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"gen-index-ts.mjs","sources":["../../src/cmd/gen-index-ts.mts"],"sourcesContent":["#!/usr/bin/env node\n\n/* transformer-ignore convert-to-readonly */\n\nimport * as cmd from 'cmd-ts';\nimport { Arr, expectType } from 'ts-data-forge';\n// eslint-disable-next-line import-x/no-internal-modules, import-x/extensions\nimport { type InputOf, type OutputOf } from 'cmd-ts/dist/esm/from.js';\nimport { type NonEmptyArray } from 'ts-type-forge';\nimport { genIndex } from '../functions/index.mjs';\n\ntype Ext = `.${string}`;\n\nconst extensionType = cmd.extendType(cmd.string, {\n from: (s) => {\n if (!s.startsWith('.')) {\n throw new Error(`ext should start with '.'`);\n }\n\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n return Promise.resolve(s as Ext);\n },\n});\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst nonEmptyArray = <T extends cmd.Type<any, any>>(\n t: T,\n commandName: string,\n): cmd.Type<readonly InputOf<T>[], NonEmptyArray<OutputOf<T>>> =>\n cmd.extendType(\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n cmd.array(t) as cmd.Type<readonly InputOf<T>[], OutputOf<T>[]>,\n {\n from: (arr) => {\n if (Arr.isEmpty(arr)) {\n throw new Error(\n `No value provided for --${commandName}. At least one value is required.`,\n );\n }\n\n // eslint-disable-next-line total-functions/no-unsafe-type-assertion\n return Promise.resolve(arr as unknown as NonEmptyArray<OutputOf<T>>);\n },\n },\n );\n\nconst cmdDef = cmd.command({\n name: 'gen-index-ts-cli',\n version: '10.1.8',\n args: {\n // required args\n targetDirectory: cmd.positional({\n type: cmd.string,\n displayName: 'target-directory',\n description:\n 'Directory where the index file will be generated (Comma-separated list can be used)',\n }),\n\n targetExtensions: cmd.multioption({\n long: 'target-ext',\n type: nonEmptyArray(extensionType, 'target-ext'),\n description: 'File extensions to include in the index file',\n }),\n indexFileExtension: cmd.option({\n long: 'index-ext',\n type: extensionType,\n description: 'Extension of the index file to be generated',\n }),\n exportStatementExtension: cmd.option({\n long: 'export-ext',\n type: cmd.union([\n extensionType,\n cmd.extendType(cmd.string, {\n from: (s) => {\n if (s !== 'none') {\n throw new Error(\n `export-ext should be 'none' or a valid extension`,\n );\n }\n\n return Promise.resolve('none' as const);\n },\n }),\n ]),\n description: 'Extension of the export statements in the index file',\n }),\n\n // optional args\n exclude: cmd.multioption({\n long: 'exclude',\n type: cmd.optional(cmd.array(cmd.string)),\n description:\n 'Glob patterns of files to exclude from the index file (e.g., \"*.d.mts\", \"*.test.mts\")',\n }),\n formatCommand: cmd.option({\n long: 'fmt',\n type: cmd.optional(cmd.string),\n description:\n 'Command to format after generating the index file (e.g., \"npm run fmt\")',\n }),\n silent: cmd.flag({\n long: 'silent',\n type: cmd.optional(cmd.boolean),\n description: 'If true, suppresses output messages (default: false)',\n }),\n minDepth: cmd.option({\n long: 'min-depth',\n type: cmd.optional(cmd.number),\n description: 'Minimum depth to start generating index files (default: 0)',\n }),\n preserve: cmd.multioption({\n long: 'preserve',\n type: cmd.optional(cmd.array(cmd.string)),\n description:\n 'Glob patterns of index files to leave untouched, matched against the path relative to the target directory (e.g., \"index.mts\", \"v*/index.mts\")',\n }),\n },\n handler: (args) => {\n expectType<typeof args.targetDirectory, string>('=');\n\n expectType<typeof args.targetExtensions, NonEmptyArray<Ext>>('=');\n\n expectType<typeof args.exportStatementExtension, Ext | 'none'>('=');\n\n expectType<typeof args.indexFileExtension, Ext>('=');\n\n expectType<typeof args.exclude, string[] | undefined>('=');\n\n expectType<typeof args.formatCommand, string | undefined>('=');\n\n expectType<typeof args.silent, boolean | undefined>('=');\n\n expectType<typeof args.minDepth, number | undefined>('=');\n\n expectType<typeof args.preserve, string[] | undefined>('=');\n\n main(args).catch((error: unknown) => {\n console.error('An error occurred:', error);\n\n process.exit(1);\n });\n },\n});\n\nconst main = async ({\n targetDirectory,\n targetExtensions,\n exportStatementExtension,\n indexFileExtension,\n exclude,\n formatCommand,\n silent,\n minDepth,\n preserve,\n}: Readonly<{\n targetDirectory: string;\n targetExtensions: readonly Ext[];\n exportStatementExtension: Ext | 'none';\n indexFileExtension: Ext;\n exclude?: readonly string[] | undefined;\n formatCommand?: string | undefined;\n silent?: boolean | undefined;\n minDepth?: number | undefined;\n preserve?: readonly string[] | undefined;\n}>): Promise<void> => {\n await genIndex({\n targetDirectory: targetDirectory.includes(',')\n ? targetDirectory.split(',').map((dir) => dir.trim())\n : targetDirectory,\n exportStatementExtension,\n targetExtensions,\n exclude,\n indexFileExtension,\n formatCommand,\n silent,\n minDepth,\n preserve,\n });\n};\n\nawait cmd.run(cmdDef, Arr.skip(process.argv, 2));\n"],"names":[],"mappings":"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"}
@@ -26,6 +26,23 @@ export type GenIndexConfig = DeepReadonly<{
26
26
  silent?: boolean;
27
27
  /** Minimum depth to start generating index files (default: 0) */
28
28
  minDepth?: number;
29
+ /**
30
+ * Glob patterns of index files to leave untouched, matched against the
31
+ * index file's own path relative to the target directory (POSIX
32
+ * separators), e.g. `'index.mts'` for the one at the root of the walk and
33
+ * `'v*\/index.mts'` for a whole generation of them.
34
+ *
35
+ * This is what protects a hand-written index file — an executable entry
36
+ * point, or a curated list of named re-exports — from being overwritten
37
+ * with a barrel. `exclude` cannot express it: it says what an index file
38
+ * may not *export*, and is matched against a bare file name as well as a
39
+ * relative path, so `'index.mts'` there would name every index file in the
40
+ * tree rather than one of them.
41
+ *
42
+ * A preserved directory is still walked, so index files below it are still
43
+ * generated, and its parent still re-exports it. (default: none)
44
+ */
45
+ preserve?: string[];
29
46
  }>;
30
47
  /**
31
48
  * Generates index.ts files recursively in `config.targetDirectory`.
@@ -1 +1 @@
1
- {"version":3,"file":"gen-index.d.mts","sourceRoot":"","sources":["../../src/functions/gen-index.mts"],"names":[],"mappings":"AAGA,OAAO,EAKL,MAAM,EAEP,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,KAAK,YAAY,EAAmB,MAAM,eAAe,CAAC;AAInE,+CAA+C;AAC/C,MAAM,MAAM,cAAc,GAAG,YAAY,CAAC;IACxC,kFAAkF;IAClF,eAAe,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAEnC;;;;OAIG;IACH,OAAO,CAAC,EACJ,MAAM,EAAE,GACR,CAAC,CACC,IAAI,EAAE,QAAQ,CAAC;QACb,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,CAAC;QACrB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC,KACC,OAAO,CAAC,CAAC;IAElB,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,IAAI,MAAM,EAAE,EAAE,CAAC;IAElC,iEAAiE;IACjE,kBAAkB,CAAC,EAAE,IAAI,MAAM,EAAE,CAAC;IAElC,kEAAkE;IAClE,wBAAwB,CAAC,EAAE,IAAI,MAAM,EAAE,GAAG,MAAM,CAAC;IAEjD,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,mEAAmE;IACnE,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB,iEAAiE;IACjE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC,CAAC;AA8BH;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,WACX,cAAc,KACrB,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CA0DpC,CAAC"}
1
+ {"version":3,"file":"gen-index.d.mts","sourceRoot":"","sources":["../../src/functions/gen-index.mts"],"names":[],"mappings":"AAGA,OAAO,EAML,MAAM,EAEP,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,KAAK,YAAY,EAGlB,MAAM,eAAe,CAAC;AAIvB,+CAA+C;AAC/C,MAAM,MAAM,cAAc,GAAG,YAAY,CAAC;IACxC,kFAAkF;IAClF,eAAe,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAEnC;;;;OAIG;IACH,OAAO,CAAC,EACJ,MAAM,EAAE,GACR,CAAC,CACC,IAAI,EAAE,QAAQ,CAAC;QACb,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,CAAC;QACrB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC,KACC,OAAO,CAAC,CAAC;IAElB,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,IAAI,MAAM,EAAE,EAAE,CAAC;IAElC,iEAAiE;IACjE,kBAAkB,CAAC,EAAE,IAAI,MAAM,EAAE,CAAC;IAElC,kEAAkE;IAClE,wBAAwB,CAAC,EAAE,IAAI,MAAM,EAAE,GAAG,MAAM,CAAC;IAEjD,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,mEAAmE;IACnE,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB,iEAAiE;IACjE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB,CAAC,CAAC;AAgCH;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,WACX,cAAc,KACrB,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,CA0DpC,CAAC"}
@@ -1,7 +1,7 @@
1
1
  import micromatch from 'micromatch';
2
2
  import * as fs from 'node:fs/promises';
3
3
  import * as path from 'node:path';
4
- import { Result, unknownToString, ISet, pipe, Arr, isString } from 'ts-data-forge';
4
+ import { Result, unknownToString, ISet, pipe, Arr, isString, hasKey } from 'ts-data-forge';
5
5
  import { assertPathExists } from './assert-path-exists.mjs';
6
6
  import { $ } from './exec-async.mjs';
7
7
 
@@ -12,7 +12,8 @@ const defaultConfig = {
12
12
  exportStatementExtension: ".js",
13
13
  // For ESM imports, .mts resolves to .mjs
14
14
  silent: false,
15
- minDepth: 0
15
+ minDepth: 0,
16
+ preserve: []
16
17
  };
17
18
  const genIndex = async (config) => {
18
19
  const filledConfig = fillConfig(config);
@@ -85,7 +86,8 @@ const fillConfig = (config) => {
85
86
  indexFileExtension: config.indexFileExtension ?? defaultConfig.indexFileExtension,
86
87
  exportStatementExtension: exportExtension,
87
88
  silent: config.silent ?? defaultConfig.silent,
88
- minDepth: config.minDepth ?? defaultConfig.minDepth
89
+ minDepth: config.minDepth ?? defaultConfig.minDepth,
90
+ preserve: config.preserve ?? defaultConfig.preserve
89
91
  };
90
92
  };
91
93
  const generateIndexFileForDir = async (dirPath, config, baseDir, currentDepth = 0) => {
@@ -124,14 +126,25 @@ const generateIndexFileForDir = async (dirPath, config, baseDir, currentDepth =
124
126
  }
125
127
  }
126
128
  if (currentDepth >= config.minDepth) {
127
- const indexContent = generateIndexContent(
128
- mut_subDirectories,
129
- mut_filesToExport,
130
- config
131
- );
132
129
  const indexPath = path.join(dirPath, `index${config.indexFileExtension}`);
133
- await fs.writeFile(indexPath, indexContent);
134
- conditionalEcho(`Generated: ${path.relative(process.cwd(), indexPath)}`);
130
+ if (isPreservedIndex(
131
+ path.relative(actualBaseDir, indexPath),
132
+ config.preserve
133
+ )) {
134
+ conditionalEcho(
135
+ `Preserved: ${path.relative(process.cwd(), indexPath)}`
136
+ );
137
+ } else {
138
+ const indexContent = generateIndexContent(
139
+ mut_subDirectories,
140
+ mut_filesToExport,
141
+ config
142
+ );
143
+ await fs.writeFile(indexPath, indexContent);
144
+ conditionalEcho(
145
+ `Generated: ${path.relative(process.cwd(), indexPath)}`
146
+ );
147
+ }
135
148
  }
136
149
  } catch (error) {
137
150
  throw new Error(
@@ -140,6 +153,10 @@ const generateIndexFileForDir = async (dirPath, config, baseDir, currentDepth =
140
153
  );
141
154
  }
142
155
  };
156
+ const isPreservedIndex = (indexRelativePath, preserve) => {
157
+ const normalized = indexRelativePath.split(path.sep).join("/");
158
+ return preserve.some((pattern) => micromatch.isMatch(normalized, pattern));
159
+ };
143
160
  const indexRegex = /^index\.[cm]?[jt]s[x]?$/u;
144
161
  const shouldExportFile = ({
145
162
  absolutePath,
@@ -163,14 +180,31 @@ const shouldExportFile = ({
163
180
  }
164
181
  return true;
165
182
  };
183
+ const specifierExtension = (sourceExtension, fallback) => hasKey(emittedExtensions, sourceExtension) ? emittedExtensions[sourceExtension] : fallback;
184
+ const emittedExtensions = {
185
+ ".cjs": ".cjs",
186
+ ".cts": ".cjs",
187
+ ".js": ".js",
188
+ ".jsx": ".js",
189
+ ".mjs": ".mjs",
190
+ ".mts": ".mjs",
191
+ ".ts": ".js",
192
+ ".tsx": ".js"
193
+ };
166
194
  const generateIndexContent = (subDirectories, filesToExport, config) => {
167
195
  const exportStatements = [
168
196
  ...subDirectories.map(
169
- (subDir) => config.exportStatementExtension === "none" ? `export * from "./${subDir}";` : `export * from "./${subDir}/index${config.exportStatementExtension}";`
197
+ (subDir) => config.exportStatementExtension === "none" ? `export * from "./${subDir}";` : `export * from "./${subDir}/index${specifierExtension(
198
+ config.indexFileExtension,
199
+ config.exportStatementExtension
200
+ )}";`
170
201
  ),
171
202
  ...filesToExport.map((file) => {
172
203
  const fileNameWithoutExt = path.basename(file, path.extname(file));
173
- return config.exportStatementExtension === "none" ? `export * from "./${fileNameWithoutExt}";` : `export * from "./${fileNameWithoutExt}${config.exportStatementExtension}";`;
204
+ return config.exportStatementExtension === "none" ? `export * from "./${fileNameWithoutExt}";` : `export * from "./${fileNameWithoutExt}${specifierExtension(
205
+ path.extname(file),
206
+ config.exportStatementExtension
207
+ )}";`;
174
208
  })
175
209
  ];
176
210
  return Arr.isEmpty(exportStatements) ? "export {};" : exportStatements.join("\n");
@@ -1 +1 @@
1
- {"version":3,"file":"gen-index.mjs","sources":["../../src/functions/gen-index.mts"],"sourcesContent":["import micromatch from 'micromatch';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport {\n Arr,\n ISet,\n isString,\n pipe,\n Result,\n unknownToString,\n} from 'ts-data-forge';\nimport { type DeepReadonly, type StrictOmit } from 'ts-type-forge';\nimport { assertPathExists } from './assert-path-exists.mjs';\nimport { $ } from './exec-async.mjs';\n\n/** Configuration for index file generation. */\nexport type GenIndexConfig = DeepReadonly<{\n /** Target directories to generate index files for (string or array of strings) */\n targetDirectory: string | string[];\n\n /**\n * Glob patterns of files or predicate function to exclude from exports\n * (default: excludes `'**\\/*.{test,spec}.?(c|m)[jt]s?(x)'` and\n * `'**\\/*.d.?(c|m)ts'`)\n */\n exclude?:\n | string[]\n | ((\n args: Readonly<{\n absolutePath: string;\n relativePath: string;\n fileName: string;\n }>,\n ) => boolean);\n\n /** File extensions of source files to export (default: ['.ts', '.tsx']) */\n targetExtensions?: `.${string}`[];\n\n /** File extension of index files to generate (default: '.ts') */\n indexFileExtension?: `.${string}`;\n\n /** File extension to use in export statements (default: '.js') */\n exportStatementExtension?: `.${string}` | 'none';\n\n /** Command to run for formatting generated files (optional) */\n formatCommand?: string;\n\n /** Whether to suppress output during execution (default: false) */\n silent?: boolean;\n\n /** Minimum depth to start generating index files (default: 0) */\n minDepth?: number;\n}>;\n\nconst defaultConfig = {\n exclude: ['**/*.{test,spec}.?(c|m)[jt]s?(x)', '**/*.d.?(c|m)ts'],\n targetExtensions: ['.ts', '.tsx'],\n indexFileExtension: '.ts',\n exportStatementExtension: '.js', // For ESM imports, .mts resolves to .mjs\n silent: false,\n minDepth: 0,\n} as const satisfies Required<\n StrictOmit<GenIndexConfig, 'targetDirectory' | 'formatCommand'>\n>;\n\ntype GenIndexConfigInternal = DeepReadonly<{\n formatCommand: string | undefined;\n targetDirectory: ISet<string>;\n exclude: (\n args: Readonly<{\n absolutePath: string;\n relativePath: string;\n fileName: string;\n }>,\n ) => boolean;\n targetExtensions: ISet<`.${string}`>;\n indexFileExtension: `.${string}`;\n exportStatementExtension: `.${string}` | 'none';\n silent: boolean;\n minDepth: number;\n}>;\n\n/**\n * Generates index.ts files recursively in `config.targetDirectory`.\n *\n * @param config - Configuration for index file generation\n * @throws Error if any step fails.\n */\nexport const genIndex = async (\n config: GenIndexConfig,\n): Promise<Result<undefined, unknown>> => {\n // Merge config with defaults\n const filledConfig: GenIndexConfigInternal = fillConfig(config);\n\n const conditionalEcho = filledConfig.silent ? () => {} : console.info;\n\n conditionalEcho('Starting index file generation...\\n');\n\n // Normalize target directories to array\n const targetDirs =\n typeof config.targetDirectory === 'string'\n ? ([config.targetDirectory] as const)\n : config.targetDirectory;\n\n try {\n // Step 1: Verify target directories exist\n for (const dir of targetDirs) {\n const resolvedDir = path.resolve(dir);\n\n // eslint-disable-next-line no-await-in-loop\n await assertPathExists(resolvedDir, `Target directory: ${dir}`);\n }\n\n // Step 2: Generate index files\n conditionalEcho('Generating index files...');\n\n for (const dir of targetDirs) {\n const resolvedDir = path.resolve(dir);\n\n // eslint-disable-next-line no-await-in-loop\n await generateIndexFileForDir(resolvedDir, filledConfig, undefined, 0);\n }\n\n conditionalEcho('✓ Index files generated\\n');\n\n // Step 3: Format generated files\n if (filledConfig.formatCommand !== undefined) {\n conditionalEcho('Formatting generated files...');\n\n const fmtResult = await $(filledConfig.formatCommand, {\n silent: filledConfig.silent,\n });\n\n if (Result.isErr(fmtResult)) {\n throw new Error(`Formatting failed: ${fmtResult.value.message}`);\n }\n\n conditionalEcho('✓ Formatting completed\\n');\n }\n\n conditionalEcho('✅ Index file generation completed successfully!\\n');\n\n return Result.ok(undefined);\n } catch (error) {\n conditionalEcho(`❌ Index generation failed: ${unknownToString(error)}\\n`);\n\n return Result.err(error);\n }\n};\n\nconst fillConfig = (config: GenIndexConfig): GenIndexConfigInternal => {\n const targetExtensions =\n config.targetExtensions ?? defaultConfig.targetExtensions;\n\n const exportExtension =\n config.exportStatementExtension ?? defaultConfig.exportStatementExtension;\n\n return {\n formatCommand: config.formatCommand,\n targetDirectory: ISet.create(\n isString(config.targetDirectory)\n ? [config.targetDirectory]\n : config.targetDirectory,\n ),\n exclude: pipe(config.exclude).map((exclude) =>\n typeof exclude === 'function'\n ? exclude\n : pipe(\n ISet.create<string>(\n Arr.generate(function* () {\n if (exclude !== undefined && Arr.isArray(exclude)) {\n yield* exclude;\n }\n\n yield* defaultConfig.exclude;\n }),\n ),\n ).map(\n (set) =>\n ({\n relativePath,\n fileName,\n }: Readonly<{\n absolutePath: string;\n relativePath: string;\n fileName: string;\n }>) => {\n for (const pattern of set.values()) {\n if (\n micromatch.isMatch(relativePath, pattern) ||\n micromatch.isMatch(fileName, pattern)\n ) {\n return true;\n }\n }\n\n return false;\n },\n ).value,\n ).value,\n targetExtensions: ISet.create(targetExtensions),\n indexFileExtension:\n config.indexFileExtension ?? defaultConfig.indexFileExtension,\n exportStatementExtension: exportExtension,\n silent: config.silent ?? defaultConfig.silent,\n minDepth: config.minDepth ?? defaultConfig.minDepth,\n };\n};\n\n/**\n * Generates an index.ts file for the given directory. Recursively calls itself\n * for subdirectories.\n *\n * @param dirPath - The absolute path to the directory to process.\n * @param config - The merged configuration object.\n * @param baseDir - The base directory path for calculating relative paths\n * (optional, defaults to dirPath).\n * @param currentDepth - The current depth of recursion.\n * @throws Error if directory processing fails.\n */\nconst generateIndexFileForDir = async (\n dirPath: string,\n config: GenIndexConfigInternal,\n baseDir?: string,\n currentDepth: number = 0,\n): Promise<void> => {\n const conditionalEcho = config.silent ? () => {} : console.info;\n\n try {\n const actualBaseDir = baseDir ?? dirPath;\n\n // eslint-disable-next-line security/detect-non-literal-fs-filename\n const entries = await fs.readdir(dirPath, { withFileTypes: true });\n\n const mut_subDirectories: string[] = [];\n\n const mut_filesToExport: string[] = [];\n\n for (const entry of entries) {\n const entryName = entry.name;\n\n const entryPath = path.join(dirPath, entryName);\n\n const relativePath = path.relative(actualBaseDir, entryPath);\n\n if (\n config.exclude({\n absolutePath: entryPath,\n relativePath,\n fileName: entryName,\n })\n ) {\n continue; // Skip excluded directories/files\n }\n\n if (entry.isDirectory()) {\n mut_subDirectories.push(entryName);\n\n // Recursively call for subdirectories first\n // eslint-disable-next-line no-await-in-loop\n await generateIndexFileForDir(\n entryPath,\n config,\n actualBaseDir,\n currentDepth + 1,\n );\n } else if (\n entry.isFile() &&\n shouldExportFile({\n absolutePath: entryPath,\n filePath: relativePath,\n config,\n })\n ) {\n mut_filesToExport.push(entryName);\n }\n }\n\n if (currentDepth >= config.minDepth) {\n const indexContent = generateIndexContent(\n mut_subDirectories,\n mut_filesToExport,\n config,\n );\n\n const indexPath = path.join(dirPath, `index${config.indexFileExtension}`);\n\n // eslint-disable-next-line security/detect-non-literal-fs-filename\n await fs.writeFile(indexPath, indexContent);\n\n conditionalEcho(`Generated: ${path.relative(process.cwd(), indexPath)}`);\n }\n } catch (error) {\n throw new Error(\n `Failed to generate index for directory ${dirPath}: ${unknownToString(error)}`,\n { cause: error },\n );\n }\n};\n\nconst indexRegex = /^index\\.[cm]?[jt]s[x]?$/u;\n\n/**\n * Determines if a file should be exported in the index file. A file is exported\n * if:\n *\n * - It has one of the configured source extensions\n * - It's not an index file itself\n * - It doesn't match any exclusion patterns\n *\n * @param filePath - The relative path to the file from the target directory.\n * @param absolutePath - The absolute path to the file.\n * @param config - The merged configuration object.\n * @returns True if the file should be exported.\n */\nconst shouldExportFile = ({\n absolutePath,\n filePath,\n config,\n}: Readonly<{\n absolutePath: string;\n filePath: string;\n config: GenIndexConfigInternal;\n}>): boolean => {\n const fileName = path.basename(filePath);\n\n const ext = path.extname(fileName);\n\n // Must have the correct source extension\n if (!config.targetExtensions.has(ext)) {\n return false;\n }\n\n // Don't export the index file itself\n if (\n indexRegex.test(fileName) // Matches index.ts, index.mts, index.js, index.tsx\n ) {\n return false;\n }\n\n // Check against exclusion patterns\n if (\n config.exclude({\n absolutePath,\n relativePath: filePath,\n fileName,\n })\n ) {\n return false;\n }\n\n return true;\n};\n\nif (import.meta.vitest !== undefined) {\n describe('index file regex', () => {\n test.each([\n ['index.ts', true],\n ['index.js', true],\n ['index.mts', true],\n ['index.mjs', true],\n ['index.cts', true],\n ['index.cjs', true],\n ['index.tsx', true],\n ['index.jsx', true],\n ['not-index.ts', false],\n ['index.txt', false],\n ] as const)('indexRegex.test($0) to be $1', (fileName, expected) => {\n expect(indexRegex.test(fileName)).toBe(expected);\n });\n });\n}\n\n/**\n * Generates the content for an index file.\n *\n * @param subDirectories - Array of subdirectory names.\n * @param filesToExport - Array of file names to export.\n * @param config - The merged configuration object.\n * @returns The index file content.\n */\nconst generateIndexContent = (\n subDirectories: readonly string[],\n filesToExport: readonly string[],\n config: GenIndexConfigInternal,\n): string => {\n const exportStatements = [\n ...subDirectories.map((subDir) =>\n config.exportStatementExtension === 'none'\n ? `export * from \"./${subDir}\";`\n : `export * from \"./${subDir}/index${config.exportStatementExtension}\";`,\n ),\n ...filesToExport.map((file) => {\n const fileNameWithoutExt = path.basename(file, path.extname(file));\n\n return config.exportStatementExtension === 'none'\n ? `export * from \"./${fileNameWithoutExt}\";`\n : `export * from \"./${fileNameWithoutExt}${config.exportStatementExtension}\";`;\n }),\n ] as const;\n\n return Arr.isEmpty(exportStatements)\n ? 'export {};'\n : exportStatements.join('\\n');\n};\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"gen-index.mjs","sources":["../../src/functions/gen-index.mts"],"sourcesContent":["import micromatch from 'micromatch';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport {\n Arr,\n hasKey,\n ISet,\n isString,\n pipe,\n Result,\n unknownToString,\n} from 'ts-data-forge';\nimport {\n type DeepReadonly,\n type ReadonlyRecord,\n type StrictOmit,\n} from 'ts-type-forge';\nimport { assertPathExists } from './assert-path-exists.mjs';\nimport { $ } from './exec-async.mjs';\n\n/** Configuration for index file generation. */\nexport type GenIndexConfig = DeepReadonly<{\n /** Target directories to generate index files for (string or array of strings) */\n targetDirectory: string | string[];\n\n /**\n * Glob patterns of files or predicate function to exclude from exports\n * (default: excludes `'**\\/*.{test,spec}.?(c|m)[jt]s?(x)'` and\n * `'**\\/*.d.?(c|m)ts'`)\n */\n exclude?:\n | string[]\n | ((\n args: Readonly<{\n absolutePath: string;\n relativePath: string;\n fileName: string;\n }>,\n ) => boolean);\n\n /** File extensions of source files to export (default: ['.ts', '.tsx']) */\n targetExtensions?: `.${string}`[];\n\n /** File extension of index files to generate (default: '.ts') */\n indexFileExtension?: `.${string}`;\n\n /** File extension to use in export statements (default: '.js') */\n exportStatementExtension?: `.${string}` | 'none';\n\n /** Command to run for formatting generated files (optional) */\n formatCommand?: string;\n\n /** Whether to suppress output during execution (default: false) */\n silent?: boolean;\n\n /** Minimum depth to start generating index files (default: 0) */\n minDepth?: number;\n\n /**\n * Glob patterns of index files to leave untouched, matched against the\n * index file's own path relative to the target directory (POSIX\n * separators), e.g. `'index.mts'` for the one at the root of the walk and\n * `'v*\\/index.mts'` for a whole generation of them.\n *\n * This is what protects a hand-written index file — an executable entry\n * point, or a curated list of named re-exports — from being overwritten\n * with a barrel. `exclude` cannot express it: it says what an index file\n * may not *export*, and is matched against a bare file name as well as a\n * relative path, so `'index.mts'` there would name every index file in the\n * tree rather than one of them.\n *\n * A preserved directory is still walked, so index files below it are still\n * generated, and its parent still re-exports it. (default: none)\n */\n preserve?: string[];\n}>;\n\nconst defaultConfig = {\n exclude: ['**/*.{test,spec}.?(c|m)[jt]s?(x)', '**/*.d.?(c|m)ts'],\n targetExtensions: ['.ts', '.tsx'],\n indexFileExtension: '.ts',\n exportStatementExtension: '.js', // For ESM imports, .mts resolves to .mjs\n silent: false,\n minDepth: 0,\n preserve: [],\n} as const satisfies Required<\n StrictOmit<GenIndexConfig, 'targetDirectory' | 'formatCommand'>\n>;\n\ntype GenIndexConfigInternal = DeepReadonly<{\n formatCommand: string | undefined;\n targetDirectory: ISet<string>;\n exclude: (\n args: Readonly<{\n absolutePath: string;\n relativePath: string;\n fileName: string;\n }>,\n ) => boolean;\n targetExtensions: ISet<`.${string}`>;\n indexFileExtension: `.${string}`;\n exportStatementExtension: `.${string}` | 'none';\n silent: boolean;\n minDepth: number;\n preserve: string[];\n}>;\n\n/**\n * Generates index.ts files recursively in `config.targetDirectory`.\n *\n * @param config - Configuration for index file generation\n * @throws Error if any step fails.\n */\nexport const genIndex = async (\n config: GenIndexConfig,\n): Promise<Result<undefined, unknown>> => {\n // Merge config with defaults\n const filledConfig: GenIndexConfigInternal = fillConfig(config);\n\n const conditionalEcho = filledConfig.silent ? () => {} : console.info;\n\n conditionalEcho('Starting index file generation...\\n');\n\n // Normalize target directories to array\n const targetDirs =\n typeof config.targetDirectory === 'string'\n ? ([config.targetDirectory] as const)\n : config.targetDirectory;\n\n try {\n // Step 1: Verify target directories exist\n for (const dir of targetDirs) {\n const resolvedDir = path.resolve(dir);\n\n // eslint-disable-next-line no-await-in-loop\n await assertPathExists(resolvedDir, `Target directory: ${dir}`);\n }\n\n // Step 2: Generate index files\n conditionalEcho('Generating index files...');\n\n for (const dir of targetDirs) {\n const resolvedDir = path.resolve(dir);\n\n // eslint-disable-next-line no-await-in-loop\n await generateIndexFileForDir(resolvedDir, filledConfig, undefined, 0);\n }\n\n conditionalEcho('✓ Index files generated\\n');\n\n // Step 3: Format generated files\n if (filledConfig.formatCommand !== undefined) {\n conditionalEcho('Formatting generated files...');\n\n const fmtResult = await $(filledConfig.formatCommand, {\n silent: filledConfig.silent,\n });\n\n if (Result.isErr(fmtResult)) {\n throw new Error(`Formatting failed: ${fmtResult.value.message}`);\n }\n\n conditionalEcho('✓ Formatting completed\\n');\n }\n\n conditionalEcho('✅ Index file generation completed successfully!\\n');\n\n return Result.ok(undefined);\n } catch (error) {\n conditionalEcho(`❌ Index generation failed: ${unknownToString(error)}\\n`);\n\n return Result.err(error);\n }\n};\n\nconst fillConfig = (config: GenIndexConfig): GenIndexConfigInternal => {\n const targetExtensions =\n config.targetExtensions ?? defaultConfig.targetExtensions;\n\n const exportExtension =\n config.exportStatementExtension ?? defaultConfig.exportStatementExtension;\n\n return {\n formatCommand: config.formatCommand,\n targetDirectory: ISet.create(\n isString(config.targetDirectory)\n ? [config.targetDirectory]\n : config.targetDirectory,\n ),\n exclude: pipe(config.exclude).map((exclude) =>\n typeof exclude === 'function'\n ? exclude\n : pipe(\n ISet.create<string>(\n Arr.generate(function* () {\n if (exclude !== undefined && Arr.isArray(exclude)) {\n yield* exclude;\n }\n\n yield* defaultConfig.exclude;\n }),\n ),\n ).map(\n (set) =>\n ({\n relativePath,\n fileName,\n }: Readonly<{\n absolutePath: string;\n relativePath: string;\n fileName: string;\n }>) => {\n for (const pattern of set.values()) {\n if (\n micromatch.isMatch(relativePath, pattern) ||\n micromatch.isMatch(fileName, pattern)\n ) {\n return true;\n }\n }\n\n return false;\n },\n ).value,\n ).value,\n targetExtensions: ISet.create(targetExtensions),\n indexFileExtension:\n config.indexFileExtension ?? defaultConfig.indexFileExtension,\n exportStatementExtension: exportExtension,\n silent: config.silent ?? defaultConfig.silent,\n minDepth: config.minDepth ?? defaultConfig.minDepth,\n preserve: config.preserve ?? defaultConfig.preserve,\n };\n};\n\n/**\n * Generates an index.ts file for the given directory. Recursively calls itself\n * for subdirectories.\n *\n * @param dirPath - The absolute path to the directory to process.\n * @param config - The merged configuration object.\n * @param baseDir - The base directory path for calculating relative paths\n * (optional, defaults to dirPath).\n * @param currentDepth - The current depth of recursion.\n * @throws Error if directory processing fails.\n */\nconst generateIndexFileForDir = async (\n dirPath: string,\n config: GenIndexConfigInternal,\n baseDir?: string,\n currentDepth: number = 0,\n): Promise<void> => {\n const conditionalEcho = config.silent ? () => {} : console.info;\n\n try {\n const actualBaseDir = baseDir ?? dirPath;\n\n // eslint-disable-next-line security/detect-non-literal-fs-filename\n const entries = await fs.readdir(dirPath, { withFileTypes: true });\n\n const mut_subDirectories: string[] = [];\n\n const mut_filesToExport: string[] = [];\n\n for (const entry of entries) {\n const entryName = entry.name;\n\n const entryPath = path.join(dirPath, entryName);\n\n const relativePath = path.relative(actualBaseDir, entryPath);\n\n if (\n config.exclude({\n absolutePath: entryPath,\n relativePath,\n fileName: entryName,\n })\n ) {\n continue; // Skip excluded directories/files\n }\n\n if (entry.isDirectory()) {\n mut_subDirectories.push(entryName);\n\n // Recursively call for subdirectories first\n // eslint-disable-next-line no-await-in-loop\n await generateIndexFileForDir(\n entryPath,\n config,\n actualBaseDir,\n currentDepth + 1,\n );\n } else if (\n entry.isFile() &&\n shouldExportFile({\n absolutePath: entryPath,\n filePath: relativePath,\n config,\n })\n ) {\n mut_filesToExport.push(entryName);\n }\n }\n\n if (currentDepth >= config.minDepth) {\n const indexPath = path.join(dirPath, `index${config.indexFileExtension}`);\n\n if (\n isPreservedIndex(\n path.relative(actualBaseDir, indexPath),\n config.preserve,\n )\n ) {\n conditionalEcho(\n `Preserved: ${path.relative(process.cwd(), indexPath)}`,\n );\n } else {\n const indexContent = generateIndexContent(\n mut_subDirectories,\n mut_filesToExport,\n config,\n );\n\n // eslint-disable-next-line security/detect-non-literal-fs-filename\n await fs.writeFile(indexPath, indexContent);\n\n conditionalEcho(\n `Generated: ${path.relative(process.cwd(), indexPath)}`,\n );\n }\n }\n } catch (error) {\n throw new Error(\n `Failed to generate index for directory ${dirPath}: ${unknownToString(error)}`,\n { cause: error },\n );\n }\n};\n\n/**\n * Whether the index file about to be written is one the caller asked to keep.\n *\n * Matched against the path relative to the target directory alone, never\n * against the bare file name — every index file in the tree shares that name,\n * so `'index.mts'` would otherwise mean \"generate nothing anywhere\" instead of\n * naming the one at the root of the walk.\n *\n * @param indexRelativePath - The index file's path relative to the target\n * directory, in this platform's separators.\n * @param preserve - Glob patterns, in POSIX separators.\n * @returns True if the file must be left as it is on disk.\n */\nconst isPreservedIndex = (\n indexRelativePath: string,\n preserve: readonly string[],\n): boolean => {\n const normalized = indexRelativePath.split(path.sep).join('/');\n\n return preserve.some((pattern) => micromatch.isMatch(normalized, pattern));\n};\n\nif (import.meta.vitest !== undefined) {\n describe('isPreservedIndex', () => {\n test.each([\n // Nothing is preserved by default.\n ['index.mts', [], false],\n // `'index.mts'` names the index at the root of the walk, and only it —\n // this is what `exclude` could not express.\n ['index.mts', ['index.mts'], true],\n ['utils/index.mts', ['index.mts'], false],\n ['v2/index.mts', ['index.mts'], false],\n // A whole generation of hand-written index files.\n ['v2/index.mts', ['v*/index.mts'], true],\n ['v2/index.mts', ['v[2-8]/index.mts'], true],\n ['v9/index.mts', ['v[2-8]/index.mts'], false],\n // A pattern matches at its own depth, not below it.\n ['v2/types/index.mts', ['v*/index.mts'], false],\n // Any one pattern matching is enough.\n ['index.mts', ['v*/index.mts', 'index.mts'], true],\n ] as const)(\n 'isPreservedIndex($0, $1) to be $2',\n (indexRelativePath, preserve, expected) => {\n expect(isPreservedIndex(indexRelativePath, preserve)).toBe(expected);\n },\n );\n });\n}\n\nconst indexRegex = /^index\\.[cm]?[jt]s[x]?$/u;\n\n/**\n * Determines if a file should be exported in the index file. A file is exported\n * if:\n *\n * - It has one of the configured source extensions\n * - It's not an index file itself\n * - It doesn't match any exclusion patterns\n *\n * @param filePath - The relative path to the file from the target directory.\n * @param absolutePath - The absolute path to the file.\n * @param config - The merged configuration object.\n * @returns True if the file should be exported.\n */\nconst shouldExportFile = ({\n absolutePath,\n filePath,\n config,\n}: Readonly<{\n absolutePath: string;\n filePath: string;\n config: GenIndexConfigInternal;\n}>): boolean => {\n const fileName = path.basename(filePath);\n\n const ext = path.extname(fileName);\n\n // Must have the correct source extension\n if (!config.targetExtensions.has(ext)) {\n return false;\n }\n\n // Don't export the index file itself\n if (\n indexRegex.test(fileName) // Matches index.ts, index.mts, index.js, index.tsx\n ) {\n return false;\n }\n\n // Check against exclusion patterns\n if (\n config.exclude({\n absolutePath,\n relativePath: filePath,\n fileName,\n })\n ) {\n return false;\n }\n\n return true;\n};\n\nif (import.meta.vitest !== undefined) {\n describe('index file regex', () => {\n test.each([\n ['index.ts', true],\n ['index.js', true],\n ['index.mts', true],\n ['index.mjs', true],\n ['index.cts', true],\n ['index.cjs', true],\n ['index.tsx', true],\n ['index.jsx', true],\n ['not-index.ts', false],\n ['index.txt', false],\n ] as const)('indexRegex.test($0) to be $1', (fileName, expected) => {\n expect(indexRegex.test(fileName)).toBe(expected);\n });\n });\n}\n\n/**\n * The extension a module specifier needs for a source file with this\n * extension.\n *\n * TypeScript resolves a specifier by the name the file *emits*, so the answer\n * follows the source's own extension rather than one configured value: `.mts`\n * emits `.mjs`, while `.tsx` — like `.ts` — emits `.js`. A directory holding\n * both therefore needs two different extensions, which is why\n * `exportStatementExtension` cannot decide this on its own; it is the fallback\n * for extensions with no known emit.\n */\nconst specifierExtension = (\n sourceExtension: string,\n fallback: `.${string}`,\n): `.${string}` =>\n hasKey(emittedExtensions, sourceExtension)\n ? emittedExtensions[sourceExtension]\n : fallback;\n\nconst emittedExtensions = {\n '.cjs': '.cjs',\n '.cts': '.cjs',\n '.js': '.js',\n '.jsx': '.js',\n '.mjs': '.mjs',\n '.mts': '.mjs',\n '.ts': '.js',\n '.tsx': '.js',\n} as const satisfies ReadonlyRecord<string, `.${string}`>;\n\nif (import.meta.vitest !== undefined) {\n describe('specifierExtension', () => {\n test.each([\n // A directory holding both needs two different extensions, whatever\n // `exportStatementExtension` says: this is what `--export-ext .mjs`\n // together with `--target-ext .tsx` used to get wrong.\n ['./a.mts', '.mjs' as const, '.mjs'],\n ['./a.tsx', '.mjs' as const, '.js'],\n ['./a.ts', '.mjs' as const, '.js'],\n ['./a.cts', '.mjs' as const, '.cjs'],\n // An extension with no known emit keeps the configured value.\n ['./a.vue', '.mjs' as const, '.mjs'],\n ['./a', '.js' as const, '.js'],\n ] as const)(\n 'specifierExtension($0, $1) to be $2',\n (filePath, fallback, expected) => {\n expect(specifierExtension(path.extname(filePath), fallback)).toBe(\n expected,\n );\n },\n );\n });\n}\n\n/**\n * Generates the content for an index file.\n *\n * @param subDirectories - Array of subdirectory names.\n * @param filesToExport - Array of file names to export.\n * @param config - The merged configuration object.\n * @returns The index file content.\n */\nconst generateIndexContent = (\n subDirectories: readonly string[],\n filesToExport: readonly string[],\n config: GenIndexConfigInternal,\n): string => {\n const exportStatements = [\n ...subDirectories.map((subDir) =>\n config.exportStatementExtension === 'none'\n ? `export * from \"./${subDir}\";`\n : `export * from \"./${subDir}/index${specifierExtension(\n config.indexFileExtension,\n config.exportStatementExtension,\n )}\";`,\n ),\n ...filesToExport.map((file) => {\n const fileNameWithoutExt = path.basename(file, path.extname(file));\n\n return config.exportStatementExtension === 'none'\n ? `export * from \"./${fileNameWithoutExt}\";`\n : `export * from \"./${fileNameWithoutExt}${specifierExtension(\n path.extname(file),\n config.exportStatementExtension,\n )}\";`;\n }),\n ] as const;\n\n return Arr.isEmpty(exportStatements)\n ? 'export {};'\n : exportStatements.join('\\n');\n};\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-repo-utils",
3
- "version": "10.3.2",
3
+ "version": "10.4.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "typescript"
@@ -49,33 +49,33 @@
49
49
  "cmd-ts": "^0.15.0",
50
50
  "fast-glob": "^3.3.3",
51
51
  "micromatch": "^4.0.8",
52
- "ts-data-forge": "^14.4.0",
52
+ "ts-data-forge": "^14.6.1",
53
53
  "ts-type-forge": "^9.2.2"
54
54
  },
55
55
  "devDependencies": {
56
- "@types/node": "26.3.0",
56
+ "@types/node": "26.4.0",
57
57
  "@vitest/coverage-v8": "4.1.11",
58
58
  "@vitest/ui": "4.1.11",
59
- "cspell": "10.0.1",
59
+ "cspell": "10.1.0",
60
60
  "dedent": "1.7.2",
61
61
  "esbuild": "0.28.2",
62
- "eslint": "10.8.1",
63
- "eslint-config-typed": "5.9.5",
64
- "eslint-plugin-ts-data-forge": "0.5.0",
65
- "eslint-plugin-ts-fortress": "1.0.4",
66
- "eslint-plugin-ts-type-forge": "0.7.0",
62
+ "eslint": "10.9.0",
63
+ "eslint-config-typed": "5.10.1",
64
+ "eslint-plugin-ts-data-forge": "0.5.3",
65
+ "eslint-plugin-ts-fortress": "1.0.6",
66
+ "eslint-plugin-ts-type-forge": "0.7.2",
67
67
  "jiti": "2.7.0",
68
68
  "npm-run-all2": "9.0.3",
69
69
  "prettier": "3.9.6",
70
70
  "prettier-plugin-organize-imports": "4.3.0",
71
71
  "prettier-plugin-packagejson": "3.0.2",
72
- "rollup": "4.62.4",
72
+ "rollup": "4.62.5",
73
73
  "tsx": "4.23.12",
74
74
  "typedoc": "0.28.20",
75
75
  "typedoc-github-theme": "0.4.0",
76
76
  "typescript": "6.0.3",
77
77
  "typescript-native": "npm:typescript@7.0.2",
78
- "vite": "8.2.1",
78
+ "vite": "8.2.2",
79
79
  "vitest": "4.1.11"
80
80
  },
81
81
  "peerDependencies": {
@@ -108,6 +108,12 @@ const cmdDef = cmd.command({
108
108
  type: cmd.optional(cmd.number),
109
109
  description: 'Minimum depth to start generating index files (default: 0)',
110
110
  }),
111
+ preserve: cmd.multioption({
112
+ long: 'preserve',
113
+ type: cmd.optional(cmd.array(cmd.string)),
114
+ description:
115
+ 'Glob patterns of index files to leave untouched, matched against the path relative to the target directory (e.g., "index.mts", "v*/index.mts")',
116
+ }),
111
117
  },
112
118
  handler: (args) => {
113
119
  expectType<typeof args.targetDirectory, string>('=');
@@ -126,6 +132,8 @@ const cmdDef = cmd.command({
126
132
 
127
133
  expectType<typeof args.minDepth, number | undefined>('=');
128
134
 
135
+ expectType<typeof args.preserve, string[] | undefined>('=');
136
+
129
137
  main(args).catch((error: unknown) => {
130
138
  console.error('An error occurred:', error);
131
139
 
@@ -143,6 +151,7 @@ const main = async ({
143
151
  formatCommand,
144
152
  silent,
145
153
  minDepth,
154
+ preserve,
146
155
  }: Readonly<{
147
156
  targetDirectory: string;
148
157
  targetExtensions: readonly Ext[];
@@ -152,6 +161,7 @@ const main = async ({
152
161
  formatCommand?: string | undefined;
153
162
  silent?: boolean | undefined;
154
163
  minDepth?: number | undefined;
164
+ preserve?: readonly string[] | undefined;
155
165
  }>): Promise<void> => {
156
166
  await genIndex({
157
167
  targetDirectory: targetDirectory.includes(',')
@@ -164,6 +174,7 @@ const main = async ({
164
174
  formatCommand,
165
175
  silent,
166
176
  minDepth,
177
+ preserve,
167
178
  });
168
179
  };
169
180
 
@@ -3,13 +3,18 @@ import * as fs from 'node:fs/promises';
3
3
  import * as path from 'node:path';
4
4
  import {
5
5
  Arr,
6
+ hasKey,
6
7
  ISet,
7
8
  isString,
8
9
  pipe,
9
10
  Result,
10
11
  unknownToString,
11
12
  } from 'ts-data-forge';
12
- import { type DeepReadonly, type StrictOmit } from 'ts-type-forge';
13
+ import {
14
+ type DeepReadonly,
15
+ type ReadonlyRecord,
16
+ type StrictOmit,
17
+ } from 'ts-type-forge';
13
18
  import { assertPathExists } from './assert-path-exists.mjs';
14
19
  import { $ } from './exec-async.mjs';
15
20
 
@@ -50,6 +55,24 @@ export type GenIndexConfig = DeepReadonly<{
50
55
 
51
56
  /** Minimum depth to start generating index files (default: 0) */
52
57
  minDepth?: number;
58
+
59
+ /**
60
+ * Glob patterns of index files to leave untouched, matched against the
61
+ * index file's own path relative to the target directory (POSIX
62
+ * separators), e.g. `'index.mts'` for the one at the root of the walk and
63
+ * `'v*\/index.mts'` for a whole generation of them.
64
+ *
65
+ * This is what protects a hand-written index file — an executable entry
66
+ * point, or a curated list of named re-exports — from being overwritten
67
+ * with a barrel. `exclude` cannot express it: it says what an index file
68
+ * may not *export*, and is matched against a bare file name as well as a
69
+ * relative path, so `'index.mts'` there would name every index file in the
70
+ * tree rather than one of them.
71
+ *
72
+ * A preserved directory is still walked, so index files below it are still
73
+ * generated, and its parent still re-exports it. (default: none)
74
+ */
75
+ preserve?: string[];
53
76
  }>;
54
77
 
55
78
  const defaultConfig = {
@@ -59,6 +82,7 @@ const defaultConfig = {
59
82
  exportStatementExtension: '.js', // For ESM imports, .mts resolves to .mjs
60
83
  silent: false,
61
84
  minDepth: 0,
85
+ preserve: [],
62
86
  } as const satisfies Required<
63
87
  StrictOmit<GenIndexConfig, 'targetDirectory' | 'formatCommand'>
64
88
  >;
@@ -78,6 +102,7 @@ type GenIndexConfigInternal = DeepReadonly<{
78
102
  exportStatementExtension: `.${string}` | 'none';
79
103
  silent: boolean;
80
104
  minDepth: number;
105
+ preserve: string[];
81
106
  }>;
82
107
 
83
108
  /**
@@ -204,6 +229,7 @@ const fillConfig = (config: GenIndexConfig): GenIndexConfigInternal => {
204
229
  exportStatementExtension: exportExtension,
205
230
  silent: config.silent ?? defaultConfig.silent,
206
231
  minDepth: config.minDepth ?? defaultConfig.minDepth,
232
+ preserve: config.preserve ?? defaultConfig.preserve,
207
233
  };
208
234
  };
209
235
 
@@ -277,18 +303,31 @@ const generateIndexFileForDir = async (
277
303
  }
278
304
 
279
305
  if (currentDepth >= config.minDepth) {
280
- const indexContent = generateIndexContent(
281
- mut_subDirectories,
282
- mut_filesToExport,
283
- config,
284
- );
285
-
286
306
  const indexPath = path.join(dirPath, `index${config.indexFileExtension}`);
287
307
 
288
- // eslint-disable-next-line security/detect-non-literal-fs-filename
289
- await fs.writeFile(indexPath, indexContent);
308
+ if (
309
+ isPreservedIndex(
310
+ path.relative(actualBaseDir, indexPath),
311
+ config.preserve,
312
+ )
313
+ ) {
314
+ conditionalEcho(
315
+ `Preserved: ${path.relative(process.cwd(), indexPath)}`,
316
+ );
317
+ } else {
318
+ const indexContent = generateIndexContent(
319
+ mut_subDirectories,
320
+ mut_filesToExport,
321
+ config,
322
+ );
290
323
 
291
- conditionalEcho(`Generated: ${path.relative(process.cwd(), indexPath)}`);
324
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
325
+ await fs.writeFile(indexPath, indexContent);
326
+
327
+ conditionalEcho(
328
+ `Generated: ${path.relative(process.cwd(), indexPath)}`,
329
+ );
330
+ }
292
331
  }
293
332
  } catch (error) {
294
333
  throw new Error(
@@ -298,6 +337,55 @@ const generateIndexFileForDir = async (
298
337
  }
299
338
  };
300
339
 
340
+ /**
341
+ * Whether the index file about to be written is one the caller asked to keep.
342
+ *
343
+ * Matched against the path relative to the target directory alone, never
344
+ * against the bare file name — every index file in the tree shares that name,
345
+ * so `'index.mts'` would otherwise mean "generate nothing anywhere" instead of
346
+ * naming the one at the root of the walk.
347
+ *
348
+ * @param indexRelativePath - The index file's path relative to the target
349
+ * directory, in this platform's separators.
350
+ * @param preserve - Glob patterns, in POSIX separators.
351
+ * @returns True if the file must be left as it is on disk.
352
+ */
353
+ const isPreservedIndex = (
354
+ indexRelativePath: string,
355
+ preserve: readonly string[],
356
+ ): boolean => {
357
+ const normalized = indexRelativePath.split(path.sep).join('/');
358
+
359
+ return preserve.some((pattern) => micromatch.isMatch(normalized, pattern));
360
+ };
361
+
362
+ if (import.meta.vitest !== undefined) {
363
+ describe('isPreservedIndex', () => {
364
+ test.each([
365
+ // Nothing is preserved by default.
366
+ ['index.mts', [], false],
367
+ // `'index.mts'` names the index at the root of the walk, and only it —
368
+ // this is what `exclude` could not express.
369
+ ['index.mts', ['index.mts'], true],
370
+ ['utils/index.mts', ['index.mts'], false],
371
+ ['v2/index.mts', ['index.mts'], false],
372
+ // A whole generation of hand-written index files.
373
+ ['v2/index.mts', ['v*/index.mts'], true],
374
+ ['v2/index.mts', ['v[2-8]/index.mts'], true],
375
+ ['v9/index.mts', ['v[2-8]/index.mts'], false],
376
+ // A pattern matches at its own depth, not below it.
377
+ ['v2/types/index.mts', ['v*/index.mts'], false],
378
+ // Any one pattern matching is enough.
379
+ ['index.mts', ['v*/index.mts', 'index.mts'], true],
380
+ ] as const)(
381
+ 'isPreservedIndex($0, $1) to be $2',
382
+ (indexRelativePath, preserve, expected) => {
383
+ expect(isPreservedIndex(indexRelativePath, preserve)).toBe(expected);
384
+ },
385
+ );
386
+ });
387
+ }
388
+
301
389
  const indexRegex = /^index\.[cm]?[jt]s[x]?$/u;
302
390
 
303
391
  /**
@@ -371,6 +459,60 @@ if (import.meta.vitest !== undefined) {
371
459
  });
372
460
  }
373
461
 
462
+ /**
463
+ * The extension a module specifier needs for a source file with this
464
+ * extension.
465
+ *
466
+ * TypeScript resolves a specifier by the name the file *emits*, so the answer
467
+ * follows the source's own extension rather than one configured value: `.mts`
468
+ * emits `.mjs`, while `.tsx` — like `.ts` — emits `.js`. A directory holding
469
+ * both therefore needs two different extensions, which is why
470
+ * `exportStatementExtension` cannot decide this on its own; it is the fallback
471
+ * for extensions with no known emit.
472
+ */
473
+ const specifierExtension = (
474
+ sourceExtension: string,
475
+ fallback: `.${string}`,
476
+ ): `.${string}` =>
477
+ hasKey(emittedExtensions, sourceExtension)
478
+ ? emittedExtensions[sourceExtension]
479
+ : fallback;
480
+
481
+ const emittedExtensions = {
482
+ '.cjs': '.cjs',
483
+ '.cts': '.cjs',
484
+ '.js': '.js',
485
+ '.jsx': '.js',
486
+ '.mjs': '.mjs',
487
+ '.mts': '.mjs',
488
+ '.ts': '.js',
489
+ '.tsx': '.js',
490
+ } as const satisfies ReadonlyRecord<string, `.${string}`>;
491
+
492
+ if (import.meta.vitest !== undefined) {
493
+ describe('specifierExtension', () => {
494
+ test.each([
495
+ // A directory holding both needs two different extensions, whatever
496
+ // `exportStatementExtension` says: this is what `--export-ext .mjs`
497
+ // together with `--target-ext .tsx` used to get wrong.
498
+ ['./a.mts', '.mjs' as const, '.mjs'],
499
+ ['./a.tsx', '.mjs' as const, '.js'],
500
+ ['./a.ts', '.mjs' as const, '.js'],
501
+ ['./a.cts', '.mjs' as const, '.cjs'],
502
+ // An extension with no known emit keeps the configured value.
503
+ ['./a.vue', '.mjs' as const, '.mjs'],
504
+ ['./a', '.js' as const, '.js'],
505
+ ] as const)(
506
+ 'specifierExtension($0, $1) to be $2',
507
+ (filePath, fallback, expected) => {
508
+ expect(specifierExtension(path.extname(filePath), fallback)).toBe(
509
+ expected,
510
+ );
511
+ },
512
+ );
513
+ });
514
+ }
515
+
374
516
  /**
375
517
  * Generates the content for an index file.
376
518
  *
@@ -388,14 +530,20 @@ const generateIndexContent = (
388
530
  ...subDirectories.map((subDir) =>
389
531
  config.exportStatementExtension === 'none'
390
532
  ? `export * from "./${subDir}";`
391
- : `export * from "./${subDir}/index${config.exportStatementExtension}";`,
533
+ : `export * from "./${subDir}/index${specifierExtension(
534
+ config.indexFileExtension,
535
+ config.exportStatementExtension,
536
+ )}";`,
392
537
  ),
393
538
  ...filesToExport.map((file) => {
394
539
  const fileNameWithoutExt = path.basename(file, path.extname(file));
395
540
 
396
541
  return config.exportStatementExtension === 'none'
397
542
  ? `export * from "./${fileNameWithoutExt}";`
398
- : `export * from "./${fileNameWithoutExt}${config.exportStatementExtension}";`;
543
+ : `export * from "./${fileNameWithoutExt}${specifierExtension(
544
+ path.extname(file),
545
+ config.exportStatementExtension,
546
+ )}";`;
399
547
  }),
400
548
  ] as const;
401
549