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 @@
+
+
+
+
+
+ {{ $style.black }}
+
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 @@
+
+
+
+
+
+ {{ $style.red }} | {{ $style.black }}
+
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 @@
+
+
+
+
+
+
+
+ {{ $style }}
+
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 @@
+
+
+ {{ $style }}
+
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 @@
+
+
+
+
+
+
+
+ {{ name1 }} | {{ name2 }}
+
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 @@
+
+
+
+
+
+
+
+ {{ name1 }} | {{ name2 }}
+
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 @@
+
+
+
+
+
+
+
+ {{ named }}
+
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 @@
+
+
+
+
+
+
+
+ {{ named }}
+
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 @@
+
+
+
+
+
+ {{ named }}
+
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