@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,79 @@
|
|
|
1
|
+
import { type StaticExtension as StaticExt, type Extension, type Cache, type HintLink, type ImplementedAction, type Registry, type StaticAsset } from '@occultist/occultist';
|
|
2
|
+
import { DependancyGraph } from './dependancy-graph.ts';
|
|
3
|
+
import { type FileInfo } from './file-info.ts';
|
|
4
|
+
import type { Directory, ReferenceParser, ReferencePreprocessor } from './types.ts';
|
|
5
|
+
export declare const defaultExtensions: {
|
|
6
|
+
readonly txt: "text/plain";
|
|
7
|
+
readonly html: "text/html";
|
|
8
|
+
readonly css: "text/css";
|
|
9
|
+
readonly xhtml: "application/xhtml+xml";
|
|
10
|
+
readonly xht: "application/xhtml+xml";
|
|
11
|
+
readonly js: "application/javascript";
|
|
12
|
+
readonly ts: "application/javascript";
|
|
13
|
+
readonly jpg: "image/jpeg";
|
|
14
|
+
readonly png: "image/png";
|
|
15
|
+
readonly woff: "font/woff";
|
|
16
|
+
readonly woff2: "font/woff2";
|
|
17
|
+
};
|
|
18
|
+
export declare const defaultParsers: ReferenceParser[];
|
|
19
|
+
export declare const defaultPreprocessors: ReferencePreprocessor[];
|
|
20
|
+
export declare const defaultCSPTypes: string[];
|
|
21
|
+
export type StaticExtensionArgs = {
|
|
22
|
+
/**
|
|
23
|
+
* An occultist registry.
|
|
24
|
+
*/
|
|
25
|
+
registry: Registry;
|
|
26
|
+
/**
|
|
27
|
+
* A cache instance
|
|
28
|
+
*/
|
|
29
|
+
cache?: Cache;
|
|
30
|
+
/**
|
|
31
|
+
* Directories to serve as static content.
|
|
32
|
+
*/
|
|
33
|
+
directories: Directory[];
|
|
34
|
+
/**
|
|
35
|
+
* A javascript object mapping file extensions
|
|
36
|
+
* to their content types.
|
|
37
|
+
*/
|
|
38
|
+
extensions?: Record<string, string>;
|
|
39
|
+
/**
|
|
40
|
+
*
|
|
41
|
+
*/
|
|
42
|
+
parsers?: ReferenceParser[];
|
|
43
|
+
/**
|
|
44
|
+
*
|
|
45
|
+
*/
|
|
46
|
+
preprocessors?: ReferencePreprocessor[];
|
|
47
|
+
/**
|
|
48
|
+
* A path prefix where static assets should be served.
|
|
49
|
+
*/
|
|
50
|
+
prefix?: string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Serves a directory of files up as static assets using hashed urls
|
|
54
|
+
* and immutable cache headers.
|
|
55
|
+
*
|
|
56
|
+
* Other endpoints can use the hint method to register early hints linking
|
|
57
|
+
* to hashed actions.
|
|
58
|
+
*/
|
|
59
|
+
export declare class StaticExtension implements Extension, StaticExt {
|
|
60
|
+
#private;
|
|
61
|
+
name: string;
|
|
62
|
+
constructor(args: StaticExtensionArgs);
|
|
63
|
+
/**
|
|
64
|
+
* Promise the action cache for all static actions.
|
|
65
|
+
*/
|
|
66
|
+
onAfterFinalize: () => Promise<void>;
|
|
67
|
+
get dependancies(): DependancyGraph | undefined;
|
|
68
|
+
get staticAliases(): string[];
|
|
69
|
+
get(name: string): ImplementedAction | undefined;
|
|
70
|
+
getFile(alias: string): FileInfo | undefined;
|
|
71
|
+
getAsset(alias: string): StaticAsset | undefined;
|
|
72
|
+
hint(name: string, args: Omit<HintLink, 'href' | 'contentType'>): HintLink | null;
|
|
73
|
+
/**
|
|
74
|
+
* Called when registered with Occultist to
|
|
75
|
+
* run any async tasks and hook into Occultist's
|
|
76
|
+
* extension event system.
|
|
77
|
+
*/
|
|
78
|
+
setup: () => ReadableStream & Promise<void>;
|
|
79
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { joinPaths } 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.js";
|
|
8
|
+
import { WorkingFileInfo } from "./file-info.js";
|
|
9
|
+
import { CSSReferenceParser } from "./css-parser.js";
|
|
10
|
+
import { JSReferenceParser } from "./js-parser.js";
|
|
11
|
+
import { HTMLParser } from "./html-parser.js";
|
|
12
|
+
import { TSReferencePreprocessor } from "./ts-preprocessor.js";
|
|
13
|
+
export const defaultExtensions = {
|
|
14
|
+
txt: 'text/plain',
|
|
15
|
+
html: 'text/html',
|
|
16
|
+
css: 'text/css',
|
|
17
|
+
xhtml: 'application/xhtml+xml',
|
|
18
|
+
xht: 'application/xhtml+xml',
|
|
19
|
+
js: 'application/javascript',
|
|
20
|
+
ts: 'application/javascript',
|
|
21
|
+
jpg: 'image/jpeg',
|
|
22
|
+
png: 'image/png',
|
|
23
|
+
woff: 'font/woff',
|
|
24
|
+
woff2: 'font/woff2',
|
|
25
|
+
};
|
|
26
|
+
export const defaultParsers = [
|
|
27
|
+
new HTMLParser(),
|
|
28
|
+
new CSSReferenceParser(),
|
|
29
|
+
new JSReferenceParser(),
|
|
30
|
+
];
|
|
31
|
+
export const defaultPreprocessors = [
|
|
32
|
+
new TSReferencePreprocessor(),
|
|
33
|
+
];
|
|
34
|
+
export const defaultCSPTypes = [
|
|
35
|
+
'text/html',
|
|
36
|
+
'application/xhtml+xml',
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* Serves a directory of files up as static assets using hashed urls
|
|
40
|
+
* and immutable cache headers.
|
|
41
|
+
*
|
|
42
|
+
* Other endpoints can use the hint method to register early hints linking
|
|
43
|
+
* to hashed actions.
|
|
44
|
+
*/
|
|
45
|
+
export class StaticExtension {
|
|
46
|
+
name = 'static';
|
|
47
|
+
#loaded = false;
|
|
48
|
+
#registry;
|
|
49
|
+
#cache;
|
|
50
|
+
#directories;
|
|
51
|
+
#staticAliases = [];
|
|
52
|
+
#extensions;
|
|
53
|
+
#parsers = new Map();
|
|
54
|
+
#preprocessors = new Map();
|
|
55
|
+
#prefix;
|
|
56
|
+
#filesByAlias = new Map();
|
|
57
|
+
#filesByURL = new Map();
|
|
58
|
+
#filesByExtension = new Map();
|
|
59
|
+
#filesByContentType = new Map();
|
|
60
|
+
#hashes = new Map();
|
|
61
|
+
#actions = new Map();
|
|
62
|
+
#dependancies;
|
|
63
|
+
constructor(args) {
|
|
64
|
+
this.#registry = args.registry;
|
|
65
|
+
this.#cache = args.cache;
|
|
66
|
+
this.#directories = args.directories;
|
|
67
|
+
this.#extensions = new Map(Object.entries(args.extensions ?? defaultExtensions));
|
|
68
|
+
this.#prefix = args.prefix ?? '/';
|
|
69
|
+
this.#registry.registerExtension(this);
|
|
70
|
+
this.#registry.addEventListener('afterfinalize', this.onAfterFinalize);
|
|
71
|
+
for (let i = 0; i < args.directories.length; i++) {
|
|
72
|
+
this.#staticAliases.push(args.directories[i].alias);
|
|
73
|
+
}
|
|
74
|
+
Object.freeze(this.#staticAliases);
|
|
75
|
+
for (const parser of args.parsers ?? defaultParsers) {
|
|
76
|
+
for (const contentType of parser.supports.values()) {
|
|
77
|
+
this.#parsers.set(contentType, parser);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
for (const preprocessor of args.preprocessors ?? defaultPreprocessors) {
|
|
81
|
+
for (const extension of preprocessor.supports.values()) {
|
|
82
|
+
this.#preprocessors.set(extension, preprocessor);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Promise the action cache for all static actions.
|
|
88
|
+
*/
|
|
89
|
+
onAfterFinalize = async () => {
|
|
90
|
+
const promises = [];
|
|
91
|
+
for (const action of this.#actions.values()) {
|
|
92
|
+
promises.push(this.#registry.primeCache(new Request(action.url())));
|
|
93
|
+
}
|
|
94
|
+
await Promise.all(promises);
|
|
95
|
+
};
|
|
96
|
+
get dependancies() {
|
|
97
|
+
return this.#dependancies;
|
|
98
|
+
}
|
|
99
|
+
get staticAliases() {
|
|
100
|
+
return this.#staticAliases;
|
|
101
|
+
}
|
|
102
|
+
get(name) {
|
|
103
|
+
return this.#actions.get(name);
|
|
104
|
+
}
|
|
105
|
+
getFile(alias) {
|
|
106
|
+
return this.#filesByAlias.get(alias);
|
|
107
|
+
}
|
|
108
|
+
getAsset(alias) {
|
|
109
|
+
return this.#filesByAlias.get(alias);
|
|
110
|
+
}
|
|
111
|
+
hint(name, args) {
|
|
112
|
+
const file = this.#filesByAlias.get(name);
|
|
113
|
+
const action = this.#actions.get(name);
|
|
114
|
+
if (file == null || action == null)
|
|
115
|
+
return null;
|
|
116
|
+
const href = action.url();
|
|
117
|
+
if (href == null)
|
|
118
|
+
return null;
|
|
119
|
+
return {
|
|
120
|
+
...args,
|
|
121
|
+
href,
|
|
122
|
+
type: file.contentType,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Called when registered with Occultist to
|
|
127
|
+
* run any async tasks and hook into Occultist's
|
|
128
|
+
* extension event system.
|
|
129
|
+
*/
|
|
130
|
+
setup = () => {
|
|
131
|
+
if (this.#loaded)
|
|
132
|
+
throw new Error('Static extension already loaded');
|
|
133
|
+
const { writable, readable } = new TransformStream();
|
|
134
|
+
readable.then = async (resolve, reject) => {
|
|
135
|
+
try {
|
|
136
|
+
for await (const _ of readable) { }
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
return reject(err);
|
|
140
|
+
}
|
|
141
|
+
resolve();
|
|
142
|
+
};
|
|
143
|
+
this.#load(writable.getWriter());
|
|
144
|
+
return readable;
|
|
145
|
+
};
|
|
146
|
+
async #load(writer) {
|
|
147
|
+
writer.write('Gathering files');
|
|
148
|
+
for await (const file of this.#traverse(this.#directories)) {
|
|
149
|
+
this.#filesByAlias.set(file.alias, file);
|
|
150
|
+
}
|
|
151
|
+
let file;
|
|
152
|
+
const files = Array.from(this.#filesByAlias.values());
|
|
153
|
+
writer.write('Generating hashes');
|
|
154
|
+
for (let i = 0; i < files.length; i++) {
|
|
155
|
+
file = files[i];
|
|
156
|
+
const content = await readFile(file.absolutePath);
|
|
157
|
+
const hash = createHash('sha1').update(content).digest('hex');
|
|
158
|
+
const parts = file.name.split('/');
|
|
159
|
+
const friendly = parts[parts.length - 1].split('.')[0];
|
|
160
|
+
const rootURL = this.#registry.rootIRI;
|
|
161
|
+
const aliasURL = joinPaths(rootURL, this.#prefix, file.alias);
|
|
162
|
+
const url = joinPaths(rootURL, this.#prefix, `${friendly}-${hash}.${file.extension}`);
|
|
163
|
+
file.finalize(hash, url, aliasURL);
|
|
164
|
+
this.#hashes.set(file.alias, hash);
|
|
165
|
+
this.#filesByAlias.set(file.alias, file);
|
|
166
|
+
this.#filesByURL.set(aliasURL, file);
|
|
167
|
+
if (!this.#filesByExtension.has(file.extension)) {
|
|
168
|
+
this.#filesByExtension.set(file.extension, []);
|
|
169
|
+
}
|
|
170
|
+
if (!this.#filesByContentType.has(file.contentType)) {
|
|
171
|
+
this.#filesByContentType.set(file.contentType, []);
|
|
172
|
+
}
|
|
173
|
+
this.#filesByExtension.get(file.extension).push(file);
|
|
174
|
+
this.#filesByContentType.get(file.contentType).push(file);
|
|
175
|
+
}
|
|
176
|
+
const dependancyMaps = [];
|
|
177
|
+
writer.write('Building dependancy tree');
|
|
178
|
+
for (const [extension, contentType] of this.#extensions.entries()) {
|
|
179
|
+
const preprocessor = this.#preprocessors.get(extension);
|
|
180
|
+
if (preprocessor != null) {
|
|
181
|
+
const files = this.#filesByExtension.get(extension);
|
|
182
|
+
if (files == null)
|
|
183
|
+
continue;
|
|
184
|
+
const dependancies = new Map();
|
|
185
|
+
for (let i = 0; i < files.length; i++) {
|
|
186
|
+
let file = files[i];
|
|
187
|
+
const content = await readFile(file.absolutePath);
|
|
188
|
+
const references = await preprocessor.parse(new Blob([content]), file, this.#filesByURL);
|
|
189
|
+
dependancies.set(file.alias, new DependancyMap(file, references));
|
|
190
|
+
}
|
|
191
|
+
dependancyMaps.push(dependancies);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const parser = this.#parsers.get(contentType);
|
|
195
|
+
if (parser == null)
|
|
196
|
+
continue;
|
|
197
|
+
const files = this.#filesByContentType.get(contentType);
|
|
198
|
+
if (files == null)
|
|
199
|
+
continue;
|
|
200
|
+
const dependancies = new Map();
|
|
201
|
+
for (let i = 0; i < files.length; i++) {
|
|
202
|
+
let file = files[i];
|
|
203
|
+
const content = await readFile(file.absolutePath);
|
|
204
|
+
const references = await parser.parse(new Blob([content]), file, this.#filesByURL);
|
|
205
|
+
dependancies.set(file.alias, new DependancyMap(file, references));
|
|
206
|
+
}
|
|
207
|
+
dependancyMaps.push(dependancies);
|
|
208
|
+
}
|
|
209
|
+
this.#dependancies = new DependancyGraph(new Map(dependancyMaps.flatMap((map => Array.from(map.entries())))));
|
|
210
|
+
writer.write('Registering actions');
|
|
211
|
+
for (const [name, file] of this.#filesByAlias.entries()) {
|
|
212
|
+
const preprocessor = this.#preprocessors.get(file.extension);
|
|
213
|
+
const parser = this.#parsers.get(file.contentType);
|
|
214
|
+
const parts = name.split('/');
|
|
215
|
+
const friendly = parts[parts.length - 1].split('.')[0];
|
|
216
|
+
const hash = this.#hashes.get(name);
|
|
217
|
+
let action = this.#registry.http.get(name, joinPaths(this.#prefix, `${friendly}-${hash}.${file.extension}`))
|
|
218
|
+
.public();
|
|
219
|
+
if (this.#cache) {
|
|
220
|
+
action = action.cache(this.#cache.store({ immutable: true }));
|
|
221
|
+
}
|
|
222
|
+
const implemented = action.handle(file.contentType, async (ctx) => {
|
|
223
|
+
if (preprocessor != null) {
|
|
224
|
+
const content = await readFile(file.absolutePath);
|
|
225
|
+
ctx.body = await preprocessor.process(new Blob([content]), file, this.#filesByURL);
|
|
226
|
+
}
|
|
227
|
+
else if (parser != null) {
|
|
228
|
+
const content = await readFile(file.absolutePath);
|
|
229
|
+
ctx.body = await parser.update(new Blob([content]), file, this.#filesByURL);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
ctx.body = Readable.toWeb(createReadStream(file.absolutePath));
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
this.#actions.set(name, implemented);
|
|
236
|
+
}
|
|
237
|
+
writer.write('Finished');
|
|
238
|
+
writer.close();
|
|
239
|
+
this.#loaded = true;
|
|
240
|
+
}
|
|
241
|
+
#traverseRe = /.(?:\.(?<lang>[a-zA-Z\-]+))?(?:\.(?<extension>[a-zA-Z0-9]+))$/;
|
|
242
|
+
/**
|
|
243
|
+
* Traverses into a list of directories outputting a file info object for every file
|
|
244
|
+
* of a configured file extension.
|
|
245
|
+
*
|
|
246
|
+
* @param directories The directories to traverse into.
|
|
247
|
+
*/
|
|
248
|
+
async *#traverse(directories, root = '') {
|
|
249
|
+
for (let i = 0; i < directories.length; i++) {
|
|
250
|
+
const dir = await opendir(directories[i].path);
|
|
251
|
+
for await (const entry of dir) {
|
|
252
|
+
const name = entry.name;
|
|
253
|
+
const alias = directories[i].alias;
|
|
254
|
+
const match = this.#traverseRe.exec(entry.name);
|
|
255
|
+
const absolutePath = join(directories[i].path, entry.name);
|
|
256
|
+
const directory = root === '' ? directories[i].path : root;
|
|
257
|
+
const relativePath = absolutePath.replace(directory, '');
|
|
258
|
+
const { lang, extension } = match?.groups ?? {};
|
|
259
|
+
const contentType = this.#extensions.get(extension);
|
|
260
|
+
if (contentType == null || extension == null) {
|
|
261
|
+
console.warn(`File ${joinPaths(alias, relativePath)} extension not known, skipping...`);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (entry.isDirectory()) {
|
|
265
|
+
yield* this.#traverse([{ alias, path: absolutePath }], root === '' ? directories[i].path : root);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
yield new WorkingFileInfo(name, alias, relativePath, absolutePath, extension, contentType, lang);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type FileInfo } from './file-info.ts';
|
|
2
|
+
import type { FilesByURL, ReferenceDetails, ReferencePreprocessor } from './types.ts';
|
|
3
|
+
export declare class TSReferencePreprocessor implements ReferencePreprocessor {
|
|
4
|
+
supports: Set<string>;
|
|
5
|
+
readonly output: 'application/javascript';
|
|
6
|
+
parse(content: Blob, file: FileInfo, filesByURL: FilesByURL): Promise<ReferenceDetails[]>;
|
|
7
|
+
process(content: Blob, file: FileInfo, filesByURL: FilesByURL): Promise<Blob>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createPrinter, createSourceFile, factory, forEachChild, isCallExpression, isImportDeclaration, isStringLiteral, NewLineKind, ScriptTarget, SyntaxKind, transform, visitEachChild, visitNode } from 'typescript';
|
|
2
|
+
export class TSReferencePreprocessor {
|
|
3
|
+
supports = new Set(['ts', 'mts', 'cts']);
|
|
4
|
+
output;
|
|
5
|
+
async parse(content, file, filesByURL) {
|
|
6
|
+
const sourceText = await content.text();
|
|
7
|
+
const source = createSourceFile(file.absolutePath, sourceText, ScriptTarget.ES2022, true);
|
|
8
|
+
const references = [];
|
|
9
|
+
function visit(node) {
|
|
10
|
+
if (isImportDeclaration(node) && node.moduleSpecifier) {
|
|
11
|
+
const path = node.moduleSpecifier.text;
|
|
12
|
+
const url = new URL(path, file.aliasURL).toString();
|
|
13
|
+
references.push({
|
|
14
|
+
url,
|
|
15
|
+
directive: 'script-src',
|
|
16
|
+
file: filesByURL.get(url),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
else if (isCallExpression(node) &&
|
|
20
|
+
node.expression.kind === SyntaxKind.ImportKeyword &&
|
|
21
|
+
node.arguments.length === 1 &&
|
|
22
|
+
isStringLiteral(node.arguments[0])) {
|
|
23
|
+
const path = node.arguments[0].text;
|
|
24
|
+
const url = new URL(path, file.aliasURL).toString();
|
|
25
|
+
references.push({
|
|
26
|
+
url,
|
|
27
|
+
directive: 'script-src',
|
|
28
|
+
file: filesByURL.get(url),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
forEachChild(node, visit);
|
|
32
|
+
}
|
|
33
|
+
visit(source);
|
|
34
|
+
return references;
|
|
35
|
+
}
|
|
36
|
+
async process(content, file, filesByURL) {
|
|
37
|
+
const sourceText = await content.text();
|
|
38
|
+
const source = createSourceFile(file.absolutePath, sourceText, ScriptTarget.ES2022, true);
|
|
39
|
+
const transformerFactory = (context) => {
|
|
40
|
+
function visitor(node) {
|
|
41
|
+
if (isImportDeclaration(node) && node.moduleSpecifier) {
|
|
42
|
+
const path = node.moduleSpecifier.text;
|
|
43
|
+
const url = new URL(path, file.aliasURL).toString();
|
|
44
|
+
const ref = filesByURL.get(url);
|
|
45
|
+
const literal = factory.createStringLiteral(ref.url);
|
|
46
|
+
return factory.updateImportDeclaration(node, node.modifiers, node.importClause, literal, node.attributes);
|
|
47
|
+
}
|
|
48
|
+
else if (isCallExpression(node) &&
|
|
49
|
+
node.expression.kind === SyntaxKind.ImportKeyword &&
|
|
50
|
+
node.arguments.length === 1 &&
|
|
51
|
+
isStringLiteral(node.arguments[0])) {
|
|
52
|
+
const path = node.arguments[0].text;
|
|
53
|
+
const url = new URL(path, file.aliasURL).toString();
|
|
54
|
+
const ref = filesByURL.get(url);
|
|
55
|
+
const literal = factory.createStringLiteral(ref.url);
|
|
56
|
+
return factory.updateCallExpression(node, node.expression, node.typeArguments, [literal]);
|
|
57
|
+
}
|
|
58
|
+
return visitEachChild(node, visitor, context);
|
|
59
|
+
}
|
|
60
|
+
return (node) => visitNode(node, visitor);
|
|
61
|
+
};
|
|
62
|
+
const result = transform(source, [transformerFactory]);
|
|
63
|
+
const printer = createPrinter({ newLine: NewLineKind.LineFeed });
|
|
64
|
+
const transformed = result.transformed[0];
|
|
65
|
+
const code = printer.printFile(transformed);
|
|
66
|
+
result.dispose();
|
|
67
|
+
return new Blob([code], { type: this.output });
|
|
68
|
+
}
|
|
69
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { FileInfo } from "./file-info.ts";
|
|
2
|
+
export type FilesByURL = Map<string, FileInfo>;
|
|
3
|
+
export type PolicyDirective = 'child-src' | 'connect-src' | 'default-src' | 'fenced-frame-src' | 'font-src' | 'frame-src' | 'img-src' | 'manifest-src' | 'media-src' | 'object-src' | 'script-src' | 'script-src-elm' | 'script-src-attr' | 'style-src' | 'style-src-elm' | 'style-src-attr' | 'worker-src';
|
|
4
|
+
export type Directory = {
|
|
5
|
+
alias: string;
|
|
6
|
+
path: string;
|
|
7
|
+
};
|
|
8
|
+
export type ReferenceDetails = {
|
|
9
|
+
url: string;
|
|
10
|
+
directive?: PolicyDirective;
|
|
11
|
+
file?: FileInfo;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* An object with methods for parsing URL references
|
|
15
|
+
* within supporting content types and then embedding
|
|
16
|
+
* URLs generated for the referenced static assets by
|
|
17
|
+
* the framework.
|
|
18
|
+
*/
|
|
19
|
+
export interface ReferenceParser {
|
|
20
|
+
/**
|
|
21
|
+
* A set of content types supported by this reference parser.
|
|
22
|
+
*/
|
|
23
|
+
readonly supports: Set<string>;
|
|
24
|
+
/**
|
|
25
|
+
* Parses URLs referenced in a file's content and
|
|
26
|
+
* returns a dependancy map of all references.
|
|
27
|
+
*
|
|
28
|
+
* @param content The file content to update.
|
|
29
|
+
* @param file File info object relating to the file.
|
|
30
|
+
* @returns A dependancy map of all references.
|
|
31
|
+
*/
|
|
32
|
+
parse(content: Blob, file: FileInfo, filesByURL: FilesByURL): Promise<ReferenceDetails[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Updates a file's embedded hyperlinks to point to
|
|
35
|
+
* final URLs of other static content.
|
|
36
|
+
*
|
|
37
|
+
* @param file File info object.
|
|
38
|
+
* @returns content with URLs updated.
|
|
39
|
+
*/
|
|
40
|
+
update(content: Blob, file: FileInfo, filesByURL: FilesByURL): Promise<Blob>;
|
|
41
|
+
}
|
|
42
|
+
export interface ReferencePreprocessor {
|
|
43
|
+
/**
|
|
44
|
+
* A set of content types supported by this reference parser.
|
|
45
|
+
*/
|
|
46
|
+
readonly supports: Set<string>;
|
|
47
|
+
/**
|
|
48
|
+
* Content type the pre-processor outputs.
|
|
49
|
+
*/
|
|
50
|
+
readonly output: string;
|
|
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(content: Blob, file: FileInfo, filesByURL: FilesByURL): Promise<ReferenceDetails[]>;
|
|
60
|
+
/**
|
|
61
|
+
* Updates a file's embedded hyperlinks to point to
|
|
62
|
+
* final URLs of other static content.
|
|
63
|
+
*
|
|
64
|
+
* @param file File info object.
|
|
65
|
+
* @returns content with URLs updated.
|
|
66
|
+
*/
|
|
67
|
+
process(content: Blob, file: FileInfo, filesByURL: FilesByURL): Promise<Blob>;
|
|
68
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type {FilesByURL, PolicyDirective, ReferenceDetails, ReferenceParser} from "./types.js";
|
|
2
|
+
import {type FileInfo} from "./file-info.ts";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
type PolicyDirectiveMap = Record<string, PolicyDirective>;
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
const ruleRe = /(?:(\@[a-z]+)|(?:([a-z][a-z\-]*\s*):))\s*(.*);/gm;
|
|
9
|
+
const urlRe = /url\(\s*(?:(?:\"(.*)\")|(?:\'(.*)\')|(.*))\s*\)/gm;
|
|
10
|
+
const defaultDirectives: PolicyDirectiveMap = {
|
|
11
|
+
'@import': 'style-src',
|
|
12
|
+
'background': 'img-src',
|
|
13
|
+
'background-image': 'img-src',
|
|
14
|
+
'src': 'font-src',
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export type CSSReferenceParserArgs = {
|
|
18
|
+
contentType?: string | string[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class CSSReferenceParser implements ReferenceParser {
|
|
22
|
+
|
|
23
|
+
ruleRe: RegExp = ruleRe;
|
|
24
|
+
urlRe: RegExp = urlRe;
|
|
25
|
+
supports = new Set(['text/css']);
|
|
26
|
+
directives: PolicyDirectiveMap = defaultDirectives;
|
|
27
|
+
|
|
28
|
+
constructor(args?: CSSReferenceParserArgs) {
|
|
29
|
+
if (Array.isArray(args?.contentType)) {
|
|
30
|
+
this.supports = new Set(args.contentType);
|
|
31
|
+
} else if (args?.contentType != null) {
|
|
32
|
+
this.supports = new Set([args.contentType]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parses all references within a css document.
|
|
38
|
+
*
|
|
39
|
+
* @param content Content of a css file.
|
|
40
|
+
*/
|
|
41
|
+
async parse(
|
|
42
|
+
content: Blob,
|
|
43
|
+
file: FileInfo,
|
|
44
|
+
filesByURL: FilesByURL,
|
|
45
|
+
): Promise<ReferenceDetails[]> {
|
|
46
|
+
let m1: RegExpExecArray | null;
|
|
47
|
+
let m2: RegExpExecArray | null;
|
|
48
|
+
let property: string;
|
|
49
|
+
let url: string;
|
|
50
|
+
let directive: PolicyDirective;
|
|
51
|
+
const references: ReferenceDetails[] = [];
|
|
52
|
+
const text = await content.text();
|
|
53
|
+
|
|
54
|
+
this.ruleRe.lastIndex = 0;
|
|
55
|
+
while ((m1 = this.ruleRe.exec(text))) {
|
|
56
|
+
property = m1[1] ?? m1[2];
|
|
57
|
+
directive = this.directives[property];
|
|
58
|
+
|
|
59
|
+
if (directive == null) continue;
|
|
60
|
+
|
|
61
|
+
this.urlRe.lastIndex = 0;
|
|
62
|
+
while ((m2 = this.urlRe.exec(m1[3]))) {
|
|
63
|
+
url = new URL(m2[1] ?? m2[2] ?? m2[3], file.aliasURL).toString();
|
|
64
|
+
|
|
65
|
+
references.push({
|
|
66
|
+
url,
|
|
67
|
+
directive,
|
|
68
|
+
file: filesByURL.get(url),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return references;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async update(
|
|
77
|
+
content: Blob,
|
|
78
|
+
file: FileInfo,
|
|
79
|
+
filesByURL: FilesByURL,
|
|
80
|
+
): Promise<Blob> {
|
|
81
|
+
const text = await content.text();
|
|
82
|
+
const updated = text.replace(ruleRe, (match) => {
|
|
83
|
+
return match.replace(urlRe, (...matches) => {
|
|
84
|
+
const src = matches[1] ?? matches[2] ?? matches[3];
|
|
85
|
+
const url = new URL(src, file.aliasURL).toString();
|
|
86
|
+
const ref = filesByURL.get(url);
|
|
87
|
+
|
|
88
|
+
if (ref == null) return matches[0];
|
|
89
|
+
|
|
90
|
+
return `url(${ref.url})`;
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
return new Blob([updated], { type: file.contentType });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|