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 packages/postcss-tape/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,10 @@ export default function runner(currentPlugin: PluginCreator<unknown>) {
let resultFilePath = `${testFilePathWithoutExtension}.result.css`;

if (testCaseOptions.expect) {
expectFilePath = expectFilePath.replace(testCaseLabel.replace(/:/g, '.') + '.expect.css', testCaseOptions.expect);
expectFilePath = path.join('.', 'test', testCaseOptions.expect);
}
if (testCaseOptions.result) {
resultFilePath = resultFilePath.replace(testCaseLabel.replace(/:/g, '.') + '.result.css', testCaseOptions.result);
resultFilePath = path.join('.', 'test', testCaseOptions.result);
}

const plugins = testCaseOptions.plugins ?? [currentPlugin(testCaseOptions.options)];
Expand Down
27 changes: 25 additions & 2 deletions plugin-packs/postcss-preset-env/.tape.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,30 @@ postcssTape(plugin)({
stage: 0
}
},
'basic:vendors-1': {
message: 'supports { minimumVendorImplementations: 1, enableClientSidePolyfills: false } usage',
options: {
stage: 1,
minimumVendorImplementations: 1,
enableClientSidePolyfills: false
}
},
'basic:vendors-2': {
message: 'supports { minimumVendorImplementations: 2, enableClientSidePolyfills: false } usage',
options: {
stage: 1,
minimumVendorImplementations: 2,
enableClientSidePolyfills: false
}
},
'basic:vendors-3': {
message: 'supports { minimumVendorImplementations: 3, enableClientSidePolyfills: false } usage',
options: {
stage: 1,
minimumVendorImplementations: 3,
enableClientSidePolyfills: false
}
},
'basic:nesting:true': {
message: 'supports { stage: false, features: { "nesting-rules": true } } usage',
options: {
Expand Down Expand Up @@ -279,7 +303,7 @@ postcssTape(plugin)({
}
}
},
'insert:after:match-result:exec': {
'insert:after:match-result:no-array': {
message: 'supports { insertAfter with a single plugin, not an array } usage when looking for a result',
options: {
stage: 0,
Expand All @@ -292,7 +316,6 @@ postcssTape(plugin)({
})
}
},
expect: 'insert.after.match-result.expect.css'
},
'import': {
message: 'supports { importFrom: { customMedia, customProperties, customSelectors, environmentVariables } } usage',
Expand Down
1 change: 1 addition & 0 deletions plugin-packs/postcss-preset-env/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- Added `@csstools/postcss-font-format-keywords` <br/> [Check the plugin README](https://github.com/csstools/postcss-plugins/tree/main/plugins/postcss-font-format-keywords#readme) for usage details.
- Added `debug` [option](https://github.com/csstools/postcss-plugins/tree/main/plugin-packs/postcss-preset-env#debug) that enables extra debugging information while processing the CSS.
- Added `enableClientSidePolyfills` [option](https://github.com/csstools/postcss-plugins/tree/main/plugin-packs/postcss-preset-env#enableclientsidepolyfills) that allows you to control every single plugin that requires a browser library to fully work. Defaults to `true` so they're enabled by default.
- Added `minimumVendorImplementations` [option](https://github.com/csstools/postcss-plugins/tree/main/plugin-packs/postcss-preset-env#minimumvendorimplementations) that allows you to enable/disable plugins based on their implementation status in browsers.
- Fix sourcemaps for `image-set()` function.
- Updated `cssdb` to `6.0.2` (major).
- Updated `postcss-custom-properties` to `12.1.3` (patch).
Expand Down
41 changes: 38 additions & 3 deletions plugin-packs/postcss-preset-env/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,30 @@ The `stage` can be `0` (experimental) through `4` (stable), or `false`. Setting
`stage` to `false` will disable every polyfill. Doing this would only be useful
if you intended to exclusively use the [`features`](#features) option.

Without any configuration options, [PostCSS Preset Env] enables **Stage 2**
features.
Default: `2`

### minimumVendorImplementations

The `minimumVendorImplementations` option determines which CSS features to polyfill, based their implementation status.
This can be used to enable plugins that are available in browsers regardless of the [spec status](#stage).

```js
postcssPresetEnv({ minimumVendorImplementations: 2 })
```

`minimumVendorImplementations` can be `0` (no vendor has implemented it) through `3` (all major vendors).<br>

Default: `0`

**Note:**

When a feature has not yet been implemented by any vendor it can be considered experimental.<br>
Even with a single implementation it might still change in the future.<br>
Sometimes issues with a feature/specification are only discovered after it becomes available.

A value of `2` is recommended when you want to use only those features that should be [stable](#stability-and-portability).

Having 2 independent implementations is [a critical step in proposals becoming standards](https://www.w3.org/2021/Process-20211102/#implementation-experience) and a good indicator of a feature's stability.

### features

Expand Down Expand Up @@ -192,7 +214,7 @@ configuration is not available.
postcssPresetEnv({ browsers: 'last 2 versions' })
```

If not valid browserslist configuration is specified, the
If no valid browserslist configuration is specified, the
[default browserslist query](https://github.com/browserslist/browserslist#queries)
will be used.

Expand Down Expand Up @@ -414,6 +436,19 @@ The `enableClientSidePolyfills` enables any feature that would need an extra bro

Note that manually enabling/disabling features via the "feature" option overrides this flag.

## Stability and Portability

[PostCSS Preset Env] will often include very modern CSS features that are not fully ready yet.
This gives users the chance to play around with these features and provide feedback.

If the specification changes or is abandoned a new major version of the plugin will be released.
This will require you to update your source code so that everything works as expected.

To have more stability between updates of [PostCSS Preset Env] you may set `stage: 3` and/or `minimumVendorImplementations: 2`.

A side effect of staying close to the standard is that you can more easily migrate your project to other tooling all together.


[cli-img]: https://github.com/csstools/postcss-plugins/workflows/test/badge.svg
[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test
[discord]: https://discord.gg/bUadyRwkJS
Expand Down
2 changes: 1 addition & 1 deletion plugin-packs/postcss-preset-env/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"lint": "eslint ./src --ext .js --ext .ts --ext .mjs --no-error-on-unmatched-pattern",
"prepublishOnly": "npm run clean && npm run build && npm run test",
"stryker": "stryker run --logLevel error",
"test": "node .tape.mjs && npm run test:exports",
"test": "node .tape.mjs && node ./src/test/test.mjs && npm run test:exports",
"test:rewrite-expects": "REWRITE_EXPECTS=true node .tape.mjs",
"test:exports": "node ./test/_import.mjs && node ./test/_require.cjs"
},
Expand Down
214 changes: 24 additions & 190 deletions plugin-packs/postcss-preset-env/src/index.js
Original file line number Diff line number Diff line change
@@ -1,174 +1,31 @@
import autoprefixer from 'autoprefixer';
import browserslist from 'browserslist';
import cssdb from 'cssdb';
import { pluginsById as plugins } from './lib/plugins-by-id';
import getTransformedInsertions from './lib/get-transformed-insertions';
import getUnsupportedBrowsersByFeature from './lib/get-unsupported-browsers-by-feature';
import idsByExecutionOrder from './lib/ids-by-execution-order';
import writeToExports from './lib/write-to-exports';
import getOptionsForBrowsersByFeature from './lib/get-options-for-browsers-by-feature';
import { pluginIdHelp } from './lib/plugin-id-help';
import { pluginHasSideEffects } from './lib/plugins-with-side-effects';
import { log, dumpLogs, resetLogger } from './lib/log-helper';
import { featuresWithClientSide } from './lib/plugins-with-client-side';
import logFeaturesList from './lib/log-features-list';

const DEFAULT_STAGE = 2;
const OUT_OF_RANGE_STAGE = 5;

const plugin = opts => {
import writeToExports from './side-effects/write-to-exports.mjs';
import { pluginIdHelp } from './plugins/plugin-id-help.mjs';
import { dumpLogs, resetLogger } from './log/helper.mjs';
import logFeaturesList from './log/features-list.mjs';
import { listFeatures } from './lib/list-features.mjs';
import { initializeSharedOptions } from './lib/shared-options.mjs';

const plugin = (opts) => {
// initialize options
const options = Object(opts);
const features = Object(options.features);
const enableClientSidePolyfills = 'enableClientSidePolyfills' in options ? options.enableClientSidePolyfills : true;
const featureNamesInOptions = Object.keys(features);
const insertBefore = Object(options.insertBefore);
const insertAfter = Object(options.insertAfter);
const featureNamesInOptions = Object.keys(Object(options.features));
const browsers = options.browsers;
let stage = DEFAULT_STAGE;

resetLogger();

if (typeof options.stage !== 'undefined') {
if (options.stage === false) {
stage = OUT_OF_RANGE_STAGE;
} else {
stage = Math.min(parseInt(options.stage, 10), OUT_OF_RANGE_STAGE) || 0;
}
}

if (stage === OUT_OF_RANGE_STAGE) {
log('Stage has been disabled, features will be handled via the "features" option.');
} else {
log(`Using features from Stage ${stage}`);
}

const autoprefixerOptions = options.autoprefixer;
const sharedOpts = initializeSharedOpts(options);
const stagedAutoprefixer = autoprefixerOptions === false
? () => {}
: autoprefixer(Object.assign({ overrideBrowserslist: browsers }, autoprefixerOptions));

// polyfillable features (those with an available postcss plugin)
const polyfillableFeatures = cssdb.concat(
// additional features to be inserted before cssdb features
getTransformedInsertions(insertBefore, 'insertBefore'),
// additional features to be inserted after cssdb features
getTransformedInsertions(insertAfter, 'insertAfter'),
).filter(
// inserted features or features with an available postcss plugin
feature => feature.insertBefore || feature.id in plugins,
).sort(
// features sorted by execution order and then insertion order
(a, b) => idsByExecutionOrder.indexOf(a.id) - idsByExecutionOrder.indexOf(b.id) || (a.insertBefore ? -1 : b.insertBefore ? 1 : 0) || (a.insertAfter ? 1 : b.insertAfter ? -1 : 0),
).map(
// polyfillable features as an object
feature => {
// target browsers for the polyfill
const unsupportedBrowsers = getUnsupportedBrowsersByFeature(feature);

return feature.insertBefore || feature.insertAfter ? {
browsers: unsupportedBrowsers,
plugin: feature.plugin,
id: `${feature.insertBefore ? 'before' : 'after'}-${feature.id}`,
stage: OUT_OF_RANGE_STAGE + 1, // So they always match
} : {
browsers: unsupportedBrowsers,
plugin: plugins[feature.id],
id: feature.id,
stage: feature.stage,
};
},
);

// staged features (those at or above the selected stage)
const stagedFeatures = polyfillableFeatures.filter(feature => {
const isAllowedStage = feature.stage >= stage;
const isAllowedByType = enableClientSidePolyfills || !featuresWithClientSide.includes(feature.id);
const isDisabled = features[feature.id] === false;
const isAllowedFeature = features[feature.id] ? features[feature.id] : isAllowedStage && isAllowedByType;

if (isDisabled) {
log(` ${feature.id} has been disabled by options`);
} else if (!isAllowedStage) {
if (isAllowedFeature) {
log(` ${feature.id} has been enabled by options`);
} else {
log(` ${feature.id} with stage ${feature.stage} has been disabled`);
}
} else if (!isAllowedByType) {
log(` ${feature.id} has been disabled by "enableClientSidePolyfills: false".`);
}

return isAllowedFeature;
}).map(
feature => {
let options;
let plugin;

options = getOptionsForBrowsersByFeature(browsers, feature, cssdb);

if (features[feature.id] === true) {
// if the plugin is enabled
options = sharedOpts ? Object.assign({}, options, sharedOpts) : undefined;
} else {
options = sharedOpts
// if the plugin has shared options and individual options
? Object.assign({}, options, sharedOpts, features[feature.id])
// if the plugin has individual options
: Object.assign({}, options, features[feature.id]);
}

if (feature.plugin.postcss) {
plugin = feature.plugin(options);
} else {
plugin = feature.plugin;
}

return {
browsers: feature.browsers,
plugin: plugin,
pluginOptions: options,
id: feature.id,
};
},
);
const sharedOptions = initializeSharedOptions(options);

// browsers supported by the configuration
const supportedBrowsers = browserslist(browsers, { ignoreUnknownVersions: true });

// - features supported by the stage
// - features with `true` or with options
// - required for the browsers
// - having "importFrom" or "exportTo" options
const supportedFeatures = stagedFeatures.filter((feature) => {
if (feature.id in features) {
return features[feature.id];
}

if (pluginHasSideEffects(feature)) {
return true;
}

const unsupportedBrowsers = browserslist(feature.browsers, {
ignoreUnknownVersions: true,
});

const needsPolyfill = supportedBrowsers.some(supportedBrowser => {
return unsupportedBrowsers.some(unsupportedBrowser => unsupportedBrowser === supportedBrowser);
});

if (!needsPolyfill) {
log(`${feature.id} disabled due to browser support`);
}

return needsPolyfill;
const features = listFeatures(cssdb, options, sharedOptions);
const plugins = features.map((feature) => {
return feature.plugin;
});

const usedPlugins = supportedFeatures.map(feature => feature.plugin);
usedPlugins.push(stagedAutoprefixer);
if (options.autoprefixer !== false) {
plugins.push(
autoprefixer(Object.assign({ overrideBrowserslist: browsers }, options.autoprefixer)),
);
}

logFeaturesList(supportedFeatures, options);
logFeaturesList(features, options);

const internalPlugin = () => {
return {
Expand All @@ -180,8 +37,11 @@ const plugin = opts => {
dumpLogs(result);
}

// Always reset the logger, if when debug is false
resetLogger();

if (options.exportTo) {
writeToExports(sharedOpts.exportTo, opts.exportTo);
writeToExports(sharedOptions.exportTo, opts.exportTo);
}
},
};
Expand All @@ -191,36 +51,10 @@ const plugin = opts => {

return {
postcssPlugin: 'postcss-preset-env',
plugins: [...usedPlugins, internalPlugin()],
plugins: [...plugins, internalPlugin()],
};
};

const initializeSharedOpts = opts => {
if ('importFrom' in opts || 'exportTo' in opts || 'preserve' in opts) {
const sharedOpts = {};

if ('importFrom' in opts) {
sharedOpts.importFrom = opts.importFrom;
}

if ('exportTo' in opts) {
sharedOpts.exportTo = {
customMedia: {},
customProperties: {},
customSelectors: {},
};
}

if ('preserve' in opts) {
sharedOpts.preserve = opts.preserve;
}

return sharedOpts;
}

return false;
};

plugin.postcss = true;

export default plugin;
Loading