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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,17 @@
drawings built another way. `toDXF` refuses to write a drawing with lint
errors. Test a new rule with `@ts-expect-error` inside `assert.throws`, as
`test/rules.test.ts` does, so both halves are proven.
- `src/standard/` is a firm's standard. `shape.ts` is the one table of a document's fields: each
field's type, the rule of its value, its default and whether the hash reads it.
`scripts/standard-files.ts` writes `standard.schema.json` from it, `standard.json` from the
built-in standard and `src/version.ts` from `package.json`, and `test/standard.test.ts` fails
until `pnpm standard:files` has run after a change to any of them. A rule of a document's
literal is a type in `src/standard/literal.ts` with the runtime check's message, as a rule of a
drawing is in `src/rules.ts`. A default, once released, never changes: a change of behavior
comes as a new field or value. The library makes `BUILT_IN_STANDARD` as it loads without the
checks, with `BUILT_IN_HASH` in `src/standard/define.ts`, and a test resolves its document with
them. A change to what it states raises its version, which the CHANGELOG gives, and sets that
hash.
- `lint` takes any plain data and never throws, since a `Drawing` can be written by hand.
`src/shape.ts` holds the fields of each object and their types, once, for `fromJSON` and for
`lint`. `lint` checks an object's shape there before a rule reads it, and skips the rules for
Expand Down
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
"cli/dist",
"cli/assets",
"GUIDE.md",
"CHANGELOG.md"
"CHANGELOG.md",
"standard.json",
"standard.schema.json"
],
"type": "module",
"sideEffects": false,
Expand All @@ -40,7 +42,9 @@
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./package.json": "./package.json"
"./package.json": "./package.json",
"./standard.json": "./standard.json",
"./standard.schema.json": "./standard.schema.json"
},
"scripts": {
"clean": "node -e \"for (const dir of ['dist', 'cli/dist']) require('node:fs').rmSync(dir, { recursive: true, force: true })\"",
Expand All @@ -57,6 +61,7 @@
"example": "node examples/site-plan.ts && node examples/outdoor-kitchen.ts",
"readme:drawing": "node scripts/readme-drawing.ts",
"bench": "pnpm build:lib && node cli/scripts/bench.ts",
"standard:files": "node scripts/standard-files.ts",
"verify": "pnpm fmt:check && pnpm lint && pnpm check && pnpm test && pnpm test:package"
},
"devDependencies": {
Expand Down
17 changes: 11 additions & 6 deletions scripts/library-alone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@
// --disallow-code-generation-from-strings.
import assert from "node:assert/strict";
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { join, posix } from "node:path";
import vm from "node:vm";

const dist = process.argv[2]!;
const names = readdirSync(dist).filter((name) => name.endsWith(".js"));
// Every built file, in its folders, by its path from `dist` with `/` between folders.
const names = readdirSync(dist, { recursive: true, encoding: "utf8" })
.map((name) => name.split("\\").join("/"))
.filter((name) => name.endsWith(".js"));
assert.ok(names.includes("index.js"), `${dist} has no index.js`);

// The context is a browser's, not Node's: what the language itself defines, and the two globals
Expand Down Expand Up @@ -58,7 +61,8 @@ for (const name of names) {
}

const link = (specifier: string, from: vm.Module): vm.Module => {
const found = modules.get(specifier);
const path = `./${posix.join(posix.dirname(from.identifier), specifier)}`;
const found = specifier.startsWith(".") ? modules.get(path) : undefined;
assert.ok(found, `${from.identifier} imports ${specifier}, which is not a file of the library`);
return found;
};
Expand All @@ -69,7 +73,7 @@ for (const module of modules.values()) {
}

const use = new vm.SourceTextModule(
`import { createDrawing, DASHED, fromJSON, lint, toDXF, toJSON, toSVG } from "./index.js";
`import { BUILT_IN_STANDARD, createDrawing, DASHED, defineStandard, fromJSON, lint, toDXF, toJSON, toSVG } from "./index.js";
const d = createDrawing({ units: "ft" });
d.layer("A-WALL", { color: 4, linetype: DASHED, lineweight: 0.5 }, () => {
d.rect([0, 0], 10, 5, { id: "room" });
Expand All @@ -80,11 +84,12 @@ const use = new vm.SourceTextModule(
});
const drawing = d.build();
const again = fromJSON(toJSON(drawing));
export default [lint(drawing).length, toSVG(again).slice(0, 4), toDXF(again).slice(-5), again.entities.length];`,
const standard = defineStandard({ id: "alone", version: "1", extends: BUILT_IN_STANDARD, layers: { "A-WALL": { color: 4, pen: "bold" } } });
export default [lint(drawing).length, toSVG(again).slice(0, 4), toDXF(again).slice(-5), again.entities.length, standard.chain[0].hash === BUILT_IN_STANDARD.hash];`,
{ identifier: "use.js", context, importModuleDynamically: refuse },
);
await use.link(link);
await use.evaluate();
const result = (use.namespace as { default: unknown[] }).default;
assert.deepEqual([...result], [0, "<svg", "EOF\r\n", 5]);
assert.deepEqual([...result], [0, "<svg", "EOF\r\n", 5, true]);
console.log(`the library stands alone: ${names.length} files, no Node module and no package`);
32 changes: 28 additions & 4 deletions scripts/package-smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ try {
"package.json",
"dist/index.js",
"dist/index.d.ts",
"dist/standard/define.js",
"standard.json",
"standard.schema.json",
"cli/dist/cli.js",
"cli/dist/worker.js",
"cli/assets/page.html",
Expand All @@ -65,11 +68,15 @@ try {
}
const stray = files.filter(
(f) =>
!/^(LICENSE|README\.md|GUIDE\.md|CHANGELOG\.md|package\.json|dist\/[\w-]+\.(js|d\.ts)|cli\/dist\/[\w-]+\.js|cli\/assets\/.+)$/.test(
!/^(LICENSE|README\.md|GUIDE\.md|CHANGELOG\.md|package\.json|standard(\.schema)?\.json|dist\/(standard\/)?[\w-]+\.(js|d\.ts)|cli\/dist\/[\w-]+\.js|cli\/assets\/.+)$/.test(
f,
),
);
assert.deepEqual(stray, [], "tarball contains only dist/, cli/ and package metadata");
assert.deepEqual(
stray,
[],
"tarball contains only dist/, cli/, the standard and package metadata",
);

const consumer = join(tmp, "consumer");
const installed = join(consumer, "node_modules", "tsdraft");
Expand Down Expand Up @@ -98,7 +105,8 @@ try {
writeFileSync(
join(consumer, "main.ts"),
[
`import { createDrawing, lint, toDXF, toSVG, type Drawing } from "tsdraft";`,
`import { BUILT_IN_STANDARD, createDrawing, defineStandard, lint, toDXF, toSVG, type Drawing } from "tsdraft";`,
`import builtIn from "tsdraft/standard.json" with { type: "json" };`,
`const d: Drawing = createDrawing({ units: "ft" }).layer("A", { color: 1 }, () => {}).rect([0, 0], 10, 5).build();`,
`if (lint(d).length > 0) throw new Error("unexpected lint problems");`,
`// The published types must still reject a bad literal, and the code must still throw.`,
Expand All @@ -115,6 +123,15 @@ try {
`const lineweight = () => createDrawing().line([0, 0], [1, 1], { lineweight: 0.33 });`,
`try { lineweight(); threw = false; } catch { threw = true; }`,
`if (!threw) throw new Error("a bad lineweight was accepted");`,
`// A standard's literal mistake is a compile error too, and the JSON the package ships is`,
`// a document that resolves to the built-in standard.`,
`const standard = defineStandard({ id: "acme", version: "1", extends: BUILT_IN_STANDARD, layers: { "A-WALL": { color: 2, pen: "bold" } } });`,
`const wall: { readonly color: number } = standard.layers["A-WALL"];`,
`// @ts-expect-error: the standard has no pen thick.`,
`const thick = () => defineStandard({ id: "acme", version: "1", extends: BUILT_IN_STANDARD, layers: { A: { color: 2, pen: "thick" } } });`,
`try { thick(); threw = false; } catch { threw = true; }`,
`if (!threw || wall.color !== 2) throw new Error("a pen the standard lacks was accepted");`,
`if (defineStandard(builtIn).hash !== BUILT_IN_STANDARD.hash) throw new Error("standard.json is not the built-in standard");`,
`const dxf: string = toDXF(d);`,
`const svg: string = toSVG(d);`,
`if (!dxf.endsWith("EOF\\r\\n") || !svg.startsWith("<svg")) throw new Error("unexpected output");`,
Expand All @@ -133,7 +150,14 @@ try {
writeFileSync(
join(consumer, config),
JSON.stringify({
compilerOptions: { ...options, strict: true, noEmit: true, skipLibCheck: false, types: [] },
compilerOptions: {
...options,
strict: true,
noEmit: true,
skipLibCheck: false,
types: [],
resolveJsonModule: true,
},
files: ["main.ts"],
}),
);
Expand Down
58 changes: 58 additions & 0 deletions scripts/standard-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Writes the files the package ships beside its library from the source: standard.json, the
// built-in standard with its title block inline, standard.schema.json, from the table of a
// standard's fields, and src/version.ts, from package.json. test/standard.test.ts fails when a
// committed file differs from what this writes.
//
// pnpm standard:files
import { execFileSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { BUILT_IN_STANDARD } from "../src/index.ts";
import { standardSchema } from "../src/standard/shape.ts";

const root = join(import.meta.dirname, "..");

/** The package's version, as package.json gives it. */
export function packageVersion(): string {
return (JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { version: string })
.version;
}

/** The URL of the schema of the tsdraft of `version`, which a document's `$schema` names. */
export const schemaUrl = (version: string): string =>
`https://cdn.jsdelivr.net/npm/tsdraft@${version}/standard.schema.json`;

/** standard.json: the built-in standard as a document, without its hash and chain. */
export function builtInDocument(version: string): Record<string, unknown> {
const { hash: _, chain: __, ...document } = BUILT_IN_STANDARD;
return { $schema: schemaUrl(version), ...document };
}

/** src/version.ts. */
export function versionSource(version: string): string {
return [
"// Written by scripts/standard-files.ts from package.json. Run `pnpm standard:files` after a",
"// change to the version.",
"",
"/** The version of this copy of tsdraft. */",
`export const VERSION = ${JSON.stringify(version)};`,
"",
].join("\n");
}

if (import.meta.main) {
const version = packageVersion();
const json = {
"standard.json": builtInDocument(version),
"standard.schema.json": standardSchema(version),
};
for (const [name, value] of Object.entries(json)) {
writeFileSync(join(root, name), `${JSON.stringify(value, null, 2)}\n`);
}
writeFileSync(join(root, "src", "version.ts"), versionSource(version));
// The formatter lays the JSON out, so `pnpm fmt:check` passes on the files as written.
execFileSync(join(root, "node_modules", ".bin", "oxfmt"), Object.keys(json), {
cwd: root,
stdio: "inherit",
});
}
24 changes: 24 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,28 @@ export type {
WidthFactor,
XrefPath,
} from "./rules.ts";
export { BUILT_IN_STANDARD, defineStandard, type BuiltInStandard } from "./standard/define.ts";
export type {
Box,
FieldSource,
SheetFiles,
Standard,
StandardDimensionStyle,
StandardDiscipline,
StandardDocument,
StandardLayer,
StandardLinetype,
StandardLink,
StandardNames,
StandardOptions,
StandardPen,
StandardScale,
StandardSheetFormat,
StandardTextRole,
StandardTitleBlock,
StandardTitleBlockField,
StandardWriting,
StandardXref,
TitleBlockFill,
} from "./standard/document.ts";
export { toSVG, type SvgOptions } from "./svg.ts";
22 changes: 20 additions & 2 deletions src/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ import { isRecord, PLAIN_OBJECT, readDrawing } from "./shape.ts";
* Throws when the drawing holds a value JSON cannot, such as `NaN`.
*/
export function toJSON(drawing: Drawing): string {
return canonical(drawing, "A drawing");
}

/**
* Writes a value as canonical JSON, as `toJSON` writes a drawing. Throws when the value holds
* one JSON cannot, such as `NaN`, naming it as `what` does. `written` may give the canonical
* JSON of an object already worked out, which is then used as it is.
*/
export function canonical(
value: unknown,
what = "A value",
written?: (value: object) => string | undefined,
): string {
let out = "";
const write = (value: unknown): void => {
switch (typeof value) {
Expand All @@ -33,6 +46,11 @@ export function toJSON(drawing: Drawing): string {
out += "null";
return;
}
const known = written?.(value);
if (known !== undefined) {
out += known;
return;
}
if (Array.isArray(value)) {
out += "[";
for (let i = 0; i < value.length; i++) {
Expand All @@ -55,9 +73,9 @@ export function toJSON(drawing: Drawing): string {
}
}
const shown = typeof value === "number" ? String(value) : typeof value;
throw new Error(`A drawing with a value of ${shown} cannot be written as JSON`);
throw new Error(`${what} with a value of ${shown} cannot be written as JSON`);
};
write(drawing);
write(value);
return out;
}

Expand Down
3 changes: 2 additions & 1 deletion src/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ export type LintRule =
| "duplicate-dimension-style"
| "undefined-dimension-style"
| "dimension"
| "zero-length";
| "zero-length"
| "standard";

export interface Diagnostic {
readonly rule: LintRule;
Expand Down
Loading
Loading