@occultist/extensions 0.0.1
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/LICENSE +21 -0
- package/dist/css-parser.d.ts +21 -0
- package/dist/css-parser.js +67 -0
- package/dist/dependancy-graph.d.ts +17 -0
- package/dist/dependancy-graph.js +74 -0
- package/dist/file-info.d.ts +97 -0
- package/dist/file-info.js +66 -0
- package/dist/html-parser.d.ts +11 -0
- package/dist/html-parser.js +171 -0
- package/dist/js-parser.d.ts +14 -0
- package/dist/js-parser.js +74 -0
- package/dist/static-extension.d.ts +79 -0
- package/dist/static-extension.js +273 -0
- package/dist/ts-preprocessor.d.ts +8 -0
- package/dist/ts-preprocessor.js +69 -0
- package/dist/types.d.ts +68 -0
- package/dist/types.js +1 -0
- package/lib/static/css-parser.ts +98 -0
- package/lib/static/dependancy-graph.ts +101 -0
- package/lib/static/file-info.ts +153 -0
- package/lib/static/html-parser.ts +225 -0
- package/lib/static/js-parser.ts +107 -0
- package/lib/static/static-extension.ts +400 -0
- package/lib/static/ts-preprocessor.ts +114 -0
- package/lib/static/types.ts +120 -0
- package/package.json +50 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import {type StaticExtension as StaticExt, type Extension, type Cache, type HintLink, type ImplementedAction, joinPaths, type Registry, type StaticAsset} from '@occultist/occultist';
|
|
2
|
+
import {createHash} from "crypto";
|
|
3
|
+
import {createReadStream} from "fs";
|
|
4
|
+
import {opendir, readFile} from "fs/promises";
|
|
5
|
+
import {join} from "path";
|
|
6
|
+
import {Readable} from "stream";
|
|
7
|
+
import {DependancyGraph, DependancyMap} from './dependancy-graph.ts';
|
|
8
|
+
import {type FileInfo, WorkingFileInfo} from './file-info.ts';
|
|
9
|
+
import type {Directory, ReferenceParser, ReferencePreprocessor} from './types.ts';
|
|
10
|
+
import {CSSReferenceParser} from './css-parser.ts';
|
|
11
|
+
import {JSReferenceParser} from './js-parser.ts';
|
|
12
|
+
import {HTMLParser} from './html-parser.ts';
|
|
13
|
+
import {TSReferencePreprocessor} from './ts-preprocessor.ts';
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
type ExtensionMap = Map<string, string>;
|
|
17
|
+
|
|
18
|
+
type FilesByAlias = Map<string, FileInfo>;
|
|
19
|
+
|
|
20
|
+
type FilesByURL = Map<string, FileInfo>;
|
|
21
|
+
|
|
22
|
+
type HashMap = Map<string, string>;
|
|
23
|
+
|
|
24
|
+
type ActionMap = Map<string, ImplementedAction>;
|
|
25
|
+
|
|
26
|
+
export const defaultExtensions = {
|
|
27
|
+
txt: 'text/plain',
|
|
28
|
+
html: 'text/html',
|
|
29
|
+
css: 'text/css',
|
|
30
|
+
xhtml: 'application/xhtml+xml',
|
|
31
|
+
xht: 'application/xhtml+xml',
|
|
32
|
+
js: 'application/javascript',
|
|
33
|
+
ts: 'application/javascript',
|
|
34
|
+
jpg: 'image/jpeg',
|
|
35
|
+
png: 'image/png',
|
|
36
|
+
woff: 'font/woff',
|
|
37
|
+
woff2: 'font/woff2',
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
40
|
+
export const defaultParsers: ReferenceParser[] = [
|
|
41
|
+
new HTMLParser(),
|
|
42
|
+
new CSSReferenceParser(),
|
|
43
|
+
new JSReferenceParser(),
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
export const defaultPreprocessors: ReferencePreprocessor[] = [
|
|
47
|
+
new TSReferencePreprocessor(),
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
export const defaultCSPTypes = [
|
|
51
|
+
'text/html',
|
|
52
|
+
'application/xhtml+xml',
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
export type StaticExtensionArgs = {
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* An occultist registry.
|
|
59
|
+
*/
|
|
60
|
+
registry: Registry;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A cache instance
|
|
64
|
+
*/
|
|
65
|
+
cache?: Cache;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Directories to serve as static content.
|
|
69
|
+
*/
|
|
70
|
+
directories: Directory[];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A javascript object mapping file extensions
|
|
74
|
+
* to their content types.
|
|
75
|
+
*/
|
|
76
|
+
extensions?: Record<string, string>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
*
|
|
80
|
+
*/
|
|
81
|
+
parsers?: ReferenceParser[];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
*
|
|
85
|
+
*/
|
|
86
|
+
preprocessors?: ReferencePreprocessor[];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A path prefix where static assets should be served.
|
|
90
|
+
*/
|
|
91
|
+
prefix?: string;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Serves a directory of files up as static assets using hashed urls
|
|
96
|
+
* and immutable cache headers.
|
|
97
|
+
*
|
|
98
|
+
* Other endpoints can use the hint method to register early hints linking
|
|
99
|
+
* to hashed actions.
|
|
100
|
+
*/
|
|
101
|
+
export class StaticExtension implements Extension, StaticExt {
|
|
102
|
+
name = 'static';
|
|
103
|
+
#loaded: boolean = false;
|
|
104
|
+
#registry: Registry;
|
|
105
|
+
#cache: Cache | undefined;
|
|
106
|
+
#directories: Directory[];
|
|
107
|
+
#staticAliases: string[] = [];
|
|
108
|
+
#extensions: ExtensionMap;
|
|
109
|
+
#parsers: Map<string, ReferenceParser> = new Map();
|
|
110
|
+
#preprocessors: Map<string, ReferencePreprocessor> = new Map();
|
|
111
|
+
#prefix: string;
|
|
112
|
+
#filesByAlias: FilesByAlias = new Map();
|
|
113
|
+
#filesByURL: FilesByURL = new Map();
|
|
114
|
+
#filesByExtension: Map<string, FileInfo[]> = new Map();
|
|
115
|
+
#filesByContentType: Map<string, FileInfo[]> = new Map();
|
|
116
|
+
#hashes: HashMap = new Map();
|
|
117
|
+
#actions: ActionMap = new Map();
|
|
118
|
+
#dependancies: DependancyGraph | undefined;
|
|
119
|
+
|
|
120
|
+
constructor(args: StaticExtensionArgs) {
|
|
121
|
+
this.#registry = args.registry;
|
|
122
|
+
this.#cache = args.cache;
|
|
123
|
+
this.#directories = args.directories;
|
|
124
|
+
this.#extensions = new Map(Object.entries(args.extensions ?? defaultExtensions)) as ExtensionMap;
|
|
125
|
+
this.#prefix = args.prefix ?? '/';
|
|
126
|
+
|
|
127
|
+
this.#registry.registerExtension(this);
|
|
128
|
+
this.#registry.addEventListener('afterfinalize', this.onAfterFinalize);
|
|
129
|
+
|
|
130
|
+
for (let i = 0; i < args.directories.length; i++) {
|
|
131
|
+
this.#staticAliases.push(args.directories[i].alias);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
Object.freeze(this.#staticAliases);
|
|
135
|
+
|
|
136
|
+
for (const parser of args.parsers ?? defaultParsers) {
|
|
137
|
+
for (const contentType of parser.supports.values()) {
|
|
138
|
+
this.#parsers.set(contentType, parser);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const preprocessor of args.preprocessors ?? defaultPreprocessors) {
|
|
143
|
+
for (const extension of preprocessor.supports.values()) {
|
|
144
|
+
this.#preprocessors.set(extension, preprocessor);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Promise the action cache for all static actions.
|
|
151
|
+
*/
|
|
152
|
+
onAfterFinalize = async () => {
|
|
153
|
+
const promises: Array<Promise<string>> = [];
|
|
154
|
+
|
|
155
|
+
for (const action of this.#actions.values()) {
|
|
156
|
+
promises.push(
|
|
157
|
+
this.#registry.primeCache(
|
|
158
|
+
new Request(action.url())
|
|
159
|
+
)
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
await Promise.all(promises);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
get dependancies(): DependancyGraph | undefined {
|
|
167
|
+
return this.#dependancies;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
get staticAliases(): string[] {
|
|
171
|
+
return this.#staticAliases;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
get(name: string): ImplementedAction | undefined {
|
|
175
|
+
return this.#actions.get(name);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
getFile(alias: string): FileInfo | undefined {
|
|
179
|
+
return this.#filesByAlias.get(alias);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
getAsset(alias: string): StaticAsset | undefined {
|
|
183
|
+
return this.#filesByAlias.get(alias);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
hint(name: string, args: Omit<HintLink, 'href' | 'contentType'>): HintLink | null {
|
|
187
|
+
const file = this.#filesByAlias.get(name);
|
|
188
|
+
const action = this.#actions.get(name);
|
|
189
|
+
|
|
190
|
+
if (file == null || action == null) return null;
|
|
191
|
+
|
|
192
|
+
const href = action.url();
|
|
193
|
+
|
|
194
|
+
if (href == null) return null;
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
...args,
|
|
198
|
+
href,
|
|
199
|
+
type: file.contentType,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Called when registered with Occultist to
|
|
205
|
+
* run any async tasks and hook into Occultist's
|
|
206
|
+
* extension event system.
|
|
207
|
+
*/
|
|
208
|
+
setup = (): ReadableStream & Promise<void> => {
|
|
209
|
+
if (this.#loaded) throw new Error('Static extension already loaded');
|
|
210
|
+
|
|
211
|
+
const { writable, readable } = new TransformStream();
|
|
212
|
+
|
|
213
|
+
(readable as unknown as Promise<void>).then = async (resolve, reject) => {
|
|
214
|
+
try {
|
|
215
|
+
for await (const _ of readable) {}
|
|
216
|
+
} catch (err) {
|
|
217
|
+
return reject(err);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
resolve();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
this.#load(writable.getWriter());
|
|
224
|
+
|
|
225
|
+
return readable as ReadableStream & Promise<void>;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async #load(writer: WritableStreamDefaultWriter): Promise<void> {
|
|
229
|
+
writer.write('Gathering files');
|
|
230
|
+
|
|
231
|
+
for await (const file of this.#traverse(this.#directories)) {
|
|
232
|
+
this.#filesByAlias.set(file.alias, file);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let file: WorkingFileInfo;
|
|
236
|
+
const files = Array.from(this.#filesByAlias.values());
|
|
237
|
+
|
|
238
|
+
writer.write('Generating hashes');
|
|
239
|
+
for (let i = 0; i < files.length; i++) {
|
|
240
|
+
file = files[i] as WorkingFileInfo;
|
|
241
|
+
|
|
242
|
+
const content = await readFile(file.absolutePath);
|
|
243
|
+
const hash = createHash('sha1').update(content).digest('hex');
|
|
244
|
+
const parts = file.name.split('/');
|
|
245
|
+
const friendly = parts[parts.length - 1].split('.')[0];
|
|
246
|
+
const rootURL = this.#registry.rootIRI;
|
|
247
|
+
const aliasURL = joinPaths(rootURL, this.#prefix, file.alias);
|
|
248
|
+
const url = joinPaths(rootURL, this.#prefix, `${friendly}-${hash}.${file.extension}`);
|
|
249
|
+
|
|
250
|
+
file.finalize(hash, url, aliasURL);
|
|
251
|
+
this.#hashes.set(file.alias, hash);
|
|
252
|
+
this.#filesByAlias.set(file.alias, file);
|
|
253
|
+
this.#filesByURL.set(aliasURL, file);
|
|
254
|
+
|
|
255
|
+
if (!this.#filesByExtension.has(file.extension)) {
|
|
256
|
+
this.#filesByExtension.set(file.extension, []);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!this.#filesByContentType.has(file.contentType)) {
|
|
260
|
+
this.#filesByContentType.set(file.contentType, []);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
this.#filesByExtension.get(file.extension).push(file);
|
|
264
|
+
this.#filesByContentType.get(file.contentType).push(file);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const dependancyMaps: Array<Map<string, DependancyMap>> = [];
|
|
268
|
+
|
|
269
|
+
writer.write('Building dependancy tree');
|
|
270
|
+
|
|
271
|
+
for (const [extension, contentType] of this.#extensions.entries()) {
|
|
272
|
+
const preprocessor = this.#preprocessors.get(extension);
|
|
273
|
+
|
|
274
|
+
if (preprocessor != null) {
|
|
275
|
+
const files = this.#filesByExtension.get(extension);
|
|
276
|
+
|
|
277
|
+
if (files == null) continue;
|
|
278
|
+
|
|
279
|
+
const dependancies: Map<string, DependancyMap> = new Map();
|
|
280
|
+
|
|
281
|
+
for (let i = 0; i < files.length; i++) {
|
|
282
|
+
let file = files[i];
|
|
283
|
+
|
|
284
|
+
const content = await readFile(file.absolutePath);
|
|
285
|
+
const references = await preprocessor.parse(new Blob([content]), file, this.#filesByURL);
|
|
286
|
+
|
|
287
|
+
dependancies.set(file.alias, new DependancyMap(file, references));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
dependancyMaps.push(dependancies);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const parser = this.#parsers.get(contentType);
|
|
295
|
+
|
|
296
|
+
if (parser == null) continue;
|
|
297
|
+
|
|
298
|
+
const files = this.#filesByContentType.get(contentType);
|
|
299
|
+
|
|
300
|
+
if (files == null) continue;
|
|
301
|
+
|
|
302
|
+
const dependancies: Map<string, DependancyMap> = new Map();
|
|
303
|
+
|
|
304
|
+
for (let i = 0; i < files.length; i++) {
|
|
305
|
+
let file = files[i];
|
|
306
|
+
|
|
307
|
+
const content = await readFile(file.absolutePath);
|
|
308
|
+
const references = await parser.parse(new Blob([content]), file, this.#filesByURL)
|
|
309
|
+
dependancies.set(file.alias, new DependancyMap(file, references));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
dependancyMaps.push(dependancies);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
this.#dependancies = new DependancyGraph(
|
|
316
|
+
new Map(dependancyMaps.flatMap((map => Array.from(map.entries())))),
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
writer.write('Registering actions');
|
|
320
|
+
for (const [name, file] of this.#filesByAlias.entries()) {
|
|
321
|
+
const preprocessor = this.#preprocessors.get(file.extension);
|
|
322
|
+
const parser = this.#parsers.get(file.contentType);
|
|
323
|
+
const parts = name.split('/');
|
|
324
|
+
const friendly = parts[parts.length - 1].split('.')[0];
|
|
325
|
+
const hash = this.#hashes.get(name) as string;
|
|
326
|
+
let action = this.#registry.http.get(name, joinPaths(this.#prefix, `${friendly}-${hash}.${file.extension}`))
|
|
327
|
+
.public()
|
|
328
|
+
|
|
329
|
+
if (this.#cache) {
|
|
330
|
+
action = action.cache(this.#cache.store({ immutable: true }));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const implemented = action.handle(file.contentType, async (ctx) => {
|
|
334
|
+
if (preprocessor != null) {
|
|
335
|
+
const content = await readFile(file.absolutePath);
|
|
336
|
+
|
|
337
|
+
ctx.body = await preprocessor.process(new Blob([content]), file, this.#filesByURL);
|
|
338
|
+
} else if (parser != null) {
|
|
339
|
+
const content = await readFile(file.absolutePath);
|
|
340
|
+
|
|
341
|
+
ctx.body = await parser.update(new Blob([content]), file, this.#filesByURL);
|
|
342
|
+
} else {
|
|
343
|
+
ctx.body = Readable.toWeb(createReadStream(file.absolutePath)) as ReadableStream;
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
this.#actions.set(name, implemented);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
writer.write('Finished');
|
|
351
|
+
writer.close();
|
|
352
|
+
|
|
353
|
+
this.#loaded = true;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
#traverseRe = /.(?:\.(?<lang>[a-zA-Z\-]+))?(?:\.(?<extension>[a-zA-Z0-9]+))$/;
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Traverses into a list of directories outputting a file info object for every file
|
|
360
|
+
* of a configured file extension.
|
|
361
|
+
*
|
|
362
|
+
* @param directories The directories to traverse into.
|
|
363
|
+
*/
|
|
364
|
+
async* #traverse(directories: Directory[], root: string = ''): AsyncGenerator<WorkingFileInfo, void, unknown> {
|
|
365
|
+
for (let i = 0; i < directories.length; i++) {
|
|
366
|
+
const dir = await opendir(directories[i].path);
|
|
367
|
+
|
|
368
|
+
for await (const entry of dir) {
|
|
369
|
+
const name = entry.name;
|
|
370
|
+
const alias = directories[i].alias;
|
|
371
|
+
const match = this.#traverseRe.exec(entry.name);
|
|
372
|
+
const absolutePath = join(directories[i].path, entry.name);
|
|
373
|
+
const directory = root === '' ? directories[i].path : root;
|
|
374
|
+
const relativePath = absolutePath.replace(directory, '');
|
|
375
|
+
const { lang, extension } = match?.groups ?? {};
|
|
376
|
+
const contentType = this.#extensions.get(extension);
|
|
377
|
+
|
|
378
|
+
if (contentType == null || extension == null) {
|
|
379
|
+
console.warn(`File ${joinPaths(alias, relativePath)} extension not known, skipping...`);
|
|
380
|
+
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (entry.isDirectory()) {
|
|
385
|
+
yield* this.#traverse([{ alias, path: absolutePath }], root === '' ? directories[i].path : root);
|
|
386
|
+
} else {
|
|
387
|
+
yield new WorkingFileInfo(
|
|
388
|
+
name,
|
|
389
|
+
alias,
|
|
390
|
+
relativePath,
|
|
391
|
+
absolutePath,
|
|
392
|
+
extension,
|
|
393
|
+
contentType,
|
|
394
|
+
lang,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {createPrinter, createSourceFile, factory, forEachChild, isCallExpression, isImportDeclaration, isStringLiteral, NewLineKind, type Node, ScriptTarget, type SourceFile, type StringLiteral, SyntaxKind, transform, type TransformerFactory, visitEachChild, visitNode} from 'typescript';
|
|
2
|
+
import {type FileInfo} from './file-info.ts';
|
|
3
|
+
import type {FilesByURL, ReferenceDetails, ReferencePreprocessor} from './types.ts';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export class TSReferencePreprocessor implements ReferencePreprocessor {
|
|
7
|
+
|
|
8
|
+
supports: Set<string> = new Set(['ts', 'mts', 'cts']);
|
|
9
|
+
|
|
10
|
+
readonly output: 'application/javascript';
|
|
11
|
+
|
|
12
|
+
async parse(content: Blob, file: FileInfo, filesByURL: FilesByURL): Promise<ReferenceDetails[]> {
|
|
13
|
+
const sourceText = await content.text();
|
|
14
|
+
const source = createSourceFile(
|
|
15
|
+
file.absolutePath,
|
|
16
|
+
sourceText,
|
|
17
|
+
ScriptTarget.ES2022,
|
|
18
|
+
true,
|
|
19
|
+
);
|
|
20
|
+
const references: ReferenceDetails[] = [];
|
|
21
|
+
|
|
22
|
+
function visit(node: Node) {
|
|
23
|
+
if (isImportDeclaration(node) && node.moduleSpecifier) {
|
|
24
|
+
const path = (node.moduleSpecifier as StringLiteral).text;
|
|
25
|
+
const url = new URL(path, file.aliasURL).toString();
|
|
26
|
+
|
|
27
|
+
references.push({
|
|
28
|
+
url,
|
|
29
|
+
directive: 'script-src',
|
|
30
|
+
file: filesByURL.get(url),
|
|
31
|
+
});
|
|
32
|
+
} else if (
|
|
33
|
+
isCallExpression(node) &&
|
|
34
|
+
node.expression.kind === SyntaxKind.ImportKeyword &&
|
|
35
|
+
node.arguments.length === 1 &&
|
|
36
|
+
isStringLiteral(node.arguments[0])
|
|
37
|
+
) {
|
|
38
|
+
const path = node.arguments[0].text;
|
|
39
|
+
const url = new URL(path, file.aliasURL).toString();
|
|
40
|
+
|
|
41
|
+
references.push({
|
|
42
|
+
url,
|
|
43
|
+
directive: 'script-src',
|
|
44
|
+
file: filesByURL.get(url),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
forEachChild(node, visit);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
visit(source)
|
|
52
|
+
|
|
53
|
+
return references;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async process(content: Blob, file: FileInfo, filesByURL: FilesByURL): Promise<Blob> {
|
|
57
|
+
const sourceText = await content.text();
|
|
58
|
+
const source = createSourceFile(
|
|
59
|
+
file.absolutePath,
|
|
60
|
+
sourceText,
|
|
61
|
+
ScriptTarget.ES2022,
|
|
62
|
+
true,
|
|
63
|
+
);
|
|
64
|
+
const transformerFactory: TransformerFactory<Node> = (context) => {
|
|
65
|
+
function visitor(node: Node) {
|
|
66
|
+
if (isImportDeclaration(node) && node.moduleSpecifier) {
|
|
67
|
+
const path = (node.moduleSpecifier as StringLiteral).text;
|
|
68
|
+
const url = new URL(path, file.aliasURL).toString();
|
|
69
|
+
const ref = filesByURL.get(url);
|
|
70
|
+
const literal = factory.createStringLiteral(ref.url);
|
|
71
|
+
|
|
72
|
+
return factory.updateImportDeclaration(
|
|
73
|
+
node,
|
|
74
|
+
node.modifiers,
|
|
75
|
+
node.importClause,
|
|
76
|
+
literal,
|
|
77
|
+
node.attributes,
|
|
78
|
+
);
|
|
79
|
+
} else if (
|
|
80
|
+
isCallExpression(node) &&
|
|
81
|
+
node.expression.kind === SyntaxKind.ImportKeyword &&
|
|
82
|
+
node.arguments.length === 1 &&
|
|
83
|
+
isStringLiteral(node.arguments[0])
|
|
84
|
+
) {
|
|
85
|
+
const path = node.arguments[0].text;
|
|
86
|
+
const url = new URL(path, file.aliasURL).toString();
|
|
87
|
+
const ref = filesByURL.get(url);
|
|
88
|
+
const literal = factory.createStringLiteral(ref.url);
|
|
89
|
+
|
|
90
|
+
return factory.updateCallExpression(
|
|
91
|
+
node,
|
|
92
|
+
node.expression,
|
|
93
|
+
node.typeArguments,
|
|
94
|
+
[literal],
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return visitEachChild(node, visitor, context);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (node) => visitNode(node, visitor)
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const result = transform(source, [transformerFactory]);
|
|
105
|
+
const printer = createPrinter({ newLine: NewLineKind.LineFeed });
|
|
106
|
+
const transformed = result.transformed[0];
|
|
107
|
+
const code = printer.printFile(transformed as SourceFile);
|
|
108
|
+
|
|
109
|
+
result.dispose();
|
|
110
|
+
|
|
111
|
+
return new Blob([code], { type: this.output });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type {DependancyMap} from "./dependancy-graph.ts";
|
|
2
|
+
import type {FileInfo} from "./file-info.ts";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export type FilesByURL = Map<string, FileInfo>;
|
|
6
|
+
|
|
7
|
+
export type PolicyDirective =
|
|
8
|
+
| 'child-src'
|
|
9
|
+
| 'connect-src'
|
|
10
|
+
| 'default-src'
|
|
11
|
+
| 'fenced-frame-src'
|
|
12
|
+
| 'font-src'
|
|
13
|
+
| 'frame-src'
|
|
14
|
+
| 'img-src'
|
|
15
|
+
| 'manifest-src'
|
|
16
|
+
| 'media-src'
|
|
17
|
+
| 'object-src'
|
|
18
|
+
| 'script-src'
|
|
19
|
+
| 'script-src-elm'
|
|
20
|
+
| 'script-src-attr'
|
|
21
|
+
| 'style-src'
|
|
22
|
+
| 'style-src-elm'
|
|
23
|
+
| 'style-src-attr'
|
|
24
|
+
| 'worker-src'
|
|
25
|
+
;
|
|
26
|
+
|
|
27
|
+
export type Directory = {
|
|
28
|
+
alias: string;
|
|
29
|
+
path: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type ReferenceDetails = {
|
|
33
|
+
url: string;
|
|
34
|
+
directive?: PolicyDirective;
|
|
35
|
+
file?: FileInfo;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* An object with methods for parsing URL references
|
|
40
|
+
* within supporting content types and then embedding
|
|
41
|
+
* URLs generated for the referenced static assets by
|
|
42
|
+
* the framework.
|
|
43
|
+
*/
|
|
44
|
+
export interface ReferenceParser {
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A set of content types supported by this reference parser.
|
|
48
|
+
*/
|
|
49
|
+
readonly supports: Set<string>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parses URLs referenced in a file's content and
|
|
53
|
+
* returns a dependancy map of all references.
|
|
54
|
+
*
|
|
55
|
+
* @param content The file content to update.
|
|
56
|
+
* @param file File info object relating to the file.
|
|
57
|
+
* @returns A dependancy map of all references.
|
|
58
|
+
*/
|
|
59
|
+
parse(
|
|
60
|
+
content: Blob,
|
|
61
|
+
file: FileInfo,
|
|
62
|
+
filesByURL: FilesByURL,
|
|
63
|
+
): Promise<ReferenceDetails[]>;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Updates a file's embedded hyperlinks to point to
|
|
67
|
+
* final URLs of other static content.
|
|
68
|
+
*
|
|
69
|
+
* @param file File info object.
|
|
70
|
+
* @returns content with URLs updated.
|
|
71
|
+
*/
|
|
72
|
+
update(
|
|
73
|
+
content: Blob,
|
|
74
|
+
file: FileInfo,
|
|
75
|
+
filesByURL: FilesByURL,
|
|
76
|
+
): Promise<Blob>;
|
|
77
|
+
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ReferencePreprocessor {
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A set of content types supported by this reference parser.
|
|
84
|
+
*/
|
|
85
|
+
readonly supports: Set<string>;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Content type the pre-processor outputs.
|
|
89
|
+
*/
|
|
90
|
+
readonly output: string;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Parses URLs referenced in a file's content and
|
|
94
|
+
* returns a dependancy map of all references.
|
|
95
|
+
*
|
|
96
|
+
* @param content The file content to update.
|
|
97
|
+
* @param file File info object relating to the file.
|
|
98
|
+
* @returns A dependancy map of all references.
|
|
99
|
+
*/
|
|
100
|
+
parse(
|
|
101
|
+
content: Blob,
|
|
102
|
+
file: FileInfo,
|
|
103
|
+
filesByURL: FilesByURL,
|
|
104
|
+
): Promise<ReferenceDetails[]>;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Updates a file's embedded hyperlinks to point to
|
|
108
|
+
* final URLs of other static content.
|
|
109
|
+
*
|
|
110
|
+
* @param file File info object.
|
|
111
|
+
* @returns content with URLs updated.
|
|
112
|
+
*/
|
|
113
|
+
process(
|
|
114
|
+
content: Blob,
|
|
115
|
+
file: FileInfo,
|
|
116
|
+
filesByURL: FilesByURL,
|
|
117
|
+
): Promise<Blob>;
|
|
118
|
+
|
|
119
|
+
}
|
|
120
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@occultist/extensions",
|
|
3
|
+
"description": "Common extensions for working with occultist.dev",
|
|
4
|
+
"author": "Matthew Quinn",
|
|
5
|
+
"homepage": "https://github.com/occultist-dev/occultist-extensions",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/mod.js",
|
|
9
|
+
"types": "dist/mod.d.ts",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/occultist-dev/occultist-extensions.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/occultist-dev/occultist-extensions/issues"
|
|
16
|
+
},
|
|
17
|
+
"directories": {
|
|
18
|
+
"lib": "lib"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"./README.md",
|
|
22
|
+
"./LICENCE",
|
|
23
|
+
"./dist",
|
|
24
|
+
"./lib"
|
|
25
|
+
],
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@occultist/occultist": "^0.0.11",
|
|
28
|
+
"typescript": "^5.9.3"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/estree": "^1.0.8",
|
|
32
|
+
"@types/jsdom": "^27.0.0",
|
|
33
|
+
"@types/node": "^24.10.1"
|
|
34
|
+
},
|
|
35
|
+
"version": "0.0.1",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@occultist/occultist": "^0.0.11",
|
|
38
|
+
"acorn": "^8.15.0",
|
|
39
|
+
"astring": "^1.9.0",
|
|
40
|
+
"jsdom": "^27.4.0",
|
|
41
|
+
"source-map": "^0.7.6",
|
|
42
|
+
"ts-blank-space": "^0.6.2",
|
|
43
|
+
"typescript": "^5.9.3",
|
|
44
|
+
"zimmerframe": "^1.1.4"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "rm -r dist/* && tsc -p tsconfig.build.json",
|
|
48
|
+
"test": "node --test"
|
|
49
|
+
}
|
|
50
|
+
}
|