Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_bun_latest_coverage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"packages/eslint-plugin/package.json": "Patch"
},
"note": "Fix sparse trailing-null autofixes to preserve authored values, complete parenthesized syntax and earlier holes using token-based edit boundaries while removing the maximal trailing run of nulls and holes only when the last element is canonical literal null; preserve regular-expression and BigInt literals without validating regex syntax, preserve existing comment and trailing-comma handling, remove an unreachable AST guard and restore complete behavior coverage on the latest Bun without exclusions or new build errors.",
"date": "2026-10-22T00:00:00.000Z"
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@ describe.each(['css' /* 'globalCss', 'keyframes'*/])(
})
ruleTester.run('css-utils-literal-only rule', cssUtilsLiteralOnly, {
valid: [
...[
"const f = ({'p': n}) => n; css({w: f({p: 1})});",
"const k = 'p'; const f = ({[k]: n}) => n; css({w: f({p: 1})});",
'const L = [1]; unknown(L[0]); css({w: L[0]});',
'const L = [1]; unknown(L[9]); css({w: L[0]});',
'const L = {p: 1}; export const holder = [L]; css({w: L.p});',
'const L = {p: 1}; module.exports.token = L; css({w: L.p});',
"const L = {p: 1}; module['exports'].token = L; css({w: L.p});",
'const N = 4; function f() { return N } css({w: f()});',
'const helper = () => 4; function f() { return helper() } css({w: f()});',
'const helper = function() { return 4 }; function f() { return helper() } css({w: f()});',
].map((use) => ({
code: `import { css } from "@devup-ui/react";\n${use}`,
filename: 'src/app/page.tsx',
})),
{
code: `import { 'create' as create } from '@stylexjs/stylex'; create({a: {width: 1}});`,
filename: 'src/app/page.tsx',
},
{
code: `import { ${code} } from "@devup-ui/react";\n${code}({w: 1})`,
filename: 'src/app/page.tsx',
Expand Down Expand Up @@ -145,6 +164,26 @@ describe.each(['css' /* 'globalCss', 'keyframes'*/])(
},
],
invalid: [
...[
"const f = ({'normalize': n}) => n; css({w: f({normalize: 1})});",
'const L = [{p: 1}]; unknown(L[0]); css({w: L[0].p});',
'const A = [{p: 1}]; const L = [1, ...A]; unknown(L[0]); css({w: A[0].p});',
'const L = {p: 1}; unknown([L]); css({w: L.p});',
'const L = {p: 1}; other.exports.token = L; css({w: L.p});',
'const helper = () => Date.now(); function f() { return helper() } css({w: f()});',
'class Token {} function f() { return Token } css({w: f()});',
].map((use) => ({
code: `import { css } from "@devup-ui/react";\n${use}`,
filename: 'src/app/page.tsx',
output: null,
errors: [{ messageId: 'cssUtilsLiteralOnly' as const }],
})),
{
code: `import { 'create' as create } from '@stylexjs/stylex'; let v = 1; create({a: {width: v}});`,
filename: 'src/app/page.tsx',
output: null,
errors: [{ messageId: 'cssUtilsLiteralOnly' }],
},
...[
[
`export function C() { let fade = keyframes({ from: { opacity: 0 } }); return css({ animationName: fade }); }`,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,110 @@
import { RuleTester } from '@typescript-eslint/rule-tester'
import { describe } from 'bun:test'
import { TSESLint } from '@typescript-eslint/utils'
import { describe, expect, it } from 'bun:test'
import { parser } from 'typescript-eslint'

import { noUselessTailingNulls } from '../index'

describe('no-useless-tailing-nulls rule', () => {
const boundaryCases = [
['[0,(1),,null]', '[0,(1)]'],
['[0,((1)),,null]', '[0,((1))]'],
['[0,(1),null]', '[0,(1)]'],
['[0,(1),,(null)]', '[0,(1)]'],
['[(null)]', '[]'],
['[1,null]', '[1]'],
['[0,((1)),,((null))]', '[0,((1))]'],
['[((null)),(null)]', '[]'],
[
'[0,( /* value */ 1 /* inner */ ), /* remove */ , (null)]',
'[0,( /* value */ 1 /* inner */ )]',
],
['[1 /* remove */, /* remove */ null /* keep */]', '[1 /* keep */]'],
['[/* keep */ (null),null]', '[/* keep */ ]'],
['[, /* remove */ (null)]', '[]'],
['[0,(1),null,]', '[0,(1),]'],
['[0,((1)),,((null)),]', '[0,((1)),]'],
['[(null),]', '[,]'],
['[null,]', '[,]'],
['[1,null, /* keep */]', '[1, /* keep */]'],
['[1,null /* keep */,]', '[1 /* keep */,]'],
[
'[0,(1), /* remove */ null /* keep */, /* tail */]',
'[0,(1) /* keep */, /* tail */]',
],
['[/* keep */ ((null)) /* tail */]', '[/* keep */ /* tail */]'],
] as const
const literalKindCases = [
['[/(/,null]', '[/(/]'],
['[/a/gg,null]', '[/a/gg]'],
['[/a/z,null]', '[/a/z]'],
['[/a/gg,,null]', '[/a/gg]'],
['[/a/g,null]', '[/a/g]'],
['[/a/g,,null]', '[/a/g]'],
['[(/(/),,(null)]', '[(/(/)]'],
['[1n,null]', '[1n]'],
['[0xFFn,,null]', '[0xFFn]'],
['[1n,null,,(null),]', '[1n,]'],
] as const
const literalKindControls = [
'[/(/]',
'[/a/gg]',
'[/a/z]',
'[1,null,/(/]',
'[/a/g]',
'[1n]',
'[0xFFn]',
'[null,1n]',
] as const
const literalKindValidTests = literalKindControls.flatMap((value) =>
[false, true].map((jsx) => ({
code: `import {css,Box} from '@devup-ui/react';\n${jsx ? `<Box w={${value}} />` : `css({w:${value}})`}`,
filename: 'src/app/page.tsx',
})),
)
const boundaryTests = [...boundaryCases, ...literalKindCases].flatMap(
([value, fixed]) =>
[false, true].map((jsx) => ({
code: `import {css,Box} from '@devup-ui/react';\n${jsx ? `<Box w={${value}} />` : `css({w:${value}})`}`,
output: `import {css,Box} from '@devup-ui/react';\n${jsx ? `<Box w={${fixed}} />` : `css({w:${fixed}})`}`,
filename: 'src/app/page.tsx',
errors: [{ messageId: 'uselessTailingNulls' as const }],
})),
)
it.each([
...boundaryTests,
...literalKindValidTests.map((test) => ({ ...test, output: test.code })),
])(
'preserves authored syntax and parses when fixing $code',
({ code, output }) => {
const linter = new TSESLint.Linter()
const config = {
files: ['**/*.tsx'],
languageOptions: {
parser,
parserOptions: { ecmaFeatures: { jsx: true } },
},
plugins: { devup: { rules: { trailing: noUselessTailingNulls } } },
rules: { 'devup/trailing': 'error' },
} satisfies TSESLint.FlatConfig.Config

const result = linter.verifyAndFix(code, config, {
filename: 'src/app/page.tsx',
})

expect(result.output).toBe(output)
expect(
linter.verify(
result.output,
{ ...config, rules: {} },
{
filename: 'src/app/page.tsx',
},
),
).toEqual([])
expect(result.messages).toEqual([])
},
)
const ruleTester = new RuleTester({
languageOptions: {
ecmaVersion: 'latest',
Expand All @@ -16,6 +117,21 @@ describe('no-useless-tailing-nulls rule', () => {
})
ruleTester.run('no-useless-tailing-nulls rule', noUselessTailingNulls, {
valid: [
...literalKindValidTests,
// Real parser holes are absent elements, not Literal null nodes.
...[
'[]',
'[,]',
'[1,null,,]',
'[null,,]',
'[,,]',
'[1,,]',
'[1,,2]',
'[1, null, 2]',
].map((value) => ({
code: `import { css } from "@devup-ui/react";\ncss({w: ${value}})`,
filename: 'src/app/page.tsx',
})),
{
code: 'import { Box } from "@devup-ui/react";\n<Box w={[1, 2, null, 3]} />',
filename: 'src/app/page.tsx',
Expand Down Expand Up @@ -44,6 +160,28 @@ describe('no-useless-tailing-nulls rule', () => {
})),
],
invalid: [
...boundaryTests,
...[
['[null]', '[]'],
['[null,null]', '[]'],
['[1,null]', '[1]'],
['[1,,null]', '[1]'],
['[,1,null]', '[,1]'],
['[1,,2,null]', '[1,,2]'],
['[1,,null,null]', '[1]'],
['[,null]', '[]'],
['[,,null,null]', '[]'],
['[/* keep */ null,null]', '[/* keep */ ]'],
['[1,null,,null]', '[1]'],
['[null,1,,null]', '[null,1]'],
['[value,,null]', '[value]'],
['[...values,,null]', '[...values]'],
].map(([value, fixed]) => ({
code: `import { css } from "@devup-ui/react";\ncss({w: ${value}})`,
output: `import { css } from "@devup-ui/react";\ncss({w: ${fixed}})`,
filename: 'src/app/page.tsx',
errors: [{ messageId: 'uselessTailingNulls' as const }],
})),
{
code: 'import { Box } from "@devup-ui/react";\n<Box icon={<Box w={[1, null]} />} m={[2, null]} />',
output:
Expand Down
51 changes: 34 additions & 17 deletions packages/eslint-plugin/src/rules/no-useless-tailing-nulls/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
AST_NODE_TYPES,
ASTUtils,
ESLintUtils,
type TSESTree,
} from '@typescript-eslint/utils'
Expand All @@ -13,36 +14,52 @@ const createRule = ESLintUtils.RuleCreator(
`https://github.com/dev-five-git/devup-ui/tree/main/packages/eslint-plugin/src/rules/${name}`,
)

function isNullLiteral(
node: TSESTree.Node | null | undefined,
): node is TSESTree.NullLiteral {
return node?.type === AST_NODE_TYPES.Literal && node.raw === 'null'
}

function checkUselessTailingNulls<T extends RuleContext<string, []>>(
node: TSESTree.ArrayExpression,
context: T,
) {
let nullCount = 0
for (let i = node.elements.length - 1; i >= 0; i--) {
const lastElement = node.elements[node.elements.length - 1]
if (!isNullLiteral(lastElement)) return
let trailingCount = 1
for (let i = node.elements.length - 2; i >= 0; i--) {
const element = node.elements[i]
if (element?.type === AST_NODE_TYPES.Literal && element.value === null) {
nullCount++
if (!element || isNullLiteral(element)) {
trailingCount++
} else {
break
}
}
if (nullCount === 0) return
const previousElement = node.elements[node.elements.length - nullCount - 1]
const firstElement = node.elements[0]
const lastElement = node.elements[node.elements.length - 1]
if (!lastElement || (node.elements.length === nullCount && !firstElement))
return
const removeStart =
node.elements.length > nullCount && previousElement
? previousElement.range[1]
: firstElement?.range[0]
if (removeStart === undefined) return

const previousElement =
node.elements[node.elements.length - trailingCount - 1]
context.report({
node,
messageId: 'uselessTailingNulls',
fix(fixer) {
return fixer.removeRange([removeStart, lastElement.range[1]])
const sourceCode = context.sourceCode
let removeStart = node.range[0] + 1
if (previousElement) {
const [separator] = sourceCode.getFirstTokensBetween(
previousElement,
lastElement,
{ count: 1, filter: ASTUtils.isCommaToken },
)
const [previousToken] = sourceCode.getTokensBefore(separator, 1)
removeStart = previousToken.range[1]
} else if (node.elements[0]) {
const [, firstToken] = sourceCode.getFirstTokens(node, 2)
removeStart = firstToken.range[0]
}
const [beforeLastToken, lastToken] = sourceCode.getLastTokens(node, 3)
const removeEnd = ASTUtils.isCommaToken(lastToken)
? beforeLastToken.range[1]
: lastToken.range[1]
return fixer.removeRange([removeStart, removeEnd])
},
})
}
Expand Down
24 changes: 24 additions & 0 deletions packages/eslint-plugin/src/utils/__tests__/import-sources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,30 @@ function inspect(
}

describe('import sources', () => {
for (const use of ['css({w: 1})', 'styled(Base, {w: 1})'])
inspect(
`import { css, styled } from "@devup-ui/react";\n${use}`,
'page.tsx',
(storage, context) => {
const statement = context.sourceCode.ast.body[1]
if (
statement.type !== 'ExpressionStatement' ||
statement.expression.type !== 'CallExpression'
)
throw new Error('Expected a parsed style call')
const call = statement.expression
expect(styleValueSite(call.callee, storage)).toBeNull()
const rules = call.arguments.at(-1)
if (!rules) throw new Error('Expected a rules argument')
expect(styleValueSite(rules, storage)).toEqual({
root: call,
start: call,
})
if (call.arguments.length === 2)
expect(styleValueSite(call.arguments[0], storage)).toBeNull()
},
)

inspect(
[
'import { css as a, Box as B, unknown as u } from "@devup-ui/react";',
Expand Down
6 changes: 6 additions & 0 deletions packages/next-plugin/src/__tests__/coordinator-port.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ describe('coordinator port file', () => {
expect(Bun.file(file).size).toBe(0)
})

it('allows first-run cleanup when no coordinator port file exists', () => {
const file = join(tmp(), 'coordinator.port')

expect(() => removeStalePortFile(file)).not.toThrow()
})

it('names port, owner pid, state and recovery when the coordinator is unreachable', () => {
const dir = tmp()
const file = join(dir, 'coordinator.port')
Expand Down
14 changes: 14 additions & 0 deletions packages/next-plugin/src/__tests__/wasm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,20 @@ describe('WASM loading', () => {
).toEqual({ isolated: true })
})

it('resolves from the project manifest without an installed or workspace plugin', () => {
const root = mkdtempSync(join(tmpdir(), 'devup-ui-next-root-'))
tempRoots.push(root)
writeFileSync(join(root, 'package.json'), '{"name":"root-fallback"}')
writeFileSync(
join(root, 'dependency.cjs'),
'module.exports = { root: true }',
)

expect(
requireFromPlugin<{ root: boolean }>('./dependency.cjs', root),
).toEqual({ root: true })
})

it('loads or injects the Webpack plugin without a static dependency', () => {
expect(typeof loadWebpackPlugin().DevupUIWebpackPlugin).toBe('function')
setWebpackPluginForTesting(webpackPlugin)
Expand Down
Loading
Loading