@ankhorage/paradox 0.1.3 → 0.1.5

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.
@@ -10,6 +10,7 @@ export function getExportMetadata(options) {
10
10
  const sourceLocation = getSourceLocation(options.node, options.root);
11
11
  const signatures = getSignatures(options.symbol, options.node);
12
12
  const members = getMembers(options.node);
13
+ const structuredRows = getStructuredRows(options.node, options.name);
13
14
  const relatedSymbols = collectRelatedSymbols(options.name, signatures.flatMap((signature) => [
14
15
  ...signature.parameters.map((parameter) => parameter.type),
15
16
  signature.returnType,
@@ -21,8 +22,12 @@ export function getExportMetadata(options) {
21
22
  relatedSymbols,
22
23
  signatures,
23
24
  members,
25
+ structuredRows,
24
26
  };
25
27
  }
28
+ /***
29
+ * Resolves the source location for a declaration relative to the package root.
30
+ */
26
31
  function getSourceLocation(node, root) {
27
32
  const sourceFile = node.getSourceFile();
28
33
  const { column, line } = sourceFile.getLineAndColumnAtPos(node.getStart(false));
@@ -32,6 +37,9 @@ function getSourceLocation(node, root) {
32
37
  column,
33
38
  };
34
39
  }
40
+ /***
41
+ * Extracts callable signatures for exported functions and callable values.
42
+ */
35
43
  function getSignatures(symbol, node) {
36
44
  const parsed = readParadoxMetadata(node);
37
45
  const signatures = getCallableDeclarations(symbol, node).map((declaration) => getSignature(declaration, parsed.params, parsed.returns));
@@ -39,6 +47,9 @@ function getSignatures(symbol, node) {
39
47
  signature.returnType !== null ||
40
48
  signature.returnDescription !== null), (signature) => signature.label);
41
49
  }
50
+ /***
51
+ * Builds one normalized call signature from a callable declaration.
52
+ */
42
53
  function getSignature(declaration, params, returns) {
43
54
  const normalizedParameters = declaration.getParameters().map((parameter) => {
44
55
  const parameterDescription = params[parameter.getName()];
@@ -60,6 +71,9 @@ function getSignature(declaration, params, returns) {
60
71
  returnDescription: returns,
61
72
  };
62
73
  }
74
+ /***
75
+ * Extracts members for interface and type literal exports.
76
+ */
63
77
  function getMembers(node) {
64
78
  if (getCallableNode(node) !== null)
65
79
  return [];
@@ -74,6 +88,9 @@ function getMembers(node) {
74
88
  }
75
89
  return [];
76
90
  }
91
+ /***
92
+ * Converts TypeScript properties into documented member metadata.
93
+ */
77
94
  function getMembersFromProperties(properties) {
78
95
  return properties.flatMap((property) => {
79
96
  const declaration = getFirstDeclaration(property.getDeclarations());
@@ -83,7 +100,14 @@ function getMembersFromProperties(properties) {
83
100
  const rawComment = getParadoxComment(declaration);
84
101
  const parsed = rawComment
85
102
  ? parseParadoxComment(rawComment)
86
- : { description: null, isConfig: false, params: {}, returns: null };
103
+ : {
104
+ description: null,
105
+ isConfig: false,
106
+ isReadme: false,
107
+ examples: [],
108
+ params: {},
109
+ returns: null,
110
+ };
87
111
  return [
88
112
  {
89
113
  name: property.getName(),
@@ -95,10 +119,102 @@ function getMembersFromProperties(properties) {
95
119
  ];
96
120
  });
97
121
  }
122
+ /***
123
+ * Extracts table-like rows from exported const arrays of object literals.
124
+ */
125
+ function getStructuredRows(node, exportName) {
126
+ const declaration = getVariableDeclaration(node, exportName);
127
+ if (declaration === null)
128
+ return [];
129
+ const initializer = getStructuredArrayLiteral(declaration.getInitializer());
130
+ if (initializer === null)
131
+ return [];
132
+ return initializer.getElements().flatMap((element) => {
133
+ if (!Node.isObjectLiteralExpression(element))
134
+ return [];
135
+ const values = getObjectLiteralValues(element);
136
+ return Object.keys(values).length > 0 ? [{ values }] : [];
137
+ });
138
+ }
139
+ /***
140
+ * Resolves const assertions and returns the array literal used for structured docs.
141
+ */
142
+ function getStructuredArrayLiteral(node) {
143
+ if (node === undefined)
144
+ return null;
145
+ if (Node.isArrayLiteralExpression(node))
146
+ return node;
147
+ if (Node.isAsExpression(node))
148
+ return getStructuredArrayLiteral(node.getExpression());
149
+ return null;
150
+ }
151
+ /***
152
+ * Resolves a variable declaration from declaration nodes used by export symbols.
153
+ */
154
+ function getVariableDeclaration(node, exportName) {
155
+ if (Node.isVariableDeclaration(node))
156
+ return node;
157
+ if (Node.isVariableStatement(node)) {
158
+ return (node
159
+ .getDeclarationList()
160
+ .getDeclarations()
161
+ .find((declaration) => declaration.getName() === exportName) ?? null);
162
+ }
163
+ if (Node.isVariableDeclarationList(node)) {
164
+ return (node.getDeclarations().find((declaration) => declaration.getName() === exportName) ?? null);
165
+ }
166
+ return null;
167
+ }
168
+ /***
169
+ * Extracts primitive and string-array values from one object literal.
170
+ */
171
+ function getObjectLiteralValues(node) {
172
+ const values = {};
173
+ for (const property of node.getProperties()) {
174
+ if (!Node.isPropertyAssignment(property))
175
+ continue;
176
+ const name = property.getName().replace(/^['"]|['"]$/g, '');
177
+ const value = getLiteralValue(property.getInitializer());
178
+ if (value !== null) {
179
+ values[name] = value;
180
+ }
181
+ }
182
+ return values;
183
+ }
184
+ /***
185
+ * Converts supported literal expressions into displayable string values.
186
+ */
187
+ function getLiteralValue(node) {
188
+ if (node === undefined)
189
+ return null;
190
+ if (Node.isStringLiteral(node))
191
+ return node.getLiteralText();
192
+ if (Node.isNoSubstitutionTemplateLiteral(node))
193
+ return node.getLiteralText();
194
+ if (node.getKindName() === 'TrueKeyword')
195
+ return 'true';
196
+ if (node.getKindName() === 'FalseKeyword')
197
+ return 'false';
198
+ if (Node.isNumericLiteral(node))
199
+ return node.getText();
200
+ if (Node.isArrayLiteralExpression(node)) {
201
+ const values = node.getElements().map((element) => getLiteralValue(element));
202
+ if (values.some((value) => value === null))
203
+ return null;
204
+ return values.join(', ');
205
+ }
206
+ return null;
207
+ }
208
+ /***
209
+ * Returns the first declaration for a symbol, or null when none exists.
210
+ */
98
211
  function getFirstDeclaration(declarations) {
99
212
  const [declaration = null] = declarations;
100
213
  return declaration;
101
214
  }
215
+ /***
216
+ * Finds callable declarations associated with an export symbol.
217
+ */
102
218
  function getCallableDeclarations(symbol, node) {
103
219
  const declarations = symbol
104
220
  .getDeclarations()
@@ -110,6 +226,9 @@ function getCallableDeclarations(symbol, node) {
110
226
  const callableNode = getCallableNode(node);
111
227
  return callableNode !== null ? [callableNode] : [];
112
228
  }
229
+ /***
230
+ * Returns the callable node represented by a declaration when one exists.
231
+ */
113
232
  function getCallableNode(node) {
114
233
  if (Node.isFunctionDeclaration(node))
115
234
  return node;
@@ -130,15 +249,31 @@ function getCallableNode(node) {
130
249
  }
131
250
  return null;
132
251
  }
252
+ /***
253
+ * Checks whether a node is a method declaration or method signature.
254
+ */
133
255
  function isMemberMethodDeclaration(node) {
134
256
  return Node.isMethodDeclaration(node) || Node.isMethodSignature(node);
135
257
  }
258
+ /***
259
+ * Reads Paradox comment metadata from a declaration.
260
+ */
136
261
  function readParadoxMetadata(node) {
137
262
  const rawComment = getParadoxComment(node);
138
263
  return rawComment
139
264
  ? parseParadoxComment(rawComment)
140
- : { description: null, isConfig: false, params: {}, returns: null };
265
+ : {
266
+ description: null,
267
+ isConfig: false,
268
+ isReadme: false,
269
+ examples: [],
270
+ params: {},
271
+ returns: null,
272
+ };
141
273
  }
274
+ /***
275
+ * Finds related exported symbols mentioned in signature and member type text.
276
+ */
142
277
  function collectRelatedSymbols(exportName, ...values) {
143
278
  const candidates = values.flatMap((entries) => entries).filter((entry) => entry !== null);
144
279
  const related = new Set();
@@ -152,9 +287,15 @@ function collectRelatedSymbols(exportName, ...values) {
152
287
  }
153
288
  return [...related].sort((left, right) => left.localeCompare(right));
154
289
  }
290
+ /***
291
+ * Normalizes platform-specific path separators for generated documentation output.
292
+ */
155
293
  function toPosixPath(path) {
156
294
  return path.replaceAll('\\', '/');
157
295
  }
296
+ /***
297
+ * Returns unique items by a caller-provided key while preserving first occurrence order.
298
+ */
158
299
  function uniqueBy(items, key) {
159
300
  const seen = new Set();
160
301
  return items.filter((item) => {
@@ -1,5 +1,5 @@
1
- import type { Node } from 'ts-morph';
1
+ import { type Node as MorphNode } from 'ts-morph';
2
2
  /***
3
3
  * Reads the nearest Paradox doc comment attached to a declaration.
4
4
  */
5
- export declare function getParadoxComment(node: Node | undefined): string | null;
5
+ export declare function getParadoxComment(node: MorphNode | undefined): string | null;
@@ -1,3 +1,4 @@
1
+ import { Node } from 'ts-morph';
1
2
  /***
2
3
  * Reads the nearest Paradox doc comment attached to a declaration.
3
4
  */
@@ -6,7 +7,31 @@ export function getParadoxComment(node) {
6
7
  return null;
7
8
  const sourceFile = node.getSourceFile();
8
9
  const text = sourceFile.getFullText();
9
- const nodeStart = node.getStart(false);
10
+ for (const nodeStart of getCommentTargetStarts(node)) {
11
+ const comment = readCommentBefore(text, nodeStart);
12
+ if (comment !== null)
13
+ return comment;
14
+ }
15
+ return null;
16
+ }
17
+ /***
18
+ * Returns declaration positions that may own a leading Paradox comment.
19
+ */
20
+ function getCommentTargetStarts(node) {
21
+ const starts = [node.getStart(false)];
22
+ if (Node.isVariableDeclaration(node)) {
23
+ const parent = node.getParent();
24
+ const statement = Node.isVariableDeclarationList(parent) ? parent.getParent() : undefined;
25
+ if (statement !== undefined && Node.isVariableStatement(statement)) {
26
+ starts.unshift(statement.getStart(false));
27
+ }
28
+ }
29
+ return starts;
30
+ }
31
+ /***
32
+ * Reads a Paradox doc comment directly before a target position.
33
+ */
34
+ function readCommentBefore(text, nodeStart) {
10
35
  const beforeNode = text.slice(0, nodeStart);
11
36
  const commentStart = beforeNode.lastIndexOf('/***');
12
37
  if (commentStart === -1)
@@ -0,0 +1,39 @@
1
+ /***
2
+ * Supported Paradox documentation tags.
3
+ *
4
+ * Paradox supports doc tags inside triple-star documentation comments.
5
+ *
6
+ * @readme
7
+ */
8
+ export declare const PARADOX_DOC_TAGS: readonly [{
9
+ readonly name: "readme";
10
+ readonly syntax: "@readme";
11
+ readonly description: "Includes a documentation block or exported symbol in README output.";
12
+ readonly appliesTo: readonly ["block", "symbol"];
13
+ readonly repeatable: false;
14
+ readonly handler: "markReadme";
15
+ }, {
16
+ readonly name: "config";
17
+ readonly syntax: "@config";
18
+ readonly description: "Marks a type or interface as part of the Paradox configuration model. @config alone does not imply README inclusion; use @config plus @readme for README output.";
19
+ readonly appliesTo: readonly ["interface", "type"];
20
+ readonly repeatable: false;
21
+ readonly handler: "markConfig";
22
+ }, {
23
+ readonly name: "example";
24
+ readonly syntax: "@example";
25
+ readonly description: "Adds a titled fenced code example to the generated documentation for a symbol.";
26
+ readonly appliesTo: readonly ["symbol"];
27
+ readonly repeatable: true;
28
+ readonly handler: "parseExample";
29
+ }];
30
+ export type ParadoxDocTagName = (typeof PARADOX_DOC_TAGS)[number]['name'];
31
+ export type ParadoxDocTagHandlerId = (typeof PARADOX_DOC_TAGS)[number]['handler'];
32
+ /***
33
+ * Looks up documentation tag metadata by tag name.
34
+ */
35
+ export declare function getParadoxDocTag(name: string): (typeof PARADOX_DOC_TAGS)[number] | null;
36
+ /***
37
+ * Checks whether a string is a supported Paradox documentation tag name.
38
+ */
39
+ export declare function isParadoxDocTagName(name: string): name is ParadoxDocTagName;
@@ -0,0 +1,45 @@
1
+ /***
2
+ * Supported Paradox documentation tags.
3
+ *
4
+ * Paradox supports doc tags inside triple-star documentation comments.
5
+ *
6
+ * @readme
7
+ */
8
+ export const PARADOX_DOC_TAGS = [
9
+ {
10
+ name: 'readme',
11
+ syntax: '@readme',
12
+ description: 'Includes a documentation block or exported symbol in README output.',
13
+ appliesTo: ['block', 'symbol'],
14
+ repeatable: false,
15
+ handler: 'markReadme',
16
+ },
17
+ {
18
+ name: 'config',
19
+ syntax: '@config',
20
+ description: 'Marks a type or interface as part of the Paradox configuration model. @config alone does not imply README inclusion; use @config plus @readme for README output.',
21
+ appliesTo: ['interface', 'type'],
22
+ repeatable: false,
23
+ handler: 'markConfig',
24
+ },
25
+ {
26
+ name: 'example',
27
+ syntax: '@example',
28
+ description: 'Adds a titled fenced code example to the generated documentation for a symbol.',
29
+ appliesTo: ['symbol'],
30
+ repeatable: true,
31
+ handler: 'parseExample',
32
+ },
33
+ ];
34
+ /***
35
+ * Looks up documentation tag metadata by tag name.
36
+ */
37
+ export function getParadoxDocTag(name) {
38
+ return PARADOX_DOC_TAGS.find((tag) => tag.name === name) ?? null;
39
+ }
40
+ /***
41
+ * Checks whether a string is a supported Paradox documentation tag name.
42
+ */
43
+ export function isParadoxDocTagName(name) {
44
+ return getParadoxDocTag(name) !== null;
45
+ }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export { defineParadoxConfig } from './config/defineParadoxConfig.js';
2
2
  export type { ParadoxConfig } from './config/types.js';
3
+ export type { ParadoxDocTagHandlerId, ParadoxDocTagName } from './doc-tags/registry.js';
4
+ export { getParadoxDocTag, isParadoxDocTagName, PARADOX_DOC_TAGS } from './doc-tags/registry.js';
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export { defineParadoxConfig } from './config/defineParadoxConfig.js';
2
+ export { getParadoxDocTag, isParadoxDocTagName, PARADOX_DOC_TAGS } from './doc-tags/registry.js';
@@ -59,6 +59,9 @@ interface BuildModelInput {
59
59
  returnDescription: string | null;
60
60
  }[];
61
61
  members: ExportMemberInput[];
62
+ structuredRows: {
63
+ values: Record<string, string>;
64
+ }[];
62
65
  }[];
63
66
  components: {
64
67
  name: string;
@@ -80,6 +83,15 @@ interface BuildModelInput {
80
83
  description: string | null;
81
84
  }[];
82
85
  }[];
86
+ sourceFunctions: {
87
+ name: string;
88
+ description: string | null;
89
+ sourceLocation: {
90
+ filePath: string;
91
+ line: number;
92
+ column: number;
93
+ };
94
+ }[];
83
95
  sequenceScenarios: {
84
96
  kind: 'bin' | 'export';
85
97
  name: string;
@@ -46,6 +46,15 @@ export function buildModel(analysis) {
46
46
  .sort((left, right) => left.path.localeCompare(right.path)),
47
47
  exports,
48
48
  components: sortByName(analysis.components.map((component) => mapComponent(component, exportsByName.get(component.name)))),
49
+ sourceFunctions: analysis.sourceFunctions.map((sourceFunction) => ({
50
+ name: sourceFunction.name,
51
+ description: sourceFunction.description,
52
+ sourceLocation: {
53
+ filePath: sourceFunction.sourceLocation.filePath,
54
+ line: sourceFunction.sourceLocation.line,
55
+ column: sourceFunction.sourceLocation.column,
56
+ },
57
+ })),
49
58
  sequenceScenarios: sortByName(analysis.sequenceScenarios.map((scenario) => ({
50
59
  kind: scenario.kind,
51
60
  name: scenario.name,
@@ -62,6 +71,9 @@ export function buildModel(analysis) {
62
71
  },
63
72
  };
64
73
  }
74
+ /***
75
+ * Maps one analyzed export into the serializable documentation shape.
76
+ */
65
77
  function mapExport(item, exportNames) {
66
78
  return {
67
79
  name: item.name,
@@ -100,8 +112,12 @@ function mapExport(item, exportNames) {
100
112
  inheritedFrom: member.inheritedFrom,
101
113
  children: member.children,
102
114
  }))),
115
+ structuredRows: item.structuredRows.map((row) => ({ values: { ...row.values } })),
103
116
  };
104
117
  }
118
+ /***
119
+ * Maps an analyzed component while preserving export metadata when available.
120
+ */
105
121
  function mapComponent(component, exportModel) {
106
122
  return {
107
123
  name: component.name,
@@ -124,6 +140,9 @@ function mapComponent(component, exportModel) {
124
140
  }))),
125
141
  };
126
142
  }
143
+ /***
144
+ * Finds the conventional config factory export for a config type when present.
145
+ */
127
146
  function findConfigFactoryName(configExportName, exportNames) {
128
147
  const prefix = configExportName.endsWith('Config')
129
148
  ? configExportName.slice(0, -'Config'.length)
@@ -131,10 +150,16 @@ function findConfigFactoryName(configExportName, exportNames) {
131
150
  const expectedFactoryName = `define${prefix}Config`;
132
151
  return exportNames.includes(expectedFactoryName) ? expectedFactoryName : null;
133
152
  }
153
+ /***
154
+ * Derives the default config file name from a package id.
155
+ */
134
156
  function getDefaultConfigFileName(packageId) {
135
157
  const packageBaseName = packageId.split('/').pop() ?? packageId;
136
158
  return `${packageBaseName}.config.ts`;
137
159
  }
160
+ /***
161
+ * Returns a copy of items sorted by their `name` property.
162
+ */
138
163
  function sortByName(items) {
139
164
  return [...items].sort((a, b) => a.name.localeCompare(b.name));
140
165
  }
@@ -12,6 +12,7 @@ export interface DocumentationModel {
12
12
  modules: ModuleModel[];
13
13
  exports: ExportModel[];
14
14
  components: ComponentModel[];
15
+ sourceFunctions: SourceFunctionModel[];
15
16
  sequenceScenarios: SequenceScenarioModel[];
16
17
  graphs: GraphModel;
17
18
  }
@@ -48,8 +49,9 @@ export interface ExportModel {
48
49
  relatedSymbols: string[];
49
50
  signatures: SignatureModel[];
50
51
  members: MemberModel[];
52
+ structuredRows: StructuredRowModel[];
51
53
  }
52
- export type ExportKind = 'function' | 'type' | 'unknown';
54
+ export type ExportKind = 'function' | 'type' | 'value' | 'unknown';
53
55
  export interface ComponentModel {
54
56
  name: string;
55
57
  description: string | null;
@@ -60,6 +62,11 @@ export interface ComponentModel {
60
62
  exportPaths: string[];
61
63
  props: PropModel[];
62
64
  }
65
+ interface SourceFunctionModel {
66
+ name: string;
67
+ description: string | null;
68
+ sourceLocation: SourceLocationModel;
69
+ }
63
70
  export interface SequenceScenarioModel {
64
71
  kind: 'bin' | 'export';
65
72
  name: string;
@@ -100,6 +107,9 @@ interface MemberModel {
100
107
  inheritedFrom?: string;
101
108
  children?: MemberModel[];
102
109
  }
110
+ interface StructuredRowModel {
111
+ values: Record<string, string>;
112
+ }
103
113
  export interface ModuleModel {
104
114
  path: string;
105
115
  isEntrypoint: boolean;
@@ -1,7 +1,19 @@
1
1
  import type { ParadoxConfig } from '../config/types.js';
2
+ /***
3
+ * Searches upward from a start directory until it finds a supported Paradox config file.
4
+ */
2
5
  export declare function findParadoxConfigFile(startDir: string): Promise<string | null>;
6
+ /***
7
+ * Loads a Paradox config module and returns its default export or an empty config.
8
+ */
3
9
  export declare function loadParadoxConfig(configFilePath: string): Promise<ParadoxConfig>;
10
+ /***
11
+ * Resolves and validates the package root from config and config directory.
12
+ */
4
13
  export declare function resolvePackageRoot(config: ParadoxConfig, configDir: string): Promise<string>;
14
+ /***
15
+ * Resolves the configured output directory and enforces that it stays inside the package root.
16
+ */
5
17
  export declare function resolveOutputRoot(config: ParadoxConfig, packageRoot: string): {
6
18
  outputDir: string;
7
19
  outputRoot: string;
@@ -7,6 +7,9 @@ const CONFIG_FILENAMES = [
7
7
  'paradox.config.mjs',
8
8
  'paradox.config.cjs',
9
9
  ];
10
+ /***
11
+ * Searches upward from a start directory until it finds a supported Paradox config file.
12
+ */
10
13
  export async function findParadoxConfigFile(startDir) {
11
14
  let current = resolve(startDir);
12
15
  let parent = dirname(current);
@@ -26,11 +29,17 @@ export async function findParadoxConfigFile(startDir) {
26
29
  }
27
30
  return null;
28
31
  }
32
+ /***
33
+ * Loads a Paradox config module and returns its default export or an empty config.
34
+ */
29
35
  export async function loadParadoxConfig(configFilePath) {
30
36
  const url = pathToFileURL(configFilePath).href;
31
37
  const mod = (await import(url));
32
38
  return mod.default ?? {};
33
39
  }
40
+ /***
41
+ * Resolves and validates the package root from config and config directory.
42
+ */
34
43
  export async function resolvePackageRoot(config, configDir) {
35
44
  const configuredRoot = config.package?.root;
36
45
  const resolvedRoot = configuredRoot
@@ -41,6 +50,9 @@ export async function resolvePackageRoot(config, configDir) {
41
50
  await assertPathExists(join(resolvedRoot, 'package.json'), `Unable to find package.json at resolved package root: ${resolvedRoot}`);
42
51
  return resolvedRoot;
43
52
  }
53
+ /***
54
+ * Resolves the configured output directory and enforces that it stays inside the package root.
55
+ */
44
56
  export function resolveOutputRoot(config, packageRoot) {
45
57
  const outputDir = config.output?.dir ?? 'paradox';
46
58
  validateOutputDir(outputDir);
@@ -48,6 +60,9 @@ export function resolveOutputRoot(config, packageRoot) {
48
60
  assertWithinRoot(outputRoot, packageRoot, `Resolved output directory escapes package root: ${outputRoot}`);
49
61
  return { outputDir, outputRoot };
50
62
  }
63
+ /***
64
+ * Checks whether a filesystem path can be accessed.
65
+ */
51
66
  async function pathExists(path) {
52
67
  try {
53
68
  await access(path);
@@ -57,11 +72,17 @@ async function pathExists(path) {
57
72
  return false;
58
73
  }
59
74
  }
75
+ /***
76
+ * Throws a supplied error message when a required path does not exist.
77
+ */
60
78
  async function assertPathExists(path, message) {
61
79
  if (!(await pathExists(path))) {
62
80
  throw new Error(message);
63
81
  }
64
82
  }
83
+ /***
84
+ * Validates an output directory string before resolving it against the package root.
85
+ */
65
86
  function validateOutputDir(outputDir) {
66
87
  const trimmed = outputDir.trim();
67
88
  if (trimmed.length === 0) {
@@ -74,18 +95,22 @@ function validateOutputDir(outputDir) {
74
95
  if (isAbsolute(normalized)) {
75
96
  throw new Error('Invalid output.dir: must be a relative path inside the package root (absolute paths are not allowed).');
76
97
  }
77
- // Reject explicit parent traversal to keep writes deterministic and scoped.
78
98
  for (const segment of splitPathSegments(trimmed)) {
79
99
  if (segment === '..') {
80
100
  throw new Error('Invalid output.dir: must not contain ".." path segments.');
81
101
  }
82
102
  }
83
103
  }
104
+ /***
105
+ * Splits a configured path into normalized non-empty path segments.
106
+ */
84
107
  function splitPathSegments(path) {
85
- // Treat both separators as potential delimiters to stay robust across platforms/config styles.
86
108
  const normalized = path.replaceAll('\\', '/');
87
109
  return normalized.split('/').filter((segment) => segment.length > 0 && segment !== '.');
88
110
  }
111
+ /***
112
+ * Throws when a resolved path is outside the expected package root.
113
+ */
89
114
  function assertWithinRoot(resolvedPath, root, message) {
90
115
  const rel = relative(root, resolvedPath);
91
116
  if (rel === '')