Skip to content

Commit

Permalink
feat: support loading esm and ts files from json config (#254)
Browse files Browse the repository at this point in the history
* feat: support loading esm and ts files from .jsonrc

* restore existing logic

* add types for lilconfig options

* simplify
  • Loading branch information
brc-dd committed Nov 30, 2023
1 parent 691cb42 commit 1928df0
Show file tree
Hide file tree
Showing 12 changed files with 145 additions and 62 deletions.
50 changes: 10 additions & 40 deletions src/index.js
@@ -1,13 +1,12 @@
// @ts-check
const { resolve } = require('node:path')
const { pathToFileURL } = require('node:url')

const config = require('lilconfig')
const yaml = require('yaml')

const loadOptions = require('./options.js')
const loadPlugins = require('./plugins.js')

const TS_EXT_RE = /\.(c|m)?ts$/
const req = require('./req.js')

const interopRequireDefault = obj =>
obj && obj.__esModule ? obj : { default: obj }
Expand All @@ -18,9 +17,9 @@ const interopRequireDefault = obj =>
* @param {Object} ctx Config Context
* @param {Object} result Cosmiconfig result
*
* @return {Object} PostCSS Config
* @return {Promise<Object>} PostCSS Config
*/
function processResult(ctx, result) {
async function processResult(ctx, result) {
let file = result.filepath || ''
let projectConfig = interopRequireDefault(result.config).default || {}

Expand All @@ -36,8 +35,8 @@ function processResult(ctx, result) {

let res = {
file,
options: loadOptions(projectConfig, file),
plugins: loadPlugins(projectConfig, file)
options: await loadOptions(projectConfig, file),
plugins: await loadPlugins(projectConfig, file)
}
delete projectConfig.plugins
return res
Expand Down Expand Up @@ -72,40 +71,11 @@ function createContext(ctx) {
return ctx
}

/** @type {import('jiti').JITI | null} */
let jiti = null

async function loader(filepath) {
try {
let module = await import(pathToFileURL(filepath).href)
return module.default
} catch (err) {
/* c8 ignore start */
if (!TS_EXT_RE.test(filepath)) {
throw err
}
if (!jiti) {
try {
jiti = (await import('jiti')).default(__filename, {
interopDefault: true
})
} catch (jitiErr) {
if (
jitiErr.code === 'ERR_MODULE_NOT_FOUND' &&
jitiErr.message.includes("Cannot find package 'jiti'")
) {
throw new Error(
`'jiti' is required for the TypeScript configuration files. Make sure it is installed\nError: ${jitiErr.message}`
)
}
throw jitiErr
}
/* c8 ignore stop */
}
return jiti(filepath)
}
return req(filepath)
}

/** @return {import('lilconfig').Options} */
const withLoaders = (options = {}) => {
let moduleName = 'postcss'

Expand All @@ -119,8 +89,8 @@ const withLoaders = (options = {}) => {
'.mjs': loader,
'.mts': loader,
'.ts': loader,
'.yaml': (filepath, content) => yaml.parse(content),
'.yml': (filepath, content) => yaml.parse(content)
'.yaml': (_, content) => yaml.parse(content),
'.yml': (_, content) => yaml.parse(content)
},
searchPlaces: [
...(options.searchPlaces || []),
Expand Down
11 changes: 6 additions & 5 deletions src/options.js
@@ -1,3 +1,4 @@
// @ts-check
const req = require('./req.js')

/**
Expand All @@ -8,12 +9,12 @@ const req = require('./req.js')
*
* @param {Object} config PostCSS Config
*
* @return {Object} options PostCSS Options
* @return {Promise<Object>} options PostCSS Options
*/
function options(config, file) {
async function options(config, file) {
if (config.parser && typeof config.parser === 'string') {
try {
config.parser = req(config.parser, file)
config.parser = await req(config.parser, file)
} catch (err) {
throw new Error(
`Loading PostCSS Parser failed: ${err.message}\n\n(@${file})`
Expand All @@ -23,7 +24,7 @@ function options(config, file) {

if (config.syntax && typeof config.syntax === 'string') {
try {
config.syntax = req(config.syntax, file)
config.syntax = await req(config.syntax, file)
} catch (err) {
throw new Error(
`Loading PostCSS Syntax failed: ${err.message}\n\n(@${file})`
Expand All @@ -33,7 +34,7 @@ function options(config, file) {

if (config.stringifier && typeof config.stringifier === 'string') {
try {
config.stringifier = req(config.stringifier, file)
config.stringifier = await req(config.stringifier, file)
} catch (err) {
throw new Error(
`Loading PostCSS Stringifier failed: ${err.message}\n\n(@${file})`
Expand Down
25 changes: 14 additions & 11 deletions src/plugins.js
@@ -1,3 +1,4 @@
// @ts-check
const req = require('./req.js')

/**
Expand All @@ -9,18 +10,19 @@ const req = require('./req.js')
* @param {String} plugin PostCSS Plugin Name
* @param {Object} options PostCSS Plugin Options
*
* @return {Function} PostCSS Plugin
* @return {Promise<Function>} PostCSS Plugin
*/
function load(plugin, options, file) {
async function load(plugin, options, file) {
try {
if (
options === null ||
options === undefined ||
Object.keys(options).length === 0
) {
return req(plugin, file)
return await req(plugin, file)
} else {
return req(plugin, file)(options)
return (await req(plugin, file))(options)
/* c8 ignore next */
}
} catch (err) {
throw new Error(
Expand All @@ -37,21 +39,22 @@ function load(plugin, options, file) {
*
* @param {Object} config PostCSS Config Plugins
*
* @return {Array} plugins PostCSS Plugins
* @return {Promise<Array>} plugins PostCSS Plugins
*/
function plugins(config, file) {
async function plugins(config, file) {
let list = []

if (Array.isArray(config.plugins)) {
list = config.plugins.filter(Boolean)
} else {
list = Object.keys(config.plugins)
.filter(plugin => {
return config.plugins[plugin] !== false ? plugin : ''
list = Object.entries(config.plugins)
.filter(([, options]) => {
return options !== false
})
.map(plugin => {
return load(plugin, config.plugins[plugin], file)
.map(([plugin, options]) => {
return load(plugin, options, file)
})
list = await Promise.all(list)
}

if (list.length && list.length > 0) {
Expand Down
41 changes: 35 additions & 6 deletions src/req.js
@@ -1,10 +1,39 @@
// eslint-disable-next-line n/no-deprecated-api
const { createRequire, createRequireFromPath } = require('node:module')
// @ts-check
const { createRequire } = require('node:module')

function req (name, rootFile) {
let create = createRequire || createRequireFromPath
let require = create(rootFile)
return require(name)
const TS_EXT_RE = /\.(c|m)?ts$/

/** @type {import('jiti').default | null} */
let jiti = null

/**
* @param {string} name
* @param {string} rootFile
* @returns {Promise<any>}
*/
async function req(name, rootFile = __filename) {
let __require = createRequire(rootFile)
let url = __require.resolve(name)

try {
return (await import(url)).default
} catch (err) {
if (!TS_EXT_RE.test(url)) {
/* c8 ignore start */
throw err
}
if (!jiti) {
try {
jiti = (await import('jiti')).default
} catch (jitiErr) {
throw new Error(
`'jiti' is required for the TypeScript configuration files. Make sure it is installed\nError: ${jitiErr.message}`
)
}
/* c8 ignore stop */
}
return jiti(rootFile, { interopDefault: true })(name)
}
}

module.exports = req
17 changes: 17 additions & 0 deletions test/ts.test.js
Expand Up @@ -150,3 +150,20 @@ describe('Array', () => {
equal(config.file, resolve(expectedPath))
}
})

describe('JSON', () => {
test('Load Config - postcss.config.mts', async () => {
let { plugins } = await postcssrc(ctx, 'test/ts/json')
equal(plugins.length, 6)
plugins.forEach((plugin, index) => {
equal(
// normalize for simplicity
plugin.path
.replace(/^.*?:\/\//, '')
.replace(/\\/g, '/')
.replace(/\.[^.]+$/, ''),
resolve(process.cwd(), `test/ts/json/postcss/${index + 1}`)
)
})
})
})
10 changes: 10 additions & 0 deletions test/ts/json/.postcssrc.json
@@ -0,0 +1,10 @@
{
"plugins": {
"./postcss/1.js": {},
"./postcss/2.cjs": {},
"./postcss/3.mjs": {},
"./postcss/4.ts": {},
"./postcss/5.cts": {},
"./postcss/6.mts": {}
}
}
8 changes: 8 additions & 0 deletions test/ts/json/postcss/1.js
@@ -0,0 +1,8 @@
module.exports = function plugin() {
return {
postcssPlugin: '1'
}
}

module.exports.postcss = true
module.exports.path = __filename
8 changes: 8 additions & 0 deletions test/ts/json/postcss/2.cjs
@@ -0,0 +1,8 @@
module.exports = function plugin() {
return {
postcssPlugin: '2'
}
}

module.exports.postcss = true
module.exports.path = __filename
9 changes: 9 additions & 0 deletions test/ts/json/postcss/3.mjs
@@ -0,0 +1,9 @@
let plugin = () => {
return {
postcssPlugin: '3'
}
}
plugin.postcss = true
plugin.path = import.meta.url

export default plugin
9 changes: 9 additions & 0 deletions test/ts/json/postcss/4.ts
@@ -0,0 +1,9 @@
let plugin: any = () => {
return {
postcssPlugin: '4'
}
}
plugin.postcss = true
plugin.path = __filename

export = plugin
9 changes: 9 additions & 0 deletions test/ts/json/postcss/5.cts
@@ -0,0 +1,9 @@
let plugin: any = () => {
return {
postcssPlugin: '5'
}
}
plugin.postcss = true
plugin.path = __filename

export = plugin
10 changes: 10 additions & 0 deletions test/ts/json/postcss/6.mts
@@ -0,0 +1,10 @@
let plugin: any = () => {
return {
postcssPlugin: '6'
}
}
plugin.postcss = true
// @ts-ignore
plugin.path = import.meta.url

export default plugin

0 comments on commit 1928df0

Please sign in to comment.