@startlibs/utils 1.1.1 → 1.2.0-rc.1
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/lib/animationKeyframes.mjs +11 -0
- package/lib/animationKeyframes.mjs.map +1 -0
- package/lib/fixMediaQuery.mjs +43 -17
- package/lib/fixMediaQuery.mjs.map +1 -1
- package/lib/fixMediaText.mjs +16 -0
- package/lib/fixMediaText.mjs.map +1 -0
- package/lib/formatDate.mjs +1 -1
- package/lib/formatDate.mjs.map +1 -1
- package/lib/index.cjs +212 -100
- package/lib/index.cjs.map +1 -1
- package/lib/index.mjs +3 -1
- package/lib/index.mjs.map +1 -1
- package/lib/insertKeyframes.mjs +68 -0
- package/lib/insertKeyframes.mjs.map +1 -0
- package/lib/styled.mjs +22 -141
- package/lib/styled.mjs.map +1 -1
- package/lib/styledShared.mjs +125 -0
- package/lib/styledShared.mjs.map +1 -0
- package/lib/timerUtils.mjs +0 -1
- package/lib/timerUtils.mjs.map +1 -1
- package/lib/yakDomTag.mjs +44 -0
- package/lib/yakDomTag.mjs.map +1 -0
- package/lib-yak/ActionUtils.mjs +117 -0
- package/lib-yak/ActionUtils.mjs.map +1 -0
- package/lib-yak/FetchUtils.mjs +100 -0
- package/lib-yak/FetchUtils.mjs.map +1 -0
- package/lib-yak/animationKeyframes.mjs +31 -0
- package/lib-yak/animationKeyframes.mjs.map +1 -0
- package/lib-yak/createPressEvent.mjs +74 -0
- package/lib-yak/createPressEvent.mjs.map +1 -0
- package/lib-yak/fixMediaQuery.mjs +90 -0
- package/lib-yak/fixMediaQuery.mjs.map +1 -0
- package/lib-yak/fixMediaText.mjs +16 -0
- package/lib-yak/fixMediaText.mjs.map +1 -0
- package/lib-yak/formatDate.mjs +110 -0
- package/lib-yak/formatDate.mjs.map +1 -0
- package/lib-yak/formatUtils.mjs +64 -0
- package/lib-yak/formatUtils.mjs.map +1 -0
- package/lib-yak/getClosestOffsetScroll.mjs +15 -0
- package/lib-yak/getClosestOffsetScroll.mjs.map +1 -0
- package/lib-yak/hooks/useEventListener.mjs +27 -0
- package/lib-yak/hooks/useEventListener.mjs.map +1 -0
- package/lib-yak/index.mjs +21 -0
- package/lib-yak/index.mjs.map +1 -0
- package/lib-yak/insertKeyframes.mjs +68 -0
- package/lib-yak/insertKeyframes.mjs.map +1 -0
- package/lib-yak/lodashUtils.mjs +131 -0
- package/lib-yak/lodashUtils.mjs.map +1 -0
- package/lib-yak/promiseUtils.mjs +92 -0
- package/lib-yak/promiseUtils.mjs.map +1 -0
- package/lib-yak/reactUtils.mjs +12 -0
- package/lib-yak/reactUtils.mjs.map +1 -0
- package/lib-yak/reduceUtils.mjs +14 -0
- package/lib-yak/reduceUtils.mjs.map +1 -0
- package/lib-yak/smoothScroll.mjs +59 -0
- package/lib-yak/smoothScroll.mjs.map +1 -0
- package/lib-yak/styled.mjs +23 -0
- package/lib-yak/styled.mjs.map +1 -0
- package/lib-yak/styledShared.mjs +129 -0
- package/lib-yak/styledShared.mjs.map +1 -0
- package/lib-yak/timeUtils.mjs +81 -0
- package/lib-yak/timeUtils.mjs.map +1 -0
- package/lib-yak/timerUtils.mjs +10 -0
- package/lib-yak/timerUtils.mjs.map +1 -0
- package/lib-yak/utils.mjs +386 -0
- package/lib-yak/utils.mjs.map +1 -0
- package/lib-yak/wrapLazy.mjs +132 -0
- package/lib-yak/wrapLazy.mjs.map +1 -0
- package/lib-yak/yakDomTag.mjs +44 -0
- package/lib-yak/yakDomTag.mjs.map +1 -0
- package/package.json +14 -3
- package/src/animationKeyframes.js +10 -0
- package/src/animationKeyframes.test.mjs +75 -0
- package/src/animationKeyframes.yak.js +30 -0
- package/src/fixMediaQuery.js +40 -19
- package/src/fixMediaText.js +18 -0
- package/src/fixMediaText.test.mjs +60 -0
- package/src/formatDate.js +1 -1
- package/src/index.js +1 -0
- package/src/insertKeyframes.js +73 -0
- package/src/insertKeyframes.test.mjs +144 -0
- package/src/styled.js +5 -97
- package/src/styled.yak.js +30 -0
- package/src/styledShared.js +96 -0
- package/src/timerUtils.jsx +0 -2
- package/src/yakDomTag.jsx +42 -0
- package/src/yakDomTag.test.mjs +49 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import isPropValid from '@emotion/is-prop-valid';
|
|
3
|
+
|
|
4
|
+
// Task 13c: @yak/react só filtra props `$`-prefixadas e `undefined` antes de repassar pro
|
|
5
|
+
// elemento real; qualquer prop custom (string, número, objeto) lida por um `styled.<tag>` no yak
|
|
6
|
+
// vaza como atributo no DOM. styled-components v5 evita isso sozinho pra tags string, usando
|
|
7
|
+
// @emotion/is-prop-valid. yakDomTag(tag) devolve um componente equivalente: filtra pelas mesmas
|
|
8
|
+
// regras antes de renderizar `tag`, pra o DOM do yak ficar idêntico ao do sc.
|
|
9
|
+
//
|
|
10
|
+
// Resolvido via `@startlibs/utils/yakDomTag` (exports["./*"] do package.json) — não passa pelo
|
|
11
|
+
// index: só existe nessa saída porque o babel-plugin-startlibs-to-yak importa explicitamente
|
|
12
|
+
// (ver tools/babel-plugin-startlibs-to-yak/domTag.cjs), nunca no fonte sc.
|
|
13
|
+
//
|
|
14
|
+
// Memoizado por nome de tag: `styled(yakDomTag('div'))` precisa devolver sempre o MESMO
|
|
15
|
+
// componente de DOM final pra identidade ficar estável entre `${X}` e composições
|
|
16
|
+
// (`styled(X)`/`styledExtend(X)`) de um mesmo `styled.div` — ver @yak/react runtime, que carrega
|
|
17
|
+
// esse componente até o fim da cadeia de composição.
|
|
18
|
+
//
|
|
19
|
+
// `as` (styled-components troca a tag renderizada com essa prop, ex.: components/src/containers/
|
|
20
|
+
// usePositioning.jsx's Placeholder as={placeholderAs}, form/src/TextInput.jsx's Input
|
|
21
|
+
// as={inputTag}) não é filtrada pelo @emotion/is-prop-valid (`isPropValid('as')` é false) — sem
|
|
22
|
+
// tratar à parte, o yak renderizaria sempre a tag original, ignorando a troca.
|
|
23
|
+
const cache = new Map();
|
|
24
|
+
const yakDomTag = tag => {
|
|
25
|
+
let Component = cache.get(tag);
|
|
26
|
+
if (!Component) {
|
|
27
|
+
Component = /*#__PURE__*/React.forwardRef((props, ref) => {
|
|
28
|
+
const domProps = {};
|
|
29
|
+
for (const key in props) {
|
|
30
|
+
// `as` troca a tag renderizada (como styled-components faz); não é uma prop do DOM.
|
|
31
|
+
if (key === 'as') continue;
|
|
32
|
+
if (key === 'className' || isPropValid(key)) domProps[key] = props[key];
|
|
33
|
+
}
|
|
34
|
+
domProps.ref = ref;
|
|
35
|
+
return /*#__PURE__*/React.createElement(props.as || tag, domProps);
|
|
36
|
+
});
|
|
37
|
+
Component.displayName = `yakDomTag(${tag})`;
|
|
38
|
+
cache.set(tag, Component);
|
|
39
|
+
}
|
|
40
|
+
return Component;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export { yakDomTag };
|
|
44
|
+
//# sourceMappingURL=yakDomTag.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"yakDomTag.mjs","sources":["../src/yakDomTag.jsx"],"sourcesContent":["import React from 'react'\nimport isPropValid from '@emotion/is-prop-valid'\n\n// Task 13c: @yak/react só filtra props `$`-prefixadas e `undefined` antes de repassar pro\n// elemento real; qualquer prop custom (string, número, objeto) lida por um `styled.<tag>` no yak\n// vaza como atributo no DOM. styled-components v5 evita isso sozinho pra tags string, usando\n// @emotion/is-prop-valid. yakDomTag(tag) devolve um componente equivalente: filtra pelas mesmas\n// regras antes de renderizar `tag`, pra o DOM do yak ficar idêntico ao do sc.\n//\n// Resolvido via `@startlibs/utils/yakDomTag` (exports[\"./*\"] do package.json) — não passa pelo\n// index: só existe nessa saída porque o babel-plugin-startlibs-to-yak importa explicitamente\n// (ver tools/babel-plugin-startlibs-to-yak/domTag.cjs), nunca no fonte sc.\n//\n// Memoizado por nome de tag: `styled(yakDomTag('div'))` precisa devolver sempre o MESMO\n// componente de DOM final pra identidade ficar estável entre `${X}` e composições\n// (`styled(X)`/`styledExtend(X)`) de um mesmo `styled.div` — ver @yak/react runtime, que carrega\n// esse componente até o fim da cadeia de composição.\n//\n// `as` (styled-components troca a tag renderizada com essa prop, ex.: components/src/containers/\n// usePositioning.jsx's Placeholder as={placeholderAs}, form/src/TextInput.jsx's Input\n// as={inputTag}) não é filtrada pelo @emotion/is-prop-valid (`isPropValid('as')` é false) — sem\n// tratar à parte, o yak renderizaria sempre a tag original, ignorando a troca.\nconst cache = new Map()\n\nexport const yakDomTag = (tag) => {\n let Component = cache.get(tag)\n if (!Component) {\n Component = React.forwardRef((props, ref) => {\n const domProps = {}\n for (const key in props) {\n // `as` troca a tag renderizada (como styled-components faz); não é uma prop do DOM.\n if (key === 'as') continue\n if (key === 'className' || isPropValid(key)) domProps[key] = props[key]\n }\n domProps.ref = ref\n return React.createElement(props.as || tag, domProps)\n })\n Component.displayName = `yakDomTag(${tag})`\n cache.set(tag, Component)\n }\n return Component\n}\n"],"names":["cache","Map","yakDomTag","tag","Component","get","React","forwardRef","props","ref","domProps","key","isPropValid","createElement","as","displayName","set"],"mappings":";;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAMA,KAAK,GAAG,IAAIC,GAAG,EAAE,CAAA;AAEVC,MAAAA,SAAS,GAAIC,GAAG,IAAK;AAChC,EAAA,IAAIC,SAAS,GAAGJ,KAAK,CAACK,GAAG,CAACF,GAAG,CAAC,CAAA;EAC9B,IAAI,CAACC,SAAS,EAAE;IACdA,SAAS,gBAAGE,KAAK,CAACC,UAAU,CAAC,CAACC,KAAK,EAAEC,GAAG,KAAK;MAC3C,MAAMC,QAAQ,GAAG,EAAE,CAAA;AACnB,MAAA,KAAK,MAAMC,GAAG,IAAIH,KAAK,EAAE;AACvB;QACA,IAAIG,GAAG,KAAK,IAAI,EAAE,SAAA;AAClB,QAAA,IAAIA,GAAG,KAAK,WAAW,IAAIC,WAAW,CAACD,GAAG,CAAC,EAAED,QAAQ,CAACC,GAAG,CAAC,GAAGH,KAAK,CAACG,GAAG,CAAC,CAAA;AACzE,OAAA;MACAD,QAAQ,CAACD,GAAG,GAAGA,GAAG,CAAA;MAClB,oBAAOH,KAAK,CAACO,aAAa,CAACL,KAAK,CAACM,EAAE,IAAIX,GAAG,EAAEO,QAAQ,CAAC,CAAA;AACvD,KAAC,CAAC,CAAA;AACFN,IAAAA,SAAS,CAACW,WAAW,GAAG,CAAA,UAAA,EAAaZ,GAAG,CAAG,CAAA,CAAA,CAAA;AAC3CH,IAAAA,KAAK,CAACgB,GAAG,CAACb,GAAG,EAAEC,SAAS,CAAC,CAAA;AAC3B,GAAA;AACA,EAAA,OAAOA,SAAS,CAAA;AAClB;;;;"}
|
package/package.json
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@startlibs/utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0-rc.1",
|
|
4
4
|
"description": "Startlibs Utils",
|
|
5
5
|
"main": "lib/index.cjs",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
8
|
"source": "./src/index.js",
|
|
9
|
+
"yak": "./lib-yak/index.mjs",
|
|
9
10
|
"require": "./lib/index.cjs",
|
|
10
11
|
"import": "./lib/index.mjs",
|
|
11
12
|
"default": "./lib/index.mjs"
|
|
12
13
|
},
|
|
13
14
|
"./package.json": "./package.json",
|
|
14
15
|
"./*": {
|
|
16
|
+
"yak": "./lib-yak/*.mjs",
|
|
15
17
|
"import": "./lib/*.mjs"
|
|
16
18
|
}
|
|
17
19
|
},
|
|
@@ -19,11 +21,17 @@
|
|
|
19
21
|
"source": "./src/index.js",
|
|
20
22
|
"peerDependencies": {
|
|
21
23
|
"@startlibs/core": "*",
|
|
24
|
+
"@yak/react": "10.0.0",
|
|
22
25
|
"lodash": "^4.17.21",
|
|
23
26
|
"polished": "^2.0.0 || ^3.0.0 || ^4.0.0",
|
|
24
27
|
"react": ">=16.8",
|
|
25
28
|
"styled-components": ">=5"
|
|
26
29
|
},
|
|
30
|
+
"peerDependenciesMeta": {
|
|
31
|
+
"@yak/react": {
|
|
32
|
+
"optional": true
|
|
33
|
+
}
|
|
34
|
+
},
|
|
27
35
|
"devDependencies": {
|
|
28
36
|
"@babel/core": "^7.26.0",
|
|
29
37
|
"@babel/plugin-transform-runtime": "^7.26.0",
|
|
@@ -35,6 +43,7 @@
|
|
|
35
43
|
"babel-plugin-styled-components": "^1.10.0",
|
|
36
44
|
"lodash": "^4.17.21",
|
|
37
45
|
"polished": "^2.0.0 || ^3.0.0 || ^4.0.0",
|
|
46
|
+
"react": "^18.3.1",
|
|
38
47
|
"rollup": "^2.79.2",
|
|
39
48
|
"rollup-plugin-auto-external": "^2.0.0",
|
|
40
49
|
"rollup-plugin-babel": "^4.0.3",
|
|
@@ -43,8 +52,8 @@
|
|
|
43
52
|
"styled-components": "^5.3.11"
|
|
44
53
|
},
|
|
45
54
|
"scripts": {
|
|
46
|
-
"test": "
|
|
47
|
-
"build": "rollup -c"
|
|
55
|
+
"test": "node --import ./test/register.mjs --test src/**/*.test.mjs",
|
|
56
|
+
"build": "rollup -c && rollup -c --environment YAK"
|
|
48
57
|
},
|
|
49
58
|
"repository": {
|
|
50
59
|
"type": "git",
|
|
@@ -62,10 +71,12 @@
|
|
|
62
71
|
"homepage": "https://github.com/felipeko/startlibs#readme",
|
|
63
72
|
"dependencies": {
|
|
64
73
|
"@babel/runtime": "^7.20.0",
|
|
74
|
+
"@emotion/is-prop-valid": "^1.1.0",
|
|
65
75
|
"create-emotion": "10.0.27"
|
|
66
76
|
},
|
|
67
77
|
"files": [
|
|
68
78
|
"lib",
|
|
79
|
+
"lib-yak",
|
|
69
80
|
"src"
|
|
70
81
|
],
|
|
71
82
|
"module": "lib/index.mjs",
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import createEmotion from 'create-emotion'
|
|
2
|
+
|
|
3
|
+
// Saída sc (lib/): o keyframes do emotion, como sempre foi. A saída yak (lib-yak/) usa
|
|
4
|
+
// animationKeyframes.yak.js no lugar deste arquivo (ver tools/rollup.base.mjs).
|
|
5
|
+
const emotion = createEmotion({
|
|
6
|
+
nonce: (typeof document !== 'undefined' &&
|
|
7
|
+
(document.querySelector('meta[property="csp-nonce"]') || {}).content) || 'animation_nonce'
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export const animationKeyframes = emotion.keyframes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {test, before, after} from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
// oneWayAnimation/twoWayAnimation na saída yak. `styled.js?yak` carrega o grafo como a
|
|
5
|
+
// lib-yak (animationKeyframes.yak.js no lugar de animationKeyframes.js, ver test/jsxLoader.mjs).
|
|
6
|
+
// A saída sc (emotion) não roda aqui: o default do create-emotion é CJS; ela fica igual à de
|
|
7
|
+
// antes, o que tools/compare-lib.sh e as goldens do compat conferem.
|
|
8
|
+
// fixMediaQuery lê `window` no import; sem document, ele não mede nada.
|
|
9
|
+
const adopted = []
|
|
10
|
+
before(() => {
|
|
11
|
+
globalThis.window = {}
|
|
12
|
+
globalThis.CSSStyleSheet = class {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.cssRules = []
|
|
15
|
+
adopted.push(this)
|
|
16
|
+
}
|
|
17
|
+
insertRule(rule, index) {
|
|
18
|
+
this.cssRules.splice(index, 0, rule)
|
|
19
|
+
}
|
|
20
|
+
replaceSync() {}
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
after(() => {
|
|
24
|
+
delete globalThis.window
|
|
25
|
+
delete globalThis.document
|
|
26
|
+
delete globalThis.CSSStyleSheet
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const withDocument = () => {
|
|
30
|
+
globalThis.document = {
|
|
31
|
+
adoptedStyleSheets: [],
|
|
32
|
+
styleSheets: [],
|
|
33
|
+
querySelector: () => null
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const rules = () => adopted.flatMap((sheet) => sheet.cssRules)
|
|
38
|
+
const squash = (text) => text.replace(/\s+/g, ' ').trim()
|
|
39
|
+
|
|
40
|
+
test('yak: mesmos offsets de antes (0..100%, frame vazio fica de fora), registrado pelo CSSOM', async () => {
|
|
41
|
+
const {oneWayAnimation} = await import('./styled.js?yak')
|
|
42
|
+
withDocument()
|
|
43
|
+
const name = oneWayAnimation('opacity: 0;', '', 'opacity: 1;')
|
|
44
|
+
assert.match(name, /^startlibs-animation-[0-9a-z]+$/)
|
|
45
|
+
const rule = rules().find((r) => r.startsWith(`@keyframes ${name} `))
|
|
46
|
+
assert.equal(squash(rule), `@keyframes ${name} { 0% { opacity: 0; }100% { opacity: 1; }}`)
|
|
47
|
+
|
|
48
|
+
const three = oneWayAnimation('a: 1;', 'a: 2;', 'a: 3;', 'a: 4;')
|
|
49
|
+
assert.equal(squash(rules().find((r) => r.startsWith(`@keyframes ${three} `))),
|
|
50
|
+
`@keyframes ${three} { 0% { a: 1; }33% { a: 2; }67% { a: 3; }100% { a: 4; }}`)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('yak: mesmos frames, mesmo nome e uma regra só; frames diferentes, nomes diferentes', async () => {
|
|
54
|
+
const {oneWayAnimation, twoWayAnimation} = await import('./styled.js?yak')
|
|
55
|
+
withDocument()
|
|
56
|
+
const before = rules().length
|
|
57
|
+
const a = oneWayAnimation('opacity: 0.25;', 'opacity: 0.5;')
|
|
58
|
+
assert.equal(oneWayAnimation('opacity: 0.25;', 'opacity: 0.5;'), a)
|
|
59
|
+
assert.equal(rules().length, before + 1)
|
|
60
|
+
|
|
61
|
+
const [enter, leave] = twoWayAnimation('opacity: 0.25;', 'opacity: 0.75;')
|
|
62
|
+
assert.notEqual(enter, leave)
|
|
63
|
+
assert.notEqual(enter, a)
|
|
64
|
+
assert.equal(squash(rules().find((r) => r.startsWith(`@keyframes ${leave} `))),
|
|
65
|
+
`@keyframes ${leave} { 0% { opacity: 0.75; }100% { opacity: 0.25; }}`)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('yak: no import (sem document) devolve um nome e registra quando o frame é pedido de novo', async () => {
|
|
69
|
+
delete globalThis.document
|
|
70
|
+
const {identityOpacityFrames, oneWayAnimation} = await import('./styled.js?yak')
|
|
71
|
+
assert.match(identityOpacityFrames, /^startlibs-animation-/)
|
|
72
|
+
withDocument()
|
|
73
|
+
assert.equal(oneWayAnimation('opacity: 1;', 'opacity:1;'), identityOpacityFrames)
|
|
74
|
+
assert.ok(rules().some((r) => r.startsWith(`@keyframes ${identityOpacityFrames} `)))
|
|
75
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {insertKeyframes} from './insertKeyframes'
|
|
2
|
+
|
|
3
|
+
// Saída yak (lib-yak/): mesmo contrato do keyframes do emotion (tag de template que devolve o
|
|
4
|
+
// nome da animação), mas registrado pelo CSSOM, sem <style> e sem nonce.
|
|
5
|
+
const interpolate = (value) =>
|
|
6
|
+
Array.isArray(value) ? value.map(interpolate).join('')
|
|
7
|
+
: value === null || value === undefined || typeof value === 'boolean' ? ''
|
|
8
|
+
: String(value)
|
|
9
|
+
|
|
10
|
+
// djb2: só precisa ser estável e curto; colisão é tratada abaixo.
|
|
11
|
+
const hash = (text) => {
|
|
12
|
+
let h = 5381
|
|
13
|
+
for (let i = 0; i < text.length; i++) h = ((h * 33) ^ text.charCodeAt(i)) >>> 0
|
|
14
|
+
return h.toString(36)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const nameByFrames = new Map()
|
|
18
|
+
const usedNames = new Set()
|
|
19
|
+
|
|
20
|
+
export const animationKeyframes = (strings, ...values) => {
|
|
21
|
+
const frames = strings.reduce((text, part, i) => text + part + (i < values.length ? interpolate(values[i]) : ''), '')
|
|
22
|
+
if (!nameByFrames.has(frames)) {
|
|
23
|
+
const base = 'startlibs-animation-' + hash(frames)
|
|
24
|
+
let name = base
|
|
25
|
+
for (let i = 2; usedNames.has(name); i++) name = base + '-' + i
|
|
26
|
+
usedNames.add(name)
|
|
27
|
+
nameByFrames.set(frames, name)
|
|
28
|
+
}
|
|
29
|
+
return insertKeyframes(nameByFrames.get(frames), frames)
|
|
30
|
+
}
|
package/src/fixMediaQuery.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
import {fixMediaText} from './fixMediaText'
|
|
2
|
+
|
|
1
3
|
let fixed = false
|
|
4
|
+
// Each rule may only get the scrollbar width added once, even if its sheet is seen again.
|
|
5
|
+
const patched = new WeakSet()
|
|
6
|
+
|
|
2
7
|
export const scrollWidth = (function() {
|
|
3
8
|
if (!window.document) return 0
|
|
4
9
|
const html = document.documentElement
|
|
@@ -15,28 +20,44 @@ const fixMediaQueryFn = function() {
|
|
|
15
20
|
|
|
16
21
|
if (scrollWidth > 0) {
|
|
17
22
|
|
|
18
|
-
[].slice.call(document.styleSheets).
|
|
19
|
-
|
|
20
|
-
[].slice.call(stylesheet.cssRules || stylesheet.media).filter(rule => rule.constructor === window.CSSMediaRule).forEach(rule => {
|
|
21
|
-
rule.media.mediaText = rule.media.mediaText.replace(/m(in|ax)-width:\s*(\d|\.)+(px|em)/gi, function(strA) {
|
|
22
|
-
if (strA.match('px')) {
|
|
23
|
-
return strA.replace(/\d+px/gi, function(strB) {
|
|
24
|
-
return parseInt(strB, 10) + scrollWidth + 'px'
|
|
25
|
-
})
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
return strA.replace(/\d.+?em/gi, function(strB) {
|
|
29
|
-
return ((parseFloat(strB) * fontSize) + scrollWidth) / fontSize + 'em'
|
|
30
|
-
})
|
|
31
|
-
}
|
|
32
|
-
})
|
|
33
|
-
})
|
|
34
|
-
} catch (e) {}
|
|
35
|
-
})
|
|
23
|
+
[].slice.call(document.styleSheets).forEach(fixStylesheet)
|
|
24
|
+
watchLateStylesheets()
|
|
36
25
|
}
|
|
37
26
|
|
|
38
27
|
fixed = true
|
|
39
28
|
|
|
29
|
+
function fixStylesheet(stylesheet) {
|
|
30
|
+
if (!stylesheet || patched.has(stylesheet) || !(isValidExternal(stylesheet) || isInline(stylesheet))) {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
[].slice.call(stylesheet.cssRules).filter(rule => rule.constructor === window.CSSMediaRule).forEach(rule => {
|
|
35
|
+
rule.media.mediaText = fixMediaText(rule.media.mediaText, scrollWidth, fontSize)
|
|
36
|
+
})
|
|
37
|
+
patched.add(stylesheet)
|
|
38
|
+
} catch (e) {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Lazy routes add <link>s, and Vite dev and HMR add or rewrite <style>s, after the first pass.
|
|
42
|
+
function watchLateStylesheets() {
|
|
43
|
+
new MutationObserver(mutations => mutations.forEach(({target, addedNodes}) => {
|
|
44
|
+
const owner = target.nodeType === Node.TEXT_NODE ? target.parentNode : target
|
|
45
|
+
if (owner && owner.nodeName === 'STYLE') {
|
|
46
|
+
fixStylesheet(owner.sheet)
|
|
47
|
+
}
|
|
48
|
+
[].slice.call(addedNodes).forEach(node => {
|
|
49
|
+
if (node.nodeName === 'STYLE') {
|
|
50
|
+
fixStylesheet(node.sheet)
|
|
51
|
+
}
|
|
52
|
+
if (node.nodeName === 'LINK' && node.rel === 'stylesheet') {
|
|
53
|
+
// Not parsed yet when added; `load` also fires again if its href changes.
|
|
54
|
+
node.addEventListener('load', () => fixStylesheet(node.sheet))
|
|
55
|
+
fixStylesheet(node.sheet)
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
})).observe(document.head, {childList: true, subtree: true, characterData: true})
|
|
59
|
+
}
|
|
60
|
+
|
|
40
61
|
function sameOrigin(url) {
|
|
41
62
|
const loc = window.location,
|
|
42
63
|
a = document.createElement('a')
|
|
@@ -68,4 +89,4 @@ export function fixMediaQuery() {
|
|
|
68
89
|
} else {
|
|
69
90
|
document.addEventListener('DOMContentLoaded', fixMediaQueryFn)
|
|
70
91
|
}
|
|
71
|
-
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Soma a largura da scrollbar aos limites de largura de um mediaText.
|
|
2
|
+
// Reconhece a forma antiga (`max-width: 600px`, `min-width: 37.5em`, que o sc emite) e a
|
|
3
|
+
// sintaxe de intervalo (`width <= 600px`, `width < 600px`, `600px <= width`,
|
|
4
|
+
// `400px < width < 600px`), que o Chromium devolve para o CSS estático do yak.
|
|
5
|
+
// Sem lookbehind de propósito: é erro de sintaxe no Safari < 16.4, e o browserslist do pacote
|
|
6
|
+
// ainda promete Safari 11. O caractere antes de `width` é capturado e devolvido igual, só pra
|
|
7
|
+
// não casar o `width` de `device-width`.
|
|
8
|
+
const LEGACY_OR_WIDTH_FIRST = /(m(?:in|ax)-width\s*:\s*|(?:^|[^-\w])width\s*[<>]=?\s*)([\d.]+)(px|em)/gi
|
|
9
|
+
const VALUE_FIRST = /([\d.]+)(px|em)(\s*[<>]=?\s*width)(?![-\w])/gi
|
|
10
|
+
|
|
11
|
+
export function fixMediaText(mediaText, scrollWidth, fontSize) {
|
|
12
|
+
const grow = (value, unit) => unit.toLowerCase() === 'px'
|
|
13
|
+
? parseFloat(value) + scrollWidth + unit
|
|
14
|
+
: ((parseFloat(value) * fontSize) + scrollWidth) / fontSize + unit
|
|
15
|
+
return mediaText
|
|
16
|
+
.replace(LEGACY_OR_WIDTH_FIRST, (match, prefix, value, unit) => prefix + grow(value, unit))
|
|
17
|
+
.replace(VALUE_FIRST, (match, value, unit, suffix) => grow(value, unit) + suffix)
|
|
18
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {test} from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import {readFileSync} from 'node:fs'
|
|
4
|
+
import {fixMediaText} from './fixMediaText.js'
|
|
5
|
+
|
|
6
|
+
const fix = (mediaText) => fixMediaText(mediaText, 15, 16)
|
|
7
|
+
|
|
8
|
+
test('forma antiga do sc: max-width/min-width em px', () => {
|
|
9
|
+
assert.equal(fix('(max-width: 600px)'), '(max-width: 615px)')
|
|
10
|
+
assert.equal(fix('screen and (min-width: 601px)'), 'screen and (min-width: 616px)')
|
|
11
|
+
assert.equal(fix('(max-width:600px)'), '(max-width:615px)')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('forma antiga do sc: em é convertido via font-size', () => {
|
|
15
|
+
assert.equal(fix('(max-width: 37.5em)'), `(max-width: ${(37.5 * 16 + 15) / 16}em)`)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('sintaxe de intervalo do yak: <= e >=', () => {
|
|
19
|
+
assert.equal(fix('(width <= 600px)'), '(width <= 615px)')
|
|
20
|
+
assert.equal(fix('(width>=601px)'), '(width>=616px)')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('sintaxe de intervalo estrita: < e >', () => {
|
|
24
|
+
assert.equal(fix('(width < 600px)'), '(width < 615px)')
|
|
25
|
+
assert.equal(fix('(width>601px)'), '(width>616px)')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('sintaxe de intervalo com o valor antes de width', () => {
|
|
29
|
+
assert.equal(fix('(600px <= width)'), '(615px <= width)')
|
|
30
|
+
assert.equal(fix('(600px<width)'), '(615px<width)')
|
|
31
|
+
assert.equal(fix('(400px < width < 600px)'), '(415px < width < 615px)')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('mantém o caractere antes de width', () => {
|
|
35
|
+
assert.equal(fix('(width <= 600px)'), '(width <= 615px)')
|
|
36
|
+
assert.equal(fix('width <= 600px'), 'width <= 615px')
|
|
37
|
+
assert.equal(fix('not all and (width<600px)'), 'not all and (width<615px)')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('não mexe em device-width nem em outros media features', () => {
|
|
41
|
+
assert.equal(fix('(device-width >= 600px)'), '(device-width >= 600px)')
|
|
42
|
+
assert.equal(fix('(600px <= device-width)'), '(600px <= device-width)')
|
|
43
|
+
assert.equal(fix('(max-device-width: 600px)'), '(max-device-width: 600px)')
|
|
44
|
+
assert.equal(fix('(height <= 600px)'), '(height <= 600px)')
|
|
45
|
+
assert.equal(fix('print'), 'print')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('várias condições na mesma regra', () => {
|
|
49
|
+
assert.equal(
|
|
50
|
+
fix('(min-width: 400px) and (max-width: 600px), (width < 300px)'),
|
|
51
|
+
'(min-width: 415px) and (max-width: 615px), (width < 315px)'
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('o fonte não usa lookbehind (quebra Safari < 16.4)', () => {
|
|
56
|
+
for (const file of ['fixMediaText.js', 'fixMediaQuery.js']) {
|
|
57
|
+
const src = readFileSync(new URL(file, import.meta.url), 'utf8')
|
|
58
|
+
assert.doesNotMatch(src, /\(\?<[=!]/, file)
|
|
59
|
+
}
|
|
60
|
+
})
|
package/src/formatDate.js
CHANGED
|
@@ -77,7 +77,7 @@ export function formatDateToPattern(format) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
export function formatDate(date, format, messages,noUTC) {
|
|
80
|
-
if (!date)
|
|
80
|
+
if (!date && date !== 0)
|
|
81
81
|
return ''
|
|
82
82
|
const separators = format.replace(escapedValidParts, '\0').split('\0')
|
|
83
83
|
const parts = format.match(escapedValidParts)
|
package/src/index.js
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Registra um @keyframes pelo CSSOM, sem criar <style> na página: funciona com CSP
|
|
2
|
+
// `style-src 'self'` sem nonce. Ordem de tentativa:
|
|
3
|
+
// 1. uma folha construída (new CSSStyleSheet) em document.adoptedStyleSheets;
|
|
4
|
+
// 2. browser sem folha construída: insertRule numa folha do próprio site já carregada
|
|
5
|
+
// (de preferência um <link>, que nenhum runtime de CSS-in-JS gerencia);
|
|
6
|
+
// 3. nenhuma das duas: <style> com o nonce do <meta property="csp-nonce">, se houver.
|
|
7
|
+
// Cada nome entra uma vez só; quem chama garante que nome igual é conteúdo igual.
|
|
8
|
+
const inserted = new Set()
|
|
9
|
+
let adoptedSheet = null
|
|
10
|
+
let pageSheet = null
|
|
11
|
+
|
|
12
|
+
const canAdoptSheets = () =>
|
|
13
|
+
typeof CSSStyleSheet !== 'undefined' &&
|
|
14
|
+
'replaceSync' in CSSStyleSheet.prototype &&
|
|
15
|
+
'adoptedStyleSheets' in document
|
|
16
|
+
|
|
17
|
+
// Outro código pode reatribuir document.adoptedStyleSheets e tirar a folha da lista: recoloca.
|
|
18
|
+
const getAdoptedSheet = () => {
|
|
19
|
+
if (!adoptedSheet) adoptedSheet = new CSSStyleSheet()
|
|
20
|
+
if (document.adoptedStyleSheets.indexOf(adoptedSheet) < 0) {
|
|
21
|
+
document.adoptedStyleSheets = document.adoptedStyleSheets.concat(adoptedSheet)
|
|
22
|
+
}
|
|
23
|
+
return adoptedSheet
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Folha que vale pra tela inteira e cujas regras dá pra ler (folha de outra origem lança erro).
|
|
27
|
+
const isWritable = (sheet) => {
|
|
28
|
+
if (sheet.disabled || !sheet.ownerNode || !sheet.ownerNode.isConnected) return false
|
|
29
|
+
if (sheet.media && sheet.media.length && sheet.media.mediaText !== 'all') return false
|
|
30
|
+
try {
|
|
31
|
+
return !!sheet.cssRules
|
|
32
|
+
} catch (e) {
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const getPageSheet = () => {
|
|
38
|
+
if (pageSheet && isWritable(pageSheet)) return pageSheet
|
|
39
|
+
const sheets = Array.prototype.slice.call(document.styleSheets).filter(isWritable)
|
|
40
|
+
pageSheet = sheets.filter((sheet) => sheet.ownerNode.tagName === 'LINK')[0] || sheets[0] || null
|
|
41
|
+
return pageSheet
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const appendRule = (sheet, rule) => {
|
|
45
|
+
try {
|
|
46
|
+
sheet.insertRule(rule, sheet.cssRules.length)
|
|
47
|
+
} catch (e) {
|
|
48
|
+
// regra que o browser não entende: igual a um <style> com ela, só não faz nada
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const appendStyleElement = (name, rule) => {
|
|
53
|
+
const style = document.createElement('style')
|
|
54
|
+
style.id = 'startlibs-keyframes-' + name
|
|
55
|
+
const nonce = (document.querySelector('meta[property="csp-nonce"]') || {}).content
|
|
56
|
+
if (nonce) style.nonce = nonce
|
|
57
|
+
style.textContent = rule
|
|
58
|
+
document.head.appendChild(style)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const insertKeyframes = (name, frames) => {
|
|
62
|
+
if (typeof document === 'undefined' || inserted.has(name)) return name
|
|
63
|
+
inserted.add(name)
|
|
64
|
+
const rule = '@keyframes ' + name + ' {' + frames + '}'
|
|
65
|
+
if (canAdoptSheets()) {
|
|
66
|
+
appendRule(getAdoptedSheet(), rule)
|
|
67
|
+
} else if (getPageSheet()) {
|
|
68
|
+
appendRule(pageSheet, rule)
|
|
69
|
+
} else {
|
|
70
|
+
appendStyleElement(name, rule)
|
|
71
|
+
}
|
|
72
|
+
return name
|
|
73
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import {test, afterEach} from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
// DOM falso, só o que insertKeyframes lê. Cada teste importa o módulo com uma query diferente
|
|
5
|
+
// pra começar sem estado (folha adotada, nomes já inseridos).
|
|
6
|
+
let instance = 0
|
|
7
|
+
const freshInsertKeyframes = async () => (await import(`./insertKeyframes.js?t${++instance}`)).insertKeyframes
|
|
8
|
+
|
|
9
|
+
const fakeSheet = ({tagName = 'LINK', media = '', readable = true} = {}) => {
|
|
10
|
+
const rules = []
|
|
11
|
+
return {
|
|
12
|
+
ownerNode: {tagName, isConnected: true},
|
|
13
|
+
media: {length: media ? 1 : 0, mediaText: media},
|
|
14
|
+
get cssRules() {
|
|
15
|
+
if (!readable) throw new Error('SecurityError')
|
|
16
|
+
return rules
|
|
17
|
+
},
|
|
18
|
+
rules,
|
|
19
|
+
insertRule(rule, index) {
|
|
20
|
+
rules.splice(index, 0, rule)
|
|
21
|
+
return index
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const fakeDocument = ({styleSheets = [], adopted = false, nonce} = {}) => {
|
|
27
|
+
const appended = []
|
|
28
|
+
const doc = {
|
|
29
|
+
styleSheets,
|
|
30
|
+
appended,
|
|
31
|
+
head: {appendChild: (el) => appended.push(el)},
|
|
32
|
+
createElement: (tag) => ({tag}),
|
|
33
|
+
querySelector: (selector) => selector === 'meta[property="csp-nonce"]' && nonce ? {content: nonce} : null
|
|
34
|
+
}
|
|
35
|
+
if (adopted) doc.adoptedStyleSheets = []
|
|
36
|
+
return doc
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class FakeConstructedSheet {
|
|
40
|
+
constructor() {
|
|
41
|
+
this.rules = []
|
|
42
|
+
this.cssRules = this.rules
|
|
43
|
+
}
|
|
44
|
+
insertRule(rule, index) {
|
|
45
|
+
this.rules.splice(index, 0, rule)
|
|
46
|
+
return index
|
|
47
|
+
}
|
|
48
|
+
replaceSync() {}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
delete globalThis.document
|
|
53
|
+
delete globalThis.CSSStyleSheet
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('sem document (SSR): devolve o nome e não faz nada', async () => {
|
|
57
|
+
const insertKeyframes = await freshInsertKeyframes()
|
|
58
|
+
assert.equal(insertKeyframes('spin', '100% { opacity: 0 }'), 'spin')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('folha construída em adoptedStyleSheets, uma só, sem repetir o mesmo nome', async () => {
|
|
62
|
+
const insertKeyframes = await freshInsertKeyframes()
|
|
63
|
+
const link = fakeSheet()
|
|
64
|
+
globalThis.document = fakeDocument({adopted: true, styleSheets: [link]})
|
|
65
|
+
globalThis.CSSStyleSheet = FakeConstructedSheet
|
|
66
|
+
|
|
67
|
+
assert.equal(insertKeyframes('a', 'from { opacity: 0 } to { opacity: 1 }'), 'a')
|
|
68
|
+
insertKeyframes('a', 'from { opacity: 0 } to { opacity: 1 }')
|
|
69
|
+
insertKeyframes('b', '100% { opacity: 0 }')
|
|
70
|
+
|
|
71
|
+
assert.equal(document.adoptedStyleSheets.length, 1)
|
|
72
|
+
assert.deepEqual(document.adoptedStyleSheets[0].rules, [
|
|
73
|
+
'@keyframes a {from { opacity: 0 } to { opacity: 1 }}',
|
|
74
|
+
'@keyframes b {100% { opacity: 0 }}'
|
|
75
|
+
])
|
|
76
|
+
assert.deepEqual(link.rules, [])
|
|
77
|
+
assert.deepEqual(document.appended, [])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('folha adotada some da lista (outro código reatribuiu): volta pra lista', async () => {
|
|
81
|
+
const insertKeyframes = await freshInsertKeyframes()
|
|
82
|
+
globalThis.document = fakeDocument({adopted: true})
|
|
83
|
+
globalThis.CSSStyleSheet = FakeConstructedSheet
|
|
84
|
+
insertKeyframes('a', '100% { opacity: 0 }')
|
|
85
|
+
const sheet = document.adoptedStyleSheets[0]
|
|
86
|
+
const other = new FakeConstructedSheet()
|
|
87
|
+
document.adoptedStyleSheets = [other]
|
|
88
|
+
|
|
89
|
+
insertKeyframes('b', '100% { opacity: 1 }')
|
|
90
|
+
|
|
91
|
+
assert.deepEqual(document.adoptedStyleSheets, [other, sheet])
|
|
92
|
+
assert.equal(sheet.rules.length, 2)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('sem folha construída: insertRule numa folha do site, preferindo <link>', async () => {
|
|
96
|
+
const insertKeyframes = await freshInsertKeyframes()
|
|
97
|
+
const crossOrigin = fakeSheet({readable: false})
|
|
98
|
+
const print = fakeSheet({media: 'print'})
|
|
99
|
+
const style = fakeSheet({tagName: 'STYLE'})
|
|
100
|
+
const link = fakeSheet()
|
|
101
|
+
globalThis.document = fakeDocument({styleSheets: [crossOrigin, print, style, link], nonce: 'n'})
|
|
102
|
+
|
|
103
|
+
insertKeyframes('a', '100% { opacity: 0 }')
|
|
104
|
+
insertKeyframes('a', '100% { opacity: 0 }')
|
|
105
|
+
|
|
106
|
+
assert.deepEqual(link.rules, ['@keyframes a {100% { opacity: 0 }}'])
|
|
107
|
+
assert.deepEqual(style.rules, [])
|
|
108
|
+
assert.deepEqual(print.rules, [])
|
|
109
|
+
assert.deepEqual(document.appended, [])
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('sem folha construída e sem <link>: usa um <style> do site que dê pra ler', async () => {
|
|
113
|
+
const insertKeyframes = await freshInsertKeyframes()
|
|
114
|
+
const style = fakeSheet({tagName: 'STYLE'})
|
|
115
|
+
globalThis.document = fakeDocument({styleSheets: [fakeSheet({readable: false}), style]})
|
|
116
|
+
|
|
117
|
+
insertKeyframes('a', '100% { opacity: 0 }')
|
|
118
|
+
|
|
119
|
+
assert.deepEqual(style.rules, ['@keyframes a {100% { opacity: 0 }}'])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('último recurso: <style> com o nonce do <meta>, uma vez por nome', async () => {
|
|
123
|
+
const insertKeyframes = await freshInsertKeyframes()
|
|
124
|
+
globalThis.document = fakeDocument({styleSheets: [fakeSheet({readable: false})], nonce: 'abc'})
|
|
125
|
+
|
|
126
|
+
insertKeyframes('a', '100% { opacity: 0 }')
|
|
127
|
+
insertKeyframes('a', '100% { opacity: 0 }')
|
|
128
|
+
|
|
129
|
+
assert.deepEqual(document.appended, [{
|
|
130
|
+
tag: 'style',
|
|
131
|
+
id: 'startlibs-keyframes-a',
|
|
132
|
+
nonce: 'abc',
|
|
133
|
+
textContent: '@keyframes a {100% { opacity: 0 }}'
|
|
134
|
+
}])
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('último recurso sem <meta> de nonce: <style> sem nonce', async () => {
|
|
138
|
+
const insertKeyframes = await freshInsertKeyframes()
|
|
139
|
+
globalThis.document = fakeDocument()
|
|
140
|
+
|
|
141
|
+
insertKeyframes('a', '100% { opacity: 0 }')
|
|
142
|
+
|
|
143
|
+
assert.equal('nonce' in document.appended[0], false)
|
|
144
|
+
})
|