@tollwyn/baukasten 2.0.1 → 3.0.1

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.
Files changed (46) hide show
  1. package/README.md +137 -5
  2. package/dist/check.d.ts +0 -2
  3. package/dist/check.js +167 -8
  4. package/dist/eslint.d.ts +2 -0
  5. package/dist/eslint.js +64 -0
  6. package/dist/gate.d.ts +32 -0
  7. package/dist/gate.js +49 -0
  8. package/dist/index.d.ts +4 -4
  9. package/dist/index.js +1 -1
  10. package/dist/knip.d.ts +14 -0
  11. package/dist/knip.js +25 -0
  12. package/dist/product.d.ts +4 -3
  13. package/dist/product.js +1 -1
  14. package/dist/server/adapter/migrate.d.ts +2 -1
  15. package/dist/server/billing.d.ts +3 -2
  16. package/dist/server/catalog.d.ts +1 -7
  17. package/dist/server/catalog.js +1 -1
  18. package/dist/server/config.d.ts +1 -1
  19. package/dist/server/core.d.ts +2 -2
  20. package/dist/server/endpoints.d.ts +0 -8
  21. package/dist/server/endpoints.js +1 -1
  22. package/dist/server/index.d.ts +3 -10
  23. package/dist/server/index.js +1 -2
  24. package/dist/server/messages.d.ts +1 -1
  25. package/dist/server/pages.d.ts +0 -3
  26. package/dist/server/pages.js +1 -1
  27. package/dist/server/ports.d.ts +3 -2
  28. package/dist/shopify/hook.d.ts +2 -1
  29. package/dist/shopify/index.d.ts +2 -7
  30. package/dist/shopify/index.js +0 -1
  31. package/dist/shopify/pages.d.ts +0 -2
  32. package/dist/shopify/pages.js +1 -1
  33. package/dist/shopify/ports.d.ts +2 -1
  34. package/dist/shopify/shops.d.ts +2 -5
  35. package/dist/shopify/shops.js +2 -2
  36. package/dist/testing/browser.d.ts +0 -2
  37. package/dist/testing/browser.js +1 -1
  38. package/dist/testing/index.d.ts +5 -5
  39. package/dist/testing/index.js +2 -2
  40. package/dist/testing/shopify.d.ts +2 -1
  41. package/dist/testing/smtp.d.ts +2 -3
  42. package/dist/testing/smtp.js +1 -1
  43. package/dist/testing/stripe.d.ts +5 -5
  44. package/dist/ui/Linked.svelte.d.ts +2 -2
  45. package/dist/ui/index.d.ts +0 -1
  46. package/package.json +17 -9
package/README.md CHANGED
@@ -12,6 +12,7 @@ A product is a SvelteKit 3 app with `@sveltejs/adapter-node` 6 and Svelte 5, tes
12
12
  ```sh
13
13
  npm install --save-exact @tollwyn/baukasten@<version>
14
14
  npm install --save-dev @sveltejs/adapter-node@6 @playwright/test smtp-server
15
+ npm install --save-dev eslint@10 knip@6 typescript-eslint@8 eslint-plugin-svelte@3 @eslint-community/eslint-plugin-eslint-comments@4 @eslint/js@10 globals@17
15
16
  npx --no-install baukasten-stubs
16
17
  # fill in what the product owns (below)
17
18
  npx --no-install baukasten-check
@@ -57,6 +58,8 @@ them: `baukasten.config.ts`, `src/app.css`, `src/lib/server/baukasten.ts` and
57
58
  previous version must run against the new schema). Every table that holds data of an account
58
59
  points at it with `REFERENCES baukasten.accounts (id) ON DELETE CASCADE`. It reads the tables of
59
60
  the baukasten only through its functions and never changes them.
61
+ - `eslint.config.js`, `knip.config.ts`, `tsconfig.json` and the script `gate` of `package.json`, as
62
+ «The gate» below says.
60
63
  - `.env.example`, naming every variable without a value (`envExampleProblems` of `/testing`).
61
64
  - `playwright.config.ts` with `testDir: 'e2e'` and `TEST_DATABASE_URL`, a Postgres server on which the
62
65
  tests create their databases. It sets the variables the configuration of the product requires in
@@ -98,15 +101,79 @@ it. `muster/` shows all of it with Tailwind CSS 4, Skeleton and a font from font
98
101
  `src/app.css`, which Vite bundles (a small font inlined as `data:`, which the policy allows), and
99
102
  pictures in `static/` or the product's code. Nothing from another server: the policy refuses it.
100
103
 
104
+ ## The gate
105
+
106
+ `npm run gate` is the gate of the product, which its workflow runs. What lints in it is the
107
+ baukasten's, and `baukasten-check` holds it:
108
+
109
+ - `eslint.config.js` spreads the config of `@tollwyn/baukasten/eslint` first and adds the product's
110
+ own rules on top:
111
+
112
+ ```js
113
+ import baukasten from '@tollwyn/baukasten/eslint';
114
+
115
+ export default [...baukasten];
116
+ ```
117
+
118
+ The config lints every file with types: the type-checked rules of typescript-eslint
119
+ (`recommendedTypeChecked`), Svelte's recommended rules for the components, and a reason after
120
+ `--` in every comment that turns a rule off, as in `@ts-expect-error`. A component, a universal
121
+ load function, hook or matcher and the service worker import nothing of `/server`, `/shopify` or
122
+ `/testing` but types. It sets no rule of formatting, so Prettier needs no config beside it. The
123
+ product's config turns no rule off, and never names `@typescript-eslint/no-floating-promises`,
124
+ `@typescript-eslint/no-misused-promises`, `@typescript-eslint/ban-ts-comment`,
125
+ `@eslint-community/eslint-comments/require-description` or `disableTypeChecked`: a promise nobody
126
+ awaits loses its error, `@ts-nocheck` and `@ts-ignore` take code out of the type check, and a rule
127
+ turned off without its reason hides why. Where a rule must stay off at one place, the comment
128
+ there says why. Imports of its own a product restricts with ESLint's `no-restricted-imports`:
129
+ `@typescript-eslint/no-restricted-imports` would replace the config's for the same files.
130
+
131
+ - `knip.config.ts` spreads the preset of `@tollwyn/baukasten/knip`, which knows the routes, the
132
+ stubs, the connection test and what the baukasten loads, and adds what is the product's own:
133
+
134
+ ```ts
135
+ import baukasten from '@tollwyn/baukasten/knip';
136
+ import type { KnipConfig } from 'knip';
137
+
138
+ export default { ...baukasten } satisfies KnipConfig;
139
+ ```
140
+
141
+ It sets no `rules`, `include` or `exclude`, and no other configuration of knip lies beside it
142
+ (`knip.json` and the like, or `knip` in `package.json`): knip finds unused files, exports and
143
+ dependencies. The preset reads the configuration of Playwright without running it; what that
144
+ loads besides its tests (`globalSetup`, a `webServer`, a reporter of its own) the product adds to
145
+ `playwright.entry`.
146
+
147
+ - The script `gate` runs `svelte-check --tsconfig ./tsconfig.json --fail-on-warnings` (without
148
+ `--tsconfig`, svelte-check reports nothing of the TypeScript files),
149
+ `eslint . --config eslint.config.js --max-warnings 0` (with `--config`, an `eslint.config.js` in a
150
+ folder below takes no files from it) and `knip`, each exactly so, after `svelte-kit sync`, which
151
+ writes the types all three read: in itself or in a script it runs with `npm run`, among commands
152
+ joined by `&&` and nothing else, since `;`, `||`, `|` or `&` could let a failure pass. The
153
+ `compilerOptions` that `vite.config.ts` passes to `sveltekit()` set no `warningFilter`:
154
+ svelte-check compiles with them and would not see the warnings it filters. One warning at its
155
+ place a `svelte-ignore` comment silences.
156
+
157
+ - `tsconfig.json` sets `strict` and `noUncheckedIndexedAccess` to `true` in its own
158
+ `compilerOptions` and turns off no part of `strict`. Its `include` names `src`, `e2e` and the
159
+ TypeScript in the root (`*.ts`), and not the JavaScript in the root: the config lints that through
160
+ a project of its own, with the same options, and typescript-eslint refuses a file that is in both.
161
+
162
+ ESLint, knip and the plugins the config imports are dev dependencies of the product, installed with
163
+ the third command above, and `baukasten-check` holds their versions. They are neither dependencies
164
+ nor peers of the baukasten: npm puts every dependency and every peer of a dependency into the image
165
+ of the product, and these have no business there.
166
+
101
167
  ## The check and the connection test
102
168
 
103
169
  `baukasten-check` runs in the root of the product, before its gate, against the installed version:
104
170
  the stubs are there and unchanged, `deploy/auslieferung.json` names the webhook and the rights of
105
- the baukasten, SvelteKit and Svelte fit its peer ranges and adapter-node is version 6, `overrides`
106
- in `package.json` replaces nothing the baukasten depends on and its `imports` map `#lib/*`, no
107
- migration of the product names the schema `baukasten` beyond its references to
108
- `baukasten.accounts`, and the configuration and `SVELTEKIT_OPTIONS` are in place. Exit 0 when the
109
- product fits; else exit 1 with one line per problem.
171
+ the baukasten, SvelteKit and Svelte fit its peer ranges, adapter-node is version 6 and ESLint, knip
172
+ and the plugins of the config are in their versions, `overrides` in `package.json` replaces nothing
173
+ the baukasten depends on and its `imports` map `#lib/*`, no migration of the product names the
174
+ schema `baukasten` beyond its references to `baukasten.accounts`, the configuration and
175
+ `SVELTEKIT_OPTIONS` are in place, and the gate is as above. Exit 0 when the product fits; else exit
176
+ 1 with one line per problem. It reads these files and runs none of them.
110
177
 
111
178
  `e2e/baukasten.spec.ts` runs the connection test in the gate of the product, against the built
112
179
  product with the doubles of Stripe and mail: sign-up with a referral tag, mail and confirmation,
@@ -138,6 +205,7 @@ connection test among them.
138
205
  ```sh
139
206
  npm install --save-exact @tollwyn/baukasten@<version>
140
207
  npm install --save-dev @sveltejs/adapter-node@6 @playwright/test smtp-server
208
+ npm install --save-dev eslint@10 knip@6 typescript-eslint@8 eslint-plugin-svelte@3 @eslint-community/eslint-plugin-eslint-comments@4 @eslint/js@10 globals@17
141
209
  npx --no-install baukasten-stubs shopify
142
210
  # fill in what the app owns (below)
143
211
  npx --no-install baukasten-check
@@ -197,6 +265,7 @@ missing, it writes the files the app owns as a starting point: `baukasten.config
197
265
  holds data of a store points at it with `REFERENCES baukasten.shops (domain) ON DELETE CASCADE`:
198
266
  so `shop/redact` deletes them with the store.
199
267
  - `.env.example` (`envExampleProblems` of `/testing`) and `playwright.config.ts`, as in a web app.
268
+ - The gate, as a web app's («The gate»).
200
269
  - The image: as a web app's, with `shopify.app.toml` in its root as well.
201
270
  - `deploy/auslieferung.json`: version 2, `art: shopify`, `name`, `subdomain` (without `shopify` or
202
271
  `example`, which the review refuses in an address), the migration
@@ -264,6 +333,69 @@ the baukasten do:
264
333
  was reached from the logs, and tells the stores concerned by email without undue delay, as the
265
334
  privacy text promises.
266
335
 
336
+ # From 2.x to 3.0
337
+
338
+ 3.0 holds the gate of a product: `baukasten-check` fails until the product lints with the config and
339
+ the preset of the baukasten, and its entries export less (below). Its pages, mails, legal texts,
340
+ variables, migrations and stubs stay as they were, and nothing changes when the product runs. A
341
+ product on 2.x keeps running as it is; it takes 3.0 in one change, a web app and a Shopify app
342
+ alike. A product on 1.x goes to 2.0 first («From 1.x to 2.0»).
343
+
344
+ 1. Two commands, with a version from 3.0.0 on:
345
+
346
+ ```sh
347
+ npm install --save-exact @tollwyn/baukasten@<version>
348
+ npm install --save-dev eslint@10 knip@6 typescript-eslint@8 eslint-plugin-svelte@3 @eslint-community/eslint-plugin-eslint-comments@4 @eslint/js@10 globals@17
349
+ ```
350
+
351
+ 2. `eslint.config.js` and `knip.config.ts` as «The gate» shows them, with the product's own rules and
352
+ entries on top. What the old config set that the baukasten's sets too goes: the block that
353
+ lints the JavaScript of the root with `disableTypeChecked`, and every rule it turned off. A
354
+ `knip.json` or `knip.jsonc` goes too, its entries and ignores into `knip.config.ts`, where the
355
+ preset does not already name them. The packages only the old configs imported go with them
356
+ (`eslint-config-prettier` and the like): knip names those it finds unused.
357
+ 3. The script `gate` runs `svelte-check --tsconfig ./tsconfig.json --fail-on-warnings`,
358
+ `eslint . --config eslint.config.js --max-warnings 0` and `knip`, `tsconfig.json` sets `strict`
359
+ and `noUncheckedIndexedAccess`, and its `include` names `src`, `e2e` and `*.ts`, and no
360
+ JavaScript of the root.
361
+ 4. Where the product imports a name the entries no longer export (below), the import goes, and
362
+ svelte-check names each. A type it takes from what uses it, like
363
+ `Parameters<NonNullable<ShopifyHooks['webhook']>>[1]` for the delivery of a webhook. For
364
+ `HYDRATED` it calls `waitForHydration`, for `WEBAPP_VARIABLES` and `SHOPIFY_VARIABLES` it takes
365
+ `BAUKASTEN_VARIABLES` or the standard variables; `LIMITS`, `StripeFailed` and `stripeSignature`
366
+ stay inside the baukasten, which applies the limits, logs a failure of Stripe and signs the
367
+ webhooks of its double itself.
368
+ 5. `npx --no-install baukasten-check`, then the gate of the product. What the stricter rules find,
369
+ the product fixes at its cause; where a rule must stay off at one place, the comment says why
370
+ after `--`.
371
+
372
+ The entries export what a sample, this README, a stub or a product imports, the counterpart in
373
+ `/server` or `/shopify` of such a name, and `ApiError`, which `kit.api.respond` turns into its
374
+ answer. They no longer export:
375
+
376
+ - `@tollwyn/baukasten`: `SHOPIFY_VARIABLES`, `WEBAPP_VARIABLES` and the types `Brand`, `Catalog`,
377
+ `CatalogProduct`, `Linked`, `NavigationLink`, `Product`, `ProductEnv`, `PurchasePrice`,
378
+ `ShopifyApp`, `ShopifyPlan`, `ShopifyPrivacy`, `ShopifyStandardVariable`, `StandardVariable`,
379
+ `SubscriptionPrice` and `Texts`.
380
+ - `/server`: `LIMITS`, `StripeFailed` and the types `Account`, `ApiKey`, `CheckoutPage`, `Config`,
381
+ `Limits`, `MailConfig`, `NodeHandler`, `OpenApiDocument`, `Operator`, `Order`, `Pages`,
382
+ `ProductNumbers`, `Purchase`, `Register`, `Rule`, `Run`, `RunOptions`, `Stripe`, `Subscription`,
383
+ `SubscriptionChange` and `SubscriptionStatus`.
384
+ - `/shopify`: `LIMITS` and the types `CustomerRequest`, `Delivery`, `Limits`, `NodeHandler`, `Plan`,
385
+ `Rule`, `Run`, `RunOptions`, `ShopifyConfig` and `ShopSession`.
386
+ - `/testing`: `HYDRATED`, `stripeSignature` and the types `AppProcess`, `DoubleEndpoint`,
387
+ `DoublePrice`, `DoubleSession`, `DoubleShop`, `DoubleSubscription`, `PageLike` and
388
+ `ReceivedMail`.
389
+ - `/ui`: the types of the data of its pages, `AccountData`, `AccountForm`, `ConfirmData`,
390
+ `ConfirmForm`, `ForgotForm`, `FrameData`, `LegalBlock`, `LegalData`, `LegalInline`, `LoginData`,
391
+ `LoginForm`, `LogoutData`, `ResetData`, `ResetForm`, `SignupData`, `SignupForm`, `VerifyData` and
392
+ `VerifyForm`.
393
+
394
+ What changes in the package: the entries `/eslint` and `/knip`, and `baukasten-check` reads
395
+ `eslint.config.js`, `knip.config.ts`, `tsconfig.json`, `vite.config.ts` for filtered warnings and
396
+ the scripts of `package.json`, and wants ESLint 10, knip 6 and the plugins of the config in the
397
+ versions it names.
398
+
267
399
  # From 1.x to 2.0
268
400
 
269
401
  2.0 moves the baukasten to SvelteKit 3 and adapter-node 6, and to version 23 of the SDK of Stripe.
package/dist/check.d.ts CHANGED
@@ -12,8 +12,6 @@ export interface ProductFiles {
12
12
  /** The names in a folder, as the migration reads them; none where there is no folder. */
13
13
  names(folder: string): readonly string[];
14
14
  }
15
- /** The migration command of a Shopify app in its delivery manifest. */
16
- export declare const SHOPIFY_MIGRATION: string[];
17
15
  /** Whether `version` lies in the caret range `range` from 1.0.0 on; a pre-release lies in none. */
18
16
  export declare function fits(version: string, range: string): boolean;
19
17
  /** What keeps the product from fitting the installed baukasten; empty when it fits. */
package/dist/check.js CHANGED
@@ -1,9 +1,16 @@
1
1
  import { z } from 'zod';
2
+ import { GATE_SCRIPT, HELD_RULES, KNIP_COMMAND, LINT_COMMAND, LINT_PLUGINS, LINT_TOOLS, TYPECHECK_COMMAND } from './gate.js';
2
3
  import { KEY_RIGHTS, WEBHOOK_EVENTS, WEBHOOK_PATH } from './server/stripe.js';
3
4
  import { APP_TOML, appTomlProblems } from './shopify/toml.js';
4
5
  import { SHOPIFY_STUBS, STUBS } from './stubs.js';
5
6
  const ANGABE_PATH = 'deploy/auslieferung.json';
6
7
  const STUBS_COMMAND = 'npx --no-install baukasten-stubs';
8
+ const ESLINT_CONFIG = 'eslint.config.js';
9
+ const ESLINT_ENTRY = '@tollwyn/baukasten/eslint';
10
+ const ESLINT_USE = `import baukasten from '${ESLINT_ENTRY}', then export default [...baukasten, …]`;
11
+ const KNIP_CONFIG = 'knip.config.ts';
12
+ const KNIP_ENTRY = '@tollwyn/baukasten/knip';
13
+ const KNIP_USE = `import baukasten from '${KNIP_ENTRY}', then export default { ...baukasten, … }`;
7
14
  const ANGABE = z.looseObject({
8
15
  version: z.literal(2, 'must be 2, the version that names the art of the product'),
9
16
  art: z.literal('webapp', 'must be webapp'),
@@ -20,7 +27,7 @@ const ANGABE = z.looseObject({
20
27
  })
21
28
  });
22
29
  /** The migration command of a Shopify app in its delivery manifest. */
23
- export const SHOPIFY_MIGRATION = [
30
+ const SHOPIFY_MIGRATION = [
24
31
  'node',
25
32
  'node_modules/@tollwyn/baukasten/dist/bin/migrate.js',
26
33
  'shopify'
@@ -36,17 +43,25 @@ const REFUSED_IN_ADDRESS = ['shopify', 'example'];
36
43
  const ART = z.looseObject({ art: z.literal('shopify') });
37
44
  const PACKAGE = z.looseObject({
38
45
  overrides: z.record(z.string(), z.unknown()).optional(),
39
- imports: z.record(z.string(), z.unknown()).optional()
46
+ imports: z.record(z.string(), z.unknown()).optional(),
47
+ scripts: z.record(z.string(), z.string()).optional(),
48
+ knip: z.unknown().optional()
40
49
  });
41
50
  /** The alias the stubs import the product's src/lib by, as SvelteKit sets it up. */
42
51
  const LIB_IMPORT = { key: '#lib/*', target: './src/lib/*' };
43
52
  const PEER = z.looseObject({ version: z.string() });
44
53
  /**
45
- * What the product builds with, checked like a peer: adapter-node 5 cannot build SvelteKit 3, and the
46
- * handler the stub server.js wraps, with its event at shutdown, is that of one major. It is no peer,
47
- * since only the build needs it, and npm keeps a peer of a dependency in the image.
54
+ * What the product builds and lints with, checked like a peer: adapter-node 5 cannot build SvelteKit
55
+ * 3, and the handler the stub server.js wraps, with its event at shutdown, is that of one major; the
56
+ * config of /eslint and the preset of /knip are written for one major of each tool and plugin. None
57
+ * is a peer, since only the build and the gate need them, and npm keeps a peer of a dependency in the
58
+ * image.
48
59
  */
49
- const BUILT_WITH = { '@sveltejs/adapter-node': '^6.0.0' };
60
+ const BUILT_WITH = {
61
+ '@sveltejs/adapter-node': '^6.0.0',
62
+ ...LINT_TOOLS,
63
+ ...LINT_PLUGINS
64
+ };
50
65
  /**
51
66
  * A reference to an account of a web app or a shop of a Shopify app: the one way a migration of the
52
67
  * product names the schema baukasten.
@@ -217,7 +232,7 @@ function packageProblems(files, baukasten) {
217
232
  const parsed = json(text);
218
233
  const manifest = parsed.kind === 'json' ? PACKAGE.safeParse(parsed.value) : null;
219
234
  if (manifest === null || !manifest.success) {
220
- return ['package.json is no JSON object, or its overrides or imports are no object'];
235
+ return ['package.json is no JSON object, or its overrides, imports or scripts are no object'];
221
236
  }
222
237
  const own = new Set([
223
238
  baukasten.name,
@@ -229,8 +244,135 @@ function packageProblems(files, baukasten) {
229
244
  if (manifest.data.imports?.[LIB_IMPORT.key] !== LIB_IMPORT.target) {
230
245
  problems.push(`package.json does not map imports "${LIB_IMPORT.key}" to "${LIB_IMPORT.target}", which the stubs import from`);
231
246
  }
247
+ if (manifest.data.knip !== undefined) {
248
+ problems.push(`package.json sets knip, which knip adds to ${KNIP_CONFIG}: move it into ${KNIP_CONFIG}`);
249
+ }
250
+ return [...problems, ...gateProblems(manifest.data.scripts ?? {})];
251
+ }
252
+ /** What `npm run gate` must run, read from the scripts, which the workflow of the product runs. */
253
+ function gateProblems(scripts) {
254
+ const commands = gateCommands(scripts, GATE_SCRIPT, new Set());
255
+ return [LINT_COMMAND, KNIP_COMMAND, TYPECHECK_COMMAND]
256
+ .filter((command) => !commands.includes(command))
257
+ .map((command) => `npm run ${GATE_SCRIPT} does not run ${command}: the script ${GATE_SCRIPT} runs it, or a script it runs with npm run, among commands joined by && and nothing else`);
258
+ }
259
+ /** The script a command runs, where running it with npm is all the command does. */
260
+ function scriptRun(command) {
261
+ return command === 'npm test' ? 'test' : /^npm run (\S+)$/.exec(command)?.[1];
262
+ }
263
+ /**
264
+ * The commands a script runs, in it and in the scripts it runs with npm, each with its words joined
265
+ * by one space. Only a script of commands joined by && counts: any other operator could let a failure
266
+ * pass, wherever it stands (`a && b || true` is `(a && b) || true`).
267
+ */
268
+ function gateCommands(scripts, name, seen) {
269
+ const script = scripts[name];
270
+ if (script === undefined || seen.has(name) || /[;&|\n]/.test(script.replaceAll('&&', ''))) {
271
+ return [];
272
+ }
273
+ seen.add(name);
274
+ return script.split('&&').flatMap((part) => {
275
+ const command = part.trim().split(/\s+/).join(' ');
276
+ const run = scriptRun(command);
277
+ return run === undefined ? [command] : gateCommands(scripts, run, seen);
278
+ });
279
+ }
280
+ /** Strings as they are, comments gone: what JavaScript, TypeScript or tsconfig.json says. */
281
+ const STRINGS_OR_COMMENTS = /'(?:[^'\\\n]|\\.)*'|"(?:[^"\\\n]|\\.)*"|`(?:[^`\\]|\\.)*`|\/\/[^\n]*|\/\*[\s\S]*?\*\//g;
282
+ const withoutComments = (text) => text.replace(STRINGS_OR_COMMENTS, (match) => (match.startsWith('/') ? '' : match));
283
+ /** Whether the code imports the default export of `entry` and spreads it whole, not a part of it. */
284
+ function spreads(code, entry) {
285
+ const name = new RegExp(String.raw `\bimport\s+([\w$]+)\s+from\s*['"]${entry}['"]`).exec(code)?.[1];
286
+ return (name !== undefined &&
287
+ new RegExp(String.raw `\.\.\.\s*${name.replaceAll('$', '\\$')}\s*[,)\]}]`).test(code));
288
+ }
289
+ /** A rule turned off: the severity `'off'`, or `0` for a rule by its name in quotes. */
290
+ const TURNED_OFF = /(['"])off\1|['"][^'"\n]+['"]\s*:\s*\[?\s*0\s*[,\]}]/;
291
+ /**
292
+ * Read from the text of eslint.config.js only: it spreads the config of the baukasten, turns no rule
293
+ * off, and names neither a rule it holds, whose options could weaken it, nor the config of
294
+ * typescript-eslint that turns off the rules needing types.
295
+ */
296
+ function eslintProblems(files) {
297
+ const text = files.read(ESLINT_CONFIG);
298
+ if (text === undefined)
299
+ return [`${ESLINT_CONFIG} is missing: ${ESLINT_USE}`];
300
+ const code = withoutComments(text);
301
+ const problems = spreads(code, ESLINT_ENTRY)
302
+ ? []
303
+ : [`${ESLINT_CONFIG} does not spread the config of ${ESLINT_ENTRY}: ${ESLINT_USE}`];
304
+ for (const rule of HELD_RULES.filter((r) => code.includes(r))) {
305
+ problems.push(`${ESLINT_CONFIG} names ${rule}, which the config of ${ESLINT_ENTRY} holds: a product adds rules on top and changes none of these`);
306
+ }
307
+ if (TURNED_OFF.test(code)) {
308
+ problems.push(`${ESLINT_CONFIG} turns a rule off: a product adds rules on top and turns none off; where one must stay off at a place, the comment there says why after --`);
309
+ }
310
+ if (/\bdisableTypeChecked\b/.test(code)) {
311
+ problems.push(`${ESLINT_CONFIG} names disableTypeChecked, which turns off the rules of ${ESLINT_ENTRY} that need types: its config gives every file types, the JavaScript in the root too`);
312
+ }
232
313
  return problems;
233
314
  }
315
+ /** Where knip looks for its configuration before knip.config.ts, in its order. */
316
+ const KNIP_FIRST = ['knip.json', 'knip.jsonc', '.knip.json', '.knip.jsonc', 'knip.ts', 'knip.js'];
317
+ /** What turns off a kind of issue of knip in its configuration. */
318
+ const KNIP_SWITCHES = ['rules', 'include', 'exclude'];
319
+ /** Read from the text of knip.config.ts only, as from eslint.config.js. */
320
+ function knipProblems(files) {
321
+ const text = files.read(KNIP_CONFIG);
322
+ const problems = [];
323
+ if (text === undefined) {
324
+ problems.push(`${KNIP_CONFIG} is missing: ${KNIP_USE}`);
325
+ }
326
+ else {
327
+ const code = withoutComments(text);
328
+ if (!spreads(code, KNIP_ENTRY)) {
329
+ problems.push(`${KNIP_CONFIG} does not spread the preset of ${KNIP_ENTRY}: ${KNIP_USE}`);
330
+ }
331
+ for (const key of KNIP_SWITCHES.filter((k) => new RegExp(String.raw `(?:^|[\s{,])['"]?${k}['"]?\s*:`).test(code))) {
332
+ problems.push(`${KNIP_CONFIG} sets ${key}, which switches off what the preset of ${KNIP_ENTRY} finds, unused files, exports and dependencies: remove it`);
333
+ }
334
+ }
335
+ for (const first of KNIP_FIRST.filter((f) => files.read(f) !== undefined)) {
336
+ problems.push(`${first} takes the place of ${KNIP_CONFIG} for knip: remove it`);
337
+ }
338
+ return problems;
339
+ }
340
+ const TSCONFIG = z.looseObject({ compilerOptions: z.record(z.string(), z.unknown()).optional() });
341
+ /** What `strict` turns on, each of which a product could turn off again beside it. */
342
+ const STRICT_FAMILY = [
343
+ 'alwaysStrict',
344
+ 'noImplicitAny',
345
+ 'noImplicitThis',
346
+ 'strictBindCallApply',
347
+ 'strictBuiltinIteratorReturn',
348
+ 'strictFunctionTypes',
349
+ 'strictNullChecks',
350
+ 'strictPropertyInitialization',
351
+ 'useUnknownInCatchVariables'
352
+ ];
353
+ /**
354
+ * Read from tsconfig.json itself, not from what it extends: the product sets both there, where its
355
+ * reader sees them.
356
+ */
357
+ function tsconfigProblems(files) {
358
+ const text = files.read('tsconfig.json');
359
+ if (text === undefined)
360
+ return ['tsconfig.json is missing'];
361
+ // tsconfig.json may end an object or array with a comma, which JSON does not take.
362
+ const plain = withoutComments(text).replace(/"(?:[^"\\]|\\.)*"|,(?=\s*[}\]])/g, (match) => match === ',' ? '' : match);
363
+ const parsed = json(plain);
364
+ const tsconfig = parsed.kind === 'json' ? TSCONFIG.safeParse(parsed.value) : null;
365
+ if (tsconfig === null || !tsconfig.success) {
366
+ return ['tsconfig.json is no JSON object, or its compilerOptions are no object'];
367
+ }
368
+ const options = tsconfig.data.compilerOptions ?? {};
369
+ return [
370
+ ...['strict', 'noUncheckedIndexedAccess']
371
+ .filter((option) => options[option] !== true)
372
+ .map((option) => `tsconfig.json does not set compilerOptions.${option} to true`),
373
+ ...STRICT_FAMILY.filter((option) => options[option] === false).map((option) => `tsconfig.json sets compilerOptions.${option} to false, which strict turns on`)
374
+ ];
375
+ }
234
376
  function migrationProblems(files, art) {
235
377
  // Like the migration: every entry read, a folder failing loudly, then the .sql files.
236
378
  return files
@@ -280,6 +422,19 @@ function configurationProblems(files, art) {
280
422
  }
281
423
  return problems;
282
424
  }
425
+ /**
426
+ * svelte-check compiles the components with the compilerOptions the product passes to sveltekit() in
427
+ * vite.config.ts (SvelteKit 3 refuses a svelte.config.*), and a warningFilter among them hides the
428
+ * warnings of Svelte before --fail-on-warnings sees them.
429
+ */
430
+ function svelteWarningProblems(files) {
431
+ const vite = files.read('vite.config.ts');
432
+ return vite !== undefined && /\bwarningFilter\b/.test(withoutComments(vite))
433
+ ? [
434
+ 'vite.config.ts sets warningFilter, which hides warnings of Svelte from svelte-check --fail-on-warnings: fix what they say, or silence one at its place with a svelte-ignore comment'
435
+ ]
436
+ : [];
437
+ }
283
438
  /** What keeps the product from fitting the installed baukasten; empty when it fits. */
284
439
  export function productProblems(files, baukasten) {
285
440
  const art = artOf(files);
@@ -289,6 +444,10 @@ export function productProblems(files, baukasten) {
289
444
  ...versionProblems(files, baukasten),
290
445
  ...packageProblems(files, baukasten),
291
446
  ...migrationProblems(files, art),
292
- ...configurationProblems(files, art)
447
+ ...configurationProblems(files, art),
448
+ ...svelteWarningProblems(files),
449
+ ...eslintProblems(files),
450
+ ...knipProblems(files),
451
+ ...tsconfigProblems(files)
293
452
  ];
294
453
  }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("eslint/config").ConfigObject[];
2
+ export default _default;
package/dist/eslint.js ADDED
@@ -0,0 +1,64 @@
1
+ // The ESLint config of every product, which its eslint.config.js spreads first and adds its own rules
2
+ // to (README, «The gate»). The plugins come from the dev dependencies of the product (gate.ts says
3
+ // why); baukasten-check holds their versions.
4
+ import js from '@eslint/js';
5
+ import comments from '@eslint-community/eslint-plugin-eslint-comments/configs';
6
+ import { defineConfig } from 'eslint/config';
7
+ import svelte from 'eslint-plugin-svelte';
8
+ import globals from 'globals';
9
+ import tseslint from 'typescript-eslint';
10
+ import { HELD_RULES } from './gate.js';
11
+ /**
12
+ * What reaches the browser: the components, the load functions, hooks and matchers that run on both
13
+ * sides, and the service worker. A file of the server, a test or src/lib/server/ is none of them.
14
+ */
15
+ const BROWSER_FILES = [
16
+ '**/*.svelte',
17
+ '**/*.svelte.{ts,js}',
18
+ 'src/routes/**/+{page,layout}.{ts,js}',
19
+ 'src/hooks.{ts,js}',
20
+ 'src/hooks.client.{ts,js}',
21
+ 'src/params/*.{ts,js}',
22
+ 'src/service-worker.{ts,js}',
23
+ 'src/service-worker/**'
24
+ ];
25
+ /** The entries of the package for the server, which fail the client build (entries.test.ts). */
26
+ const SERVER_ENTRIES = ['server', 'shopify', 'testing'].map((entry) => ({
27
+ name: `@tollwyn/baukasten/${entry}`,
28
+ message: 'it runs on the server only: import it in a +page.server.ts, +layout.server.ts, +server.ts, src/hooks.server.ts or src/lib/server/',
29
+ allowTypeImports: true
30
+ }));
31
+ export default defineConfig({ ignores: ['.svelte-kit/', 'build/', 'coverage/', 'test-results/', 'playwright-report/'] }, js.configs.recommended, tseslint.configs.recommendedTypeChecked, svelte.configs.recommended, comments.recommended, {
32
+ languageOptions: {
33
+ parserOptions: {
34
+ // The stub server.js and the configuration files in the root lie in no tsconfig: they get
35
+ // the options of the product's own, so that every rule, the product's too, has types.
36
+ projectService: {
37
+ allowDefaultProject: ['*.js', '*.mjs', '*.cjs'],
38
+ defaultProject: 'tsconfig.json'
39
+ },
40
+ extraFileExtensions: ['.svelte']
41
+ }
42
+ },
43
+ rules: Object.fromEntries(HELD_RULES.map((rule) => [rule, 'error']))
44
+ },
45
+ // A product runs at the root of its own host without a base path, so its links are plain paths and
46
+ // need no resolve(); they carry the referral tag through useLink of /ui.
47
+ {
48
+ files: ['**/*.svelte', '**/*.svelte.{ts,js}'],
49
+ languageOptions: {
50
+ parserOptions: { parser: tseslint.parser, svelteFeatures: { runes: true } }
51
+ },
52
+ rules: { 'svelte/no-navigation-without-resolve': 'off' }
53
+ },
54
+ // The globals no-undef knows: components run in the browser, and any other JavaScript, the stub
55
+ // server.js and the configuration files among it, in Node or there. TypeScript knows those of its
56
+ // own files, and typescript-eslint turns no-undef off for them.
57
+ { files: ['**/*.svelte'], languageOptions: { globals: globals.browser } }, {
58
+ files: ['**/*.{js,mjs,cjs}'],
59
+ languageOptions: { globals: { ...globals.node, ...globals.browser } }
60
+ }, {
61
+ files: BROWSER_FILES,
62
+ ignores: ['**/*.test.ts', '**/*.spec.ts'],
63
+ rules: { '@typescript-eslint/no-restricted-imports': ['error', { paths: SERVER_ENTRIES }] }
64
+ });
package/dist/gate.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ /** The script the workflow of the product runs as its gate. */
2
+ export declare const GATE_SCRIPT = "gate";
3
+ /**
4
+ * Lint, each warning failing it; a product writes it exactly so, with no flag that weakens it. With
5
+ * --config, ESLint looks for no config from the folder of each file, where one in a folder below
6
+ * would take the place of the product's.
7
+ */
8
+ export declare const LINT_COMMAND = "eslint . --config eslint.config.js --max-warnings 0";
9
+ /** knip with knip.config.ts, the only configuration it reads in a product. */
10
+ export declare const KNIP_COMMAND = "knip";
11
+ /**
12
+ * The type check, each warning failing it; without --tsconfig, svelte-check reports nothing of the
13
+ * TypeScript files.
14
+ */
15
+ export declare const TYPECHECK_COMMAND = "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings";
16
+ /**
17
+ * Rules of the config of /eslint that a product's eslint.config.js never names, and so never turns
18
+ * off or weakens by an option: a promise nobody awaits loses its error, a disable without its reason
19
+ * hides why, and `@ts-nocheck` or `@ts-ignore` take a file or a line out of the type check.
20
+ */
21
+ export declare const HELD_RULES: readonly ["@typescript-eslint/no-floating-promises", "@typescript-eslint/no-misused-promises", "@typescript-eslint/ban-ts-comment", "@eslint-community/eslint-comments/require-description"];
22
+ /**
23
+ * What the config of /eslint imports, with the ranges it is written for. The product installs them
24
+ * as dev dependencies and resolves them for the package: as dependencies or peers of the package,
25
+ * npm would put them into the image of every product.
26
+ */
27
+ export declare const LINT_PLUGINS: Readonly<Record<string, string>>;
28
+ /**
29
+ * The tools the config of /eslint and the preset of /knip are written for, from the dev dependencies
30
+ * of the product, with their ranges.
31
+ */
32
+ export declare const LINT_TOOLS: Readonly<Record<string, string>>;
package/dist/gate.js ADDED
@@ -0,0 +1,49 @@
1
+ // What the baukasten holds of the gate of a product: the commands its `npm run gate` runs, the rules
2
+ // of the config of /eslint a product never changes, and what the product lints with. Pure, so that
3
+ // baukasten-check reads it without loading ESLint.
4
+ /** The script the workflow of the product runs as its gate. */
5
+ export const GATE_SCRIPT = 'gate';
6
+ /**
7
+ * Lint, each warning failing it; a product writes it exactly so, with no flag that weakens it. With
8
+ * --config, ESLint looks for no config from the folder of each file, where one in a folder below
9
+ * would take the place of the product's.
10
+ */
11
+ export const LINT_COMMAND = 'eslint . --config eslint.config.js --max-warnings 0';
12
+ /** knip with knip.config.ts, the only configuration it reads in a product. */
13
+ export const KNIP_COMMAND = 'knip';
14
+ /**
15
+ * The type check, each warning failing it; without --tsconfig, svelte-check reports nothing of the
16
+ * TypeScript files.
17
+ */
18
+ export const TYPECHECK_COMMAND = 'svelte-check --tsconfig ./tsconfig.json --fail-on-warnings';
19
+ /**
20
+ * Rules of the config of /eslint that a product's eslint.config.js never names, and so never turns
21
+ * off or weakens by an option: a promise nobody awaits loses its error, a disable without its reason
22
+ * hides why, and `@ts-nocheck` or `@ts-ignore` take a file or a line out of the type check.
23
+ */
24
+ export const HELD_RULES = [
25
+ '@typescript-eslint/no-floating-promises',
26
+ '@typescript-eslint/no-misused-promises',
27
+ '@typescript-eslint/ban-ts-comment',
28
+ '@eslint-community/eslint-comments/require-description'
29
+ ];
30
+ /**
31
+ * What the config of /eslint imports, with the ranges it is written for. The product installs them
32
+ * as dev dependencies and resolves them for the package: as dependencies or peers of the package,
33
+ * npm would put them into the image of every product.
34
+ */
35
+ export const LINT_PLUGINS = {
36
+ '@eslint/js': '^10.0.1',
37
+ '@eslint-community/eslint-plugin-eslint-comments': '^4.8.1',
38
+ 'eslint-plugin-svelte': '^3.23.1',
39
+ globals: '^17.13.0',
40
+ 'typescript-eslint': '^8.71.2'
41
+ };
42
+ /**
43
+ * The tools the config of /eslint and the preset of /knip are written for, from the dev dependencies
44
+ * of the product, with their ranges.
45
+ */
46
+ export const LINT_TOOLS = {
47
+ eslint: '^10.12.0',
48
+ knip: '^6.41.0'
49
+ };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { z } from 'zod';
2
- export { defineProduct, LANGUAGES, type Brand, type Catalog, type CatalogProduct, type Language, type NavigationLink, type Product, type ProductEnv, type ProductLegal, type PurchasePrice, type SubscriptionPrice } from './product.js';
3
- export { defineShopifyApp, type ShopifyApp, type ShopifyPlan, type ShopifyPrivacy } from './shopify/app.js';
2
+ export { defineProduct, LANGUAGES, type Language, type ProductLegal } from './product.js';
3
+ export { defineShopifyApp } from './shopify/app.js';
4
4
  export { SHOPIFY_SVELTEKIT_OPTIONS } from './shopify/sveltekit.js';
5
5
  export { SVELTEKIT_OPTIONS } from './sveltekit.js';
6
- export { TEXTS, type Linked, type Texts } from './texts.js';
7
- export { BAUKASTEN_VARIABLES, SHOPIFY_STANDARD_VARIABLES, SHOPIFY_VARIABLES, STANDARD_VARIABLES, WEBAPP_VARIABLES, type ShopifyStandardVariable, type StandardVariable } from './variables.js';
6
+ export { TEXTS } from './texts.js';
7
+ export { BAUKASTEN_VARIABLES, SHOPIFY_STANDARD_VARIABLES, STANDARD_VARIABLES } from './variables.js';
package/dist/index.js CHANGED
@@ -6,4 +6,4 @@ export { defineShopifyApp } from './shopify/app.js';
6
6
  export { SHOPIFY_SVELTEKIT_OPTIONS } from './shopify/sveltekit.js';
7
7
  export { SVELTEKIT_OPTIONS } from './sveltekit.js';
8
8
  export { TEXTS } from './texts.js';
9
- export { BAUKASTEN_VARIABLES, SHOPIFY_STANDARD_VARIABLES, SHOPIFY_VARIABLES, STANDARD_VARIABLES, WEBAPP_VARIABLES } from './variables.js';
9
+ export { BAUKASTEN_VARIABLES, SHOPIFY_STANDARD_VARIABLES, STANDARD_VARIABLES } from './variables.js';
package/dist/knip.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ /** The part of the configuration of knip that the preset sets. */
2
+ export interface KnipPreset {
3
+ entry: string[];
4
+ sveltekit: {
5
+ entry: string[];
6
+ };
7
+ playwright: {
8
+ config: string[];
9
+ entry: string[];
10
+ };
11
+ ignoreDependencies: string[];
12
+ }
13
+ declare const preset: KnipPreset;
14
+ export default preset;
package/dist/knip.js ADDED
@@ -0,0 +1,25 @@
1
+ // The knip preset of every product, which its knip.config.ts spreads and adds to (README, «The
2
+ // gate»). knip finds the routes, the hooks of server and client and the matchers through its plugin
3
+ // for SvelteKit, the stub server.js as the file Node starts, and the tests through Vitest; this names
4
+ // what only the baukasten knows, and what the plugin misses.
5
+ import { LINT_PLUGINS } from './gate.js';
6
+ const preset = {
7
+ // The baukasten loads it, in the migration command and the connection test.
8
+ entry: ['baukasten.config.ts'],
9
+ // SvelteKit loads the universal hooks, which the plugin does not name. Here, not under entry, a
10
+ // product without them gets no hint that the pattern finds nothing; the plugin still reads
11
+ // vite.config.ts for the rest.
12
+ sveltekit: { entry: ['src/hooks.{js,ts}'] },
13
+ // Read, not run: the configuration of Playwright may need the test database to load. Its tests lie
14
+ // in e2e/, the connection test of the baukasten among them, as Playwright finds them there. What
15
+ // else the configuration loads (globalSetup, a webServer, a reporter of its own) the product adds
16
+ // here itself.
17
+ playwright: {
18
+ config: [],
19
+ entry: ['playwright.config.ts', 'e2e/**/*.@(spec|test).?(c|m)[jt]s?(x)']
20
+ },
21
+ // What the baukasten imports from the product: the plugins of its ESLint config, and the server of
22
+ // the SMTP double.
23
+ ignoreDependencies: [...Object.keys(LINT_PLUGINS), 'smtp-server']
24
+ };
25
+ export default preset;