1 |
|
2 | import { fileURLToPath as fileURLToPath4 } from "node:url";
|
3 |
|
4 |
|
5 | import { createRequire } from "node:module";
|
6 | import mdx from "@astrojs/mdx";
|
7 | import react from "@astrojs/react";
|
8 | import { pluginCollapsibleSections } from "@expressive-code/plugin-collapsible-sections";
|
9 | import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers";
|
10 | import expressiveCode from "astro-expressive-code";
|
11 | import UnoCSS from "unocss/astro";
|
12 | import { getInlineContentForPackage } from "@tutorialkit/theme";
|
13 | function extraIntegrations({ root }) {
|
14 | return [
|
15 | react(),
|
16 | expressiveCode({
|
17 | plugins: [pluginCollapsibleSections(), pluginLineNumbers()],
|
18 | themes: ["dark-plus", "light-plus"],
|
19 | customizeTheme: (theme) => {
|
20 | const isDark = theme.type === "dark";
|
21 | theme.styleOverrides = {
|
22 | borderColor: "var(--tk-border-secondary)",
|
23 | borderWidth: "1px",
|
24 | borderRadius: "var(--code-border-radius, 0px)",
|
25 | frames: {
|
26 | terminalTitlebarBackground: `var(--tk-background-${isDark ? "primary" : "secondary"})`,
|
27 | terminalTitlebarBorderBottomColor: `var(--tk-background-${isDark ? "primary" : "secondary"})`,
|
28 | editorTabBorderRadius: "var(--code-border-radius, 0px)",
|
29 | editorTabBarBackground: `var(--tk-background-${isDark ? "primary" : "secondary"})`
|
30 | }
|
31 | };
|
32 | },
|
33 | themeCssSelector: (theme) => {
|
34 | let customThemeName = "light";
|
35 | if (theme.name === "dark-plus") {
|
36 | customThemeName = "dark";
|
37 | }
|
38 | return `[data-theme='${customThemeName}']`;
|
39 | },
|
40 | defaultProps: {
|
41 | showLineNumbers: false
|
42 | },
|
43 | styleOverrides: {
|
44 | frames: {
|
45 | shadowColor: "none"
|
46 | }
|
47 | }
|
48 | }),
|
49 | mdx(),
|
50 | UnoCSS({
|
51 | configDeps: ["./theme.ts"],
|
52 | injectReset: createRequire(root).resolve("@unocss/reset/tailwind.css"),
|
53 | content: {
|
54 | inline: getInlineContentForPackage({
|
55 | name: "@tutorialkit/astro",
|
56 | pattern: "/dist/default/**/*.astro",
|
57 | root
|
58 | })
|
59 | }
|
60 | })
|
61 | ];
|
62 | }
|
63 |
|
64 |
|
65 | import path2 from "node:path";
|
66 | import remarkDirective from "remark-directive";
|
67 |
|
68 |
|
69 | import { h } from "hastscript";
|
70 | import {
|
71 | directiveToMarkdown
|
72 | } from "mdast-util-directive";
|
73 | import { toMarkdown } from "mdast-util-to-markdown";
|
74 | import { visit } from "unist-util-visit";
|
75 | var callouts = {
|
76 | tip: {
|
77 | title: "Tip",
|
78 | icon: "i-ph-rocket-launch"
|
79 | },
|
80 | info: {
|
81 | title: "Info",
|
82 | icon: "i-ph-info"
|
83 | },
|
84 | warn: {
|
85 | title: "Warning",
|
86 | icon: "i-ph-warning-circle"
|
87 | },
|
88 | success: {
|
89 | title: "Success",
|
90 | icon: "i-ph-check-circle"
|
91 | },
|
92 | danger: {
|
93 | title: "Danger",
|
94 | icon: "i-ph-x-circle"
|
95 | }
|
96 | };
|
97 | var variants = new Set(["tip", "info", "warn", "danger", "success"]);
|
98 | function isNodeDirective(node) {
|
99 | return node.type === "textDirective" || node.type === "leafDirective" || node.type === "containerDirective";
|
100 | }
|
101 | function isContainerDirective(node) {
|
102 | return node.type === "containerDirective";
|
103 | }
|
104 | function transformUnhandledDirective(node, index, parent) {
|
105 | const textNode = {
|
106 | type: "text",
|
107 | value: toMarkdown(node, { extensions: [directiveToMarkdown()] })
|
108 | };
|
109 | if (node.type === "textDirective") {
|
110 | parent.children[index] = textNode;
|
111 | } else {
|
112 | parent.children[index] = {
|
113 | type: "paragraph",
|
114 | children: [textNode]
|
115 | };
|
116 | }
|
117 | }
|
118 | function remarkCalloutsPlugin() {
|
119 | return () => {
|
120 | return (tree) => {
|
121 | visit(tree, (node, index, parent) => {
|
122 | if (!parent || index === void 0 || !isNodeDirective(node)) {
|
123 | return;
|
124 | }
|
125 | if (node.type === "textDirective" || node.type === "leafDirective") {
|
126 | transformUnhandledDirective(node, index, parent);
|
127 | return;
|
128 | }
|
129 | if (isContainerDirective(node)) {
|
130 | if (!variants.has(node.name)) {
|
131 | return;
|
132 | }
|
133 | const variant = node.name;
|
134 | const callout = callouts[variant];
|
135 | const data = node.data || (node.data = {});
|
136 | const attributes = node.attributes ?? {};
|
137 | const title = attributes.title ?? callout.title;
|
138 | const noBorder = attributes.noBorder === "true";
|
139 | const hideTitle = attributes.hideTitle === "true";
|
140 | const hideIcon = attributes.hideIcon === "true";
|
141 | const classes = [
|
142 | `callout callout-${variant} my-4 flex flex-col p-3 bg-tk-elements-markdown-callouts-backgroundColor text-tk-elements-markdown-callouts-textColor`,
|
143 | attributes.class ?? "",
|
144 | ...noBorder ? [] : ["border-l-3", "border-tk-elements-markdown-callouts-borderColor"]
|
145 | ];
|
146 | node.attributes = {
|
147 | ...attributes,
|
148 | class: classes.filter((calloutClass) => !!calloutClass).join(" "),
|
149 | "aria-label": title
|
150 | };
|
151 | node.children = generate(title, node.children, callout, hideIcon, hideTitle);
|
152 | const tagName = "aside";
|
153 | const hast = h(tagName, node.attributes);
|
154 | data.hName = hast.tagName;
|
155 | data.hProperties = hast.properties;
|
156 | }
|
157 | });
|
158 | };
|
159 | };
|
160 | }
|
161 | function generate(title, children, callout, hideIcon, hideTitle) {
|
162 | return [
|
163 | ...hideTitle ? [] : [
|
164 | {
|
165 | type: "paragraph",
|
166 | data: {
|
167 | hName: "div",
|
168 | hProperties: {
|
169 | className: "w-full flex gap-2 items-center text-tk-elements-markdown-callouts-titleTextColor",
|
170 | ariaHidden: true
|
171 | }
|
172 | },
|
173 | children: [
|
174 | ...hideIcon ? [] : [
|
175 | {
|
176 | type: "html",
|
177 | value: `<span class="text-6 inline-block text-tk-elements-markdown-callouts-iconColor ${callout.icon}"></span>`
|
178 | }
|
179 | ],
|
180 | {
|
181 | type: "html",
|
182 | value: `<span class="text-4 font-semibold inline-block"> ${title}</span>`
|
183 | }
|
184 | ]
|
185 | }
|
186 | ],
|
187 | {
|
188 | type: "paragraph",
|
189 | data: {
|
190 | hName: "section",
|
191 | hProperties: { className: ["callout-content mt-1"] }
|
192 | },
|
193 | children
|
194 | }
|
195 | ];
|
196 | }
|
197 |
|
198 |
|
199 | import frontMatter from "front-matter";
|
200 | import * as kleur from "kleur/colors";
|
201 | import fs from "node:fs";
|
202 | import path from "node:path";
|
203 | import { visit as visit2 } from "unist-util-visit";
|
204 | function remarkImportFilePlugin(options) {
|
205 | return () => {
|
206 | return (tree, file) => {
|
207 | if (!file.basename || !/^content\.mdx?$/.test(file.basename)) {
|
208 | return;
|
209 | }
|
210 | const cwd = path.dirname(file.path);
|
211 | const templateName = getTemplateName(file.path);
|
212 | const templatesPath = path.join(options.templatesPath, templateName);
|
213 | visit2(tree, (node) => {
|
214 | if (node.type !== "code") {
|
215 | return void 0;
|
216 | }
|
217 | if (node.lang && /^(files?|solution):/.test(node.lang)) {
|
218 | const relativeFilePath = node.lang.replace(/^(files?|solution):\//, "");
|
219 | let content;
|
220 | if (/^files?\:/.test(node.lang)) {
|
221 | content = tryRead(path.join(cwd, "_files", relativeFilePath), false);
|
222 | if (!content) {
|
223 | content = tryRead(path.join(templatesPath, relativeFilePath));
|
224 | }
|
225 | } else if (node.lang.startsWith("solution:")) {
|
226 | content = tryRead(path.join(cwd, "_solution", relativeFilePath));
|
227 | }
|
228 | if (content) {
|
229 | node.value = content;
|
230 | node.lang = path.extname(relativeFilePath).slice(1);
|
231 | }
|
232 | }
|
233 | return void 0;
|
234 | });
|
235 | };
|
236 | };
|
237 | }
|
238 | function tryRead(filePath, warn = true) {
|
239 | try {
|
240 | return fs.readFileSync(filePath, "utf8");
|
241 | } catch {
|
242 | if (warn) {
|
243 | printWarning(`Could not read '${filePath}'. Are you sure this file exists?`);
|
244 | }
|
245 | }
|
246 | return void 0;
|
247 | }
|
248 | function getTemplateName(file) {
|
249 | const content = fs.readFileSync(file, "utf8");
|
250 | const meta = frontMatter(
|
251 | content
|
252 | );
|
253 | if (meta.attributes.template) {
|
254 | return meta.attributes.template;
|
255 | }
|
256 | if (meta.attributes.type === "tutorial") {
|
257 | return "default";
|
258 | }
|
259 | return getTemplateName(path.join(path.dirname(path.dirname(file)), "meta.md"));
|
260 | }
|
261 | function printWarning(message) {
|
262 | const time = ( new Date()).toTimeString().slice(0, 8);
|
263 | console.warn(kleur.yellow(`${kleur.bold(time)} [WARN] [remarkImportFilePlugin] ${message}`));
|
264 | }
|
265 |
|
266 |
|
267 | function updateMarkdownConfig({ updateConfig }) {
|
268 | updateConfig({
|
269 | markdown: {
|
270 | remarkPlugins: [
|
271 | remarkDirective,
|
272 | remarkCalloutsPlugin(),
|
273 | remarkImportFilePlugin({ templatesPath: path2.join(process.cwd(), "src/templates") })
|
274 | ]
|
275 | }
|
276 | });
|
277 | }
|
278 |
|
279 |
|
280 | import path3 from "node:path";
|
281 | import { fileURLToPath } from "node:url";
|
282 |
|
283 |
|
284 | function withResolvers() {
|
285 | let resolve;
|
286 | let reject;
|
287 | const promise = new Promise((_resolve, _reject) => {
|
288 | resolve = _resolve;
|
289 | reject = _reject;
|
290 | });
|
291 | return {
|
292 | resolve,
|
293 | reject,
|
294 | promise
|
295 | };
|
296 | }
|
297 | function normalizeImportPath(importPath) {
|
298 | return importPath.replaceAll("\\", "/");
|
299 | }
|
300 |
|
301 |
|
302 | var __dirname = path3.dirname(fileURLToPath(import.meta.url));
|
303 | var virtualModuleId = "tutorialkit:core";
|
304 | var resolvedVirtualModuleId = `${virtualModuleId}`;
|
305 | var tutorialkitCore = {
|
306 | name: "tutorialkit-core-virtual-mod-plugin",
|
307 | resolveId(id) {
|
308 | if (id === virtualModuleId) {
|
309 | return resolvedVirtualModuleId;
|
310 | }
|
311 | return void 0;
|
312 | },
|
313 | async load(id) {
|
314 | if (id === resolvedVirtualModuleId) {
|
315 | const pathToInit = normalizeImportPath(path3.join(__dirname, "default/components/webcontainer.ts"));
|
316 | return `
|
317 | export { webcontainer } from '${pathToInit}';
|
318 | `;
|
319 | }
|
320 | return void 0;
|
321 | }
|
322 | };
|
323 |
|
324 |
|
325 | import { watch } from "chokidar";
|
326 | import fs2 from "fs/promises";
|
327 | import path4 from "path";
|
328 | var CUSTOM_PATHS = ["theme.css", "theme/index.css"];
|
329 | var virtualModuleId2 = "@tutorialkit/custom.css";
|
330 | var resolvedVirtualModuleId2 = `/${virtualModuleId2}`;
|
331 | var projectRoot = process.cwd();
|
332 | var userlandCSS = {
|
333 | name: "@tutorialkit/custom.css",
|
334 | resolveId(id) {
|
335 | if (id === virtualModuleId2) {
|
336 | return resolvedVirtualModuleId2;
|
337 | }
|
338 | return void 0;
|
339 | },
|
340 | configResolved({ root }) {
|
341 | projectRoot = root;
|
342 | },
|
343 | async load(id) {
|
344 | if (id === resolvedVirtualModuleId2) {
|
345 | const path9 = await findCustomCSSFilePath(projectRoot);
|
346 | if (path9) {
|
347 | return `@import '${path9}';`;
|
348 | } else {
|
349 | return "";
|
350 | }
|
351 | }
|
352 | return void 0;
|
353 | }
|
354 | };
|
355 | function watchUserlandCSS(server, logger) {
|
356 | const projectRoot2 = server.config.root;
|
357 | const watchedNames = new Map(CUSTOM_PATHS.map((path9) => [path9, false]));
|
358 | const watcher = watch(projectRoot2, {
|
359 | ignoreInitial: true,
|
360 | cwd: projectRoot2,
|
361 | depth: 2,
|
362 | ignored: ["dist", "src", "node_modules"]
|
363 | });
|
364 | watcher.on("all", (eventName, filePath) => {
|
365 | if (eventName === "addDir" || eventName === "unlinkDir" || !watchedNames.has(filePath)) {
|
366 | return;
|
367 | }
|
368 | watchedNames.set(filePath, eventName !== "unlink");
|
369 | checkConflicts(watchedNames, logger);
|
370 | const module = server.moduleGraph.getModuleById(resolvedVirtualModuleId2);
|
371 | if (module) {
|
372 | server.reloadModule(module);
|
373 | }
|
374 | });
|
375 | }
|
376 | async function findCustomCSSFilePath(projectRoot2) {
|
377 | for (const cssFilePath of CUSTOM_PATHS) {
|
378 | try {
|
379 | const stats = await fs2.stat(path4.join(projectRoot2, cssFilePath));
|
380 | if (stats.isFile()) {
|
381 | return path4.resolve(cssFilePath);
|
382 | }
|
383 | } catch {
|
384 | }
|
385 | }
|
386 | return void 0;
|
387 | }
|
388 | function checkConflicts(watchedNames, logger) {
|
389 | const conflictCount = [...watchedNames.values()].reduce((count, value) => value ? count + 1 : count, 0);
|
390 | if (conflictCount > 1) {
|
391 | let errorMessage = "";
|
392 | let index = 0;
|
393 | const lastIndex = conflictCount - 1;
|
394 | for (const [configName, isPresent] of watchedNames.entries()) {
|
395 | if (isPresent) {
|
396 | if (index === 0) {
|
397 | errorMessage = `File '${configName}' takes precedences over`;
|
398 | } else if (index === 1) {
|
399 | errorMessage += ` '${configName}'${index === lastIndex ? "." : ""}`;
|
400 | } else if (index !== lastIndex) {
|
401 | errorMessage += `, '${configName}'`;
|
402 | } else {
|
403 | errorMessage += ` and '${configName}'.`;
|
404 | }
|
405 | index += 1;
|
406 | }
|
407 | }
|
408 | errorMessage += " You might want to remove one of those files.";
|
409 | logger.warn(errorMessage);
|
410 | }
|
411 | }
|
412 |
|
413 |
|
414 | import path5 from "node:path";
|
415 | import { fileURLToPath as fileURLToPath2 } from "node:url";
|
416 | var __dirname2 = path5.dirname(fileURLToPath2(import.meta.url));
|
417 | var virtualModuleId3 = "tutorialkit:store";
|
418 | var resolvedVirtualModuleId3 = `${virtualModuleId3}`;
|
419 | var tutorialkitStore = {
|
420 | name: "tutorialkit-store-virtual-mod-plugin",
|
421 | resolveId(id) {
|
422 | if (id === virtualModuleId3) {
|
423 | return resolvedVirtualModuleId3;
|
424 | }
|
425 | return void 0;
|
426 | },
|
427 | async load(id) {
|
428 | if (id === resolvedVirtualModuleId3) {
|
429 | const pathToInit = normalizeImportPath(path5.join(__dirname2, "default/components/webcontainer.ts"));
|
430 | return `
|
431 | import { tutorialStore } from '${pathToInit}';
|
432 | export default tutorialStore;
|
433 | `;
|
434 | }
|
435 | return void 0;
|
436 | }
|
437 | };
|
438 |
|
439 |
|
440 | var virtualModuleId4 = "tutorialkit:override-components";
|
441 | var resolvedId = `\0${virtualModuleId4}`;
|
442 | function overrideComponents({ components, defaultRoutes }) {
|
443 | return {
|
444 | name: "tutorialkit-override-components-plugin",
|
445 | resolveId(id) {
|
446 | if (id === virtualModuleId4) {
|
447 | return resolvedId;
|
448 | }
|
449 | return void 0;
|
450 | },
|
451 | async load(id) {
|
452 | if (id === resolvedId) {
|
453 | const topBar = components?.TopBar || resolveDefaultTopBar(defaultRoutes);
|
454 | return `
|
455 | export { default as TopBar } from '${topBar}';
|
456 | `;
|
457 | }
|
458 | return void 0;
|
459 | }
|
460 | };
|
461 | }
|
462 | function resolveDefaultTopBar(defaultRoutes) {
|
463 | if (defaultRoutes) {
|
464 | return "@tutorialkit/astro/default/components/TopBar.astro";
|
465 | }
|
466 | return "./src/components/TopBar.astro";
|
467 | }
|
468 |
|
469 |
|
470 | import { watch as watch2 } from "chokidar";
|
471 | import { dim as dim2 } from "kleur/colors";
|
472 | import fs4 from "node:fs";
|
473 | import path8 from "node:path";
|
474 | import { fileURLToPath as fileURLToPath3 } from "node:url";
|
475 |
|
476 |
|
477 | import { dim } from "kleur/colors";
|
478 | import path7 from "node:path";
|
479 |
|
480 |
|
481 | var IGNORED_FILES = ["**/.DS_Store", "**/*.swp", "**/node_modules/**"];
|
482 | var EXTEND_CONFIG_FILEPATH = "/.tk-config.json";
|
483 | var FILES_FOLDER_NAME = "_files";
|
484 | var SOLUTION_FOLDER_NAME = "_solution";
|
485 |
|
486 |
|
487 | import glob from "fast-glob";
|
488 | import fs3 from "node:fs";
|
489 | import path6 from "node:path";
|
490 | import { z } from "zod";
|
491 | var configSchema = z.object({
|
492 | extends: z.string()
|
493 | }).strict();
|
494 | var FilesMapGraph = class {
|
495 | constructor(_nodes = /* @__PURE__ */ new Map()) {
|
496 | this._nodes = _nodes;
|
497 | }
|
498 | getFilesMapByFolder(folder) {
|
499 | try {
|
500 | const resolvedPath = fs3.realpathSync(folder);
|
501 | return this._nodes.get(resolvedPath);
|
502 | } catch {
|
503 | return void 0;
|
504 | }
|
505 | }
|
506 | allFilesMap() {
|
507 | return this._nodes.values();
|
508 | }
|
509 | updateFilesMapByFolder(folder, logger) {
|
510 | const resolvedPath = fs3.realpathSync(folder);
|
511 | let node = this._nodes.get(resolvedPath);
|
512 | if (!node) {
|
513 | node = new FilesMap(resolvedPath);
|
514 | this._nodes.set(resolvedPath, node);
|
515 | }
|
516 | node.update(this._nodes, logger);
|
517 | }
|
518 | };
|
519 | var FilesMap = class _FilesMap {
|
520 | |
521 |
|
522 |
|
523 |
|
524 |
|
525 |
|
526 | constructor(path9) {
|
527 | this.path = path9;
|
528 | }
|
529 | |
530 |
|
531 |
|
532 |
|
533 |
|
534 |
|
535 |
|
536 |
|
537 |
|
538 |
|
539 | static async initGraph(folders, logger) {
|
540 | const nodes = new Map();
|
541 | const resolvedPaths = await Promise.all(folders.map((folder) => fs3.promises.realpath(folder)));
|
542 | for (const resolvedPath of resolvedPaths) {
|
543 | nodes.set(resolvedPath, new _FilesMap(resolvedPath));
|
544 | }
|
545 | for (const node of nodes.values()) {
|
546 | node.update(nodes, logger);
|
547 | }
|
548 | return new FilesMapGraph(nodes);
|
549 | }
|
550 | _extend = null;
|
551 | _dependents = new Set();
|
552 | update(graph, logger) {
|
553 | const configPath = path6.join(this.path, EXTEND_CONFIG_FILEPATH);
|
554 | if (!fs3.existsSync(configPath)) {
|
555 | this.unlink();
|
556 | return;
|
557 | }
|
558 | try {
|
559 | const jsonConfig = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
|
560 | const config = configSchema.parse(jsonConfig);
|
561 | const dependOnPath = fs3.realpathSync(path6.join(this.path, config.extends));
|
562 | let dependOn = graph.get(dependOnPath);
|
563 | if (!dependOn) {
|
564 | dependOn = new _FilesMap(dependOnPath);
|
565 | graph.set(dependOnPath, dependOn);
|
566 | dependOn.update(graph, logger);
|
567 | }
|
568 | this.extend(dependOn);
|
569 | } catch (error) {
|
570 | logger.warn(`Failed to parse '${configPath}', content won't be included.
|
571 | Error: ${error}`);
|
572 | }
|
573 | }
|
574 | extend(other) {
|
575 | this.unlink();
|
576 | other._dependents.add(this);
|
577 | this._extend = other;
|
578 | }
|
579 | unlink() {
|
580 | this._extend?._dependents.delete(this);
|
581 | this._extend = null;
|
582 | }
|
583 | *allDependents() {
|
584 | for (const dependency of this._dependents) {
|
585 | yield dependency;
|
586 | yield* dependency.allDependents();
|
587 | }
|
588 | }
|
589 | async toFiles(logger) {
|
590 | const filePathsObj = await this._toFilePathsCheckCycles({
|
591 | logger,
|
592 | visitedNodes: []
|
593 | });
|
594 | delete filePathsObj[EXTEND_CONFIG_FILEPATH];
|
595 | const filePaths = Object.entries(filePathsObj);
|
596 | filePaths.sort();
|
597 | const files = {};
|
598 | for (const [webcontainerPath2, filePath] of filePaths) {
|
599 | const buffer = fs3.readFileSync(filePath);
|
600 | try {
|
601 | const stringContent = new TextDecoder("utf-8", { fatal: true }).decode(buffer);
|
602 | files[webcontainerPath2] = stringContent;
|
603 | } catch {
|
604 | files[webcontainerPath2] = { base64: buffer.toString("base64") };
|
605 | }
|
606 | }
|
607 | return files;
|
608 | }
|
609 | async _toFilePathsCheckCycles(context) {
|
610 | const seenIndex = context.visitedNodes.indexOf(this.path);
|
611 | if (seenIndex !== -1) {
|
612 | context.logger.warn(
|
613 | [
|
614 | "Cycle detected:",
|
615 | ...context.visitedNodes.map((filePath, index) => {
|
616 | return ` * '${filePath}' ${index === seenIndex ? "<-- Cycle points back to that file" : ""}`;
|
617 | }),
|
618 | "Content will be ignored after that point."
|
619 | ].join("\n")
|
620 | );
|
621 | return {};
|
622 | }
|
623 | context.visitedNodes.push(this.path);
|
624 | const filePaths = this._extend ? await this._extend._toFilePathsCheckCycles(context) : {};
|
625 | await getAllFiles(this.path, filePaths);
|
626 | return filePaths;
|
627 | }
|
628 | };
|
629 | async function getAllFiles(dir, result) {
|
630 | const filePaths = await glob(`${glob.convertPathToPattern(dir)}/**/*`, {
|
631 | onlyFiles: true,
|
632 | dot: true,
|
633 | ignore: IGNORED_FILES
|
634 | });
|
635 | for (const filePath of filePaths) {
|
636 | result[webcontainerPath(dir, filePath)] = filePath;
|
637 | }
|
638 | }
|
639 | function webcontainerPath(dir, filePath) {
|
640 | const result = `/${path6.relative(dir, filePath)}`;
|
641 | if (path6.sep !== "/") {
|
642 | return result.replaceAll(path6.sep, "/");
|
643 | }
|
644 | return result;
|
645 | }
|
646 |
|
647 |
|
648 | import { folderPathToFilesRef } from "@tutorialkit/types";
|
649 | import glob2 from "fast-glob";
|
650 | function getFilesRef(pathToFolder, { contentDir, templatesDir }) {
|
651 | if (pathToFolder.startsWith(contentDir)) {
|
652 | pathToFolder = pathToFolder.slice(contentDir.length + 1);
|
653 | } else if (pathToFolder.startsWith(templatesDir)) {
|
654 | pathToFolder = "template" + pathToFolder.slice(templatesDir.length);
|
655 | }
|
656 | return folderPathToFilesRef(pathToFolder);
|
657 | }
|
658 | function getAllFilesMap({ contentDir, templatesDir }) {
|
659 | return glob2(
|
660 | [
|
661 | `${glob2.convertPathToPattern(contentDir)}/**/${FILES_FOLDER_NAME}`,
|
662 | `${glob2.convertPathToPattern(contentDir)}/**/${SOLUTION_FOLDER_NAME}`,
|
663 | `${glob2.convertPathToPattern(templatesDir)}/*`
|
664 | ],
|
665 | { onlyDirectories: true, ignore: IGNORED_FILES }
|
666 | );
|
667 | }
|
668 |
|
669 |
|
670 | var FilesMapCache = class {
|
671 | constructor(_filesMapGraph, _logger, _server, _dirs) {
|
672 | this._filesMapGraph = _filesMapGraph;
|
673 | this._logger = _logger;
|
674 | this._server = _server;
|
675 | this._dirs = _dirs;
|
676 | const { promise, resolve } = withResolvers();
|
677 | this._readiness = promise;
|
678 | this._resolve = resolve;
|
679 | for (const filesMap of _filesMapGraph.allFilesMap()) {
|
680 | this._requestsQueue.add(filesMap.path);
|
681 | this._cache.set(getFilesRef(filesMap.path, this._dirs), void 0);
|
682 | }
|
683 | this._generateFileMaps();
|
684 | }
|
685 |
|
686 | _cache = new Map();
|
687 |
|
688 | _requestsQueue = new Set();
|
689 |
|
690 | _readiness;
|
691 | _resolve;
|
692 |
|
693 | _hotPaths = new Set();
|
694 | _timeoutId = null;
|
695 | generateFileMapForPath(filePath) {
|
696 | const filesMapFolderPath = resolveFilesFolderPath(filePath, this._logger, this._dirs);
|
697 | if (!filesMapFolderPath) {
|
698 | this._logger.warn(`File ${filePath} is not part of the tutorial content or templates folders.`);
|
699 | return;
|
700 | }
|
701 | const filesRef = getFilesRef(filesMapFolderPath, this._dirs);
|
702 | this._cache.set(filesRef, void 0);
|
703 | this._invalidateCacheForDependencies(filesMapFolderPath);
|
704 | this._requestsQueue.add(filesMapFolderPath);
|
705 | if (this._timeoutId) {
|
706 | return;
|
707 | }
|
708 | const { promise, resolve } = withResolvers();
|
709 | this._readiness = promise;
|
710 | this._resolve = resolve;
|
711 | this._timeoutId = setTimeout(this._generateFileMaps, 100);
|
712 | }
|
713 | async canHandle(reqURL) {
|
714 | const fileMapPath = new URL(reqURL ?? "/", "http://a").pathname.slice(1);
|
715 | if (!this._cache.has(fileMapPath)) {
|
716 | return false;
|
717 | }
|
718 | let cacheValue = this._cache.get(fileMapPath);
|
719 | if (typeof cacheValue === "undefined") {
|
720 | await this._readiness;
|
721 | cacheValue = this._cache.get(fileMapPath);
|
722 | }
|
723 | if (typeof cacheValue === "undefined") {
|
724 | this._logger.error(`The cache never resolved for ${fileMapPath}.`);
|
725 | return false;
|
726 | }
|
727 | this._hotPaths.add(fileMapPath);
|
728 | return cacheValue;
|
729 | }
|
730 | _invalidateCacheForDependencies(folderPath) {
|
731 | const filesMap = this._filesMapGraph.getFilesMapByFolder(folderPath);
|
732 | if (!filesMap) {
|
733 | return;
|
734 | }
|
735 | for (const dependency of filesMap.allDependents()) {
|
736 | this._cache.set(getFilesRef(dependency.path, this._dirs), void 0);
|
737 | }
|
738 | }
|
739 | _generateFileMaps = async () => {
|
740 | const hotFilesRefs = [];
|
741 | while (this._requestsQueue.size > 0) {
|
742 | for (const folderPath of this._requestsQueue) {
|
743 | this._filesMapGraph.updateFilesMapByFolder(folderPath, this._logger);
|
744 | }
|
745 | for (const folderPath of this._requestsQueue) {
|
746 | for (const dependency of this._filesMapGraph.getFilesMapByFolder(folderPath).allDependents()) {
|
747 | this._requestsQueue.add(dependency.path);
|
748 | }
|
749 | }
|
750 | const requests = [...this._requestsQueue].map((folderPath) => {
|
751 | return [getFilesRef(folderPath, this._dirs), this._filesMapGraph.getFilesMapByFolder(folderPath)];
|
752 | });
|
753 | this._requestsQueue.clear();
|
754 | await Promise.all(
|
755 | requests.map(async ([filesRef, filesMap]) => {
|
756 | if (this._hotPaths.has(filesRef)) {
|
757 | hotFilesRefs.push(filesRef);
|
758 | }
|
759 | const timeNow = performance.now();
|
760 | const fileMap = await filesMap.toFiles(this._logger);
|
761 | this._cache.set(filesRef, JSON.stringify(fileMap));
|
762 | const elapsed = performance.now() - timeNow;
|
763 | this._logger.info(`Generated ${filesRef} ${dim(Math.round(elapsed) + "ms")}`);
|
764 | })
|
765 | );
|
766 | }
|
767 | this._resolve();
|
768 | if (hotFilesRefs.length > 0) {
|
769 | this._hotPaths.clear();
|
770 | this._server.hot.send({ type: "custom", event: "tk:refresh-wc-files", data: hotFilesRefs });
|
771 | }
|
772 | this._timeoutId = null;
|
773 | };
|
774 | };
|
775 | function resolveFilesFolderPath(filePath, logger, { contentDir, templatesDir }) {
|
776 | if (filePath.startsWith(templatesDir)) {
|
777 | const index = filePath.indexOf(path7.sep, templatesDir.length + 1);
|
778 | if (index === -1) {
|
779 | logger.error(`Bug: ${filePath} is not in a directory under ${templatesDir}`);
|
780 | return void 0;
|
781 | }
|
782 | return filePath.slice(0, index);
|
783 | }
|
784 | if (filePath.startsWith(contentDir)) {
|
785 | let filesFolder = filePath;
|
786 | while (filesFolder && !filesFolder.endsWith(FILES_FOLDER_NAME) && !filesFolder.endsWith(SOLUTION_FOLDER_NAME)) {
|
787 | if (filesFolder === contentDir) {
|
788 | logger.error(`Bug: ${filePath} was not under ${FILES_FOLDER_NAME} or ${SOLUTION_FOLDER_NAME}`);
|
789 | return void 0;
|
790 | }
|
791 | filesFolder = path7.dirname(filesFolder);
|
792 | }
|
793 | return filesFolder;
|
794 | }
|
795 | return void 0;
|
796 | }
|
797 |
|
798 |
|
799 | var WebContainerFiles = class {
|
800 | _watcher;
|
801 | async serverSetup(projectRoot2, { server, logger }) {
|
802 | const { contentDir, templatesDir } = this._folders(projectRoot2);
|
803 | const graph = await FilesMap.initGraph(await getAllFilesMap({ contentDir, templatesDir }), logger);
|
804 | const cache = new FilesMapCache(graph, logger, server, { contentDir, templatesDir });
|
805 | this._watcher = watch2(
|
806 | [
|
807 |
|
808 | path8.join(contentDir, `**/${FILES_FOLDER_NAME}/**/*`),
|
809 | path8.join(contentDir, `**/${SOLUTION_FOLDER_NAME}/**/*`),
|
810 | templatesDir
|
811 | ],
|
812 | {
|
813 | ignored: IGNORED_FILES,
|
814 | ignoreInitial: true
|
815 | }
|
816 | );
|
817 | this._watcher.on("all", (eventName, filePath) => {
|
818 | if (eventName === "addDir") {
|
819 | return;
|
820 | }
|
821 | cache.generateFileMapForPath(filePath);
|
822 | });
|
823 | server.middlewares.use(async (req, res, next) => {
|
824 | const result = await cache.canHandle(req.url);
|
825 | if (!result) {
|
826 | next();
|
827 | return;
|
828 | }
|
829 | res.writeHead(200, {
|
830 | "Content-Type": "application/json"
|
831 | });
|
832 | res.end(result);
|
833 | });
|
834 | }
|
835 | serverDone() {
|
836 | return this._watcher?.close();
|
837 | }
|
838 | async buildAssets(projectRoot2, { dir, logger }) {
|
839 | const { contentDir, templatesDir } = this._folders(projectRoot2);
|
840 | const filesMapFolders = await getAllFilesMap({ contentDir, templatesDir });
|
841 | const graph = await FilesMap.initGraph(filesMapFolders, logger);
|
842 | await Promise.all(
|
843 | filesMapFolders.map(async (folder) => {
|
844 | folder = path8.normalize(folder);
|
845 | const filesRef = getFilesRef(folder, { contentDir, templatesDir });
|
846 | const dest = fileURLToPath3(new URL(filesRef, dir));
|
847 | const fileMap = await graph.getFilesMapByFolder(folder).toFiles(logger);
|
848 | await fs4.promises.writeFile(dest, JSON.stringify(fileMap));
|
849 | logger.info(`${dim2(filesRef)}`);
|
850 | })
|
851 | );
|
852 | }
|
853 | _folders(projectRoot2) {
|
854 | const contentDir = path8.join(projectRoot2, "./src/content/tutorial");
|
855 | const templatesDir = path8.join(projectRoot2, "./src/templates");
|
856 | return { contentDir, templatesDir };
|
857 | }
|
858 | };
|
859 |
|
860 |
|
861 | function createPlugin({
|
862 | defaultRoutes = true,
|
863 | components,
|
864 | isolation,
|
865 | enterprise
|
866 | } = {}) {
|
867 | const webcontainerFiles = new WebContainerFiles();
|
868 | let _config;
|
869 | return {
|
870 | name: "@tutorialkit/astro",
|
871 | hooks: {
|
872 | async "astro:config:setup"(options) {
|
873 | const { injectRoute, updateConfig, config } = options;
|
874 | updateConfig({
|
875 | server: {
|
876 | headers: {
|
877 | "Cross-Origin-Embedder-Policy": isolation ?? "require-corp",
|
878 | "Cross-Origin-Opener-Policy": "same-origin"
|
879 | }
|
880 | },
|
881 | vite: {
|
882 | optimizeDeps: {
|
883 | entries: ["!**/src/(content|templates)/**"],
|
884 | include: null ? [] : ["@tutorialkit/react"]
|
885 | },
|
886 | define: {
|
887 | __ENTERPRISE__: `${!!enterprise}`,
|
888 | __WC_CONFIG__: enterprise ? JSON.stringify(enterprise) : "undefined"
|
889 | },
|
890 | ssr: {
|
891 | noExternal: ["@tutorialkit/astro", "@tutorialkit/react"]
|
892 | },
|
893 | plugins: [
|
894 | userlandCSS,
|
895 | tutorialkitStore,
|
896 | tutorialkitCore,
|
897 | overrideComponents({ components, defaultRoutes: !!defaultRoutes }),
|
898 | null ? (await null).default() : null
|
899 | ]
|
900 | }
|
901 | });
|
902 | updateMarkdownConfig(options);
|
903 | if (defaultRoutes) {
|
904 | if (defaultRoutes !== "tutorial-only") {
|
905 | injectRoute({
|
906 | pattern: "/",
|
907 | entrypoint: "@tutorialkit/astro/default/pages/index.astro",
|
908 | prerender: true
|
909 | });
|
910 | }
|
911 | injectRoute({
|
912 | pattern: "[...slug]",
|
913 | entrypoint: "@tutorialkit/astro/default/pages/[...slug].astro",
|
914 | prerender: true
|
915 | });
|
916 | }
|
917 | const selfIndex = config.integrations.findIndex((integration) => integration.name === "@tutorialkit/astro");
|
918 | config.integrations.splice(selfIndex + 1, 0, ...extraIntegrations({ root: fileURLToPath4(config.root) }));
|
919 | },
|
920 | "astro:config:done"({ config }) {
|
921 | _config = config;
|
922 | },
|
923 | async "astro:server:setup"(options) {
|
924 | if (!_config) {
|
925 | return;
|
926 | }
|
927 | const { server, logger } = options;
|
928 | const projectRoot2 = fileURLToPath4(_config.root);
|
929 | await webcontainerFiles.serverSetup(projectRoot2, options);
|
930 | watchUserlandCSS(server, logger);
|
931 | },
|
932 | async "astro:server:done"() {
|
933 | await webcontainerFiles.serverDone();
|
934 | },
|
935 | async "astro:build:done"(astroBuildDoneOptions) {
|
936 | const projectRoot2 = fileURLToPath4(_config.root);
|
937 | await webcontainerFiles.buildAssets(projectRoot2, astroBuildDoneOptions);
|
938 | }
|
939 | }
|
940 | };
|
941 | }
|
942 | export {
|
943 | createPlugin as default
|
944 | };
|