Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support postcss.config.ts #218

Merged
merged 7 commits into from
Jun 14, 2021
Merged
Show file tree
Hide file tree
Changes from 4 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
9 changes: 9 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict'

module.exports = {
testEnvironment: 'node',
transform: {
'\\.[j]sx?$': 'babel-jest',
'\\.ts': './test/ts-node-transformer.js'
}
}
14 changes: 13 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"import-cwd": "^3.0.0"
},
"devDependencies": {
"@types/cssnano": "^4.0.0",
"@types/postcss-import": "^12.0.0",
"clean-publish": "^1.1.8",
"cssnano": "^4.0.0",
"jest": "^26.4.2",
Expand All @@ -34,7 +36,17 @@
"postcss-import": "^12.0.0",
"postcss-nested": "^5.0.0",
"standard": "^14.3.4",
"sugarss": "^3.0.0"
"sugarss": "^3.0.0",
"ts-node": "^9.0.0",
"typescript": "4.3.2"
},
"peerDependencies": {
"ts-node": ">=9.0.0"
},
"peerDependenciesMeta": {
"ts-node": {
"optional": true
}
},
"keywords": [
"postcss",
Expand Down
72 changes: 49 additions & 23 deletions src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,71 @@
// based on @types/postcss-load-config@2.0.1
// Type definitions for postcss-load-config 2.1
import Processor from 'postcss/lib/processor'
import { Plugin, ProcessOptions, Transformer } from "postcss";
import Processor from 'postcss/lib/processor';
import { Plugin, ProcessOptions, Transformer } from 'postcss';
import { Options as CosmiconfigOptions } from 'cosmiconfig';

// In the ConfigContext, these three options can be instances of the
// appropriate class, or strings. If they are strings, postcss-load-config will
// require() them and pass the instances along.
interface ProcessOptionsPreload {
declare function postcssrc(
ctx?: postcssrc.ConfigContext,
path?: string,
options?: CosmiconfigOptions
): Promise<postcssrc.Result>;

declare namespace postcssrc {
function sync(
ctx?: ConfigContext,
path?: string,
options?: CosmiconfigOptions
): Result;

// In the ConfigContext, these three options can be instances of the
// appropriate class, or strings. If they are strings, postcss-load-config will
// require() them and pass the instances along.
export interface ProcessOptionsPreload {
parser?: string | ProcessOptions['parser'];
stringifier?: string | ProcessOptions['stringifier'];
syntax?: string | ProcessOptions['syntax'];
}
}

// The remaining ProcessOptions, sans the three above.
type RemainingProcessOptions =
Pick<ProcessOptions, Exclude<keyof ProcessOptions, keyof ProcessOptionsPreload>>;
// The remaining ProcessOptions, sans the three above.
export type RemainingProcessOptions = Pick<
ProcessOptions,
Exclude<keyof ProcessOptions, keyof ProcessOptionsPreload>
>;

// Additional context options that postcss-load-config understands.
interface Context {
// Additional context options that postcss-load-config understands.
export interface Context {
cwd?: string;
env?: string;
}
}

// The full shape of the ConfigContext.
type ConfigContext = Context & ProcessOptionsPreload & RemainingProcessOptions;
// The full shape of the ConfigContext.
export type ConfigContext = Context &
ProcessOptionsPreload &
RemainingProcessOptions;

// Result of postcssrc is a Promise containing the filename plus the options
// and plugins that are ready to pass on to postcss.
type ResultPlugin = Plugin | Transformer | Processor;
// Result of postcssrc is a Promise containing the filename plus the options
// and plugins that are ready to pass on to postcss.
export type ResultPlugin = Plugin | Transformer | Processor;

interface Result {
export interface Result {
file: string;
options: ProcessOptions;
plugins: ResultPlugin[];
}
}

declare function postcssrc(ctx?: ConfigContext, path?: string, options?: CosmiconfigOptions): Promise<Result>;
export type ConfigPlugin = Transformer | Plugin | Processor;

declare namespace postcssrc {
function sync(ctx?: ConfigContext, path?: string, options?: CosmiconfigOptions): Result;
export interface Config {
parser?: string | ProcessOptions['parser'] | false;
stringifier?: string | ProcessOptions['stringifier'] | false;
syntax?: string | ProcessOptions['syntax'] | false;
map?: string | false;
from?: string;
to?: string;
plugins?: Array<ConfigPlugin | false> | Record<string, object | false>;
}

export type ConfigFn = (ctx: ConfigContext) => Config | Promise<Config>;
}

export = postcssrc;
75 changes: 70 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ const config = require('cosmiconfig')
const loadOptions = require('./options.js')
const loadPlugins = require('./plugins.js')

/* istanbul ignore next */
const interopRequireDefault = (obj) => obj && obj.__esModule ? obj : { default: obj }

/**
* Process the result from cosmiconfig
*
Expand All @@ -17,7 +20,7 @@ const loadPlugins = require('./plugins.js')
*/
const processResult = (ctx, result) => {
const file = result.filepath || ''
let config = result.config || {}
let config = interopRequireDefault(result.config).default || {}

if (typeof config === 'function') {
config = config(ctx)
Expand Down Expand Up @@ -62,6 +65,68 @@ const createContext = (ctx) => {
return ctx
}

const addTypeScriptLoader = (options = {}, loader) => {
const moduleName = 'postcss'

return {
...options,
searchPlaces: [
...(options.searchPlaces || []),
'package.json',
`.${moduleName}rc`,
`.${moduleName}rc.json`,
`.${moduleName}rc.yaml`,
`.${moduleName}rc.yml`,
`.${moduleName}rc.ts`,
`.${moduleName}rc.js`,
G-Rath marked this conversation as resolved.
Show resolved Hide resolved
`${moduleName}.config.ts`,
`${moduleName}.config.js`
G-Rath marked this conversation as resolved.
Show resolved Hide resolved
],
loaders: {
...options.loaders,
'.ts': loader
}
}
}

const withTypeScriptLoader = (rcFunc) => {
let registerer

// Register TypeScript compiler instance
try {
registerer = require('ts-node').register()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we call require('ts-node') only if .ts file as found?

I do not want to load heavy TypeScript for the projects with .js configs, but with ts-node in the system.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll have a look, but this doesn't actually load the heavy side of the TypeScript compiler until it's actually needed, making it a very cheap call.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I right that we are calling require('ts-node').register() on any config loading?

(I may be wrong, I am not sure)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes; like I said the require cost is negligible since it doesn't actually do any work (including creating the TypeScript compiler, and compiling code, which are the expensive operations) until a .ts file is required.

However I think we might be able to move it into the loader function.

} catch (err) {
registerer = err
registerer.enabled = () => {}
}

registerer.enabled(true)

const loader = registerer instanceof Error
? () => {
if (registerer.code === 'MODULE_NOT_FOUND') {
throw new Error(
`'ts-node' is required for the TypeScript configuration files. Make sure it is installed\nError: ${registerer.message}`
)
}

throw registerer
}
: require

return (ctx, path, options) => {
const configObject = rcFunc(ctx, path, addTypeScriptLoader(options, loader))

if (configObject instanceof Promise) {
return configObject.finally(() => registerer.enabled(false))
}

registerer.enabled(false)

return configObject
}
}

/**
* Load Config
*
Expand All @@ -73,7 +138,7 @@ const createContext = (ctx) => {
*
* @return {Promise} config PostCSS Config
*/
const rc = (ctx, path, options) => {
const rc = withTypeScriptLoader((ctx, path, options) => {
/**
* @type {Object} The full Config Context
*/
Expand All @@ -93,9 +158,9 @@ const rc = (ctx, path, options) => {

return processResult(ctx, result)
})
}
})

rc.sync = (ctx, path, options) => {
rc.sync = withTypeScriptLoader((ctx, path, options) => {
/**
* @type {Object} The full Config Context
*/
Expand All @@ -113,7 +178,7 @@ rc.sync = (ctx, path, options) => {
}

return processResult(ctx, result)
}
})

/**
* Autoload Config for PostCSS
Expand Down
9 changes: 9 additions & 0 deletions test/ts-node-transformer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict'

const { create: createTypeScriptCompiler } = require('ts-node')

const tsCompiler = createTypeScriptCompiler()

module.exports = {
process: tsCompiler.compile.bind(tsCompiler)
}
138 changes: 138 additions & 0 deletions test/ts.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
'use strict'

const path = require('path')

const postcss = require('postcss')
const postcssrc = require('../src/index.js')

const { fixture, expected } = require('./utils.js')

// beforeEach(() => {
G-Rath marked this conversation as resolved.
Show resolved Hide resolved
// jest.mockModule
// })

describe('postcss.config.ts - {Object} - Load Config', () => {
const ctx = {
parser: true,
syntax: true
}

const expected = (config) => {
expect(config.options.parser).toEqual(require('sugarss'))
expect(config.options.syntax).toEqual(require('sugarss'))
expect(config.options.map).toEqual(false)
expect(config.options.from).toEqual('./test/ts/object/fixtures/index.css')
expect(config.options.to).toEqual('./test/ts/object/expect/index.css')

expect(config.plugins.length).toEqual(2)
expect(typeof config.plugins[0]).toBe('function')
expect(typeof config.plugins[1]).toBe('function')

expect(config.file)
.toEqual(path.resolve('test/ts/object', 'postcss.config.ts'))
}

test('Async', () => {
return postcssrc(ctx, 'test/ts/object').then(expected)
})

test('Sync', () => {
const config = postcssrc.sync(ctx, 'test/ts/object')

expected(config)
})
})

test('postcss.config.ts - {Object} - Process CSS', () => {
const ctx = {
parser: false,
syntax: false
}

return postcssrc(ctx, 'test/ts/object').then((config) => {
return postcss(config.plugins)
.process(fixture('ts/object', 'index.css'), config.options)
.then((result) => {
expect(result.css).toEqual(expected('ts/object', 'index.css'))
})
})
})

test('postcss.config.ts - {Object} - Process SSS', () => {
const ctx = {
from: './test/ts/object/fixtures/index.sss',
parser: true,
syntax: false
}

return postcssrc(ctx, 'test/ts/object').then((config) => {
return postcss(config.plugins)
.process(fixture('ts/object', 'index.sss'), config.options)
.then((result) => {
expect(result.css).toEqual(expected('ts/object', 'index.sss'))
})
})
})

describe('postcss.config.ts - {Array} - Load Config', () => {
const ctx = {
parser: true,
syntax: true
}

const expected = (config) => {
expect(config.options.parser).toEqual(require('sugarss'))
expect(config.options.syntax).toEqual(require('sugarss'))
expect(config.options.map).toEqual(false)
expect(config.options.from).toEqual('./test/ts/array/fixtures/index.css')
expect(config.options.to).toEqual('./test/ts/array/expect/index.css')

expect(config.plugins.length).toEqual(2)
expect(typeof config.plugins[0]).toBe('function')
expect(typeof config.plugins[1]).toBe('object')

expect(config.file)
.toEqual(path.resolve('test/ts/array', 'postcss.config.ts'))
}

test('Async', () => {
return postcssrc(ctx, 'test/ts/array').then(expected)
})

test('Sync', () => {
const config = postcssrc.sync(ctx, 'test/ts/array')

expected(config)
})
})

test('postcss.config.ts - {Array} - Process CSS', () => {
const ctx = {
parser: false,
syntax: false
}

return postcssrc(ctx, 'test/ts/array').then((config) => {
return postcss(config.plugins)
.process(fixture('ts/array', 'index.css'), config.options)
.then((result) => {
expect(result.css).toEqual(expected('ts/array', 'index.css'))
})
})
})

test('postcss.config.ts - {Array} - Process SSS', () => {
const ctx = {
from: './test/ts/array/fixtures/index.sss',
parser: true,
syntax: false
}

return postcssrc(ctx, 'test/ts/array').then((config) => {
return postcss(config.plugins)
.process(fixture('ts/array', 'index.sss'), config.options)
.then((result) => {
expect(result.css).toEqual(expected('ts/array', 'index.sss'))
})
})
})
Loading