@workadventure/map-starter-kit-core 0.0.1 → 1.0.2

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 (31) hide show
  1. package/dist/server.js +524 -0
  2. package/dist/server.js.map +1 -0
  3. package/package.json +3 -2
  4. package/.github/workflows/release.yml +0 -47
  5. package/semantic-release.config.js +0 -8
  6. package/src/controllers/FrontController.ts +0 -95
  7. package/src/controllers/MapController.ts +0 -104
  8. package/src/controllers/UploaderController.ts +0 -333
  9. package/src/getCoreRoot.ts +0 -40
  10. package/src/views/index.html +0 -169
  11. package/src/views/step1-git.html +0 -154
  12. package/src/views/step2-hosting.html +0 -153
  13. package/src/views/step3-steps-selfhosted.html +0 -502
  14. package/src/views/step3-steps.html +0 -549
  15. package/src/views/step4-validated-selfhosted.html +0 -188
  16. package/src/views/step4-validated.html +0 -80
  17. package/tsconfig.json +0 -35
  18. package/vite.config.ts +0 -53
  19. /package/{public → dist}/assets/index.js +0 -0
  20. /package/{public → dist}/images/badumtss.svg +0 -0
  21. /package/{public → dist}/images/brand-discord.svg +0 -0
  22. /package/{public → dist}/images/brand-github.svg +0 -0
  23. /package/{public → dist}/images/brand-linkedin.svg +0 -0
  24. /package/{public → dist}/images/brand-x.svg +0 -0
  25. /package/{public → dist}/images/brand-youtube.svg +0 -0
  26. /package/{public → dist}/images/favicon.svg +0 -0
  27. /package/{public → dist}/images/logo.svg +0 -0
  28. /package/{public → dist}/images/unknown-room-image copy.png +0 -0
  29. /package/{public → dist}/images/unknown-room-image.png +0 -0
  30. /package/{public → dist}/styles/styles.css +0 -0
  31. /package/{public → dist}/styles/styles.css.map +0 -0
@@ -1,47 +0,0 @@
1
- name: Release
2
-
3
- on:
4
- push:
5
- branches:
6
- - main
7
- - master
8
-
9
- permissions:
10
- contents: read
11
-
12
- jobs:
13
- release:
14
- name: Release
15
- runs-on: ubuntu-latest
16
- permissions:
17
- contents: write # to be able to publish a GitHub release
18
- issues: write # to be able to comment on released issues
19
- pull-requests: write # to be able to comment on released pull requests
20
- id-token: write # for npm trusted publishing (OIDC) and provenance
21
- steps:
22
- - name: Checkout
23
- uses: actions/checkout@v4
24
- with:
25
- fetch-depth: 0
26
-
27
- - name: Setup Node.js
28
- uses: actions/setup-node@v4
29
- with:
30
- node-version: 20
31
-
32
- - name: Install dependencies
33
- run: npm install
34
-
35
- - name: Verify the integrity of provenance attestations and registry signatures
36
- run: npm audit signatures
37
-
38
- - name: Build JS File
39
- run: npm run build
40
-
41
- - name: Release
42
- env:
43
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44
- # npm: classic tokens are revoked (Dec 2025). Use Trusted Publishing (OIDC, no token)
45
- # on npmjs.com, or a granular access token in NPM_TOKEN (npm token create / npmjs.com)
46
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
47
- run: npx semantic-release
@@ -1,8 +0,0 @@
1
- export const branches = ["master"];
2
- export const plugins = [
3
- "@semantic-release/commit-analyzer",
4
- "@semantic-release/release-notes-generator",
5
- "@semantic-release/npm",
6
- "@semantic-release/git",
7
- "@semantic-release/github",
8
- ];
@@ -1,95 +0,0 @@
1
- import express from 'express';
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import Mustache from 'mustache';
5
- import { getCoreRoot } from '../getCoreRoot.js';
6
-
7
- export class FrontController {
8
- private app: express.Application;
9
-
10
- constructor(app: express.Application) {
11
- this.app = app;
12
- // Assets are now configured in app.ts
13
- this.setupRoutes();
14
- this.setupRoutesStep1();
15
- this.setupRoutesStep2();
16
- this.setupRoutesStep3();
17
- this.setupRoutesStep4();
18
- }
19
-
20
- private setupRoutes() {
21
- // Route for the Mustache renderer on "/"
22
- this.app.get('/', async (_, res) => {
23
- try {
24
- res.send(await this.renderTemplate('index'));
25
- } catch (error) {
26
- console.error('Error rendering template:', error);
27
- res.status(500).send('Error rendering template');
28
- }
29
- });
30
- }
31
-
32
- /**
33
- * Render a template file
34
- * @param filename - The filename of the template to render
35
- * @returns The rendered template
36
- */
37
- private async renderTemplate(filename: string): Promise<string> {
38
- const coreRoot = getCoreRoot();
39
- const templatesDir = path.join(coreRoot, 'src/views');
40
- if(!fs.existsSync(templatesDir)) {
41
- throw new Error(`Templates directory not found: ${templatesDir}`);
42
- }
43
- const templatePath = path.join(templatesDir, `${filename}.html`);
44
- const template = await fs.promises.readFile(templatePath, 'utf-8');
45
- // Render the template with Mustache (without data for now)
46
- return Mustache.render(template, {});
47
- }
48
-
49
- /**
50
- * Setup the routes for file "step1-git.html"
51
- * @returns void
52
- */
53
- private setupRoutesStep1() {
54
- this.app.get('/step1-git', async (_, res) => {
55
- res.send(await this.renderTemplate('step1-git'));
56
- });
57
- }
58
-
59
- /**
60
- * Setup the routes for file "step2-hosting.html"
61
- * @returns void
62
- */
63
- private setupRoutesStep2() {
64
- this.app.get('/step2-hosting', async (_, res) => {
65
- res.send(await this.renderTemplate('step2-hosting'));
66
- });
67
- }
68
-
69
- /**
70
- * Setup the routes for file "step3-steps.html"
71
- * @returns void
72
- */
73
- private setupRoutesStep3() {
74
- this.app.get('/step3-steps', async (_, res) => {
75
- res.send(await this.renderTemplate('step3-steps'));
76
- });
77
- this.app.get('/step3-steps-selfhosted', async (_, res) => {
78
- res.send(await this.renderTemplate('step3-steps-selfhosted'));
79
- });
80
- }
81
-
82
- /**
83
- * Setup the routes for file "step4-map.html"
84
- * @returns void
85
- */
86
- private setupRoutesStep4() {
87
- this.app.get('/step4-validated', async (_, res) => {
88
- res.send(await this.renderTemplate('step4-validated'));
89
- });
90
- this.app.get('/step4-validated-selfhosted', async (_, res) => {
91
- res.send(await this.renderTemplate('step4-validated-selfhosted'));
92
- });
93
- }
94
-
95
- }
@@ -1,104 +0,0 @@
1
- import express from 'express';
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
-
5
- export class MapController {
6
- private router: express.Router;
7
- private app: express.Application;
8
-
9
- constructor(app: express.Application) {
10
- this.app = app;
11
- this.router = express.Router();
12
- this.setupRoutes();
13
- // Register the router on the application with the "/maps" prefix
14
- this.app.use('/maps', this.router);
15
- }
16
-
17
- /**
18
- * Setup the routes for the map controller
19
- * @returns void
20
- */
21
- private setupRoutes() {
22
- // Route to retrieve the list of maps with their properties
23
- this.router.get('/list', async (_, res) => {
24
- try {
25
- const mapsDir = './';
26
- const maps = await this.getMapsWithProperties(mapsDir);
27
- res.json(maps);
28
- } catch (error) {
29
- console.error('Error getting maps list:', error);
30
- res.status(500).json({ error: 'Error getting maps list' });
31
- }
32
- });
33
- }
34
-
35
- /**
36
- * Get the list of maps with their properties
37
- * @param dir - The directory to search for maps
38
- * @param baseDir - The base directory to use for relative paths
39
- * @returns The list of maps with their properties
40
- */
41
- private async getMapsWithProperties(dir: string, baseDir: string = dir): Promise<any[]> {
42
- let files = await fs.promises.readdir(dir, { withFileTypes: true });
43
- const maps: any[] = [];
44
-
45
- for (const file of files) {
46
- const fullPath = path.join(dir, file.name);
47
-
48
- // Exclude the "dist" folder
49
- if(file.name === 'dist') continue;
50
-
51
- if (file.isDirectory()) {
52
- // Recursively search subdirectories
53
- const subMaps = await this.getMapsWithProperties(fullPath, baseDir);
54
- maps.push(...subMaps);
55
- } else if (file.name.endsWith('.tmj')) {
56
- try {
57
- // Read and parse TMJ file
58
- const tmjContent = await fs.promises.readFile(fullPath, 'utf-8');
59
- const tmjData = JSON.parse(tmjContent);
60
-
61
- // Extract properties
62
- const properties = tmjData.properties || [];
63
- const findProperty = (key: string) => {
64
- const item = properties.find((p: any) => p.name === key);
65
- return item ? item.value : null;
66
- };
67
-
68
- // Get file stats for size and date
69
- const stats = await fs.promises.stat(fullPath);
70
- const fileSizeInMB = (stats.size / (1024 * 1024)).toFixed(2);
71
- const lastModified = stats.mtime;
72
-
73
- // Get relative path
74
- const relativePath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
75
-
76
- // Extract filename without extension
77
- const filename = path.basename(file.name, '.tmj');
78
-
79
- maps.push({
80
- path: relativePath,
81
- filename: filename,
82
- mapName: findProperty('mapName') || filename,
83
- mapImage: findProperty('mapImage') || null,
84
- mapDescription: findProperty('mapDescription') || '',
85
- mapCopyright: findProperty('mapCopyright') || '',
86
- size: fileSizeInMB,
87
- lastModified: lastModified.toISOString(),
88
- lastModifiedFormatted: lastModified.toLocaleDateString('fr-FR', {
89
- day: '2-digit',
90
- month: '2-digit',
91
- year: 'numeric',
92
- hour: '2-digit',
93
- minute: '2-digit'
94
- })
95
- });
96
- } catch (error) {
97
- console.error(`Error reading TMJ file ${fullPath}:`, error);
98
- }
99
- }
100
- }
101
-
102
- return maps;
103
- }
104
- }
@@ -1,333 +0,0 @@
1
- import express from 'express';
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import { exec } from "node:child_process";
5
- import { promisify } from "node:util";
6
-
7
- const execAsync = promisify(exec);
8
-
9
- /** Metadata returned by map-storage for each map (WAM file). */
10
- export interface MapStorageMetadata {
11
- copyright?: string;
12
- description?: string;
13
- name?: string;
14
- thumbnail?: string;
15
- }
16
-
17
- /** Single map entry returned by map-storage GET /maps/ (value in the map). */
18
- export interface MapStorageEntry {
19
- mapUrl: string;
20
- wamFileUrl?: string;
21
- metadata?: MapStorageMetadata;
22
- }
23
-
24
- /** Response shape: Map<wamfile, { mapUrl, metadata }>. In JSON this is a plain object. */
25
- export interface MapStorageListResponse {
26
- [wamFile: string]: MapStorageEntry;
27
- }
28
-
29
- /** Hydrated map item returned by our API to the frontend. */
30
- export interface MapListItem {
31
- wamFileUrl: string;
32
- filename: string;
33
- mapName: string | null;
34
- mapDescription?: string;
35
- mapCopyright?: string;
36
- mapImage: string | null;
37
- mapUrl: string;
38
- }
39
-
40
- /**
41
- * Converts the map-storage response (Map<wamfile, entry>) into a list of MapListItem.
42
- */
43
- export function hydrateMapStorageList(
44
- raw: MapStorageListResponse,
45
- mapStorageBaseUrl: string
46
- ): MapListItem[] {
47
- const list: MapListItem[] = [];
48
- const baseUrl = mapStorageBaseUrl.replace(/\/$/, '');
49
-
50
- for (const [wamFile, entry] of Object.entries(raw)) {
51
- if (!entry || typeof entry !== 'object') continue;
52
-
53
- const rawWamFileUrl = entry.wamFileUrl ?? wamFile;
54
- const wamFileUrlNormalized = rawWamFileUrl.startsWith('http')
55
- ? new URL(rawWamFileUrl).pathname.replace(/^\//, '')
56
- : rawWamFileUrl.replace(/^\//, '');
57
-
58
- const filename = wamFileUrlNormalized.split('/').pop() ?? wamFileUrlNormalized;
59
- const nameFromFile = filename.replace(/\.(tmj|wam)$/i, '');
60
-
61
- const meta = entry.metadata ?? {};
62
- const mapUrl = entry.mapUrl ?? (entry as unknown as { mapUrl?: string }).mapUrl ?? '';
63
-
64
- let mapImage: string | null = null;
65
- if (meta.thumbnail) {
66
- if (meta.thumbnail.startsWith('http')) {
67
- mapImage = meta.thumbnail;
68
- } else {
69
- const thumbFile = meta.thumbnail.replace(/^\//, '');
70
- const wamDir = wamFileUrlNormalized.includes('/')
71
- ? wamFileUrlNormalized.replace(/\/[^/]*$/, '')
72
- : '';
73
- const thumbPath = wamDir ? `${wamDir}/${thumbFile}` : thumbFile;
74
- mapImage = `${baseUrl}/${thumbPath}`;
75
- }
76
- }
77
-
78
- list.push({
79
- wamFileUrl: wamFileUrlNormalized,
80
- filename,
81
- mapName: meta.name ?? nameFromFile,
82
- mapDescription: meta.description,
83
- mapCopyright: meta.copyright,
84
- mapImage,
85
- mapUrl: mapUrl.startsWith('http') ? mapUrl : `${baseUrl}/${mapUrl.replace(/^\//, '')}`,
86
- });
87
- }
88
-
89
- return list;
90
- }
91
-
92
- export class UploaderController {
93
- private router: express.Router;
94
- private app: express.Application;
95
-
96
- constructor(app: express.Application) {
97
- this.app = app;
98
- this.router = express.Router();
99
- this.setupMiddleware();
100
- this.setupRoutes();
101
- // Register the router on the application with the "/uploader" prefix
102
- this.app.use('/uploader', this.router);
103
- }
104
-
105
- private setupMiddleware() {
106
- // Middleware to parse JSON bodies
107
- this.router.use(express.json());
108
- }
109
-
110
- private setupRoutes() {
111
- // Route to configure MAP_STORAGE mode
112
- this.router.post('/configure', async (req, res) => {
113
- try {
114
- const { mapStorageUrl, mapStorageApiKey, uploadDirectory } = req.body;
115
-
116
- // Validate required fields
117
- if (!mapStorageUrl || !mapStorageApiKey || !uploadDirectory) {
118
- return res.status(400).json({
119
- error: 'Missing required fields',
120
- required: ['mapStorageUrl', 'mapStorageApiKey', 'uploadDirectory']
121
- });
122
- }
123
-
124
- // Create or update .env.secret file
125
- await this.createEnvSecretFile({
126
- mapStorageUrl,
127
- mapStorageApiKey,
128
- uploadDirectory
129
- });
130
-
131
- return res.json({
132
- success: true,
133
- message: 'Configuration updated successfully'
134
- });
135
- } catch (error) {
136
- console.error('Error configuring uploader:', error);
137
- return res.status(500).json({
138
- error: 'Error configuring uploader',
139
- message: error instanceof Error ? error.message : 'Unknown error'
140
- });
141
- }
142
- });
143
-
144
- // Route to get current configuration status
145
- this.router.get('/status', async (_, res) => {
146
- try {
147
- const envSecretPath = path.join(process.cwd(), 'src/.env.secret');
148
-
149
- const hasSecretFile = await fs.promises.access(envSecretPath)
150
- .then(() => true)
151
- .catch(() => false);
152
-
153
- let secretConfig: {
154
- mapStorageUrl: string | null;
155
- mapStorageApiKey: string | null;
156
- uploadDirectory: string | null;
157
- } | null = null;
158
- if (hasSecretFile) {
159
- const secretContent = await fs.promises.readFile(envSecretPath, 'utf-8');
160
- const mapStorageUrl = secretContent.match(/MAP_STORAGE_URL=(.+)/)?.[1]?.trim();
161
- const mapStorageApiKey = secretContent.match(/MAP_STORAGE_API_KEY=(.+)/)?.[1]?.trim();
162
- const uploadDirectory = secretContent.match(/UPLOAD_DIRECTORY=(.+)/)?.[1]?.trim();
163
-
164
- secretConfig = {
165
- mapStorageUrl: mapStorageUrl || null,
166
- mapStorageApiKey: mapStorageApiKey || null, // Hide the actual key
167
- uploadDirectory: uploadDirectory || null
168
- };
169
- }
170
-
171
- res.json({
172
- hasSecretFile,
173
- secretConfig
174
- });
175
- } catch (error) {
176
- console.error('Error getting uploader status:', error);
177
- res.status(500).json({
178
- error: 'Error getting uploader status',
179
- message: error instanceof Error ? error.message : 'Unknown error'
180
- });
181
- }
182
- });
183
-
184
- // Route to get list of maps from map-storage (for self-hosted step4)
185
- this.router.get('/maps-storage-list', async (_, res) => {
186
- try {
187
- const envSecretPath = path.join(process.cwd(), '.env.secret');
188
- const hasSecretFile = await fs.promises.access(envSecretPath)
189
- .then(() => true)
190
- .catch(() => false);
191
-
192
- if (!hasSecretFile) {
193
- return res.status(400).json({
194
- error: 'Configuration not found',
195
- message: 'Please configure the upload settings first.'
196
- });
197
- }
198
-
199
- const secretContent = await fs.promises.readFile(envSecretPath, 'utf-8');
200
- const mapStorageUrl = secretContent.match(/MAP_STORAGE_URL=(.+)/)?.[1]?.trim();
201
- const mapStorageApiKey = secretContent.match(/MAP_STORAGE_API_KEY=(.+)/)?.[1]?.trim();
202
-
203
- if (!mapStorageUrl || !mapStorageApiKey) {
204
- return res.status(400).json({
205
- error: 'Missing map-storage configuration',
206
- message: 'MAP_STORAGE_URL and MAP_STORAGE_API_KEY must be set in .env.secret.'
207
- });
208
- }
209
-
210
- const baseUrl = mapStorageUrl.replace(/\/$/, '');
211
- const listUrl = `${baseUrl}/maps/`;
212
-
213
- const response = await fetch(listUrl, {
214
- method: 'GET',
215
- headers: {
216
- 'Authorization': `Bearer ${mapStorageApiKey}`,
217
- 'Content-Type': 'application/json'
218
- }
219
- });
220
-
221
- if (!response.ok) {
222
- const text = await response.text();
223
- console.error('Map-storage list error:', response.status, text);
224
- return res.status(response.status).json({
225
- error: 'Map-storage request failed',
226
- message: response.status === 401
227
- ? 'Invalid API key. Check MAP_STORAGE_API_KEY in your .env.secret.'
228
- : `Map-storage returned ${response.status}: ${text.slice(0, 200)}`
229
- });
230
- }
231
-
232
- const data = await response.json();
233
- // Map-storage returns Map<wamfile, { mapUrl, metadata }> (as a plain object in JSON)
234
- const raw = typeof data === 'object' && data !== null ? data : {};
235
- const rawMap: MapStorageListResponse = !Array.isArray(raw) && typeof raw.maps === 'object' && raw.maps !== null
236
- ? (raw.maps as MapStorageListResponse)
237
- : !Array.isArray(raw)
238
- ? (raw as MapStorageListResponse)
239
- : {};
240
- const maps = hydrateMapStorageList(rawMap, baseUrl);
241
- const playBaseUrl = secretContent.match(/PLAY_BASE_URL=(.+)/)?.[1]?.trim() || null;
242
- return res.json({ maps, mapStorageUrl: baseUrl, playBaseUrl });
243
- } catch (error) {
244
- console.error('Error fetching maps from map-storage:', error);
245
- return res.status(500).json({
246
- error: 'Error fetching maps list',
247
- message: error instanceof Error ? error.message : 'Unknown error'
248
- });
249
- }
250
- });
251
-
252
- // Route to upload map
253
- this.router.post('/upload', async (_, res) => {
254
- try {
255
- // Verify that configuration exists
256
- const envSecretPath = path.join(process.cwd(), '.env.secret');
257
-
258
- // Check if .env.secret exists
259
- const hasSecretFile = await fs.promises.access(envSecretPath)
260
- .then(() => true)
261
- .catch(() => false);
262
-
263
- if (!hasSecretFile) {
264
- return res.status(400).json({
265
- error: 'Configuration not found',
266
- message: 'Please configure the upload settings first using /uploader/configure'
267
- });
268
- }
269
-
270
- // Execute upload
271
- await this.runUpload();
272
-
273
- return res.json({
274
- success: true,
275
- message: 'Map uploaded successfully'
276
- });
277
- } catch (error) {
278
- console.error('Error uploading map:', error);
279
- return res.status(500).json({
280
- error: 'Error uploading map',
281
- message: error instanceof Error ? error.message : 'Unknown error'
282
- });
283
- }
284
- });
285
- }
286
-
287
- private async runUpload(): Promise<void> {
288
- const projectRoot = process.cwd();
289
-
290
- try {
291
- // Execute npm run upload-only
292
- // The command will read environment variables from .env and .env.secret
293
- const { stdout, stderr } = await execAsync('npm run upload', {
294
- cwd: projectRoot,
295
- env: {
296
- ...process.env,
297
- // Ensure we're using the current process environment
298
- NODE_ENV: process.env.NODE_ENV || 'development'
299
- },
300
- maxBuffer: 10 * 1024 * 1024 // 10MB buffer for output
301
- });
302
-
303
- if (stderr && !stderr.includes('warning')) {
304
- console.warn('Upload stderr:', stderr);
305
- }
306
-
307
- console.info('Upload stdout:', stdout);
308
- console.info('Upload completed successfully');
309
- } catch (error) {
310
- console.error('Error executing upload-only:', error);
311
- throw error;
312
- }
313
- }
314
-
315
- private async createEnvSecretFile(config: {
316
- mapStorageUrl: string;
317
- mapStorageApiKey: string;
318
- uploadDirectory: string;
319
- }): Promise<void> {
320
- const envSecretPath = path.join(process.cwd(), '.env.secret');
321
-
322
- // Create the .env.secret file with the provided configuration
323
- const secretContent = `# Secret configuration file for MAP_STORAGE upload mode
324
- # This file is not committed to git (see .gitignore)
325
-
326
- MAP_STORAGE_URL=${config.mapStorageUrl}
327
- MAP_STORAGE_API_KEY=${config.mapStorageApiKey}
328
- UPLOAD_DIRECTORY=${config.uploadDirectory}
329
- `;
330
-
331
- await fs.promises.writeFile(envSecretPath, secretContent, 'utf-8');
332
- }
333
- }
@@ -1,40 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { fileURLToPath } from "node:url";
4
-
5
- let coreRoot: string | null = null;
6
-
7
- /**
8
- * Returns the root directory of the "core" package (app + templates + public).
9
- * - When running from the project root: returns process.cwd().
10
- * - When running from packages/map-starter-kit-core or node_modules: returns the package directory.
11
- * Allows updating the core package without touching user files (maps, .env, tilesets).
12
- */
13
- export function getCoreRoot(): string {
14
- if (coreRoot !== null) {
15
- return coreRoot;
16
- }
17
- try {
18
- const dir = path.dirname(fileURLToPath(import.meta.url));
19
- const candidate = path.dirname(dir);
20
- const packagePath = path.join(candidate, "package.json");
21
- if (fs.existsSync(packagePath)) {
22
- const pkg = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
23
- if (pkg.name === "@workadventure/map-starter-kit-core") {
24
- coreRoot = candidate;
25
- return coreRoot;
26
- }
27
- }
28
- } catch {
29
- // ignore
30
- }
31
- coreRoot = process.cwd();
32
- return coreRoot;
33
- }
34
-
35
- /**
36
- * Override the core root (e.g. for tests or custom layout).
37
- */
38
- export function setCoreRoot(root: string): void {
39
- coreRoot = root;
40
- }