-
Notifications
You must be signed in to change notification settings - Fork 68
/
Copy pathmodel.js
210 lines (176 loc) · 7.33 KB
/
model.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
import { readFile } from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import importJson from './import-json.js';
import Fixture from './model/Fixture.js';
import Manufacturer from './model/Manufacturer.js';
/**
* Look up the fixture definition in the directory structure and create a Fixture instance.
* @param {string} absolutePath The fixture file absolute path, including filename.
* @returns {Promise<Fixture, Error>} A Promise that resolves to the created Fixture instance or is rejected with a `MODULE_NOT_FOUND` error if the given fixture file does not exist.
*/
export async function fixtureFromFile(absolutePath) {
let manufacturerKey = path.basename(path.dirname(absolutePath));
let fixtureKey = path.basename(absolutePath, path.extname(absolutePath));
let fixtureJson = await importJson(absolutePath);
if (fixtureJson.$schema.endsWith(`/fixture-redirect.json`)) {
[manufacturerKey, fixtureKey] = fixtureJson.redirectTo.split(`/`);
absolutePath = path.join(path.dirname(absolutePath), `../${manufacturerKey}/${fixtureKey}.json`);
fixtureJson = await importJson(absolutePath);
}
const manufacturer = await manufacturerFromRepository(manufacturerKey);
await embedResourcesIntoFixtureJson(fixtureJson);
return new Fixture(manufacturer, fixtureKey, fixtureJson);
}
/**
* Look up the fixture definition in the directory structure and create a Fixture instance.
* @param {string} manufacturerKey The manufacturer's key (directory name)
* @param {string} fixtureKey The fixture's key (filename without .json)
* @returns {Promise<Fixture, Error>} A Promise that resolves to the created Fixture instance or is rejected with a `MODULE_NOT_FOUND` error if the given fixture file does not exist.
*/
export async function fixtureFromRepository(manufacturerKey, fixtureKey) {
let fixturePath = `../fixtures/${manufacturerKey}/${fixtureKey}.json`;
let fixtureJson = await importJson(fixturePath, import.meta.url);
if (fixtureJson.$schema.endsWith(`/fixture-redirect.json`)) {
fixturePath = `../fixtures/${fixtureJson.redirectTo}.json`;
fixtureJson = {
...await importJson(fixturePath, import.meta.url),
name: fixtureJson.name,
};
}
const manufacturer = await manufacturerFromRepository(manufacturerKey);
await embedResourcesIntoFixtureJson(fixtureJson);
return new Fixture(manufacturer, fixtureKey, fixtureJson);
}
/**
* Look up the manufacturer definition in the directory structure and create a Manufacturer instance.
* @param {string} manufacturerKey The manufacturer's key (directory name).
* @returns {Promise<Manufacturer>} A Promise that resolves to the created Manufacturer instance.
*/
export async function manufacturerFromRepository(manufacturerKey) {
const manufacturers = await importJson(`../fixtures/manufacturers.json`, import.meta.url);
return new Manufacturer(manufacturerKey, manufacturers[manufacturerKey]);
}
/**
* @param {object} fixtureJson The fixture JSON to embed resoures into.
*/
export async function embedResourcesIntoFixtureJson(fixtureJson) {
if (`wheels` in fixtureJson) {
for (const wheel of Object.values(fixtureJson.wheels)) {
for (const slot of wheel.slots) {
if (typeof slot.resource === `string`) {
slot.resource = await getResourceFromString(slot.resource);
}
}
}
}
}
/**
* @param {string} resourceName The resource name, as specified in a fixture.
* @returns {Promise<object>} A Promise that resolves to the resource object to be embedded into the fixture.
*/
export async function getResourceFromString(resourceName) {
const { type, key, alias } = await resolveResourceName(resourceName);
const resourceBaseUrl = new URL(`../resources/${type}/`, import.meta.url);
const resourceUrl = new URL(`${key}.json`, resourceBaseUrl);
let resourceData;
try {
resourceData = await importJson(resourceUrl);
}
catch (error) {
throw error instanceof SyntaxError
? new Error(`Resource file '${fileURLToPath(resourceUrl)}' could not be parsed as JSON.`)
: new Error(`Resource '${resourceName}' not found.`);
}
resourceData.key = key;
resourceData.type = type;
resourceData.alias = alias;
resourceData.image = await getImageForResource(type, resourceBaseUrl, key);
delete resourceData.$schema;
return resourceData;
}
/**
* @typedef {object} ResolvedResourceName
* @property {string} type The resource type, i.e. name of the directory inside the resources directory.
* @property {string} key The resource key.
* @property {string | null} alias The original resource name if it's an alias, null otherwise.
*/
/**
* @param {string} resourceName The resource name, as specified in a fixture.
* @returns {Promise<ResolvedResourceName>} A Promise that resolves to the resolved resource name object.
*/
async function resolveResourceName(resourceName) {
const [type, ...remainingParts] = resourceName.split(`/`);
if (remainingParts[0] === `aliases`) {
const aliasFileName = remainingParts[1];
const aliasKey = remainingParts.slice(2).join(`/`);
const aliasesFilePath = `resources/${type}/aliases/${aliasFileName}.json`;
let aliases;
try {
aliases = await importJson(`../${aliasesFilePath}`, import.meta.url);
}
catch {
throw new Error(`Resource aliases file '${aliasesFilePath}' not found.`);
}
if (!(aliasKey in aliases)) {
throw new Error(`Resource alias '${aliasKey}' not defined in file '${aliasesFilePath}'.`);
}
return {
type,
key: aliases[aliasKey],
alias: `${aliasFileName}/${aliasKey}`,
};
}
return {
type,
key: remainingParts.join(`/`),
alias: null,
};
}
/**
* @typedef {object} ResourceImage
* @property {string} dataUrl The data URL of the image.
* @property {string} extension The extension of the image file.
*/
const resourceFileFormats = [
{
extension: `svg`,
mimeType: `image/svg+xml;charset=utf8`,
encoding: `utf8`,
},
{
extension: `png`,
mimeType: `image/png`,
encoding: `base64`,
},
];
/**
* @param {string} type The resource type, i.e. name of the directory inside the resources directory.
* @param {URL} baseUrl The path of the resource directory.
* @param {string} key The resource key.
* @returns {Promise<ResourceImage | undefined>} A Promise that resolves to the resource image, or undefined if none could be found.
*/
async function getImageForResource(type, baseUrl, key) {
for (const { extension, mimeType, encoding } of resourceFileFormats) {
try {
let data = await readFile(new URL(`${key}.${extension}`, baseUrl), encoding);
if (extension === `svg`) {
// see https://cloudfour.com/thinks/simple-svg-placeholder/#how-it-works
data = data
.replaceAll(/[\t\n\r]/gim, ``) // Strip newlines and tabs
.replaceAll(/\s\s+/g, ` `) // Condense multiple spaces
.replaceAll(/'/gim, `\\i`) // Normalize quotes
.replaceAll(/<!--(.*(?=-->))-->/gim, ``); // Strip comments
}
return { mimeType, extension, data, encoding };
}
catch {
// image does not exist
}
}
if (type === `gobos`) {
const fileExtensions = resourceFileFormats.map(({ extension }) => extension).join(`, `);
throw new Error(`Expected gobo image for resource '${fileURLToPath(new URL(key, baseUrl))}' not found (supported file extensions: ${fileExtensions}).`);
}
return undefined;
}