@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.
- package/dist/server.js +524 -0
- package/dist/server.js.map +1 -0
- package/package.json +3 -2
- package/.github/workflows/release.yml +0 -47
- package/semantic-release.config.js +0 -8
- package/src/controllers/FrontController.ts +0 -95
- package/src/controllers/MapController.ts +0 -104
- package/src/controllers/UploaderController.ts +0 -333
- package/src/getCoreRoot.ts +0 -40
- package/src/views/index.html +0 -169
- package/src/views/step1-git.html +0 -154
- package/src/views/step2-hosting.html +0 -153
- package/src/views/step3-steps-selfhosted.html +0 -502
- package/src/views/step3-steps.html +0 -549
- package/src/views/step4-validated-selfhosted.html +0 -188
- package/src/views/step4-validated.html +0 -80
- package/tsconfig.json +0 -35
- package/vite.config.ts +0 -53
- /package/{public → dist}/assets/index.js +0 -0
- /package/{public → dist}/images/badumtss.svg +0 -0
- /package/{public → dist}/images/brand-discord.svg +0 -0
- /package/{public → dist}/images/brand-github.svg +0 -0
- /package/{public → dist}/images/brand-linkedin.svg +0 -0
- /package/{public → dist}/images/brand-x.svg +0 -0
- /package/{public → dist}/images/brand-youtube.svg +0 -0
- /package/{public → dist}/images/favicon.svg +0 -0
- /package/{public → dist}/images/logo.svg +0 -0
- /package/{public → dist}/images/unknown-room-image copy.png +0 -0
- /package/{public → dist}/images/unknown-room-image.png +0 -0
- /package/{public → dist}/styles/styles.css +0 -0
- /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,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
|
-
}
|
package/src/getCoreRoot.ts
DELETED
|
@@ -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
|
-
}
|