mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Add support for defining simple custom utilities in CSS via @utility (#14044)
This PR allows you to add custom utilities to your project via the new
`@utility` rule.
For example, given the following:
```css
@utility text-trim {
text-box-trim: both;
text-box-edge: cap alphabetic;
}
```
A new `text-trim` utility is available and can be used directly or with
variants:
```html
<div class="text-trim">...</div>
<div class="hover:text-trim">...</div>
<div class="lg:dark:text-trim">...</div>
```
If a utility is defined more than one time the latest definition will be
used:
```css
@utility text-trim {
text-box-trim: both;
text-box-edge: cap alphabetic;
}
@utility text-trim {
text-box-trim: both;
text-box-edge: cap ideographic;
}
```
Then using `text-trim` will produce the following CSS:
```css
.text-trim {
text-box-trim: both;
text-box-edge: cap ideographic;
}
```
You may also override specific existing utilities with this — for
example to add a `text-rendering` property to the `text-sm` utility:
```css
@utility text-sm {
font-size: var(--font-size-sm, 0.875rem);
line-height: var(--font-size-sm--line-height, 1.25rem);
text-rendering: optimizeLegibility;
}
```
Though it's preferred, should you not need to add properties, to
override the theme instead.
Lastly, utilities with special characters do not need to be escaped like
you would for a class name in a selector:
```css
@utility push-1/2 {
right: 50%;
}
```
We do however explicitly error on certain patterns that we want to
reserve for future use, for example `push-*` and `push-[15px]`.
---------
Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
This commit is contained in:
parent
300524b893
commit
f0e9343b5b
@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
- Add support for basic `addVariant` plugins with new `@plugin` directive ([#13982](https://github.com/tailwindlabs/tailwindcss/pull/13982), [#14008](https://github.com/tailwindlabs/tailwindcss/pull/14008))
|
||||
- Add `@variant` at-rule for defining custom variants in CSS ([#13992](https://github.com/tailwindlabs/tailwindcss/pull/13992), [#14008](https://github.com/tailwindlabs/tailwindcss/pull/14008))
|
||||
- Add `@utility` at-rule for defining custom utilities in CSS ([#14044](https://github.com/tailwindlabs/tailwindcss/pull/14044))
|
||||
|
||||
## [4.0.0-alpha.17] - 2024-07-04
|
||||
|
||||
|
||||
@ -249,6 +249,25 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi
|
||||
base = base.slice(1)
|
||||
}
|
||||
|
||||
// Candidates that start with a dash are the negative versions of another
|
||||
// candidate, e.g. `-mx-4`.
|
||||
if (base[0] === '-') {
|
||||
negative = true
|
||||
base = base.slice(1)
|
||||
}
|
||||
|
||||
// Check for an exact match of a static utility first as long as it does not
|
||||
// look like an arbitrary value.
|
||||
if (designSystem.utilities.has(base, 'static') && !base.includes('[')) {
|
||||
return {
|
||||
kind: 'static',
|
||||
root: base,
|
||||
variants: parsedCandidateVariants,
|
||||
negative,
|
||||
important,
|
||||
}
|
||||
}
|
||||
|
||||
// Figure out the new base and the modifier segment if present.
|
||||
//
|
||||
// E.g.:
|
||||
@ -307,13 +326,6 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi
|
||||
}
|
||||
}
|
||||
|
||||
// Candidates that start with a dash are the negative versions of another
|
||||
// candidate, e.g. `-mx-4`.
|
||||
if (baseWithoutModifier[0] === '-') {
|
||||
negative = true
|
||||
baseWithoutModifier = baseWithoutModifier.slice(1)
|
||||
}
|
||||
|
||||
// The root of the utility, e.g.: `bg-red-500`
|
||||
// ^^
|
||||
let root: string | null = null
|
||||
@ -345,28 +357,16 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi
|
||||
|
||||
// The root of the utility should exist as-is in the utilities map. If not,
|
||||
// it's an invalid utility and we can skip continue parsing.
|
||||
if (!designSystem.utilities.has(root)) return null
|
||||
if (!designSystem.utilities.has(root, 'functional')) return null
|
||||
|
||||
value = baseWithoutModifier.slice(idx + 1)
|
||||
}
|
||||
|
||||
// Not an arbitrary value
|
||||
else {
|
||||
;[root, value] = findRoot(baseWithoutModifier, designSystem.utilities)
|
||||
}
|
||||
|
||||
// If the root is null, but it contains a `/`, then it could be that we are
|
||||
// dealing with a functional utility that contains a modifier but doesn't
|
||||
// contain a value.
|
||||
//
|
||||
// E.g.: `@container/parent`
|
||||
if (root === null && base.includes('/')) {
|
||||
let [rootWithoutModifier, rootModifierSegment = null] = segment(base, '/')
|
||||
|
||||
modifierSegment = rootModifierSegment
|
||||
|
||||
// Try to find the root and value, without the modifier present
|
||||
;[root, value] = findRoot(rootWithoutModifier, designSystem.utilities)
|
||||
;[root, value] = findRoot(baseWithoutModifier, (root: string) => {
|
||||
return designSystem.utilities.has(root, 'functional')
|
||||
})
|
||||
}
|
||||
|
||||
// If there's no root, the candidate isn't a valid class and can be discarded.
|
||||
@ -377,24 +377,6 @@ export function parseCandidate(input: string, designSystem: DesignSystem): Candi
|
||||
// can skip any further parsing.
|
||||
if (value === '') return null
|
||||
|
||||
let kind = designSystem.utilities.kind(root)
|
||||
|
||||
if (kind === 'static') {
|
||||
// Static utilities do not have a value
|
||||
if (value !== null) return null
|
||||
|
||||
// Static utilities do not have a modifier
|
||||
if (modifierSegment !== null) return null
|
||||
|
||||
return {
|
||||
kind: 'static',
|
||||
root,
|
||||
variants: parsedCandidateVariants,
|
||||
negative,
|
||||
important,
|
||||
}
|
||||
}
|
||||
|
||||
let candidate: Candidate = {
|
||||
kind: 'functional',
|
||||
root,
|
||||
@ -560,7 +542,9 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia
|
||||
// - `group-hover/foo/bar`
|
||||
if (additionalModifier) return null
|
||||
|
||||
let [root, value] = findRoot(variantWithoutModifier, designSystem.variants)
|
||||
let [root, value] = findRoot(variantWithoutModifier, (root) => {
|
||||
return designSystem.variants.has(root)
|
||||
})
|
||||
|
||||
// Variant is invalid, therefore the candidate is invalid and we can skip
|
||||
// continue parsing it.
|
||||
@ -629,10 +613,10 @@ export function parseVariant(variant: string, designSystem: DesignSystem): Varia
|
||||
|
||||
function findRoot(
|
||||
input: string,
|
||||
lookup: { has: (input: string) => boolean },
|
||||
exists: (input: string) => boolean,
|
||||
): [string | null, string | null] {
|
||||
// If the lookup has an exact match, then that's the root.
|
||||
if (lookup.has(input)) return [input, null]
|
||||
// If there is an exact match, then that's the root.
|
||||
if (exists(input)) return [input, null]
|
||||
|
||||
// Otherwise test every permutation of the input by iteratively removing
|
||||
// everything after the last dash.
|
||||
@ -640,15 +624,14 @@ function findRoot(
|
||||
if (idx === -1) {
|
||||
// Variants starting with `@` are special because they don't need a `-`
|
||||
// after the `@` (E.g.: `@-lg` should be written as `@lg`).
|
||||
if (input[0] === '@' && lookup.has('@')) {
|
||||
if (input[0] === '@' && exists('@')) {
|
||||
return ['@', input.slice(1)]
|
||||
}
|
||||
|
||||
return [null, null]
|
||||
}
|
||||
|
||||
// Determine the root and value by testing permutations of the incoming input
|
||||
// against the lookup table.
|
||||
// Determine the root and value by testing permutations of the incoming input.
|
||||
//
|
||||
// In case of a candidate like `bg-red-500`, this looks like:
|
||||
//
|
||||
@ -658,7 +641,7 @@ function findRoot(
|
||||
do {
|
||||
let maybeRoot = input.slice(0, idx)
|
||||
|
||||
if (lookup.has(maybeRoot)) {
|
||||
if (exists(maybeRoot)) {
|
||||
return [maybeRoot, input.slice(idx + 1)]
|
||||
}
|
||||
|
||||
|
||||
@ -117,14 +117,20 @@ export function compileAstNodes(rawCandidate: string, designSystem: DesignSystem
|
||||
|
||||
// Handle named utilities
|
||||
else if (candidate.kind === 'static' || candidate.kind === 'functional') {
|
||||
// Safety: At this point it is safe to use TypeScript's non-null assertion
|
||||
// operator because if the `candidate.root` didn't exist, `parseCandidate`
|
||||
// would have returned `null` and we would have returned early resulting
|
||||
// in not hitting this code path.
|
||||
let { compileFn } = designSystem.utilities.get(candidate.root)!
|
||||
let fns = designSystem.utilities.get(candidate.root)
|
||||
|
||||
// Build the node
|
||||
let compiledNodes = compileFn(candidate)
|
||||
let compiledNodes: AstNode[] | undefined
|
||||
|
||||
for (let i = fns.length - 1; i >= 0; i--) {
|
||||
let fn = fns[i]
|
||||
|
||||
if (candidate.kind !== fn.kind) continue
|
||||
|
||||
compiledNodes = fn.compileFn(candidate)
|
||||
if (compiledNodes) break
|
||||
}
|
||||
|
||||
if (compiledNodes === undefined) return null
|
||||
|
||||
nodes = compiledNodes
|
||||
|
||||
@ -17,9 +17,12 @@ import { buildDesignSystem, type DesignSystem } from './design-system'
|
||||
import { Theme } from './theme'
|
||||
import { segment } from './utils/segment'
|
||||
|
||||
const IS_VALID_UTILITY_NAME = /^[a-z][a-zA-Z0-9/%._-]*$/
|
||||
|
||||
type PluginAPI = {
|
||||
addVariant(name: string, variant: string | string[] | CssInJs): void
|
||||
}
|
||||
|
||||
type Plugin = (api: PluginAPI) => void
|
||||
|
||||
type CompileOptions = {
|
||||
@ -52,6 +55,7 @@ export function compile(
|
||||
let theme = new Theme()
|
||||
let plugins: Plugin[] = []
|
||||
let customVariants: ((designSystem: DesignSystem) => void)[] = []
|
||||
let customUtilities: ((designSystem: DesignSystem) => void)[] = []
|
||||
let firstThemeRule: Rule | null = null
|
||||
let keyframesRules: Rule[] = []
|
||||
|
||||
@ -65,6 +69,33 @@ export function compile(
|
||||
return
|
||||
}
|
||||
|
||||
// Collect custom `@utility` at-rules
|
||||
if (node.selector.startsWith('@utility ')) {
|
||||
let name = node.selector.slice(9).trim()
|
||||
|
||||
if (!IS_VALID_UTILITY_NAME.test(name)) {
|
||||
throw new Error(
|
||||
`\`@utility ${name}\` defines an invalid utility name. Utilities should be alphanumeric and start with a lowercase letter.`,
|
||||
)
|
||||
}
|
||||
|
||||
if (node.nodes.length === 0) {
|
||||
throw new Error(
|
||||
`\`@utility ${name}\` is empty. Utilities should include at least one property.`,
|
||||
)
|
||||
}
|
||||
|
||||
customUtilities.push((designSystem) => {
|
||||
designSystem.utilities.static(name, (candidate) => {
|
||||
if (candidate.negative) return
|
||||
return structuredClone(node.nodes)
|
||||
})
|
||||
})
|
||||
|
||||
replaceWith([])
|
||||
return
|
||||
}
|
||||
|
||||
// Register custom variants from `@variant` at-rules
|
||||
if (node.selector.startsWith('@variant ')) {
|
||||
if (parent !== null) {
|
||||
@ -224,6 +255,10 @@ export function compile(
|
||||
customVariant(designSystem)
|
||||
}
|
||||
|
||||
for (let customUtility of customUtilities) {
|
||||
customUtility(designSystem)
|
||||
}
|
||||
|
||||
let api: PluginAPI = {
|
||||
addVariant(name, variant) {
|
||||
// Single selector
|
||||
|
||||
@ -11,18 +11,13 @@ export type ClassEntry = [string, ClassMetadata]
|
||||
export function getClassList(design: DesignSystem): ClassEntry[] {
|
||||
let list: [string, ClassMetadata][] = []
|
||||
|
||||
for (let [utility, fn] of design.utilities.entries()) {
|
||||
if (typeof utility !== 'string') {
|
||||
continue
|
||||
}
|
||||
// Static utilities only work as-is
|
||||
for (let utility of design.utilities.keys('static')) {
|
||||
list.push([utility, { modifiers: [] }])
|
||||
}
|
||||
|
||||
// Static utilities only work as-is
|
||||
if (fn.kind === 'static') {
|
||||
list.push([utility, { modifiers: [] }])
|
||||
continue
|
||||
}
|
||||
|
||||
// Functional utilities have their own list of completions
|
||||
// Functional utilities have their own list of completions
|
||||
for (let utility of design.utilities.keys('functional')) {
|
||||
let completions = design.utilities.getCompletions(utility)
|
||||
|
||||
for (let group of completions) {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { expect, test } from 'vitest'
|
||||
import { compileCss, run } from './test-utils/run'
|
||||
import { describe, expect, test } from 'vitest'
|
||||
import { compile } from '.'
|
||||
import { compileCss, optimizeCss, run } from './test-utils/run'
|
||||
|
||||
const css = String.raw
|
||||
|
||||
@ -14946,3 +14947,206 @@ test('@container', () => {
|
||||
]),
|
||||
).toEqual('')
|
||||
})
|
||||
|
||||
describe('custom utilities', () => {
|
||||
test('custom static utility', () => {
|
||||
let compiled = compile(css`
|
||||
@layer utilities {
|
||||
@tailwind utilities;
|
||||
}
|
||||
|
||||
@theme reference {
|
||||
--breakpoint-lg: 1024px;
|
||||
}
|
||||
|
||||
@utility text-trim {
|
||||
text-box-trim: both;
|
||||
text-box-edge: cap alphabetic;
|
||||
}
|
||||
`).build(['text-trim', 'lg:text-trim'])
|
||||
|
||||
expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`
|
||||
"@layer utilities {
|
||||
.text-trim {
|
||||
text-box-trim: both;
|
||||
text-box-edge: cap alphabetic;
|
||||
}
|
||||
|
||||
@media (width >= 1024px) {
|
||||
.lg\\:text-trim {
|
||||
text-box-trim: both;
|
||||
text-box-edge: cap alphabetic;
|
||||
}
|
||||
}
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('The later version of a static utility is used', () => {
|
||||
let compiled = compile(css`
|
||||
@layer utilities {
|
||||
@tailwind utilities;
|
||||
}
|
||||
|
||||
@utility really-round {
|
||||
--custom-prop: hi;
|
||||
border-radius: 50rem;
|
||||
}
|
||||
|
||||
@utility really-round {
|
||||
border-radius: 30rem;
|
||||
}
|
||||
`).build(['really-round'])
|
||||
|
||||
expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`
|
||||
"@layer utilities {
|
||||
.really-round {
|
||||
border-radius: 30rem;
|
||||
}
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('custom utilities support some special chracters', () => {
|
||||
let compiled = compile(css`
|
||||
@layer utilities {
|
||||
@tailwind utilities;
|
||||
}
|
||||
|
||||
@utility push-1/2 {
|
||||
right: 50%;
|
||||
}
|
||||
|
||||
@utility push-50% {
|
||||
right: 50%;
|
||||
}
|
||||
`).build(['push-1/2', 'push-50%'])
|
||||
|
||||
expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`
|
||||
"@layer utilities {
|
||||
.push-1\\/2, .push-50\\% {
|
||||
right: 50%;
|
||||
}
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('can override specific versions of a functional utility with a static utility', () => {
|
||||
let compiled = compile(css`
|
||||
@layer utilities {
|
||||
@tailwind utilities;
|
||||
}
|
||||
|
||||
@theme reference {
|
||||
--font-size-sm: 0.875rem;
|
||||
--font-size-sm--line-height: 1.25rem;
|
||||
}
|
||||
|
||||
@utility text-sm {
|
||||
font-size: var(--font-size-sm, 0.875rem);
|
||||
line-height: var(--font-size-sm--line-height, 1.25rem);
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
`).build(['text-sm'])
|
||||
|
||||
expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`
|
||||
"@layer utilities {
|
||||
.text-sm {
|
||||
font-size: var(--font-size-sm, .875rem);
|
||||
line-height: var(--font-size-sm--line-height, 1.25rem);
|
||||
text-rendering: optimizelegibility;
|
||||
}
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('can override the default value of a functional utility', () => {
|
||||
let compiled = compile(css`
|
||||
@layer utilities {
|
||||
@tailwind utilities;
|
||||
}
|
||||
|
||||
@theme reference {
|
||||
--radius-xl: 16px;
|
||||
}
|
||||
|
||||
@utility rounded {
|
||||
border-radius: 50rem;
|
||||
}
|
||||
`).build(['rounded', 'rounded-xl', 'rounded-[33px]'])
|
||||
|
||||
expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`
|
||||
"@layer utilities {
|
||||
.rounded {
|
||||
border-radius: 50rem;
|
||||
}
|
||||
|
||||
.rounded-\\[33px\\] {
|
||||
border-radius: 33px;
|
||||
}
|
||||
|
||||
.rounded-xl {
|
||||
border-radius: var(--radius-xl, 16px);
|
||||
}
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('custom utilities are sorted by used properties', () => {
|
||||
let compiled = compile(css`
|
||||
@layer utilities {
|
||||
@tailwind utilities;
|
||||
}
|
||||
|
||||
@utility push-left {
|
||||
right: 100%;
|
||||
}
|
||||
`).build(['top-[100px]', 'push-left', 'right-[100px]', 'bottom-[100px]'])
|
||||
|
||||
expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(`
|
||||
"@layer utilities {
|
||||
.top-\\[100px\\] {
|
||||
top: 100px;
|
||||
}
|
||||
|
||||
.push-left {
|
||||
right: 100%;
|
||||
}
|
||||
|
||||
.right-\\[100px\\] {
|
||||
right: 100px;
|
||||
}
|
||||
|
||||
.bottom-\\[100px\\] {
|
||||
bottom: 100px;
|
||||
}
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('custom utilities must use a valid name definitions ', () => {
|
||||
expect(() =>
|
||||
compile(css`
|
||||
@utility push-* {
|
||||
right: 100%;
|
||||
}
|
||||
`),
|
||||
).toThrowError(/should be alphanumeric/)
|
||||
|
||||
expect(() =>
|
||||
compile(css`
|
||||
@utility ~push {
|
||||
right: 100%;
|
||||
}
|
||||
`),
|
||||
).toThrowError(/should be alphanumeric/)
|
||||
|
||||
expect(() =>
|
||||
compile(css`
|
||||
@utility @push {
|
||||
right: 100%;
|
||||
}
|
||||
`),
|
||||
).toThrowError(/should be alphanumeric/)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import { decl, rule, type AstNode, type Rule } from './ast'
|
||||
import type { Candidate, CandidateModifier, NamedUtilityValue } from './candidate'
|
||||
import type { ColorThemeKey, Theme, ThemeKey } from './theme'
|
||||
import { DefaultMap } from './utils/default-map'
|
||||
import { inferDataType } from './utils/infer-data-type'
|
||||
import { replaceShadowColors } from './utils/replace-shadow-colors'
|
||||
import { segment } from './utils/segment'
|
||||
|
||||
const ARBITRARY_VARIANT = Symbol('ARBITRARY_VARIANT')
|
||||
|
||||
type CompileFn<T extends Candidate['kind']> = (
|
||||
value: Extract<Candidate, { kind: T }>,
|
||||
) => AstNode[] | undefined
|
||||
@ -29,38 +28,35 @@ type SuggestionDefinition =
|
||||
}
|
||||
|
||||
export class Utilities {
|
||||
private utilities = new Map<
|
||||
string | symbol,
|
||||
private arbitraryFn!: CompileFn<'arbitrary'>
|
||||
private utilities = new DefaultMap<
|
||||
string,
|
||||
{
|
||||
kind: Candidate['kind']
|
||||
kind: 'static' | 'functional'
|
||||
compileFn: CompileFn<any>
|
||||
}
|
||||
>()
|
||||
}[]
|
||||
>(() => [])
|
||||
|
||||
private completions = new Map<string, () => SuggestionGroup[]>()
|
||||
|
||||
static(name: string, compileFn: CompileFn<'static'>) {
|
||||
this.set(name, { kind: 'static', compileFn: compileFn })
|
||||
this.utilities.get(name).push({ kind: 'static', compileFn })
|
||||
}
|
||||
|
||||
functional(name: string, compileFn: CompileFn<'functional'>) {
|
||||
this.set(name, { kind: 'functional', compileFn: compileFn })
|
||||
this.utilities.get(name).push({ kind: 'functional', compileFn })
|
||||
}
|
||||
|
||||
arbitrary(compileFn: CompileFn<'arbitrary'>) {
|
||||
this.set(ARBITRARY_VARIANT, { kind: 'arbitrary', compileFn: compileFn })
|
||||
this.arbitraryFn = compileFn
|
||||
}
|
||||
|
||||
has(name: string) {
|
||||
return this.utilities.has(name)
|
||||
has(name: string, kind: 'static' | 'functional') {
|
||||
return this.utilities.has(name) && this.utilities.get(name).some((fn) => fn.kind === kind)
|
||||
}
|
||||
|
||||
get(name: string | symbol) {
|
||||
return this.utilities.get(name)
|
||||
}
|
||||
|
||||
kind(name: string) {
|
||||
return this.utilities.get(name)!.kind
|
||||
get(name: string) {
|
||||
return this.utilities.has(name) ? this.utilities.get(name) : []
|
||||
}
|
||||
|
||||
getCompletions(name: string): SuggestionGroup[] {
|
||||
@ -73,35 +69,23 @@ export class Utilities {
|
||||
this.completions.set(name, groups)
|
||||
}
|
||||
|
||||
keys() {
|
||||
return this.utilities.keys()
|
||||
}
|
||||
keys(kind: 'static' | 'functional') {
|
||||
let keys: string[] = []
|
||||
|
||||
entries() {
|
||||
return this.utilities.entries()
|
||||
}
|
||||
|
||||
getArbitrary() {
|
||||
return this.get(ARBITRARY_VARIANT)!.compileFn
|
||||
}
|
||||
|
||||
private set<T extends Candidate['kind']>(
|
||||
name: string | symbol,
|
||||
{ kind, compileFn }: { kind: T; compileFn: CompileFn<T> },
|
||||
) {
|
||||
// In test mode, throw an error if we accidentally override another utility
|
||||
// by mistake when implementing a new utility that shares the same root
|
||||
// without realizing the definitions need to be merged.
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
if (this.utilities.has(name)) {
|
||||
throw new Error(`Duplicate utility prefix [${name.toString()}]`)
|
||||
for (let [key, fns] of this.utilities.entries()) {
|
||||
for (let fn of fns) {
|
||||
if (fn.kind === kind) {
|
||||
keys.push(key)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.utilities.set(name, {
|
||||
kind,
|
||||
compileFn: compileFn,
|
||||
})
|
||||
return keys
|
||||
}
|
||||
|
||||
getArbitrary() {
|
||||
return this.arbitraryFn
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user