Skip to content

Commit d73e6a8

Browse files
committed
uptd
1 parent 0ef3964 commit d73e6a8

File tree

10 files changed

+208
-22
lines changed

10 files changed

+208
-22
lines changed

assets/libs/db/sql-wasm.esm.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ if (typeof window !== 'undefined') {
55
await new Promise((resolve, reject) => {
66
const script = document.createElement('script');
77
// Load the UMD bundle which exposes initSqlJs on the window object
8-
script.src = new URL('./sql-wasm.js', import.meta.url).pathname;
8+
script.src = new URL('./sql-wasm.js', import.meta.url).toString();
99
script.onload = resolve;
1010
script.onerror = reject;
1111
document.head.appendChild(script);

src/adapters/ai/DetectionAdapter.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import loadScript from '../../utils/loadScript.js';
22

33
import NullEventBus from '../../core/events/NullEventBus.js';
44
import IDetection from '../../ports/IDetection.js';
5+
import { resolveAssetUrl } from '../../config/assetsBaseUrl.js';
56

67
export default class DetectionService extends IDetection {
78
constructor(logger, stateService = null, eventBus = new NullEventBus()) {
@@ -18,16 +19,16 @@ export default class DetectionService extends IDetection {
1819
}
1920

2021
async loadAssets() {
21-
await loadScript('/assets/libs/tf.min.js');
22-
await loadScript('/assets/libs/coco-ssd.min.js');
22+
await loadScript(resolveAssetUrl('libs/tf.min.js'));
23+
await loadScript(resolveAssetUrl('libs/coco-ssd.min.js'));
2324
}
2425

2526
async loadModel() {
2627
if (typeof window.cocoSsd === 'undefined') {
2728
this.logger.warn('cocoSsd global not found');
2829
return;
2930
}
30-
this.model = await cocoSsd.load({ modelUrl: '/assets/models/coco-ssd/model.json' });
31+
this.model = await cocoSsd.load({ modelUrl: resolveAssetUrl('models/coco-ssd/model.json') });
3132
this.logger.info('COCO-SSD model loaded from assets');
3233
}
3334

src/adapters/database/DatabaseAdapter.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import initSqlJs from '../../../assets/libs/db/sql-wasm.esm.js';
44

55
import IDatabase from '../../ports/IDatabase.js';
6+
import { resolveAssetUrl } from '../../config/assetsBaseUrl.js';
67

78
export default class DatabaseService extends IDatabase {
89
constructor(path = ':memory:', logger, initOverride, persistence = null) {
@@ -21,7 +22,7 @@ export default class DatabaseService extends IDatabase {
2122
const init = this._initSqlJs || initSqlJs;
2223
// When running in the browser we always load assets from the public path.
2324
// Node.js tests may override `_initSqlJs` to provide a custom initializer.
24-
const basePath = '/assets/libs/db/';
25+
const basePath = resolveAssetUrl('libs/db/');
2526
const SQL = await init({
2627
locateFile: file => `${basePath}${file}`,
2728
});

src/application/services/DualityConfigService.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
/**
22
* Loads duality configuration files for spirits.
33
*/
4+
import { AssetUrlMapper } from '../../config/assetsBaseUrl.js';
5+
46
export default class DualityConfigService {
57
/**
68
* @param {import('../../ports/IConfigLoader.js').default} configLoader
79
* @param {string} [basePath]
10+
* @param {AssetUrlMapper} [assetUrlMapper]
811
*/
9-
constructor(configLoader, basePath = 'src/config/spirits') {
12+
constructor(configLoader, basePath = 'src/config/spirits', assetUrlMapper = new AssetUrlMapper()) {
1013
if (!configLoader || typeof configLoader.load !== 'function') {
1114
throw new TypeError('DualityConfigService requires a config loader implementing IConfigLoader.');
1215
}
1316
this.configLoader = configLoader;
1417
this.basePath = basePath;
18+
this.assetUrlMapper = assetUrlMapper;
1519
}
1620

1721
/**
@@ -24,6 +28,6 @@ export default class DualityConfigService {
2428
if (!Array.isArray(json.stages)) {
2529
throw new Error('Stages array is required');
2630
}
27-
return json;
31+
return this.assetUrlMapper.mapConfig(json);
2832
}
2933
}

src/config/assetsBaseUrl.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
export class AssetsBaseUrlResolver {
2+
constructor({ documentRef = typeof document !== 'undefined' ? document : null, moduleUrl = import.meta.url } = {}) {
3+
this.documentRef = documentRef;
4+
this.moduleUrl = moduleUrl;
5+
this._cachedBaseUrl = null;
6+
}
7+
8+
getBaseUrl() {
9+
if (this._cachedBaseUrl) return this._cachedBaseUrl;
10+
const fromEmbed = this._resolveFromEmbedMarker();
11+
const fromModule = this._resolveFromModuleUrl();
12+
const fallback = '/assets/';
13+
this._cachedBaseUrl = this._normalizeBaseUrl(fromEmbed || fromModule || fallback);
14+
return this._cachedBaseUrl;
15+
}
16+
17+
resolve(assetPath = '') {
18+
if (!assetPath) return this.getBaseUrl();
19+
if (this._isAbsolute(assetPath)) return assetPath;
20+
const baseUrl = this.getBaseUrl();
21+
const normalizedBase = this._normalizeBaseUrl(baseUrl);
22+
const trimmedPath = this._stripAssetsPrefix(assetPath).replace(/^\/+/, '');
23+
return `${normalizedBase}${trimmedPath}`;
24+
}
25+
26+
_resolveFromEmbedMarker() {
27+
if (!this.documentRef) return null;
28+
const candidates = [];
29+
if (this.documentRef.currentScript) {
30+
candidates.push(this.documentRef.currentScript);
31+
}
32+
if (this.documentRef.querySelector) {
33+
const explicit = this.documentRef.querySelector('[data-interdead-assets-base]');
34+
if (explicit) candidates.push(explicit);
35+
}
36+
for (const candidate of candidates) {
37+
const attr = candidate.getAttribute?.('data-interdead-assets-base');
38+
const dataset = candidate.dataset?.interdeadAssetsBase;
39+
const value = attr || dataset;
40+
if (value && value.trim()) return value.trim();
41+
}
42+
return null;
43+
}
44+
45+
_resolveFromModuleUrl() {
46+
if (!this.moduleUrl) return null;
47+
const url = new URL(this.moduleUrl);
48+
const marker = '/assets/';
49+
const index = url.pathname.lastIndexOf(marker);
50+
if (index === -1) return null;
51+
const basePath = url.pathname.slice(0, index + marker.length);
52+
return `${url.origin}${basePath}`;
53+
}
54+
55+
_normalizeBaseUrl(baseUrl) {
56+
if (!baseUrl) return '/assets/';
57+
return baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
58+
}
59+
60+
_stripAssetsPrefix(assetPath) {
61+
if (assetPath.startsWith('/assets/')) return assetPath.slice('/assets/'.length);
62+
if (assetPath.startsWith('assets/')) return assetPath.slice('assets/'.length);
63+
return assetPath;
64+
}
65+
66+
_isAbsolute(value) {
67+
return /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(value);
68+
}
69+
}
70+
71+
export class AssetUrlMapper {
72+
constructor(resolver = null) {
73+
this.resolver = resolver || new AssetsBaseUrlResolver();
74+
}
75+
76+
mapConfig(value) {
77+
return this._mapValue(value);
78+
}
79+
80+
_mapValue(value) {
81+
if (Array.isArray(value)) {
82+
return value.map(item => this._mapValue(item));
83+
}
84+
if (value && typeof value === 'object') {
85+
return Object.fromEntries(Object.entries(value).map(([key, val]) => [key, this._mapValue(val)]));
86+
}
87+
if (typeof value === 'string') {
88+
return this._mapString(value);
89+
}
90+
return value;
91+
}
92+
93+
_mapString(value) {
94+
if (value.startsWith('/assets/') || value.startsWith('assets/')) {
95+
const stripped = value.replace(/^\/?assets\//, '');
96+
return this.resolver.resolve(stripped);
97+
}
98+
return value;
99+
}
100+
}
101+
102+
export const assetsBaseUrlResolver = new AssetsBaseUrlResolver();
103+
export const assetsBaseUrl = assetsBaseUrlResolver.getBaseUrl();
104+
export const resolveAssetUrl = assetPath => assetsBaseUrlResolver.resolve(assetPath);
105+
export const assetUrlMapper = new AssetUrlMapper(assetsBaseUrlResolver);

src/config/reactionFinale.config.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { resolveAssetUrl } from './assetsBaseUrl.js';
2+
13
export default {
24
guest1: {
35
excludedStages: ['guest1-farewell'],
@@ -8,7 +10,7 @@ export default {
810
success: {
911
titleKey: 'reactions.finale.guest1.title',
1012
messageKey: 'reactions.finale.guest1.message',
11-
imageUrl: '/assets/images/static-image.webp',
13+
imageUrl: resolveAssetUrl('images/static-image.webp'),
1214
imageAltKey: 'reactions.finale.guest1.image_alt'
1315
}
1416
},
@@ -21,7 +23,7 @@ export default {
2123
success: {
2224
titleKey: 'reactions.finale.guide.title',
2325
messageKey: 'reactions.finale.guide.message',
24-
imageUrl: '/assets/images/pencil.png',
26+
imageUrl: resolveAssetUrl('images/pencil.png'),
2527
imageAltKey: 'reactions.finale.guide.image_alt'
2628
}
2729
}

src/config/spirits/guest1.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1+
import { resolveAssetUrl } from '../assetsBaseUrl.js';
2+
13
export default {
24
id: 'guest1',
3-
avatar: '/assets/images/static-image.webp',
5+
avatar: resolveAssetUrl('images/static-image.webp'),
46
reactions: {
57
'guest1-chat-1': ['😮'],
68
'guest1-chat-2': ['😍'],
79
'guest1-farewell': ['😢']
810
},
911
sounds: {
1012
message: {
11-
ghost: '/assets/audio/ghost_effect.mp3',
12-
user: '/assets/audio/type_sound.mp3'
13+
ghost: resolveAssetUrl('audio/ghost_effect.mp3'),
14+
user: resolveAssetUrl('audio/type_sound.mp3')
1315
},
14-
detection: '/assets/audio/ghost_effect.mp3'
16+
detection: resolveAssetUrl('audio/ghost_effect.mp3')
1517
},
1618
stages: [
1719
{

src/config/spirits/guide.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1+
import { resolveAssetUrl } from '../assetsBaseUrl.js';
2+
13
export default {
24
id: 'guide',
3-
avatar: '/assets/images/pencil.png',
5+
avatar: resolveAssetUrl('images/pencil.png'),
46
reactions: {
57
'guide-intro': ['🙂'],
68
'guide-outro': ['🤔']
79
},
810
sounds: {
911
message: {
10-
ghost: '/assets/audio/ghost_effect.mp3',
11-
user: '/assets/audio/type_sound.mp3'
12+
ghost: resolveAssetUrl('audio/ghost_effect.mp3'),
13+
user: resolveAssetUrl('audio/type_sound.mp3')
1214
},
13-
detection: '/assets/audio/ghost_effect.mp3'
15+
detection: resolveAssetUrl('audio/ghost_effect.mp3')
1416
},
1517
stages: [
1618
{
@@ -90,4 +92,3 @@ export default {
9092
'scroll-down': [{ type: 'always' }]
9193
}
9294
};
93-

src/config/spirits/sample.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
22
"unlock": "all",
3-
"avatar": "/assets/images/pencil.png",
3+
"avatar": "assets/images/pencil.png",
44
"sounds": {
55
"message": {
6-
"ghost": "/assets/audio/ghost_effect.mp3",
7-
"user": "/assets/audio/type_sound.mp3"
6+
"ghost": "assets/audio/ghost_effect.mp3",
7+
"user": "assets/audio/type_sound.mp3"
88
},
9-
"detection": "/assets/audio/ghost_effect.mp3"
9+
"detection": "assets/audio/ghost_effect.mp3"
1010
},
1111
"stages": [
1212
{

tests/config/assetsBaseUrl.test.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import assert from 'assert';
2+
3+
import { AssetUrlMapper, AssetsBaseUrlResolver } from '../../src/config/assetsBaseUrl.js';
4+
5+
describe('assets base url resolver', () => {
6+
it('prefers explicit embed marker attributes', () => {
7+
const documentRef = {
8+
currentScript: {
9+
getAttribute: name => (name === 'data-interdead-assets-base' ? 'https://cdn.example/assets' : null),
10+
dataset: {}
11+
},
12+
querySelector: () => null
13+
};
14+
const resolver = new AssetsBaseUrlResolver({ documentRef, moduleUrl: 'https://example.com/assets/app.js' });
15+
assert.strictEqual(resolver.getBaseUrl(), 'https://cdn.example/assets/');
16+
});
17+
18+
it('derives base url from module asset path', () => {
19+
const resolver = new AssetsBaseUrlResolver({
20+
documentRef: null,
21+
moduleUrl: 'https://example.com/InterDeadProto/assets/chunks/app.js'
22+
});
23+
assert.strictEqual(resolver.getBaseUrl(), 'https://example.com/InterDeadProto/assets/');
24+
});
25+
26+
it('falls back to root assets when no marker is detected', () => {
27+
const resolver = new AssetsBaseUrlResolver({
28+
documentRef: null,
29+
moduleUrl: 'https://example.com/InterDeadProto/main.js'
30+
});
31+
assert.strictEqual(resolver.getBaseUrl(), '/assets/');
32+
});
33+
34+
it('resolves asset paths against the base url', () => {
35+
const resolver = new AssetsBaseUrlResolver({
36+
documentRef: null,
37+
moduleUrl: 'https://example.com/InterDeadProto/assets/app.js'
38+
});
39+
assert.strictEqual(
40+
resolver.resolve('images/pic.png'),
41+
'https://example.com/InterDeadProto/assets/images/pic.png'
42+
);
43+
});
44+
});
45+
46+
describe('asset url mapper', () => {
47+
it('maps nested asset strings onto the base url', () => {
48+
const resolver = new AssetsBaseUrlResolver({
49+
documentRef: null,
50+
moduleUrl: 'https://example.com/InterDeadProto/assets/app.js'
51+
});
52+
const mapper = new AssetUrlMapper(resolver);
53+
const mapped = mapper.mapConfig({
54+
avatar: 'assets/images/pencil.png',
55+
sounds: {
56+
message: {
57+
ghost: '/assets/audio/ghost_effect.mp3'
58+
}
59+
}
60+
});
61+
assert.deepStrictEqual(mapped, {
62+
avatar: 'https://example.com/InterDeadProto/assets/images/pencil.png',
63+
sounds: {
64+
message: {
65+
ghost: 'https://example.com/InterDeadProto/assets/audio/ghost_effect.mp3'
66+
}
67+
}
68+
});
69+
});
70+
});

0 commit comments

Comments
 (0)