From 209478f400eac42fcdabab24a736c252d5c18081 Mon Sep 17 00:00:00 2001 From: evilebottnawi Date: Tue, 19 Feb 2019 16:33:52 +0300 Subject: [PATCH] fix: normalize path segment separation --- package-lock.json | 34 +-- package.json | 3 +- src/preProcessPattern.js | 19 +- src/processPattern.js | 8 +- src/utils/{escape.js => normalize.js} | 8 +- test/CopyPlugin.test.js | 242 +++++++++++++++++- .../nested/deep-nested/deepnested.txt | 0 7 files changed, 281 insertions(+), 33 deletions(-) rename src/utils/{escape.js => normalize.js} (77%) create mode 100644 test/helpers/directory/nested/deep-nested/deepnested.txt diff --git a/package-lock.json b/package-lock.json index 1cd18062..16e5b3aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2217,9 +2217,9 @@ "dev": true }, "chokidar": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.1.tgz", - "integrity": "sha512-gfw3p2oQV2wEt+8VuMlNsPjCxDxvvgnm/kz+uATu805mWVF8IJN7uz9DN7iBz+RMJISmiVbCOBFs9qBGMjtPfQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.2.tgz", + "integrity": "sha512-IwXUx0FXc5ibYmPC2XeEj5mpXoV66sR+t3jqu2NS2GYwCktt3KF1/Qqjws/NkegajBA4RbZ5+DDwlOiJsxDHEg==", "dev": true, "requires": { "anymatch": "^2.0.0", @@ -3788,9 +3788,9 @@ } }, "eslint": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.14.0.tgz", - "integrity": "sha512-jrOhiYyENRrRnWlMYANlGZTqb89r2FuRT+615AabBoajhNjeh9ywDNlh2LU9vTqf0WYN+L3xdXuIi7xuj/tK9w==", + "version": "5.14.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.14.1.tgz", + "integrity": "sha512-CyUMbmsjxedx8B0mr79mNOqetvkbij/zrXnFeK2zc3pGRn3/tibjiNAv/3UxFEyfMDjh+ZqTrJrEGBFiGfD5Og==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", @@ -4637,7 +4637,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -5052,7 +5053,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -5108,6 +5110,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5151,12 +5154,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -8117,8 +8122,7 @@ "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, "npm-path": { "version": "2.0.4", @@ -9061,9 +9065,9 @@ } }, "regexp-tree": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.4.tgz", - "integrity": "sha512-DohP6WXzgrc7gFs9GsTQgigUfMXZqXkPk+20qkMF6YCy0Qk0FsHL1/KtxTycGR/62DHRtJ1MHQF2g8YzywP4kA==", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.5.tgz", + "integrity": "sha512-nUmxvfJyAODw+0B13hj8CFVAxhe7fDEAgJgaotBu3nnR+IgGgZq59YedJP5VYTlkEfqjuK6TuRpnymKdatLZfQ==", "dev": true }, "regexpp": { diff --git a/package.json b/package.json index 543d986f..5e6d4a79 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,8 @@ "is-glob": "^4.0.0", "loader-utils": "^1.1.0", "minimatch": "^3.0.4", - "p-limit": "^2.1.0" + "p-limit": "^2.1.0", + "normalize-path": "^3.0.0" }, "devDependencies": { "@babel/cli": "^7.1.5", diff --git a/src/preProcessPattern.js b/src/preProcessPattern.js index 19a9f42f..e1c9c359 100644 --- a/src/preProcessPattern.js +++ b/src/preProcessPattern.js @@ -1,8 +1,9 @@ import path from 'path'; import isGlob from 'is-glob'; +import normalizePath from 'normalize-path'; -import escape from './utils/escape'; +import normalize from './utils/normalize'; import isObject from './utils/isObject'; import { stat } from './utils/promisify'; @@ -41,6 +42,7 @@ export default function preProcessPattern(globalRef, pattern) { pattern.context = path.join(context, pattern.context); } + pattern.context = normalizePath(pattern.context); pattern.ignore = globalRef.ignore.concat(pattern.ignore || []); info(`processing from: '${pattern.from}' to: '${pattern.to}'`); @@ -68,9 +70,11 @@ export default function preProcessPattern(globalRef, pattern) { const globOptions = Object.assign({}, pattern.from); delete globOptions.glob; - pattern.glob = escape(pattern.context, pattern.from.glob); + pattern.glob = normalize(pattern.context, pattern.from.glob); pattern.globOptions = globOptions; - pattern.absoluteFrom = path.resolve(pattern.context, pattern.from.glob); + pattern.absoluteFrom = normalizePath( + path.resolve(pattern.context, pattern.from.glob) + ); return Promise.resolve(pattern); } @@ -81,6 +85,9 @@ export default function preProcessPattern(globalRef, pattern) { pattern.absoluteFrom = path.resolve(pattern.context, pattern.from); } + // Normalize path when path separators are mixed (like `C:\\directory/nested-directory/`) + pattern.absoluteFrom = normalizePath(pattern.absoluteFrom); + debug( `determined '${pattern.from}' to be read from '${pattern.absoluteFrom}'` ); @@ -89,7 +96,7 @@ export default function preProcessPattern(globalRef, pattern) { // If from doesn't appear to be a glob, then log a warning if (isGlob(pattern.from) || pattern.from.indexOf('*') !== -1) { pattern.fromType = 'glob'; - pattern.glob = escape(pattern.context, pattern.from); + pattern.glob = normalize(pattern.context, pattern.from); // We need to add context directory as dependencies to avoid problems when new files added in directories // when we already in watch mode and this directories are not in context dependencies @@ -130,7 +137,7 @@ export default function preProcessPattern(globalRef, pattern) { pattern.fromType = 'dir'; pattern.context = pattern.absoluteFrom; - pattern.glob = escape(pattern.absoluteFrom, '**/*'); + pattern.glob = normalize(pattern.absoluteFrom, '**/*'); pattern.absoluteFrom = path.join(pattern.absoluteFrom, '**/*'); pattern.globOptions = { dot: true, @@ -140,7 +147,7 @@ export default function preProcessPattern(globalRef, pattern) { pattern.fromType = 'file'; pattern.context = path.dirname(pattern.absoluteFrom); - pattern.glob = escape(pattern.absoluteFrom); + pattern.glob = normalize(pattern.absoluteFrom); pattern.globOptions = { dot: true, }; diff --git a/src/processPattern.js b/src/processPattern.js index 3714a2be..d3446434 100644 --- a/src/processPattern.js +++ b/src/processPattern.js @@ -3,6 +3,7 @@ import path from 'path'; import globby from 'globby'; import pLimit from 'p-limit'; import minimatch from 'minimatch'; +import normalizePath from 'normalize-path'; import isObject from './utils/isObject'; @@ -41,6 +42,9 @@ export default function processPattern(globalRef, pattern) { file.relativeFrom = path.basename(file.relativeFrom); } + // Ensure forward slashes + file.relativeFrom = normalizePath(file.relativeFrom); + debug(`found ${from}`); // Check the ignore list @@ -106,8 +110,8 @@ export default function processPattern(globalRef, pattern) { file.webpackTo = path.relative(output, file.webpackTo); } - // ensure forward slashes - file.webpackTo = file.webpackTo.replace(/\\/g, '/'); + // Ensure forward slashes + file.webpackTo = normalizePath(file.webpackTo); info(`determined that '${from}' should write to '${file.webpackTo}'`); diff --git a/src/utils/escape.js b/src/utils/normalize.js similarity index 77% rename from src/utils/escape.js rename to src/utils/normalize.js index f5c239df..a2572779 100644 --- a/src/utils/escape.js +++ b/src/utils/normalize.js @@ -1,6 +1,8 @@ import path from 'path'; -export default function escape(context, from) { +import normalizePath from 'normalize-path'; + +function escape(context, from) { if (from && path.isAbsolute(from)) { return from; } @@ -24,3 +26,7 @@ export default function escape(context, from) { return `${absoluteContext}/${from}`; } + +export default function normalize(context, from) { + return normalizePath(escape(context, from)); +} diff --git a/test/CopyPlugin.test.js b/test/CopyPlugin.test.js index 8794d2ec..c0f59f4d 100644 --- a/test/CopyPlugin.test.js +++ b/test/CopyPlugin.test.js @@ -327,6 +327,7 @@ describe('apply function', () => { runEmit({ expectedAssetKeys: [ 'directory/directoryfile.txt', + 'directory/nested/deep-nested/deepnested.txt', 'directory/nested/nestedfile.txt', 'file.txt', 'noextension', @@ -380,6 +381,7 @@ describe('apply function', () => { 'file.txt', 'file.txt.gz', 'directory/directoryfile.txt', + 'directory/nested/deep-nested/deepnested.txt', 'directory/nested/nestedfile.txt', '[special?directory]/directoryfile.txt', '[special?directory]/(special-*file).txt', @@ -404,6 +406,7 @@ describe('apply function', () => { 'nested/file.txt', 'nested/file.txt.gz', 'nested/directory/directoryfile.txt', + 'nested/directory/nested/deep-nested/deepnested.txt', 'nested/directory/nested/nestedfile.txt', 'nested/[special?directory]/directoryfile.txt', 'nested/[special?directory]/(special-*file).txt', @@ -426,6 +429,7 @@ describe('apply function', () => { expectedAssetKeys: [ '/some/path/(special-*file).txt.tst', '/some/path/binextension.bin.tst', + '/some/path/deepnested.txt.tst', '/some/path/file.txt.tst', '/some/path/file.txt.gz.tst', '/some/path/directoryfile.txt.tst', @@ -459,6 +463,7 @@ describe('apply function', () => { 'transformed/file-22af64.txt', 'transformed/file.txt-5b311c.gz', 'transformed/directory/directoryfile-22af64.txt', + 'transformed/directory/nested/deep-nested/deepnested-d41d8c.txt', 'transformed/directory/nested/nestedfile-d41d8c.txt', 'transformed/noextension-d41d8c', ], @@ -482,6 +487,7 @@ describe('apply function', () => { runEmit({ expectedAssetKeys: [ 'nested/directoryfile.txt', + 'nested/nested/deep-nested/deepnested.txt', 'nested/nested/nestedfile.txt', ], patterns: [ @@ -535,6 +541,7 @@ describe('apply function', () => { it('can use a glob to flatten multiple files in a relative context to a non-root directory', (done) => { runEmit({ expectedAssetKeys: [ + 'nested/deepnested.txt', 'nested/directoryfile.txt', 'nested/nestedfile.txt', ], @@ -555,6 +562,7 @@ describe('apply function', () => { runEmit({ expectedAssetKeys: [ 'nested/directoryfile.txt', + 'nested/nested/deep-nested/deepnested.txt', 'nested/nested/nestedfile.txt', ], patterns: [ @@ -588,6 +596,7 @@ describe('apply function', () => { '[!]/hello.txt', 'file.txt', 'directory/directoryfile.txt', + 'directory/nested/deep-nested/deepnested.txt', 'directory/nested/nestedfile.txt', '[special?directory]/directoryfile.txt', '[special?directory]/(special-*file).txt', @@ -611,6 +620,7 @@ describe('apply function', () => { 'nested/file-22af64.txt', 'nested/file.txt-5b311c.gz', 'nested/directory/directoryfile-22af64.txt', + 'nested/directory/nested/deep-nested/deepnested-d41d8c.txt', 'nested/directory/nested/nestedfile-d41d8c.txt', 'nested/[special?directory]/(special-*file)-0bd650.txt', 'nested/[special?directory]/directoryfile-22af64.txt', @@ -1226,6 +1236,7 @@ describe('apply function', () => { '[!]/hello.txt', 'binextension.bin', 'directory/directoryfile.txt', + 'directory/nested/deep-nested/deepnested.txt', 'directory/nested/nestedfile.txt', '[special?directory]/directoryfile.txt', '[special?directory]/(special-*file).txt', @@ -1352,6 +1363,7 @@ describe('apply function', () => { expectedAssetKeys: [ '.dottedfile', 'directoryfile.txt', + 'nested/deep-nested/deepnested.txt', 'nested/nestedfile.txt', ], patterns: [ @@ -1368,6 +1380,7 @@ describe('apply function', () => { runEmit({ expectedAssetKeys: [ '/some/path/.dottedfile', + '/some/path/deepnested.txt', '/some/path/directoryfile.txt', '/some/path/nestedfile.txt', ], @@ -1451,6 +1464,7 @@ describe('apply function', () => { expectedAssetKeys: [ '.dottedfile', 'directoryfile.txt', + 'nested/deep-nested/deepnested.txt', 'nested/nestedfile.txt', ], patterns: [ @@ -1468,6 +1482,7 @@ describe('apply function', () => { expectedAssetKeys: [ 'newdirectory/.dottedfile', 'newdirectory/directoryfile.txt', + 'newdirectory/nested/deep-nested/deepnested.txt', 'newdirectory/nested/nestedfile.txt', ], patterns: [ @@ -1483,7 +1498,10 @@ describe('apply function', () => { it("can move a directory's contents to a new directory using a pattern context", (done) => { runEmit({ - expectedAssetKeys: ['newdirectory/nestedfile.txt'], + expectedAssetKeys: [ + 'newdirectory/deep-nested/deepnested.txt', + 'newdirectory/nestedfile.txt', + ], patterns: [ { context: 'directory', @@ -1500,6 +1518,7 @@ describe('apply function', () => { runEmit({ expectedAssetKeys: [ 'newdirectory/.dottedfile', + 'newdirectory/deepnested.txt', 'newdirectory/directoryfile.txt', 'newdirectory/nestedfile.txt', ], @@ -1520,6 +1539,7 @@ describe('apply function', () => { expectedAssetKeys: [ '../tempdir/.dottedfile', '../tempdir/directoryfile.txt', + '../tempdir/nested/deep-nested/deepnested.txt', '../tempdir/nested/nestedfile.txt', ], patterns: [ @@ -1535,7 +1555,7 @@ describe('apply function', () => { it("can move a nested directory's contents to the root directory", (done) => { runEmit({ - expectedAssetKeys: ['nestedfile.txt'], + expectedAssetKeys: ['deep-nested/deepnested.txt', 'nestedfile.txt'], patterns: [ { from: 'directory/nested', @@ -1548,7 +1568,10 @@ describe('apply function', () => { it("can move a nested directory's contents to a new directory", (done) => { runEmit({ - expectedAssetKeys: ['newdirectory/nestedfile.txt'], + expectedAssetKeys: [ + 'newdirectory/deep-nested/deepnested.txt', + 'newdirectory/nestedfile.txt', + ], patterns: [ { from: 'directory/nested', @@ -1564,7 +1587,10 @@ describe('apply function', () => { const absolutePath = path.resolve(HELPER_DIR, 'directory', 'nested'); runEmit({ - expectedAssetKeys: ['newdirectory/nestedfile.txt'], + expectedAssetKeys: [ + 'newdirectory/deep-nested/deepnested.txt', + 'newdirectory/nestedfile.txt', + ], patterns: [ { from: absolutePath, @@ -1647,6 +1673,7 @@ describe('apply function', () => { expectedAssetKeys: [ '.dottedfile', 'directoryfile.txt', + 'nested/deep-nested/deepnested.txt', 'nested/nestedfile.txt', 'tempfile1.txt', 'tempfile2.txt', @@ -1671,6 +1698,7 @@ describe('apply function', () => { expectedAssetKeys: [ 'nested/.dottedfile-79d39f', 'nested/directoryfile-22af64.txt', + 'nested/nested/deep-nested/deepnested-d41d8c.txt', 'nested/nested/nestedfile-d41d8c.txt', ], patterns: [ @@ -1714,6 +1742,189 @@ describe('apply function', () => { }); }); + describe('with difference path segment separation', () => { + it('can normalize backslash path with glob in from', (done) => { + runEmit({ + expectedAssetKeys: ['directory/nested/nestedfile.txt'], + patterns: [ + { + from: { + glob: 'directory\\nested\\*', + }, + }, + ], + }) + .then(done) + .catch(done); + }); + + it('can normalize backslash path with glob in from (mixed path segment separation)', (done) => { + runEmit({ + expectedAssetKeys: ['directory/nested/nestedfile.txt'], + patterns: [ + { + from: { + glob: 'directory/nested\\*', + }, + }, + ], + }) + .then(done) + .catch(done); + }); + + it('can normalize backslash path with glob in from (simple)', (done) => { + runEmit({ + expectedAssetKeys: ['directory/nested/nestedfile.txt'], + patterns: [ + { + from: 'directory\\nested\\*', + }, + ], + }) + .then(done) + .catch(done); + }); + + it('can normalize mixed path segment separation with glob in from (simple)', (done) => { + runEmit({ + expectedAssetKeys: ['directory/nested/nestedfile.txt'], + patterns: [ + { + from: 'directory/nested\\*', + }, + ], + }) + .then(done) + .catch(done); + }); + + it('can normalize backslash path with file in from', (done) => { + runEmit({ + expectedAssetKeys: ['nestedfile.txt'], + patterns: [ + { + from: 'directory\\nested\\nestedfile.txt', + }, + ], + }) + .then(done) + .catch(done); + }); + + it('can normalize backslash path with file in from (mixed path segment separation)', (done) => { + runEmit({ + expectedAssetKeys: ['nestedfile.txt'], + patterns: [ + { + from: 'directory\\nested/nestedfile.txt', + }, + ], + }) + .then(done) + .catch(done); + }); + + it('can normalize backslash path with directory in from', (done) => { + runEmit({ + expectedAssetKeys: ['deepnested.txt'], + patterns: [ + { + from: 'directory\\nested\\deep-nested', + }, + ], + }) + .then(done) + .catch(done); + }); + + it('can normalize backslash path with directory in from (mixed path segment separation)', (done) => { + runEmit({ + expectedAssetKeys: ['deepnested.txt'], + patterns: [ + { + from: 'directory\\nested\\deep-nested', + }, + ], + }) + .then(done) + .catch(done); + }); + + it('can exclude path', (done) => { + runEmit({ + expectedAssetKeys: [ + '[!]/hello.txt', + '[special?directory]/(special-*file).txt', + '[special?directory]/directoryfile.txt', + '[special?directory]/nested/nestedfile.txt', + ], + patterns: [ + { + from: '!(directory)/**/*.txt', + }, + ], + }) + .then(done) + .catch(done); + }); + + it('can exclude path with backslash path', (done) => { + runEmit({ + expectedAssetKeys: [ + '[!]/hello.txt', + '[special?directory]/(special-*file).txt', + '[special?directory]/directoryfile.txt', + '[special?directory]/nested/nestedfile.txt', + ], + patterns: [ + { + from: '!(directory)\\**\\*.txt', + }, + ], + }) + .then(done) + .catch(done); + }); + + it('can normalize backslash in context', (done) => { + runEmit({ + expectedAssetKeys: [ + 'newdirectory/deep-nested/deepnested.txt', + 'newdirectory/nestedfile.txt', + ], + options: { + context: path.resolve(HELPER_DIR, 'directory\\'), + }, + patterns: [ + { + from: 'nested', + to: 'newdirectory', + }, + ], + }) + .then(done) + .catch(done); + }); + + it('can normalize backslash in context (2)', (done) => { + runEmit({ + expectedAssetKeys: ['newdirectory/deepnested.txt'], + options: { + context: path.resolve(HELPER_DIR, 'directory\\nested'), + }, + patterns: [ + { + from: 'deep-nested', + to: 'newdirectory', + }, + ], + }) + .then(done) + .catch(done); + }); + }); + describe('options', () => { describe('ignore', () => { it('ignores files when from is a file', (done) => { @@ -1737,7 +1948,11 @@ describe('apply function', () => { it('ignores files when from is a directory', (done) => { runEmit({ - expectedAssetKeys: ['.dottedfile', 'directoryfile.txt'], + expectedAssetKeys: [ + '.dottedfile', + 'directoryfile.txt', + 'nested/deep-nested/deepnested.txt', + ], options: { ignore: ['*/nestedfile.*'], }, @@ -1775,6 +1990,7 @@ describe('apply function', () => { 'file.txt', 'file.txt.gz', 'directory/directoryfile.txt', + 'directory/nested/deep-nested/deepnested.txt', 'directory/nested/nestedfile.txt', '[special?directory]/directoryfile.txt', '[special?directory]/(special-*file).txt', @@ -1904,7 +2120,10 @@ describe('apply function', () => { describe('context', () => { it('overrides webpack config context with absolute path', (done) => { runEmit({ - expectedAssetKeys: ['newdirectory/nestedfile.txt'], + expectedAssetKeys: [ + 'newdirectory/deep-nested/deepnested.txt', + 'newdirectory/nestedfile.txt', + ], options: { context: path.resolve(HELPER_DIR, 'directory'), }, @@ -1921,7 +2140,10 @@ describe('apply function', () => { it('overrides webpack config context with relative path', (done) => { runEmit({ - expectedAssetKeys: ['newdirectory/nestedfile.txt'], + expectedAssetKeys: [ + 'newdirectory/deep-nested/deepnested.txt', + 'newdirectory/nestedfile.txt', + ], options: { context: 'directory', }, @@ -1938,7 +2160,10 @@ describe('apply function', () => { it('is overridden by pattern context', (done) => { runEmit({ - expectedAssetKeys: ['newdirectory/nestedfile.txt'], + expectedAssetKeys: [ + 'newdirectory/deep-nested/deepnested.txt', + 'newdirectory/nestedfile.txt', + ], options: { context: 'directory', }, @@ -2008,6 +2233,7 @@ describe('apply function', () => { expectedAssetKeys: [ '.dottedfile', 'directoryfile.txt', + 'nested/deep-nested/deepnested.txt', 'nested/nestedfile.txt', ], expectedAssetContent: { diff --git a/test/helpers/directory/nested/deep-nested/deepnested.txt b/test/helpers/directory/nested/deep-nested/deepnested.txt new file mode 100644 index 00000000..e69de29b