@webround/editor 0.0.0-stage → 0.3.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.
Files changed (227) hide show
  1. package/dist/blocks.d.ts +90 -0
  2. package/dist/blocks.js +119 -0
  3. package/dist/bridge.d.ts +96 -0
  4. package/dist/bridge.js +970 -0
  5. package/dist/check/aria.d.ts +19 -0
  6. package/dist/check/aria.js +106 -0
  7. package/dist/check/cli.d.ts +3 -0
  8. package/dist/check/cli.js +30 -0
  9. package/dist/check/css.d.ts +13 -0
  10. package/dist/check/css.js +63 -0
  11. package/dist/check/html.d.ts +8 -0
  12. package/dist/check/html.js +29 -0
  13. package/dist/check/index.d.ts +4 -0
  14. package/dist/check/index.js +5 -0
  15. package/dist/check/links.d.ts +7 -0
  16. package/dist/check/links.js +23 -0
  17. package/dist/check/orphans.d.ts +20 -0
  18. package/dist/check/orphans.js +57 -0
  19. package/dist/check/producer-code.d.ts +53 -0
  20. package/dist/check/producer-code.js +379 -0
  21. package/dist/check/producer-dom.d.ts +21 -0
  22. package/dist/check/producer-dom.js +52 -0
  23. package/dist/check/project.d.ts +20 -0
  24. package/dist/check/project.js +51 -0
  25. package/dist/check/rules.d.ts +7 -0
  26. package/dist/check/rules.js +517 -0
  27. package/dist/check/types.d.ts +160 -0
  28. package/dist/check/types.js +22 -0
  29. package/dist/components/WrAltcha.d.ts +5 -0
  30. package/dist/components/WrAltcha.js +8 -0
  31. package/dist/components/WrButton.d.ts +8 -0
  32. package/dist/components/WrButton.js +34 -0
  33. package/dist/components/WrForm.d.ts +6 -0
  34. package/dist/components/WrForm.js +29 -0
  35. package/dist/components/WrFormPrivacy.d.ts +4 -0
  36. package/dist/components/WrFormPrivacy.js +7 -0
  37. package/dist/components/WrFormSuccess.d.ts +4 -0
  38. package/dist/components/WrFormSuccess.js +12 -0
  39. package/dist/components/WrIframe.d.ts +12 -0
  40. package/dist/components/WrIframe.js +30 -0
  41. package/dist/components/WrImage.d.ts +9 -0
  42. package/dist/components/WrImage.js +28 -0
  43. package/dist/components/WrInput.d.ts +8 -0
  44. package/dist/components/WrInput.js +29 -0
  45. package/dist/components/WrLink.d.ts +10 -0
  46. package/dist/components/WrLink.js +38 -0
  47. package/dist/components/WrNav.d.ts +26 -0
  48. package/dist/components/WrNav.js +30 -0
  49. package/dist/components/WrSection.d.ts +11 -0
  50. package/dist/components/WrSection.js +5 -0
  51. package/dist/components/WrShared.d.ts +12 -0
  52. package/dist/components/WrShared.js +20 -0
  53. package/dist/components/WrText.d.ts +11 -0
  54. package/dist/components/WrText.js +41 -0
  55. package/dist/components/WrVideo.d.ts +9 -0
  56. package/dist/components/WrVideo.js +73 -0
  57. package/dist/components/text-editor/CodeThemeContext.d.ts +8 -0
  58. package/dist/components/text-editor/CodeThemeContext.js +10 -0
  59. package/dist/components/text-editor/ExampleTheme.d.ts +77 -0
  60. package/dist/components/text-editor/ExampleTheme.js +76 -0
  61. package/dist/components/text-editor/ToolbarContext.d.ts +17 -0
  62. package/dist/components/text-editor/ToolbarContext.js +32 -0
  63. package/dist/components/text-editor/WrTextEditor.d.ts +13 -0
  64. package/dist/components/text-editor/WrTextEditor.js +48 -0
  65. package/dist/components/text-editor/common/constants.d.ts +109 -0
  66. package/dist/components/text-editor/common/constants.js +58 -0
  67. package/dist/components/text-editor/common/surface.d.ts +3 -0
  68. package/dist/components/text-editor/common/surface.js +8 -0
  69. package/dist/components/text-editor/common/utils.d.ts +7 -0
  70. package/dist/components/text-editor/common/utils.js +78 -0
  71. package/dist/components/text-editor/hooks/useLexicalCommitOnBlur.d.ts +5 -0
  72. package/dist/components/text-editor/hooks/useLexicalCommitOnBlur.js +41 -0
  73. package/dist/components/text-editor/hooks/useLexicalMarkdown.d.ts +33 -0
  74. package/dist/components/text-editor/hooks/useLexicalMarkdown.js +284 -0
  75. package/dist/components/text-editor/nodes/CodeBlockNode.d.ts +23 -0
  76. package/dist/components/text-editor/nodes/CodeBlockNode.js +138 -0
  77. package/dist/components/text-editor/nodes/HorizontalRuleNode.d.ts +13 -0
  78. package/dist/components/text-editor/nodes/HorizontalRuleNode.js +22 -0
  79. package/dist/components/text-editor/plugins/CodeBlockPlugin.d.ts +2 -0
  80. package/dist/components/text-editor/plugins/CodeBlockPlugin.js +38 -0
  81. package/dist/components/text-editor/plugins/FontPlugin.d.ts +12 -0
  82. package/dist/components/text-editor/plugins/FontPlugin.js +114 -0
  83. package/dist/components/text-editor/plugins/FontSizePlugin.d.ts +7 -0
  84. package/dist/components/text-editor/plugins/FontSizePlugin.js +105 -0
  85. package/dist/components/text-editor/plugins/FontWeightPlugin.d.ts +6 -0
  86. package/dist/components/text-editor/plugins/FontWeightPlugin.js +103 -0
  87. package/dist/components/text-editor/plugins/HTMLTagPlugin.d.ts +7 -0
  88. package/dist/components/text-editor/plugins/HTMLTagPlugin.js +82 -0
  89. package/dist/components/text-editor/plugins/HorizontalRulePlugin.d.ts +2 -0
  90. package/dist/components/text-editor/plugins/HorizontalRulePlugin.js +35 -0
  91. package/dist/components/text-editor/plugins/LineSpacingPlugin.d.ts +7 -0
  92. package/dist/components/text-editor/plugins/LineSpacingPlugin.js +83 -0
  93. package/dist/components/text-editor/plugins/LinkPlugin.d.ts +8 -0
  94. package/dist/components/text-editor/plugins/LinkPlugin.js +183 -0
  95. package/dist/components/text-editor/plugins/ListToolbarPlugin.d.ts +7 -0
  96. package/dist/components/text-editor/plugins/ListToolbarPlugin.js +40 -0
  97. package/dist/components/text-editor/plugins/LoadContentPlugin.d.ts +4 -0
  98. package/dist/components/text-editor/plugins/LoadContentPlugin.js +63 -0
  99. package/dist/components/text-editor/plugins/MarkdownExportPlugin.d.ts +4 -0
  100. package/dist/components/text-editor/plugins/MarkdownExportPlugin.js +17 -0
  101. package/dist/components/text-editor/plugins/MarkdownPastePlugin.d.ts +2 -0
  102. package/dist/components/text-editor/plugins/MarkdownPastePlugin.js +45 -0
  103. package/dist/components/text-editor/plugins/SanitizePastePlugin.d.ts +3 -0
  104. package/dist/components/text-editor/plugins/SanitizePastePlugin.js +40 -0
  105. package/dist/components/text-editor/plugins/TextAlignPlugin.d.ts +7 -0
  106. package/dist/components/text-editor/plugins/TextAlignPlugin.js +65 -0
  107. package/dist/components/text-editor/plugins/TextColorPlugin.d.ts +7 -0
  108. package/dist/components/text-editor/plugins/TextColorPlugin.js +117 -0
  109. package/dist/components/text-editor/plugins/ToolbarPlugin.d.ts +10 -0
  110. package/dist/components/text-editor/plugins/ToolbarPlugin.js +73 -0
  111. package/dist/components/text-editor/theme-colors.d.ts +11 -0
  112. package/dist/components/text-editor/theme-colors.js +28 -0
  113. package/dist/context/WrAppContext.d.ts +35 -0
  114. package/dist/context/WrAppContext.js +113 -0
  115. package/dist/context/WrLayout.d.ts +15 -0
  116. package/dist/context/WrLayout.js +22 -0
  117. package/dist/context/WrPageContext.d.ts +28 -0
  118. package/dist/context/WrPageContext.js +282 -0
  119. package/dist/context/restoreValues.d.ts +2 -0
  120. package/dist/context/restoreValues.js +6 -0
  121. package/dist/contrast.d.ts +45 -0
  122. package/dist/contrast.js +122 -0
  123. package/dist/drop.d.ts +7 -0
  124. package/dist/drop.js +51 -0
  125. package/dist/gaps.d.ts +14 -0
  126. package/dist/gaps.js +50 -0
  127. package/dist/hooks/useWrMessage.d.ts +5 -0
  128. package/dist/hooks/useWrMessage.js +24 -0
  129. package/dist/hooks/useWrSave.d.ts +2 -0
  130. package/dist/hooks/useWrSave.js +11 -0
  131. package/dist/index.css +467 -0
  132. package/dist/index.d.ts +20 -0
  133. package/dist/index.js +20 -0
  134. package/dist/interactive.d.ts +8 -0
  135. package/dist/interactive.js +13 -0
  136. package/dist/measure-rules.d.ts +34 -0
  137. package/dist/measure-rules.js +54 -0
  138. package/dist/measure.d.ts +11 -0
  139. package/dist/measure.js +139 -0
  140. package/dist/messages.d.ts +860 -0
  141. package/dist/messages.js +2 -0
  142. package/dist/nav.d.ts +5 -0
  143. package/dist/nav.js +10 -0
  144. package/dist/operations/animations-store.d.ts +6 -0
  145. package/dist/operations/animations-store.js +63 -0
  146. package/dist/operations/cli.d.ts +3 -0
  147. package/dist/operations/cli.js +52 -0
  148. package/dist/operations/comments-store.d.ts +17 -0
  149. package/dist/operations/comments-store.js +62 -0
  150. package/dist/operations/context.d.ts +12 -0
  151. package/dist/operations/context.js +35 -0
  152. package/dist/operations/define.d.ts +81 -0
  153. package/dist/operations/define.js +86 -0
  154. package/dist/operations/index.d.ts +6 -0
  155. package/dist/operations/index.js +6 -0
  156. package/dist/operations/ops/check.d.ts +5 -0
  157. package/dist/operations/ops/check.js +53 -0
  158. package/dist/operations/ops/comments.d.ts +6 -0
  159. package/dist/operations/ops/comments.js +133 -0
  160. package/dist/operations/ops/content.d.ts +16 -0
  161. package/dist/operations/ops/content.js +73 -0
  162. package/dist/operations/ops/json.d.ts +4 -0
  163. package/dist/operations/ops/json.js +27 -0
  164. package/dist/operations/ops/langs.d.ts +4 -0
  165. package/dist/operations/ops/langs.js +32 -0
  166. package/dist/operations/ops/nodes.d.ts +8 -0
  167. package/dist/operations/ops/nodes.js +270 -0
  168. package/dist/operations/ops/pages.d.ts +8 -0
  169. package/dist/operations/ops/pages.js +154 -0
  170. package/dist/operations/ops/project.d.ts +36 -0
  171. package/dist/operations/ops/project.js +97 -0
  172. package/dist/operations/ops/seo.d.ts +17 -0
  173. package/dist/operations/ops/seo.js +358 -0
  174. package/dist/operations/ops/style.d.ts +76 -0
  175. package/dist/operations/ops/style.js +268 -0
  176. package/dist/operations/ops/stylesets.d.ts +9 -0
  177. package/dist/operations/ops/stylesets.js +145 -0
  178. package/dist/operations/ops/theme.d.ts +6 -0
  179. package/dist/operations/ops/theme.js +304 -0
  180. package/dist/operations/registry.d.ts +8 -0
  181. package/dist/operations/registry.js +19 -0
  182. package/dist/operations/stylesets-store.d.ts +50 -0
  183. package/dist/operations/stylesets-store.js +216 -0
  184. package/dist/registry.d.ts +5 -0
  185. package/dist/registry.js +24 -0
  186. package/dist/save.d.ts +65 -0
  187. package/dist/save.js +86 -0
  188. package/dist/schema.d.ts +69 -0
  189. package/dist/schema.js +110 -0
  190. package/dist/scope.d.ts +41 -0
  191. package/dist/scope.js +44 -0
  192. package/dist/vite-plugins/ast.d.ts +3 -0
  193. package/dist/vite-plugins/ast.js +238 -0
  194. package/dist/vite-plugins/core-swap.d.ts +3 -0
  195. package/dist/vite-plugins/core-swap.js +57 -0
  196. package/dist/vite-plugins/create-page.d.ts +4 -0
  197. package/dist/vite-plugins/create-page.js +59 -0
  198. package/dist/vite-plugins/guard.d.ts +15 -0
  199. package/dist/vite-plugins/guard.js +35 -0
  200. package/dist/vite-plugins/head.d.ts +13 -0
  201. package/dist/vite-plugins/head.js +53 -0
  202. package/dist/vite-plugins/index.d.ts +7 -0
  203. package/dist/vite-plugins/index.js +7 -0
  204. package/dist/vite-plugins/jsx-tree.d.ts +118 -0
  205. package/dist/vite-plugins/jsx-tree.js +686 -0
  206. package/dist/vite-plugins/langs.d.ts +3 -0
  207. package/dist/vite-plugins/langs.js +44 -0
  208. package/dist/vite-plugins/operations.d.ts +10 -0
  209. package/dist/vite-plugins/operations.js +66 -0
  210. package/dist/vite-plugins/pages-api.d.ts +3 -0
  211. package/dist/vite-plugins/pages-api.js +112 -0
  212. package/dist/vite-plugins/pages.d.ts +3 -0
  213. package/dist/vite-plugins/pages.js +64 -0
  214. package/dist/vite-plugins/read.d.ts +3 -0
  215. package/dist/vite-plugins/read.js +45 -0
  216. package/dist/vite-plugins/seo-files.d.ts +8 -0
  217. package/dist/vite-plugins/seo-files.js +42 -0
  218. package/dist/vite-plugins/theme.d.ts +3 -0
  219. package/dist/vite-plugins/theme.js +207 -0
  220. package/dist/vite-plugins/update.d.ts +3 -0
  221. package/dist/vite-plugins/update.js +46 -0
  222. package/dist/vite-plugins/wr-plugins.d.ts +15 -0
  223. package/dist/vite-plugins/wr-plugins.js +34 -0
  224. package/dist/vite-plugins.d.ts +11 -0
  225. package/dist/vite-plugins.js +518 -0
  226. package/package.json +96 -5
  227. package/README.md +0 -3
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=messages.js.map
package/dist/nav.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /** What the page's path is, as the editor knows it: the preview's own prefix is not part of it. */
2
+ export declare const pathOf: (pathname: string) => string;
3
+ /** The path to tell the editor, or null when it is the one it was last told (a change of the query or of the hash only). `last` is null before anything was told. */
4
+ export declare function navigationTo(last: string | null, pathname: string): string | null;
5
+ //# sourceMappingURL=nav.d.ts.map
package/dist/nav.js ADDED
@@ -0,0 +1,10 @@
1
+ // When the page the preview shows changed: the path is what says which page it is. A change of the query or of the hash only (a shop's filters, a search, the page of a listing) is the same page
2
+ // in another state: it is not told to the editor and what is selected stays selected.
3
+ /** What the page's path is, as the editor knows it: the preview's own prefix is not part of it. */
4
+ export const pathOf = (pathname) => pathname.replace(/^\/preview(?=\/|$)/, '') || '/';
5
+ /** The path to tell the editor, or null when it is the one it was last told (a change of the query or of the hash only). `last` is null before anything was told. */
6
+ export function navigationTo(last, pathname) {
7
+ const path = pathOf(pathname);
8
+ return path === last ? null : path;
9
+ }
10
+ //# sourceMappingURL=nav.js.map
@@ -0,0 +1,6 @@
1
+ import { type OperationContext } from './define.js';
2
+ /** The entry animations a className uses (`motion-safe:animate-fade-in`, at any screen). */
3
+ export declare const entryAnimationsIn: (className: string) => string[];
4
+ /** Makes sure the theme has what the entry animations in this className need; nothing is written when it has it already. A site without `theme.css` cannot run them, and is told before anything is written. */
5
+ export declare function ensureEntryAnimations(ctx: OperationContext, className: string): void;
6
+ //# sourceMappingURL=animations-store.d.ts.map
@@ -0,0 +1,63 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import postcss from 'postcss';
4
+ import { WR_ENTRY_ANIMATIONS, WR_ENTRY_ANIMATIONS_CSS, wrParseClassName } from '@webround/core/style';
5
+ import { OperationError } from './define.js';
6
+ // The entry animations (fade in, slide up, zoom in) are the project's, not Tailwind's: the class `animate-fade-in` exists because the theme has `--animate-fade-in` and its keyframes in an
7
+ // `@theme`. This is the ONE place that writes them into `src/theme.css`, the first time a class uses one. The text of them is `WR_ENTRY_ANIMATIONS_CSS` (core): the tests compile the same.
8
+ const INTRO = 'Entry animations (fade in, slide up, zoom in), used as animate-fade-in… from the editor\'s Motion. They run with `motion-safe:`, so not for who asked their system for less motion.';
9
+ /** How the file indents (the first thing nested in a rule), so what is written looks like what is there. */
10
+ function indentOf(root) {
11
+ let found = '';
12
+ root.walkDecls((decl) => {
13
+ if (!found && decl.parent?.type === 'rule' && /^\n[ \t]+$/.test(decl.raws.before ?? ''))
14
+ found = decl.raws.before.slice(1);
15
+ });
16
+ return found || ' ';
17
+ }
18
+ const isPlainTheme = (n) => n.type === 'atrule' && n.name === 'theme' && n.params.trim() === '';
19
+ /** The entry animations a className uses (`motion-safe:animate-fade-in`, at any screen). */
20
+ export const entryAnimationsIn = (className) => WR_ENTRY_ANIMATIONS.filter(id => wrParseClassName(className).some(t => t.utility === `animate-${id}`));
21
+ /** Makes sure the theme has what the entry animations in this className need; nothing is written when it has it already. A site without `theme.css` cannot run them, and is told before anything is written. */
22
+ export function ensureEntryAnimations(ctx, className) {
23
+ const used = entryAnimationsIn(className);
24
+ if (used.length === 0)
25
+ return;
26
+ const file = path.join(ctx.srcRoot, 'theme.css');
27
+ if (!fs.existsSync(file))
28
+ throw new OperationError(409, 'no-theme-file', `this site has no src/theme.css: the keyframes of ${used.map(id => `animate-${id}`).join(', ')} are kept there`);
29
+ const root = postcss.parse(fs.readFileSync(file, 'utf-8'));
30
+ let block = root.nodes.find(isPlainTheme);
31
+ const indent = indentOf(root);
32
+ let changed = false;
33
+ for (const id of used) {
34
+ if (block?.nodes?.some(n => n.type === 'decl' && n.prop === `--animate-${id}`))
35
+ continue;
36
+ if (!block) {
37
+ block = postcss.atRule({ name: 'theme', params: '', nodes: [] });
38
+ block.raws.before = '\n\n';
39
+ block.raws.afterName = '';
40
+ block.raws.between = ' ';
41
+ const intro = postcss.comment({ text: INTRO });
42
+ intro.raws.before = `\n${indent}`;
43
+ block.prepend(intro);
44
+ root.append(block);
45
+ }
46
+ const { value, keyframes } = WR_ENTRY_ANIMATIONS_CSS[id];
47
+ block.append(postcss.decl({ prop: `--animate-${id}`, value }));
48
+ const frames = postcss.atRule({ name: 'keyframes', params: value.split(' ')[0] });
49
+ frames.raws.before = `\n\n${indent}`;
50
+ frames.raws.between = ' ';
51
+ frames.raws.after = `\n${indent}`;
52
+ // (a copy: appending a frame takes it out of the list being walked)
53
+ for (const frame of [...postcss.parse(keyframes).nodes]) {
54
+ frame.raws.before = `\n${indent}${indent}`;
55
+ frames.append(frame);
56
+ }
57
+ block.append(frames);
58
+ changed = true;
59
+ }
60
+ if (changed)
61
+ fs.writeFileSync(file, root.toString());
62
+ }
63
+ //# sourceMappingURL=animations-store.js.map
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'fs';
3
+ import { createOperationContext } from './context.js';
4
+ import { OperationError } from './define.js';
5
+ import { createRegistry } from './registry.js';
6
+ // wr-op: the operations on the project in the current folder, as a command.
7
+ //
8
+ // wr-op list [--all] the operations as JSON (--all: the editor's own too)
9
+ // wr-op run <name> [--input '{}'] run one; the input is JSON, from --input or from the standard input
10
+ //
11
+ // The answer is always one JSON document: { ok: true, data } or { ok: false, code, error, status }; the exit code is 1 when it is not ok.
12
+ // It is what the container's tools run (see webround-dev-container/image/tools): the editor's page and an agent do the same things
13
+ // through the same code.
14
+ const [command, ...rest] = process.argv.slice(2);
15
+ const registry = createRegistry();
16
+ const print = (value) => process.stdout.write(`${JSON.stringify(value)}\n`);
17
+ function readInput() {
18
+ const at = rest.indexOf('--input');
19
+ const raw = at !== -1 ? rest[at + 1] ?? '' : process.stdin.isTTY ? '' : fs.readFileSync(0, 'utf-8');
20
+ if (!raw.trim())
21
+ return {};
22
+ const parsed = JSON.parse(raw);
23
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
24
+ throw new OperationError(400, 'invalid-input', 'the input must be a JSON object');
25
+ return parsed;
26
+ }
27
+ async function main() {
28
+ if (command === 'list') {
29
+ print({ operations: registry.list({ all: rest.includes('--all') }) });
30
+ return 0;
31
+ }
32
+ if (command === 'run' && rest[0]) {
33
+ try {
34
+ const data = await registry.run(rest[0], readInput(), createOperationContext(process.cwd()));
35
+ print({ ok: true, data });
36
+ return 0;
37
+ }
38
+ catch (err) {
39
+ if (err instanceof OperationError)
40
+ print({ ok: false, status: err.status, code: err.code, error: err.message });
41
+ else if (err instanceof SyntaxError)
42
+ print({ ok: false, status: 400, code: 'invalid-input', error: `the input is not JSON: ${err.message}` });
43
+ else
44
+ print({ ok: false, status: 500, code: 'internal', error: err instanceof Error ? err.message : String(err) });
45
+ return 1;
46
+ }
47
+ }
48
+ process.stderr.write('usage: wr-op list [--all] | wr-op run <name> [--input <json>]\n');
49
+ return 2;
50
+ }
51
+ process.exitCode = await main();
52
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,17 @@
1
+ import type { WrComment } from '../messages';
2
+ import { type OperationContext } from './define.js';
3
+ export declare const COMMENTS_FILE: string;
4
+ export declare const COMMENT_TEXT_MAX = 2000;
5
+ export declare const COMMENTS_MAX = 1000;
6
+ export type CommentStatus = WrComment['status'];
7
+ export type { WrComment };
8
+ /** Every comment of the project. A project with none (no file) has an empty list; a file that cannot be read is an error, never an empty list: it would be overwritten. */
9
+ export declare function readComments(ctx: OperationContext): WrComment[];
10
+ export declare function writeComments(ctx: OperationContext, comments: WrComment[]): void;
11
+ /** The comments of elements that are gone (the element and everything it contained): they go with them. Answers how many. */
12
+ export declare function dropElementComments(ctx: OperationContext, scope: string, elementIds: readonly string[]): number;
13
+ /** Everything said about a page that is deleted (its elements and the page itself). */
14
+ export declare const dropScopeComments: (ctx: OperationContext, scope: string) => number;
15
+ /** A page that is renamed keeps its comments. */
16
+ export declare function renameScopeComments(ctx: OperationContext, from: string, to: string): void;
17
+ //# sourceMappingURL=comments-store.d.ts.map
@@ -0,0 +1,62 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { OperationError } from './define.js';
4
+ // The comments people (and an agent) leave on the elements of a site. They are NOT the site's: they live in `.wr/comments.json`, at the
5
+ // root of the project, where nothing the site imports can reach them, so they are never in the published build. ONE file, ONE reader and
6
+ // writer (this module): the operations, and what happens to a comment when its element or its page goes, all come through here.
7
+ export const COMMENTS_FILE = path.join('.wr', 'comments.json');
8
+ export const COMMENT_TEXT_MAX = 2000;
9
+ export const COMMENTS_MAX = 1000;
10
+ const fileOf = (ctx) => path.join(ctx.root, COMMENTS_FILE);
11
+ /** Every comment of the project. A project with none (no file) has an empty list; a file that cannot be read is an error, never an empty list: it would be overwritten. */
12
+ export function readComments(ctx) {
13
+ const file = fileOf(ctx);
14
+ if (!fs.existsSync(file))
15
+ return [];
16
+ try {
17
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
18
+ if (!Array.isArray(parsed.comments))
19
+ throw new Error('no list of comments');
20
+ return parsed.comments;
21
+ }
22
+ catch (err) {
23
+ throw new OperationError(500, 'comments-unreadable', `${COMMENTS_FILE} cannot be read (${err instanceof Error ? err.message : 'invalid'}): fix or remove it`);
24
+ }
25
+ }
26
+ export function writeComments(ctx, comments) {
27
+ const file = fileOf(ctx);
28
+ if (comments.length === 0) {
29
+ // Nothing to keep: the file goes, and so does the folder when it was only for it
30
+ fs.rmSync(file, { force: true });
31
+ try {
32
+ fs.rmdirSync(path.dirname(file));
33
+ }
34
+ catch { /* it holds something else */ }
35
+ return;
36
+ }
37
+ fs.mkdirSync(path.dirname(file), { recursive: true });
38
+ fs.writeFileSync(file, JSON.stringify({ comments }, null, 2));
39
+ }
40
+ function rewrite(ctx, change) {
41
+ const all = readComments(ctx);
42
+ const next = change(all);
43
+ if (next.length !== all.length || next.some((c, i) => c !== all[i]))
44
+ writeComments(ctx, next);
45
+ return all.length - next.length;
46
+ }
47
+ /** The comments of elements that are gone (the element and everything it contained): they go with them. Answers how many. */
48
+ export function dropElementComments(ctx, scope, elementIds) {
49
+ if (elementIds.length === 0)
50
+ return 0;
51
+ const gone = new Set(elementIds);
52
+ return rewrite(ctx, all => all.filter(c => !(c.scope === scope && c.elementId !== undefined && gone.has(c.elementId))));
53
+ }
54
+ /** Everything said about a page that is deleted (its elements and the page itself). */
55
+ export const dropScopeComments = (ctx, scope) => rewrite(ctx, all => all.filter(c => c.scope !== scope));
56
+ /** A page that is renamed keeps its comments. */
57
+ export function renameScopeComments(ctx, from, to) {
58
+ if (from === to)
59
+ return;
60
+ rewrite(ctx, all => all.map(c => (c.scope === from ? { ...c, scope: to } : c)));
61
+ }
62
+ //# sourceMappingURL=comments-store.js.map
@@ -0,0 +1,12 @@
1
+ import { Project } from 'ts-morph';
2
+ import type { OperationContext } from './define.js';
3
+ /** A path relative to `src/` made absolute, or null when it leaves `src/` (traversal, an absolute path, a null byte). */
4
+ export declare function resolveInside(srcRoot: string, file: unknown): string | null;
5
+ export declare const isSafePageName: (v: unknown) => v is string;
6
+ export declare const isSafeComponentName: (v: unknown) => v is string;
7
+ export interface ContextOptions {
8
+ /** Another project to work on (tests use one in memory); by default the one of the project's tsconfig. */
9
+ project?: () => Project;
10
+ }
11
+ export declare function createOperationContext(root?: string, options?: ContextOptions): OperationContext;
12
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1,35 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { Project } from 'ts-morph';
4
+ /** A path relative to `src/` made absolute, or null when it leaves `src/` (traversal, an absolute path, a null byte). */
5
+ export function resolveInside(srcRoot, file) {
6
+ if (typeof file !== 'string' || file.includes('\0'))
7
+ return null;
8
+ const resolved = path.resolve(srcRoot, file);
9
+ return resolved.startsWith(srcRoot + path.sep) ? resolved : null;
10
+ }
11
+ // Names that end up in paths and in generated code: never trust arbitrary strings.
12
+ // A page name is a route path (`about`, `blog/post`, `shop/[slug]`), and it ends up in file paths (`config/pages/<name>.json`): every segment
13
+ // is a plain word or a `[parameter]`, so nothing can climb out of the folder (no `..`, no empty segment, no leading slash or underscore).
14
+ const PAGE_SEGMENT = /^(?:[A-Za-z0-9][A-Za-z0-9_-]*|\[[A-Za-z][A-Za-z0-9_]*\])$/;
15
+ export const isSafePageName = (v) => typeof v === 'string' && /^[A-Za-z0-9[]/.test(v) && v.split('/').every(segment => PAGE_SEGMENT.test(segment));
16
+ export const isSafeComponentName = (v) => typeof v === 'string' && /^[A-Za-z_][A-Za-z0-9_]*$/.test(v);
17
+ /** The project's own tsconfig when it has one; a project without it (a bare folder) still reads and writes JSX. */
18
+ function defaultProject(root) {
19
+ const tsConfigFilePath = path.resolve(root, 'tsconfig.json');
20
+ return fs.existsSync(tsConfigFilePath)
21
+ ? new Project({ tsConfigFilePath, skipAddingFilesFromTsConfig: true })
22
+ : new Project({ skipAddingFilesFromTsConfig: true, compilerOptions: { jsx: 4 } });
23
+ }
24
+ export function createOperationContext(root = process.cwd(), options = {}) {
25
+ const srcRoot = path.resolve(root, 'src');
26
+ let project;
27
+ return {
28
+ root,
29
+ srcRoot,
30
+ publicRoot: path.resolve(root, 'public'),
31
+ resolveInSrc: file => resolveInside(srcRoot, file),
32
+ project: () => (project ??= options.project?.() ?? defaultProject(root)),
33
+ };
34
+ }
35
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1,81 @@
1
+ import type { Project } from 'ts-morph';
2
+ /** How a parameter is described: plain data, the same shape the container's tools use (image/tools/registry.mjs), so one can feed the other. */
3
+ export interface ParamSpec {
4
+ name: string;
5
+ /** `any`: whatever JSON value (the operation checks it against what it is for). */
6
+ type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'any';
7
+ help: string;
8
+ required?: boolean;
9
+ default?: unknown;
10
+ choices?: string[];
11
+ /** The first bare word on a command line. */
12
+ positional?: boolean;
13
+ }
14
+ /** Why an operation did not do what was asked: a status like HTTP's, a code a caller can act on, and the words. */
15
+ export declare class OperationError extends Error {
16
+ readonly status: number;
17
+ readonly code: string;
18
+ constructor(status: number, code: string, message: string);
19
+ }
20
+ export interface OperationContext {
21
+ /** The project's root (where package.json is). */
22
+ root: string;
23
+ /** `src/` of the project: the only place operations read and write. */
24
+ srcRoot: string;
25
+ /** `public/` of the project: what is served as it is (robots.txt, the sitemaps). Only the SEO files are written there. */
26
+ publicRoot: string;
27
+ /** A path relative to `src/`, made absolute; null when it would leave `src/` (a traversal, an absolute path, a null byte). */
28
+ resolveInSrc(file: unknown): string | null;
29
+ /** The ts-morph project the code operations work on (created the first time it is asked for). */
30
+ project(): Project;
31
+ }
32
+ export type OperationInput = Record<string, unknown>;
33
+ export interface Operation {
34
+ name: string;
35
+ summary: string;
36
+ /** The draft permission it needs from outside: "write" is site:update (the default), "view" is site:read. */
37
+ access: 'view' | 'write';
38
+ /**
39
+ * Who may use it: "all" (the editor and an agent) or "editor" (only the page's own client: the raw access it needs for its history,
40
+ * which an agent has a better, meaningful operation for).
41
+ */
42
+ expose: 'all' | 'editor';
43
+ params: ParamSpec[];
44
+ /** It changes nothing in the project: a client may let it run without asking. */
45
+ readOnly: boolean;
46
+ /** It takes something away (an element, a page, a theme item): a client may ask first. */
47
+ destructive: boolean;
48
+ run(input: OperationInput, ctx: OperationContext): unknown | Promise<unknown>;
49
+ }
50
+ export declare function defineOperation(op: Pick<Operation, 'name' | 'summary' | 'run'> & Partial<Pick<Operation, 'access' | 'expose' | 'params' | 'readOnly' | 'destructive'>>): Operation;
51
+ export declare const invalid: (message: string, code?: string) => OperationError;
52
+ export declare function text(input: OperationInput, key: string, options?: {
53
+ required?: boolean;
54
+ }): string | undefined;
55
+ export declare const requiredText: (input: OperationInput, key: string) => string;
56
+ export declare function flag(input: OperationInput, key: string): boolean | undefined;
57
+ export declare function list(input: OperationInput, key: string, options?: {
58
+ required?: boolean;
59
+ }): string[] | undefined;
60
+ export declare function object(input: OperationInput, key: string, options?: {
61
+ required?: boolean;
62
+ }): Record<string, unknown> | undefined;
63
+ export interface OperationDescription {
64
+ name: string;
65
+ summary: string;
66
+ access: Operation['access'];
67
+ expose: Operation['expose'];
68
+ params: ParamSpec[];
69
+ readOnly: boolean;
70
+ destructive: boolean;
71
+ }
72
+ export interface OperationRegistry {
73
+ /** What there is, as plain data. `all` includes the editor-only ones. */
74
+ list(options?: {
75
+ all?: boolean;
76
+ }): OperationDescription[];
77
+ get(name: string): Operation | undefined;
78
+ run(name: string, input: OperationInput, ctx: OperationContext): Promise<unknown>;
79
+ }
80
+ export declare function createOperationRegistry(operations: readonly Operation[]): OperationRegistry;
81
+ //# sourceMappingURL=define.d.ts.map
@@ -0,0 +1,86 @@
1
+ /** Why an operation did not do what was asked: a status like HTTP's, a code a caller can act on, and the words. */
2
+ export class OperationError extends Error {
3
+ status;
4
+ code;
5
+ constructor(status, code, message) {
6
+ super(message);
7
+ this.status = status;
8
+ this.code = code;
9
+ }
10
+ }
11
+ export function defineOperation(op) {
12
+ if (!/^[a-z][a-z0-9-]*$/.test(op.name))
13
+ throw new Error(`operation name must be lowercase words: ${op.name}`);
14
+ if (op.readOnly && op.destructive)
15
+ throw new Error(`operation ${op.name}: it cannot be both read-only and destructive`);
16
+ return { access: 'write', expose: 'all', params: [], readOnly: false, destructive: false, ...op };
17
+ }
18
+ // ── Reading what a caller gave, with the same words for every operation ──────
19
+ export const invalid = (message, code = 'invalid-input') => new OperationError(400, code, message);
20
+ export function text(input, key, options = {}) {
21
+ const value = input[key];
22
+ if (value === undefined || value === null) {
23
+ if (options.required)
24
+ throw invalid(`${key} required`);
25
+ return undefined;
26
+ }
27
+ if (typeof value !== 'string')
28
+ throw invalid(`${key} must be text`);
29
+ if (options.required && !value)
30
+ throw invalid(`${key} required`);
31
+ return value;
32
+ }
33
+ export const requiredText = (input, key) => text(input, key, { required: true });
34
+ export function flag(input, key) {
35
+ const value = input[key];
36
+ if (value === undefined || value === null)
37
+ return undefined;
38
+ if (typeof value !== 'boolean')
39
+ throw invalid(`${key} must be true or false`);
40
+ return value;
41
+ }
42
+ export function list(input, key, options = {}) {
43
+ const value = input[key];
44
+ if (value === undefined || value === null) {
45
+ if (options.required)
46
+ throw invalid(`${key} required`);
47
+ return undefined;
48
+ }
49
+ if (!Array.isArray(value) || value.some(item => typeof item !== 'string'))
50
+ throw invalid(`${key} must be a list of text`);
51
+ return value;
52
+ }
53
+ export function object(input, key, options = {}) {
54
+ const value = input[key];
55
+ if (value === undefined || value === null) {
56
+ if (options.required)
57
+ throw invalid(`${key} required`);
58
+ return undefined;
59
+ }
60
+ if (typeof value !== 'object' || Array.isArray(value))
61
+ throw invalid(`${key} must be an object`);
62
+ return value;
63
+ }
64
+ export function createOperationRegistry(operations) {
65
+ const byName = new Map();
66
+ for (const op of operations) {
67
+ if (byName.has(op.name))
68
+ throw new Error(`two operations are called ${op.name}`);
69
+ byName.set(op.name, op);
70
+ }
71
+ return {
72
+ list: ({ all = false } = {}) => [...byName.values()]
73
+ .filter(op => all || op.expose === 'all')
74
+ .map(({ name, summary, access, expose, params, readOnly, destructive }) => ({ name, summary, access, expose, params, readOnly, destructive })),
75
+ get: name => byName.get(name),
76
+ async run(name, input, ctx) {
77
+ const op = byName.get(name);
78
+ if (!op)
79
+ throw new OperationError(404, 'unknown-operation', `no operation called "${name}"`);
80
+ if (typeof input !== 'object' || input === null || Array.isArray(input))
81
+ throw invalid('the input must be an object');
82
+ return op.run(input, ctx);
83
+ },
84
+ };
85
+ }
86
+ //# sourceMappingURL=define.js.map
@@ -0,0 +1,6 @@
1
+ export * from './define.js';
2
+ export { createOperationContext, resolveInside, isSafePageName, isSafeComponentName } from './context.js';
3
+ export { OPERATIONS, createRegistry } from './registry.js';
4
+ export { wrPageScaffold } from './ops/pages.js';
5
+ export { writeSeoFiles } from './ops/seo.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,6 @@
1
+ export * from './define.js';
2
+ export { createOperationContext, resolveInside, isSafePageName, isSafeComponentName } from './context.js';
3
+ export { OPERATIONS, createRegistry } from './registry.js';
4
+ export { wrPageScaffold } from './ops/pages.js';
5
+ export { writeSeoFiles } from './ops/seo.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,5 @@
1
+ export declare const projectCheck: import("../define.js").Operation;
2
+ export declare const issueMute: import("../define.js").Operation;
3
+ export declare const jsonPrune: import("../define.js").Operation;
4
+ export declare const CHECK_OPERATIONS: import("../define.js").Operation[];
5
+ //# sourceMappingURL=check.d.ts.map
@@ -0,0 +1,53 @@
1
+ import { wrPageConfigPath } from '@webround/core/routing';
2
+ import { CHECKS_FILE, checkProject, mutedKeys } from '../../check/project.js';
3
+ import { findOrphanKeys } from '../../check/orphans.js';
4
+ import { defineOperation, flag, invalid, OperationError, requiredText, text } from '../define.js';
5
+ import { patchJsonFile } from './project.js';
6
+ export const projectCheck = defineOperation({
7
+ name: 'project-check',
8
+ readOnly: true,
9
+ // An agent already has `check` (the same function, run as a command in the container): this is how the editor's page asks
10
+ expose: 'editor',
11
+ summary: 'Everything wr-check finds in the project, in every language of the site: problems of the pages (an image without alt text, a missing title, a broken link…) and of the site (no favicon…), each with where to fix it and the languages it happens in.',
12
+ run: (_input, ctx) => ({ issues: checkProject(ctx.root), muted: mutedKeys(ctx.root) }),
13
+ });
14
+ export const issueMute = defineOperation({
15
+ name: 'issue-mute',
16
+ expose: 'editor',
17
+ summary: 'Silences a problem the person chose not to deal with (or brings it back): it stays in the project, counted apart, and wr-check no longer fails because of it. The key is the one the problem has in project-check.',
18
+ params: [
19
+ { name: 'key', type: 'string', positional: true, required: true, help: 'the problem\'s key' },
20
+ { name: 'muted', type: 'boolean', default: true, help: 'false brings the problem back' },
21
+ ],
22
+ run: (input, ctx) => {
23
+ const key = requiredText(input, 'key');
24
+ if (key.length > 300 || !key.includes('|'))
25
+ throw invalid('that is not a problem key', 'invalid-key');
26
+ const muted = flag(input, 'muted') !== false;
27
+ const current = new Set(mutedKeys(ctx.root));
28
+ if (muted)
29
+ current.add(key);
30
+ else
31
+ current.delete(key);
32
+ patchJsonFile(ctx, CHECKS_FILE, { muted: current.size > 0 ? [...current].sort() : null });
33
+ return { muted: [...current].sort() };
34
+ },
35
+ });
36
+ export const jsonPrune = defineOperation({
37
+ name: 'json-prune',
38
+ destructive: true,
39
+ summary: 'Removes from the JSON the values of elements that are no longer in the code (their keys stay behind when an element is deleted or renamed in the code, and come back with the old text if the id is used again). Refuses when some element computes its id: a key that looks unused might be its.',
40
+ params: [{ name: 'page', type: 'string', positional: true, help: 'only this page (or a shared component: _shared/header, _shared/footer, _shared/<name>); every page by default' }],
41
+ run: (input, ctx) => {
42
+ const only = text(input, 'page');
43
+ const { orphans, dynamicIds } = findOrphanKeys(ctx);
44
+ if (dynamicIds)
45
+ throw new OperationError(409, 'dynamic-ids', 'some elements compute their ids in the code, so no key can be said to be unused: remove them by hand if you are sure');
46
+ const removed = orphans.filter(({ scope }) => only === undefined || scope === only);
47
+ for (const { scope, keys } of removed)
48
+ patchJsonFile(ctx, wrPageConfigPath(scope), Object.fromEntries(keys.map(key => [key, null])));
49
+ return { removed };
50
+ },
51
+ });
52
+ export const CHECK_OPERATIONS = [projectCheck, issueMute, jsonPrune];
53
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1,6 @@
1
+ export declare const commentAdd: import("../define.js").Operation;
2
+ export declare const commentList: import("../define.js").Operation;
3
+ export declare const commentResolve: import("../define.js").Operation;
4
+ export declare const commentDelete: import("../define.js").Operation;
5
+ export declare const COMMENT_OPERATIONS: import("../define.js").Operation[];
6
+ //# sourceMappingURL=comments.d.ts.map