Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/css/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@knighted/css",
"version": "1.1.0-rc.2",
"version": "1.1.0-rc.3",
"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.",
"type": "module",
"main": "./dist/css.js",
Expand Down
38 changes: 36 additions & 2 deletions packages/css/src/generateTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { createMatchPath, type MatchPath } from 'tsconfig-paths'

import { cssWithMeta, DEFAULT_EXTENSIONS } from './css.js'
import { analyzeModule, type DefaultExportSignal } from './lexer.js'
import { createResolverFactory, resolveWithFactory } from './moduleResolution.js'
import { buildStableSelectorsLiteral } from './stableSelectorsLiteral.js'
import { resolveStableNamespace } from './stableNamespace.js'

Expand Down Expand Up @@ -117,6 +118,10 @@ function getImportMetaUrl(): string | undefined {
const SELECTOR_REFERENCE = '.knighted-css'
const SELECTOR_MODULE_SUFFIX = '.knighted-css.ts'
const STYLE_EXTENSIONS = DEFAULT_EXTENSIONS.map(ext => ext.toLowerCase())
const SCRIPT_EXTENSIONS = Array.from(SUPPORTED_EXTENSIONS)
const RESOLUTION_EXTENSIONS = Array.from(
new Set<string>([...SCRIPT_EXTENSIONS, ...STYLE_EXTENSIONS]),
)
const EXTENSION_FALLBACKS: Record<string, string[]> = {
'.js': ['.ts', '.tsx', '.jsx', '.mjs', '.cjs'],
'.mjs': ['.mts', '.mjs', '.js', '.ts', '.tsx'],
Expand All @@ -127,7 +132,7 @@ const EXTENSION_FALLBACKS: Record<string, string[]> = {
export async function generateTypes(
options: GenerateTypesOptions = {},
): Promise<GenerateTypesResult> {
const rootDir = path.resolve(options.rootDir ?? process.cwd())
const rootDir = await resolveRootDir(path.resolve(options.rootDir ?? process.cwd()))
const include = normalizeIncludeOptions(options.include, rootDir)
const cacheDir = path.resolve(options.outDir ?? path.join(rootDir, '.knighted-css'))
const tsconfig = loadTsconfigResolutionContext(rootDir)
Expand All @@ -146,10 +151,23 @@ export async function generateTypes(
return generateDeclarations(internalOptions)
}

async function resolveRootDir(rootDir: string): Promise<string> {
try {
return await fs.realpath(rootDir)
} catch {
return rootDir
}
}

async function generateDeclarations(
options: GenerateTypesInternalOptions,
): Promise<GenerateTypesResult> {
const peerResolver = createProjectPeerResolver(options.rootDir)
const resolverFactory = createResolverFactory(
options.rootDir,
RESOLUTION_EXTENSIONS,
SCRIPT_EXTENSIONS,
)
const files = await collectCandidateFiles(options.include)
const selectorModulesManifestPath = path.join(options.cacheDir, 'selector-modules.json')
const previousSelectorManifest = await readManifest(selectorModulesManifestPath)
Expand All @@ -176,6 +194,8 @@ async function generateDeclarations(
match.importer,
options.rootDir,
options.tsconfig,
resolverFactory,
RESOLUTION_EXTENSIONS,
)
if (!resolvedPath) {
warnings.push(
Expand Down Expand Up @@ -352,7 +372,8 @@ function stripInlineLoader(specifier: string): string {
}

function splitResourceAndQuery(specifier: string): { resource: string; query: string } {
const hashIndex = specifier.indexOf('#')
const hashOffset = specifier.startsWith('#') ? 1 : 0
const hashIndex = specifier.indexOf('#', hashOffset)
const trimmed = hashIndex >= 0 ? specifier.slice(0, hashIndex) : specifier
const queryIndex = trimmed.indexOf('?')
if (queryIndex < 0) {
Expand Down Expand Up @@ -391,6 +412,8 @@ async function resolveImportPath(
importerPath: string,
rootDir: string,
tsconfig?: TsconfigResolutionContext,
resolverFactory?: ReturnType<typeof createResolverFactory>,
resolutionExtensions: string[] = RESOLUTION_EXTENSIONS,
): Promise<string | undefined> {
if (!resourceSpecifier) return undefined
if (resourceSpecifier.startsWith('.')) {
Expand All @@ -405,6 +428,17 @@ async function resolveImportPath(
if (tsconfigResolved) {
return resolveWithExtensionFallback(tsconfigResolved)
}
if (resolverFactory) {
const resolved = resolveWithFactory(
resolverFactory,
resourceSpecifier,
importerPath,
resolutionExtensions,
)
if (resolved) {
return resolved
}
}
const requireFromRoot = getProjectRequire(rootDir)
try {
return requireFromRoot.resolve(resourceSpecifier)
Expand Down
159 changes: 11 additions & 148 deletions packages/css/src/moduleGraph.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import path from 'node:path'
import { builtinModules } from 'node:module'
import { existsSync, promises as fs, statSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { promises as fs } from 'node:fs'

import { parseSync, Visitor } from 'oxc-parser'
import type {
Expand All @@ -10,15 +9,16 @@ import type {
ImportExpression,
TSImportEqualsDeclaration,
} from 'oxc-parser'
import {
ResolverFactory,
type NapiResolveOptions,
type TsconfigOptions as ResolverTsconfigOptions,
} from 'oxc-resolver'
import { createMatchPath } from 'tsconfig-paths'
import { getTsconfig } from 'get-tsconfig'

import type { CssResolver } from './types.js'
import {
createResolverFactory,
findExistingFile,
normalizeResolverResult,
resolveWithFactory,
} from './moduleResolution.js'

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

Expand Down Expand Up @@ -84,7 +84,10 @@ export async function collectStyleImports(
cwd,
resolutionExtensions,
scriptExtensions,
graphOptions,
{
conditions: graphOptions?.conditions,
tsconfig: graphOptions?.tsConfig,
},
)

async function walk(filePath: string): Promise<void> {
Expand Down Expand Up @@ -496,103 +499,6 @@ function unwrapExpression(expression: Expression): Expression {
return expression
}

function normalizeResolverResult(
result: string | undefined,
cwd: string,
): string | undefined {
if (!result) {
return undefined
}
if (result.startsWith('file://')) {
try {
return fileURLToPath(new URL(result))
} catch {
return undefined
}
}
return path.isAbsolute(result) ? result : path.resolve(cwd, result)
}

function resolveWithFactory(
factory: ResolverFactory,
specifier: string,
importer: string,
extensions: string[],
): string | undefined {
if (specifier.startsWith('file://')) {
try {
return findExistingFile(fileURLToPath(new URL(specifier)), extensions)
} catch {
return undefined
}
}
if (/^[a-z][\w+.-]*:/i.test(specifier)) {
return undefined
}
try {
const result = factory.resolveFileSync(importer, specifier)
return result?.path
} catch {
return undefined
}
}

function createResolverFactory(
cwd: string,
extensions: string[],
scriptExtensions: string[],
graphOptions?: ModuleGraphOptions,
): ResolverFactory {
const options: NapiResolveOptions = {
extensions,
conditionNames: graphOptions?.conditions,
}
const extensionAlias = buildExtensionAlias(scriptExtensions)
if (extensionAlias) {
options.extensionAlias = extensionAlias
}
const tsconfigOption = resolveResolverTsconfig(graphOptions?.tsConfig, cwd)
options.tsconfig = tsconfigOption ?? 'auto'
return new ResolverFactory(options)
}

function buildExtensionAlias(
scriptExtensions: string[],
): Record<string, string[]> | undefined {
const alias: Record<string, string[]> = {}
const jsTargets = dedupeExtensions(
scriptExtensions.filter(ext =>
['.js', '.ts', '.tsx', '.mjs', '.cjs', '.mts', '.cts'].includes(ext),
),
)
if (jsTargets.length > 0) {
for (const key of ['.js', '.mjs', '.cjs']) {
alias[key] = jsTargets
}
}
const jsxTargets = dedupeExtensions(
scriptExtensions.filter(ext => ext === '.jsx' || ext === '.tsx'),
)
if (jsxTargets.length > 0) {
alias['.jsx'] = jsxTargets
}
return Object.keys(alias).length > 0 ? alias : undefined
}

function resolveResolverTsconfig(
input: TsconfigLike | undefined,
cwd: string,
): ResolverTsconfigOptions | undefined {
if (!input || typeof input !== 'string') {
return undefined
}
const resolved = resolveTsconfigPath(input, cwd)
if (!resolved) {
return undefined
}
return { configFile: resolved }
}

function createTsconfigMatcher(
input: TsconfigLike | undefined,
cwd: string,
Expand Down Expand Up @@ -674,46 +580,3 @@ function normalizeTsconfigCompilerOptions(
: path.resolve(configDir, compilerOptions.baseUrl)
return { absoluteBaseUrl, paths: normalizedPaths }
}

function resolveTsconfigPath(tsconfigPath: string, cwd: string): string | undefined {
const absolute = path.isAbsolute(tsconfigPath)
? tsconfigPath
: path.resolve(cwd, tsconfigPath)
if (!existsSync(absolute)) {
return undefined
}
const stats = statSync(absolute)
if (stats.isDirectory()) {
const candidate = path.join(absolute, 'tsconfig.json')
return existsSync(candidate) ? candidate : undefined
}
return absolute
}

function findExistingFile(candidate: string, extensions: string[]): string | undefined {
const candidateHasExt = hasExtension(candidate)
if (candidateHasExt && existsSync(candidate)) {
return candidate
}
if (!candidateHasExt) {
for (const ext of extensions) {
const withExt = `${candidate}${ext}`
if (existsSync(withExt)) {
return withExt
}
}
}
if (existsSync(candidate) && statSync(candidate).isDirectory()) {
for (const ext of extensions) {
const indexPath = path.join(candidate, `index${ext}`)
if (existsSync(indexPath)) {
return indexPath
}
}
}
return undefined
}

function hasExtension(filePath: string): boolean {
return Boolean(path.extname(filePath))
}
Loading
Loading