@growing-web/esm-pack-core
Version:
esm pack build core.
621 lines (537 loc) • 16.2 kB
text/typescript
import type { PackageJson } from 'pkg-types'
import { readPackageJSON } from 'pkg-types'
import fg from 'fast-glob'
import _ from 'lodash'
import path from 'path'
import fs from 'fs-extra'
import { fileResolveByExtension, fileReader } from './utils'
import { parse as cjsParse } from 'cjs-esm-exports'
import { init as esInit, parse as esParse } from 'es-module-lexer'
import { FILE_EXCLUDES, FILE_EXTENSIONS, FILES_IGNORE } from './constants'
import { recursionExportsValues } from './recursion'
type Recordable = Record<string, any>
const DEFAULT_ENTRY = 'index.js'
const IGNORE_KEYS = ['default', 'types', 'typings', 'development', 'browser']
const JS_RE = /\.[m]?js$/
export async function resolvePackage(cachePath: string) {
const pkg = await readPackageJSON(cachePath)
// @ts-ignore
if (pkg.__ESMD__ === true) {
return pkg
}
const pkgExports = await resolveExports(pkg, cachePath)
const files = await resolveFiles(pkg, cachePath, pkgExports)
pkg.exports = pkgExports
pkg.files = files
// pkg.imports
Object.assign(pkg, await resolveImports(pkg))
// @ts-ignore
pkg.__ESMD__ = true
return pkg
}
export async function resolveImports(pkg: Recordable) {
const { browser } = pkg
if (!browser || !_.isObject(browser)) {
return {}
}
const pkgBrowser: any = browser
const resultImport = pkgBrowser?.import ?? {}
for (const [key, value] of Object.entries(pkgBrowser)) {
const importKey = `#${key.replace(/^((\.+)?\/?)/, '')}`
if (resultImport[importKey]) {
continue
}
Object.assign(resultImport, {
[importKey]: {
browser: normalizeExport(value as string),
default: normalizeExport(key),
},
})
}
return { imports: resultImport }
}
export async function resolveExports(pkg: PackageJson, root: string) {
let {
exports: pkgExports,
module: pkgModule,
main: pkgMain,
browser: pkgBrowser,
files: pkgFiles = [],
} = pkg as any
const cjsMainFiles: string[] = []
const unStandard = !pkgExports && !pkgMain && !pkgModule
// exports exits
if (pkgExports !== undefined && pkgExports !== null) {
// export include {"./*":"./*"}
if (_.isObject(pkgExports)) {
if (pkgExports['./*'] === './*') {
const result = await addCjsFiledToExports({
root,
pkgExports,
cjsMainFiles,
})
return await developmentifyExports(result, root)
}
pkgExports['./package.json'] = './package.json.js'
}
if (_.isString(pkgExports)) {
pkgExports = { '.': pkgExports }
}
for (const [key, pattern] of Object.entries(pkgExports)) {
if (
(key.includes('*') && _.isString(pattern) && pattern.includes('*')) ||
(key.endsWith('/') && _.isString(pattern) && pattern.endsWith('/'))
) {
await addMatchFileToExports(pattern, pkgExports, root)
continue
}
if (_.isObject(pattern)) {
await handleObjectPattern({
root,
pkgExports,
key,
pattern: pattern as Recordable,
})
}
}
const result = await addCjsFiledToExports({
root,
pkgExports,
cjsMainFiles,
})
return await developmentifyExports(result, root)
} else if (!pkgExports) {
// const cjsManiFiles: string = []
// no exports exist
const resultExports: Recordable = {}
// module and main do not exist
if (!pkgModule && !pkgMain) {
const normalizePkgMain = normalizeExport(guessEntryFile(root, pkgFiles))
const addonCjsMainFiles = await createCjsMainFiles(root, normalizePkgMain)
cjsMainFiles.push(...addonCjsMainFiles)
Object.assign(resultExports, {
'.': normalizePkgMain,
})
}
// module and main is exits
else if (pkgModule && pkgMain) {
const normalizePkgMain = normalizeExport(pkgMain)
const normalizepkgModule = normalizeExport(pkgModule)
const addonCjsMainFiles = await createCjsMainFiles(root, normalizePkgMain)
cjsMainFiles.push(...addonCjsMainFiles)
Object.assign(resultExports, {
'.': await resolveMainAndModule(
root,
normalizePkgMain,
normalizepkgModule,
pkgBrowser,
),
})
if (!pkgBrowser) {
Object.assign(resultExports, await joinFilesToExports(root, pkgFiles))
}
resultExports['./package.json'] = './package.json.js'
Object.assign(resultExports, {
[normalizePkgMain]: await resolveMain(root, normalizePkgMain),
[normalizepkgModule]: normalizepkgModule,
})
} else if (!pkgModule && pkgMain) {
const normalizePkgMain = normalizeExport(pkgMain)
const addonCjsMainFiles = await createCjsMainFiles(root, normalizePkgMain)
cjsMainFiles.push(...addonCjsMainFiles)
Object.assign(resultExports, {
'.': await resolveMain(root, normalizePkgMain),
[normalizePkgMain]: await resolveMain(root, normalizePkgMain),
})
resultExports['./package.json'] = './package.json.js'
Object.assign(resultExports, await joinFilesToExports(root, pkgFiles))
}
pkgExports = resultExports
}
if (unStandard) {
pkgExports['./package.json'] = './package.json.js'
}
let result = await addCjsFiledToExports({ root, pkgExports, cjsMainFiles })
result = await handlerPkgBrowser(pkgBrowser as any, result)
return await developmentifyExports(result, root)
}
export async function resolveFiles(
pkg: PackageJson,
root: string,
pkgExports: Record<string, any>,
) {
let { files = [] } = pkg
files = []
// add package.json、package.json.js to files
files.push(...['', '.js'].map((item) => `package.json${item}`))
// exports file
const values = recursionExportsValues(pkgExports)
files.push(
...values.map((item) => {
return item.replace(/^\.?\//, '')
}),
)
// Add .map to js file
files.forEach((item) => {
if (/\.[m|c]?js$/.test(item) && !item.includes('*')) {
files.push(`${item}.map`)
}
})
// add license and readme.md to files
const matchFiles = await fg(['license', 'readme.md', 'changelog.md'], {
cwd: root,
caseSensitiveMatch: false,
})
const matchTsFiles = await fg('./*.d.ts', {
cwd: root,
caseSensitiveMatch: false,
})
files.push(...matchFiles, ...matchTsFiles)
files = files.filter((item) => !['*', '.', './', './*'].includes(item))
return Array.from(new Set(files)).sort()
}
async function developmentifyExports(pkgExports: Recordable, root: string) {
for (const [key, value] of Object.entries(pkgExports)) {
const ext = path.extname(key)
if (
!ext ||
key.endsWith('.prod.cjs') ||
!['.js', '.cjs'].includes(ext) ||
!_.isString(value) ||
key === './index.js'
) {
continue
}
const filepath = path.join(root, key)
if (!fs.existsSync(filepath)) {
continue
}
const { isEsm } = await getFileType(root, key)
if (isEsm) {
continue
}
const content = fileReader(filepath)
const dynamicEntry = await isDynamicEntry(content)
if (dynamicEntry) {
const devKeys = await resolveDevFilename(key)
const itemExports = {
default: key,
development: devKeys,
}
pkgExports[key] = itemExports
const unExtKey = key.substring(0, key.length - ext.length)
if (pkgExports[unExtKey]) {
pkgExports[unExtKey] = itemExports
}
}
}
return pkgExports
}
async function joinFilesToExports(root: string, pkgFiles: string[]) {
const resultExports: Recordable = {}
const getFiles = await findPkgFiles(root, pkgFiles)
getFiles.forEach((file) => {
const fileKey = normalizeExport(file)
if (!fileKey.endsWith('.cjs.js')) {
resultExports[fileKey] = fileKey
if (JS_RE.test(fileKey)) {
resultExports[removeFileExt(fileKey)] = fileKey
}
}
})
return resultExports
}
async function handlerPkgBrowser(
pkgBrowser: Recordable | undefined,
pkgExports: Recordable,
) {
if (!pkgBrowser || _.isString(pkgBrowser)) {
return pkgExports
}
const resultExports = pkgExports
for (const [key, value] of Object.entries(pkgBrowser)) {
if (!resultExports[key] || _.isString(resultExports[key])) {
resultExports[removeFileExt(key)] = resultExports[key] = {
default: normalizeExport(key),
browser: normalizeExport(value),
}
}
}
return resultExports
}
async function createCjsMainFiles(root: string, pkgMain: string) {
const { isCjs } = await getFileType(root, pkgMain)
// const isDynamic = await getIsDynamic(root, pkgMain)
const cjsMainFiles: string[] = []
if (isCjs) {
cjsMainFiles.push(...[pkgMain])
}
return cjsMainFiles
}
async function findPkgFiles(root: string, pkgFiles: string[] = []) {
const pattern =
pkgFiles.length === 0
? '**/**'
: pkgFiles.map((item) => {
if (item.includes('*') || path.extname(item)) {
return item
} else {
const filepath = path.join(root, item)
if (fs.existsSync(filepath) && fs.statSync(filepath).isFile()) {
return item
}
return `${item}${item.endsWith('/') ? '' : '/'}**/**.js`
}
})
const files = await fg(pattern, { cwd: root, ignore: FILES_IGNORE })
const pkg = await readPackageJSON(root)
return files.filter((item) => {
const ext = path.extname(item)
if (!ext || ext === '.ts' || excludeFiles(pkg, item)) {
return false
}
return FILE_EXTENSIONS.includes(ext)
})
}
function excludeFiles(pkg: Recordable, file: string) {
return (
// test files
FILE_EXCLUDES.some((item) => file.includes(item)) ||
// min files
file.endsWith('.min.js') ||
file.endsWith('.prod.js') ||
// dev files
file.endsWith('.development.js') ||
file.endsWith('.dev.js') ||
// global files
file.endsWith('.global.js')
)
}
// function filterFiles() {}
async function handleObjectPattern({
root,
pkgExports,
key,
pattern,
}: {
root: string
pkgExports: Recordable
key: string
pattern: Recordable
}) {
for (const [pkey, pval] of Object.entries(pattern)) {
if (IGNORE_KEYS.includes(pkey)) {
continue
}
if (_.isString(pval)) {
if (
pattern.import &&
pattern.require &&
_.isString(pattern.import) &&
_.isString(pattern.require) &&
pattern.require !== pattern.import
) {
continue
}
pkgExports[key][pkey] = await resolveMain(root, pval)
} else if (_.isObject(pval)) {
for (const [ik, iv] of Object.entries(pval)) {
if (IGNORE_KEYS.includes(ik)) {
continue
}
if (_.isString(iv)) {
pkgExports[key][pkey][ik] = await resolveMain(root, iv)
}
}
}
}
}
async function addMatchFileToExports(
pattern: string,
pkgExports: Recordable,
cwd: string,
) {
const files = fg.sync(pattern.endsWith('/') ? `${pattern}*` : pattern, {
cwd,
})
for (const key of files) {
pkgExports[key] = normalizeExport(key)
}
}
async function addCjsFiledToExports({
root,
pkgExports,
cjsMainFiles,
}: {
root: string
pkgExports: Recordable
cjsMainFiles: string[]
}) {
for (const [key, value] of Object.entries(pkgExports)) {
// {require:'./index'}
if (_.isObject(value)) {
const requireFile = (value as any).require
if (requireFile) {
if (_.isString(requireFile)) {
pkgExports[`${requireFile}!cjs`] = requireFile
} else if (_.isObject(requireFile)) {
for (const rval of Object.values(requireFile)) {
pkgExports[`${rval}!cjs`] = rval
}
}
}
} else {
try {
if (!key.includes('*') && !key.startsWith('.ts')) {
if (JS_RE.test(key)) {
const { isCjs } = await getFileType(root, key)
if (isCjs) {
pkgExports[`${key}!cjs`] = key
}
} else if (key.endsWith('package.json')) {
pkgExports[`${value}!cjs`] = value
}
}
} catch (error) {
// No error will be reported for files that cannot be obtained
}
}
}
for (const cjsMainFile of cjsMainFiles) {
pkgExports[`${cjsMainFile}!cjs`] = cjsMainFile
}
return pkgExports
}
// For packages that do not specify any identity, you need to try to guess his entry
function guessEntryFile(root: string, pkgFiles: string[]) {
let indexFile = pkgFiles.find((file) => path.basename(file) === 'index')
if (indexFile) {
indexFile = path.basename(indexFile)
} else {
indexFile = 'index'
}
const entryFile = fileResolveByExtension(path.join(root || '', indexFile))
if (entryFile) {
return path.relative(root, entryFile)
}
return DEFAULT_ENTRY
}
async function resolveMainAndModule(
root: string,
pkgMain: string,
pkgModule: string,
pkgBrowser: any,
) {
const result = {
module: normalizeExport(pkgModule),
default: await resolveMain(root, pkgMain),
}
if (_.isString(pkgBrowser)) {
Object.assign(result, {
browser: normalizeExport(pkgBrowser),
})
}
return result
}
async function resolveMain(root: string, pkgMain: string) {
const normalizeMain = normalizeExport(pkgMain)
const isDynamicEntry = await getIsDynamic(root, pkgMain)
const { isUmd } = await getFileType(root, pkgMain)
if (isUmd || pkgMain.endsWith('.mjs') || !isDynamicEntry) {
return normalizeMain
}
return {
// TODO dev file
// development: normalizeMain,
development: !isDynamicEntry
? normalizeMain
: // "./index.js" => "./dev.index.js"
await resolveDevFilename(normalizeMain),
default: normalizeMain,
}
}
export async function resolveDevFilename(filename: string) {
// "./index.js" => "./dev.index.js"
const basename = path.basename(filename)
const newBaseName = basename.replace(
/^(\.?\/?)*/,
(m, $1) => `${$1 || ''}dev.`,
)
return filename.replace(basename, newBaseName)
}
async function getIsDynamic(root: string, filepath: string) {
const mainContent = fileReader(path.join(root, filepath))
let isDynamic = false
if (JS_RE.test(filepath)) {
isDynamic = await isDynamicEntry(mainContent)
}
return isDynamic
}
export async function isDynamicEntry(source: string) {
try {
// const { exports: devExports, reexports: devReexports } = cjsParse(
// filename,
// source,
// 'development',
// )
// const { exports: prodExports, reexports: prodReexports } = cjsParse(
// filename,
// source,
// 'production',
// )
// return devReexports?.length !== 0 && prodReexports?.length !== 0
// process.env.NODE_ENV === 'production'
return (
source.replace(/\s*/g, '').includes(`process.env.NODE_ENV==`) ||
source.replace(/\s*/g, '').includes(`process.env.NODE_ENV!=`)
)
} catch (error) {
return false
}
}
async function getFileType(root: string, filepath: string) {
if (!JS_RE.test(filepath)) {
return { isCjs: false, isEsm: false, isUmd: false }
}
const pkg = await readPackageJSON(root)
await esInit
const source = fileReader(path.join(root, filepath))
const [importer, exporter] = esParse(source)
const { reexports } = cjsParse(filepath, source, 'production')
const isCjs =
pkg.type !== 'module' &&
importer.length === 0 &&
exporter.length === 0 &&
!filepath.endsWith('.mjs')
if (isCjs) {
const isUmd = !exporter.length && !importer.length && reexports.length === 0
return {
isCjs: true,
isEsm: false,
isUmd,
}
}
const isEsm =
importer.length !== 0 ||
exporter.length !== 0 ||
pkg.type === 'module' ||
filepath.endsWith('.mjs')
return {
isCjs,
isEsm,
isUmd: isCjs && exporter.length === 0,
}
}
export function normalizeExport(str: string) {
if (str?.startsWith('./')) {
return str
}
if (str?.startsWith('/')) {
return `.${str}`
}
return `./${str}`
}
export function removeFileExt(file: string) {
const lastIndex = file.lastIndexOf(path.extname(file))
return file.substring(0, lastIndex)
}