Alias System - Dual-Mode Path Resolution
This document explains the core innovation that makes @nextspark/core work both as a monorepo package and as an npm package.
The Problem
The package needs to import registries that are:
- Not bundled with the package (they're project-specific)
- Generated at install time in the consumer project
- Located differently depending on the mode
// This import exists in compiled package code
import { ENTITY_REGISTRY } from '@nextspark/registries/entity-registry'
// But the actual file is in:
// - Monorepo: packages/core/src/lib/registries/entity-registry.ts
// - NPM Mode: .nextspark/registries/entity-registry.ts
The Solution: External Imports + Alias Resolution
Step 1: Mark Registry Imports as External (tsup)
In tsup.config.ts:
export default defineConfig({
// Keep registry imports external - NOT bundled into dist/
external: [/^@nextspark\/registries\/.*/],
// ...
})
This means when the package is built, imports like:
import { ENTITY_REGISTRY } from '@nextspark/registries/entity-registry'
Are kept as-is in the compiled output, not resolved at build time.
Step 2: Package Source Uses @nextspark/registries/*
All 47 source files that need registries import from @nextspark/registries/*:
// In packages/core/src/lib/entities/EntityListWrapper.tsx
import { ENTITY_REGISTRY } from '@nextspark/registries/entity-registry'
import { PERMISSIONS_REGISTRY } from '@nextspark/registries/permissions-registry'
Step 3: Consumer Project Resolves the Alias
The consumer project configures THREE resolution layers:
A. TypeScript Resolution (tsconfig.json)
Generated automatically by the postinstall hook:
{
"compilerOptions": {
"paths": {
"@nextspark/registries/*": ["./.nextspark/registries/*"],
"@nextspark/core/*": ["./node_modules/@nextspark/core/dist/*"]
}
},
"include": [".nextspark/**/*.ts"]
}
B. Turbopack Resolution (next.config.ts)
For Next.js 15 with Turbopack (default in dev):
const nextConfig: NextConfig = {
turbopack: {
resolveAlias: {
'@nextspark/core/lib/registries/*': './.nextspark/registries/*',
'@nextspark/registries/*': './.nextspark/registries/*',
}
},
// ...
}
C. Webpack Resolution (next.config.ts)
For production builds or if using webpack:
const nextConfig: NextConfig = {
webpack: (config) => {
config.resolve.alias = {
...config.resolve.alias,
'@nextspark/core/lib/registries': path.resolve(__dirname, '.nextspark/registries'),
'@nextspark/registries': path.resolve(__dirname, '.nextspark/registries'),
}
return config
},
}
Resolution Flow
┌─────────────────────────────────────────────────────────────────────┐
│ Compiled Package Code (dist/) │
│ │
│ import { X } from '@nextspark/registries/entity-registry' │
│ │ │
└───────────────────────────┼─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Consumer Project Bundler │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐ │
│ │ Turbopack │ │ Webpack │ │ TypeScript │ │
│ │ resolveAlias │ │ resolve.alias │ │ paths │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬─────────┘ │
│ │ │ │ │
│ └──────────────┬──────┴──────────────────────┘ │
│ ▼ │
│ .nextspark/registries/entity-registry.ts │
│ │
└─────────────────────────────────────────────────────────────────────┘
Registry Generation
The registries in .nextspark/registries/ are generated by scripts/build/registry.mjs:
How Generation Detects Mode
// scripts/build/registry/config.mjs
export function getConfig(projectRoot = null) {
const root = projectRoot || detectProjectRoot()
// Check if running in npm mode
const isNpmMode = isInstalledAsPackage(root)
// Output location differs by mode
const outputDir = isNpmMode
? join(root, '.nextspark/registries') // NPM: project root
: join(root, 'packages/core/src/lib/registries') // Monorepo
return { projectRoot: root, isNpmMode, outputDir, ... }
}
Import Path Conversion
Generated registries use convertCorePath() for their imports:
// scripts/build/registry/config.mjs
export function convertCorePath(importPath, outputFilePath, config) {
// In monorepo mode, keep @/core/ aliases
if (!config.isNpmMode) {
return importPath // e.g., '@/core/lib/permissions/types'
}
// In NPM mode, convert to package import
if (importPath.startsWith('@/core/')) {
const relativePart = importPath.replace('@/core/', '')
return `@nextspark/core/${relativePart}` // e.g., '@nextspark/core/lib/permissions/types'
}
return importPath
}
Example generated registry:
// .nextspark/registries/permissions-registry.ts (NPM mode)
// Import from the package (resolved by bundler)
import { CORE_PERMISSIONS_CONFIG } from '@nextspark/core/lib/permissions/system'
// Import from consumer's theme (resolved by @/ alias)
import { PERMISSIONS_CONFIG_OVERRIDES } from '@/contents/themes/default/config/permissions.config'
TSConfig Auto-Generation
The update-tsconfig.mjs script automatically adds aliases in NPM mode:
// scripts/build/update-tsconfig.mjs
if (isNpmPackage) {
baseConfig.compilerOptions.paths = baseConfig.compilerOptions.paths || {}
// Registry alias
baseConfig.compilerOptions.paths['@nextspark/registries/*'] = ['./.nextspark/registries/*']
// Core package alias
baseConfig.compilerOptions.paths['@nextspark/core/*'] = ['./node_modules/@nextspark/core/dist/*']
// Include .nextspark folder
baseConfig.include.push('.nextspark/**/*.ts')
}
Comparison: Monorepo vs NPM Mode
| Aspect | Monorepo Mode | NPM Mode |
|---|---|---|
| Registry location | packages/core/src/lib/registries/ |
.nextspark/registries/ |
| @/core/ imports | Resolved via tsconfig | Converted to @nextspark/core/ |
| @/contents/ imports | Works directly | Works directly |
| Generated at | Build time | Postinstall |
| tsconfig.json | Manual | Auto-generated |
Debugging
Check if Alias is Working
Add to next.config.ts temporarily:
webpack: (config) => {
console.log('Aliases:', config.resolve.alias)
return config
}
Verify Registry Exists
ls -la .nextspark/registries/
Should show:
entity-registry.ts
template-registry.ts
template-registry.client.ts
permissions-registry.ts
plugin-registry.ts
theme-registry.ts
...
Check Import in Compiled Package
grep -r "@nextspark/registries" node_modules/@nextspark/core/dist/
Should show external imports preserved.
Common Issues
"Cannot find module '@nextspark/registries/...'"
-
Registries not generated:
node node_modules/@nextspark/core/scripts/build/registry.mjs -
Missing turbopack alias - check next.config.ts
-
tsconfig.json not updated - regenerate:
node node_modules/@nextspark/core/scripts/build/update-tsconfig.mjs
"Module '@nextspark/core/...' not found"
-
Package not built:
cd packages/core && pnpm build -
Missing transpilePackages:
transpilePackages: ['@nextspark/core']
Related
- 10-tsup-build.md - Build configuration
- 06-path-resolution.md - Path patterns
- 03-build-scripts.md - Registry generation