@lernapps/app-templates 0.0.1 → 0.1.1

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 (232) 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-Dy359O1O.mjs +1696 -0
  5. package/dist/block-BK_ohsH1.mjs +82 -0
  6. package/dist/blocks-CxtCPAa5.mjs +149 -0
  7. package/dist/blocks.mjs +4 -0
  8. package/dist/check-CteIFN96.mjs +254 -0
  9. package/dist/content-DO6oEJxj.mjs +140 -0
  10. package/dist/contract-PONpxJtd.mjs +52 -0
  11. package/dist/e2e-CU0n_unV.mjs +89 -0
  12. package/dist/engine-CArxqizW.mjs +136 -0
  13. package/dist/explainer-e2e.mjs +1311 -0
  14. package/dist/explainer-islands.mjs +197 -0
  15. package/dist/explainer-plugin.mjs +2132 -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-RosvaPZf.mjs +16976 -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-DnW-xtCh.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-D5kwkNt1.mjs +507 -0
  33. package/dist/render-DqHOJyHK.mjs +146 -0
  34. package/dist/render-GS1Htlu8.mjs +545 -0
  35. package/dist/result-D_mmCVwN.mjs +180 -0
  36. package/dist/tasks-DADUYy52.mjs +74 -0
  37. package/dist/validate-BDwL7Su3.mjs +1076 -0
  38. package/dist/validate-Bxy9NfB8.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 +143 -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 +448 -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 +1488 -0
  142. package/runtime/explainer/exercises.ts +326 -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 +306 -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 +433 -0
  221. package/runtime/shared/questions/question.v1.schema.json +346 -0
  222. package/runtime/shared/questions/questions.css +192 -0
  223. package/runtime/shared/questions/render.ts +255 -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/speech.ts +39 -0
  230. package/runtime/shared/text/text.v1.schema.json +34 -0
  231. package/runtime/shared/vue-code.ts +395 -0
  232. package/runtime/shared/vue.ts +38 -0
@@ -0,0 +1,395 @@
1
+ // The checks of the code of single-file Vue components at build time, for every archetype that has components (the
2
+ // explainer, the interactive archetype). They run on the output of Vue's own SFC compiler, the script with the template
3
+ // compiled into it, mapped back to the line and column of the .vue file, and on the TypeScript modules of the app that
4
+ // it imports: no network (fetch, XMLHttpRequest, WebSocket, EventSource, sendBeacon, an import from a URL), no code or
5
+ // HTML from strings (eval, new Function, innerHTML, v-html), imports only from vue, the runtime
6
+ // @lernapps/app-templates, @lernapps/tooling/storage, i18n and a11y and the app's own modules (the CodeScope says
7
+ // which), and browser globals (window, document, localStorage, …) only inside onMounted and the hooks that run after
8
+ // it, because the build renders the component without a browser.
9
+ //
10
+ // What the interactive archetype's components may import is wider than the explainer's: any of the app's own files in
11
+ // src/ (sourceScope), and the libraries the app lists in the dependencies of its package.json (a bare import of such a
12
+ // package, bundled by the build; their licences are lernapps check's dependency-licence). An import from a URL stays
13
+ // forbidden.
14
+ import { createRequire } from "node:module";
15
+ import { existsSync, readFileSync } from "node:fs";
16
+ import { dirname, join, posix } from "node:path";
17
+ import type { Node, Program } from "@babel/types";
18
+ import type { SourceMapConsumer as Consumer } from "source-map-js";
19
+
20
+ export type Sfc = typeof import("vue/compiler-sfc");
21
+
22
+ const require = createRequire(import.meta.url);
23
+ let compiler: Sfc | undefined;
24
+ /** Vue's SFC compiler, loaded on first use: an app without components never loads it. */
25
+ export const sfc = (): Sfc => (compiler ??= require("vue/compiler-sfc") as Sfc);
26
+
27
+ /** Where a problem is: a line, and a column where it is known. */
28
+ export interface Place {
29
+ line: number;
30
+ column?: number;
31
+ }
32
+
33
+ const TOOLING = ["@lernapps/tooling/storage", "@lernapps/tooling/i18n", "@lernapps/tooling/a11y"];
34
+ /** The packages a component may import, for a message. */
35
+ const PACKAGES = `vue, @lernapps/app-templates, ${TOOLING.join(", ")}`;
36
+ const NETWORK = new Set(["fetch", "XMLHttpRequest", "WebSocket", "EventSource"]);
37
+ const BROWSER = new Set([
38
+ "window",
39
+ "document",
40
+ "navigator",
41
+ "location",
42
+ "history",
43
+ "localStorage",
44
+ "sessionStorage",
45
+ "indexedDB",
46
+ "matchMedia",
47
+ "requestAnimationFrame",
48
+ "cancelAnimationFrame",
49
+ "getComputedStyle",
50
+ "alert",
51
+ "confirm",
52
+ "prompt",
53
+ "screen",
54
+ "customElements",
55
+ "IntersectionObserver",
56
+ "ResizeObserver",
57
+ "MutationObserver",
58
+ ]);
59
+ /** The hooks that never run while the build renders the component: inside them, browser globals are allowed. */
60
+ const BROWSER_HOOKS = new Set(["onMounted", "onBeforeUnmount", "onUnmounted"]);
61
+ const HTML = new Set(["innerHTML", "outerHTML", "insertAdjacentHTML"]);
62
+ const GLOBAL_OBJECTS = new Set(["window", "self", "globalThis"]);
63
+
64
+ /** A line of a problem in `file`, at `place` when it is known. */
65
+ export const line = (file: string, place: Place | undefined, message: string) =>
66
+ `${file}${place === undefined ? "" : `:${place.line}${place.column === undefined ? "" : `:${place.column}`}`}: ${message}`;
67
+
68
+ const isNode = (value: unknown): value is Node =>
69
+ typeof value === "object" && value !== null && typeof (value as { type?: unknown }).type === "string";
70
+
71
+ /** Every node of `root`, with its parents from the root down. */
72
+ export function* nodes(root: Node, parents: Node[] = []): Generator<[Node, readonly Node[]]> {
73
+ yield [root, parents];
74
+ for (const [key, value] of Object.entries(root)) {
75
+ if (key === "loc" || key === "leadingComments" || key === "trailingComments" || key === "innerComments") continue;
76
+ for (const child of Array.isArray(value) ? value : [value]) {
77
+ if (isNode(child)) yield* nodes(child, [...parents, root]);
78
+ }
79
+ }
80
+ }
81
+
82
+ export const parse = (code: string): Program =>
83
+ sfc().babelParse(code, { sourceType: "module", plugins: ["typescript"] }).program as Program;
84
+
85
+ /** The name of a member's property: x.innerHTML, x["innerHTML"]. */
86
+ function propertyName(node: Node): string | undefined {
87
+ if (node.type !== "MemberExpression" && node.type !== "OptionalMemberExpression") return undefined;
88
+ if (!node.computed && node.property.type === "Identifier") return node.property.name;
89
+ return node.property.type === "StringLiteral" ? node.property.value : undefined;
90
+ }
91
+
92
+ /** What one file of a component may not do, found in its code (the SFC compiler's output, or a module). */
93
+ interface Finding {
94
+ message: string;
95
+ node: Node;
96
+ }
97
+
98
+ /** The import specifiers of `program`, with their nodes: static, re-exports and dynamic imports. */
99
+ function imports(program: Program): { source: string; node: Node }[] {
100
+ const found: { source: string; node: Node }[] = [];
101
+ for (const [node] of nodes(program)) {
102
+ if (
103
+ (node.type === "ImportDeclaration" && node.importKind !== "type") ||
104
+ ((node.type === "ExportNamedDeclaration" || node.type === "ExportAllDeclaration") &&
105
+ node.source !== null &&
106
+ node.source !== undefined &&
107
+ node.exportKind !== "type")
108
+ ) {
109
+ if (node.source) found.push({ source: node.source.value, node });
110
+ } else if (node.type === "CallExpression" && node.callee.type === "Import") {
111
+ const [argument] = node.arguments;
112
+ found.push({ source: argument?.type === "StringLiteral" ? argument.value : "(an expression)", node });
113
+ } else if (node.type === "ImportExpression") {
114
+ found.push({ source: node.source.type === "StringLiteral" ? node.source.value : "(an expression)", node });
115
+ }
116
+ }
117
+ return found;
118
+ }
119
+
120
+ /** The package a bare import specifier names: "yaml" for "yaml/util", "@scope/name" for "@scope/name/sub". */
121
+ const packageOf = (source: string): string =>
122
+ source
123
+ .split("/")
124
+ .slice(0, source.startsWith("@") ? 2 : 1)
125
+ .join("/");
126
+
127
+ /**
128
+ * What the code of `program` may not do, and its imports of the app's own modules (`isLocal`); `own` names these for
129
+ * a message, `packages` are the libraries it may import besides.
130
+ */
131
+ function findings(
132
+ program: Program,
133
+ isLocal: (source: string) => boolean,
134
+ own: string,
135
+ packages: readonly string[] = [],
136
+ ): { found: Finding[]; locals: { source: string; node: Node }[] } {
137
+ const found: Finding[] = [];
138
+ const locals: { source: string; node: Node }[] = [];
139
+ for (const { source, node } of imports(program)) {
140
+ if (/^[a-z][a-z0-9+.-]*:|^\/\//i.test(source)) {
141
+ found.push({
142
+ node,
143
+ message: `an import from ${source}; a component loads nothing from the network, so bundle what it needs (vue-component-no-network)`,
144
+ });
145
+ } else if (isLocal(source)) locals.push({ source, node });
146
+ else if (
147
+ source !== "vue" &&
148
+ source !== "@lernapps/app-templates" &&
149
+ !source.startsWith("@lernapps/app-templates/") &&
150
+ !TOOLING.includes(source) &&
151
+ !packages.includes(packageOf(source))
152
+ ) {
153
+ found.push({
154
+ node,
155
+ message: `an import from ${source}; a component imports only from ${PACKAGES} and ${own} (vue-component-imports)`,
156
+ });
157
+ }
158
+ }
159
+ const { walkIdentifiers } = sfc();
160
+ if (program.body.length === 0) return { found, locals };
161
+ walkIdentifiers(program, (identifier, parent, parents, isReference, isLocal) => {
162
+ if (!isReference || isLocal) return;
163
+ const { name } = identifier;
164
+ if (NETWORK.has(name)) {
165
+ found.push({
166
+ node: identifier,
167
+ message: `${name}: a component makes no network request; give it what it needs through its props (vue-component-no-network)`,
168
+ });
169
+ } else if (
170
+ name === "eval" ||
171
+ (name === "Function" &&
172
+ (parent?.type === "NewExpression" || parent?.type === "CallExpression") &&
173
+ parent.callee === identifier)
174
+ ) {
175
+ found.push({
176
+ node: identifier,
177
+ message: `${name === "eval" ? "eval" : "Function"}: a component runs no code from strings (vue-component-no-html-injection)`,
178
+ });
179
+ } else if (BROWSER.has(name)) {
180
+ const stack = [...parents, identifier];
181
+ const inHook = stack.some(
182
+ (node, index) =>
183
+ node.type === "CallExpression" &&
184
+ node.callee.type === "Identifier" &&
185
+ BROWSER_HOOKS.has(node.callee.name) &&
186
+ node.arguments.some((argument) => argument === stack[index + 1]),
187
+ );
188
+ if (!inHook) {
189
+ found.push({
190
+ node: identifier,
191
+ message: `${name} outside onMounted; the build renders the component where there is no ${name}, so use it only inside onMounted (vue-component-browser-globals)`,
192
+ });
193
+ }
194
+ }
195
+ });
196
+ for (const [node] of nodes(program)) {
197
+ const property = propertyName(node);
198
+ if (property !== undefined && HTML.has(property)) {
199
+ found.push({
200
+ node,
201
+ message: `${property}: a component writes no HTML from strings; let the template render it (vue-component-no-html-injection)`,
202
+ });
203
+ } else if (property === "sendBeacon") {
204
+ found.push({
205
+ node,
206
+ message: `sendBeacon: a component makes no network request (vue-component-no-network)`,
207
+ });
208
+ } else if (
209
+ property !== undefined &&
210
+ NETWORK.has(property) &&
211
+ (node.type === "MemberExpression" || node.type === "OptionalMemberExpression") &&
212
+ node.object.type === "Identifier" &&
213
+ GLOBAL_OBJECTS.has(node.object.name)
214
+ ) {
215
+ found.push({
216
+ node,
217
+ message: `${property}: a component makes no network request; give it what it needs through its props (vue-component-no-network)`,
218
+ });
219
+ } else if (
220
+ node.type === "ObjectProperty" &&
221
+ !node.computed &&
222
+ ((node.key.type === "Identifier" && node.key.name === "innerHTML") ||
223
+ (node.key.type === "StringLiteral" && node.key.value === "innerHTML"))
224
+ ) {
225
+ // what the template compiler makes of v-html
226
+ found.push({
227
+ node,
228
+ message: `v-html: a component writes no HTML from strings; let the template render it (vue-component-no-html-injection)`,
229
+ });
230
+ }
231
+ }
232
+ return { found, locals };
233
+ }
234
+
235
+ /** The problem lines of `found`, each once, at its place; one without a place only when no other has the message. */
236
+ function placed(file: string, found: readonly Finding[], place: (node: Node) => Place | undefined): string[] {
237
+ const lines: string[] = [];
238
+ const withPlace = new Set<string>();
239
+ const without: string[] = [];
240
+ for (const finding of found) {
241
+ const at = place(finding.node);
242
+ if (at === undefined) without.push(finding.message);
243
+ else {
244
+ withPlace.add(finding.message);
245
+ const text = line(file, at, finding.message);
246
+ if (!lines.includes(text)) lines.push(text);
247
+ }
248
+ }
249
+ for (const message of new Set(without)) if (!withPlace.has(message)) lines.push(line(file, undefined, message));
250
+ return lines;
251
+ }
252
+
253
+ /**
254
+ * Which imports of a component's code are the app's own modules: `resolve` gives, for an import of `source` in `file`
255
+ * (both from the app's root), the module it names, or the problem when it names none a component may import, or
256
+ * undefined when the import is not one of the app's own. `own` names the app's own modules (and the packages) for a
257
+ * message; `packages` are the libraries a component may import besides, by their package names.
258
+ */
259
+ export interface CodeScope {
260
+ own: string;
261
+ packages?: readonly string[];
262
+ resolve(file: string, source: string): { file: string } | { problem: string } | undefined;
263
+ }
264
+
265
+ /** The libraries in the dependencies of the app's package.json, by their package names. */
266
+ function dependencies(root: string): string[] {
267
+ const file = join(root, "package.json");
268
+ if (!existsSync(file)) return [];
269
+ const manifest = JSON.parse(readFileSync(file, "utf8")) as { dependencies?: Record<string, string> };
270
+ return Object.keys(manifest.dependencies ?? {});
271
+ }
272
+
273
+ /**
274
+ * The scope of an app whose components may import any of its files in src/ and the libraries in the dependencies of
275
+ * its package.json (the interactive archetype).
276
+ */
277
+ export const sourceScope = (root: string): CodeScope => ({
278
+ own: "the app's own files in src/ or a library in the dependencies of package.json",
279
+ packages: dependencies(root),
280
+ resolve(file, source) {
281
+ if (!source.startsWith("./") && !source.startsWith("../")) return undefined;
282
+ const target = posix.normalize(posix.join(dirname(file), source));
283
+ if (!target.startsWith("src/")) {
284
+ return { problem: `${source} is outside src/; a component imports only the app's own files in src/` };
285
+ }
286
+ return existsSync(join(root, target))
287
+ ? { file: target }
288
+ : { problem: `${source} does not exist; import a file of src/, with its extension` };
289
+ },
290
+ });
291
+
292
+ /** A single-file component as the compiler reads it; undefined, with its problems, when it has none to check. */
293
+ export function readSfc(
294
+ root: string,
295
+ file: string,
296
+ problems: string[],
297
+ ):
298
+ | {
299
+ descriptor: ReturnType<Sfc["parse"]>["descriptor"];
300
+ setup: NonNullable<ReturnType<Sfc["parse"]>["descriptor"]["scriptSetup"]>;
301
+ }
302
+ | undefined {
303
+ const source = readFileSync(join(root, file), "utf8");
304
+ const { descriptor, errors } = sfc().parse(source, { filename: join(root, file), sourceMap: true });
305
+ for (const error of errors) {
306
+ const loc = "loc" in error ? error.loc : undefined;
307
+ problems.push(line(file, loc ? { line: loc.start.line, column: loc.start.column } : undefined, error.message));
308
+ }
309
+ if (errors.length > 0) return undefined;
310
+ const setup = descriptor.scriptSetup;
311
+ if (setup === null || setup.lang !== "ts") {
312
+ problems.push(line(file, undefined, `a component has <script setup lang="ts">`));
313
+ return undefined;
314
+ }
315
+ return { descriptor, setup };
316
+ }
317
+
318
+ /**
319
+ * The checks of components' code, each problem into `problems`: `compile` compiles a component's <script setup> with
320
+ * its template and checks the output, `module` checks a TypeScript module; each follows the imports of the app's own
321
+ * TypeScript modules (`scope`) and checks each module once.
322
+ */
323
+ export function codeChecker(root: string, scope: CodeScope, problems: string[]) {
324
+ const { SourceMapConsumer } = require("source-map-js") as typeof import("source-map-js");
325
+ const checked = new Set<string>();
326
+ const check = (file: string, program: Program): Finding[] => {
327
+ const { found, locals } = findings(
328
+ program,
329
+ (source) => scope.resolve(file, source) !== undefined,
330
+ scope.own,
331
+ scope.packages,
332
+ );
333
+ for (const { source, node } of locals) {
334
+ const target = scope.resolve(file, source);
335
+ if (target === undefined) continue;
336
+ if ("problem" in target) found.push({ node, message: target.problem });
337
+ else if (target.file.endsWith(".ts")) module(target.file);
338
+ }
339
+ return found;
340
+ };
341
+ const module = (file: string): void => {
342
+ if (checked.has(file)) return;
343
+ checked.add(file);
344
+ problems.push(
345
+ ...placed(file, check(file, parse(readFileSync(join(root, file), "utf8"))), (node) =>
346
+ node.loc ? { line: node.loc.start.line, column: node.loc.start.column + 1 } : undefined,
347
+ ),
348
+ );
349
+ };
350
+ const compile = (
351
+ file: string,
352
+ descriptor: ReturnType<Sfc["parse"]>["descriptor"],
353
+ ): { program: Program; consumer: Consumer | undefined } | undefined => {
354
+ let compiled: ReturnType<Sfc["compileScript"]>;
355
+ try {
356
+ compiled = sfc().compileScript(descriptor, {
357
+ id: file.replace(/^.*\//, "").replace(/\.vue$/, ""),
358
+ inlineTemplate: true,
359
+ sourceMap: true,
360
+ isProd: true,
361
+ fs: { fileExists: existsSync, readFile: (name) => readFileSync(name, "utf8") },
362
+ });
363
+ } catch (error) {
364
+ const loc = (error as { loc?: { start: { line: number; column: number } } }).loc;
365
+ const message = (error instanceof Error ? error.message : String(error)).split("\n")[0] ?? "";
366
+ problems.push(line(file, loc ? { line: loc.start.line, column: loc.start.column } : undefined, message));
367
+ return undefined;
368
+ }
369
+ const program = parse(compiled.content);
370
+ const consumer: Consumer | undefined = compiled.map ? new SourceMapConsumer(compiled.map) : undefined;
371
+ problems.push(
372
+ ...placed(file, check(file, program), (node) => {
373
+ if (!node.loc || consumer === undefined) return undefined;
374
+ const original = consumer.originalPositionFor({ line: node.loc.start.line, column: node.loc.start.column });
375
+ return original.line === null ? undefined : { line: original.line, column: original.column + 1 };
376
+ }),
377
+ );
378
+ return { program, consumer };
379
+ };
380
+ return { module, compile };
381
+ }
382
+
383
+ /**
384
+ * The checks of the components' code for the single-file components `files` (from the app's root) and the app's own
385
+ * TypeScript modules they import (`scope`): one line per problem, with the file and its place.
386
+ */
387
+ export function codeProblems(root: string, files: readonly string[], scope: CodeScope): string[] {
388
+ const problems: string[] = [];
389
+ const code = codeChecker(root, scope, problems);
390
+ for (const file of files) {
391
+ const read = readSfc(root, file, problems);
392
+ if (read !== undefined) code.compile(file, read.descriptor);
393
+ }
394
+ return problems;
395
+ }
@@ -0,0 +1,38 @@
1
+ // Vue at build time, shared by the build steps of the archetypes with Vue components (the explainer, the interactive
2
+ // archetype): vue resolved to the copy this runtime depends on, so that an app's components and the runtime share one
3
+ // Vue, the options of @vitejs/plugin-vue, and a Vite server of its own that loads an app's modules for Node.
4
+ import { createRequire } from "node:module";
5
+ import { dirname } from "node:path";
6
+ import type { Alias } from "vite-plus";
7
+
8
+ const require = createRequire(import.meta.url);
9
+ /** vue, and vue/<path>, resolved to the copy this runtime depends on. */
10
+ export const vueAlias = (): Alias[] => [
11
+ { find: /^vue(?=$|\/)/, replacement: dirname(require.resolve("vue/package.json")) },
12
+ ];
13
+ /** The options of @vitejs/plugin-vue: a component's scope id from its file and source alone, the same in the build and
14
+ * in the server that renders it. */
15
+ export const VUE_OPTIONS = { features: { componentIdGenerator: "filepath-source" } } as const;
16
+
17
+ /** Loads a module of an app for Node, by its id from the app's root (/src/core/index.ts). */
18
+ export type Load = (id: string) => Promise<Record<string, unknown>>;
19
+
20
+ /** Loads modules of the app in `root` for Node in a Vite server of its own, with the components compiled for it. */
21
+ export async function withServer<T>(root: string, act: (load: Load) => Promise<T>): Promise<T> {
22
+ const [{ createServer }, { default: vue }] = await Promise.all([import("vite-plus"), import("@vitejs/plugin-vue")]);
23
+ const server = await createServer({
24
+ root,
25
+ configFile: false,
26
+ logLevel: "silent",
27
+ plugins: [vue(VUE_OPTIONS)],
28
+ resolve: { alias: vueAlias() },
29
+ appType: "custom",
30
+ server: { middlewareMode: true, hmr: false, ws: false, watch: null },
31
+ optimizeDeps: { noDiscovery: true, include: [] },
32
+ });
33
+ try {
34
+ return await act((id) => server.ssrLoadModule(id));
35
+ } finally {
36
+ await server.close();
37
+ }
38
+ }