mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Respect root path for static assets and HTML links
* feat: rootpath for static assets and links1 * fix: static asset handler respects root path * fix: use rootPath for links * tests: fix the tests after adding consuctor params * feat: change matchregex instead of stored URLs * feat: baseUrl for handlebar engine and templates * feat: full baseUrl passed to templates * test: fix integration tests + templates * chore: implement requested changes * docs: Describe TemplateEngine interface changes Co-authored-by: Joachim Van Herwegen <joachimvh@gmail.com>
This commit is contained in:
parent
771d138037
commit
2814e72b34
@ -42,6 +42,7 @@ These changes are relevant if you wrote custom modules for the server that depen
|
|||||||
- `YargsCliExtractor` was changed to now take as input an array of parameter objects.
|
- `YargsCliExtractor` was changed to now take as input an array of parameter objects.
|
||||||
- `RedirectAllHttpHandler` was removed and fully replaced by `RedirectingHttpHandler`.
|
- `RedirectAllHttpHandler` was removed and fully replaced by `RedirectingHttpHandler`.
|
||||||
- `SingleThreadedResourceLocker` has been renamed to `MemoryResourceLocker`.
|
- `SingleThreadedResourceLocker` has been renamed to `MemoryResourceLocker`.
|
||||||
|
- Both `TemplateEngine` implementations now take a `baseUrl` parameter as input.
|
||||||
|
|
||||||
A new interface `SingleThreaded` has been added. This empty interface can be implemented to mark a component as not-threadsafe. When the CSS starts in multithreaded mode, it will error and halt if any SingleThreaded components are instantiated.
|
A new interface `SingleThreaded` has been added. This empty interface can be implemented to mark a component as not-threadsafe. When the CSS starts in multithreaded mode, it will error and halt if any SingleThreaded components are instantiated.
|
||||||
|
|
||||||
|
@ -17,7 +17,10 @@
|
|||||||
"@type": "TemplatedResourcesGenerator",
|
"@type": "TemplatedResourcesGenerator",
|
||||||
"templateFolder": "@css:templates/root/prefilled",
|
"templateFolder": "@css:templates/root/prefilled",
|
||||||
"factory": { "@type": "ExtensionBasedMapperFactory" },
|
"factory": { "@type": "ExtensionBasedMapperFactory" },
|
||||||
"templateEngine": { "@type": "HandlebarsTemplateEngine" }
|
"templateEngine": {
|
||||||
|
"@type": "HandlebarsTemplateEngine",
|
||||||
|
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"args_storageKey": "rootInitialized",
|
"args_storageKey": "rootInitialized",
|
||||||
"args_storage": { "@id": "urn:solid-server:default:SetupStorage" }
|
"args_storage": { "@id": "urn:solid-server:default:SetupStorage" }
|
||||||
|
@ -17,7 +17,10 @@
|
|||||||
"@type": "TemplatedResourcesGenerator",
|
"@type": "TemplatedResourcesGenerator",
|
||||||
"templateFolder": "@css:templates/root/empty",
|
"templateFolder": "@css:templates/root/empty",
|
||||||
"factory": { "@type": "ExtensionBasedMapperFactory" },
|
"factory": { "@type": "ExtensionBasedMapperFactory" },
|
||||||
"templateEngine": { "@type": "HandlebarsTemplateEngine" }
|
"templateEngine": {
|
||||||
|
"@type": "HandlebarsTemplateEngine",
|
||||||
|
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"args_storageKey": "rootInitialized",
|
"args_storageKey": "rootInitialized",
|
||||||
"args_storage": { "@id": "urn:solid-server:default:SetupStorage" }
|
"args_storage": { "@id": "urn:solid-server:default:SetupStorage" }
|
||||||
|
@ -27,12 +27,14 @@
|
|||||||
{
|
{
|
||||||
"comment": "Renders the main setup template.",
|
"comment": "Renders the main setup template.",
|
||||||
"@type": "EjsTemplateEngine",
|
"@type": "EjsTemplateEngine",
|
||||||
"template": "@css:templates/setup/index.html.ejs"
|
"template": "@css:templates/setup/index.html.ejs",
|
||||||
|
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"comment": "Will embed the result of the first engine into the main HTML template.",
|
"comment": "Will embed the result of the first engine into the main HTML template.",
|
||||||
"@type": "EjsTemplateEngine",
|
"@type": "EjsTemplateEngine",
|
||||||
"template": "@css:templates/main.html.ejs"
|
"template": "@css:templates/main.html.ejs",
|
||||||
|
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -60,7 +62,10 @@
|
|||||||
"@type": "TemplatedResourcesGenerator",
|
"@type": "TemplatedResourcesGenerator",
|
||||||
"templateFolder": "@css:templates/root/empty",
|
"templateFolder": "@css:templates/root/empty",
|
||||||
"factory": { "@type": "ExtensionBasedMapperFactory" },
|
"factory": { "@type": "ExtensionBasedMapperFactory" },
|
||||||
"templateEngine": { "@type": "HandlebarsTemplateEngine" }
|
"templateEngine": {
|
||||||
|
"@type": "HandlebarsTemplateEngine",
|
||||||
|
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"args_storageKey": "rootInitialized",
|
"args_storageKey": "rootInitialized",
|
||||||
"args_storage": { "@id": "urn:solid-server:default:SetupStorage" }
|
"args_storage": { "@id": "urn:solid-server:default:SetupStorage" }
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
"@id": "urn:solid-server:default:StaticAssetHandler",
|
"@id": "urn:solid-server:default:StaticAssetHandler",
|
||||||
"@type": "StaticAssetHandler",
|
"@type": "StaticAssetHandler",
|
||||||
"options_expires": 86400,
|
"options_expires": 86400,
|
||||||
|
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"StaticAssetHandler:_assets_key": "/favicon.ico",
|
"StaticAssetHandler:_assets_key": "/favicon.ico",
|
||||||
|
@ -17,7 +17,10 @@
|
|||||||
"@type": "TemplatedResourcesGenerator",
|
"@type": "TemplatedResourcesGenerator",
|
||||||
"templateFolder": "@css:templates/root/empty",
|
"templateFolder": "@css:templates/root/empty",
|
||||||
"factory": { "@type": "ExtensionBasedMapperFactory" },
|
"factory": { "@type": "ExtensionBasedMapperFactory" },
|
||||||
"templateEngine": { "@type": "HandlebarsTemplateEngine" }
|
"templateEngine": {
|
||||||
|
"@type": "HandlebarsTemplateEngine",
|
||||||
|
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"args_storageKey": "idpContainerInitialized",
|
"args_storageKey": "idpContainerInitialized",
|
||||||
"args_storage": { "@id": "urn:solid-server:default:SetupStorage" }
|
"args_storage": { "@id": "urn:solid-server:default:SetupStorage" }
|
||||||
|
@ -17,7 +17,10 @@
|
|||||||
"@type": "TemplatedResourcesGenerator",
|
"@type": "TemplatedResourcesGenerator",
|
||||||
"templateFolder": "@css:templates/root/empty",
|
"templateFolder": "@css:templates/root/empty",
|
||||||
"factory": { "@type": "ExtensionBasedMapperFactory" },
|
"factory": { "@type": "ExtensionBasedMapperFactory" },
|
||||||
"templateEngine": { "@type": "HandlebarsTemplateEngine" }
|
"templateEngine": {
|
||||||
|
"@type": "HandlebarsTemplateEngine",
|
||||||
|
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"args_storageKey": "wellKnownContainerInitialized",
|
"args_storageKey": "wellKnownContainerInitialized",
|
||||||
"args_storage": { "@id": "urn:solid-server:default:SetupStorage" }
|
"args_storage": { "@id": "urn:solid-server:default:SetupStorage" }
|
||||||
|
@ -17,7 +17,8 @@
|
|||||||
"args_accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
|
"args_accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
|
||||||
"args_templateEngine": {
|
"args_templateEngine": {
|
||||||
"@type": "EjsTemplateEngine",
|
"@type": "EjsTemplateEngine",
|
||||||
"template": "@css:templates/identity/email-password/reset-password-email.html.ejs"
|
"template": "@css:templates/identity/email-password/reset-password-email.html.ejs",
|
||||||
|
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
|
||||||
},
|
},
|
||||||
"args_emailSender": { "@id": "urn:solid-server:default:EmailSender" },
|
"args_emailSender": { "@id": "urn:solid-server:default:EmailSender" },
|
||||||
"args_resetRoute": { "@id": "urn:solid-server:auth:password:ResetPasswordRoute" }
|
"args_resetRoute": { "@id": "urn:solid-server:auth:password:ResetPasswordRoute" }
|
||||||
|
@ -12,12 +12,14 @@
|
|||||||
"engines": [
|
"engines": [
|
||||||
{
|
{
|
||||||
"comment": "Will be called with specific templates to generate HTML snippets.",
|
"comment": "Will be called with specific templates to generate HTML snippets.",
|
||||||
"@type": "EjsTemplateEngine"
|
"@type": "EjsTemplateEngine",
|
||||||
|
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"comment": "Will embed the result of the first engine into the main HTML template.",
|
"comment": "Will embed the result of the first engine into the main HTML template.",
|
||||||
"@type": "EjsTemplateEngine",
|
"@type": "EjsTemplateEngine",
|
||||||
"template": "@css:templates/main.html.ejs"
|
"template": "@css:templates/main.html.ejs",
|
||||||
|
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||||
"configStorage": { "@id": "urn:solid-server:default:PodConfigurationStorage" }
|
"configStorage": { "@id": "urn:solid-server:default:PodConfigurationStorage" }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -10,7 +10,8 @@
|
|||||||
"@type": "ExtensionBasedMapperFactory"
|
"@type": "ExtensionBasedMapperFactory"
|
||||||
},
|
},
|
||||||
"templateEngine": {
|
"templateEngine": {
|
||||||
"@type": "HandlebarsTemplateEngine"
|
"@type": "HandlebarsTemplateEngine",
|
||||||
|
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -12,12 +12,14 @@
|
|||||||
"engines": [
|
"engines": [
|
||||||
{
|
{
|
||||||
"comment": "Will be called with specific templates to generate HTML snippets.",
|
"comment": "Will be called with specific templates to generate HTML snippets.",
|
||||||
"@type": "EjsTemplateEngine"
|
"@type": "EjsTemplateEngine",
|
||||||
|
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"comment": "Will embed the result of the first engine into the main HTML template.",
|
"comment": "Will embed the result of the first engine into the main HTML template.",
|
||||||
"@type": "EjsTemplateEngine",
|
"@type": "EjsTemplateEngine",
|
||||||
"template": "@css:templates/main.html.ejs"
|
"template": "@css:templates/main.html.ejs",
|
||||||
|
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,10 @@
|
|||||||
"comment": "Converts an error into a Markdown description of its details.",
|
"comment": "Converts an error into a Markdown description of its details.",
|
||||||
"@id": "urn:solid-server:default:ErrorToTemplateConverter",
|
"@id": "urn:solid-server:default:ErrorToTemplateConverter",
|
||||||
"@type": "ErrorToTemplateConverter",
|
"@type": "ErrorToTemplateConverter",
|
||||||
"templateEngine": { "@type": "HandlebarsTemplateEngine" }
|
"templateEngine": {
|
||||||
|
"@type": "HandlebarsTemplateEngine",
|
||||||
|
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,8 @@
|
|||||||
"@type": "MarkdownToHtmlConverter",
|
"@type": "MarkdownToHtmlConverter",
|
||||||
"templateEngine": {
|
"templateEngine": {
|
||||||
"@type": "EjsTemplateEngine",
|
"@type": "EjsTemplateEngine",
|
||||||
"template": "@css:templates/main.html.ejs"
|
"template": "@css:templates/main.html.ejs",
|
||||||
|
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -16,7 +17,8 @@
|
|||||||
"@type": "ContainerToTemplateConverter",
|
"@type": "ContainerToTemplateConverter",
|
||||||
"templateEngine": {
|
"templateEngine": {
|
||||||
"@type": "HandlebarsTemplateEngine",
|
"@type": "HandlebarsTemplateEngine",
|
||||||
"template": "@css:templates/container.md.hbs"
|
"template": "@css:templates/container.md.hbs",
|
||||||
|
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
|
||||||
},
|
},
|
||||||
"contentType": "text/markdown",
|
"contentType": "text/markdown",
|
||||||
"identifierStrategy": { "@id": "urn:solid-server:default:IdentifierStrategy" }
|
"identifierStrategy": { "@id": "urn:solid-server:default:IdentifierStrategy" }
|
||||||
|
@ -31,6 +31,7 @@ export class TemplatedPodGenerator implements PodGenerator {
|
|||||||
private readonly variableHandler: VariableHandler;
|
private readonly variableHandler: VariableHandler;
|
||||||
private readonly configStorage: KeyValueStorage<string, unknown>;
|
private readonly configStorage: KeyValueStorage<string, unknown>;
|
||||||
private readonly configTemplatePath: string;
|
private readonly configTemplatePath: string;
|
||||||
|
private readonly baseUrl: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param storeFactory - Factory used for Components.js instantiation.
|
* @param storeFactory - Factory used for Components.js instantiation.
|
||||||
@ -39,10 +40,11 @@ export class TemplatedPodGenerator implements PodGenerator {
|
|||||||
* @param configTemplatePath - Where to find the configuration templates.
|
* @param configTemplatePath - Where to find the configuration templates.
|
||||||
*/
|
*/
|
||||||
public constructor(storeFactory: ComponentsJsFactory, variableHandler: VariableHandler,
|
public constructor(storeFactory: ComponentsJsFactory, variableHandler: VariableHandler,
|
||||||
configStorage: KeyValueStorage<string, unknown>, configTemplatePath?: string) {
|
configStorage: KeyValueStorage<string, unknown>, baseUrl: string, configTemplatePath?: string) {
|
||||||
this.storeFactory = storeFactory;
|
this.storeFactory = storeFactory;
|
||||||
this.variableHandler = variableHandler;
|
this.variableHandler = variableHandler;
|
||||||
this.configStorage = configStorage;
|
this.configStorage = configStorage;
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
this.configTemplatePath = configTemplatePath ?? DEFAULT_CONFIG_PATH;
|
this.configTemplatePath = configTemplatePath ?? DEFAULT_CONFIG_PATH;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +77,11 @@ export class TemplatedPodGenerator implements PodGenerator {
|
|||||||
variables[TEMPLATE_VARIABLE.templateConfig] = joinFilePath(this.configTemplatePath, settings.template);
|
variables[TEMPLATE_VARIABLE.templateConfig] = joinFilePath(this.configTemplatePath, settings.template);
|
||||||
|
|
||||||
const store: ResourceStore =
|
const store: ResourceStore =
|
||||||
await this.storeFactory.generate(variables[TEMPLATE_VARIABLE.templateConfig]!, TEMPLATE.ResourceStore, variables);
|
await this.storeFactory.generate(
|
||||||
|
variables[TEMPLATE_VARIABLE.templateConfig]!,
|
||||||
|
TEMPLATE.ResourceStore,
|
||||||
|
{ ...variables, 'urn:solid-server:default:variable:baseUrl': this.baseUrl },
|
||||||
|
);
|
||||||
this.logger.debug(`Generating store ${identifier.path} with variables ${JSON.stringify(variables)}`);
|
this.logger.debug(`Generating store ${identifier.path} with variables ${JSON.stringify(variables)}`);
|
||||||
|
|
||||||
// Store the variables permanently
|
// Store the variables permanently
|
||||||
|
@ -5,7 +5,7 @@ import { getLoggerFor } from '../../logging/LogUtil';
|
|||||||
import { APPLICATION_OCTET_STREAM } from '../../util/ContentTypes';
|
import { APPLICATION_OCTET_STREAM } from '../../util/ContentTypes';
|
||||||
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
|
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
|
||||||
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError';
|
||||||
import { joinFilePath, resolveAssetPath } from '../../util/PathUtil';
|
import { ensureTrailingSlash, joinFilePath, resolveAssetPath, trimLeadingSlashes } from '../../util/PathUtil';
|
||||||
import { pipeSafely } from '../../util/StreamUtil';
|
import { pipeSafely } from '../../util/StreamUtil';
|
||||||
import type { HttpHandlerInput } from '../HttpHandler';
|
import type { HttpHandlerInput } from '../HttpHandler';
|
||||||
import { HttpHandler } from '../HttpHandler';
|
import { HttpHandler } from '../HttpHandler';
|
||||||
@ -29,22 +29,24 @@ export class StaticAssetHandler extends HttpHandler {
|
|||||||
* where URL paths ending in a slash are interpreted as entire folders.
|
* where URL paths ending in a slash are interpreted as entire folders.
|
||||||
* @param options - Cache expiration time in seconds.
|
* @param options - Cache expiration time in seconds.
|
||||||
*/
|
*/
|
||||||
public constructor(assets: Record<string, string>, options: { expires?: number } = {}) {
|
public constructor(assets: Record<string, string>, baseUrl: string, options: { expires?: number } = {}) {
|
||||||
super();
|
super();
|
||||||
this.mappings = {};
|
this.mappings = {};
|
||||||
|
const rootPath = ensureTrailingSlash(new URL(baseUrl).pathname);
|
||||||
|
|
||||||
for (const [ url, path ] of Object.entries(assets)) {
|
for (const [ url, path ] of Object.entries(assets)) {
|
||||||
this.mappings[url] = resolveAssetPath(path);
|
this.mappings[trimLeadingSlashes(url)] = resolveAssetPath(path);
|
||||||
}
|
}
|
||||||
this.pathMatcher = this.createPathMatcher(assets);
|
this.pathMatcher = this.createPathMatcher(rootPath);
|
||||||
this.expires = Number.isInteger(options.expires) ? Math.max(0, options.expires!) : 0;
|
this.expires = Number.isInteger(options.expires) ? Math.max(0, options.expires!) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a regular expression that matches the URL paths.
|
* Creates a regular expression that matches the URL paths.
|
||||||
*/
|
*/
|
||||||
private createPathMatcher(assets: Record<string, string>): RegExp {
|
private createPathMatcher(rootPath: string): RegExp {
|
||||||
// Sort longest paths first to ensure the longest match has priority
|
// Sort longest paths first to ensure the longest match has priority
|
||||||
const paths = Object.keys(assets)
|
const paths = Object.keys(this.mappings)
|
||||||
.sort((pathA, pathB): number => pathB.length - pathA.length);
|
.sort((pathA, pathB): number => pathB.length - pathA.length);
|
||||||
|
|
||||||
// Collect regular expressions for files and folders separately
|
// Collect regular expressions for files and folders separately
|
||||||
@ -55,7 +57,7 @@ export class StaticAssetHandler extends HttpHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Either match an exact document or a file within a folder (stripping the query string)
|
// Either match an exact document or a file within a folder (stripping the query string)
|
||||||
return new RegExp(`^(?:(${files.join('|')})|(${folders.join('|')})([^?]+))(?:\\?.*)?$`, 'u');
|
return new RegExp(`^${rootPath}(?:(${files.join('|')})|(${folders.join('|')})([^?]+))(?:\\?.*)?$`, 'u');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -97,6 +97,17 @@ export function ensureLeadingSlash(path: string): string {
|
|||||||
return path.replace(/^\/*/u, '/');
|
return path.replace(/^\/*/u, '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes sure the input path has no slashes at the beginning.
|
||||||
|
*
|
||||||
|
* @param path - Path to check.
|
||||||
|
*
|
||||||
|
* @returns The potentially changed path.
|
||||||
|
*/
|
||||||
|
export function trimLeadingSlashes(path: string): string {
|
||||||
|
return path.replace(/^\/+/u, '');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts the extension (without dot) from a path.
|
* Extracts the extension (without dot) from a path.
|
||||||
* Custom function since `path.extname` does not work on all cases (e.g. ".acl")
|
* Custom function since `path.extname` does not work on all cases (e.g. ".acl")
|
||||||
|
@ -11,13 +11,17 @@ import Dict = NodeJS.Dict;
|
|||||||
*/
|
*/
|
||||||
export class EjsTemplateEngine<T extends Dict<any> = Dict<any>> implements TemplateEngine<T> {
|
export class EjsTemplateEngine<T extends Dict<any> = Dict<any>> implements TemplateEngine<T> {
|
||||||
private readonly applyTemplate: Promise<TemplateFunction>;
|
private readonly applyTemplate: Promise<TemplateFunction>;
|
||||||
|
private readonly baseUrl: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param baseUrl - Base URL of the server.
|
||||||
* @param template - The default template @range {json}
|
* @param template - The default template @range {json}
|
||||||
*/
|
*/
|
||||||
public constructor(template?: Template) {
|
public constructor(baseUrl: string, template?: Template) {
|
||||||
// EJS requires the `filename` parameter to be able to include partial templates
|
// EJS requires the `filename` parameter to be able to include partial templates
|
||||||
const filename = getTemplateFilePath(template);
|
const filename = getTemplateFilePath(template);
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
|
||||||
this.applyTemplate = readTemplate(template)
|
this.applyTemplate = readTemplate(template)
|
||||||
.then((templateString: string): TemplateFunction => compile(templateString, { filename }));
|
.then((templateString: string): TemplateFunction => compile(templateString, { filename }));
|
||||||
}
|
}
|
||||||
@ -25,7 +29,7 @@ export class EjsTemplateEngine<T extends Dict<any> = Dict<any>> implements Templ
|
|||||||
public async render(contents: T): Promise<string>;
|
public async render(contents: T): Promise<string>;
|
||||||
public async render<TCustom = T>(contents: TCustom, template: Template): Promise<string>;
|
public async render<TCustom = T>(contents: TCustom, template: Template): Promise<string>;
|
||||||
public async render<TCustom = T>(contents: TCustom, template?: Template): Promise<string> {
|
public async render<TCustom = T>(contents: TCustom, template?: Template): Promise<string> {
|
||||||
const options = { ...contents, filename: getTemplateFilePath(template) };
|
const options = { ...contents, filename: getTemplateFilePath(template), baseUrl: this.baseUrl };
|
||||||
return template ? render(await readTemplate(template), options) : (await this.applyTemplate)(options);
|
return template ? render(await readTemplate(template), options) : (await this.applyTemplate)(options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,11 +11,14 @@ import Dict = NodeJS.Dict;
|
|||||||
*/
|
*/
|
||||||
export class HandlebarsTemplateEngine<T extends Dict<any> = Dict<any>> implements TemplateEngine<T> {
|
export class HandlebarsTemplateEngine<T extends Dict<any> = Dict<any>> implements TemplateEngine<T> {
|
||||||
private readonly applyTemplate: Promise<TemplateDelegate>;
|
private readonly applyTemplate: Promise<TemplateDelegate>;
|
||||||
|
private readonly baseUrl: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @params baseUrl - Base URL of the server.
|
||||||
* @param template - The default template @range {json}
|
* @param template - The default template @range {json}
|
||||||
*/
|
*/
|
||||||
public constructor(template?: Template) {
|
public constructor(baseUrl: string, template?: Template) {
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
this.applyTemplate = readTemplate(template)
|
this.applyTemplate = readTemplate(template)
|
||||||
.then((templateString: string): TemplateDelegate => compile(templateString));
|
.then((templateString: string): TemplateDelegate => compile(templateString));
|
||||||
}
|
}
|
||||||
@ -24,6 +27,6 @@ export class HandlebarsTemplateEngine<T extends Dict<any> = Dict<any>> implement
|
|||||||
public async render<TCustom = T>(contents: TCustom, template: Template): Promise<string>;
|
public async render<TCustom = T>(contents: TCustom, template: Template): Promise<string>;
|
||||||
public async render<TCustom = T>(contents: TCustom, template?: Template): Promise<string> {
|
public async render<TCustom = T>(contents: TCustom, template?: Template): Promise<string> {
|
||||||
const applyTemplate = template ? compile(await readTemplate(template)) : await this.applyTemplate;
|
const applyTemplate = template ? compile(await readTemplate(template)) : await this.applyTemplate;
|
||||||
return applyTemplate(contents);
|
return applyTemplate({ ...contents, baseUrl: this.baseUrl });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
"import": [
|
"import": [
|
||||||
"css:config/util/auxiliary/acl.json",
|
"css:config/util/auxiliary/acl.json",
|
||||||
"css:config/util/index/default.json",
|
"css:config/util/index/default.json",
|
||||||
"css:config/util/representation-conversion/default.json"
|
"css:config/util/representation-conversion/default.json",
|
||||||
|
"css:config/util/variables/default.json"
|
||||||
],
|
],
|
||||||
"@graph": [
|
"@graph": [
|
||||||
{
|
{
|
||||||
|
@ -4,12 +4,12 @@
|
|||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
<title><%= extractTitle(htmlBody) %></title>
|
<title><%= extractTitle(htmlBody) %></title>
|
||||||
<link rel="stylesheet" href="/.well-known/css/styles/main.css" type="text/css">
|
<link rel="stylesheet" href="<%= baseUrl -%>.well-known/css/styles/main.css" type="text/css">
|
||||||
<script type="text/javascript" src="/.well-known/css/scripts/util.js"></script>
|
<script type="text/javascript" src="<%= baseUrl -%>.well-known/css/scripts/util.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<a href="/"><img src="/.well-known/css/images/solid.svg" alt="[Solid logo]" /></a>
|
<a href="<%= baseUrl %>"><img src="<%= baseUrl -%>.well-known/css/images/solid.svg" alt="[Solid logo]" /></a>
|
||||||
<h1>Community Solid Server</h1>
|
<h1>Community Solid Server</h1>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
|
@ -13,6 +13,7 @@ describe('A TemplatedPodGenerator', (): void => {
|
|||||||
const template = 'config-template.json';
|
const template = 'config-template.json';
|
||||||
const templatePath = `${configTemplatePath}${template}`;
|
const templatePath = `${configTemplatePath}${template}`;
|
||||||
const identifier = { path: 'http://test.com/alice/' };
|
const identifier = { path: 'http://test.com/alice/' };
|
||||||
|
const baseUrl = 'http://test.com';
|
||||||
let settings: PodSettings;
|
let settings: PodSettings;
|
||||||
let storeFactory: ComponentsJsFactory;
|
let storeFactory: ComponentsJsFactory;
|
||||||
let variableHandler: VariableHandler;
|
let variableHandler: VariableHandler;
|
||||||
@ -32,7 +33,7 @@ describe('A TemplatedPodGenerator', (): void => {
|
|||||||
|
|
||||||
configStorage = new Map<string, unknown>() as any;
|
configStorage = new Map<string, unknown>() as any;
|
||||||
|
|
||||||
generator = new TemplatedPodGenerator(storeFactory, variableHandler, configStorage, configTemplatePath);
|
generator = new TemplatedPodGenerator(storeFactory, variableHandler, configStorage, baseUrl, configTemplatePath);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('only supports settings with a template.', async(): Promise<void> => {
|
it('only supports settings with a template.', async(): Promise<void> => {
|
||||||
@ -45,9 +46,11 @@ describe('A TemplatedPodGenerator', (): void => {
|
|||||||
expect(variableHandler.handleSafe).toHaveBeenCalledTimes(1);
|
expect(variableHandler.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
expect(variableHandler.handleSafe).toHaveBeenLastCalledWith({ identifier, settings });
|
expect(variableHandler.handleSafe).toHaveBeenLastCalledWith({ identifier, settings });
|
||||||
expect(storeFactory.generate).toHaveBeenCalledTimes(1);
|
expect(storeFactory.generate).toHaveBeenCalledTimes(1);
|
||||||
expect(storeFactory.generate).toHaveBeenLastCalledWith(
|
expect(storeFactory.generate)
|
||||||
templatePath, TEMPLATE.ResourceStore, { [TEMPLATE_VARIABLE.templateConfig]: templatePath },
|
.toHaveBeenLastCalledWith(templatePath, TEMPLATE.ResourceStore, {
|
||||||
);
|
[TEMPLATE_VARIABLE.templateConfig]: templatePath,
|
||||||
|
'urn:solid-server:default:variable:baseUrl': baseUrl,
|
||||||
|
});
|
||||||
expect(configStorage.get(identifier.path)).toEqual({ [TEMPLATE_VARIABLE.templateConfig]: templatePath });
|
expect(configStorage.get(identifier.path)).toEqual({ [TEMPLATE_VARIABLE.templateConfig]: templatePath });
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -72,13 +75,14 @@ describe('A TemplatedPodGenerator', (): void => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('uses a default template folder if none is provided.', async(): Promise<void> => {
|
it('uses a default template folder if none is provided.', async(): Promise<void> => {
|
||||||
generator = new TemplatedPodGenerator(storeFactory, variableHandler, configStorage);
|
generator = new TemplatedPodGenerator(storeFactory, variableHandler, configStorage, baseUrl);
|
||||||
const defaultPath = joinFilePath(__dirname, '../../../../templates/config/', template);
|
const defaultPath = joinFilePath(__dirname, '../../../../templates/config/', template);
|
||||||
|
|
||||||
await expect(generator.generate(identifier, settings)).resolves.toBe('store');
|
await expect(generator.generate(identifier, settings)).resolves.toBe('store');
|
||||||
expect(storeFactory.generate)
|
expect(storeFactory.generate)
|
||||||
.toHaveBeenLastCalledWith(defaultPath, TEMPLATE.ResourceStore, {
|
.toHaveBeenLastCalledWith(defaultPath, TEMPLATE.ResourceStore, {
|
||||||
[TEMPLATE_VARIABLE.templateConfig]: defaultPath,
|
[TEMPLATE_VARIABLE.templateConfig]: defaultPath,
|
||||||
|
'urn:solid-server:default:variable:baseUrl': baseUrl,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -48,7 +48,7 @@ async function genToArray<T>(iterable: AsyncIterable<T>): Promise<T[]> {
|
|||||||
describe('A TemplatedResourcesGenerator', (): void => {
|
describe('A TemplatedResourcesGenerator', (): void => {
|
||||||
const rootFilePath = '/templates/pod';
|
const rootFilePath = '/templates/pod';
|
||||||
// Using handlebars engine since it's smaller than any possible dummy
|
// Using handlebars engine since it's smaller than any possible dummy
|
||||||
const generator = new TemplatedResourcesGenerator(rootFilePath, new DummyFactory(), new HandlebarsTemplateEngine());
|
const generator = new TemplatedResourcesGenerator(rootFilePath, new DummyFactory(), new HandlebarsTemplateEngine('http://test.com/'));
|
||||||
let cache: { data: any };
|
let cache: { data: any };
|
||||||
const template = '<{{webId}}> a <http://xmlns.com/foaf/0.1/Person>.';
|
const template = '<{{webId}}> a <http://xmlns.com/foaf/0.1/Person>.';
|
||||||
const location = { path: 'http://test.com/alice/' };
|
const location = { path: 'http://test.com/alice/' };
|
||||||
|
@ -20,7 +20,7 @@ describe('A StaticAssetHandler', (): void => {
|
|||||||
'/foo/bar/folder1/': '/assets/folders/1/',
|
'/foo/bar/folder1/': '/assets/folders/1/',
|
||||||
'/foo/bar/folder2/': '/assets/folders/2',
|
'/foo/bar/folder2/': '/assets/folders/2',
|
||||||
'/foo/bar/folder2/subfolder/': '/assets/folders/3',
|
'/foo/bar/folder2/subfolder/': '/assets/folders/3',
|
||||||
});
|
}, 'http://localhost:3000');
|
||||||
|
|
||||||
afterEach(jest.clearAllMocks);
|
afterEach(jest.clearAllMocks);
|
||||||
|
|
||||||
@ -217,7 +217,7 @@ describe('A StaticAssetHandler', (): void => {
|
|||||||
jest.spyOn(Date, 'now').mockReturnValue(0);
|
jest.spyOn(Date, 'now').mockReturnValue(0);
|
||||||
const cachedHandler = new StaticAssetHandler({
|
const cachedHandler = new StaticAssetHandler({
|
||||||
'/foo/bar/style': '/assets/styles/bar.css',
|
'/foo/bar/style': '/assets/styles/bar.css',
|
||||||
}, {
|
}, 'http://localhost:3000', {
|
||||||
expires: 86400,
|
expires: 86400,
|
||||||
});
|
});
|
||||||
const request = { method: 'GET', url: '/foo/bar/style' };
|
const request = { method: 'GET', url: '/foo/bar/style' };
|
||||||
|
@ -11,7 +11,7 @@ describe('A EjsTemplateEngine', (): void => {
|
|||||||
let templateEngine: EjsTemplateEngine;
|
let templateEngine: EjsTemplateEngine;
|
||||||
|
|
||||||
beforeEach((): void => {
|
beforeEach((): void => {
|
||||||
templateEngine = new EjsTemplateEngine(defaultTemplate);
|
templateEngine = new EjsTemplateEngine('http://localhost:3000', defaultTemplate);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses the default template when no template was passed.', async(): Promise<void> => {
|
it('uses the default template when no template was passed.', async(): Promise<void> => {
|
||||||
|
@ -10,7 +10,7 @@ describe('A HandlebarsTemplateEngine', (): void => {
|
|||||||
let templateEngine: HandlebarsTemplateEngine;
|
let templateEngine: HandlebarsTemplateEngine;
|
||||||
|
|
||||||
beforeEach((): void => {
|
beforeEach((): void => {
|
||||||
templateEngine = new HandlebarsTemplateEngine(template);
|
templateEngine = new HandlebarsTemplateEngine('http://localhost:3000/', template);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses the default template when no template was passed.', async(): Promise<void> => {
|
it('uses the default template when no template was passed.', async(): Promise<void> => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user