UNPKG

30.2 kBJavaScriptView Raw
1// src/index.ts
2import { fileURLToPath as fileURLToPath4 } from "node:url";
3
4// src/integrations.ts
5import { createRequire } from "node:module";
6import mdx from "@astrojs/mdx";
7import react from "@astrojs/react";
8import { pluginCollapsibleSections } from "@expressive-code/plugin-collapsible-sections";
9import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers";
10import expressiveCode from "astro-expressive-code";
11import UnoCSS from "unocss/astro";
12import { getInlineContentForPackage } from "@tutorialkit/theme";
13function 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// src/remark/index.ts
65import path2 from "node:path";
66import remarkDirective from "remark-directive";
67
68// src/remark/callouts.ts
69import { h } from "hastscript";
70import {
71 directiveToMarkdown
72} from "mdast-util-directive";
73import { toMarkdown } from "mdast-util-to-markdown";
74import { visit } from "unist-util-visit";
75var 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};
97var variants = /* @__PURE__ */ new Set(["tip", "info", "warn", "danger", "success"]);
98function isNodeDirective(node) {
99 return node.type === "textDirective" || node.type === "leafDirective" || node.type === "containerDirective";
100}
101function isContainerDirective(node) {
102 return node.type === "containerDirective";
103}
104function 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}
118function 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}
161function 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// src/remark/import-file.ts
199import frontMatter from "front-matter";
200import * as kleur from "kleur/colors";
201import fs from "node:fs";
202import path from "node:path";
203import { visit as visit2 } from "unist-util-visit";
204function 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}
238function 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}
248function 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}
261function printWarning(message) {
262 const time = (/* @__PURE__ */ new Date()).toTimeString().slice(0, 8);
263 console.warn(kleur.yellow(`${kleur.bold(time)} [WARN] [remarkImportFilePlugin] ${message}`));
264}
265
266// src/remark/index.ts
267function 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// src/vite-plugins/core.ts
280import path3 from "node:path";
281import { fileURLToPath } from "node:url";
282
283// src/utils.ts
284function 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}
297function normalizeImportPath(importPath) {
298 return importPath.replaceAll("\\", "/");
299}
300
301// src/vite-plugins/core.ts
302var __dirname = path3.dirname(fileURLToPath(import.meta.url));
303var virtualModuleId = "tutorialkit:core";
304var resolvedVirtualModuleId = `${virtualModuleId}`;
305var 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// src/vite-plugins/css.ts
325import { watch } from "chokidar";
326import fs2 from "fs/promises";
327import path4 from "path";
328var CUSTOM_PATHS = ["theme.css", "theme/index.css"];
329var virtualModuleId2 = "@tutorialkit/custom.css";
330var resolvedVirtualModuleId2 = `/${virtualModuleId2}`;
331var projectRoot = process.cwd();
332var 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};
355function 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}
376async 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}
388function 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// src/vite-plugins/store.ts
414import path5 from "node:path";
415import { fileURLToPath as fileURLToPath2 } from "node:url";
416var __dirname2 = path5.dirname(fileURLToPath2(import.meta.url));
417var virtualModuleId3 = "tutorialkit:store";
418var resolvedVirtualModuleId3 = `${virtualModuleId3}`;
419var 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// src/vite-plugins/override-components.ts
440var virtualModuleId4 = "tutorialkit:override-components";
441var resolvedId = `\0${virtualModuleId4}`;
442function 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}
462function resolveDefaultTopBar(defaultRoutes) {
463 if (defaultRoutes) {
464 return "@tutorialkit/astro/default/components/TopBar.astro";
465 }
466 return "./src/components/TopBar.astro";
467}
468
469// src/webcontainer-files/index.ts
470import { watch as watch2 } from "chokidar";
471import { dim as dim2 } from "kleur/colors";
472import fs4 from "node:fs";
473import path8 from "node:path";
474import { fileURLToPath as fileURLToPath3 } from "node:url";
475
476// src/webcontainer-files/cache.ts
477import { dim } from "kleur/colors";
478import path7 from "node:path";
479
480// src/webcontainer-files/constants.ts
481var IGNORED_FILES = ["**/.DS_Store", "**/*.swp", "**/node_modules/**"];
482var EXTEND_CONFIG_FILEPATH = "/.tk-config.json";
483var FILES_FOLDER_NAME = "_files";
484var SOLUTION_FOLDER_NAME = "_solution";
485
486// src/webcontainer-files/filesmap.ts
487import glob from "fast-glob";
488import fs3 from "node:fs";
489import path6 from "node:path";
490import { z } from "zod";
491var configSchema = z.object({
492 extends: z.string()
493}).strict();
494var 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};
519var FilesMap = class _FilesMap {
520 /**
521 * Construct a new FileMap. To connect it to its graph, call
522 * `FileMap.initGraph` or `init(graph)`.
523 *
524 * @param path resolved path of this file map.
525 */
526 constructor(path9) {
527 this.path = path9;
528 }
529 /**
530 * Initialize the graph of nodes by connecting them to one another.
531 * The graph is expected to be acyclic but this function won't throw any
532 * exception if it isn't.
533 *
534 * Instead when a `FilesMap` is turned into a JSON file, a warning will be
535 * printed if a cycle is found.
536 *
537 * @param folders initial folders found after a lookup.
538 */
539 static async initGraph(folders, logger) {
540 const nodes = /* @__PURE__ */ 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 = /* @__PURE__ */ 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.
571Error: ${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};
629async 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}
639function 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// src/webcontainer-files/utils.ts
648import { folderPathToFilesRef } from "@tutorialkit/types";
649import glob2 from "fast-glob";
650function 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}
658function 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// src/webcontainer-files/cache.ts
670var 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 // map of filesRef to file content
686 _cache = /* @__PURE__ */ new Map();
687 // files that will be generated
688 _requestsQueue = /* @__PURE__ */ new Set();
689 // a promise to wait on before serving a request for the end user
690 _readiness;
691 _resolve;
692 // this is to know which FileMaps are in use to decide whether or not the page should be reloaded
693 _hotPaths = /* @__PURE__ */ 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};
775function 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// src/webcontainer-files/index.ts
799var 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 // TODO: does this work on Windows?
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// src/index.ts
861function 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}
942export {
943 createPlugin as default
944};