tslaws 0.3.2 → 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 +1 -0
- package/dist/browser.js +70 -16
- package/dist/bundlerPlugins.d.ts +20 -0
- package/dist/bundlerPlugins.js +71 -0
- package/dist/coverage.d.ts +2 -0
- package/dist/coverage.js +5 -1
- package/package.json +9 -7
package/README.md
CHANGED
|
@@ -159,6 +159,7 @@ See [docs/laws.md](../../docs/laws.md#the-dashboard).
|
|
|
159
159
|
|
|
160
160
|
`tslaws mutate [files...] [--max N] [--seed N] [--json]` mutation-tests the laws: it mutates the covered code, re-runs the laws against each mutant, and lists the survivors no law caught. It handles `.tsx`/`.jsx` and projects with a lock, and takes the run flags of `check`. See [docs/laws.md](../../docs/laws.md#mutation-testing).
|
|
161
161
|
|
|
162
|
+
- **Other source formats:** `tslaws.json` `bundlerPlugins` lists esbuild plugin modules (paths relative to the file) that teach the browser bundler formats like `.tsrx`. See [docs/laws.md](../../docs/laws.md#bundler-plugins).
|
|
162
163
|
- **Settings:** `tslaws.json` `runs`, `bound`, `proofTimeoutMs` and `propertyTimeoutMs` set the run defaults; a command line flag beats them. See [docs/laws.md](../../docs/laws.md#settings).
|
|
163
164
|
|
|
164
165
|
## Agents cannot move the goalposts
|
package/dist/browser.js
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
// each input is sent to a page where the compiled law and the code it checks run against a live DOM.
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import { mkdirSync } from 'node:fs';
|
|
5
|
-
import { delimiter, join } from 'node:path';
|
|
5
|
+
import { delimiter, join, relative as pathRelative, resolve, sep } from 'node:path';
|
|
6
6
|
import { build as bundleWith } from 'esbuild';
|
|
7
|
+
import { explainBundleError, loadBundlerPlugins } from "./bundlerPlugins.js";
|
|
8
|
+
import { readConfig } from "./coverage.js";
|
|
7
9
|
import { chromium, firefox, webkit } from 'playwright-core';
|
|
8
10
|
import { toWire } from "./generators.js";
|
|
9
11
|
import { runProperty, summarise } from "./property.js";
|
|
@@ -156,21 +158,70 @@ function projectModulesPlugin(cwd) {
|
|
|
156
158
|
},
|
|
157
159
|
};
|
|
158
160
|
}
|
|
161
|
+
// The compiler writes only what it compiles to .tslaws/out, so a relative import of any other source file (a `.tsrx`
|
|
162
|
+
// template, a stylesheet, an image) would look for it there. Such an import resolves to the file beside the source.
|
|
163
|
+
function sourceFilesPlugin(cwd) {
|
|
164
|
+
const out = join(cwd, '.tslaws', 'out');
|
|
165
|
+
return {
|
|
166
|
+
name: 'tslaws-source-files',
|
|
167
|
+
setup(build) {
|
|
168
|
+
build.onResolve({ filter: /^\.\.?\// }, (args) => {
|
|
169
|
+
if (args.resolveDir !== out && !args.resolveDir.startsWith(out + sep))
|
|
170
|
+
return undefined;
|
|
171
|
+
const inOut = resolve(args.resolveDir, args.path);
|
|
172
|
+
if (existsSync(inOut))
|
|
173
|
+
return undefined;
|
|
174
|
+
const source = join(cwd, pathRelative(out, inOut));
|
|
175
|
+
return existsSync(source) && !source.endsWith('.js') ? { path: source } : undefined;
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const assetExtensions = [
|
|
181
|
+
'.png',
|
|
182
|
+
'.jpg',
|
|
183
|
+
'.jpeg',
|
|
184
|
+
'.gif',
|
|
185
|
+
'.webp',
|
|
186
|
+
'.avif',
|
|
187
|
+
'.svg',
|
|
188
|
+
'.ico',
|
|
189
|
+
'.woff',
|
|
190
|
+
'.woff2',
|
|
191
|
+
'.ttf',
|
|
192
|
+
'.otf',
|
|
193
|
+
'.eot',
|
|
194
|
+
];
|
|
195
|
+
/** The project's `bundlerPlugins` from tslaws.json come first, so they can claim a format before the built-in resolver. */
|
|
159
196
|
async function bundleOf(jsFile, cwd) {
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
197
|
+
const projectPlugins = await loadBundlerPlugins(cwd, readConfig(cwd).bundlerPlugins);
|
|
198
|
+
try {
|
|
199
|
+
const result = await bundleWith({
|
|
200
|
+
plugins: [...projectPlugins, sourceFilesPlugin(cwd), projectModulesPlugin(cwd)],
|
|
201
|
+
entryPoints: [jsFile],
|
|
202
|
+
bundle: true,
|
|
203
|
+
write: false,
|
|
204
|
+
// Stylesheets a law imports come out as a separate .css file, which needs an output directory to be named.
|
|
205
|
+
outdir: join(cwd, '.tslaws', 'bundle'),
|
|
206
|
+
// Images and fonts become data URLs, so a url() in the CSS or an import in the JavaScript needs no file server.
|
|
207
|
+
loader: Object.fromEntries(assetExtensions.map((extension) => [extension, 'dataurl'])),
|
|
208
|
+
format: 'iife',
|
|
209
|
+
globalName: '__lawModule',
|
|
210
|
+
platform: 'browser',
|
|
211
|
+
target: 'es2022',
|
|
212
|
+
absWorkingDir: cwd,
|
|
213
|
+
logLevel: 'silent',
|
|
214
|
+
define: { 'process.env.NODE_ENV': '"development"', global: 'globalThis' },
|
|
215
|
+
});
|
|
216
|
+
const emitted = (extension) => result.outputFiles
|
|
217
|
+
.filter((file) => file.path.endsWith(extension))
|
|
218
|
+
.map((file) => file.text)
|
|
219
|
+
.join('\n');
|
|
220
|
+
return { js: emitted('.js'), css: emitted('.css') };
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
throw explainBundleError(error);
|
|
224
|
+
}
|
|
174
225
|
}
|
|
175
226
|
/** Runs the counterexample once more, leaves the DOM as it is, and photographs the page. Best effort. */
|
|
176
227
|
async function capture(page, law, args, options) {
|
|
@@ -187,7 +238,7 @@ async function capture(page, law, args, options) {
|
|
|
187
238
|
}
|
|
188
239
|
/** Property-tests one law in a page; `'timeout'` when it does not finish within `options.propertyTimeoutMs`. */
|
|
189
240
|
export async function runBrowserLaw(law, options) {
|
|
190
|
-
const code = await bundleOf(law.jsFile, options.cwd);
|
|
241
|
+
const { js: code, css } = await bundleOf(law.jsFile, options.cwd);
|
|
191
242
|
const page = await (await launch(resolveEngine(options.browser))).newPage();
|
|
192
243
|
// A watcher needs only the first few runs to linger; the rest should not eat the time limit.
|
|
193
244
|
let calls = 0;
|
|
@@ -216,6 +267,9 @@ export async function runBrowserLaw(law, options) {
|
|
|
216
267
|
: undefined;
|
|
217
268
|
try {
|
|
218
269
|
await page.setContent('<!doctype html><html><head><meta charset="utf-8"></head><body></body></html>');
|
|
270
|
+
// The stylesheet goes in before the script runs, as a linked one would.
|
|
271
|
+
if (css !== '')
|
|
272
|
+
await page.addStyleTag({ content: css });
|
|
219
273
|
await page.addScriptTag({ content: code });
|
|
220
274
|
await page.addScriptTag({ content: harness });
|
|
221
275
|
const call = async (...args) => {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Plugin } from 'esbuild';
|
|
2
|
+
/** What a plugin factory receives. */
|
|
3
|
+
export interface BundlerPluginContext {
|
|
4
|
+
/** The project root: the directory holding tslaws.json. */
|
|
5
|
+
cwd: string;
|
|
6
|
+
}
|
|
7
|
+
export type BundlerPluginExport = Plugin | ((context: BundlerPluginContext) => Plugin | Promise<Plugin>);
|
|
8
|
+
/** The setting the messages name. */
|
|
9
|
+
export declare const bundlerPluginsSetting = "bundlerPlugins";
|
|
10
|
+
/**
|
|
11
|
+
* Loads each listed module (paths relative to `cwd`, where tslaws.json lives) with a dynamic import of its file URL.
|
|
12
|
+
* Throws one error naming the setting and the module when a module is missing, throws while loading or in its
|
|
13
|
+
* factory, or does not export a plugin, so the law file reports an `error` rather than the run crashing.
|
|
14
|
+
*/
|
|
15
|
+
export declare function loadBundlerPlugins(cwd: string, paths: readonly string[] | undefined): Promise<Plugin[]>;
|
|
16
|
+
/**
|
|
17
|
+
* Rewrites an esbuild failure into advice when the cause is a source format the bundler has no loader for (the usual
|
|
18
|
+
* sign that a `bundlerPlugins` entry is missing). Other messages pass through unchanged.
|
|
19
|
+
*/
|
|
20
|
+
export declare function explainBundleError(error: Error): Error;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
// Bundler plugins: a project teaches the browser bundler a source format (`.tsrx`, `.svelte`, ...) by listing esbuild
|
|
10
|
+
// plugin modules under `bundlerPlugins` in tslaws.json. Each module default-exports an esbuild Plugin, or a function
|
|
11
|
+
// (sync or async) that takes `{ cwd }` and returns one.
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
14
|
+
import { pathToFileURL } from 'node:url';
|
|
15
|
+
/** The setting the messages name. */
|
|
16
|
+
export const bundlerPluginsSetting = 'bundlerPlugins';
|
|
17
|
+
/**
|
|
18
|
+
* Loads each listed module (paths relative to `cwd`, where tslaws.json lives) with a dynamic import of its file URL.
|
|
19
|
+
* Throws one error naming the setting and the module when a module is missing, throws while loading or in its
|
|
20
|
+
* factory, or does not export a plugin, so the law file reports an `error` rather than the run crashing.
|
|
21
|
+
*/
|
|
22
|
+
export async function loadBundlerPlugins(cwd, paths) {
|
|
23
|
+
const plugins = [];
|
|
24
|
+
for (const path of paths ?? []) {
|
|
25
|
+
const file = isAbsolute(path) ? path : resolve(cwd, path);
|
|
26
|
+
const fail = (why) => {
|
|
27
|
+
throw new Error(`tslaws.json ${bundlerPluginsSetting}: cannot use "${path}" (${file}): ${why}`);
|
|
28
|
+
};
|
|
29
|
+
if (!existsSync(file))
|
|
30
|
+
fail('no such file; paths are relative to the directory of tslaws.json');
|
|
31
|
+
let exported;
|
|
32
|
+
try {
|
|
33
|
+
exported = (await import(__rewriteRelativeImportExtension(pathToFileURL(file).href))).default;
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
return fail(`it failed to load: ${error.message}`);
|
|
37
|
+
}
|
|
38
|
+
let plugin = exported;
|
|
39
|
+
if (typeof exported === 'function') {
|
|
40
|
+
try {
|
|
41
|
+
plugin = await exported({ cwd });
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return fail(`its plugin function threw: ${error.message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const candidate = plugin;
|
|
48
|
+
if (typeof candidate !== 'object' ||
|
|
49
|
+
candidate === null ||
|
|
50
|
+
typeof candidate.name !== 'string' ||
|
|
51
|
+
typeof candidate.setup !== 'function')
|
|
52
|
+
fail('its default export must be an esbuild plugin ({ name, setup }) or a function returning one');
|
|
53
|
+
plugins.push(plugin);
|
|
54
|
+
}
|
|
55
|
+
return plugins;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Rewrites an esbuild failure into advice when the cause is a source format the bundler has no loader for (the usual
|
|
59
|
+
* sign that a `bundlerPlugins` entry is missing). Other messages pass through unchanged.
|
|
60
|
+
*/
|
|
61
|
+
export function explainBundleError(error) {
|
|
62
|
+
const match = /No loader is configured for "(\.[^"]+)" files/.exec(error.message);
|
|
63
|
+
if (match === null)
|
|
64
|
+
return error;
|
|
65
|
+
const extension = match[1];
|
|
66
|
+
const advice = `the browser bundler cannot load "${extension}" files. Add a bundler plugin for that extension in tslaws.json ` +
|
|
67
|
+
`"${bundlerPluginsSetting}", for example { "${bundlerPluginsSetting}": ["./tools/${extension.slice(1)}-plugin.mjs"] }, ` +
|
|
68
|
+
`where the module default-exports an esbuild plugin whose onLoad({ filter: /\${extension}$/ }) returns the compiled ` +
|
|
69
|
+
`JavaScript (see "Bundler plugins" in docs/laws.md)`;
|
|
70
|
+
return new Error(`${advice}. esbuild said: ${error.message.split('\n')[0]}`);
|
|
71
|
+
}
|
package/dist/coverage.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export interface CoverageConfig {
|
|
|
6
6
|
compilerOptions?: Record<string, unknown>;
|
|
7
7
|
/** End-to-end laws: the app to drive; see `parseE2eConfig`. */
|
|
8
8
|
e2e?: E2eConfig;
|
|
9
|
+
/** esbuild plugin modules (paths relative to tslaws.json) that teach the browser bundler new source formats. */
|
|
10
|
+
bundlerPlugins?: string[];
|
|
9
11
|
/** Run settings; a command line flag beats them and they beat the built-in default. */
|
|
10
12
|
runs?: number;
|
|
11
13
|
propertyTimeoutMs?: number;
|
package/dist/coverage.js
CHANGED
|
@@ -9,7 +9,7 @@ import { dirname, join, relative, resolve } from 'node:path';
|
|
|
9
9
|
import ts from 'typescript';
|
|
10
10
|
import { parseE2eConfig } from "./e2e.js";
|
|
11
11
|
const settingKeys = ['runs', 'propertyTimeoutMs', 'proofTimeoutMs', 'bound'];
|
|
12
|
-
const configKeys = ['cover', 'compilerOptions', 'e2e', ...settingKeys];
|
|
12
|
+
const configKeys = ['cover', 'compilerOptions', 'e2e', 'bundlerPlugins', ...settingKeys];
|
|
13
13
|
/** Validates the top level of tslaws.json: known keys only, and the run settings are integers of at least 1. */
|
|
14
14
|
export function parseConfig(raw) {
|
|
15
15
|
const fail = (message) => {
|
|
@@ -27,6 +27,10 @@ export function parseConfig(raw) {
|
|
|
27
27
|
if (value !== undefined && (typeof value !== 'number' || !Number.isInteger(value) || value < 1))
|
|
28
28
|
fail(`"${key}" must be an integer of at least 1, got ${JSON.stringify(value)}`);
|
|
29
29
|
}
|
|
30
|
+
const plugins = object.bundlerPlugins;
|
|
31
|
+
if (plugins !== undefined &&
|
|
32
|
+
(!Array.isArray(plugins) || plugins.some((entry) => typeof entry !== 'string' || entry === '')))
|
|
33
|
+
fail(`"bundlerPlugins" must be an array of module paths, got ${JSON.stringify(plugins)}`);
|
|
30
34
|
return object;
|
|
31
35
|
}
|
|
32
36
|
export function readConfig(cwd) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tslaws",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Runs and checks `law` declarations compiled by tsc-laws: property tests, SMT proofs, tamper lock.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"tslaws": "bin/tslaws.js"
|
|
@@ -58,7 +58,9 @@
|
|
|
58
58
|
"z3-solver": "^4.15.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@
|
|
61
|
+
"@tsrx/ripple": "0.2.11",
|
|
62
|
+
"@types/node": "^24.0.0",
|
|
63
|
+
"ripple": "0.6.1"
|
|
62
64
|
},
|
|
63
65
|
"peerDependencies": {
|
|
64
66
|
"axe-core": "^4.0.0"
|
|
@@ -69,11 +71,11 @@
|
|
|
69
71
|
}
|
|
70
72
|
},
|
|
71
73
|
"optionalDependencies": {
|
|
72
|
-
"tslaws-compiler-darwin-arm64": "0.
|
|
73
|
-
"tslaws-compiler-darwin-x64": "0.
|
|
74
|
-
"tslaws-compiler-linux-arm64-gnu": "0.
|
|
75
|
-
"tslaws-compiler-linux-x64-gnu": "0.
|
|
76
|
-
"tslaws-compiler-win32-x64-msvc": "0.
|
|
74
|
+
"tslaws-compiler-darwin-arm64": "0.4.0",
|
|
75
|
+
"tslaws-compiler-darwin-x64": "0.4.0",
|
|
76
|
+
"tslaws-compiler-linux-arm64-gnu": "0.4.0",
|
|
77
|
+
"tslaws-compiler-linux-x64-gnu": "0.4.0",
|
|
78
|
+
"tslaws-compiler-win32-x64-msvc": "0.4.0"
|
|
77
79
|
},
|
|
78
80
|
"publishConfig": {
|
|
79
81
|
"access": "public"
|