diff --git a/src/cssModules.ts b/src/cssModules.ts index e95a27e91..c15252173 100644 --- a/src/cssModules.ts +++ b/src/cssModules.ts @@ -10,13 +10,20 @@ export function genCSSModulesCode( // inject variable const name = typeof moduleName === 'string' ? moduleName : '$style' - code += `\ncssModules["${name}"] = ${styleVar}` + + const moduleAdd = ` + if(cssModules["${name}"]){ + Object.assign(cssModules["${name}"], ${styleVar}); + } else { + cssModules["${name}"] = ${styleVar}; + }` + code += `${moduleAdd}` if (needsHotReload) { code += ` if (module.hot) { module.hot.accept(${request}, () => { - cssModules["${name}"] = ${styleVar} + ${moduleAdd} __VUE_HMR_RUNTIME__.rerender("${id}") }) }` diff --git a/test/advanced.spec.ts b/test/advanced.spec.ts index 9fee7e833..d7f0a0a01 100644 --- a/test/advanced.spec.ts +++ b/test/advanced.spec.ts @@ -187,10 +187,10 @@ test('support rules with oneOf', async () => { ) expect(style).toContain('comp-a h2 {\n color: #f00;\n}') - const { window, instance } = await run('css-modules-simple.vue') + const { window, instance } = await run('css-modules/default.vue') const className = instance.$style.red - expect(className).toMatch(/^red_\w{5}/) + expect(className).toMatch(/^red_[\w-+]{5}/) style = normalizeNewline(window.document.querySelector('style')!.textContent!) expect(style).toContain('.' + className + ' {\n color: red;\n}') }) diff --git a/test/fixtures/css-modules.vue b/test/fixtures/css-modules.vue deleted file mode 100644 index 473f90ce9..000000000 --- a/test/fixtures/css-modules.vue +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/test/fixtures/css-modules/default-extend-composes-css.vue b/test/fixtures/css-modules/default-extend-composes-css.vue new file mode 100644 index 000000000..46cd247cf --- /dev/null +++ b/test/fixtures/css-modules/default-extend-composes-css.vue @@ -0,0 +1,16 @@ + + + + + diff --git a/test/fixtures/css-modules/default-extend-diffclass.vue b/test/fixtures/css-modules/default-extend-diffclass.vue new file mode 100644 index 000000000..fbf2fa3fa --- /dev/null +++ b/test/fixtures/css-modules/default-extend-diffclass.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/test/fixtures/css-modules-extend.vue b/test/fixtures/css-modules/default-extend.vue similarity index 56% rename from test/fixtures/css-modules-extend.vue rename to test/fixtures/css-modules/default-extend.vue index de95d1a25..88831f959 100644 --- a/test/fixtures/css-modules-extend.vue +++ b/test/fixtures/css-modules/default-extend.vue @@ -5,7 +5,7 @@ \ No newline at end of file +
{{ $style.red }}
+ diff --git a/test/fixtures/css-modules/default-multiple.vue b/test/fixtures/css-modules/default-multiple.vue new file mode 100644 index 000000000..26a70eea6 --- /dev/null +++ b/test/fixtures/css-modules/default-multiple.vue @@ -0,0 +1,19 @@ + + + + + + + diff --git a/test/fixtures/css-modules-simple.vue b/test/fixtures/css-modules/default.vue similarity index 62% rename from test/fixtures/css-modules-simple.vue rename to test/fixtures/css-modules/default.vue index f677aa29c..79fcedbf5 100644 --- a/test/fixtures/css-modules-simple.vue +++ b/test/fixtures/css-modules/default.vue @@ -7,3 +7,7 @@ + + diff --git a/test/fixtures/css-modules/named-multiple-diffname-diffclass.vue b/test/fixtures/css-modules/named-multiple-diffname-diffclass.vue new file mode 100644 index 000000000..358a4292b --- /dev/null +++ b/test/fixtures/css-modules/named-multiple-diffname-diffclass.vue @@ -0,0 +1,19 @@ + + + + + + + diff --git a/test/fixtures/css-modules/named-multiple-diffname-sameclass.vue b/test/fixtures/css-modules/named-multiple-diffname-sameclass.vue new file mode 100644 index 000000000..c61fb15c9 --- /dev/null +++ b/test/fixtures/css-modules/named-multiple-diffname-sameclass.vue @@ -0,0 +1,19 @@ + + + + + + + diff --git a/test/fixtures/css-modules/named-multiple-samename-diffclass.vue b/test/fixtures/css-modules/named-multiple-samename-diffclass.vue new file mode 100644 index 000000000..ffd5852d0 --- /dev/null +++ b/test/fixtures/css-modules/named-multiple-samename-diffclass.vue @@ -0,0 +1,19 @@ + + + + + + + diff --git a/test/fixtures/css-modules/named-multiple-samename-sameclass.vue b/test/fixtures/css-modules/named-multiple-samename-sameclass.vue new file mode 100644 index 000000000..f2353eac4 --- /dev/null +++ b/test/fixtures/css-modules/named-multiple-samename-sameclass.vue @@ -0,0 +1,19 @@ + + + + + + + diff --git a/test/fixtures/css-modules/named.vue b/test/fixtures/css-modules/named.vue new file mode 100644 index 000000000..bca461115 --- /dev/null +++ b/test/fixtures/css-modules/named.vue @@ -0,0 +1,13 @@ + + + + + diff --git a/test/fixtures/css-modules/red.css b/test/fixtures/css-modules/red.css new file mode 100644 index 000000000..c6dd2c88f --- /dev/null +++ b/test/fixtures/css-modules/red.css @@ -0,0 +1,3 @@ +.red { + color: red; +} diff --git a/test/style.spec.ts b/test/style.spec.ts index 94a04eb0e..9fddbc2f4 100644 --- a/test/style.spec.ts +++ b/test/style.spec.ts @@ -1,4 +1,9 @@ -import { mockBundleAndRun, genId, normalizeNewline } from './utils' +import { + mockBundleAndRun, + genId, + normalizeNewline, + normalizeEscapedHash, +} from './utils' test('scoped style', async () => { const { window, instance, componentModule } = await mockBundleAndRun({ @@ -86,10 +91,14 @@ test('postcss', async () => { expect(style).toContain(`h1[${id}] {\n color: red;\n font-size: 14px\n}`) }) -test('CSS Modules', async () => { +describe('CSS Modules', () => { + const fixtureFolder = 'css-modules/' + const defaultIdent = [undefined, /^[\w-+]{21,}/, /^[\w-+]{21,}/] // pass w/ apply + const customIdent = '[path][name]---[local]---[hash:base64:5]' + const testWithIdent = async ( - localIdentName: string | undefined, - regexToMatch: RegExp + entry: string, + localIdentName: string | undefined ) => { const baseLoaders = [ 'style-loader', @@ -102,9 +111,8 @@ test('CSS Modules', async () => { }, }, ] - - const { window, instance } = await mockBundleAndRun({ - entry: 'css-modules.vue', + const mockResult = await mockBundleAndRun({ + entry: fixtureFolder + entry, modify: (config: any) => { config!.module!.rules = [ { @@ -122,75 +130,384 @@ test('CSS Modules', async () => { ] }, }) - - // get local class name - const className = instance.$style.red - expect(className).toMatch(regexToMatch) - - // class name in style - let style = [].slice - .call(window.document.querySelectorAll('style')) - .map((style: any) => { - return style!.textContent - }) - .join('\n') - style = normalizeNewline(style) - expect(style).toContain('.' + className + ' {\n color: red;\n}') - - // animation name - const match = style.match(/@keyframes\s+(\S+)\s+{/) - expect(match).toHaveLength(2) - const animationName = match[1] - expect(animationName).not.toBe('fade') - expect(style).toContain('animation: ' + animationName + ' 1s;') - - // default module + pre-processor + scoped - const anotherClassName = instance.$style.red - expect(anotherClassName).toMatch(regexToMatch) - const id = 'data-v-' + genId('css-modules.vue') - expect(style).toContain('.' + anotherClassName + '[' + id + ']') + return mockResult } - // default ident - await testWithIdent(undefined, /^\w{21,}/) + test('default/nameless module ($style)', async () => { + const ENTRY = 'default.vue' - // custom ident - await testWithIdent( - '[path][name]---[local]---[hash:base64:5]', - /css-modules---red---\w{5}/ - ) -}) + const expectations = async ( + localIdentName: string | undefined, + regexToMatch: RegExp + ) => { + const { window, instance } = await testWithIdent(ENTRY, localIdentName) + // get local class name + const className = instance.$style.red + expect(className).toMatch(regexToMatch) -test('CSS Modules Extend', async () => { - const baseLoaders = [ - 'style-loader', - { - loader: 'css-loader', - options: { - modules: true, - }, - }, - ] + // class name in style + let style = [].slice + .call(window.document.querySelectorAll('style')) + .map((style: any) => { + return style!.textContent + }) + .join('\n') + style = normalizeEscapedHash(normalizeNewline(style)) + expect(style).toContain('.' + className + ' {\n color: red;\n}') + } - const { window, instance } = await mockBundleAndRun({ - entry: 'css-modules-extend.vue', - modify: (config: any) => { - config!.module!.rules = [ - { - test: /\.vue$/, - loader: 'vue-loader', - }, - { - test: /\.css$/, - use: baseLoaders, - }, - ] - }, + await expectations.apply(undefined, defaultIdent) + await expectations( + customIdent, + /test-fixtures-css-modules-default---red---[\w-+]{5}/ + ) + }) + + test('named module (module="named")', async () => { + const ENTRY = 'named.vue' + + const expectations = async ( + localIdentName: string | undefined, + regexToMatch: RegExp + ) => { + const { window, instance } = await testWithIdent(ENTRY, localIdentName) + // get local class name + const className = instance.named.red + expect(className).toMatch(regexToMatch) + + // class name in style + let style = [].slice + .call(window.document.querySelectorAll('style')) + .map((style: any) => { + return style!.textContent + }) + .join('\n') + style = normalizeEscapedHash(normalizeNewline(style)) + expect(style).toContain('.' + className + ' {\n color: red;\n}') + } + + await expectations.apply(undefined, defaultIdent) + await expectations( + customIdent, + /test-fixtures-css-modules-named---red---[\w-+]{5}/ + ) + }) + + test('multiple default/multiple modules', async () => { + const ENTRY = 'default-multiple.vue' + + const expectations = async ( + localIdentName: string | undefined, + regexToMatchA: RegExp, + regexToMatchB: RegExp + ) => { + const { window, instance } = await testWithIdent(ENTRY, localIdentName) + // get local class name + const classNameA = instance.$style.red + const classNameB = instance.$style.blue + expect(classNameA).toMatch(regexToMatchA) + expect(classNameB).toMatch(regexToMatchB) + + // class name in style + let style = [].slice + .call(window.document.querySelectorAll('style')) + .map((style: any) => { + return style!.textContent + }) + .join('\n') + style = normalizeEscapedHash(normalizeNewline(style)) + expect(style).toContain('.' + classNameA + ' {\n color: red;\n}') + expect(style).toContain('.' + classNameB + ' {\n color: blue;\n}') + } + + await expectations.apply(undefined, defaultIdent) + await expectations( + customIdent, + /test-fixtures-css-modules-default-multiple---red---[\w-+]{5}/, + /test-fixtures-css-modules-default-multiple---blue---[\w-+]{5}/ + ) + }) + + test('multiple named modules (same module names, different class names)', async () => { + const ENTRY = 'named-multiple-samename-diffclass.vue' + + const expectations = async ( + localIdentName: string | undefined, + regexToMatchA: RegExp, + regexToMatchB: RegExp + ) => { + const { window, instance } = await testWithIdent(ENTRY, localIdentName) + // get local class name + const classNameA = instance.named.red + const classNameB = instance.named.blue + expect(classNameA).toMatch(regexToMatchA) + expect(classNameB).toMatch(regexToMatchB) + + // class name in style + let style = [].slice + .call(window.document.querySelectorAll('style')) + .map((style: any) => { + return style!.textContent + }) + .join('\n') + style = normalizeEscapedHash(normalizeNewline(style)) + expect(style).toContain('.' + classNameA + ' {\n color: red;\n}') + expect(style).toContain('.' + classNameB + ' {\n color: blue;\n}') + } + + await expectations.apply(undefined, defaultIdent) + await expectations( + customIdent, + /test-fixtures-css-modules-named-multiple-samename-diffclass---red---[\w-+]{5}/, + /test-fixtures-css-modules-named-multiple-samename-diffclass---blue---[\w-+]{5}/ + ) + }) + + test('multiple named modules (same module names, same class names)', async () => { + const ENTRY = 'named-multiple-samename-sameclass.vue' + + const expectations = async ( + localIdentName: string | undefined, + regexToMatch: RegExp + ) => { + const { window, instance } = await testWithIdent(ENTRY, localIdentName) + // get local class name + const className = instance.named.red + expect(className).toMatch(regexToMatch) + + // class name in style + let style = [].slice + .call(window.document.querySelectorAll('style')) + .map((style: any) => { + return style!.textContent + }) + .join('\n') + style = normalizeEscapedHash(normalizeNewline(style)) + expect(style).toContain('.' + className + ' {\n color: blue;\n}') + } + + await expectations.apply(undefined, defaultIdent) + await expectations( + customIdent, + /test-fixtures-css-modules-named-multiple-samename-sameclass---red---[\w-+]{5}/ + ) }) - expect(instance.$el.className).toBe(instance.$style.red) - const style = window.document.querySelectorAll('style')![1]!.textContent - expect(style).toContain(`.${instance.$style.red} {\n color: #FF0000;\n}`) + test('multiple named modules (different module names, different class names)', async () => { + const ENTRY = 'named-multiple-diffname-diffclass.vue' + + const expectations = async ( + localIdentName: string | undefined, + regexToMatchA: RegExp, + regexToMatchB: RegExp + ) => { + const { window, instance } = await testWithIdent(ENTRY, localIdentName) + // get local class name + const classNameA = instance.name1.red + const classNameB = instance.name2.blue + expect(classNameA).toMatch(regexToMatchA) + expect(classNameB).toMatch(regexToMatchB) + + // class name in style + let style = [].slice + .call(window.document.querySelectorAll('style')) + .map((style: any) => { + return style!.textContent + }) + .join('\n') + style = normalizeEscapedHash(normalizeNewline(style)) + expect(style).toContain('.' + classNameA + ' {\n color: red;\n}') + expect(style).toContain('.' + classNameB + ' {\n color: blue;\n}') + } + + await expectations.apply(undefined, defaultIdent) + await expectations( + customIdent, + /test-fixtures-css-modules-named-multiple-diffname-diffclass---red---[\w-+]{5}/, + /test-fixtures-css-modules-named-multiple-diffname-diffclass---blue---[\w-+]{5}/ + ) + }) + + test('multiple named modules (different module names, same class name)', async () => { + const ENTRY = 'named-multiple-diffname-sameclass.vue' + + const expectations = async ( + localIdentName: string | undefined, + regexToMatchA: RegExp, + regexToMatchB: RegExp + ) => { + const { window, instance } = await testWithIdent(ENTRY, localIdentName) + // get local class name + const classNameA = instance.name1.red + const classNameB = instance.name2.red + expect(classNameA).toMatch(regexToMatchA) + expect(classNameB).toMatch(regexToMatchB) + + expect(classNameA).not.toMatch(classNameB) + /* ^!BROKEN!^ + classes have same name+hash + hash not taking module name into account + vue-loader needs to pass to css-loader too! + */ + + // class name in style + let style = [].slice + .call(window.document.querySelectorAll('style')) + .map((style: any) => { + return style!.textContent + }) + .join('\n') + style = normalizeEscapedHash(normalizeNewline(style)) + expect(style).toContain('.' + classNameA + ' {\n color: red;\n}') + expect(style).toContain('.' + classNameB + ' {\n color: red;\n}') + } + + await expectations.apply(undefined, defaultIdent) + await expectations( + customIdent, + /test-fixtures-css-modules-named-multiple-diffname-sameclass---red---[\w-+]{5}/, + /test-fixtures-css-modules-named-multiple-diffname-sameclass---blue---[\w-+]{5}/ + ) + }) + + test('default/nameless extend (same class name)', async () => { + const ENTRY = 'default-extend.vue' + + const expectations = async ( + localIdentName: string | undefined, + regexToMatch: RegExp + ) => { + const { window, instance } = await testWithIdent(ENTRY, localIdentName) + // get local class name + const className = instance.$style.red // extended + expect(className).toMatch(regexToMatch) + // class name in style + let style = [].slice + .call(window.document.querySelectorAll('style')) + .map((style: any) => { + return style!.textContent + }) + .join('\n') + style = normalizeEscapedHash(normalizeNewline(style)) + + expect(style).toContain('.' + className + ' {\n color: red;\n}') + /* ^!BROKEN!^ + extend is adding both classes to style + but instance.$style.red className points + to the file's original style (hexcolor) + (a677feb62ef42886b712f1b16b71e851-vue) + and not the extended version (red keyword) + e.g. + + ._7ef3af38102f7bc2284518b4f9dda8d9-vue { + color: red; + } + .a677feb62ef42886b712f1b16b71e851-vue { + color: #FF0000; + } + */ + } + + await expectations.apply(undefined, defaultIdent) + await expectations( + customIdent, + /test-fixtures-css-modules-default-extend---red---[\w-+]{5}/ + ) + }) + + test('default/nameless extend (different classes)', async () => { + const ENTRY = 'default-extend-diffclass.vue' + + const expectations = async ( + localIdentName: string | undefined, + regexToMatchA: RegExp, + regexToMatchB: RegExp + ) => { + const { window, instance } = await testWithIdent(ENTRY, localIdentName) + // get local class name + const classNameA = instance.$style.black // own style + const classNameB = instance.$style.red // extended + + expect(classNameA).toMatch(regexToMatchA) + + expect(classNameB).toMatch(regexToMatchB) + /* ^!BROKEN!^, + styles for both own file's style tag and the + extended file are being added to style tags + in document BUT instance.$style has not received the + `red` in the hashmap so instance.$style.red does't exist + + instance.$style + { black: "dd07afd7f1529b35227b9b3bc7609e28-vue" } + + + styles + ._7ef3af38102f7bc2284518b4f9dda8d9-vue { + color: red; + } + + + .dd07afd7f1529b35227b9b3bc7609e28-vue { + color: #000000; + } + */ + // class name in style + let style = [].slice + .call(window.document.querySelectorAll('style')) + .map((style: any) => { + return style!.textContent + }) + .join('\n') + style = normalizeEscapedHash(normalizeNewline(style)) + expect(style).toContain('.' + classNameA + ' {\n color: #000000;\n}') + expect(style).toContain('.' + classNameB + ' {\n color: red;\n}') + } + + await expectations.apply(undefined, defaultIdent) + await expectations( + customIdent, + /test-fixtures-css-modules-default-extend-diffclass---black---[\w-+]{5}/, + /test-fixtures-css-modules-default-extend-diffclass---red---[\w-+]{5}/ + ) + }) + + test('default/nameless extend w/ compose importing css file', async () => { + const ENTRY = 'default-extend-composes-css.vue' + + const expectations = async ( + localIdentName: string | undefined, + regexToMatchA: RegExp, + regexToMatchB: RegExp + ) => { + const { window, instance } = await testWithIdent(ENTRY, localIdentName) + // get local class name + const className = instance.$style.black // own style + const classList = className.split(' ') + expect(classList[0]).toMatch(regexToMatchA) + expect(classList[1]).toMatch(regexToMatchB) + + let style = [].slice + .call(window.document.querySelectorAll('style')) + .map((style: any) => { + return style!.textContent + }) + .join('\n') + style = normalizeEscapedHash(normalizeNewline(style)) + // own style, w/ font-weight + expect(style).toContain( + '.' + classList[0] + ' {\n font-weight: bold;\n}' + ) + // composed style, w/ red + expect(style).toContain('.' + classList[1] + ' {\n color: red;\n}') + } + + await expectations.apply(undefined, defaultIdent) + await expectations( + customIdent, + /test-fixtures-css-modules-default-extend-composes-css---black---[\w-+]{5}/, + /test-fixtures-css-modules-red---red---[\w-+]{5}/ + ) + }) }) test.todo('experimental