Skip to content

Commit 922fe57

Browse files
committed
#1 Implementing the server on this repo to be able to serve the built app in 'standalone' mode.
1 parent 3c16978 commit 922fe57

File tree

7 files changed

+176
-11
lines changed

7 files changed

+176
-11
lines changed

README.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,35 @@
11
# Yehyecoa
22

3-
> **[yeh-yeh-CO-ah](https://nahuatl.wired-humanities.org/content/yeyecoa)** means to try something; to try or experiment with something; to rehearse;
3+
> **[yeh-yeh-CO-ah](https://nahuatl.wired-humanities.org/content/yeyecoa)** in Nahuatl means to try something; to try or experiment with something; to rehearse;
44
5-
This is my own spin on the [Vue Playground](https://github.com/vuejs/core/blob/main/packages-private/sfc-playground/README.md). Originally intended to be used within a [TutorialKit](https://tutorialkit.dev/) preview window.
5+
This is our own spin on the [Vue Playground](https://github.com/vuejs/core/blob/main/packages-private/sfc-playground/README.md). Originally intended to be used within a [TutorialKit](https://tutorialkit.dev/) (_tk_ in this readme to save some keystrokes) preview window.
66

77
Most notably, it doesn't allow for switching Vue versions nor it displays the compiled code. Since this will be focused (at least at first) on the basic concepts, we don't want all of that.
88

99
![screenshot](docs/screenshot.png)
1010

11-
TODO: Update reference to TutorialKit project.
11+
## Usage
12+
13+
### As part of [amoxtli-vue](https://github.com/ackzell/amoxtli-vue)
14+
15+
This is the playground that will load into a preview pane in the `amoxtli-vue` project.
16+
It reads the different [code files in the lessons](https://tutorialkit.dev/guides/creating-content/#a-lesson-code) provided from that repo and loads them into the monaco editor to interact with them.
17+
18+
![screenshot](docs/amoxtli-integration.png)
19+
20+
#### Installing it in amoxtli-vue
21+
22+
An npm task is available for when you want to see your changes embedded into the tk project:
23+
24+
```bash
25+
# build and install on tk project
26+
npm bi
27+
```
28+
This will first build the project and then copy the required contents from the `/dist` folder into the `amoxtli-vue/src/templates/yehyecoa/` folder.
29+
30+
### Standalone
31+
> [!Note]
32+
> Take a look at `amoxtli-vue/src/templates/yehyecoa/server.js` as it contains logic to _notify_ `yehyecoa-vue` when the contents of the file have been updated. This is if you want to dynamically update the contents of the editor (and thus perfect for embedding into a tk webcontainer).
1233
1334
# Original Readme:
1435

docs/amoxtli-integration.png

983 KB
Loading

eslint.config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { globalIgnores } from 'eslint/config'
1+
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
22
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
33
import pluginVue from 'eslint-plugin-vue'
4-
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
4+
import { globalIgnores } from 'eslint/config'
55

66
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
77
// import { configureVueProject } from '@vue/eslint-config-typescript'
@@ -14,7 +14,7 @@ export default defineConfigWithVueTs(
1414
files: ['**/*.{ts,mts,tsx,vue}'],
1515
},
1616

17-
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
17+
globalIgnores(['**/public/**', '**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
1818

1919
pluginVue.configs['flat/essential'],
2020
vueTsConfigs.recommended,

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@
88
},
99
"scripts": {
1010
"dev": "vite",
11-
"build": "run-p type-check \"build-only {@}\" --",
11+
"build": "run-p type-check \"build-only {@}\" -- && npm run build-server",
1212
"preview": "vite preview",
1313
"build-only": "vite build",
1414
"type-check": "vue-tsc --build",
1515
"lint": "eslint . --fix",
1616
"format": "prettier --write src/",
17-
"install-in-amoxtli-vue": "cp -r dist/assets ../amoxtli-vue/src/templates/yehyecoa/ && cp dist/index.html ../amoxtli-vue/src/templates/yehyecoa/",
18-
"bi": "npm run build && npm run install-in-amoxtli-vue"
17+
"install-in-amoxtli-vue": "cp -r dist ../amoxtli-vue/src/templates/yehyecoa/",
18+
"bi": "npm run build && npm run install-in-amoxtli-vue",
19+
"build-server": "cp server.js dist/server.js",
20+
"serve": "node dist/server.js"
1921
},
2022
"dependencies": {
2123
"@vue/repl": "^4.6.3",

public/lessonFile.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script setup>
2+
3+
// this file will be replaced by amoxtli-vue on each lesson
4+
</script>
5+
6+
<template>
7+
8+
</template>

server.js

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import http from 'node:http';
2+
import fs from 'node:fs';
3+
import path from 'node:path';
4+
import { fileURLToPath } from 'node:url';
5+
6+
const __filename = fileURLToPath(import.meta.url);
7+
const __dirname = path.dirname(__filename);
8+
9+
const PORT = process.env.PORT || 3000;
10+
const baseDir = __dirname;
11+
12+
// MIME types
13+
const mimeTypes = {
14+
'.html': 'text/html',
15+
'.js': 'application/javascript',
16+
'.css': 'text/css',
17+
'.json': 'application/json',
18+
'.ico': 'image/x-icon',
19+
'.png': 'image/png',
20+
'.jpg': 'image/jpeg',
21+
'.jpeg': 'image/jpeg',
22+
'.svg': 'image/svg+xml',
23+
'.txt': 'text/plain',
24+
'.wasm': 'application/wasm'
25+
};
26+
27+
// SSE clients
28+
const clients = [];
29+
30+
// Load index.html once at startup for instant serving
31+
const indexPath = path.join(baseDir, 'index.html');
32+
let indexHTML = '';
33+
try {
34+
indexHTML = fs.readFileSync(indexPath, 'utf-8');
35+
console.log('[startup] Loaded index.html into memory.');
36+
} catch (err) {
37+
console.error('[startup] Could not read index.html:', err);
38+
}
39+
40+
// SSE helpers
41+
function sendSSE(res, event, data) {
42+
res.write(`event: ${event}\n`);
43+
res.write(`data: ${JSON.stringify(data)}\n\n`);
44+
}
45+
46+
function broadcastFileChange(filename, content) {
47+
clients.forEach(client => {
48+
sendSSE(client, 'fileUpdate', { filename, content });
49+
});
50+
}
51+
52+
// Watch the root for changes to lessonFile.vue
53+
fs.watch(baseDir, { recursive: false }, (eventType, filename) => {
54+
if (filename === 'lessonFile.vue') {
55+
const filePath = path.join(baseDir, filename);
56+
fs.readFile(filePath, 'utf-8', (err, content) => {
57+
if (err) {
58+
console.error(`[watch] Failed to read ${filename}:`, err);
59+
return;
60+
}
61+
console.log(`[watch] ${filename} changed (${eventType})`);
62+
broadcastFileChange(filename, content);
63+
});
64+
}
65+
});
66+
67+
// HTTP server
68+
const server = http.createServer((req, res) => {
69+
// SSE endpoint
70+
if (req.url === '/events') {
71+
res.writeHead(200, {
72+
'Content-Type': 'text/event-stream',
73+
'Cache-Control': 'no-cache',
74+
'Connection': 'keep-alive',
75+
});
76+
res.write('\n');
77+
clients.push(res);
78+
79+
console.log('[SSE] Client connected');
80+
81+
req.on('close', () => {
82+
console.log('[SSE] Client disconnected');
83+
const idx = clients.indexOf(res);
84+
if (idx !== -1) clients.splice(idx, 1);
85+
});
86+
return;
87+
}
88+
89+
// Strip query string/hash
90+
const cleanUrl = req.url.split('?')[0].split('#')[0];
91+
let filePath = path.join(baseDir, cleanUrl);
92+
93+
// Serve preloaded index.html instantly for root
94+
if (cleanUrl === '/' || cleanUrl === '') {
95+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
96+
res.end(indexHTML);
97+
return;
98+
}
99+
100+
// Prevent directory traversal
101+
if (!filePath.startsWith(baseDir)) {
102+
res.writeHead(403);
103+
res.end('Forbidden');
104+
return;
105+
}
106+
107+
// Serve static files or SPA fallback
108+
fs.stat(filePath, (err, stats) => {
109+
if (err || !stats.isFile()) {
110+
// SPA fallback: serve cached index.html
111+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
112+
res.end(indexHTML);
113+
return;
114+
}
115+
116+
const ext = path.extname(filePath).toLowerCase();
117+
const contentType = mimeTypes[ext] || 'application/octet-stream';
118+
res.writeHead(200, { 'Content-Type': contentType });
119+
fs.createReadStream(filePath).pipe(res);
120+
});
121+
});
122+
123+
server.listen(PORT, () => {
124+
console.log(`Server running at http://localhost:${PORT}`);
125+
});

src/App.vue

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,19 @@ setVH()
2222
2323
const store = useStore()
2424
25+
const errormessage = ref('')
26+
2527
onMounted(async () => {
2628
// Initial load
27-
const fileContents = await fetch('/lessonFile.vue').then((r) => r.text())
28-
store.setFiles({ 'src/App.vue': fileContents })
29+
try {
30+
const fileContents = await fetch('/lessonFile.vue').then((r) => r.text())
31+
store.setFiles({ 'src/App.vue': fileContents })
32+
} catch {
33+
errormessage.value = 'Failed to load lessonFile.vue'
34+
console.error(
35+
'Failed to load `lessonFile.vue`, makes sure you created one in the root directory.',
36+
)
37+
}
2938
3039
// Live updates via SSE
3140
const es = new EventSource('/events')

0 commit comments

Comments
 (0)