@turri-dev/cli 0.1.14 → 0.1.15-next.5bcaf8b
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/dist/plain/graph.d.ts +10 -0
- package/dist/plain/graph.js +18 -8
- package/dist/plain/parser.d.ts +6 -0
- package/dist/plain/parser.js +6 -4
- package/dist/regenerate/resources.d.ts +2 -0
- package/dist/regenerate/resources.js +11 -13
- package/docs/hosting.md +3 -2
- package/package.json +1 -1
package/dist/plain/graph.d.ts
CHANGED
|
@@ -49,6 +49,16 @@ export interface PlainItemNode {
|
|
|
49
49
|
concept: string;
|
|
50
50
|
definition: string | null;
|
|
51
51
|
}>;
|
|
52
|
+
/**
|
|
53
|
+
* The linked resources it names, a functional spec's acceptance tests' too - `[text](target)` as the parser reads
|
|
54
|
+
* them - with the file each is read from when regenerating, null when it is not there, and the line the link is on.
|
|
55
|
+
*/
|
|
56
|
+
resources: Array<{
|
|
57
|
+
text: string;
|
|
58
|
+
target: string;
|
|
59
|
+
file: string | null;
|
|
60
|
+
line: number;
|
|
61
|
+
}>;
|
|
52
62
|
}
|
|
53
63
|
/** A concept as defined in one place - or ***plain's own - and every item that names it there. */
|
|
54
64
|
export interface PlainConceptNode {
|
package/dist/plain/graph.js
CHANGED
|
@@ -2,10 +2,11 @@ import { readdirSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
3
|
import { resolveTemplateDirs } from "../regenerate/paths.js";
|
|
4
4
|
import { locateModule } from "../regenerate/requires.js";
|
|
5
|
+
import { resourceFile } from "../regenerate/resources.js";
|
|
5
6
|
import { DEFAULT_CONCEPT_DEFINITIONS, DEFAULT_CONCEPTS, definitionText, extractConceptsFromDefinition, findConcepts } from "./concepts.js";
|
|
6
7
|
import { lineNumber, renderToMarkdown } from "./markdown.js";
|
|
7
8
|
import { attempt, namesIn, readModule } from "./module-concepts.js";
|
|
8
|
-
import { IMPORT_DIRECTIVE, modulePlainFile, parsePlainFile, PLAIN_SOURCE_FILE_EXTENSION, plainFileParser, readPlainSourceMetadata, REQUIRES_DIRECTIVE, withParseCache, } from "./parser.js";
|
|
9
|
+
import { IMPORT_DIRECTIVE, linksIn, modulePlainFile, parsePlainFile, PLAIN_SOURCE_FILE_EXTENSION, plainFileParser, readPlainSourceMetadata, REQUIRES_DIRECTIVE, withParseCache, } from "./parser.js";
|
|
9
10
|
import { DEFINITIONS, FUNCTIONAL_REQUIREMENTS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS } from "./spec.js";
|
|
10
11
|
import { isStdlibModule } from "./stdlib.js";
|
|
11
12
|
const SECTIONS = [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS, FUNCTIONAL_REQUIREMENTS];
|
|
@@ -88,10 +89,18 @@ export function plainGraph(projectDir) {
|
|
|
88
89
|
const defines = section === DEFINITIONS ? extractConceptsFromDefinition(text)[0] : [];
|
|
89
90
|
const named = [...new Set(findConcepts(text).map((found) => found.concept))].filter((concept) => !defines.includes(concept));
|
|
90
91
|
const id = idOf(item, node.file);
|
|
92
|
+
const line = lineOf(item, node.file, lines);
|
|
93
|
+
// A link is on the line the parser has it on, moved as Liquid moved its item.
|
|
94
|
+
const resources = linksIn(item).map(({ text, target, node: link }) => ({
|
|
95
|
+
text,
|
|
96
|
+
target,
|
|
97
|
+
file: resourceFile(target, dirs),
|
|
98
|
+
line: lineOf(link, node.file, lines, { at: lineNumber(link) + line - lineNumber(item), needle: `](${target})`, from: line }),
|
|
99
|
+
}));
|
|
91
100
|
const uses = named.map((concept) => ({ concept, definition: DEFAULT_CONCEPTS.has(concept) ? null : (visible.get(concept) ?? null) }));
|
|
92
101
|
for (const use of uses)
|
|
93
102
|
concepts.get(`${use.concept}\0${use.definition ?? ""}`)?.usedBy.push(id);
|
|
94
|
-
items.push({ id, file: node.file, module: node.module, section, line
|
|
103
|
+
items.push({ id, file: node.file, module: node.module, section, line, text, ...(section === FUNCTIONAL_REQUIREMENTS ? { frid: String(index + 1) } : {}), defines, uses, resources });
|
|
95
104
|
node.items.push(id);
|
|
96
105
|
});
|
|
97
106
|
}
|
|
@@ -116,19 +125,20 @@ function parseError(node, dirs) {
|
|
|
116
125
|
/**
|
|
117
126
|
* The line an item starts on in its file. The parser's is a line of the text after Liquid has run, which is the file's
|
|
118
127
|
* own unless an include or a tag changed the lines above it: then the first line of the file that has the item's
|
|
119
|
-
* first line is taken.
|
|
128
|
+
* first line is taken. A link is looked for so too - by its `](target)`, from its item's line on.
|
|
120
129
|
*/
|
|
121
|
-
function lineOf(node, file, cache) {
|
|
130
|
+
function lineOf(node, file, cache, look = {}) {
|
|
122
131
|
let lines = cache.get(file);
|
|
123
132
|
if (lines === undefined) {
|
|
124
133
|
lines = (attempt(() => readFileSync(file, "utf8")) ?? "").split(/\r?\n/);
|
|
125
134
|
cache.set(file, lines);
|
|
126
135
|
}
|
|
127
|
-
const at = lineNumber(node);
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
136
|
+
const at = look.at ?? lineNumber(node);
|
|
137
|
+
const needle = look.needle ?? renderToMarkdown(node).trim().split("\n")[0].replace(/^[-*+]\s+/, "").trim();
|
|
138
|
+
if (needle === "" || lines[at - 1]?.includes(needle))
|
|
130
139
|
return at;
|
|
131
|
-
const
|
|
140
|
+
const from = Math.max(0, (look.from ?? 1) - 1);
|
|
141
|
+
const found = lines.findIndex((line, index) => index >= from && line.includes(needle));
|
|
132
142
|
return found === -1 ? at : found + 1;
|
|
133
143
|
}
|
|
134
144
|
function messageOf(error) {
|
package/dist/plain/parser.d.ts
CHANGED
|
@@ -21,6 +21,12 @@ export interface PlainFileParseResult {
|
|
|
21
21
|
}
|
|
22
22
|
export declare function getFilenameFromModuleName(moduleName: string): string;
|
|
23
23
|
export declare function getModuleNameFromFilename(filename: string): string;
|
|
24
|
+
/** The links written in an item - what each says and where it points - as the parser reads them, checked or not. */
|
|
25
|
+
export declare function linksIn(section: AnyNode): Array<{
|
|
26
|
+
text: string;
|
|
27
|
+
target: string;
|
|
28
|
+
node: AnyNode;
|
|
29
|
+
}>;
|
|
24
30
|
export declare function checkSectionForLinkedResources(section: AnyNode, dirs?: string[]): void;
|
|
25
31
|
export declare function checkForLinkedResources(plainSource: ParsedPlainSource, dirs?: string[]): void;
|
|
26
32
|
export declare function processSectionCodeVariables(section: SpecificationItem, codeVariables: Record<string, Record<string, string>>): void;
|
package/dist/plain/parser.js
CHANGED
|
@@ -88,13 +88,15 @@ function parseNoting(run) {
|
|
|
88
88
|
function stillHolds(kept, fingerprint) {
|
|
89
89
|
return kept !== undefined && fingerprint !== undefined && kept.fingerprint === fingerprint && linkedStamp(kept.linked) === kept.linkedStamp;
|
|
90
90
|
}
|
|
91
|
+
/** The links written in an item - what each says and where it points - as the parser reads them, checked or not. */
|
|
92
|
+
export function linksIn(section) {
|
|
93
|
+
return [...traverse(section, "link")].map((link) => ({ text: String(link.children?.[0]?.value ?? ""), target: String(link.url ?? ""), node: link }));
|
|
94
|
+
}
|
|
91
95
|
export function checkSectionForLinkedResources(section, dirs = []) {
|
|
92
96
|
const linkedResources = [];
|
|
93
|
-
for (const link of
|
|
94
|
-
const target = String(link.url ?? "");
|
|
97
|
+
for (const { text, target, node: link } of linksIn(section)) {
|
|
95
98
|
const children = link.children ?? [];
|
|
96
99
|
if (hasScheme(target) || isAbsolute(target)) {
|
|
97
|
-
const text = children[0]?.value ?? "";
|
|
98
100
|
throw new PlainSyntaxError(`Plain syntax error: Only relative links are allowed (text: ${text}, target: ${target}).`);
|
|
99
101
|
}
|
|
100
102
|
const file = linkedFile(target, dirs);
|
|
@@ -112,7 +114,7 @@ export function checkSectionForLinkedResources(section, dirs = []) {
|
|
|
112
114
|
"Referencing .plain files through Linked Resources is not supported. " +
|
|
113
115
|
"Please use the import section to include definitions or implementation/test requirements from another module.");
|
|
114
116
|
}
|
|
115
|
-
linkedResources.push({ text
|
|
117
|
+
linkedResources.push({ text, target });
|
|
116
118
|
}
|
|
117
119
|
if (linkedResources.length > 0)
|
|
118
120
|
section.linked_resources = linkedResources;
|
|
@@ -32,6 +32,8 @@ export declare class LinkedImageError extends Error {
|
|
|
32
32
|
export declare function loadLinkedResources(tree: MarshalledPlainSource,
|
|
33
33
|
/** Up to and including this functionality. Null: the whole module. */
|
|
34
34
|
frid: string | null, templateDirs: string[]): LoadedResource[];
|
|
35
|
+
/** The file a linked resource is read from: the first template folder that has it, else the current folder - or null. */
|
|
36
|
+
export declare function resourceFile(target: string, templateDirs: string[]): string | null;
|
|
35
37
|
/**
|
|
36
38
|
* A linked resource that is code - a source file, or a build manifest: the specification points at code that lives
|
|
37
39
|
* elsewhere, a host's module the code plugs into, for the code to build on as it says. It is never copied into the
|
|
@@ -42,8 +42,8 @@ frid, templateDirs) {
|
|
|
42
42
|
loaded.push({ text: resource.text, target: resource.target, content: `[an image: attached to your first message, image ${number}]`, image });
|
|
43
43
|
continue;
|
|
44
44
|
}
|
|
45
|
-
const source =
|
|
46
|
-
if (source ===
|
|
45
|
+
const source = resourceFile(resource.target, templateDirs);
|
|
46
|
+
if (source === null)
|
|
47
47
|
continue;
|
|
48
48
|
let content = readFileSync(source, "utf8");
|
|
49
49
|
const whole = content.length;
|
|
@@ -52,10 +52,15 @@ frid, templateDirs) {
|
|
|
52
52
|
? `${content.slice(0, MAX_RESOURCE_CHARS)}\n... [cut here: ${whole} characters whole - code that lives outside your codebase: build on it as the specification says, never copy it]`
|
|
53
53
|
: `${content.slice(0, MAX_RESOURCE_CHARS)}\n... [cut here: ${whole} characters whole - copy_resource puts the whole file at its code-copy path, to read it there]`;
|
|
54
54
|
}
|
|
55
|
-
loaded.push({ text: resource.text, target: resource.target, content, source
|
|
55
|
+
loaded.push({ text: resource.text, target: resource.target, content, source, ...(whole > MAX_RESOURCE_CHARS ? { cutFrom: whole } : {}) });
|
|
56
56
|
}
|
|
57
57
|
return loaded;
|
|
58
58
|
}
|
|
59
|
+
/** The file a linked resource is read from: the first template folder that has it, else the current folder - or null. */
|
|
60
|
+
export function resourceFile(target, templateDirs) {
|
|
61
|
+
const found = [...templateDirs.map((dir) => join(dir, target)), target].find((path) => isFileAt(path));
|
|
62
|
+
return found === undefined ? null : resolve(found);
|
|
63
|
+
}
|
|
59
64
|
function isFileAt(path) {
|
|
60
65
|
try {
|
|
61
66
|
return statSync(path).isFile();
|
|
@@ -145,16 +150,9 @@ export function resourceImages(resources) {
|
|
|
145
150
|
}
|
|
146
151
|
/** A linked image's bytes, found as a text resource is: the template folders first, then the current directory. */
|
|
147
152
|
function loadImage(templateDirs, target, mimeType) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (!statSync(path).isFile())
|
|
152
|
-
continue;
|
|
153
|
-
size = statSync(path).size;
|
|
154
|
-
}
|
|
155
|
-
catch {
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
153
|
+
const path = resourceFile(target, templateDirs);
|
|
154
|
+
if (path !== null) {
|
|
155
|
+
const size = statSync(path).size;
|
|
158
156
|
if (size > MAX_IMAGE_BYTES) {
|
|
159
157
|
throw new LinkedImageError(`The linked image ${target} is ${(size / 1024 / 1024).toFixed(1)} MB - a model takes images up to ${MAX_IMAGE_BYTES / 1024 / 1024} MB. Make it smaller, or link a smaller one.`);
|
|
160
158
|
}
|
package/docs/hosting.md
CHANGED
|
@@ -122,12 +122,13 @@ app.requires; // [{ name: "base", file: "/…/base.plain" }] - and imports,
|
|
|
122
122
|
app.items; // ["/…/app.plain:11", "/…/app.plain:15"] - its own items, in order
|
|
123
123
|
items.find((item) => item.id === app.items[1]);
|
|
124
124
|
// { id, file, module: "app", section: "functional specs", line: 15, frid: "1", text: "- :User: can fill :Cart:.",
|
|
125
|
-
// defines: [], uses: [{ concept: ":User:", definition: "/…/base.plain:8" }, { concept: ":Cart:", definition: "/…/app.plain:11" }]
|
|
125
|
+
// defines: [], uses: [{ concept: ":User:", definition: "/…/base.plain:8" }, { concept: ":Cart:", definition: "/…/app.plain:11" }],
|
|
126
|
+
// resources: [{ text: "the schema", target: "schema.json", file: "/…/schema.json", line: 15 }] }
|
|
126
127
|
concepts.find((concept) => concept.definition === "/…/base.plain:8");
|
|
127
128
|
// { concept: ":User:", definition: "/…/base.plain:8", module: "base", text: "is a person who signs up.", usedBy: [...] }
|
|
128
129
|
```
|
|
129
130
|
|
|
130
|
-
- **The graph.** `plainGraph` reads every .plain file in the project's folder and its `template/` folder, and every module they import or require, wherever the parser finds it - the standard library's templates too. A **module** is a file: its imports, resolved as the parser reads an import (a `stdlib/` module from turri's own folder), its required modules, resolved as the requires chain finds them - `file: null` when one is not found - and the reverse of both, `importedBy` and `requiredBy`, so a host goes up and down the requires chain without inverting anything. An **item** is one item of a section - a definition, an implementation req, a test req, a functional spec with its acceptance tests - with its file, the line it starts on, its text, a functional spec's `frid`, the concepts a definition defines, and each concept it names with the item that defines it: its own module's definitions, its imports', and those its required modules export, as the parser sees them from that module. `definition` is null for \*\*\*plain's own concepts, and for one defined nowhere the module can see. A **concept** is one definition of it - or \*\*\*plain's own, `definition: null` - with every item that names it there. An item's id is `<file>:<line>`: the file to open and the line to go to.
|
|
131
|
+
- **The graph.** `plainGraph` reads every .plain file in the project's folder and its `template/` folder, and every module they import or require, wherever the parser finds it - the standard library's templates too. A **module** is a file: its imports, resolved as the parser reads an import (a `stdlib/` module from turri's own folder), its required modules, resolved as the requires chain finds them - `file: null` when one is not found - and the reverse of both, `importedBy` and `requiredBy`, so a host goes up and down the requires chain without inverting anything. An **item** is one item of a section - a definition, an implementation req, a test req, a functional spec with its acceptance tests - with its file, the line it starts on, its text, a functional spec's `frid`, the concepts a definition defines, and each concept it names with the item that defines it: its own module's definitions, its imports', and those its required modules export, as the parser sees them from that module. `definition` is null for \*\*\*plain's own concepts, and for one defined nowhere the module can see. Its `resources` are the linked resources it names - `[text](target)`, a functional spec's acceptance tests' too, as the parser reads them - each with `file`, the file regenerating reads it from (the template folders first, then the current folder), null when it is not there, and `line`, the line the link is on. `resources` is on every item, `[]` when it names none: a turri that gives no resources has no field. A **concept** is one definition of it - or \*\*\*plain's own, `definition: null` - with every item that names it there. An item's id is `<file>:<line>`: the file to open and the line to go to.
|
|
131
132
|
- **What does not parse.** Nothing throws. A file that does not parse has `error`, the parser's message; its links are what its front matter says, and its items what of its own text reads without its imports. A file whose items Liquid moves keeps the lines of its own text.
|
|
132
133
|
- **Concepts.** `conceptsOf(plainFile)` is what one module can name - `{ concept, definition, module }`, `definition` the text after its concepts, `module` null for \*\*\*plain's own - for suggestions as the person types. `findConcepts(text)` is every concept written in a piece of text, `{ concept, start, end }`, and `conceptBeingTyped(text)` the one at its end still open - `{ typed: ":Us", start }`, or undefined - both with the parser's own pattern.
|
|
133
134
|
- **The standard library.** `stdlibTemplates()` is every template in turri's `stdlib/`, sorted: `name` - what a module imports it as - `file`, `kind` (`backend`, `frontend`, `cli`, `library` or `lang`), `description`, `text`, `imports` as the parser reads its front matter, a stack's `conformance` template when it has one, and a conformance template's stack as `tests`. `isStack(template)` says whether it is a stack to import; `STDLIB_KINDS` is the kinds in order, each with its title.
|