diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 37d37879..00000000 --- a/.eslintrc +++ /dev/null @@ -1,34 +0,0 @@ ---- -ecmaFeatures: - modules: true - -env: - es6: true - browser: true - node: true - -rules: - indent: [2, 2] # 2 spaces indentation - max-len: [2, 80, 4] - quotes: [2, "double"] - semi: [2, "never"] - no-multiple-empty-lines: [2, {"max": 1}] - - brace-style: [2, "stroustrup"] - comma-dangle: [2, "always-multiline"] - comma-style: [2, "last"] - computed-property-spacing: [2, "never"] - dot-location: [2, "property"] - - one-var: [2, "never"] - no-bitwise: [2] - - space-after-keywords: [2, "always"] - space-before-blocks: [2, "always"] - space-before-function-paren: [2, "never"] - space-in-brackets: [2, "never"] - space-in-parens: [2, "never"] - spaced-line-comment: [2, "always"] - - # this plugin need a refactoring... - no-use-before-define: 0 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..194751cd --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,21 @@ +name: Node.js CI + +on: + push: + branches: master + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x, 20.x, 22.x, 24.x] + steps: + - uses: actions/checkout@v5 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm run ci diff --git a/.gitignore b/.gitignore index 7ab649f4..426da765 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,64 @@ -node_modules +# Test output test/fixtures/*.actual.css + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +/node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..43c97e71 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 587bd3e0..00000000 --- a/.travis.yml +++ /dev/null @@ -1 +0,0 @@ -language: node_js diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d6100c4..4c2d1b3a 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,259 @@ +# 16.1.1 / 2025-06-17 + +- Fix incorrect cascade layer order when some resources can not be inlined ([#567](https://github.com/postcss/postcss-import/issues/567), [#574](https://github.com/postcss/postcss-import/pull/574)) + +# 16.1.0 / 2024-03-20 + +- Allow bundling URLs with fragments (useful for Vite users) ([#560](https://github.com/postcss/postcss-import/issues/560), [#561](https://github.com/postcss/postcss-import/pull/561)) + +# 16.0.1 / 2024-02-14 + +- Fix crash when handling some `@import`s with media conditions ([#557](https://github.com/postcss/postcss-import/issues/557), [#558](https://github.com/postcss/postcss-import/pull/558)) + +# 16.0.0 / 2024-01-02 + +- **BREAKING:** Require Node.js v18+ ([#550](https://github.com/postcss/postcss-import/issues/550), [#551](https://github.com/postcss/postcss-import/pull/551)) +- **BREAKING:** Signifigant rewrite, with small behavioral tweaks in a number of edge cases +- Support for `@supports` conditional imports added ([#532](https://github.com/postcss/postcss-import/issues/532), [#548](https://github.com/postcss/postcss-import/pull/548)) +- When `skipDuplicates` is `false`, handles import cycles correctly ([#462](https://github.com/postcss/postcss-import/issues/462), [#535](https://github.com/postcss/postcss-import/pull/535)) +- Add `warnOnEmpty` option to allow disabling warnings for empty files ([#84](https://github.com/postcss/postcss-import/issues/84), [#541](https://github.com/postcss/postcss-import/pull/541)) +- Use proper `node.error`s ([#518](https://github.com/postcss/postcss-import/issues/518), [#540](https://github.com/postcss/postcss-import/pull/540)) + +Huge thanks to [`@romainmenke`](https://github.com/romainmenke) for all the hard work he put into this release. + +# 15.1.0 / 2022-12-07 + +- Add `data:` URL support (this is not useful for most consumers) ([#515](https://github.com/postcss/postcss-import/pull/515)) + +# 15.0.1 / 2022-12-01 + +- Preserve layer in ignored `@import`s ([#510](https://github.com/postcss/postcss-import/issues/510), [#511](https://github.com/postcss/postcss-import/pull/511)) +- Join media queries in the correct order ([#512](https://github.com/postcss/postcss-import/issues/512), [#513](https://github.com/postcss/postcss-import/pull/513)) + +# 15.0.0 / 2022-08-30 + +- **BREAKING:** Require Node.js v14+ ([#497](https://github.com/postcss/postcss-import/pull/497)) +- **BREAKING:** Require `nameLayer` option for handling anonymous layers ([#496](https://github.com/postcss/postcss-import/pull/496)) +- Fix handling of `@media` queries inside layered imports ([#495](https://github.com/postcss/postcss-import/issues/495), [#496](https://github.com/postcss/postcss-import/pull/496)) + +# 14.1.0 / 2022-03-22 + +- Add `@layer` support ([#483](https://github.com/postcss/postcss-import/pull/483)) + +# 14.0.2 / 2021-05-10 + +- Remove remaining direct import of `postcss` package ([#455](https://github.com/postcss/postcss-import/issues/455), [#456](https://github.com/postcss/postcss-import/pull/456)) + +# 14.0.1 / 2021-03-31 + +- Fix bug with `@charset` statements in media imports ([#448](https://github.com/postcss/postcss-import/issues/448), [#453](https://github.com/postcss/postcss-import/pull/453)) + +# 14.0.0 / 2020-12-14 + +This release should not have breaking changes for the vast majority of users; only those with `@charset` statements in their CSS _may_ be affected. + +- **BREAKING:** Error if multiple incompatible `@charset` statements ([#447](https://github.com/postcss/postcss-import/pull/447)) +- **BREAKING:** Warn if `@charset` statements are not at the top of files ([#447](https://github.com/postcss/postcss-import/pull/447)) +- Fix handing of `@charset` ([#436](https://github.com/postcss/postcss-import/issues/436), [#447](https://github.com/postcss/postcss-import/pull/447)) + +# 13.0.0 / 2020-10-20 + +- **BREAKING:** Require Node 10+ ([#429](https://github.com/postcss/postcss-import/pull/429)) +- **BREAKING:** Upgrade to postcss v8 and require it as a `peerDependency` ([#427](https://github.com/postcss/postcss-import/issues/427), [#432](https://github.com/postcss/postcss-import/pull/432)) +- Update dependencies + +# 12.0.1 / 2018-10-22 + +- Add `plugin` property to dependency messages ([#379](https://github.com/postcss/postcss-import/issues/379), [#380](https://github.com/postcss/postcss-import/pull/380)) + +# 12.0.0 - 2018-08-04 + +- Removed: Support for Node.js v4 +- Changed: Uses PostCSS v7 (https://github.com/postcss/postcss/releases/tag/7.0.0) + +# 11.1.0 - 2018-02-10 + +- Added: `filter` option + +# 11.0.0 - 2017-09-16 + +- Changed: A syntax error in an imported file now throws an error instead of just warning ([#264](https://github.com/postcss/postcss-import/issues/264)) +- Changed: Symlink handling to be consistent with Node.js `require` ([#300](https://github.com/postcss/postcss-import/pull/300)) + +# 10.0.0 - 2017-05-12 + +- Removed: Support for Node.js versions less than 4.5.x ([#283](https://github.com/postcss/postcss-import/pull/283)) +- Changed: Upgraded to Postcss v6 ([#283](https://github.com/postcss/postcss-import/pull/283)) +- Removed: jspm support ([#283](https://github.com/postcss/postcss-import/pull/283)) +- Removed: deprecated `addDependencyTo` option +- Removed: `onImport` option +- Changed: Doesn't depend on promise-each ([#281](https://github.com/postcss/postcss-import/pull/281)) + +# 9.1.0 - 2017-01-10 + +- Added: `addModulesDirectories` option ([#256](https://github.com/postcss/postcss-import/pull/256)) + +# 9.0.0 - 2016-12-02 + +- Removed: `transform` option + ([#250](https://github.com/postcss/postcss-import/pull/250)) +- Removed: `pkg-resolve` is no longer a dependency; this should fix some issues + with webpack. jspm users must manually install `pkg-resolve` if they want to + load jspm modules (see https://github.com/postcss/postcss-import#jspm-usage + for more info) ([#243](https://github.com/postcss/postcss-import/pull/243)) +- Changed: If a file is not found, it will now throw an error instead of just + raising a warning ([#247](https://github.com/postcss/postcss-import/pull/247)) +- Changed: If a custom resolver does not return an absolute path, the default + resolver will be applied to the returned path. + ([#249](https://github.com/postcss/postcss-import/pull/249)) +- Changed: postcss-import will try to guess the correct parser for imported + files, based on the file extension. + ([#245](https://github.com/postcss/postcss-import/pull/245)) +- Changed: Deprecated `addDependencyTo` option, it is not needed if using + postcss-loader >= v1.0.0 + ([#251](https://github.com/postcss/postcss-import/pull/251)) + +# 8.2.0 - 2016-11-09 + +- Fixed: Warn about all `@import`s after other CSS declarations + ([#240](https://github.com/postcss/postcss-import/pull/240)) +- Added: `dependency` message + ([#241](https://github.com/postcss/postcss-import/pull/241)) + +# 8.1.3 - 2016-11-03 + +- Fixed: Nested import ordering + ([#236](https://github.com/postcss/postcss-import/pull/236) - @RyanZim) + +# 8.1.2 - 2016-05-07 + +- Fixed: prevent JSPM to throw unrecoverable error + ([#205](https://github.com/postcss/postcss-import/pull/205)) + +# 8.1.1 - 2016-05-04 + +- Fixed: JSPM support + ([#194](https://github.com/postcss/postcss-import/pull/194)) + +# 8.1.0 - 2016-04-04 + +- Added: JSPM browser field + ([#186](https://github.com/postcss/postcss-import/pull/186)) + +# 8.0.2 - 2015-01-27 + +- Fixed: Comments between imports statements are ignored +([#164](https://github.com/postcss/postcss-import/pull/164)) + +# 8.0.1 - 2015-01-27 + +- Fixed: missing "lib" folder +([#161](https://github.com/postcss/postcss-import/issues/161)) + +# 8.0.0 - 2015-01-27 + +**All imports statements must be at the top of your file now, per CSS specification.** +You should use [postcss-reporter](https://github.com/postcss/postcss-reporter) to see the warnings raised. + +- Removed: async mode/option (now async by default) +([#107](https://github.com/postcss/postcss-import/pull/107)) +- Removed: "bower_components" not supported by default anymore, +use "path" option to add it back +- Removed: `encoding` option. Encoding can be specified in custom `load` option + +```js +postcssImport({ + load: function(filename) { + return fs.readFileSync(filename, "utf-8") + } +}) +``` +([#144](https://github.com/postcss/postcss-import/pull/144)) + +- Removed: glob support +([#146](https://github.com/postcss/postcss-import/pull/146)) + +Globs can be implemented with custom `resolve` option + +```js +postcssImport({ + resolve: function(id, base) { + return glob.sync(path.join(base, id)) + } +}) +``` + +([#116](https://github.com/postcss/postcss-import/pull/116)) +- Changed: custom resolve has more responsibility for paths resolving. +See [resolve option](https://github.com/postcss/postcss-import#resolve) +for more information about this change +([#116](https://github.com/postcss/postcss-import/pull/116)) +- Changed: support promise in `transform` option and `undefined` result will be +skipped +([#147](https://github.com/postcss/postcss-import/pull/147)) +- Changed: `options.plugins` are applied to unprocessed ast before imports +detecting +([157](https://github.com/postcss/postcss-import/pull/157)) +- Added: custom resolve function can return array of paths +([#120](https://github.com/postcss/postcss-import/pull/120)) +- Added: custom syntax in imported files support +([#130](https://github.com/postcss/postcss-import/pull/130)) +- Added: support custom `load` option +([#144](https://github.com/postcss/postcss-import/pull/144)) +- Added: detect css extension in package.json `main` field +([153](https://github.com/postcss/postcss-import/pull/153)) + +**Note:** +_If you miss options/default behavior (glob etc), a new plugin will handle all +those things. +Please follow issue [#145](https://github.com/postcss/postcss-import/issues/145) +_ + +# 7.1.3 - 2015-11-05 + +- Fixed: ensure node 0.12 compatibility, round 2 +([#93](https://github.com/postcss/postcss-import/pull/93)) + +# 7.1.2 - 2015-11-05 + +- Fixed: performance issue because of cloned options +([#90](https://github.com/postcss/postcss-import/pull/90)) + +# 7.1.1 - 2015-11-05 + +- Added: ensure node 0.12 compatibility + +# 7.0.0 - 2015-08-25 + +- Removed: compatibility with postcss v4.x +([#75](https://github.com/postcss/postcss-import/pull/75)) +- Added: compatibility with postcss v5.x +([#76](https://github.com/postcss/postcss-import/pull/76)) +- Added: lighter package by upgrading some dependencies +([#73](https://github.com/postcss/postcss-import/issues/73)) + +# 6.2.0 - 2015-07-21 + +- Added: `skipDuplicates` option now allows you to **not** skip duplicated files +([#67](https://github.com/postcss/postcss-import/issues/67)) + +# 6.1.1 - 2015-07-07 + +- Fixed: Prevent mutability issue, round 2 +([#44](https://github.com/postcss/postcss-import/issues/44)) +- Added: `plugins` option, to run some postcss plugin on imported files +([#55](https://github.com/postcss/postcss-import/issues/55)) +- Added: `bower_components` is now part of the default paths +([#66](https://github.com/postcss/postcss-import/issues/66)) +- Added: `async` option allow to use enable PostCSS async API usage. +Note that it's not enabling async fs read yet. It has been added to fix breaking +change introduced by 6.1.0. + +# 6.1.0 - 2015-07-07 **YANKED** + +_This release was not respecting semver and introduced a major breaking change. +It has been unpublished for now._ + # 6.0.0 - 2015-06-17 - Changed: warnings messages are now using postcss message api (4.1.x) diff --git a/README.md b/README.md old mode 100755 new mode 100644 index 5c3fc042..b0103238 --- a/README.md +++ b/README.md @@ -1,57 +1,94 @@ -# postcss-import [![Travis Build Status](https://travis-ci.org/postcss/postcss-import.svg)](https://travis-ci.org/postcss/postcss-import) [![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/u8l6u3lr6s5u5tpi?svg=true)](https://ci.appveyor.com/project/MoOx/postcss-import) +# postcss-import -> [PostCSS](https://github.com/postcss/postcss) plugin to transform `@import` rules by inlining content. +[![Build](https://img.shields.io/travis/postcss/postcss-import/master)](https://travis-ci.org/postcss/postcss-import) +[![Version](https://img.shields.io/npm/v/postcss-import)](https://github.com/postcss/postcss-import/blob/master/CHANGELOG.md) +[![postcss compatibility](https://img.shields.io/npm/dependency-version/postcss-import/peer/postcss)](https://postcss.org/) -This plugin can consume local files or node modules. -To resolve path of an `@import` rule, it can look into root directory (by default `process.cwd()`), `node_modules`, `web_modules` or local modules. +> [PostCSS](https://github.com/postcss/postcss) plugin to transform `@import` +rules by inlining content. + +This plugin can consume local files, node modules or web_modules. +To resolve path of an `@import` rule, it can look into root directory +(by default `process.cwd()`), `web_modules`, `node_modules` +or local modules. +_When importing a module, it will look for `index.css` or file referenced in +`package.json` in the `style` or `main` fields._ You can also provide manually multiples paths where to look at. **Notes:** -- This plugin works great with [postcss-url](https://github.com/postcss/postcss-url) plugin, -which will allow you to adjust assets `url()` (or even inline them) after inlining imported files. -- In order to optimize output, this plugin will only import a file once on a given scope (root, media query...). Tests are made from the path & the content of imported files (using a hash table). +- **This plugin should probably be used as the first plugin of your list. +This way, other plugins will work on the AST as if there were only a single file +to process, and will probably work as you can expect**. +- Running [postcss-url](https://github.com/postcss/postcss-url) after +postcss-import in your plugin chain will allow you to adjust assets `url()` (or +even inline them) after inlining imported files. +- In order to optimize output, **this plugin will only import a file once** on +a given scope (root, media query...). +Tests are made from the path & the content of imported files (using a hash +table). +If this behavior is not what you want, look at `skipDuplicates` option +- If you are looking for **Glob Imports**, you can use [postcss-import-ext-glob](https://github.com/dimitrinicolas/postcss-import-ext-glob) to extend postcss-import. +- If you want to import remote sources, you can use [postcss-import-url](https://github.com/unlight/postcss-import-url) with its `dataUrls` plugin option to extend postcss-import. +- Imports which are not modified (by `options.filter` or because they are remote + imports) are moved to the top of the output. +- **This plugin attempts to follow the CSS `@import` spec**; `@import` + statements must precede all other statements (besides `@charset`). ## Installation ```console -$ npm install postcss-import +$ npm install -D postcss-import ``` ## Usage -If your stylesheets are not in the same place where you run postcss (`process.cwd()`), you will need to use `from` option to make relative imports work from input dirname. +Unless your stylesheet is in the same place where you run postcss +(`process.cwd()`), you will need to use `from` option to make relative imports +work. ```js // dependencies -var fs = require("fs") -var postcss = require("postcss") -var atImport = require("postcss-import") +const fs = require("fs") +const postcss = require("postcss") +const atImport = require("postcss-import") // css to be processed -var css = fs.readFileSync("css/input.css", "utf8") +const css = fs.readFileSync("css/input.css", "utf8") // process css -var output = postcss() +postcss() .use(atImport()) .process(css, { - // `from` option is required so relative import can work from input dirname + // `from` option is needed here from: "css/input.css" }) - .css + .then((result) => { + const output = result.css -console.log(output) + console.log(output) + }) ``` -Using this `input.css`: +`css/input.css`: ```css +/* remote urls are preserved */ +@import "https://example.com/styles.css"; + /* can consume `node_modules`, `web_modules` or local modules */ -@import "cssrecipes-defaults"; /* == @import "./node_modules/cssrecipes-defaults/index.css"; */ +@import "cssrecipes-defaults"; /* == @import "../node_modules/cssrecipes-defaults/index.css"; */ +@import "normalize.css"; /* == @import "../node_modules/normalize.css/normalize.css"; */ + +@import "foo.css"; /* relative to css/ according to `from` option above */ + +/* all standard notations of the "url" value are supported */ +@import url(foo-1.css); +@import url("foo-2.css"); -@import "css/foo.css"; /* relative to stylesheets/ according to `from` option above */ +@import "bar.css" (min-width: 25em); -@import "css/bar.css" (min-width: 25em); +@import 'baz.css' layer(baz-layer); body { background: black; @@ -61,12 +98,22 @@ body { will give you: ```css -/* ... content of ./node_modules/my-css-on-npm/index.css */ +@import "https://example.com/styles.css"; -/* ... content of foo.css */ +/* ... content of ../node_modules/cssrecipes-defaults/index.css */ +/* ... content of ../node_modules/normalize.css/normalize.css */ + +/* ... content of css/foo.css */ + +/* ... content of css/foo-1.css */ +/* ... content of css/foo-2.css */ @media (min-width: 25em) { -/* ... content of bar.css */ +/* ... content of css/bar.css */ +} + +@layer baz-layer { +/* ... content of css/baz.css */ } body { @@ -74,87 +121,132 @@ body { } ``` -Checkout [tests](test) for more examples. +Checkout the [tests](test) for more examples. ### Options +#### `filter` +Type: `Function` +Default: `() => true` + +Only transform imports for which the test function returns `true`. Imports for +which the test function returns `false` will be left as is. The function gets +the path to import as an argument and should return a boolean. + #### `root` Type: `String` -Default: `process.cwd()` +Default: `process.cwd()` or _dirname of +[the postcss `from`](https://github.com/postcss/postcss#node-source)_ -Define the root where to resolve path (eg: place where `node_modules` is). Should not be used that much. +Define the root where to resolve path (eg: place where `node_modules` are). +Should not be used that much. +_Note: nested `@import` will additionally benefit of the relative dirname of +imported files._ #### `path` Type: `String|Array` -Default: `process.cwd()` or _dirname of [the postcss `from`](https://github.com/postcss/postcss#node-source)_ +Default: `[]` -A string or an array of paths in where to look for files. -_Note: nested `@import` will additionally benefit of the relative dirname of imported files._ +A string or an array of paths in where to look for files. -#### `transform` +#### `plugins` -Type: `Function` -Default: `null` +Type: `Array` +Default: `undefined` -A function to transform the content of imported files. Take one argument (file content) & should return the modified content. +An array of plugins to be applied on each imported files. -#### `encoding` +#### `resolve` -Type: `String` -Default: `utf8` +Type: `Function` +Default: `null` -Use if your CSS is encoded in anything other than UTF-8. +You can provide a custom path resolver with this option. This function gets +`(id, basedir, importOptions, astNode)` arguments and should return a path, an array of +paths or a promise resolving to the path(s). If you do not return an absolute +path, your path will be resolved to an absolute path using the default +resolver. +You can use [resolve](https://github.com/substack/node-resolve) for this. -#### `onImport` +#### `load` Type: `Function` -Default: `null` +Default: null -Function called after the import process. Take one argument (array of imported files). +You can overwrite the default loading way by setting this option. +This function gets `(filename, importOptions)` arguments and returns content or +promised content. -#### `glob` +#### `skipDuplicates` Type: `Boolean` -Default: `false` +Default: `true` -Set to `true` if you want @import rules to parse glob patterns. +By default, similar files (based on the same content) are being skipped. +It's to optimize output and skip similar files like `normalize.css` for example. +If this behavior is not what you want, just set this option to `false` to +disable it. -#### `resolve` +#### `addModulesDirectories` -Type: `Function` -Default: `null` +Type: `Array` +Default: `[]` + +An array of folder names to add to [Node's resolver](https://github.com/substack/node-resolve). +Values will be appended to the default resolve directories: +`["node_modules", "web_modules"]`. + +This option is only for adding additional directories to default resolver. If +you provide your own resolver via the `resolve` configuration option above, then +this value will be ignored. -You can overwrite the default path resolving way by setting this option, using the `resolve.sync(id, opts)` signature that [resolve.sync](https://github.com/substack/node-resolve#resolvesyncid-opts) has. +#### `warnOnEmpty` + +Type: `Boolean` +Default: `true` + +By default `postcss-import` warns when an empty file is imported. +Set this option to `false` to disable this warning. #### Example with some options ```js -var postcss = require("postcss") -var atImport = require("postcss-import") +const postcss = require("postcss") +const atImport = require("postcss-import") -var css = postcss() +postcss() .use(atImport({ - path: ["src/css"] - transform: require("css-whitespace") + path: ["src/css"], })) .process(cssString) - .css + .then((result) => { + const { css } = result + }) ``` ---- +## `dependency` Message Support -## Contributing +`postcss-import` adds a message to `result.messages` for each `@import`. Messages are in the following format: -Work on a branch, install dev-dependencies, respect coding style & run tests before submitting a bug fix or a feature. - -```console -$ git clone https://github.com/postcss/postcss-import.git -$ git checkout -b patch-1 -$ npm install -$ npm test ``` +{ + type: 'dependency', + file: absoluteFilePath, + parent: fileContainingTheImport +} +``` + +This is mainly for use by postcss runners that implement file watching. + +--- + +## CONTRIBUTING + +* ⇄ Pull requests and ★ Stars are always welcome. +* For bugs and feature requests, please create an issue. +* Pull requests must be accompanied by passing automated tests (`$ npm test`). ## [Changelog](CHANGELOG.md) diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 9a75a362..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,19 +0,0 @@ -# http://www.appveyor.com/docs/appveyor-yml - -environment: - matrix: - - nodejs_version: 0.10 - -version: "{build}" -build: off -deploy: off - -install: - - ps: Install-Product node $env:nodejs_version - - npm install - -test_script: - - node --version - - npm --version - - ps: "npm test # PowerShell" - - cmd: "npm test" diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..0398af23 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,27 @@ +"use strict" + +const problems = require("eslint-config-problems") +const prettier = require("eslint-plugin-prettier") +const globals = require("globals") + +module.exports = [ + problems, + { + languageOptions: { + globals: { + ...globals.node, + }, + sourceType: "commonjs", + }, + plugins: { prettier }, + rules: { + "prettier/prettier": [ + "error", + { + semi: false, + arrowParens: "avoid", + }, + ], + }, + }, +] diff --git a/index.js b/index.js index 69da8901..17869882 100755 --- a/index.js +++ b/index.js @@ -1,525 +1,70 @@ -/** - * Module dependencies. - */ -var fs = require("fs") -var path = require("path") +"use strict" +// builtin tooling +const path = require("path") + +// internal tooling +const applyConditions = require("./lib/apply-conditions") +const applyRaws = require("./lib/apply-raws") +const applyStyles = require("./lib/apply-styles") +const loadContent = require("./lib/load-content") +const parseStyles = require("./lib/parse-styles") +const resolveId = require("./lib/resolve-id") -var assign = require("object-assign") -var clone = require("clone") -var resolve = require("resolve") -var postcss = require("postcss") -var helpers = require("postcss-message-helpers") -var hash = require("string-hash") -var glob = require("glob") - -/** - * Constants - */ -var moduleDirectories = [ - "web_modules", - "node_modules", -] - -var warnNodesMessage = - "It looks like you didn't end correctly your @import statement. " + - "Some children nodes are attached to it" - -/** - * Inline `@import`ed files - * - * @param {Object} options - */ function AtImport(options) { - options = assign({}, options || {}) - options.root = options.root || process.cwd() - options.path = ( - // convert string to an array of a single element - typeof options.path === "string" ? - [options.path] : - (options.path || []) // fallback to empty array - ) - - return function(styles, result) { - // auto add from option if possible - if ( - !options.from && - styles && - styles.nodes && - styles.nodes[0] && - styles.nodes[0].source && - styles.nodes[0].source.input && - styles.nodes[0].source.input.file - ) { - options.from = styles.nodes[0].source.input.file - } - - // if from available, prepend from directory in the path array - addInputToPath(options) - - // if we got nothing for the path, just use cwd - if (options.path.length === 0) { - options.path.push(process.cwd()) - } - - var importedFiles = {} - if (options.from) { - importedFiles[options.from] = { - "": true, - } - } - var ignoredAtRules = [] - - var hashFiles = {} - - parseStyles( - result, - styles, - options, - insertRules, - importedFiles, - ignoredAtRules, - null, - hashFiles - ) - addIgnoredAtRulesOnTop(styles, ignoredAtRules) - - if (typeof options.onImport === "function") { - options.onImport(Object.keys(importedFiles)) - } - } -} - -/** - * lookup for @import rules - * - * @param {Object} styles - * @param {Object} options - */ -function parseStyles( - result, - styles, - options, - cb, - importedFiles, - ignoredAtRules, - media, - hashFiles -) { - var imports = [] - styles.eachAtRule("import", function checkAtRule(atRule) { - if (atRule.nodes) { - result.warn(warnNodesMessage, {node: atRule}) - } - if (options.glob && glob.hasMagic(atRule.params)) { - imports = parseGlob(atRule, options, imports) - } - else { - imports.push(atRule) - } - }) - imports.forEach(function(atRule) { - helpers.try(function transformAtImport() { - readAtImport( - result, - atRule, - options, - cb, - importedFiles, - ignoredAtRules, - media, - hashFiles - ) - }, atRule.source) - }) -} - -/** - * parse glob patterns (for relative paths only) - * - * @param {Object} atRule - * @param {Object} options - * @param {Array} imports - */ -function parseGlob(atRule, options, imports) { - var globPattern = atRule.params - .replace(/['"]/g, "") - .replace(/(?:url\(|\))/g, "") - var paths = options.path.concat(moduleDirectories) - var files = [] - var dir = options.source && options.source.input && options.source.input.file - ? path.dirname(path.resolve(options.root, options.source.input.file)) - : options.root - paths.forEach(function(p) { - p = path.resolve(dir, p) - var globbed = glob.sync(path.join(p, globPattern)) - globbed.forEach(function(file) { - file = path.relative(p, file) - files.push(file) - }) - }) - - files.forEach(function(file) { - var deglobbedAtRule = atRule.clone({ - params: "\"" + file + "\"", - }) - if ( - deglobbedAtRule.source && - deglobbedAtRule.source.input && - deglobbedAtRule.source.input.css - ) { - deglobbedAtRule.source.input.css = atRule.source.input.css - .replace(globPattern, file) - } - atRule.parent.insertBefore(atRule, deglobbedAtRule) - imports.push(deglobbedAtRule) - }) - atRule.removeSelf() - - return imports -} - -/** - * put back at the top ignored url (absolute url) - * - * @param {Object} styles - * @param {Array} ignoredAtRules - */ -function addIgnoredAtRulesOnTop(styles, ignoredAtRules) { - var i = ignoredAtRules.length - if (i) { - var first = styles.first - - while (i--) { - var ignoredAtRule = ignoredAtRules[i][0] - ignoredAtRule.params = ignoredAtRules[i][1].fullUri + - (ignoredAtRules[i][1].media ? " " + ignoredAtRules[i][1].media : "") - - // keep ast ref - ignoredAtRule.parent = styles - - // don't use prepend() to avoid weird behavior of normalize() - styles.nodes.unshift(ignoredAtRule) - } - - // separate remote import a little with others rules if no newlines already - if (first && - first.before.indexOf("\n") === -1) { - first.before = "\n\n" + first.before - } - } -} - -/** - * parse @import rules & inline appropriate rules - * - * @param {Object} atRule postcss atRule - * @param {Object} options - */ -function readAtImport( - result, - atRule, - options, - cb, - importedFiles, - ignoredAtRules, - media, - hashFiles -) { - // parse-import module parse entire line - // @todo extract what can be interesting from this one - var parsedAtImport = parseImport(atRule.params, atRule.source) - - // adjust media according to current scope - media = parsedAtImport.media - ? (media ? media + " and " : "") + parsedAtImport.media - : (media ? media : null) - - // just update protocol base uri (protocol://url) or protocol-relative - // (//url) if media needed - if (parsedAtImport.uri.match(/^(?:[a-z]+:)?\/\//i)) { - parsedAtImport.media = media - - // save - ignoredAtRules.push([atRule, parsedAtImport]) - - // detach - detach(atRule) - - return - } - - addInputToPath(options) - var resolvedFilename = resolveFilename( - parsedAtImport.uri, - options.root, - options.path, - atRule.source, - options.resolve - ) - - // skip files already imported at the same scope - if ( - importedFiles[resolvedFilename] && - importedFiles[resolvedFilename][media] - ) { - detach(atRule) - return - } - - // save imported files to skip them next time - if (!importedFiles[resolvedFilename]) { - importedFiles[resolvedFilename] = {} - } - importedFiles[resolvedFilename][media] = true - - readImportedContent( - result, - atRule, - parsedAtImport, - clone(options), - resolvedFilename, - cb, - importedFiles, - ignoredAtRules, - media, - hashFiles - ) -} - -/** - * insert imported content at the right place - * - * @param {Object} atRule - * @param {Object} parsedAtImport - * @param {Object} options - * @param {String} resolvedFilename - * @param {Function} cb - */ -function readImportedContent( - result, - atRule, - parsedAtImport, - options, - resolvedFilename, - cb, - importedFiles, - ignoredAtRules, - media, - hashFiles -) { - // add directory containing the @imported file in the paths - // to allow local import from this file - var dirname = path.dirname(resolvedFilename) - if (options.path.indexOf(dirname) === -1) { - options.path = options.path.slice() - options.path.unshift(dirname) - } - - options.from = resolvedFilename - var fileContent = readFile( - resolvedFilename, - options.encoding, - options.transform || function(value) { - return value - } - ) - - if (fileContent.trim() === "") { - result.warn(resolvedFilename + " is empty", {node: atRule}) - detach(atRule) - return - } - - // skip files wich only contain @import rules - var newFileContent = fileContent.replace(/@import (.*);/, "") - if (newFileContent.trim() !== "") { - var fileContentHash = hash(fileContent) - - // skip files already imported at the same scope and same hash - if (hashFiles[fileContentHash] && hashFiles[fileContentHash][media]) { - detach(atRule) - return - } - - // save hash files to skip them next time - if (!hashFiles[fileContentHash]) { - hashFiles[fileContentHash] = {} - } - hashFiles[fileContentHash][media] = true + options = { + root: process.cwd(), + path: [], + skipDuplicates: true, + resolve: resolveId, + load: loadContent, + plugins: [], + addModulesDirectories: [], + warnOnEmpty: true, + ...options, } - var newStyles = postcss.parse(fileContent, options) - - // recursion: import @import from imported file - parseStyles( - result, - newStyles, - options, - cb, - importedFiles, - ignoredAtRules, - parsedAtImport.media, - hashFiles - ) - - cb(atRule, parsedAtImport, newStyles, resolvedFilename) -} - -/** - * insert new imported rules at the right place - * - * @param {Object} atRule - * @param {Object} parsedAtImport - * @param {Object} newStyles - */ -function insertRules(atRule, parsedAtImport, newStyles) { - var newNodes = newStyles.nodes - - // wrap rules if the @import have a media query - if (parsedAtImport.media && parsedAtImport.media.length) { - // better output - if (newStyles.nodes && newStyles.nodes.length) { - newStyles.nodes[0].before = newStyles.nodes[0].before || "\n" - } - - // wrap new rules with media (media query) - var wrapper = postcss.atRule({ - name: "media", - params: parsedAtImport.media, - }) + options.root = path.resolve(options.root) - // keep AST clean - newNodes.forEach(function(node) { - node.parent = wrapper - }) - wrapper.source = atRule.source + // convert string to an array of a single element + if (typeof options.path === "string") options.path = [options.path] - // copy code style - wrapper.before = atRule.before - wrapper.after = atRule.after + if (!Array.isArray(options.path)) options.path = [] - // move nodes - wrapper.nodes = newNodes - newNodes = [wrapper] - } - else if (newNodes && newNodes.length) { - newNodes[0].before = atRule.before - } - - // keep AST clean - newNodes.forEach(function(node) { - node.parent = atRule.parent - }) - - // replace atRule by imported nodes - var nodes = atRule.parent.nodes - nodes.splice.apply(nodes, [nodes.indexOf(atRule), 0].concat(newNodes)) - detach(atRule) -} - -/** - * parse @import parameter - */ -function parseImport(str, source) { - var regex = /((?:url\s?\()?(?:'|")?([^)'"]+)(?:'|")?\)?)(?:(?:\s)(.*))?/gi - var matches = regex.exec(str) - if (matches === null) { - throw new Error("Unable to find uri in '" + str + "'", source) - } + options.path = options.path.map(p => path.resolve(options.root, p)) return { - fullUri: matches[1], - uri: matches[2], - media: matches[3] ? matches[3] : null, - } -} - -/** - * Check if a file exists - * - * @param {String} name - */ -function resolveFilename(name, root, paths, source, resolver) { - var dir = source && source.input && source.input.file - ? path.dirname(path.resolve(root, source.input.file)) - : root - - try { - var resolveOpts = { - basedir: dir, - moduleDirectory: moduleDirectories.concat(paths), - paths: paths, - extensions: [".css"], - packageFilter: function processPackage(pkg) { - pkg.main = pkg.style || "index.css" - return pkg - }, - } - var file - resolver = resolver || resolve.sync - try { - file = resolver(name, resolveOpts) - } - catch (e) { - // fix to try relative files on windows with "./" - // if it's look like it doesn't start with a relative path already - // if (name.match(/^\.\.?/)) {throw e} - try { - file = resolver("./" + name, resolveOpts) + postcssPlugin: "postcss-import", + async Once(styles, { result, atRule, postcss }) { + const state = { + importedFiles: {}, + hashFiles: {}, } - catch (err) { - // LAST HOPE - if (!paths.some(function(dir2) { - file = path.join(dir2, name) - return fs.existsSync(file) - })) { - throw err - } + + if (styles.source?.input?.file) { + state.importedFiles[styles.source.input.file] = {} } - } - return path.normalize(file) - } - catch (e) { - throw new Error( - "Failed to find '" + name + "' from " + root + - "\n in [ " + - "\n " + paths.join(",\n ") + - "\n ]", - source - ) - } -} + if (options.plugins && !Array.isArray(options.plugins)) { + throw new Error("plugins option must be an array") + } -/** - * Read the contents of a file - * - * @param {String} file - */ -function readFile(file, encoding, transform) { - return transform(fs.readFileSync(file, encoding || "utf8"), file) -} + const bundle = await parseStyles( + result, + styles, + options, + state, + [], + [], + postcss, + ) -/** - * add `from` dirname to `path` if not already present - * - * @param {Object} options - */ -function addInputToPath(options) { - if (options.from) { - var fromDir = path.dirname(options.from) - if (options.path.indexOf(fromDir) === -1) { - options.path.unshift(fromDir) - } + applyRaws(bundle) + applyConditions(bundle, atRule) + applyStyles(bundle, styles) + }, } } -function detach(node) { - node.parent.nodes.splice(node.parent.nodes.indexOf(node), 1) -} +AtImport.postcss = true -module.exports = postcss.plugin( - "postcss-import", - AtImport -) -module.exports.warnNodesMessage = warnNodesMessage +module.exports = AtImport diff --git a/lib/apply-conditions.js b/lib/apply-conditions.js new file mode 100644 index 00000000..2005c6be --- /dev/null +++ b/lib/apply-conditions.js @@ -0,0 +1,118 @@ +"use strict" + +const base64EncodedConditionalImport = require("./base64-encoded-import") + +module.exports = function applyConditions(bundle, atRule) { + const firstImportStatementIndex = bundle.findIndex( + stmt => stmt.type === "import", + ) + const lastImportStatementIndex = bundle.findLastIndex( + stmt => stmt.type === "import", + ) + + bundle.forEach((stmt, index) => { + if (stmt.type === "charset" || stmt.type === "warning") { + return + } + + if ( + stmt.type === "layer" && + ((index < lastImportStatementIndex && stmt.conditions?.length) || + (index > firstImportStatementIndex && index < lastImportStatementIndex)) + ) { + stmt.type = "import" + stmt.node = stmt.node.clone({ + name: "import", + params: base64EncodedConditionalImport( + `'data:text/css;base64,${Buffer.from(stmt.node.toString()).toString( + "base64", + )}'`, + stmt.conditions, + ), + }) + + return + } + + if (!stmt.conditions?.length) { + return + } + + if (stmt.type === "import") { + stmt.node.params = base64EncodedConditionalImport( + stmt.fullUri, + stmt.conditions, + ) + return + } + + let nodes + let parent + if (stmt.type === "layer") { + nodes = [stmt.node] + parent = stmt.node.parent + } else { + nodes = stmt.nodes + parent = nodes[0].parent + } + + const atRules = [] + + // Convert conditions to at-rules + for (const condition of stmt.conditions) { + if (typeof condition.media !== "undefined") { + const mediaNode = atRule({ + name: "media", + params: condition.media, + source: parent.source, + }) + + atRules.push(mediaNode) + } + + if (typeof condition.supports !== "undefined") { + const supportsNode = atRule({ + name: "supports", + params: `(${condition.supports})`, + source: parent.source, + }) + + atRules.push(supportsNode) + } + + if (typeof condition.layer !== "undefined") { + const layerNode = atRule({ + name: "layer", + params: condition.layer, + source: parent.source, + }) + + atRules.push(layerNode) + } + } + + // Add nodes to AST + const outerAtRule = atRules.shift() + const innerAtRule = atRules.reduce((previous, next) => { + previous.append(next) + return next + }, outerAtRule) + + parent.insertBefore(nodes[0], outerAtRule) + + // remove nodes + nodes.forEach(node => { + node.parent = undefined + }) + + // better output + nodes[0].raws.before = nodes[0].raws.before || "\n" + + // wrap new rules with media query and/or layer at rule + innerAtRule.append(nodes) + + stmt.type = "nodes" + stmt.nodes = [outerAtRule] + delete stmt.node + }) +} diff --git a/lib/apply-raws.js b/lib/apply-raws.js new file mode 100644 index 00000000..12caaa84 --- /dev/null +++ b/lib/apply-raws.js @@ -0,0 +1,15 @@ +"use strict" + +module.exports = function applyRaws(bundle) { + bundle.forEach((stmt, index) => { + if (index === 0) return + + if (stmt.parent) { + const { before } = stmt.parent.node.raws + if (stmt.type === "nodes") stmt.nodes[0].raws.before = before + else stmt.node.raws.before = before + } else if (stmt.type === "nodes") { + stmt.nodes[0].raws.before = stmt.nodes[0].raws.before || "\n" + } + }) +} diff --git a/lib/apply-styles.js b/lib/apply-styles.js new file mode 100644 index 00000000..a92e7ff0 --- /dev/null +++ b/lib/apply-styles.js @@ -0,0 +1,18 @@ +"use strict" + +module.exports = function applyStyles(bundle, styles) { + styles.nodes = [] + + // Strip additional statements. + bundle.forEach(stmt => { + if (["charset", "import", "layer"].includes(stmt.type)) { + stmt.node.parent = undefined + styles.append(stmt.node) + } else if (stmt.type === "nodes") { + stmt.nodes.forEach(node => { + node.parent = undefined + styles.append(node) + }) + } + }) +} diff --git a/lib/base64-encoded-import.js b/lib/base64-encoded-import.js new file mode 100644 index 00000000..5e49779c --- /dev/null +++ b/lib/base64-encoded-import.js @@ -0,0 +1,32 @@ +"use strict" + +const formatImportPrelude = require("./format-import-prelude") + +// Base64 encode an import with conditions +// The order of conditions is important and is interleaved with cascade layer declarations +// Each group of conditions and cascade layers needs to be interpreted in order +// To achieve this we create a list of base64 encoded imports, where each import contains a stylesheet with another import. +// Each import can define a single group of conditions and a single cascade layer. +module.exports = function base64EncodedConditionalImport(prelude, conditions) { + if (!conditions?.length) return prelude + + conditions.reverse() + const first = conditions.pop() + let params = `${prelude} ${formatImportPrelude( + first.layer, + first.media, + first.supports, + )}` + + for (const condition of conditions) { + params = `'data:text/css;base64,${Buffer.from(`@import ${params}`).toString( + "base64", + )}' ${formatImportPrelude( + condition.layer, + condition.media, + condition.supports, + )}` + } + + return params +} diff --git a/lib/data-url.js b/lib/data-url.js new file mode 100644 index 00000000..69c4ce94 --- /dev/null +++ b/lib/data-url.js @@ -0,0 +1,29 @@ +"use strict" + +const anyDataURLRegexp = /^data:text\/css(?:;(base64|plain))?,/i +const base64DataURLRegexp = /^data:text\/css;base64,/i +const plainDataURLRegexp = /^data:text\/css;plain,/i + +function isValid(url) { + return anyDataURLRegexp.test(url) +} + +function contents(url) { + if (base64DataURLRegexp.test(url)) { + // "data:text/css;base64,".length === 21 + return Buffer.from(url.slice(21), "base64").toString() + } + + if (plainDataURLRegexp.test(url)) { + // "data:text/css;plain,".length === 20 + return decodeURIComponent(url.slice(20)) + } + + // "data:text/css,".length === 14 + return decodeURIComponent(url.slice(14)) +} + +module.exports = { + isValid, + contents, +} diff --git a/lib/format-import-prelude.js b/lib/format-import-prelude.js new file mode 100644 index 00000000..9003442c --- /dev/null +++ b/lib/format-import-prelude.js @@ -0,0 +1,24 @@ +"use strict" + +module.exports = function formatImportPrelude(layer, media, supports) { + const parts = [] + + if (typeof layer !== "undefined") { + let layerParams = "layer" + if (layer) { + layerParams = `layer(${layer})` + } + + parts.push(layerParams) + } + + if (typeof supports !== "undefined") { + parts.push(`supports(${supports})`) + } + + if (typeof media !== "undefined") { + parts.push(media) + } + + return parts.join(" ") +} diff --git a/lib/load-content.js b/lib/load-content.js new file mode 100644 index 00000000..d138afd8 --- /dev/null +++ b/lib/load-content.js @@ -0,0 +1,12 @@ +"use strict" + +const readCache = require("read-cache") +const dataURL = require("./data-url") + +module.exports = function loadContent(filename) { + if (dataURL.isValid(filename)) { + return dataURL.contents(filename) + } + + return readCache(filename, "utf-8") +} diff --git a/lib/parse-statements.js b/lib/parse-statements.js new file mode 100644 index 00000000..3df0bad0 --- /dev/null +++ b/lib/parse-statements.js @@ -0,0 +1,253 @@ +"use strict" + +// external tooling +const valueParser = require("postcss-value-parser") + +// extended tooling +const { stringify } = valueParser + +module.exports = function parseStatements(result, styles, conditions, from) { + const statements = [] + let nodes = [] + let encounteredNonImportNodes = false + + styles.each(node => { + let stmt + if (node.type === "atrule") { + if (node.name === "import") + stmt = parseImport(result, node, conditions, from) + else if (node.name === "charset") + stmt = parseCharset(result, node, conditions, from) + else if ( + node.name === "layer" && + !encounteredNonImportNodes && + !node.nodes + ) + stmt = parseLayer(result, node, conditions, from) + } else if (node.type !== "comment") { + encounteredNonImportNodes = true + } + + if (stmt) { + if (nodes.length) { + statements.push({ + type: "nodes", + nodes, + conditions: [...conditions], + from, + }) + nodes = [] + } + statements.push(stmt) + } else nodes.push(node) + }) + + if (nodes.length) { + statements.push({ + type: "nodes", + nodes, + conditions: [...conditions], + from, + }) + } + + return statements +} + +function parseCharset(result, atRule, conditions, from) { + if (atRule.prev()) { + return result.warn("@charset must precede all other statements", { + node: atRule, + }) + } + return { + type: "charset", + node: atRule, + conditions: [...conditions], + from, + } +} + +function parseImport(result, atRule, conditions, from) { + let prev = atRule.prev() + + // `@import` statements may follow other `@import` statements. + if (prev) { + do { + if ( + prev.type === "comment" || + (prev.type === "atrule" && prev.name === "import") + ) { + prev = prev.prev() + continue + } + + break + } while (prev) + } + + // All `@import` statements may be preceded by `@charset` or `@layer` statements. + // But the `@import` statements must be consecutive. + if (prev) { + do { + if ( + prev.type === "comment" || + (prev.type === "atrule" && + (prev.name === "charset" || (prev.name === "layer" && !prev.nodes))) + ) { + prev = prev.prev() + continue + } + + return result.warn( + "@import must precede all other statements (besides @charset or empty @layer)", + { node: atRule }, + ) + } while (prev) + } + + if (atRule.nodes) { + return result.warn( + "It looks like you didn't end your @import statement correctly. " + + "Child nodes are attached to it.", + { node: atRule }, + ) + } + + const params = valueParser(atRule.params).nodes + const stmt = { + type: "import", + uri: "", + fullUri: "", + node: atRule, + conditions: [...conditions], + from, + } + + let layer + let media + let supports + + for (let i = 0; i < params.length; i++) { + const node = params[i] + + if (node.type === "space" || node.type === "comment") continue + + if (node.type === "string") { + if (stmt.uri) { + return result.warn(`Multiple url's in '${atRule.toString()}'`, { + node: atRule, + }) + } + + if (!node.value) { + return result.warn(`Unable to find uri in '${atRule.toString()}'`, { + node: atRule, + }) + } + + stmt.uri = node.value + stmt.fullUri = stringify(node) + continue + } + + if (node.type === "function" && /^url$/i.test(node.value)) { + if (stmt.uri) { + return result.warn(`Multiple url's in '${atRule.toString()}'`, { + node: atRule, + }) + } + + if (!node.nodes?.[0]?.value) { + return result.warn(`Unable to find uri in '${atRule.toString()}'`, { + node: atRule, + }) + } + + stmt.uri = node.nodes[0].value + stmt.fullUri = stringify(node) + continue + } + + if (!stmt.uri) { + return result.warn(`Unable to find uri in '${atRule.toString()}'`, { + node: atRule, + }) + } + + if ( + (node.type === "word" || node.type === "function") && + /^layer$/i.test(node.value) + ) { + if (typeof layer !== "undefined") { + return result.warn(`Multiple layers in '${atRule.toString()}'`, { + node: atRule, + }) + } + + if (typeof supports !== "undefined") { + return result.warn( + `layers must be defined before support conditions in '${atRule.toString()}'`, + { + node: atRule, + }, + ) + } + + if (node.nodes) { + layer = stringify(node.nodes) + } else { + layer = "" + } + + continue + } + + if (node.type === "function" && /^supports$/i.test(node.value)) { + if (typeof supports !== "undefined") { + return result.warn( + `Multiple support conditions in '${atRule.toString()}'`, + { + node: atRule, + }, + ) + } + + supports = stringify(node.nodes) + + continue + } + + media = stringify(params.slice(i)) + break + } + + if (!stmt.uri) { + return result.warn(`Unable to find uri in '${atRule.toString()}'`, { + node: atRule, + }) + } + + if ( + typeof media !== "undefined" || + typeof layer !== "undefined" || + typeof supports !== "undefined" + ) { + stmt.conditions.push({ + layer, + media, + supports, + }) + } + + return stmt +} + +function parseLayer(result, atRule, conditions, from) { + return { + type: "layer", + node: atRule, + conditions: [...conditions], + from, + } +} diff --git a/lib/parse-styles.js b/lib/parse-styles.js new file mode 100644 index 00000000..766de9c2 --- /dev/null +++ b/lib/parse-styles.js @@ -0,0 +1,243 @@ +"use strict" + +const path = require("path") + +const dataURL = require("./data-url") +const parseStatements = require("./parse-statements") +const processContent = require("./process-content") +const resolveId = require("./resolve-id") +const formatImportPrelude = require("./format-import-prelude") + +async function parseStyles( + result, + styles, + options, + state, + conditions, + from, + postcss, +) { + const statements = parseStatements(result, styles, conditions, from) + + for (const stmt of statements) { + if (stmt.type !== "import" || !isProcessableURL(stmt.uri)) { + continue + } + + if (options.filter && !options.filter(stmt.uri)) { + // rejected by filter + continue + } + + await resolveImportId(result, stmt, options, state, postcss) + } + + let charset + const beforeBundle = [] + const bundle = [] + + function handleCharset(stmt) { + if (!charset) charset = stmt + // charsets aren't case-sensitive, so convert to lower case to compare + else if ( + stmt.node.params.toLowerCase() !== charset.node.params.toLowerCase() + ) { + throw stmt.node.error( + `Incompatible @charset statements: + ${stmt.node.params} specified in ${stmt.node.source.input.file} + ${charset.node.params} specified in ${charset.node.source.input.file}`, + ) + } + } + + // squash statements and their children + statements.forEach(stmt => { + if (stmt.type === "charset") handleCharset(stmt) + else if (stmt.type === "import") { + if (stmt.children) { + stmt.children.forEach((child, index) => { + if (child.type === "import") beforeBundle.push(child) + else if (child.type === "layer") beforeBundle.push(child) + else if (child.type === "charset") handleCharset(child) + else bundle.push(child) + // For better output + if (index === 0) child.parent = stmt + }) + } else beforeBundle.push(stmt) + } else if (stmt.type === "layer") { + beforeBundle.push(stmt) + } else if (stmt.type === "nodes") { + bundle.push(stmt) + } + }) + + return charset + ? [charset, ...beforeBundle.concat(bundle)] + : beforeBundle.concat(bundle) +} + +async function resolveImportId(result, stmt, options, state, postcss) { + if (dataURL.isValid(stmt.uri)) { + // eslint-disable-next-line require-atomic-updates + stmt.children = await loadImportContent( + result, + stmt, + stmt.uri, + options, + state, + postcss, + ) + + return + } else if (dataURL.isValid(stmt.from.slice(-1))) { + // Data urls can't be used as a base url to resolve imports. + throw stmt.node.error( + `Unable to import '${stmt.uri}' from a stylesheet that is embedded in a data url`, + ) + } + + const atRule = stmt.node + let sourceFile + if (atRule.source?.input?.file) { + sourceFile = atRule.source.input.file + } + const base = sourceFile + ? path.dirname(atRule.source.input.file) + : options.root + + const paths = [await options.resolve(stmt.uri, base, options, atRule)].flat() + + // Ensure that each path is absolute: + const resolved = await Promise.all( + paths.map(file => { + return !path.isAbsolute(file) + ? resolveId(file, base, options, atRule) + : file + }), + ) + + // Add dependency messages: + resolved.forEach(file => { + result.messages.push({ + type: "dependency", + plugin: "postcss-import", + file, + parent: sourceFile, + }) + }) + + const importedContent = await Promise.all( + resolved.map(file => { + return loadImportContent(result, stmt, file, options, state, postcss) + }), + ) + + // Merge loaded statements + // eslint-disable-next-line require-atomic-updates + stmt.children = importedContent.flat().filter(x => !!x) +} + +async function loadImportContent( + result, + stmt, + filename, + options, + state, + postcss, +) { + const atRule = stmt.node + const { conditions, from } = stmt + const stmtDuplicateCheckKey = conditions + .map(condition => + formatImportPrelude(condition.layer, condition.media, condition.supports), + ) + .join(":") + + if (options.skipDuplicates) { + // skip files already imported at the same scope + if (state.importedFiles[filename]?.[stmtDuplicateCheckKey]) { + return + } + + // save imported files to skip them next time + if (!state.importedFiles[filename]) { + state.importedFiles[filename] = {} + } + state.importedFiles[filename][stmtDuplicateCheckKey] = true + } + + if (from.includes(filename)) { + return + } + + const content = await options.load(filename, options) + + if (content.trim() === "" && options.warnOnEmpty) { + result.warn(`${filename} is empty`, { node: atRule }) + return + } + + // skip previous imported files not containing @import rules + if ( + options.skipDuplicates && + state.hashFiles[content]?.[stmtDuplicateCheckKey] + ) { + return + } + + const importedResult = await processContent( + result, + content, + filename, + options, + postcss, + ) + + const styles = importedResult.root + result.messages = result.messages.concat(importedResult.messages) + + if (options.skipDuplicates) { + const hasImport = styles.some(child => { + return child.type === "atrule" && child.name === "import" + }) + if (!hasImport) { + // save hash files to skip them next time + if (!state.hashFiles[content]) { + state.hashFiles[content] = {} + } + + state.hashFiles[content][stmtDuplicateCheckKey] = true + } + } + + // recursion: import @import from imported file + return parseStyles( + result, + styles, + options, + state, + conditions, + [...from, filename], + postcss, + ) +} + +function isProcessableURL(uri) { + // skip protocol base uri (protocol://url) or protocol-relative + if (/^(?:[a-z]+:)?\/\//i.test(uri)) { + return false + } + + // check for fragment or query + try { + // needs a base to parse properly + const url = new URL(uri, "https://example.com") + if (url.search) { + return false + } + } catch {} // Ignore + + return true +} + +module.exports = parseStyles diff --git a/lib/process-content.js b/lib/process-content.js new file mode 100644 index 00000000..a0484039 --- /dev/null +++ b/lib/process-content.js @@ -0,0 +1,60 @@ +"use strict" + +// builtin tooling +const path = require("path") + +// placeholder tooling +let sugarss + +module.exports = function processContent( + result, + content, + filename, + options, + postcss, +) { + const { plugins } = options + const ext = path.extname(filename) + + const parserList = [] + + // SugarSS support: + if (ext === ".sss") { + if (!sugarss) { + /* c8 ignore next 3 */ + try { + sugarss = require("sugarss") + } catch {} // Ignore + } + if (sugarss) + return runPostcss(postcss, content, filename, plugins, [sugarss]) + } + + // Syntax support: + if (result.opts.syntax?.parse) { + parserList.push(result.opts.syntax.parse) + } + + // Parser support: + if (result.opts.parser) parserList.push(result.opts.parser) + // Try the default as a last resort: + parserList.push(null) + + return runPostcss(postcss, content, filename, plugins, parserList) +} + +function runPostcss(postcss, content, filename, plugins, parsers, index) { + if (!index) index = 0 + return postcss(plugins) + .process(content, { + from: filename, + parser: parsers[index], + }) + .catch(err => { + // If there's an error, try the next parser + index++ + // If there are no parsers left, throw it + if (index === parsers.length) throw err + return runPostcss(postcss, content, filename, plugins, parsers, index) + }) +} diff --git a/lib/resolve-id.js b/lib/resolve-id.js new file mode 100644 index 00000000..3929a957 --- /dev/null +++ b/lib/resolve-id.js @@ -0,0 +1,42 @@ +"use strict" + +// external tooling +const resolve = require("resolve") + +const moduleDirectories = ["web_modules", "node_modules"] + +function resolveModule(id, opts) { + return new Promise((res, rej) => { + resolve(id, opts, (err, path) => (err ? rej(err) : res(path))) + }) +} + +module.exports = function resolveId(id, base, options, node) { + const paths = options.path + + const resolveOpts = { + basedir: base, + moduleDirectory: moduleDirectories.concat(options.addModulesDirectories), + paths, + extensions: [".css"], + packageFilter: function processPackage(pkg) { + if (pkg.style) pkg.main = pkg.style + else if (!pkg.main || !/\.css$/.test(pkg.main)) pkg.main = "index.css" + return pkg + }, + preserveSymlinks: false, + } + + return resolveModule(`./${id}`, resolveOpts) + .catch(() => resolveModule(id, resolveOpts)) + .catch(() => { + if (paths.indexOf(base) === -1) paths.unshift(base) + + throw node.error( + `Failed to find '${id}' + in [ + ${paths.join(",\n ")} + ]`, + ) + }) +} diff --git a/package.json b/package.json index 81aebe4b..174c42a9 100644 --- a/package.json +++ b/package.json @@ -1,41 +1,49 @@ { "name": "postcss-import", - "version": "6.0.0", + "version": "16.1.1", "description": "PostCSS plugin to import CSS files", "keywords": [ "css", "postcss", - "postcss-plugins", + "postcss-plugin", "import", "node modules", "npm" ], "author": "Maxime Thirouin", "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/postcss/postcss-import.git" - }, + "repository": "https://github.com/postcss/postcss-import.git", "files": [ - "CHANGELOG.md", - "LICENSE", - "index.js" + "index.js", + "lib" ], + "engines": { + "node": ">=18.0.0" + }, "dependencies": { - "clone": "^0.1.17", - "glob": "^5.0.1", - "object-assign": "^3.0.0", - "postcss": "^4.1.4", - "postcss-message-helpers": "^2.0.0", - "resolve": "^1.0.0", - "string-hash": "^1.1.0" + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" }, "devDependencies": { - "css-whitespace": "^1.1.0", - "eslint": "^0.23.0", - "tape": "^3.0.0" + "ava": "^6.0.0", + "c8": "^10.0.0", + "eslint": "^9.28.0", + "eslint-config-problems": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "globals": "^16.2.0", + "postcss": "^8.0.0", + "postcss-scss": "^4.0.0", + "prettier": "~3.6.0", + "sugarss": "^5.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" }, "scripts": { - "test": "eslint . && tape test" + "ci": "eslint . && ava", + "lint": "eslint . --fix", + "pretest": "npm run lint", + "test": "c8 ava" } } diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..98372bff --- /dev/null +++ b/renovate.json @@ -0,0 +1,7 @@ +{ + "extends": [ + "config:recommended", + ":preserveSemverRanges", + ":label(deps)" + ] +} diff --git a/test/custom-load.js b/test/custom-load.js new file mode 100644 index 00000000..872b15a7 --- /dev/null +++ b/test/custom-load.js @@ -0,0 +1,14 @@ +"use strict" +// external tooling +const test = require("ava") + +// internal tooling +const checkFixture = require("./helpers/check-fixture") + +test.serial("should accept content", checkFixture, "custom-load", { + load: () => "custom-content {}", +}) + +test.serial("should accept promised content", checkFixture, "custom-load", { + load: () => Promise.resolve("custom-content {}"), +}) diff --git a/test/custom-resolve.js b/test/custom-resolve.js new file mode 100644 index 00000000..094d3831 --- /dev/null +++ b/test/custom-resolve.js @@ -0,0 +1,77 @@ +"use strict" +// builtin tooling +const path = require("path") + +// external tooling +const test = require("ava") +const postcss = require("postcss") + +// plugin +const atImport = require("..") + +// internal tooling +const checkFixture = require("./helpers/check-fixture") + +test.serial("should accept file", checkFixture, "custom-resolve-file", { + resolve: () => path.resolve("test/fixtures/imports/custom-resolve-1.css"), +}) + +test.serial( + "should accept promised file", + checkFixture, + "custom-resolve-file", + { + resolve: () => { + return Promise.resolve( + path.resolve("test/fixtures/imports/custom-resolve-1.css"), + ) + }, + }, +) + +test.serial( + "should accept array of files", + checkFixture, + "custom-resolve-array", + { + resolve: () => { + return [ + path.resolve("test/fixtures/imports/custom-resolve-1.css"), + path.resolve("test/fixtures/imports/custom-resolve-2.css"), + path.resolve("test/fixtures/imports/custom-resolve-1.css"), + ] + }, + }, +) + +test.serial( + "should accept promised array of files", + checkFixture, + "custom-resolve-array", + { + resolve: () => { + return Promise.resolve([ + path.resolve("test/fixtures/imports/custom-resolve-1.css"), + path.resolve("test/fixtures/imports/custom-resolve-2.css"), + path.resolve("test/fixtures/imports/custom-resolve-1.css"), + ]) + }, + }, +) + +test("should apply default resolver when custom doesn't return an absolute path", t => { + return postcss() + .use( + atImport({ + resolve: path => path.replace("foo", "imports/bar"), + load: p => { + t.is(p, path.resolve("test/fixtures/imports", "bar.css")) + return "/* comment */" + }, + }), + ) + .process(`@import "foo.css";`, { + from: "test/fixtures/custom-resolve-file", + }) + .then(result => t.is(result.css, "/* comment */")) +}) diff --git a/test/custom-syntax-parser.js b/test/custom-syntax-parser.js new file mode 100644 index 00000000..aa32504e --- /dev/null +++ b/test/custom-syntax-parser.js @@ -0,0 +1,52 @@ +"use strict" +// external tooling +const test = require("ava") +const scss = require("postcss-scss") +const sugarss = require("sugarss") + +// internal tooling +const checkFixture = require("./helpers/check-fixture") + +test("should process custom syntax", checkFixture, "scss-syntax", null, { + syntax: scss, +}) + +test( + "should process custom syntax by parser", + checkFixture, + "scss-parser", + null, + { parser: scss }, +) + +test(".css importing .sss should work", checkFixture, "import-sss") + +test( + ".sss importing .sss should work", + checkFixture, + { name: "sugar", ext: ".sss" }, + null, + { parser: sugarss }, +) + +test( + ".sss importing .css should work", + checkFixture, + { name: "sugar-import-css", ext: ".sss" }, + null, + { parser: sugarss }, +) + +test( + ".css importing .sss importing .css should work", + checkFixture, + "import-sss-css", +) + +test( + ".sss importing .css importing .sss should work", + checkFixture, + { name: "import-css-sss", ext: ".sss" }, + null, + { parser: sugarss }, +) diff --git a/test/data-url.js b/test/data-url.js new file mode 100644 index 00000000..353b97b5 --- /dev/null +++ b/test/data-url.js @@ -0,0 +1,27 @@ +"use strict" +// external tooling +const test = require("ava") +const postcss = require("postcss") + +// plugin +const atImport = require("..") + +// internal tooling +const checkFixture = require("./helpers/check-fixture") + +test("should inline data urls", checkFixture, "data-url") + +test("should error on relative urls from stylesheets in data urls", t => { + return postcss() + .use(atImport()) + .process( + "@import url(data:text/css;base64,QGltcG9ydCB1cmwoZm9vLmNzcyk7CgpwIHsKICBjb2xvcjogYmx1ZTsKfQo=);", + { from: undefined }, + ) + .catch(error => + t.regex( + error.message, + /Unable to import '(?:.*?)' from a stylesheet that is embedded in a data url/, + ), + ) +}) diff --git a/test/filter.js b/test/filter.js new file mode 100644 index 00000000..1e2f70d6 --- /dev/null +++ b/test/filter.js @@ -0,0 +1,18 @@ +"use strict" +// external tooling +const test = require("ava") + +// internal tooling +const checkFixture = require("./helpers/check-fixture") + +test("should filter all imported stylesheets", checkFixture, "filter-all", { + filter: () => false, +}) + +test("should filter some stylesheets", checkFixture, "filter-some", { + filter: url => url !== "foobar.css", +}) + +test("shouldn't accept ignored stylesheets", checkFixture, "filter-ignore", { + filter: () => true, +}) diff --git a/test/fixtures/charset-error.css b/test/fixtures/charset-error.css new file mode 100644 index 00000000..3456d4a5 --- /dev/null +++ b/test/fixtures/charset-error.css @@ -0,0 +1,2 @@ +@charset "foobar"; +@import "imports/charset.css"; diff --git a/test/fixtures/charset-import.css b/test/fixtures/charset-import.css new file mode 100644 index 00000000..f06162a3 --- /dev/null +++ b/test/fixtures/charset-import.css @@ -0,0 +1,4 @@ +@charset "UTF-8"; +@import "test/fixtures/imports/foo.css"; +@import "test/fixtures/imports/charset.css"; +bar{} diff --git a/test/fixtures/charset-import.expected.css b/test/fixtures/charset-import.expected.css new file mode 100644 index 00000000..5262c38e --- /dev/null +++ b/test/fixtures/charset-import.expected.css @@ -0,0 +1,3 @@ +@charset "UTF-8"; +foo{} +bar{} diff --git a/test/fixtures/custom-load.css b/test/fixtures/custom-load.css new file mode 100644 index 00000000..0a2e522a --- /dev/null +++ b/test/fixtures/custom-load.css @@ -0,0 +1 @@ +@import "foo" diff --git a/test/fixtures/custom-load.expected.css b/test/fixtures/custom-load.expected.css new file mode 100644 index 00000000..e36a838c --- /dev/null +++ b/test/fixtures/custom-load.expected.css @@ -0,0 +1 @@ +custom-content {} diff --git a/test/fixtures/custom-resolve-array.css b/test/fixtures/custom-resolve-array.css new file mode 100644 index 00000000..f611628b --- /dev/null +++ b/test/fixtures/custom-resolve-array.css @@ -0,0 +1 @@ +@import "any-path"; diff --git a/test/fixtures/custom-resolve-array.expected.css b/test/fixtures/custom-resolve-array.expected.css new file mode 100644 index 00000000..a21c4ad8 --- /dev/null +++ b/test/fixtures/custom-resolve-array.expected.css @@ -0,0 +1,2 @@ +custom-resolve-1 {} +custom-resolve-2 {} diff --git a/test/fixtures/custom-resolve-file.css b/test/fixtures/custom-resolve-file.css new file mode 100644 index 00000000..f611628b --- /dev/null +++ b/test/fixtures/custom-resolve-file.css @@ -0,0 +1 @@ +@import "any-path"; diff --git a/test/fixtures/custom-resolve-file.expected.css b/test/fixtures/custom-resolve-file.expected.css new file mode 100644 index 00000000..aed3f77d --- /dev/null +++ b/test/fixtures/custom-resolve-file.expected.css @@ -0,0 +1 @@ +custom-resolve-1 {} diff --git a/test/fixtures/custom-resolve-modules.css b/test/fixtures/custom-resolve-modules.css deleted file mode 100644 index fe166c6e..00000000 --- a/test/fixtures/custom-resolve-modules.css +++ /dev/null @@ -1,3 +0,0 @@ -@import "sass-module"; - -content{} diff --git a/test/fixtures/custom-resolve-modules.expected.css b/test/fixtures/custom-resolve-modules.expected.css deleted file mode 100644 index 2922e174..00000000 --- a/test/fixtures/custom-resolve-modules.expected.css +++ /dev/null @@ -1,3 +0,0 @@ -.sass-module{} - -content{} diff --git a/test/fixtures/cwd.expected.css b/test/fixtures/cwd.expected.css deleted file mode 100755 index ab23ff91..00000000 --- a/test/fixtures/cwd.expected.css +++ /dev/null @@ -1,4 +0,0 @@ -foo{} -bar{} - -foo.recursive{} \ No newline at end of file diff --git a/test/fixtures/cyclical-skip-duplicates.css b/test/fixtures/cyclical-skip-duplicates.css new file mode 100644 index 00000000..4dc4d4a8 --- /dev/null +++ b/test/fixtures/cyclical-skip-duplicates.css @@ -0,0 +1,4 @@ +@import "cyclical-a.css"; +@import "cyclical-b.css"; + +@import "cyclical-screen.css" screen; diff --git a/test/fixtures/cyclical-skip-duplicates.expected.css b/test/fixtures/cyclical-skip-duplicates.expected.css new file mode 100644 index 00000000..ab8eac83 --- /dev/null +++ b/test/fixtures/cyclical-skip-duplicates.expected.css @@ -0,0 +1,26 @@ + + +.b { + color: red; +} + +.a { + color: blue; +} + +@media screen { + + @media all { + +.a { + color: aquamarine; +} + } +} + +@media screen { + +.a { + color: pink; +} +} diff --git a/test/fixtures/cyclical.css b/test/fixtures/cyclical.css new file mode 100644 index 00000000..4dc4d4a8 --- /dev/null +++ b/test/fixtures/cyclical.css @@ -0,0 +1,4 @@ +@import "cyclical-a.css"; +@import "cyclical-b.css"; + +@import "cyclical-screen.css" screen; diff --git a/test/fixtures/cyclical.expected.css b/test/fixtures/cyclical.expected.css new file mode 100644 index 00000000..819ad6be --- /dev/null +++ b/test/fixtures/cyclical.expected.css @@ -0,0 +1,34 @@ + + +.b { + color: red; +} + +.a { + color: blue; +} + +.a { + color: blue; +} + +.b { + color: red; +} + +@media screen { + + @media all { + +.a { + color: aquamarine; +} + } +} + +@media screen { + +.a { + color: pink; +} +} diff --git a/test/fixtures/data-url.css b/test/fixtures/data-url.css new file mode 100644 index 00000000..c26bdc03 --- /dev/null +++ b/test/fixtures/data-url.css @@ -0,0 +1,9 @@ +@import url(data:text/css;base64,QGltcG9ydCB1cmwoZGF0YTp0ZXh0L2NzcztiYXNlNjQsY0NCN0lHTnZiRzl5T2lCbmNtVmxianNnZlE9PSk7CgpwIHsgY29sb3I6IGJsdWU7IH0K); +@import url("DATA:TEXT/CSS;BASE64,QGltcG9ydCB1cmwoZGF0YTp0ZXh0L2NzcztiYXNlNjQsY0NCN0lHTnZiRzl5T2lCbmNtVmxianNnZlE9PSk7CgpwIHsgY29sb3I6IGJsdWU7IH0K") layer(foo) (min-width: 320px); + +/* Mixed imports: */ +@import url(data-url.css); + +/* url encoded: */ +@import url(data:text/css;plain,bar%20%7B%20color%3A%20green%20%7D); +@import url(data:text/css,bar%20%7B%20color%3A%20pink%20%7D); diff --git a/test/fixtures/data-url.expected.css b/test/fixtures/data-url.expected.css new file mode 100644 index 00000000..cad24950 --- /dev/null +++ b/test/fixtures/data-url.expected.css @@ -0,0 +1,24 @@ +p { color: green; } + +p { color: blue; } + +@media (min-width: 320px) { + + @layer foo { +p { color: green; } } } + +@media (min-width: 320px) { + + @layer foo { + +p { color: blue; } } } + +/* Mixed imports: */ + +p { color: pink; } + +/* url encoded: */ + +bar { color: green } + +bar { color: pink } diff --git a/test/fixtures/duplicates.css b/test/fixtures/duplicates.css new file mode 100644 index 00000000..95642af8 --- /dev/null +++ b/test/fixtures/duplicates.css @@ -0,0 +1,11 @@ +@import "foo.css"; +@import "foo.css"; +@import "foo-duplicate.css"; + +@import "foo.css" screen; +@import "foo-duplicate2" screen; + +@import "proxy-file/index.css"; +@import "proxy-file/sub-directory/index.css"; + +content{} diff --git a/test/fixtures/duplicates.expected.css b/test/fixtures/duplicates.expected.css new file mode 100644 index 00000000..f736160e --- /dev/null +++ b/test/fixtures/duplicates.expected.css @@ -0,0 +1,12 @@ +foo{} +foo{} +foo{} +@media screen{ +foo{} +} +@media screen{ +foo{} +} +proxy {} +import {} +content{} diff --git a/test/fixtures/empty-and-useless.expected.css b/test/fixtures/empty-and-useless.expected.css index c001610e..082e4c09 100644 --- a/test/fixtures/empty-and-useless.expected.css +++ b/test/fixtures/empty-and-useless.expected.css @@ -1,3 +1,2 @@ - /* useless */ diff --git a/test/fixtures/filter-all.css b/test/fixtures/filter-all.css new file mode 100644 index 00000000..3d304f24 --- /dev/null +++ b/test/fixtures/filter-all.css @@ -0,0 +1,12 @@ +@import "foo.css"; +@import "foo.css" screen; +@import 'bar.css'; +@import 'bar.css' screen; +@import url(baz.css); +@import url(baz.css) screen; +@import url("foobar.css"); +@import url("foobar.css") screen and (min-width: 25em); +@import url('foobarbaz.css'); +@import url('foobarbaz.css') print, screen and (min-width: 25em); + +content{} diff --git a/test/fixtures/filter-all.expected.css b/test/fixtures/filter-all.expected.css new file mode 100644 index 00000000..1cbbdbe4 --- /dev/null +++ b/test/fixtures/filter-all.expected.css @@ -0,0 +1,11 @@ +@import "foo.css"; +@import "foo.css" screen; +@import 'bar.css'; +@import 'bar.css' screen; +@import url(baz.css); +@import url(baz.css) screen; +@import url("foobar.css"); +@import url("foobar.css") screen and (min-width: 25em); +@import url('foobarbaz.css'); +@import url('foobarbaz.css') print, screen and (min-width: 25em); +content{} diff --git a/test/fixtures/filter-ignore.css b/test/fixtures/filter-ignore.css new file mode 100644 index 00000000..f4099c80 --- /dev/null +++ b/test/fixtures/filter-ignore.css @@ -0,0 +1,16 @@ +@import "ignore.css" (min-width: 25em); +@import "http://css"; +@import "https://css"; +@import 'http://css'; +@import 'https://css'; +@import url(http://css); +@import url(https://css); +@import url("http://css"); +@import url("https://css"); +@import url('http://css'); +@import url('https://css'); +@import url("//css"); +@import url('//css'); +@import url(//css); +@import url('foo.css?query=string'); +content{} diff --git a/test/fixtures/filter-ignore.expected.css b/test/fixtures/filter-ignore.expected.css new file mode 100644 index 00000000..f16a2d75 --- /dev/null +++ b/test/fixtures/filter-ignore.expected.css @@ -0,0 +1,20 @@ +@import "http://css" (min-width: 25em); +@import 'data:text/css;base64,QGltcG9ydCAiaHR0cDovL2Nzcy1zY3JlZW4iIChtaW4td2lkdGg6IDI1ZW0p' screen; +@import "http://css"; +@import "https://css"; +@import 'http://css'; +@import 'https://css'; +@import url(http://css); +@import url(https://css); +@import url("http://css"); +@import url("https://css"); +@import url('http://css'); +@import url('https://css'); +@import url("//css"); +@import url('//css'); +@import url(//css); +@import url('foo.css?query=string'); +@media (min-width: 25em){ +ignore{} +} +content{} diff --git a/test/fixtures/filter-some.css b/test/fixtures/filter-some.css new file mode 100644 index 00000000..3d304f24 --- /dev/null +++ b/test/fixtures/filter-some.css @@ -0,0 +1,12 @@ +@import "foo.css"; +@import "foo.css" screen; +@import 'bar.css'; +@import 'bar.css' screen; +@import url(baz.css); +@import url(baz.css) screen; +@import url("foobar.css"); +@import url("foobar.css") screen and (min-width: 25em); +@import url('foobarbaz.css'); +@import url('foobarbaz.css') print, screen and (min-width: 25em); + +content{} diff --git a/test/fixtures/filter-some.expected.css b/test/fixtures/filter-some.expected.css new file mode 100644 index 00000000..aa4dcf69 --- /dev/null +++ b/test/fixtures/filter-some.expected.css @@ -0,0 +1,20 @@ + +@import url("foobar.css"); +@import url("foobar.css") screen and (min-width: 25em); +foo{} +@media screen{ +foo{} +} +bar{} +@media screen{ +bar{} +} +baz{} +@media screen{ +baz{} +} +foobarbaz{} +@media print, screen and (min-width: 25em){ +foobarbaz{} +} +content{} diff --git a/test/fixtures/glob-alt.css b/test/fixtures/glob-alt.css deleted file mode 100644 index 2ab8edc3..00000000 --- a/test/fixtures/glob-alt.css +++ /dev/null @@ -1 +0,0 @@ -@import url('./foobar*.css'); diff --git a/test/fixtures/glob-alt.expected.css b/test/fixtures/glob-alt.expected.css deleted file mode 100644 index 3c6dd4b1..00000000 --- a/test/fixtures/glob-alt.expected.css +++ /dev/null @@ -1,2 +0,0 @@ -foobar{} -foobarbaz{} diff --git a/test/fixtures/glob.css b/test/fixtures/glob.css deleted file mode 100644 index b8047be0..00000000 --- a/test/fixtures/glob.css +++ /dev/null @@ -1,2 +0,0 @@ -@import "./foobar*.css"; -@import "by-hand/*.css"; diff --git a/test/fixtures/glob.expected.css b/test/fixtures/glob.expected.css deleted file mode 100644 index f7815894..00000000 --- a/test/fixtures/glob.expected.css +++ /dev/null @@ -1,3 +0,0 @@ -foobar{} -foobarbaz{} -.byHand{} diff --git a/test/fixtures/ignore.css b/test/fixtures/ignore.css index 71a7951e..5f66c649 100644 --- a/test/fixtures/ignore.css +++ b/test/fixtures/ignore.css @@ -12,5 +12,9 @@ @import url("//css"); @import url('//css'); @import url(//css); +@import "http://css" layer; +@import "http://css" layer(bar); +@import "http://css" layer screen and (min-width: 25em), print; +@import "http://css" layer(bar) screen and (min-width: 25em), print; content{} diff --git a/test/fixtures/ignore.expected.css b/test/fixtures/ignore.expected.css index 1c7013f5..0baad264 100644 --- a/test/fixtures/ignore.expected.css +++ b/test/fixtures/ignore.expected.css @@ -1,5 +1,5 @@ @import "http://css" (min-width: 25em); -@import "http://css-screen" (min-width: 25em) and screen; +@import 'data:text/css;base64,QGltcG9ydCAiaHR0cDovL2Nzcy1zY3JlZW4iIChtaW4td2lkdGg6IDI1ZW0p' screen; @import "http://css"; @import "https://css"; @import 'http://css'; @@ -13,9 +13,11 @@ @import url("//css"); @import url('//css'); @import url(//css); - +@import "http://css" layer; +@import "http://css" layer(bar); +@import "http://css" layer screen and (min-width: 25em), print; +@import "http://css" layer(bar) screen and (min-width: 25em), print; @media (min-width: 25em){ ignore{} } - content{} diff --git a/test/fixtures/import-css-sss.expected.css b/test/fixtures/import-css-sss.expected.css new file mode 100644 index 00000000..13de4796 --- /dev/null +++ b/test/fixtures/import-css-sss.expected.css @@ -0,0 +1,9 @@ +.sugarbar{ + color: blue +} + +import.sugarbar{} + +.sugar{ + color: white +} diff --git a/test/fixtures/import-css-sss.sss b/test/fixtures/import-css-sss.sss new file mode 100644 index 00000000..1af8c35a --- /dev/null +++ b/test/fixtures/import-css-sss.sss @@ -0,0 +1,4 @@ +@import "import-sugarbar.css" + +.sugar + color: white diff --git a/test/fixtures/import-sss-css.css b/test/fixtures/import-sss-css.css new file mode 100644 index 00000000..c9b32274 --- /dev/null +++ b/test/fixtures/import-sss-css.css @@ -0,0 +1 @@ +@import "foo-recursive.sss"; diff --git a/test/fixtures/import-sss-css.expected.css b/test/fixtures/import-sss-css.expected.css new file mode 100644 index 00000000..333f411a --- /dev/null +++ b/test/fixtures/import-sss-css.expected.css @@ -0,0 +1,5 @@ +bar{} + +foo.recursive{ + color: red +} diff --git a/test/fixtures/import-sss.css b/test/fixtures/import-sss.css new file mode 100644 index 00000000..52e99911 --- /dev/null +++ b/test/fixtures/import-sss.css @@ -0,0 +1 @@ +@import "sugarbar.sss"; diff --git a/test/fixtures/import-sss.expected.css b/test/fixtures/import-sss.expected.css new file mode 100644 index 00000000..f9db5d72 --- /dev/null +++ b/test/fixtures/import-sss.expected.css @@ -0,0 +1,3 @@ +.sugarbar { + color: blue +} diff --git a/test/fixtures/imports/bar-decl.css b/test/fixtures/imports/bar-decl.css new file mode 100644 index 00000000..f00d6f94 --- /dev/null +++ b/test/fixtures/imports/bar-decl.css @@ -0,0 +1,4 @@ +body { + bar: bar; + qux: qux; +} diff --git a/test/fixtures/imports/bar/baz.css b/test/fixtures/imports/bar/baz.css new file mode 100644 index 00000000..b355e071 --- /dev/null +++ b/test/fixtures/imports/bar/baz.css @@ -0,0 +1 @@ +bar {} diff --git a/test/fixtures/imports/bar/index.css b/test/fixtures/imports/bar/index.css new file mode 100644 index 00000000..ba24e9b6 --- /dev/null +++ b/test/fixtures/imports/bar/index.css @@ -0,0 +1 @@ +@import "baz.css" diff --git a/test/fixtures/imports/charset-content.css b/test/fixtures/imports/charset-content.css new file mode 100644 index 00000000..0058c1e6 --- /dev/null +++ b/test/fixtures/imports/charset-content.css @@ -0,0 +1,2 @@ +@charset "UTF-8"; +.charset-content {} diff --git a/test/fixtures/imports/charset.css b/test/fixtures/imports/charset.css new file mode 100644 index 00000000..9f44090c --- /dev/null +++ b/test/fixtures/imports/charset.css @@ -0,0 +1 @@ +@charset "UTF-8"; diff --git a/test/fixtures/imports/custom-resolve-1.css b/test/fixtures/imports/custom-resolve-1.css new file mode 100644 index 00000000..aed3f77d --- /dev/null +++ b/test/fixtures/imports/custom-resolve-1.css @@ -0,0 +1 @@ +custom-resolve-1 {} diff --git a/test/fixtures/imports/custom-resolve-2.css b/test/fixtures/imports/custom-resolve-2.css new file mode 100644 index 00000000..d46e6fba --- /dev/null +++ b/test/fixtures/imports/custom-resolve-2.css @@ -0,0 +1 @@ +custom-resolve-2 {} diff --git a/test/fixtures/imports/cyclical-a.css b/test/fixtures/imports/cyclical-a.css new file mode 100644 index 00000000..ec5adb43 --- /dev/null +++ b/test/fixtures/imports/cyclical-a.css @@ -0,0 +1,5 @@ +@import url(cyclical-b.css); + +.a { + color: blue; +} diff --git a/test/fixtures/imports/cyclical-all.css b/test/fixtures/imports/cyclical-all.css new file mode 100644 index 00000000..f1b5ab14 --- /dev/null +++ b/test/fixtures/imports/cyclical-all.css @@ -0,0 +1,5 @@ +@import url(cyclical-screen.css) screen; + +.a { + color: aquamarine; +} diff --git a/test/fixtures/imports/cyclical-b.css b/test/fixtures/imports/cyclical-b.css new file mode 100644 index 00000000..8452916c --- /dev/null +++ b/test/fixtures/imports/cyclical-b.css @@ -0,0 +1,5 @@ +@import url(cyclical-a.css); + +.b { + color: red; +} diff --git a/test/fixtures/imports/cyclical-screen.css b/test/fixtures/imports/cyclical-screen.css new file mode 100644 index 00000000..1e51af76 --- /dev/null +++ b/test/fixtures/imports/cyclical-screen.css @@ -0,0 +1,5 @@ +@import url(cyclical-all.css) all; + +.a { + color: pink; +} diff --git a/test/fixtures/imports/data-url.css b/test/fixtures/imports/data-url.css new file mode 100644 index 00000000..d6804421 --- /dev/null +++ b/test/fixtures/imports/data-url.css @@ -0,0 +1 @@ +@import url("DATA:text/CSS;BASE64,cCB7IGNvbG9yOiBwaW5rOyB9"); diff --git a/test/fixtures/imports/foo-decl.css b/test/fixtures/imports/foo-decl.css new file mode 100644 index 00000000..0a8b6aa8 --- /dev/null +++ b/test/fixtures/imports/foo-decl.css @@ -0,0 +1,4 @@ +body { + foo: foo; + baz: baz; +} diff --git a/test/fixtures/imports/foo-first.css b/test/fixtures/imports/foo-first.css new file mode 100644 index 00000000..f66fb3e7 --- /dev/null +++ b/test/fixtures/imports/foo-first.css @@ -0,0 +1,5 @@ +@import "bar.css"; + +foo.first{ + color: red; +} diff --git a/test/fixtures/imports/foo-layered.css b/test/fixtures/imports/foo-layered.css new file mode 100755 index 00000000..30f6252e --- /dev/null +++ b/test/fixtures/imports/foo-layered.css @@ -0,0 +1,3 @@ +@layer foo { + foo {} +} diff --git a/test/fixtures/imports/foo-recursive.sss b/test/fixtures/imports/foo-recursive.sss new file mode 100644 index 00000000..0e24a764 --- /dev/null +++ b/test/fixtures/imports/foo-recursive.sss @@ -0,0 +1,4 @@ +@import "bar.css" + +foo.recursive + color: red diff --git a/test/fixtures/imports/foo-second.css b/test/fixtures/imports/foo-second.css new file mode 100644 index 00000000..0521ab8a --- /dev/null +++ b/test/fixtures/imports/foo-second.css @@ -0,0 +1,5 @@ +@import "bar.css"; + +foo.second{ + color: blue; +} diff --git a/test/fixtures/imports/foo.styl b/test/fixtures/imports/foo.styl deleted file mode 100755 index 59bb3b52..00000000 --- a/test/fixtures/imports/foo.styl +++ /dev/null @@ -1,2 +0,0 @@ -foo - styl: true diff --git a/test/fixtures/imports/foo/baz.css b/test/fixtures/imports/foo/baz.css new file mode 100644 index 00000000..bbcc7d3c --- /dev/null +++ b/test/fixtures/imports/foo/baz.css @@ -0,0 +1 @@ +foo {} diff --git a/test/fixtures/imports/foo/index.css b/test/fixtures/imports/foo/index.css new file mode 100644 index 00000000..ba24e9b6 --- /dev/null +++ b/test/fixtures/imports/foo/index.css @@ -0,0 +1 @@ +@import "baz.css" diff --git a/test/fixtures/imports/glob-missing.css b/test/fixtures/imports/glob-missing.css deleted file mode 100644 index a9a40a79..00000000 --- a/test/fixtures/imports/glob-missing.css +++ /dev/null @@ -1,2 +0,0 @@ -@import "./missing*.css"; -@import "foobar.css"; diff --git a/test/fixtures/imports/import-sugarbar.css b/test/fixtures/imports/import-sugarbar.css new file mode 100644 index 00000000..32cbada9 --- /dev/null +++ b/test/fixtures/imports/import-sugarbar.css @@ -0,0 +1,3 @@ +@import "sugarbar.sss"; + +import.sugarbar{} diff --git a/test/fixtures/imports/inline-comment.scss b/test/fixtures/imports/inline-comment.scss new file mode 100644 index 00000000..bdf8f0f7 --- /dev/null +++ b/test/fixtures/imports/inline-comment.scss @@ -0,0 +1 @@ +// inline comment diff --git a/test/fixtures/imports/layer-anonymous.css b/test/fixtures/imports/layer-anonymous.css new file mode 100644 index 00000000..589aeeb1 --- /dev/null +++ b/test/fixtures/imports/layer-anonymous.css @@ -0,0 +1,33 @@ +@import url("layer-level-2-anonymous.css") layer; + +body { + order: 1; +} + +@media (min-width: 50rem) { + body { + order: 2; + } +} + +@keyframes RED_TO_BLUE { + 0% { + background-color: red; + } + 100% { + background-color: blue; + } +} + +@supports (display: grid) { + body { + display: grid; + order: 3; + } +} + +@layer A { + body { + order: 4; + } +} diff --git a/test/fixtures/imports/layer-followed-by-ignore.css b/test/fixtures/imports/layer-followed-by-ignore.css new file mode 100644 index 00000000..13f9646c --- /dev/null +++ b/test/fixtures/imports/layer-followed-by-ignore.css @@ -0,0 +1,4 @@ +/* a comment */ + +@layer layer-alpha; +@import "http://css"; diff --git a/test/fixtures/imports/layer-level-2-anonymous.css b/test/fixtures/imports/layer-level-2-anonymous.css new file mode 100644 index 00000000..6846c756 --- /dev/null +++ b/test/fixtures/imports/layer-level-2-anonymous.css @@ -0,0 +1,9 @@ +@import "http://css" layer; +@import "http://css-bar" layer(bar); +@import url("layer-level-3.css") layer (min-width: 320px); + +@layer Y { + body { + color: purple; + } +} diff --git a/test/fixtures/imports/layer-level-2.css b/test/fixtures/imports/layer-level-2.css new file mode 100644 index 00000000..8ec5e25b --- /dev/null +++ b/test/fixtures/imports/layer-level-2.css @@ -0,0 +1,7 @@ +@import url("layer-level-3.css") layer(level-3) (min-width: 320px); + +@layer Y { + body { + color: purple; + } +} diff --git a/test/fixtures/imports/layer-level-3.css b/test/fixtures/imports/layer-level-3.css new file mode 100644 index 00000000..1567ae96 --- /dev/null +++ b/test/fixtures/imports/layer-level-3.css @@ -0,0 +1,5 @@ +@layer Z { + body { + color: cyan; + } +} diff --git a/test/fixtures/imports/layer-only.css b/test/fixtures/imports/layer-only.css new file mode 100644 index 00000000..9dc1accb --- /dev/null +++ b/test/fixtures/imports/layer-only.css @@ -0,0 +1 @@ +@layer layer-beta; diff --git a/test/fixtures/imports/layer.css b/test/fixtures/imports/layer.css new file mode 100644 index 00000000..9e554b76 --- /dev/null +++ b/test/fixtures/imports/layer.css @@ -0,0 +1,33 @@ +@import url("layer-level-2.css") layer(level-2); + +body { + order: 1; +} + +@media (min-width: 50rem) { + body { + order: 2; + } +} + +@keyframes RED_TO_BLUE { + 0% { + background-color: red; + } + 100% { + background-color: blue; + } +} + +@supports (display: grid) { + body { + display: grid; + order: 3; + } +} + +@layer A { + body { + order: 4; + } +} diff --git a/test/fixtures/imports/local-module/main.css b/test/fixtures/imports/local-module/main.css deleted file mode 100644 index 68c41250..00000000 --- a/test/fixtures/imports/local-module/main.css +++ /dev/null @@ -1 +0,0 @@ -.local-module{} diff --git a/test/fixtures/imports/local-module/package.json b/test/fixtures/imports/local-module/package.json deleted file mode 100644 index ae14fa6b..00000000 --- a/test/fixtures/imports/local-module/package.json +++ /dev/null @@ -1 +0,0 @@ -{"style": "main.css"} diff --git a/test/fixtures/imports/media-combine-nested.css b/test/fixtures/imports/media-combine-nested.css new file mode 100644 index 00000000..9aabfb5f --- /dev/null +++ b/test/fixtures/imports/media-combine-nested.css @@ -0,0 +1,5 @@ +@media one-2 {} + +@media or-left-2, or-right-2 {} + +@media and-left-2 and and-right-2 {} diff --git a/test/fixtures/imports/media-content-level-2.css b/test/fixtures/imports/media-content-level-2.css new file mode 100644 index 00000000..43a7525a --- /dev/null +++ b/test/fixtures/imports/media-content-level-2.css @@ -0,0 +1,3 @@ +@import "media-content-level-3"; + +level-2 {} diff --git a/test/fixtures/imports/media-content-level-3.css b/test/fixtures/imports/media-content-level-3.css new file mode 100644 index 00000000..9cc1707c --- /dev/null +++ b/test/fixtures/imports/media-content-level-3.css @@ -0,0 +1 @@ +level-3 {} diff --git a/test/fixtures/imports/media-import-level-2.css b/test/fixtures/imports/media-import-level-2.css new file mode 100644 index 00000000..bcf49251 --- /dev/null +++ b/test/fixtures/imports/media-import-level-2.css @@ -0,0 +1,3 @@ +@import "media-import-level-3" level-2; + +@import "//" level-2; diff --git a/test/fixtures/imports/media-import-level-3.css b/test/fixtures/imports/media-import-level-3.css new file mode 100644 index 00000000..d42d12fa --- /dev/null +++ b/test/fixtures/imports/media-import-level-3.css @@ -0,0 +1 @@ +@import "//" level-3; diff --git a/test/fixtures/imports/media-query-level-2.css b/test/fixtures/imports/media-query-level-2.css new file mode 100644 index 00000000..ca98b965 --- /dev/null +++ b/test/fixtures/imports/media-query-level-2.css @@ -0,0 +1,3 @@ +@import "media-query-level-3" level-2; + +@media level-2 {} diff --git a/test/fixtures/imports/media-query-level-3.css b/test/fixtures/imports/media-query-level-3.css new file mode 100644 index 00000000..4b3f801e --- /dev/null +++ b/test/fixtures/imports/media-query-level-3.css @@ -0,0 +1 @@ +@media level-3 {} diff --git a/test/fixtures/imports/media-query-screen.css b/test/fixtures/imports/media-query-screen.css new file mode 100644 index 00000000..e96a63d0 --- /dev/null +++ b/test/fixtures/imports/media-query-screen.css @@ -0,0 +1 @@ +@import "foo.css" screen; diff --git a/test/fixtures/imports/modules/empty/index.css b/test/fixtures/imports/modules/empty/index.css new file mode 100644 index 00000000..983b6170 --- /dev/null +++ b/test/fixtures/imports/modules/empty/index.css @@ -0,0 +1 @@ +empty {} diff --git a/test/fixtures/imports/modules/empty/package.json b/test/fixtures/imports/modules/empty/package.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/test/fixtures/imports/modules/empty/package.json @@ -0,0 +1 @@ +{} diff --git a/test/fixtures/imports/modules/main-js/index.css b/test/fixtures/imports/modules/main-js/index.css new file mode 100644 index 00000000..b70c1c75 --- /dev/null +++ b/test/fixtures/imports/modules/main-js/index.css @@ -0,0 +1 @@ +main-js {} diff --git a/test/fixtures/imports/modules/main-js/main.js b/test/fixtures/imports/modules/main-js/main.js new file mode 100644 index 00000000..588e23ee --- /dev/null +++ b/test/fixtures/imports/modules/main-js/main.js @@ -0,0 +1,2 @@ +"use strict" +module.exports = {} diff --git a/test/fixtures/imports/modules/main-js/package.json b/test/fixtures/imports/modules/main-js/package.json new file mode 100644 index 00000000..c13b8cf6 --- /dev/null +++ b/test/fixtures/imports/modules/main-js/package.json @@ -0,0 +1,3 @@ +{ + "main": "main.js" +} diff --git a/test/fixtures/imports/modules/main-style/main.css b/test/fixtures/imports/modules/main-style/main.css new file mode 100644 index 00000000..30381c75 --- /dev/null +++ b/test/fixtures/imports/modules/main-style/main.css @@ -0,0 +1 @@ +style-mained {} diff --git a/test/fixtures/imports/modules/main-style/package.json b/test/fixtures/imports/modules/main-style/package.json new file mode 100644 index 00000000..39fd5f3c --- /dev/null +++ b/test/fixtures/imports/modules/main-style/package.json @@ -0,0 +1,4 @@ +{ + "main": "main.css", + "style": "style.css" +} diff --git a/test/fixtures/imports/modules/main-style/style.css b/test/fixtures/imports/modules/main-style/style.css new file mode 100644 index 00000000..0d9e85bd --- /dev/null +++ b/test/fixtures/imports/modules/main-style/style.css @@ -0,0 +1 @@ +main-styled {} diff --git a/test/fixtures/imports/modules/main/main.css b/test/fixtures/imports/modules/main/main.css new file mode 100644 index 00000000..99358f44 --- /dev/null +++ b/test/fixtures/imports/modules/main/main.css @@ -0,0 +1 @@ +main {} diff --git a/test/fixtures/imports/modules/main/package.json b/test/fixtures/imports/modules/main/package.json new file mode 100644 index 00000000..93731918 --- /dev/null +++ b/test/fixtures/imports/modules/main/package.json @@ -0,0 +1,3 @@ +{ + "main": "main.css" +} diff --git a/test/fixtures/imports/modules/simple/index.css b/test/fixtures/imports/modules/simple/index.css new file mode 100644 index 00000000..edf99844 --- /dev/null +++ b/test/fixtures/imports/modules/simple/index.css @@ -0,0 +1 @@ +simple {} diff --git a/test/fixtures/imports/modules/style/package.json b/test/fixtures/imports/modules/style/package.json new file mode 100644 index 00000000..9785e0d5 --- /dev/null +++ b/test/fixtures/imports/modules/style/package.json @@ -0,0 +1,3 @@ +{ + "style": "style.css" +} diff --git a/test/fixtures/imports/modules/style/style.css b/test/fixtures/imports/modules/style/style.css new file mode 100644 index 00000000..e44f0fb0 --- /dev/null +++ b/test/fixtures/imports/modules/style/style.css @@ -0,0 +1 @@ +style {} diff --git a/test/fixtures/imports/modules/subpath/a.css b/test/fixtures/imports/modules/subpath/a.css new file mode 100644 index 00000000..213d0676 --- /dev/null +++ b/test/fixtures/imports/modules/subpath/a.css @@ -0,0 +1,3 @@ +.foo { + color: green; +} diff --git a/test/fixtures/imports/modules/subpath/package.json b/test/fixtures/imports/modules/subpath/package.json new file mode 100644 index 00000000..42b36fb4 --- /dev/null +++ b/test/fixtures/imports/modules/subpath/package.json @@ -0,0 +1,6 @@ +{ + "style": "style.css", + "imports": { + "#a.css": "./a.css" + } +} diff --git a/test/fixtures/imports/modules/subpath/style.css b/test/fixtures/imports/modules/subpath/style.css new file mode 100644 index 00000000..fbf05686 --- /dev/null +++ b/test/fixtures/imports/modules/subpath/style.css @@ -0,0 +1 @@ +@import '#a.css'; diff --git a/test/fixtures/imports/relative/baz.css b/test/fixtures/imports/relative/baz.css deleted file mode 100755 index b82a6aa0..00000000 --- a/test/fixtures/imports/relative/baz.css +++ /dev/null @@ -1,5 +0,0 @@ -@import "qux.css"; - -baz{ - is: relative -} \ No newline at end of file diff --git a/test/fixtures/imports/relative/import.css b/test/fixtures/imports/relative/import.css deleted file mode 100644 index e96c6c41..00000000 --- a/test/fixtures/imports/relative/import.css +++ /dev/null @@ -1 +0,0 @@ -@import "../bar.css"; \ No newline at end of file diff --git a/test/fixtures/imports/relative/qux.css b/test/fixtures/imports/relative/qux.css deleted file mode 100755 index 6697c874..00000000 --- a/test/fixtures/imports/relative/qux.css +++ /dev/null @@ -1,3 +0,0 @@ -qux{ - is: relative -} diff --git a/test/fixtures/imports/sass-module/main.scss b/test/fixtures/imports/sass-module/main.scss deleted file mode 100644 index f31ea4d8..00000000 --- a/test/fixtures/imports/sass-module/main.scss +++ /dev/null @@ -1 +0,0 @@ -.sass-module{} diff --git a/test/fixtures/imports/sass-module/package.json b/test/fixtures/imports/sass-module/package.json deleted file mode 100644 index 6f24fdbb..00000000 --- a/test/fixtures/imports/sass-module/package.json +++ /dev/null @@ -1,2 +0,0 @@ -{"sass": "main"} - diff --git a/test/fixtures/imports/sugarbar.sss b/test/fixtures/imports/sugarbar.sss new file mode 100644 index 00000000..602386df --- /dev/null +++ b/test/fixtures/imports/sugarbar.sss @@ -0,0 +1,2 @@ +.sugarbar + color: blue diff --git a/test/fixtures/imports/supports-condition-level-2.css b/test/fixtures/imports/supports-condition-level-2.css new file mode 100644 index 00000000..89dd1086 --- /dev/null +++ b/test/fixtures/imports/supports-condition-level-2.css @@ -0,0 +1,3 @@ +@import "supports-condition-level-3.css" layer supports(color: green) screen; + +@supports (level: 2) {} diff --git a/test/fixtures/imports/supports-condition-level-3.css b/test/fixtures/imports/supports-condition-level-3.css new file mode 100644 index 00000000..c6335b40 --- /dev/null +++ b/test/fixtures/imports/supports-condition-level-3.css @@ -0,0 +1 @@ +@supports (level: 3) {} diff --git a/test/fixtures/imports/syntax-error.css b/test/fixtures/imports/syntax-error.css new file mode 100644 index 00000000..8751d52d --- /dev/null +++ b/test/fixtures/imports/syntax-error.css @@ -0,0 +1,7 @@ +body { + bar: bar; + qux: qux; + +a { + foo: foo; +} diff --git a/test/fixtures/layer-duplicate-anonymous-imports-skip.css b/test/fixtures/layer-duplicate-anonymous-imports-skip.css new file mode 100644 index 00000000..3c667937 --- /dev/null +++ b/test/fixtures/layer-duplicate-anonymous-imports-skip.css @@ -0,0 +1,3 @@ +@import url("foo.css") layer; +@import url("bar.css") layer; +@import url("foo.css") layer; diff --git a/test/fixtures/layer-duplicate-anonymous-imports-skip.expected.css b/test/fixtures/layer-duplicate-anonymous-imports-skip.expected.css new file mode 100644 index 00000000..0d325eaa --- /dev/null +++ b/test/fixtures/layer-duplicate-anonymous-imports-skip.expected.css @@ -0,0 +1,6 @@ +@layer{ +foo{} +} +@layer{ +bar{} +} diff --git a/test/fixtures/layer-duplicate-anonymous-imports.css b/test/fixtures/layer-duplicate-anonymous-imports.css new file mode 100644 index 00000000..3c667937 --- /dev/null +++ b/test/fixtures/layer-duplicate-anonymous-imports.css @@ -0,0 +1,3 @@ +@import url("foo.css") layer; +@import url("bar.css") layer; +@import url("foo.css") layer; diff --git a/test/fixtures/layer-duplicate-anonymous-imports.expected.css b/test/fixtures/layer-duplicate-anonymous-imports.expected.css new file mode 100644 index 00000000..210194f5 --- /dev/null +++ b/test/fixtures/layer-duplicate-anonymous-imports.expected.css @@ -0,0 +1,9 @@ +@layer{ +foo{} +} +@layer{ +bar{} +} +@layer{ +foo{} +} diff --git a/test/fixtures/layer-followed-by-ignore-with-conditions.css b/test/fixtures/layer-followed-by-ignore-with-conditions.css new file mode 100644 index 00000000..e5cf2e59 --- /dev/null +++ b/test/fixtures/layer-followed-by-ignore-with-conditions.css @@ -0,0 +1,2 @@ +@import "layer-only.css" print; +@import "layer-followed-by-ignore.css" screen; diff --git a/test/fixtures/layer-followed-by-ignore-with-conditions.expected.css b/test/fixtures/layer-followed-by-ignore-with-conditions.expected.css new file mode 100644 index 00000000..3d1a219d --- /dev/null +++ b/test/fixtures/layer-followed-by-ignore-with-conditions.expected.css @@ -0,0 +1,6 @@ +@import 'data:text/css;base64,QGxheWVyIGxheWVyLWJldGE=' print; +@import 'data:text/css;base64,QGxheWVyIGxheWVyLWFscGhh' screen; +@import "http://css" screen; +@media screen{ +/* a comment */ +} diff --git a/test/fixtures/layer-followed-by-ignore-without-conditions.css b/test/fixtures/layer-followed-by-ignore-without-conditions.css new file mode 100644 index 00000000..6b48f602 --- /dev/null +++ b/test/fixtures/layer-followed-by-ignore-without-conditions.css @@ -0,0 +1,3 @@ +@import "http://css-a"; +@import url("layer-only.css"); +@import "http://css-b"; diff --git a/test/fixtures/layer-followed-by-ignore-without-conditions.expected.css b/test/fixtures/layer-followed-by-ignore-without-conditions.expected.css new file mode 100644 index 00000000..6e8818af --- /dev/null +++ b/test/fixtures/layer-followed-by-ignore-without-conditions.expected.css @@ -0,0 +1,3 @@ +@import "http://css-a"; +@import 'data:text/css;base64,QGxheWVyIGxheWVyLWJldGE='; +@import "http://css-b"; diff --git a/test/fixtures/layer-followed-by-ignore.css b/test/fixtures/layer-followed-by-ignore.css new file mode 100644 index 00000000..1c4c793e --- /dev/null +++ b/test/fixtures/layer-followed-by-ignore.css @@ -0,0 +1,2 @@ +@layer layer-alpha; +@import "http://css"; diff --git a/test/fixtures/layer-followed-by-ignore.expected.css b/test/fixtures/layer-followed-by-ignore.expected.css new file mode 100644 index 00000000..1c4c793e --- /dev/null +++ b/test/fixtures/layer-followed-by-ignore.expected.css @@ -0,0 +1,2 @@ +@layer layer-alpha; +@import "http://css"; diff --git a/test/fixtures/layer-import-atrules-anonymous.css b/test/fixtures/layer-import-atrules-anonymous.css new file mode 100644 index 00000000..dc0eb164 --- /dev/null +++ b/test/fixtures/layer-import-atrules-anonymous.css @@ -0,0 +1,7 @@ +@import url("layer-anonymous.css") layer screen; + +@import url("layer-anonymous.css") layer; + +@import url("layer-anonymous.css") layer(named-layer-1); + +@import "http://css-top-level" layer; diff --git a/test/fixtures/layer-import-atrules-anonymous.expected.css b/test/fixtures/layer-import-atrules-anonymous.expected.css new file mode 100644 index 00000000..ae0bd671 --- /dev/null +++ b/test/fixtures/layer-import-atrules-anonymous.expected.css @@ -0,0 +1,184 @@ +@import 'data:text/css;base64,QGltcG9ydCAnZGF0YTp0ZXh0L2NzcztiYXNlNjQsUUdsdGNHOXlkQ0FpYUhSMGNEb3ZMMk56Y3lJZ2JHRjVaWElnYzJOeVpXVnUnIGxheWVy' layer; +@import 'data:text/css;base64,QGltcG9ydCAnZGF0YTp0ZXh0L2NzcztiYXNlNjQsUUdsdGNHOXlkQ0FpYUhSMGNEb3ZMMk56Y3kxaVlYSWlJR3hoZVdWeUlITmpjbVZsYmc9PScgbGF5ZXIoYmFyKQ==' layer; +@import 'data:text/css;base64,QGltcG9ydCAnZGF0YTp0ZXh0L2NzcztiYXNlNjQsUUdsdGNHOXlkQ0FpYUhSMGNEb3ZMMk56Y3lJZ2JHRjVaWEk9JyBsYXllcg==' layer; +@import 'data:text/css;base64,QGltcG9ydCAnZGF0YTp0ZXh0L2NzcztiYXNlNjQsUUdsdGNHOXlkQ0FpYUhSMGNEb3ZMMk56Y3kxaVlYSWlJR3hoZVdWeScgbGF5ZXIoYmFyKQ==' layer; +@import 'data:text/css;base64,QGltcG9ydCAnZGF0YTp0ZXh0L2NzcztiYXNlNjQsUUdsdGNHOXlkQ0FpYUhSMGNEb3ZMMk56Y3lJZ2JHRjVaWElvYm1GdFpXUXRiR0Y1WlhJdE1Taz0nIGxheWVy' layer; +@import 'data:text/css;base64,QGltcG9ydCAnZGF0YTp0ZXh0L2NzcztiYXNlNjQsUUdsdGNHOXlkQ0FpYUhSMGNEb3ZMMk56Y3kxaVlYSWlJR3hoZVdWeUtHNWhiV1ZrTFd4aGVXVnlMVEVwJyBsYXllcihiYXIp' layer; +@import "http://css-top-level" layer; +@media screen{ +@layer{ +@layer{ +@media (min-width: 320px){ +@layer{ +@layer Z { + body { + color: cyan; + } +} +} +} +} +} +} +@media screen{ +@layer{ +@layer{ + +@layer Y { + body { + color: purple; + } +} +} +} +} +@media screen{ +@layer{ + +body { + order: 1; +} + +@media (min-width: 50rem) { + body { + order: 2; + } +} + +@keyframes RED_TO_BLUE { + 0% { + background-color: red; + } + 100% { + background-color: blue; + } +} + +@supports (display: grid) { + body { + display: grid; + order: 3; + } +} + +@layer A { + body { + order: 4; + } +} +} +} +@layer{ +@layer{ +@media (min-width: 320px){ +@layer{ +@layer Z { + body { + color: cyan; + } +} +} +} +} +} +@layer{ +@layer{ + +@layer Y { + body { + color: purple; + } +} +} +} +@layer{ + +body { + order: 1; +} + +@media (min-width: 50rem) { + body { + order: 2; + } +} + +@keyframes RED_TO_BLUE { + 0% { + background-color: red; + } + 100% { + background-color: blue; + } +} + +@supports (display: grid) { + body { + display: grid; + order: 3; + } +} + +@layer A { + body { + order: 4; + } +} +} +@layer named-layer-1{ +@layer{ +@media (min-width: 320px){ +@layer{ +@layer Z { + body { + color: cyan; + } +} +} +} +} +} +@layer named-layer-1{ +@layer{ + +@layer Y { + body { + color: purple; + } +} +} +} +@layer named-layer-1{ + +body { + order: 1; +} + +@media (min-width: 50rem) { + body { + order: 2; + } +} + +@keyframes RED_TO_BLUE { + 0% { + background-color: red; + } + 100% { + background-color: blue; + } +} + +@supports (display: grid) { + body { + display: grid; + order: 3; + } +} + +@layer A { + body { + order: 4; + } +} +} diff --git a/test/fixtures/layer-import-atrules.css b/test/fixtures/layer-import-atrules.css new file mode 100644 index 00000000..dab4c0e7 --- /dev/null +++ b/test/fixtures/layer-import-atrules.css @@ -0,0 +1,3 @@ +@import url("layer.css") layer(imported-with-media) screen; + +@import url("layer.css") layer(imported-without-media); diff --git a/test/fixtures/layer-import-atrules.expected.css b/test/fixtures/layer-import-atrules.expected.css new file mode 100644 index 00000000..636333bd --- /dev/null +++ b/test/fixtures/layer-import-atrules.expected.css @@ -0,0 +1,125 @@ +@media screen { +@layer imported-with-media { +@layer level-2 { +@media (min-width: 320px) { +@layer level-3 { +@layer Z { + body { + color: cyan; + } +} +} +} +} +} +} + +@media screen { +@layer imported-with-media { +@layer level-2 { + +@layer Y { + body { + color: purple; + } +} +} +} +} + +@media screen { +@layer imported-with-media { + +body { + order: 1; +} + +@media (min-width: 50rem) { + body { + order: 2; + } +} + +@keyframes RED_TO_BLUE { + 0% { + background-color: red; + } + 100% { + background-color: blue; + } +} + +@supports (display: grid) { + body { + display: grid; + order: 3; + } +} + +@layer A { + body { + order: 4; + } +} +} +} + +@layer imported-without-media { +@layer level-2 { +@media (min-width: 320px) { +@layer level-3 { +@layer Z { + body { + color: cyan; + } +} +} +} +} +} + +@layer imported-without-media { +@layer level-2 { + +@layer Y { + body { + color: purple; + } +} +} +} + +@layer imported-without-media { + +body { + order: 1; +} + +@media (min-width: 50rem) { + body { + order: 2; + } +} + +@keyframes RED_TO_BLUE { + 0% { + background-color: red; + } + 100% { + background-color: blue; + } +} + +@supports (display: grid) { + body { + display: grid; + order: 3; + } +} + +@layer A { + body { + order: 4; + } +} +} diff --git a/test/fixtures/layer-rule-grouping.expected.css b/test/fixtures/layer-rule-grouping.expected.css new file mode 100644 index 00000000..9a0608d9 --- /dev/null +++ b/test/fixtures/layer-rule-grouping.expected.css @@ -0,0 +1,50 @@ +@media screen { + +rule-one {} + +rule-two {} + +@media (min-width: 50rem) { + rule-three {} + + rule-four {} +} + +rule-five {} + +rule-six {} +} + +@layer { + +rule-one {} + +rule-two {} + +@media (min-width: 50rem) { + rule-three {} + + rule-four {} +} + +rule-five {} + +rule-six {} +} + +@layer named { + +rule-one {} + +rule-two {} + +@media (min-width: 50rem) { + rule-three {} + + rule-four {} +} + +rule-five {} + +rule-six {} +} diff --git a/test/fixtures/layer-statement-with-conditions.css b/test/fixtures/layer-statement-with-conditions.css new file mode 100644 index 00000000..2256df49 --- /dev/null +++ b/test/fixtures/layer-statement-with-conditions.css @@ -0,0 +1 @@ +@import "layer-only.css" print; diff --git a/test/fixtures/layer-statement-with-conditions.expected.css b/test/fixtures/layer-statement-with-conditions.expected.css new file mode 100644 index 00000000..9f57df60 --- /dev/null +++ b/test/fixtures/layer-statement-with-conditions.expected.css @@ -0,0 +1,3 @@ +@media print{ +@layer layer-beta +} diff --git a/test/fixtures/layer.css b/test/fixtures/layer.css new file mode 100644 index 00000000..b9894dab --- /dev/null +++ b/test/fixtures/layer.css @@ -0,0 +1,10 @@ +@layer layer-alpha, layer-beta.one; + +@import "foo.css" layer(foo); +@import 'bar.css' layer(bar); +@import 'bar.css' layer(bar) level-1 and level-2; +@import url(baz.css) layer(baz); +@import url("foobar.css") layer(foobar); +@import url("foo-layered.css") layer(foo-layered); + +content{} diff --git a/test/fixtures/layer.expected.css b/test/fixtures/layer.expected.css new file mode 100644 index 00000000..bc73a558 --- /dev/null +++ b/test/fixtures/layer.expected.css @@ -0,0 +1,24 @@ +@layer layer-alpha, layer-beta.one; +@layer foo{ +foo{} +} +@layer bar{ +bar{} +} +@media level-1 and level-2{ +@layer bar{ +bar{} +} +} +@layer baz{ +baz{} +} +@layer foobar{ +foobar{} +} +@layer foo-layered{ +@layer foo { + foo {} +} +} +content{} diff --git a/test/fixtures/media-charset.css b/test/fixtures/media-charset.css new file mode 100644 index 00000000..b7faa401 --- /dev/null +++ b/test/fixtures/media-charset.css @@ -0,0 +1 @@ +@import "charset-content" level-1; diff --git a/test/fixtures/media-charset.expected.css b/test/fixtures/media-charset.expected.css new file mode 100644 index 00000000..8b65f60c --- /dev/null +++ b/test/fixtures/media-charset.expected.css @@ -0,0 +1,4 @@ +@charset "UTF-8"; +@media level-1{ +.charset-content {} +} diff --git a/test/fixtures/media-combine.css b/test/fixtures/media-combine.css new file mode 100644 index 00000000..2d757131 --- /dev/null +++ b/test/fixtures/media-combine.css @@ -0,0 +1,4 @@ +@import "media-combine-nested.css" one-1; +@import "media-combine-nested.css" or-left-1, or-right-1; +@import "media-combine-nested.css" and-left-1 and and-right-1; +@import "media-query-screen.css" screen; diff --git a/test/fixtures/media-combine.expected.css b/test/fixtures/media-combine.expected.css new file mode 100644 index 00000000..a9325004 --- /dev/null +++ b/test/fixtures/media-combine.expected.css @@ -0,0 +1,33 @@ +@media one-1 { + +@media one-2 {} + +@media or-left-2, or-right-2 {} + +@media and-left-2 and and-right-2 {} +} + +@media or-left-1, or-right-1 { + +@media one-2 {} + +@media or-left-2, or-right-2 {} + +@media and-left-2 and and-right-2 {} +} + +@media and-left-1 and and-right-1 { + +@media one-2 {} + +@media or-left-2, or-right-2 {} + +@media and-left-2 and and-right-2 {} +} + +@media screen { + +@media screen { +foo{} +} +} diff --git a/test/fixtures/media-content.css b/test/fixtures/media-content.css new file mode 100644 index 00000000..d5c25aaf --- /dev/null +++ b/test/fixtures/media-content.css @@ -0,0 +1,3 @@ +@import "media-content-level-2" level-1; + +level-1 {} diff --git a/test/fixtures/media-content.expected.css b/test/fixtures/media-content.expected.css new file mode 100644 index 00000000..b38a4d5f --- /dev/null +++ b/test/fixtures/media-content.expected.css @@ -0,0 +1,10 @@ +@media level-1 { +level-3 {} +} + +@media level-1 { + +level-2 {} +} + +level-1 {} diff --git a/test/fixtures/media-import.css b/test/fixtures/media-import.css new file mode 100644 index 00000000..7be819a7 --- /dev/null +++ b/test/fixtures/media-import.css @@ -0,0 +1,3 @@ +@import "media-import-level-2" level-1; + +@import "//" level-1; diff --git a/test/fixtures/media-import.expected.css b/test/fixtures/media-import.expected.css new file mode 100644 index 00000000..784d09cd --- /dev/null +++ b/test/fixtures/media-import.expected.css @@ -0,0 +1,5 @@ +@import 'data:text/css;base64,QGltcG9ydCAnZGF0YTp0ZXh0L2NzcztiYXNlNjQsUUdsdGNHOXlkQ0FpTHk4aUlHeGxkbVZzTFRFPScgbGV2ZWwtMw==' level-2; + +@import 'data:text/css;base64,QGltcG9ydCAiLy8iIGxldmVsLTE=' level-2; + +@import "//" level-1; diff --git a/test/fixtures/media-query.css b/test/fixtures/media-query.css new file mode 100644 index 00000000..9a6dc3a0 --- /dev/null +++ b/test/fixtures/media-query.css @@ -0,0 +1,3 @@ +@import "media-query-level-2" level-1; + +@media level-1 {} diff --git a/test/fixtures/media-query.expected.css b/test/fixtures/media-query.expected.css new file mode 100644 index 00000000..5c0a1449 --- /dev/null +++ b/test/fixtures/media-query.expected.css @@ -0,0 +1,12 @@ +@media level-1 { +@media level-2 { +@media level-3 {} +} +} + +@media level-1 { + +@media level-2 {} +} + +@media level-1 {} diff --git a/test/fixtures/no-duplicate.expected.css b/test/fixtures/no-duplicate.expected.css index d317be60..82382a1e 100644 --- a/test/fixtures/no-duplicate.expected.css +++ b/test/fixtures/no-duplicate.expected.css @@ -1,10 +1,7 @@ foo{} - @media screen{ foo{} } - proxy {} import {} - content{} diff --git a/test/fixtures/order.css b/test/fixtures/order.css new file mode 100644 index 00000000..0df43601 --- /dev/null +++ b/test/fixtures/order.css @@ -0,0 +1,6 @@ +@import "foo-first.css"; +@import "foo-second.css"; + +.baz { + color: green; +} diff --git a/test/fixtures/order.expected.css b/test/fixtures/order.expected.css new file mode 100644 index 00000000..cfdac22a --- /dev/null +++ b/test/fixtures/order.expected.css @@ -0,0 +1,13 @@ +bar{} + +foo.first{ + color: red; +} + +foo.second{ + color: blue; +} + +.baz { + color: green; +} diff --git a/test/fixtures/plugins.css b/test/fixtures/plugins.css new file mode 100644 index 00000000..4c5a01e9 --- /dev/null +++ b/test/fixtures/plugins.css @@ -0,0 +1,4 @@ +@import 'foo/index.css'; +@import 'bar.css'; +@level-1-1 {} +@level-1-2 {} diff --git a/test/fixtures/plugins.expected.css b/test/fixtures/plugins.expected.css new file mode 100644 index 00000000..d6eb96b0 --- /dev/null +++ b/test/fixtures/plugins.expected.css @@ -0,0 +1,3 @@ +foo-converted {} +@level-1-1 {} +@level-1-2 {} diff --git a/test/fixtures/recursive.css b/test/fixtures/recursive.css deleted file mode 100755 index 399d7235..00000000 --- a/test/fixtures/recursive.css +++ /dev/null @@ -1,3 +0,0 @@ -@import "foo-recursive.css" (min-width: 25em); - -content{} diff --git a/test/fixtures/recursive.expected.css b/test/fixtures/recursive.expected.css deleted file mode 100755 index 808988d0..00000000 --- a/test/fixtures/recursive.expected.css +++ /dev/null @@ -1,7 +0,0 @@ -@media (min-width: 25em){ -bar{} - -foo.recursive{} -} - -content{} diff --git a/test/fixtures/relative-to-source.css b/test/fixtures/relative-to-source.css deleted file mode 100755 index 8fd38de5..00000000 --- a/test/fixtures/relative-to-source.css +++ /dev/null @@ -1,3 +0,0 @@ -@import "imports/foo.css"; - -content{} \ No newline at end of file diff --git a/test/fixtures/relative-to-source.expected.css b/test/fixtures/relative-to-source.expected.css deleted file mode 100755 index 09fd6d47..00000000 --- a/test/fixtures/relative-to-source.expected.css +++ /dev/null @@ -1,3 +0,0 @@ -foo{} - -content{} diff --git a/test/fixtures/relative.css b/test/fixtures/relative.css deleted file mode 100755 index d4fe624a..00000000 --- a/test/fixtures/relative.css +++ /dev/null @@ -1,3 +0,0 @@ -@import "relative/baz.css"; -@import "qux.css"; -content{} diff --git a/test/fixtures/relative.expected.css b/test/fixtures/relative.expected.css deleted file mode 100755 index f7b63ea3..00000000 --- a/test/fixtures/relative.expected.css +++ /dev/null @@ -1,11 +0,0 @@ -qux{ - is: relative -} - -baz{ - is: relative -} -quux{ - not: "relative/qux" -} -content{} \ No newline at end of file diff --git a/test/fixtures/resolve-custom-modules.css b/test/fixtures/resolve-custom-modules.css new file mode 100644 index 00000000..b80b56ee --- /dev/null +++ b/test/fixtures/resolve-custom-modules.css @@ -0,0 +1,7 @@ +@import "shared-fake"; +@import "shared-auto"; +@import "shared-nest"; +@import "shared-by-hand/style.css"; +@import "shared-use-dep"; +@import "shared-use-dep-too"; +@import "shared-use-dep" screen; diff --git a/test/fixtures/resolve-custom-modules.expected.css b/test/fixtures/resolve-custom-modules.expected.css new file mode 100644 index 00000000..011e8074 --- /dev/null +++ b/test/fixtures/resolve-custom-modules.expected.css @@ -0,0 +1,8 @@ +.shared-fake{} +.shared-auto{} +.shared-nested{} +.shared-byHand{} +.shared-dep{} +@media screen{ +.shared-dep{} +} diff --git a/test/fixtures/cwd.css b/test/fixtures/resolve-cwd.css old mode 100755 new mode 100644 similarity index 100% rename from test/fixtures/cwd.css rename to test/fixtures/resolve-cwd.css diff --git a/test/fixtures/resolve-cwd.expected.css b/test/fixtures/resolve-cwd.expected.css new file mode 100644 index 00000000..ead37b55 --- /dev/null +++ b/test/fixtures/resolve-cwd.expected.css @@ -0,0 +1,3 @@ +foo{} +bar{} +foo.recursive{} diff --git a/test/fixtures/resolve-from.css b/test/fixtures/resolve-from.css new file mode 100644 index 00000000..bdc682c0 --- /dev/null +++ b/test/fixtures/resolve-from.css @@ -0,0 +1,2 @@ +@import "imports/foo.css"; +@import "imports/foo-recursive.css"; diff --git a/test/fixtures/resolve-from.expected.css b/test/fixtures/resolve-from.expected.css new file mode 100644 index 00000000..ead37b55 --- /dev/null +++ b/test/fixtures/resolve-from.expected.css @@ -0,0 +1,3 @@ +foo{} +bar{} +foo.recursive{} diff --git a/test/fixtures/resolve-local-modules.css b/test/fixtures/resolve-local-modules.css new file mode 100644 index 00000000..301cf92d --- /dev/null +++ b/test/fixtures/resolve-local-modules.css @@ -0,0 +1,6 @@ +@import "test/fixtures/imports/modules/simple"; +@import "test/fixtures/imports/modules/empty"; +@import "test/fixtures/imports/modules/style"; +@import "test/fixtures/imports/modules/main"; +@import "test/fixtures/imports/modules/main-js"; +@import "test/fixtures/imports/modules/main-style"; diff --git a/test/fixtures/resolve-local-modules.expected.css b/test/fixtures/resolve-local-modules.expected.css new file mode 100644 index 00000000..3aa72d80 --- /dev/null +++ b/test/fixtures/resolve-local-modules.expected.css @@ -0,0 +1,6 @@ +simple {} +empty {} +style {} +main {} +main-js {} +main-styled {} diff --git a/test/fixtures/modules.css b/test/fixtures/resolve-modules.css similarity index 90% rename from test/fixtures/modules.css rename to test/fixtures/resolve-modules.css index cf3c881e..99a4816b 100644 --- a/test/fixtures/modules.css +++ b/test/fixtures/resolve-modules.css @@ -13,7 +13,3 @@ @import "web-use-dep"; @import "web-use-dep-too"; @import "web-use-dep" screen; - -@import "local-module"; - -content{} \ No newline at end of file diff --git a/test/fixtures/modules.expected.css b/test/fixtures/resolve-modules.expected.css similarity index 84% rename from test/fixtures/modules.expected.css rename to test/fixtures/resolve-modules.expected.css index 4fa34101..572ab9b1 100644 --- a/test/fixtures/modules.expected.css +++ b/test/fixtures/resolve-modules.expected.css @@ -6,7 +6,6 @@ @media screen{ .dep{} } - .web-fake{} .web-auto{} .web-nested{} @@ -15,7 +14,3 @@ @media screen{ .web-dep{} } - -.local-module{} - -content{} \ No newline at end of file diff --git a/test/fixtures/resolve-npm-subpackages.css b/test/fixtures/resolve-npm-subpackages.css new file mode 100644 index 00000000..a04781e4 --- /dev/null +++ b/test/fixtures/resolve-npm-subpackages.css @@ -0,0 +1,2 @@ +@import "modularized-css/foo"; +@import "modularized-css/foo/boo/boomod.css"; diff --git a/test/fixtures/resolve-npm-subpackages.expected.css b/test/fixtures/resolve-npm-subpackages.expected.css new file mode 100644 index 00000000..b9a426af --- /dev/null +++ b/test/fixtures/resolve-npm-subpackages.expected.css @@ -0,0 +1,2 @@ +.i-pity-the-foo{} +.i-pity-the-boo{} diff --git a/test/fixtures/resolve-path-cwd.css b/test/fixtures/resolve-path-cwd.css new file mode 100644 index 00000000..3f2e57dd --- /dev/null +++ b/test/fixtures/resolve-path-cwd.css @@ -0,0 +1,2 @@ +@import "foo.css"; +@import "foo-recursive.css"; diff --git a/test/fixtures/resolve-path-cwd.expected.css b/test/fixtures/resolve-path-cwd.expected.css new file mode 100644 index 00000000..ead37b55 --- /dev/null +++ b/test/fixtures/resolve-path-cwd.expected.css @@ -0,0 +1,3 @@ +foo{} +bar{} +foo.recursive{} diff --git a/test/fixtures/resolve-path-modules.css b/test/fixtures/resolve-path-modules.css new file mode 100644 index 00000000..b95cb98c --- /dev/null +++ b/test/fixtures/resolve-path-modules.css @@ -0,0 +1,6 @@ +@import "simple"; +@import "empty"; +@import "style"; +@import "main"; +@import "main-js"; +@import "main-style"; diff --git a/test/fixtures/resolve-path-modules.expected.css b/test/fixtures/resolve-path-modules.expected.css new file mode 100644 index 00000000..3aa72d80 --- /dev/null +++ b/test/fixtures/resolve-path-modules.expected.css @@ -0,0 +1,6 @@ +simple {} +empty {} +style {} +main {} +main-js {} +main-styled {} diff --git a/test/fixtures/resolve-path-root.css b/test/fixtures/resolve-path-root.css new file mode 100644 index 00000000..3f2e57dd --- /dev/null +++ b/test/fixtures/resolve-path-root.css @@ -0,0 +1,2 @@ +@import "foo.css"; +@import "foo-recursive.css"; diff --git a/test/fixtures/resolve-path-root.expected.css b/test/fixtures/resolve-path-root.expected.css new file mode 100644 index 00000000..ead37b55 --- /dev/null +++ b/test/fixtures/resolve-path-root.expected.css @@ -0,0 +1,3 @@ +foo{} +bar{} +foo.recursive{} diff --git a/test/fixtures/resolve-root.css b/test/fixtures/resolve-root.css new file mode 100644 index 00000000..bdc682c0 --- /dev/null +++ b/test/fixtures/resolve-root.css @@ -0,0 +1,2 @@ +@import "imports/foo.css"; +@import "imports/foo-recursive.css"; diff --git a/test/fixtures/resolve-root.expected.css b/test/fixtures/resolve-root.expected.css new file mode 100644 index 00000000..ead37b55 --- /dev/null +++ b/test/fixtures/resolve-root.expected.css @@ -0,0 +1,3 @@ +foo{} +bar{} +foo.recursive{} diff --git a/test/fixtures/same.css b/test/fixtures/same.css new file mode 100644 index 00000000..6fc51d18 --- /dev/null +++ b/test/fixtures/same.css @@ -0,0 +1,2 @@ +@import "foo/index.css"; +@import "bar/index.css"; diff --git a/test/fixtures/same.expected.css b/test/fixtures/same.expected.css new file mode 100644 index 00000000..4ce9d584 --- /dev/null +++ b/test/fixtures/same.expected.css @@ -0,0 +1,2 @@ +foo {} +bar {} diff --git a/test/fixtures/scss-parser.css b/test/fixtures/scss-parser.css new file mode 100644 index 00000000..dd521736 --- /dev/null +++ b/test/fixtures/scss-parser.css @@ -0,0 +1 @@ +@import "inline-comment.scss"; diff --git a/test/fixtures/scss-parser.expected.css b/test/fixtures/scss-parser.expected.css new file mode 100644 index 00000000..87791d5c --- /dev/null +++ b/test/fixtures/scss-parser.expected.css @@ -0,0 +1 @@ +/* inline comment*/ diff --git a/test/fixtures/scss-syntax.css b/test/fixtures/scss-syntax.css new file mode 100644 index 00000000..dd521736 --- /dev/null +++ b/test/fixtures/scss-syntax.css @@ -0,0 +1 @@ +@import "inline-comment.scss"; diff --git a/test/fixtures/scss-syntax.expected.css b/test/fixtures/scss-syntax.expected.css new file mode 100644 index 00000000..bdf8f0f7 --- /dev/null +++ b/test/fixtures/scss-syntax.expected.css @@ -0,0 +1 @@ +// inline comment diff --git a/test/fixtures/simple.css b/test/fixtures/simple.css index 3d304f24..2d3ba9e0 100755 --- a/test/fixtures/simple.css +++ b/test/fixtures/simple.css @@ -7,6 +7,6 @@ @import url("foobar.css"); @import url("foobar.css") screen and (min-width: 25em); @import url('foobarbaz.css'); -@import url('foobarbaz.css') print, screen and (min-width: 25em); +@import url('foobarbaz.css') print,screen and (min-width: 25em); content{} diff --git a/test/fixtures/simple.expected.css b/test/fixtures/simple.expected.css index f67047c3..6a9c1ca7 100755 --- a/test/fixtures/simple.expected.css +++ b/test/fixtures/simple.expected.css @@ -15,8 +15,7 @@ foobar{} foobar{} } foobarbaz{} -@media print, screen and (min-width: 25em){ +@media print,screen and (min-width: 25em){ foobarbaz{} } - content{} diff --git a/test/fixtures/subpath.css b/test/fixtures/subpath.css new file mode 100644 index 00000000..c2cd8b6a --- /dev/null +++ b/test/fixtures/subpath.css @@ -0,0 +1 @@ +@import "subpath"; diff --git a/test/fixtures/subpath.expected.css b/test/fixtures/subpath.expected.css new file mode 100644 index 00000000..213d0676 --- /dev/null +++ b/test/fixtures/subpath.expected.css @@ -0,0 +1,3 @@ +.foo { + color: green; +} diff --git a/test/fixtures/sugar-import-css.expected.css b/test/fixtures/sugar-import-css.expected.css new file mode 100644 index 00000000..20df4744 --- /dev/null +++ b/test/fixtures/sugar-import-css.expected.css @@ -0,0 +1,4 @@ +bar{} +.sugar{ + color: white +} diff --git a/test/fixtures/sugar-import-css.sss b/test/fixtures/sugar-import-css.sss new file mode 100644 index 00000000..e9d794f5 --- /dev/null +++ b/test/fixtures/sugar-import-css.sss @@ -0,0 +1,3 @@ +@import "bar.css" +.sugar + color: white diff --git a/test/fixtures/sugar.expected.css b/test/fixtures/sugar.expected.css new file mode 100644 index 00000000..1e6583a1 --- /dev/null +++ b/test/fixtures/sugar.expected.css @@ -0,0 +1,6 @@ +.sugarbar { + color: blue +} +.sugar { + color: white +} diff --git a/test/fixtures/sugar.sss b/test/fixtures/sugar.sss new file mode 100644 index 00000000..58815ae3 --- /dev/null +++ b/test/fixtures/sugar.sss @@ -0,0 +1,3 @@ +@import "sugarbar.sss" +.sugar + color: white diff --git a/test/fixtures/supports-condition-import.css b/test/fixtures/supports-condition-import.css new file mode 100644 index 00000000..04240011 --- /dev/null +++ b/test/fixtures/supports-condition-import.css @@ -0,0 +1,3 @@ +@import "supports-condition-level-2" layer supports(filter: blur(1px)) (width > 100px); + +@supports (level: 1) {} diff --git a/test/fixtures/supports-condition-import.expected.css b/test/fixtures/supports-condition-import.expected.css new file mode 100644 index 00000000..0f40229d --- /dev/null +++ b/test/fixtures/supports-condition-import.expected.css @@ -0,0 +1,24 @@ +@media (width > 100px) { + @supports (filter: blur(1px)) { + @layer { + @media screen { + @supports (color: green) { + @layer { +@supports (level: 3) {} + } + } + } + } + } +} + +@media (width > 100px) { + @supports (filter: blur(1px)) { + @layer { + +@supports (level: 2) {} + } + } +} + +@supports (level: 1) {} diff --git a/test/fixtures/syntax-error.css b/test/fixtures/syntax-error.css new file mode 100644 index 00000000..c5a8220f --- /dev/null +++ b/test/fixtures/syntax-error.css @@ -0,0 +1,6 @@ +@import "syntax-error.css"; + +syntax.error { + /* Error isn't here, it's in the imported file */ + color: green; +} diff --git a/test/fixtures/transform.css b/test/fixtures/transform.css deleted file mode 100755 index 6ac154ab..00000000 --- a/test/fixtures/transform.css +++ /dev/null @@ -1,4 +0,0 @@ -@import "foo.styl"; -@import "foo.styl" (min-width: 25em); - -content{} diff --git a/test/fixtures/transform.expected.css b/test/fixtures/transform.expected.css deleted file mode 100755 index 4e5f4602..00000000 --- a/test/fixtures/transform.expected.css +++ /dev/null @@ -1,10 +0,0 @@ -foo { - styl: true; -} -@media (min-width: 25em) { -foo { - styl: true; -} -} - -content{} \ No newline at end of file diff --git a/test/helpers/ast-checker.js b/test/helpers/ast-checker.js new file mode 100644 index 00000000..2bb78bea --- /dev/null +++ b/test/helpers/ast-checker.js @@ -0,0 +1,48 @@ +"use strict" + +const astCheckerPlugin = () => { + return { + postcssPlugin: "ast-checker-plugin", + OnceExit(root) { + root.walkAtRules(node => { + if (typeof node.params !== "string") { + throw node.error( + `Params must be of type 'string', found '${typeof node.params}' instead`, + ) + } + + if (typeof node.type !== "string") { + throw node.error( + `Type must be of type 'string', found '${typeof node.type}' instead`, + ) + } + + if (node.type !== "atrule") { + throw node.error( + `Type must be 'atrule', found '${node.type}' instead`, + ) + } + + if (typeof node.name !== "string") { + throw node.error( + `Name must be of type 'string', found '${typeof node.name}' instead`, + ) + } + + if (node.nodes && !Array.isArray(node.nodes)) { + throw node.error( + `Nodes must be of type 'Array' when it is present, found '${typeof node.nodes}' instead`, + ) + } + + if (!("parent" in node)) { + throw node.error("AtRule must have a 'parent' property") + } + }) + }, + } +} + +astCheckerPlugin.postcss = true + +module.exports = astCheckerPlugin diff --git a/test/helpers/check-fixture.js b/test/helpers/check-fixture.js new file mode 100644 index 00000000..aa8a5d7b --- /dev/null +++ b/test/helpers/check-fixture.js @@ -0,0 +1,52 @@ +"use strict" + +// builtin tooling +const fs = require("fs") + +// external tooling +const postcss = require("postcss") + +// plugin +const atImport = require("../..") +const astCheckerPlugin = require("./ast-checker") + +function read(name, ext) { + ext = ext || ".css" + return fs.readFileSync(`test/fixtures/${name}${ext}`, "utf8") +} + +module.exports = function (t, file, opts, postcssOpts, warnings) { + opts = { path: "test/fixtures/imports", ...opts } + postcssOpts = { from: undefined, ...postcssOpts } + if (typeof file === "string") file = { name: file, ext: ".css" } + const { name, ext } = file + + return postcss([atImport(opts), astCheckerPlugin()]) + .process(read(name, ext), postcssOpts || {}) + .then(result => { + const actual = result.css + const expected = read(`${name}.expected`) + // handy thing: checkout actual in the *.actual.css file + fs.writeFile(`test/fixtures/${name}.actual.css`, actual, err => { + // eslint-disable-next-line no-console + if (err) console.warn(`Warning: ${err}; not fatal, continuing`) + }) + t.is(actual, expected) + if (!warnings) warnings = [] + result.warnings().forEach((warning, index) => { + t.is( + warning.text, + warnings[index], + `unexpected warning: "${warning.text}"`, + ) + }) + + t.is( + warnings.length, + result.warnings().length, + `expected ${warnings.length} warning(s), got ${ + result.warnings().length + }`, + ) + }) +} diff --git a/test/import-events.js b/test/import-events.js new file mode 100644 index 00000000..970baf17 --- /dev/null +++ b/test/import-events.js @@ -0,0 +1,39 @@ +"use strict" +// builtin tooling +const { readFileSync } = require("fs") +const { resolve } = require("path") + +// external tooling +const test = require("ava") +const postcss = require("postcss") + +// plugin +const atImport = require("..") + +test("should add dependency message for each import", t => { + return postcss() + .use(atImport({ path: "test/fixtures/imports" })) + .process(readFileSync("test/fixtures/media-import.css"), { + from: "test/fixtures/media-import.css", + }) + .then(result => { + const deps = result.messages.filter( + message => message.type === "dependency", + ) + const expected = [ + { + type: "dependency", + plugin: "postcss-import", + file: resolve("test/fixtures/imports/media-import-level-2.css"), + parent: resolve("test/fixtures/media-import.css"), + }, + { + type: "dependency", + plugin: "postcss-import", + file: resolve("test/fixtures/imports/media-import-level-3.css"), + parent: resolve("test/fixtures/imports/media-import-level-2.css"), + }, + ] + t.deepEqual(deps, expected) + }) +}) diff --git a/test/import.js b/test/import.js new file mode 100644 index 00000000..e2a2bd17 --- /dev/null +++ b/test/import.js @@ -0,0 +1,170 @@ +"use strict" +// builtin tooling +const { readFileSync } = require("fs") +const path = require("path") + +// external tooling +const test = require("ava") +const postcss = require("postcss") + +// plugin +const atImport = require("..") + +// internal tooling +const checkFixture = require("./helpers/check-fixture") + +test("should import stylesheets", checkFixture, "simple") + +test("should not import a stylesheet twice", checkFixture, "no-duplicate") + +test( + "should be able to import a stylesheet twice", + checkFixture, + "duplicates", + { + skipDuplicates: false, + }, +) + +test( + "should be able to import a stylesheet with cyclical dependencies", + checkFixture, + "cyclical", + { + skipDuplicates: false, + }, +) + +test( + "should be able to import a stylesheet with cyclical dependencies and skip duplicates is true", + checkFixture, + "cyclical-skip-duplicates", + { + skipDuplicates: true, + }, +) + +test("should import stylesheets with same content", checkFixture, "same") + +test("should ignore & adjust external import", checkFixture, "ignore") + +test("should not fail with only one absolute import", t => { + const base = "@import url(http://)" + return postcss() + .use(atImport()) + .process(base, { from: undefined }) + .then(result => { + t.is(result.warnings().length, 0) + t.is(result.css, base) + }) +}) + +test("should not fail with absolute and local import", t => { + return postcss() + .use(atImport()) + .process( + "@import url('http://');\n@import 'test/fixtures/imports/foo.css';", + { from: undefined }, + ) + .then(result => t.is(result.css, "@import url('http://');\nfoo{}")) +}) + +test("should keep @charset first", t => { + const base = '@charset "UTF-8";\n@import url(http://);' + return postcss() + .use(atImport()) + .process(base, { from: undefined }) + .then(result => { + t.is(result.warnings().length, 0) + t.is(result.css, base) + }) +}) + +test( + "should handle multiple @charset statements", + checkFixture, + "charset-import", +) + +test("should error if incompatible @charset statements", t => { + t.plan(2) + const file = "test/fixtures/charset-error.css" + return postcss() + .use(atImport()) + .process(readFileSync(file), { from: file }) + .catch(err => { + t.truthy(err) + t.regex( + err.message, + /Incompatible @charset statements:.+specified in.+specified in.+/s, + ) + }) +}) + +test("should error when file not found", t => { + t.plan(1) + const file = "test/fixtures/imports/import-missing.css" + return postcss() + .use(atImport()) + .process(readFileSync(file), { from: file }) + .catch(err => t.truthy(err)) +}) + +test("should contain a correct sourcemap", t => { + return postcss() + .use(atImport()) + .process(readFileSync("test/sourcemap/in.css"), { + from: "test/sourcemap/in.css", + to: null, + map: { inline: false }, + }) + .then(result => { + t.is( + result.map.toString(), + readFileSync( + process.platform === "win32" + ? "test/sourcemap/out.css.win.map" + : "test/sourcemap/out.css.map", + "utf8", + ).trim(), + ) + }) +}) + +test("inlined @import should keep PostCSS AST references clean", t => { + return postcss() + .use(atImport()) + .process("@import 'test/fixtures/imports/foo.css';\nbar{}", { + from: undefined, + }) + .then(result => { + result.root.nodes.forEach(node => { + t.is(result.root, node.parent) + }) + }) +}) + +test( + "should work with empty files", + checkFixture, + "empty-and-useless", + { path: "test/fixtures/imports" }, + null, + [`${path.resolve("test/fixtures/imports/empty.css")} is empty`], +) + +test( + "should be able to disable warnings for empty files", + checkFixture, + "empty-and-useless", + { path: "test/fixtures/imports", warnOnEmpty: false }, +) + +test("should work with no styles without throwing an error", t => { + return postcss() + .use(atImport()) + .process("", { from: undefined }) + .then(result => { + t.is(result.warnings().length, 0) + }) +}) diff --git a/test/index.js b/test/index.js deleted file mode 100755 index f59ec60c..00000000 --- a/test/index.js +++ /dev/null @@ -1,254 +0,0 @@ -var test = require("tape") - -var path = require("path") -var fs = require("fs") - -var atImport = require("..") -var postcss = require("postcss") - -var fixturesDir = path.join(__dirname, "fixtures") -var importsDir = path.join(fixturesDir, "imports") - -function read(name) { - return fs.readFileSync("test/" + name + ".css", "utf8").trim() -} - -function compareFixtures(t, name, msg, opts, postcssOpts) { - opts = opts || {path: importsDir} - var actual = postcss() - .use(atImport(opts)) - .process(read("fixtures/" + name), postcssOpts) - .css.trim() - var expected = read("fixtures/" + name + ".expected") - - // handy thing: checkout actual in the *.actual.css file - fs.writeFile("test/fixtures/" + name + ".actual.css", actual) - - t.equal(actual, expected, msg) -} - -test("@import", function(t) { - compareFixtures(t, "simple", "should import stylsheets") - - compareFixtures(t, "no-duplicate", "should not import a stylsheet twice") - - compareFixtures(t, "ignore", "should ignore & adjust external import") - - compareFixtures(t, "glob", "should handle a glob pattern", { - root: __dirname, - path: importsDir, - glob: true, - }) - - compareFixtures( - t, - "glob-alt", - "should handle a glob pattern with single quote and/or url(...)", { - path: importsDir, - glob: true, - }) - compareFixtures(t, "recursive", "should import stylsheets recursively") - - compareFixtures(t, "relative", "should import stylsheets relatively") - - compareFixtures(t, "empty-and-useless", "should work with empty files") - - compareFixtures(t, "transform", "should support transform", { - path: importsDir, - transform: require("css-whitespace"), - }) - - compareFixtures(t, "cwd", "should work without a specified path", {}) - - compareFixtures( - t, - "relative-to-source", - "should not need `path` option if `source` option has been passed", - null, - {from: "test/fixtures/relative-to-source.css"} - ) - - compareFixtures( - t, - "modules", - "should be able to consume npm package or local modules", - {root: __dirname, path: importsDir} - ) - - var base = "@import url(http://)" - t.equal( - postcss() - .use(atImport()) - .process(base) - .css.trim(), - base, - "should not fail with only one absolute import" - ) - - t.equal( - postcss() - .use(atImport()) - .process( - "@import url('http://');\n@import 'test/fixtures/imports/foo.css';" - ) - .css.trim(), - "@import url('http://');\nfoo{}", - "should not fail with absolute and local import" - ) - - t.end() -}) - -test("@import error output", function(t) { - var file = importsDir + "/import-missing.css" - t.throws( - function() { - postcss() - .use(atImport()) - .process(fs.readFileSync(file), {from: file}) - .css.trim() - }, - /* eslint-disable max-len */ - /import-missing.css:2:5: Failed to find 'missing-file.css' from .*\n\s+in \[/gm, - /* eslint-enabme max-len */ - "should output readable trace" - ) - - t.end() -}) - -test("@import glob pattern matches no files", function(t) { - var file = importsDir + "/glob-missing.css" - t.equal( - postcss() - .use(atImport({glob: true})) - .process(fs.readFileSync(file), {from: file}) - .css.trim(), - "foobar{}", - "should fail silently, skipping the globbed import, if no files found" - ) - - t.end() -}) - -test("@import sourcemap", function(t) { - t.equal( - postcss() - .use(atImport()) - .process(read("sourcemap/in"), { - from: "./test/sourcemap/in.css", - to: null, - map: { - inline: true, - sourcesContent: true, - }, - }) - .css.trim(), - read("sourcemap/out"), - "should contain a correct sourcemap" - ) - - t.end() -}) - -test("@import callback", function(t) { - postcss() - .use(atImport({ - path: importsDir, - onImport: function onImport(files) { - t.deepEqual( - files, - [ - path.join(__dirname, "fixtures", "recursive.css"), - path.join(__dirname, "fixtures", "imports", "foo-recursive.css"), - path.join(__dirname, "fixtures", "imports", "bar.css"), - ], - "should have a callback that returns an object containing imported " + - "files" - ) - - t.end() - }, - })) - .process(read("fixtures/recursive"), { - from: "./test/fixtures/recursive.css", - }) - .css.trim() -}) - -test("import relative files using path option only", function(t) { - t.equal( - postcss() - .use(atImport({path: "test/fixtures/imports/relative"})) - .process(read("fixtures/imports/relative/import")) - .css.trim(), - read("fixtures/imports/bar") - ) - t.end() -}) - -test("inlined @import should keep PostCSS AST references clean", function(t) { - var root = postcss() - .use(atImport()) - .process("@import 'test/fixtures/imports/foo.css';\nbar{}") - .root - root.nodes.forEach(function(node) { - t.equal(root, node.parent) - }) - - t.end() -}) - -test("works with no styles at all", function(t) { - t.doesNotThrow(function() { - postcss() - .use(atImport()) - .process("") - .css.trim() - }, "should works with nothing without throwing an error") - - t.end() -}) - -test("@import custom resolve", function(t) { - var resolve = require("resolve") - var sassResolve = function(file, opts) { - opts = opts || {} - opts.extensions = [".scss", ".css"] - opts.packageFilter = function(pkg) { - pkg.main = pkg.sass || pkg.style || "index" - return pkg - } - return resolve.sync(file, opts) - } - compareFixtures( - t, - "custom-resolve-modules", - "should be able to consume modules in the custom-resolve way", - {root: __dirname, path: importsDir, resolve: sassResolve} - ) - - t.end() -}) - -test("warn when a import doesn't have ;", function(t) { - t.equal( - postcss() - .use(atImport()) - .process("@import url('http://') :root{}") - .warnings()[0].text, - atImport.warnNodesMessage, - "should warn when a user didn't close an import with ;" - ) - - t.equal( - postcss() - .use(atImport()) - .process("@import url('http://');") - .warnings().length, - 0, - "should not warn when a user closed an import with ;" - ) - - t.end() -}) diff --git a/test/layer.js b/test/layer.js new file mode 100644 index 00000000..52236a68 --- /dev/null +++ b/test/layer.js @@ -0,0 +1,59 @@ +"use strict" +// external tooling +const test = require("ava") + +// internal tooling +const checkFixture = require("./helpers/check-fixture") + +test("should resolve layers of import statements", checkFixture, "layer") + +test( + "should correctly wrap imported at rules in layers", + checkFixture, + "layer-import-atrules", +) + +test( + "should correctly wrap imported at rules in anonymous layers", + checkFixture, + "layer-import-atrules-anonymous", +) + +test( + "should correctly handle duplicate anonymous imports", + checkFixture, + "layer-duplicate-anonymous-imports", + { + skipDuplicates: false, + }, +) + +test( + "should correctly handle duplicate anonymous imports and skip duplicates is true", + checkFixture, + "layer-duplicate-anonymous-imports-skip", +) + +test( + "should correctly handle layer statements followed by ignored imports", + checkFixture, + "layer-followed-by-ignore", +) + +test( + "should correctly handle layer statements followed by ignored imports in conditional imports", + checkFixture, + "layer-followed-by-ignore-with-conditions", +) + +test( + "should correctly handle layer statements followed by ignored imports in unconditional imports", + checkFixture, + "layer-followed-by-ignore-without-conditions", +) + +test( + "should correctly handle layer statements in conditional imports", + checkFixture, + "layer-statement-with-conditions", +) diff --git a/test/lint.js b/test/lint.js new file mode 100644 index 00000000..6f531de3 --- /dev/null +++ b/test/lint.js @@ -0,0 +1,287 @@ +"use strict" +// external tooling +const test = require("ava") +const postcss = require("postcss") + +// plugin +const atImport = require("..") + +const processor = postcss().use(atImport()) + +test("should warn when not @charset and not @import statement before", t => { + return Promise.all([ + processor.process(`a {} @import "";`, { from: undefined }), + processor.process(`@media {} @import "";`, { from: undefined }), + ]).then(results => { + results.forEach(result => { + const warnings = result.warnings() + t.is(warnings.length, 1) + t.is( + warnings[0].text, + "@import must precede all other statements (besides @charset or empty @layer)", + ) + }) + }) +}) + +test("should warn about all imports after some other CSS declaration", t => { + return processor + .process( + ` + a {} + @import "a.css"; + @import "b.css"; + `, + { from: undefined }, + ) + .then(result => { + t.plan(2) + result.warnings().forEach(warning => { + t.is( + warning.text, + "@import must precede all other statements (besides @charset or empty @layer)", + ) + }) + }) +}) + +test("should warn if non-empty @layer before @import", t => { + return processor + .process(`@layer { a {} } @import "a.css";`, { from: undefined }) + .then(result => { + t.plan(1) + result.warnings().forEach(warning => { + t.is( + warning.text, + "@import must precede all other statements (besides @charset or empty @layer)", + ) + }) + }) +}) + +test("should warn when import statements are not consecutive", t => { + return processor + .process( + ` + @import "bar.css"; + @layer a; + @import "bar.css"; + `, + { from: "test/fixtures/imports/foo.css" }, + ) + .then(result => { + t.plan(1) + result.warnings().forEach(warning => { + t.is( + warning.text, + "@import must precede all other statements (besides @charset or empty @layer)", + ) + }) + }) +}) + +test("should not warn if empty @layer before @import", t => { + return processor + .process(`@layer a; @import "";`, { from: undefined }) + .then(result => { + const warnings = result.warnings() + t.is(warnings.length, 1) + t.is(warnings[0].text, `Unable to find uri in '@import ""'`) + }) +}) + +test("should not warn if comments before @import", t => { + return processor + .process(`/* skipped comment */ @import "";`, { from: undefined }) + .then(result => { + const warnings = result.warnings() + t.is(warnings.length, 1) + t.is(warnings[0].text, `Unable to find uri in '@import ""'`) + }) +}) + +test("should warn if something before comments", t => { + return processor + .process(`a{} /* skipped comment */ @import "";`, { from: undefined }) + .then(result => { + t.is(result.warnings().length, 1) + }) +}) + +test("should not warn when @charset or @import statement before", t => { + return Promise.all([ + processor.process(`@import "bar.css"; @import "bar.css";`, { + from: "test/fixtures/imports/foo.css", + }), + processor.process(`@charset "bar.css"; @import "bar.css";`, { + from: "test/fixtures/imports/foo.css", + }), + ]).then(results => { + results.forEach(result => { + t.is(result.warnings().length, 0) + }) + }) +}) + +test("should warn when @charset is not first", t => { + return Promise.all([ + processor.process(`a {} @charset "utf-8";`, { from: undefined }), + processor.process(`@media {} @charset "utf-8";`, { from: undefined }), + processor.process(`/* foo */ @charset "utf-8";`, { from: undefined }), + processor.process(`@import "bar.css"; @charset "utf-8";`, { + from: "test/fixtures/imports/foo.css", + }), + ]).then(results => { + results.forEach(result => { + const warnings = result.warnings() + t.is(warnings.length, 1) + t.is(warnings[0].text, "@charset must precede all other statements") + }) + }) +}) + +test("should warn when a user didn't close an import with ;", t => { + return processor + .process(`@import url('http://') :root{}`, { from: undefined }) + .then(result => { + const warnings = result.warnings() + t.is(warnings.length, 1) + t.is( + warnings[0].text, + "It looks like you didn't end your @import statement correctly. " + + "Child nodes are attached to it.", + ) + }) +}) + +test("should warn on invalid url", t => { + return processor + .process( + ` + @import foo-bar; + @import ; + @import ''; + @import ""; + @import url(); + @import url(''); + @import url(""); + @import layer url(""); + @import supports(foo: bar) url(""); + `, + { from: undefined }, + ) + .then(result => { + const warnings = result.warnings() + t.is(warnings.length, 9) + t.is(warnings[0].text, `Unable to find uri in '@import foo-bar'`) + t.is(warnings[1].text, `Unable to find uri in '@import '`) + t.is(warnings[2].text, `Unable to find uri in '@import '''`) + t.is(warnings[3].text, `Unable to find uri in '@import ""'`) + t.is(warnings[4].text, `Unable to find uri in '@import url()'`) + t.is(warnings[5].text, `Unable to find uri in '@import url('')'`) + t.is(warnings[6].text, `Unable to find uri in '@import url("")'`) + t.is(warnings[7].text, `Unable to find uri in '@import layer url("")'`) + t.is( + warnings[8].text, + `Unable to find uri in '@import supports(foo: bar) url("")'`, + ) + }) +}) + +test("should warn on duplicate url's", t => { + return processor + .process( + ` + @import 'foo' "bar"; + @import "foo" url(bar); + @import url(foo) "bar"; + @import url('foo') url(bar); + @import url('foo') layer url(bar); + @import url('foo') layer(foo) "bar"; + @import url('foo') supports(foo: bar) url(bar); + `, + { from: undefined }, + ) + .then(result => { + const warnings = result.warnings() + t.is(warnings.length, 7) + t.is(warnings[0].text, `Multiple url's in '@import 'foo' "bar"'`) + t.is(warnings[1].text, `Multiple url's in '@import "foo" url(bar)'`) + t.is(warnings[2].text, `Multiple url's in '@import url(foo) "bar"'`) + t.is(warnings[3].text, `Multiple url's in '@import url('foo') url(bar)'`) + t.is( + warnings[4].text, + `Multiple url's in '@import url('foo') layer url(bar)'`, + ) + t.is( + warnings[5].text, + `Multiple url's in '@import url('foo') layer(foo) "bar"'`, + ) + t.is( + warnings[6].text, + `Multiple url's in '@import url('foo') supports(foo: bar) url(bar)'`, + ) + }) +}) + +test("should warn on multiple layer clauses", t => { + return processor + .process( + ` + @import url('foo') layer layer(bar); + `, + { from: undefined }, + ) + .then(result => { + const warnings = result.warnings() + t.is(warnings.length, 1) + t.is( + warnings[0].text, + `Multiple layers in '@import url('foo') layer layer(bar)'`, + ) + }) +}) + +test("should warn on when support conditions precede layer clauses", t => { + return processor + .process( + ` + @import url('foo') supports(selector(&)) layer(bar); + `, + { from: undefined }, + ) + .then(result => { + const warnings = result.warnings() + t.is(warnings.length, 1) + t.is( + warnings[0].text, + `layers must be defined before support conditions in '@import url('foo') supports(selector(&)) layer(bar)'`, + ) + }) +}) + +test("should warn on multiple support conditions", t => { + return processor + .process( + ` + @import url('foo') supports(selector(&)) supports((display: grid)); + `, + { from: undefined }, + ) + .then(result => { + const warnings = result.warnings() + t.is(warnings.length, 1) + t.is( + warnings[0].text, + `Multiple support conditions in '@import url('foo') supports(selector(&)) supports((display: grid))'`, + ) + }) +}) + +test("should not warn when a user closed an import with ;", t => { + return processor + .process(`@import url('http://');`, { from: undefined }) + .then(result => { + t.is(result.warnings().length, 0) + }) +}) diff --git a/test/media.js b/test/media.js new file mode 100644 index 00000000..38ce5c10 --- /dev/null +++ b/test/media.js @@ -0,0 +1,28 @@ +"use strict" +// external tooling +const test = require("ava") + +// internal tooling +const checkFixture = require("./helpers/check-fixture") + +test( + "should resolve media queries of import statements", + checkFixture, + "media-import", +) + +test("should resolve media queries", checkFixture, "media-query") + +test( + "should resolve content inside import with media queries", + checkFixture, + "media-content", +) + +test( + "should resolve media query imports with charset", + checkFixture, + "media-charset", +) + +test("should correctly combine media queries", checkFixture, "media-combine") diff --git a/test/node_modules/modularized-css/foo/boo/boomod.css b/test/node_modules/modularized-css/foo/boo/boomod.css new file mode 100644 index 00000000..35898965 --- /dev/null +++ b/test/node_modules/modularized-css/foo/boo/boomod.css @@ -0,0 +1 @@ +.i-pity-the-boo{} diff --git a/test/node_modules/modularized-css/foo/index.css b/test/node_modules/modularized-css/foo/index.css new file mode 100644 index 00000000..21da7bd3 --- /dev/null +++ b/test/node_modules/modularized-css/foo/index.css @@ -0,0 +1 @@ +.i-pity-the-foo{} diff --git a/test/order.js b/test/order.js new file mode 100644 index 00000000..86af6642 --- /dev/null +++ b/test/order.js @@ -0,0 +1,26 @@ +"use strict" +// external tooling +const test = require("ava") + +// internal tooling +const checkFixture = require("./helpers/check-fixture") + +test(`should order nested imports correctly`, t => { + let first = true + const path = require("path") + + return checkFixture(t, "order", { + path: "test/fixtures/imports", + resolve: id => { + return new Promise(res => { + const doResolve = () => res(path.resolve("test/fixtures/imports", id)) + + if (first) { + // Delay the first import so the second gets loaded first + setTimeout(doResolve, 100) + first = false + } else doResolve() + }) + }, + }) +}) diff --git a/test/plugins.js b/test/plugins.js new file mode 100644 index 00000000..31df1ffb --- /dev/null +++ b/test/plugins.js @@ -0,0 +1,46 @@ +"use strict" +// external tooling +const test = require("ava") +const postcss = require("postcss") + +// plugin +const atImport = require("..") + +// internal tooling +const checkFixture = require("./helpers/check-fixture") + +test("should apply plugins to root", t => { + const atRules = [] + const rules = [] + return checkFixture(t, "plugins", { + plugins: [ + css => { + css.walk(node => { + if (node.type === "rule") { + rules.push(node.selector) + if (node.selector === "bar") node.remove() + else node.selector += "-converted" + } + if (node.type === "atrule") atRules.push(node.name) + }) + }, + ], + }).then(() => { + t.deepEqual(atRules, ["import"]) + t.deepEqual(rules, ["foo", "bar"]) + }) +}) + +test("should error when value is not an array", t => { + return postcss() + .use(atImport({ plugins: "foo" })) + .process("", { from: undefined }) + .catch(error => t.regex(error.message, /plugins option must be an array/)) +}) + +test("should remain silent when value is an empty array", t => { + return postcss() + .use(atImport({ plugins: [] })) + .process("", { from: undefined }) + .then(result => t.is(result.css, "")) +}) diff --git a/test/resolve.js b/test/resolve.js new file mode 100644 index 00000000..53d475c3 --- /dev/null +++ b/test/resolve.js @@ -0,0 +1,97 @@ +"use strict" +// external tooling +const test = require("ava") +const fs = require("fs") +const path = require("path") + +// internal tooling +const checkFixture = require("./helpers/check-fixture") + +test("should resolve relative to cwd", checkFixture, "resolve-cwd", { + path: null, +}) + +test(`should resolve relative to 'root' option`, checkFixture, "resolve-root", { + root: "test/fixtures", + path: null, +}) + +test( + `should resolve relative to postcss 'from' option`, + checkFixture, + "resolve-from", + { path: null }, + { from: "test/fixtures/file.css" }, +) + +test( + `should resolve relative to 'path' which resolved with cwd`, + checkFixture, + "resolve-path-cwd", + { path: "test/fixtures/imports" }, +) + +test( + `should resolve relative to 'path' which resolved with 'root'`, + checkFixture, + "resolve-path-root", + { root: "test/fixtures", path: "imports" }, +) + +test("should resolve local modules", checkFixture, "resolve-local-modules", { + path: null, +}) + +test( + "should resolve modules with path option", + checkFixture, + "resolve-path-modules", + { + path: "test/fixtures/imports/modules", + }, +) + +test( + "should be able to consume npm package or local modules", + checkFixture, + "resolve-modules", + { path: null }, + { from: "test/fixtures/imports/foo.css" }, +) + +test( + "should be able to consume npm sub packages", + checkFixture, + "resolve-npm-subpackages", + { path: null }, + { from: "test/fixtures/imports/foo.css" }, +) + +test( + "should be able to consume modules from custom modules directories", + checkFixture, + "resolve-custom-modules", + { path: null, addModulesDirectories: ["shared_modules"] }, + { from: "test/fixtures/imports/foo.css" }, +) + +test( + "should resolve modules with node subpath imports with a custom resolver", + checkFixture, + "subpath", + { + resolve: (id, basedir) => { + // see: https://nodejs.org/api/packages.html#subpath-imports + if (id.startsWith("#")) { + const pkgJSON = JSON.parse( + fs.readFileSync(path.join(basedir, "package.json")), + ) + + return path.join(basedir, pkgJSON.imports[id]) + } + + return id + }, + path: "test/fixtures/imports/modules", + }, +) diff --git a/test/shared_modules/shared-auto/index.css b/test/shared_modules/shared-auto/index.css new file mode 100644 index 00000000..c0389260 --- /dev/null +++ b/test/shared_modules/shared-auto/index.css @@ -0,0 +1 @@ +.shared-auto{} diff --git a/test/shared_modules/shared-by-hand/style.css b/test/shared_modules/shared-by-hand/style.css new file mode 100644 index 00000000..8c4529b7 --- /dev/null +++ b/test/shared_modules/shared-by-hand/style.css @@ -0,0 +1 @@ +.shared-byHand{} diff --git a/test/shared_modules/shared-dep/index.css b/test/shared_modules/shared-dep/index.css new file mode 100644 index 00000000..bcb816d4 --- /dev/null +++ b/test/shared_modules/shared-dep/index.css @@ -0,0 +1 @@ +.shared-dep{} diff --git a/test/shared_modules/shared-fake/main.css b/test/shared_modules/shared-fake/main.css new file mode 100644 index 00000000..7154d977 --- /dev/null +++ b/test/shared_modules/shared-fake/main.css @@ -0,0 +1 @@ +.shared-fake{} diff --git a/test/shared_modules/shared-fake/package.json b/test/shared_modules/shared-fake/package.json new file mode 100644 index 00000000..f0181f31 --- /dev/null +++ b/test/shared_modules/shared-fake/package.json @@ -0,0 +1,3 @@ +{ + "style": "main.css" +} diff --git a/test/shared_modules/shared-nest/index.css b/test/shared_modules/shared-nest/index.css new file mode 100644 index 00000000..82685050 --- /dev/null +++ b/test/shared_modules/shared-nest/index.css @@ -0,0 +1 @@ +@import "shared-ed"; diff --git a/test/shared_modules/shared-nest/shared_modules/shared-ed/index.css b/test/shared_modules/shared-nest/shared_modules/shared-ed/index.css new file mode 100644 index 00000000..5f420fc8 --- /dev/null +++ b/test/shared_modules/shared-nest/shared_modules/shared-ed/index.css @@ -0,0 +1 @@ +.shared-nested{} diff --git a/test/shared_modules/shared-use-dep-too/index.css b/test/shared_modules/shared-use-dep-too/index.css new file mode 100644 index 00000000..8d53b982 --- /dev/null +++ b/test/shared_modules/shared-use-dep-too/index.css @@ -0,0 +1 @@ +@import "shared-dep"; diff --git a/test/shared_modules/shared-use-dep/index.css b/test/shared_modules/shared-use-dep/index.css new file mode 100644 index 00000000..8d53b982 --- /dev/null +++ b/test/shared_modules/shared-use-dep/index.css @@ -0,0 +1 @@ +@import "shared-dep"; diff --git a/test/sourcemap/imported.css b/test/sourcemap/imported.css index c9941e71..c9133230 100755 --- a/test/sourcemap/imported.css +++ b/test/sourcemap/imported.css @@ -1,3 +1,3 @@ html { background: blue; -} \ No newline at end of file +} diff --git a/test/sourcemap/out.css b/test/sourcemap/out.css index 0bba98fc..33f0115e 100755 --- a/test/sourcemap/out.css +++ b/test/sourcemap/out.css @@ -5,4 +5,5 @@ html { body { color: red; } -/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInRlc3Qvc291cmNlbWFwL2ltcG9ydGVkLmNzcyIsInRlc3Qvc291cmNlbWFwL2luLmNzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTtFQUNFLGlCQUFpQjtDQUNsQjs7QUNBRDtFQUNFLFdBQVc7Q0FDWiIsImZpbGUiOiJ0ZXN0L3NvdXJjZW1hcC9pbi5jc3MiLCJzb3VyY2VzQ29udGVudCI6WyJodG1sIHtcbiAgYmFja2dyb3VuZDogYmx1ZTtcbn0iLCJAaW1wb3J0IFwiaW1wb3J0ZWQuY3NzXCI7XG5cbmJvZHkge1xuICBjb2xvcjogcmVkO1xufSJdfQ== */ + +/*# sourceMappingURL=out.css.map */ diff --git a/test/sourcemap/out.css.map b/test/sourcemap/out.css.map new file mode 100644 index 00000000..e2a31ac7 --- /dev/null +++ b/test/sourcemap/out.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["test/sourcemap/imported.css","test/sourcemap/in.css"],"names":[],"mappings":"AAAA;EACE,gBAAgB;AAClB;;ACAA;EACE,UAAU;AACZ","file":"test/sourcemap/in.css","sourcesContent":["html {\n background: blue;\n}\n","@import \"imported.css\";\n\nbody {\n color: red;\n}\n"]} diff --git a/test/sourcemap/out.css.win.map b/test/sourcemap/out.css.win.map new file mode 100644 index 00000000..fa8890c4 --- /dev/null +++ b/test/sourcemap/out.css.win.map @@ -0,0 +1 @@ +{"version":3,"sources":["test/sourcemap/imported.css","test/sourcemap/in.css"],"names":[],"mappings":"AAAA;EACE,gBAAgB;AAClB;;ACAA;EACE,UAAU;AACZ","file":"test\\sourcemap\\in.css","sourcesContent":["html {\n background: blue;\n}\n","@import \"imported.css\";\n\nbody {\n color: red;\n}\n"]} diff --git a/test/supports-condition.js b/test/supports-condition.js new file mode 100644 index 00000000..f985264d --- /dev/null +++ b/test/supports-condition.js @@ -0,0 +1,12 @@ +"use strict" +// external tooling +const test = require("ava") + +// internal tooling +const checkFixture = require("./helpers/check-fixture") + +test( + "should resolve supports conditions of import statements", + checkFixture, + "supports-condition-import", +) diff --git a/test/syntax-error.js b/test/syntax-error.js new file mode 100644 index 00000000..98330607 --- /dev/null +++ b/test/syntax-error.js @@ -0,0 +1,19 @@ +"use strict" +// builtin tooling +const fs = require("fs") + +// external tooling +const test = require("ava") +const postcss = require("postcss") + +// plugin +const atImport = require("..") + +test("SyntaxError in imported file throws", t => { + return postcss(atImport({ path: "test/fixtures/imports" })) + .process(fs.readFileSync("test/fixtures/syntax-error.css", "utf8"), { + from: undefined, + }) + .then(() => t.fail("should error out")) + .catch(err => t.truthy(err)) +})