@evertondgn/solid-tooling 0.2.0 → 0.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.
package/README.md CHANGED
@@ -1,9 +1,11 @@
1
1
  # @evertondgn/solid-tooling
2
2
 
3
- Lint preset, formatter config and TypeScript base for SolidJS 2 applications
3
+ Lint preset, formatter config, TypeScript configs, test configs, the Knip and
4
+ Stylelint configs and the Vite+ application config for SolidJS 2 applications
4
5
  built with [Vite+](https://viteplus.dev/guide/).
5
6
 
6
- The package contains the lint, format and TypeScript setup of
7
+ The package contains the lint, format, TypeScript, test, build, dead code and
8
+ CSS lint setup of
7
9
  [boilerplate_solidjs](https://github.com/everton-dgn/boilerplate_solidjs), a
8
10
  SolidJS application template. Applications created from it install the package
9
11
  instead of copying the files, so a rule change reaches them through a version
@@ -24,8 +26,24 @@ exact versions the package is tested with. Install the versions listed in its
24
26
  `peerDependencies`. `typescript` is not a peer dependency: the package is tested
25
27
  with the version in its `devDependencies`.
26
28
 
27
- `eslint-plugin-solid` comes with the package, so the application does not
28
- declare it.
29
+ `eslint-plugin-solid`, `stylelint` and `stylelint-use-nesting` come with the
30
+ package, so the application does not declare them.
31
+
32
+ Some entries need tools the application already runs:
33
+
34
+ | Entry | The application installs |
35
+ | -------------- | ------------------------------------------------------------------------------- |
36
+ | `./vitest` | `@vitest/browser-playwright`, `@vitest/coverage-v8`, `happy-dom`, `playwright` |
37
+ | `./playwright` | `@playwright/test`, an optional peer dependency pinned like the others |
38
+ | `./knip` | `knip` and `filesystem-routing`, optional peer dependencies pinned the same way |
39
+
40
+ `./vite` also needs the test tools of `./vitest`, and loads four plugin
41
+ packages: `@solidjs/vite-plugin`, `nitro`, `filesystem-routing` and
42
+ `unplugin-icons`. They are optional peer dependencies pinned like the others.
43
+ Install the versions listed in `peerDependencies` as direct dependencies of the
44
+ application: Bun applies `overrides` and `patchedDependencies` only from the
45
+ root `package.json`, and the Knip plugin of `unplugin-icons` only turns on with
46
+ a direct dependency.
29
47
 
30
48
  ## Install
31
49
 
@@ -68,6 +86,323 @@ export default defineConfig({ fmt, lint })
68
86
 
69
87
  `vp check`, `vp lint` and `vp fmt` read both configs from `vite.config.ts`.
70
88
 
89
+ ## Tests
90
+
91
+ `solidTest` returns the `test` block of `vite.config.ts`, with three Vitest
92
+ projects:
93
+
94
+ | Project | Files | Environment |
95
+ | --------- | -------------------------------- | ------------------------------------------------ |
96
+ | `node` | `src/**/*.node.test.{ts,tsx}` | Node, with stand-ins for the build-only modules |
97
+ | `dom` | `src/**/*.dom.test.{ts,tsx}` | happy-dom at `http://localhost/en` |
98
+ | `browser` | `src/**/*.browser.test.{ts,tsx}` | Chromium through Playwright, with the CSS loaded |
99
+
100
+ `defineSolidApp` from `./vite` already calls `solidTest`. An application that
101
+ builds its own Vite config calls it directly:
102
+
103
+ ```ts
104
+ // vite.config.ts
105
+ import { solidTest } from '@evertondgn/solid-tooling/vitest'
106
+ import solid from '@solidjs/vite-plugin'
107
+ import { defineConfig } from 'vite-plus'
108
+
109
+ export default defineConfig({
110
+ test: solidTest({
111
+ root: import.meta.dirname,
112
+ plugins: () => [solid({ serverFunctions: true })]
113
+ })
114
+ })
115
+ ```
116
+
117
+ `plugins` is a function that returns the Vite plugins the `dom` and `browser`
118
+ projects need to compile components: Solid, routes, icons and i18n. The preset
119
+ calls it once per project, because each project starts its own Vite server and a
120
+ plugin instance keeps the state of one server. The preset also sets the coverage
121
+ thresholds, the reporters and the workers on CI, passes `BASE_URL_TEST`, `HOST`
122
+ and `PORT` to the tests when they are set, and resets the Paraglide locale
123
+ before each browser test.
124
+
125
+ The `node` project does not load the Solid and routing plugins, so a plugin of
126
+ the preset serves `server-only`, `virtual:file-routes` and
127
+ `virtual:solid-server-function-handler`. Knip does not know `server-only`, a
128
+ virtual module of `@solidjs/vite-plugin`: pass it in the `ignoreDependencies` of
129
+ [`defineKnipConfig`](#knip).
130
+
131
+ `definePlaywrightConfig` builds the Playwright config from the application root:
132
+
133
+ ```ts
134
+ // playwright.config.ts
135
+ import { definePlaywrightConfig } from '@evertondgn/solid-tooling/playwright'
136
+
137
+ export default definePlaywrightConfig({ root: import.meta.dirname })
138
+ ```
139
+
140
+ It reads `BASE_URL_TEST` from `.env.test`, builds the application (`e2e` mode
141
+ for the fixture routes, `production` with `TEST_PRODUCTION_ROUTES=true`), starts
142
+ the preview, and checks the server log for private markers when the suite ends.
143
+ `TEST_PREBUILT=true` reuses an existing production build.
144
+
145
+ | Option | Default | Use |
146
+ | ---------------- | ----------------- | ---------------------------------------------------------------------- |
147
+ | `testDir` | `src/tests/pages` | Folder of the `*.e2e.test.ts` files |
148
+ | `fixtureServers` | none | Extra servers for the fixture routes, as `{ command, url }` |
149
+ | `suites` | none | Globs of serial suites: `serverLogs`, `markdown` and `publicationLoad` |
150
+
151
+ Each serial suite gets its own project: `markdown` runs with one worker,
152
+ `serverLogs` runs last as the teardown of the others, and `publicationLoad` runs
153
+ alone, against two servers, with `TEST_PUBLICATION_LOAD=true`.
154
+
155
+ ## Application config
156
+
157
+ `defineSolidApp` returns the whole `vite.config.ts` of a boilerplate
158
+ application: the Solid, routing, icons and Nitro plugins, the headers of the
159
+ static files, the build, CSS, server and preview blocks, the `vp run` tasks, and
160
+ `lint`, `fmt` and `test` from the presets above.
161
+
162
+ ```ts
163
+ // vite.config.ts
164
+ import { defineSolidApp } from '@evertondgn/solid-tooling/vite'
165
+
166
+ import { SECURITY_HEADERS } from './src/middleware/securityHeaders/constants.ts'
167
+
168
+ export default defineSolidApp({
169
+ root: import.meta.dirname,
170
+ middleware: './src/middleware/index.ts',
171
+ securityHeaders: SECURITY_HEADERS,
172
+ staticPaths: ['/favicon/**']
173
+ })
174
+ ```
175
+
176
+ | Option | Default | Use |
177
+ | ----------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
178
+ | `root` | required | Application folder. `.env`, `.env.test`, `project.inlang/settings.json` and the icon folders resolve from it |
179
+ | `securityHeaders` | required | Values of `strict-transport-security`, `x-content-type-options` and `cross-origin-resource-policy` for the static files |
180
+ | `middleware` | none | `start.middleware` of the Solid plugin. It has to sit inside the application folder |
181
+ | `serverSetup` | `serverFunctions: true` | `serverFunctions.serverSetup` of the Solid plugin |
182
+ | `e2e` | production paths | `{ middleware, routes }` for `--mode e2e`: the middleware chain and the route folder of the fixtures, without route types |
183
+ | `icons` | none | Own `unplugin-icons` collections, as name to folder |
184
+ | `publicAssets` | none | `publicAssets` of Nitro. Each entry needs a `baseURL`, which also gets the static headers |
185
+ | `staticPaths` | none | Other static paths that get the headers, such as `/favicon/**` |
186
+ | `routeRules` | none | More Nitro route rules, applied after the static ones. A rule with the same path replaces the static one |
187
+ | `extend` | none | `(context) => ({ plugins, nitroModules })`, for plugins and Nitro modules the application still owns |
188
+ | `tasks` | none | Adds or replaces `vp run` tasks by name |
189
+ | `workspace` | none | `{ dependsOn, inputs }` for an application inside a monorepo, whose `bun.lock`, `bunfig.toml` and package builds sit at the root |
190
+
191
+ The preset fixes the rest:
192
+
193
+ - Plugins, in this order: the `extend` plugins, `unplugin-icons` (Solid
194
+ compiler, `aria-hidden` on every icon), `@solidjs/vite-plugin` with SSR and
195
+ server functions, `filesystem-routing` with HTTP methods and the route types
196
+ in `src/@types/routes.d.ts`, and Nitro with the `vercel` preset. The `dom` and
197
+ `browser` test projects get the `extend` plugins, the icons, Solid with server
198
+ functions only and the routes. The `test` mode loads no application plugin and
199
+ reads `.env.test` into `process.env`.
200
+ - The static headers go to `/assets/**`, each `publicAssets` folder, the
201
+ `staticPaths`, `/<locale>/**.md` for each locale of
202
+ `project.inlang/settings.json`, `/llms.txt`, `/llms-full.txt`, `/sitemap.xml`,
203
+ `/sitemaps/pages/**` and `/_markdown/static.json`. The static files leave the
204
+ CDN without going through the middleware, so these rules give them the headers
205
+ the middleware writes on the other responses.
206
+ - `build.manifest` only in the `e2e` mode, which the `TEST_PREBUILT` check of
207
+ `./playwright` relies on, and no comments in the output.
208
+ - Lightning CSS, with CSS Modules classes named `[content-hash]_[local]`.
209
+ - `resolve.tsconfigPaths`, and the dev server and the preview on the `HOST` and
210
+ `PORT` of `.env`. The dev server forwards the browser console to the terminal.
211
+
212
+ The plugins are imported inside `lazyPlugins`, so `vp lint`, `vp fmt` and
213
+ `vp check` read the config without loading them.
214
+
215
+ Nitro compiles the route rules with the `preMerge` of h3, which refuses rules
216
+ that overlap in part, such as `/pt/blog/**` next to `/pt/**.md`, and falls back
217
+ to its simple mode, with the same result and a warning. `defineSolidApp` throws
218
+ when a `publicAssets` entry has no `baseURL`, or one with a trailing slash, and
219
+ when a static path does not start with `/`. Build, dev and preview fail to load
220
+ the plugins when a `publicAssets` folder or a static path sits inside a locale
221
+ folder.
222
+
223
+ ### `extend`
224
+
225
+ `extend` runs inside `lazyPlugins`: once for the application in build, dev and
226
+ preview (`target: 'app'`), and once for each test project with a Vite server
227
+ (`target: 'test'`). Every load of the config calls it again, so a build also
228
+ calls it for the two test projects, whose plugins Vite does not use; keep side
229
+ effects out of it. It never runs while Vite+ only reads the config. Return new
230
+ plugin instances on each call. The `plugins` go before the fixed ones, and the
231
+ `nitroModules` go to Nitro; the `test` target ignores them. One call returns
232
+ both, so a value that a plugin and a module share, such as a build id, stays the
233
+ same.
234
+
235
+ ```ts
236
+ export default defineSolidApp({
237
+ root: import.meta.dirname,
238
+ securityHeaders: SECURITY_HEADERS,
239
+ async extend() {
240
+ const { i18nPlugin } = await import('./tooling/i18n/i18nPlugin/index.ts')
241
+ return { plugins: [i18nPlugin()] }
242
+ }
243
+ })
244
+ ```
245
+
246
+ ### Tasks
247
+
248
+ | Task | Command |
249
+ | ------------------- | ------------------------------------------------------------------------------------------------------- |
250
+ | `i18n-generate` | `bun tooling/i18n/generate.ts` |
251
+ | `lint-css` | `bun tooling/css/check.ts` |
252
+ | `dead-code-project` | `knip-bun --config tooling/knip.ts`, then again with `--strict` |
253
+ | `lint-project` | `vp lint`, after `lint-css` |
254
+ | `check-project` | `vp fmt --check && vp lint`, after `lint-css` |
255
+ | `typecheck-node` | `tsc --project tsconfig.node.json` |
256
+ | `test-tooling` | `node --test` over `tooling/**/__tests__` except `tooling/release/`, three workers on CI, with no cache |
257
+
258
+ The commands run the scripts of the boilerplate layout. An application without
259
+ one of them replaces the task through `tasks`. Knip reads task commands only
260
+ from a `run.tasks` block written in `vite.config.ts`, so it does not see these:
261
+ list the scripts they run as Knip entries.
262
+
263
+ With `workspace`, the cache inputs read `bun.lock`, `bunfig.toml` and the
264
+ `inputs` from the workspace root, and `lint-css`, `dead-code-project` and
265
+ `typecheck-node` depend on the tasks in `dependsOn`:
266
+
267
+ ```ts
268
+ workspace: {
269
+ dependsOn: ['@evertondgn/solid-tooling#build'],
270
+ inputs: ['packages/solid-tooling/dist/**']
271
+ }
272
+ ```
273
+
274
+ ### Changing the result
275
+
276
+ Wrap the result in `defineConfig` and merge what the application needs. Vite+
277
+ removes the plugins it injected before it injects them again, so they are not
278
+ duplicated:
279
+
280
+ ```ts
281
+ import { defineSolidApp } from '@evertondgn/solid-tooling/vite'
282
+ import { defineConfig, mergeConfig } from 'vite-plus'
283
+
284
+ const app = defineSolidApp({ root: import.meta.dirname, securityHeaders })
285
+
286
+ export default defineConfig(env =>
287
+ mergeConfig(app(env), { server: { strictPort: true } })
288
+ )
289
+ ```
290
+
291
+ ## Knip
292
+
293
+ `defineKnipConfig` returns the Knip config of the application as a function, the
294
+ form Knip accepts as the default export of its config file:
295
+
296
+ ```ts
297
+ // tooling/knip.ts
298
+ import path from 'node:path'
299
+
300
+ import { defineKnipConfig } from '@evertondgn/solid-tooling/knip'
301
+
302
+ export default defineKnipConfig({
303
+ root: path.resolve(import.meta.dirname, '..'),
304
+ ignoreDependencies: ['server-only']
305
+ })
306
+ ```
307
+
308
+ Run it with `knip-bun --config tooling/knip.ts`, then with `--strict`. The
309
+ config lists:
310
+
311
+ - `src/entry-server.tsx`, `src/entry-client.tsx`, `src/middleware/index.ts`,
312
+ `env.ts` and `paraglide/runtime.js` as production entries. The Solid plugin
313
+ and the Paraglide runtime load them by path, and Knip does not read the Solid
314
+ plugin when `vite.config.ts` imports it with `import()`, as `./vite` does. An
315
+ application that imports `solid()` statically, with `start`, passes
316
+ `solidPluginImport: 'static'`: Knip's Solid plugin then finds the first four
317
+ itself, and listing them again would be a configuration hint.
318
+ - Every page and HTTP handler that `filesystem-routing` finds in `src/routes/`,
319
+ as production entries, and in `src/tests/fixtures/routes/` and
320
+ `src/tests/fixtures/e2e/routes/`, as test entries. The `filesystem-routing`
321
+ plugin of Knip is turned off, because it takes every file of `src/routes/` as
322
+ an entry and would hide unused helpers next to the routes.
323
+ - `src/**/*.{ts,tsx}` and `tooling/**/*.ts` as the project, with the tests out
324
+ of the production files.
325
+
326
+ The config turns on `treatConfigHintsAsErrors`, and Knip reports an entry or a
327
+ project pattern without files as a configuration hint. So each default above
328
+ enters only when the application has the file or the folder.
329
+
330
+ | Option | Default | Use |
331
+ | -------------------- | ----------------- | -------------------------------------------------------------- |
332
+ | `root` | required | The application folder |
333
+ | `configFile` | `tooling/knip.ts` | Path of the config file, relative to `root` |
334
+ | `entry` | none | More entries, such as a public API that only test fixtures use |
335
+ | `solidPluginImport` | `'lazy'` | `'static'` when `vite.config.ts` imports `solid()` statically |
336
+ | `ignoreBinaries` | none | Passed to Knip as is |
337
+ | `ignoreDependencies` | none | Passed to Knip as is |
338
+ | `ignoreIssues` | none | Passed to Knip, merged with the entry for `configFile` |
339
+ | `exclude` | none | Issue types to leave out, such as `catalogReferences` |
340
+
341
+ Knip adds the file given to `--config` to the entries in production mode, and
342
+ `--strict` only accepts `dependencies` there, so it reports this package as an
343
+ unlisted dependency of the config file. The config ignores `unlisted` issues in
344
+ `configFile` only: an import of this package from `src/` is still reported. An
345
+ application that keeps the config somewhere else passes its path in
346
+ `configFile`.
347
+
348
+ Nothing that depends on the application is a default. An ignore that matches
349
+ nothing is a configuration hint too, so `server-only` and the binaries of the
350
+ scripts are options. For a key without an option, wrap the function:
351
+
352
+ ```ts
353
+ const configure = defineKnipConfig({ root })
354
+
355
+ export default async function configureKnip() {
356
+ return { ...(await configure()), ignoreUnresolved: ['virtual:example'] }
357
+ }
358
+ ```
359
+
360
+ ## CSS lint
361
+
362
+ `checkCss(root)` lints `src/**/*.css` with Stylelint and the CSS policy of the
363
+ boilerplate, and checks the name of every CSS file in `src/` as
364
+ `project/css-filename` does for imports: `styles.module.css`, or `camelCase.css`
365
+ inside `src/theme/`. It returns the report and whether anything failed:
366
+
367
+ ```ts
368
+ // tooling/css/check.ts
369
+ import path from 'node:path'
370
+ import process from 'node:process'
371
+
372
+ import { checkCss } from '@evertondgn/solid-tooling/stylelint'
373
+
374
+ const { report, hasErrors } = await checkCss(
375
+ path.resolve(import.meta.dirname, '../..')
376
+ )
377
+
378
+ process.stdout.write(report)
379
+ if (hasErrors) process.exitCode = 1
380
+ ```
381
+
382
+ `defineStylelintConfig(root)` returns the Stylelint config that `checkCss` uses.
383
+ The policy:
384
+
385
+ - Classes use snake_case, nesting goes at most two levels deep, and adjacent
386
+ state selectors and media queries are nested (`stylelint-use-nesting`).
387
+ - Unknown or invalid properties, values, units, selectors, at-rules and media
388
+ features are errors, as are deprecated and unmatchable selectors.
389
+ - Outside `src/theme/`, colors, gradients, radii, shadows, font sizes and
390
+ weights and `z-index` come from tokens with `var()`, from the matching token
391
+ family. Literal colors, color functions and system colors are errors.
392
+ - Inside `src/theme/`, the token rules are off, so the theme can define the
393
+ values with `oklch()`, `oklab()`, `color-mix()`, `light-dark()` and system
394
+ colors. Hex, named colors and the `rgb()`, `hsl()`, `hwb()`, `lab()`, `lch()`
395
+ and `color()` functions are still errors there.
396
+ - `var()` and animation names resolve against `src/theme/tokens/**/*.css` and
397
+ `src/theme/class/animation.css`. Stylelint stops the whole run when a
398
+ reference matches no file, so each one is passed only when it exists.
399
+ - An exception needs a `stylelint-disable` comment with the reason after `--`.
400
+
401
+ `stylelint` and `stylelint-use-nesting` are dependencies of this package. The
402
+ config passes the plugin by a path resolved from the package, because Stylelint
403
+ looks up a plugin name from the application and its working directory, where it
404
+ is not installed.
405
+
71
406
  ## Project layout the preset expects
72
407
 
73
408
  The rules match file paths relative to the folder where lint runs, so run it
@@ -205,20 +540,30 @@ overrides: [
205
540
 
206
541
  ## Entry points
207
542
 
208
- | Entry | Content |
209
- | ----------------------- | ------------------------------------------------------------------------- |
210
- | `./lint` | `lint`: the lint preset, with the plugins below registered |
211
- | `./fmt` | `fmt`: the formatter config |
212
- | `./tsconfig.base.json` | Compiler options, without application paths |
213
- | `./project-plugin` | The `project` rules |
214
- | `./backend-plugin` | The `backend` rules |
215
- | `./architecture-plugin` | The `architecture` rule |
216
- | `./solid-plugin` | Re-export of `eslint-plugin-solid` |
217
- | `./css-filename` | `cssFilenameError(path)`, the CSS naming check, for a script outside lint |
218
-
219
- An application only imports `./lint`, `./fmt` and `./tsconfig.base.json`. The
220
- plugin entries exist because Oxlint loads plugins by specifier, from the
221
- application.
543
+ | Entry | Content |
544
+ | ------------------------ | ------------------------------------------------------------------------- |
545
+ | `./lint` | `lint`: the lint preset, with the plugins below registered |
546
+ | `./fmt` | `fmt`: the formatter config |
547
+ | `./tsconfig.base.json` | Compiler options, without application paths |
548
+ | `./tsconfig.app.json` | The base plus the `types` and `include` of the boilerplate layout |
549
+ | `./vite` | `defineSolidApp(options)`, the whole `vite.config.ts` of an application |
550
+ | `./vitest` | `solidTest(options)`, the `test` block of `vite.config.ts` |
551
+ | `./playwright` | `definePlaywrightConfig(options)`, the Playwright config |
552
+ | `./vitest-setup/browser` | Setup file of the browser project, loaded by `./vitest` |
553
+ | `./playwright-teardown` | Server log check, loaded by `./playwright` |
554
+ | `./project-plugin` | The `project` rules |
555
+ | `./backend-plugin` | The `backend` rules |
556
+ | `./architecture-plugin` | The `architecture` rule |
557
+ | `./solid-plugin` | Re-export of `eslint-plugin-solid` |
558
+ | `./css-filename` | `cssFilenameError(path)`, the CSS naming check, for a script outside lint |
559
+ | `./knip` | `defineKnipConfig(options)`, the Knip config |
560
+ | `./stylelint` | `checkCss(root)` and `defineStylelintConfig(root)`, the CSS lint |
561
+
562
+ An application imports `./vite`, which brings `./lint`, `./fmt` and `./vitest`
563
+ along, `./playwright`, `./knip`, `./stylelint` and one of the TypeScript
564
+ configs. An application that only lints imports `./lint` and `./fmt`. The plugin
565
+ entries exist because Oxlint loads plugins by specifier, from the application;
566
+ the setup and teardown entries are loaded by the test presets.
222
567
 
223
568
  ## Formatter
224
569
 
@@ -230,8 +575,36 @@ generated route types, SVG and lock files.
230
575
 
231
576
  `tsconfig.base.json` targets ES2024 with bundler resolution, keeps JSX for the
232
577
  Solid compiler (`jsxImportSource: @solidjs/web`), emits nothing and turns on the
233
- unused code and unchecked index checks. It has no `paths`, `include` or `types`:
234
- those depend on the application and stay in its `tsconfig.json`.
578
+ unused code and unchecked index checks. It has no `paths`, `include` or `types`.
579
+
580
+ `tsconfig.app.json` extends the base with the `types` of a boilerplate
581
+ application and an `include` resolved from the application folder with
582
+ `${configDir}`, which needs TypeScript 5.5 or later. Each entry of `types` comes
583
+ from a package the application installs:
584
+
585
+ | Entry of `types` | Package |
586
+ | --------------------------------------- | --------------------------- |
587
+ | `node` | `@types/node` |
588
+ | `vite/client` | `vite` |
589
+ | `vite-plus/test/globals` | `vite-plus` |
590
+ | `@testing-library/jest-dom/vitest` | `@testing-library/jest-dom` |
591
+ | `@solidjs/vite-plugin/boundary-modules` | `@solidjs/vite-plugin` |
592
+ | `@solidjs/vite-plugin/virtual-modules` | `@solidjs/vite-plugin` |
593
+
594
+ The application still declares its `paths`:
595
+
596
+ ```json
597
+ {
598
+ "extends": "@evertondgn/solid-tooling/tsconfig.app.json",
599
+ "compilerOptions": {
600
+ "paths": { "@/*": ["./src/*"], "@paraglide/*": ["./paraglide/*"] }
601
+ }
602
+ }
603
+ ```
604
+
605
+ Playwright reads the `tsconfig.json` of the test files with its own loader,
606
+ which does not expand `${configDir}`, so paths declared in the package would
607
+ break the `@/` imports of the end-to-end tests.
235
608
 
236
609
  ## Known limits
237
610
 
@@ -246,6 +619,19 @@ those depend on the application and stay in its `tsconfig.json`.
246
619
  installed even in an application without translations.
247
620
  - Some rule messages point to `docs/architecture.md` and `AGENTS.md`. Those
248
621
  files belong to the application created from boilerplate_solidjs.
622
+ - The test presets expect the boilerplate layout: tests named by suffix
623
+ (`.node.test`, `.dom.test`, `.browser.test`, `.e2e.test`), page tests in
624
+ `src/tests/pages/`, Paraglide output in `paraglide/` and `.env.test` at the
625
+ root.
626
+ - The Knip and CSS configs expect the same layout: pages in `src/routes/`,
627
+ fixture routes in `src/tests/fixtures/` and the theme in `src/theme/`.
628
+ - The Knip config ignores every unlisted dependency in its own file, not only
629
+ this package.
630
+ - `./vite` expects the same layout: its tasks run scripts in `tooling/`, and the
631
+ Markdown rules need the locale prefix of the Paraglide `urlPatterns` in every
632
+ URL. Nitro always uses the `vercel` preset. The plugin packages are pinned, so
633
+ a new version of Solid, Nitro, the router or the icons plugin needs a package
634
+ version tested with it.
249
635
 
250
636
  ## Versions
251
637
 
@@ -0,0 +1,9 @@
1
+ import { glob } from "node:fs/promises";
2
+ // src/has-match.ts
3
+ async function hasMatch({ root, pattern }) {
4
+ const matches = glob(pattern, { cwd: root });
5
+ const { done } = await matches.next();
6
+ await matches.return?.();
7
+ return !done;
8
+ }
9
+ export { hasMatch as t };
@@ -0,0 +1,12 @@
1
+ import { KnipConfiguration } from "knip";
2
+ type AppSettings = Pick<KnipConfiguration, 'ignoreBinaries' | 'ignoreDependencies' | 'ignoreIssues' | 'exclude'>;
3
+ type SolidPluginImport = 'lazy' | 'static';
4
+ type KnipOptions = AppSettings & {
5
+ root: string;
6
+ configFile?: string;
7
+ entry?: string[];
8
+ solidPluginImport?: SolidPluginImport;
9
+ };
10
+ type ConfigureKnip = () => Promise<KnipConfiguration>;
11
+ export declare function defineKnipConfig({ root, configFile, entry, solidPluginImport, ignoreBinaries, ignoreDependencies, ignoreIssues, exclude }: KnipOptions): ConfigureKnip;
12
+ export {};
package/dist/knip.mjs ADDED
@@ -0,0 +1,92 @@
1
+ import { t as hasMatch } from "./has-match-BITlj0EE.mjs";
2
+ import path from "node:path";
3
+ import { PageFileSystemRouter } from "filesystem-routing";
4
+ import { DEFAULT_EXTENSIONS } from "filesystem-routing/vite";
5
+ // src/knip/index.ts
6
+ const DEFAULT_CONFIG_FILE = "tooling/knip.ts";
7
+ const SOLID_ENTRIES = [
8
+ "src/entry-server.tsx",
9
+ "src/entry-client.tsx",
10
+ "src/middleware/index.ts",
11
+ "env.ts"
12
+ ];
13
+ const RUNTIME_ENTRIES = ["paraglide/runtime.js"];
14
+ const ROUTE_DIRECTORIES = [
15
+ {
16
+ directory: "src/routes",
17
+ isProduction: true
18
+ },
19
+ {
20
+ directory: "src/tests/fixtures/routes",
21
+ isProduction: false
22
+ },
23
+ {
24
+ directory: "src/tests/fixtures/e2e/routes",
25
+ isProduction: false
26
+ }
27
+ ];
28
+ const TOOLING_PROJECT = "tooling/**/*.ts";
29
+ function isModuleRef(value) {
30
+ return typeof value === "object" && value !== null && "src" in value && typeof value.src === "string";
31
+ }
32
+ function routeSources(route) {
33
+ return Object.entries(route).flatMap(([key, value]) => key.startsWith("$") && isModuleRef(value) ? [value.src] : []);
34
+ }
35
+ async function discoverRouteModules({ root, directory, isProduction }) {
36
+ const routes = await new PageFileSystemRouter({
37
+ dir: path.join(root, directory),
38
+ extensions: DEFAULT_EXTENSIONS,
39
+ httpMethods: true
40
+ }).getRoutes();
41
+ return [...new Set(routes.flatMap((route) => routeSources(route)))].map((source) => path.relative(root, source) + (isProduction ? "!" : ""));
42
+ }
43
+ async function conventionEntries({ root, patterns }) {
44
+ return (await Promise.all(patterns.map(async (pattern) => await hasMatch({
45
+ root,
46
+ pattern
47
+ }) ? [`${pattern}!`] : []))).flat();
48
+ }
49
+ function defineKnipConfig({ root, configFile = DEFAULT_CONFIG_FILE, entry = [], solidPluginImport = "lazy", ignoreBinaries, ignoreDependencies, ignoreIssues = {}, exclude }) {
50
+ const patterns = solidPluginImport === "static" ? RUNTIME_ENTRIES : [...SOLID_ENTRIES, ...RUNTIME_ENTRIES];
51
+ return async function configureKnip() {
52
+ const [conventions, routes, hasTooling] = await Promise.all([
53
+ conventionEntries({
54
+ root,
55
+ patterns
56
+ }),
57
+ Promise.all(ROUTE_DIRECTORIES.map((route) => discoverRouteModules({
58
+ root,
59
+ ...route
60
+ }))),
61
+ hasMatch({
62
+ root,
63
+ pattern: TOOLING_PROJECT
64
+ })
65
+ ]);
66
+ const configIssues = ignoreIssues[configFile] ?? [];
67
+ return {
68
+ entry: [.../* @__PURE__ */ new Set([
69
+ ...conventions,
70
+ ...entry,
71
+ ...routes.flat()
72
+ ])],
73
+ project: [
74
+ "src/**/*.{ts,tsx}!",
75
+ ...hasTooling ? [TOOLING_PROJECT] : [],
76
+ "!src/tests/**!",
77
+ "!src/**/__tests__/**!",
78
+ "!src/**/*.test.{ts,tsx}!"
79
+ ],
80
+ "filesystem-routing": false,
81
+ ...ignoreBinaries ? { ignoreBinaries } : {},
82
+ ...ignoreDependencies ? { ignoreDependencies } : {},
83
+ ignoreIssues: {
84
+ ...ignoreIssues,
85
+ [configFile]: [.../* @__PURE__ */ new Set([...configIssues, "unlisted"])]
86
+ },
87
+ ...exclude ? { exclude } : {},
88
+ treatConfigHintsAsErrors: true
89
+ };
90
+ };
91
+ }
92
+ export { defineKnipConfig };