@parcel/transformer-postcss 2.0.0-nightly.102 → 2.0.0-nightly.1027

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,212 +1,292 @@
1
1
  // @flow
2
2
 
3
- import type {FilePath, MutableAsset} from '@parcel/types';
3
+ import type {FilePath, Asset, MutableAsset, PluginOptions} from '@parcel/types';
4
4
 
5
- import {md5FromString} from '@parcel/utils';
5
+ import {hashString} from '@parcel/hash';
6
+ import {glob} from '@parcel/utils';
6
7
  import {Transformer} from '@parcel/plugin';
7
- import FileSystemLoader from 'css-modules-loader-core/lib/file-system-loader';
8
8
  import nullthrows from 'nullthrows';
9
9
  import path from 'path';
10
- import postcss from 'postcss';
11
10
  import semver from 'semver';
12
11
  import valueParser from 'postcss-value-parser';
12
+ import typeof * as Postcss from 'postcss';
13
13
 
14
- import loadPlugins from './loadPlugins';
14
+ import {load} from './loadConfig';
15
+ import {POSTCSS_RANGE} from './constants';
16
+ import {md, generateJSONCodeHighlights} from '@parcel/diagnostic';
15
17
 
16
18
  const COMPOSES_RE = /composes:.+from\s*("|').*("|')\s*;?/;
17
19
  const FROM_IMPORT_RE = /.+from\s*(?:"|')(.*)(?:"|')\s*;?/;
20
+ const LEGACY_MODULE_RE = /@value|(:global|:local)(?!\s*\()/i;
18
21
  const MODULE_BY_NAME_RE = /\.module\./;
19
22
 
20
- type ParcelPostCSSConfig = {
21
- plugins: Array<mixed>,
22
- from: FilePath,
23
- to: FilePath,
24
- ...
25
- };
26
-
27
- export default new Transformer({
28
- async getConfig({asset, resolve, options}): Promise<?ParcelPostCSSConfig> {
29
- let configFile: mixed = await asset.getConfig(
30
- ['.postcssrc', '.postcssrc.json', '.postcssrc.js', 'postcss.config.js'],
31
- {packageKey: 'postcss'},
23
+ export default (new Transformer({
24
+ loadConfig({config, options, logger}) {
25
+ return load({config, options, logger});
26
+ },
27
+
28
+ canReuseAST({ast}) {
29
+ return (
30
+ ast.type === 'postcss' && semver.satisfies(ast.version, POSTCSS_RANGE)
32
31
  );
32
+ },
33
33
 
34
- // Use a basic, modules-only PostCSS config if the file opts in by a name
35
- // like foo.module.css
36
- if (configFile == null && asset.filePath.match(MODULE_BY_NAME_RE)) {
37
- configFile = {
38
- plugins: {
39
- 'postcss-modules': {},
34
+ async parse({asset, config, options}) {
35
+ let isLegacy = await isLegacyCssModule(asset);
36
+ if (!config && !isLegacy) {
37
+ return;
38
+ }
39
+
40
+ const postcss = await loadPostcss(options, asset.filePath);
41
+
42
+ return {
43
+ type: 'postcss',
44
+ version: '8.2.1',
45
+ program: postcss
46
+ .parse(await asset.getCode(), {
47
+ from: asset.filePath,
48
+ })
49
+ .toJSON(),
50
+ };
51
+ },
52
+
53
+ async transform({asset, config, options, resolve, logger}) {
54
+ asset.type = 'css';
55
+ let isLegacy = await isLegacyCssModule(asset);
56
+ if (isLegacy && !config) {
57
+ config = {
58
+ raw: {},
59
+ filePath: '',
60
+ hydrated: {
61
+ plugins: [],
62
+ from: asset.filePath,
63
+ to: asset.filePath,
64
+ modules: {},
40
65
  },
41
66
  };
42
- }
43
67
 
44
- if (configFile == null) {
45
- return;
68
+ // TODO: warning?
46
69
  }
47
70
 
48
- if (typeof configFile !== 'object') {
49
- throw new Error('PostCSS config should be an object.');
71
+ if (!config) {
72
+ return [asset];
50
73
  }
51
74
 
52
- if (
53
- configFile.plugins == null ||
54
- typeof configFile.plugins !== 'object' ||
55
- Object.keys(configFile.plugins) === 0
56
- ) {
57
- throw new Error('PostCSS config must have plugins');
58
- }
75
+ const postcss: Postcss = await loadPostcss(options, asset.filePath);
76
+ let ast = nullthrows(await asset.getAST());
77
+ let program = postcss.fromJSON(ast.program);
59
78
 
60
- let originalModulesConfig;
61
- let configFilePlugins = configFile.plugins;
62
- if (
63
- configFilePlugins != null &&
64
- typeof configFilePlugins === 'object' &&
65
- configFilePlugins['postcss-modules'] != null
66
- ) {
67
- originalModulesConfig = configFilePlugins['postcss-modules'];
68
- // $FlowFixMe
69
- delete configFilePlugins['postcss-modules'];
70
- }
79
+ let plugins = [...config.hydrated.plugins];
80
+ let cssModules: ?{|[string]: string|} = null;
81
+ if (config.hydrated.modules) {
82
+ asset.meta.cssModulesCompiled = true;
71
83
 
72
- let plugins = await loadPlugins(configFilePlugins, asset.filePath, options);
84
+ let code = asset.isASTDirty() ? null : await asset.getCode();
85
+ if (
86
+ Object.keys(config.hydrated.modules).length === 0 &&
87
+ code &&
88
+ !isLegacy &&
89
+ !LEGACY_MODULE_RE.test(code)
90
+ ) {
91
+ let filename = path.basename(config.filePath);
92
+ let message;
93
+ let configKey;
94
+ let hint;
95
+ if (config.raw.modules) {
96
+ message = md`The "modules" option in __${filename}__ can be replaced with configuration for @parcel/transformer-css to improve build performance.`;
97
+ configKey = '/modules';
98
+ hint = md`Remove the "modules" option from __${filename}__`;
99
+ } else {
100
+ message = md`The "postcss-modules" plugin in __${filename}__ can be replaced with configuration for @parcel/transformer-css to improve build performance.`;
101
+ configKey = '/plugins/postcss-modules';
102
+ hint = md`Remove the "postcss-modules" plugin from __${filename}__`;
103
+ }
73
104
 
74
- if (originalModulesConfig || configFile.modules) {
105
+ let hints = [
106
+ 'Enable the "cssModules" option for "@parcel/transformer-css" in your package.json',
107
+ ];
108
+ if (plugins.length === 0) {
109
+ message += md` Since there are no other plugins, __${filename}__ can be deleted safely.`;
110
+ hints.push(md`Delete __${filename}__`);
111
+ } else {
112
+ hints.push(hint);
113
+ }
114
+
115
+ let codeFrames;
116
+ if (path.extname(filename) !== '.js') {
117
+ let contents = await asset.fs.readFile(config.filePath, 'utf8');
118
+ codeFrames = [
119
+ {
120
+ language: 'json',
121
+ filePath: config.filePath,
122
+ code: contents,
123
+ codeHighlights: generateJSONCodeHighlights(contents, [
124
+ {
125
+ key: configKey,
126
+ type: 'key',
127
+ },
128
+ ]),
129
+ },
130
+ ];
131
+ } else {
132
+ codeFrames = [
133
+ {
134
+ filePath: config.filePath,
135
+ codeHighlights: [
136
+ {
137
+ start: {line: 1, column: 1},
138
+ end: {line: 1, column: 1},
139
+ },
140
+ ],
141
+ },
142
+ ];
143
+ }
144
+
145
+ logger.warn({
146
+ message,
147
+ hints,
148
+ documentationURL:
149
+ 'https://parceljs.org/languages/css/#enabling-css-modules-globally',
150
+ codeFrames,
151
+ });
152
+ }
153
+
154
+ // TODO: should this be resolved from the project root?
75
155
  let postcssModules = await options.packageManager.require(
76
156
  'postcss-modules',
77
157
  asset.filePath,
158
+ {
159
+ range: '^4.3.0',
160
+ saveDev: true,
161
+ shouldAutoInstall: options.shouldAutoInstall,
162
+ },
78
163
  );
79
164
 
80
165
  plugins.push(
81
166
  postcssModules({
82
- getJSON: (filename, json) => (asset.meta.cssModules = json),
83
- Loader: createLoader(asset, resolve),
84
- generateScopedName: (name, filename, css) =>
85
- `_${name}_${md5FromString(filename + css).substr(0, 5)}`,
86
- ...originalModulesConfig,
167
+ getJSON: (filename, json) => (cssModules = json),
168
+ Loader: await createLoader(asset, resolve, options),
169
+ generateScopedName: (name, filename) =>
170
+ `${name}_${hashString(
171
+ path.relative(options.projectRoot, filename),
172
+ ).substr(0, 6)}`,
173
+ ...config.hydrated.modules,
87
174
  }),
88
175
  );
89
- }
90
-
91
- return {
92
- plugins,
93
- from: asset.filePath,
94
- to: asset.filePath,
95
- };
96
- },
97
-
98
- canReuseAST({ast}) {
99
- return ast.type === 'postcss' && semver.satisfies(ast.version, '^7.0.0');
100
- },
101
-
102
- async parse({asset, config}) {
103
- if (!config) {
104
- return;
105
- }
106
176
 
107
- return {
108
- type: 'postcss',
109
- version: '7.0.0',
110
- program: postcss.parse(await asset.getCode(), {
111
- from: asset.filePath,
112
- }),
113
- };
114
- },
115
-
116
- async transform({asset, config}) {
117
- if (!config) {
118
- return [asset];
119
- }
177
+ if (code == null || COMPOSES_RE.test(code)) {
178
+ program.walkDecls(decl => {
179
+ let [, importPath] = FROM_IMPORT_RE.exec(decl.value) || [];
180
+ if (decl.prop === 'composes' && importPath != null) {
181
+ let parsed = valueParser(decl.value);
120
182
 
121
- let ast = nullthrows(asset.ast);
122
- if (COMPOSES_RE.test(await asset.getCode())) {
123
- ast.program.walkDecls(decl => {
124
- let [, importPath] = FROM_IMPORT_RE.exec(decl.value) || [];
125
- if (decl.prop === 'composes' && importPath != null) {
126
- let parsed = valueParser(decl.value);
127
-
128
- parsed.walk(node => {
129
- if (node.type === 'string') {
130
- asset.addDependency({
131
- moduleSpecifier: importPath,
132
- loc: {
133
- filePath: importPath,
134
- start: decl.source.start,
135
- end: {
136
- line: decl.source.start.line,
137
- column: decl.source.start.column + importPath.length,
183
+ parsed.walk(node => {
184
+ if (node.type === 'string') {
185
+ asset.addDependency({
186
+ specifier: importPath,
187
+ specifierType: 'url',
188
+ loc: {
189
+ filePath: asset.filePath,
190
+ start: decl.source.start,
191
+ end: {
192
+ line: decl.source.start.line,
193
+ column: decl.source.start.column + importPath.length,
194
+ },
138
195
  },
139
- },
140
- });
141
- }
142
- });
143
- }
144
- });
196
+ });
197
+ }
198
+ });
199
+ }
200
+ });
201
+ }
145
202
  }
146
203
 
147
- let {messages, root} = await postcss(config.plugins).process(
148
- ast.program,
149
- config,
204
+ // $FlowFixMe Added in Flow 0.121.0 upgrade in #4381
205
+ let {messages, root} = await postcss(plugins).process(
206
+ program,
207
+ config.hydrated,
150
208
  );
151
- ast.program = root;
152
- ast.isDirty = true;
209
+ asset.setAST({
210
+ type: 'postcss',
211
+ version: '8.2.1',
212
+ program: root.toJSON(),
213
+ });
153
214
  for (let msg of messages) {
154
215
  if (msg.type === 'dependency') {
155
- // $FlowFixMe merely a convention
156
- msg = (msg: {|
157
- type: 'dependency',
158
- plugin: string,
159
- file: string,
160
- parent: string,
161
- |});
162
-
163
- asset.addIncludedFile({
164
- filePath: msg.file,
165
- });
216
+ asset.invalidateOnFileChange(msg.file);
217
+ } else if (msg.type === 'dir-dependency') {
218
+ let pattern = `${msg.dir}/${msg.glob ?? '**/*'}`;
219
+ let files = await glob(pattern, asset.fs, {onlyFiles: true});
220
+ for (let file of files) {
221
+ asset.invalidateOnFileChange(path.normalize(file));
222
+ }
223
+ asset.invalidateOnFileCreate({glob: pattern});
166
224
  }
167
225
  }
168
226
 
169
227
  let assets = [asset];
170
- if (asset.meta.cssModules) {
171
- let code = JSON.stringify(asset.meta.cssModules, null, 2);
172
- let deps = asset.getDependencies().filter(dep => !dep.isURL);
228
+ if (cssModules) {
229
+ // $FlowFixMe
230
+ let cssModulesList = (Object.entries(cssModules): Array<
231
+ [string, string],
232
+ >);
233
+ let deps = asset.getDependencies().filter(dep => dep.priority === 'sync');
234
+ let code: string;
173
235
  if (deps.length > 0) {
174
236
  code = `
175
237
  module.exports = Object.assign({}, ${deps
176
- .map(dep => `require(${JSON.stringify(dep.moduleSpecifier)})`)
177
- .join(', ')}, ${code});
238
+ .map(dep => `require(${JSON.stringify(dep.specifier)})`)
239
+ .join(', ')}, ${JSON.stringify(cssModules, null, 2)});
178
240
  `;
179
241
  } else {
180
- code = `module.exports = ${code};`;
242
+ code = cssModulesList
243
+ .map(
244
+ // This syntax enables shaking the invidual statements, so that unused classes don't even exist in JS.
245
+ ([className, classNameHashed]) =>
246
+ `module.exports[${JSON.stringify(className)}] = ${JSON.stringify(
247
+ classNameHashed,
248
+ )};`,
249
+ )
250
+ .join('\n');
251
+ }
252
+
253
+ asset.symbols.ensure();
254
+ for (let [k, v] of cssModulesList) {
255
+ asset.symbols.set(k, v);
181
256
  }
257
+ asset.symbols.set('default', 'default');
182
258
 
183
259
  assets.push({
184
260
  type: 'js',
185
- filePath: asset.filePath + '.js',
186
- code,
261
+ content: code,
187
262
  });
188
263
  }
189
264
  return assets;
190
265
  },
191
266
 
192
- generate({asset}) {
193
- let ast = nullthrows(asset.ast);
267
+ async generate({asset, ast, options}) {
268
+ const postcss: Postcss = await loadPostcss(options, asset.filePath);
194
269
 
195
270
  let code = '';
196
- postcss.stringify(ast.program, c => {
271
+ postcss.stringify(postcss.fromJSON(ast.program), c => {
197
272
  code += c;
198
273
  });
199
274
 
200
275
  return {
201
- code,
276
+ content: code,
202
277
  };
203
278
  },
204
- });
279
+ }): Transformer);
205
280
 
206
- function createLoader(
281
+ async function createLoader(
207
282
  asset: MutableAsset,
208
283
  resolve: (from: FilePath, to: string) => Promise<FilePath>,
284
+ options: PluginOptions,
209
285
  ) {
286
+ let {default: FileSystemLoader} = await options.packageManager.require(
287
+ 'postcss-modules/build/css-loader-core/loader',
288
+ asset.filePath,
289
+ );
210
290
  return class ParcelFileSystemLoader extends FileSystemLoader {
211
291
  async fetch(composesPath, relativeTo) {
212
292
  let importPath = composesPath.replace(/^["']|["']$/g, '');
@@ -224,6 +304,7 @@ function createLoader(
224
304
  source,
225
305
  rootRelativePath,
226
306
  undefined,
307
+ // $FlowFixMe[method-unbinding]
227
308
  this.fetch.bind(this),
228
309
  );
229
310
  return exportTokens;
@@ -234,3 +315,20 @@ function createLoader(
234
315
  }
235
316
  };
236
317
  }
318
+
319
+ function loadPostcss(options: PluginOptions, from: FilePath): Promise<Postcss> {
320
+ return options.packageManager.require('postcss', from, {
321
+ range: POSTCSS_RANGE,
322
+ saveDev: true,
323
+ shouldAutoInstall: options.shouldAutoInstall,
324
+ });
325
+ }
326
+
327
+ async function isLegacyCssModule(asset: Asset | MutableAsset) {
328
+ if (!MODULE_BY_NAME_RE.test(asset.filePath)) {
329
+ return false;
330
+ }
331
+
332
+ let code = await asset.getCode();
333
+ return LEGACY_MODULE_RE.test(code);
334
+ }
@@ -0,0 +1,3 @@
1
+ // @flow
2
+
3
+ export const POSTCSS_RANGE = '^8.2.1';
@@ -0,0 +1,215 @@
1
+ // @flow
2
+ import type {
3
+ Config,
4
+ FilePath,
5
+ PluginOptions,
6
+ PluginLogger,
7
+ } from '@parcel/types';
8
+ import path from 'path';
9
+ import {relativePath} from '@parcel/utils';
10
+ import {md, generateJSONCodeHighlights} from '@parcel/diagnostic';
11
+ import nullthrows from 'nullthrows';
12
+ import clone from 'clone';
13
+ import {POSTCSS_RANGE} from './constants';
14
+
15
+ import loadExternalPlugins from './loadPlugins';
16
+
17
+ type ConfigResult = {|
18
+ raw: any,
19
+ filePath: string,
20
+ hydrated: {|
21
+ plugins: Array<any>,
22
+ from: FilePath,
23
+ to: FilePath,
24
+ modules: any,
25
+ |},
26
+ |};
27
+
28
+ async function configHydrator(
29
+ configFile: any,
30
+ config: Config,
31
+ resolveFrom: FilePath,
32
+ options: PluginOptions,
33
+ logger: PluginLogger,
34
+ ): Promise<?ConfigResult> {
35
+ if (configFile == null) {
36
+ return;
37
+ }
38
+
39
+ // Load the custom config...
40
+ let modulesConfig;
41
+ let configFilePlugins = clone(configFile.plugins);
42
+ if (
43
+ configFilePlugins != null &&
44
+ typeof configFilePlugins === 'object' &&
45
+ configFilePlugins['postcss-modules'] != null
46
+ ) {
47
+ modulesConfig = configFilePlugins['postcss-modules'];
48
+ delete configFilePlugins['postcss-modules'];
49
+ }
50
+
51
+ if (!modulesConfig && configFile.modules) {
52
+ modulesConfig = {};
53
+ }
54
+
55
+ let plugins = await loadExternalPlugins(
56
+ configFilePlugins,
57
+ nullthrows(resolveFrom),
58
+ options,
59
+ );
60
+
61
+ // contents is either:
62
+ // from JSON: { plugins: { 'postcss-foo': { ...opts } } }
63
+ // from JS (v8): { plugins: [ { postcssPlugin: 'postcss-foo', ...visitor callback functions } ]
64
+ // from JS (v7): { plugins: [ [Function: ...] ]
65
+ let pluginArray = Array.isArray(configFilePlugins)
66
+ ? configFilePlugins
67
+ : Object.keys(configFilePlugins);
68
+ for (let p of pluginArray) {
69
+ if (typeof p === 'string') {
70
+ config.addDevDependency({
71
+ specifier: p,
72
+ resolveFrom: nullthrows(resolveFrom),
73
+ });
74
+ }
75
+ }
76
+
77
+ let redundantPlugins = pluginArray.filter(
78
+ p => p === 'autoprefixer' || p === 'postcss-preset-env',
79
+ );
80
+ if (redundantPlugins.length > 0) {
81
+ let filename = path.basename(resolveFrom);
82
+ let message;
83
+ let hints = [];
84
+ if (redundantPlugins.length === pluginArray.length) {
85
+ message = md`Parcel includes CSS transpilation and vendor prefixing by default. PostCSS config __${filename}__ contains only redundant plugins. Deleting it may significantly improve build performance.`;
86
+ hints.push(md`Delete __${filename}__`);
87
+ } else {
88
+ message = md`Parcel includes CSS transpilation and vendor prefixing by default. PostCSS config __${filename}__ contains the following redundant plugins: ${[
89
+ ...redundantPlugins,
90
+ ].map(p =>
91
+ md.underline(p),
92
+ )}. Removing these may improve build performance.`;
93
+ hints.push(md`Remove the above plugins from __${filename}__`);
94
+ }
95
+
96
+ let codeFrames;
97
+ if (path.extname(filename) !== '.js') {
98
+ let contents = await options.inputFS.readFile(resolveFrom, 'utf8');
99
+ codeFrames = [
100
+ {
101
+ language: 'json',
102
+ filePath: resolveFrom,
103
+ code: contents,
104
+ codeHighlights: generateJSONCodeHighlights(
105
+ contents,
106
+ redundantPlugins.map(plugin => ({
107
+ key: `/plugins/${plugin}`,
108
+ type: 'key',
109
+ })),
110
+ ),
111
+ },
112
+ ];
113
+ } else {
114
+ codeFrames = [
115
+ {
116
+ filePath: resolveFrom,
117
+ codeHighlights: [
118
+ {
119
+ start: {line: 1, column: 1},
120
+ end: {line: 1, column: 1},
121
+ },
122
+ ],
123
+ },
124
+ ];
125
+ }
126
+
127
+ logger.warn({
128
+ message,
129
+ hints,
130
+ documentationURL: 'https://parceljs.org/languages/css/#default-plugins',
131
+ codeFrames,
132
+ });
133
+ }
134
+
135
+ return {
136
+ raw: configFile,
137
+ filePath: resolveFrom,
138
+ hydrated: {
139
+ plugins,
140
+ from: config.searchPath,
141
+ to: config.searchPath,
142
+ modules: modulesConfig,
143
+ },
144
+ };
145
+ }
146
+
147
+ export async function load({
148
+ config,
149
+ options,
150
+ logger,
151
+ }: {|
152
+ config: Config,
153
+ options: PluginOptions,
154
+ logger: PluginLogger,
155
+ |}): Promise<?ConfigResult> {
156
+ if (!config.isSource) {
157
+ return;
158
+ }
159
+
160
+ let configFile: any = await config.getConfig(
161
+ ['.postcssrc', '.postcssrc.json', '.postcssrc.js', 'postcss.config.js'],
162
+ {packageKey: 'postcss'},
163
+ );
164
+
165
+ let contents = null;
166
+ if (configFile) {
167
+ config.addDevDependency({
168
+ specifier: 'postcss',
169
+ resolveFrom: config.searchPath,
170
+ range: POSTCSS_RANGE,
171
+ });
172
+
173
+ contents = configFile.contents;
174
+ let isDynamic = configFile && path.extname(configFile.filePath) === '.js';
175
+ if (isDynamic) {
176
+ // We have to invalidate on startup in case the config is non-deterministic,
177
+ // e.g. using unknown environment variables, reading from the filesystem, etc.
178
+ logger.warn({
179
+ message:
180
+ 'WARNING: Using a JavaScript PostCSS config file means losing out on caching features of Parcel. Use a .postcssrc(.json) file whenever possible.',
181
+ });
182
+
183
+ config.invalidateOnStartup();
184
+
185
+ // Also add the config as a dev dependency so we attempt to reload in watch mode.
186
+ config.addDevDependency({
187
+ specifier: relativePath(
188
+ path.dirname(config.searchPath),
189
+ configFile.filePath,
190
+ ),
191
+ resolveFrom: config.searchPath,
192
+ });
193
+ }
194
+
195
+ if (typeof contents !== 'object') {
196
+ throw new Error('PostCSS config should be an object.');
197
+ }
198
+
199
+ if (
200
+ contents.plugins == null ||
201
+ typeof contents.plugins !== 'object' ||
202
+ Object.keys(contents.plugins).length === 0
203
+ ) {
204
+ throw new Error('PostCSS config must have plugins');
205
+ }
206
+ }
207
+
208
+ return configHydrator(
209
+ contents,
210
+ config,
211
+ configFile?.filePath,
212
+ options,
213
+ logger,
214
+ );
215
+ }