diff --git a/packages/core/core/src/requests/ParcelConfigRequest.js b/packages/core/core/src/requests/ParcelConfigRequest.js index b2562c93855..463a984dfc5 100644 --- a/packages/core/core/src/requests/ParcelConfigRequest.js +++ b/packages/core/core/src/requests/ParcelConfigRequest.js @@ -264,6 +264,16 @@ function processPipeline( } } +const RESERVED_PIPELINES = new Set([ + 'node:', + 'npm:', + 'http:', + 'https:', + 'data:', + 'tel:', + 'mailto:', +]); + async function processMap( // $FlowFixMe map: ?ConfigMap, @@ -277,12 +287,12 @@ async function processMap( // $FlowFixMe let res: ConfigMap = {}; for (let k in map) { - if (k.startsWith('node:')) { + let i = k.indexOf(':'); + if (i > 0 && RESERVED_PIPELINES.has(k.slice(0, i + 1))) { let code = await options.inputFS.readFile(filePath, 'utf8'); throw new ThrowableDiagnostic({ diagnostic: { - message: - 'Named pipeline `node:` is reserved for builtin Node.js libraries', + message: `Named pipeline '${k.slice(0, i + 1)}' is reserved.`, origin: '@parcel/core', codeFrames: [ { diff --git a/packages/core/core/src/requests/PathRequest.js b/packages/core/core/src/requests/PathRequest.js index ada5fb8dddf..73bdcd62c93 100644 --- a/packages/core/core/src/requests/PathRequest.js +++ b/packages/core/core/src/requests/PathRequest.js @@ -1,11 +1,6 @@ // @flow strict-local import type {Diagnostic} from '@parcel/diagnostic'; -import type { - Async, - FileCreateInvalidation, - FilePath, - QueryParameters, -} from '@parcel/types'; +import type {Async, FileCreateInvalidation, FilePath} from '@parcel/types'; import type {StaticRunOpts} from '../RequestTracker'; import type {AssetGroup, Dependency, ParcelOptions} from '../types'; import type {ConfigAndCachePath} from './ParcelConfigRequest'; @@ -14,9 +9,7 @@ import ThrowableDiagnostic, {errorToDiagnostic, md} from '@parcel/diagnostic'; import {PluginLogger} from '@parcel/logger'; import nullthrows from 'nullthrows'; import path from 'path'; -import URL from 'url'; import {normalizePath} from '@parcel/utils'; -import querystring from 'querystring'; import {report} from '../ReporterRunner'; import PublicDependency from '../public/Dependency'; import PluginOptions from '../public/PluginOptions'; @@ -50,7 +43,6 @@ type RunOpts = {| |}; const type = 'path_request'; -const QUERY_PARAMS_REGEX = /^([^\t\r\n\v\f?]*)(\?.*)?/; const PIPELINE_REGEX = /^([a-z0-9-]+?):(.*)$/i; export default function createPathRequest( @@ -166,8 +158,7 @@ export class ResolverRunner { let resolvers = await this.config.getResolvers(); let pipeline; - let filePath; - let query: ?QueryParameters; + let specifier; let validPipelines = new Set(this.config.getNamedPipelines()); let match = dependency.specifier.match(PIPELINE_REGEX); if ( @@ -176,74 +167,20 @@ export class ResolverRunner { // and include e.g. `C:\` on Windows, conflicting with pipelines. !path.isAbsolute(dependency.specifier) ) { - if (dependency.specifier.startsWith('node:')) { - filePath = dependency.specifier; - } else { - [, pipeline, filePath] = match; - if (!validPipelines.has(pipeline)) { - if (dep.specifierType === 'url') { - // This may be a url protocol or scheme rather than a pipeline, such as - // `url('http://example.com/foo.png')` - return {assetGroup: null}; - } else { - return { - assetGroup: null, - diagnostics: [ - await this.getDiagnostic( - dependency, - md`Unknown pipeline: ${pipeline}.`, - ), - ], - }; - } - } - } - } else { - if (dep.specifierType === 'url') { - if (dependency.specifier.startsWith('//')) { - // A protocol-relative URL, e.g `url('//example.com/foo.png')` - return {assetGroup: null}; - } - if (dependency.specifier.startsWith('#')) { - // An ID-only URL, e.g. `url(#clip-path)` for CSS rules - return {assetGroup: null}; - } - } - filePath = dependency.specifier; - } - - let queryPart = null; - if (dep.specifierType === 'url') { - let parsed = URL.parse(filePath); - if (typeof parsed.pathname !== 'string') { - return { - assetGroup: null, - diagnostics: [ - await this.getDiagnostic( - dependency, - md`Received URL without a pathname ${filePath}.`, - ), - ], - }; - } - filePath = decodeURIComponent(parsed.pathname); - if (parsed.query != null) { - queryPart = parsed.query; + [, pipeline, specifier] = match; + if (!validPipelines.has(pipeline)) { + // This may be a url protocol or scheme rather than a pipeline, such as + // `url('http://example.com/foo.png')`. Pass it to resolvers to handle. + specifier = dependency.specifier; + pipeline = null; } } else { - let matchesQuerystring = filePath.match(QUERY_PARAMS_REGEX); - if (matchesQuerystring && matchesQuerystring[2] != null) { - filePath = matchesQuerystring[1]; - queryPart = matchesQuerystring[2].substr(1); - } - } - if (queryPart != null) { - query = querystring.parse(queryPart); + specifier = dependency.specifier; } // Entrypoints, convert ProjectPath in module specifier to absolute path if (dep.resolveFrom == null) { - filePath = path.join(this.options.projectRoot, filePath); + specifier = path.join(this.options.projectRoot, specifier); } let diagnostics: Array = []; let invalidateOnFileCreate = []; @@ -251,7 +188,7 @@ export class ResolverRunner { for (let resolver of resolvers) { try { let result = await resolver.plugin.resolve({ - filePath, + specifier, pipeline, dependency: dep, options: this.pluginOptions, @@ -302,7 +239,7 @@ export class ResolverRunner { this.options.projectRoot, resultFilePath, ), - query, + query: result.query, sideEffects: result.sideEffects, code: result.code, env: dependency.env, @@ -322,7 +259,7 @@ export class ResolverRunner { new ThrowableDiagnostic({diagnostic: result.diagnostics}), { origin: resolver.name, - filePath, + filePath: specifier, }, ); diagnostics.push(...errorDiagnostic); @@ -332,7 +269,7 @@ export class ResolverRunner { // Add error to error map, we'll append these to the standard error if we can't resolve the asset let errorDiagnostic = errorToDiagnostic(e, { origin: resolver.name, - filePath, + filePath: specifier, }); if (Array.isArray(errorDiagnostic)) { diagnostics.push(...errorDiagnostic); diff --git a/packages/core/core/test/ParcelConfig.test.js b/packages/core/core/test/ParcelConfig.test.js index e58cfb8190a..4d1c23760d8 100644 --- a/packages/core/core/test/ParcelConfig.test.js +++ b/packages/core/core/test/ParcelConfig.test.js @@ -289,8 +289,7 @@ describe('ParcelConfig', () => { name: 'Error', diagnostics: [ { - message: - 'Named pipeline `node:` is reserved for builtin Node.js libraries', + message: "Named pipeline 'node:' is reserved.", origin: '@parcel/core', codeFrames: [ { diff --git a/packages/core/integration-tests/test/css.js b/packages/core/integration-tests/test/css.js index a9b46c6484f..766590d9439 100644 --- a/packages/core/integration-tests/test/css.js +++ b/packages/core/integration-tests/test/css.js @@ -381,4 +381,41 @@ describe('css', () => { }, ); }); + + it('should support importing CSS from node_modules with the npm: scheme', async () => { + let b = await bundle( + path.join(__dirname, '/integration/css-node-modules/index.css'), + ); + + assertBundles(b, [ + { + name: 'index.css', + assets: ['index.css', 'foo.css'], + }, + ]); + }); + + it('should support external CSS imports', async () => { + let b = await bundle( + path.join(__dirname, '/integration/css-external/a.css'), + ); + + assertBundles(b, [ + { + name: 'a.css', + assets: ['a.css', 'b.css'], + }, + ]); + + let res = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8'); + assert( + res.startsWith(`@import "http://example.com/external.css"; +.b { + color: red; +} +.a { + color: blue; +}`), + ); + }); }); diff --git a/packages/core/integration-tests/test/integration/cache/node_modules/parcel-transformer-mock/index.js b/packages/core/integration-tests/test/integration/cache/node_modules/parcel-transformer-mock/index.js index 9aa8b706deb..476deaef29f 100644 --- a/packages/core/integration-tests/test/integration/cache/node_modules/parcel-transformer-mock/index.js +++ b/packages/core/integration-tests/test/integration/cache/node_modules/parcel-transformer-mock/index.js @@ -4,7 +4,8 @@ module.exports = new Transformer({ transform({asset}) { if (asset.isSource) { asset.addDependency({ - specifier: 'foo' + specifier: 'foo', + specifierType: 'esm' }); return [asset]; } diff --git a/packages/core/integration-tests/test/integration/css-external/a.css b/packages/core/integration-tests/test/integration/css-external/a.css new file mode 100644 index 00000000000..f3aff0a9f8d --- /dev/null +++ b/packages/core/integration-tests/test/integration/css-external/a.css @@ -0,0 +1,6 @@ +@import 'b.css'; +@import 'http://example.com/external.css'; + +.a { + color: blue; +} diff --git a/packages/core/integration-tests/test/integration/css-external/b.css b/packages/core/integration-tests/test/integration/css-external/b.css new file mode 100644 index 00000000000..49533bc2338 --- /dev/null +++ b/packages/core/integration-tests/test/integration/css-external/b.css @@ -0,0 +1,3 @@ +.b { + color: red; +} \ No newline at end of file diff --git a/packages/core/integration-tests/test/integration/css-node-modules/index.css b/packages/core/integration-tests/test/integration/css-node-modules/index.css new file mode 100644 index 00000000000..f59e597326c --- /dev/null +++ b/packages/core/integration-tests/test/integration/css-node-modules/index.css @@ -0,0 +1 @@ +@import 'npm:foo/foo.css'; diff --git a/packages/core/integration-tests/test/integration/css-node-modules/node_modules/foo/foo.css b/packages/core/integration-tests/test/integration/css-node-modules/node_modules/foo/foo.css new file mode 100644 index 00000000000..dc1b2983f0b --- /dev/null +++ b/packages/core/integration-tests/test/integration/css-node-modules/node_modules/foo/foo.css @@ -0,0 +1,3 @@ +.foo { + background-color: red; +} \ No newline at end of file diff --git a/packages/core/integration-tests/test/integration/css-node-modules/node_modules/foo/package.json b/packages/core/integration-tests/test/integration/css-node-modules/node_modules/foo/package.json new file mode 100644 index 00000000000..bde99de9287 --- /dev/null +++ b/packages/core/integration-tests/test/integration/css-node-modules/node_modules/foo/package.json @@ -0,0 +1,3 @@ +{ + "name": "foo" +} diff --git a/packages/core/integration-tests/test/integration/image/reformat.js b/packages/core/integration-tests/test/integration/image/reformat.js index cd6ae6e6202..9302ce8284d 100755 --- a/packages/core/integration-tests/test/integration/image/reformat.js +++ b/packages/core/integration-tests/test/integration/image/reformat.js @@ -1 +1,3 @@ -module.exports = require('./image.jpg?as=webp'); +import url from './image.jpg?as=webp'; + +module.exports = url; diff --git a/packages/core/integration-tests/test/integration/resolver-canDefer/node_modules/parcel-resolver-no-defer/index.js b/packages/core/integration-tests/test/integration/resolver-canDefer/node_modules/parcel-resolver-no-defer/index.js index fb640ccf4a6..cf3c1f39b13 100644 --- a/packages/core/integration-tests/test/integration/resolver-canDefer/node_modules/parcel-resolver-no-defer/index.js +++ b/packages/core/integration-tests/test/integration/resolver-canDefer/node_modules/parcel-resolver-no-defer/index.js @@ -5,7 +5,7 @@ const path = require('path'); const {default: NodeResolver} = require('@parcel/node-resolver-core'); module.exports = new Resolver({ - async resolve({dependency, options, filePath}) { + async resolve({dependency, options, specifier}) { let mainFields = ['source', 'browser', 'module', 'main']; const resolver = new NodeResolver({ @@ -15,8 +15,8 @@ module.exports = new Resolver({ mainFields, }); let result = await resolver.resolve({ - filename: filePath, - isURL: dependency.specifierType === 'url', + filename: specifier, + specifierType: dependency.specifierType, parent: dependency.sourcePath, env: dependency.env, }); diff --git a/packages/core/integration-tests/test/integration/swc-helpers/yarn.lock b/packages/core/integration-tests/test/integration/swc-helpers/yarn.lock new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/core/integration-tests/test/javascript.js b/packages/core/integration-tests/test/javascript.js index 5f948d2abd3..808996bbb84 100644 --- a/packages/core/integration-tests/test/javascript.js +++ b/packages/core/integration-tests/test/javascript.js @@ -4571,7 +4571,7 @@ describe('javascript', function() { name: 'BuildError', diagnostics: [ { - message: 'Unknown pipeline: strange-pipeline.', + message: "Failed to resolve 'strange-pipeline:./b.js' from './a.js'", origin: '@parcel/core', codeFrames: [ { @@ -4592,6 +4592,10 @@ describe('javascript', function() { }, ], }, + { + message: "Unknown url scheme or pipeline 'strange-pipeline:'", + origin: '@parcel/resolver-default', + }, ], }); }); diff --git a/packages/core/integration-tests/test/transpilation.js b/packages/core/integration-tests/test/transpilation.js index 6b6865d7b54..5397e170279 100644 --- a/packages/core/integration-tests/test/transpilation.js +++ b/packages/core/integration-tests/test/transpilation.js @@ -258,7 +258,7 @@ describe('transpilation', function() { .slice(2), ); await outputFS.mkdirp(dir); - ncp(path.join(__dirname, '/integration/swc-helpers'), dir); + await ncp(path.join(__dirname, '/integration/swc-helpers'), dir); await bundle(path.join(dir, 'index.js'), { mode: 'production', inputFS: overlayFS, diff --git a/packages/core/types/index.js b/packages/core/types/index.js index bbd7cef5f1f..2db2bfe7b88 100644 --- a/packages/core/types/index.js +++ b/packages/core/types/index.js @@ -1449,14 +1449,19 @@ export type FileCreateInvalidation = * @section resolver */ export type ResolveResult = {| - /** An absolute path to the file. */ + /** An absolute path to the resolved file. */ +filePath?: FilePath, + /** An optional named pipeline to use to compile the resolved file. */ +pipeline?: ?string, + /** Query parameters to be used by transformers when compiling the resolved file. */ + +query?: QueryParameters, + /** Whether the resolved file should be excluded from the build. */ +isExcluded?: boolean, + /** Overrides the priority set on the dependency. */ +priority?: DependencyPriority, /** Corresponds to BaseAsset's sideEffects. */ +sideEffects?: boolean, - /** A resolver might want to resolve to a dummy, in this case filePath is rather "resolve from". */ + /** The code of the resolved asset. If provided, this is used rather than reading the file from disk. */ +code?: string, /** Whether this dependency can be deferred by Parcel itself (true by default). */ +canDefer?: boolean, @@ -1464,7 +1469,9 @@ export type ResolveResult = {| +diagnostics?: Diagnostic | Array, /** Is spread (shallowly merged) onto the request's dependency.meta */ +meta?: JSONObject, + /** A list of file paths or patterns that should invalidate the resolution if created. */ +invalidateOnFileCreate?: Array, + /** A list of files that should invalidate the resolution if modified or deleted. */ +invalidateOnFileChange?: Array, |}; @@ -1595,7 +1602,7 @@ export type Resolver = {| dependency: Dependency, options: PluginOptions, logger: PluginLogger, - filePath: FilePath, + specifier: FilePath, pipeline: ?string, |}): Async, |}; diff --git a/packages/core/utils/src/alternatives.js b/packages/core/utils/src/alternatives.js index a0f8b9ec0c0..14f2740a0f0 100644 --- a/packages/core/utils/src/alternatives.js +++ b/packages/core/utils/src/alternatives.js @@ -63,6 +63,7 @@ async function findAllFilesUp({ maxlength, collected, leadingDotSlash = true, + includeDirectories = true, }: {| fs: FileSystem, dir: string, @@ -71,6 +72,7 @@ async function findAllFilesUp({ maxlength: number, collected: Array, leadingDotSlash?: boolean, + includeDirectories?: boolean, |}): Promise { let dirContent = (await fs.readdir(dir)).sort(); return Promise.all( @@ -80,7 +82,7 @@ async function findAllFilesUp({ if (relativeFilePath.length < maxlength) { let stats = await fs.stat(fullPath); let isDir = stats.isDirectory(); - if (isDir || stats.isFile()) { + if ((isDir && includeDirectories) || stats.isFile()) { collected.push(relativeFilePath); } @@ -106,6 +108,8 @@ export async function findAlternativeFiles( dir: string, projectRoot: string, leadingDotSlash?: boolean = true, + includeDirectories?: boolean = true, + includeExtension?: boolean = false, ): Promise> { let potentialFiles: Array = []; // Find our root, we won't recommend files above the package root as that's bad practise @@ -125,9 +129,10 @@ export async function findAlternativeFiles( maxlength: fileSpecifier.length + 10, collected: potentialFiles, leadingDotSlash, + includeDirectories, }); - if (path.extname(fileSpecifier) === '') { + if (path.extname(fileSpecifier) === '' && !includeExtension) { potentialFiles = potentialFiles.map(p => { let ext = path.extname(p); return ext.length > 0 ? p.slice(0, -ext.length) : p; diff --git a/packages/packagers/css/src/CSSPackager.js b/packages/packagers/css/src/CSSPackager.js index f41b76c3c4f..5030953d4c8 100644 --- a/packages/packagers/css/src/CSSPackager.js +++ b/packages/packagers/css/src/CSSPackager.js @@ -28,8 +28,22 @@ export default (new Packager({ let queue = new PromiseQueue({ maxConcurrent: 32, }); - bundle.traverseAssets({ - exit: asset => { + let hoistedImports = []; + bundle.traverse({ + exit: node => { + if (node.type === 'dependency') { + // Hoist unresolved external dependencies (i.e. http: imports) + if ( + node.value.priority === 'sync' && + !bundleGraph.getResolvedAsset(node.value, bundle) + ) { + hoistedImports.push(node.value.specifier); + } + return; + } + + let asset = node.value; + // Figure out which media types this asset was imported with. // We only want to import the asset once, so group them all together. let media = []; @@ -75,6 +89,12 @@ export default (new Packager({ let contents = ''; let map = new SourceMap(options.projectRoot); let lineOffset = 0; + + for (let url of hoistedImports) { + contents += `@import "${url}";\n`; + lineOffset++; + } + for (let [asset, code, mapBuffer] of outputs) { contents += code + '\n'; if (bundle.env.sourceMap) { diff --git a/packages/resolvers/default/src/DefaultResolver.js b/packages/resolvers/default/src/DefaultResolver.js index bfe30b809e6..3d6585b4b14 100644 --- a/packages/resolvers/default/src/DefaultResolver.js +++ b/packages/resolvers/default/src/DefaultResolver.js @@ -8,7 +8,7 @@ import NodeResolver from '@parcel/node-resolver-core'; const WEBPACK_IMPORT_REGEX = /\S+-loader\S*!\S+/g; export default (new Resolver({ - resolve({dependency, options, filePath}) { + resolve({dependency, options, specifier}) { if (WEBPACK_IMPORT_REGEX.test(dependency.specifier)) { throw new Error( `The import path: ${dependency.specifier} is using webpack specific loader import syntax, which isn't supported by Parcel.`, @@ -18,13 +18,18 @@ export default (new Resolver({ const resolver = new NodeResolver({ fs: options.inputFS, projectRoot: options.projectRoot, - extensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'css', 'styl', 'vue'], + // Extensions are always required in URL dependencies. + extensions: + dependency.specifierType === 'commonjs' || + dependency.specifierType === 'esm' + ? ['ts', 'tsx', 'js', 'jsx', 'json'] + : [], mainFields: ['source', 'browser', 'module', 'main'], }); return resolver.resolve({ - filename: filePath, - isURL: dependency.specifierType === 'url', + filename: specifier, + specifierType: dependency.specifierType, parent: dependency.resolveFrom, env: dependency.env, sourcePath: dependency.sourcePath, diff --git a/packages/resolvers/glob/src/GlobResolver.js b/packages/resolvers/glob/src/GlobResolver.js index 9690bbf9d6d..134aa120574 100644 --- a/packages/resolvers/glob/src/GlobResolver.js +++ b/packages/resolvers/glob/src/GlobResolver.js @@ -7,8 +7,8 @@ import nullthrows from 'nullthrows'; import ThrowableDiagnostic from '@parcel/diagnostic'; export default (new Resolver({ - async resolve({dependency, options, filePath, pipeline}) { - if (!isGlob(filePath)) { + async resolve({dependency, options, specifier, pipeline}) { + if (!isGlob(specifier)) { return; } @@ -47,13 +47,13 @@ export default (new Resolver({ }); } - filePath = path.resolve(path.dirname(sourceFile), filePath); - let normalized = normalizeSeparators(filePath); + specifier = path.resolve(path.dirname(sourceFile), specifier); + let normalized = normalizeSeparators(specifier); let files = await glob(normalized, options.inputFS, { onlyFiles: true, }); - let dir = path.dirname(filePath); + let dir = path.dirname(specifier); let results = files.map(file => { let relative = relativePath(dir, file); if (pipeline) { @@ -88,7 +88,9 @@ export default (new Resolver({ return { filePath: path.join( dir, - path.basename(filePath, path.extname(filePath)) + '.' + sourceAssetType, + path.basename(specifier, path.extname(specifier)) + + '.' + + sourceAssetType, ), code, invalidateOnFileCreate: [{glob: normalized}], diff --git a/packages/transformers/css/src/CSSTransformer.js b/packages/transformers/css/src/CSSTransformer.js index c7fbe10c1df..a5437582245 100644 --- a/packages/transformers/css/src/CSSTransformer.js +++ b/packages/transformers/css/src/CSSTransformer.js @@ -5,11 +5,7 @@ import type {FilePath} from '@parcel/types'; import SourceMap from '@parcel/source-map'; import {Transformer} from '@parcel/plugin'; -import { - createDependencyLocation, - isURL, - remapSourceLocation, -} from '@parcel/utils'; +import {createDependencyLocation, remapSourceLocation} from '@parcel/utils'; import postcss from 'postcss'; import nullthrows from 'nullthrows'; import valueParser from 'postcss-value-parser'; @@ -108,35 +104,29 @@ export default (new Transformer({ throw new Error('Could not find import name for ' + String(rule)); } - if (isURL(specifier)) { - name.value = asset.addURLDependency(specifier, { - loc: createLoc(nullthrows(rule.source.start), asset.filePath, 0, 8), - }); - } else { - // If this came from an inline