Skip to content

Commit 2423c9e

Browse files
fix: better module resolution for cli. (#60)
1 parent af0d9d2 commit 2423c9e

10 files changed

Lines changed: 347 additions & 164 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/css/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@knighted/css",
3-
"version": "1.1.0-rc.2",
3+
"version": "1.1.0-rc.3",
44
"description": "A build-time utility that traverses JavaScript/TypeScript module dependency graphs to extract, compile, and optimize all imported CSS into a single, in-memory string.",
55
"type": "module",
66
"main": "./dist/css.js",

packages/css/src/generateTypes.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { createMatchPath, type MatchPath } from 'tsconfig-paths'
1111

1212
import { cssWithMeta, DEFAULT_EXTENSIONS } from './css.js'
1313
import { analyzeModule, type DefaultExportSignal } from './lexer.js'
14+
import { createResolverFactory, resolveWithFactory } from './moduleResolution.js'
1415
import { buildStableSelectorsLiteral } from './stableSelectorsLiteral.js'
1516
import { resolveStableNamespace } from './stableNamespace.js'
1617

@@ -117,6 +118,10 @@ function getImportMetaUrl(): string | undefined {
117118
const SELECTOR_REFERENCE = '.knighted-css'
118119
const SELECTOR_MODULE_SUFFIX = '.knighted-css.ts'
119120
const STYLE_EXTENSIONS = DEFAULT_EXTENSIONS.map(ext => ext.toLowerCase())
121+
const SCRIPT_EXTENSIONS = Array.from(SUPPORTED_EXTENSIONS)
122+
const RESOLUTION_EXTENSIONS = Array.from(
123+
new Set<string>([...SCRIPT_EXTENSIONS, ...STYLE_EXTENSIONS]),
124+
)
120125
const EXTENSION_FALLBACKS: Record<string, string[]> = {
121126
'.js': ['.ts', '.tsx', '.jsx', '.mjs', '.cjs'],
122127
'.mjs': ['.mts', '.mjs', '.js', '.ts', '.tsx'],
@@ -127,7 +132,7 @@ const EXTENSION_FALLBACKS: Record<string, string[]> = {
127132
export async function generateTypes(
128133
options: GenerateTypesOptions = {},
129134
): Promise<GenerateTypesResult> {
130-
const rootDir = path.resolve(options.rootDir ?? process.cwd())
135+
const rootDir = await resolveRootDir(path.resolve(options.rootDir ?? process.cwd()))
131136
const include = normalizeIncludeOptions(options.include, rootDir)
132137
const cacheDir = path.resolve(options.outDir ?? path.join(rootDir, '.knighted-css'))
133138
const tsconfig = loadTsconfigResolutionContext(rootDir)
@@ -146,10 +151,23 @@ export async function generateTypes(
146151
return generateDeclarations(internalOptions)
147152
}
148153

154+
async function resolveRootDir(rootDir: string): Promise<string> {
155+
try {
156+
return await fs.realpath(rootDir)
157+
} catch {
158+
return rootDir
159+
}
160+
}
161+
149162
async function generateDeclarations(
150163
options: GenerateTypesInternalOptions,
151164
): Promise<GenerateTypesResult> {
152165
const peerResolver = createProjectPeerResolver(options.rootDir)
166+
const resolverFactory = createResolverFactory(
167+
options.rootDir,
168+
RESOLUTION_EXTENSIONS,
169+
SCRIPT_EXTENSIONS,
170+
)
153171
const files = await collectCandidateFiles(options.include)
154172
const selectorModulesManifestPath = path.join(options.cacheDir, 'selector-modules.json')
155173
const previousSelectorManifest = await readManifest(selectorModulesManifestPath)
@@ -176,6 +194,8 @@ async function generateDeclarations(
176194
match.importer,
177195
options.rootDir,
178196
options.tsconfig,
197+
resolverFactory,
198+
RESOLUTION_EXTENSIONS,
179199
)
180200
if (!resolvedPath) {
181201
warnings.push(
@@ -352,7 +372,8 @@ function stripInlineLoader(specifier: string): string {
352372
}
353373

354374
function splitResourceAndQuery(specifier: string): { resource: string; query: string } {
355-
const hashIndex = specifier.indexOf('#')
375+
const hashOffset = specifier.startsWith('#') ? 1 : 0
376+
const hashIndex = specifier.indexOf('#', hashOffset)
356377
const trimmed = hashIndex >= 0 ? specifier.slice(0, hashIndex) : specifier
357378
const queryIndex = trimmed.indexOf('?')
358379
if (queryIndex < 0) {
@@ -391,6 +412,8 @@ async function resolveImportPath(
391412
importerPath: string,
392413
rootDir: string,
393414
tsconfig?: TsconfigResolutionContext,
415+
resolverFactory?: ReturnType<typeof createResolverFactory>,
416+
resolutionExtensions: string[] = RESOLUTION_EXTENSIONS,
394417
): Promise<string | undefined> {
395418
if (!resourceSpecifier) return undefined
396419
if (resourceSpecifier.startsWith('.')) {
@@ -405,6 +428,17 @@ async function resolveImportPath(
405428
if (tsconfigResolved) {
406429
return resolveWithExtensionFallback(tsconfigResolved)
407430
}
431+
if (resolverFactory) {
432+
const resolved = resolveWithFactory(
433+
resolverFactory,
434+
resourceSpecifier,
435+
importerPath,
436+
resolutionExtensions,
437+
)
438+
if (resolved) {
439+
return resolved
440+
}
441+
}
408442
const requireFromRoot = getProjectRequire(rootDir)
409443
try {
410444
return requireFromRoot.resolve(resourceSpecifier)

packages/css/src/moduleGraph.ts

Lines changed: 11 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import path from 'node:path'
22
import { builtinModules } from 'node:module'
3-
import { existsSync, promises as fs, statSync } from 'node:fs'
4-
import { fileURLToPath } from 'node:url'
3+
import { promises as fs } from 'node:fs'
54

65
import { parseSync, Visitor } from 'oxc-parser'
76
import type {
@@ -10,15 +9,16 @@ import type {
109
ImportExpression,
1110
TSImportEqualsDeclaration,
1211
} from 'oxc-parser'
13-
import {
14-
ResolverFactory,
15-
type NapiResolveOptions,
16-
type TsconfigOptions as ResolverTsconfigOptions,
17-
} from 'oxc-resolver'
1812
import { createMatchPath } from 'tsconfig-paths'
1913
import { getTsconfig } from 'get-tsconfig'
2014

2115
import type { CssResolver } from './types.js'
16+
import {
17+
createResolverFactory,
18+
findExistingFile,
19+
normalizeResolverResult,
20+
resolveWithFactory,
21+
} from './moduleResolution.js'
2222

2323
const SCRIPT_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']
2424

@@ -84,7 +84,10 @@ export async function collectStyleImports(
8484
cwd,
8585
resolutionExtensions,
8686
scriptExtensions,
87-
graphOptions,
87+
{
88+
conditions: graphOptions?.conditions,
89+
tsconfig: graphOptions?.tsConfig,
90+
},
8891
)
8992

9093
async function walk(filePath: string): Promise<void> {
@@ -496,103 +499,6 @@ function unwrapExpression(expression: Expression): Expression {
496499
return expression
497500
}
498501

499-
function normalizeResolverResult(
500-
result: string | undefined,
501-
cwd: string,
502-
): string | undefined {
503-
if (!result) {
504-
return undefined
505-
}
506-
if (result.startsWith('file://')) {
507-
try {
508-
return fileURLToPath(new URL(result))
509-
} catch {
510-
return undefined
511-
}
512-
}
513-
return path.isAbsolute(result) ? result : path.resolve(cwd, result)
514-
}
515-
516-
function resolveWithFactory(
517-
factory: ResolverFactory,
518-
specifier: string,
519-
importer: string,
520-
extensions: string[],
521-
): string | undefined {
522-
if (specifier.startsWith('file://')) {
523-
try {
524-
return findExistingFile(fileURLToPath(new URL(specifier)), extensions)
525-
} catch {
526-
return undefined
527-
}
528-
}
529-
if (/^[a-z][\w+.-]*:/i.test(specifier)) {
530-
return undefined
531-
}
532-
try {
533-
const result = factory.resolveFileSync(importer, specifier)
534-
return result?.path
535-
} catch {
536-
return undefined
537-
}
538-
}
539-
540-
function createResolverFactory(
541-
cwd: string,
542-
extensions: string[],
543-
scriptExtensions: string[],
544-
graphOptions?: ModuleGraphOptions,
545-
): ResolverFactory {
546-
const options: NapiResolveOptions = {
547-
extensions,
548-
conditionNames: graphOptions?.conditions,
549-
}
550-
const extensionAlias = buildExtensionAlias(scriptExtensions)
551-
if (extensionAlias) {
552-
options.extensionAlias = extensionAlias
553-
}
554-
const tsconfigOption = resolveResolverTsconfig(graphOptions?.tsConfig, cwd)
555-
options.tsconfig = tsconfigOption ?? 'auto'
556-
return new ResolverFactory(options)
557-
}
558-
559-
function buildExtensionAlias(
560-
scriptExtensions: string[],
561-
): Record<string, string[]> | undefined {
562-
const alias: Record<string, string[]> = {}
563-
const jsTargets = dedupeExtensions(
564-
scriptExtensions.filter(ext =>
565-
['.js', '.ts', '.tsx', '.mjs', '.cjs', '.mts', '.cts'].includes(ext),
566-
),
567-
)
568-
if (jsTargets.length > 0) {
569-
for (const key of ['.js', '.mjs', '.cjs']) {
570-
alias[key] = jsTargets
571-
}
572-
}
573-
const jsxTargets = dedupeExtensions(
574-
scriptExtensions.filter(ext => ext === '.jsx' || ext === '.tsx'),
575-
)
576-
if (jsxTargets.length > 0) {
577-
alias['.jsx'] = jsxTargets
578-
}
579-
return Object.keys(alias).length > 0 ? alias : undefined
580-
}
581-
582-
function resolveResolverTsconfig(
583-
input: TsconfigLike | undefined,
584-
cwd: string,
585-
): ResolverTsconfigOptions | undefined {
586-
if (!input || typeof input !== 'string') {
587-
return undefined
588-
}
589-
const resolved = resolveTsconfigPath(input, cwd)
590-
if (!resolved) {
591-
return undefined
592-
}
593-
return { configFile: resolved }
594-
}
595-
596502
function createTsconfigMatcher(
597503
input: TsconfigLike | undefined,
598504
cwd: string,
@@ -674,46 +580,3 @@ function normalizeTsconfigCompilerOptions(
674580
: path.resolve(configDir, compilerOptions.baseUrl)
675581
return { absoluteBaseUrl, paths: normalizedPaths }
676582
}
677-
678-
function resolveTsconfigPath(tsconfigPath: string, cwd: string): string | undefined {
679-
const absolute = path.isAbsolute(tsconfigPath)
680-
? tsconfigPath
681-
: path.resolve(cwd, tsconfigPath)
682-
if (!existsSync(absolute)) {
683-
return undefined
684-
}
685-
const stats = statSync(absolute)
686-
if (stats.isDirectory()) {
687-
const candidate = path.join(absolute, 'tsconfig.json')
688-
return existsSync(candidate) ? candidate : undefined
689-
}
690-
return absolute
691-
}
692-
693-
function findExistingFile(candidate: string, extensions: string[]): string | undefined {
694-
const candidateHasExt = hasExtension(candidate)
695-
if (candidateHasExt && existsSync(candidate)) {
696-
return candidate
697-
}
698-
if (!candidateHasExt) {
699-
for (const ext of extensions) {
700-
const withExt = `${candidate}${ext}`
701-
if (existsSync(withExt)) {
702-
return withExt
703-
}
704-
}
705-
}
706-
if (existsSync(candidate) && statSync(candidate).isDirectory()) {
707-
for (const ext of extensions) {
708-
const indexPath = path.join(candidate, `index${ext}`)
709-
if (existsSync(indexPath)) {
710-
return indexPath
711-
}
712-
}
713-
}
714-
return undefined
715-
}
716-
717-
function hasExtension(filePath: string): boolean {
718-
return Boolean(path.extname(filePath))
719-
}

0 commit comments

Comments
 (0)