Skip to content

Commit 9c83ed3

Browse files
committed
feat: add contributor guide and expand shared/config tests for full coverage
1 parent ca75fb5 commit 9c83ed3

File tree

13 files changed

+308
-2
lines changed

13 files changed

+308
-2
lines changed

AGENTS.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Repository Guidelines
2+
3+
## Environment Setup
4+
Use Node.js 18 or newer and install dependencies with `pnpm` (enforced by `preinstall`). Run `pnpm install` from the repo root to link workspace packages and set up local post-install patches via `tw-patch`.
5+
6+
## Project Structure & Module Organization
7+
Source lives in `packages/`, with `@tailwindcss-mangle/core` providing the transformation engine, `shared` for cross-package utils, `config` for preset defaults, `tailwindcss-patch` to shim Tailwind versions, and `unplugin-tailwindcss-mangle` for build-tool integrations. Example applications live under `apps/` for framework-specific smoke tests, while `website/` hosts the Astro-powered docs. Reusable scripts live in `scripts/`, and assets used by docs and samples are under `assets/`.
8+
9+
## Build, Test, and Development Commands
10+
- `pnpm dev`: start package-level dev modes across the workspace (e.g., watch builds).
11+
- `pnpm build`: run Turbo builds for every package in `packages/*`.
12+
- `pnpm build:docs`: build the Astro documentation site in `website/`.
13+
- `pnpm lint`: apply ESLint fixes across TypeScript sources.
14+
- `pnpm script:clean`: remove generated artifacts via the monorepo helper.
15+
16+
## Coding Style & Naming Conventions
17+
The codebase is TypeScript-first with strict ESM modules. Formatting is handled by the shared `@icebreakers/eslint-config` and Prettier defaults (2-space indentation, semicolons omitted). Prefer PascalCase for exported classes (e.g., `ClassGenerator`) and camelCase for functions and variables. Keep filenames lowercase with dashes or dots (`css/index.ts`, `test/utils.ts`).
18+
19+
## Testing Guidelines
20+
Vitest drives unit tests via `vitest.config.ts`, discovering suites inside each package’s `test/` directory. Name files `*.test.ts` and keep snapshots in `__snapshots__/`. Run the full suite with `pnpm test`; use `pnpm test:dev` for focused watch mode, or filter with `pnpm --filter @tailwindcss-mangle/core test`. Coverage is enabled by default and stored in `coverage/` directories.
21+
22+
## Commit & Pull Request Guidelines
23+
Follow Conventional Commits enforced by Commitlint (e.g., `feat(core): add selector mangling`). Group related changes per package and mention affected workspace names in the scope. Open pull requests with a concise summary, testing notes, and links to any tracking issues. Include screenshots or CLI output when altering developer tooling or docs. Use Changesets (`pnpm release`) when preparing published releases.
24+
25+
## Release & Automation Notes
26+
Changesets orchestrate versioning (`pnpm publish-packages`). Turbo caches builds, so clear with `pnpm script:clean` if results look stale. Renovate keeps dependencies current; note any pinning decisions in the PR description.

packages/config/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/* c8 ignore start */
2+
13
import type { FilterPattern } from '@rollup/pluginutils'
24
import type { IClassGeneratorOptions } from '@tailwindcss-mangle/shared'
35
import type { SourceEntry } from '@tailwindcss/oxide'
@@ -71,3 +73,5 @@ export interface UserConfig {
7173
patch?: PatchUserConfig
7274
mangle?: MangleUserConfig
7375
}
76+
77+
/* c8 ignore end */
Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import fs from 'fs-extra'
2+
import os from 'node:os'
23
import path from 'pathe'
3-
import { initConfig } from '@/config'
4+
import { defineConfig, getConfig, initConfig } from '@/config'
5+
import { getDefaultUserConfig } from '@/defaults'
46

57
describe('config', () => {
68
it('init config', async () => {
@@ -9,4 +11,29 @@ describe('config', () => {
911
const dest = path.resolve(cwd, 'tailwindcss-mangle.config.ts')
1012
expect(await fs.readFile(dest, 'utf8')).toMatchSnapshot()
1113
})
14+
15+
it('defineConfig helper returns provided config', () => {
16+
const config = defineConfig({
17+
patch: {
18+
output: {
19+
filename: 'custom.json',
20+
},
21+
},
22+
})
23+
24+
expect(config).toEqual({
25+
patch: {
26+
output: {
27+
filename: 'custom.json',
28+
},
29+
},
30+
})
31+
})
32+
33+
it('getConfig falls back to defaults when config file is absent', async () => {
34+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'config-test-'))
35+
const { config } = await getConfig(tempDir)
36+
expect(config).toEqual(getDefaultUserConfig())
37+
await fs.remove(tempDir)
38+
})
1239
})

packages/config/test/defaults.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createFilter } from '@rollup/pluginutils'
22
import { omit } from 'lodash-es'
3-
import { getDefaultPatchConfig, getDefaultUserConfig } from '@/defaults'
3+
import { getDefaultMangleUserConfig, getDefaultPatchConfig, getDefaultUserConfig } from '@/defaults'
44

55
function omitCwdPath(o: any) {
66
return omit(o, ['tailwindcss.cwd', 'patch.tailwindcss.cwd'])
@@ -14,6 +14,25 @@ describe('defaults', () => {
1414
it('getDefaultUserConfig', () => {
1515
expect(omitCwdPath(getDefaultUserConfig())).toMatchSnapshot()
1616
})
17+
18+
it('getDefaultMangleUserConfig reflects NODE_ENV', () => {
19+
const originalEnv = process.env.NODE_ENV
20+
21+
vi.stubEnv('NODE_ENV', 'development')
22+
expect(getDefaultMangleUserConfig().disabled).toBe(true)
23+
vi.unstubAllEnvs()
24+
25+
vi.stubEnv('NODE_ENV', 'production')
26+
expect(getDefaultMangleUserConfig().disabled).toBe(false)
27+
vi.unstubAllEnvs()
28+
29+
if (originalEnv !== undefined) {
30+
process.env.NODE_ENV = originalEnv
31+
}
32+
else {
33+
delete process.env.NODE_ENV
34+
}
35+
})
1736
})
1837

1938
describe('createFilter', () => {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as entry from '@/index'
2+
3+
describe('entrypoint exports', () => {
4+
it('exposes defineConfig and helpers', () => {
5+
expect(typeof entry.defineConfig).toBe('function')
6+
expect(typeof entry.getConfig).toBe('function')
7+
expect(typeof entry.getDefaultUserConfig).toBe('function')
8+
})
9+
})
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
3+
describe('getConfig delegation', () => {
4+
it('passes cwd and defaults to c12.loadConfig', async () => {
5+
const loadConfig = vi.fn().mockResolvedValue({ config: {} })
6+
const getDefaultUserConfig = vi.fn().mockReturnValue({ patch: {}, mangle: {} })
7+
const constants = await vi.importActual<typeof import('../src/constants')>('../src/constants')
8+
9+
vi.doMock('c12', () => ({
10+
loadConfig,
11+
createDefineConfig: () => (value: unknown) => value,
12+
}))
13+
vi.doMock('../src/defaults', () => ({
14+
getDefaultUserConfig,
15+
}))
16+
17+
vi.resetModules()
18+
const { getConfig } = await import('../src/config')
19+
await getConfig('custom-cwd')
20+
21+
expect(loadConfig).toHaveBeenCalledWith(expect.objectContaining({
22+
name: constants.CONFIG_NAME,
23+
cwd: 'custom-cwd',
24+
}))
25+
expect(getDefaultUserConfig).toHaveBeenCalledTimes(1)
26+
27+
vi.resetModules()
28+
vi.doUnmock('c12')
29+
vi.doUnmock('../src/defaults')
30+
})
31+
})

packages/config/vitest.config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,14 @@ export default defineProject({
1111
],
1212
globals: true,
1313
testTimeout: 60_000,
14+
coverage: {
15+
exclude: [
16+
'src/types.ts',
17+
'dist/**',
18+
'tsup.config.ts',
19+
'vitest.config.ts',
20+
'test/fixtures/**',
21+
],
22+
},
1423
},
1524
})

packages/shared/src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/* c8 ignore start */
2+
13
export interface IClassGeneratorContextItem {
24
name: string
35
usedBy: Set<string>
@@ -18,3 +20,5 @@ export interface IClassGenerator {
1820
newClassSize: number
1921
context: Record<string, any>
2022
}
23+
24+
/* c8 ignore end */
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { ClassGenerator } from '@/classGenerator'
2+
3+
describe('ClassGenerator behaviour', () => {
4+
it('respects include/exclude filters and ignore rules', () => {
5+
const generator = new ClassGenerator({
6+
include: [/src\/.+\.ts$/u],
7+
exclude: [/src\/skip/u],
8+
ignoreClass: [/^skip-me$/u],
9+
classPrefix: 'pref-',
10+
})
11+
12+
expect(generator.includeFilePath('src/file.ts')).toBe(true)
13+
expect(generator.includeFilePath('pkg/file.ts')).toBe(false)
14+
expect(generator.excludeFilePath('src/skip/component.ts')).toBe(true)
15+
expect(generator.isFileIncluded('src/skip/component.ts')).toBe(false)
16+
expect(generator.isFileIncluded('src/ok/component.ts')).toBe(true)
17+
18+
expect(generator.ignoreClassName('skip-me')).toBe(true)
19+
expect(generator.ignoreClassName('keep-me')).toBe(false)
20+
21+
const defaultGenerator = new ClassGenerator()
22+
expect(defaultGenerator.includeFilePath('any/path.vue')).toBe(true)
23+
expect(defaultGenerator.excludeFilePath('any/path.vue')).toBe(false)
24+
})
25+
26+
it('transforms mapped class names using the escape-stripped key', () => {
27+
const generator = new ClassGenerator()
28+
generator.newClassMap['text[50]'] = {
29+
name: 'pref-a',
30+
usedBy: new Set(),
31+
}
32+
33+
expect(generator.transformCssClass('text\\[50\\]')).toBe('pref-a')
34+
expect(generator.transformCssClass('text-unknown')).toBe('text-unknown')
35+
})
36+
37+
it('reuses generated classes and supports custom generators', () => {
38+
const generator = new ClassGenerator({
39+
customGenerate: (original, _opts, ctx) => {
40+
ctx[original] = (ctx[original] ?? 0) + 1
41+
return `${original}-${ctx[original]}`
42+
},
43+
})
44+
45+
const first = generator.generateClassName('foo')
46+
expect(first.name).toBe('foo-1')
47+
expect(generator.context.foo).toBe(1)
48+
49+
const again = generator.generateClassName('foo')
50+
expect(again).toBe(first)
51+
})
52+
53+
it('falls back to default generator and skips reserved results', () => {
54+
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined)
55+
const generator = new ClassGenerator({
56+
reserveClassName: [/^tw-a$/u],
57+
log: true,
58+
})
59+
60+
const result = generator.generateClassName('foo')
61+
expect(result.name).toBe('tw-b')
62+
expect(generator.newClassSize).toBe(2)
63+
expect(logSpy).toHaveBeenCalledWith('The class name has been reserved. tw-a')
64+
65+
logSpy.mockRestore()
66+
})
67+
})

packages/shared/test/index.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as shared from '@/index'
2+
3+
describe('package entrypoint', () => {
4+
it('exposes key helpers', () => {
5+
expect(typeof shared.ClassGenerator).toBe('function')
6+
expect(typeof shared.defu).toBe('function')
7+
expect(typeof shared.defaultMangleClassFilter).toBe('function')
8+
})
9+
})

0 commit comments

Comments
 (0)