ember-scoped-css 2.2.2 → 3.1.0
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.
- package/README.md +21 -1
- package/dist/cjs/all-BI7LOSuT.cjs +1519 -0
- package/dist/cjs/all.cjs +1 -1
- package/dist/cjs/babel.cjs +1 -1
- package/dist/cjs/rollup.cjs +1 -1
- package/dist/cjs/vite.cjs +1 -1
- package/dist/runtime/test-support.js +11 -1
- package/dist/runtime/test-support.js.map +1 -1
- package/package.json +7 -2
- package/src/build/template-plugin.js +14 -1
- package/src/lib/css/rewrite.attributes.test.ts +119 -0
- package/src/lib/css/rewrite.js +57 -5
- package/src/lib/css/utils.js +98 -6
- package/src/lib/renameClass.js +12 -4
- package/src/lib/rewriteHbs.attributes.test.ts +118 -0
- package/src/lib/rewriteHbs.js +65 -15
- package/types.d.ts +30 -0
- package/dist/cjs/all-DYmccIER.cjs +0 -1416
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import rewriteHbs from './rewriteHbs.js';
|
|
4
|
+
|
|
5
|
+
const postfix = 'pfx';
|
|
6
|
+
|
|
7
|
+
/** Add the postfix class to elements that carry one of `attributeNames`. */
|
|
8
|
+
function scopeByAttribute(hbs, attributeNames) {
|
|
9
|
+
return rewriteHbs(
|
|
10
|
+
hbs,
|
|
11
|
+
new Set(),
|
|
12
|
+
new Set(),
|
|
13
|
+
postfix,
|
|
14
|
+
new Set(attributeNames),
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('attribute scoping', () => {
|
|
19
|
+
it('adds the postfix class to an element that has a scoped attribute', () => {
|
|
20
|
+
expect(scopeByAttribute('<input type="text">', ['type'])).to.equal(
|
|
21
|
+
'<input type="text" class="pfx">',
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('appends to an existing class attribute', () => {
|
|
26
|
+
expect(
|
|
27
|
+
scopeByAttribute('<input type="text" class="foo">', ['type']),
|
|
28
|
+
).to.equal('<input type="text" class="foo pfx">');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('matches attribute names case-insensitively, like CSS does', () => {
|
|
32
|
+
expect(scopeByAttribute('<input TYPE="text">', ['type'])).to.equal(
|
|
33
|
+
'<input TYPE="text" class="pfx">',
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('does not scope elements without the scoped attribute', () => {
|
|
38
|
+
expect(scopeByAttribute('<a href="/x">link</a>', ['type'])).to.equal(
|
|
39
|
+
'<a href="/x">link</a>',
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('adds the postfix class only once when tag and attribute both match', () => {
|
|
44
|
+
const out = rewriteHbs(
|
|
45
|
+
'<input type="text">',
|
|
46
|
+
new Set(),
|
|
47
|
+
new Set(['input']),
|
|
48
|
+
postfix,
|
|
49
|
+
new Set(['type']),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(out).to.equal('<input type="text" class="pfx">');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('component invocations are scoped too', () => {
|
|
56
|
+
// The postfix class reaches the component's root element through
|
|
57
|
+
// `...attributes`, the same way the matched attribute does.
|
|
58
|
+
it('scopes capitalized components', () => {
|
|
59
|
+
expect(scopeByAttribute('<Foo type="text" />', ['type'])).to.equal(
|
|
60
|
+
'<Foo type="text" class="pfx" />',
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('scopes path-based components', () => {
|
|
65
|
+
expect(scopeByAttribute('<foo.bar type="text" />', ['type'])).to.equal(
|
|
66
|
+
'<foo.bar type="text" class="pfx" />',
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('dynamic class values still receive the postfix class', () => {
|
|
72
|
+
it('appends to a mustache class by wrapping it in a concat', () => {
|
|
73
|
+
expect(
|
|
74
|
+
scopeByAttribute('<input type="text" class={{this.cls}}>', ['type']),
|
|
75
|
+
).to.equal('<input type="text" class="{{this.cls}} pfx">');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('appends to a concat class', () => {
|
|
79
|
+
expect(
|
|
80
|
+
scopeByAttribute('<input type="text" class="foo {{bar}}">', ['type']),
|
|
81
|
+
).to.equal('<input type="text" class="foo {{bar}} pfx">');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('appends to a mustache class for tag-scoped elements', () => {
|
|
85
|
+
const out = rewriteHbs(
|
|
86
|
+
'<div class={{this.cls}}></div>',
|
|
87
|
+
new Set(),
|
|
88
|
+
new Set(['div']),
|
|
89
|
+
postfix,
|
|
90
|
+
new Set(),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
expect(out).to.equal('<div class="{{this.cls}} pfx"></div>');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('...attributes may deliver any scoped attribute at runtime', () => {
|
|
98
|
+
it('scopes elements that spread ...attributes', () => {
|
|
99
|
+
expect(scopeByAttribute('<div ...attributes></div>', ['type'])).to.equal(
|
|
100
|
+
'<div ...attributes class="pfx"></div>',
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('does not scope ...attributes when the CSS has no attribute selectors', () => {
|
|
105
|
+
expect(scopeByAttribute('<div ...attributes></div>', [])).to.equal(
|
|
106
|
+
'<div ...attributes></div>',
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('non-attribute bindings are ignored', () => {
|
|
112
|
+
it('does not match named args', () => {
|
|
113
|
+
expect(scopeByAttribute('<Foo @type="text" />', ['type'])).to.equal(
|
|
114
|
+
'<Foo @type="text" />',
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
package/src/lib/rewriteHbs.js
CHANGED
|
@@ -2,7 +2,35 @@ import * as recast from 'ember-template-recast';
|
|
|
2
2
|
|
|
3
3
|
import { renameClass } from './renameClass.js';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Whether an element may carry an attribute that appears in a scoped
|
|
7
|
+
* attribute selector. This includes component invocations: a component
|
|
8
|
+
* receives the postfix class through its `...attributes` the same way it
|
|
9
|
+
* receives the matched attribute, so `[type="text"]` scopes
|
|
10
|
+
* `<Foo type="text">` just as it scopes `<input type="text">`.
|
|
11
|
+
*
|
|
12
|
+
* An element that spreads `...attributes` can receive any attribute from its
|
|
13
|
+
* caller at runtime, so it counts as long as the CSS scopes by attribute at
|
|
14
|
+
* all — the preserved attribute selector still decides which rules actually
|
|
15
|
+
* apply.
|
|
16
|
+
*
|
|
17
|
+
* `attributes` comes from parsing the CSS, so it can only contain valid
|
|
18
|
+
* attribute-selector names — `@args` can never be in it and needs no special
|
|
19
|
+
* handling.
|
|
20
|
+
*
|
|
21
|
+
* Names in `attributes` are lowercased at discovery (CSS matches HTML
|
|
22
|
+
* attribute names case-insensitively), so compare lowercased.
|
|
23
|
+
*/
|
|
24
|
+
function elementHasScopedAttribute(node, attributes) {
|
|
25
|
+
if (attributes.size === 0) return false;
|
|
26
|
+
|
|
27
|
+
return node.attributes.some(
|
|
28
|
+
(attr) =>
|
|
29
|
+
attr.name === '...attributes' || attributes.has(attr.name.toLowerCase()),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function templatePlugin({ classes, tags, attributes, postfix }) {
|
|
6
34
|
let stack = [];
|
|
7
35
|
// scoped-class is a global we allow in hbs
|
|
8
36
|
// scopedClass is importable, and we'll error if someone tries to rename it
|
|
@@ -46,18 +74,34 @@ export function templatePlugin({ classes, tags, postfix }) {
|
|
|
46
74
|
},
|
|
47
75
|
|
|
48
76
|
ElementNode(node) {
|
|
49
|
-
if
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
77
|
+
// An element is in scope if its tag matches a tag selector, or if it
|
|
78
|
+
// carries an attribute named in a scoped attribute selector. We add the
|
|
79
|
+
// postfix class at most once regardless of how many things matched.
|
|
80
|
+
const shouldScope =
|
|
81
|
+
tags.has(node.tag) || elementHasScopedAttribute(node, attributes);
|
|
82
|
+
|
|
83
|
+
if (!shouldScope) return;
|
|
84
|
+
|
|
85
|
+
// check if class attribute already exists
|
|
86
|
+
const classAttr = node.attributes.find((attr) => attr.name === 'class');
|
|
87
|
+
|
|
88
|
+
if (!classAttr) {
|
|
89
|
+
// push class attribute
|
|
90
|
+
node.attributes.push(
|
|
91
|
+
recast.builders.attr('class', recast.builders.text(postfix)),
|
|
92
|
+
);
|
|
93
|
+
} else if (classAttr.value.type === 'TextNode') {
|
|
94
|
+
classAttr.value.chars += ' ' + postfix;
|
|
95
|
+
} else if (classAttr.value.type === 'ConcatStatement') {
|
|
96
|
+
// class="foo {{bar}}"
|
|
97
|
+
classAttr.value.parts.push(recast.builders.text(' ' + postfix));
|
|
98
|
+
} else {
|
|
99
|
+
// class={{this.foo}} — wrap in a concat so we can append the text part
|
|
100
|
+
classAttr.value = recast.builders.concat([
|
|
101
|
+
classAttr.value,
|
|
102
|
+
recast.builders.text(' ' + postfix),
|
|
103
|
+
]);
|
|
104
|
+
classAttr.quoteType = '"';
|
|
61
105
|
}
|
|
62
106
|
},
|
|
63
107
|
|
|
@@ -133,10 +177,16 @@ function getValue(path) {
|
|
|
133
177
|
return path.original;
|
|
134
178
|
}
|
|
135
179
|
|
|
136
|
-
export default function rewriteHbs(
|
|
180
|
+
export default function rewriteHbs(
|
|
181
|
+
hbs,
|
|
182
|
+
classes,
|
|
183
|
+
tags,
|
|
184
|
+
postfix,
|
|
185
|
+
attributes = new Set(),
|
|
186
|
+
) {
|
|
137
187
|
let ast = recast.parse(hbs);
|
|
138
188
|
|
|
139
|
-
recast.traverse(ast, templatePlugin({ classes, tags, postfix }));
|
|
189
|
+
recast.traverse(ast, templatePlugin({ classes, tags, attributes, postfix }));
|
|
140
190
|
|
|
141
191
|
let result = recast.print(ast);
|
|
142
192
|
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This declaration file teaches TypeScript (via Glint) about the special
|
|
3
|
+
* attributes that ember-scoped-css adds to the `<style>` element,
|
|
4
|
+
* so that `<style scoped>` and `<style inline>` type-check.
|
|
5
|
+
*
|
|
6
|
+
* Add it to your tsconfig.json:
|
|
7
|
+
*
|
|
8
|
+
* ```jsonc
|
|
9
|
+
* {
|
|
10
|
+
* "compilerOptions": {
|
|
11
|
+
* "types": ["ember-scoped-css/types"]
|
|
12
|
+
* }
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* or, if you don't use `compilerOptions.types`, import it from a
|
|
17
|
+
* type-declaration file that is included in your project:
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* import 'ember-scoped-css/types';
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
import '@glint/template';
|
|
24
|
+
|
|
25
|
+
declare global {
|
|
26
|
+
interface HTMLStyleElementAttributes {
|
|
27
|
+
scoped: '';
|
|
28
|
+
inline: '';
|
|
29
|
+
}
|
|
30
|
+
}
|