ts-repo-utils 10.3.3 → 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 +16 -0
- package/dist/cmd/gen-index-ts.mjs +9 -2
- package/dist/cmd/gen-index-ts.mjs.map +1 -1
- package/dist/functions/gen-index.d.mts +17 -0
- package/dist/functions/gen-index.d.mts.map +1 -1
- package/dist/functions/gen-index.mjs +26 -9
- package/dist/functions/gen-index.mjs.map +1 -1
- package/package.json +8 -8
- package/src/cmd/gen-index-ts.mts +11 -0
- package/src/functions/gen-index.mts +92 -9
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,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;
|
|
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"}
|
|
@@ -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
|
-
|
|
134
|
-
|
|
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,
|
|
@@ -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 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\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 * 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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"}
|
|
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
|
+
"version": "10.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript"
|
|
@@ -49,21 +49,21 @@
|
|
|
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.
|
|
52
|
+
"ts-data-forge": "^14.6.1",
|
|
53
53
|
"ts-type-forge": "^9.2.2"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
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
|
|
59
|
+
"cspell": "10.1.0",
|
|
60
60
|
"dedent": "1.7.2",
|
|
61
61
|
"esbuild": "0.28.2",
|
|
62
|
-
"eslint": "10.
|
|
63
|
-
"eslint-config-typed": "5.10.
|
|
64
|
-
"eslint-plugin-ts-data-forge": "0.5.
|
|
65
|
-
"eslint-plugin-ts-fortress": "1.0.
|
|
66
|
-
"eslint-plugin-ts-type-forge": "0.7.
|
|
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",
|
package/src/cmd/gen-index-ts.mts
CHANGED
|
@@ -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
|
|
|
@@ -55,6 +55,24 @@ export type GenIndexConfig = DeepReadonly<{
|
|
|
55
55
|
|
|
56
56
|
/** Minimum depth to start generating index files (default: 0) */
|
|
57
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[];
|
|
58
76
|
}>;
|
|
59
77
|
|
|
60
78
|
const defaultConfig = {
|
|
@@ -64,6 +82,7 @@ const defaultConfig = {
|
|
|
64
82
|
exportStatementExtension: '.js', // For ESM imports, .mts resolves to .mjs
|
|
65
83
|
silent: false,
|
|
66
84
|
minDepth: 0,
|
|
85
|
+
preserve: [],
|
|
67
86
|
} as const satisfies Required<
|
|
68
87
|
StrictOmit<GenIndexConfig, 'targetDirectory' | 'formatCommand'>
|
|
69
88
|
>;
|
|
@@ -83,6 +102,7 @@ type GenIndexConfigInternal = DeepReadonly<{
|
|
|
83
102
|
exportStatementExtension: `.${string}` | 'none';
|
|
84
103
|
silent: boolean;
|
|
85
104
|
minDepth: number;
|
|
105
|
+
preserve: string[];
|
|
86
106
|
}>;
|
|
87
107
|
|
|
88
108
|
/**
|
|
@@ -209,6 +229,7 @@ const fillConfig = (config: GenIndexConfig): GenIndexConfigInternal => {
|
|
|
209
229
|
exportStatementExtension: exportExtension,
|
|
210
230
|
silent: config.silent ?? defaultConfig.silent,
|
|
211
231
|
minDepth: config.minDepth ?? defaultConfig.minDepth,
|
|
232
|
+
preserve: config.preserve ?? defaultConfig.preserve,
|
|
212
233
|
};
|
|
213
234
|
};
|
|
214
235
|
|
|
@@ -282,18 +303,31 @@ const generateIndexFileForDir = async (
|
|
|
282
303
|
}
|
|
283
304
|
|
|
284
305
|
if (currentDepth >= config.minDepth) {
|
|
285
|
-
const indexContent = generateIndexContent(
|
|
286
|
-
mut_subDirectories,
|
|
287
|
-
mut_filesToExport,
|
|
288
|
-
config,
|
|
289
|
-
);
|
|
290
|
-
|
|
291
306
|
const indexPath = path.join(dirPath, `index${config.indexFileExtension}`);
|
|
292
307
|
|
|
293
|
-
|
|
294
|
-
|
|
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
|
+
);
|
|
323
|
+
|
|
324
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
325
|
+
await fs.writeFile(indexPath, indexContent);
|
|
295
326
|
|
|
296
|
-
|
|
327
|
+
conditionalEcho(
|
|
328
|
+
`Generated: ${path.relative(process.cwd(), indexPath)}`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
297
331
|
}
|
|
298
332
|
} catch (error) {
|
|
299
333
|
throw new Error(
|
|
@@ -303,6 +337,55 @@ const generateIndexFileForDir = async (
|
|
|
303
337
|
}
|
|
304
338
|
};
|
|
305
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
|
+
|
|
306
389
|
const indexRegex = /^index\.[cm]?[jt]s[x]?$/u;
|
|
307
390
|
|
|
308
391
|
/**
|