From cb52ece4d3f4e1a3f15b72142c117be717b9e116 Mon Sep 17 00:00:00 2001 From: littensy <56808540+littensy@users.noreply.github.com> Date: Tue, 23 Sep 2025 16:50:39 -0700 Subject: [PATCH] refactor: rewrite untracked, update alien-signals --- benches/propagate.luau | 20 +++ package.json | 2 +- packages/charm-sync/src/types.luau | 16 +- .../charm/src/__tests__/core/effect.spec.luau | 21 +++ packages/charm/src/__tests__/listen.spec.luau | 21 +++ .../charm/src/__tests__/observe.spec.luau | 32 ++++ .../charm/src/__tests__/untracked.spec.luau | 22 +++ packages/charm/src/batched.luau | 5 +- packages/charm/src/core.luau | 40 ++--- packages/charm/src/init.luau | 2 + packages/charm/src/observe.luau | 6 +- packages/charm/src/system.luau | 137 +++++++++--------- packages/charm/src/untracked.luau | 17 ++- rokit.toml | 10 +- selene.toml | 2 +- 15 files changed, 239 insertions(+), 114 deletions(-) diff --git a/benches/propagate.luau b/benches/propagate.luau index 9d40d9b..7473b6d 100644 --- a/benches/propagate.luau +++ b/benches/propagate.luau @@ -4,6 +4,7 @@ local bench = require("./bench") local atom = Charm.atom local computed = Charm.computed local effect = Charm.effect +local listen = Charm.listen for _, w in { 10, 100 } do for _, h in { 10, 100 } do @@ -25,3 +26,22 @@ for _, w in { 10, 100 } do end) end end + +for _, w in { 10, 100 } do + for _, h in { 10, 100 } do + bench.run(`propagate (listen) {w}*{h}`, function() + local src = atom(1) + for _ = 1, w do + local last = src + for _ = 1, h do + local prev = last + last = computed(function() + return prev() + 1 + end) + end + listen(last, function() end) + end + src(src() + 1) + end) + end +end diff --git a/package.json b/package.json index 49c9f01..2646252 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "publish:npm": "pnpm publish -r --access public", "publish:wally": "pnpm -r --workspace-concurrency 1 publish:wally" }, - "packageManager": "pnpm@10.12.4", + "packageManager": "pnpm@10.17.1", "devDependencies": { "@rbxts-js/react": "17.1.20-ts.1", "@rbxts-js/react-roblox": "17.1.20-ts.1", diff --git a/packages/charm-sync/src/types.luau b/packages/charm-sync/src/types.luau index 791707f..3ccd746 100644 --- a/packages/charm-sync/src/types.luau +++ b/packages/charm-sync/src/types.luau @@ -4,13 +4,15 @@ type Atom = Charm.Atom --- A payload that can be sent from the server to the client to synchronize --- state between the two. -export type SyncPayload = { - type: "init", - data: { [string | number]: any }, -} | { - type: "patch", - data: { [string | number]: any }, -} +export type SyncPayload = + | { + type: "init", + data: { [string | number]: any }, + } + | { + type: "patch", + data: { [string | number]: any }, + } export type AtomMap = { [string | number]: Atom, diff --git a/packages/charm/src/__tests__/core/effect.spec.luau b/packages/charm/src/__tests__/core/effect.spec.luau index e02512a..ab3db2f 100644 --- a/packages/charm/src/__tests__/core/effect.spec.luau +++ b/packages/charm/src/__tests__/core/effect.spec.luau @@ -356,4 +356,25 @@ test("should run cleanups outside any evaluation context", function() assert(calls == 0, "should not run after b update") end) +test("should allow self cleanup", function() + local calls = 0 + local cleanups = 0 + local a = atom(0) + + effect(function(stopEffect) + onCleanup(function() + cleanups += 1 + end) + stopEffect() + a() + calls += 1 + end) + + assert(calls == 1, "should call effect once") + assert(cleanups == 1, "should call cleanup once") + a(1) + assert(calls == 1, "should not call effect after a update") + assert(cleanups == 1, "should not call cleanup after a update") +end) + return {} diff --git a/packages/charm/src/__tests__/listen.spec.luau b/packages/charm/src/__tests__/listen.spec.luau index d8c1764..d73bf23 100644 --- a/packages/charm/src/__tests__/listen.spec.luau +++ b/packages/charm/src/__tests__/listen.spec.luau @@ -4,6 +4,7 @@ local suite = require("../../../../test/suite") local atom = Charm.atom local listen = Charm.listen local effect = Charm.effect +local onCleanup = Charm.onCleanup local test = suite.test test("should listen to selector", function() @@ -115,4 +116,24 @@ test("should skip redundant updates", function() assert(table.concat(order) == "evenoddeven", `bad order: {table.concat(order)}`) end) +test("should allow self cleanup", function() + local calls = 0 + local cleanups = 0 + local a = atom(0) + + listen(a, function(_, _, stopEffect) + onCleanup(function() + cleanups += 1 + end) + stopEffect() + calls += 1 + end) + + assert(calls == 1, "should call effect once") + assert(cleanups == 1, "should call cleanup once") + a(1) + assert(calls == 1, "should not call effect after a update") + assert(cleanups == 1, "should not call cleanup after a update") +end) + return {} diff --git a/packages/charm/src/__tests__/observe.spec.luau b/packages/charm/src/__tests__/observe.spec.luau index ed392f4..ed2bdda 100644 --- a/packages/charm/src/__tests__/observe.spec.luau +++ b/packages/charm/src/__tests__/observe.spec.luau @@ -61,4 +61,36 @@ test("should dispose scopes", function() assert(table.concat(runs) == "", `should not rerun: {table.concat(runs)}`) end) +test("should queue addition", function() + local items = atom({ 1 }) + local edits = {} + + observe(items, function(item) + items({ 1, 2 }) + items({ 1, 2, 3, 4 }) + table.insert(edits, `+{item}`) + return function() + table.insert(edits, `-{item}`) + end + end) + + assert(table.concat(edits) == "+1+2+3+4", `bad edits: {table.concat(edits)}`) +end) + +test("should queue removal", function() + local items = atom({ 1, 2, 3, 4 }) + local edits = {} + + observe(items, function(item) + items({ 1, 2, 3 }) + items({ 1 }) + table.insert(edits, `+{item}`) + return function() + table.insert(edits, `-{item}`) + end + end) + + assert(table.concat(edits) == "+1+2+3+4-2-3-4", `bad edits: {table.concat(edits)}`) +end) + return {} diff --git a/packages/charm/src/__tests__/untracked.spec.luau b/packages/charm/src/__tests__/untracked.spec.luau index 7bc7f9f..6d7fb55 100644 --- a/packages/charm/src/__tests__/untracked.spec.luau +++ b/packages/charm/src/__tests__/untracked.spec.luau @@ -105,4 +105,26 @@ test("should block tracking inside computed atoms", function() assert(c() == 3, "c should be 3") end) +test("should track outside untracked", function() + local a = atom(1) + local b = atom(2) + local c = atom(3) + local spyCalls = 0 + + effect(function() + a() + untracked(c) + b() + spyCalls += 1 + end) + + assert(spyCalls == 1, "should be called once") + a(10) + assert(spyCalls == 2, "should be called twice") + b(20) + assert(spyCalls == 3, "should be called three times") + c(30) + assert(spyCalls == 3, "should not run on c change") +end) + return {} diff --git a/packages/charm/src/batched.luau b/packages/charm/src/batched.luau index 685365a..29a3d28 100644 --- a/packages/charm/src/batched.luau +++ b/packages/charm/src/batched.luau @@ -1,7 +1,10 @@ local core = require("./core") +local flags = require("./flags") +local utils = require("./utils") local startBatch = core.startBatch local endBatch = core.endBatch +local noYield = utils.noYield local function handleBatched(ok: boolean, ...) endBatch() @@ -14,7 +17,7 @@ end local function batched(callback: (Args...) -> Results..., ...: Args...): Results... startBatch() - return handleBatched(pcall(callback, ...)) + return handleBatched((if flags.strict then noYield else pcall)(callback, ...)) end return batched diff --git a/packages/charm/src/core.luau b/packages/charm/src/core.luau index 403e101..81296cc 100644 --- a/packages/charm/src/core.luau +++ b/packages/charm/src/core.luau @@ -21,14 +21,16 @@ local WATCHING = 0b0000010 local DIRTY = 0b0010000 local PENDING = 0b0100000 local QUEUED = 0b1000000 +local UNTRACKED = 0b10000000 type EffectScope = ReactiveNode & { cleanups: { () -> () }, } type Effect = ReactiveNode & { - fn: () -> ...(() -> ())?, + fn: (dispose: () -> ()) -> ...(() -> ())?, cleanups: { () -> () }, + cleanup: () -> (), } type Computed = ReactiveNode & { @@ -59,7 +61,6 @@ local notifyIndex = 0 local activeScope: Effect | EffectScope? local activeSub: ReactiveNode? -local activeSubWatching = true local function getCurrentSub(): ReactiveNode? return activeSub @@ -81,16 +82,10 @@ local function setCurrentScope(scope: Effect | EffectScope?) return prevScope end -local function setCurrentWatching(watching: boolean): boolean - local prevSubWatching = activeSubWatching - activeSubWatching = watching - return prevSubWatching -end - local function runCleanups(effect: EffectScope) for _, cleanup in effect.cleanups do local prev = setCurrentSub(nil) - local success, result: any = (config.strict and noYield or pcall)(cleanup) + local success, result: any = (if config.strict then noYield else pcall)(cleanup) setCurrentSub(prev) if not success then error(result) @@ -113,12 +108,10 @@ local function run(effect: Effect, flags: ReactiveFlags): () if bit32.btest(flags, DIRTY) or (bit32.btest(flags, PENDING) and checkDirty(effect.deps :: Link, effect)) then runCleanups(effect) local prev = setCurrentSub(effect) - local prevTracking = setCurrentWatching(true) startTracking(effect) - local success, result: (() -> ())? = (config.strict and noYield or pcall)(effect.fn) + local success, result: (() -> ())? = (if config.strict then noYield else pcall)(effect.fn, effect.cleanup) setCurrentSub(prev) endTracking(effect) - setCurrentWatching(prevTracking) if not success then error(result) elseif result then @@ -170,13 +163,11 @@ end local function updateComputed(computed: Computed): boolean local prevSub = setCurrentSub(computed) - local prevWatching = setCurrentWatching(true) startTracking(computed) local oldValue = computed.value - local success, result = (config.strict and noYield or pcall)(computed.getter, oldValue) + local success, result = (if config.strict then noYield else pcall)(computed.getter, oldValue) setCurrentSub(prevSub) endTracking(computed) - setCurrentWatching(prevWatching) if not success then error(result) end @@ -209,7 +200,7 @@ local function readAtom(atom: Atom): T end end end - if activeSub and activeSubWatching then + if activeSub and not bit32.btest(activeSub.flags, UNTRACKED) then link(atom, activeSub) end return value @@ -244,7 +235,7 @@ local function atomGetterSetter(atom: Atom): (...((T) -> T) | T) -> T local newValue = ... if type(newValue) == "function" then local prev = setCurrentSub(nil) - local success, result = (config.strict and noYield or pcall)(newValue, oldValue) + local success, result = (if config.strict then noYield else pcall)(newValue, oldValue) setCurrentSub(prev) if not success then error(result) @@ -270,7 +261,7 @@ local function computedGetter(computed: Computed): () -> T elseif bit32.btest(flags, PENDING) then computed.flags = bit32.band(flags, bit32.bnot(PENDING)) end - if activeSub and activeSubWatching then + if activeSub and not bit32.btest(activeSub.flags, UNTRACKED) then link(computed, activeSub) elseif activeScope then link(computed, activeScope) @@ -350,28 +341,28 @@ local function computed(getter: (previousValue: T?) -> T, equals: ((T, T) -> }) end -local function effect(fn: () -> ...(() -> ())?): () -> () +local function effect(fn: (dispose: () -> ()) -> ...(() -> ())?): () -> () local self: Effect = { fn = fn, flags = WATCHING, cleanups = {}, + cleanup = nil :: any, } + self.cleanup = effectStop(self) if activeSub then link(self, activeSub) elseif activeScope then link(self, activeScope) end local prev = setCurrentSub(self) - local prevWatching = setCurrentWatching(true) - local success, result: (() -> ())? = (config.strict and noYield or pcall)(fn) + local success, result: (() -> ())? = (if config.strict then noYield else pcall)(fn, self.cleanup) setCurrentSub(prev) - setCurrentWatching(prevWatching) if not success then error(result) elseif result then table.insert(self.cleanups, result) end - return effectStop(self) + return self.cleanup end local function effectScope(fn: () -> ...(() -> ())?, detached: boolean?): () -> () @@ -384,7 +375,7 @@ local function effectScope(fn: () -> ...(() -> ())?, detached: boolean?): () -> end local prevSub = setCurrentSub(nil) local prevScope = setCurrentScope(self) - local success, result: (() -> ())? = (config.strict and noYield or pcall)(fn) + local success, result: (() -> ())? = (if config.strict then noYield else pcall)(fn) setCurrentScope(prevScope) setCurrentSub(prevSub) if not success then @@ -416,7 +407,6 @@ return { setCurrentSub = setCurrentSub, getCurrentScope = getCurrentScope, setCurrentScope = setCurrentScope, - setCurrentWatching = setCurrentWatching, onCleanup = onCleanup, getBatchDepth = getBatchDepth, startBatch = startBatch, diff --git a/packages/charm/src/init.luau b/packages/charm/src/init.luau index b388e5d..36236b2 100644 --- a/packages/charm/src/init.luau +++ b/packages/charm/src/init.luau @@ -1,3 +1,5 @@ +--!nolint DeprecatedApi + local batched = require("@self/batched") local core = require("@self/core") local flags = require("@self/flags") diff --git a/packages/charm/src/observe.luau b/packages/charm/src/observe.luau index 7272584..219ae72 100644 --- a/packages/charm/src/observe.luau +++ b/packages/charm/src/observe.luau @@ -3,6 +3,8 @@ local core = require("./core") local effect = core.effect local effectScope = core.effectScope local onCleanup = core.onCleanup +local startBatch = core.startBatch +local endBatch = core.endBatch local function observe(selector: () -> { [K]: V }, observer: (item: V, key: K) -> ...(() -> ())?): () -> () local scopes: { [K]: () -> () } = {} @@ -18,7 +20,7 @@ local function observe(selector: () -> { [K]: V }, observer: (item: V, key end for key, value in currentItems do - if previousItems[key] == nil then + if previousItems[key] == nil and scopes[key] == nil then scopes[key] = effectScope(function() return observer(value, key) end, true) @@ -37,7 +39,9 @@ local function observe(selector: () -> { [K]: V }, observer: (item: V, key local previousItems = currentItems currentItems = selector() if currentItems ~= previousItems then + startBatch() updateScopes(currentItems, previousItems) + endBatch() end end) diff --git a/packages/charm/src/system.luau b/packages/charm/src/system.luau index dbd5e2c..ee80b9b 100644 --- a/packages/charm/src/system.luau +++ b/packages/charm/src/system.luau @@ -11,6 +11,7 @@ export type ReactiveNode = { } export type Link = { + version: number, dep: ReactiveNode, sub: ReactiveNode, prevSub: Link?, @@ -26,13 +27,14 @@ type Stack = { export type ReactiveFlags = number -local NONE = 0b0000000 -local MUTABLE = 0b0000001 -local WATCHING = 0b0000010 -local RECURSED_CHECK = 0b0000100 -local RECURSED = 0b0001000 -local DIRTY = 0b0010000 -local PENDING = 0b0100000 +local NONE = 0b00000000 +local MUTABLE = 0b00000001 +local WATCHING = 0b00000010 +local RECURSED_CHECK = 0b00000100 +local RECURSED = 0b00001000 +local DIRTY = 0b00010000 +local PENDING = 0b00100000 +local UNTRACKED = 0b10000000 local ReactiveFlags = { None = NONE, @@ -42,6 +44,7 @@ local ReactiveFlags = { Recursed = RECURSED, Dirty = DIRTY, Pending = PENDING, + Untracked = UNTRACKED, } local function createReactiveSystem(props: { @@ -52,20 +55,15 @@ local function createReactiveSystem(props: { local update = props.update local notify = props.notify local unwatched = props.unwatched + local currentVersion = 0 local function isValidLink(checkLink: Link, sub: ReactiveNode): boolean - local depsTail = sub.depsTail - if depsTail then - local link = sub.deps :: Link - repeat - if link == checkLink then - return true - end - if link == depsTail then - break - end - link = link.nextDep :: Link - until not link + local link = sub.depsTail + while link do + if link == checkLink then + return true + end + link = link.prevDep end return false end @@ -75,20 +73,18 @@ local function createReactiveSystem(props: { if prevDep and prevDep.dep == dep then return end - local nextDep: Link? - local recursedCheck = bit32.btest(sub.flags, RECURSED_CHECK) - if recursedCheck then - nextDep = if prevDep then prevDep.nextDep else sub.deps - if nextDep and nextDep.dep == dep then - sub.depsTail = nextDep - return - end + local nextDep = if prevDep then prevDep.nextDep else sub.deps + if nextDep and nextDep.dep == dep then + nextDep.version = currentVersion + sub.depsTail = nextDep + return end local prevSub = dep.subsTail - if prevSub and prevSub.sub == sub and (not recursedCheck or isValidLink(prevSub, sub)) then + if prevSub and prevSub.version == currentVersion and prevSub.sub == sub then return end local newLink: Link = { + version = currentVersion, dep = dep, sub = sub, prevDep = prevDep, @@ -154,35 +150,34 @@ local function createReactiveSystem(props: { local flags = sub.flags - if bit32.btest(flags, bit32.bor(MUTABLE, WATCHING)) then - if not bit32.btest(flags, bit32.bor(RECURSED_CHECK, RECURSED, DIRTY, PENDING)) then - sub.flags = bit32.bor(flags, PENDING) - elseif not bit32.btest(flags, bit32.bor(RECURSED_CHECK, RECURSED)) then - flags = NONE - elseif not bit32.btest(flags, RECURSED_CHECK) then - sub.flags = bit32.bor(bit32.band(flags, bit32.bnot(RECURSED)), PENDING) - elseif not bit32.btest(flags, bit32.bor(DIRTY, PENDING)) and isValidLink(link, sub) then - sub.flags = bit32.bor(flags, bit32.bor(RECURSED, PENDING)) - flags = bit32.band(flags, MUTABLE) - else - -- selene: allow(if_same_then_else) - flags = NONE - end + if not bit32.btest(flags, bit32.bor(RECURSED_CHECK, RECURSED, DIRTY, PENDING)) then + sub.flags = bit32.bor(flags, PENDING) + elseif not bit32.btest(flags, bit32.bor(RECURSED_CHECK, RECURSED)) then + flags = NONE + elseif not bit32.btest(flags, RECURSED_CHECK) then + sub.flags = bit32.bor(bit32.band(flags, bit32.bnot(RECURSED)), PENDING) + elseif not bit32.btest(flags, bit32.bor(DIRTY, PENDING)) and isValidLink(link, sub) then + sub.flags = bit32.bor(flags, bit32.bor(RECURSED, PENDING)) + flags = bit32.band(flags, MUTABLE) + else + -- selene: allow(if_same_then_else) + flags = NONE + end - if bit32.btest(flags, WATCHING) then - notify(sub) - end + if bit32.btest(flags, WATCHING) then + notify(sub) + end - if bit32.btest(flags, MUTABLE) then - local subSubs = sub.subs - if subSubs then - link = subSubs - if subSubs.nextSub then - stack = { value = next, prev = stack } - next = link.nextSub - end - continue + if bit32.btest(flags, MUTABLE) then + local subSubs = sub.subs + if subSubs then + link = subSubs + local nextSub = link.nextSub + if nextSub then + stack = { value = next, prev = stack } + next = nextSub end + continue end end @@ -214,6 +209,7 @@ local function createReactiveSystem(props: { end local function startTracking(sub: ReactiveNode): () + currentVersion += 1 sub.depsTail = nil sub.flags = bit32.bor(bit32.band(sub.flags, bit32.bnot(bit32.bor(RECURSED, DIRTY, PENDING))), RECURSED_CHECK) end @@ -230,10 +226,10 @@ local function createReactiveSystem(props: { local function shallowPropagate(link: Link): () repeat local sub = link.sub - local subFlags = sub.flags - if bit32.band(subFlags, bit32.bor(PENDING, DIRTY)) == PENDING then - sub.flags = bit32.bor(subFlags, DIRTY) - if bit32.btest(subFlags, WATCHING) then + local flags = sub.flags + if bit32.band(flags, bit32.bor(PENDING, DIRTY)) == PENDING then + sub.flags = bit32.bor(flags, DIRTY) + if bit32.btest(flags, WATCHING) then notify(sub) end end @@ -244,16 +240,15 @@ local function createReactiveSystem(props: { local function checkDirty(link: Link, sub: ReactiveNode): boolean local stack: Stack? local checkDepth = 0 + local dirty = false repeat local dep = link.dep - local depFlags = dep.flags - - local dirty = false + local flags = dep.flags if bit32.btest(sub.flags, DIRTY) then dirty = true - elseif bit32.band(depFlags, bit32.bor(MUTABLE, DIRTY)) == bit32.bor(MUTABLE, DIRTY) then + elseif bit32.band(flags, bit32.bor(MUTABLE, DIRTY)) == bit32.bor(MUTABLE, DIRTY) then if update(dep) then local subs = dep.subs :: Link if subs.nextSub then @@ -261,7 +256,7 @@ local function createReactiveSystem(props: { end dirty = true end - elseif bit32.band(depFlags, bit32.bor(MUTABLE, PENDING)) == bit32.bor(MUTABLE, PENDING) then + elseif bit32.band(flags, bit32.bor(MUTABLE, PENDING)) == bit32.bor(MUTABLE, PENDING) then if link.nextSub or link.prevSub then stack = { value = link, prev = stack } end @@ -271,9 +266,12 @@ local function createReactiveSystem(props: { continue end - if not dirty and link.nextDep then - link = link.nextDep - continue + if not dirty then + local nextDep = link.nextDep + if nextDep then + link = nextDep + continue + end end local continueTop = false @@ -296,16 +294,17 @@ local function createReactiveSystem(props: { sub = link.sub continue end + dirty = false else sub.flags = bit32.band(sub.flags, bit32.bnot(PENDING)) end sub = link.sub - if link.nextDep then - link = link.nextDep + local nextDep = link.nextDep + if nextDep then + link = nextDep continueTop = true break end - dirty = false end if continueTop then diff --git a/packages/charm/src/untracked.luau b/packages/charm/src/untracked.luau index 3c5121f..9661fa3 100644 --- a/packages/charm/src/untracked.luau +++ b/packages/charm/src/untracked.luau @@ -1,9 +1,12 @@ local core = require("./core") -local setCurrentWatching = core.setCurrentWatching +local UNTRACKED = core.ReactiveFlags.Untracked +local getCurrentSub = core.getCurrentSub -local function handleUntracked(prevWatching: boolean, ok: boolean, ...) - setCurrentWatching(prevWatching) +local function handleUntracked(sub: core.ReactiveNode?, ok: boolean, ...) + if sub then + sub.flags = bit32.band(sub.flags, bit32.bnot(UNTRACKED)) + end if ok then return ... else @@ -12,7 +15,13 @@ local function handleUntracked(prevWatching: boolean, ok: boolean, ...) end local function untracked(callback: (Args...) -> Results..., ...: Args...): Results... - return handleUntracked(setCurrentWatching(false), pcall(callback, ...)) + local sub = getCurrentSub() + if not sub or bit32.btest(sub.flags, UNTRACKED) then + return callback(...) + else + sub.flags = bit32.bor(sub.flags, UNTRACKED) + end + return handleUntracked(sub, pcall(callback, ...)) end return untracked diff --git a/rokit.toml b/rokit.toml index 23db8e9..1b1c5d7 100644 --- a/rokit.toml +++ b/rokit.toml @@ -6,8 +6,8 @@ [tools] wally = "UpliftGames/wally@0.3.2" rojo = "rojo-rbx/rojo@7.5.1" -stylua = "johnnymorganz/stylua@2.1.0" -selene = "Kampfkarren/selene@0.28.0" -luau-lsp = "johnnymorganz/luau-lsp@1.51.0" -lune = "lune-org/lune@0.9.4" -lute = "luau-lang/lute@0.1.0-nightly.20250701" +stylua = "johnnymorganz/stylua@2.2.0" +selene = "Kampfkarren/selene@0.29.0" +luau-lsp = "johnnymorganz/luau-lsp@1.53.5" +lune = "lune-org/lune@0.10.2" +lute = "luau-lang/lute@0.1.0-nightly.20250923" diff --git a/selene.toml b/selene.toml index 7da7261..1b04ccf 100644 --- a/selene.toml +++ b/selene.toml @@ -1,5 +1,5 @@ std = "selene_defs" -exclude = ["**/node_modules/**"] +exclude = ["*/node_modules/*"] [lints] global_usage = "allow"