@loomcli/loom 0.6.0 → 0.8.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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The Loom CLI toolchain. Its public surface is `@loomcli/loom/build`, which exports `packet()`.
|
|
4
4
|
|
|
5
|
-
`packet()` is a `Bun.build` plugin. A Loom application keeps a `loom.packet.json` that reads `{ "build": "development" }` at its package root and passes it to its `Application` as `packet`. While Bun bundles the application, `packet()` writes `distributed` into the packet the bundle carries, so an operator sees one generic message for a defect, and the source file keeps reading `development`.
|
|
5
|
+
`packet()` is a `Bun.build` plugin. A Loom application keeps a `loom.packet.json` that reads `{ "build": "development" }` at its package root and passes it to its `Application` as `packet`. While Bun bundles the application, `packet()` writes `distributed` into the packet the bundle carries, so an operator sees one generic message for a defect, and the source file keeps reading `development`. An application bundled without it, by any bundler, starts and reads the source packet, `development`, so it is a development build.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
package/dist/build.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ interface PacketBuilder {
|
|
|
8
8
|
filter: RegExp;
|
|
9
9
|
}, callback: (args: PacketLoad) => Promise<{
|
|
10
10
|
contents: string;
|
|
11
|
-
loader: '
|
|
11
|
+
loader: 'json';
|
|
12
12
|
}>): unknown;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
@@ -24,9 +24,8 @@ interface PacketPlugin {
|
|
|
24
24
|
* The `Bun.build` plugin that makes a bundle a distributed build. While Bun bundles, it answers the
|
|
25
25
|
* import of every `loom.packet.json` with the file's members and `build` set to `distributed`, and
|
|
26
26
|
* it never writes to the source tree, so the source run keeps reading `development`. A compiled
|
|
27
|
-
* binary built through `Bun.build` with this plugin is distributed too. It
|
|
28
|
-
*
|
|
29
|
-
* a bundle nor a compiled binary can start.
|
|
27
|
+
* binary built through `Bun.build` with this plugin is distributed too. It answers no other module,
|
|
28
|
+
* so the bundle holds every other module as Bun bundles it.
|
|
30
29
|
*/
|
|
31
30
|
export declare function packet(): PacketPlugin;
|
|
32
31
|
export type { PacketBuilder, PacketLoad, PacketPlugin };
|
package/dist/build.js
CHANGED
|
@@ -1,30 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
/** Every module whose file name is the packet's, wherever the entry imports it from. */
|
|
3
3
|
const packetFile = /(?:^|[\\/])loom\.packet\.json$/u;
|
|
4
|
-
/** The generated modules of core's Unicode tables, which read their data files at run time. */
|
|
5
|
-
const unicodeTables = /[\\/]@rockorager[\\/]uucode[\\/]dist[\\/]src[\\/]generated[\\/][\w-]+\.js$/u;
|
|
6
|
-
/** One table module's read of the data file beside it. */
|
|
7
|
-
const tableRead = /JSON\.parse\(readFileSync\(new URL\("\.\/(?<file>[\w-]+\.json)", import\.meta\.url\), "utf8"\)\)/gu;
|
|
8
|
-
/**
|
|
9
|
-
* A table module with each data file it reads beside itself imported as a JSON module instead, so
|
|
10
|
-
* the bundle carries the data. A bundle, and a compiled binary most of all, holds no file beside
|
|
11
|
-
* the module that the original read could find. A table module with no read of that shape fails
|
|
12
|
-
* the build, because a bundle built from it would fail at run time instead.
|
|
13
|
-
*/
|
|
14
|
-
async function inlinedTables(path) {
|
|
15
|
-
const source = await readFile(path, 'utf8');
|
|
16
|
-
const imports = [];
|
|
17
|
-
// The one capture is the data file's name, which the callback receives after the whole match.
|
|
18
|
-
const contents = source.replaceAll(tableRead, (_read, file) => {
|
|
19
|
-
const name = `table${String(imports.length)}`;
|
|
20
|
-
imports.push(`import ${name} from './${file}' with { type: 'json' };`);
|
|
21
|
-
return name;
|
|
22
|
-
});
|
|
23
|
-
if (imports.length === 0) {
|
|
24
|
-
throw new Error(`packet() found no data file read in the Unicode table module ${path}. Update @loomcli/loom to a version that supports the installed @rockorager/uucode.`);
|
|
25
|
-
}
|
|
26
|
-
return [...imports, contents].join('\n');
|
|
27
|
-
}
|
|
28
4
|
/** Whether a parsed JSON value is a plain object, whose members the writer keeps. */
|
|
29
5
|
function isRecord(value) {
|
|
30
6
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -45,9 +21,8 @@ async function distributedPacket(path) {
|
|
|
45
21
|
* The `Bun.build` plugin that makes a bundle a distributed build. While Bun bundles, it answers the
|
|
46
22
|
* import of every `loom.packet.json` with the file's members and `build` set to `distributed`, and
|
|
47
23
|
* it never writes to the source tree, so the source run keeps reading `development`. A compiled
|
|
48
|
-
* binary built through `Bun.build` with this plugin is distributed too. It
|
|
49
|
-
*
|
|
50
|
-
* a bundle nor a compiled binary can start.
|
|
24
|
+
* binary built through `Bun.build` with this plugin is distributed too. It answers no other module,
|
|
25
|
+
* so the bundle holds every other module as Bun bundles it.
|
|
51
26
|
*/
|
|
52
27
|
export function packet() {
|
|
53
28
|
return {
|
|
@@ -57,10 +32,6 @@ export function packet() {
|
|
|
57
32
|
contents: await distributedPacket(path),
|
|
58
33
|
loader: 'json',
|
|
59
34
|
}));
|
|
60
|
-
build.onLoad({ filter: unicodeTables }, async ({ path }) => ({
|
|
61
|
-
contents: await inlinedTables(path),
|
|
62
|
-
loader: 'js',
|
|
63
|
-
}));
|
|
64
35
|
},
|
|
65
36
|
};
|
|
66
37
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
+
import { checkDecisions } from './release-decisions.js';
|
|
2
3
|
import { prepareLockfile, releaseInsertion } from './release-files.js';
|
|
3
4
|
import { prepareRelease, releaseDate, requireReleaseNotes } from './release.js';
|
|
4
5
|
import { currentVersion, git, readLibraries, readRegularFile } from './repository.js';
|
|
@@ -43,11 +44,13 @@ export function checkRelease(root, base, head, version, changed) {
|
|
|
43
44
|
throw new Error(`${library.path}: release cuts change only the version field.`);
|
|
44
45
|
}
|
|
45
46
|
}
|
|
47
|
+
const decisions = checkDecisions(root, base, head, { date: options.date, version }, changed);
|
|
46
48
|
const allowed = new Set([
|
|
47
49
|
'CHANGELOG.md',
|
|
48
50
|
'pnpm-lock.yaml',
|
|
49
51
|
...libraries.map((library) => library.path),
|
|
50
52
|
...release.fragments.map((fragment) => `.changes/${fragment.name}`),
|
|
53
|
+
...decisions,
|
|
51
54
|
]);
|
|
52
55
|
for (const path of changed) {
|
|
53
56
|
if (!allowed.has(path)) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admits the decision files a release cut may change: each record it marks accepted, and the index
|
|
3
|
+
* row of each one. Any other change to a record or the index throws naming the file. Returns the
|
|
4
|
+
* admitted paths.
|
|
5
|
+
*/
|
|
6
|
+
export declare function checkDecisions(root: string, base: string, head: string, release: {
|
|
7
|
+
date: string;
|
|
8
|
+
version: string;
|
|
9
|
+
}, changed: string[]): string[];
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { git, readRegularFile } from './repository.js';
|
|
2
|
+
const decisionsDirectory = 'docs/decisions/';
|
|
3
|
+
const decisionIndex = `${decisionsDirectory}README.md`;
|
|
4
|
+
const recordName = /^\d{4}-[^/]+\.md$/u;
|
|
5
|
+
/**
|
|
6
|
+
* The record a release cut writes when it marks a proposed decision accepted: the status, the
|
|
7
|
+
* modified date, and a Status section that names the release, with every other line kept.
|
|
8
|
+
* Undefined when the source is not a proposed record with one modified date and one Status section.
|
|
9
|
+
*/
|
|
10
|
+
function acceptedRecord(source, version, date) {
|
|
11
|
+
const lines = source.split('\n');
|
|
12
|
+
const close = lines.indexOf('---', 1);
|
|
13
|
+
if (lines[0] !== '---' || close === -1) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const field = (key) => lines.slice(1, close).flatMap((line, index) => (line.startsWith(`${key}:`) ? [index + 1] : []));
|
|
17
|
+
const [status, ...extraStatus] = field('status');
|
|
18
|
+
const [modified, ...extraModified] = field('modified');
|
|
19
|
+
const headings = lines.flatMap((line, index) => index > close && line === '## Status' ? [index] : []);
|
|
20
|
+
const [heading, ...extraHeadings] = headings;
|
|
21
|
+
if (status === undefined ||
|
|
22
|
+
lines[status] !== 'status: proposed' ||
|
|
23
|
+
modified === undefined ||
|
|
24
|
+
heading === undefined ||
|
|
25
|
+
extraStatus.length + extraModified.length + extraHeadings.length > 0) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const next = lines.findIndex((line, index) => index > heading && /^#{1,2} /u.test(line));
|
|
29
|
+
const end = next === -1 ? lines.length : next;
|
|
30
|
+
const fields = lines.map((line, index) => {
|
|
31
|
+
if (index === status) {
|
|
32
|
+
return 'status: accepted';
|
|
33
|
+
}
|
|
34
|
+
return index === modified ? `modified: ${date}` : line;
|
|
35
|
+
});
|
|
36
|
+
return [
|
|
37
|
+
...fields.slice(0, heading + 1),
|
|
38
|
+
'',
|
|
39
|
+
`Accepted in ${version}.`,
|
|
40
|
+
'',
|
|
41
|
+
...fields.slice(end),
|
|
42
|
+
].join('\n');
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The index with each named record's status cell changed from proposed to accepted. Undefined when
|
|
46
|
+
* a named record has no single row whose first cell links it and whose last cell reads proposed.
|
|
47
|
+
*/
|
|
48
|
+
function acceptedIndex(source, names) {
|
|
49
|
+
const lines = source.split('\n');
|
|
50
|
+
for (const name of names) {
|
|
51
|
+
const rows = lines.flatMap((line, index) => /^\[[^\]]+\]\((?<target>[^)]+)\)$/u.exec(line.split('|')[1]?.trim() ?? '')?.groups?.target ===
|
|
52
|
+
name
|
|
53
|
+
? [index]
|
|
54
|
+
: []);
|
|
55
|
+
const [row, ...extraRows] = rows;
|
|
56
|
+
const text = row === undefined ? undefined : lines[row];
|
|
57
|
+
const flipped = text?.replace(/\|(?<before>\s*)proposed(?<after>\s*)\|$/u, '|$1accepted$2|');
|
|
58
|
+
if (row === undefined || extraRows.length > 0 || flipped === undefined || flipped === text) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
lines[row] = flipped;
|
|
62
|
+
}
|
|
63
|
+
return lines.join('\n');
|
|
64
|
+
}
|
|
65
|
+
function present(root, ref, path) {
|
|
66
|
+
return git(root, ['ls-tree', ref, '--', path]) !== '';
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Admits the decision files a release cut may change: each record it marks accepted, and the index
|
|
70
|
+
* row of each one. Any other change to a record or the index throws naming the file. Returns the
|
|
71
|
+
* admitted paths.
|
|
72
|
+
*/
|
|
73
|
+
export function checkDecisions(root, base, head, release, changed) {
|
|
74
|
+
const records = changed.filter((path) => path.startsWith(decisionsDirectory) && recordName.test(path.slice(decisionsDirectory.length)));
|
|
75
|
+
for (const path of records) {
|
|
76
|
+
const expected = present(root, base, path) && present(root, head, path)
|
|
77
|
+
? acceptedRecord(readRegularFile(root, path, base), release.version, release.date)
|
|
78
|
+
: undefined;
|
|
79
|
+
if (expected === undefined || readRegularFile(root, path, head) !== expected) {
|
|
80
|
+
throw new Error(`${path}: a release cut changes a decision record only to mark a proposed decision accepted, with status accepted, modified ${release.date}, and a Status section that reads "Accepted in ${release.version}.". Make any other change in an ordinary PR.`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (records.length === 0 && !changed.includes(decisionIndex)) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
const expected = present(root, base, decisionIndex)
|
|
87
|
+
? acceptedIndex(readRegularFile(root, decisionIndex, base), records.map((path) => path.slice(decisionsDirectory.length)))
|
|
88
|
+
: undefined;
|
|
89
|
+
if (expected === undefined ||
|
|
90
|
+
!present(root, head, decisionIndex) ||
|
|
91
|
+
readRegularFile(root, decisionIndex, head) !== expected) {
|
|
92
|
+
throw new Error(`${decisionIndex}: a release cut changes the decision index only to mark each decision it accepts from proposed to accepted. Make any other change in an ordinary PR.`);
|
|
93
|
+
}
|
|
94
|
+
return [...records, decisionIndex];
|
|
95
|
+
}
|
|
@@ -33,7 +33,7 @@ function pnpmManifestPath(resolve = (specifier) => import.meta.resolve(specifier
|
|
|
33
33
|
return fileURLToPath(resolve('pnpm/package.json'));
|
|
34
34
|
}
|
|
35
35
|
catch (error) {
|
|
36
|
-
throw new Error('Preparing pnpm-lock.yaml requires pnpm, an optional peer dependency of @loomcli/loom. Install pnpm 12.
|
|
36
|
+
throw new Error('Preparing pnpm-lock.yaml requires pnpm, an optional peer dependency of @loomcli/loom. Install pnpm 12.8.1 beside @loomcli/loom and run the command again.', { cause: error });
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
function updateLockfile(root) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loomcli/loom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "The Loom CLI toolchain. Provides the packet() Bun.build plugin that marks a bundled Loom CLI application as a distributed build.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -19,16 +19,16 @@
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@loomcli/core": "0.
|
|
22
|
+
"@loomcli/core": "0.8.0",
|
|
23
23
|
"mdast-util-from-markdown": "^2.0.3",
|
|
24
|
-
"yaml": "^2.9.
|
|
25
|
-
"zod": "^4.5
|
|
24
|
+
"yaml": "^2.9.1",
|
|
25
|
+
"zod": "^4.6.5"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"pnpm": "12.
|
|
28
|
+
"pnpm": "12.8.1"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"pnpm": "12.
|
|
31
|
+
"pnpm": "12.8.1"
|
|
32
32
|
},
|
|
33
33
|
"peerDependenciesMeta": {
|
|
34
34
|
"pnpm": {
|