simplestyle-js 3.3.0 → 3.4.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.
Files changed (67) hide show
  1. package/.coveralls.yml +2 -0
  2. package/.editorconfig +7 -0
  3. package/.prettierrc +7 -0
  4. package/.tool-versions +3 -0
  5. package/.travis.yml +8 -0
  6. package/.vscode/launch.json +36 -0
  7. package/.vscode/settings.json +3 -0
  8. package/CHANGELOG.md +273 -0
  9. package/README.md +4 -1
  10. package/eslint.config.js +3 -0
  11. package/package.json +134 -34
  12. package/setup.sh +4 -0
  13. package/src/createStyles.ts +251 -0
  14. package/src/generateClassName.ts +43 -0
  15. package/src/index.ts +6 -0
  16. package/src/numToAlpha.ts +5 -0
  17. package/src/plugins.ts +11 -0
  18. package/src/react/index.ts +1 -0
  19. package/src/react/useCreateStyles.ts +58 -0
  20. package/src/types.ts +7 -0
  21. package/src/util/deepEqual.ts +20 -0
  22. package/src/util/index.ts +1 -0
  23. package/test/createStyles.spec.ts +330 -0
  24. package/test/deepEqual.spec.ts +97 -0
  25. package/test/generateClassName.spec.ts +48 -0
  26. package/test/keyframes.spec.ts +19 -0
  27. package/test/plugins.spec.ts +43 -0
  28. package/test/react/useCreateStyles.spec.tsx +65 -0
  29. package/test/updateStyles.spec.ts +41 -0
  30. package/tsconfig.build.json +8 -0
  31. package/tsconfig.json +37 -0
  32. package/vite.config.ts +9 -0
  33. package/createStyles.d.ts +0 -36
  34. package/createStyles.js +0 -192
  35. package/createStyles.js.map +0 -7
  36. package/generateClassName.d.ts +0 -3
  37. package/generateClassName.js +0 -47
  38. package/generateClassName.js.map +0 -7
  39. package/index.cjs.js +0 -285
  40. package/index.d.ts +0 -6
  41. package/index.js +0 -11
  42. package/index.js.map +0 -7
  43. package/numToAlpha.d.ts +0 -1
  44. package/numToAlpha.js +0 -8
  45. package/numToAlpha.js.map +0 -7
  46. package/plugins.d.ts +0 -3
  47. package/plugins.js +0 -12
  48. package/plugins.js.map +0 -7
  49. package/react/index.cjs.js +0 -299
  50. package/react/index.d.ts +0 -1
  51. package/react/index.js +0 -2
  52. package/react/index.js.map +0 -7
  53. package/react/package.json +0 -6
  54. package/react/useCreateStyles.d.ts +0 -5
  55. package/react/useCreateStyles.js +0 -40
  56. package/react/useCreateStyles.js.map +0 -7
  57. package/types.d.ts +0 -7
  58. package/types.js +0 -1
  59. package/types.js.map +0 -7
  60. package/util/deepEqual.d.ts +0 -1
  61. package/util/deepEqual.js +0 -22
  62. package/util/deepEqual.js.map +0 -7
  63. package/util/index.cjs.js +0 -46
  64. package/util/index.d.ts +0 -1
  65. package/util/index.js +0 -2
  66. package/util/index.js.map +0 -7
  67. package/util/package.json +0 -6
@@ -0,0 +1,251 @@
1
+ import { Properties } from 'csstype';
2
+ import merge from 'deepmerge';
3
+
4
+ import { generateClassName } from './generateClassName.js';
5
+ import { getPosthooks } from './plugins.js';
6
+ import { SimpleStyleRules } from './types.js';
7
+
8
+ export type CreateStylesOptions = Partial<{
9
+ /**
10
+ * If true, automatically renders generated styles
11
+ * to the DOM in an injected <style /> tag
12
+ */
13
+ flush: boolean;
14
+
15
+ /**
16
+ * If set, along with flush: true,
17
+ * will render the injected <style /> after this element
18
+ */
19
+ insertAfter?: HTMLElement;
20
+ /**
21
+ * If set, along with flush: true,
22
+ * will render the injects <style /> before this element
23
+ */
24
+ insertBefore?: HTMLElement;
25
+ }>;
26
+
27
+ function isNestedSelector(r: string): boolean {
28
+ return /&/g.test(r);
29
+ }
30
+
31
+ function isMedia(r: string): boolean {
32
+ return r.toLowerCase().startsWith('@media');
33
+ }
34
+
35
+ function formatCSSRuleName(rule: string): string {
36
+ return rule.replaceAll(/([A-Z])/g, p1 => `-${p1.toLowerCase()}`);
37
+ }
38
+
39
+ function formatCSSRules(cssRules: Properties): string {
40
+ return Object.entries(cssRules).reduce(
41
+ (prev, [cssProp, cssVal]) => `${prev}${formatCSSRuleName(cssProp)}:${String(cssVal)};`,
42
+ '',
43
+ );
44
+ }
45
+
46
+ function execCreateStyles<T extends SimpleStyleRules, K extends keyof T, O extends Record<K, string>>(
47
+ rules: T,
48
+ options: CreateStylesOptions,
49
+ parentSelector: string | null,
50
+ noGenerateClassName = false,
51
+ ): { classes: O; sheetBuffer: string; mediaQueriesBuffer: string } {
52
+ const out = {} as O;
53
+ let sheetBuffer = '';
54
+ let mediaQueriesBuffer = '';
55
+ const styleEntries = Object.entries(rules);
56
+ let ruleWriteOpen = false;
57
+ const guardCloseRuleWrite = () => {
58
+ if (ruleWriteOpen) sheetBuffer += '}';
59
+ ruleWriteOpen = false;
60
+ };
61
+ for (const [classNameOrCSSRule, classNameRules] of styleEntries) {
62
+ // if the classNameRules is a string, we are dealing with a display: none; type rule
63
+ if (isMedia(classNameOrCSSRule)) {
64
+ if (typeof classNameRules !== 'object')
65
+ throw new Error('Unable to map @media query because rules / props are an invalid type');
66
+ guardCloseRuleWrite();
67
+ mediaQueriesBuffer += `${classNameOrCSSRule}{`;
68
+ const { mediaQueriesBuffer: mediaQueriesOutput, sheetBuffer: regularOutput } = execCreateStyles(
69
+ classNameRules as T,
70
+ options,
71
+ parentSelector,
72
+ );
73
+ mediaQueriesBuffer += regularOutput;
74
+ mediaQueriesBuffer += '}';
75
+ mediaQueriesBuffer += mediaQueriesOutput;
76
+ } else if (isNestedSelector(classNameOrCSSRule)) {
77
+ if (!parentSelector) throw new Error('Unable to generate nested rule because parentSelector is missing');
78
+ guardCloseRuleWrite();
79
+ // format of { '& > span': { display: 'none' } } (or further nesting)
80
+ const replaced = classNameOrCSSRule.replaceAll('&', parentSelector);
81
+ for (const selector of replaced.split(/,\s*/)) {
82
+ const { mediaQueriesBuffer: mediaQueriesOutput, sheetBuffer: regularOutput } = execCreateStyles(
83
+ classNameRules as T,
84
+ options,
85
+ selector,
86
+ );
87
+ sheetBuffer += regularOutput;
88
+ mediaQueriesBuffer += mediaQueriesOutput;
89
+ }
90
+ } else if (!parentSelector && typeof classNameRules === 'object') {
91
+ guardCloseRuleWrite();
92
+ const generated = noGenerateClassName ? classNameOrCSSRule : generateClassName(classNameOrCSSRule);
93
+ // @ts-expect-error - yes, we can index this object here, so be quiet
94
+ out[classNameOrCSSRule] = generated;
95
+ const generatedSelector = `${noGenerateClassName ? '' : '.'}${generated}`;
96
+ const { mediaQueriesBuffer: mediaQueriesOutput, sheetBuffer: regularOutput } = execCreateStyles(
97
+ classNameRules as T,
98
+ options,
99
+ generatedSelector,
100
+ );
101
+ sheetBuffer += regularOutput;
102
+ mediaQueriesBuffer += mediaQueriesOutput;
103
+ } else {
104
+ if (!parentSelector) throw new Error('Unable to write css props because parent selector is null');
105
+ if (ruleWriteOpen) {
106
+ sheetBuffer += formatCSSRules({ [classNameOrCSSRule]: classNameRules });
107
+ } else {
108
+ sheetBuffer += `${parentSelector}{${formatCSSRules({ [classNameOrCSSRule]: classNameRules })}`;
109
+ ruleWriteOpen = true;
110
+ }
111
+ }
112
+ }
113
+ guardCloseRuleWrite();
114
+ return {
115
+ classes: out,
116
+ sheetBuffer,
117
+ mediaQueriesBuffer,
118
+ };
119
+ }
120
+
121
+ function replaceBackReferences<O extends Record<string, string>>(out: O, sheetContents: string): string {
122
+ let outputSheetContents = sheetContents;
123
+ const toReplace: string[] = [];
124
+ const toReplaceRegex = /\$\w([a-zA-Z0-9_-]+)?/gm;
125
+ let matches = toReplaceRegex.exec(outputSheetContents);
126
+ while (matches) {
127
+ toReplace.push(matches[0].valueOf());
128
+ matches = toReplaceRegex.exec(outputSheetContents);
129
+ }
130
+ for (const r of toReplace) {
131
+ outputSheetContents = outputSheetContents.replace(r, `.${out[r.slice(1)] ?? ''}`);
132
+ }
133
+ return getPosthooks().reduce((prev, hook) => hook(prev), outputSheetContents);
134
+ }
135
+
136
+ function createSheet(sheetContents: string) {
137
+ const doc = globalThis.document as Partial<typeof globalThis.document> | null | undefined;
138
+ if (doc === undefined) return null;
139
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
140
+ if (typeof doc?.head?.appendChild !== 'function' || typeof doc.createElement !== 'function') return null;
141
+ const styleTag = doc.createElement('style');
142
+ styleTag.innerHTML = sheetContents;
143
+ return styleTag;
144
+ }
145
+
146
+ function flushSheetContents(sheetContents: string, options?: CreateStylesOptions) {
147
+ // In case we're in come weird test environment that doesn't support JSDom
148
+ const styleTag = createSheet(sheetContents);
149
+ if (styleTag) {
150
+ if (options?.insertAfter && options.insertBefore) {
151
+ throw new Error('Both insertAfter and insertBefore were provided. Please choose only one.');
152
+ }
153
+ if (options?.insertAfter?.after) options.insertAfter.after(styleTag as Node);
154
+ else if (options?.insertBefore?.before) options.insertBefore.before(styleTag as Node);
155
+ else document.head.append(styleTag);
156
+ }
157
+ return styleTag;
158
+ }
159
+
160
+ function coerceCreateStylesOptions(options?: CreateStylesOptions): CreateStylesOptions {
161
+ return {
162
+ flush: options && typeof options.flush === 'boolean' ? options.flush : true,
163
+ };
164
+ }
165
+
166
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
167
+ export function rawStyles<T extends SimpleStyleRules, K extends keyof T, O extends Record<K, string>>(
168
+ rules: T,
169
+ options?: Partial<CreateStylesOptions>,
170
+ ) {
171
+ const coerced = coerceCreateStylesOptions(options);
172
+ const { sheetBuffer: sheetContents, mediaQueriesBuffer: mediaQueriesContents } = execCreateStyles(
173
+ rules,
174
+ coerced,
175
+ null,
176
+ true,
177
+ );
178
+
179
+ const mergedContents = `${sheetContents}${mediaQueriesContents}`;
180
+
181
+ if (coerced.flush) flushSheetContents(mergedContents, options);
182
+ return mergedContents;
183
+ }
184
+
185
+ export function keyframes<T extends Record<string, Properties>>(
186
+ frames: T,
187
+ options?: CreateStylesOptions,
188
+ ): [string, string] {
189
+ const coerced = coerceCreateStylesOptions(options);
190
+ const keyframeName = generateClassName('keyframes_');
191
+ const { sheetBuffer: keyframesContents } = execCreateStyles(frames, coerced, null, true);
192
+ const sheetContents = `@keyframes ${keyframeName}{${keyframesContents}}`;
193
+ if (coerced.flush) flushSheetContents(sheetContents);
194
+ return [keyframeName, sheetContents];
195
+ }
196
+
197
+ export default function createStyles<T extends SimpleStyleRules, K extends keyof T, O extends Record<K, string>>(
198
+ rules: T,
199
+ options?: Partial<CreateStylesOptions>,
200
+ ) {
201
+ const coerced = coerceCreateStylesOptions(options);
202
+ const {
203
+ classes: out,
204
+ sheetBuffer: sheetContents,
205
+ mediaQueriesBuffer: mediaQueriesContents,
206
+ } = execCreateStyles(rules, coerced, null);
207
+
208
+ const mergedContents = `${sheetContents}${mediaQueriesContents}`;
209
+
210
+ const replacedSheetContents = replaceBackReferences(out, mergedContents);
211
+
212
+ let sheet: ReturnType<typeof flushSheetContents> = null;
213
+
214
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
215
+ const updateSheet = <T2 extends SimpleStyleRules, K2 extends keyof T2, O2 extends Record<K2, string>>(
216
+ updatedRules: Partial<T2>,
217
+ ) => {
218
+ if ((options?.flush && sheet) || !options?.flush) {
219
+ // We prefer the first set, and then we shallow merge
220
+ const {
221
+ classes: updatedOut,
222
+ sheetBuffer: updatedSheetContents,
223
+ mediaQueriesBuffer: updatedMediaQueriesContents,
224
+ } = execCreateStyles(merge(rules, updatedRules), { flush: false }, null);
225
+
226
+ const updatedMergedContents = `${updatedSheetContents}${updatedMediaQueriesContents}`;
227
+
228
+ const updatedReplacedSheetContents = replaceBackReferences(out, updatedMergedContents);
229
+ if (sheet) sheet.innerHTML = updatedReplacedSheetContents;
230
+ return { classes: updatedOut, stylesheet: updatedSheetContents } as {
231
+ classes: typeof updatedOut;
232
+ stylesheet: string;
233
+ };
234
+ }
235
+ return null;
236
+ };
237
+
238
+ if (coerced.flush) sheet = flushSheetContents(replacedSheetContents, options);
239
+ // Need this TS cast to get solid code assist from the consumption-side
240
+ return {
241
+ classes: out as unknown,
242
+ stylesheet: replacedSheetContents,
243
+ updateSheet,
244
+ } as {
245
+ classes: O;
246
+ stylesheet: string;
247
+ updateSheet: typeof updateSheet;
248
+ };
249
+ }
250
+
251
+ export type CreateStylesArgs = Parameters<typeof createStyles>;
@@ -0,0 +1,43 @@
1
+ import numToAlpha from './numToAlpha.js';
2
+
3
+ let inc = Date.now();
4
+
5
+ export function setSeed(seed: number | null): void {
6
+ if (seed === null) {
7
+ inc = Date.now();
8
+ return;
9
+ }
10
+ if (typeof seed !== 'number') throw new Error('Unable to setSeed as provided seed was not a valid number');
11
+ if (seed === Number.MAX_SAFE_INTEGER)
12
+ throw new Error('Unable to setSeed because the seed was already the maximum safe JavaScript number allowed');
13
+ if (seed === Number.POSITIVE_INFINITY || seed === Number.NEGATIVE_INFINITY)
14
+ throw new Error('Unable to setSeed. Positive or negative infinity is not allowed');
15
+ if (seed < 0) throw new Error('Unable to setSeed. Seed must be a number >= 0');
16
+ inc = seed;
17
+ }
18
+
19
+ const numPairsRegex = /(\d{1,2})/g;
20
+
21
+ export function getUniqueSuffix(): string {
22
+ const numPairs: string[] = [];
23
+ const incStr = inc.toString();
24
+ let result = numPairsRegex.exec(incStr);
25
+ while (result) {
26
+ numPairs.push(result[0]);
27
+ result = numPairsRegex.exec(incStr);
28
+ }
29
+ let out = '_';
30
+ for (const pair of numPairs) {
31
+ const val = +pair;
32
+ if (val > 25) {
33
+ const [first, second] = pair.split('');
34
+ out += `${numToAlpha(Number(first))}${numToAlpha(Number(second))}`;
35
+ } else out += numToAlpha(val);
36
+ }
37
+ inc += 1;
38
+ return out;
39
+ }
40
+
41
+ export function generateClassName(c: string): string {
42
+ return `${c}${getUniqueSuffix()}`;
43
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export type { CreateStylesArgs, CreateStylesOptions } from './createStyles.js';
2
+ export { default as createStyles, keyframes, rawStyles } from './createStyles.js';
3
+ export { setSeed } from './generateClassName.js';
4
+ export type { PosthookPlugin } from './plugins.js';
5
+ export { registerPosthook } from './plugins.js';
6
+ export type { SimpleStyleRules } from './types.js';
@@ -0,0 +1,5 @@
1
+ const alphas = 'abcdefghijklmnopqrstuvwxyz'.split('');
2
+
3
+ export default function numToAlpha(num: number): string {
4
+ return String(alphas[num]);
5
+ }
package/src/plugins.ts ADDED
@@ -0,0 +1,11 @@
1
+ export type PosthookPlugin = (sheetContents: string) => string;
2
+
3
+ const posthooks: PosthookPlugin[] = [];
4
+
5
+ export function getPosthooks(): PosthookPlugin[] {
6
+ return posthooks;
7
+ }
8
+
9
+ export function registerPosthook(posthook: PosthookPlugin) {
10
+ posthooks.push(posthook);
11
+ }
@@ -0,0 +1 @@
1
+ export * from './useCreateStyles.js';
@@ -0,0 +1,58 @@
1
+ import { useEffect, useMemo, useRef, useState } from 'react';
2
+
3
+ import createStyles, { CreateStylesOptions } from '../createStyles.js';
4
+ import { SimpleStyleRules } from '../types.js';
5
+ import { deepEqual } from '../util/index.js';
6
+
7
+ export function useCreateStyles<T extends SimpleStyleRules, K extends keyof T, O extends Record<K, string>>(
8
+ rules: T,
9
+ options?: Partial<Omit<CreateStylesOptions, 'flush'>>,
10
+ ) {
11
+ // cache rules to compare later
12
+ const [cachedRules, setCachedRules] = useState(() => rules);
13
+
14
+ // memoize options but keep them live
15
+ const cachedOptions = useMemo(() => ({ ...options }) as Partial<Omit<CreateStylesOptions, 'flush'>>, [options]);
16
+
17
+ const didFirstWriteRef = useRef(false);
18
+ const styleTagRef = useRef(typeof document === 'undefined' ? null : document.createElement('style'));
19
+
20
+ // initialize styles
21
+ const [styleState, setStyleState] = useState(() => createStyles<T, K, O>(rules, { ...cachedOptions, flush: false }));
22
+
23
+ const { classes, stylesheet, updateSheet } = styleState;
24
+
25
+ // mount/unmount style tag
26
+ useEffect(() => {
27
+ if (!styleTagRef.current) return;
28
+ const { current: s } = styleTagRef;
29
+ document.head.append(s);
30
+ return () => {
31
+ s.remove();
32
+ };
33
+ }, []);
34
+
35
+ // update stylesheet when rules change
36
+ useEffect(() => {
37
+ if (!styleTagRef.current) return;
38
+
39
+ if (!didFirstWriteRef.current) {
40
+ didFirstWriteRef.current = true;
41
+ styleTagRef.current.innerHTML = stylesheet;
42
+ return;
43
+ }
44
+
45
+ if (!deepEqual(rules, cachedRules)) {
46
+ setCachedRules(rules);
47
+ const updated = updateSheet(rules);
48
+ if (updated) {
49
+ styleTagRef.current.innerHTML = updated.stylesheet;
50
+ // use the fresh updateSheet from updated
51
+ // @ts-expect-error - this cast is safe and is only for us, internally, anyways
52
+ setStyleState({ ...updated, updateSheet });
53
+ }
54
+ }
55
+ }, [cachedRules, rules, stylesheet, updateSheet]); // only depend on rules + updater
56
+
57
+ return classes;
58
+ }
package/src/types.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { Properties } from 'csstype';
2
+
3
+ export type SimpleStyleRules = {
4
+ [key: string]: Properties | SimpleStyleRules;
5
+ };
6
+
7
+ export type RenderableSimpleStyleRules = SimpleStyleRules & Record<string, Properties[]>;
@@ -0,0 +1,20 @@
1
+ export function deepEqual<
2
+ O1 extends Record<string | number | symbol, any>,
3
+ O2 extends Record<string | number | symbol, any>,
4
+ >(o1: O1, o2: O2): boolean {
5
+ // We'll sort the keys, just in case the user kept things the same but the object is all wonky, order-wise
6
+ const o1Keys = Object.keys(o1).sort();
7
+ const o2Keys = Object.keys(o2).sort();
8
+ if (o1Keys.length !== o2Keys.length) return false;
9
+ if (o1Keys.some((key, i) => o2Keys[i] !== key)) return false;
10
+ // Okay, the keys SHOULD be the same
11
+ // so we need to test their values, recursively, to verify equality
12
+ return o1Keys.reduce<boolean>((prev, key) => {
13
+ if (!prev) return prev; // we've already failed equality checks here
14
+ if (!(key in o2)) return false;
15
+ if (typeof o1[key] !== 'object') {
16
+ return o1[key] === o2[key];
17
+ }
18
+ return deepEqual(o1[key], o2[key]);
19
+ }, true);
20
+ }
@@ -0,0 +1 @@
1
+ export * from './deepEqual.js';