@lernapps/app-templates 0.0.1 → 0.1.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 (231) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +195 -2
  3. package/dist/appname-Bh23cUuz.mjs +12 -0
  4. package/dist/assessment-By2GGTJC.mjs +1696 -0
  5. package/dist/block-BK_ohsH1.mjs +82 -0
  6. package/dist/blocks-BmRrD7UY.mjs +149 -0
  7. package/dist/blocks.mjs +4 -0
  8. package/dist/check-BkEl-m-F.mjs +254 -0
  9. package/dist/content-cYBPYg_J.mjs +140 -0
  10. package/dist/contract-PONpxJtd.mjs +52 -0
  11. package/dist/e2e-BiMI-pCL.mjs +89 -0
  12. package/dist/engine-BqF2Vffn.mjs +136 -0
  13. package/dist/explainer-e2e.mjs +1297 -0
  14. package/dist/explainer-islands.mjs +197 -0
  15. package/dist/explainer-plugin.mjs +2131 -0
  16. package/dist/explainer.mjs +1246 -0
  17. package/dist/html-mffkHGHh.mjs +89 -0
  18. package/dist/image-CspJVfyQ.mjs +21 -0
  19. package/dist/interactive-e2e.mjs +140 -0
  20. package/dist/interactive-plugin.mjs +242 -0
  21. package/dist/interactive.mjs +75 -0
  22. package/dist/load-C-0hUaSC.mjs +16947 -0
  23. package/dist/needs-BzEahJ6X.mjs +97 -0
  24. package/dist/pages-9RaaomoO.mjs +81 -0
  25. package/dist/questions-e2e.mjs +1 -0
  26. package/dist/questions-engine.mjs +3 -0
  27. package/dist/questions.mjs +6 -0
  28. package/dist/quiz-BKc8JYtC.mjs +70 -0
  29. package/dist/quiz-e2e.mjs +174 -0
  30. package/dist/quiz-plugin.mjs +266 -0
  31. package/dist/quiz.mjs +246 -0
  32. package/dist/render-BJey0bwl.mjs +540 -0
  33. package/dist/render-BL6fV_NI.mjs +507 -0
  34. package/dist/render-DpRO_RKm.mjs +146 -0
  35. package/dist/result-_eQcamoz.mjs +180 -0
  36. package/dist/tasks-DADUYy52.mjs +74 -0
  37. package/dist/validate-COaZso7u.mjs +1072 -0
  38. package/dist/validate-CxtdeQ5S.mjs +146 -0
  39. package/dist/vue-DO1m6q1R.mjs +40 -0
  40. package/dist/vue-code-BkJjW_I_.mjs +320 -0
  41. package/docs/arc42/01-introduction-and-goals.arc42.md +82 -0
  42. package/docs/arc42/02-constraints.arc42.md +228 -0
  43. package/docs/arc42/03-system-context.arc42.md +118 -0
  44. package/docs/arc42/04-solution-strategy.arc42.md +47 -0
  45. package/docs/arc42/05-building-blocks.arc42.md +307 -0
  46. package/docs/arc42/06-runtime-view.arc42.md +98 -0
  47. package/docs/arc42/07-deployment-view.arc42.md +160 -0
  48. package/docs/arc42/08-concepts.arc42.md +123 -0
  49. package/docs/arc42/09-decisions.arc42.md +246 -0
  50. package/docs/arc42/10-quality-requirements.arc42.md +155 -0
  51. package/docs/arc42/11-risks.arc42.md +113 -0
  52. package/docs/arc42/12-glossary.arc42.md +162 -0
  53. package/docs/arc42/architecture-evidence.md +395 -0
  54. package/explainer/.arc42ignore +3 -0
  55. package/explainer/README.md +54 -0
  56. package/explainer/_gitignore +5 -0
  57. package/explainer/docs/arc42/03-explainer-system-context.arc42.md +35 -0
  58. package/explainer/docs/arc42/05-explainer-building-blocks.arc42.md +659 -0
  59. package/explainer/docs/arc42/06-explainer-runtime-view.arc42.md +105 -0
  60. package/explainer/docs/arc42/07-explainer-deployment-view.arc42.md +34 -0
  61. package/explainer/docs/arc42/08-explainer-concepts.arc42.md +17 -0
  62. package/explainer/docs/arc42/09-explainer-decisions.arc42.md +443 -0
  63. package/explainer/docs/arc42/10-explainer-quality-requirements.arc42.md +54 -0
  64. package/explainer/docs/arc42/11-explainer-risks.arc42.md +17 -0
  65. package/explainer/docs/arc42/12-explainer-glossary.arc42.md +185 -0
  66. package/explainer/e2e/explainer.e2e.ts +3 -0
  67. package/explainer/index.html +11 -0
  68. package/explainer/package.json +18 -0
  69. package/explainer/playwright.config.ts +1 -0
  70. package/explainer/src/app.yaml +20 -0
  71. package/explainer/src/components/ProzentFeld.test.ts +30 -0
  72. package/explainer/src/components/ProzentFeld.ts +121 -0
  73. package/explainer/src/components/ProzentFeld.vue +84 -0
  74. package/explainer/src/images/hunderterfeld-klammern.svg +105 -0
  75. package/explainer/src/main.ts +4 -0
  76. package/explainer/src/prozent/chapter.yaml +18 -0
  77. package/explainer/src/prozent/grundwert.yaml +115 -0
  78. package/explainer/src/prozent/prozentsatz.yaml +99 -0
  79. package/explainer/src/prozent/prozentwert.yaml +119 -0
  80. package/explainer/tsconfig.json +1 -0
  81. package/explainer/vite.config.ts +4 -0
  82. package/interactive/.arc42ignore +5 -0
  83. package/interactive/README.md +28 -0
  84. package/interactive/_gitignore +4 -0
  85. package/interactive/docs/arc42/05-interactive-building-blocks.arc42.md +518 -0
  86. package/interactive/docs/arc42/06-interactive-runtime-view.arc42.md +58 -0
  87. package/interactive/docs/arc42/07-interactive-deployment-view.arc42.md +35 -0
  88. package/interactive/docs/arc42/08-interactive-concepts.arc42.md +52 -0
  89. package/interactive/docs/arc42/09-interactive-decisions.arc42.md +130 -0
  90. package/interactive/docs/arc42/10-interactive-quality-requirements.arc42.md +45 -0
  91. package/interactive/docs/arc42/11-interactive-risks.arc42.md +20 -0
  92. package/interactive/docs/arc42/12-interactive-glossary.arc42.md +80 -0
  93. package/interactive/e2e/interactive.e2e.ts +3 -0
  94. package/interactive/index.html +11 -0
  95. package/interactive/package.json +18 -0
  96. package/interactive/playwright.config.ts +1 -0
  97. package/interactive/src/app.yaml +28 -0
  98. package/interactive/src/core/index.ts +101 -0
  99. package/interactive/src/data/lessons.json +4 -0
  100. package/interactive/src/env.d.ts +6 -0
  101. package/interactive/src/main.ts +6 -0
  102. package/interactive/src/messages/de.json +12 -0
  103. package/interactive/src/playthroughs/grundreihe.yaml +33 -0
  104. package/interactive/src/playthroughs/obere-reihe.yaml +14 -0
  105. package/interactive/src/ui/App.vue +85 -0
  106. package/interactive/tsconfig.json +1 -0
  107. package/interactive/vite.config.ts +4 -0
  108. package/package.json +142 -4
  109. package/quiz/README.md +31 -0
  110. package/quiz/_gitignore +5 -0
  111. package/quiz/docs/arc42/05-quiz-building-blocks.arc42.md +283 -0
  112. package/quiz/docs/arc42/06-quiz-runtime-view.arc42.md +21 -0
  113. package/quiz/docs/arc42/07-quiz-deployment-view.arc42.md +33 -0
  114. package/quiz/docs/arc42/09-quiz-decisions.arc42.md +36 -0
  115. package/quiz/docs/arc42/10-quiz-quality-requirements.arc42.md +53 -0
  116. package/quiz/docs/arc42/11-quiz-risks.arc42.md +19 -0
  117. package/quiz/docs/arc42/12-quiz-glossary.arc42.md +27 -0
  118. package/quiz/e2e/quiz.e2e.ts +3 -0
  119. package/quiz/index.html +11 -0
  120. package/quiz/package.json +18 -0
  121. package/quiz/playwright.config.ts +1 -0
  122. package/quiz/src/images/berge-ordnen.jpg +0 -0
  123. package/quiz/src/images/everest-hoehe.jpg +0 -0
  124. package/quiz/src/images/galapagos.jpg +0 -0
  125. package/quiz/src/images/grand-canyon.jpg +0 -0
  126. package/quiz/src/images/totes-meer.jpg +0 -0
  127. package/quiz/src/images/uluru.jpg +0 -0
  128. package/quiz/src/images/wasserfaelle-laender.jpg +0 -0
  129. package/quiz/src/main.ts +4 -0
  130. package/quiz/src/quiz.json +312 -0
  131. package/quiz/tsconfig.json +1 -0
  132. package/quiz/vite.config.ts +4 -0
  133. package/runtime/explainer/assessment-client.ts +478 -0
  134. package/runtime/explainer/assessment-page.ts +446 -0
  135. package/runtime/explainer/assessment.ts +393 -0
  136. package/runtime/explainer/check-page.ts +520 -0
  137. package/runtime/explainer/components/exercise.ts +409 -0
  138. package/runtime/explainer/components/props.ts +230 -0
  139. package/runtime/explainer/components/read.ts +428 -0
  140. package/runtime/explainer/content.ts +277 -0
  141. package/runtime/explainer/e2e.ts +1474 -0
  142. package/runtime/explainer/exercises.ts +324 -0
  143. package/runtime/explainer/explainer.css +1283 -0
  144. package/runtime/explainer/explainer.ts +838 -0
  145. package/runtime/explainer/explainer.v1.schema.json +1165 -0
  146. package/runtime/explainer/expression.ts +407 -0
  147. package/runtime/explainer/fraction.ts +107 -0
  148. package/runtime/explainer/generated/comparator.ts +31 -0
  149. package/runtime/explainer/generated/question.ts +165 -0
  150. package/runtime/explainer/generated/soundness.ts +280 -0
  151. package/runtime/explainer/generated/texts.ts +257 -0
  152. package/runtime/explainer/generated/types.ts +103 -0
  153. package/runtime/explainer/generated/values.ts +136 -0
  154. package/runtime/explainer/islands.ts +281 -0
  155. package/runtime/explainer/load.ts +457 -0
  156. package/runtime/explainer/messages/de.json +200 -0
  157. package/runtime/explainer/messages/en.json +200 -0
  158. package/runtime/explainer/messages.ts +13 -0
  159. package/runtime/explainer/needs.ts +122 -0
  160. package/runtime/explainer/open-task.ts +235 -0
  161. package/runtime/explainer/outline.ts +299 -0
  162. package/runtime/explainer/plugin.ts +488 -0
  163. package/runtime/explainer/position.ts +62 -0
  164. package/runtime/explainer/prompt.ts +75 -0
  165. package/runtime/explainer/render.ts +686 -0
  166. package/runtime/explainer/result.ts +238 -0
  167. package/runtime/explainer/routes.ts +119 -0
  168. package/runtime/explainer/schema.ts +140 -0
  169. package/runtime/explainer/scope.ts +87 -0
  170. package/runtime/explainer/stuck.ts +57 -0
  171. package/runtime/explainer/tasks.ts +109 -0
  172. package/runtime/explainer/tutor.ts +469 -0
  173. package/runtime/explainer/tutorpage.ts +669 -0
  174. package/runtime/explainer/video.ts +123 -0
  175. package/runtime/interactive/contract.ts +121 -0
  176. package/runtime/interactive/e2e.ts +190 -0
  177. package/runtime/interactive/interactive.css +82 -0
  178. package/runtime/interactive/interactive.ts +115 -0
  179. package/runtime/interactive/interactive.v1.schema.json +146 -0
  180. package/runtime/interactive/load.ts +188 -0
  181. package/runtime/interactive/messages/de.json +14 -0
  182. package/runtime/interactive/messages/en.json +14 -0
  183. package/runtime/interactive/messages.ts +11 -0
  184. package/runtime/interactive/playthrough.v1.schema.json +143 -0
  185. package/runtime/interactive/plugin.ts +266 -0
  186. package/runtime/interactive/render.ts +126 -0
  187. package/runtime/quiz/e2e.ts +233 -0
  188. package/runtime/quiz/engine.ts +242 -0
  189. package/runtime/quiz/messages/de.json +23 -0
  190. package/runtime/quiz/messages/en.json +23 -0
  191. package/runtime/quiz/plugin.ts +150 -0
  192. package/runtime/quiz/quiz.css +154 -0
  193. package/runtime/quiz/quiz.ts +56 -0
  194. package/runtime/quiz/quiz.v1.schema.json +72 -0
  195. package/runtime/quiz/render.ts +173 -0
  196. package/runtime/quiz/validate.ts +108 -0
  197. package/runtime/shared/appname.ts +16 -0
  198. package/runtime/shared/blocks/block.ts +162 -0
  199. package/runtime/shared/blocks/blocks.css +190 -0
  200. package/runtime/shared/blocks/blocks.v1.schema.json +432 -0
  201. package/runtime/shared/blocks/code.ts +26 -0
  202. package/runtime/shared/blocks/figures.ts +49 -0
  203. package/runtime/shared/blocks/image.ts +25 -0
  204. package/runtime/shared/blocks/index.ts +13 -0
  205. package/runtime/shared/blocks/messages/de.json +4 -0
  206. package/runtime/shared/blocks/messages/en.json +4 -0
  207. package/runtime/shared/blocks/render.ts +134 -0
  208. package/runtime/shared/blocks/validate.ts +165 -0
  209. package/runtime/shared/content.ts +197 -0
  210. package/runtime/shared/examples.ts +20 -0
  211. package/runtime/shared/html.ts +10 -0
  212. package/runtime/shared/languages.ts +94 -0
  213. package/runtime/shared/page.css +101 -0
  214. package/runtime/shared/pages.ts +114 -0
  215. package/runtime/shared/questions/e2e.ts +115 -0
  216. package/runtime/shared/questions/engine.ts +209 -0
  217. package/runtime/shared/questions/index.ts +39 -0
  218. package/runtime/shared/questions/messages/de.json +26 -0
  219. package/runtime/shared/questions/messages/en.json +26 -0
  220. package/runtime/shared/questions/question.ts +430 -0
  221. package/runtime/shared/questions/question.v1.schema.json +342 -0
  222. package/runtime/shared/questions/questions.css +192 -0
  223. package/runtime/shared/questions/render.ts +247 -0
  224. package/runtime/shared/questions/validate.ts +518 -0
  225. package/runtime/shared/schema.ts +44 -0
  226. package/runtime/shared/text/check.ts +68 -0
  227. package/runtime/shared/text/formula.ts +69 -0
  228. package/runtime/shared/text/markdown.ts +185 -0
  229. package/runtime/shared/text/text.v1.schema.json +34 -0
  230. package/runtime/shared/vue-code.ts +395 -0
  231. package/runtime/shared/vue.ts +38 -0
@@ -0,0 +1,146 @@
1
+ import { i as inLanguage } from "./html-mffkHGHh.mjs";
2
+ import { n as markerOf } from "./block-BK_ohsH1.mjs";
3
+ import { t as checkText } from "./check-BkEl-m-F.mjs";
4
+ // runtime/shared/blocks/validate.ts
5
+ /** The definition of each type of block in the schema of the blocks (blocks.v1.schema.json). */
6
+ const MATERIAL_DEFINITIONS = {
7
+ text: "textBlock",
8
+ formula: "formula",
9
+ figure: "figure",
10
+ "labelled-figure": "labelledFigure",
11
+ table: "table",
12
+ quote: "quote",
13
+ code: "code"
14
+ };
15
+ /** Whether `text` names `marker`: as a word of its own, so that the marker A is not found in "Grundriss". */
16
+ const names = (text, marker) => new RegExp(`(?<![\\p{L}\\p{N}])${marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![\\p{L}\\p{N}])`, "u").test(text);
17
+ /** The problems of `blocks`. */
18
+ function checkMaterial(blocks, checks) {
19
+ const problems = [];
20
+ const text = (at, value, kind) => {
21
+ if (value === void 0) return;
22
+ const { findings, links } = checkText(value, kind, checks.languages, checks.declaredIn);
23
+ for (const finding of findings) problems.push({
24
+ ...finding,
25
+ path: [...at, ...finding.path]
26
+ });
27
+ for (const link of links) {
28
+ const place = [...at, ...link.path];
29
+ if (checks.onLink !== void 0) checks.onLink({
30
+ ...link,
31
+ path: place
32
+ });
33
+ else problems.push({
34
+ path: place,
35
+ lang: link.lang,
36
+ message: `${link.href} is a link to a page of the app; here there are no pages: link an https address`
37
+ });
38
+ }
39
+ };
40
+ blocks.forEach((block, index) => {
41
+ const at = [index];
42
+ switch (block.type) {
43
+ case "text":
44
+ text([...at, "text"], block.text, "markdown");
45
+ break;
46
+ case "formula":
47
+ text([...at, "formula"], block.formula, "latex");
48
+ break;
49
+ case "figure":
50
+ case "labelled-figure":
51
+ if (block.type === "figure" && block.draw === void 0 === (block.image === void 0)) problems.push({
52
+ path: at,
53
+ message: block.draw === void 0 ? "draw or image is missing; a figure is drawn from data (draw) or a bundled image (image)" : "draw and image: a figure is either drawn from data or a bundled image"
54
+ });
55
+ if (block.image !== void 0 && !checks.imageExists(block.image)) problems.push({
56
+ path: [...at, "image"],
57
+ message: `${block.image} is missing: put the file into src/images/`
58
+ });
59
+ text([...at, "alt"], block.alt, "plain");
60
+ text([...at, "caption"], block.caption, "markdown");
61
+ if (block.type === "labelled-figure") {
62
+ const seen = /* @__PURE__ */ new Set();
63
+ block.labels.forEach((label, number) => {
64
+ text([
65
+ ...at,
66
+ "labels",
67
+ number,
68
+ "text"
69
+ ], label.text, "plain");
70
+ const marker = markerOf(label, number);
71
+ if (seen.has(marker)) problems.push({
72
+ path: [
73
+ ...at,
74
+ "labels",
75
+ number,
76
+ "marker"
77
+ ],
78
+ message: `${marker} is used twice; give each label its own marker`
79
+ });
80
+ seen.add(marker);
81
+ });
82
+ if (block.legend === false) for (const lang of checks.languages) {
83
+ const alt = inLanguage(block.alt, lang);
84
+ block.labels.forEach((label, number) => {
85
+ const marker = markerOf(label, number);
86
+ if (!names(alt, marker)) problems.push({
87
+ path: [...at, "alt"],
88
+ lang,
89
+ message: `alt does not name the marker ${marker}; with legend: false, alt names every marker with its part`
90
+ });
91
+ });
92
+ }
93
+ }
94
+ text([...at, "source"], block.source, "credit");
95
+ break;
96
+ case "table": {
97
+ text([...at, "caption"], block.caption, "plain");
98
+ block.header?.forEach((cell, number) => text([
99
+ ...at,
100
+ "header",
101
+ number
102
+ ], cell, "markdown"));
103
+ const columns = block.header?.length ?? block.rows[0]?.length ?? 0;
104
+ block.rows.forEach((row, line) => {
105
+ if (row.length !== columns) problems.push({
106
+ path: [
107
+ ...at,
108
+ "rows",
109
+ line
110
+ ],
111
+ message: `${row.length} cells, but the table has ${columns} columns; give each row a cell per column`
112
+ });
113
+ row.forEach((_cell, number) => text([
114
+ ...at,
115
+ "rows",
116
+ line,
117
+ number
118
+ ], row[number], "markdown"));
119
+ });
120
+ break;
121
+ }
122
+ case "quote":
123
+ text([...at, "text"], block.text, "markdown");
124
+ text([
125
+ ...at,
126
+ "origin",
127
+ "author"
128
+ ], block.origin.author, "name");
129
+ text([
130
+ ...at,
131
+ "origin",
132
+ "work"
133
+ ], block.origin.work, "name");
134
+ if (typeof block.origin.year !== "number") text([
135
+ ...at,
136
+ "origin",
137
+ "year"
138
+ ], block.origin.year, "plain");
139
+ text([...at, "source"], block.source, "credit");
140
+ break;
141
+ case "code": text([...at, "caption"], block.caption, "plain");
142
+ }
143
+ });
144
+ return problems;
145
+ }
146
+ export { checkMaterial as n, MATERIAL_DEFINITIONS as t };
@@ -0,0 +1,40 @@
1
+ import { createRequire } from "node:module";
2
+ import { dirname } from "node:path";
3
+ // runtime/shared/vue.ts
4
+ const require = createRequire(import.meta.url);
5
+ /** vue, and vue/<path>, resolved to the copy this runtime depends on. */
6
+ const vueAlias = () => [{
7
+ find: /^vue(?=$|\/)/,
8
+ replacement: dirname(require.resolve("vue/package.json"))
9
+ }];
10
+ /** The options of @vitejs/plugin-vue: a component's scope id from its file and source alone, the same in the build and
11
+ * in the server that renders it. */
12
+ const VUE_OPTIONS = { features: { componentIdGenerator: "filepath-source" } };
13
+ /** Loads modules of the app in `root` for Node in a Vite server of its own, with the components compiled for it. */
14
+ async function withServer(root, act) {
15
+ const [{ createServer }, { default: vue }] = await Promise.all([import("vite-plus"), import("@vitejs/plugin-vue")]);
16
+ const server = await createServer({
17
+ root,
18
+ configFile: false,
19
+ logLevel: "silent",
20
+ plugins: [vue(VUE_OPTIONS)],
21
+ resolve: { alias: vueAlias() },
22
+ appType: "custom",
23
+ server: {
24
+ middlewareMode: true,
25
+ hmr: false,
26
+ ws: false,
27
+ watch: null
28
+ },
29
+ optimizeDeps: {
30
+ noDiscovery: true,
31
+ include: []
32
+ }
33
+ });
34
+ try {
35
+ return await act((id) => server.ssrLoadModule(id));
36
+ } finally {
37
+ await server.close();
38
+ }
39
+ }
40
+ export { vueAlias as n, withServer as r, VUE_OPTIONS as t };
@@ -0,0 +1,320 @@
1
+ import { createRequire } from "node:module";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { dirname, join, posix } from "node:path";
4
+ // runtime/shared/vue-code.ts
5
+ const require = createRequire(import.meta.url);
6
+ let compiler;
7
+ /** Vue's SFC compiler, loaded on first use: an app without components never loads it. */
8
+ const sfc = () => compiler ??= require("vue/compiler-sfc");
9
+ const TOOLING = [
10
+ "@lernapps/tooling/storage",
11
+ "@lernapps/tooling/i18n",
12
+ "@lernapps/tooling/a11y"
13
+ ];
14
+ /** The packages a component may import, for a message. */
15
+ const PACKAGES = `vue, @lernapps/app-templates, ${TOOLING.join(", ")}`;
16
+ const NETWORK = /* @__PURE__ */ new Set([
17
+ "fetch",
18
+ "XMLHttpRequest",
19
+ "WebSocket",
20
+ "EventSource"
21
+ ]);
22
+ const BROWSER = /* @__PURE__ */ new Set([
23
+ "window",
24
+ "document",
25
+ "navigator",
26
+ "location",
27
+ "history",
28
+ "localStorage",
29
+ "sessionStorage",
30
+ "indexedDB",
31
+ "matchMedia",
32
+ "requestAnimationFrame",
33
+ "cancelAnimationFrame",
34
+ "getComputedStyle",
35
+ "alert",
36
+ "confirm",
37
+ "prompt",
38
+ "screen",
39
+ "customElements",
40
+ "IntersectionObserver",
41
+ "ResizeObserver",
42
+ "MutationObserver"
43
+ ]);
44
+ /** The hooks that never run while the build renders the component: inside them, browser globals are allowed. */
45
+ const BROWSER_HOOKS = /* @__PURE__ */ new Set([
46
+ "onMounted",
47
+ "onBeforeUnmount",
48
+ "onUnmounted"
49
+ ]);
50
+ const HTML = /* @__PURE__ */ new Set([
51
+ "innerHTML",
52
+ "outerHTML",
53
+ "insertAdjacentHTML"
54
+ ]);
55
+ const GLOBAL_OBJECTS = /* @__PURE__ */ new Set([
56
+ "window",
57
+ "self",
58
+ "globalThis"
59
+ ]);
60
+ /** A line of a problem in `file`, at `place` when it is known. */
61
+ const line = (file, place, message) => `${file}${place === void 0 ? "" : `:${place.line}${place.column === void 0 ? "" : `:${place.column}`}`}: ${message}`;
62
+ const isNode = (value) => typeof value === "object" && value !== null && typeof value.type === "string";
63
+ /** Every node of `root`, with its parents from the root down. */
64
+ function* nodes(root, parents = []) {
65
+ yield [root, parents];
66
+ for (const [key, value] of Object.entries(root)) {
67
+ if (key === "loc" || key === "leadingComments" || key === "trailingComments" || key === "innerComments") continue;
68
+ for (const child of Array.isArray(value) ? value : [value]) if (isNode(child)) yield* nodes(child, [...parents, root]);
69
+ }
70
+ }
71
+ const parse = (code) => sfc().babelParse(code, {
72
+ sourceType: "module",
73
+ plugins: ["typescript"]
74
+ }).program;
75
+ /** The name of a member's property: x.innerHTML, x["innerHTML"]. */
76
+ function propertyName(node) {
77
+ if (node.type !== "MemberExpression" && node.type !== "OptionalMemberExpression") return void 0;
78
+ if (!node.computed && node.property.type === "Identifier") return node.property.name;
79
+ return node.property.type === "StringLiteral" ? node.property.value : void 0;
80
+ }
81
+ /** The import specifiers of `program`, with their nodes: static, re-exports and dynamic imports. */
82
+ function imports(program) {
83
+ const found = [];
84
+ for (const [node] of nodes(program)) if (node.type === "ImportDeclaration" && node.importKind !== "type" || (node.type === "ExportNamedDeclaration" || node.type === "ExportAllDeclaration") && node.source !== null && node.source !== void 0 && node.exportKind !== "type") {
85
+ if (node.source) found.push({
86
+ source: node.source.value,
87
+ node
88
+ });
89
+ } else if (node.type === "CallExpression" && node.callee.type === "Import") {
90
+ const [argument] = node.arguments;
91
+ found.push({
92
+ source: argument?.type === "StringLiteral" ? argument.value : "(an expression)",
93
+ node
94
+ });
95
+ } else if (node.type === "ImportExpression") found.push({
96
+ source: node.source.type === "StringLiteral" ? node.source.value : "(an expression)",
97
+ node
98
+ });
99
+ return found;
100
+ }
101
+ /** The package a bare import specifier names: "yaml" for "yaml/util", "@scope/name" for "@scope/name/sub". */
102
+ const packageOf = (source) => source.split("/").slice(0, source.startsWith("@") ? 2 : 1).join("/");
103
+ /**
104
+ * What the code of `program` may not do, and its imports of the app's own modules (`isLocal`); `own` names these for
105
+ * a message, `packages` are the libraries it may import besides.
106
+ */
107
+ function findings(program, isLocal, own, packages = []) {
108
+ const found = [];
109
+ const locals = [];
110
+ for (const { source, node } of imports(program)) if (/^[a-z][a-z0-9+.-]*:|^\/\//i.test(source)) found.push({
111
+ node,
112
+ message: `an import from ${source}; a component loads nothing from the network, so bundle what it needs (vue-component-no-network)`
113
+ });
114
+ else if (isLocal(source)) locals.push({
115
+ source,
116
+ node
117
+ });
118
+ else if (source !== "vue" && source !== "@lernapps/app-templates" && !source.startsWith("@lernapps/app-templates/") && !TOOLING.includes(source) && !packages.includes(packageOf(source))) found.push({
119
+ node,
120
+ message: `an import from ${source}; a component imports only from ${PACKAGES} and ${own} (vue-component-imports)`
121
+ });
122
+ const { walkIdentifiers } = sfc();
123
+ if (program.body.length === 0) return {
124
+ found,
125
+ locals
126
+ };
127
+ walkIdentifiers(program, (identifier, parent, parents, isReference, isLocal) => {
128
+ if (!isReference || isLocal) return;
129
+ const { name } = identifier;
130
+ if (NETWORK.has(name)) found.push({
131
+ node: identifier,
132
+ message: `${name}: a component makes no network request; give it what it needs through its props (vue-component-no-network)`
133
+ });
134
+ else if (name === "eval" || name === "Function" && (parent?.type === "NewExpression" || parent?.type === "CallExpression") && parent.callee === identifier) found.push({
135
+ node: identifier,
136
+ message: `${name === "eval" ? "eval" : "Function"}: a component runs no code from strings (vue-component-no-html-injection)`
137
+ });
138
+ else if (BROWSER.has(name)) {
139
+ const stack = [...parents, identifier];
140
+ if (!stack.some((node, index) => node.type === "CallExpression" && node.callee.type === "Identifier" && BROWSER_HOOKS.has(node.callee.name) && node.arguments.some((argument) => argument === stack[index + 1]))) found.push({
141
+ node: identifier,
142
+ message: `${name} outside onMounted; the build renders the component where there is no ${name}, so use it only inside onMounted (vue-component-browser-globals)`
143
+ });
144
+ }
145
+ });
146
+ for (const [node] of nodes(program)) {
147
+ const property = propertyName(node);
148
+ if (property !== void 0 && HTML.has(property)) found.push({
149
+ node,
150
+ message: `${property}: a component writes no HTML from strings; let the template render it (vue-component-no-html-injection)`
151
+ });
152
+ else if (property === "sendBeacon") found.push({
153
+ node,
154
+ message: `sendBeacon: a component makes no network request (vue-component-no-network)`
155
+ });
156
+ else if (property !== void 0 && NETWORK.has(property) && (node.type === "MemberExpression" || node.type === "OptionalMemberExpression") && node.object.type === "Identifier" && GLOBAL_OBJECTS.has(node.object.name)) found.push({
157
+ node,
158
+ message: `${property}: a component makes no network request; give it what it needs through its props (vue-component-no-network)`
159
+ });
160
+ else if (node.type === "ObjectProperty" && !node.computed && (node.key.type === "Identifier" && node.key.name === "innerHTML" || node.key.type === "StringLiteral" && node.key.value === "innerHTML")) found.push({
161
+ node,
162
+ message: `v-html: a component writes no HTML from strings; let the template render it (vue-component-no-html-injection)`
163
+ });
164
+ }
165
+ return {
166
+ found,
167
+ locals
168
+ };
169
+ }
170
+ /** The problem lines of `found`, each once, at its place; one without a place only when no other has the message. */
171
+ function placed(file, found, place) {
172
+ const lines = [];
173
+ const withPlace = /* @__PURE__ */ new Set();
174
+ const without = [];
175
+ for (const finding of found) {
176
+ const at = place(finding.node);
177
+ if (at === void 0) without.push(finding.message);
178
+ else {
179
+ withPlace.add(finding.message);
180
+ const text = line(file, at, finding.message);
181
+ if (!lines.includes(text)) lines.push(text);
182
+ }
183
+ }
184
+ for (const message of new Set(without)) if (!withPlace.has(message)) lines.push(line(file, void 0, message));
185
+ return lines;
186
+ }
187
+ /** The libraries in the dependencies of the app's package.json, by their package names. */
188
+ function dependencies(root) {
189
+ const file = join(root, "package.json");
190
+ if (!existsSync(file)) return [];
191
+ const manifest = JSON.parse(readFileSync(file, "utf8"));
192
+ return Object.keys(manifest.dependencies ?? {});
193
+ }
194
+ /**
195
+ * The scope of an app whose components may import any of its files in src/ and the libraries in the dependencies of
196
+ * its package.json (the interactive archetype).
197
+ */
198
+ const sourceScope = (root) => ({
199
+ own: "the app's own files in src/ or a library in the dependencies of package.json",
200
+ packages: dependencies(root),
201
+ resolve(file, source) {
202
+ if (!source.startsWith("./") && !source.startsWith("../")) return void 0;
203
+ const target = posix.normalize(posix.join(dirname(file), source));
204
+ if (!target.startsWith("src/")) return { problem: `${source} is outside src/; a component imports only the app's own files in src/` };
205
+ return existsSync(join(root, target)) ? { file: target } : { problem: `${source} does not exist; import a file of src/, with its extension` };
206
+ }
207
+ });
208
+ /** A single-file component as the compiler reads it; undefined, with its problems, when it has none to check. */
209
+ function readSfc(root, file, problems) {
210
+ const source = readFileSync(join(root, file), "utf8");
211
+ const { descriptor, errors } = sfc().parse(source, {
212
+ filename: join(root, file),
213
+ sourceMap: true
214
+ });
215
+ for (const error of errors) {
216
+ const loc = "loc" in error ? error.loc : void 0;
217
+ problems.push(line(file, loc ? {
218
+ line: loc.start.line,
219
+ column: loc.start.column
220
+ } : void 0, error.message));
221
+ }
222
+ if (errors.length > 0) return void 0;
223
+ const setup = descriptor.scriptSetup;
224
+ if (setup === null || setup.lang !== "ts") {
225
+ problems.push(line(file, void 0, `a component has <script setup lang="ts">`));
226
+ return;
227
+ }
228
+ return {
229
+ descriptor,
230
+ setup
231
+ };
232
+ }
233
+ /**
234
+ * The checks of components' code, each problem into `problems`: `compile` compiles a component's <script setup> with
235
+ * its template and checks the output, `module` checks a TypeScript module; each follows the imports of the app's own
236
+ * TypeScript modules (`scope`) and checks each module once.
237
+ */
238
+ function codeChecker(root, scope, problems) {
239
+ const { SourceMapConsumer } = require("source-map-js");
240
+ const checked = /* @__PURE__ */ new Set();
241
+ const check = (file, program) => {
242
+ const { found, locals } = findings(program, (source) => scope.resolve(file, source) !== void 0, scope.own, scope.packages);
243
+ for (const { source, node } of locals) {
244
+ const target = scope.resolve(file, source);
245
+ if (target === void 0) continue;
246
+ if ("problem" in target) found.push({
247
+ node,
248
+ message: target.problem
249
+ });
250
+ else if (target.file.endsWith(".ts")) module(target.file);
251
+ }
252
+ return found;
253
+ };
254
+ const module = (file) => {
255
+ if (checked.has(file)) return;
256
+ checked.add(file);
257
+ problems.push(...placed(file, check(file, parse(readFileSync(join(root, file), "utf8"))), (node) => node.loc ? {
258
+ line: node.loc.start.line,
259
+ column: node.loc.start.column + 1
260
+ } : void 0));
261
+ };
262
+ const compile = (file, descriptor) => {
263
+ let compiled;
264
+ try {
265
+ compiled = sfc().compileScript(descriptor, {
266
+ id: file.replace(/^.*\//, "").replace(/\.vue$/, ""),
267
+ inlineTemplate: true,
268
+ sourceMap: true,
269
+ isProd: true,
270
+ fs: {
271
+ fileExists: existsSync,
272
+ readFile: (name) => readFileSync(name, "utf8")
273
+ }
274
+ });
275
+ } catch (error) {
276
+ const loc = error.loc;
277
+ const message = (error instanceof Error ? error.message : String(error)).split("\n")[0] ?? "";
278
+ problems.push(line(file, loc ? {
279
+ line: loc.start.line,
280
+ column: loc.start.column
281
+ } : void 0, message));
282
+ return;
283
+ }
284
+ const program = parse(compiled.content);
285
+ const consumer = compiled.map ? new SourceMapConsumer(compiled.map) : void 0;
286
+ problems.push(...placed(file, check(file, program), (node) => {
287
+ if (!node.loc || consumer === void 0) return void 0;
288
+ const original = consumer.originalPositionFor({
289
+ line: node.loc.start.line,
290
+ column: node.loc.start.column
291
+ });
292
+ return original.line === null ? void 0 : {
293
+ line: original.line,
294
+ column: original.column + 1
295
+ };
296
+ }));
297
+ return {
298
+ program,
299
+ consumer
300
+ };
301
+ };
302
+ return {
303
+ module,
304
+ compile
305
+ };
306
+ }
307
+ /**
308
+ * The checks of the components' code for the single-file components `files` (from the app's root) and the app's own
309
+ * TypeScript modules they import (`scope`): one line per problem, with the file and its place.
310
+ */
311
+ function codeProblems(root, files, scope) {
312
+ const problems = [];
313
+ const code = codeChecker(root, scope, problems);
314
+ for (const file of files) {
315
+ const read = readSfc(root, file, problems);
316
+ if (read !== void 0) code.compile(file, read.descriptor);
317
+ }
318
+ return problems;
319
+ }
320
+ export { parse as a, sourceScope as c, nodes as i, codeProblems as n, readSfc as o, line as r, sfc as s, codeChecker as t };
@@ -0,0 +1,82 @@
1
+ # Introduction and Goals
2
+
3
+ <!--
4
+ Arc42 chapter 1. Describes the relevant requirements and the driving forces that
5
+ architects and the development team must consider.
6
+
7
+ This chapter has three subsections:
8
+
9
+ 1.1 Requirements Overview
10
+ Short description of the functional requirements and business goals — not a full
11
+ requirements spec. Write a compact summary or refer to an external requirements
12
+ document. A brief prose paragraph and/or a table of the most important use cases.
13
+
14
+ 1.2 Quality Goals
15
+ The top three to five quality goals for the architecture (highest-importance only).
16
+ Write a single sentence or short paragraph, then link to chapter 10.
17
+ Do NOT repeat the quality-goal blocks here — they live in 10-quality-requirements.arc42.md.
18
+
19
+ Example:
20
+
21
+ See [10-quality-requirements.arc42.md](10-quality-requirements.arc42.md) for the
22
+ complete quality catalog with priorities and measurable scenarios.
23
+
24
+ 1.3 Stakeholders
25
+ A table of all stakeholders: their role, contact, and expectations from the
26
+ architecture and its documentation. Think broadly — include teams that must
27
+ maintain the system, operators, auditors, and external partners.
28
+
29
+ See https://docs.arc42.org/section-1/ for further guidance.
30
+
31
+ Example:
32
+
33
+ ## 1.1 Requirements Overview
34
+
35
+ Every app of lernapps.net is a small, frontend-only learning app, made by a creator with their AI assistant from one
36
+ of three archetypes: a quiz about things to know (a question bank), an explainer (pages of content with an exercise on
37
+ every page), or an interactive app in which doing is the content (a typing game, a Kniffel trainer, a proofreading
38
+ app). The archetypes stay separate, but they share their quality goals and so one architecture: this generic
39
+ architecture, which each archetype extends in its template folder (`quiz/docs/arc42/`, `explainer/docs/arc42/`,
40
+ `interactive/docs/arc42/`). Every app has the same frame: static pages rendered at build in every declared language,
41
+ the archetype's code in the browser started by a runtime the app does not change, build checks and end-to-end tests
42
+ that come with the runtime, the tooling's helpers, and an optional tutor, which hands the current task to the
43
+ learner's own AI assistant (no model runs in the browser). The app reaches learners through one entry in the
44
+ lernapps.net app overview, which links to it.
45
+
46
+ | Use case | Who | What the architecture must allow |
47
+ | --- | --- | --- |
48
+ | Find out what the app does | learner, teacher, parent | static pages in every declared language; what each archetype shows without JavaScript is in its extension |
49
+ | Learn and practise | learner | the archetype's code in the browser, its state only on the device |
50
+ | Open a part directly | learner, catalog entry | deep links (a question, a page and task, a mode or level), a seed in the address |
51
+ | Build and extend the app | creator and their AI assistant | what the archetype lets the creator write, under rules the build and `lernapps check` verify |
52
+ | Get help with a task | learner | an optional handoff of the task to the learner's own assistant, after a click |
53
+
54
+ ## 1.2 Quality Goals
55
+
56
+ The top quality goals, in this order:
57
+
58
+ - `qg-privacy`: the app sends nothing anywhere and requests nothing from another server before a click; what it
59
+ remembers stays on the learner's device.
60
+ - `qg-checkable`: the rules an app keeps are verified by machine, at build and in the browser.
61
+ - `qg-accessibility`: the pages meet WCAG 2.1 AA and work at 360 px by default; an app that cannot declares its
62
+ limits in its entry.
63
+ - `qg-understandable`: a reviewer or the creator's agent understands an app's structure from its architecture.
64
+
65
+ The set and the priorities (privacy and checkable high, accessibility and understandable medium) are the owner's
66
+ (2026-10-16). The quiz and the explainer, both text-heavy, add readability without JavaScript as a goal of their own
67
+ in their extensions.
68
+
69
+ See [10-quality-requirements.arc42.md](10-quality-requirements.arc42.md) for the complete
70
+ quality catalog with priorities and measurable scenarios.
71
+
72
+ ## 1.3 Stakeholders
73
+
74
+ | Role | Contact | Expectations |
75
+ | --- | --- | --- |
76
+ | Learner (adults and students, often in German schools) | — | An app that works right away, in the browser, without an account, that keeps no data off the device and does not overwhelm |
77
+ | Creator | the app's repo | To build and list an app with almost no effort, and to hear when it helped |
78
+ | The creator's AI assistant | the app's repo, the `lernapps-*` skills | A clear frame: which parts are fixed, what the archetype lets it write, which rules the build checks |
79
+ | Platform owner | Oliver Jägle (@mrsimpson), lernapps@beimir.net | Every app keeps the platform's rules (privacy, accessibility default, licences) so it can be listed without doubt |
80
+ | Teachers and parents | lernapps.net | To see from the pages and the entry what the app teaches and what it does not offer, and that it may be used |
81
+ | Reviewers (the review agent before listing, the owner who decides the listing) | `lernapps/apps` | An architecture that says what each part does and why, readable without the code |
82
+ | Maintainers of an archetype | `lernapps/app-templates` | One generic architecture and one extension per archetype, so a change to the frame is made once |