pkg-scaffold 2.2.0 → 2.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.
@@ -0,0 +1,92 @@
1
+ import path from 'path';
2
+ import fs from 'fs/promises';
3
+
4
+ /**
5
+ * Cross-Reference Dependency Matrix & Breakage Risk Auditor
6
+ * Traces dynamic runtime usage patterns to prevent code pruning from breaking downstream systems.
7
+ */
8
+ export class ImpactAnalyzer {
9
+ constructor(context) {
10
+ this.context = context;
11
+ this.safetyOverlays = [/\.json$/, /\.json5$/, /\.html$/, /\.yaml$/, /\.yml$/];
12
+ }
13
+
14
+ /**
15
+ * Scans non-code assets and dynamic lookups to check if an unused export is needed elsewhere.
16
+ * @param {string} originFile - Absolute path of component housing target symbol
17
+ * @param {string} symbolName - Literal identifier being evaluated for deletion
18
+ * @param {Map} projectGraph - Full project structural graph representation
19
+ */
20
+ async verifyRefactorSafety(originFile, symbolName, projectGraph) {
21
+ // Avoid dropping generic single letter tokens or framework primitives
22
+ if (symbolName === 'default' || symbolName.length <= 2) {
23
+ return { isSafeToPrune: false, blockReason: 'PROTECTED_SYSTEM_CONTRACT_KEYWORD' };
24
+ }
25
+
26
+ // Rule 1: Check across all code files for loose string-based token references
27
+ for (const [filePath, fileNode] of projectGraph.entries()) {
28
+ if (filePath === originFile) continue;
29
+
30
+ // If the symbol name is explicitly referenced in an element lookup or template slice, flag it as risky
31
+ if (fileNode.rawStringReferences && fileNode.rawStringReferences.has(symbolName)) {
32
+ return {
33
+ isSafeToPrune: false,
34
+ blockReason: `LOOSE_STRING_ACCESS_MATCH_FOUND_IN: ${path.relative(this.context.cwd, filePath)}`
35
+ };
36
+ }
37
+
38
+ // Check member property chain lookups: customer.profile.billingAddress
39
+ if (fileNode.propertyAccessChains) {
40
+ for (const chain of fileNode.propertyAccessChains) {
41
+ if (chain.endsWith(`.${symbolName}`) || chain.includes(`.${symbolName}.`)) {
42
+ return {
43
+ isSafeToPrune: false,
44
+ blockReason: `DYNAMIC_PROPERTY_ACCESS_CHAIN_HIT: ${chain}`
45
+ };
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ // Rule 2: Crawl through external static manifests (JSON metadata, HTML routing templates, workflow files)
52
+ const configurations = await this.gatherMetadataFiles(this.context.cwd);
53
+
54
+ for (const confPath of configurations) {
55
+ try {
56
+ const payload = await fs.readFile(confPath, 'utf8');
57
+
58
+ // Match string references inside configuration boundaries
59
+ if (payload.includes(`"${symbolName}"`) || payload.includes(`'${symbolName}'`) || payload.includes(`data-${symbolName}`)) {
60
+ return {
61
+ isSafeToPrune: false,
62
+ blockReason: `METADATA_MANIFEST_DEPENDENCY_FOUND_IN: ${path.relative(this.context.cwd, confPath)}`
63
+ };
64
+ }
65
+ } catch {
66
+ // Read step error; skip unreadable descriptors
67
+ }
68
+ }
69
+
70
+ return { isSafeToPrune: true, blockReason: null };
71
+ }
72
+
73
+ async gatherMetadataFiles(dir, collected = []) {
74
+ try {
75
+ const entities = await fs.readdir(dir, { withFileTypes: true });
76
+ for (const ent of entities) {
77
+ const resolutionPath = path.join(dir, ent.name);
78
+
79
+ if (ent.isDirectory()) {
80
+ if (ent.name === 'node_modules' || ent.name === '.git' || ent.name === '.scaffold-cache' || ent.name === 'dist') continue;
81
+ await this.gatherMetadataFiles(resolutionPath, collected);
82
+ } else if (ent.isFile()) {
83
+ const actsAsMetaAsset = this.safetyOverlays.some(regex => regex.test(ent.name));
84
+ if (actsAsMetaAsset) {
85
+ collected.push(resolutionPath);
86
+ }
87
+ }
88
+ }
89
+ } catch {}
90
+ return collected;
91
+ }
92
+ }
@@ -0,0 +1,86 @@
1
+ import ts from 'typescript';
2
+ import fs from 'fs/promises';
3
+
4
+ /**
5
+ * Format-Preserving Abstract Syntax Tree Source Mutation Engine
6
+ * Executes targeted updates using token position logic while preserving trivia (comments/JSDoc).
7
+ */
8
+ export class SourceRewriter {
9
+ constructor(context) {
10
+ this.context = context;
11
+ }
12
+
13
+ /**
14
+ * Removes a specific named export symbol declaration block from code text.
15
+ * @param {string} filePath - Absolute path to on-disk code component
16
+ * @param {string} symbolName - Target export key to prune
17
+ * @param {Object} exportMeta - Mapped token offset indices (start/end offsets)
18
+ */
19
+ async stripNamedExportSignature(filePath, symbolName, exportMeta) {
20
+ const rawSource = await fs.readFile(filePath, 'utf8');
21
+
22
+ // Case A: Token is grouped inside a multi-export destructured statement block: export { a, b, c };
23
+ if (exportMeta.type === 'named') {
24
+ const updatedText = this.pruneFromSharedExportClause(rawSource, symbolName, exportMeta);
25
+ if (updatedText) return updatedText;
26
+ }
27
+
28
+ // Case B: Isolated node removal. Calculate indices from start to end while preserving comments.
29
+ const startOffset = exportMeta.start;
30
+ const endOffset = exportMeta.end;
31
+
32
+ // Split source without dropping trailing line structural punctuation or text formatting configurations
33
+ const prefix = rawSource.slice(0, startOffset);
34
+ let suffix = rawSource.slice(endOffset);
35
+
36
+ // Clean up trailing commas or semicolons to prevent syntax errors
37
+ if (suffix.startsWith(';') || suffix.startsWith(',')) {
38
+ suffix = suffix.slice(1);
39
+ }
40
+
41
+ return prefix + suffix;
42
+ }
43
+
44
+ /**
45
+ * Handles selective pruning of export elements within inline groupings like `export { x, y as z }`.
46
+ */
47
+ pruneFromSharedExportClause(sourceText, symbolName, meta) {
48
+ // Find the enclosing export declaration node using structural boundaries
49
+ const sourceFile = ts.createSourceFile(
50
+ 'temp.ts',
51
+ sourceText,
52
+ ts.ScriptTarget.Latest,
53
+ true
54
+ );
55
+
56
+ let targetText = null;
57
+
58
+ const findAndPrune = (node) => {
59
+ if (ts.isExportDeclaration(node) && node.exportClause && ts.isNamedExports(node.exportClause)) {
60
+ const start = node.getStart(sourceFile);
61
+ const end = node.getEnd();
62
+
63
+ // Check if the target node spans the exact offset coordinates of the dead symbol
64
+ if (meta.start >= start && meta.end <= end) {
65
+ const activeElements = node.exportClause.elements;
66
+ const keptSymbols = activeElements
67
+ .filter(el => el.name.text !== symbolName)
68
+ .map(el => el.getText(sourceFile));
69
+
70
+ if (keptSymbols.length === 0) {
71
+ // Drop the entire declaration block if no active exports remain inside it
72
+ targetText = sourceText.slice(0, start) + sourceText.slice(end);
73
+ } else {
74
+ // Rewrite the export group inline, preserving formatting and comments
75
+ const reconstructedClause = `export { ${keptSymbols.join(', ')} };`;
76
+ targetText = sourceText.slice(0, start) + reconstructedClause + sourceText.slice(end);
77
+ }
78
+ }
79
+ }
80
+ ts.forEachChild(node, findAndPrune);
81
+ };
82
+
83
+ findAndPrune(sourceFile);
84
+ return targetText;
85
+ }
86
+ }
@@ -0,0 +1,131 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Transactional File System Operations Supervisor
6
+ * Guarantees atomicity across multi-file transformations by tracking rolling backups.
7
+ */
8
+ export class TransactionManager {
9
+ constructor(context) {
10
+ this.context = context;
11
+ this.backupDirectory = path.join(context.cacheDir, 'backups');
12
+ this.journal = [];
13
+ this.isLocked = false;
14
+ }
15
+
16
+ /**
17
+ * Begins a new transaction lifecycle loop, initializing isolation vectors.
18
+ */
19
+ async begin() {
20
+ if (this.isLocked) {
21
+ throw new Error('Transaction Manager concurrency fault: Another transaction is already staged.');
22
+ }
23
+ this.isLocked = true;
24
+ this.journal = [];
25
+ await fs.mkdir(this.backupDirectory, { recursive: true });
26
+ }
27
+
28
+ /**
29
+ * Stages a destructive file modification or creation sequence.
30
+ * @param {string} filePath - Absolute system file target location
31
+ * @param {string} nextContent - The completely rewritten proposed source structure
32
+ */
33
+ async stageWrite(filePath, nextContent) {
34
+ this.assertLock();
35
+ let originalContent = null;
36
+ let backupPath = null;
37
+ let operationType = 'UPDATE';
38
+
39
+ try {
40
+ await fs.access(filePath);
41
+ originalContent = await fs.readFile(filePath, 'utf8');
42
+
43
+ const fileId = Buffer.from(filePath).toString('base64url');
44
+ backupPath = path.join(this.backupDirectory, `${fileId}.bak`);
45
+ await fs.writeFile(backupPath, originalContent, 'utf8');
46
+ } catch {
47
+ operationType = 'CREATE';
48
+ }
49
+
50
+ // Attempt target file mutation write directly to disk
51
+ await fs.writeFile(filePath, nextContent, 'utf8');
52
+
53
+ this.journal.push({
54
+ type: operationType,
55
+ targetFile: filePath,
56
+ backupLocation: backupPath
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Stages an element deletion sequence safely.
62
+ * @param {string} filePath - Target component being evaluated as dead
63
+ */
64
+ async stageDeletion(filePath) {
65
+ this.assertLock();
66
+
67
+ // Read previous state data for archiving before dropping linkage
68
+ const originalContent = await fs.readFile(filePath, 'utf8');
69
+ const fileId = Buffer.from(filePath).toString('base64url');
70
+ const backupPath = path.join(this.backupDirectory, `${fileId}.bak`);
71
+
72
+ await fs.writeFile(backupPath, originalContent, 'utf8');
73
+ await fs.unlink(filePath);
74
+
75
+ this.journal.push({
76
+ type: 'DELETE',
77
+ targetFile: filePath,
78
+ backupLocation: backupPath
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Finalizes the transaction sequence, cleaning up local transaction cache points.
84
+ */
85
+ async commit() {
86
+ this.assertLock();
87
+ try {
88
+ for (const record of this.journal) {
89
+ if (record.backupLocation) {
90
+ await fs.unlink(record.backupLocation).catch(() => {});
91
+ }
92
+ }
93
+ } finally {
94
+ this.releaseLock();
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Reverts changes to their original states if any error or verification failure is flagged.
100
+ */
101
+ async rollback() {
102
+ this.assertLock();
103
+ try {
104
+ // Process journal logs in reverse execution order to preserve dependencies
105
+ for (let i = this.journal.length - 1; i >= 0; i--) {
106
+ const record = this.journal[i];
107
+
108
+ if (record.type === 'CREATE') {
109
+ await fs.unlink(record.targetFile).catch(() => {});
110
+ } else if (record.type === 'UPDATE' || record.type === 'DELETE') {
111
+ const originalContent = await fs.readFile(record.backupLocation, 'utf8');
112
+ await fs.writeFile(record.targetFile, originalContent, 'utf8');
113
+ await fs.unlink(record.backupLocation).catch(() => {});
114
+ }
115
+ }
116
+ } finally {
117
+ this.releaseLock();
118
+ }
119
+ }
120
+
121
+ assertLock() {
122
+ if (!this.isLocked) {
123
+ throw new Error('Transaction Manager boundary violation: No active tracking block exists.');
124
+ }
125
+ }
126
+
127
+ releaseLock() {
128
+ this.isLocked = false;
129
+ this.journal = [];
130
+ }
131
+ }
@@ -0,0 +1,75 @@
1
+ import ts from 'typescript';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+
5
+ /**
6
+ * Ambient Type Declaration (`.d.ts`) Alignment Supervisor
7
+ * Updates declaration mapping entries to keep compiler checks stable.
8
+ */
9
+ export class TypeIntegrity {
10
+ constructor(context) {
11
+ this.context = context;
12
+ }
13
+
14
+ /**
15
+ * Re-evaluates ambient files to verify type alignment after optimization changes.
16
+ * @param {string} sourceFilePath - Absolute filename target context location
17
+ * @param {string} prunedSymbolName - Target variable token removed from active source graph
18
+ */
19
+ async synchronizeDeclarationFile(sourceFilePath, prunedSymbolName) {
20
+ const fileDirectory = path.dirname(sourceFilePath);
21
+ const baselineName = path.basename(sourceFilePath, path.extname(sourceFilePath));
22
+
23
+ // Map standard ambient types build output combinations
24
+ const declarationTargets = [
25
+ path.join(fileDirectory, `${baselineName}.d.ts`),
26
+ path.join(this.context.cwd, 'dist', `${baselineName}.d.ts`),
27
+ path.join(this.context.cwd, 'types', `${baselineName}.d.ts`)
28
+ ];
29
+
30
+ for (const dtsPath of declarationTargets) {
31
+ try {
32
+ await fs.access(dtsPath);
33
+ const code = await fs.readFile(dtsPath, 'utf8');
34
+
35
+ const sourceFile = ts.createSourceFile(
36
+ dtsPath,
37
+ code,
38
+ ts.ScriptTarget.Latest,
39
+ true,
40
+ ts.ScriptKind.TS
41
+ );
42
+
43
+ const replacementIntervals = [];
44
+ this.inspectDtsNodes(sourceFile, prunedSymbolName, replacementIntervals);
45
+
46
+ if (replacementIntervals.length > 0) {
47
+ let updatedDtsText = code;
48
+ // Apply substring text deletions from back to front to keep offset indexes accurate
49
+ replacementIntervals.sort((a, b) => b.start - a.start);
50
+
51
+ for (const interval of replacementIntervals) {
52
+ updatedDtsText = updatedDtsText.slice(0, interval.start) + updatedDtsText.slice(interval.end);
53
+ }
54
+
55
+ await fs.writeFile(dtsPath, updatedDtsText, 'utf8');
56
+ }
57
+ } catch {
58
+ // Declaration file variation target absent; proceed to fallback check routes
59
+ }
60
+ }
61
+ }
62
+
63
+ inspectDtsNodes(node, matchSymbol, intervals) {
64
+ if (!node) return;
65
+
66
+ // Match type mutations inside ambient structural parameters
67
+ if (ts.isExportSpecifier(node) && node.name.text === matchSymbol) {
68
+ intervals.push({ start: node.getStart(), end: node.getEnd() });
69
+ } else if ((ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isClassDeclaration(node) || ts.isFunctionDeclaration(node)) && node.name && node.name.text === matchSymbol) {
70
+ intervals.push({ start: node.getStart(), end: node.getEnd() });
71
+ }
72
+
73
+ ts.forEachChild(node, child => this.inspectDtsNodes(child, matchSymbol, intervals));
74
+ }
75
+ }
@@ -0,0 +1,120 @@
1
+ import resolve from 'enhanced-resolve';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Industrial-Strength Module Resolution Supervisor
6
+ * Integrates path mapping and workspace topologies using enhanced-resolve.
7
+ */
8
+ export class DependencyResolver {
9
+ constructor(context, pathMapper, workspaceGraph) {
10
+ this.context = context;
11
+ this.pathMapper = pathMapper;
12
+ this.workspaceGraph = workspaceGraph;
13
+
14
+ // Instantiate production-grade enhanced-resolve workspace parameters
15
+ this.nativeResolver = resolve.create.sync({
16
+ conditionNames: ['import', 'module', 'require', 'node', 'types'],
17
+ extensions: ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.json', '.vue'],
18
+ mainFields: ['module', 'main', 'types'],
19
+ exportsFields: ['exports'],
20
+ symlinks: true
21
+ });
22
+ }
23
+
24
+ /**
25
+ * Resolves a raw import string from a source file into an absolute file path.
26
+ * @param {string} containingFile - Absolute path of the file containing the import declaration
27
+ * @param {string} importSpecifier - Raw import target string (e.g., '../components/Button' or '@utils/math')
28
+ * @returns {string|null} Resolved absolute file path location on disk, or null if external/third-party node_module
29
+ */
30
+ resolveModulePath(containingFile, importSpecifier) {
31
+ const containingDir = path.dirname(containingFile);
32
+
33
+ // Rule A: Intercept and resolve local monorepo workspace cross-links
34
+ if (this.workspaceGraph.isLocalWorkspaceSpecifier(importSpecifier)) {
35
+ const match = this.workspaceGraph.getWorkspacePackageMatch(importSpecifier);
36
+ if (match) {
37
+ if (importSpecifier === match.packageName) {
38
+ // Point directly to the target package's configured index entry file
39
+ return match.entryPoints[0] || null;
40
+ }
41
+
42
+ // Handle deep sub-path monorepo target imports
43
+ const subPathOffset = importSpecifier.slice(match.packageName.length + 1);
44
+ try {
45
+ return this.nativeResolver(match.rootDirectory, `./${subPathOffset}`);
46
+ } catch {
47
+ // Fall back to scanning the package root directly if the sub-path lookup fails
48
+ }
49
+ }
50
+ }
51
+
52
+ // Rule B: Intercept and expand path mapping aliases (@/*)
53
+ const aliasedCandidates = this.pathMapper.resolveCandidatePaths(importSpecifier);
54
+ if (aliasedCandidates.length > 0) {
55
+ for (const candidate of aliasedCandidates) {
56
+ try {
57
+ const resolvedPath = this.nativeResolver(containingDir, candidate);
58
+ if (this.isAbsoluteInternalPath(resolvedPath)) {
59
+ return resolvedPath;
60
+ }
61
+ } catch {
62
+ // Candidate target path absent; try the next fallback pattern entry
63
+ }
64
+ }
65
+ }
66
+
67
+ // Rule C: Standard file system lookups for standard files or package assets
68
+ try {
69
+ const resolvedPath = this.nativeResolver(containingDir, importSpecifier);
70
+ if (this.isAbsoluteInternalPath(resolvedPath)) {
71
+ return resolvedPath;
72
+ }
73
+ } catch (err) {
74
+ if (this.context.verbose) {
75
+ // Output trace logs for unresolvable dependencies during deep code investigations
76
+ console.debug(`[Resolution Trace Skip] Specifier unresolvable from context: ${importSpecifier} inside ${containingFile}`);
77
+ }
78
+ }
79
+
80
+ return null; // Target is an external node_module dependency or an unresolvable asset
81
+ }
82
+
83
+ /**
84
+ * Ensures our tracking focus stays locked onto internal codebase components, bypassing third-party node_modules.
85
+ */
86
+ isAbsoluteInternalPath(resolvedPath) {
87
+ if (!resolvedPath) return false;
88
+ const normalized = resolvedPath.replace(/\\/g, '/');
89
+
90
+ // Ignore external node_modules blocks, but preserve local monorepo packages that live inside symlinked node_modules
91
+ if (normalized.includes('/node_modules/')) {
92
+ for (const [name, meta] of this.workspaceGraph.packageManifests.entries()) {
93
+ if (normalized.startsWith(meta.rootDirectory.replace(/\\/g, '/'))) {
94
+ return true;
95
+ }
96
+ }
97
+ return false;
98
+ }
99
+
100
+ return path.isAbsolute(resolvedPath);
101
+ }
102
+
103
+ /**
104
+ * Challenge #17 Intent Detection. Evaluates if an export serves as a consumer contract distribution node.
105
+ */
106
+ determineIntentProfile(filePath, declaredExportsManifest) {
107
+ const fileName = path.basename(filePath);
108
+ const isPublicContractFile = /(^index|^public\-api|^entry)\.(ts|js|tsx|jsx)$/i.test(fileName);
109
+
110
+ // If the file is a primary bundle entry point, flag its exports as protected public contracts
111
+ if (isPublicContractFile) {
112
+ for (const [symbolKey, metadata] of declaredExportsManifest.entries()) {
113
+ metadata.isLibraryContract = true;
114
+ }
115
+ return 'LIBRARY_CONSUMPTION_TARGET';
116
+ }
117
+
118
+ return 'INTERNAL_CODEBASE_ELEMENT';
119
+ }
120
+ }
@@ -0,0 +1,115 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Advanced TSConfig / JSConfig Compilation Path Alias Mapper
6
+ * Resolves deeply nested route mappings, wildcards, and base URL overrides.
7
+ */
8
+ export class PathMapper {
9
+ constructor(context) {
10
+ this.context = context;
11
+ this.baseUrl = '.';
12
+ this.absoluteBaseUrl = context.cwd;
13
+ this.mappings = []; // Collection of { prefix, suffix, targets[] }
14
+ }
15
+
16
+ /**
17
+ * Reads, cleans, and indexes custom alias entries from tsconfig.json files.
18
+ * @param {string} tsconfigFilename - Target designator (typically tsconfig.json)
19
+ */
20
+ async loadMappings(tsconfigFilename = 'tsconfig.json') {
21
+ const configPath = path.resolve(this.context.cwd, tsconfigFilename);
22
+
23
+ try {
24
+ await fs.access(configPath);
25
+ const rawText = await fs.readFile(configPath, 'utf8');
26
+
27
+ // Strip inline single-line and block comments before parsing
28
+ const jsonCleanText = rawText.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1');
29
+ const tsconfig = JSON.parse(jsonCleanText);
30
+
31
+ if (!tsconfig.compilerOptions) return;
32
+
33
+ const opts = tsconfig.compilerOptions;
34
+ if (opts.baseUrl) {
35
+ this.baseUrl = opts.baseUrl;
36
+ this.absoluteBaseUrl = path.resolve(this.context.cwd, this.baseUrl);
37
+ }
38
+
39
+ if (opts.paths) {
40
+ for (const [aliasPattern, targetArrays] of Object.entries(opts.paths)) {
41
+ this.registerPatternRule(aliasPattern, targetArrays);
42
+ }
43
+ }
44
+ } catch (error) {
45
+ if (this.context.verbose) {
46
+ console.warn(`⚠️ [PathMapper Override] Proceeding without custom path configurations. Source: ${error.message}`);
47
+ }
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Registers structural lookup parameters for alias strings.
53
+ */
54
+ registerPatternRule(pattern, targets) {
55
+ const wildcardIndex = pattern.indexOf('*');
56
+
57
+ if (wildcardIndex === -1) {
58
+ this.mappings.push({
59
+ isExact: true,
60
+ pattern,
61
+ targets: targets.map(t => path.normalize(t))
62
+ });
63
+ return;
64
+ }
65
+
66
+ const prefix = pattern.slice(0, wildcardIndex);
67
+ const suffix = pattern.slice(wildcardIndex + 1);
68
+
69
+ this.mappings.push({
70
+ isExact: false,
71
+ prefix,
72
+ suffix,
73
+ targets: targets.map(t => path.normalize(t))
74
+ });
75
+ }
76
+
77
+ /**
78
+ * Resolves a raw import specifier against mapped path patterns.
79
+ * @param {string} specifier - Raw text from import declaration (e.g., '@ui/button')
80
+ * @returns {Array<string>} Candidates of absolute filesystem paths to try resolving
81
+ */
82
+ resolveCandidatePaths(specifier) {
83
+ const matchingCandidates = [];
84
+
85
+ for (const rule of this.mappings) {
86
+ if (rule.isExact) {
87
+ if (specifier === rule.pattern) {
88
+ rule.targets.forEach(target => {
89
+ matchingCandidates.push(path.resolve(this.absoluteBaseUrl, target));
90
+ });
91
+ }
92
+ } else {
93
+ // Evaluate wildcard pattern matches
94
+ if (specifier.startsWith(rule.prefix) && specifier.endsWith(rule.suffix)) {
95
+ const extractedWildcardContent = specifier.slice(
96
+ rule.prefix.length,
97
+ specifier.length - rule.suffix.length
98
+ );
99
+
100
+ rule.targets.forEach(targetTemplate => {
101
+ const interpolatedTarget = targetTemplate.replace('*', extractedWildcardContent);
102
+ matchingCandidates.push(path.resolve(this.absoluteBaseUrl, interpolatedTarget));
103
+ });
104
+ }
105
+ }
106
+ }
107
+
108
+ // Fall back to direct lookup relative to the base URL
109
+ if (!specifier.startsWith('.') && !path.isAbsolute(specifier)) {
110
+ matchingCandidates.push(path.resolve(this.absoluteBaseUrl, specifier));
111
+ }
112
+
113
+ return matchingCandidates;
114
+ }
115
+ }